Compare commits
No commits in common. "55b9e144efd1151b33cb254623ab17bd08a9160f" and "07c1f9868accdea6e18590c772a6dda509f38b61" have entirely different histories.
55b9e144ef
...
07c1f9868a
@ -1,70 +0,0 @@
|
||||
# 柜台收费缴费后连续预存修复验证
|
||||
|
||||
## 根因
|
||||
|
||||
单客户账单收费成功后,页面保留收讫展示行,并将其标记为 `displayChargeState: 'settled'`;同时已缴账单仍保留在 `selectedChargeRows` 中。
|
||||
|
||||
`noArrearsTopupMode` 要求不存在 `settled` 展示行,因此收费按钮持续禁用,用户无法在缴清账单后立即继续办理预存。保留的选中行还会让上一次账单金额继续参与金额计算。
|
||||
|
||||
## 修复
|
||||
|
||||
- 单客户普通账单收费成功后,将收讫行标记为只读 `history` 展示。
|
||||
- 保留 `payState: 1`、`payStateName: '收讫'` 和 `displayChargeType: 'bill'`,确保账单不可重复选择且详情链路不变。
|
||||
- 收费成功后清空 `selectedChargeRows`,由现有金额同步逻辑清空上一次实收金额。
|
||||
- 页面无待缴账单且仅存在历史展示行时,自动进入 `noArrearsTopupMode`,后续提交调用预存接口。
|
||||
- 集收号收费成功后的 `settled` 展示和选中状态保持原样,未改变其批量收费约束。
|
||||
|
||||
## TDD 证据
|
||||
|
||||
### RED
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
结果:退出码 `1`,9 个测试中 7 个通过、2 个失败。失败分别证明单客户收费成功后未切换为历史展示,以及已缴账单未从选中数据中清除。
|
||||
|
||||
### GREEN
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
结果:退出码 `0`,9/9 通过。
|
||||
|
||||
## 回归验证
|
||||
|
||||
### 柜台收费既有测试
|
||||
|
||||
```bash
|
||||
pnpm test:counter-charging
|
||||
```
|
||||
|
||||
结果:退出码 `0`,4/4 通过。
|
||||
|
||||
### 组合测试
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs tests/operatingCharges/counterChargingInventory.test.mjs
|
||||
```
|
||||
|
||||
结果:退出码 `0`,13/13 通过。
|
||||
|
||||
### 前端构建
|
||||
|
||||
```bash
|
||||
NODE_OPTIONS=--max-old-space-size=8192 pnpm build:dev
|
||||
```
|
||||
|
||||
结果:退出码 `0`,输出 `Build successful. Please see dist directory`。构建过程仅出现项目既有的 SVG symbolId 和 Rollup 注释警告。
|
||||
|
||||
## 代码基线
|
||||
|
||||
- 前端分支:`fix/counter-topup-detail`
|
||||
- RED 测试提交:`188ba232`
|
||||
- 连续预存修复提交:`3c5d6545`
|
||||
- 验证日期:2026-07-14
|
||||
|
||||
## 约束
|
||||
|
||||
按用户要求,未运行 `vue-tsc`、`pnpm ts:check` 或任何会调用 `vue-tsc` 的命令。
|
||||
@ -1,74 +0,0 @@
|
||||
# 柜台收费预存款详情修复验证
|
||||
|
||||
## 根因
|
||||
|
||||
柜台收费页面的预存款展示行使用支付记录 ID,但“详情”按钮统一调用账单详情接口 `/business/charge/get`。支付记录 ID 被误作账单 ID 后,接口无法返回对应预存业务数据,账单详情弹窗中的字段全部显示为 `--`。
|
||||
|
||||
## 修复
|
||||
|
||||
- 按 `displayChargeType` 分流普通账单与预存款详情。
|
||||
- 预存款不再调用账单详情接口,改用专用弹窗展示客户、支付和余额快照。
|
||||
- 历史预存记录保留 `lastDeposit`、`deposit`、预存金额、收费时间和收费方式。
|
||||
- 刚办理成功的预存优先使用重新查询到的持久化记录;查询未恢复记录时使用成功响应降级展示。
|
||||
- 普通账单继续使用原 `/business/charge/get` 详情链路。
|
||||
- 金额或余额为 `0` 时显示 `0.00`,仅空值显示 `--`。
|
||||
|
||||
## TDD 证据
|
||||
|
||||
### RED
|
||||
|
||||
命令:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
结果:退出码 `1`,共 7 个测试,原有 4 个通过,新增 3 个失败。失败分别证明缺少预存款详情分流、专用字段与余额映射、零值格式化。
|
||||
|
||||
### GREEN
|
||||
|
||||
命令:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
结果:退出码 `0`,7/7 通过。
|
||||
|
||||
## 回归验证
|
||||
|
||||
### 柜台收费既有测试
|
||||
|
||||
```bash
|
||||
pnpm test:counter-charging
|
||||
```
|
||||
|
||||
结果:退出码 `0`,4/4 通过。
|
||||
|
||||
### 组合测试
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs tests/operatingCharges/counterChargingInventory.test.mjs
|
||||
```
|
||||
|
||||
结果:退出码 `0`,11/11 通过。
|
||||
|
||||
### 前端构建
|
||||
|
||||
```bash
|
||||
NODE_OPTIONS=--max-old-space-size=8192 pnpm build:dev
|
||||
```
|
||||
|
||||
结果:退出码 `0`,输出 `Build successful. Please see dist directory`。构建过程中存在项目既有的 SVG symbolId 和 Rollup 注释警告,但未导致构建失败。
|
||||
|
||||
## 代码基线
|
||||
|
||||
- 前端分支:`fix/counter-topup-detail`
|
||||
- RED 测试提交:`2aeecaa8`
|
||||
- 余额快照提交:`83142c72`
|
||||
- 专用详情实现提交:`c6941001`
|
||||
- 验证日期:2026-07-14
|
||||
|
||||
## 约束
|
||||
|
||||
按用户要求,未运行 `vue-tsc`、`pnpm ts:check` 或任何会调用 `vue-tsc` 的命令。
|
||||
@ -1,52 +0,0 @@
|
||||
# 柜台预存持久回显修复验证证据
|
||||
|
||||
- 验证日期:2026-07-14
|
||||
- 后端基线:`06ed57830fe0ccd50f0a74e3e5cf02114b0ace45`
|
||||
- 后端修复提交:`27c27aacc96f9ad139b966bf3ea400643859efcb`
|
||||
- 前端基线:`a0239b384f7e072c11c67ae6d405980260f4af14`
|
||||
- 前端修复提交:`c73a13b6e234d146ea3e667643ab7012d9078a4d`
|
||||
- 设计:`docs/superpowers/specs/2026-07-13-counter-topup-persistent-display-design.md`
|
||||
- 计划:`docs/superpowers/plans/2026-07-14-counter-topup-persistent-display.md`
|
||||
|
||||
## 实现结果
|
||||
|
||||
1. 客户缴费历史不再截断为最新一条,按有效记录完整分页并保持汇总、导出一致。
|
||||
2. 新版缴费记录查询增加可选 `bizScene`,柜台页面用 `DEPOSIT_TOPUP` 精确查询最近有效预存。
|
||||
3. 新柜台预存先生成支付主单,再把 `paymentRecordId` 写入账户流水 `payDetailId`。
|
||||
4. 缴费历史通过账户流水展示新预存的期初、期末余额;旧记录没有可靠关联时保持空值。
|
||||
5. 无欠费客户重新查询时可从后端恢复最近预存行,不依赖前端临时缓存或浏览器存储。
|
||||
6. 恢复的历史预存行不可勾选,也不阻塞客户再次办理预存。
|
||||
|
||||
## TDD 红绿证据
|
||||
|
||||
| 场景 | 修复前失败 | 修复后结果 |
|
||||
|---|---|---|
|
||||
| 完整缴费历史 | 期望 `2` 条,实际仅 `1` 条 | PASS |
|
||||
| 业务场景过滤 | 请求 VO 不存在 `bizScene` | PASS |
|
||||
| 预存流水关联 | 支付主单在余额增加之后生成,流水 `payDetailId` 为空 | PASS |
|
||||
| 余额快照 | 期望期初 `10.00`,实际为 `null` | PASS |
|
||||
| 前端持久回显 | 新增契约首次执行 `0/3` 通过 | 最终 `4/4` PASS |
|
||||
|
||||
## 验证结果
|
||||
|
||||
| 范围 | 命令 | 结果 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 后端定向单测 | `mvn -q -pl sw-business/sw-business-server -am -Dtest=PaymentQueryServiceTest,ChargeServiceCounterPaymentTest,PaymentRecordServiceImplTest,CounterSettleApplicationServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test` | PASS | 73 个测试,0 失败,0 错误 |
|
||||
| 后端编译 | `mvn -q -pl sw-business/sw-business-server -am -DskipTests compile` | PASS | 退出码 `0` |
|
||||
| 前端新增契约 | `node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs` | PASS | 4 个测试全部通过 |
|
||||
| 前端既有柜台契约 | `pnpm test:counter-charging` | PASS | 4 个测试全部通过 |
|
||||
| 前端路由恢复契约 | `node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs tests/operatingCharges/counterChargingRouteRestore.test.mjs` | PARTIAL | 6 个测试中 5 个通过;既有 URL 搜索状态持久化测试仍失败,与本次预存回显修改无关 |
|
||||
| 前端构建(默认堆) | `pnpm build:dev` | FAIL | Node 默认约 4 GB 堆触发 OOM,退出码 `134`,未出现源码编译错误 |
|
||||
| 前端构建(8 GB 堆) | `NODE_OPTIONS=--max-old-space-size=8192 pnpm build:dev` | PASS | 输出 `Build successful. Please see dist directory` |
|
||||
|
||||
## 未使用的验证入口
|
||||
|
||||
- 不运行 `vue-tsc` / `pnpm ts:check`。用户已明确要求本项目后续不要运行 `vue-tsc`,因此不将其作为本次验收入口。
|
||||
|
||||
## 已知边界
|
||||
|
||||
- 旧预存支付记录没有可靠 `payDetailId` 时,期初、期末余额显示 `-`,不按金额和时间模糊匹配。
|
||||
- 主副卡付款户解析和柜台有效余额读取不在本次修复范围。
|
||||
- 柜台收费路由恢复测试的既有失败仍待独立任务修复。
|
||||
- 前端全量构建需要显式提高 Node 堆上限;默认堆配置仍可能 OOM。
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
# LATE_FEE_DECIMAL config verification and fix
|
||||
|
||||
Date: 2026-07-22
|
||||
|
||||
## Scope
|
||||
|
||||
- Daily late-fee recalculation now rounds each charge detail late-fee item by `LATE_FEE_DECIMAL` before summing.
|
||||
- Accounting split adjustment write-back now allocates child `lateFee` by `LATE_FEE_DECIMAL` for both water-ratio split and fee-component split.
|
||||
- Unsold adjustment and split adjustment frontend forms no longer expose the inactive "calculate late fee" switch. Split water preview also no longer shows an always-empty child late-fee column.
|
||||
|
||||
## Non-scope
|
||||
|
||||
- Payment, bank reconciliation, and list/detail display flows continue to use the stored `lateFee` amount and existing money display rules.
|
||||
- Unsold/open-bill/manual billing style flows that only set `lateFeeBeginDate` remain unchanged; they do not calculate late-fee amount in that path.
|
||||
|
||||
## Verification
|
||||
|
||||
- `mvn -q -pl sw-business/sw-business-server -am -Dtest=LateFeeConfigResolverTest,LateFeeDailyCalculatorTest,LateFeeRecalculationServiceTest,LateFeeDateModeCalculatorTest,AccountingAdjustActionServiceImplTest test` passed.
|
||||
- `node --test src/views/accountProcess/unsoldAdjustment/components/UnsoldAdjustmentForm.contract.test.mjs src/views/accountProcess/unsoldAdjustment/components/SplitAdjustmentForm.contract.test.mjs` passed: 11/11.
|
||||
- `git diff --check` passed in `water-backend` and `water-frontend`.
|
||||
- `NODE_OPTIONS=--max-old-space-size=8192 pnpm ts:check --pretty false` still fails on existing project-wide TypeScript errors; the captured log had no errors for `UnsoldAdjustmentForm.vue` or `SplitAdjustmentForm.vue`.
|
||||
@ -1,32 +0,0 @@
|
||||
# REV004 已销/核销调整 BPM 与账务日志修复证据
|
||||
|
||||
日期:2026-06-30
|
||||
|
||||
## 背景
|
||||
|
||||
现场反馈:提交已销调整申请后,审批开关开启但工单/审批流程中无记录;点击撤销成功后,账务日志中也看不到撤销结果。
|
||||
|
||||
## 修复点
|
||||
|
||||
- `sold-submit` 不再只写业务 formal 记录,改为调用 BPM 核销调整流程 API 创建工单与流程实例。
|
||||
- 新增 `WrittenoffAdjustBpmApi`,对齐价差调整、呆坏账等已有 BPM API 调用模式。
|
||||
- BPM 核销调整保存 VO 支持透传 `adjustmentNo`,保证兼容日志、formal 记录、BPM 工单使用同一调整单号。
|
||||
- 账务日志查询增加 `revokeOfAdjustmentNo` 聚合:撤销日志会覆盖原申请单的展示状态、处理人、处理时间与撤销成功文案。
|
||||
|
||||
## 验证
|
||||
|
||||
命令:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=AccountingAdjustProcessServiceImplTest#createSoldAction_shouldCreateWrittenoffFormalPendingRecord,AccountingAdjustLogProcessServiceImplTest#getLogPage_shouldOverlaySyntheticRevokeOnOriginalAdjustment test
|
||||
```
|
||||
|
||||
结果:`BUILD SUCCESS`,`Tests run: 2, Failures: 0, Errors: 0, Skipped: 0`。
|
||||
|
||||
命令:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-module-bpm/sw-module-bpm-server -am -DskipTests compile
|
||||
```
|
||||
|
||||
结果:`BUILD SUCCESS`。
|
||||
@ -1,197 +0,0 @@
|
||||
# Counter Charge Then Topup Continuation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 让单客户普通账单缴费成功后保留收讫记录,同时立即进入可继续预存的状态。
|
||||
|
||||
**Architecture:** 将单客户刚缴纳的账单保存为只读 `history` 展示行,并清空 `selectedChargeRows`。现有 `noArrearsTopupMode` 会忽略历史展示行并自动切换到预存提交路径;集收号路径不调整。
|
||||
|
||||
**Tech Stack:** Vue 3、TypeScript、Node.js `node:test`、现有柜台收费状态模型
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `water-frontend/tests/operatingCharges/counterChargingPersistentTopup.test.mjs`
|
||||
- 增加缴费后连续预存的状态契约测试。
|
||||
- Modify: `water-frontend/src/views/operatingCharges/counterCharging/index.vue`
|
||||
- 允许收讫账单使用历史展示状态,并在单客户缴费成功后清空选中行。
|
||||
- Create: `water-docs/docs/evidence/bugfix/2026-07-14-counter-charge-then-topup-continuation.md`
|
||||
- 记录根因、TDD 和回归验证结果。
|
||||
|
||||
### Task 1: Reproduce the blocked continuation state
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-frontend/tests/operatingCharges/counterChargingPersistentTopup.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Add a failing test for single-customer post-payment state**
|
||||
|
||||
```js
|
||||
test('single-customer payment keeps paid bills as history and clears the payable selection', () => {
|
||||
const submitStart = pageSource.indexOf('const confirmCharge = async')
|
||||
const submitEnd = pageSource.indexOf('const editCustomer =')
|
||||
const submitSection = pageSource.slice(submitStart, submitEnd)
|
||||
|
||||
assert.match(
|
||||
submitSection,
|
||||
/setSettledChargeDisplayRows\(settledChargeRowsSnapshot,\s*'history'\)[\s\S]*selectedChargeRows\.value\s*=\s*\[\]/
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add a failing test for history-row semantics and unchanged hub behavior**
|
||||
|
||||
```js
|
||||
test('paid bill history remains read-only while hub charging keeps its existing settled flow', () => {
|
||||
assert.match(
|
||||
pageSource,
|
||||
/const setSettledChargeDisplayRows = \([\s\S]*displayChargeState:\s*ChargeDisplayRowVO\['displayChargeState'\] = 'settled'[\s\S]*displayChargeState,[\s\S]*displayChargeType:\s*displayType/
|
||||
)
|
||||
assert.match(pageSource, /payState:\s*1,[\s\S]*payStateName:\s*'收讫'/)
|
||||
|
||||
const submitStart = pageSource.indexOf('const confirmCharge = async')
|
||||
const submitEnd = pageSource.indexOf('const editCustomer =')
|
||||
const submitSection = pageSource.slice(submitStart, submitEnd)
|
||||
assert.match(
|
||||
submitSection,
|
||||
/if \(isHubChargeMode\.value\) \{[\s\S]*setSettledHubChargeDisplayRows\(settledChargeRowsSnapshot\)[\s\S]*selectedChargeRows\.value = \[\.\.\.settledChargeRowsSnapshot\]/
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the focused test and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
Expected: 9 个测试中原有 7 个通过,新增 2 个因缺少历史状态参数和选中行清理而失败。
|
||||
|
||||
- [ ] **Step 4: Commit the RED test**
|
||||
|
||||
```bash
|
||||
git add tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
git commit -m "test: reproduce blocked charge then topup flow"
|
||||
```
|
||||
|
||||
### Task 2: Convert paid bills to non-blocking history rows
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-frontend/src/views/operatingCharges/counterCharging/index.vue`
|
||||
|
||||
- [ ] **Step 1: Parameterize the paid-bill display state**
|
||||
|
||||
将 `setSettledChargeDisplayRows` 改为:
|
||||
|
||||
```ts
|
||||
const setSettledChargeDisplayRows = (
|
||||
rows: CounterChargingChargeVO[],
|
||||
displayChargeState: ChargeDisplayRowVO['displayChargeState'] = 'settled',
|
||||
displayType: ChargeDisplayRowVO['displayChargeType'] = 'bill'
|
||||
) => {
|
||||
settledChargeDisplayRows.value = rows.map((row) => ({
|
||||
...row,
|
||||
payState: 1,
|
||||
payStateName: '收讫',
|
||||
displayChargeState,
|
||||
displayChargeType: displayType
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Change only the single-customer successful payment branch**
|
||||
|
||||
将普通账单收费成功分支改为:
|
||||
|
||||
```ts
|
||||
} else {
|
||||
setSettledChargeDisplayRows(settledChargeRowsSnapshot, 'history')
|
||||
selectedChargeRows.value = []
|
||||
}
|
||||
syncActualAmountFromSelection()
|
||||
```
|
||||
|
||||
集收号分支继续保留:
|
||||
|
||||
```ts
|
||||
setSettledHubChargeDisplayRows(settledChargeRowsSnapshot)
|
||||
selectedChargeRows.value = [...settledChargeRowsSnapshot]
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the focused test and verify GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
Expected: 9/9 通过。
|
||||
|
||||
- [ ] **Step 4: Run the existing counter-charging regression test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm test:counter-charging
|
||||
```
|
||||
|
||||
Expected: 4/4 通过。
|
||||
|
||||
- [ ] **Step 5: Commit the implementation**
|
||||
|
||||
```bash
|
||||
git add src/views/operatingCharges/counterCharging/index.vue
|
||||
git commit -m "fix: allow topup after counter payment"
|
||||
```
|
||||
|
||||
### Task 3: Verify and record evidence
|
||||
|
||||
**Files:**
|
||||
- Create: `water-docs/docs/evidence/bugfix/2026-07-14-counter-charge-then-topup-continuation.md`
|
||||
|
||||
- [ ] **Step 1: Run the combined focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs tests/operatingCharges/counterChargingInventory.test.mjs
|
||||
```
|
||||
|
||||
Expected: 13/13 通过。
|
||||
|
||||
- [ ] **Step 2: Run the frontend build**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
NODE_OPTIONS=--max-old-space-size=8192 pnpm build:dev
|
||||
```
|
||||
|
||||
Expected: 退出码 `0`,输出 `Build successful. Please see dist directory`。
|
||||
|
||||
不得运行 `vue-tsc`、`pnpm ts:check` 或任何会调用 `vue-tsc` 的命令。
|
||||
|
||||
- [ ] **Step 3: Check the final diff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --check develop...HEAD
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: 无空白错误,前端 worktree 干净。
|
||||
|
||||
- [ ] **Step 4: Record exact evidence**
|
||||
|
||||
证据文档记录:阻塞根因、RED 结果 7 通过/2 失败、GREEN 结果 9/9、组合测试 13/13、构建成功、集收号路径未调整,以及未运行 `vue-tsc`。
|
||||
|
||||
- [ ] **Step 5: Commit evidence**
|
||||
|
||||
```bash
|
||||
git add docs/evidence/bugfix/2026-07-14-counter-charge-then-topup-continuation.md
|
||||
git commit -m "docs: record charge then topup verification"
|
||||
```
|
||||
@ -1,382 +0,0 @@
|
||||
# Counter Topup Detail Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 修复柜台收费预存款记录点击详情后全部显示 `--` 的问题,为预存款提供独立且语义正确的详情展示。
|
||||
|
||||
**Architecture:** 前端根据展示行的 `displayChargeType` 分流详情链路。普通账单继续查询 `/business/charge/get`;预存款直接使用支付记录快照与当前客户资料打开专用详情弹窗,不把支付记录 ID 当作账单 ID。
|
||||
|
||||
**Tech Stack:** Vue 3、TypeScript、Element Plus、现有 Axios API 封装、Node.js `node:test`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `water-frontend/src/views/operatingCharges/counterCharging/index.vue`
|
||||
- 扩充预存展示行字段、映射支付余额快照、详情类型分流、增加预存款专用弹窗。
|
||||
- Modify: `water-frontend/tests/operatingCharges/counterChargingPersistentTopup.test.mjs`
|
||||
- 增加预存款详情分流、字段展示、余额映射和普通账单链路不回归的契约测试。
|
||||
- Create: `water-docs/docs/evidence/bugfix/2026-07-14-counter-topup-detail.md`
|
||||
- 记录根因、RED/GREEN 结果、构建结果与未运行 `vue-tsc` 的约束。
|
||||
|
||||
### Task 1: Add failing topup-detail contract tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-frontend/tests/operatingCharges/counterChargingPersistentTopup.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Add a failing test for detail routing**
|
||||
|
||||
在现有测试文件中增加源码契约断言,要求 `openChargeDetail` 对 `displayChargeType === 'topup'` 分流,并且仅普通账单调用 `getChargeById`:
|
||||
|
||||
```js
|
||||
test('counter charging routes topup rows to a dedicated payment detail', () => {
|
||||
assert.match(
|
||||
source,
|
||||
/if \(row\.displayChargeType === 'topup'\) \{[\s\S]*openTopupDetail\(row\)[\s\S]*return[\s\S]*getChargeById\(row\.id\)/
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add a failing test for dedicated fields and balance snapshots**
|
||||
|
||||
```js
|
||||
test('topup detail shows payment semantics and keeps balance snapshots', () => {
|
||||
for (const label of ['预存款详情', '业务类型', '支付记录ID', '预存金额', '期初余额', '期末余额', '收费时间', '收费方式', '收费状态']) {
|
||||
assert.match(source, new RegExp(label))
|
||||
}
|
||||
assert.match(source, /lastDeposit:\s*record\.lastDeposit/)
|
||||
assert.match(source, /deposit:\s*record\.deposit/)
|
||||
assert.match(source, /displayTopupMoney\(topupDetailData\?\.lastDeposit\)/)
|
||||
assert.match(source, /displayTopupMoney\(topupDetailData\?\.deposit\)/)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add a failing test for preserving normal bill details and zero values**
|
||||
|
||||
```js
|
||||
test('normal bills keep their original detail request and zero topup values remain visible', () => {
|
||||
assert.match(source, /chargeDetailData\.value = await getChargeById\(row\.id\)/)
|
||||
assert.match(source, /value === null \|\| value === undefined \|\| value === ''/)
|
||||
assert.match(source, /Number\(value\)\.toFixed\(2\)/)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the focused test and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL on the new detail-routing/detail-fields assertions because no topup-specific detail path exists yet.
|
||||
|
||||
- [ ] **Step 5: Commit the RED test**
|
||||
|
||||
```bash
|
||||
git add tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
git commit -m "test: reproduce empty counter topup detail"
|
||||
```
|
||||
|
||||
### Task 2: Preserve complete topup display data
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-frontend/src/views/operatingCharges/counterCharging/index.vue`
|
||||
|
||||
- [ ] **Step 1: Extend the display-row type with payment snapshots**
|
||||
|
||||
```ts
|
||||
type ChargeDisplayRowVO = CounterChargingChargeVO & {
|
||||
displayChargeState?: 'settled' | 'history'
|
||||
displayChargeType?: 'bill' | 'topup'
|
||||
payTime?: string
|
||||
chargeWay?: number
|
||||
lastDeposit?: number
|
||||
deposit?: number
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Extend the topup-row builder input and output**
|
||||
|
||||
```ts
|
||||
const setSettledTopupDisplayRow = (record: {
|
||||
id: number
|
||||
amount: number
|
||||
payTime: string
|
||||
chargeWay: number
|
||||
lastDeposit?: number
|
||||
deposit?: number
|
||||
}, displayChargeState: 'settled' | 'history' = 'settled') => {
|
||||
const current = currentCustomer.value
|
||||
settledChargeDisplayRows.value = [
|
||||
{
|
||||
id: record.id,
|
||||
meterId: 0,
|
||||
recordId: 0,
|
||||
billMonth: '预存款',
|
||||
custId: current?.id || 0,
|
||||
custCode: current?.custCode || current?.code,
|
||||
custName: current?.custName || current?.name,
|
||||
custAddress: current?.custAddress || current?.address,
|
||||
billAmount: record.amount,
|
||||
lateFee: 0,
|
||||
extendedAmount: record.amount,
|
||||
payState: 1,
|
||||
payStateName: '收讫',
|
||||
meterCode: '预存',
|
||||
recordNo: '预存',
|
||||
payTime: record.payTime,
|
||||
chargeWay: record.chargeWay,
|
||||
lastDeposit: record.lastDeposit,
|
||||
deposit: record.deposit,
|
||||
displayChargeState,
|
||||
displayChargeType: 'topup'
|
||||
} as ChargeDisplayRowVO
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
客户地址使用 `current?.custAddress || current?.address`;金额继续放在 `billAmount` 和 `extendedAmount`,余额快照分别放入 `lastDeposit`、`deposit`。
|
||||
|
||||
- [ ] **Step 3: Map persisted payment snapshots**
|
||||
|
||||
在 `loadLatestCounterTopupDisplay` 中传入:
|
||||
|
||||
```ts
|
||||
setSettledTopupDisplayRow({
|
||||
id: record.id,
|
||||
amount: Number(record.actualMoney ?? 0),
|
||||
payTime: record.payDate || record.creationTime || '',
|
||||
chargeWay: Number(record.chargeWay ?? 1),
|
||||
lastDeposit: record.lastDeposit,
|
||||
deposit: record.deposit
|
||||
}, 'history')
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Avoid overwriting the persisted row after an immediate topup**
|
||||
|
||||
收费成功刷新后,只有在 `loadCustomerChargeData` 未恢复任何预存行时才用成功响应建立降级行:
|
||||
|
||||
```ts
|
||||
if (!settledChargeDisplayRows.value.length && counterTopupResult?.paymentRecordId) {
|
||||
setSettledTopupDisplayRow({
|
||||
id: counterTopupResult.paymentRecordId,
|
||||
amount: Number(counterTopupResult.amount ?? enteredAmount.value),
|
||||
payTime: counterTopupResult.payTime || payDate,
|
||||
chargeWay: chargeWayKeyToValueMap[payType.value as keyof typeof chargeWayKeyToValueMap] || 1,
|
||||
deposit: counterTopupResult.balanceAfter
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the focused test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
Expected: detail-routing test remains FAIL; balance-mapping assertions PASS.
|
||||
|
||||
- [ ] **Step 6: Commit the data mapping**
|
||||
|
||||
```bash
|
||||
git add src/views/operatingCharges/counterCharging/index.vue tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
git commit -m "fix: preserve counter topup detail snapshots"
|
||||
```
|
||||
|
||||
### Task 3: Add the dedicated topup-detail dialog and routing
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-frontend/src/views/operatingCharges/counterCharging/index.vue`
|
||||
|
||||
- [ ] **Step 1: Add topup-detail state and zero-safe formatting**
|
||||
|
||||
```ts
|
||||
const showTopupDetailDialog = ref(false)
|
||||
const topupDetailData = ref<ChargeDisplayRowVO | null>(null)
|
||||
|
||||
const displayTopupMoney = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') return '--'
|
||||
const amount = Number(value)
|
||||
return Number.isFinite(amount) ? amount.toFixed(2) : '--'
|
||||
}
|
||||
|
||||
const getChargeWayLabel = (value: unknown) => {
|
||||
const chargeWay = Number(value)
|
||||
return chargeWayLabelMap.value[chargeWay] || '--'
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the dedicated dialog**
|
||||
|
||||
在账单详情弹窗后增加以下完整弹窗:
|
||||
|
||||
```vue
|
||||
<el-dialog v-model="showTopupDetailDialog" title="预存款详情" width="760px">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="客户编号">{{ topupDetailData?.custCode || '--' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户名称">{{ topupDetailData?.custName || '--' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户地址">{{ topupDetailData?.custAddress || '--' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="业务类型">预存款</el-descriptions-item>
|
||||
<el-descriptions-item label="支付记录ID">{{ topupDetailData?.id || '--' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预存金额">{{ displayTopupMoney(topupDetailData?.billAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="期初余额">{{ displayTopupMoney(topupDetailData?.lastDeposit) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="期末余额">{{ displayTopupMoney(topupDetailData?.deposit) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收费时间">{{ formatChargeDateTime(topupDetailData?.payTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收费方式">{{ getChargeWayLabel(topupDetailData?.chargeWay) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收费状态">收讫</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="showTopupDetailDialog = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Route topup rows before the bill request**
|
||||
|
||||
```ts
|
||||
const openTopupDetail = (row: ChargeDisplayRowVO) => {
|
||||
if (!row.id) {
|
||||
ElMessage.warning('预存记录信息不完整')
|
||||
return
|
||||
}
|
||||
topupDetailData.value = row
|
||||
showTopupDetailDialog.value = true
|
||||
}
|
||||
|
||||
const openChargeDetail = async (row: ChargeDisplayRowVO) => {
|
||||
if (row.displayChargeType === 'topup') {
|
||||
openTopupDetail(row)
|
||||
return
|
||||
}
|
||||
if (!row?.id) return
|
||||
chargeDetailLoading.value = true
|
||||
showChargeDetailDialog.value = true
|
||||
try {
|
||||
chargeDetailData.value = await getChargeById(row.id)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '查看账单详情失败')
|
||||
showChargeDetailDialog.value = false
|
||||
} finally {
|
||||
chargeDetailLoading.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Clear topup-detail state with the page state**
|
||||
|
||||
在 `resetCustomerChargeState` 与 `onPageActivated` 的无恢复参数分支中加入:
|
||||
|
||||
```ts
|
||||
showTopupDetailDialog.value = false
|
||||
topupDetailData.value = null
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the focused test and verify GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS, including all pre-existing topup persistence tests and new detail tests.
|
||||
|
||||
- [ ] **Step 6: Run the existing counter-charging regression test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm test:counter-charging
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Commit the UI fix**
|
||||
|
||||
```bash
|
||||
git add src/views/operatingCharges/counterCharging/index.vue tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
git commit -m "fix: show counter topup payment details"
|
||||
```
|
||||
|
||||
### Task 4: Verify and record evidence
|
||||
|
||||
**Files:**
|
||||
- Create: `water-docs/docs/evidence/bugfix/2026-07-14-counter-topup-detail.md`
|
||||
|
||||
- [ ] **Step 1: Run focused tests together**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs tests/operatingCharges/counterChargingInventory.test.mjs
|
||||
```
|
||||
|
||||
Expected: all focused tests PASS.
|
||||
|
||||
- [ ] **Step 2: Run the frontend build with sufficient heap**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
NODE_OPTIONS=--max-old-space-size=8192 pnpm build:dev
|
||||
```
|
||||
|
||||
Expected: `Build successful. Please see dist directory`.
|
||||
|
||||
Do not run `vue-tsc`, `pnpm ts:check`, or any command that invokes `vue-tsc`.
|
||||
|
||||
- [ ] **Step 3: Check the diff and worktree**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: no whitespace errors; only intentional files are present before the final commit.
|
||||
|
||||
- [ ] **Step 4: Write verification evidence**
|
||||
|
||||
全部验证达到本计划预期后,创建以下证据;若实际结果不同,停止提交证据并先处理差异:
|
||||
|
||||
```markdown
|
||||
# 柜台收费预存款详情修复验证
|
||||
|
||||
## 根因
|
||||
|
||||
预存款展示行使用支付记录 ID,但详情按钮统一调用账单详情接口 `/business/charge/get`,导致支付记录 ID 被误作账单 ID,详情字段全部显示为 `--`。
|
||||
|
||||
## 修复
|
||||
|
||||
- 按 `displayChargeType` 分流普通账单与预存款详情。
|
||||
- 预存款使用专用弹窗展示支付及余额快照。
|
||||
- 普通账单继续使用原账单详情接口。
|
||||
|
||||
## TDD 证据
|
||||
|
||||
- RED:`node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs`
|
||||
- 结果:7 个测试中原有 4 个通过,新增 3 个按预期失败,失败原因分别为缺少详情分流、专用字段和零值格式化。
|
||||
- GREEN:`node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs`
|
||||
- 结果:7/7 通过。
|
||||
|
||||
## 回归验证
|
||||
|
||||
- `pnpm test:counter-charging`:退出码 0,4/4 通过。
|
||||
- `node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs tests/operatingCharges/counterChargingInventory.test.mjs`:退出码 0,11/11 通过。
|
||||
- `NODE_OPTIONS=--max-old-space-size=8192 pnpm build:dev`:退出码 0,输出 `Build successful. Please see dist directory`。
|
||||
|
||||
## 约束
|
||||
|
||||
按用户要求,未运行 `vue-tsc`、`pnpm ts:check` 或任何会调用 `vue-tsc` 的命令。
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit evidence**
|
||||
|
||||
```bash
|
||||
git add docs/evidence/bugfix/2026-07-14-counter-topup-detail.md
|
||||
git commit -m "docs: record counter topup detail verification"
|
||||
```
|
||||
@ -1,640 +0,0 @@
|
||||
# 柜台预存持久回显与缴费历史修复实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 修复柜台预存只在成功后临时可见的问题,使重新查询能够从支付记录恢复最近有效预存,并让客户缴费历史完整分页且为新预存展示可靠的期初、期末余额。
|
||||
|
||||
**Architecture:** 后端以 `biz_payment_record` 作为历史主数据源,取消“仅最新一条”截断,增加 `bizScene` 精确过滤;新柜台预存先创建支付主单,再把支付主单 ID 写到账户流水 `payDetailId`,查询层批量映射余额快照。前端在无待缴账单时调用持久化支付记录接口恢复最近预存,成功后的即时展示也使用真实支付主单 ID。
|
||||
|
||||
**Tech Stack:** Java 17、Spring Boot、MyBatis-Plus、JUnit 5、Mockito、Vue 3、TypeScript、Element Plus、Node.js `node:test`、pnpm
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
后端修改:
|
||||
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/PaymentRecordPageNewReqVO.java`:增加可选业务场景过滤字段。
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java`:返回完整有效历史、应用场景过滤、批量读取账户流水并映射余额。
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java`:调整柜台预存事务内顺序,并把支付主单 ID 写入账户流水。
|
||||
- `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java`:覆盖完整历史、红冲过滤、场景过滤、余额映射和导出。
|
||||
- `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceCounterPaymentTest.java`:覆盖支付主单先创建及账户流水关联。
|
||||
|
||||
前端修改:
|
||||
|
||||
- `src/api/operatingCharges/counterCharging/index.ts`:增加最近有效预存查询类型与 API。
|
||||
- `src/views/operatingCharges/counterCharging/index.vue`:从持久化记录恢复预存展示行,使用真实支付主单 ID,并确保历史行不进入收费选择。
|
||||
- `tests/operatingCharges/counterChargingPersistentTopup.test.mjs`:验证接口参数和页面恢复契约。
|
||||
|
||||
验证证据:
|
||||
|
||||
- `docs/evidence/bugfix/2026-07-14-counter-topup-persistent-display.md`:记录命令、退出码、通过数量和未覆盖边界。
|
||||
|
||||
### Task 1: 后端缴费历史返回全部有效记录
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java`
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/PaymentRecordPageNewReqVO.java`
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java`
|
||||
|
||||
- [ ] **Step 1: 修改现有测试,要求返回全部有效记录**
|
||||
|
||||
将 `getPaymentRecordPageNew_shouldOnlyExposeLatestVisiblePaymentRecord` 改为:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void getPaymentRecordPageNew_shouldExposeAllVisiblePaymentRecords() {
|
||||
// 保留 latestReversed、latestVisible、olderVisible 三条测试数据和既有依赖桩。
|
||||
when(paymentRecordMapper.selectList(any()))
|
||||
.thenReturn(List.of(latestReversed, latestVisible, olderVisible));
|
||||
when(paymentRecordDetailMapper.selectByPaymentRecordIds(List.of(91L, 92L)))
|
||||
.thenReturn(List.of(detail));
|
||||
|
||||
PaymentRecordPageNewReqVO reqVO = new PaymentRecordPageNewReqVO();
|
||||
reqVO.setCustId(703L);
|
||||
reqVO.setSkipCount(0);
|
||||
reqVO.setMaxResultCount(10);
|
||||
|
||||
var result = paymentQueryService.getPaymentRecordPageNew(reqVO);
|
||||
|
||||
assertEquals(2L, result.getPaySubtotalPageListDto().getTotalCount());
|
||||
assertEquals(List.of(91L, 92L), result.getPaySubtotalPageListDto().getItems().stream()
|
||||
.map(PaymentRecordPageNewRespVO.PaySubtotalItem::getId)
|
||||
.toList());
|
||||
assertEquals(new BigDecimal("47.60"), result.getTotalAmount());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行单测并确认按旧行为失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am \
|
||||
-Dtest=PaymentQueryServiceTest#getPaymentRecordPageNew_shouldExposeAllVisiblePaymentRecords \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL,`totalCount` 实际为 `1` 而期望为 `2`。
|
||||
|
||||
- [ ] **Step 3: 实现完整候选记录返回**
|
||||
|
||||
在请求 VO 增加:
|
||||
|
||||
```java
|
||||
@Schema(description = "业务场景", example = "DEPOSIT_TOPUP")
|
||||
private String bizScene;
|
||||
```
|
||||
|
||||
将查询方法统一改名为 `selectVisiblePaymentRecordCandidates`,并实现:
|
||||
|
||||
```java
|
||||
private List<PaymentRecordDO> selectVisiblePaymentRecordCandidates(PaymentRecordPageNewReqVO pageReqVO) {
|
||||
return paymentRecordMapper.selectList(new LambdaQueryWrapperX<PaymentRecordDO>()
|
||||
.eqIfPresent(PaymentRecordDO::getCustId, pageReqVO.getCustId())
|
||||
.eqIfPresent(PaymentRecordDO::getBizScene, pageReqVO.getBizScene())
|
||||
.geIfPresent(PaymentRecordDO::getPayTime, pageReqVO.getSTime())
|
||||
.leIfPresent(PaymentRecordDO::getPayTime, pageReqVO.getETime())
|
||||
.orderByDesc(PaymentRecordDO::getPayTime, PaymentRecordDO::getId))
|
||||
.stream()
|
||||
.filter(this::isVisiblePaymentRecord)
|
||||
.filter(record -> pageReqVO.getBizScene() == null
|
||||
|| Objects.equals(pageReqVO.getBizScene(), record.getBizScene()))
|
||||
.toList();
|
||||
}
|
||||
```
|
||||
|
||||
`getPaymentRecordPageNew()` 和 `getPaymentRecordExportList()` 均调用新方法,不再执行 `findFirst()`。
|
||||
|
||||
- [ ] **Step 4: 运行测试确认变绿**
|
||||
|
||||
Run: 与 Step 2 相同。
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 5: 增加场景过滤和导出回归测试**
|
||||
|
||||
新增测试:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void getPaymentRecordPageNew_shouldFilterByBizScene() {
|
||||
PaymentRecordDO topup = buildVisibleRecord(101L, "DEPOSIT_TOPUP", new BigDecimal("5.00"));
|
||||
PaymentRecordDO charge = buildVisibleRecord(102L, "CHARGE_PAYMENT", new BigDecimal("20.00"));
|
||||
when(paymentRecordMapper.selectList(any())).thenReturn(List.of(topup, charge));
|
||||
|
||||
PaymentRecordPageNewReqVO reqVO = new PaymentRecordPageNewReqVO();
|
||||
reqVO.setCustId(703L);
|
||||
reqVO.setBizScene("DEPOSIT_TOPUP");
|
||||
|
||||
var result = paymentQueryService.getPaymentRecordPageNew(reqVO);
|
||||
|
||||
assertEquals(1L, result.getPaySubtotalPageListDto().getTotalCount());
|
||||
assertEquals(101L, result.getPaySubtotalPageListDto().getItems().get(0).getId());
|
||||
assertEquals(1, result.getTopUpCount());
|
||||
assertEquals(new BigDecimal("5.00"), result.getTopUpTotalMoney());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPaymentRecordExportList_shouldExportAllVisibleRecords() {
|
||||
PaymentRecordDO first = buildVisibleRecord(101L, "DEPOSIT_TOPUP", new BigDecimal("5.00"));
|
||||
PaymentRecordDO second = buildVisibleRecord(102L, "CHARGE_PAYMENT", new BigDecimal("20.00"));
|
||||
when(paymentRecordMapper.selectList(any())).thenReturn(List.of(first, second));
|
||||
|
||||
var result = paymentQueryService.getPaymentRecordExportList(new PaymentRecordPageNewReqVO());
|
||||
|
||||
assertEquals(List.of(new BigDecimal("5.00"), new BigDecimal("20.00")), result.stream()
|
||||
.map(PaymentRecordExportRespVO::getActualMoney)
|
||||
.toList());
|
||||
}
|
||||
|
||||
private PaymentRecordDO buildVisibleRecord(Long id, String bizScene, BigDecimal amount) {
|
||||
return PaymentRecordDO.builder()
|
||||
.id(id)
|
||||
.custId(703L)
|
||||
.custCode("C001")
|
||||
.custName("张三")
|
||||
.paymentAmount(amount)
|
||||
.billAmount(amount)
|
||||
.lateFeeAmount(BigDecimal.ZERO)
|
||||
.chargeMethod(1)
|
||||
.chargeWay(1)
|
||||
.cashierId("1001")
|
||||
.payTime(LocalDateTime.of(2026, 5, 13, 10, 0).minusMinutes(id))
|
||||
.bizScene(bizScene)
|
||||
.payInOut("IN")
|
||||
.settleStatus(PaymentSettleStatusEnum.UNSETTLED.getCode())
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 运行完整查询服务单测**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am \
|
||||
-Dtest=PaymentQueryServiceTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: 所有 `PaymentQueryServiceTest` 测试通过。
|
||||
|
||||
- [ ] **Step 7: 提交完整历史查询修复**
|
||||
|
||||
```bash
|
||||
git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/PaymentRecordPageNewReqVO.java \
|
||||
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java \
|
||||
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java
|
||||
git commit -m "fix: return complete payment history"
|
||||
```
|
||||
|
||||
### Task 2: 新预存支付主单与账户流水建立可靠关联
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceCounterPaymentTest.java`
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java`
|
||||
|
||||
- [ ] **Step 1: 先把柜台预存测试改为验证顺序和来源 ID**
|
||||
|
||||
在 `counterTopup_shouldIncreaseDepositAndCapturePaymentRecord` 中增加:
|
||||
|
||||
```java
|
||||
InOrder inOrder = inOrder(paymentCommandApplicationService, accountService);
|
||||
inOrder.verify(paymentCommandApplicationService).captureCounterTopup(
|
||||
cust, new BigDecimal("100.00"), reqVO.getPayTime(), 1, "1001", "柜台现金预存");
|
||||
|
||||
ArgumentCaptor<AccountLogContext> contextCaptor = ArgumentCaptor.forClass(AccountLogContext.class);
|
||||
inOrder.verify(accountService).increaseDeposit(
|
||||
eq(910001L), eq(new BigDecimal("100.00")), contextCaptor.capture());
|
||||
assertEquals(7001L, contextCaptor.getValue().getPayDetailId());
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试并确认旧顺序失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am \
|
||||
-Dtest=ChargeServiceCounterPaymentTest#counterTopup_shouldIncreaseDepositAndCapturePaymentRecord \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL,旧实现先调用 `increaseDeposit()`,且 `payDetailId` 为空。
|
||||
|
||||
- [ ] **Step 3: 调整事务内执行顺序**
|
||||
|
||||
`counterTopup()` 改为:
|
||||
|
||||
```java
|
||||
PaymentRecordDO paymentRecord = paymentCommandApplicationService.captureCounterTopup(
|
||||
cust,
|
||||
reqVO.getAmount(),
|
||||
reqVO.getPayTime(),
|
||||
reqVO.getChargeWay(),
|
||||
reqVO.getCashierId(),
|
||||
reqVO.getRemark()
|
||||
);
|
||||
AccountDO account = accountService.increaseDeposit(mainCustId, reqVO.getAmount(),
|
||||
AccountLogContext.builder()
|
||||
.accLogType(accLogType)
|
||||
.accInOut(1)
|
||||
.payDetailId(paymentRecord.getId())
|
||||
.relatedCustId(isTransfer ? reqVO.getCustId() : null)
|
||||
.remark(logRemark)
|
||||
.build());
|
||||
```
|
||||
|
||||
保持方法级 `@Transactional(rollbackFor = Exception.class)`,返回结构不变。
|
||||
|
||||
- [ ] **Step 4: 运行测试确认变绿**
|
||||
|
||||
Run: 与 Step 2 相同。
|
||||
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 5: 提交预存账户流水关联修复**
|
||||
|
||||
```bash
|
||||
git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java \
|
||||
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceCounterPaymentTest.java
|
||||
git commit -m "fix: link counter topup to account log"
|
||||
```
|
||||
|
||||
### Task 3: 缴费历史映射预存期初和期末余额
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java`
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java`
|
||||
|
||||
- [ ] **Step 1: 新增余额映射失败测试**
|
||||
|
||||
给测试类增加 `@Mock AccountLogMapper accountLogMapper`,新增:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void getPaymentRecordPageNew_shouldMapTopupBalanceFromAccountLog() {
|
||||
PaymentRecordDO topup = buildVisibleRecord(101L, "DEPOSIT_TOPUP", new BigDecimal("5.00"));
|
||||
AccountLogDO accountLog = AccountLogDO.builder()
|
||||
.id(201L)
|
||||
.payDetailId(101L)
|
||||
.accLogType(2)
|
||||
.accInOut(1)
|
||||
.balanceBefore(new BigDecimal("10.00"))
|
||||
.balanceAfter(new BigDecimal("15.00"))
|
||||
.build();
|
||||
when(paymentRecordMapper.selectList(any())).thenReturn(List.of(topup));
|
||||
when(accountLogMapper.selectList(any())).thenReturn(List.of(accountLog));
|
||||
|
||||
var result = paymentQueryService.getPaymentRecordPageNew(new PaymentRecordPageNewReqVO());
|
||||
var item = result.getPaySubtotalPageListDto().getItems().get(0);
|
||||
|
||||
assertEquals(new BigDecimal("10.00"), item.getLastDeposit());
|
||||
assertEquals(new BigDecimal("15.00"), item.getDeposit());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPaymentRecordPageNew_shouldLeaveBalanceEmptyWithoutReliableAccountLog() {
|
||||
PaymentRecordDO topup = buildVisibleRecord(101L, "DEPOSIT_TOPUP", new BigDecimal("5.00"));
|
||||
when(paymentRecordMapper.selectList(any())).thenReturn(List.of(topup));
|
||||
when(accountLogMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
var result = paymentQueryService.getPaymentRecordPageNew(new PaymentRecordPageNewReqVO());
|
||||
var item = result.getPaySubtotalPageListDto().getItems().get(0);
|
||||
|
||||
assertNull(item.getLastDeposit());
|
||||
assertNull(item.getDeposit());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试并确认余额尚未赋值**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am \
|
||||
-Dtest=PaymentQueryServiceTest#getPaymentRecordPageNew_shouldMapTopupBalanceFromAccountLog \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL,`lastDeposit` 和 `deposit` 为 `null`。
|
||||
|
||||
- [ ] **Step 3: 批量加载账户流水并写入查询上下文**
|
||||
|
||||
在 `PaymentQueryServiceImpl` 注入 `AccountLogMapper`,增加:
|
||||
|
||||
```java
|
||||
private Map<Long, AccountLogDO> buildTopupAccountLogMap(List<PaymentRecordDO> records) {
|
||||
List<Long> paymentRecordIds = records.stream()
|
||||
.filter(record -> "DEPOSIT_TOPUP".equals(record.getBizScene()))
|
||||
.map(PaymentRecordDO::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (paymentRecordIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return accountLogMapper.selectList(new LambdaQueryWrapperX<AccountLogDO>()
|
||||
.in(AccountLogDO::getPayDetailId, paymentRecordIds)
|
||||
.in(AccountLogDO::getAccLogType, List.of(2, 3))
|
||||
.eq(AccountLogDO::getAccInOut, 1)
|
||||
.orderByDesc(AccountLogDO::getId))
|
||||
.stream()
|
||||
.filter(log -> log.getPayDetailId() != null)
|
||||
.collect(Collectors.toMap(
|
||||
AccountLogDO::getPayDetailId,
|
||||
Function.identity(),
|
||||
(newer, older) -> newer,
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
```
|
||||
|
||||
将 `PaymentQueryContext` 增加 `Map<Long, AccountLogDO> topupAccountLogMap`,在 `buildQueryContext()` 中填充。`toPaySubtotalItem()` 和 `toPaymentRecordExportResp()` 增加 `AccountLogDO accountLog` 参数并设置:
|
||||
|
||||
```java
|
||||
item.setLastDeposit(accountLog != null ? accountLog.getBalanceBefore() : null);
|
||||
item.setDeposit(accountLog != null ? accountLog.getBalanceAfter() : null);
|
||||
```
|
||||
|
||||
导出 VO 同样映射 `lastDeposit` 和 `deposit`,不再用 `unallocatedAmount` 充当期末余额。
|
||||
|
||||
- [ ] **Step 4: 运行余额测试和完整查询测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am \
|
||||
-Dtest=PaymentQueryServiceTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: 所有查询测试通过,旧数据无账户流水关联时余额保持空值。
|
||||
|
||||
- [ ] **Step 5: 提交余额快照查询修复**
|
||||
|
||||
```bash
|
||||
git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java \
|
||||
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java
|
||||
git commit -m "fix: expose topup balance snapshots"
|
||||
```
|
||||
|
||||
### Task 4: 前端从持久化支付记录恢复最近预存
|
||||
|
||||
**Files:**
|
||||
- Create: `water-frontend/tests/operatingCharges/counterChargingPersistentTopup.test.mjs`
|
||||
- Modify: `water-frontend/src/api/operatingCharges/counterCharging/index.ts`
|
||||
- Modify: `water-frontend/src/views/operatingCharges/counterCharging/index.vue`
|
||||
|
||||
- [ ] **Step 1: 新增前端失败契约测试**
|
||||
|
||||
创建测试:
|
||||
|
||||
```javascript
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const apiSource = readFileSync(resolve(process.cwd(), 'src/api/operatingCharges/counterCharging/index.ts'), 'utf8')
|
||||
const pageSource = readFileSync(resolve(process.cwd(), 'src/views/operatingCharges/counterCharging/index.vue'), 'utf8')
|
||||
|
||||
test('counter charging queries the latest persisted deposit topup', () => {
|
||||
assert.match(apiSource, /export const getLatestCounterTopup\s*=/)
|
||||
assert.match(apiSource, /bizScene:\s*'DEPOSIT_TOPUP'/)
|
||||
assert.match(apiSource, /skipCount:\s*0/)
|
||||
assert.match(apiSource, /maxResultCount:\s*1/)
|
||||
})
|
||||
|
||||
test('counter charging restores persisted topup only when no unpaid charges exist', () => {
|
||||
assert.match(pageSource, /const loadLatestCounterTopupDisplay\s*=\s*async/)
|
||||
assert.match(pageSource, /if\s*\(chargeList\.value\.length\s*===\s*0\)[\s\S]*?loadLatestCounterTopupDisplay\(custId\)/)
|
||||
assert.match(pageSource, /paymentRecordId|record\.id/)
|
||||
assert.doesNotMatch(pageSource, /localStorage|sessionStorage/)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试并确认接口和恢复逻辑缺失**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL,找不到 `getLatestCounterTopup`。
|
||||
|
||||
- [ ] **Step 3: 增加最近预存 API**
|
||||
|
||||
在 API 文件增加:
|
||||
|
||||
```typescript
|
||||
export interface CounterTopupPaymentRecordVO {
|
||||
id: number
|
||||
creationTime?: string
|
||||
payDate?: string
|
||||
actualMoney?: number
|
||||
chargeWay?: number
|
||||
payInOut?: number
|
||||
lastDeposit?: number
|
||||
deposit?: number
|
||||
}
|
||||
|
||||
export const getLatestCounterTopup = async (custId: number) => {
|
||||
return await request.get<{
|
||||
paySubtotalPageListDto?: {
|
||||
totalCount?: number
|
||||
items?: CounterTopupPaymentRecordVO[]
|
||||
}
|
||||
}>({
|
||||
url: '/business/charge/payment-record/page-new',
|
||||
params: {
|
||||
custId,
|
||||
bizScene: 'DEPOSIT_TOPUP',
|
||||
skipCount: 0,
|
||||
maxResultCount: 1
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 增加持久回显转换和加载逻辑**
|
||||
|
||||
把 `setSettledTopupDisplayRow` 改为接受持久记录:
|
||||
|
||||
```typescript
|
||||
const setSettledTopupDisplayRow = (record: {
|
||||
id: number
|
||||
amount: number
|
||||
payTime: string
|
||||
chargeWay: number
|
||||
}) => {
|
||||
const current = currentCustomer.value
|
||||
settledChargeDisplayRows.value = [{
|
||||
id: record.id,
|
||||
meterId: 0,
|
||||
recordId: 0,
|
||||
billMonth: '预存款',
|
||||
custId: current?.id || 0,
|
||||
custCode: current?.custCode,
|
||||
custName: current?.custName,
|
||||
billAmount: record.amount,
|
||||
lateFee: 0,
|
||||
extendedAmount: record.amount,
|
||||
payState: 1,
|
||||
payStateName: '收讫',
|
||||
meterCode: '预存',
|
||||
recordNo: '预存',
|
||||
payTime: record.payTime,
|
||||
chargeWay: record.chargeWay,
|
||||
displayChargeState: 'settled',
|
||||
displayChargeType: 'topup'
|
||||
} as ChargeDisplayRowVO]
|
||||
}
|
||||
|
||||
const loadLatestCounterTopupDisplay = async (custId: number) => {
|
||||
try {
|
||||
const result = await getLatestCounterTopup(custId)
|
||||
const record = result?.paySubtotalPageListDto?.items?.[0]
|
||||
if (!record?.id) return
|
||||
setSettledTopupDisplayRow({
|
||||
id: record.id,
|
||||
amount: Number(record.actualMoney ?? 0),
|
||||
payTime: record.payDate || record.creationTime || dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
chargeWay: Number(record.chargeWay ?? 1)
|
||||
})
|
||||
} catch (error: any) {
|
||||
ElMessage.warning(error?.message || '最近预存记录加载失败')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 `loadCustomerChargeData()` 完成 `chargeList` 赋值后增加:
|
||||
|
||||
```typescript
|
||||
if (chargeList.value.length === 0) {
|
||||
await loadLatestCounterTopupDisplay(custId)
|
||||
}
|
||||
```
|
||||
|
||||
预存提交时保存 `submitCounterTopup()` 返回值,成功后以真实 `paymentRecordId` 设置即时行;若刷新查询已经恢复同一记录,赋相同 ID 不产生重复行。
|
||||
|
||||
- [ ] **Step 5: 保证历史回显不参与收费选择**
|
||||
|
||||
保持收费计算和选中逻辑只使用 `chargeList.value`,展示使用 `settledChargeDisplayRows`。预存成功或重新查询后,不把历史行写入 `chargeList`;`selectedChargeRows` 在无欠费预存模式下保持空数组。
|
||||
|
||||
- [ ] **Step 6: 运行前端契约测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
```
|
||||
|
||||
Expected: 2 个测试全部通过。
|
||||
|
||||
- [ ] **Step 7: 提交前端持久回显修复**
|
||||
|
||||
```bash
|
||||
git add src/api/operatingCharges/counterCharging/index.ts \
|
||||
src/views/operatingCharges/counterCharging/index.vue \
|
||||
tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
git commit -m "fix: restore persisted counter topup display"
|
||||
```
|
||||
|
||||
### Task 5: 回归验证和证据回写
|
||||
|
||||
**Files:**
|
||||
- Create: `water-docs/docs/evidence/bugfix/2026-07-14-counter-topup-persistent-display.md`
|
||||
|
||||
- [ ] **Step 1: 运行后端相关测试集合**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am \
|
||||
-Dtest=PaymentQueryServiceTest,ChargeServiceCounterPaymentTest,PaymentRecordServiceImplTest,CounterSettleApplicationServiceImplTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: 退出码 `0`,无失败和错误。
|
||||
|
||||
- [ ] **Step 2: 运行后端编译**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am -DskipTests compile
|
||||
```
|
||||
|
||||
Expected: `BUILD SUCCESS`。
|
||||
|
||||
- [ ] **Step 3: 运行前端定向测试和既有柜台收费契约**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
|
||||
pnpm test:counter-charging
|
||||
node --test tests/operatingCharges/counterChargingRouteRestore.test.mjs
|
||||
```
|
||||
|
||||
Expected: 新增测试和 `test:counter-charging` 通过;若既有 route restore 测试仍失败,记录其与本次修改是否相关,不隐藏结果。
|
||||
|
||||
- [ ] **Step 4: 运行前端构建**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Expected: 退出码 `0`;若存在既有警告,记录但不将警告表述为错误。
|
||||
|
||||
- [ ] **Step 5: 写验证证据**
|
||||
|
||||
证据文档必须记录:
|
||||
|
||||
```markdown
|
||||
# 柜台预存持久回显修复验证证据
|
||||
|
||||
- 验证日期:2026-07-14
|
||||
- 后端基线:`06ed57830fe0ccd50f0a74e3e5cf02114b0ace45`
|
||||
- 前端基线:`a0239b384f7e072c11c67ae6d405980260f4af14`
|
||||
- 设计:`docs/superpowers/specs/2026-07-13-counter-topup-persistent-display-design.md`
|
||||
|
||||
## 验证结果
|
||||
|
||||
| 范围 | 命令 | 结果 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 后端单测 | `mvn -pl sw-business/sw-business-server -am -Dtest=PaymentQueryServiceTest,ChargeServiceCounterPaymentTest,PaymentRecordServiceImplTest,CounterSettleApplicationServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test` | PASS/FAIL | 测试数、失败数 |
|
||||
| 后端编译 | `mvn -pl sw-business/sw-business-server -am -DskipTests compile` | PASS/FAIL | `BUILD SUCCESS` 或错误摘要 |
|
||||
| 前端契约 | `node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs`、`pnpm test:counter-charging` | PASS/FAIL | 测试数、失败数 |
|
||||
| 前端构建 | `pnpm build` | PASS/FAIL | 构建结果 |
|
||||
|
||||
## 已知边界
|
||||
|
||||
- 旧预存支付记录没有可靠 `payDetailId` 时,期初、期末余额显示 `-`。
|
||||
- 主副卡付款户解析和有效余额读取不在本次修复范围。
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 最终差异检查**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git -C water-backend diff --check
|
||||
git -C water-frontend diff --check
|
||||
git -C water-docs diff --check
|
||||
git -C water-backend status --short
|
||||
git -C water-frontend status --short
|
||||
git -C water-docs status --short
|
||||
```
|
||||
|
||||
Expected: 无空白错误;状态中仅包含本计划范围内的文件。
|
||||
|
||||
- [ ] **Step 7: 提交验证证据**
|
||||
|
||||
```bash
|
||||
git add docs/evidence/bugfix/2026-07-14-counter-topup-persistent-display.md
|
||||
git commit -m "docs: record counter topup display verification"
|
||||
```
|
||||
@ -1,159 +0,0 @@
|
||||
# 柜台预存持久回显与缴费历史修复设计
|
||||
|
||||
日期:2026-07-13
|
||||
|
||||
## 背景
|
||||
|
||||
柜台无欠费客户完成预存后,页面会临时插入一条“预存款 / 收讫”记录。该记录只存在于前端内存,重新查询客户时会被清除。与此同时,客户缴费记录接口虽然查询了多条有效支付主单,却只返回其中最新一条,导致较早的预存记录被后续缴费记录覆盖。缴费记录响应中的期初余额、期末余额字段也未从可靠的账务快照赋值。
|
||||
|
||||
本次修复以 `biz_payment_record` 为缴费记录主数据源,以 `biz_account_log` 的余额变动快照为余额证据,建立“预存成功后显示、重新查询后仍能从后端恢复、客户历史可完整分页查询”的闭环。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 无欠费客户完成柜台预存后,当前页面继续展示“预存款 / 收讫”记录。
|
||||
2. 重新查询同一无欠费客户时,页面从后端支付记录恢复最近一笔有效预存,不依赖前端缓存。
|
||||
3. 客户详情的缴费记录按时间倒序返回全部有效历史,并保持正确分页、统计和导出语义。
|
||||
4. 新产生的预存记录能够展示可追溯的期初余额和期末余额。
|
||||
5. 已红冲支付记录不作为有效预存回显,也不进入正常缴费历史。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 本次不修复主副卡付款户解析错误和柜台有效余额读取错误。
|
||||
- 本次不对无法可靠关联账户流水的旧支付记录伪造期初、期末余额。
|
||||
- 本次不把所有历史预存与当前待收费账单混合展示。
|
||||
- 本次不引入浏览器本地缓存、`localStorage` 或跨会话前端状态作为账务记录来源。
|
||||
|
||||
## 方案选择
|
||||
|
||||
采用“后端持久记录回显”方案:
|
||||
|
||||
- 缴费历史接口返回全部符合条件的有效支付主单,再由现有分页参数截取当前页。
|
||||
- 查询请求增加可选业务场景过滤;柜台页面用 `DEPOSIT_TOPUP` 精确查询最近一笔有效预存。
|
||||
- 柜台页面仅在当前客户没有待缴账单时回显最近预存,避免历史记录进入可勾选的待收费账单集合。
|
||||
- 新预存交易将支付主单 ID 写入对应账户流水来源字段,并由查询层映射余额变动前后值。
|
||||
|
||||
不采用以下方案:
|
||||
|
||||
- 仅保留前端临时数组或写入浏览器缓存:换页面、换设备或重新登录后仍会丢失,也不能作为账务证据。
|
||||
- 无条件把最近预存和待缴账单混合展示:会让历史收款记录进入当前收费语境,增加误选和重复收费风险。
|
||||
|
||||
## 后端设计
|
||||
|
||||
### 缴费历史查询
|
||||
|
||||
将当前“查询全部候选记录后只保留第一条”的实现改为返回全部有效候选记录:
|
||||
|
||||
- 继续按 `payTime`、`id` 倒序排列。
|
||||
- 继续使用现有可见性规则过滤已红冲及非正常记录。
|
||||
- `skipCount`、`maxResultCount` 在过滤后的完整集合上分页。
|
||||
- `totalCount`、`totalAmount`、`topUpCount`、`topUpTotalMoney` 均基于过滤后的完整集合计算。
|
||||
- 导出使用与页面查询相同的过滤规则,但不受页面分页截断。
|
||||
|
||||
`PaymentRecordPageNewReqVO` 增加可选 `bizScene`:
|
||||
|
||||
- 未传时保持客户缴费历史的全场景查询。
|
||||
- 传 `DEPOSIT_TOPUP` 时只返回柜台预存记录。
|
||||
- 过滤由后端查询条件完成,不由前端拉取一页数据后自行猜测。
|
||||
|
||||
### 预存余额快照关联
|
||||
|
||||
柜台预存保持单事务执行,但调整事务内顺序:
|
||||
|
||||
1. 创建 `DEPOSIT_TOPUP` 支付主单,获得 `paymentRecordId`。
|
||||
2. 增加实际付款账户预存余额。
|
||||
3. 写入账户流水时,将 `paymentRecordId` 写入 `payDetailId`。
|
||||
4. 返回支付主单信息和变更后余额。
|
||||
|
||||
任一步失败时整个事务回滚,不留下只有支付记录或只有余额变动的半成品。
|
||||
|
||||
查询缴费历史时,针对 `DEPOSIT_TOPUP` 通过 `payDetailId = paymentRecordId` 获取账户流水:
|
||||
|
||||
- `lastDeposit` 映射 `balanceBefore`。
|
||||
- `deposit` 映射 `balanceAfter`。
|
||||
|
||||
旧数据如果没有 `payDetailId` 关联,不按金额和时间做模糊匹配,期初、期末余额显示为空值,由前端展示 `-`,避免错误余额成为账务证据。
|
||||
|
||||
### 红冲兼容
|
||||
|
||||
- 已红冲原支付主单继续由现有可见性规则排除。
|
||||
- 红冲反向支付记录不作为正常 `DEPOSIT_TOPUP` 回显。
|
||||
- 柜台页面查询最近预存时只取最新一笔有效、未红冲的收入记录。
|
||||
|
||||
## 前端设计
|
||||
|
||||
### 柜台预存成功展示
|
||||
|
||||
预存成功后仍可立即显示本次“预存款 / 收讫”记录,但显示内容以接口返回的支付主单 ID、支付时间、金额和收费方式为准,不再使用时间戳伪造记录 ID。
|
||||
|
||||
### 重新查询恢复
|
||||
|
||||
`loadCustomerChargeData(custId)` 完成待缴账单和统计查询后:
|
||||
|
||||
- 有待缴账单:按现有逻辑展示待缴账单,不混入历史预存。
|
||||
- 无待缴账单:调用缴费历史接口,传入 `custId`、`bizScene=DEPOSIT_TOPUP`、`skipCount=0`、`maxResultCount=1`。
|
||||
- 查询到有效预存:转换为不可勾选的“预存款 / 收讫”展示行。
|
||||
- 没有有效预存:保持空表状态。
|
||||
- 恢复查询失败:不影响客户和待缴账单主查询,记录错误并提示“最近预存记录加载失败”。
|
||||
|
||||
历史回显行只用于确认最近一次成功预存,不参与选中账单、应收统计或再次收费计算。
|
||||
|
||||
### 客户缴费记录
|
||||
|
||||
客户详情沿用现有分页组件和接口参数。后端返回完整结果后:
|
||||
|
||||
- 翻页能看到较早记录。
|
||||
- 统计金额和笔数使用完整过滤结果。
|
||||
- 有可靠账户流水关联的新预存显示期初、期末余额。
|
||||
- 无可靠余额证据的旧记录继续显示 `-`,不得转成 `0`。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 支付主单创建或余额增加失败时事务整体回滚,前端不插入成功行。
|
||||
- 最近预存查询失败不清空客户基本信息和待缴账单结果。
|
||||
- 接口返回空记录时不沿用上一个客户的预存展示行。
|
||||
- 已红冲记录不可通过页面刷新重新出现。
|
||||
|
||||
## 测试设计
|
||||
|
||||
### 后端单元测试
|
||||
|
||||
1. 两条有效支付记录均进入候选集合,分页返回正确记录和总数。
|
||||
2. 最新记录已红冲时被过滤,较早有效记录仍可查询。
|
||||
3. `bizScene=DEPOSIT_TOPUP` 时只返回预存记录。
|
||||
4. 汇总金额、预存笔数和预存金额基于全部有效记录计算。
|
||||
5. 新柜台预存先生成支付主单,再写入带 `paymentRecordId` 的账户流水。
|
||||
6. 有账户流水关联时正确映射 `balanceBefore`、`balanceAfter`;无关联时余额字段为空。
|
||||
7. 导出包含全部有效记录,不退化为只导出最新一条。
|
||||
|
||||
### 前端测试
|
||||
|
||||
1. 无欠费客户重新查询时调用 `DEPOSIT_TOPUP` 过滤接口并恢复最近预存行。
|
||||
2. 有待缴账单时不把历史预存混入待收费列表。
|
||||
3. 切换客户时清空上一客户的临时或持久回显行。
|
||||
4. 最近预存加载失败时不破坏客户待缴账单查询结果。
|
||||
5. 客户缴费记录翻页使用后端完整总数,余额空值显示 `-` 而不是 `0`。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 无欠费客户预存成功后可立即看到“预存款 / 收讫”记录。
|
||||
2. 刷新页面或重新输入同一客户查询,仍能从后端恢复最近一笔有效预存。
|
||||
3. 新增另一笔缴费后,客户缴费记录仍可通过分页查到之前的预存记录。
|
||||
4. 已红冲预存不再作为最近有效预存回显。
|
||||
5. 新预存记录显示正确的期初、期末余额;无法可靠恢复余额的旧数据显示 `-`。
|
||||
6. 历史预存回显不参与当前收费选择和金额统计。
|
||||
|
||||
## 影响文件
|
||||
|
||||
后端预计修改:
|
||||
|
||||
- `PaymentRecordPageNewReqVO.java`
|
||||
- `PaymentQueryServiceImpl.java`
|
||||
- `ChargeServiceImpl.java`
|
||||
- `AccountLogMapper.java`
|
||||
- 对应后端单元测试
|
||||
|
||||
前端预计修改:
|
||||
|
||||
- `src/api/operatingCharges/counterCharging/index.ts`
|
||||
- `src/views/operatingCharges/counterCharging/index.vue`
|
||||
- 对应前端源码契约或页面状态测试
|
||||
@ -1,110 +0,0 @@
|
||||
# 兴业银行联调基础配置确认表设计
|
||||
|
||||
## 目标
|
||||
|
||||
生成一份可由福建水务营收系统项目组与兴业银行共同填写、确认和回传的 Markdown 配置表,用于联调前的信息交换、网络开通、安全参数确认和责任边界核对。
|
||||
|
||||
## 文档定位
|
||||
|
||||
- 文档类型:兴业银行专项联调配置确认表。
|
||||
- 使用阶段:联调准备、网络申请、配置交换和联调准入检查。
|
||||
- 对接双方:兴业银行与福建水务营收系统项目组。
|
||||
- 目标文件:`docs/evidence/bank-integration/兴业银行联调基础配置确认表.md`。
|
||||
|
||||
## 状态规则
|
||||
|
||||
每一项配置使用以下状态之一:
|
||||
|
||||
| 状态 | 含义 |
|
||||
|---|---|
|
||||
| 已确认 | 可由当前项目代码、正式文档或已验证环境直接确认 |
|
||||
| 可预填 | 可根据当前实现预填,但仍需部署、运维或银行书面确认 |
|
||||
| 待兴业银行提供 | 当前项目无法确定,需兴业银行提供 |
|
||||
| 待我方提供 | 需我方运维、安全、业务或项目负责人提供 |
|
||||
| 双方待确认 | 涉及双方共同选择或约定,不能由单方确定 |
|
||||
| 不适用 | 经双方确认后不适用于本次联调范围 |
|
||||
|
||||
不得把本地 Docker 测试账号、密码、端口或样本目录标记为正式联调配置。
|
||||
|
||||
## 表格结构
|
||||
|
||||
### 1. 文档基本信息
|
||||
|
||||
记录银行名称、分支机构、项目名称、环境、文档版本、双方负责人和更新时间。银行名称与项目名称可预填,分支机构及联系人待补充。
|
||||
|
||||
### 2. 兴业银行提供给我方
|
||||
|
||||
包括:
|
||||
|
||||
- 银行测试环境接口地址、机构号、渠道号和商户号;
|
||||
- 银行出口 IP、端口、访问方向和网络要求;
|
||||
- SFTP/FTP 地址、端口、认证方式及送盘、回盘、对账目录;
|
||||
- 数据格式、字符集、金额单位、日期格式和文件命名规则;
|
||||
- 加密、签名、证书、时间戳、防重放和密钥交换规则;
|
||||
- 正常与异常请求报文、送盘、回盘和对账样本;
|
||||
- 错误码、超时、重试、对账和结算规则;
|
||||
- 银行技术、业务及网络联系人。
|
||||
|
||||
### 3. 我方提供给兴业银行
|
||||
|
||||
包括:
|
||||
|
||||
- 福建水务营收系统联调环境域名、入口 IP、端口和 HTTPS 证书信息;
|
||||
- 兴业银行需要访问的接口相对路径;
|
||||
- 我方访问银行的固定出口 IP;
|
||||
- 我方机构编码、租户/机构映射和测试客户数据;
|
||||
- 我方支持的数据格式、加密能力和文件传输协议;
|
||||
- 我方技术、业务、运维和安全联系人;
|
||||
- 联调时间窗口、故障通知方式和日志查询信息。
|
||||
|
||||
接口路径只预填当前代码可确认的相对路径,不虚构正式域名、外网 IP 或端口。
|
||||
|
||||
### 4. 双方共同确认
|
||||
|
||||
包括联调范围、接口版本、环境用途、网络访问矩阵、幂等键、超时时间、重试次数、文件处理时点、对账日切、异常补偿、验收标准和上线切换方式。
|
||||
|
||||
### 5. 当前填写完成度
|
||||
|
||||
汇总:
|
||||
|
||||
- 已能从项目确认的内容;
|
||||
- 可预填但需要复核的内容;
|
||||
- 兴业银行必须提供的内容;
|
||||
- 我方运维、安全和业务必须补充的内容;
|
||||
- 阻止正式联调启动的关键缺口。
|
||||
|
||||
## 预填原则
|
||||
|
||||
当前可预填:
|
||||
|
||||
- 对接银行为兴业银行;
|
||||
- 项目名称为福建水务营收系统;
|
||||
- 当前实现支持 XML/JSON 报文转换;
|
||||
- 当前文件传输解析支持 SFTP/FTP,正式联调推荐 SFTP;
|
||||
- 当前接口类别包括欠费查询、实时缴费、缴费红冲、代扣/托收签解约、客户校验、送盘、回盘及状态查询;
|
||||
- 当前网关存在 `/app-api/bankbusiness/**` 银行侧路由。
|
||||
|
||||
必须保持待确认:
|
||||
|
||||
- 兴业银行具体分支机构;
|
||||
- 双方测试环境域名、IP、端口及白名单;
|
||||
- 银行机构号、渠道号、商户号;
|
||||
- 正式接口是否统一经网关访问;
|
||||
- 兴业银行采用的加密、签名和证书规则;
|
||||
- SFTP 地址、账号、目录及认证方式;
|
||||
- 兴业银行私有报文和文件格式;
|
||||
- 生产或联调密钥、密码、私钥。
|
||||
|
||||
## 安全约束
|
||||
|
||||
- 表内不记录密码、私钥、完整对称密钥或可直接使用的生产凭据。
|
||||
- 敏感项只记录密钥编号、证书指纹、有效期、交换方式和责任人。
|
||||
- IP、域名和证书信息在对外发送前由我方运维或安全人员复核。
|
||||
- 所有本地 Docker smoke 参数只作为内部验证证据,不进入对外配置值。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- 表格明确区分双方提供项和共同确认项。
|
||||
- 所有预填值都有当前项目证据,不确定内容明确留空并标注责任方。
|
||||
- 表格可以直接发送给兴业银行填写,不要求对方阅读项目源码或内部设计文档。
|
||||
- 文档末尾明确给出当前是否具备接口预联调、网络联调和端到端业务联调条件。
|
||||
@ -1,60 +0,0 @@
|
||||
# 柜台收费缴费后连续预存设计
|
||||
|
||||
## 背景与问题
|
||||
|
||||
单客户柜台收费完成后,页面会保留刚缴纳的账单作为“收讫”展示行。当前实现同时将这些账单标记为 `displayChargeState: 'settled'` 并继续保留在 `selectedChargeRows` 中。
|
||||
|
||||
无欠费预存模式要求页面不存在 `settled` 展示行,因此缴费完成后收费按钮被禁用,用户无法在同一客户、同一页面中继续办理预存。
|
||||
|
||||
## 目标
|
||||
|
||||
- 单客户普通账单缴费成功后,可以立即继续办理预存。
|
||||
- 已缴账单继续显示“收讫”,用于核对和查看账单详情。
|
||||
- 已缴账单不得再次进入收费提交数据。
|
||||
- 缴费完成后清空上一次实收金额,避免误将账单金额作为预存金额再次提交。
|
||||
- 集收号收费逻辑保持不变。
|
||||
|
||||
## 方案
|
||||
|
||||
### 已缴账单展示状态
|
||||
|
||||
单客户普通账单收费成功后:
|
||||
|
||||
1. 保留账单展示行;
|
||||
2. `payState` 和 `payStateName` 继续表示“收讫”;
|
||||
3. 将 `displayChargeState` 设为 `history`,表示该行仅供回看,不再阻塞后续操作;
|
||||
4. `displayChargeType` 保持 `bill`,详情继续使用普通账单详情链路。
|
||||
|
||||
### 选中状态与金额
|
||||
|
||||
- 收费成功后将 `selectedChargeRows` 清空。
|
||||
- 调用现有金额同步逻辑后,`actualAmount` 清空。
|
||||
- 已缴展示行因 `payState === 1` 继续保持不可勾选,不能再次加入收费数据。
|
||||
|
||||
### 预存模式
|
||||
|
||||
后台刷新后不存在待缴账单,且历史展示行不计入 `hasSettledChargeDisplay`,因此 `noArrearsTopupMode` 自动成立。收费按钮重新可用,用户输入新的实收金额后,提交路径调用预存接口而不是账单收费接口。
|
||||
|
||||
### 集收号边界
|
||||
|
||||
集收号收费完成后的展示和按钮限制不调整。集收号模式仍不支持缴费后直接多缴或预存,避免改变现有批量收费约束。
|
||||
|
||||
## 异常与安全约束
|
||||
|
||||
- 如果收费成功后的账单刷新失败,沿用现有异常提示,不把失败状态误切换成预存模式。
|
||||
- 历史收讫行必须不可选择、不可删除,但可以查看详情和打印。
|
||||
- 连续预存提交时,支付载荷中不得包含刚缴纳账单的 ID。
|
||||
- 页面切换客户或关闭客户时,继续沿用现有状态清理逻辑。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
新增前端 `node:test` 契约测试,先验证失败再实施:
|
||||
|
||||
1. 单客户普通账单收费成功后,收讫展示行使用 `history` 状态;
|
||||
2. 收费成功后 `selectedChargeRows` 被清空;
|
||||
3. 历史收讫行仍保持 `payState: 1`、`payStateName: '收讫'` 和 `displayChargeType: 'bill'`;
|
||||
4. 历史收讫行不阻塞 `noArrearsTopupMode`;
|
||||
5. 连续操作时进入 `submitCounterTopup` 路径,不重复调用 `submitCashCharge`;
|
||||
6. 集收号收费成功后的现有处理保持不变。
|
||||
|
||||
验证运行相关 `node:test`、柜台收费现有测试和前端构建;按用户要求不运行 `vue-tsc`。
|
||||
@ -1,77 +0,0 @@
|
||||
# 柜台收费预存款详情展示修复设计
|
||||
|
||||
## 背景与问题
|
||||
|
||||
柜台收费在客户无欠费时支持办理预存。预存成功后,页面会把支付记录作为一条只读的“预存款”记录展示。
|
||||
|
||||
当前预存款行的 `id` 是支付记录 ID,但点击“详情”时统一调用 `/business/charge/get`,该接口要求账单 ID。由于对象类型和 ID 语义不一致,账单详情弹窗无法取得数据,所有字段显示为 `--`。
|
||||
|
||||
## 目标
|
||||
|
||||
- 预存款记录点击“详情”时展示符合预存业务语义的信息。
|
||||
- 预存款详情不得调用账单详情接口。
|
||||
- 普通账单详情链路保持不变。
|
||||
- 历史恢复的最新预存记录与刚办理成功的预存记录使用同一套详情展示。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不新增后端支付记录详情接口。
|
||||
- 不改造普通账单详情接口。
|
||||
- 不扩展为完整的历史预存记录列表。
|
||||
- 不调整预存、收费或余额入账逻辑。
|
||||
|
||||
## 方案
|
||||
|
||||
### 详情分流
|
||||
|
||||
点击详情时根据行的 `displayChargeType` 判断记录类型:
|
||||
|
||||
- `topup`:打开预存款专用详情,不调用 `/business/charge/get`。
|
||||
- `bill` 或未标记:继续使用现有账单详情链路。
|
||||
|
||||
### 预存详情数据
|
||||
|
||||
预存款显示行补充并保留以下支付快照字段:
|
||||
|
||||
- 支付记录 ID
|
||||
- 预存金额
|
||||
- 收费时间
|
||||
- 收费方式
|
||||
- 期初余额
|
||||
- 期末余额
|
||||
- 收费状态
|
||||
|
||||
客户编号、客户名称、客户地址优先取当前客户信息;支付字段取预存记录行。历史恢复时使用 `/business/charge/payment-record/page-new` 已返回的 `lastDeposit`、`deposit`、`actualMoney`、`chargeWay` 和支付时间。
|
||||
|
||||
刚办理成功的预存记录在刷新最新预存记录后展示,以确保详情字段与持久化支付记录一致;若刷新失败,则保留成功响应和当前客户信息作为降级展示,不影响预存成功结果。
|
||||
|
||||
### 页面展示
|
||||
|
||||
预存款专用详情使用独立弹窗,展示:
|
||||
|
||||
1. 客户编号、客户名称、客户地址;
|
||||
2. 业务类型(固定为“预存款”)、支付记录 ID;
|
||||
3. 预存金额、期初余额、期末余额;
|
||||
4. 收费时间、收费方式、收费状态(“收讫”)。
|
||||
|
||||
不展示账务年月、抄码、水量、账单金额、违约金、开票状态等账单专属字段。
|
||||
|
||||
## 异常处理
|
||||
|
||||
- 预存行缺少支付记录 ID 时,不发起账单详情请求,提示“预存记录信息不完整”。
|
||||
- 金额或余额为 `0` 时必须显示 `0.00`,不得因假值判断显示 `--`。
|
||||
- 期初或期末余额确实缺失时显示 `--`。
|
||||
- 普通账单详情请求失败时沿用现有错误提示和关闭弹窗行为。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
新增前端 `node:test` 契约测试,先验证失败再实施修复,覆盖:
|
||||
|
||||
1. 预存款详情按 `displayChargeType === 'topup'` 分流;
|
||||
2. 预存款详情不调用 `getChargeById`;
|
||||
3. 专用弹窗包含客户、支付、余额和状态字段;
|
||||
4. 历史预存记录映射 `lastDeposit` 与 `deposit`;
|
||||
5. 普通账单仍调用原账单详情接口;
|
||||
6. `0` 金额与余额正确显示。
|
||||
|
||||
验证仅运行相关 `node:test`、柜台收费现有测试和前端构建;按用户要求不运行 `vue-tsc`。
|
||||
@ -1,151 +0,0 @@
|
||||
# Sold Adjustment and Red Flush Fix Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make sold adjustment query settled charges with correct multi-select parameters and complete red-flush record filtering, list, and export fields.
|
||||
|
||||
**Architecture:** Keep the existing backend status model and API routes. Correct the frontend request contract for sold adjustment, then extend the red-flush response projection with operator and original charge time while reusing the existing system-user option API for filtering.
|
||||
|
||||
**Tech Stack:** Vue 3, TypeScript, Element Plus, Node test runner, Spring Boot, MyBatis Plus, JUnit 5, Mockito.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Sold adjustment request contract
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-frontend/src/views/accountProcess/soldAdjustment/sold-adjustment.contract.test.mjs`
|
||||
- Modify: `water-frontend/src/views/accountProcess/soldAdjustment/index.vue`
|
||||
- Modify: `water-frontend/src/api/accountProcess/soldAdjustment/index.ts`
|
||||
|
||||
- [ ] **Step 1: Write failing contract tests**
|
||||
|
||||
Add assertions that `buildQueryParams()` contains `isHistory: queryParams.isHistory` and maps the four arrays to `collectionMethodList`, `custTypeList`, `waterNatureList`, and `meterTypeList`. Assert the request interface declares those fields.
|
||||
|
||||
- [ ] **Step 2: Verify the tests fail**
|
||||
|
||||
Run: `node --test src/views/accountProcess/soldAdjustment/sold-adjustment.contract.test.mjs`
|
||||
|
||||
Expected: FAIL because `isHistory` and `*List` request mappings are missing.
|
||||
|
||||
- [ ] **Step 3: Implement the minimal request mapping**
|
||||
|
||||
Update `buildQueryParams()` to emit:
|
||||
|
||||
```ts
|
||||
isHistory: queryParams.isHistory,
|
||||
collectionMethodList: queryParams.collectionMethod.length ? queryParams.collectionMethod : undefined,
|
||||
custTypeList: queryParams.custType.length ? queryParams.custType.map(String) : undefined,
|
||||
waterNatureList: queryParams.waterNature.length ? queryParams.waterNature : undefined,
|
||||
meterTypeList: queryParams.meterType.length ? queryParams.meterType.map(String) : undefined
|
||||
```
|
||||
|
||||
Remove the array assignments to the single-value fields and add matching optional properties to `SoldAdjustmentPageReqVO`.
|
||||
|
||||
- [ ] **Step 4: Verify the contract tests pass**
|
||||
|
||||
Run: `node --test src/views/accountProcess/soldAdjustment/sold-adjustment.contract.test.mjs`
|
||||
|
||||
Expected: 0 failures.
|
||||
|
||||
- [ ] **Step 5: Commit frontend batch 1**
|
||||
|
||||
```bash
|
||||
git add src/views/accountProcess/soldAdjustment/index.vue \
|
||||
src/views/accountProcess/soldAdjustment/sold-adjustment.contract.test.mjs \
|
||||
src/api/accountProcess/soldAdjustment/index.ts
|
||||
git commit -m "fix: align sold adjustment settled query"
|
||||
```
|
||||
|
||||
### Task 2: Red-flush frontend filter and columns
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-frontend/src/views/operatingCharges/redReversalRecord/payment-no-filter.contract.test.mjs`
|
||||
- Modify: `water-frontend/src/views/operatingCharges/redReversalRecord/index.vue`
|
||||
- Modify: `water-frontend/src/api/business/charge/counterSettle.ts`
|
||||
|
||||
- [ ] **Step 1: Write failing frontend contract tests**
|
||||
|
||||
Assert that the cashier field uses `el-select`, loads system simple-user options, and that columns/types include `operatorName` and `chargeTime`.
|
||||
|
||||
- [ ] **Step 2: Verify the tests fail**
|
||||
|
||||
Run: `node --test src/views/operatingCharges/redReversalRecord/payment-no-filter.contract.test.mjs`
|
||||
|
||||
Expected: FAIL because the cashier is an input and the new fields do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the filter and columns**
|
||||
|
||||
Replace the cashier input with a filterable clearable select backed by the existing system simple-user API. Add list columns “操作员” and “收费时间”, using the existing date formatter.
|
||||
|
||||
Extend `CounterRedFlushRecordRespVO` with:
|
||||
|
||||
```ts
|
||||
operatorName?: string
|
||||
chargeTime?: string
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify frontend tests pass**
|
||||
|
||||
Run the red-flush contract test and the related revenue display contract tests.
|
||||
|
||||
- [ ] **Step 5: Commit frontend batch 2**
|
||||
|
||||
```bash
|
||||
git add src/views/operatingCharges/redReversalRecord \
|
||||
src/api/business/charge/counterSettle.ts
|
||||
git commit -m "feat: complete red flush record fields"
|
||||
```
|
||||
|
||||
### Task 3: Red-flush backend projection and export
|
||||
|
||||
**Files:**
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterRedFlushRecordRespVO.java`
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImpl.java`
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImplTest.java`
|
||||
- Modify: `water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterChargeExportHeaderTest.java`
|
||||
|
||||
- [ ] **Step 1: Write failing backend tests**
|
||||
|
||||
Add assertions that settled red-flush rows use detail `procPerson` and original payment `payTime`, and unsettled rows expose the original payment time. Add Excel-header assertions for “操作员” and “收费时间”.
|
||||
|
||||
- [ ] **Step 2: Verify the tests fail**
|
||||
|
||||
Run the two targeted Maven tests using the repository-supported JDK and reactor options.
|
||||
|
||||
Expected: compilation/test failure because the response fields are absent.
|
||||
|
||||
- [ ] **Step 3: Implement the response projection**
|
||||
|
||||
Add Excel-enabled fields:
|
||||
|
||||
```java
|
||||
@ExcelProperty("操作员")
|
||||
private String operatorName;
|
||||
|
||||
@ExcelProperty("收费时间")
|
||||
private LocalDateTime chargeTime;
|
||||
```
|
||||
|
||||
Populate them in both `toRedFlushRecordResp` and `toUnsettledRedFlushRecordResp` without changing red-flush state transitions.
|
||||
|
||||
- [ ] **Step 4: Verify backend tests pass**
|
||||
|
||||
Run targeted service and export-header tests. If the repository-wide pre-existing compilation issue prevents Maven from reaching the tests, run the available contract tests and report the blocker explicitly.
|
||||
|
||||
- [ ] **Step 5: Commit backend changes**
|
||||
|
||||
```bash
|
||||
git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterRedFlushRecordRespVO.java \
|
||||
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImpl.java \
|
||||
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImplTest.java \
|
||||
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterChargeExportHeaderTest.java
|
||||
git commit -m "feat: expose red flush operator and charge time"
|
||||
```
|
||||
|
||||
### Task 4: Final verification
|
||||
|
||||
- [ ] Run all changed frontend contract tests.
|
||||
- [ ] Run TypeScript type checking or the narrowest repository-supported check covering changed files.
|
||||
- [ ] Run targeted backend tests or document the pre-existing build blocker.
|
||||
- [ ] Inspect `git diff --check` and final repository statuses.
|
||||
- [ ] Report commit hashes and any remaining environment-only verification gap.
|
||||
@ -1,45 +0,0 @@
|
||||
# 已销调整与红冲记录缺陷修复设计
|
||||
|
||||
## 目标
|
||||
|
||||
修复腾讯文档“测试组测试问题”中无歧义的四组问题:
|
||||
|
||||
1. 已销调整默认展示已收费、未结账且可调整的账单;勾选“历史”后展示柜台已结账账单(252)。
|
||||
2. 已销调整多选筛选参数与后端契约一致(195/197)。
|
||||
3. 红冲记录收费员筛选改为下拉选择(271)。
|
||||
4. 红冲记录列表与导出补充操作员、收费时间字段(272/273)。
|
||||
|
||||
不实现含义未确认的红冲记录“操作按钮”。
|
||||
|
||||
## 设计
|
||||
|
||||
### 已销调整
|
||||
|
||||
前端继续保留“历史”复选框,默认值设为 `false`。默认查询 `PAID` 可调整记录,勾选后查询 `SETTLED` 已结账历史记录;`buildQueryParams()` 必须显式传递 `isHistory`。多选控件分别传递 `collectionMethodList`、`custTypeList`、`waterNatureList`、`meterTypeList`,不再把数组写入后端单值字段。
|
||||
|
||||
后端已有 `isHistory=true -> PayStateEnum.SETTLED` 的查询规则,不改变状态模型。补充契约测试,保证前端请求参数和后端 VO 字段一致。
|
||||
|
||||
### 红冲记录
|
||||
|
||||
收费员筛选复用系统用户精简列表,显示用户名称并以用户 ID 查询。查询接口继续使用 `cashierId`,不改变后端筛选语义。
|
||||
|
||||
后端响应新增 `operatorName` 和 `chargeTime`:
|
||||
|
||||
- 已结账红冲:操作员来自结账明细 `procPerson`,收费时间来自原支付记录 `payTime`。
|
||||
- 未结账红冲:操作员优先取反向支付记录收费员,收费时间来自原支付记录 `payTime`。
|
||||
|
||||
列表和 Excel 导出统一使用同一响应模型字段,避免页面与导出数据口径不同。导出标题继续由现有 Excel 工具生成。
|
||||
|
||||
## 测试
|
||||
|
||||
- 前端契约测试验证已销调整默认 `isHistory=false`,并发送 `isHistory` 和四个 `*List` 参数。
|
||||
- 后端已销调整单测验证 `isHistory=false` 查询 `PAID`,`isHistory=true` 只查询 `SETTLED`。
|
||||
- 前端红冲记录契约测试验证收费员下拉和新增列。
|
||||
- 后端红冲服务单测验证已结账、未结账两条映射路径。
|
||||
- 导出表头测试验证“操作员”“收费时间”。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不调整已销调整申请原因、账单名称权威来源或其他旧营收字段。
|
||||
- 不新增红冲记录操作按钮。
|
||||
- 不改变柜台结账聚合或红冲业务状态机。
|
||||
Loading…
x
Reference in New Issue
Block a user