fix(delivery): 禁用Rock二次生成二维码并治理上游110001频控报错
1. 业务防乱(专属通道锁定):提货页面一旦生成二维码后,直接禁用'重新生成二维码'按钮并锁定游戏UID输入框,杜绝Token浪费与绑定混乱; 2. 频控治理(前端3秒冷却):手动点击'检查扫码状态'增加3秒倒计时防抖冷却,且对'过于频繁/110001'错误进行平滑提示,不再触发大红报错; 3. 频控治理(后端瞬时防抖与软降级):RockService增加800ms内存并发安全防抖缓存,当上游返回110001时自动优雅降级返回等待扫码状态,不抛出HTTP 400错误。
This commit is contained in:
@@ -15,11 +15,17 @@ import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type rockRefreshCacheItem struct {
|
||||
response *RockRefreshResponse
|
||||
cachedAt time.Time
|
||||
}
|
||||
|
||||
// RockService 负责管理 Rock 平台的配置、库存池、采购、UID强一致风控与状态机
|
||||
type RockService struct {
|
||||
db *gorm.DB
|
||||
client *RockClient
|
||||
mu sync.Mutex
|
||||
db *gorm.DB
|
||||
client *RockClient
|
||||
mu sync.Mutex
|
||||
refreshCache sync.Map // key: upstreamTokenID string, value: rockRefreshCacheItem
|
||||
}
|
||||
|
||||
func NewRockService(db *gorm.DB, client *RockClient) *RockService {
|
||||
@@ -426,10 +432,48 @@ func (s *RockService) CheckBindingAndVerifyUID(ctx context.Context, tokenID uint
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 调上游 refresh 接口
|
||||
ref, err := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("刷新绑定状态失败: %w", err)
|
||||
// 调上游 refresh 接口(带 800ms 瞬时防抖缓存,避免自动轮询与手动高频点击撞上上游 110001 频控)
|
||||
var ref *RockRefreshResponse
|
||||
cacheVal, hasCache := s.refreshCache.Load(token.UpstreamTokenID)
|
||||
if hasCache {
|
||||
item := cacheVal.(rockRefreshCacheItem)
|
||||
if time.Since(item.cachedAt) < 800*time.Millisecond {
|
||||
ref = item.response
|
||||
}
|
||||
}
|
||||
|
||||
if ref == nil {
|
||||
upstreamRef, err := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID)
|
||||
if err != nil {
|
||||
// 若上游返回 110001 "请求过于频繁",进行平滑降级处理,绝不报致命错误打断买家
|
||||
if strings.Contains(err.Error(), "过于频繁") || strings.Contains(err.Error(), "110001") {
|
||||
if hasCache {
|
||||
ref = cacheVal.(rockRefreshCacheItem).response
|
||||
} else if token.Status == model.RockStatusBound {
|
||||
return &CheckBindingResult{
|
||||
Bound: true,
|
||||
RoleName: token.ActualRoleName,
|
||||
RoleID: token.ActualRoleID,
|
||||
Channel: token.ActualChannel,
|
||||
ExpectedUID: token.ExpectedUID,
|
||||
Message: "扫码授权成功,UID核验一致",
|
||||
}, nil
|
||||
} else {
|
||||
return &CheckBindingResult{
|
||||
Bound: false,
|
||||
Message: "正在同步微信/QQ扫码状态,请稍后再次检查",
|
||||
}, nil
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("刷新绑定状态失败: %w", err)
|
||||
}
|
||||
} else {
|
||||
ref = upstreamRef
|
||||
s.refreshCache.Store(token.UpstreamTokenID, rockRefreshCacheItem{
|
||||
response: ref,
|
||||
cachedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 检查上游是否已绑定 (上游状态 bound 或 bound_account_id 有值)
|
||||
@@ -631,6 +675,8 @@ func (s *RockService) ResetMismatchToken(ctx context.Context, tokenID uint) erro
|
||||
return fmt.Errorf("仅 mismatch 状态的 Token 允许人工解冻重置")
|
||||
}
|
||||
|
||||
s.refreshCache.Delete(token.UpstreamTokenID)
|
||||
|
||||
// 重置回 available,清空订单关联
|
||||
return s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
|
||||
"status": model.RockStatusAvailable,
|
||||
|
||||
@@ -433,6 +433,22 @@ Content-Type: application/json
|
||||
- 断言 2:空闲库存为 0 时,已分配 Token 的订单必须 100% 允许正常发货(`CanShip = true`)。
|
||||
- 包含新增用例在内的 5 项 Rock 专项测试与全量 service 测试全部 100% PASS。
|
||||
|
||||
### 7.6 提货页面二次绑定禁用与上游 110001 频控防抖治理
|
||||
|
||||
#### 证据分类审计表
|
||||
| 维度 | 内容详情 | 确信度 |
|
||||
| :--- | :--- | :--- |
|
||||
| **已知 (Known)** | 1. Rock 平台生成的二维码与 Token 属于 1:1 强绑定关系,多次调用会导致 Token 浪费或绑定覆盖;<br>2. 用户手动点击“检查扫码状态”时频繁触发 400 报错:`未知错误码: (110001) 抱歉,请求过于频繁,请稍后再试!谢谢!`,而自动轮询正常。 | **100% (有实测报错报文)** |
|
||||
| **未知 (Unknown)** | 为什么自动轮询正常而手动点击报错? | **已探明**:自动轮询每 2.5 秒触发一次,手动点击与定时器极易并发,使 0666.pro 瞬间接收多笔相同 Token 的 refresh 请求触发 WAF 限流。 |
|
||||
| **目标 (Goal)** | 1. 禁用提货页面“重新生成二维码”按钮,锁定专属通道;<br>2. 后端建立 800ms 瞬时防抖缓存与 110001 软降级;<br>3. 前端增加 3 秒倒计时冷却与友好提示。 | **100% 达成** |
|
||||
|
||||
|
||||
#### 落地措施:
|
||||
1. **提货页防抖与单次专属锁定**(`frontend/src/pages/Delivery.tsx`):
|
||||
- `bindResult` 生成后,按钮变为“专属二维码已生成”并直接处于 `disabled` 状态;
|
||||
- 游戏 UID 输入框置灰禁用,防止中途修改;
|
||||
- 手动点击“检查扫码状态”增加 3 秒冷却倒计时(`检查扫码状态 (3s)`),错误处理中对“过于频繁/110001”进行平滑文案提示,不再弹出大红错误框。
|
||||
2. **后端瞬时缓存与错误降级**(`backend/internal/service/rock_service.go`):
|
||||
- 在 `RockService` 中维护并发安全防抖缓存 `refreshCache sync.Map`(800ms 窗口);
|
||||
- 当遇到上游 110001 频控响应时,执行平滑兜底,返回正在等待扫码,不抛出 HTTP 400 异常。
|
||||
3. **验证结果**:
|
||||
- 前后端静态编译构建与 5 项 Rock 单元测试全部通过。
|
||||
|
||||
@@ -78,6 +78,15 @@ export default function Delivery() {
|
||||
const [generatingImage, setGeneratingImage] = useState(false)
|
||||
const [copyingImage, setCopyingImage] = useState(false)
|
||||
const [previewModalOpen, setPreviewModalOpen] = useState(false)
|
||||
const [manualCheckCooldown, setManualCheckCooldown] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (manualCheckCooldown <= 0) return
|
||||
const timer = setTimeout(() => {
|
||||
setManualCheckCooldown((prev) => Math.max(0, prev - 1))
|
||||
}, 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [manualCheckCooldown])
|
||||
|
||||
const decodedOrderNo = useMemo(() => decodeURIComponent(orderNo), [orderNo])
|
||||
const deliveryAuth = useMemo(() => {
|
||||
@@ -225,7 +234,12 @@ export default function Delivery() {
|
||||
}
|
||||
} catch (e) {
|
||||
if (!silent) {
|
||||
message.error(e instanceof Error ? e.message : '检查绑定状态失败')
|
||||
const errStr = e instanceof Error ? e.message : String(e || '')
|
||||
if (errStr.includes('频繁') || errStr.includes('110001')) {
|
||||
message.info('正在同步扫码状态,请稍候...')
|
||||
} else {
|
||||
message.error(errStr || '检查绑定状态失败')
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!silent) setCheckingBind(false)
|
||||
@@ -234,6 +248,12 @@ export default function Delivery() {
|
||||
[bindResult?.bind_uuid, decodedOrderNo, deliveryAuth]
|
||||
)
|
||||
|
||||
const handleManualCheckBind = () => {
|
||||
if (manualCheckCooldown > 0 || checkingBind) return
|
||||
setManualCheckCooldown(3)
|
||||
checkBindStatus(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!bindResult?.bind_uuid || boundInfo?.bound || submitting || submitResult) {
|
||||
return
|
||||
@@ -579,9 +599,13 @@ export default function Delivery() {
|
||||
name="game_account"
|
||||
label="游戏账号(用于接收充值)"
|
||||
rules={[{ required: true, message: '请输入接收道具的游戏账号 UID' }]}
|
||||
extra="请务必核对游戏账号 UID,充值成功后无法撤回"
|
||||
extra={bindResult ? '已锁定专属发货通道,如需修改账号请刷新页面重试' : '请务必核对游戏账号 UID,充值成功后无法撤回'}
|
||||
>
|
||||
<Input placeholder="请输入游戏账号 UID" size="large" />
|
||||
<Input
|
||||
placeholder="请输入游戏账号 UID"
|
||||
size="large"
|
||||
disabled={Boolean(bindResult)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -591,16 +615,18 @@ export default function Delivery() {
|
||||
icon={<QrcodeOutlined />}
|
||||
loading={binding}
|
||||
onClick={bindAccount}
|
||||
disabled={Boolean(bindResult)}
|
||||
>
|
||||
{bindResult ? '重新生成二维码' : '生成绑定二维码'}
|
||||
{bindResult ? '专属二维码已生成' : '生成绑定二维码'}
|
||||
</Button>
|
||||
{bindResult && !boundInfo?.bound && (
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={checkingBind}
|
||||
onClick={() => checkBindStatus(false)}
|
||||
disabled={manualCheckCooldown > 0}
|
||||
onClick={handleManualCheckBind}
|
||||
>
|
||||
检查扫码状态
|
||||
{manualCheckCooldown > 0 ? `检查扫码状态 (${manualCheckCooldown}s)` : '检查扫码状态'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user