fix: make SMS retry side effects idempotent

This commit is contained in:
hectorzhao
2026-07-26 22:31:30 +08:00
parent 04f78695ed
commit e0f6eed0d4
11 changed files with 516 additions and 105 deletions
@@ -1750,3 +1750,12 @@
3. 迟到的旧尝试结果只允许更新其对应提交尝试和分片审计,不得覆盖短信主记录当前尝试的通道、上游消息号或终态。
4. 发送详情中的“通道发送与回执”必须优先按`SmsMessageSegmentAudit.submitId`重建逐次尝试,显示每次真实通道、发送时间、提交结果及各分片回执;不得用短信主记录最终通道回填所有历史尝试。
5. 通道组成员顺序、优先级、权重或主备关系变更必须写操作审计,保存修改前后有序成员及真实通道名称,便于解释某条短信发送当时使用的路由配置。
## 2026-07-26 长短信失败并发补发与账务幂等补充要求
1. 同一`SmsSubmitRecord`无论收到多少个分片失败回执、重复聚合结果或并发工作线程,只允许创建一个下一跳补发记录。数据库必须以来源提交记录建立唯一补发关系,不能只依靠进程内锁、先查后建或短信主记录当前状态判断。
2. 任一分片明确失败仍可及时判定本次长短信尝试失败,不要求等待其余失败回执;后续分片回执继续完整落库和写通讯日志,但只能复用已取得的补发决定,不得再次向Gateway发布提交命令。
3. 短信扣费、冻结释放和最终失败退款必须使用稳定业务幂等键。企业账户余额变更在数据库事务内按企业串行并使用原子增量,禁止读取旧余额后覆盖写入;相同幂等键重复调用必须返回原交易,不得再次改变余额。
4. 同一短信记录只允许生成一条最终回执投递。CMPP下游投递使用数据库唯一去重键,HTTP Webhook使用稳定事件号和事件/端点唯一关系;重复终态处理返回原投递,不得再次向客户发送。
5. 补发抢占成功、并发复用及下游投递去重必须写结构化日志,至少包含平台消息号、短信记录、来源提交记录、下一跳`submitId`、通道和复用的投递记录。
6. 历史重复提交、通讯报文、回执、退款及客户ACK属于事故审计证据,不得在功能migration中删除或覆盖。历史余额修正必须先完成账户与流水专项对账,再通过可审计冲正处理。
+12
View File
@@ -3895,3 +3895,15 @@ npm run verify:phase8
| TC-SUBMIT-ATTR-005 | 两分片长短信在通道A失败后由通道B补发 | 每个分片结果归属正确`submitId`和通道;任一迟到结果不污染另一尝试 |
| TC-SMS-DETAIL-006 | 查看先经富泷失败、再经铁布衫失败的历史短信详情 | “通道发送与回执”显示两次真实通道及各自分片回执,不把两行都显示为最终通道 |
| TC-CHANNEL-GROUP-007 | 调整通道组成员顺序、优先级、权重或主备 | 操作日志保存修改前后有序成员、通道编号和名称,可还原短信发送时配置 |
## 2026-07-26 长短信并发失败幂等用例
| 用例编号 | 场景 | 预期结果 |
|---|---|---|
| TC-RETRY-RACE-001 | 三分片长短信的三个失败回执并发进入API,备用通道可用 | 三个回执和通讯报文全部保存;来源提交记录只关联一个补发记录,只发布一个Gateway命令、三个补发分片 |
| TC-RETRY-RACE-002 | 三个线程在唯一补发记录提交前后交错执行 | 只有一个线程取得`retryOfSubmitRecordId`唯一关系;其他线程返回同一下一跳`submitId`并写复用日志,不退款、不生成最终回执 |
| TC-RETRY-RACE-003 | 三个失败回执并发处理且没有可用备用通道 | 主记录最终失败;只产生一笔退款交易和一次余额增量,只生成一个CMPP最终失败回执及一个HTTP回调事件 |
| TC-BILLING-IDEM-004 | 三个线程使用同一短信退款幂等键并发退款 | 三次调用返回同一交易ID,`AccountTransaction`只有一条,账户余额只增加一次 |
| TC-BILLING-ATOMIC-005 | 同一企业同时发生扣费、退款和充值 | 账户级事务锁串行化余额变更,使用数据库原子增量;每条流水`balanceAfter`连续且最终余额与流水一致 |
| TC-DOWNSTREAM-IDEM-006 | 同一短信终态被重复处理,企业同时启用CMPP和HTTP | CMPP只有一条`CmppDownstreamDelivery`且只发送一次;HTTP只有一个稳定事件和一条端点投递 |
| TC-MIGRATION-IDEM-007 | 在含历史重复最终回执的预生产数据上执行migration | 历史行全部保留;每个短信只给最早一条历史回执设置唯一键,其余保持空键;新数据开始强制唯一 |
+10
View File
@@ -2529,3 +2529,13 @@ git diff --check
- 标准部署脚本完成两套干净依赖安装、安全门禁、Prisma生成/迁移检查、前端/API/Gateway构建和服务重启;72条migration齐全且无待执行项,`.deployed-commit=0857de09d82aec7233108fb6700c40a6183c21f8`。生产依赖审计仍报告前端2个high(未使用的React Router RSC路径)和API 3个moderatePrisma CLI工具链),自动安全门禁确认既定缓解继续有效,未执行破坏兼容性的`audit fix --force`
- 发布后API、Gateway、Nginx、PostgreSQL、MinIO均activeRedis`PONG``12026/17890/8090/3000/6379/5432/9000`均监听,内外健康页及运营/客户端入口HTTP 200,公网CMPP 17890可连接;Redis提交流消费者1、`pending=0``lag=0`,发布后API/Gateway无error级日志。
- Gateway重启时供应商曾对“会员营销-富泷”首次登录返回`auth failed`,系统按鉴权失败5分钟慢重试策略等待而未高频重连;21:42:17自动重试成功,最终4个启用通道全部`connected/currentConnections=1/desiredConnections=1`。历史17:53短信数据库只读验证仍为富泷两个`FLBLACK`分片、铁布衫两个`WL:FSNM`分片,证明部署未改写证据且新详情具备正确重建数据。
## 2026-07-26 `MSG-65a47514`长短信并发重复补发P0修复(发布前)
- 预生产只读证据确认`MSG-65a47514-7f30-49af-8113-b76747550199`正文408字、3个计费/协议分片。12:33:33经会员营销-铁布衫提交一次;12:58:17三个`WL:CGMT`失败回执在9毫秒内到达,三个处理线程分别触发整条补发,在约13毫秒内创建三个富泷`submitId`,形成“铁布衫1次+富泷3次”、共4次整条尝试和12个真实Submit分片。所有尝试均为失败回执,没有`DELIVRD`成功证据。
- 并发影响还包括三个已被客户`Result=0`确认的最终失败回执、三条各1053分的退款流水,以及非原子余额覆盖导致的实际多退。通道尝试记录成本为铁布衫993分、富泷3168分;是否形成供应商真实账单需另行对账。本轮不删除历史提交、回执、投递、ACK或账务证据,不自动冲正现有余额。
- 新migration`20260726223000_prevent_duplicate_retry_side_effects``SmsSubmitRecord`增加唯一`retryOfSubmitRecordId`自关联、为`AccountTransaction`增加唯一`idempotencyKey`、为`CmppDownstreamDelivery`增加唯一`dedupeKey`。历史重复下游回执只给每条短信最早的回执设置稳定键,其余历史行原样保留。
- 补发记录、提交会话计数和短信当前尝试改为同一数据库事务创建;相同来源提交记录的并发线程只有一个能取得下一跳,其他线程读取并复用已存在的补发,禁止再次向Gateway发布。缺少可审计来源提交记录时安全停止补发并写结构化错误。
- 企业账户通用余额变更改为企业级PostgreSQL事务锁加原子`increment`;短信扣费、提交成功释放冻结、最终释放和退款使用平台消息号稳定幂等键。三次并发退款专项用例返回同一交易ID,只创建一条流水并只改变一次余额。
- CMPP最终回执使用`receipt:{messageRecordId}`唯一键;HTTP回执事件使用`evt_receipt_{messageRecordId}`稳定事件号并复用事件/端点唯一投递。并发专项用例证明三个完成线程只向Gateway发送一次最终回执;所有抢占、复用和投递去重均写结构化日志。
- 当前专项门禁:发送链、账务和HTTP API共3 suites / 121 tests通过,包含三个长短信失败处理线程并发抢占、三次并发退款及三次最终回执创建。全量API回归26 suites / 346 tests通过,API TypeScript构建检查、Prisma schema校验、前端TypeScript/Vite/生产依赖安全门禁及Gateway Go测试/vet均通过;提交、推送和预生产部署结果待完成后补记。