Files
lislgosms/docs/sendchain-service-decomposition-plan-20260828.md

233 lines
12 KiB
Markdown
Raw Permalink 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.
# 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 <responsibility> behavior`
2. `refactor(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 行为锁定 | 23人日 | 中 |
| S1~S3 出站低/中耦合拆分 | 2~4人日 | 中 |
| S4 Worker拆分 | 12人日 | 中高 |
| S5S6 入站认证/Inbox拆分 | 24人日 | 高 |
| S7~S8 持久化与门面收口 | 2~4人日 | 高 |
总计建议按9~17人日安排,并分至少4个测试版本。开始实施前需要用户再次明确授权C8专项和测试环境发布窗口。
## 10. 最终关闭标准
- 两个超大实现类被删除或只保留不超过300行的兼容转发层。
- 每个目标服务职责单一,构造依赖和公开方法有明确边界。
- 所有行为锁定、故障注入、真实PostgreSQL/Redis和Gateway契约通过。
- 任务、消息、Submit、Outbox、计费和回执语义与S0一致。
- 同口径性能基线下降不超过5%,无新增DB池等待或队列积压。
- 测试环境发布后服务健康、错误日志为0、Stream最终 `pending=0/lag=0`
- 浏览器只需验证受影响的任务/消息状态展示;不得用页面显示替代后端全链证据。