fix: 已发货 Token 不再计为异常,清理遗留不可达代码

- 异常判定排除 status=redeemed 或 redeemed_at 非空的 Token,发货前的 mismatch/失败不再计入
- 筛选、统计徽标、列表派生标记统一复用同一口径
- 新增已发货 Token 排除的单元测试
- 删除 rock_assign.go 中 redeeming 分支替换后残留的不可达 return
This commit is contained in:
2026-11-14 16:05:29 +08:00
parent a9473990be
commit ad8d1704db
3 changed files with 107 additions and 26 deletions
+23 -25
View File
@@ -12,14 +12,12 @@ import (
// 异常判定口径(与产品确认一致):
// 1. 状态流转历史中出现过 mismatch / abnormal(绑定错误、上游失效);
// 2. 状态流转原因或备注中出现失败类关键字(兑换失败、刷新失败、卡密不存在、超时等);
// 3. 只要命中任意一条即为「异常」,不改变 Token 主状态。
// 3. 已最终发货的 Token(status=redeemed 或 redeemed_at 非空)不再算异常:
// 发货前的 mismatch / 失败属于可恢复阻断,最终已履约成功,不需要人工处理;
// 4. 只要命中任意一条即为「异常」,不改变 Token 主状态。
//
// 采用子查询而不是落库字段,保证历史数据无需迁移即可被筛选到。
func rockTokenAnomalyExistsSQL() string {
return `EXISTS (
SELECT 1 FROM rock_token_status_logs l
WHERE l.token_id = rock_tokens.id
AND (
const rockAnomalyLogConditionSQL = `(
l.to_status IN ('mismatch', 'abnormal')
OR l.reason LIKE '%失败%'
OR l.reason LIKE '%不存在%'
@@ -27,7 +25,18 @@ func rockTokenAnomalyExistsSQL() string {
OR l.reason LIKE '%超时%'
OR l.reason LIKE '%拦截%'
OR l.reason LIKE '%不符%'
)
)`
// rockAnomalyTokenScopeSQL 排除已最终发货的 Token(别名固定为 t)。
const rockAnomalyTokenScopeSQL = `t.status <> 'redeemed' AND t.redeemed_at IS NULL`
func rockTokenAnomalyExistsSQL() string {
return `EXISTS (
SELECT 1 FROM rock_token_status_logs l
JOIN rock_tokens t ON t.id = l.token_id
WHERE l.token_id = rock_tokens.id
AND ` + rockAnomalyTokenScopeSQL + `
AND ` + rockAnomalyLogConditionSQL + `
)`
}
@@ -39,32 +48,20 @@ const rockAnomalyRecentSQL = `
l.reason,
l.created_at
FROM rock_token_status_logs l
JOIN rock_tokens t ON t.id = l.token_id
WHERE l.token_id IN (?)
AND (
l.to_status IN ('mismatch', 'abnormal')
OR l.reason LIKE '%失败%'
OR l.reason LIKE '%不存在%'
OR l.reason LIKE '%异常%'
OR l.reason LIKE '%超时%'
OR l.reason LIKE '%拦截%'
OR l.reason LIKE '%不符%'
)
AND ` + rockAnomalyTokenScopeSQL + `
AND ` + rockAnomalyLogConditionSQL + `
ORDER BY l.token_id, l.id DESC
`
const rockAnomalyCountSQL = `
SELECT l.token_id, COUNT(*) AS total
FROM rock_token_status_logs l
JOIN rock_tokens t ON t.id = l.token_id
WHERE l.token_id IN (?)
AND (
l.to_status IN ('mismatch', 'abnormal')
OR l.reason LIKE '%失败%'
OR l.reason LIKE '%不存在%'
OR l.reason LIKE '%异常%'
OR l.reason LIKE '%超时%'
OR l.reason LIKE '%拦截%'
OR l.reason LIKE '%不符%'
)
AND ` + rockAnomalyTokenScopeSQL + `
AND ` + rockAnomalyLogConditionSQL + `
GROUP BY l.token_id
`
@@ -81,6 +78,7 @@ type rockAnomalyCountRow struct {
}
// fillRockTokenAnomalyFlags 为一页 Token 批量补齐异常标记,避免 N+1 查询。
// 已最终发货的 Token 不会命中,因此不会带上异常标记。
func (s *RockService) fillRockTokenAnomalyFlags(ctx context.Context, tokens []model.RockToken) {
if len(tokens) == 0 {
return
@@ -177,3 +177,87 @@ func TestGetStats_AnomalyStockCountsDerivedAnomalies(t *testing.T) {
t.Fatalf("异常标记不应把 Token 计入已上报失效,实际 %d", stats.AbnormalStock)
}
}
// 已最终发货的 Token 不再算异常:发货前的 mismatch 属于可恢复阻断,最终已履约成功。
func TestListTokens_AnomalyFilterExcludesDeliveredTokens(t *testing.T) {
db := newRockTestDB(t)
svc := NewRockService(db, NewRockClient("https://example.com"))
if _, err := svc.GetConfig(context.Background()); err != nil {
t.Fatalf("初始化配置失败: %v", err)
}
now := time.Now()
deliveredByStatus := model.RockToken{
UpstreamTokenID: "DELIVERED-1",
Token: "jwt-delivered-1",
RedeemCode: "CODE-DELIVERED-1",
Color: model.RockColorBlack,
Status: model.RockStatusRedeemed,
BoundAt: &now,
}
deliveredByTime := model.RockToken{
UpstreamTokenID: "DELIVERED-2",
Token: "jwt-delivered-2",
RedeemCode: "CODE-DELIVERED-2",
Color: model.RockColorBlack,
Status: model.RockStatusBound,
BoundAt: &now,
RedeemedAt: &now,
}
pendingToken := model.RockToken{
UpstreamTokenID: "BOUND-1",
Token: "jwt-bound-1",
RedeemCode: "CODE-BOUND-1",
Color: model.RockColorBlack,
Status: model.RockStatusBound,
BoundAt: &now,
}
// 三个 Token 都有 mismatch 历史,只有未发货的那条应算异常。
for _, token := range []*model.RockToken{&deliveredByStatus, &deliveredByTime, &pendingToken} {
if err := db.Create(token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if err := db.Create(&model.RockTokenStatusLog{
TokenID: token.ID,
UpstreamTokenID: token.UpstreamTokenID,
EventType: rockTokenEventStatusChanged,
FromStatus: model.RockStatusUsing,
ToStatus: model.RockStatusMismatch,
Source: rockTokenSourceBindCheck,
Reason: "预期UID(3829515948) 与 扫码UID(3829515950) 不一致",
}).Error; err != nil {
t.Fatalf("创建状态日志失败: %v", err)
}
}
list, total, err := svc.ListTokens(context.Background(), 1, 20, model.RockStatusAnomaly, "")
if err != nil {
t.Fatalf("查询异常列表失败: %v", err)
}
if total != 1 || len(list) != 1 || list[0].ID != pendingToken.ID {
t.Fatalf("已发货 Token 不应命中异常筛选,实际 total=%d len=%d", total, len(list))
}
all, _, err := svc.ListTokens(context.Background(), 1, 20, "", "")
if err != nil {
t.Fatalf("查询全部失败: %v", err)
}
flagged := 0
for i := range all {
if all[i].HasAnomaly {
flagged++
}
}
if flagged != 1 {
t.Fatalf("已发货 Token 不应带异常标记,实际 flagged=%d", flagged)
}
stats, err := svc.GetStats(context.Background())
if err != nil {
t.Fatalf("获取统计失败: %v", err)
}
if stats.AnomalyStock != 1 {
t.Fatalf("异常徽标应只统计未发货的 1 条,实际 %d", stats.AnomalyStock)
}
}
-1
View File
@@ -60,7 +60,6 @@ func (s *RockService) AssignTokenForOrder(ctx context.Context, orderNo, expected
switch token.Status {
case model.RockStatusAbnormal:
return nil, nil, errors.New(rockOrderTokenLockedMessage)
return nil, nil, errors.New("该订单已提交发货,上游官方处理中,不能再次领取提货号")
case model.RockStatusRedeemed:
return nil, nil, errors.New("该订单已发货,不能再次领取提货号")
case model.RockStatusMismatch: