Files
zszq-trs/项目文档/互换债券三字段互算踩坑总结与测试指南.md
hjhan 0747deb909 fix: 步骤3-b 多字段连续手动编辑应累计REV(不再仅最后赢)
根因:applyBondManualEdit 每次调用先重置全部 bondRev 再只标当前字段,
导致连续手动编辑多字段(不回车)时只有最后编辑的字段留 REV,
中间手工编辑字段的 REV 被静默抹掉,UI 与步骤3-b需求不符。

修复:applyBondManualEdit 改为累计 REV(仅置当前字段、保留其它已手工编辑字段的 REV),
driver/AUTO 仍清。单字段编辑(步骤2/3)行为不变,无回归。

配套:
- 更新 a2d2bb31 中3个旧断言("仅最后留REV")为断言累加,避免测试守护错误行为
- 文档约定3 补充"连续编辑多字段时各手动字段累计REV"

swapCalc.js 为独立 script(?v=JsVersion),部署 dotnet publish 即生效,无需重建 bundle。
测试:bondCalc 全套 72 例通过。
2026-07-30 09:55:57 +08:00

12 KiB
Raw Permalink Blame History

互换债券三字段互算踩坑总结与测试指南

本文档总结 EQD-6838 债券净价/全价/收益率三字段互算功能从初始实现到最终可用过程中踩过的所有坑, 包括根因分析、修复方案、能否被不同技术栈避免、以及如何用单测在早期拦截。 关联文件:swapTradeEdit.js / swapCalc.js / fastVue.base.js / TradeEdit.cshtml / base/main.js


一、功能背景

债券标的在互换交易录入页有三个价格字段:净价(PosiNetNoFeePrice)、全价(PosiGrossPrice)、收益率(InitYtm)。 三者可互相推导(知道任一可算另两个,通过后端 /Bond/CalcBond 接口)。

交互约定

  • 约定1:在任一字段敲回车 → 以该字段为"源"调计算器,反算另两个并覆盖。
  • 约定2:计算器调用成功→源字段标"源"、另两个标"AUTO";失败→保留源字段值、另两个清空、三标识全清。
  • 约定3:编辑某字段(失焦/按键输入)但未回车 → 不联动另两字段,清掉源/AUTO标识,本字段标"REV"(人工输入)。连续手动编辑多字段(均不回车)时各被改过的字段均累计REV(不清空其它已手工编辑字段的REV),使 UI 正确显示"全部为人工输入"、且后续计算器回写时不会被误覆盖。

二、踩坑时间线与根因

d662399a56256cad 共 7 次提交,涉及 5 个独立根因。每次修复后部署测试发现问题仍未解决,再定位新根因。

根因 1.native 修饰符被 jQuery 事件系统拦截

项目 内容
提交 d662399a(初始实现)→ cee69a02(修复)
现象 用户按键时"源/AUTO"标识不消失,onBondPriceKeydown 从未执行
根因 vue-number-input 组件在 mounted() 中通过 jQuery $(_el).on('keydown', __keyHandle) 重新绑定了 keydown。jQuery 的事件绑定机制与 Vue 的 .native 修饰符冲突,导致父组件的 v-on:keydown.native 监听器收不到事件
修复 在组件 mounted() 内部用原生 this.$el.addEventListener('keydown', fn) + self.$emit('keydown', e) 绕过 jQuery 事件系统;cshtml 从 v-on:keydown.native 改为 v-on:keydown
代码位置 fastVue.base.js mounted() 钩子

根因 2:Vue 2 响应式失效——非 data() 声明的属性不可响应

项目 内容
提交 d662399a(初始实现)→ cee69a02(修复)
现象 applyBondManualEdit 等纯函数正确修改了 item.bondDriverType 等属性,但视图不更新
根因 bondDriverType/bondAuto/bondRev 这三个属性不在后端返回的数据模型中,item 对象从未在 Vue 的 data() 中声明过这些属性。Vue 2 基于 Object.defineProperty,只能追踪初始化时已存在的属性,后加属性是非响应式的,赋值后不触发视图更新
修复 新增 syncBondFlags(item) 方法,用 this.$set(item, 'bondDriverType', ...) 强制将属性注册为响应式
代码位置 swapTradeEdit.js syncBondFlags()

根因 3$set 设置相同对象引用不触发更新

