77 lines
9.4 KiB
Markdown
77 lines
9.4 KiB
Markdown
# 上行短信待认领只读诊断
|
||
|
||
核查日期:2026-09-17,北京时间。状态:诊断完成;2026-09-17本地修复与隔离验收见第6节,线上未认领、未投递。范围为预生产既存上行和当前匹配代码,不沿用测试环境发送/补发授权。
|
||
|
||
## 1. 基线与结论
|
||
|
||
本地main及实际远端均为`4eb7b16d122da14f921093716d4ca1ed390d9e4c`,暂存区为空。预生产实际运行`010ba3216889032a6160cdb14d8536b616ae7102`。已有版本、metrics、发布工具、部署及前轮优化方案等修改全部保留。
|
||
|
||
当前共253条上行:159条ambiguous(页面“待认领”)、92条matched、2条unmatched。“待认领”不是接收失败:上行已入库,但没有确定应用归属,当前不会按正常已匹配路径推送给客户。
|
||
|
||
| 待认领原因 | 数量 | 真实数据复核 |
|
||
|---|---:|---|
|
||
| 接入号匹配多个应用 | 147 | 143条在接收前72小时仅有一个发送应用,且属于原候选应用、具有同上行通道accepted提交;4条在此限定口径下无下发记录 |
|
||
| 手机号窗口有多条下发 | 12 | 保存的两个候选全属同一应用/企业;完整时间窗口复核也只有一个应用,且有同通道accepted提交 |
|
||
|
||
因此155/159条具有“应用归属可以进一步收敛”的真实证据,不代表155条均能唯一确定被回复的具体业务短信,也不构成直接历史认领/投递授权。4条无证据记录保持未决,不因为某条路由现在有效就推断历史归属。历史源记录可能变化或被治理,当前回查不能替代完整历史快照。
|
||
|
||
接入号多应用147条分别位于移动物业-富泷78、三网物业-百信互动52、三网物业-铁布衫15、赛邮行业-王斯评中转2;它们的destId均等于当前通道srcId。12条手机号多记录位于三网物业-百信互动10、联电物业-富泷2。9月16日有31条待认领,9月15日12条。
|
||
|
||
## 2. 已确认根因
|
||
|
||
匹配实现:`api/src/send-chain/send-downstream-delivery.service.ts`的`resolveUplinkMatch`。页面`src/apps/admin/AdminSmsUplinkRecordsPage.tsx`将ambiguous直接映射为待认领,不是前端计算错误。
|
||
|
||
### 2.1 共享接入号过早返回
|
||
|
||
当前顺序为messageId精确匹配→接入号路由查应用。接入号得到多个active应用后,立即返回ambiguous与应用候选,完全不执行后面的手机号时间窗查询。
|
||
|
||
这与需求中“仍无唯一应用时按手机号和最近下发时间窗口匹配”的描述存在差距。共享通道接入号只表示有多个可能客户,不足以否定“该手机近期仅被其中一个客户发送”的进一步证据。
|
||
|
||
真实只读复现记录`cmu48o30s00fo34nkyp3phwt9`:运行中的编译类只调用`channelRouteRule.findMany`和`smsApplication.findMany`,返回3个应用候选;没有查询短信记录。同手机号接收前72小时实际只有一个应用的同通道accepted发送。
|
||
|
||
### 2.2 将短信记录歧义等同应用归属歧义
|
||
|
||
手机号分支取最近两条消息,按记录数量判断唯一或歧义,没有先按tenantId/applicationId归并。一个应用给同一手机发送两次,也被阻断客户归属。
|
||
|
||
真实只读复现`cmtmq3vef0ioyeankps3d80yl`:运行类返回两条phone_window候选,但distinct application数为1。12条同类存量均符合这一情况。可以有唯一客户归属而没有唯一原短信,应分别表达,不能为了绑定客户随意填最近一条messageRecordId/messageId。
|
||
|
||
## 3. 代码边界风险(不等于已经发生错误投递)
|
||
|
||
1. 时间窗下限由`Date.now()`计算而非事件receivedAt,且没有`submittedAt <= receivedAt`上限。延迟消费/故障恢复可能错过真实历史下发,或把上行之后的发送纳入。
|
||
2. 手机号回查未限定实际发送通道/供应商边界,也未校验真实accepted提交;仅按message.submittedAt查。不能在修复时直接扩大自动匹配,必须先收紧证据范围。确需同供应商跨物理连接兼容时,另核对账号、主机、端口、协议等边界,不能跨任意通道匹配。
|
||
3. 路由查询`take:10`发生在应用去重之前、没有稳定排序:前10条若同属一个应用可能漏掉其他应用,造成假唯一或候选遗漏。手机号`take:2`也只适于证明多条记录,不能据此证明窗口内只有一个应用。
|
||
4. 接入号只与通道静态srcId精确比较,没有完整核对实际发送号码/应用扩展码。其差异会进入宽泛手机号兜底,不能用当前路由代替历史发送证据。
|
||
5. messageId精确分支未同时验证手机号/通道来源;Gateway普通MO通过packet Msg_Id尝试查本地command。供应商上行ID应继续独立保存为gatewayMessageId,不能把它当已证明的原下发ID。本轮未发现碰撞或错误关联实例。
|
||
6. 上行入库、候选写入与投递分别进行;eventId已存在即返回,部分失败恢复可能留缺候选/缺投递。人工认领是先读状态再更新,未见条件更新原子竞争保护。属于附带发现的可靠性风险,本轮未做故障注入或并发写入复现。
|
||
|
||
对现有85条“手机号窗口唯一匹配”和4条“messageId精确匹配”执行只读边界核对:关联消息均存在、手机号一致、关联提交时间不晚于上行,均存在此前同通道accepted提交。未在这89条中检出上述明显异常;此结果不证明所有潜在风险不存在。
|
||
|
||
## 4. 建议修复方向(未实施)
|
||
|
||
- 分开“应用归属”与“原短信关联”。限定证据范围后应用唯一即可确定客户;具体消息仍多条时messageRecordId/messageId留空,并保留真实候选及原因。
|
||
- 共享接入号多应用时继续在候选应用中,结合手机号、上行接收时刻之前的72小时以及真实发送通道/接入号收敛;候选间有冲突仍待认领,不按最后一条或最高分随意选。
|
||
- 查询完整的distinct应用集合或以安全的唯一性检测查询证明唯一;展示候选可以分页,唯一性判断不能先截断。时间窗配置须校验正数有限值,上行receivedAt须校验合法性。
|
||
- 原始供应商MO ID、平台业务ID保持不同语义;无可信原短信关联不得伪造业务messageId。
|
||
- 配套原子入库/认领、通知意图与故障接续;客户查询仍严格隔离应用,ambiguous不得泄露给候选客户。
|
||
- 历史159条先生成只读建议清单,核验保留数据、有效应用、完整号码/通道证据及投递历史,再在明确授权后决定是否认领及是否推送。修复代码不自动批量认领、不自动把旧上行重新入队或投递。
|
||
|
||
## 5. 证据、复现限制与后续验证
|
||
|
||
忽略目录`.local-data/cpu-20260917/`中的`uplink-audit.json`、`uplink-correlation.json`、`uplink-reproduce.json`、`uplink-boundaries.json`及同名mjs为本机证据;输出只含统计、内部记录ID和必要通道标签,不导出手机号、正文或凭据。
|
||
|
||
复现直接加载预生产当前编译的`SendDownstreamDeliveryService.resolveUplinkMatch`,使用真实PG只读事务(8~15秒statement_timeout),不启动Nest生命周期,不调用handleUplink/claim/投递方法。仅在该独立诊断进程将Date.now固定到被查上行的receivedAt以复现旧事件分支,配置仍是当前数据库路由,不能宣称重建了当时全部配置。未执行管理HTTP或浏览器交互验收。
|
||
|
||
后续修复验收至少覆盖共享接入号+唯一应用、同应用多消息、多应用真实冲突、无记录、超过10条路由/超过2条消息、延迟消费/跨日、扩展码、跨供应商隔离、MO ID不冒充MT ID、事务中断、重复事件、并发认领、无重复投递及租户隔离。真实发送/推送只能在后续明确授权的隔离范围执行。
|
||
|
||
本轮业务代码、数据库和线上配置均未修改;仅新增诊断及追加进度记录,未提交、推送或部署。
|
||
|
||
## 6. 2026-09-17 本地修复
|
||
|
||
用户授权修复并本地提交。新增`api/src/send-chain/uplink-matching.ts`,共享接入号继续核对手机号在事件receivedAt前72小时的同通道accepted记录;按tenant/application归并,不以消息条数制造歧义;无take10/take2提前截断。messageId分支同时验证手机号和通道发送事实;收到的供应商gatewayMessageId独立保存。只能确定应用时不填写原短信ID,多应用/配置与事实冲突继续待认领。独占接入号无发送证据的原兼容规则保留;供应商扩展号码改写与跨物理通道归并没有扩大自动归属规则,需有真实协议证据后另行处理。
|
||
|
||
上行记录、候选和CMPP/HTTP通知意图纳入同一事务,沿用发送链路事务适配器;按事件ID事务锁和唯一键去重。人工认领按上行ID串行核验,只有一个候选可成功,认领与通知意图同事务;不在事务里执行外部推送,后续沿用已有耐久队列消费。已有事件直接返回,不擅自修补或重推历史半完成事件。
|
||
|
||
真实隔离PG验证:同应用多消息、第三条不同应用、接收前窗口、其他通道拒绝、原短信为空、重复事件只入一次/通知一次、两候选并发仅一次成功、通知存储失败回滚并重试恢复。自动化入口`tools/testing/verify-uplink-matching.mjs`及签名验收脚本;API全量回归见testing-progress。
|
||
|
||
原159条待认领保持原状;155条可收敛是取证结果,不是已经认领/推送。本轮未连接线上执行认领、重投或修配置,未推送/部署。历史建议清单、实际供应商扩展码、客户端最终收到MO和线上故障恢复仍未验证。
|