docs: plan counter topup detail fix

This commit is contained in:
tangweijie 2026-07-14 14:39:23 +08:00
parent c031348919
commit dc30e99b2d

View File

@ -0,0 +1,382 @@
# 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`:退出码 04/4 通过。
- `node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs tests/operatingCharges/counterChargingInventory.test.mjs`:退出码 011/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"
```