Skip to content

feat(store): 未付款订单催付(下单 24h 后一次,按计划时间戳扫描) - #41

Merged
worable233 merged 2 commits into
mainfrom
feat/unpaid-order-reminder
Oct 25, 2026
Merged

worable233 merged 2 commits into
mainfrom
feat/unpaid-order-reminder

Conversation

@worable233

Copy link
Copy Markdown
Owner

对标 #12.5 的最后一条业务口径:对方(ZJMF)有 orderOverdue「未付款订单提醒」,我方完全没有 —— 未支付订单除了被取消,不会收到任何提示。

依赖 #40(提醒链)先合;合完 GitHub 会自动把本 PR 的 base 改回 main,差异只剩这一个提交。

做法

订单新增两列(无需迁移:12.1 加 deletedAt/deletedAtMs 时已验证插件模型是 schema 驱动):

  • unpaidRemindAtMs(索引,数值)= 计划催付时间,下单时写 now + 24h
  • unpaidRemindedAt(索引,字符串)= 已催付时间,非空即「已催过」

新任务 unpaid-order-reminder(每小时):扫 state = PENDING + unpaidRemindedAt IS NULL + unpaidRemindAtMs <= now,认领(条件更新)→ 发通知 → 失败退回未催付。三个条件都落在索引列上,不会退化成全表扫描;已催过的行不会反复占批次。

三个刻意的选择

  1. 按「计划时间戳」而不是「每天扫一遍创建时间」:任务停两天,恢复后照样补发。对方的每日等值窗口一旦那天没跑,这条提醒就永久丢了
  2. 每小时扫一次,不是每天一次:代价只是一次索引查询,换来更小的补发延迟
  3. 认领后、发送前再查一次状态:用户刚付完款就收到「请尽快支付」是最招投诉的那种 bug,宁可多一次读

验证

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

  • 到点且未支付 → 发一条并标记
  • 认领失败(别的副本已处理)→ 不发
  • 认领后已支付 → 不发
  • 通知失败 → 退回未催付等重试
  • 催付窗口 = 24h

jobs-contract.test.ts 的清单加入 unpaid-order-reminder 60min(改名/改频率即红);tsc 干净。

toOrder 把这两个内部列从接口返回里剥掉(与 deletedAtMs 同一处理),避免内部标记泄漏到 API。

@worable233
worable233 force-pushed the feat/unpaid-order-reminder branch from 8486a71 to 334209c Compare October 25, 2026 09:25
对标 #12.5 的最后一条业务口径:对方(ZJMF)有 `orderOverdue`「未付款订单提醒」,
我方完全没有 —— 未支付订单除了被取消,不会收到任何提示。

## 做法

- 订单新增两列(无需迁移:12.1 加 `deletedAt`/`deletedAtMs` 时已验证插件模型是 schema 驱动):
  - `unpaidRemindAtMs`(索引,数值)= **计划催付时间**,下单时写 `now + 24h`;
  - `unpaidRemindedAt`(索引,字符串)= 已催付时间,非空即「已催过」。
- 新任务 `unpaid-order-reminder`(每小时):扫 `state = PENDING` + `unpaidRemindedAt IS NULL`
  + `unpaidRemindAtMs <= now`,认领(条件更新)→ 发通知 → 失败退回未催付。
- 三个条件都落在索引列上,不会退化成全表扫描;已催过的行不会反复占批次。

## 三个刻意的选择

1. **按「计划时间戳」而不是「每天扫一遍创建时间」**:任务停两天,恢复后照样补发。
   对方的每日等值窗口一旦那天没跑,这条提醒就永久丢了。
2. **每小时扫一次,不是每天一次**:代价只是一次索引查询,换来更小的补发延迟。
3. **认领后、发送前再查一次状态**:用户刚付完款就收到「请尽快支付」是最招投诉的
   那种 bug,宁可多一次读。

## 验证

`packages/plugins/store` **208 例全绿**(原 203 + 新增 5):
到点且未支付 → 发一条并标记;认领失败 → 不发;认领后已支付 → 不发;
通知失败 → 退回未催付等重试;催付窗口 = 24h。
`jobs-contract.test.ts` 的清单加入 `unpaid-order-reminder` 60min(改名/改频率即红);
`tsc` 干净。

顺带:`toOrder` 把这两个内部列从接口返回里剥掉(与 `deletedAtMs` 同一处理),
避免内部标记泄漏到 API。
CI 的 `notification-type-coverage` 抓到:`unpaid-reminder.ts` 发 `order.unpaid`,
但通知目录里没有这个类型 —— 用户设置里看不到、也关不掉。

- `notifications.ts` 增加 `order.unpaid`(「订单待支付提醒」),与下单即发的
  `order.pending` 分开:用户可以只关催付、保留下单回执。
- `apps/api` 的 `PLUGIN_EMITTERS` 加入 `unpaid-reminder.ts`,否则这个文件里的
  发出点以后不受守门保护。

验证:`apps/api/tests/unit/notification-type-coverage.test.ts` 本地 3 例通过;
反向验证 —— 删掉 `service.overdue` 那行后测试转红并精确报出
`service.overdue (packages/plugins/store/src/expiry.ts)`,说明守门真的在跑。
@worable233
worable233 force-pushed the feat/unpaid-order-reminder branch from 334209c to 2edd7fd Compare October 25, 2026 09:40
@worable233
worable233 changed the base branch from feat/expiry-reminders to main October 25, 2026 09:40
@worable233
worable233 merged commit 159cfda 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