feat: complete cmpp gateway delivery recovery workflows
This commit is contained in:
@@ -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 返回 SubmitResp,Gateway 回调 NestJS `SubmitResult`。
|
||||
7. 上游 SMSC 下发 deliver receipt,Gateway 解析为 `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/login,Gateway 按账号拉取 pending 投递并补发,成功后回写 delivered。
|
||||
- 预期结果:
|
||||
- 客户 bind/login 使用真实数据库账号、密码、状态和 IP 白名单校验。
|
||||
- 业务校验失败时不调用上游 submit,不扣费,不伪造成功。
|
||||
- API 入队后不依赖同步调用 Gateway `/upstream/submit`;Gateway 停止时命令留在 Redis Stream,Gateway 恢复后继续消费。
|
||||
- 长短信 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 模拟 SMSC;NestJS 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 group;Gateway 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
|
||||
|
||||
Reference in New Issue
Block a user