From 48fd6cff41876de4ef9c9ac1b614d0246362aa7f Mon Sep 17 00:00:00 2001 From: tangweijie <877588133@qq.com> Date: Tue, 14 Jul 2026 09:10:18 +0800 Subject: [PATCH] docs: plan counter topup persistent display --- ...-07-14-counter-topup-persistent-display.md | 640 ++++++++++++++++++ 1 file changed, 640 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-14-counter-topup-persistent-display.md diff --git a/docs/superpowers/plans/2026-07-14-counter-topup-persistent-display.md b/docs/superpowers/plans/2026-07-14-counter-topup-persistent-display.md new file mode 100644 index 0000000..357110c --- /dev/null +++ b/docs/superpowers/plans/2026-07-14-counter-topup-persistent-display.md @@ -0,0 +1,640 @@ +# 柜台预存持久回显与缴费历史修复实施计划 + +> **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" +```