docs: plan counter topup detail fix
This commit is contained in:
parent
c031348919
commit
dc30e99b2d
382
docs/superpowers/plans/2026-07-14-counter-topup-detail.md
Normal file
382
docs/superpowers/plans/2026-07-14-counter-topup-detail.md
Normal 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`:退出码 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"
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user