diff --git a/项目文档/互换模块长期改进方案.md b/项目文档/互换模块长期改进方案.md new file mode 100644 index 00000000..9c2a0b97 --- /dev/null +++ b/项目文档/互换模块长期改进方案.md @@ -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 (独立 script,1963 行) + ↓ 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、测试、构建工具都是"安全网",但它们不能替代代码审查和设计文档。每个新需求仍需要:先写设计文档 → 识别风险点 → 补测试 → 再写代码。