docs: 互换模块长期改进方案——从106次提交47%修复率到可预测交付

This commit is contained in:
hjhan
2026-07-29 22:13:28 +08:00
parent ffa51c6ae6
commit daaf993126
@@ -0,0 +1,301 @@
# 互换模块长期改进方案——从"一个需求改一周"到"可预测交付"
> 基于 EQD-6838(债券三字段互算)从 0aad4c80 到 ffa51c6a 共 106 次提交、
> 7 天开发周期、50 次 fix/revert(47%)的惨痛教训,提炼出的长远改进路线。
> 不是一次性重构方案,而是"每个迭代都往前走一步"的渐进式改善。
---
## 一、数据事实:到底有多痛
| 指标 | 数值 | 含义 |
|------|------|------|
| 开发周期 | 7 天 | 一个"小需求"从开始到可用 |
| 提交总数 | 106 次 | 日均 15 次提交 |
| 修复/回退/调试 | 50 次(47%) | 近一半提交是在修自己引入的 bug |
| Revert | 10 次 | 10 次回退(含精度调整批量回退 6 次) |
| Merge | 21 次 | 7 天内 21 次合并,多人多分支并行冲突频繁 |
| Bundle/构建 | 8 次 | 8 次提交只为解决"改了源文件没生效" |
| console.log 调试 | 是 | 不得不靠打印日志+部署来试错 |
**对比行业基准**:成熟团队的 fix 提交占比通常在 10-20%。47% 意味着每写 2 行代码就要花 1 行来修 bug。
---
## 二、根因分析:为什么会这样
### 2.1 架构层面:jQuery 与 Vue 的"伪混用"
这是**所有踩坑的总根源**。项目有 67 个 `new Vue(...)` 组件,但它们不是真正的 Vue 组件——它们是 jQuery 代码包了个 Vue 壳:
```
当前架构(问题根源):
cshtml (Razor)
↓ 引入 bundle.js (801KB,包含 lodash/jquery/vue/fastVue)
↓ 引入 swapTradeEdit.js (独立 script1963 行)
↓ new Vue({ methods: { ... jQuery 逻辑 ... } })
↓ vue-number-input 组件 = jQuery.numberInput 包了个 Vue template
真正的问题:
- jQuery 事件系统覆盖 Vue 事件代理 → .native 失效
- jQuery $() 操作 DOM 绕过 Vue 响应式 → 视图不更新
- jQuery Deferred 与 Vue 生命周期不同步 → 回调时机不可控
- 组件没有 props 验证 / 没有模板编译 → 运行时才发现错误
```
**这不是"Vue2 不行"**,而是 Vue 的响应式契约被 jQuery 破坏了。纯 Vue2 原生组件不会出现这些问题。
### 2.2 构建层面:Bundle 是手工艺品
```
当前流程:
改源文件 → 手动跑 rebuild-bundles.py → 提交 bundle.js → dotnet publish → 部署
问题链:
① 忘记重新打 bundle → 部署的是旧代码
② BOM/换行符不一致 → bundle 产物有差异
③ Linux 路径大小写敏感 → CI 构建失败
④ JsVersion 缓存键不刷新 → 浏览器加载旧缓存
⑤ bundle.js 801KB 被提交到 git → 每次改动产生巨大 diff
```
一个现代前端构建工具(Vite/Webpack/esbuild)能在 200ms 内完成同样的工作,且不需要提交产物到 git。
### 2.3 测试层面:纯函数绿 ≠ 系统对
```
已有测试:
C# 单测:133 个文件,37933 行(主要测后端 BLL)
JS 单测:22 个文件(主要测 swapCalc.js 纯函数)
缺失的测试层:
✗ 前端组件测试(@vue/test-utils
✗ 事件链路测试(keydown → emit → handler → state
✗ API 集成测试(main.post resolve/reject 链路)
✗ 前后端契约测试(/Bond/CalcBond 的请求/响应格式)
✗ E2E 测试(从用户输入到数据库落库的全链路)
```
纯函数测试全绿,但集成层每层都有断裂点。这导致"改了纯函数以为修好了 → 部署发现没生效 → 再改集成层 → 再部署"的循环。
### 2.4 流程层面:没有"安全网"的开发
| 问题 | 当前状态 | 后果 |
|------|---------|------|
| 本地验证 | 只能 `dotnet publish` + 部署测试环境 | 每轮验证 10+ 分钟 |
| 代码审查 | 无强制 review | 质量取决于个人经验 |
| CI/CD | 有 Jenkins 但不跑前端测试 | 前端 bug 到部署后才发现 |
| 分支策略 | 多人直接 merge 到 feature 分支 | 21 次 merge = 21 次冲突风险 |
| 回归测试 | 靠人工 | 改 A 坏 B(如精度调整批量回退 6 次) |
### 2.5 领域知识层面:字段命名混乱
已在《互换价格字段命名规范决策文档》中详细记录:`PosiNetPrice` 名为"Net"实为"含费全价",连文档作者都被误导。这种命名混乱导致每次改动都要反复核对数据流向,极易引入新 bug。
---
## 三、改进路线:5 个阶段,每阶段都有可交付产出
### 阶段 0:止血(1-2 周,零风险改动)
**目标**:不改架构,只加"安全网",让下一个需求不再靠 console.log 试错。
| 动作 | 产出 | 风险 |
|------|------|------|
| 把 `swapCalc.js` 的纯函数测试覆盖到 100% | 所有计算逻辑有回归守卫 | 零 |
| 为 `vue-number-input``@vue/test-utils` 组件测试 | 事件链路可自动验证 | 零 |
| 为 `main.post` 的 resolve/reject 链路补集成测试 | Promise 失败分支不遗漏 | 零 |
| CI 接入前端测试(`npm test`) | 提交即验证,不通过不能 merge | 零 |
| `guard_arch.js` 接入 pre-commit hook | 新代码不允许内联金额计算 | 零 |
**为什么先做这个**:纯函数测试已经在本次迭代中证明了价值——`bondCalc.test.js` 全绿说明逻辑正确,问题只在集成层。补上集成层测试后,下一个需求的"改了不生效"可以在本地 5 秒内发现,而不是部署后 10 分钟。
### 阶段 1:构建现代化(2-3 周)
**目标**:消灭 bundle 手工艺品,让"改了源文件不生效"成为历史。
| 动作 | 产出 | 风险 |
|------|------|------|
| 引入 esbuild/Vite 做 JS 打包 | 200ms 增量构建,不需要提交产物到 git | 低(产物逐字节比对验证) |
| `bundle.js` 从 git 移除,改为 CI 构建 | git diff 不再有 801KB 噪音 | 低 |
| `JsVersion` 缓存键改为 content hash | 浏览器永远加载最新版本 | 低 |
| `.editorconfig` 统一换行符/BOM 规则 | 不再有 BOM/换行符问题 | 零 |
| Source map 上线 | 生产环境可定位到源文件行号 | 零 |
**预期收益**:本次迭代中 8 次 bundle 相关提交全部可以避免。
### 阶段 2:前端组件可测试化(3-4 周/模块)
**目标**:让每个 Vue 组件都能脱离浏览器独立测试。
**策略**:不一次性重写,而是**逐模块迁移**。每次只改一个页面,不影响其他页面。
```
迁移优先级(按 bug 密度排序):
1. swapTradeEdit.js (1963 行,本次踩坑主战场)
2. unwindSwapTrade.js (536 行,平仓逻辑复杂)
3. incomeSwapTrade.js (390 行,结息逻辑)
4. 其他 swaptrade/*.js
5. 其他模块
```
每个模块的迁移步骤:
1.`new Vue({ methods: { ... } })` 中的方法提取为独立模块(如 `swapTradeEditHandler.js`
2. 方法只接收 `state``event` 参数,不依赖 `this`
3. 用 jest 测试 handler 模块
4. Vue 组件只做 `v-on:keydown="handler.onBondPriceKeydown(item, type, $event)"` 的薄绑定
5.`@vue/test-utils` mount 组件验证事件绑定
**关键原则**
- **不改业务逻辑**,只改代码组织方式
- **不改 cshtml 模板**(模板已经是 Vue 语法)
- **不改后端 API**
- 每步都有测试覆盖后才继续下一步
### 阶段 3:jQuery 逐步退场(持续进行)
**目标**:消除 jQuery 与 Vue 的事件系统/响应式冲突。
**策略**:不一次性替换 jQuery,而是**逐个组件替换 jQuery 依赖**。
| jQuery 用途 | 替代方案 | 迁移难度 |
|-------------|---------|---------|
| `$.ajax` / `main.post` | `fetch` / `axios` + `async/await` | 低(API 层替换) |
| `$(el).on('keydown')` | `el.addEventListener` + Vue `$emit` | 低(已在 vue-number-input 中验证) |
| `$.Deferred` | `Promise` / `async-await` | 低(语法替换) |
| `$.confirm` / `$.alert` | 自定义 Vue 弹窗组件 | 中(需要写组件) |
| `$(selector)` DOM 操作 | Vue `ref` + `data` 绑定 | 高(需要重构模板) |
| jQuery UI autocomplete | Vue autocomplete 组件 | 中 |
**优先迁移**`main.post``fetch`/`axios`。这一步就能消除"reject 不走 .done"的整类问题。
**迁移节奏**:每次改一个页面时,顺手把该页面的 jQuery 依赖替换掉。不为了迁移而迁移,而是"借修 bug 的机会还技术债"。
### 阶段 4:字段命名规范化(持续进行)
**目标**:消除"PosiNetPrice 名为 Net 实为含费全价"这类命名混乱。
按《互换价格字段命名规范决策文档》的落地策略,分阶段执行:
- 阶段 4a:给最混乱的字段加 `[Obsolete]` + XML 注释(零风险)
- 阶段 4b:新增字段/新功能强制用规范命名
- 阶段 4c:前端统一取值封装(`swapPriceHelper.js`
- 阶段 4d:大版本升级时做 DB 列重命名
---
## 四、每个阶段的具体执行清单
### 阶段 0 止血——执行清单
```
□ 1. CI 接入前端测试
- Jenkins pipeline 加 npm test 步骤
- 前端测试不通过 = 构建失败 = 不能部署
□ 2. 补集成测试(已在本次迭代完成 bondCalc.integration.test.js
- 后续每个新需求都要同步补集成测试
- guard_arch.js 接入 pre-commit
□ 3. main.post 包装层加错误守卫
- 在 main.post 返回的 promise 上强制挂 .fail
- 或者封装为 async/await 形式的 postAsync
□ 4. 建立前端测试覆盖率报告
- jest --coverage
- 目标:新代码覆盖率 > 80%
```
### 阶段 1 构建现代化——执行清单
```
□ 1. 引入 esbuild
- npm install esbuild
- 写 build.js 脚本:把 bundleconfig.json 的输入文件列表用 esbuild 打包
- 产出与现有 bundle.js 逐字节比对(允许差异但需验证功能一致)
□ 2. CI 自动构建 bundle
- dotnet publish 前自动跑 esbuild
- 不再提交 bundle.js 到 git
□ 3. JsVersion 改为 content hash
- _MainLayout.cshtml 中 bundle.js 的版本号改为文件内容的 hash
- 浏览器永远加载最新版本
□ 4. 统一 .editorconfig
- end_of_line = lf
- charset = utf-8(不带 BOM
- insert_final_newline = true
```
### 阶段 2 组件可测试化——执行清单(以 swapTradeEdit 为例)
```
□ 1. 提取 handler 模块
- 创建 swapTradeEditHandler.js
- 把 onBondPriceEnter/onBondPriceEdit/onBondPriceKeydown/
onDpPriceInput/calcBondForItem/syncBondFlags 等方法搬出
- 方法签名改为 function(state, event, ...args),不依赖 this
□ 2. 为 handler 写测试
- 每个方法至少 3 个用例:正常路径、边界值、错误路径
- mock main.post 的 resolve/reject
□ 3. Vue 组件改为薄绑定
- methods 中只做 handler(state, event) 的调用
- 不含任何业务逻辑
□ 4. 用 @vue/test-utils 验证组件
- mount 组件
- 模拟 keydown/enter/input 事件
- 断言 state 变化 + 视图渲染
```
---
## 五、优先级矩阵
```
高收益
阶段0止血 │ 阶段1构建
←───────────────┼───────────────→
低风险 │ 中风险
阶段2测试 │ 阶段3去jQuery
│ 阶段4命名
低收益
```
**执行顺序**:阶段0 → 阶段1 → 阶段2(逐模块)→ 阶段3(逐组件)→ 阶段4(持续)
**关键原则**:每个阶段都是**可交付的独立产出**,即使只完成阶段0,下一个需求的踩坑频率也会大幅下降。
---
## 六、预期收益量化
| 改进措施 | 预期减少的提交 | 预期减少的调试时间 |
|---------|:---:|:---:|
| 阶段0:集成测试 + CI | -15 次(集成层 bug 本地发现) | -2 天 |
| 阶段1:构建现代化 | -8 次(bundle 相关全消除) | -0.5 天 |
| 阶段2:组件可测试化 | -10 次(响应式/事件 bug 本地发现) | -1.5 天 |
| 阶段3:去 jQuery | -5 次(Promise/事件类 bug 消除) | -0.5 天 |
| 阶段4:命名规范 | -5 次(字段混淆类 bug 消除) | -0.5 天 |
| **合计** | **-43 次(40%** | **-5 天** |
从 7 天降到 2 天,从 106 次提交降到 60 次左右,fix 占比从 47% 降到 20% 以下。
---
## 七、不走弯路的注意事项
1. **不要一次性重写**。这个项目有 2450 个 C# 文件、289 个 JS 文件。一次性重写 = 一次性引入 100 个新 bug。渐进式改造是唯一可行路径。
2. **不要为了迁移而迁移**。每次迁移都应该是"借修 bug 的机会还债",而不是"停下所有业务开发来做技术改造"。业务方不会接受后者。
3. **不要跳过测试直接改架构**。如果当前代码没有测试覆盖,改架构等于裸奔——你不知道改动是否破坏了什么。先加测试(阶段0),再改架构(阶段2-3)。
4. **不要忽视构建工具**。bundle 问题看起来是"小事",但它消耗了 8 次提交和大量调试时间。构建工具是基础设施,基础设施不稳,上面的一切都不稳。
5. **不要指望工具解决流程问题**。CI、测试、构建工具都是"安全网",但它们不能替代代码审查和设计文档。每个新需求仍需要:先写设计文档 → 识别风险点 → 补测试 → 再写代码。