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

24 KiB
Raw Blame History

柜台预存持久回显与缴费历史修复实施计划

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 改为:

@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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=PaymentQueryServiceTest#getPaymentRecordPageNew_shouldExposeAllVisiblePaymentRecords \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected: FAILtotalCount 实际为 1 而期望为 2

  • Step 3: 实现完整候选记录返回

在请求 VO 增加:

@Schema(description = "业务场景", example = "DEPOSIT_TOPUP")
private String bizScene;

将查询方法统一改名为 selectVisiblePaymentRecordCandidates,并实现:

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: 增加场景过滤和导出回归测试

新增测试:

@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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=PaymentQueryServiceTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected: 所有 PaymentQueryServiceTest 测试通过。

  • Step 7: 提交完整历史查询修复
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 中增加:

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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=ChargeServiceCounterPaymentTest#counterTopup_shouldIncreaseDepositAndCapturePaymentRecord \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected: FAIL旧实现先调用 increaseDeposit(),且 payDetailId 为空。

  • Step 3: 调整事务内执行顺序

counterTopup() 改为:

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: 提交预存账户流水关联修复
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,新增:

@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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=PaymentQueryServiceTest#getPaymentRecordPageNew_shouldMapTopupBalanceFromAccountLog \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected: FAILlastDepositdepositnull

  • Step 3: 批量加载账户流水并写入查询上下文

PaymentQueryServiceImpl 注入 AccountLogMapper,增加:

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 参数并设置:

item.setLastDeposit(accountLog != null ? accountLog.getBalanceBefore() : null);
item.setDeposit(accountLog != null ? accountLog.getBalanceAfter() : null);

导出 VO 同样映射 lastDepositdeposit,不再用 unallocatedAmount 充当期末余额。

  • Step 4: 运行余额测试和完整查询测试

Run:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=PaymentQueryServiceTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected: 所有查询测试通过,旧数据无账户流水关联时余额保持空值。

  • Step 5: 提交余额快照查询修复
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: 新增前端失败契约测试

创建测试:

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:

node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs

Expected: FAIL找不到 getLatestCounterTopup

  • Step 3: 增加最近预存 API

在 API 文件增加:

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 改为接受持久记录:

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 赋值后增加:

if (chargeList.value.length === 0) {
  await loadLatestCounterTopupDisplay(custId)
}

预存提交时保存 submitCounterTopup() 返回值,成功后以真实 paymentRecordId 设置即时行;若刷新查询已经恢复同一记录,赋相同 ID 不产生重复行。

  • Step 5: 保证历史回显不参与收费选择

保持收费计算和选中逻辑只使用 chargeList.value,展示使用 settledChargeDisplayRows。预存成功或重新查询后,不把历史行写入 chargeListselectedChargeRows 在无欠费预存模式下保持空数组。

  • Step 6: 运行前端契约测试

Run:

node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs

Expected: 2 个测试全部通过。

  • Step 7: 提交前端持久回显修复
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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=PaymentQueryServiceTest,ChargeServiceCounterPaymentTest,PaymentRecordServiceImplTest,CounterSettleApplicationServiceImplTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected: 退出码 0,无失败和错误。

  • Step 2: 运行后端编译

Run:

mvn -pl sw-business/sw-business-server -am -DskipTests compile

Expected: BUILD SUCCESS

  • Step 3: 运行前端定向测试和既有柜台收费契约

Run:

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:

pnpm build

Expected: 退出码 0;若存在既有警告,记录但不将警告表述为错误。

  • Step 5: 写验证证据

证据文档必须记录:

# 柜台预存持久回显修复验证证据

- 验证日期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:

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: 提交验证证据
git add docs/evidence/bugfix/2026-07-14-counter-topup-persistent-display.md
git commit -m "docs: record counter topup display verification"