641 lines
24 KiB
Markdown
641 lines
24 KiB
Markdown
# 柜台预存持久回显与缴费历史修复实施计划
|
||
|
||
> **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"
|
||
```
|