项目 内容
提交 cee69a02(初版修复)→ 7c0572ad(二次修复)
现象 加了 syncBondFlags 后标识仍不更新。排查发现 $set 传入了相同对象引用
根因 Vue 2 的 $set 内部会做 newVal === oldVal 判断。如果 applyBondManualEdit 原地修改 state.bondAuto 后引用没变,this.$set(item, 'bondAuto', item.bondAuto) 的 newVal 与 oldVal 是同一个对象引用,Vue 会跳过更新
修复 每次都创建全新的对象字面量:{ CP: !!item.bondAuto.CP, DP: !!item.bondAuto.DP, YD: !!item.bondAuto.YD },保证引用不同。再加 this.$forceUpdate() 兜底
代码位置 swapTradeEdit.js syncBondFlags()

根因 4main.post 业务错误走 reject.done() 不执行

项目 内容
提交 d662399a(初始实现)→ ce4ef629(修复)
现象 计算器返回业务错误(如债券不存在)时,"另两字段清空+三标识全清"逻辑(约定2补充)不执行
根因 main.post 是 jQuery $.ajax 的包装。main.__postresp.success===false(业务错误)时调用 deferred.reject(resp),而 calcBondForItem 只挂了 .done() 回调。Promise reject 不会触发 .done(),整个失败处理分支被静默跳过
修复 .fail(function(resp) { ... applyBondCalcFailure + syncBondFlags }) 处理 reject 路径
代码位置 swapTradeEdit.js calcBondForItem() + base/main.js __post()

根因 5:部署/缓存/构建路径问题

项目 内容
提交 8124bb46 + 56256cad
现象 代码改了部署后不生效
根因 swapTradeEdit.js 是独立 <script> 引入(不在 bundle.js),需 dotnet publish 刷新 JsVersion 缓存键才生效;② Linux 上 bundle 输入路径大小写敏感导致构建失败;③ bundle 缺失输入时应非致命否则连累 publish
修复 加版本标记日志确认部署生效;补 rebuild-bundles.py 路径大小写注释 + 非致命处理
代码位置 rebuild-bundles.py + YLErpWeb.csproj

三、根因分类与技术栈对比

# 根因 类别 纯Vue2原生组件能否避免 前后端分离能否避免 单测能否拦截
1 .native 被 jQuery 拦截 jQuery/Vue 混用 组件测试
2 非 data() 属性不可响应 Vue 2 限制 ⚠️ 需注意 ⚠️ Vue3 可避免 响应式测试
3 $set 相同引用不触发 Vue 2 优化 ⚠️ 需注意 ⚠️ Vue3 可避免 响应式测试
4 main.post reject 不走 .done jQuery Promise (async/await) mock 测试
5 部署缓存/路径 构建/部署 无关 无关 需本地验证

核心结论

  1. 根因1-4 都是 jQuery 与 Vue 混用导致的vue-number-input 是用 jQuery 包装的"伪 Vue 组件",破坏了 Vue 的响应式和事件契约。纯 Vue2 原生组件能消除这些坑。
  2. Vue 2 的 Object.defineProperty 响应式限制(根因2-3)在 Vue 3 中通过 Proxy 解决,但仍需注意 reactive() 的使用方式。
  3. 纯函数测试只能守卫逻辑层。现有 bondCalc.test.js 测试 swapCalc.js 纯函数全绿——因为纯函数逻辑本身正确。问题出在纯函数与宿主框架的集成层(事件绑定、响应式、Promise 链),这层完全没有测试覆盖。

四、为什么多次改动都没修好

4.1 纯函数全绿带来的虚假信心

