# 柜台预存持久回显与缴费历史修复实施计划 > **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` 改为: ```java @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: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=PaymentQueryServiceTest#getPaymentRecordPageNew_shouldExposeAllVisiblePaymentRecords \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: FAIL,`totalCount` 实际为 `1` 而期望为 `2`。 - [ ] **Step 3: 实现完整候选记录返回** 在请求 VO 增加: ```java @Schema(description = "业务场景", example = "DEPOSIT_TOPUP") private String bizScene; ``` 将查询方法统一改名为 `selectVisiblePaymentRecordCandidates`,并实现: ```java private List selectVisiblePaymentRecordCandidates(PaymentRecordPageNewReqVO pageReqVO) { return paymentRecordMapper.selectList(new LambdaQueryWrapperX() .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: 增加场景过滤和导出回归测试** 新增测试: ```java @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: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=PaymentQueryServiceTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: 所有 `PaymentQueryServiceTest` 测试通过。 - [ ] **Step 7: 提交完整历史查询修复** ```bash 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` 中增加: ```java InOrder inOrder = inOrder(paymentCommandApplicationService, accountService); inOrder.verify(paymentCommandApplicationService).captureCounterTopup( cust, new BigDecimal("100.00"), reqVO.getPayTime(), 1, "1001", "柜台现金预存"); ArgumentCaptor 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: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=ChargeServiceCounterPaymentTest#counterTopup_shouldIncreaseDepositAndCapturePaymentRecord \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: FAIL,旧实现先调用 `increaseDeposit()`,且 `payDetailId` 为空。 - [ ] **Step 3: 调整事务内执行顺序** `counterTopup()` 改为: ```java 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: 提交预存账户流水关联修复** ```bash 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`,新增: ```java @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: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=PaymentQueryServiceTest#getPaymentRecordPageNew_shouldMapTopupBalanceFromAccountLog \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: FAIL,`lastDeposit` 和 `deposit` 为 `null`。 - [ ] **Step 3: 批量加载账户流水并写入查询上下文** 在 `PaymentQueryServiceImpl` 注入 `AccountLogMapper`,增加: ```java private Map buildTopupAccountLogMap(List records) { List 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() .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 topupAccountLogMap`,在 `buildQueryContext()` 中填充。`toPaySubtotalItem()` 和 `toPaymentRecordExportResp()` 增加 `AccountLogDO accountLog` 参数并设置: ```java item.setLastDeposit(accountLog != null ? accountLog.getBalanceBefore() : null); item.setDeposit(accountLog != null ? accountLog.getBalanceAfter() : null); ``` 导出 VO 同样映射 `lastDeposit` 和 `deposit`,不再用 `unallocatedAmount` 充当期末余额。 - [ ] **Step 4: 运行余额测试和完整查询测试** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=PaymentQueryServiceTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: 所有查询测试通过,旧数据无账户流水关联时余额保持空值。 - [ ] **Step 5: 提交余额快照查询修复** ```bash 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: 新增前端失败契约测试** 创建测试: ```javascript 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: ```bash node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs ``` Expected: FAIL,找不到 `getLatestCounterTopup`。 - [ ] **Step 3: 增加最近预存 API** 在 API 文件增加: ```typescript 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` 改为接受持久记录: ```typescript 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` 赋值后增加: ```typescript if (chargeList.value.length === 0) { await loadLatestCounterTopupDisplay(custId) } ``` 预存提交时保存 `submitCounterTopup()` 返回值,成功后以真实 `paymentRecordId` 设置即时行;若刷新查询已经恢复同一记录,赋相同 ID 不产生重复行。 - [ ] **Step 5: 保证历史回显不参与收费选择** 保持收费计算和选中逻辑只使用 `chargeList.value`,展示使用 `settledChargeDisplayRows`。预存成功或重新查询后,不把历史行写入 `chargeList`;`selectedChargeRows` 在无欠费预存模式下保持空数组。 - [ ] **Step 6: 运行前端契约测试** Run: ```bash node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs ``` Expected: 2 个测试全部通过。 - [ ] **Step 7: 提交前端持久回显修复** ```bash 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: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=PaymentQueryServiceTest,ChargeServiceCounterPaymentTest,PaymentRecordServiceImplTest,CounterSettleApplicationServiceImplTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: 退出码 `0`,无失败和错误。 - [ ] **Step 2: 运行后端编译** Run: ```bash mvn -pl sw-business/sw-business-server -am -DskipTests compile ``` Expected: `BUILD SUCCESS`。 - [ ] **Step 3: 运行前端定向测试和既有柜台收费契约** Run: ```bash 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: ```bash pnpm build ``` Expected: 退出码 `0`;若存在既有警告,记录但不将警告表述为错误。 - [ ] **Step 5: 写验证证据** 证据文档必须记录: ```markdown # 柜台预存持久回显修复验证证据 - 验证日期: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: ```bash 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: 提交验证证据** ```bash git add docs/evidence/bugfix/2026-07-14-counter-topup-persistent-display.md git commit -m "docs: record counter topup display verification" ```