From ec5c1996a70fcc84f41f1eedcd1bb1d74a5f310c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E8=BF=9B?= Date: Sat, 24 Oct 2026 17:28:18 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=87=8D=E7=AE=97=E5=88=B0=E6=9C=9F?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E4=B8=8D=E6=8A=B5=E6=B6=88=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E9=80=80=E9=81=BF=EF=BC=8C=E9=87=8D=E7=BD=AE=E6=97=A5=E5=89=8D?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=8F=AA=E8=AE=A1=E6=9C=89=E7=94=9F=E6=95=88?= =?UTF-8?q?=E5=A5=97=E9=A4=90=E7=9A=84=E5=8D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 上次检查时间只在查询成功时更新,按"上次检查 + 新间隔"会把失败或限流延后的卡拉到立即执行; 过期或从未检查时候选改为"现在 + 新间隔",只提前排队超过一个新间隔的成员 - 重置日前加密统计只记录有生效套餐的卡,与"有生效套餐但窗口内未查到"口径一致 --- internal/polling/pre_reset.go | 4 ++++ internal/polling/recalculate.go | 16 +++++++++++++++- .../tasks.md | 8 ++++---- 3 files changed, 23 insertions(+), 5 deletions(-) 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 构造结算慢返回且另一入口先激活:激活前用量只记已结算。