fix: Rock Token 超时按满 2/3 天判定并统一显示口径
This commit is contained in:
@@ -98,6 +98,80 @@ func TestListTokens_OverdueFilters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestListTokens_OverdueBoundaryInclusive 锁定「满 2 天 / 满 3 天」的含边界口径:
|
||||
// 刚好达到或超过阈值必须进入筛选并带超时标记,差 1 小时则不能进入。
|
||||
func TestListTokens_OverdueBoundaryInclusive(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
svc := NewRockService(db, NewRockClient("https://example.com"))
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
exactBoundAt := now.AddDate(0, 0, -2).Add(-time.Second) // 2 天零 1 秒
|
||||
almostBoundAt := now.AddDate(0, 0, -2).Add(time.Hour) // 1 天 23 小时
|
||||
exactUsingAt := now.AddDate(0, 0, -3).Add(-time.Second) // 3 天零 1 秒
|
||||
almostUsingAt := now.AddDate(0, 0, -3).Add(time.Hour) // 2 天 23 小时
|
||||
|
||||
tokens := []model.RockToken{
|
||||
{UpstreamTokenID: "bound-exact", RedeemCode: "BOUND-1", Status: model.RockStatusBound, BoundAt: &exactBoundAt},
|
||||
{UpstreamTokenID: "bound-almost", RedeemCode: "BOUND-2", Status: model.RockStatusBound, BoundAt: &almostBoundAt},
|
||||
{UpstreamTokenID: "using-exact", RedeemCode: "USING-1", Status: model.RockStatusUsing, UsingAt: &exactUsingAt},
|
||||
{UpstreamTokenID: "using-almost", RedeemCode: "USING-2", Status: model.RockStatusUsing, UsingAt: &almostUsingAt},
|
||||
}
|
||||
for i := range tokens {
|
||||
if err := db.Create(&tokens[i]).Error; err != nil {
|
||||
t.Fatalf("创建 Token 失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
boundList, boundTotal, err := svc.ListTokens(ctx, 1, 20, rockTokenStatusBoundOverdue, "")
|
||||
if err != nil {
|
||||
t.Fatalf("查询绑定超时失败: %v", err)
|
||||
}
|
||||
if boundTotal != 1 || len(boundList) != 1 || boundList[0].ID != tokens[0].ID {
|
||||
t.Fatalf("绑定超时边界错误: total=%d list=%+v", boundTotal, boundList)
|
||||
}
|
||||
if !boundList[0].BoundOverdue || boundList[0].BoundDays < 2 {
|
||||
t.Fatalf("绑定超时标记/天数错误: overdue=%v days=%.4f", boundList[0].BoundOverdue, boundList[0].BoundDays)
|
||||
}
|
||||
|
||||
usingList, usingTotal, err := svc.ListTokens(ctx, 1, 20, rockTokenStatusUsingOverdue, "")
|
||||
if err != nil {
|
||||
t.Fatalf("查询使用超时失败: %v", err)
|
||||
}
|
||||
if usingTotal != 1 || len(usingList) != 1 || usingList[0].ID != tokens[2].ID {
|
||||
t.Fatalf("使用超时边界错误: total=%d list=%+v", usingTotal, usingList)
|
||||
}
|
||||
if !usingList[0].UsingOverdue || usingList[0].UsingDays < 3 {
|
||||
t.Fatalf("使用超时标记/天数错误: overdue=%v days=%.4f", usingList[0].UsingOverdue, usingList[0].UsingDays)
|
||||
}
|
||||
|
||||
all, _, err := svc.ListTokens(ctx, 1, 20, "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("查询全部 Token 失败: %v", err)
|
||||
}
|
||||
byID := make(map[uint]model.RockToken, len(all))
|
||||
for _, token := range all {
|
||||
byID[token.ID] = token
|
||||
}
|
||||
if byID[tokens[1].ID].BoundOverdue {
|
||||
t.Fatal("差 1 小时满 2 天的 Token 不应标记绑定超时")
|
||||
}
|
||||
if byID[tokens[3].ID].UsingOverdue {
|
||||
t.Fatal("差 1 小时满 3 天的 Token 不应标记使用超时")
|
||||
}
|
||||
if !byID[tokens[0].ID].BoundOverdue || !byID[tokens[2].ID].UsingOverdue {
|
||||
t.Fatal("刚好达到阈值的 Token 必须标记超时")
|
||||
}
|
||||
|
||||
stats, err := svc.GetStats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("获取统计失败: %v", err)
|
||||
}
|
||||
if stats.BoundOverdueStock != 1 || stats.UsingOverdueStock != 1 {
|
||||
t.Fatalf("超时统计不符: bound=%d using=%d", stats.BoundOverdueStock, stats.UsingOverdueStock)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshStaleTokens_SkipsWhenAutoRefreshDisabled(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
var calls int32
|
||||
|
||||
@@ -572,8 +572,9 @@ func (s *RockService) ListTokens(ctx context.Context, page, pageSize int, status
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
warnDays, _, boundWarnDays := rockOverdueThresholds(cfg)
|
||||
db := applyRockTokenListFilters(s.db.WithContext(ctx).Model(&model.RockToken{}), status, search, time.Now(), warnDays, boundWarnDays)
|
||||
warnDays, dangerDays, boundWarnDays := rockOverdueThresholds(cfg)
|
||||
now := time.Now()
|
||||
db := applyRockTokenListFilters(s.db.WithContext(ctx).Model(&model.RockToken{}), status, search, now, warnDays, boundWarnDays)
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
@@ -589,6 +590,7 @@ func (s *RockService) ListTokens(ctx context.Context, page, pageSize int, status
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
applyRockTokenOverdueFlags(tokens, now, warnDays, dangerDays, boundWarnDays)
|
||||
s.prepareRockTokenList(ctx, tokens)
|
||||
|
||||
return tokens, total, nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
)
|
||||
@@ -27,6 +28,13 @@ func (s *RockService) ListSupportBoundTokens(ctx context.Context, page, pageSize
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
cfg, err := s.GetConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
warnDays, dangerDays, boundWarnDays := rockOverdueThresholds(cfg)
|
||||
now := time.Now()
|
||||
|
||||
db := s.db.WithContext(ctx).Model(&model.RockToken{}).Where("status = ?", model.RockStatusBound)
|
||||
term := "%" + search + "%"
|
||||
db = db.Where(
|
||||
@@ -45,6 +53,7 @@ func (s *RockService) ListSupportBoundTokens(ctx context.Context, page, pageSize
|
||||
Find(&tokens).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
applyRockTokenOverdueFlags(tokens, now, warnDays, dangerDays, boundWarnDays)
|
||||
s.prepareRockTokenList(ctx, tokens)
|
||||
return tokens, total, nil
|
||||
}
|
||||
|
||||
@@ -121,6 +121,39 @@ func rockOverdueCutoff(now time.Time, days int) time.Time {
|
||||
return now.AddDate(0, 0, -days)
|
||||
}
|
||||
|
||||
// applyRockTokenOverdueFlags 用与筛选、统计完全相同的 now 和阈值,
|
||||
// 为列表补充超时标记与实际天数,避免前端用本地时间算出「显示 2.0 天但未超时」。
|
||||
func applyRockTokenOverdueFlags(tokens []model.RockToken, now time.Time, warnDays, dangerDays, boundWarnDays int) {
|
||||
usingCutoff := rockOverdueCutoff(now, warnDays)
|
||||
dangerCutoff := rockOverdueCutoff(now, dangerDays)
|
||||
boundCutoff := rockOverdueCutoff(now, boundWarnDays)
|
||||
for i := range tokens {
|
||||
switch tokens[i].Status {
|
||||
case model.RockStatusUsing:
|
||||
if tokens[i].UsingAt == nil {
|
||||
continue
|
||||
}
|
||||
days := now.Sub(*tokens[i].UsingAt).Hours() / 24
|
||||
if days < 0 {
|
||||
days = 0
|
||||
}
|
||||
tokens[i].UsingDays = days
|
||||
tokens[i].UsingOverdue = !tokens[i].UsingAt.After(usingCutoff)
|
||||
tokens[i].UsingDanger = !tokens[i].UsingAt.After(dangerCutoff)
|
||||
case model.RockStatusBound:
|
||||
if tokens[i].BoundAt == nil {
|
||||
continue
|
||||
}
|
||||
days := now.Sub(*tokens[i].BoundAt).Hours() / 24
|
||||
if days < 0 {
|
||||
days = 0
|
||||
}
|
||||
tokens[i].BoundDays = days
|
||||
tokens[i].BoundOverdue = !tokens[i].BoundAt.After(boundCutoff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// applyRockTokenListFilters 与 Token 列表使用同一套筛选条件,避免“看到多少、删多少”不一致。
|
||||
// 列表、删除预览、全部删除、回收站共用;now 由调用方传入以便同一请求内阈值一致。
|
||||
func applyRockTokenListFilters(db *gorm.DB, status, search string, now time.Time, warnDays, boundWarnDays int) *gorm.DB {
|
||||
|
||||
@@ -55,10 +55,11 @@ func (s *RockService) ListDeletedRockTokens(ctx context.Context, page, pageSize
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
warnDays, _, boundWarnDays := rockOverdueThresholds(cfg)
|
||||
warnDays, dangerDays, boundWarnDays := rockOverdueThresholds(cfg)
|
||||
now := time.Now()
|
||||
db := applyRockTokenListFilters(
|
||||
s.db.WithContext(ctx).Unscoped().Model(&model.RockToken{}).Where("deleted_at IS NOT NULL"),
|
||||
status, search, time.Now(), warnDays, boundWarnDays,
|
||||
status, search, now, warnDays, boundWarnDays,
|
||||
)
|
||||
|
||||
var total int64
|
||||
@@ -79,6 +80,7 @@ func (s *RockService) ListDeletedRockTokens(ctx context.Context, page, pageSize
|
||||
tokens[i].DeletedAtText = tokens[i].DeletedAt.Time.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
applyRockTokenOverdueFlags(tokens, now, warnDays, dangerDays, boundWarnDays)
|
||||
return tokens, total, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user