Files
lislgosms/docs/codebase-modularization-roadmap.md
T

1141 lines
49 KiB
Markdown
Raw 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.
# CMPP 平台代码文件渐进式拆分路线图
更新日期:2026-07-30
适用仓库:CMPP 平台主仓库
规划性质:跨多个发布版本逐步实施,不要求一次完成
> R0 已于 2026-07-30 在本地建立,不包含生产代码移动。执行资料见
> `docs/refactoring/r0-responsibility-index.md`、
> `docs/refactoring/r0-release-gate.md` 和
> `docs/contracts/refactoring-r0-manifest.json`;当前仍未提交、未推送、未部署。
## 1. 目标
本路线图用于解决部分代码文件持续膨胀、职责混杂、修改影响面难以判断的问题,同时避免“大文件一次性拆分后引入大量回归 Bug”。
拆分的首要目标不是追求更少的行数,而是:
1. 保持现有业务行为、接口、数据库事务、队列协议和页面交互不变。
2. 让每次需求修改只需要进入一个清晰的业务模块。
3. 降低多人或多个 AI 会话同时修改同一文件时的冲突概率。
4. 让测试能够按业务域定位,而不是所有测试集中在一个超大文件。
5. 让每个拆分版本都可以单独验证、发布、观察和回滚。
## 2. 当前基线
本次盘点基于本地 `main` 分支,`HEAD``origin/main` 均为:
`0af671b4ed4713912e703defd08791f164d4eb25`
当前工作区存在未提交业务修改、其他会话修改及构建产物。未来执行拆分前必须重新确认基线,不得把本文件记录的状态直接视为届时事实。
### 2.1 主要大文件
| 文件 | 当前约行数 | 最近80次提交中的变更次数 | 主要问题 | 初步风险 |
|---|---:|---:|---|---|
| `src/styles/global.css` | 10322 | 42 | 全局样式、页面样式、响应式规则持续叠加,级联影响难判断 | 高 |
| `api/src/send-chain/send-chain.service.ts` | 5155 | 50 | 入站、审核、计费、路由、提交、回执、补发、下游投递集中 | 极高 |
| `api/src/send-chain/send-chain.service.spec.ts` | 3583 | - | 多类业务共用巨型 Mock,失败定位困难 | 高 |
| `api/src/channels/channels.service.ts` | 2443 | 30 | 通道配置、连接、测试、路由、报备、删除治理混合 | 高 |
| `api/src/operations/operations.service.ts` | 2282 | 27 | 短信记录、看板、统计、日志、下游恢复、追踪混合 | 中高 |
| `src/api/adminApi.ts` | 2177 | 54 | 请求基础设施、全部 DTO/类型、所有运营端接口集中 | 中高 |
| `api/src/sms-config/sms-config.service.ts` | 2143 | 38 | 应用、签名、模板、引流、审核、连接生命周期混合 | 高 |
| `gateway/internal/inbound/server.go` | 约1600 | 18 | 登录、提交、会话、下游投递、ACK、恢复集中 | 极高 |
| `gateway/internal/upstream/manager.go` | 约1500 | 9 | 连接池、重连、窗口、提交、回执、上行集中 | 极高 |
| `api/src/report-materials/report-materials.service.ts` | 1077 | - | 导入、审核、批次生成、导出和幂等操作混合 | 中高 |
| `src/apps/admin/AdminEnterpriseSignaturesPage.tsx` | 829 | - | 查询、表格、编辑、报备资料和多个弹窗集中 | 中 |
| `src/apps/admin/AdminChannelsPage.tsx` | 734 | - | 列表、状态、测试、连接详情和编辑交互集中 | 中 |
行数只能用于发现候选文件,不能单独决定优先级。发送链和 Gateway 即使测试较多,仍因并发、幂等、账务和协议状态机而具有最高拆分风险。
## 3. 必须遵守的拆分原则
### 3.1 先锁定行为,再移动代码
每个拆分版本先补“特征测试”,记录当前真实行为,再执行移动。特征测试至少覆盖:
- 请求和响应结构;
- 数据库写入及事务边界;
- 幂等键和唯一约束;
- Redis Stream 或队列消息结构;
- 状态流转;
- 错误码和用户可见提示;
- 时间、时区及金额精度;
- CMPP 报文、Sequence_Id、Msg_Id 和分片行为。
如果当前行为本身存在 Bug,应先单独修复并发布,再开始对应模块的纯重构。禁止在同一个提交中同时“大范围拆分 + 修改业务规则”。
### 3.2 保留稳定门面
拆分初期保留原有类、导出名和控制器调用方式:
```text
Controller
原 Service / API 门面(签名不变)
新拆分的领域服务、查询服务或纯函数
```
例如:
- `SendChainService` 在较长时间内继续存在,控制器和其他模块不立即改依赖。
- `adminApi.ts` 继续导出 `adminApi`,页面不在同一版本内批量改 import。
- Gateway 的 `Server``Manager` 对外方法签名先保持不变。
只有新模块稳定运行至少一个发布周期后,才评估是否缩小或删除旧门面。
### 3.3 每个版本只拆一个业务域
一个拆分版本不得同时横跨发送链、通道、运营统计和 Gateway。建议边界:
- 一个后端业务域;
- 或一个前端 API 域;
- 或一个页面;
- 或一个 Gateway 状态机子域。
如果一个版本的 diff 已经难以由人工逐段复核,应继续切小。
### 3.4 先拆纯逻辑,再拆副作用
优先移动:
- DTO 和类型;
- 常量;
- 数据格式转换;
- 状态判定;
- 号码、运营商、报文和时间计算;
- 查询条件构造;
- 响应映射。
后移动:
- PostgreSQL 事务;
- 账户扣费和退款;
- Redis Stream 发布;
- Gateway 控制命令;
- 补发抢占;
- 最终回执投递;
- CMPP 连接与 ACK 状态机。
### 3.5 事务不能被“拆没”
原本在一个 `$transaction` 中完成的操作,不得因为拆成多个 Service 就变成多个独立事务。
推荐做法:
- 顶层用例服务持有事务;
- 子模块接收 `Prisma.TransactionClient`
- 子模块不得自行提交与顶层业务重复的事务;
- 明确记录锁顺序、唯一约束和幂等键;
- 对并发路径保留真实 PostgreSQL 验证。
### 3.6 依赖只能单向流动
建议依赖方向:
```text
controller / transport
application use case
domain policy / pure logic
repository and infrastructure adapter
```
禁止新模块之间互相注入形成循环依赖。出现循环依赖时,优先提取共享契约或重新确定用例归属,不用更多 `forwardRef` 掩盖问题。
### 3.7 每个移动都要可追溯
纯移动时尽量保留原代码,不同时重命名、格式化和改写逻辑。推荐顺序:
1. 原样复制到新文件;
2. 原门面改为委托;
3. 测试通过;
4. 再在后续小提交中优化命名或结构。
## 4. 目标目录结构
目标结构不是一次性创建,只有实际拆到对应版本时才新增目录。
### 4.1 前端 API
```text
src/api/
├─ core/
│ ├─ httpClient.ts
│ ├─ requestError.ts
│ ├─ query.ts
│ └─ upload.ts
├─ admin/
│ ├─ auth.api.ts
│ ├─ tenants.api.ts
│ ├─ applications.api.ts
│ ├─ channels.api.ts
│ ├─ riskReview.api.ts
│ ├─ operations.api.ts
│ ├─ reports.api.ts
│ └─ billing.api.ts
├─ client/
│ └─ ...
├─ types/
│ ├─ common.ts
│ ├─ channel.ts
│ ├─ sms.ts
│ ├─ risk.ts
│ └─ operations.ts
└─ adminApi.ts
```
`adminApi.ts` 暂时作为兼容门面重新组合各领域 API,现有页面仍可继续从原路径导入。
### 4.2 NestJS 业务模块
每个现有模块内部优先按用例拆分,不急于创建新的顶级 Nest Module
```text
api/src/operations/
├─ operations.service.ts
├─ queries/
│ ├─ message-query.service.ts
│ ├─ dashboard-query.service.ts
│ ├─ quality-query.service.ts
│ └─ system-log-query.service.ts
├─ downstream/
│ ├─ downstream-delivery-query.service.ts
│ └─ downstream-recovery-query.service.ts
├─ mappers/
└─ types/
```
只有当子域具有独立控制器、生命周期和明确依赖边界时,才升级为独立 Nest Module。
### 4.3 发送链
```text
api/src/send-chain/
├─ send-chain.service.ts # 稳定门面
├─ contracts/
│ ├─ send-command.types.ts
│ ├─ gateway-event.types.ts
│ └─ send-result.types.ts
├─ ingress/
│ ├─ http-ingress.service.ts
│ ├─ cmpp-ingress.service.ts
│ └─ batch-ingress.service.ts
├─ policy/
│ ├─ send-resource.policy.ts
│ ├─ message-classification.policy.ts
│ └─ channel-selection.policy.ts
├─ dispatch/
│ ├─ dispatch.service.ts
│ └─ gateway-submit.publisher.ts
├─ receipts/
│ ├─ submit-result.service.ts
│ ├─ segment-receipt.service.ts
│ └─ receipt-aggregation.service.ts
├─ retry/
│ └─ retry-orchestrator.service.ts
├─ downstream/
│ └─ final-receipt-delivery.service.ts
└─ persistence/
└─ send-chain.repository.ts
```
该结构只是目标边界。不得在一个版本中一次创建并迁移全部目录。
### 4.4 Gateway
```text
gateway/internal/inbound/
├─ server.go # 对外门面和生命周期
├─ auth.go
├─ submit_handler.go
├─ session_registry.go
├─ downstream_delivery.go
├─ acknowledgement.go
├─ recovery.go
└─ protocol_log.go
gateway/internal/upstream/
├─ manager.go # 对外门面
├─ pool.go
├─ connection.go
├─ reconnect.go
├─ submit.go
├─ deliver.go
├─ heartbeat.go
└─ protocol_log.go
```
Go 拆分仍保留同一个 package,第一阶段不跨 package,以免扩大可见性和循环依赖问题。
## 5. 分版本实施路线
### 版本 R0:建立重构安全护栏
本版本不拆生产代码。
工作内容:
1. 固化当前全量测试基线和关键业务路径清单。
2. 为大文件建立职责索引,记录每个公开方法的调用方、数据库表、队列和副作用。
3. 补齐当前缺失的特征测试,优先覆盖发送链并发、通道重连、Gateway ACK 和账号账务。
4. 保存典型 API 响应、Redis Stream 消息和 CMPP 报文样本。
5. 建立每版本固定验收清单和回滚清单。
6. 约定同一重构版本内,大文件只由一个会话负责结构修改。
完成标准:
- 没有业务代码移动;
- 已能用测试回答“拆前行为是什么”;
- 所有后续版本都有可复用的门禁命令。
### 版本 R1:拆分 `adminApi.ts`
这是推荐的第一个实际拆分版本,风险相对可控,且能快速减少多人修改冲突。
> 本地实施状态(2026-07-31):R1.1~R1.5 已完成,尚未提交、推送或部署。
> `src/api/adminApi.ts` 保留为兼容门面,页面 import 未修改;HTTP/会话基础能力、
> 运营端五个业务域、客户端 API 和五组类型文件已经拆开。拆分前的 183 个运营端方法、
> 60 个客户端方法、7 个会话方法及 9 个 HTTP 核心函数由
> `docs/contracts/admin-api-r1-methods.json` 和
> `tools/quality/verify-admin-api-r1.mjs` 固定验证。
工作内容:
1. 先把通用请求、错误处理、文件上传和查询字符串逻辑移动到 `src/api/core/`
2. 把类型按业务域移动到 `src/api/types/`
3. 每次只移动一个领域 API,例如先认证和企业,再通道,再运营统计。
4. `adminApi.ts` 继续汇总并导出同名方法,页面 import 暂时不变。
5. 不修改 URL、HTTP 方法、请求体、返回类型和会话处理。
建议拆成多个小发布:
- R1.1:请求核心与公共类型;
- R1.2:企业、用户、应用;
- R1.3:通道、通道组、报备;
- R1.4:运营统计、日志、短信记录;
- R1.5:审核、风控、账务。
专项验证:
- 登录、401、403重新认证和会话锁定;
- 文件上传与Blob导出;
- 所有页面 TypeScript 构建;
- 运营端关键菜单真实 API 冒烟。
本地实施目录:
```text
src/api/
├─ core/httpClient.ts
├─ admin/
│ ├─ session.api.ts
│ ├─ identity.api.ts
│ ├─ channels-reports.api.ts
│ ├─ operations.api.ts
│ ├─ governance.api.ts
│ └─ files.api.ts
├─ client/client.api.ts
├─ types/
│ ├─ common.ts
│ ├─ identity-config.ts
│ ├─ channels-reports.ts
│ ├─ operations.ts
│ ├─ governance.ts
│ └─ index.ts
└─ adminApi.ts
```
### 版本 R2:拆分运营查询 `operations.service.ts`
先拆读多写少的查询,暂不动发送链副作用。
> 本地实施状态(2026-07-31):R2 已完成,尚未提交、推送或部署。
> `OperationsService` 从2368行缩减为150行兼容门面,29个公开方法签名保持不变;
> 原查询实现按短信记录、上行与监控、看板、质量统计、日志、下游恢复、追踪对账
> 七个领域迁移。`docs/contracts/operations-r2-methods.json` 和
> `tools/quality/verify-operations-r2.mjs` 固定校验方法签名、原方法体、查询契约及辅助函数。
拆分顺序:
1. 短信记录查询和 CSV 导出;
2. 上行短信查询;
3. 运营看板;
4. 发送质量与签名质量统计;
5. 系统日志与导出;
6. 下游投递、恢复状态和详情;
7. 追踪、对账和审计汇总。
保留 `OperationsService` 作为门面。控制器路由、查询参数和响应结构不变。
专项验证:
- PostgreSQL分页总数与当前页一致;
- 北京时间边界和UTC存储解释;
- 运营商兼容值与未识别分类;
- CSV字段、顺序、编码;
- 客户端安全视图不泄漏内部通道信息;
- 大数据量查询计划和索引仍有效。
本地实施目录:
```text
api/src/operations/
├─ operations.service.ts
├─ operations.contracts.ts
├─ operations.helpers.ts
└─ queries/
├─ messages.queries.ts
├─ uplink.queries.ts
├─ dashboard.queries.ts
├─ quality.queries.ts
├─ logs.queries.ts
├─ downstream.queries.ts
└─ trace.queries.ts
```
### 版本 R3:拆分短信配置 `sms-config.service.ts`
按业务对象拆分,而不是按“增删改查”拆分:
> 本地实施状态(2026-07-31):R3 已完成,尚未提交、推送或部署。
> `SmsConfigService` 从2268行缩减为约244行稳定兼容门面,51个公开方法签名保持不变;
> 21个内部方法和原有业务实现迁移到七个领域服务。这里的“第一阶段”是指暂时保留统一门面,
> 不是只迁移部分短信配置业务;控制器和其他模块仍只依赖门面,后续无需再次拆分该主体。
1. 应用配置与接入参数;
2. 应用停用生命周期和下游连接;
3. 签名及报备字段快照;
4. 引流信息;
5. 模板;
6. 审核记录与审核动作;
7. 共享报备资料校验。
第一阶段仍由 `SmsConfigService` 委托各子服务。DTO 从 Service 文件移到独立 contract 文件,控制器不再直接从实现类文件导入 DTO。
本地实施目录:
```text
api/src/sms-config/
├─ sms-config.service.ts
├─ sms-config.contracts.ts
├─ sms-config.helpers.ts
├─ application-config.service.ts
├─ application-lifecycle.service.ts
├─ signature.service.ts
├─ drainage.service.ts
├─ template.service.ts
├─ audit.service.ts
└─ report-validation.service.ts
```
`docs/contracts/sms-config-r3-methods.json`
`tools/quality/verify-sms-config-r3.mjs` 固定校验门面签名、领域归属、
迁移前方法体、DTO/查询契约及共享校验实现。
专项验证:
- 客户端租户边界;
- 应用停用状态机;
- CMPP账号和接入号唯一性;
- 签名、模板、引流审核状态;
- 报备字段快照;
- 操作日志和审核人员;
- 删除治理接口保持不变。
### 版本 R4:拆分报备资料与大页面
后端 `report-materials.service.ts` 按以下边界拆分:
> 本地实施状态(2026-07-31):R4 已完成,尚未提交、推送或部署。
> 后端 `ReportMaterialsService` 从1134行缩减为82行稳定门面,12个公开方法签名保持;
> 11个内部方法迁移到七个领域服务。前端按单版本约束仅拆
> `AdminEnterpriseSignaturesPage.tsx`,从884行缩减为238行页面容器。
> “只选择一个页面”是R4的明确安全边界,不代表后端只拆一部分,也不代表所有大页面已拆完。
1. 官方模板与导出;
2. 导入解析和映射;
3. 暂存与逐行审核;
4. 待生成资料查询;
5. 批次预检与生成;
6. 通道文件导出;
7. 幂等操作记录。
前端只选择一个页面实施,例如先拆 `AdminEnterpriseSignaturesPage.tsx`
- 页面容器负责查询参数和协调;
- 表格列独立;
- 编辑弹窗独立;
- 报备资料弹窗独立;
- API请求仍由页面容器或专用 Hook 统一发起。
不得在同一版本同时拆多个大页面。
本地实施目录:
```text
api/src/report-materials/
├─ report-materials.service.ts
├─ report-materials.contracts.ts
├─ report-materials.helpers.ts
├─ official-export.service.ts
├─ import-parser.service.ts
├─ import-review.service.ts
├─ pending-query.service.ts
├─ batch-generation.service.ts
├─ channel-export.service.ts
└─ batch-operation.service.ts
src/apps/admin/
├─ AdminEnterpriseSignaturesPage.tsx
└─ enterprise-signatures/
├─ signature.types.ts
├─ signature.helpers.tsx
├─ SignatureMaterialFields.tsx
├─ SignatureFormModal.tsx
├─ DrainageFormModal.tsx
├─ SignatureReportModals.tsx
└─ EnterpriseSignaturesTable.tsx
```
`verify-report-materials-r4.mjs`固定后端领域归属和迁移前实现;
`verify-enterprise-signatures-r4.mjs`固定页面移出函数、表格JSX、真实API调用和页面状态。
### 版本 R5:拆分通道服务 `channels.service.ts`
建议边界:
1. 通道配置 CRUD
2. 通道连接状态和重连控制;
3. 通道测试短信;
4. 通道组和路由规则;
5. 报备字段映射;
6. 通道复制;
7. 删除预检和审计。
高风险约束:
- 编辑非连接参数不得触发重连;
- Gateway先连接/断开控制语义不变;
- 不修改真实通道账号、密码和启停状态做测试;
- 测试短信必须单独授权;
- 通道组顺序、权重、主备和补发规则保持一致。
> 本地实施状态(2026-07-31):R5 已完成,尚未提交、推送或部署。
>
> 本轮已完整拆分上述七个边界,不是仅拆一个试点域。原 `ChannelsService`
> 保留为 204 行稳定兼容门面,控制器、模块和既有测试继续依赖同一入口。
> 连接服务统一持有定时器、Redis、Gateway 控制和队列副作用;配置服务仅在
> 连接参数实际变化时委托重连;测试短信继续是独立入口,本轮未调用。
本地结构如下:
```text
api/src/channels/
├─ channels.service.ts # 204行稳定门面
├─ channel-configuration.service.ts # 213行,配置CRUD与状态
├─ channel-connection.service.ts # 612行,连接、重连、Redis和Gateway
├─ channel-test.service.ts # 117行,测试短信
├─ channel-group-routing.service.ts # 221行,通道组和路由规则
├─ channel-reporting.service.ts # 541行,报备字段、任务、回执和记录
├─ channel-copy.service.ts # 101行,通道复制
├─ channel-deletion.service.ts # 19行,删除入口
├─ channels.contracts.ts # 174行,DTO和查询契约
└─ channels.helpers.ts # 685行,共享纯函数、常量和类型
```
`docs/contracts/channels-r5-methods.json`
`tools/quality/verify-channels-r5.mjs` 固定 38 个公开方法、14 个内部方法、
17 个契约及 61 个辅助声明,并专项锁定连接参数重连条件、Gateway
连接/断开路径、定时器、Redis队列、测试短信单次尝试和控制器兼容入口。
### 版本 R6:拆分 Gateway 入站服务
先在 `gateway/internal/inbound` 同一 package 内移动代码:
1. 协议辅助函数和报文转换;
2. 登录认证;
3. 下游会话注册表;
4. Submit处理;
5. 下游Deliver发送;
6. ACK追踪与超时;
7. 恢复扫描;
8. 协议日志。
`Server.ListenAndServe``DisconnectAccount``PushReceiptWithResult``PushUplinkWithResult` 等现有入口保持不变。
专项验证:
- CMPP 2.0/3.0登录;
- 单号码、多号码、长短信;
- SubmitResp只返回一次;
- 原始Msg_Id和重启恢复;
- 下游Deliver ACK
- ACK超时、断线恢复和重复投递幂等;
- `go test ./...``go vet ./...`和本地SMSC集成测试。
> 本地实施状态(2026-07-31):R6 已完成,尚未提交、推送或部署。
>
> 本轮只在 `gateway/internal/inbound` 同一 package 内移动声明,没有修改
> 公开入口、CMPP报文、HTTP回调、Redis恢复锁或队列契约。原1671行
> `server.go`缩减为37行稳定启动入口;迁移前93个声明由契约逐项锁定,
> 迁移后实现哈希全部一致。
本地结构如下:
```text
gateway/internal/inbound/
├─ server.go # 37行,Server与ListenAndServe稳定入口
├─ authentication.go # 127行,CMPP 2.0/2.1/3.0登录认证
├─ submit.go # 333行,Submit、长短信和报文转换
├─ sessions.go # 262行,会话注册、连接状态和断开
├─ delivery.go # 342行,回执与上行Deliver
├─ acknowledgement.go # 187行,ACK追踪、超时和SubmitResp屏障
├─ pending_recovery.go # 281行,待投递刷新和恢复扫描
├─ protocol_log.go # 114行,SubmitResp与Deliver协议日志
└─ transport.go # 84行,共享HTTP回调和规范化辅助
```
`docs/contracts/inbound-r6-declarations.json`
`go run tools/quality/verify-inbound-r6.go` 固定全部93个迁移声明的文件归属
和实现哈希,同时检查四个既有公开入口、控制服务调用关系及12项关键协议
测试仍然存在。复杂并发边界补充了“为什么”注释,未改动实现。
### 版本 R7:拆分 Gateway 上游管理
按状态机拆分:
1. Manager与通道连接池注册;
2. connectionPool
3. connection生命周期;
4. 重连调度;
5. 窗口和心跳;
6. Submit与长短信分片;
7. Deliver、回执和上行;
8. 协议日志与API回调。
保持同一 package,避免同时修改公开接口和队列契约。
专项验证:
- 多连接池;
- 窗口满和TPS限制;
- 网络断开与自动重连;
- 鉴权失败分类;
- 长短信每分片提交结果;
- 迟到回执;
- 上行内容编码;
- ConnectionState汇报。
> 本地实施状态(2026-07-31):R7 已完成,尚未提交、推送或部署。
>
> 本轮只在 `gateway/internal/upstream` 同一 package 内移动声明,没有修改
> Manager公开方法、控制服务调用、CMPP报文、连接参数、重连策略或API回调。
> 原1495行 `manager.go` 缩减为198行稳定管理入口;迁移前68个声明按接收者
> 类型分别建立契约,迁移后实现哈希全部一致。既有153行
> `long_message.go` 已具备单一职责,本轮保持原样。
本地结构如下:
```text
gateway/internal/upstream/
├─ manager.go # 198行,Manager、连接池注册与公开连接入口
├─ pool.go # 121行,连接池成员和生命周期
├─ connection.go # 202行,物理连接、读循环和断线处理
├─ reconnect.go # 171行,重连状态机、退避和错误分类
├─ flow_control.go # 138行,窗口分配、心跳和超时
├─ submit.go # 387行,Submit、分片结果和报文构造
├─ long_message.go # 153行,既有长短信拆分与上行组装
├─ deliver.go # 180行,回执与上行Deliver处理
├─ protocol_log.go # 81行,安全协议日志
└─ transport.go # 105行,ConnectionState与API回调
```
`docs/contracts/upstream-r7-declarations.json`
`go run tools/quality/verify-upstream-r7.go` 固定68个迁移声明的接收者、
文件归属和实现哈希,同时检查Manager稳定入口、控制服务调用关系、
既有长短信模块及15项关键状态机测试仍然存在。
### 版本 R8:发送链第一阶段——抽离纯逻辑
这是发送链正式拆分的准备版本,不先动核心事务。
优先抽离:
1. DTO、事件和队列契约;
2. 状态常量和错误分类;
3. 号码和发送资源判定;
4. 模板、签名、引流分类;
5. 通道候选排序的纯策略;
6. 回执状态映射;
7. 分片聚合计算;
8. 幂等键和事件键生成。
完成后 `SendChainService` 仍负责数据库事务、队列发布和顶层编排。
专项验证:
- 移动前后的输入输出逐例一致;
- 不新增数据库查询;
- 不改变事务范围;
- 不改变日志字段、幂等键和队列消息。
> 本地实施状态(2026-07-31):R8 已完成,尚未提交、推送或部署。
`api/src/send-chain/send-chain.service.ts` 从5345行缩减为4578行,仍保留
98个数据库事务、队列发布、Gateway调用和顶层业务编排方法。24个DTO、
事件和队列契约迁移到 `send-chain.contracts.ts`64个既有常量、状态映射、
号码/资源判定、模板/签名/引流分类和事件键等纯声明迁移到
`send-chain.helpers.ts`
在不增加查询或副作用的前提下,进一步把通道候选选择、通道可发送性、
分片最终状态聚合、上游端点身份比较和回执事件键生成改为显式纯函数。
通道候选策略继续保留数据库既有顺序,并按省内优先、全国兜底选择;
分片聚合继续执行“任一明确失败优先,全部预期分片成功才最终成功”。
控制器继续注入同一个 `SendChainService`,仅把DTO导入切换到独立契约文件。
`docs/contracts/send-chain-r8-pure-logic.json`
`node tools/quality/verify-send-chain-r8.mjs` 锁定24个契约和64个迁移声明的
实现哈希,确认98个编排方法及数据库事务、队列、重试和分片审计副作用仍在
原服务中,并检查4项新增纯策略的委托关系。R9、R10再分别处理入口/提交和
回执/补发/下游投递编排;R8不以单纯降低行数为目标。
### 版本 R9:发送链第二阶段——拆分入口和提交
每次只迁移一个入口:
1. 客户端批量任务入口;
2. HTTP API入口;
3. CMPP入站入口;
4. 审核通过后的继续发送;
5. Gateway提交发布。
`SendChainService` 作为Facade保持控制器和其他模块调用稳定。
专项验证:
- 格式非法和黑名单号码不计频控;
- 任务级风控与号码级风控;
- 每号码独立记录;
- 余额预占、扣费和释放;
- 应用日限额;
- 一次业务短信、长短信分片和补发的计数口径;
- SubmitResp与最终回执的区别。
> 本地实施状态(2026-07-31):R9 已完成,尚未提交、推送或部署。
`SendChainService` 从R8完成时的4578行缩减为2983行,仍是控制器、
Open API、审核中心和其他模块使用的唯一稳定NestJS门面。45个入口和提交
编排方法按五个职责域迁移:
```text
send-submission.service.ts # 305行,内部兼容门面
send-batch-entry.service.ts # 552行,客户端批量、HTTP和导入入口
send-inbound-entry.service.ts # 840行,CMPP认证、长短信聚合和入站提交
send-review-continuation.service.ts # 109行,审核通过/拒绝后的续发
send-scheduled-dispatch.service.ts # 160行,定时任务认领和恢复调度
send-gateway-submit.service.ts # 491行,Worker、路由和Gateway提交发布
```
内部跨方法调用仍返回 `SendChainService` 稳定门面,再由内部兼容门面分派,
以保留既有覆盖点、测试缝和调用可观察性。数据库事务、余额预占、应用日限额、
号码频控、长短信业务条数、通道报备检查、BullMQ和Redis Stream消息体均沿用
原方法体和原调用顺序。
`docs/contracts/send-chain-r9-submission.json`
`node tools/quality/verify-send-chain-r9.mjs` 锁定45个迁移方法的实现哈希、
五个文件归属、双层门面委托和原日志上下文。Gateway提交结果、分片审计、
最终回执、补发抢占、退款及下游投递仍在 `SendChainService`,没有提前进入
R10范围。
### 版本 R10:发送链第三阶段——拆分回执、补发和下游投递
最后处理事故影响最大的部分:
1. Gateway提交结果;
2. 分片提交审计;
3. 分片回执;
4. 最终状态聚合;
5. 失败补发抢占;
6. 退款和预占释放;
7. CMPP/HTTP最终回执投递;
8. 超时扫描。
必须保留:
- 来源提交记录唯一补发关系;
- PostgreSQL事务锁和唯一约束;
- 账务稳定幂等键;
- 每短信唯一最终回执;
- 迟到旧尝试不得覆盖当前尝试;
- 历史事故记录不得删除。
> 本地实施状态(2026-07-31):R10 已完成,尚未提交、推送或部署。
`SendChainService` 从R9完成时的2983行缩减为878行,控制器、Open API、
Gateway事件和其他模块仍只依赖这个包含98个稳定方法的NestJS门面。41个
完成链方法经323行的 `send-completion.service.ts` 内部兼容门面,按事故
边界迁移到七个领域文件:
```text
send-gateway-result.service.ts # 390行,Gateway提交结果与分片提交审计
send-receipt.service.ts # 593行,上游回执收件箱、分片回执和最终聚合
send-retry.service.ts # 359行,失败补发资格、抢占和新尝试创建
send-accounting.service.ts # 142行,扣费、退款和余额预占释放
send-downstream-state.service.ts # 510行,最终回执状态、认领、ACK和恢复状态
send-downstream-delivery.service.ts # 489行,CMPP/HTTP最终回执投递与人工重排
send-timeout.service.ts # 104行,回执超时扫描
```
跨领域调用继续返回 `SendChainService` 稳定门面,保留既有测试缝、调用
可观察性和R9提交域依赖方向。原方法体、事务范围、PostgreSQL锁和唯一约束、
当前尝试判定、分片聚合、稳定账务幂等键、最终回执去重键、下游投递去重与
人工重排键均未改写;七个领域文件中不允许删除历史记录。
`docs/contracts/send-chain-r10-completion.json`
`node tools/quality/verify-send-chain-r10.mjs` 锁定41个迁移方法的实现
哈希、七个领域归属、双层门面委托、98个稳定方法及上述事故不变量。该本地
版本一次完成路线图列出的八项结构迁移,但仍作为单独R10版本验收和发布;
后续不得把新的发送完成链业务重新堆回稳定门面。
### 版本 R11:样式和剩余页面整理
`global.css` 不按固定行数硬切,而按作用域迁移:
1. tokens和reset
2. AppShell与通用布局;
3. 通用表格、表单、弹窗;
4. admin页面域;
5. client页面域;
6. 单页面样式。
迁移时保持入口加载顺序,优先使用CSS Layers或明确的文件顺序,不在同一提交中同时修改选择器权重。
每次只迁移一个页面域并完成:
- 桌面宽屏;
- 1024px
- 768px
- 375px
- 弹窗、长表格、空状态、错误状态;
- 截图对比和控制台检查。
> 本地实施状态(2026-07-31):R11既定九个步骤已全部完成,尚未提交、推送或部署;第六至九步真实登录后页面点击验收因本地浏览器无可复用登录态而待补。9/9表示本轮计划范围完成,不表示剩余所有单页面CSS已被一次性清空。
本版严格执行“每次只迁移一个页面域”,选择剩余页面中最大的
`AdminChannelsPage.tsx`。稳定页面入口从782行缩减为197行,仅保留真实查询、
状态协调和无副作用弹窗编排;类型、API映射、通道表格、编辑弹窗、测试短信
弹窗和连接日志弹窗迁入 `src/apps/admin/channels/` 的聚焦文件。所有调用继续
直接使用真实 `adminApi`,没有引入barrel、mock、静态数据或localStorage。
通道页面专属的列表、质量指标、编辑表单、测试结果和连接日志样式迁入
`AdminChannelsPage.css``global.css` 从12270行缩减为11800行。该步骤结束时
通道组仍使用的 `.channel-confirm` 保留在全局样式,第八步已将其迁入
`admin.css`;移动端连接摘要规则随页面样式迁移。
页面通过入口直接导入该CSS,未改变 `main.tsx` 中tokens、global和components
三层加载顺序,也未在本版修改选择器权重或视觉设计。
`docs/contracts/admin-channels-r11.json`
`node tools/quality/verify-admin-channels-r11.mjs` 锁定稳定入口、五个聚焦
模块、真实API调用、交互入口和页面样式归属。
R11 第二页面域选择 `AdminSmsTaskProgressPage.tsx`,稳定入口从657行缩减为
163行,只协调真实任务查询、筛选选项、选中状态和终止操作。纯任务映射、筛选区、
主表、详情弹窗、真实号码分页弹窗和终止确认拆入
`src/apps/admin/sms-task-progress/`;没有改变任务口径、聚合公式、分页参数、
终止API或弹窗交互。
短信任务详情、运营商卡片和页面移动端规则共273行专属样式迁入
`AdminSmsTaskProgressPage.css``global.css` 从11800行缩减为11527行。
该步骤结束时报备记录、下游记录等页面继续使用的 `.admin-task-filter`
`.admin-task-table-card``.admin-task-id``.admin-task-enterprise`
`.admin-task-card` 仍保留全局;第八步已将这些跨运营页面模式迁入
`admin.css`,避免页面域拆分改变共享视觉。
`docs/contracts/admin-sms-task-progress-r11.json`
`node tools/quality/verify-admin-sms-task-progress-r11.mjs` 锁定稳定入口、
六个聚焦模块、真实任务/号码/终止API边界、交互入口和专属样式归属。
R11 第三页面域选择 `AdminSmsRecordsPage.tsx`,稳定入口从640行缩减为195行,
只协调真实记录分页、筛选项、分片审计、后端CSV导出和选中详情状态。时间/状态/
运营商/路由映射、筛选区、记录卡片与分页、发送详情弹窗拆入
`src/apps/admin/sms-records/`;记录默认日期、提交失败覆盖口径、通道尝试顺序、
接入号拼接和分片审计排序保持原实现。
短信记录列表、状态、详情、通道路由、分片审计和两级响应式规则迁入
`AdminSmsRecordsPage.css``global.css` 从11527行缩减为11098行。该步骤结束时
多个运营页面共用的 `.template-modal-title``.muted``.ui-table__empty`
仍保留全局;第七步已将前两项组件选择器迁入 `components.css``.muted`
继续留在全局,未改变公共弹窗标题或空状态视觉。
`docs/contracts/admin-sms-records-r11.json`
`node tools/quality/verify-admin-sms-records-r11.mjs` 锁定稳定入口、四个聚焦
模块、五个真实API调用、十五项交互入口和页面样式归属。其他大页面与其专属样式
继续留待后续独立小版本,不能在同一实施步骤中批量迁移。
R11 第四页面域选择 `AdminEnterpriseApplicationsPage.tsx`,稳定入口从616行
缩减为273行,只协调真实应用分页、企业选项、筛选状态、应用生命周期和参数详情
请求。筛选区、主表、生命周期弹窗、CMPP/HTTP参数弹窗、连接详情及纯映射拆入
`src/apps/admin/enterprise-applications/`;查询草稿与已应用条件分离、分页、
停用预检、等待/强制停用、启用、删除和参数加载时序保持原实现。
企业应用筛选、连接状态、操作区、连接详情、参数详情和新增应用提示共222行
专属样式迁入 `AdminEnterpriseApplicationsPage.css``global.css` 从11098行
缩减为10881行。该步骤结束时 `.admin-split-filter``.admin-confirm-text`
`.template-modal-title``.section-stack``.form-grid` 等共享样式仍保留
原共享层;第六步已将 `.section-stack` 迁入 `shell.css`,第七步已将
`.template-modal-title``.form-grid` 迁入 `components.css`,其余共享筛选和
确认样式在第八步迁入 `admin.css`。780px筛选单列以及900px和520px连接详情
布局保持原规则。
`docs/contracts/admin-enterprise-applications-r11.json`
`node tools/quality/verify-admin-enterprise-applications-r11.mjs` 锁定稳定入口、
六个聚焦模块、六类真实API边界、十六项交互文案、页面样式归属和筛选状态分层。
R11 第五步进入共享CSS域,但只处理“tokens和reset”。现有97行
`tokens.css`继续作为唯一设计令牌入口,76个颜色、排版、间距、形状、阴影、
布局和组件基础变量不改值、不改名;新建84行`reset.css`,从`global.css`
迁出13组通配符、文档、链接、表单控件、焦点和标题基础规则。`global.css`
从10881行缩减为10801行。
`main.tsx`将基础样式加载顺序显式固定为
`tokens.css → reset.css → global.css → components.css → AppRoutes`,确保打包器
先收集四层基础样式,再进入页面依赖图。本步骤没有迁移
AppShell、通用组件、admin/client共享域、响应式页面规则或任何单页面样式,
避免在同一步骤中改变选择器权重和多类职责。
`docs/contracts/foundation-styles-r11.json`
`node tools/quality/verify-foundation-styles-r11.mjs` 锁定76个令牌、13组reset
规则的声明哈希、reset选择器白名单、原`global.css`所有权清理和四层确定性
加载顺序,并以生产产物位置复核实际拼接顺序。剩余四个共享CSS域步骤继续独立推进。
R11 第六步只迁移 `AppShell` 与通用页面布局基础域。新增811行
`shell.css`,从 `global.css` 迁出侧栏、折叠导航、顶栏、通知/用户菜单、
移动端抽屉与遮罩、减少动画适配,以及 `.page-content``.page-stack`
`.page-heading``.page-heading__actions``.page-actions``.surface`
`.section-stack``.section-heading` 九组通用布局选择器;`global.css`
10801行缩减为10001行。被表单页面复用的 `.icon-button`、通用页签、表格、
表单和弹窗样式继续保留原层,避免提前进入第七步范围。
`main.tsx` 的当前加载顺序固定为
`tokens.css → reset.css → shell.css → global.css → components.css → AppRoutes`
生产产物中tokens、reset、shell、global和components的代表标记依次位于
76、1971、3030、15675和36609,确认新增壳层实际进入正确级联位置。
`docs/contracts/app-shell-styles-r11.json`
`node tools/quality/verify-app-shell-styles-r11.mjs` 锁定118组规则、44个壳层
类、九组布局原语、桌面折叠、780px移动端抽屉和减少动画边界;既有企业应用
契约同步改为确认 `.section-stack``shell.css` 托管。
本步骤代码门禁与全量构建测试通过。后续诊断确认恢复运营会话时返回500并非
AppShell拆分缺陷:当时本地PostgreSQL未监听,而API和Redis仍正常;有效Redis
会话进入用户查询后,Prisma连接数据库被拒绝,Nest因而返回500。恢复本地
PostgreSQL后,真实Prisma用户查询、API health和无会话401边界均恢复;浏览器
仍无可复用运营端登录态,Chrome亦无已登录运营端标签。遵守验证码和真实会话
边界,没有绕过登录或伪造状态,因此桌面展开/折叠、移动端抽屉和通知/用户菜单
的真实点击验收仍需在可用登录态下补做。
R11 第七步只迁移“通用表格、表单和弹窗”。从 `global.css` 迁出52组规则、
57个选择器,包括 `.ui-table*``.ui-modal*``.form-grid*``.radio-row`
`.table-actions`、表格文本辅助类、`.icon-button``.ui-tabs__tab`、XL弹窗和
780px移动端卡片/表单/弹窗规则;`global.css` 从10001行缩减为9660行,
`components.css` 从1333行增加为1694行。页面域复合选择器仍留在原页面或全局
层,没有顺带迁移admin/client业务样式,也没有改React组件、API、文案或数据。
为保持既有级联,旧兼容规则放在 `components.css` 的现有规范组件规则之前,
响应式规则放在对应组件规则之后;`main.tsx`
`tokens → reset → shell → global → components → AppRoutes` 加载顺序不变。
新增 `docs/contracts/shared-components-r11.json`
`node tools/quality/verify-shared-components-r11.mjs`,锁定259组规则、
291个选择器、14组通用类族、桌面/780px所有权和Button、Input、Select、Table、
Modal真实组件绑定;短信记录和企业应用旧契约同步改为校验组件层所有权。
第七步的结构门禁、生产构建和全量后端/Gateway门禁均通过。浏览器确认数据库
恢复后未登录访问不再出现500,而是正常跳转登录页且控制台无warning/error
由于应用内浏览器和Edge均无可复用登录态,未求解验证码,登录后桌面/移动端
表格、弹窗及第六步AppShell交互仍按用例保留待补。第八步只能继续admin共享域,
不得顺带迁移client域或单页面样式。
R11 第八步只迁移运营端跨页面共享样式。以“至少被两个admin源码文件复用且
client源码不依赖”为主要判定,再按完整样式族补齐同一模式的变体和响应式规则。
新增678行 `admin.css`,从 `global.css` 迁出117组规则、141个选择器;
`global.css` 从9660行缩减为9056行。迁移范围包括审核筛选、任务/报备记录、
黑名单与敏感词安全页、系统管理、三类统计筛选、跨页面确认与表单提示、
下游明细、通道字段配置等运营端共享模式。
两个带单页面上下文的覆盖规则
`.report-task-detail .admin-task-card`
`.gateway-exception-page .report-task-table-card > .ui-pagination`
继续留在 `global.css`,避免把单页面特例误归为共享样式。通用 `ui-*` 只在
admin所有者选择器的后代上下文中出现;客户端源码不引用admin所有权类。
本步骤没有迁移client域、单页面业务样式,也没有修改React组件、API或数据。
`main.tsx` 的确定性顺序更新为
`tokens → reset → shell → global → admin → components → AppRoutes`
新增 `docs/contracts/admin-shared-styles-r11.json`
`node tools/quality/verify-admin-shared-styles-r11.mjs`,锁定117组规则、
141个选择器、11项跨页面复用下限、admin/client所有权边界以及780px/360px
响应式规则;通道、企业应用和短信任务进度旧契约同步改为校验 `admin.css`
归属。生产构建、API 29套/389项测试、Prisma、Gateway测试/vet、安全及全部
结构门禁通过。浏览器运行时确认编译CSS包含admin任务与统计规则且控制台无
warning/error,但仍无可复用运营端登录态,因此登录后代表页面验收继续待补。
第九步只处理client共享样式域,不得回收本步骤保留的单页面覆盖规则。
R11 第九步只处理client跨页面共享样式。静态引用盘点发现,真正满足“至少被
两个client源码文件复用且admin源码完全不依赖”的全局类只有 `.eyebrow`
由客户端首页和账户账单共同使用。因此新增8行 `client.css`,迁移这一组声明,
`global.css` 从9056行缩减为9049行;页面上下文覆盖
`.overview-hero .eyebrow` 继续留在global。
`.sms-send-title``.system-page-toolbar``.system-table-card` 虽被多个
客户端页面使用,但运营端系统日志页也真实依赖,继续作为跨门户兼容样式留在
global,不能错误归入client。`client-signature-*`、发送页、企业认证、发送
详情和模板卡片等样式当前均为单页面所有权,也没有为了“显得拆得多”而批量搬迁。
这些保留边界由新契约锁定,后续只能在对应单页面小版本中处理。
`main.tsx` 最终确定性顺序为
`tokens → reset → shell → global → admin → client → components → AppRoutes`
新增 `docs/contracts/client-shared-styles-r11.json`
`node tools/quality/verify-client-shared-styles-r11.mjs`,锁定client唯一所有权、
两页面使用下限、三组跨门户兼容边界和五类单页面保留边界。R11既定九步至此完成;
生产构建、API 29套/389项测试、Prisma、Gateway测试/vet、安全及全部18个结构
门禁通过。浏览器运行时确认`.eyebrow`、首页上下文覆盖和跨门户标题规则均进入
生产CSS,375px登录页无横向溢出且控制台无warning/error;因无客户端登录态,
登录后首页/账单视觉验收继续待补。后续如继续拆单页面CSS,应建立新的小版本
编号,而不是扩大R11第九步。
## 6. 每个拆分版本的固定执行步骤
### 6.1 开始前
1. 执行 `git status --short --branch`
2. 执行 `git diff`
3. 执行 `git fetch`
4. 分别确认 `HEAD``origin/main`
5. 完整阅读 `docs/testing-progress.md` 最新记录。
6. 识别并保护其他会话的未提交修改和未跟踪文件。
7. 明确本版本唯一拆分边界、负责人和禁止触碰的文件。
8. 记录拆分前全量测试结果。
### 6.2 实施中
1. 先补特征测试。
2. 只移动代码,不改业务。
3. 保留原门面和公开签名。
4. 每完成一个委托点立即运行定向测试和类型检查。
5. 检查数据库事务、锁、幂等键和查询次数。
6. 检查是否形成循环依赖。
7. 给复杂并发、事务和协议边界添加解释“为什么”的注释。
8. 如果修改范围超过原计划,先停止并重新评估,不顺手扩大重构。
### 6.3 发布前门禁
按模块风险选择,但完整发布至少包括:
- `git diff --check`
- Prisma format、validate、generate
- API TypeScript正式构建;
- API全量测试;
- 前端TypeScript和Vite生产构建;
- Gateway `go test ./...`
- Gateway `go vet ./...`
- 依赖安全门禁;
- 真实PostgreSQL和真实API关键路径;
- 对应页面登录后视觉和交互;
- Redis Stream pending/lag
- 不发送真实短信的无副作用验证。
### 6.4 发布后
1. 只使用标准部署脚本和精确Git提交。
2. 发布前备份PostgreSQL、运行源码和环境文件。
3. Gateway先于API重启。
4. 检查服务、端口、health、Redis、Stream和错误日志。
5. 对本次拆分的业务域进行真实只读或安全写入验证。
6. 至少观察一个稳定窗口,再开始下一拆分版本。
7.`docs/testing-progress.md` 记录实际结果和未完成项。
## 7. 回滚策略
拆分版本必须做到:
- 不包含无关需求;
- 不包含不可逆数据迁移;
- 原门面仍存在;
- 新旧模块之间只有清晰委托关系;
- 可以通过回滚该版本Git提交恢复原实现。
如果拆分确实需要migration,应只做向前兼容的新增,先部署兼容代码,再迁移读取,最后在更晚版本清理旧结构。禁止在同一版本删除旧字段或历史数据。
触发回滚的条件包括:
- API响应结构变化;
- 数据库写入数量或事务边界变化;
- 余额、退款、频控、补发或回执出现重复;
- Redis Stream pending或lag异常增长;
- Gateway连接、ACK或重连行为变化;
- error日志明显增加;
- 页面关键流程无法完成;
- 无法在短时间内解释差异来自何处。
## 8. 文件大小和依赖健康目标
这些是方向性目标,不作为机械验收条件:
- 普通业务Service建议控制在300至600行;
- Facade建议控制在100至300行;
- 单一React页面建议控制在300至500行;
- 单个测试文件建议控制在800行以内,并按业务域拆分;
- 单个文件不应同时包含DTO、数据库查询、状态机、外部调用和页面展示五类职责;
- 新业务应直接进入对应子模块,不再回填到旧大文件。
如果拆分后文件虽小但依赖更多、事务更散、调用链更长,则视为失败拆分。
## 9. 不建议采用的做法
1. 一次性重写整个大文件。
2. 一边拆分一边更换框架、状态库或ORM。
3. 同时重命名大量方法和字段。
4. 仅依靠TypeScript编译通过判断行为没变。
5. 先删除旧门面,再批量修改所有调用方。
6. 为了减少行数把代码拆成大量无业务含义的 `utils.ts`
7. 把事务拆到多个Service后分别提交。
8. 用Mock通过替代真实数据库、Redis、Gateway和页面验证。
9. 多个会话同时修改同一个核心文件。
10. 在发送链或Gateway重构版本顺手加入新功能。
## 10. 推荐起步顺序
实际实施时推荐从以下三个版本开始:
1. **R0 安全护栏**:没有生产代码移动,先补特征测试和职责索引。
2. **R1.1 前端请求核心**:只抽 `adminApi.ts` 的HTTP、错误和上传基础能力,保留全部原导出。
3. **R1.2 企业与用户API**:选择低副作用领域验证兼容门面模式。
完成并稳定发布后,再进入 `OperationsService`。发送链和Gateway不要作为第一个拆分试点。
## 11. 每个版本的决策记录模板
```markdown
### 拆分版本
- 目标文件:
- 本次唯一业务域:
- 明确不改的行为:
- 原公开入口:
- 新模块:
- 数据库表:
- 事务和锁:
- Redis/队列/Gateway副作用:
- 特征测试:
- 真实后端验证:
- 页面验证:
- 发布观察指标:
- 回滚提交:
- 遗留项:
```
每次开始新拆分版本时,在本路线图基础上另写该版本的短实施计划,不直接把路线图当作可执行变更清单。