Skip to content

feat(store): 到期提醒改成阶段链(到期前 2 次 + 逾期 2 次),并修掉暂停阶段的认领漏洞 - #40

Merged
worable233 merged 2 commits into
mainfrom
feat/expiry-reminders
Oct 25, 2026
Merged

worable233 merged 2 commits into
mainfrom
feat/expiry-reminders

Conversation

@worable233

Copy link
Copy Markdown
Owner

对标 #12.5 的两条结论落成代码。

之前

  • 到期前只发 1 次 提醒(T-7),逾期后 0 次,直接进宽限期 → 暂停
  • expiryStage 只有 reminded → suspending 一条路,suspendExpired 只认 'reminded' | null 作为可认领前置阶段

现在

提醒链是一张表(EXPIRY_REMINDER_STEPS),每档一次 CAS:

null → reminded(T-7) → reminded-final(T-1) → overdue-1(T+1) → overdue-2(T+2) → suspending
  • 逐档查询(listReminderCandidates)而不是一次捞全再在内存里筛:每档单独限批,一档积压不会把后面几档饿死;fromStage 直接就是 claimExpiryStage 的 CAS 条件
  • 漏跑能补:一次 sweep 可以把落下的几档全部补齐(服务停机两天 → 四档一起发)。漏发的提醒比迟发的提醒更糟
  • 通知失败只退回上一档(resetExpiryStage(id, from, to)),不是置空 —— 置空会把第一档提醒重复发给用户
  • 暂停不再卡死:suspendExpired 接受「仍在提醒链上」的任意阶段,否则停在 overdue-2 的服务永远不会被暂停(这是旧实现的真 bug,只是旧阶段名少而没暴露)

为什么不做对方那样的「逾期 3 次」

ZJMF 的 3 次逾期提醒是按它更长的宽限期设计的。我方宽限期固定 3 天,硬塞第 3 次会让「即将暂停」和「已暂停」在同一轮发出 —— 用户同时收到两条相反信息。所以逾期排 2 档,第 3 次由「已暂停」通知承担。要改回 3 档加一行常量即可,不需要迁移。

验证

packages/plugins/store 203 例全绿(原 198 + 新增 5):

  • T-1 发第二档、类型仍是 service.expiring
  • 逾期第 1/2 天各发一次、类型是 service.overdue
  • 停机补发:null + 已到期 2 天 → 一次 sweep 连推 4 档,终态 overdue-2
  • 某档通知失败 → 阶段退回上一档(不是 null),恢复后只补这一档
  • 停在 overdue-2 且过了宽限期的服务仍能被暂停(旧实现会永久卡住)

tsc 干净;listDueExpiryReminders 随旧单档逻辑删除(无引用)。

对标 #12.5 的两条结论落成代码。

## 之前

- 到期前只发 **1 次** 提醒(T-7),逾期后 **0 次**,直接进宽限期 → 暂停。
- `expiryStage` 只有 `reminded → suspending` 一条路,`suspendExpired` 只认
  `'reminded' | null` 作为可认领前置阶段。

## 现在

提醒链是一张表(`EXPIRY_REMINDER_STEPS`),每档一次 CAS:

    null → reminded(T-7) → reminded-final(T-1) → overdue-1(T+1) → overdue-2(T+2) → suspending

- **逐档查询**(`listReminderCandidates`)而不是一次捞全再在内存里筛:每档单独限批,
  一档积压不会把后面几档饿死;`fromStage` 直接就是 `claimExpiryStage` 的 CAS 条件。
- **漏跑能补**:一次 sweep 可以把落下的几档全部补齐(服务停机两天 → 四档一起发)。
  漏发的提醒比迟发的提醒更糟。
- **通知失败只退回上一档**(`resetExpiryStage(id, from, to)`),不是置空 —— 置空会把
  第一档提醒重复发给用户。
- **暂停不再卡死**:`suspendExpired` 接受「仍在提醒链上」的任意阶段,否则停在
  `overdue-2` 的服务永远不会被暂停(这是旧实现的真 bug,只是旧阶段名少而没暴露)。

## 为什么不做对方那样的「逾期 3 次」

ZJMF 的 3 次逾期提醒是按它更长的宽限期设计的。我方宽限期固定 3 天,硬塞第 3 次会让
「即将暂停」和「已暂停」在同一轮发出 —— 用户同时收到两条相反信息。所以逾期排 2 档,
第 3 次由「已暂停」通知承担。要改回 3 档加一行常量即可,不需要迁移。

## 验证

`packages/plugins/store` **203 例全绿**(原 198 + 新增 5):
- T-1 发第二档、类型仍是 `service.expiring`;
- 逾期第 1/2 天各发一次、类型是 `service.overdue`;
- 停机补发:`null` + 已到期 2 天 → 一次 sweep 连推 4 档,终态 `overdue-2`;
- 某档通知失败 → 阶段退回上一档(不是 null),恢复后只补这一档;
- 停在 `overdue-2` 且过了宽限期的服务仍能被暂停(旧实现会永久卡住)。

`tsc` 干净;`listDueExpiryReminders` 随旧单档逻辑删除(无引用)。
@worable233
worable233 merged commit 29fb047 into main Oct 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant