fujian_water_biz_doc/docs/superpowers/plans/2026-07-14-counter-topup-detail.md

13 KiB
Raw Blame History

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

在现有测试文件中增加源码契约断言,要求 openChargeDetaildisplayChargeType === '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;金额继续放在 billAmountextendedAmount,余额快照分别放入 lastDepositdeposit

  • 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

resetCustomerChargeStateonPageActivated 的无恢复参数分支中加入:

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`:退出码 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
git add docs/evidence/bugfix/2026-07-14-counter-topup-detail.md
git commit -m "docs: record counter topup detail verification"