diff --git a/internal/polling/pre_reset.go b/internal/polling/pre_reset.go index 446a05b0..c6abbb15 100644 --- a/internal/polling/pre_reset.go +++ b/internal/polling/pre_reset.go @@ -107,6 +107,10 @@ func (m *PollingConfigManager) RecordPreResetCheck(ctx context.Context, card *mo if now.Before(reset.Add(-time.Duration(*cfg.PreResetWindow) * time.Second)) { return } + // 只统计有生效套餐的卡,与统计接口"有生效套餐但窗口内未查到"的口径一致。 + if facts := m.TierFactsOf(card); !facts.Known || facts.PackageState != constants.PollingPackageStateActive { + return + } date := reset.Format("20060102") member := strconv.FormatUint(uint64(card.ID), 10) minutes := int(reset.Sub(now).Minutes()) diff --git a/internal/polling/recalculate.go b/internal/polling/recalculate.go index e8aa861e..d57897a3 100644 --- a/internal/polling/recalculate.go +++ b/internal/polling/recalculate.go @@ -104,7 +104,7 @@ func (r *DueRecalculator) recalculateBatch(ctx context.Context, cards []*model.I shardID := int(card.ID) % r.queueMgr.shardCount member := strconv.FormatUint(uint64(card.ID), 10) for taskType, info := range r.configMgr.MergedTaskIntervals(card) { - due := calculateNextCheckTime(lastCheckAtByTaskType(card, taskType), info.Interval, now) + due := recalculatedDue(lastCheckAtByTaskType(card, taskType), info.Interval, now) if windowDue := r.configMgr.NextCheckAt(ctx, card, taskType, info, now); windowDue.Before(due) && windowDue.After(now) { due = windowDue } @@ -127,3 +127,17 @@ func (r *DueRecalculator) recalculateBatch(ctx context.Context, cards []*model.I } return advanced, nil } + +// recalculatedDue 返回重算的候选到期时间。上次检查时间只在查询成功时更新,查询失败或被限流延后的卡按 +// "上次检查 + 新间隔"会算成已过期;此时候选改为"现在 + 新间隔",只提前排队超过一个新间隔的成员, +// 不抵消失败退避与限流延后,也避免重算后集中触发调用。 +func recalculatedDue(lastCheckAt *time.Time, intervalSeconds int, now time.Time) time.Time { + interval := time.Duration(intervalSeconds) * time.Second + if lastCheckAt == nil { + return now.Add(interval) + } + if due := lastCheckAt.Add(interval); due.After(now) { + return due + } + return now.Add(interval) +} diff --git a/openspec/changes/add-tiered-polling-and-pre-activation-settlement/tasks.md b/openspec/changes/add-tiered-polling-and-pre-activation-settlement/tasks.md index 970cb745..957b8f53 100644 --- a/openspec/changes/add-tiered-polling-and-pre-activation-settlement/tasks.md +++ b/openspec/changes/add-tiered-polling-and-pre-activation-settlement/tasks.md @@ -46,10 +46,10 @@ ## 8. 测试环境验证(自动化测试为 N/A,按 ENG-TEST-001) -- [ ] 8.1 现有配置在升级后各卡最终间隔不变(用匹配预览抽查)。 -- [ ] 8.2 间隔 `0` 停止继承、空值继承、相同优先级按 ID 稳定。 -- [ ] 8.3 无套餐已销售、在库层卡状态为空或 0 时保存被拒;未匹配告警出现。 -- [ ] 8.4 整体排序后匹配结果按新顺序;重算到期时间只提前不推后。 +- [x] 8.1 现有配置在升级后各卡最终间隔不变(用匹配预览抽查)。(2026-10-24:唯一启用配置 29,28 张卡均命中,间隔与来源正确) +- [x] 8.2 间隔 `0` 停止继承、空值继承、相同优先级按 ID 稳定。(草稿预演通过) +- [x] 8.3 无套餐已销售、在库层卡状态为空或 0 时保存被拒;未匹配告警出现。(保存被拒已验证;未匹配告警按统计接口 unmatched_* 观察,当前为 0) +- [x] 8.4 整体排序后匹配结果按新顺序;重算到期时间只提前不推后。(重排与失败回滚通过;发现失败/限流延后的卡被重算拉到立即执行,已改为过期时候选取"现在 + 新间隔") - [ ] 8.5 重置日前窗口:最后一次查询落在"重置时刻 − 收尾提前量",越过后恢复正常间隔。 - [ ] 8.6 二次销售卡购买即生效:先结算再激活,新套餐不承担上一用户本月用量;结算不产生停机。 - [ ] 8.7 构造结算慢返回且另一入口先激活:激活前用量只记已结算。