13 KiB
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的约束。
- 记录根因、RED/GREEN 结果、构建结果与未运行
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:
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
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
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:
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
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
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
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 中传入:
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 未恢复任何预存行时才用成功响应建立降级行:
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:
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
Expected: detail-routing test remains FAIL; balance-mapping assertions PASS.
- Step 6: Commit the data mapping
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
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
在账单详情弹窗后增加以下完整弹窗:
<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
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 的无恢复参数分支中加入:
showTopupDetailDialog.value = false
topupDetailData.value = null
- Step 5: Run the focused test and verify GREEN
Run:
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:
pnpm test:counter-charging
Expected: PASS.
- Step 7: Commit the UI fix
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:
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:
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:
git diff --check
git status --short
Expected: no whitespace errors; only intentional files are present before the final commit.
- Step 4: Write verification evidence
全部验证达到本计划预期后,创建以下证据;若实际结果不同,停止提交证据并先处理差异:
# 柜台收费预存款详情修复验证
## 根因
预存款展示行使用支付记录 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
git add docs/evidence/bugfix/2026-07-14-counter-topup-detail.md
git commit -m "docs: record counter topup detail verification"