114 lines
6.9 KiB
Markdown
Raw 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.

# REV-003 营业收费 P0 审计记录
## 1. 记录信息
- 审计日期2026-07-14 至 2026-07-15
- 后端基线:`5fa11d5392f759be8560b5c696ace2bc814b2faf`
- 前端基线:`bd515cc3ade9a95a5ccb884f87d5625ebe3131b9`
- 文档基线:`8617af66fd45e6889bc02bebb4ba5fa95fc93cde`
- 审计范围:柜台收费、预存充值与抵扣、支付主明细、柜员结账、红冲、银行冲正、预存审批回调、前端收费和结账页面
- 执行约束:未运行 `vue-tsc`
## 2. 总体结论
基线审计时,营业收费虽已有客户查询、欠费查询、柜台收费、预存充值、支付记录、柜员结账和红冲骨架,但不能按财务级闭环验收通过。问题集中在金额口径、跨账单事务、结账范围、渠道确认、余额并发、幂等和反向流水。
截至 2026-07-15本记录列出的 9 项 P0 已完成代码整改并通过定向测试、后端编译和前端 Vite 构建。新的柜台收费主链路达到“服务端原子提交、请求幂等、账户与账单加锁、金额拆分可对账、显式记录结账、账单与预存可红冲”的首批上线条件;非现金渠道、打印/补打和柜员现金盘点仍不在本轮 P0 范围内。
## 3. P0 缺陷清单
### P0-01 应收金额口径断裂
`biz_charge.extended_amount` 的实体定义是不含违约金的本金金额,`late_fee` 独立保存;现有柜台支付却以 `extended_amount` 作为支付总额,再在支付明细中执行 `extended_amount - late_fee`。真实开账后产生违约金时,存在漏收违约金并错误压缩本金的风险。
统一口径:
```text
应收总额 = 本金 + 违约金
渠道实收 + 预存抵扣 = 应收总额 + 多缴转预存
```
### P0-02 多账单收费与多缴转预存不原子
前端逐账单调用 `PUT /business/charge/update`,全部成功后才单独调用预存充值。任一中间请求失败会形成部分账单已缴、部分账单未缴,或者账单已销但多缴未进入预存的状态。
### P0-03 通用营业账更新可作为支付入口
通用 `ChargeSaveReqVO` 暴露支付状态、收费员、收费时间、收费途径和金额字段。后端对零付、负数、少付和绕过前端的直接请求缺少完整金额覆盖校验,且客户端可指定收费员和收费时间。
### P0-04 柜员结账确认范围与实际范围不一致
前端确认框按当前分页和当前筛选行计算金额,请求只传 `cashierId`;后端收到后结清该收费员全部未结支付记录。页面确认金额不能约束真实落账范围。
### P0-05 预存抵扣无法与柜员实交资金对账
支付主单的 `payment_amount` 记录整张账单金额,柜员结账直接汇总该字段,会把预存抵扣也计入柜员应交资金。支付事实未分别保存渠道实收、预存抵扣和多缴转预存。
### P0-06 余额和支付记录缺少并发、幂等保护
账户余额采用查询后计算再全行更新,没有行锁、版本号或条件原子更新。柜台充值没有业务请求号;同一请求重试可能重复充值。同一账单并发收费也缺少数据库唯一约束。
### P0-07 已结账预存红冲无法成功
已结账预存红冲调用只匹配 `CHARGE_PAYMENT` 的 Mapper 方法;`DEPOSIT_TOPUP` 记录更新数必为零。现有单元测试通过 mock 返回成功,未覆盖真实 SQL 条件。
### P0-08 银行冲正和业务支付状态可能分裂
银行冲正以非空营业账对象作为成功条件。营业账不是可冲状态时可能没有发生业务反向处理,但银行交易仍被标记为已冲正。银行交易状态和业务支付入账也没有可重放的统一状态机。
### P0-09 预存审批回调可重放
审批成功回调缺少完成状态门禁,余额修改发生在支付退款去重之前,并存在绕过账户流水服务直接写余额的路径。重复回调可能重复扣款或重复转账。
## 4. 当前相对正确的链路
- 单线程柜台预存正常路径处于同一事务,并写支付记录、余额和账户流水。
- 单线程单账单收费可以写支付主明细并投影营业账状态。
- 柜台账单收费红冲已有反向支付、预存恢复和账单状态恢复结构。
- 柜员结账已有结账主明细、条件更新和重复结账防护基础。
- 缴费历史主要从 `PaymentRecord` / `PaymentRecordDetail` 读取,而不是完全依赖营业账结果字段。
这些正确性只在合法金额、单线程、合法入口和数据库约束完整部署的条件下成立。
## 5. 基线验证
前端在隔离工作树执行以下相关源码契约测试:
```bash
node --test \
src/views/operatingCharges/counterCharging/counterTopup.contract.test.mjs \
tests/revenue-bugs/counterChargeAndCheckoutDisplay.contract.test.mjs \
tests/rev006/counterCheckoutOldPageInventory.test.mjs
```
结果30 项22 通过、8 失败。失败属于整改前基线,其中既有旧测试与当前代码漂移,也有结账聚合和预存展示契约未对齐。
后端聚合定向测试首次执行超过等待窗口后被中止,未形成通过结论;实施阶段必须按单测试类执行并记录明确退出码。
## 6. 数据排查建议
若当前环境已经存在真实收费数据,实施上线前至少核对:
1. 已收账单的 `extended_amount + late_fee` 是否等于支付分配金额。
2. 支付主单、支付明细与账单投影是否一一对应。
3. 柜员结账金额是否剔除了预存抵扣并包含真实渠道实收。
4. 同客户、同金额、相近时间的预存充值是否存在重复请求。
5. 账户当前余额是否等于期初余额加全部有效账户流水净额。
6. 已冲正银行交易是否都有对应业务反向支付及账单状态恢复。
## 7. P0 整改结果
| 缺陷 | 整改结果 |
|------|----------|
| P0-01 金额口径 | 服务端和前端统一按本金 + 违约金计算;支付主单显式保存渠道实收、预存抵扣和多缴转预存 |
| P0-02 非原子收费 | 新增 `POST /business/charge/counter-charge/submit`,多账单、预存抵扣和多缴转预存在单事务内完成 |
| P0-03 通用支付旁路 | 通用营业账更新禁止财务状态迁移,通用账户更新禁止直接改余额 |
| P0-04 结账范围漂移 | 前端真实维护选择状态,后端只结清请求中的 `paymentRecordIds` |
| P0-05 柜员实交失真 | 结账与汇总优先使用 `channel_amount`,预存抵扣单独展示 |
| P0-06 并发与幂等 | 支付记录增加请求号/批次/唯一约束;账单与账户按序加锁并条件更新 |
| P0-07 预存红冲 | 已结和未结 `DEPOSIT_TOPUP` 分别进入正确状态路径并写反向账户流水 |
| P0-08 银行冲正分裂 | 业务反向未成功时银行交易不再标记已冲正 |
| P0-09 回调重放 | 预存审批完成回调增加状态门禁、正式记录锁和账户服务幂等变更 |
详细命令、退出结果、提交清单和剩余风险见 `docs/evidence/rev003-charging/2026-07-15-p0-verification.md`