12 KiB
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. 拆分目标
SendSubmissionService只保留稳定门面和跨子域编排。- 每个子服务只有一个主要变化原因,建议控制在500行以内;复杂持久化服务可放宽至700行,但不得继续混合Worker生命周期、业务校验和发布协议。
- 保持现有公开方法签名、队列名、Stream字段、幂等键、数据库状态和错误码不变。
- 拆分过程中不改变吞吐参数、路由权重、计费口径和定时器周期。
- 每一步均可独立回退,不要求一次性完成全部拆分。
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.commandsgateway.submit.resultsgateway.protocol.logs- BullMQ
send-messagejob 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. 提交策略
每个阶段至少拆成两个提交:
test(send-chain): lock <responsibility> behaviorrefactor(send-chain): extract <responsibility> 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。 - 浏览器只需验证受影响的任务/消息状态展示;不得用页面显示替代后端全链证据。