# 收银台「最低 1 元」闸 —— 前端执行级验证台 ## 这台子回答哪一句话 > 操作员在收银台输入 `0.5`,按下「收款」——**那一枪会不会发出去**? 「源码里写了 `if (fen < MIN_FEN) return;`」不是答案。这页有 8 个场景, 答案是**数出来的**:`createCashierQr` 被调了几次。 ## 怎么跑 ```bash cd .claude/plan/2026-09-29-lsy-amount-floor-harness node amount-floor-harness.mjs # 默认拿本仓的 cashier/index.vue node amount-floor-harness.mjs <另一份 SFC 路径> ``` 不带参数、不联网、不碰库、不写任何东西。**只认最后那行 `SUMMARY`** —— 失败也 `exit 0`(与同目录各台一致),别拿 exit code 当判据。 ## 它怎么做到「真」 | 环节 | 真的还是假的 | |---|---| | SFC | **真**:`@vue/compiler-sfc` 解析 + `compileScript` 编译本仓那个 `index.vue` | | vue | **真**:`frontend/node_modules` 里那份 `vue`(`createRenderer` 挂载) | | 被驱动的函数 | **真**:`start()` 是 setup 返回的那袋绑定里的那一个,本台直接调它 | | 金额换算 | **真**:页面的 `Math.round(Number(text) * 100)` 原样跑 | | DOM | 假:`createRenderer` 接一套哑节点,模板不渲染(`inlineTemplate: false`) | | 接口 | 假:`@/api/module_payment/collection` 换成**记账**桩(记下每次调用) | | element-plus / qrcode | 假:可调用、可无限取属性的 Proxy | 「假」的那三样与要证的那句话无关:本台问的是**有没有发那一枪**,不是页面长什么样。 副作用一则:模板不渲染 ⇒ `amountEl.value` 是 undefined ⇒ 页面里的 `amountEl.value?.focus()` 是空操作。 ## 八个场景 | # | 输入(元) | 期望 | 它挡的是什么 | |---|---|---|---| | A | `0.5` | 不下单,提示「收款金额最低 1 元」 | 门槛外侧 | | B | `0.99` | 不下单,同上 | 门槛内侧最后一格 | | C | `1` | **下单** `amount=100` | 门槛本身(放行侧正对照) | | D | `1.01` | **下单** `amount=101` | 门槛之上 | | E | `0` | 不下单,提示「请先填一个大于 0 的金额」 | 旧的 `fen <= 0` 分支没被新闸顶掉 | | F | `""` | 不下单,同上 | 空串(`Number("")` 是 0,不是 NaN) | | G | `abc` | 不下单,同上 | 非数字 | | H | `0.999` | **下单** `amount=100` | 四舍五入到分正好 1 元 ⇒ 放行(**读数,不是缺陷**) | **A/B/E/F/G 是拦下侧,C/D/H 是放行侧 —— 两侧都必须有。** 只有拦下侧的话,把 `start()` 改成第一行 `return` 也能全绿。 ## 两向变异(读数在 `logs/_mutate-fe-floor.txt`) | 变异 | 期望 | 实读 | |---|---|---| | `MIN_FEN` 100 → 0(把闸拆掉) | 必须红 | `SUMMARY FAIL 场景=8 不符=2`,A 变成「下单 50」、B 变成「下单 99」 | | 只改一行注释 | 必须仍绿 | `SUMMARY PASS 场景=8 不符=0` | 两条变异还原后复量 sha256,与变异前逐字节一致(`e1e495e8ad5c…`)。 ## 踩过的两个坑 1. **台子跑完不退出。** 桩永远回 `PAYING` ⇒ `start()` 成功后 `poll()` 每 3 秒自己 重排一次定时器(`schedulePoll`),`startTicking` 另有一个 1 秒 interval。8 条读数 全对、`SUMMARY` 也打了,进程却一直挂着。修法是每个场景后 `b.stopTimers?.()`, 末尾再 `process.exit(0)` 兜底。 ★ 挂着那次是靠 `Get-CimInstance Win32_Process` 按命令行认出 `amount-floor-harness.mjs` 那个 PID 才**只杀掉自己**的(同机另有 codegraph / MCP / IDE 的十几个 node)。 2. **`process.exit(0)` 不设 exitCode。** 与同目录各台一致:判据是 `SUMMARY` 那行, 不是退出码 —— 否则「台子崩了」与「场景不符」在 CI 里分不开。 ## 与后端那道闸的关系 后端 `LsyAmountFloor.MIN = 100`(四个下单入口的 `@Min`)是**真的闸**: 开放 API 的调用方根本不经过这一页。这一页拦的是**体验** —— 不拦的话操作员收到的是一句报文措辞(`amount: amount 不能低于 1 元(100 分)`), 而屏幕上正站着顾客。 两处的数值是**同一件事写了两遍**:`frontend/.../cashier/index.vue` 的 `MIN_FEN` 与 `java/.../util/LsyAmountFloor.java` 的 `MIN`。**改一处必须改另一处**, 两边各有测试/台子钉着(后端 `LsyAmountFloorTest`,前端本台)。