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

21 KiB
Raw Blame History

互换模块长期改进方案——从"一个需求改一周"到"可预测交付"

基于 EQD-6838(债券三字段互算)从 0aad4c80ffa51c6a 共 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 的加载顺序:

<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 v3Vue3 独立项目)中开发:

宿主 (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.jsbundleV2.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)。

安装方式(每位开发者执行一次):

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. 方法只接收 stateevent 参数,不依赖 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、测试、构建工具都是"安全网",但它们不能替代代码审查和设计文档。每个新需求仍需要:先写设计文档 → 识别风险点 → 补测试 → 再写代码。