fix: 已上报失效的 Token 不再计入异常
- 异常判定排除 status=abnormal 的 Token:库存状态已明确标识失效,不重复标记 - 与已发货排除共用 rockAnomalyTokenScopeSQL,筛选/统计/列表标记口径一致 - 新增已上报失效 Token 排除的单元测试
This commit is contained in:
@@ -12,8 +12,10 @@ import (
|
||||
// 异常判定口径(与产品确认一致):
|
||||
// 1. 状态流转历史中出现过 mismatch / abnormal(绑定错误、上游失效);
|
||||
// 2. 状态流转原因或备注中出现失败类关键字(兑换失败、刷新失败、卡密不存在、超时等);
|
||||
// 3. 已最终发货的 Token(status=redeemed 或 redeemed_at 非空)不再算异常:
|
||||
// 发货前的 mismatch / 失败属于可恢复阻断,最终已履约成功,不需要人工处理;
|
||||
// 3. 已有明确结论的 Token 不再算异常:
|
||||
// - 已最终发货(status=redeemed 或 redeemed_at 非空):发货前的 mismatch / 失败属于可恢复
|
||||
// 阻断,最终已履约成功,不需要人工处理;
|
||||
// - 已上报失效(status=abnormal):库存状态列已明确标识,无需重复标记;
|
||||
// 4. 只要命中任意一条即为「异常」,不改变 Token 主状态。
|
||||
//
|
||||
// 采用子查询而不是落库字段,保证历史数据无需迁移即可被筛选到。
|
||||
@@ -27,8 +29,9 @@ const rockAnomalyLogConditionSQL = `(
|
||||
OR l.reason LIKE '%不符%'
|
||||
)`
|
||||
|
||||
// rockAnomalyTokenScopeSQL 排除已最终发货的 Token(别名固定为 t)。
|
||||
const rockAnomalyTokenScopeSQL = `t.status <> 'redeemed' AND t.redeemed_at IS NULL`
|
||||
// rockAnomalyTokenScopeSQL 排除已有明确结论的 Token(别名固定为 t):
|
||||
// 已最终发货(redeemed 或 redeemed_at 非空)与已上报失效(abnormal)。
|
||||
const rockAnomalyTokenScopeSQL = `t.status NOT IN ('redeemed', 'abnormal') AND t.redeemed_at IS NULL`
|
||||
|
||||
func rockTokenAnomalyExistsSQL() string {
|
||||
return `EXISTS (
|
||||
|
||||
@@ -261,3 +261,66 @@ func TestListTokens_AnomalyFilterExcludesDeliveredTokens(t *testing.T) {
|
||||
t.Fatalf("异常徽标应只统计未发货的 1 条,实际 %d", stats.AnomalyStock)
|
||||
}
|
||||
}
|
||||
|
||||
// 已上报失效(abnormal)的 Token 状态列已明确标识,不再叠加异常标记。
|
||||
func TestListTokens_AnomalyFilterExcludesAbnormalTokens(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()
|
||||
abnormalToken := model.RockToken{
|
||||
UpstreamTokenID: "ABNORMAL-1",
|
||||
Token: "jwt-abnormal-1",
|
||||
RedeemCode: "CODE-ABNORMAL-1",
|
||||
Color: model.RockColorBlack,
|
||||
Status: model.RockStatusAbnormal,
|
||||
BoundAt: &now,
|
||||
}
|
||||
boundToken := model.RockToken{
|
||||
UpstreamTokenID: "BOUND-2",
|
||||
Token: "jwt-bound-2",
|
||||
RedeemCode: "CODE-BOUND-2",
|
||||
Color: model.RockColorBlack,
|
||||
Status: model.RockStatusBound,
|
||||
BoundAt: &now,
|
||||
}
|
||||
|
||||
for _, token := range []*model.RockToken{&abnormalToken, &boundToken} {
|
||||
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.RockStatusBound,
|
||||
ToStatus: model.RockStatusAbnormal,
|
||||
Source: rockTokenSourceRefresh,
|
||||
Reason: "上游卡密不存在(刷新自动同步)",
|
||||
}).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 != boundToken.ID {
|
||||
t.Fatalf("已上报失效 Token 不应命中异常筛选,实际 total=%d len=%d", total, len(list))
|
||||
}
|
||||
|
||||
stats, err := svc.GetStats(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("获取统计失败: %v", err)
|
||||
}
|
||||
if stats.AnomalyStock != 1 {
|
||||
t.Fatalf("异常徽标应只统计 1 条,实际 %d", stats.AnomalyStock)
|
||||
}
|
||||
if stats.AbnormalStock != 1 {
|
||||
t.Fatalf("已上报失效库存应正常统计为 1,实际 %d", stats.AbnormalStock)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user