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

641 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 柜台预存持久回显与缴费历史修复实施计划
> **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<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: 增加场景过滤和导出回归测试**
新增测试:
```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<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:
```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<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` 参数并设置:
```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"
```