# SendChain Service 职责拆分专项方案 - 方案日期:2026-08-28 - 对应整改项:`C8 SendChain Service 职责拆分` - 当前范围:只制定方案,不在本轮实施发送链重构 - 适用环境:本地和测试环境 `100.93.204.60` - 禁止范围:未经新授权不得部署预生产;不得用生产短信验证重构 ## 1. 当前状态与问题 当前发送链已经存在一层门面 `send-submission.service.ts`,但两个实现类仍然过大: | 文件 | 当前规模 | 集中职责 | |---|---:|---| | `send-inbound-entry.service.ts` | 约1852行、84.2 KiB | CMPP认证、入站持久化、长短信、Inbox claim、企业微批、配额、模板/引流匹配、风控、指标 | | `send-gateway-submit.service.ts` | 约1159行、52.2 KiB | BullMQ Worker、批处理、路由、签名报备、限速、Submit命令、Outbox、任务进度、资源回收 | | `send-submission.service.ts` | 约280行 | 对外门面、跨服务回调与编排 | 主要风险不是文件长度本身,而是以下行为边界在同一个类中耦合: - PostgreSQL事务与 advisory lock 顺序。 - Inbox claim、租约、重试与恢复。 - 长短信分片幂等和完整消息恢复。 - 计费冻结、扣费、释放与失败补偿。 - 路由、签名报备、通道限速和主备策略。 - Submit Outbox 的租约、发布、确认和重复消费。 - 消息状态与批量任务进度的单飞/尾随刷新。 因此专项必须以“行为锁定后机械提取”为主,不允许借拆分类的机会同时重写SQL、事务或状态机。 ## 2. 拆分目标 1. `SendSubmissionService` 只保留稳定门面和跨子域编排。 2. 每个子服务只有一个主要变化原因,建议控制在500行以内;复杂持久化服务可放宽至700行,但不得继续混合Worker生命周期、业务校验和发布协议。 3. 保持现有公开方法签名、队列名、Stream字段、幂等键、数据库状态和错误码不变。 4. 拆分过程中不改变吞吐参数、路由权重、计费口径和定时器周期。 5. 每一步均可独立回退,不要求一次性完成全部拆分。 ## 3. 目标结构 建议新增以下内部服务,目录仍保留在 `api/src/send-chain/`,暂不建立更深目录,避免一次性路径迁移过大。 ### 3.1 入站侧 | 目标服务 | 从现有文件提取的职责 | 明确不负责 | |---|---|---| | `inbound-authentication.service.ts` | `authenticateInboundApplication`、连接认证结果记录、IP白名单和接口状态检查 | 消息持久化、计费 | | `inbound-long-message.service.ts` | 分片收集、完成判定、超时清理、已完成结果恢复 | 普通单条业务创建 | | `inbound-inbox-repository.service.ts` | Inbox持久化、请求哈希、claim、租约续期/完成/失败 | 风控和模板匹配 | | `inbound-workflow-worker.service.ts` | pump、企业公平分组、微批调度、停止与指标刷新 | SQL细节、消息业务规则 | | `inbound-message-preparation.service.ts` | 应用快照、模板/签名/引流匹配、号码频控与风控输入组装 | 事务写入和队列发布 | | `inbound-message-persistence.service.ts` | 单条/微批消息、任务、记录、配额预留和幂等持久化 | Worker生命周期、Gateway Submit | ### 3.2 出站侧 | 目标服务 | 从现有文件提取的职责 | 明确不负责 | |---|---|---| | `send-worker-lifecycle.service.ts` | BullMQ Worker启动/停止、并发槽位、微批flush、队列指标 | 路由、计费和命令构造 | | `message-send-processor.service.ts` | 单条/批量消息处理、状态迁移、失败补偿编排 | Worker定时器和Outbox发布器生命周期 | | `channel-routing.service.ts` | 运营商/省份识别、应用路由、候选通道、签名报备检查、主备选择 | 发布命令和数据库Outbox | | `channel-rate-limit.service.ts` | 通道TPS等待和相关低基数指标 | 路由选择 | | `gateway-submit-command.service.ts` | 命令构造、开放会话ID复用、直接Stream发布契约 | Outbox claim和业务状态变更 | | `submit-outbox-publisher.service.ts` | Outbox claim、租约、批量发布、确认、重试和恢复 | 消息计费和路由 | | `task-progress-refresher.service.ts` | 真实总量/状态聚合、单飞和尾随刷新 | Submit或回执业务处理 | ### 3.3 共享边界 - `send-chain.contracts.ts`:只保留稳定输入输出契约,不放实现函数。 - `send-chain.helpers.ts`:只保留无副作用纯函数;需要数据库、Redis、时间器或日志的逻辑必须进入Service。 - 计费继续通过现有 `BillingService`,不得复制金额算法。 - 所有数据库访问保持现有 Prisma 条件和事务客户端传递方式。 - 所有跨服务调用通过构造注入,不通过全局单例或循环引用。 ## 4. 拆分前行为锁定 在移动代码前补齐特征测试,测试的是当前语义,不是期望中的新设计。 ### 4.1 入站测试 - 认证成功、未知账号、禁用应用、未认证企业、IP不在白名单。 - 同一请求键并发两次只产生一份任务/消息/计费预留。 - 请求键相同但payload不同返回冲突。 - 长短信乱序、重复分片、缺片、超时、重启恢复。 - Inbox claim不重复、租约超时可恢复、失败次数和最终状态一致。 - 企业微批公平性、单企业失败不阻塞其他企业。 - 模板、签名、引流匹配和人工审核关联不改变。 - 配额预留不足时不产生半成品消息或费用。 ### 4.2 出站测试 - 普通批量与CMPP内部任务入队语义保持不同且正确。 - Worker微批开启/关闭结果一致。 - 路由优先级、weighted分流、主备切换和运营商过滤不变。 - 签名未报备时的拒绝状态和费用释放一致。 - 通道TPS限制不会跨通道串扰。 - Submit命令字段、Sequence_Id、Msg_Id、Src_Id和幂等键保持一致。 - Outbox重复claim、租约恢复、发布成功/失败、Stream重复消费结果一致。 - 计费冻结、扣费、退款和释放只发生一次。 - 任务进度刷新保持单飞,并在运行中出现新状态时执行一次尾随刷新。 ### 4.3 契约快照 拆分前固定以下非敏感契约样本: - `gateway.submit.commands` - `gateway.submit.results` - `gateway.protocol.logs` - BullMQ `send-message` job data - Submit Outbox payload 契约测试只固定字段、类型、必填关系和幂等语义,不固定时间戳、随机ID或日志文本。 ## 5. 分阶段实施顺序 ### 阶段 S0:基线冻结 - 记录Git commit、两个文件行数/哈希、95项migration状态。 - 运行现有SendChain专项、API全量、Gateway全量、队列契约和性能基线。 - 在隔离数据库和Redis记录测试前快照。 出口:所有既有测试通过,所有临时数据带独立前缀且可清理。 ### 阶段 S1:提取纯路由与命令构造 先提取 `channel-routing.service.ts` 和 `gateway-submit-command.service.ts`。这两部分事务耦合相对较低,最适合验证构造注入和门面调用方式。 规则:只移动代码;输入输出、查询条件、排序和错误文本保持不变。 ### 阶段 S2:提取任务进度刷新与限速 提取 `task-progress-refresher.service.ts` 和 `channel-rate-limit.service.ts`,保留单飞Map、dirty尾随集合及Redis限速键格式。 出口:并发刷新和跨通道限速特征测试通过。 ### 阶段 S3:提取Submit Outbox 提取 `submit-outbox-publisher.service.ts`,将定时器生命周期和claim/publish/ack逻辑封装在同一服务;不得改变租约所有者、批量大小、轮询周期或重试条件。 出口:故障注入后无丢失、无双发,Stream最终 `pending=0/lag=0`。 ### 阶段 S4:提取Worker生命周期和消息处理 将BullMQ启动/停止/微批flush移入 `send-worker-lifecycle.service.ts`,业务处理移入 `message-send-processor.service.ts`。 出口:Worker关闭会等待当前flush;并发槽位与处理指标不变;吞吐基线不下降超过5%。 ### 阶段 S5:提取入站认证和长短信 先移动认证,再移动长短信。两者均有明确入口和状态表,避免直接触碰Inbox微批主流程。 出口:认证审计字段、长短信幂等和超时恢复全部一致。 ### 阶段 S6:提取Inbox Repository和Worker 先封装原SQL与事务为Repository,再将pump/企业分组/claim循环移入Worker。Repository方法必须接受现有事务客户端,不允许内部偷偷开启嵌套事务。 出口:claim锁顺序、租约、重试、企业公平性和停机等待不变。 ### 阶段 S7:提取消息准备与持久化 这是入站侧风险最高的阶段,最后执行。先把纯准备结果定义为显式结构,再机械移动持久化;计费操作继续调用现有BillingService。 出口:消息、任务、计费、审核任务和Inbox状态在成功与每个失败点均与基线一致。 ### 阶段 S8:清理门面 删除仅为旧类内部跳转存在的回调,`SendSubmissionService`仅保留公开API编排。清理必须单独提交,不能与任何业务优化合并。 ## 6. 每阶段验证矩阵 | 层级 | 必须执行 | |---|---| | 静态 | API TypeScript、ESLint、Prettier、结构门禁、`git diff --check` | | 单元 | 新子服务定向测试、SendChain既有全套特征测试 | | API | 全量Jest及全源/增量覆盖率 | | PostgreSQL | Prisma validate、95项migration、真实事务/锁/幂等契约 | | Redis | BullMQ、限速键、Stream、Outbox幂等和最终pending/lag | | Gateway | `go test ./... -count=1`、`go vet ./...`、五份队列契约 | | 故障注入 | Redis短暂不可用、Gateway响应超时、DB事务失败、Worker中断恢复 | | 性能 | 与S0同数据和同参数,入口延迟、完整Submit吞吐、DB池等待、队列积压 | 任何阶段出现以下情况立即停止并回退该阶段: - 重复计费、重复Submit或消息丢失。 - 状态机出现无法收敛的新状态。 - advisory lock顺序或事务边界变化。 - 相同负载完整Submit吞吐下降超过5%,且无法由环境波动解释。 - Stream pending/lag、Inbox processing或Outbox leased无法清零。 ## 7. 提交策略 每个阶段至少拆成两个提交: 1. `test(send-chain): lock behavior` 2. `refactor(send-chain): extract service` 禁止在重构提交中混入: - SQL优化或索引变更。 - 状态、错误码、队列字段变更。 - TPS、批量大小、超时或重试参数调整。 - 依赖大版本升级和全仓格式化。 ## 8. 测试环境发布与回退 每个可部署阶段发布前都必须重新建立并校验:PostgreSQL custom dump、运行目录、环境/systemd/Nginx、Redis RDB与Stream状态、MinIO清单、部署commit和migration状态。 建议S1~S3合并为第一个测试版本,S4为第二个,S5~S6为第三个,S7~S8为最终版本。每个版本均使用独立恢复点,禁止复用旧备份冒充当前基线。 回退只允许回退本阶段部署包与对应恢复资产;如已发生业务写入,应优先代码前滚修复,不得盲目恢复数据库覆盖测试期间其他数据。 ## 9. 工作量与决策点 | 阶段 | 预计工作量 | 风险 | |---|---:|---:| | S0 行为锁定 | 2~3人日 | 中 | | S1~S3 出站低/中耦合拆分 | 2~4人日 | 中 | | S4 Worker拆分 | 1~2人日 | 中高 | | S5~S6 入站认证/Inbox拆分 | 2~4人日 | 高 | | S7~S8 持久化与门面收口 | 2~4人日 | 高 | 总计建议按9~17人日安排,并分至少4个测试版本。开始实施前需要用户再次明确授权C8专项和测试环境发布窗口。 ## 10. 最终关闭标准 - 两个超大实现类被删除或只保留不超过300行的兼容转发层。 - 每个目标服务职责单一,构造依赖和公开方法有明确边界。 - 所有行为锁定、故障注入、真实PostgreSQL/Redis和Gateway契约通过。 - 任务、消息、Submit、Outbox、计费和回执语义与S0一致。 - 同口径性能基线下降不超过5%,无新增DB池等待或队列积压。 - 测试环境发布后服务健康、错误日志为0、Stream最终 `pending=0/lag=0`。 - 浏览器只需验证受影响的任务/消息状态展示;不得用页面显示替代后端全链证据。