纯函数层(swapCalc.js           ← bondCalc.test.js 全绿 ✅
    ↓ 直接赋值 state.xxx
Vue 响应式层(syncBondFlags      ← 无测试 ❌
    ↓ $set / $forceUpdate
jQuery 事件层(vue-number-input  ← 无测试 ❌
    ↓ $emit('keydown')
事件处理层(onBondPriceKeydown    ← 无测试 ❌
    ↓ main.post
Promise 链(.done/.fail          ← 无测试 ❌

每一层都是潜在的断裂点,但只有最底层的纯函数有测试。上层任何一层断裂,表现都是"标识不更新/逻辑不生效",难以区分是哪层的问题。

4.2 发布循环成本极高

每次修改 → dotnet publish → 部署测试环境 → 用户验证 → 发现问题 → 重新修改。一轮循环 10+ 分钟,7 次提交意味着至少 70 分钟的等待。如果当时有组件集成测试,可以在本地 5 秒内发现"keydown 事件没触发"或"响应式没更新"。

4.3 不得不靠 console.log 试错

7c0572ad 提交在 onBondPriceKeydown 中加了 console.log before/after,帮助确认了"keydown 事件能触发、状态值也正确变了,但视图没刷新"——从而定位到 Vue 响应式问题。这是被动的、发布后才发现的调试方式。正确的做法是用组件测试在 CI 中主动拦截。


五、测试覆盖方案

5.1 已有测试(纯函数层)

测试文件 覆盖范围 状态
bondCalc.test.js swapCalc.js 纯函数:applyBondCalcResult/Success/Failure/ManualEdit、单位换算、错误判断 全绿
swapCalc.test.js swapCalc.js 纯函数:回归守卫4个历史bug + C#金标准交叉校验 + D1/D2/D3 回归 全绿
fastVue.enter.test.js FastVue.numberInput 回车链路 + vueNumberInput $emit enter/input 分发 全绿
numberInput.paste.test.js FastVue.numberInput 粘贴路径回归 全绿

5.2 新增测试(集成层)

测试文件 覆盖的根因 测试要点
bondCalc.integration.test.js 根因1-4 见下方详细说明

测试要点 1vue-number-input keydown emit(根因1

验证组件 mounted() 中的 addEventListener('keydown') + $emit('keydown') 链路。 模拟 keydown 事件,断言组件是否正确 emit 了 keydown 事件给父组件。 如果 .native 修饰符仍被 jQuery 拦截,此测试会红。

测试要点 2:Vue 2 响应式——$set 新引用 vs 旧引用(根因2-3)

验证 syncBondFlags 的三种写法:

  • 直接赋值(非响应式,视图不更新)
  • $set 传入相同引用(Vue 跳过更新)
  • $set 传入新对象引用(正确触发更新)

用 mock Vue 实例模拟 $set 行为,断言新旧引用是否不同。

测试要点 3main.post Promise 链——reject 走 .fail 不走 .done(根因4

用 jQuery $.Deferred 模拟 main.post 的 resolve/reject 行为。 验证:

  • resolve 时 .done() 被调用、.fail() 不被调用
  • reject 时 .fail() 被调用、.done() 不被调用
  • 仅挂 .done() 时 reject 会导致失败分支静默跳过

测试要点 4:约定2补充——所有失败分支都调 applyBondCalcFailure + syncBondFlags

验证 calcBondForItem 的所有退出路径都正确清空了非源字段和标识:

  • 源字段为空/非数
  • 估值日缺失
  • 计算器返回业务错误(reject 路径)
  • 计算器返回网络错误(reject 路径)
  • 计算器返回成功但值域异常(done 内拦截)

5.3 运行方式

cd YLErpWeb/fe-tests && npm i && npm test
# 或单独运行集成测试
cd YLErpWeb/fe-tests && npx jest bondCalc.integration

六、预防清单

阶段 措施 预防的根因
设计阶段 画出"DOM事件→jQuery处理→Vue emit→handler→纯函数→syncBondFlags→视图"的完整链路图,识别每层风险 1-4
编码阶段 状态属性在 Vue data()created() 中预先声明;不用 jQuery 包装 Vue 组件 2, 1
测试阶段 纯函数测试 + 组件集成测试 + Promise 链测试三层覆盖 1-4
调试阶段 用 Vue DevTools 观察响应式数据变化,比 console.log 更高效 缩短调试周期
部署阶段 本地 dotnet publish 验证 + 版本标记日志确认缓存刷新 5

通用规则

  1. 纯函数是必要条件,不是充分条件。纯函数测试全绿不代表集成层没问题。
  2. jQuery 与 Vue 混用时,jQuery 的事件绑定会覆盖 Vue 的事件系统。如必须混用,在组件内部用原生 addEventListener + $emit 桥接。
  3. Vue 2 中动态添加的属性必须用 $set,且 $set 传入的对象引用必须与旧值不同,否则不触发更新。
  4. jQuery Promise 的 reject 不走 .done()。凡要区分成功/失败,必须同时挂 .done().fail()。改用 async/await + try/catch 可根本消除此问题。
  5. 独立 <script> 文件的改动需刷新 JsVersion 缓存键。调试时加版本标记日志确认部署生效。

七、提交历史索引

提交 说明 修复的根因
d662399a 初始实现:约定1/2/3 + 簿记逻辑1
cee69a02 修复Vue响应式 + 组件内emit keydown + 去掉重算按钮 1, 2
7c0572ad 按键清标识加排查日志 + forceUpdate兜底 + 新对象引用 3
8124bb46 加版本标记日志确认部署是否加载最新代码 5
f5ed65aa 补网络错误/响应异常分支的约定2补充处理 4(部分)
ce4ef629 修复 main.post reject 未走 .done(约定2补充静默失效) 4(完整)
56256cad 补充踩坑注释:main.post reject 机制 + 部署机制 + bundle 路径 文档化