Files
lislgosms/docs/code-quality-continuous-optimization-plan-20260828.md

465 lines
19 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.
# CMPP 平台代码质量持续优化整改方案
- 方案日期:2026-08-28
- 依据报告:`docs/code-quality-reassessment-20260828-v2.md`
- 当前代码基线:`3af145abe5eb8cae567bb18e29116b4940889258`
- 适用范围:React/Vite 客户端、NestJS API、依赖治理、测试与工程门禁
- 实施边界:先在本地完成代码和自动化验证;如需部署,只允许部署到测试环境 `100.93.204.60`,且部署前必须重新建立并校验恢复资产
- 禁止范围:未经新的明确授权,不得访问、部署、覆盖或回退预生产环境
## 1. 背景与当前结论
V2 复评确认上一轮两个 P0 已关闭,代码质量从 58 分提升至 82 分。当前不存在新的运行态 P0,但仍有以下持续优化空间:
1. 少量客户端写接口仍使用内联 TypeScript 类型,缺少运行时严格校验。
2. 前端没有自动化测试,登录、权限、懒加载失败和页面异常状态主要依赖人工验证。
3. API 全源覆盖率刚刚超过门槛,余量不足。
4. 客户端仍发送 `x-tenant-id` 并保留默认租户回退,与服务端可信会话租户的最终设计不一致。
5. 登录保护只有账号维度的主要限流,需要补充 IP 和验证码请求维度。
6. API 依赖树存在审计公告,需要按真实依赖路径和运行时可利用面逐项治理。
7. 当前 `lint` 不是完整 ESLint,格式检查也只依赖 `git diff --check`
8. 两个 SendChain Service 仍然过大,但直接拆分具有较高业务回归风险。
9. 工作区存在双锁文件口径,可能继续误导安装和依赖审计。
本方案遵循“先补小范围安全缺口和测试,再治理供应链和工程门禁,最后拆分发送链”的顺序。
## 2. 整改目标
本轮持续优化完成后应达到:
- 所有客户端写接口具有运行时 DTO 校验,不再使用裸内联 body 类型。
- 客户端租户授权完全依赖服务端认证会话,浏览器不再负责选择或回退租户。
- 建立可持续运行的前端测试框架,并覆盖核心登录、权限、异步路由和异常状态。
- API 全源覆盖率形成合理缓冲,新增代码不能依靠全局低门槛掩盖未测试分支。
- 登录防护具备账号、IP、IP+账号和验证码请求多维度限制与指标。
- API 依赖公告具有逐项路径、影响、处理方式和回归证据。
- `lint``format:check` 成为真实、稳定、可逐步扩展的工程门禁。
- SendChain 重构有行为锁定测试,拆分过程中不改变事务、锁、幂等和队列语义。
- 仓库只保留一种正式包管理器和一种正式锁文件。
## 3. 优先级和最小充分范围
| 批次 | 内容 | 风险 | 预估工作量 | 是否建议下一轮立即实施 |
|---|---|---:|---:|---|
| C1 | 剩余客户端 DTO 和负向测试 | 低 | 0.51.5 人日 | 是 |
| C2 | 删除客户端租户头和默认租户回退 | 中低 | 0.5~1 人日 | 是 |
| C3 | 最小前端测试框架和核心用例 | 中 | 1~3 人日 | 是 |
| C4 | 覆盖率缓冲和增量覆盖门禁 | 低 | 0.5~1 人日 | 是,依赖 C1/C3 |
| C5 | IP/账号/验证码多维登录保护 | 中 | 1~2 人日 | 建议 |
| C6 | API 依赖公告治理矩阵 | 中 | 1~3 人日 | 建议先诊断后升级 |
| C7 | ESLint、Prettier 和真实格式门禁 | 中 | 1~2 人日 | 建议分阶段启用 |
| C8 | SendChain Service 职责拆分 | 高 | 510 人日 | 单独专项 |
| C9 | 唯一包管理器和锁文件 | 中 | 0.5~1 人日 | 需先确认文件归属 |
下一轮最小充分范围为 C1~C4。它们投入较小,能够实质关闭 `CQ-API-001` 的客户端部分并改善 `CQ-TEST-001`,不需要数据库迁移,也不需要修改发送链业务规则。
## 4. C1:补齐客户端写接口 DTO
### 4.1 当前缺口
首批至少覆盖:
- `api/src/open-api/client-open-api.controller.ts`
- 创建 HTTP API 凭据。
- 新增或更新 Webhook。
- `api/src/operations/client-operations.controller.ts`
- 系统日志导出。
- `api/src/files/client-files.controller.ts`
- Multipart 上传的 `purpose``prefix`
### 4.2 实施要求
1. 新增独立 DTO class,不再使用运行时会被擦除的内联 TypeScript 类型。
2. 对上述接口启用现有 `strictValidationPipe`
3. DTO 规则至少包含:
- 凭据名称长度和空白处理。
- `expiresAt` 使用 ISO 日期时间校验,并拒绝已过期时间。
- Webhook `eventType` 只允许平台支持的事件。
- Webhook URL 限制协议、长度和格式;继续由 Service 执行 SSRF 与 HTTPS 策略。
- Webhook `status` 使用明确枚举。
- 日志级别、模块、日期区间使用明确格式和长度限制。
- 上传 `purpose` 使用白名单;`prefix` 限制长度、字符集和路径穿越。
4. 将当前过宽的 `ClientStatusChangeDto` 按应用、签名、模板和引流信息拆成更窄的 DTO,避免无关字段被不同接口接受。
5.`scheduledAt``requestedAt` 等时间字段使用日期格式校验。
6.`variables``materials``reportValues` 等对象补充:
- 最大键数量。
- 最大嵌套深度。
- 键名和字符串值长度。
- 禁止原型污染相关键。
### 4.3 自动化测试
每个接口至少覆盖:
- 合法最小请求成功。
- 额外字段返回 400。
- 超长字符串返回 400。
- 非法 URL、日期和状态返回 400。
- 客户端提交 `createdById/operatorId/tenantId` 等身份字段不能覆盖服务端身份。
- 校验失败时 Service 不被调用。
- 文件 purpose/prefix 校验失败时 MinIO 不发生写入。
### 4.4 出口标准
- 客户端 POST/PUT/PATCH 中不再存在未评审的裸 `@Body()` 内联类型。
- 对应控制器和 DTO 负向测试通过。
- 错误请求稳定返回 400,不返回 500。
- 不改变现有合法前端请求协议。
## 5. C2:移除客户端租户头和默认租户回退
### 5.1 目标设计
客户端租户范围只能来自服务端认证会话:
```text
客户端请求
-> session cookie
-> 服务端查询会话用户
-> request.sessionTenantId
-> CurrentTenantId
-> 数据库 tenantId 复合条件
```
浏览器不再通过 localStorage、默认常量或 `x-tenant-id` 参与客户端授权。
### 5.2 实施内容
1. 修改客户端 HTTP 请求封装,客户端路由默认不发送 `x-tenant-id`
2. 删除 `getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID` 的客户端回退路径。
3. 清理客户端 API 方法中仅为请求头服务的 `tenantId` 参数。
4. 管理端明确跨企业查询所需的租户参数继续保留,并与客户端请求封装分离。
5. 服务端暂时保留“收到客户端租户头时做一致性检查”的兼容逻辑,经过一个测试版本确认没有旧客户端后再决定移除。
6. 质量门禁增加:
- `src/api/client/**` 不得设置 `x-tenant-id`
- 客户端 API 不得引用 `DEFAULT_CLIENT_TENANT_ID`
- 管理端的显式租户参数不受此规则影响。
### 5.3 必测用例
- 不带租户头的客户端全部核心页面和接口正常。
- 浏览器 localStorage 中 tenantId 缺失、错误或过期,不影响服务端租户识别。
- 运营端跨租户查询仍按权限正常工作。
- 旧客户端携带正确租户头仍兼容;错误租户头仍返回 403 并记录安全事件。
- 双租户真实 API 隔离矩阵继续通过。
### 5.4 出口标准
- 客户端代码不再选择租户授权范围。
- 不出现登录后初始化阶段因默认租户错误导致的 403。
- 服务端可信租户门禁继续通过。
## 6. C3:建立最小前端测试集
### 6.1 技术方案
建议采用:
- Vitest:测试运行器。
- React Testing Library:组件和页面行为测试。
- jsdomDOM 环境。
- MSW:模拟真实 HTTP 协议的成功、空、失败和延迟响应。
测试必须围绕用户可见行为和真实请求契约,不以大量快照替代断言。
### 6.2 首批核心用例
1. 客户端登录:成功、密码错误、验证码错误、接口不可用。
2. 未登录访问客户端深层路由:跳转登录页。
3. 权限不足:显示稳定提示,不白屏。
4. `RouteLoadBoundary`:异步 chunk 加载失败、重试和重新加载。
5. 列表页:loading、empty、error、成功、分页和筛选。
6. 高风险操作:删除、重置密钥、创建凭据等确认流程。
7. API 返回 500:页面显示业务化错误,不直接显示裸 `Internal server error`
8. 会话失效:401 后清理展示会话并回登录页。
### 6.3 工程门禁
新增命令建议:
```text
npm run test:frontend
npm run test:frontend:coverage
```
并将 `test:frontend` 纳入 `verify:quality`。首轮覆盖率门槛以核心模块为主,不为追求全局数字测试纯展示组件。
### 6.4 出口标准
- 至少 6~10 个核心行为用例稳定通过。
- 登录、权限、路由失败和列表异常状态均有自动化覆盖。
- 测试可以在全新依赖安装后重复执行。
- CI/质量门禁中前端测试失败会阻止合并。
## 7. C4:覆盖率缓冲和增量门禁
### 7.1 当前问题
当前全源覆盖率虽然通过,但接近门槛:
- statements 59.84%,门槛 59%。
- branches 51.29%,门槛 50%。
- functions 60.15%,门槛 60%。
- lines 62.60%,门槛 62%。
functions 仅有 0.15 个百分点余量,新增少量未测试函数就可能失败。
### 7.2 实施内容
1. 先通过 C1 和 C3 增加有业务价值的测试,不立即盲目提高门槛。
2. 建立修改文件或新增文件覆盖率门禁,建议初始目标:
- statements/lines 不低于 80%。
- branches 不低于 70%。
- functions 不低于 80%。
3. 全源门槛在覆盖率稳定后分批上调,每次不超过 2~3 个百分点。
4. 覆盖率报告明确使用全源 `collectCoverageFrom` 口径,避免再次混用“已加载源码”和“全部源码”。
### 7.3 出口标准
- 新增 DTO、校验器和安全逻辑具有负向分支测试。
- 修改文件覆盖率门禁能够阻止新增无测试逻辑。
- 文档和质量门禁使用同一统计口径。
## 8. C5:多维登录和验证码保护
### 8.1 实施内容
在现有 Redis 原子计数基础上增加:
- 账号维度失败窗口。
- 来源 IP 维度失败窗口。
- IP+账号组合维度。
- 验证码获取频率限制。
- 单 IP 随机账号扫描保护。
- key 数量、失败次数、锁定次数和 Redis 异常指标。
来源 IP 必须基于受信 Nginx/代理链解析,不能直接信任任意客户端 `X-Forwarded-For`
### 8.2 验证
- 多 API 实例共享相同锁定状态。
- 同账号换 IP、同 IP 换账号均能触发对应保护。
- 正常用户偶发输错不会被过度锁定。
- Redis 不可用继续 fail closed,并产生监控告警。
- 随机账号和验证码请求压测后 Redis key 数量能随 TTL 回落。
## 9. C6API 依赖公告治理
### 9.1 原则
依赖审计公告不直接等于业务可利用漏洞。不得直接执行 `npm audit fix --force`。应先建立矩阵:
| 字段 | 内容 |
|---|---|
| 公告编号 | GHSA/CVE |
| 依赖路径 | 直接依赖到受影响包的完整路径 |
| 实际锁定版本 | 以 `api/package-lock.json` 为准 |
| 环境 | 生产运行时、构建期、开发期、可选工具 |
| 不可信输入 | 是否处理客户输入、文件、URL、模板或命令行参数 |
| 当前缓解 | override、功能未启用、输入边界、兼容适配 |
| 修复方案 | 补丁升级、依赖替换、隔离或书面风险接受 |
| 回归范围 | 构建、Excel 导出、Swagger、Prisma、数据库和部署 |
### 9.2 优先顺序
1. 直接生产运行时且处理不可信输入的路径。
2. Excel 导出、压缩、YAML/Swagger 等数据处理路径。
3. Prisma 的生产客户端路径。
4. 仅 CLI、Studio、构建或可选工具链路径。
### 9.3 出口标准
- 7 项公告均有明确依赖路径和处置结论。
- 可安全升级的依赖完成升级并通过全量回归。
- 暂不能升级的项目有代码级缓解门禁和明确复查日期。
- 根项目和 API 子项目审计口径分别记录,不互相替代。
## 10. C7:真实 lint 和格式门禁
### 10.1 分阶段实施
第一阶段只检查新增和修改文件,启用高价值规则:
- 未使用变量和导入。
- 未处理 Promise。
- 不安全的 `any` 和类型断言。
- React Hooks 依赖和调用规则。
- 无效条件、重复分支和不可达代码。
- Node/NestJS 常见异步错误。
第二阶段再逐目录修复历史问题并扩大到全仓库。
Prettier 首轮只执行 `check`,不得在同一提交中格式化全部历史文件。大规模格式化必须独立提交,避免掩盖业务改动。
### 10.2 脚本语义
- `lint`:真实 ESLint。
- `typecheck`:前端 TypeScript。
- `quality:verify`:结构安全门禁。
- `format:check`Prettier check + `git diff --check`
不要继续用 `lint` 名称包装结构检查和 TypeScript,使开发者误判实际门禁能力。
## 11. C8SendChain 超大服务专项拆分
### 11.1 前置条件
拆分前必须补齐行为锁定测试:
- 事务边界。
- advisory lock 和锁顺序。
- 消息、Submit 和 Outbox 幂等键。
- 主备路由和补发。
- 计费冻结、扣费、退款和释放。
- Inbox claim、租约、超时和恢复。
- Redis Stream 发布及重复消费。
- 回执、上行和下游投递。
### 11.2 建议拆分方向
`send-inbound-entry.service.ts`
- 入站协议数据转换。
- Inbox 持久化和 claim。
- 长短信聚合。
- 企业分组微批。
- 业务校验和消息创建。
`send-gateway-submit.service.ts`
- 路由选择。
- Gateway 命令构造。
- Submit Outbox。
- Submit 结果处理。
- 重试和恢复。
### 11.3 实施规则
- 每次只移动一个职责。
- 先机械提取,再优化逻辑。
- 不同时修改事务、SQL、幂等键或状态语义。
- 每个拆分提交都运行 API 全量、Gateway 全量、真实 PostgreSQL/Redis 契约和队列排空检查。
### 11.4 出口标准
- 原 Service 只保留编排职责。
- 行为测试、全链契约和性能基线不下降。
- 不新增重复查询、跨事务状态漂移或锁顺序变化。
## 12. C9:唯一包管理器和锁文件
当前正式跟踪文件为根目录和 API 的 `package-lock.json`,工作区另有未跟踪 `pnpm-lock.yaml`。该文件属于受保护的既有工作区内容,本方案不授权删除。
需要文件所有者明确选择:
### 方案 A:统一 npm(建议)
- 保留两个正式 `package-lock.json`
- 确认后删除或归档旧 `pnpm-lock.yaml`
- 部署、审计和 CI 全部使用 `npm ci`
- 门禁禁止新增 pnpm/yarn 锁文件。
### 方案 B:统一 pnpm
- 重新生成受控 pnpm 锁文件。
- 重写本地、CI、部署和审计命令。
- 在全新目录验证安装、构建、Prisma、测试和生产部署。
- 完成后再移除 npm 锁文件。
不得长期同时维护两种锁文件,也不得只改锁文件而不改部署流程。
## 13. 不建议在下一轮实施的动作
- 不一次性为全部 API 开启全局严格 `ValidationPipe`
- 不直接执行 `npm audit fix --force`
- 不为了 Vite 的 500 KiB 原始体积警告替换整个 ECharts;当前 gzip 包体仍在预算内。
- 不在安全补丁提交中同时拆分 SendChain。
- 不以大量快照测试提高前端覆盖率。
- 不擅自删除 `=`, `pnpm-lock.yaml` 或其他侧边任务文件。
- 不把测试环境健康检查等同于登录后页面和 CMPP 全链验收。
## 14. 验证矩阵
| 层级 | 必须验证 |
|---|---|
| 静态 | TypeScript、ESLint、Prettier、结构安全门禁、`git diff --check` |
| API | DTO 正负向、租户隔离、密码迁移、登录限流、账务和发送幂等 |
| 前端 | 登录、权限、懒加载、loading/empty/error、确认操作、401/500 |
| 数据库 | Prisma validate、95 项 migration 一致性、真实 PostgreSQL 关键查询 |
| Redis | 验证码 TTL、登录锁定、多实例、Stream pending/lag |
| Gateway | `go test ./... -count=1``go vet ./...`、5 份队列契约 |
| 浏览器 | 登录后页面、控制台、请求、刷新、深层链接和移动端 |
| 发布后 | 部署 commit、服务、端口、健康、日志、队列和临时数据清理 |
## 15. 测试环境发布要求
如下一轮包含部署,必须:
1. 明确目标是测试环境 `100.93.204.60`,显式绕过 Clash/系统代理进行健康探测。
2. 发布前重新建立:
- PostgreSQL custom dump。
- 当前运行目录归档。
- 环境、systemd 和 Nginx 配置。
- Redis RDB 和关键 Stream/队列状态。
- MinIO 数据或业务对象清单。
- 当前部署 commit 和 95 项 migration 状态。
3. 校验 `pg_restore --list`、tar 目录、Redis RDB 和 SHA-256。
4. 发布后核对所有相关服务、API/Gateway health、Redis pending/lag 和 error 日志。
5. 清理临时测试账号、凭据、文件和脚本。
6. 测试环境通过不自动授权预生产发布。
## 16. 建议提交拆分
1. `security: validate remaining client write payloads`
2. `test: cover client dto rejection paths`
3. `security: remove client-controlled tenant headers`
4. `test: add frontend auth and route failure baseline`
5. `test: enforce changed-file coverage`
6. `security: add ip-aware login throttling`
7. `build: document and remediate api audit paths`
8. `build: add eslint and prettier checks`
9. `refactor: extract inbound workflow responsibilities`
10. `refactor: extract gateway submit responsibilities`
11. `chore: enforce the selected package manager`
每个提交只处理一个主题,不混入测试环境部署资产、历史文档改写或其他会话的工作区文件。
## 17. 最终关闭标准
| 项目 | 关闭证据 |
|---|---|
| 客户端 DTO | 所有写接口使用 class DTO 和严格管道;负向测试通过 |
| 租户头清理 | 客户端不再发送/回退 tenantId;双租户矩阵通过 |
| 前端测试 | 核心用例进入质量门禁并稳定运行 |
| 覆盖率 | 全源口径稳定且修改文件满足增量门槛 |
| 登录保护 | 账号/IP/组合/验证码限流和指标通过多实例测试 |
| 依赖公告 | 每项有路径、影响、处理和回归证据 |
| lint/format | ESLint 和 Prettier 真实执行,不再仅使用脚本别名 |
| SendChain | 行为锁定后完成职责拆分,全链语义和性能不下降 |
| 锁文件 | 唯一包管理器、唯一正式锁文件、全新安装可重复 |
| 测试环境 | 恢复资产、真实后端、浏览器和发布后状态证据齐全 |
## 18. 建议执行顺序
```text
C1 客户端 DTO
-> C2 租户头清理
-> C3 前端测试
-> C4 覆盖率缓冲
-> C5 登录保护
-> C6 依赖治理
-> C7 lint/format
-> C8 SendChain 专项拆分
-> C9 锁文件统一
-> 测试环境完整验收
```
其中 C1~C4 可作为下一轮独立交付;C8 必须保持为单独的高风险重构任务。
## 19. 2026-08-28 执行补充
- 文件所有者已明确要求完成其余未关闭项,并确认不接受任何 npm 或依赖降级。
- C9 继续采用既有 npm 10.9.4 基线,没有执行 npm、Node、Prisma或业务依赖降级;旧的未跟踪 `pnpm-lock.yaml` 已移除,正式安装仍使用根目录和API目录各自的 `package-lock.json``npm ci`
- C8 本轮只形成专项拆分方案 `docs/sendchain-service-decomposition-plan-20260828.md`,没有实施高风险发送链代码重构。
- 登录后页面由用户按 `docs/code-quality-browser-acceptance-checklist-20260828.md` 人工验收,结果回填前不得标记浏览器验收关闭。