feat: complete cmpp gateway delivery recovery workflows

This commit is contained in:
hectorzhao
2026-07-08 16:30:06 +08:00
parent cc628d0214
commit 8144f08652
60 changed files with 8901 additions and 94 deletions
@@ -30,6 +30,15 @@
"feeCode": "0",
"feeType": "01"
},
"upstream": {
"gatewayHost": "127.0.0.1",
"gatewayPort": 17890,
"account": "cmpp-account-demo",
"passwordCipher": "secret-demo",
"cmppVersion": "3.0",
"desiredConnections": 2,
"windowSize": 16
},
"retry": {
"attempt": 0,
"maxAttempts": 3
+11 -1
View File
@@ -8,5 +8,15 @@
"sequenceId": 1024,
"gatewayMessageId": "gw-msg-20260701-000001",
"submitStatus": "accepted",
"submittedAt": "2026-07-01T09:00:00.118Z"
"submittedAt": "2026-07-01T09:00:00.118Z",
"segments": [
{
"segmentTotal": 1,
"segmentIndex": 1,
"sequenceId": 1024,
"gatewayMessageId": "gw-msg-20260701-000001",
"submitStatus": "accepted",
"submittedAt": "2026-07-01T09:00:00.118Z"
}
]
}
@@ -41,6 +41,7 @@
"queuePriority",
"route",
"cmpp",
"upstream",
"retry"
],
"properties": {
@@ -78,6 +79,19 @@
"feeType": { "type": "string" }
}
},
"upstream": {
"type": "object",
"required": ["gatewayHost", "gatewayPort", "account", "passwordCipher", "cmppVersion"],
"properties": {
"gatewayHost": { "type": "string", "minLength": 1 },
"gatewayPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
"account": { "type": "string", "minLength": 1 },
"passwordCipher": { "type": "string", "minLength": 1 },
"cmppVersion": { "enum": ["2.0", "3.0"] },
"desiredConnections": { "type": "integer", "minimum": 1, "maximum": 32 },
"windowSize": { "type": "integer", "minimum": 1, "maximum": 1024 }
}
},
"retry": {
"type": "object",
"required": ["attempt", "maxAttempts"],
@@ -103,7 +117,24 @@
"submitStatus": { "enum": ["accepted", "rejected", "timeout"] },
"errorCode": { "type": "string" },
"errorMessage": { "type": "string" },
"submittedAt": { "type": "string", "format": "date-time" }
"submittedAt": { "type": "string", "format": "date-time" },
"segments": {
"type": "array",
"items": {
"type": "object",
"required": ["segmentTotal", "segmentIndex", "sequenceId", "gatewayMessageId", "submitStatus", "submittedAt"],
"properties": {
"segmentTotal": { "type": "integer", "minimum": 1 },
"segmentIndex": { "type": "integer", "minimum": 1 },
"sequenceId": { "type": "integer", "minimum": 0 },
"gatewayMessageId": { "type": "string", "minLength": 1 },
"submitStatus": { "enum": ["accepted", "rejected", "timeout"] },
"errorCode": { "type": "string" },
"errorMessage": { "type": "string" },
"submittedAt": { "type": "string", "format": "date-time" }
}
}
}
}
}
]
+52 -6
View File
@@ -105,6 +105,8 @@
5. 短信应用必须配置发送队列等级:普通队列或优先队列。未配置时默认普通队列;优先队列用于验证码、登录确认、交易通知等高时效短信,普通队列用于营销、通知等常规短信。
6. 发送队列等级属于真实业务配置,必须保存到后端数据库,并在客户端、运营端创建/编辑应用时展示和可修改;不得只作为前端展示字段。
7. 运营端代企业新增短信应用时,第一步选择企业必须使用项目通用 Select/下拉控件,选项来自真实企业 API,支持加载中、空数据、错误态,不允许写死企业列表。
8. 短信应用必须有独立 6 位数字 CMPP 接入账号 `cmppAccount`;运营端添加/编辑应用时可显式配置,留空时后端自动生成且全局唯一。
9. 短信应用必须可配置客户侧 CMPP 最大连接数 `cmppMaxConnections` 和客户提交窗口 `cmppWindowSize`;这两个字段是平台运行配置,不是 CMPP 协议字段,也不是 gocmpp 库参数。
### 4.3 签名与引流信息
@@ -187,6 +189,7 @@
1. Gateway 必须按运营端通道配置连接上游 SMSC,使用通道的 `gatewayHost/gatewayPort/account/passwordCipher/srcId/cmppVersion` 完成 CMPP 2.0/3.0 connect/login。
2. Gateway 必须校验上游 connect/login 返回码,区分 connected、auth_failed、connect_timeout、network_error、protocol_error 等状态,并回写 NestJS 真实连接状态。
3. Gateway 必须支持每个通道配置期望连接数,建立多条长连接,并按连接维度维护 currentConnections、lastConnectedAt、lastHeartbeatAt、lastError、reconnectCount。
- `desiredConnections``windowSize` 是平台对上游通道连接池和提交窗口的运行配置,必须通过运营端通道配置页面保存到真实后端;它们不是 CMPP 标准 PDU 字段,也不是 gocmpp 的原生配置字段。
4. Gateway 必须实现 ActiveTest 心跳与超时检测;连续心跳失败后连接进入 heartbeat_timeout/reconnecting,重连成功前该连接不可参与发送。
5. Gateway 必须支持断线自动重连、指数退避或固定退避、最大重试间隔、重连日志和状态回写。
6. Gateway 必须维护 CMPP sequenceId 与平台 messageId、submitId、channelId 的映射,submit resp 和 deliver 回执必须能追溯到原短信记录和提交尝试。
@@ -221,23 +224,66 @@
#### 4.8.4 当前实现缺口标记
截至当前版本,Go Gateway 已有 HTTP 控制服务、健康检查、连接上游 SMSC 的 `ConnectChannel` 控制入口、gocmpp 协议 spike、队列消息结构,并已补齐第一阶段下游 CMPP 入站能力:
截至当前版本,Go Gateway 已有 HTTP 控制服务、健康检查、连接上游 SMSC 的 `ConnectChannel` 控制入口、gocmpp 协议 spike、队列消息结构,并已补齐第一阶段下游 CMPP 入站、上游提交和下游回执/上行推送能力:
- 已实现 `17890` 入站 CMPP Server 监听,生产部署由 `GATEWAY_CMPP_ADDR=0.0.0.0:17890` 启动。
- 已实现下游客户 connect/login 鉴权:CMPP `Source_Addr` 使用企业应用独立 6 位 `cmppAccount`,密码使用应用 CMPP 参数中的 `passwordCipher`Gateway 将 CMPP `AuthSource/Timestamp` 交由 NestJS 根据真实数据库校验。
- 已实现客户端应用 IP 白名单、应用状态、企业状态和企业认证状态校验;校验失败返回 CMPP connect 失败。
- 已实现下游 CMPP Submit 到平台发送请求的转换:Gateway 解码 CMPP 3.0 submit 内容,调用 NestJS 真实入站接口,NestJS 复用模板/签名/风控/余额/路由/队列优先级发送链路,接受后返回 CMPP submit_resp。
- 已实现 NestJS 校验通过后的异步 Gateway 提交:`SubmitCommand` 保留 BullMQ 审计/兼容投递,同时写入 Redis Stream `gateway.submit.commands` 主命令流;Go Gateway 以 consumer group 独立消费该命令流,携带通道 `gatewayHost/gatewayPort/account/passwordCipher/cmppVersion` 作为 SP 客户端连接上游 SMSC 并发送 CMPP Submit。
- 已实现上游 submit_resp 回传:Gateway 将 accepted/rejected/timeout 转换为 `SubmitResult` 调用 NestJSNestJS 继续执行 accepted 扣费、失败/超时补发或释放冻结等既有逻辑。
- 已实现上游 deliver receipt 和普通 deliver 上行解析:Gateway 在上游连接读循环中解析 receipt/uplink,调用 NestJS `/gateway/events/receipt``/gateway/events/uplink` 写入真实发送记录、回执和上行表。
- 已实现上游长短信第一版拆分和上行长短信重组:Gateway 按 CMPP 标准 6 字节 UDH 将超过 140 字节的 Submit 内容拆成多个分片,设置 `PkTotal/PkNumber/TpUdhi` 后逐包提交;上游普通 Deliver 携带 UDH 分片时,Gateway 在同一连接内按通道、主叫、被叫、引用号和总片数缓存并重组后再回传 NestJS。
- 已实现上游多连接和窗口控制第一版:NestJS 将通道配置中的 `desiredConnections/windowSize` 写入 `SubmitCommand.upstream`,Go Gateway 按通道建立连接池,每条连接独立维护 submit pending 映射、receipt/uplink 处理和窗口令牌;窗口满时等待可用窗口或按提交超时返回。
- 已实现 SubmitCommand 在途恢复第一版:Go Gateway submit worker 在消费新消息前会对 Redis Stream consumer group 中空闲超过阈值的 pending 命令执行 `XAUTOCLAIM`,重新提交并按正常成功路径 ack,避免 Gateway 重启后命令永久滞留在 PEL。
- 已实现上游连接断开时的 pending submit 状态补偿第一版:如果某条上游 CMPP 连接在收到 submit resp 前断开,Gateway 会立即唤醒该连接上等待中的 pending submit,请求返回 `timeout/CONNECTION_LOST`,由 NestJS 进入既有补发或释放冻结逻辑,不再只依赖固定超时。
- 已实现“上游可能已受理但 submit resp 丢失”场景的保守补偿第一版:Gateway 在 receipt 事件中补充手机号;NestJS 对无法按 `messageId/gatewayMessageId` 精确命中的回执,只在“同通道、同手机号、72 小时窗口内、且仅存在 1 条 `timeout + gatewayMessageId=null` 的 submit 记录”时才回填并接收该回执,避免误绑到其他短信。
- 已实现 SubmitCommand 死信治理第一版:Go Gateway 对多次处理仍失败的 `SubmitCommand` 不再无限滞留在 PEL,而是按阈值写入 NestJS 真实 `GatewaySubmitDeadLetter` 表;运营端后端接口可分页查询死信,并支持人工将原始 `SubmitCommand` 重新写回 Redis Stream。
- 已实现客户侧下游投递重试第二版:客户系统负责断线后重连;平台在客户离线或投递失败时把 Deliver Receipt/上行 Deliver 保留在 `CmppDownstreamDelivery`,客户 bind 成功后立即拉取 pending,且 Gateway 会对当前在线账号周期补投;超过重试上限后转 `failed` 并写失败审计。
- 已实现下游投递失败审计与人工重投第一版:运营端后端与页面可分页查看 `CmppDownstreamDelivery` 的 pending/failed/delivered 记录,支持按状态、类型、应用和关键字筛选,并可对单条记录执行人工重投,真实调用 Gateway `/downstream/receipt``/downstream/uplink`
- 已实现下游投递批量重投第一版:运营端可在当前页勾选多条 `pending/failed` 下游投递记录,调用真实批量接口逐条重投并返回成功/失败汇总,不允许用前端循环假装成功。
- 已实现下游投递告警第一版:运营看板与右上角通知基于真实 `CmppDownstreamDelivery` 聚合显示下游投递告警数,当前告警口径包括“pending 超过阈值仍未投出”和“最近失败记录数”,用于提醒运营及时进入下游投递记录页处理。
- 已实现下游投递 Dashboard 第一版:运营端“下游投递记录”页面顶部新增真实聚合总览,直接按 `tenantId/applicationId/deliveryType` 统计投递总量、pending/delivered/failed、积压告警、按类型分布、重试压力分布和应用告警排行,数据源必须来自 `CmppDownstreamDelivery`,不能靠前端本地汇总。
- 已实现下游连接映射持久化第一步:Gateway 在客户 CMPP 账号 bind 成功、下游 submit 建链和回执/上行下发时,会把账号在线状态、实例标识、最近活跃时间写入 Redis presence;该状态不再只保留在 Gateway 进程内存中,为后续“Gateway 重启后的 pending 恢复”提供外部状态基础。
- 已实现下游连接映射持久化第二步:Gateway 启动时会读取 Redis presence 与当前内存在线账号,形成“恢复候选视图”,并通过控制面 `GET /downstream/recovery-candidates` 暴露候选账号列表,供后续恢复逻辑与运维排查使用;本阶段仍不等同于自动恢复 pending 投递。
- 已实现下游 pending 恢复执行第一版:Gateway 启动后会立即按恢复候选账号拉取真实 `CmppDownstreamDelivery.pending`,后续每轮补投周期也会继续扫描恢复候选;若账号已有可用下游连接则继续推送回执/上行,若账号尚未重连则保持 `pending` 等待后续恢复,不能因为 Gateway 重启就把未投递记录误标成失败。
- 已实现下游恢复控制第一版:Gateway 对恢复候选账号增加账号级恢复锁、失败/等待连接退避和恢复状态持久化,避免同一账号被并发重复恢复或每轮高频空转;控制面新增 `GET /downstream/recovery-statuses` 可查看最近一次恢复状态、重试次数、下一次可恢复时间和错误原因。
- 已实现下游恢复观测第一版:控制面新增 `GET /downstream/recovery-overview`,一次性返回恢复候选账号与恢复状态,便于联调和生产排查。
- 已实现下游恢复状态回流第一版:Gateway 在每次恢复状态变化后,调用 NestJS `/api/gateway/events/downstream/recovery-status` 真实回传账号恢复状态;NestJS 将状态写入 Prisma/PostgreSQL `GatewayDownstreamRecoveryStatus`
- 已实现下游恢复状态运营化第一版:运营端新增独立“恢复状态管理”页面,支持真实列表、分页、详情查看和当前筛选结果 CSV 导出;原“下游投递记录”页面只保留投递记录本身,不再混放恢复状态区块。
- 已实现下游恢复失败分类第一版:Gateway/NestJS 共同维护 `failureCategory`,覆盖 `client_disconnected``backoff``lock_contended``lock_lost``flush_failed``partial_delivery_failed``unknown`;运营端“恢复状态管理”页面支持失败分类筛选、分类分布统计、详情展示和导出字段。
- 已实现多 Gateway 恢复抢占协调第一版:恢复锁从单纯实例名升级为 Redis token 租约,状态记录 `lockOwner/lockExpiresAt`;恢复完成时必须通过 Lua 原子校验锁 token,只有持锁实例才能写入最终恢复状态并释放锁,避免旧实例超时后误删新实例锁或覆盖新实例恢复结果;运营端详情/列表可查看锁持有实例。
- 已实现长短信分片审计第一版:Gateway `SubmitResult` 回传真实 `segments[]`,包含 `segmentTotal/segmentIndex/sequenceId/gatewayMessageId/submitStatus/submittedAt`NestJS 写入 Prisma/PostgreSQL `SmsMessageSegmentAudit`,回执按 `gatewayMessageId` 回填分片回执状态,补偿归因可记录 `compensationType`;运营端短信记录详情可查看真实分片提交、回执和补偿审计。
- 下游投递重试已改为指数退避第一版:首次失败后按基础间隔重试,随后按 2 倍递增,并受最大退避上限约束,避免客户长时间离线时平台每分钟机械重试。
- 已实现客户侧最终 Deliver 推送的第一版能力:Gateway 在下游 Submit 被接受后记录 messageId 到客户连接的内存映射;NestJS 收到最终 receipt/uplink 并入库后调用 Gateway `/downstream/receipt``/downstream/uplink`Gateway 向仍在线的客户 CMPP 连接下发 Deliver Receipt 或普通 Deliver。
- 已实现客户侧 Deliver 持久化第一版能力:NestJS 收到最终 receipt/uplink 后写入 `CmppDownstreamDelivery` 待投递记录;在线推送成功标记 delivered,客户断线或 Gateway 不可达时保留 pending 并记录 retry 信息;客户重新 bind 后 Gateway 按账号拉取 pending 记录补发。
- 已实现普通上行匹配与人工认领第一版:优先按 messageId 精确匹配;无 messageId 时按接入号匹配应用路由;仍无唯一应用时按手机号和最近下发时间窗口匹配;多候选标记 ambiguous 并写入 `SmsUplinkMatchCandidate` 候选,运营端可人工认领候选应用/下发记录;认领后更新上行记录、保留候选审计,并创建真实客户侧上行 Deliver 投递记录。
仍缺少生产完整闭环能力:
- 应用级下游连接数限制和连接状态回写尚未完整产品化。
- 当前下游 CMPP Submit 通过 `sourceType=cmpp` 的系统批次兼容承载,尚未拆成完全独立于批量任务模型的单条发送模型。
- 未实现客户侧最终 Deliver Receipt 和上行 Deliver 投递
- 未实现 Gateway 消费 `SubmitCommand` 并真实 submit 到上游通道的 worker
- 未实现上游 deliver receipt 和普通上行 deliver 的生产解析与事件回传闭环
- 未实现多连接窗口管理、在途消息恢复、断线重连后的消息状态处理
- Gateway 控制面 `/upstream/submit` 仅保留为本地调试、人工补偿和运维验证入口;生产主链路应由 Gateway submit worker 消费 Redis Stream `gateway.submit.commands` 触发
- 客户侧 Deliver 重投当前已经具备账号级周期恢复、退避、Redis token 租约锁和控制面状态观测;恢复状态已同步到 NestJS 持久化审计表并进入运营端独立页面,且具备第一版失败分类分析和多 Gateway 抢占协调
- 普通上行匹配已覆盖 messageId、接入号、手机号时间窗口和共享接入号多候选人工认领;后续仍需补更复杂的批量认领、认领规则推荐和认领准确率指标
- 长短信分片当前已具备真实分片提交/回执/补偿审计,运营端短信详情可查看 `SmsMessageSegmentAudit`;后续仍需补“按单个分片自动重投”和分片级人工补偿操作
- 多连接窗口当前覆盖单进程内连接池和窗口满等待;在途恢复当前覆盖 Redis Stream pending claim、连接断开时的 pending submit 唤醒、receipt 驱动的保守唯一候选补偿、SubmitCommand 死信入库/人工重入队第一版,以及下游客户在线时的周期补投、Gateway 重启后的恢复候选扫描、账号级 Redis token 租约锁/退避/状态观测、恢复状态入库/运营端可视化;尚未实现连接级状态持久化、窗口指标回写、死信后台自动重试策略、长恢复任务锁续租,以及“上游已受理但 submit resp 丢失”场景的强确认或完整幂等补偿。
这些缺口未补齐前,只能把 `17890` 端口监听、客户账号密码鉴权、客户 IP 白名单和下游 submit 入平台发送链路作为第一阶段验收通过;不能把客户侧最终回执投递、上游真实运营商 submit/receipt/uplink 或完整多连接窗口恢复作为“生产已验收通过”。
基于当前真实代码,CMPP 端到端链路剩余缺口可以明确收敛为以下几类:
1. 下游恢复审计产品化:
- 恢复状态已能写回 NestJS/Prisma/PostgreSQL,并已在运营端“恢复状态管理”页面提供独立列表、详情、导出和失败分类分布。
- 后续仍需补恢复吞吐、趋势、耗时、连续失败账号等更细指标看板。
2. 高阶幂等与跨实例协调:
- 多 Gateway 实例下的账号级恢复抢占协调已具备 token 租约和完成校验;后续仍需增强分片级/消息级更细粒度去重,以及长恢复任务中的锁续租。
- “上游已受理但 submit_resp 永久丢失”的强确认补偿还不完整。
3. 分片和复杂场景补偿:
- 长短信分片级提交明细、回执和补偿归因已进入真实审计表和运营端短信详情;后续仍需补分片级自动重投/人工重投。
- 共享接入号、多候选普通上行已具备第一版人工认领和认领后下游投递;后续仍需补批量认领、人工认领复核和指标分析。
4. 运营观测与指标:
- 窗口利用率、连接级心跳、恢复吞吐、恢复耗时趋势等指标尚未回写到运营端真实页面。
这些缺口未补齐前,可以把客户 connect/login、IP 白名单、客户 submit 入平台、NestJS 业务校验、Gateway 上游连接池 submit、窗口满等待、长短信基础拆分/重组、submit_resp、receipt/uplink 入库、分片级提交/回执/补偿审计、共享接入号上行人工认领,以及在线或重连客户 Deliver 推送、Gateway 重启后的 pending 恢复、恢复状态入库、运营端恢复状态独立页/详情/导出和失败分类分布作为第一版真实链路验收;不能把连接级指标回写、分片级自动重投、批量认领和认领指标分析作为“生产已验收通过”。
### 4.9 回执与上行
+337 -4
View File
@@ -238,12 +238,13 @@
- 优先级:P0
- 前置条件:运营管理员已登录。
- 步骤:
1. 创建 CMPP 通道,填写网关地址、端口、账号、密码密文、接入号、限速。
1. 创建 CMPP 通道,填写网关地址、端口、账号、密码密文、接入号、限速、期望连接数和提交窗口
2. 查询通道列表。
3. 停用通道后创建发送任务。
- 预期结果:
- 通道协议默认为 CMPP,版本默认为 3.0。
- 通道限速保存正确。
- 通道真实保存 `desiredConnections/windowSize`,后续 Gateway `ConnectChannel``SubmitCommand.upstream` 使用该配置。
- 停用通道不会被路由选中。
### TC-ADMIN-004 通道组与路由规则
@@ -479,13 +480,14 @@
1. 打开运营端企业应用管理,点击新增短信应用。
2. 在第一步选择企业下拉框中查看企业选项、加载态和空态。
3. 选择企业后进入应用参数表单。
4. 配置应用名称、客户单价、IP 白名单、发送队列等级、移动/联通/电信通道组后保存。
4. 配置应用名称、客户单价、IP 白名单、发送队列等级、CMPP 6 位账号、客户最大连接数、客户提交窗口、移动/联通/电信通道组后保存。
5. 刷新列表并打开编辑页。
- 预期结果:
- 企业选择使用项目通用 Select/下拉控件,样式、禁用态、错误态与系统其他下拉一致。
- 企业选项来自真实企业 API,不使用静态数组、mock 或 localStorage。
- 请求体包含 tenantId、queuePriority、客户单价、IP 白名单和通道组绑定。
- 后端真实保存应用队列等级,刷新列表和编辑页后仍显示正确
- 请求体包含 tenantId、queuePriority、客户单价、IP 白名单`cmppAccount``cmppMaxConnections``cmppWindowSize` 和通道组绑定。
- `cmppAccount` 可显式填写 6 位数字;留空时由后端自动生成唯一账号;重复或非法格式保存失败并提示可读错误
- 后端真实保存应用队列等级和 CMPP 参数,刷新列表、编辑页和 CMPP 参数弹窗后仍显示正确。
- 不选择任何通道组或缺少必填字段时不能保存,并显示可读提示。
### TC-ADMIN-019 通道连接日志展示
@@ -994,6 +996,337 @@
- submit 被接受后返回 CMPP SubmitResp 成功,并在真实数据库创建 `sourceType=cmpp` 的发送记录,进入真实发送链路。
- submit 内容不匹配审核模板、余额不足、无可用通道时返回明确失败,不得伪造成功。
### TC-GW-007 CMPP 客户到上游 SMSC 完整闭环
- 优先级:P0
- 前置条件:企业已认证通过;短信应用 active 且配置独立 `cmppAccount/passwordCipher/IP 白名单`;签名、模板、报备、余额、通道组、通道连接状态均满足发送;Go Gateway、NestJS API、Redis、PostgreSQL 均使用真实本地或生产验证服务;上游使用真实 SMSC 测试环境或本地 gocmpp 模拟 SMSC。
- 步骤:
1. 客户 CMPP 客户端连接 Gateway `17890` 并完成 bind/login。
2. 客户分别提交匹配模板的短短信 SubmitReq,以及超过 140 字节、需要 CMPP UDH 分片的长短信 SubmitReq。
3. NestJS 入站接口执行应用状态、企业认证、IP 白名单、手机号、模板、签名报备、余额、黑名单、风控、运营商识别、通道组路由和通道连接可用性校验。
4. NestJS 生成真实发送记录、SubmitCommand 和 SmsSubmitRecord,将 SubmitCommand 写入 Redis Stream `gateway.submit.commands`,同时保留 BullMQ 审计/兼容投递。
5. Go Gateway submit worker 通过 consumer group 独立消费 SubmitCommand,使用其中的上游通道连接参数连接 SMSC,发送真实 CMPP Submit;长短信应按 6 字节 UDH 分片,设置 `PkTotal/PkNumber/TpUdhi` 并逐包提交。
6. 上游 SMSC 返回 SubmitRespGateway 回调 NestJS `SubmitResult`
7. 上游 SMSC 下发 deliver receiptGateway 解析为 `ReceiptEvent`NestJS 入库并更新最终状态。
8. NestJS 调用 Gateway `/downstream/receipt`,Gateway 向仍在线的客户连接下发 CMPP Deliver Receipt。
9. 上游 SMSC 下发普通 deliver 上行;长上行使用 UDH 分片乱序下发时,Gateway 应等待分片齐全后重组成一条 `UplinkEvent`NestJS 入库。
10. NestJS 调用 Gateway `/downstream/uplink`Gateway 对可关联 messageId 且客户仍在线的上行下发普通 CMPP Deliver。
11. 客户断开 CMPP 连接后再次产生 receipt/uplink,确认 NestJS 写入客户侧待投递记录。
12. 客户重新 bind/loginGateway 按账号拉取 pending 投递并补发,成功后回写 delivered。
- 预期结果:
- 客户 bind/login 使用真实数据库账号、密码、状态和 IP 白名单校验。
- 业务校验失败时不调用上游 submit,不扣费,不伪造成功。
- API 入队后不依赖同步调用 Gateway `/upstream/submit`Gateway 停止时命令留在 Redis StreamGateway 恢复后继续消费。
- 长短信 Submit 每个 CMPP 分片长度不超过 140 字节,分片 UDH 正确;所有 accepted 分片的上游 `MsgId` 均可映射回同一平台消息。
- 上游 submit accepted 后只按应用客户费率扣费一次;submit rejected/timeout 进入补发或释放冻结。
- deliver failed 触发补发或最终退款;重复/迟到回执不重复扣费或退款。
- 客户在线且 Gateway 仍保留 messageId 或账号会话时,可以收到最终 Deliver Receipt 和可关联上行 Deliver。
- 客户断线或 Gateway 控制面暂不可达时,`CmppDownstreamDelivery` 保留 pending、retryCount、nextRetryAt、lastError;客户重连后可补发并标记 delivered。
- 长上行分片未齐全前不入库不推送;分片齐全后只入库一条完整上行内容,且可继续执行 messageId、接入号或手机号时间窗口匹配。
- 无 messageId 上行优先按接入号匹配应用;接入号无法唯一匹配时按手机号和时间窗口匹配;多候选标记 ambiguous,不误推;完全匹配不到标记 unmatched 但仍入库。
- 后台周期重试、死信队列、过期策略和人工认领流程需按后续用例验收。
### TC-GW-008 Gateway 多连接窗口与窗口满等待
- 优先级:P0
- 前置条件:运营端通道配置 `desiredConnections=2``windowSize=1`,上游使用可延迟 SubmitResp 的真实测试 SMSC 或本地 gocmpp 模拟 SMSCNestJS API、Redis、PostgreSQL、Go Gateway 均运行真实服务。
- 步骤:
1. 通过真实发送链路连续提交 3 条可通过业务校验的短信,保证前 2 条 SubmitResp 暂不返回。
2. 观察 `SubmitCommand.upstream` 是否携带 `desiredConnections=2``windowSize=1`
3. 观察 Gateway 是否为同一通道建立 2 条上游 CMPP 连接,并将前 2 条短信分别占用两个连接窗口。
4. 第 3 条短信在两个窗口均满时等待,不得越过窗口容量继续 submit。
5. 释放任一 SubmitResp 后,确认第 3 条短信继续提交。
- 预期结果:
- Gateway 每条连接独立维护 sequence/pending 映射,SubmitResp 可正确回到原 `messageId/submitId/channelId`
- 窗口满时消息等待可用窗口;等待超过提交超时时返回 timeout,并由 NestJS 进入既有补发或释放冻结逻辑。
- 多连接窗口只改变 Gateway 提交并发,不绕过 API 侧模板、签名、余额、风控、通道组、通道连接可用性和限速校验。
- 当前阶段不要求断线后的 pending submit 恢复;该能力在后续在途恢复用例验收。
### TC-GW-009 Gateway 重启后认领 pending SubmitCommand
- 优先级:P0
- 前置条件:Redis Stream `gateway.submit.commands` 已创建 consumer groupGateway worker A 已消费一条 `SubmitCommand` 但尚未 ack;该消息空闲时间超过 pending claim 阈值。
- 步骤:
1. 停止或模拟断开 worker A,使该消息留在 PEL。
2. 启动 worker B 或重启 Gateway。
3. 观察 worker B 在消费新消息前执行 pending claim。
4. 观察该消息被重新提交、回调 `SubmitResult`,成功后 ack。
- 预期结果:
- 空闲超过阈值的 pending `SubmitCommand` 会被新的 consumer 认领,不会永久卡在 PEL。
- 被认领消息仍走正常发送链路,`messageId/submitId/channelId` 和上游回执映射保持一致。
- 提交成功后消息从 PEL 移除;提交失败时保留待后续重试或死信治理,不得静默丢失。
### TC-GW-010 上游连接断开时 pending submit 立即补偿
- 优先级:P0
- 前置条件:Gateway 已与上游 SMSC 建立连接;某条连接已成功发送 submit,但上游故意不立即返回 submit resp,随后主动断开 TCP 连接。
- 步骤:
1. 提交一条可通过真实业务校验的短信,使 Gateway 进入等待 submit resp 状态。
2.`defaultSubmitTimeout` 到达前,模拟上游连接断开。
3. 观察 Gateway 对该 pending submit 的处理,以及 NestJS 收到的 `SubmitResult`
- 预期结果:
- Gateway 不会一直等到固定超时才处理,而是立即将该 pending submit 补偿为 `timeout`,错误码为 `CONNECTION_LOST` 或等价可读值。
- NestJS 收到 `SubmitResult` 后走既有补发或释放冻结逻辑,短信状态不会永久卡在 `submit_queued`
- 同一连接上的其他 pending submit 也会被明确唤醒,不会静默丢失。
### TC-GW-011 submit_resp 丢失后按 receipt 保守归因
- 优先级:P0
- 前置条件:某条短信提交上游后,Gateway 因连接断开或 submit resp 丢失将该次提交记为 `timeout`,对应 `sms_submit_record.gatewayMessageId` 仍为空;后续上游真实回执已到达,receipt 中带有该手机号、通道和运营商侧 `MsgId`
- 步骤:
1. 构造一条无法按平台 `messageId/gatewayMessageId` 精确命中的 receipt 事件,但带上真实手机号、通道和运营商侧 `MsgId`
2. 保证同通道、同手机号、72 小时窗口内只有 1 条 `timeout + gatewayMessageId=null` 的 submit 记录。
3. 观察 Gateway 是否将手机号一并上送 NestJS,并检查 NestJS 的归因与入库结果。
4. 再构造“多候选”场景,重复执行同类 receipt 归因。
- 预期结果:
- 只有在唯一候选成立时,NestJS 才接收该 receipt,并回填真实 `sms_submit_record.gatewayMessageId/sequenceId`,同时写入 `sms_receipt_record``sms_message_record`
- 如果存在多条候选或无候选,则拒绝归因,不得误绑到其他短信。
- 已经由新通道成功送达的短信,旧尝试迟到回执仍只记历史,不覆盖最终送达状态。
### TC-GW-012 SubmitCommand 死信入库与人工重入队
- 优先级:P0
- 前置条件:Gateway submit worker 已连接 Redis Stream `gateway.submit.commands`;准备一条会持续触发处理错误的 `SubmitCommand`NestJS `/gateway/events/dead-letter` 和运营端 `/api/admin/operations/gateway-submit-dead-letters` 真实可用。
- 步骤:
1. 让同一条 `SubmitCommand` 连续处理失败,达到 Gateway 配置的死信阈值。
2. 检查 Redis PEL 中该消息是否被 ack,不再无限 pending。
3. 检查 NestJS 是否在真实数据库写入一条 `GatewaySubmitDeadLetter`,保存失败原因、尝试次数和原始命令载荷。
4. 调用运营端真实接口查询死信列表。
5. 调用人工重入队接口,将该死信重新写回 `gateway.submit.commands`
- 预期结果:
- 达到阈值后,Gateway 会把该消息转为死信,而不是永久卡在 PEL。
- 死信记录来自真实数据库,包含 `streamMessageId``messageId/submitId`、失败原因、尝试次数和原始 `SubmitCommand`
- 人工重入队成功后,死信状态更新为 `requeued`,记录新的 Redis Stream 消息 ID,并写系统日志。
- 重入队后如后续收到真实 `SubmitResult`,对应死信记录应自动转为 `resolved`
### TC-GW-013 下游客户在线时周期补投与失败封顶
- 优先级:P0
- 前置条件:客户应用 CMPP 账号真实可登录;平台已有至少 1 条 `CmppDownstreamDelivery.status=pending` 的回执或上行待投递记录;Gateway 周期补投任务已启动。
- 步骤:
1. 让客户先断线,制造一次投递失败,确认 `CmppDownstreamDelivery` 留在 `pending``retryCount` 递增。
2. 让客户重新 bind,检查 Gateway 是否立即拉取一次 pending 进行补发。
3. 在客户保持在线的情况下,继续制造一次临时投递失败,等待周期补投触发。
4. 把同一条待投递连续失败到重试上限。
- 预期结果:
- 客户重连后会立即补发 pending 记录;客户在线但上次投递失败时,Gateway 会按周期再次拉取并补投。
- 重试未超过上限时,`CmppDownstreamDelivery` 维持 `pending`,更新 `retryCount/nextRetryAt/lastError`
- 达到上限后,记录转为 `failed`,不再无限 pending,且写真实失败审计日志。
- 该能力只负责“客户已在线时的平台补投”和“消息不丢”;客户断线后的重新建链仍由客户系统自己负责。
### TC-GW-014 运营端下游投递记录查询与人工重投
- 优先级:P1
- 前置条件:数据库中已有 `CmppDownstreamDelivery` 记录,至少覆盖 `pending``failed``delivered` 三类状态;运营端已登录;Gateway 控制面和 NestJS API 均为真实服务。
- 步骤:
1. 进入运营端“下游投递记录”页面。
2. 分别按状态、投递类型、应用、关键字进行筛选,检查分页。
3. 打开一条记录详情,核对 payload、重试次数、最后错误和时间字段。
4. 对一条 `pending``failed` 记录执行人工重投。
- 预期结果:
- 页面列表来自真实 `/api/admin/operations/downstream-deliveries`,不是前端静态数组或本地状态拼装。
- 详情展示真实 payload、`retryCount/nextRetryAt/deliveredAt/lastError`
- 人工重投调用真实 `/api/admin/operations/downstream-deliveries/{id}/requeue`,由后端实际触发 Gateway `/downstream/receipt``/downstream/uplink`
- 重投后记录状态、失败原因和系统日志都与真实后端处理结果一致。
### TC-GW-015 下游投递指数退避
- 优先级:P1
- 前置条件:存在一条可重复触发失败的 `CmppDownstreamDelivery`;系统已配置真实基础重试间隔和最大退避上限。
- 步骤:
1. 连续触发同一条下游投递失败 3 到 4 次。
2. 每次失败后记录 `nextRetryAt` 与当前时间的差值。
3. 持续失败直到接近退避上限。
- 预期结果:
- `nextRetryAt` 不是固定 60 秒,而是随失败次数递增。
- 退避间隔符合基础间隔的 2 倍递增趋势,并在达到最大退避上限后停止继续增大。
- 达到总重试上限后仍按既有规则转为 `failed`,不会无限重试。
### TC-GW-016 下游投递批量重投
- 优先级:P1
- 前置条件:当前页至少有多条 `pending``failed``CmppDownstreamDelivery`;运营端“下游投递记录”页面和真实批量重投接口可用。
- 步骤:
1. 在页面勾选多条可重投记录。
2. 点击“批量重投”。
3. 检查后端返回的成功/失败汇总,并刷新列表。
- 预期结果:
- 页面调用真实 `/api/admin/operations/downstream-deliveries/requeue` 批量接口,不是前端逐条伪造结果。
- 后端逐条执行真实重投,返回 `total/successCount/failedCount/results`
- 成功和失败记录都会保留真实后端状态与错误信息;空选择时接口拒绝执行。
### TC-GW-017 下游投递告警聚合
- 优先级:P1
- 前置条件:真实 `CmppDownstreamDelivery` 中准备一批 `pending` 记录,其中部分已超过告警阈值;同时准备一批最近失败的 `failed` 记录。
- 步骤:
1. 访问运营端 Dashboard 和右上角通知区域。
2. 调用真实 `/api/admin/operations/dashboard/statistics`,核对返回的下游投递告警聚合。
3. 点击“下游投递告警”通知,跳转到下游投递记录页进一步筛查。
- 预期结果:
- Dashboard 返回真实 `downstreamDeliverySummary`,至少包含 `pending/failed/delivered/stalledPending/recentFailed/alertCount`
- 右上角通知中的“下游投递告警”数量与真实 Dashboard 聚合一致,不是前端写死值。
- 点击通知后可以进入真实下游投递记录页继续处理。
### TC-GW-018 下游投递 Dashboard 聚合视图
- 优先级:P1
- 前置条件:真实 `CmppDownstreamDelivery` 中存在多应用、多类型和多重试次数的记录,至少覆盖 `receipt/uplink``pending/delivered/failed`
- 步骤:
1. 打开运营端“下游投递记录”页面,查看顶部总览卡片、类型分布、重试压力和应用告警排行。
2. 调用真实 `/api/admin/operations/downstream-deliveries/dashboard`,核对 `summary/typeBreakdown/retryBuckets/topApplications`
3. 切换应用和类型筛选,确认顶部 Dashboard 与下方记录列表同时切换到同一筛选范围。
- 预期结果:
- 顶部 Dashboard 必须来自真实聚合接口,不能由当前页列表条目在前端临时汇总。
- `summary``total/pending/delivered/failed/stalledPending/recentFailed/alertCount` 与数据库真实结果一致。
- `typeBreakdown` 能正确区分 `receipt``uplink` 的状态分布。
- `retryBuckets` 真实反映 `pending/failed` 记录的重试压力分布。
- `topApplications` 以告警量优先排序,切换筛选后结果实时刷新。
### TC-GW-019 下游在线账号 Presence 持久化
- 优先级:P1
- 前置条件:Gateway 已配置真实 `REDIS_URL`;客户端应用存在可用的 6 位 `cmppAccount`Gateway inbound 服务可正常接收 bind 和 submit。
- 步骤:
1. 使用真实 CMPP 客户端账号 bind Gateway。
2. 发送一条 submit,并触发至少一次下游回执或上行下发。
3. 检查 Redis 中该账号的下游 presence 记录。
4. 断开连接或触发发送失败清理后,再次检查 Redis。
- 预期结果:
- bind 成功后,Redis 中存在该 `cmppAccount` 的 presence 记录,不再只保存在 Gateway 内存 map。
- presence 至少包含账号、Gateway 实例标识、最近更新时间等信息。
- submit 或下游投递后,presence 的最近活跃时间会刷新。
- 连接清理后,presence 会被删除或过期,不把离线账号长期误判为在线。
### TC-GW-020 Gateway 恢复候选视图
- 优先级:P1
- 前置条件:Gateway 已配置真实 `REDIS_URL`;Redis 中已有部分下游账号 presence;另有至少一个账号当前在 Gateway 内存中处于在线状态。
- 步骤:
1. 重启 Gateway。
2. 观察 Gateway 启动日志中的恢复候选加载结果。
3. 调用 `GET /downstream/recovery-candidates`
4. 比对 Redis presence 和当前在线账号,确认返回候选列表。
- 预期结果:
- Gateway 启动后会读取 Redis presence,不再完全依赖进程内存冷启动。
- `/downstream/recovery-candidates` 返回恢复候选账号视图,至少包含账号、实例标识、状态、最近更新时间。
- 当前内存在线账号与 Redis presence 会合并成同一候选视图。
- 本阶段只提供恢复候选视图,不应误报为“已自动补投所有 pending 下游投递”。
### TC-GW-021 Gateway 重启后的 pending 恢复
- 优先级:P0
- 前置条件:真实 `CmppDownstreamDelivery` 中存在某账号的 `pending` 回执或上行;Redis presence 中保留该账号最近在线记录;Gateway 可正常重启。
- 步骤:
1. 让客户账号先在线并制造至少一条 `pending` 下游投递。
2. 重启 Gateway。
3. 检查 Gateway 启动后是否按恢复候选账号重新拉取 pending。
4. 若客户已重新 bind,则观察 pending 是否继续投递成功;若客户未重连,则观察记录是否仍保持 `pending`
- 预期结果:
- Gateway 重启后会重新尝试按恢复候选账号拉取真实 pending 下游投递。
- 客户重新 bind 后,pending 回执/上行可继续投递,不依赖重启前的内存连接映射。
- 若客户尚未重连,记录应继续保留为 `pending`,不能仅因 Gateway 重启或当前无连接就错误转成 `failed`
- 恢复执行基于真实后端 `CmppDownstreamDelivery`,不是前端或 Gateway 内存伪造状态。
### TC-GW-022 Gateway 恢复退避与状态审计
- 优先级:P1
- 前置条件:Gateway 已配置真实 `REDIS_URL`;存在可恢复账号;能人为制造“客户未重连”或“拉取 pending 失败”等恢复异常。
- 步骤:
1. 触发某账号恢复一次,但让客户保持未连接,或让 pending 拉取接口暂时失败。
2. 连续观察两轮恢复周期,检查是否出现对同一账号的高频重复恢复。
3. 调用 `GET /downstream/recovery-statuses` 查看恢复状态。
4. 恢复客户连接后,再次观察状态是否转为成功。
- 预期结果:
- 同一账号恢复过程中有恢复锁,不能并发重复恢复。
- 恢复失败或等待连接后会进入退避,下一轮不会无节制重复尝试。
- `/downstream/recovery-statuses` 能返回真实恢复状态、尝试次数、下一次可恢复时间和错误原因。
- 客户恢复连接后,后续状态可转为 `success`,而不是长期卡在错误状态。
### TC-GW-023 Gateway 恢复总览接口
- 优先级:P2
- 前置条件:Gateway 已配置真实 `REDIS_URL`;恢复候选与恢复状态已有真实数据。
- 步骤:
1. 调用 `GET /downstream/recovery-candidates``GET /downstream/recovery-statuses`
2. 调用 `GET /downstream/recovery-overview`
3. 比较总览接口与两个明细接口返回结果。
- 预期结果:
- `/downstream/recovery-overview` 同时返回候选账号列表和恢复状态列表。
- 总览接口中的 `candidates/statuses` 与两个明细接口真实结果一致,不允许返回静态拼装样例。
- 运维可仅通过总览接口快速判断“哪些账号待恢复、哪些账号处于退避或错误状态”。
### TC-GW-024 恢复状态回流与运营端展示
- 优先级:P1
- 前置条件:Gateway 已产生至少一条真实恢复状态;NestJS API、PostgreSQL 和运营端页面可访问。
- 步骤:
1. 触发某账号恢复状态变化,例如 `waiting_connection``success``failed`
2. 检查 Gateway 是否调用 `/api/gateway/events/downstream/recovery-status`
3. 查询数据库 `GatewayDownstreamRecoveryStatus`
4. 打开运营端“恢复状态管理”页面,查看恢复摘要、恢复状态列表和详情弹窗。
5. 按失败分类筛选,例如“客户未连接”“退避等待”“恢复执行失败”。
6. 使用当前筛选条件执行 CSV 导出。
- 预期结果:
- 恢复状态会从 Gateway 真实回流到 NestJS,并持久化到 PostgreSQL,不只停留在 Redis 或 Gateway 控制面。
- `GatewayDownstreamRecoveryStatus` 至少能查到账号、状态、失败分类、尝试次数、下一次恢复时间、错误原因、应用和企业关联。
- 运营端页面展示的数据来自真实 API/数据库,不是前端本地拼装;详情接口返回字段与数据库一致。
- 失败分类分布来自后端聚合,筛选后列表与统计同步变化。
- 导出文件来自真实后端接口,包含失败分类字段,内容与当前筛选结果一致。
- 页面刷新后恢复状态仍然存在,可继续用于生产排查。
### TC-GW-025 多 Gateway 恢复抢占协调
- 优先级:P0
- 前置条件:Redis 使用真实实例;至少准备两个不同 `GATEWAY_INSTANCE_ID` 的 Gateway 进程或可重复调用恢复锁逻辑的测试环境。
- 步骤:
1. Gateway-A 对同一 `cmppAccount` 发起 pending 恢复,获取 Redis token 租约锁。
2. 在 Gateway-A 未完成前,Gateway-B 对同一账号发起恢复,应被识别为 `lock_contended`
3. 等待 Gateway-A 锁过期后,Gateway-B 再次发起恢复,应能接管并获得新的锁 token。
4. 模拟 Gateway-A 迟到完成恢复。
5. 查看 Redis 锁、Gateway 恢复状态、NestJS `GatewayDownstreamRecoveryStatus` 和运营端“恢复状态管理”详情。
- 预期结果:
- 同一账号同一时间只能由一个 Gateway 实例持有恢复锁。
- 迟到的旧实例完成恢复时,因 token 不匹配不能释放新实例锁,也不能覆盖新实例恢复状态。
- 锁冲突、锁丢失等场景会以 `failureCategory=lock_contended``lock_lost` 进入真实恢复状态。
- `lockOwner/lockExpiresAt` 会回流到 PostgreSQL,并在运营端详情/列表中可见。
### TC-GW-026 长短信分片补偿审计
- 优先级:P0
- 前置条件:真实 PostgreSQL、Redis、NestJS API、Go Gateway 和上游 SMSC 模拟器均已启动;存在可用企业、应用、签名、模板、通道组和在线上游通道。
- 步骤:
1. 通过客户 CMPP 或发送接口提交一条需要 UDH 分片的长短信。
2. 确认 Gateway 向上游 SMSC 逐片提交,并在 `SubmitResult.segments[]` 中回传每个分片的 `segmentIndex/sequenceId/gatewayMessageId/submitStatus/submittedAt`
3. 查询数据库 `SmsMessageSegmentAudit`,确认同一平台短信记录下有对应分片审计行。
4. 模拟部分分片或全部分片回执,检查 NestJS 是否按 `gatewayMessageId` 回填分片回执状态。
5. 对同一短信触发重投或补偿提交,检查新 `submitId` 的分片审计是否保留历史 attempt 与 `compensationType`
6. 打开运营端短信记录详情,查看“分片补偿审计”列表。
- 预期结果:
- 分片审计来自真实 Gateway SubmitResult、NestJS API 和 PostgreSQL,不允许前端静态拼装。
- 每个分片至少记录分片序号、总片数、submitId、sequenceId、上游 MsgId、提交状态和提交时间。
- 回执按分片上游 MsgId 回填到对应审计行,迟到或失败回执不得覆盖短信最终 delivered 状态。
- 重投/补偿产生的新 submitId 与历史 submitId 可以并存审计,运营端能看到补偿归因。
- 当前第一版只要求分片级审计可追踪;按单个分片自动重投和分片级人工重投另行验收。
### TC-GW-027 共享接入号上行人工认领
- 优先级:P0
- 前置条件:真实 PostgreSQL、NestJS API、Go Gateway 和客户侧下游 CMPP 连接可用;至少两个应用共享同一接入号或同一手机号时间窗口内存在多条候选下发记录。
- 步骤:
1. 模拟一条不携带 messageId 的普通上行 Deliver,接入号或手机号时间窗口可匹配多个应用/下发记录。
2. 检查 `SmsUplinkMessage.matchStatus` 是否为 `ambiguous`,并查询 `SmsUplinkMatchCandidate` 候选。
3. 打开运营端“短信上行记录”详情,查看候选企业、候选应用、候选来源、置信度、候选下发短信和候选原因。
4. 选择正确候选执行“认领并推送”。
5. 查询 `SmsUplinkMessage``SmsUplinkMatchCandidate``CmppDownstreamDelivery` 和操作日志。
6. 若客户 CMPP 连接在线,检查 Gateway 是否尝试向认领应用下发普通上行 Deliver;若客户离线,检查待投递记录是否保留 pending/failed 重试状态。
- 预期结果:
- 多候选上行不会误推给任意客户应用,必须先进入 `ambiguous` 并保留候选。
- 候选来自真实接入号路由或手机号时间窗口下发记录,不允许前端静态生成。
- 人工认领后上行记录更新为 `matched`,写入 tenant/application/messageRecord 关联。
- 被选候选状态变为 `claimed`,其他 pending 候选变为 `rejected`,认领动作写入操作日志。
- 认领后创建真实 `CmppDownstreamDelivery(deliveryType=uplink)`,并按现有下游投递链路在线推送或离线保留重试。
### TC-SEND-021 优先队列插队发送
- 优先级:P0
+494
View File
@@ -606,6 +606,500 @@ npm run verify:phase8
- 下游 submit 当前通过 `sourceType=cmpp` 的系统批次兼容承载,尚未完全拆成独立单条发送模型。
- 客户侧最终 Deliver Receipt 投递、客户侧上行 Deliver 推送、上游真实 SMSC submit worker、上游 receipt/uplink 生产解析仍未完成。
## 2026-07-07 Gateway 上游提交与下游 Deliver 闭环补齐
### 本轮修复
- `SubmitCommand` 契约、示例和 Go 结构增加 `upstream.gatewayHost/gatewayPort/account/passwordCipher/cmppVersion`,API 发送链路在真实业务校验通过后保留 BullMQ 审计投递,同时写入 Redis Stream `gateway.submit.commands` 主命令流。
- Go Gateway 新增上游提交管理器,按通道建立/复用 gocmpp 客户端连接,发送真实 CMPP Submit,接收 SubmitResp,并回调 NestJS `SubmitResult`
- Go Gateway 上游读循环开始处理 deliver receipt 和普通 deliver 上行:receipt 解析后回调 NestJS `/gateway/events/receipt`,普通上行解码后回调 `/gateway/events/uplink`
- Gateway 下游入站服务记录客户 Submit 对应的 messageId 到在线客户连接映射;NestJS 收到最终 receipt/uplink 并入库后调用 Gateway `/downstream/receipt``/downstream/uplink`Gateway 向在线客户下发 CMPP Deliver Receipt 或普通 Deliver。
- `api/src/send-chain/send-chain.service.spec.ts` 覆盖 SubmitCommand 上游配置和 Redis Stream 发布;`gateway/internal/inbound/server_test.go` 覆盖客户 submit 后平台下发 Deliver ReceiptGateway 契约示例覆盖新 upstream 字段。
### 验证状态
- `npm --prefix api test -- send-chain.service.spec.ts`:通过。
- `npm --prefix api test`:通过,12 个 suites、82 个 tests。
- `npm --prefix api run build`:通过。
- `go test ./...`Gateway):通过。
- `npm run spike:contracts`:通过,4 个 Gateway 队列契约示例通过。
### 剩余缺口
- Gateway 控制面 `/upstream/submit` 仅保留为调试/补偿入口;生产主链路由 Go Gateway submit worker 消费 Redis Stream `gateway.submit.commands` 触发。worker 当前覆盖新消息 `>` 消费和 ack,pending 历史消息扫描与精细重试治理放入后续在途恢复阶段。
- 客户侧 Deliver Receipt/上行 Deliver 当前依赖 Gateway 内存在线连接映射;客户断线、Gateway 重启或映射丢失时尚未实现持久化缓存、重试和投递失败审计。
- 普通上行只有能关联 messageId 的事件可推送给客户;仅按接入号、手机号、应用和时间窗口匹配客户连接仍待产品化。
- 长短信拆分/重组、多连接窗口、窗口满、在途消息恢复、断线重连后的状态补偿仍待后续实现和压测。
## 2026-07-07 阶段 1Gateway SubmitCommand 独立消费
### 本轮修复
- NestJS SendChain 取消主链路同步调用 Gateway `/upstream/submit`;真实业务校验通过后创建 SmsSubmitRecord、保留 BullMQ `gateway.submit.queue` 审计/兼容投递,并向 Redis Stream `gateway.submit.commands` 写入 `SubmitCommand`
- Go Gateway 新增 `submitworker`,启动时默认创建/复用 consumer group `cmpp-gateway`,独立消费 Redis Stream 中的 `SubmitCommand`,调用同一个上游提交管理器真实 submit 到上游 SMSC。
- Gateway `/upstream/submit` 保留为调试/运维补偿接口,不作为 API 主发送路径。
- Gateway worker 支持环境变量:`REDIS_URL``GATEWAY_SUBMIT_STREAM``GATEWAY_SUBMIT_GROUP``GATEWAY_SUBMIT_CONSUMER``GATEWAY_SUBMIT_WORKER_DISABLED=true`
### 验收口径
- API 入队后不再因为 Gateway 控制面短暂不可达而自己生成 timeoutSubmitResult 必须由 Gateway worker 真实消费和提交后回调。
- Gateway 停止时,SubmitCommand 留在 Redis StreamGateway 恢复后由 consumer group 继续消费新消息。
- BullMQ `gateway.submit.queue` 仅作为审计/兼容,不再是唯一主提交通道。
### 剩余边界
- 当前 worker 先覆盖新消息 `>` 消费和 ackpending 历史消息扫描、claim、重试退避和死信审计放到在途恢复阶段继续做。
## 2026-07-07 阶段 2/3:客户侧 Deliver 持久化重投与普通上行匹配
### 本轮修复
- Prisma 新增 `CmppDownstreamDelivery`,用于保存客户侧待投递 Deliver Receipt 和普通 Deliver 上行;状态覆盖 pending/delivered,记录 retryCount、nextRetryAt、lastError、payload、message/application 关联。
- `SmsUplinkMessage` 增加 `applicationId``messageRecordId``matchStatus``matchReason`,并建立应用和匹配下发记录关系。
- NestJS 收到最终 receipt 后,先写平台回执和消息状态,再创建客户侧待投递记录,尝试调用 Gateway `/downstream/receipt`;成功标记 delivered,客户不在线或 Gateway 不可达时保留 pending 并记录失败原因。
- NestJS 收到普通上行后执行匹配:messageId 精确匹配优先;无 messageId 时按接入号匹配应用路由;仍无唯一应用时按手机号和最近下发时间窗口匹配;多候选标记 ambiguous,未匹配标记 unmatched,但均真实入库。
- Gateway 下游客户 bind/login 成功后保存账号级在线连接,并调用 NestJS `/gateway/events/downstream/pending` 拉取 pending 投递;补发成功后回调 `/gateway/events/downstream/delivered`,失败回调 `/gateway/events/downstream/failed`
- 运营/客户端上行查询 include 应用和匹配下发记录,便于页面展示 matchStatus/matchReason。
### 验证状态
- `npm --prefix api run prisma:generate`:通过。
- `npm --prefix api test -- send-chain.service.spec.ts`:通过。
- `npm --prefix api test`:通过,12 个 suites、82 个 tests。
- `npm --prefix api run build`:通过。
- `go test ./...`Gateway):通过。
- `npm run spike:contracts`:通过。
- `npm run build`:通过,仅既有 Vite chunk size warning。
### 剩余边界
- 待投递 pending 目前在客户 bind/login 时拉取补发;后台周期扫描、指数退避、过期策略、死信队列和运营端失败审计页面仍待后续实现。
- 上行匹配已覆盖 messageId、接入号和手机号时间窗口;共享接入号、多应用多候选时不会误推,但人工认领/改派流程尚未实现。
- 客户连接断开检测和应用级连接数状态回写仍需继续产品化。
## 2026-07-08 阶段 4Gateway 长短信拆分与长上行重组
### 本轮修复
- Go Gateway 上游 Submit 支持长短信第一版拆分:超过 140 字节的短信按 CMPP 标准 6 字节 UDH 生成分片,每片总长度不超过 140 字节,并设置 `PkTotal/PkNumber/TpUdhi` 后逐包发送到上游 SMSC。
- 同一平台 `SubmitCommand` 的多个 accepted 分片 `MsgId` 均登记到 Gateway 映射表,后续任一分片 receipt 可回溯到原 `messageId/submitId/channelId`
- Go Gateway 上游普通 Deliver 支持长上行第一版重组:收到 `TpUdhi=1` 且携带标准 UDH 的分片时,按通道、主叫、被叫、引用号和总片数缓存;分片齐全后只回传一条完整 `UplinkEvent` 给 NestJS。
- 新增 `gateway/internal/upstream/long_message_test.go`,覆盖 UCS2 长短信拆分、短短信不分片、长上行乱序重组。
### 验证状态
- `go test ./...`Gateway):通过。
### 剩余边界
- 阶段 4 后长短信仍按单条平台消息记录展示,尚未提供运营端分片级提交明细、分片级补发审计和部分分片失败后的精细补偿;该审计缺口已在阶段 23 补齐第一版。
- 长上行分片缓存当前为 Gateway 进程内内存;Gateway 重启、跨连接分片漂移或超过缓存 TTL 的残片不会恢复,后续在“在途消息恢复/状态补偿”阶段继续做。
## 2026-07-08 阶段 5Gateway 多连接窗口与窗口满控制
### 本轮修复
- `SubmitCommand.upstream` 契约、示例、Go 结构和 NestJS 生产者增加 `desiredConnections/windowSize`,字段来自通道真实配置;未配置时默认 `desiredConnections=1``windowSize=16`
- Go Gateway 上游提交管理器从单连接升级为通道级连接池:同一通道按 `desiredConnections` 建立多条 CMPP 客户端连接,每条连接独立维护 submit pending、receipt/uplink 映射和长上行分片缓存。
- 每条上游连接增加窗口令牌;提交前必须获得窗口,SubmitResp、reject 或 timeout 后释放窗口;所有连接窗口均满时等待可用窗口,超过提交超时时返回 `WINDOW_TIMEOUT`
- 长短信分片也复用连接池窗口调度,同一条平台消息的多个 accepted 分片仍映射回原 `messageId/submitId/channelId`
- 新增 `gateway/internal/upstream/pool_test.go`,覆盖连接池跨连接获取窗口、窗口满拒绝继续占用、释放后可重新获取。
### 验证状态
- `go test ./...`Gateway):通过。
- `npm --prefix api test -- send-chain.service.spec.ts`:通过。
### 剩余边界
- 当前窗口状态为 Gateway 进程内控制,尚未把连接级窗口占用、等待队列长度、submit latency 等指标回写到 NestJS 或运营端页面。
- 当前阶段只处理窗口容量和多连接发送;Gateway 重启、上游连接断开时的在途 submit 恢复、pending claim、状态补偿和死信审计仍在下一阶段处理。
## 2026-07-08 阶段 6CMPP 配置入口补齐
### 本轮修复
- 运营端通道创建/编辑表单新增上游 `desiredConnections``windowSize` 输入,真实提交到 NestJS 通道 API,并规范化写入 `SmsChannel.config`
- NestJS `ChannelsService``desiredConnections/windowSize` 增加正整数校验;通道激活后的 `ConnectChannel` 请求和发送链路 `SubmitCommand.upstream` 均复用该真实配置。
- Prisma 为 `SmsApplication` 新增 `cmppMaxConnections``cmppWindowSize` 字段;运营端短信应用创建/编辑表单新增 `cmppAccount`、客户最大连接数、客户提交窗口输入。
- 企业应用 `cmppAccount` 现在支持两种真实路径:显式填写 6 位数字账号,或留空由后端自动生成唯一账号;重复账号和非法格式会被后端拒绝。
- 企业应用 CMPP 参数接口改为从应用真实字段返回 `account/maxConnections/windowSize`,不再借用任意通道默认值拼装客户参数。
### 验证状态
- `npm --prefix api run prisma:generate`:通过。
- `npm --prefix api test -- sms-config.service.spec.ts channels.service.spec.ts`:通过,2 个 suites、34 个 tests。
- `npm --prefix api run build`:通过。
- `npm run build`:通过,仅既有 Vite chunk size warning。
### 说明
- `desiredConnections/windowSize` 不是 CMPP 协议标准字段,也不是 gocmpp 的原生配置项;它们是本平台对上游通道连接池和提交窗口的运行参数。
- `cmppAccount` 是客户侧应用接入账号;当前已支持真实生成、真实保存和显式配置。
## 2026-07-08 阶段 7SubmitCommand 在途恢复第一步
### 本轮修复
- Go Gateway `submitworker` 在正常消费新消息前新增 pending 恢复流程:对 Redis Stream consumer group 中空闲超过阈值的消息执行 `XAUTOCLAIM`,将滞留在 PEL 的 `SubmitCommand` 认领到当前 consumer。
- 被认领的 pending 命令复用现有 `handleMessage -> Upstream.Submit -> XAck` 成功路径处理;成功后 ack,失败时保留在 PEL,留给后续重试/死信治理。
- `submitworker` 增加可注入 `Submit` 函数,便于单测覆盖消息处理路径;新增单测覆盖 injected submit 和默认 `minIdle` 阈值。
### 验证状态
- `go test ./...`Gateway):通过。
### 剩余边界
- 当前恢复能力只覆盖 Redis Stream PEL 中“已被读走但未 ack”的 pending 命令;尚未实现恢复次数上限、死信队列、失败审计页面和人工补偿入口。
- Gateway 重启时上游连接内已经发出但尚未收到 submit resp 的 in-flight CMPP 请求,仍未完成状态补偿;这部分继续放在后续“断线重连后的消息状态处理”阶段。
## 2026-07-08 阶段 8:上游连接断开时 pending submit 补偿
### 本轮修复
- Go Gateway 上游连接读循环开始区分“空读超时”和“真实连接断开”;空读超时继续等待,真实断开则进入连接丢失处理。
- 某条上游连接断开时,Gateway 会把该连接上所有等待 submit resp 的 pending submit 立即唤醒,返回 `timeout` + `CONNECTION_LOST`,不再机械等待固定 `SUBMIT_TIMEOUT`
- 连接池在再次分配连接前会重新执行 `ensureConnected()`;旧连接断开后,后续新消息可重新建立物理连接继续提交。
- 新增 `gateway/internal/upstream/connection_loss_test.go`,覆盖 pending submit 被唤醒和临时读超时识别。
### 验证状态
- `go test ./...`Gateway):通过。
- `npm --prefix api test -- send-chain.service.spec.ts`:通过。
### 剩余边界
- 当前补偿只覆盖“连接断开且 submit resp 尚未返回”的场景;尚未覆盖“上游其实已受理,但 submit resp 在断线前后丢失”的二次确认和幂等回查。
- submit 结果死信队列、失败审计、恢复次数上限和人工补偿入口仍在后续阶段。
## 2026-07-08 阶段 9receipt 驱动的保守二次归因
### 本轮修复
- Gateway 上游 receipt 事件补充 `phoneNumber`,即使无法从内存 tracker 中精确恢复平台 `messageId`,也会把运营商回执手机号带回 NestJS。
- NestJS `handleReceipt` 新增保守归因:如果 receipt 无法按平台 `messageId/gatewayMessageId` 精确命中,只在“同通道、同手机号、72 小时窗口内、且仅存在 1 条 `timeout + gatewayMessageId=null` 的 submit 记录”时才接收该回执。
- 归因成功后会先回填该次 `sms_submit_record.gatewayMessageId/sequenceId`,再写入真实 `sms_receipt_record` 并按既有逻辑更新 `sms_message_record`、下游客户回执推送和幂等保护。
- 新增 SendChainService 单测,覆盖唯一候选归因成功和多候选拒绝归因两种场景。
### 验证状态
- `go test ./...`Gateway):待本轮统一回归。
- `npm --prefix api test -- send-chain.service.spec.ts`:待本轮统一回归。
### 剩余边界
- 当前只做“唯一候选才归因”的保守版本,仍未实现面向运营商或供应商的 submit 结果主动回查。
- 如果同通道同手机号在窗口内存在多条 timeout 候选,系统会拒绝归因,后续仍需人工补偿或更强的协议级关联键。
## 2026-07-08 阶段 10SubmitCommand 死信治理第一版
### 本轮修复
- Prisma 新增真实表 `GatewaySubmitDeadLetter`,保存 Gateway SubmitCommand 死信的消息 ID、租户/应用/通道、失败原因、尝试次数、原始命令载荷、人工重入队状态和解决状态。
- Go Gateway `submitworker` 新增失败次数治理:同一条 Stream 消息处理失败达到阈值后,调用 NestJS `/gateway/events/dead-letter` 入库死信,并对原消息执行 ack,避免它无限滞留在 PEL。
- Gateway 对非法 `SubmitCommand` 载荷也会直接转死信,防止 poison message 持续阻塞消费。
- NestJS 新增真实死信接口:Gateway 可上报死信;运营端后端可分页查询 `/api/admin/operations/gateway-submit-dead-letters`;可通过 `/api/admin/operations/gateway-submit-dead-letters/:id/requeue` 将原始 `SubmitCommand` 重新写回 Redis Stream。
- NestJS 在收到同一 `submitId/messageId` 的后续真实 `SubmitResult` 时,会把对应死信自动标记为 `resolved`
### 验证状态
- `npm --prefix api run prisma:generate`:通过。
- `npm --prefix api test -- send-chain.service.spec.ts operations.service.spec.ts`:通过,2 个 suites、26 个测试通过。
- `npm --prefix api run build`:通过。
- `go test ./...`Gateway):通过。
### 剩余边界
- 当前死信治理只提供“达到阈值后入库 + 人工重入队”的第一版,尚未实现后台自动重放、重放节流、过期清理和专门的前端运营页面。
- 非法载荷死信如果缺少完整 `SubmitCommand`,当前不可人工重放,只能用于审计和人工排查。
## 2026-07-08 阶段 11:下游客户在线时周期补投与失败封顶
### 本轮修复
- 明确责任边界:客户系统负责断线后的重新 bind;平台负责客户不在线或临时投递失败时的消息不丢、待投递保存和补投。
- Go Gateway 下游入站服务新增在线账号周期补投:除客户 bind 成功后立即拉取 pending 外,Gateway 还会按周期为当前在线账号再次调用 `/gateway/events/downstream/pending`,继续补发未投递成功的 Deliver Receipt/上行 Deliver。
- Gateway 向下游发送 Deliver 失败时会清理失效的内存会话映射,避免对已失效连接无休止重复尝试。
- NestJS `markDownstreamDeliveryFailed` 新增失败上限:未超过阈值时继续 `pending` 并推进 `retryCount/nextRetryAt`;达到阈值后转为 `failed`,停止无限重试,并写 `gateway.downstream_delivery_failed` 系统日志。
### 验证状态
- `npm --prefix api test -- send-chain.service.spec.ts`:通过,1 个 suite、21 个测试通过。
- `go test ./internal/inbound ./internal/control ./...`Gateway):通过。
### 剩余边界
- 当前周期补投只针对“Gateway 认为客户在线”的账号;尚未实现下游投递失败专门列表、人工重投页面和跨 Gateway 实例共享的客户在线状态。
- `CmppDownstreamDelivery` 目前仍使用固定重试间隔,尚未实现指数退避、不同消息类型差异化策略和过期归档。
## 2026-07-08 阶段 12:下游投递失败审计与人工重投
### 本轮修复
- 运营端新增真实下游投递查询接口 `/api/admin/operations/downstream-deliveries`,支持按 `tenantId/applicationId/deliveryType/status/keyword` 筛选并分页返回真实 `CmppDownstreamDelivery` 数据。
- NestJS 新增 `/api/admin/operations/downstream-deliveries/:id/requeue`,可对单条下游投递记录执行人工重投,真实调用 Gateway `/downstream/receipt``/downstream/uplink`,并写 `gateway.downstream_delivery_requeue` 系统日志。
- 运营端新增“下游投递记录”页面,列表、详情、筛选和重投均接真实后端,不使用 mock、本地状态或静态数组。
### 验证状态
- `npm --prefix api test -- send-chain.service.spec.ts operations.service.spec.ts`:通过,2 个 suites、29 个测试通过。
- `npm --prefix api run build`:通过。
- `npm run build`:通过,仅有既有 Vite chunk size warning。
### 剩余边界
- 当前人工重投仍是单条操作,尚未提供批量重投、失败聚合告警和专门的下游投递 Dashboard。
- 页面侧暂未做自动轮询刷新,需要手动查询或重进页面观察状态变化。
## 2026-07-08 阶段 13:下游投递自动退避第一版
### 本轮修复
- `CmppDownstreamDelivery` 的失败重试从固定 60 秒改为指数退避:基础间隔来自 `CMPP_DOWNSTREAM_RETRY_DELAY_MS`,每次失败按 2 倍递增,并受 `CMPP_DOWNSTREAM_RETRY_MAX_DELAY_MS` 上限约束。
- 这样在客户长时间离线或网络持续抖动时,平台不会每分钟机械重试同一条下游投递,能更温和地消耗 API、Gateway 和连接资源。
- 总重试次数上限逻辑保持不变,超过 `CMPP_DOWNSTREAM_MAX_RETRIES` 后仍转 `failed` 并写失败审计。
### 验证状态
- `npm --prefix api test -- send-chain.service.spec.ts`:通过,新增指数退避单测。
### 剩余边界
- 当前退避策略还没有加入随机抖动,多个记录在同一时间失败时,后续重试时刻仍可能比较集中。
- 退避参数当前是全局环境变量,尚未细分到 receipt/uplink 或不同客户应用级别。
## 2026-07-08 阶段 14:下游投递批量重投
### 本轮修复
- 运营端下游投递记录页新增勾选和“批量重投”操作,仅允许对当前页的 `pending/failed` 记录执行批量重投。
- NestJS 新增真实批量接口 `/api/admin/operations/downstream-deliveries/requeue`,逐条调用既有单条重投逻辑,返回成功/失败汇总,不用前端自行拼结果。
- SendChainService 新增批量重投结果汇总与空选择拦截单测。
### 验证状态
- `npm --prefix api test -- send-chain.service.spec.ts`:通过,新增批量重投单测。
- `npm --prefix api run build`:通过。
- `npm run build`:待本轮统一回归。
### 剩余边界
- 当前批量重投只支持“勾选当前页记录”,还不支持“按筛选条件全量重投”或后台异步大批量任务。
- 批量结果当前以内联提示为主,尚未做专门的批量执行历史与导出。
## 2026-07-08 阶段 15:下游投递告警第一版
### 本轮修复
- `OperationsService.dashboard()` 新增真实下游投递告警聚合 `downstreamDeliverySummary`,统计 `pending/failed/delivered` 总量,以及“积压过久的 pending”和“最近失败”两类告警计数。
- 运营端右上角通知新增“下游投递告警”,数量直接来自真实 Dashboard 聚合。
- 运营看板新增下游投递告警摘要卡片,帮助运营从总览页直接感知当前下游投递异常。
### 验证状态
- `npm --prefix api test -- operations.service.spec.ts`:随定向测试通过。
- `npm --prefix api run build`:通过。
- `npm run build`:通过,仅有既有 Vite chunk size warning。
### 剩余边界
- 当前告警仍是站内聚合提醒,尚未接短信、邮件、企业微信等外部告警通道。
- 告警口径当前采用全局阈值环境变量,尚未按客户应用、消息类型或时间段细分。
## 2026-07-08 阶段 16:下游投递 Dashboard 第一版
### 本轮修复
- 新增真实接口 `/api/admin/operations/downstream-deliveries/dashboard`,直接按 `CmppDownstreamDelivery` 聚合返回 `summary/typeBreakdown/retryBuckets/topApplications`
- 运营端“下游投递记录”页面顶部补上真实 Dashboard 区域,展示投递总量、待投递、已投递、告警、类型分布、重试压力和应用告警排行。
- Dashboard 筛选范围与页面应用/类型筛选保持一致,不允许由前端只根据当前页列表数据临时拼装。
### 验证状态
- `npm --prefix api test -- operations.service.spec.ts`:通过。
- `npm --prefix api run build`:通过。
- `npm run build`:通过,仅有既有 Vite chunk size warning。
- `git diff --check`:无空白错误,仅 Windows LF/CRLF 提示。
### 剩余边界
- 当前 Dashboard 仍偏运营处置视角,尚未补时间趋势、按客户/账号维度的更细颗粒聚合。
- 应用告警排行当前以 `pending + failed` 为主排序,尚未加入更复杂的权重和 SLA 指标。
## 2026-07-08 阶段 17:下游在线账号 Presence 持久化底座
### 本轮修复
- Gateway inbound 新增 Redis presence store,客户 `cmppAccount` 在 bind 成功、submit 建链和下游回执/上行投递时,会把在线账号状态写入 Redis。
- presence 数据至少包含 `account/srcId/remoteIp/gatewayInstanceId/state/connectedAt/updatedAt`,并按 TTL 自动过期,避免该状态只存在单进程内存中。
- Gateway 发送失败触发连接清理时,会同步移除该账号的 Redis presence 记录。
- 该阶段先完成“在线状态外部化”,尚未宣称“Gateway 重启后 pending 投递自动恢复”已完成;恢复逻辑在后续阶段继续补。
### 验证状态
- `go test ./internal/inbound/...`:通过。
- `go test ./cmd/gateway/...`:通过。
### 剩余边界
- 当前 presence 主要服务于后续恢复能力,Gateway 还未在启动时主动根据 Redis presence 扫描并恢复 pending 投递。
- 连接断开当前主要依赖发送失败清理和 TTL 过期兜底,尚未建立更完整的显式断线回收机制。
## 2026-07-08 阶段 18Gateway 恢复候选视图
### 本轮修复
- Gateway 启动时会读取 Redis presence,并输出恢复候选账号加载日志。
- 新增控制面接口 `GET /downstream/recovery-candidates`,返回 Redis presence 与当前内存在线账号合并后的恢复候选视图。
- 候选视图当前用于后续恢复逻辑和运维排查,不直接触发 pending 下游投递补发。
### 验证状态
- `go test ./internal/inbound/...`:通过。
- `go test ./internal/control/...`:通过。
- `go test ./cmd/gateway/...`:通过。
### 剩余边界
- 当前只是“识别谁值得恢复”,还没有执行“把这些账号的 pending 回执/上行自动继续补投”。
- 候选视图默认按 Redis TTL 和最近活跃时间保留,尚未叠加更复杂的健康判定和跨实例去重策略。
## 2026-07-08 阶段 19Gateway pending 恢复执行第一版
### 本轮修复
- Gateway 启动时会立即按恢复候选账号执行一次 pending 下游投递恢复扫描。
- 后续每轮补投周期除扫描当前内存在线账号外,也会继续扫描恢复候选账号,尝试恢复 `CmppDownstreamDelivery.pending`
- 当前恢复策略是“能投就投,投不了继续 pending”:若账号尚无可用下游连接,Gateway 不会把记录误标成失败,而是等待客户重连后的后续恢复机会。
### 验证状态
- `go test ./internal/inbound/...`:通过。
- `go test ./internal/control/...`:通过。
- `go test ./cmd/gateway/...`:通过。
### 剩余边界
- 当前恢复仍按固定扫描周期触发,尚未做更细的按账号退避、恢复批次追踪和恢复告警。
- 仍未覆盖更复杂的长短信分片恢复、跨实例抢占协调和恢复中的重复投递防抖。
## 2026-07-08 阶段 20Gateway 恢复退避、锁与状态审计
### 本轮修复
- Gateway 新增账号级恢复锁,避免同一 `cmppAccount` 被并发重复恢复。
- 恢复失败、等待连接和部分成功场景会写入真实恢复状态,并按指数退避计算下一次可恢复时间,减少无意义高频重试。
- 控制面新增 `GET /downstream/recovery-statuses`,可查看账号最近恢复状态、尝试次数、下一次重试时间和错误原因。
### 验证状态
- `go test ./internal/inbound/...`:通过。
- `go test ./internal/control/...`:通过。
- `go test ./cmd/gateway/...`:通过。
### 剩余边界
- 当前恢复状态审计仍停留在 Gateway 控制面和 Redis,尚未同步到运营端页面或 NestJS 持久化审计表。
- 恢复退避当前按账号统一处理,尚未细分到回执/上行类型、失败类别或跨实例抢占优先级。
## 2026-07-08 阶段 21Gateway 恢复总览与链路缺口收口
### 本轮修复
- 控制面新增 `GET /downstream/recovery-overview`,一次性返回恢复候选账号和恢复状态,便于生产联调与排查。
- 需求文档已按当前真实代码重新梳理 CMPP 端到端链路剩余缺口,明确区分“已能验收的真实链路能力”和“尚未产品化完成的恢复审计/指标/复杂补偿能力”。
- 系统测试用例新增恢复总览接口口径,便于后续生产验证直接对照。
### 验证状态
- `go test ./internal/control/...`:通过。
- `go test ./internal/inbound/...`:通过(延续前一阶段验证结果,本轮未改动 inbound 核心分支逻辑)。
- `go test ./cmd/gateway/...`:通过。
### 阶段 21 后剩余真实缺口
- 恢复状态仍未写回 NestJS/Prisma/PostgreSQL,运营端暂无真实恢复状态页面。
- 多 Gateway 实例下更强的恢复抢占协调、分片级补偿审计、共享接入号上行人工认领仍未完成。
- 连接级窗口利用率、恢复吞吐、恢复失败分布等运营指标仍未进入真实后台页面。
## 2026-07-08 阶段 22:恢复状态回流 NestJS 与运营端展示
### 本轮修复
- NestJS 新增真实恢复状态接收接口 `/api/gateway/events/downstream/recovery-status`
- Prisma/PostgreSQL 新增 `GatewayDownstreamRecoveryStatus` 表,按 `cmppAccount` 持久化恢复状态、尝试次数、下一次恢复时间、错误原因及应用/企业关联。
- 运营端新增独立“恢复状态管理”页面,支持真实恢复状态列表、分页、详情接口和当前筛选结果 CSV 导出。
- 原“下游投递记录”页面仅保留投递记录与重投能力,不再混放恢复状态列表。
- 恢复状态新增 `failureCategory` 失败分类字段,Gateway 回传、NestJS 兜底归类并落库,运营端支持分类筛选、分布统计、详情展示和导出。
- 多 Gateway 恢复抢占协调补强:恢复锁升级为 Redis token 租约,恢复完成时通过 Lua 原子校验 token 后才写状态和释放锁;迟到旧实例不能误删新实例锁。
- `GatewayDownstreamRecoveryStatus` 新增 `lockOwner/lockExpiresAt`Gateway 回传并由 NestJS 入库,运营端恢复状态列表和详情可查看锁持有实例。
- 本地启动脚本补充 `.local-tools\minio.exe` 查找路径,并已验证本机 MinIO 可通过 `npm run start:local:minio` 启动。
### 验证状态
- `npm --prefix api run prisma:generate`:通过。
- `npm --prefix api test -- operations.service.spec.ts send-chain.service.spec.ts`:通过。
- `npm --prefix api run build`:通过。
- `go test ./internal/inbound/... ./internal/control/... ./cmd/gateway/...`:通过。
- `npm run build`:通过,仅有既有 Vite chunk size warning。
- `npm --prefix api run prisma:migrate:deploy`:通过,已应用 `20260708213000_add_recovery_lock_observability`
- `npm run start:local:minio`:通过,MinIO API `http://localhost:9000`、Console `http://localhost:9001` 已监听。
### 阶段 22 后剩余真实缺口
- 恢复状态已回流 NestJS,并已具备独立运营页、详情、导出和第一版失败分类分布;后续仍缺少恢复吞吐、耗时趋势、连续失败账号等更细指标。
- 多 Gateway 账号级恢复抢占协调已具备 token 租约和完成校验;分片级补偿审计、共享接入号上行人工认领仍未完成。
- 连接级窗口利用率、连接级心跳、恢复吞吐和恢复耗时等运营指标仍未进入真实后台页面。
## 2026-07-08 阶段 23:长短信分片级补偿审计
### 本轮修复
- Prisma/PostgreSQL 新增 `SmsMessageSegmentAudit`,按短信记录、submitId、分片序号保存真实分片提交、回执和补偿归因。
- Go Gateway 上游提交结果 `SubmitResult` 增加 `segments[]`,逐片回传 `segmentTotal/segmentIndex/sequenceId/gatewayMessageId/submitStatus/submittedAt`,长短信不再只暴露首个分片结果。
- NestJS `handleSubmitResult` 写入分片提交审计,`handleReceipt` 按上游 `gatewayMessageId` 回填分片回执状态;重投或补偿产生的新 submitId 与历史 submitId 可并存追踪。
- 运营端短信记录详情新增“分片补偿审计”列表,从真实 API 查询 `SmsMessageSegmentAudit`,展示分片、submitId、通道、Sequence、MsgId、提交状态、回执状态、补偿类型和错误信息。
- 契约文档和示例补充 `SubmitResult.segments[]`,系统测试用例新增 `TC-GW-026 长短信分片补偿审计`
### 验证状态
- `npm --prefix api run prisma:generate`:通过。
- `go test ./internal/upstream/... ./internal/queue/... ./internal/submitworker/...`:通过。
- `npm --prefix api test -- send-chain.service.spec.ts operations.service.spec.ts`:通过。
- `npm --prefix api run build`:通过。
- `npm run build`:通过,仅有既有 Vite chunk size warning。
### 阶段 23 后剩余真实缺口
- 长短信分片级提交、回执和补偿归因已具备真实审计;后续仍需补按单个分片自动重投、分片级人工重投和更细的补偿指标。
- 共享接入号、多候选普通上行的人工认领流程仍未完成。
- 连接级窗口利用率、连接级心跳、恢复吞吐、恢复耗时趋势和连续失败账号等运营指标仍未进入真实后台页面。
## 2026-07-08 阶段 24:共享接入号上行人工认领
### 本轮修复
- Prisma/PostgreSQL 新增 `SmsUplinkMatchCandidate`,用于保存普通上行 ambiguous 场景下的候选企业、应用、下发短信、候选来源、置信度、认领状态和认领时间。
- NestJS 上行匹配逻辑增强:接入号匹配多个应用、或手机号时间窗口匹配多条下发时,不误推客户;上行记录标记 `ambiguous`,并真实写入候选表。
- 运营端“短信上行记录”详情新增候选认领区,展示候选企业、候选应用、候选来源、置信度、候选下发短信和候选原因,支持“认领并推送”。
- 新增 `POST /admin/operations/uplink-messages/:id/claim`:认领后更新 `SmsUplinkMessage``matched`,选中候选置为 `claimed`,其他候选置为 `rejected`,写入操作日志,并创建真实 `CmppDownstreamDelivery(deliveryType=uplink)` 走客户侧下游投递链路。
- 系统测试用例新增 `TC-GW-027 共享接入号上行人工认领`
### 验证状态
- `npm --prefix api run prisma:generate`:通过。
- `npm --prefix api run prisma:migrate:deploy`:通过,已应用 `20260708233000_add_uplink_match_candidates`
- `npm --prefix api test -- send-chain.service.spec.ts operations.service.spec.ts`:通过,40 个测试。
- `npm --prefix api run build`:通过。
- `npm run build`:通过,仅有既有 Vite chunk size warning。
### 当前剩余真实缺口
- 共享接入号上行已具备候选记录、人工认领和认领后下游投递第一版;后续仍需补批量认领、认领复核和认领准确率/积压指标。
- 长短信分片级提交、回执和补偿归因已具备真实审计;后续仍需补按单个分片自动重投、分片级人工重投和更细的补偿指标。
- 连接级窗口利用率、连接级心跳、恢复吞吐、恢复耗时趋势和连续失败账号等运营指标仍未进入真实后台页面。
## 2026-07-03 阶段 9:运营端报备回执导入真实上传/解析
### 本轮修复