Files
lislgosms/docs/cmpp-protocol-field-compatibility-remediation-20260920.md
T
hectorzhao 001d5f2cbd
CSS quality / css-quality (push) Has been cancelled
fix: 修复 CMPP 协议字段容量与版本兼容性
2026-09-20 15:49:37 +08:00

212 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CMPP 协议字段兼容性整改方案
日期:2026-09-20。状态:六类整改已在 `b24cd7c` 基线上实施并完成本地定向验收;目标环境未迁移、未发布。实施证据及剩余边界见第 10 节。
## 1. 目标、范围与授权
解决平台内部字段容量和编解码规则小于 CMPP 协议允许范围的问题,避免合法回执、上行和提交结果被拒绝,或被错误解释。
本方案覆盖 CMPP 2.0/3.0 的 Gateway 编解码、Redis/HTTP 事件、后端参数、Prisma、PostgreSQL 和下游投递。初稿授权仅为文档审查;后续用户明确授权基于最新代码执行方案、修复并本地提交。实施范围为本地代码、隔离基础设施验证和文档,不包含推送、环境发布、预生产迁移、再次重投或业务配置变更。应急恢复独立记录,不作为本方案验收结果。
本方案是 [发送链路设计](phase-4-send-pipeline-redesign.md) 的字段兼容性补充,不替代其发送尝试归属、终态、补发、事务和账务设计;不替代 [计费方案](phase-5-billing-plan.md)、[测试计划](testing-plan.md) 和 [部署手册](production-deployment.md)。协议兼容整改不改变客户费率、路由、补发策略或退款规则。
实施前复读 [需求](first-version-development-requirements.md)、[系统测试用例](system-functional-test-cases.md)、[测试进度](testing-progress.md) 与当前代码;将本文新增用例纳入既有测试体系,避免形成两套验收标准。
## 2. 初稿审查基线与证据边界(历史记录)
- 本地审查基线:main / `c20c224`;存在大量其他会话未提交修改,不能将工作区整体作为本方案可提交内容。上述提交包含的长短信终态修复与本方案不是同一变更。
- 预生产只读核验:2026-09-20 14:56:46(北京时间),应用标记为 `1676cfe622f648bbfe09ac027e3db91737a7789d`。通过 `information_schema.columns` 核对实际字段类型,并核对部署目录源码/后端产物中的相关实现。该记录不是以后发布时可复用的现场结论,也不是对运行 Gateway 二进制逐项反编译验证。
- 已知线上故障:上游回执头序号 `2245918284``2245918288` 超过 PostgreSQL integer 上限,原业务回执入库受阻。完整应急恢复清单、处理结果和副作用由主任务独立记录,本方案不宣称恢复已完成。
- 其余发现来自协议原文、静态调用路径和内存边界核算;尚未用真实 TCP、真实后端和隔离 PostgreSQL 完成回归,不能写成线上已经发生同类故障。
- 本次未修改代码、数据库、Redis、业务配置或服务,未发送测试报文。
协议依据为中国移动规范原文的公开镜像:
1. [CMPP 2.0](https://www.kannel.org/~tolj/specs/CMPP2/CMPP-2.0.pdf):第 7.3 节消息头及 SP/ISMG 消息定义。
2. [CMPP 3.0](https://www.kannel.org/~tolj/specs/CMPP2/CMPP-v30.pdf):第 8.3 节消息头、第 8.4.1.2 节连接响应、第 8.4.3 节 Submit、第 8.4.5 节 Deliver/状态报告及 Deliver 响应。
协议中的字段字节数还须结合语义限制解释,不能把字段可表示范围当作所有业务值都有效。例如 Msg_Length 虽占 1 字节,仍受对应编码的内容长度规定约束。
## 3. 问题清单
| 编号 | 优先级 | 问题及证据 | 后果与当前确认程度 |
|---|---|---|---|
| CMPP-FIELD-01 | P1,首批必修 | 5 个 sequenceId 字段为有符号 integer;协议为 4 字节无符号整数 | 已发生回执入库故障;其他字段同类风险已确认,尚未逐条实测 |
| CMPP-FIELD-02 | P2 | 两张下游表的 ackResult 为 integerCMPP 3.0 DELIVER_RESP.Result 为 4 字节无符号整数,9 及以上可表示其他错误 | 大错误码可能入库失败,常见 0~9 不触发此容量问题;未确认线上命中 |
| CMPP-FIELD-03 | P1 | 2.0/3.0 共用固定 60 字节状态报告编解码,号码字段固定 21 字节 | 3.0 应为 32 字节号码字段、71 字节报告体;可能错读 SMSC_sequence,发出不符合 3.0 格式的报告 |
| CMPP-FIELD-04 | P1 | 下游目标选择过滤 sequenceId <= 0Gateway 用 0 表示缺失 | 0 可由无符号字段表示,已核对章节未将其保留;合法 0 序号无法正确生成或恢复通知 |
| CMPP-FIELD-05 | P1 | 3.0 CONNECT_RESP.Status 从 uint32 强转 uint8 | 256 会截为 0,客户端误判认证成功;这是本地状态误判,不代表上游真的授予权限 |
| CMPP-FIELD-06 | P2 | CMPP3_PACKET_MAX 固定 3335 | 合法的 99 个收件人、140 字节内容的 Submit 总长 3471,会在解码前被拒绝 |
### 3.1 数据库字段逐项清单
协议 uint32 表示范围为 `0..4294967295`,现有 integer 为 `-2147483648..2147483647`
| 表 | 字段 | 当前实际类型 | 对应业务 |
|---|---|---|---|
| UpstreamReceiptInbox | sequenceId | integer,可空 | 上游回执接收 |
| SmsReceiptRecord | sequenceId | integer,可空 | 业务回执记录 |
| SmsUplinkMessage | sequenceId | integer,可空 | 上行短信 |
| SmsSubmitRecord | sequenceId | integer,可空 | 向上游提交结果 |
| SmsMessageSegmentAudit | sequenceId | integer,可空 | 长短信分片提交/回执审计 |
| CmppDownstreamDelivery | ackResult | integer,可空 | 下游确认结果 |
| CmppDownstreamDeliveryAttempt | ackResult | integer,可空 | 每次下游投递确认结果 |
代码入口:[schema.prisma](../api/prisma/schema.prisma)、[回执处理](../api/src/send-chain/send-receipt.service.ts)、[提交结果](../api/src/send-chain/send-gateway-result.service.ts)、[上行处理](../api/src/send-chain/send-downstream-delivery.service.ts)、[下游状态](../api/src/send-chain/send-downstream-state.service.ts)。不得只修改收件箱,否则后续落库仍可能失败。
### 3.2 协议与调用路径证据
- [Gateway 事件结构](../gateway/internal/queue/messages.go) 的 SequenceID 已是 uint32Msg_Id 转为十进制字符串进入事件,序号超限发生在后端持久化边界。
- [receipt.go](../gateway/third_party/gocmpp/receipt.go) 的 CmppReceiptPktLen=60Pack/Unpack 均使用 21 字节号码。[上游处理](../gateway/internal/upstream/deliver.go) 对 2.0/3.0 共用此解析器;[下游投递](../gateway/internal/inbound/delivery.go) 共用此打包器。
- [下游目标选择](../api/src/send-chain/downstream-receipt-targets.ts) 过滤 <=0[下游投递](../gateway/internal/inbound/delivery.go) 使用 ==0/!=0 判定是否提供原 Submit 序号。
- [client.go](../gateway/third_party/gocmpp/client.go) 执行 `status = uint8(rsp.Status)` 后按是否为 0 判断成功;[上游连接](../gateway/internal/upstream/connection.go) 实际调用该客户端。
- [packet.go](../gateway/third_party/gocmpp/packet.go) 设置 3335 上限;[conn.go](../gateway/third_party/gocmpp/conn.go) 在收包时执行;[submit.go](../gateway/third_party/gocmpp/submit.go) 的 3.0 长度公式为 `12 + 129 + 32*N + 1 + 1 + MsgLength + 20`。N=99、MsgLength=140 得到 3471,且 99 满足规范“小于100”。
内存核算还确认:正确 71 字节回执中 SMSC_sequence 的起始偏移为 67;旧代码从偏移 56 读取。对普通 11 位号码,号码可能仍正确,但后续序号会读到补零。不能以手机号看起来正常证明整个报文正确。
## 4. 目标设计
### 4.1 uint32 存储和接口边界
建议统一采用:Gateway uint32 → JSON number → 后端校验后的 number → 持久化适配器 BigInt → Prisma BigInt / PostgreSQL bigint。
- 上述 7 字段统一升级;保留可空属性,已有 NULL 表示历史未采集,不转换成 0。
- 非空合法范围为 0~4294967295;拒绝负数、小数、NaN、无穷大和超上限值。必填协议头字段不应因内部接口可空而被静默省略。
- number 精确覆盖全部 uint32;进入 ORM 时显式转换为 BigInt,读取后检查范围再转 number,保持现有事件/API 的数值契约,避免把 Prisma BigInt 直接送入 JSON。
- 使用字段专用转换函数,覆盖查询条件、create/update/upsert、原始 SQL、批量查询、DTO、队列回放、审计、报表和导出。不能靠修改全局 BigInt.toJSON 掩盖边界。
- 数据库增加非空值范围约束。旧负值先列入异常清单,不擅自加 2^32“纠正”;须有原始报文佐证并单独处理。
- API、Worker、Callback 等所有读取这些字段的进程必须一起完成兼容验证。协议字段升级不得污染既有金额 BigInt 的单位和序列化规则。
`Msg_Id` 不套用上述方案:它是 uint64,完整上限为 18446744073709551615,超出 JavaScript 安全整数及 PostgreSQL 有符号 bigint。继续使用 Gateway uint64、跨服务十进制字符串和数据库 text;必要时校验十进制格式及 uint64 上限,不使用 Number/parseInt 中转。
### 4.2 CMPP 2.0/3.0 状态报告编解码
- 显式区分协议版本:2.0 的号码字段 21 字节、报告体 60 字节;3.0 分别为 32、71 字节。
- 收包按协商版本与报告体实际长度校验后解析;发送按下游连接协商版本打包。同步更新协议日志解析路径,避免业务解析正确但日志仍错位。
- 严格校验截断、额外字节、填充和字段边界;不能仅更改总长度常量,必须同时更改字段偏移与读写长度。
- 某些供应商可能在 3.0 连接上发送 60 字节历史格式。先盘点脱敏报文;如确有兼容需求,设计显式、可审计的兼容策略并测试,不凭长度默默切换,也不强制修改通道配置。本方案不默认启用降级。
- 不新增 SMSC_sequence、LinkID、供应商时间字段的业务存储需求;它们的原始报文保留和追踪另按既有审计方案执行。暂不落库不等于可以错读字段位置。
### 4.3 序号 0 与缺失的区别
- 用 null/undefined 或明确存在标志表示未提供;0 是合法已提供的数值。
- Gateway 可用可空 uint32 或等价带存在标志的结构,检查 JSON omitempty、默认值、恢复映射、幂等键和分片目标生成的全部路径。
- 不将空字符串经 Number 转换为 0;旧空字符串仍视为缺失。分别测试数字 0、字符串 "0"、空串、null 和未提供字段。
- 保留原始客户 Submit 序号及 Msg_Id 对应关系,不生成替代序号。回卷到 0 后,未完成请求不得发生映射碰撞。
### 4.4 连接响应状态与收包长度
- 3.0 CONNECT_RESP.Status 全程使用 uint322.0 uint8 可无损提升。只有原始值 0 表示成功,未知非零值仍失败,日志保留完整原值。
- 根据支持的命令、版本、合法收件人数及内容长度重新计算收包边界;既不能保留过小总上限,也不能取消上限或直接接受任意长度。
- 检查长度公式、实际编码长度、读缓冲区和内存分配顺序;拒绝长度头与正文不符、越界人数、畸形截断包及超大包。
- 不把扩大总包上限误写成扩大单条短信正文限制;编码长度与字符数量分别校验。
## 5. 已核对的非问题项与范围限制
- 本次检查的 Msg_Id/gatewayMessageId/ackMessageId 使用字符串存储,未发现数据库容量缩小。
- 客户原始序号、下游 ACK 序号已有 text 存储,没有此次 integer 上限问题,但仍须处理序号 0 的代码语义。
- 已查手机号、接入号、正文、原始状态码等数据库列为 text,没有 varchar 长度不足问题;协议封包处的字节长度校验仍需保留。
- 长短信 8/16 位引用号、分片数量和编码值可被现有 integer 容纳;语义校验仍须保留,不能只靠数据库类型。
- CONNECT 时间戳按 MMDDHHMMSS 编码,最大合法日历组合不超过 1231235959,不属于此次 integer 超限风险。
- 本次不是完整 CMPP 一致性认证;ISMG 间路由、所有扩展命令、二进制短信全场景及国际号码产品范围不在本次结论内。
## 6. 历史数据与应急恢复衔接
1. 实施前重新取得主任务最终恢复记录,核对原始事件摘要、业务匹配、发送尝试、扣退费、通知与客户 ACK,不直接继续任何旧脚本。
2. 迁移只扩大存储能力,不自动重投历史事件、不重新生成通知、不重算金额,也不回写短信成功状态。
3. 临时恢复时省略的原始序号,仅在存在原始事件且与记录唯一匹配时才可计划补录。补录审计字段不得重新触发补发、退款或下游推送;需要独立清单和恢复授权。
4. 事件匹配必须包含通道/供应商身份、Msg_Id、号码和发送尝试,不以号码单独认领,不能跨客户合并。
5. 原始事件可在确认耐久业务接管后按现有恢复流程收尾,但 Inbox matched 不能单独证明短信终态、账务或客户通知已经完成;还须核对收尾工作和最终结果。
6. 再次恢复时使用原业务幂等键,尊重已完成的最终决策,禁止为让客户“看到成功”覆盖真实失败。
## 7. 实施、迁移与发布顺序
建议同一维护窗口覆盖全部问题;如必须拆批,首批至少完成 FIELD-01~04 并独立闭环验证,FIELD-05/06 必须有明确后续计划。优先级是实施顺序,不代表本轮授权执行。
1. 固定当前分支、精确提交、工作区保护清单、线上版本;重新盘点受影响调用点和数据规模。将本文用例与需求、设计和测试进度同步。
2. 完成字段适配、编解码、序号存在性、状态和长度修复及测试。既有第三方库以仓库内 fork 修改留痕,不顺带升级整套依赖。
3. 在隔离 PostgreSQL 上演练旧 schema 升级,保留旧值、NULL、索引、约束及关联,测量迁移锁持有时间、磁盘/WAL 增量和失败回滚。
4. integer → bigint 可能引发表重写和强锁;按真实表规模选维护窗口。设置明确 lock_timeout/statement_timeout,超时退出而非无限等待,不在未知流量下直接执行。
5. 迁移、Prisma Client 与全部相关进程作为兼容整体交付。约束可按演练结果使用 NOT VALID 后校验,但上线不能留下未登记的未验证约束。
6. 授权发布后按 [部署手册](production-deployment.md) 使用标准 `npm run release -- ...`,依次 plan、有效测试证据、preflight、prepare、deploy、verify/status/report。每个目标环境独立核验,不用临时脚本替代应用发布。
7. 发布前保存独立数据库/应用恢复资产,确认主任务恢复不会与迁移同时处理同一批数据。需要暂停消费者时明确范围和时长,防止新旧进程交叉写入。
8. 发布后核对精确版本、实际列类型、迁移状态、全链路与队列;真实业务发送验收需另有明确环境和流量授权,不能借回归向真实客户发送测试短信。
回退限制:旧代码/旧 Prisma Client 可能无法读取新写入的大序号。应用回退不代表兼容,更不能把 bigint 直接缩回 integer。保留宽字段,优先前向修复;确需回退时必须证明旧版本兼容、未完成事件可接续。不能删除大值记录来让回退成功,已发送短信和客户已确认回执也无法用数据库快照撤销。
## 8. 验收用例
以下全部为新增待执行用例,不计入既有通过数。使用独立隔离数据;mock 只用于隔离,不替代真实 API/PG/Redis/Gateway 证据。
| 用例 | 场景 | 预期与证据 |
|---|---|---|
| CMPP-FIELD-T01 | 7 字段依次写入 0、2147483647、2147483648、4294967295 | 真实后端/PG精确保存及读回,JSON不丢精度 |
| CMPP-FIELD-T02 | -1、4294967296、小数、NaN、空串及缺失 | 按必填/可空规则拒绝或保留缺失;不误转0,不产生假成功 |
| CMPP-FIELD-T03 | 真实回执高序号,单条与批量回调入口 | 耐久接收、匹配、分片聚合、终态及通知闭环 |
| CMPP-FIELD-T04 | 上行高序号及重复事件 | 上行入库、应用归属及投递正确,无重复业务记录 |
| CMPP-FIELD-T05 | Submit/分片高序号返回 | 提交结果、分片记录及后续回执均可关联 |
| CMPP-FIELD-T06 | 3.0 DELIVER_RESP 的大非零 Result | 保存完整错误码,进入失败/重试路径,不判为成功 |
| CMPP-FIELD-T07 | uint64 Msg_Id 最大值跨 Gateway/Redis/API/PG/UI | 全链路字符串精确一致,无 Number 中转 |
| CMPP-FIELD-T08 | 2.0/3.0 标准60/71字节状态报告收发 | 逐字段、偏移、长度正确;实际TCP对端解析通过 |
| CMPP-FIELD-T09 | 3.0 32字节号码字段、最大SMSC_sequence;错版本/截断包 | 不错位,异常显式拒绝;兼容例外仅按批准策略执行 |
| CMPP-FIELD-T10 | 客户Submit序号0、4294967295及回卷 | 单条/长短信回执正常,原Msg_Id一致,不漏目标 |
| CMPP-FIELD-T11 | 序号0在Gateway重启、重连后恢复 | 使用持久化身份恢复;缺失序号不伪造为0 |
| CMPP-FIELD-T12 | CONNECT_RESP.Status=0、5、255、256、4294967295 | 只有0成功;全部非零失败并保留原码 |
| CMPP-FIELD-T13 | 3.0 Submit 99人×140字节、编码允许边界 | 3471字节合法包进入正常校验;100人按规范限制拒绝 |
| CMPP-FIELD-T14 | 巨大/过小Total_Length、错长度、截断包 | 有界读取与内存占用,明确失败,无崩溃或无界分配 |
| CMPP-FIELD-T15 | 旧值/NULL升级,迁移锁超时/中断 | 数据不变、失败可识别、恢复步骤真实演练 |
| CMPP-FIELD-T16 | 高序号重复、乱序、跨尝试、并发与故障接管 | 关联不串客户/尝试;不重复补发、扣退费或最终通知 |
| CMPP-FIELD-T17 | 真实下游ACK与断线重连 | 区分待发送、已发未确认和客户已确认,不将入队当送达 |
| CMPP-FIELD-T18 | 应急省略序号的历史记录与原始事件核对 | 只补审计不重开业务;证据不足保留待核查 |
自动检查按 [测试计划](testing-plan.md) 执行 API 定向/全量测试、类型检查、构建及质量门禁,Gateway `go test ./...``go vet ./...`。覆盖 vendored 协议库本身的测试:不能默认 Gateway 的 `./...` 会跨越嵌套 Go module。若影响前端接口或展示,补前端定向与必要回归、真实页面验收。保留未执行原因,不以编译通过替代协议互通。
停止条件:新增重复Submit、账务不一致、终态被覆盖、租户串联、无法恢复的协议解析异常、持续积压或迁移锁超时。停止放量并保留证据,不靠手工改最终状态掩盖失败。
## 9. 完成标准与交接
仅在以下事实全部具备后,才能声明目标环境整改完成:
- 7字段覆盖、版本化编解码、序号0、连接状态与收包上限已实现,并满足选定发布范围。
- 上述用例有对应证据,真实PG及TCP互通通过;历史数据兼容和故障恢复经过验证。
- 精确提交已通过标准发布,目标环境版本与实际schema一致。
- 原始事件、耐久接收、业务终态、补发尝试、账务和客户ACK分层对账,无未解释缺口;客户离线等外部阻塞单独列明。
- 更新需求、设计、系统测试用例和测试进度,分别报告本地修改、提交、推送、测试部署、预生产部署状态。
目标环境实施人接手时重新核对主任务最终恢复结果和现场,不从本方案推定任何待处理队列已排空或获得生产操作授权。
## 10. 2026-09-20 实施审查与本地验收
### 实施基线与规则补充
- 最新本地基线为 `b24cd7c`(模板拒收策略与运营页面),已包含 `c20c224` 的终态/发送尝试归属修复;已核对远端 main 仍为 `5e4d644`,本轮不推送。其他会话未提交文件及共享文档原有差异保留。
- 五个 `sequenceId` 与两个 `ackResult` 改为 nullable BigInt,迁移 `20260920160000_cmpp_protocol_uint32` 在一个事务内扩容并加入 0..4294967295 检查,锁等待 5 秒、单语句超时 5 分钟。负数历史值阻断并回滚,NULL 不回填。发布前需要按真实表规模重新评估重写/WAL/锁时间。
- `protocol-uint32.ts` 明确执行数值校验、BigInt 写入、校验后读回;API 与 callback 使用专用字段响应转换,仅处理 `sequenceId`/`ackResult`,金额 BigInt 和日期保持原有行为。协议输入不接受数字字符串、空串、浮点及越界;历史文本 Submit 序号单独解析,字符串 `"0"` 有效,空白/缺失无效。Msg_Id 保持十进制字符串。
- 版本化 `PackVersion`/`UnpackVersion` 严格检查 2.0/2.1 的 60 字节与 3.0 的 71 字节。上游、协议日志、下游均显式传版本;原 Pack/Unpack 仅作为 2.0 兼容调用保留,3.0 不自动接受 60 字节。目的号码字段为 21/32 字节,SMSC_sequence 最后 4 字节,超长字段不能截断。
- 原 Submit 序号使用可空指针区分缺失与 0;入站 JSON 不省略 0。长短信首段判断改为实际段位置。上游分配序号时避开在途 Submit/心跳,下游 ACK 注册冲突则保留原记录、返回可重试错误,不覆盖待确认回执。
- CONNECT_RESP 状态全程 uint32,非 0 一律失败并保留完整错误码。3.0 收包上限复用现有 Submit 最大长度常量 3491,覆盖 99 号码、140 字节的 3471,以及 ASCII 159 字节的 3490;规范原文为 ASCII <160,因此 160 字节拒绝。3491 只是保守的有界内存上限,不代表允许 160 字节内容。打包验证号码个数和数组、声明长度和实际内容一致;解包拒绝截断和多余字节。
- T16 并发真实验收复现一个相邻缺陷:可选 connectionId 未传时 Prisma 的空 update upsert 可能退化为先查后插,两个首次相同回执发生 P2002。本轮仅在该错误后按相同 receiptKey 回读已持久化记录再确认接收;没有对应记录或其他数据库错误仍抛出,避免假成功。
### 验收映射
| 用例 | 本地证据与结果 | 尚未覆盖的目标环境项 |
|---|---|---|
| T01T06 | 新脚本 `tools/testing/verify-protocol-fields.mjs` 用真实 Nest callback、PG、Redis 验证四个边界、七字段、单/批回执、Submit 分片、上行去重、大 ACK 非成功、非法输入无写入;通过 | 真实供应商/客户在线业务回归 |
| T07 | 最大 Msg_Id `18446744073709551615` 经过 Redis/HTTP/Prisma 保持字符串;Go TCP/回执往返不截断 | 线上供应商端到端互通 |
| T08T09 | vendored codec 与 upstream tests 验证 60/71 字节、32 字节号码、最大 SMSC、错误版本/截断;下游 2.0/3.0 TCP 回执验证通过 | 实际通道 3.0 是否存在非规范 60 字节回执需发布前抽样 |
| T10T11、T17 | 入站序号 0 JSON、缺失与 null 区分;新连接无原消息内存映射时恢复 Msg_Id2.0/3.0 真 TCP DELIVER/DELIVER_RESP Result=0;最大/0 序号冲突保护测试通过 | 实际客户端重连验证 |
| T12T14 | 真 TCP CONNECT 状态 0/5/255/256/429496729599 号码 3471/3490100 号码、超长内容、截断/多余内容、巨大/过短头拒绝;通过 | 目标环境日志、吞吐与异常隔离观察 |
| T15 | 全量 113 个已提交基线+本轮迁移在新隔离库成功;缩小结构演练末表非法值导致前面 DDL 整体回滚、5 秒锁超时回滚、NULL/0/2147483647 与七个索引保留 | 生产规模 WAL/耗时、发布切换与实际备份恢复未演练 |
| T16 | 同一大序号回执并发接收、重复批回调、相反迟到回执;另现有 receipt-finality/attempt-completion 的并发、跨尝试、故障事务恢复真实 PG 测试通过 | 整个 Gateway→Redis→API→客户单进程链路未联合运行;各边界分别验证 |
| T18 | 模拟已持久化但省略序号的历史回执,重复接收后仍 NULL,终态/通知数量不重新打开;通过 | 本轮不对线上应急历史数据做字段回填 |
验收脚本要求**新建空的本机 `cmpp_qa_*` 数据库**,应用基线及本轮迁移后构建 API,设置 `PROTOCOL_TEST_DATABASE_URL``PROTOCOL_TEST_REDIS_URL`。最高 Msg_Id 为固定边界 fixture,不能把多轮 fixture 混在同库当成新的发送尝试。脚本只连接本机,通道禁用、地址 127.0.0.1:1,不启动发送工作进程;产生合成业务/通知记录,不向真实运营商或客户发报文。
证据目录 `.local-data/protocol-fields-20260920/` 不入 Git。关键日志为 `real-chain-verified.log``migrate-verified.log``api-coverage-final.log``api-incremental.log``go-final.log``gocmpp-final.log``receipt-finality.log``attempt-completion.log`。最终迁移小样本为七表 21 行,扩容阶段约 106ms、WAL 81800 字节;不是线上容量承诺。保留首次 Redis 从错误工作目录读取旧 RDB 而启动失败、重复使用固定边界 fixture 库而匹配冲突、以及实际 P2002 并发失败日志,最终以专用 Redis 目录和全新库复验。
本地 Redis 为 5.0.14.1,实际 Stream 读写通过,但低于 BullMQ 推荐的 6.2;没有以此宣称发布环境队列版本验收。PG 驱动给出并发 query 弃用提示,当前测试通过,未修改驱动。API 响应结构保持数字/字符串不变,无前端代码或 CSS 改动;未重复浏览器验收。部署时必须一起发布 schema、API/callback/相关 worker 及 Gateway,旧 Prisma 客户端不能视为已支持超 31 位数据;不能通过缩回 integer 做应用回滚。
门禁:API 全量和增量覆盖率各 87 套 / 990 项通过,TypeScript 构建通过;Gateway 全量测试及根模块 vet 通过,vendored 模块测试通过;5 个队列契约、变更代码 lint(0 error32 个存量 any warning)、格式检查和 diff 检查通过。额外运行 vendored 模块完整 vet 发现两个原有 stdmethods 提示:`packetWriter.WriteByte`/`packetReader.ReadByte` 使用累积错误接口而非标准 io 签名;本轮未更名整个协议库,记录为既有待治理项,不宣称该额外检查通过。没有通过禁用检查隐藏问题。
测试在保留其他会话修改的当前工作区执行,提交仅纳入本轮文件和共享文档新增段落;这些本地结果不能冒充标准发布工具绑定精确提交的发布证据。隔离 PostgreSQL、Redis 已关闭,数据/日志保留。上线前仍应按标准工具从精确提交构建并生成对应验证证据。