Files
zszq-trs/项目文档/互换模块长期改进方案.md

435 lines
21 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.
# 互换模块长期改进方案——从"一个需求改一周"到"可预测交付"
> 基于 EQD-6838(债券三字段互算)从 0aad4c80 到 ffa51c6a 共 106 次提交、
> 7 天开发周期、50 次 fix/revert(47%)的惨痛教训,提炼出的长远改进路线。
> 不是一次性重构方案,而是"每个迭代都往前走一步"的渐进式改善。
---
## 〇、项目架构现状(接手必读)
### 0.1 整体技术栈
```
┌─────────────────────────────────────────────────────────────┐
│ 宿主应用 (zszq-trs / YLErpWeb) │
│ ├── 后端: ASP.NET Core 6 MVC + Razor Views (600 个 cshtml) │
│ ├── 前端: jQuery 3.5 + Vue 2 + Bootstrap + JqGrid │
│ ├── 规模: 2450 个 C# 文件, 289 个 JS 文件, 67 个 new Vue() │
│ └── 微前端: qiankun v2.10.5 (加载 otcdms-ui v3 子应用) │
│ └── Vue3 独立项目,部署在 /otcdms-ui/ 路径 │
│ 宿主通过 /v3 路由激活微应用 │
│ 通信: qiankun.initGlobalState (logout/查看交易等) │
└─────────────────────────────────────────────────────────────┘
```
### 0.2 两套 Layout + 两套 Bundle
项目有**两套独立的页面布局**,各自加载不同的 JS/CSS bundle。一个页面只会加载其中一套,互不冲突。
#### bundle.js — 交易管理主站
| 属性 | 值 |
|------|-----|
| 加载者 | `_MainLayout.cshtml` / `_InfoLayout.cshtml` / `_LayoutMini.cshtml` |
| 用在哪 | 互换交易、期权交易、风控、客户管理等业务页面 |
| 文件大小 | 823 KB |
| 包含 main.js? | ✅ (第 49 行) |
| 包含 fastVue? | ✅ (fastVue.base.js + fastVue.components.js) |
| UI 框架 | Bootstrap 3 + JqGrid 4 + jquery-confirm |
| 日期库 | dayjs + 插件 |
`_MainLayout.cshtml` 的加载顺序:
```html
<head>
<link href="bundle.min.css"> ← 样式
<script src="jquery.js"> ← jQuery 3.5 + jQuery UI + layer + numeral
</head>
<body>
@RenderBody() ← 页面内容
<script src="bundle.js"> ← lodash + bootstrap + JqGrid + fastVue + main.js
<script src="vue.js"> ← Vue 2.6 + vue.custom
<script src="mainLayout.js"> ← 布局交互
<script src="qiankun-v2.10.5/index.umd.min.js"> ← 微前端运行时
</body>
```
#### bundleV2.js — 系统管理后台
| 属性 | 值 |
|------|-----|
| 加载者 | `Areas/Admin/Views/Shared/_Layout.cshtml` |
| 用在哪 | 系统管理后台(AppConfig、OtcFormat、数据库升级、确认书信息等) |
| 文件大小 | 562 KB |
| 包含 main.js? | ✅ (第 123 行) |
| 包含 fastVue? | ❌ |
| UI 框架 | Bootstrap 4 (tabler) + bootstrap-table + toastr |
| 与 bundle.js 的区别 | 自带 jQuery + Vue(不依赖 jquery.js/vue.js),用 bootstrap-table 替代 JqGrid |
**为什么改 main.js 后两个 bundle 都要重建**:因为 `main.js` 同时出现在两个 bundle 的 `inputFiles` 中(`bundleconfig.json` 第 49 行和第 123 行)。`rebuild-bundles.py` 会自动重建全部 6 个产物,不需要手动选择。
#### 完整 Bundle 清单
| 产物 | 大小 | 内容 | 被谁加载 |
|------|------|------|---------|
| `jquery.js` | 340 KB | jQuery 3.5 + jQuery UI + layer + numeral + validate | 主站 (`_MainLayout` 等) |
| `bundle.js` | 823 KB | lodash + dayjs + Bootstrap 3 + JqGrid + fastVue + main.js + jqGridEx | 主站 |
| `vue.js` | 97 KB | Vue 2.6 min + vue.custom | 主站 |
| `bundleV2.js` | 562 KB | lodash + jQuery + Bootstrap 4 + bootstrap-table + main.js + Vue 2 | 管理后台 |
| `bundle.css` | 358 KB | Bootstrap 3 CSS + JqGrid CSS + 公共样式 | 主站 |
| `bundleV2.css` | 409 KB | tabler CSS + toastr CSS + bootstrap-table CSS | 管理后台 |
### 0.3 qiankun 微前端架构
项目已通过 qiankun 实现渐进式现代化,部分新页面在 `otcdms-ui v3`Vue3 独立项目)中开发:
```
宿主 (zszq-trs) 微应用 (otcdms-ui v3)
┌──────────────────────┐ ┌──────────────────────┐
│ _MainLayout.cshtml │ │ Vue3 独立项目 │
│ └─ qiankun-v2.10.5 │──register──→ │ entry: /otcdms-ui/ │
│ registerMicroApps │ │ activeRule: /v3 │
│ start() │ │ │
└──────────┬───────────┘ └──────────────────────┘
│ initGlobalState
│ (logout, retrieveTradeDetail,
│ quotaTrial, retrieveSwapDetail...)
V3/Index.cshtml: /v3 开头的路由
自动加载微应用到 #subapp-viewport
```
**对改进方案的影响**
- 新页面走 v3 微应用(Vue3),不再受 jQuery/Vue2 踩坑困扰
- 存量页面(如 `swapTradeEdit.js`)仍在宿主中,是踩坑主战场
- 阶段 2 组件可测试化的优先级取决于**该页面是否会迁移到 v3**:
- 短期不迁移的页面 → 值得做组件可测试化
- 已计划迁移的页面 → 不值得投入,直接在 v3 中重写
---
## 一、数据事实:到底有多痛
| 指标 | 数值 | 含义 |
|------|------|------|
| 开发周期 | 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 是手工艺品
项目有 6 个 bundle 产物(见 0.2 节),其中 `main.js` 同时出现在 `bundle.js``bundleV2.js` 中——改了 `main.js` 必须同时重建两个 bundle,否则一个页面生效另一个不生效。
```
当前流程:
改源文件 → 手动跑 rebuild-bundles.py → 提交 bundle.js + bundleV2.js → dotnet publish → 部署
问题链:
① 忘记重新打 bundle → 部署的是旧代码
② 只重建了 bundle.js 忘了 bundleV2.js → 管理后台行为不一致
③ BOM/换行符不一致 → bundle 产物有差异
④ Linux 路径大小写敏感 → CI 构建失败
⑤ JsVersion 缓存键不刷新 → 浏览器加载旧缓存
⑥ bundle.js 823KB + bundleV2.js 562KB 被提交到 git → 每次改动产生巨大 diff
```
**已改善**`rebuild-bundles.py` 已自动重建全部 6 个产物;MSBuild `GenerateBundlesBeforeBuild` target 在 Build 前自动调用;CI 脚本 `run-ci-checks.sh``--verify` 模式可检测产物与源文件不同步。但产物仍提交到 git,diff 噪音问题未解决。
### 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` 纯函数测试 | 所有计算逻辑有回归守卫 | 零 | ✅ 已完成 |
| `vue-number-input` 事件链路测试 | keydown/enter/paste 可自动验证 | 零 | ✅ 已完成 |
| `main.post` resolve/reject 链路集成测试 | Promise 失败分支不遗漏 | 零 | ✅ 已完成 |
| `main.postSafe()` Promise 封装 | 新代码可用 async/await 替代 Deferred | 零 | ✅ 已完成 |
| pre-commit hookguard_arch + jest | 提交前自动验证架构+测试 | 零 | ✅ 已完成 |
| CI 脚本 `run-ci-checks.sh` | guard_arch + jest + bundle 校验 | 零 | ✅ 已完成 |
| `jest.config.js` + 覆盖率配置 | 测试配置标准化 | 零 | ✅ 已完成 |
| `otcformat.js` tradeSinglePrice 精度修复 | precision 2→9,修复 pre-existing bug | 低 | ✅ 已完成 |
**当前测试状态**10 suites, 190 tests, 全绿(commit `6b32f254`)。
**安装方式**(每位开发者执行一次):
```bash
cp YLErpWeb/fe-tests/hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
# 需要 Node.js 18+ (nvm install 20)
# 首次: cd YLErpWeb/fe-tests && npm install
```
**为什么先做这个**:纯函数测试已经在本次迭代中证明了价值——`bondCalc.test.js` 全绿说明逻辑正确,问题只在集成层。补上集成层测试后,下一个需求的"改了不生效"可以在本地 1 秒内发现(jest 运行时间),而不是部署后 10 分钟。
### 阶段 1:构建现代化(2-3 周)
**目标**:消灭 bundle 手工艺品,让"改了源文件不生效"成为历史。
**当前已改善的部分**`rebuild-bundles.py` + MSBuild target + CI `--verify` 已解决"忘了打 bundle"和"产物与源不同步"的问题。但产物仍提交到 git,diff 噪音和缓存问题未解决。
| 动作 | 产出 | 风险 |
|------|------|------|
| 引入 esbuild 做 JS 打包 | 200ms 增量构建 | 低(产物逐字节比对验证) |
| bundle.js + bundleV2.js 从 git 移除,改为 CI 构建 | git diff 不再有 823KB+562KB 噪音 | 低 |
| `JsVersion` 缓存键改为 content hash | 浏览器永远加载最新版本 | 低 |
| `.editorconfig` 统一换行符/BOM 规则 | 不再有 BOM/换行符问题 | 零 |
| Source map 上线 | 生产环境可定位到源文件行号 | 零 |
**关于 esbuild 的评估**:本项目 bundle 是纯文件拼接(非 ES module 打包),esbuild 的核心价值(tree-shaking / code splitting)在当前阶段用不上。esbuild 的真正价值在于阶段 2——当组件方法被提取为 `import/export` 模块后,esbuild 才能发挥优势。当前阶段保留 `rebuild-bundles.py` 即可,等阶段 2 再引入 esbuild。
**预期收益**:本次迭代中 8 次 bundle 相关提交全部可以避免。
### 阶段 2:前端组件可测试化(3-4 周/模块)
**目标**:让每个 Vue 组件都能脱离浏览器独立测试。
**策略**:不一次性重写,而是**逐模块迁移**。每次只改一个页面,不影响其他页面。
**与 qiankun 迁移的关系**:如果一个页面已计划迁移到 otcdms-ui v3(Vue3),则不值得在旧代码上做组件可测试化,直接在 v3 中重写即可。优先改那些**短期不会迁移**的页面。
```
迁移优先级(按 bug 密度 + 迁移可能性排序):
1. swapTradeEdit.js (1963 行,本次踩坑主战场,短期不迁移)
2. unwindSwapTrade.js (536 行,平仓逻辑复杂,短期不迁移)
3. incomeSwapTrade.js (390 行,结息逻辑,短期不迁移)
4. 已计划迁移到 v3 的页面 → 跳过,直接在 v3 中重写
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 依赖**。新页面走 v3 微应用(Vue3),天然不含 jQuery;存量页面借修 bug 的机会逐步替换。
| jQuery 用途 | 替代方案 | 迁移难度 | 状态 |
|-------------|---------|---------|:---:|
| `$.ajax` / `main.post` | `main.postSafe()` (Promise 封装) | 低 | ✅ 已提供 |
| `$(el).on('keydown')` | `el.addEventListener` + Vue `$emit` | 低 | ✅ 已在 vue-number-input 中验证 |
| `$.Deferred` | `Promise` / `async-await` | 低 | 通过 `main.postSafe` 间接解决 |
| `$.confirm` / `$.alert` | 自定义 Vue 弹窗组件 | 中 | 待做 |
| `$(selector)` DOM 操作 | Vue `ref` + `data` 绑定 | 高 | 待做 |
| jQuery UI autocomplete | Vue autocomplete 组件 | 中 | 待做 |
**已完成的过渡方案**`main.postSafe()` 已在 `main.js` 中实现(commit `6b32f254`),返回标准 Promise 而非 jQuery Deferred,新代码可以直接用 `async/await`,不再需要记住"业务错误走 reject、.done 不触发"的陷阱。旧代码不受影响。
**迁移节奏**:每次改一个页面时,顺手把该页面的 jQuery 依赖替换掉。不为了迁移而迁移,而是"借修 bug 的机会还技术债"。
### 阶段 4:字段命名规范化(持续进行)
**目标**:消除"PosiNetPrice 名为 Net 实为含费全价"这类命名混乱。
按《互换价格字段命名规范决策文档》的落地策略,分阶段执行:
- 阶段 4a:给最混乱的字段加 `[Obsolete]` + XML 注释(零风险)
- 阶段 4b:新增字段/新功能强制用规范命名
- 阶段 4c:前端统一取值封装(`swapPriceHelper.js`
- 阶段 4d:大版本升级时做 DB 列重命名
---
## 四、每个阶段的具体执行清单
### 阶段 0 止血——执行清单 — ✅ 已完成
```
✅ 1. pre-commit hook (guard_arch + jest)
- cp YLErpWeb/fe-tests/hooks/pre-commit .git/hooks/pre-commit
- 提交前自动跑架构闸门 + 前端单测
✅ 2. 集成测试 (bondCalc.integration.test.js)
- 覆盖 Vue 响应式 / jQuery 事件 / main.post reject 链路
✅ 3. main.postSafe() Promise 封装
- 新代码可用 async/await 替代 jQuery Deferred
- 旧代码 (main.post) 不受影响
✅ 4. jest.config.js + 覆盖率配置
- testEnvironment: node
- collectCoverageFrom + coverageThreshold
✅ 5. CI 脚本 (run-ci-checks.sh)
- 3 项检查: guard_arch + jest + bundle 校验
- Jenkins/GitLab CI 通用
✅ 6. otcformat.js tradeSinglePrice precision 修复 (2→9)
- 修复 80fbb184 (EQD-6597) 引入的 pre-existing bug
- otcformat.test.js 守卫测试现在全绿
⬜ 7. Jenkins pipeline 接入 run-ci-checks.sh (待 CI 管理员配置)
```
### 阶段 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、测试、构建工具都是"安全网",但它们不能替代代码审查和设计文档。每个新需求仍需要:先写设计文档 → 识别风险点 → 补测试 → 再写代码。