1. AGENTS.md 更新 - water-docs: 新增 specs/ 与 docs/design/ 生命周期规则章节 - water-backend: 更新协作引用(建设期/建成后、evidence 模块化) 2. specs/ 重复合并 - 006-reminder-event-design 合并入 003-rev006-reminder-event-design - 001-rev004-accounting 删除冗余 data-model.md + contracts/ - 002-rev005-invoice-flow 删除冗余 data-model.md + contracts/ 3. evidence 按模块归档 - 35 个 REV-004 文件归入 evidence/rev004-accounting/ - 7 个通用 bugfix 文件归入 evidence/bugfix/ 和 bugfix/frontend/ - 新建 rev005-invoice/、rev006-reminder/、rev007-statistics/ 目录 4. guides/ 清理 - 14 个 REV004_*.md 移入 evidence/rev004-accounting/ 5. 遗留文件处理 - docs/research/ 归档到 Archive/06_Migration_Plans/ - backend-check detached worktrees 清理 6. 交叉引用修复 - 006-reminder-event-design → 003-rev006-reminder-event-design - docs/guides/REV004_ → docs/evidence/rev004-accounting/REV004_ 7. DB 设计文档修正(01_Database_Design.md) - biz_invoice 明确为开票配置表,非发票记录表 - 新增 biz_invoice_record 为发票申请/结果主表 - 新增 biz_charge_invoice_rel 账单-发票关联说明 - REV-005 承接口径表名全部修正 8. 发票审计证据 - 新增 evidence/rev005-invoice/2026-06-16-invoice-document-audit.md
37 KiB
催缴管理真实数据与 Playwright 闭环验证 Implementation Plan
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: 消除催缴管理页面假数据,向测试库写入可重复的闭环 seed 数据,并用 Playwright 验证页面显示值与数据库/API 预期一致。
Architecture: 后端先补齐催缴记录统计/导出接口并稳定待催池分页/导出;测试库通过固定 ID + 固定客户编号写入最小闭环数据;前端两个页面只消费真实 API;Playwright 登录后进入真实页面,断言 seed 客户、统计值、催缴记录、明细展开和导出响应。
Tech Stack: Java 17, Spring Boot, MyBatis Plus, PostgreSQL, Vue 3, TypeScript, Element Plus, Axios wrapper (@/config/axios), Playwright, pnpm
Scope Check
本计划覆盖一个业务闭环:催缴管理 的待催池、催缴记录、统计、导出、测试造数与页面级验证。停水管理页面也存在 mock 数据,但属于独立停水/复水流程,不纳入本计划,避免跨 feature 混改。
Current Evidence
pending-summary已返回真实统计:custCount=8, arrearsCount=586, totalAmount=433917pending-page当前线上返回:{"code":500,"data":null,"msg":"系统异常"}pending-export当前线上返回:{"code":500,"data":null,"msg":"系统异常"}page当前线上返回:{"code":0,"data":{"list":[],"total":0},"msg":""}collectionRecord/index.vue统计条仍硬编码:267595¥1251256.78
- 测试库可直连:
- Host:
192.168.10.130 - Port:
5436 - DB:
sw_system - User:
sw_system - Password:
Em@123456
- Host:
File Structure
Backend
- Modify:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/arrearagereminder/ArrearageReminderController.java- 新增催缴记录统计
/summary - 新增催缴记录导出
/export - 确认待催池分页
/pending-page、待催池导出/pending-export不再 500
- 新增催缴记录统计
- Modify:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/arrearagereminder/ArrearageReminderService.java- 新增
getSummary - 新增
getExportList
- 新增
- Modify:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/arrearagereminder/ArrearageReminderServiceImpl.java- 实现催缴记录统计和导出列表
- Modify:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/arrearagereminder/ArrearageReminderMapper.java- 新增
selectListForExport - 新增
selectSummary
- 新增
- Create:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/arrearagereminder/vo/ArrearageReminderSummaryRespVO.java- 催缴记录统计响应 VO
- Modify/Test:
../water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/controller/admin/arrearagereminder/ArrearageReminderControllerTest.java- 覆盖
/summary和/export
- 覆盖
- Modify/Test:
../water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/arrearagereminder/ArrearageReminderServiceImplTest.java- 覆盖
getSummary和getExportList
- 覆盖
Seed Data
- Create:
../water-docs/sql/e2e/arrearage_reminder_seed.sql- 固定 seed 数据,重复执行安全
- Create:
../water-docs/docs/evidence/arrearage-reminder-real-data-e2e.md- 记录 SQL 执行结果、API 响应、Playwright 结果
Frontend
- Modify:
../water-frontend/src/api/collectionManage/arrears/index.ts- 新增
getReminderSummary - 新增
exportReminder
- 新增
- Modify:
../water-frontend/src/views/collectionManage/collectionRecord/index.vue- 删除统计硬编码
- 统计条接
/summary - 导出接
/export - 查询条件使用 seed 可验证字段
- Modify:
../water-frontend/src/views/collectionManage/arrears/index.vue- 确认待催池统计、列表、导出全走 API;无硬编码统计
- Modify:
../water-frontend/tests/e2e/arrearageReminder.e2e.spec.ts- 从 API-only 测试升级为页面级断言
- 断言页面显示与 seed 数据一致
Expected Seed Values
固定 seed 前缀:E2E_AR_
待催池筛选 code=E2E_AR
- 客户数:
2 - 欠费笔数:
5 - 水量:
150.000 - 账单金额:
1500.00 - 违约金:
150.00 - 合计金额:
1650.00 - 应缴金额:
1650.00
催缴记录筛选 reminderUser=E2E催缴员
- 记录数:
2 - 总水量:
150.000 - 账单金额:
1500.00 - 违约金:
150.00 - 预存余额合计:
35.00 - 手机号:
1380000000113800000002
Task 1: 后端补齐催缴记录统计与导出接口
Files:
-
Create:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/arrearagereminder/vo/ArrearageReminderSummaryRespVO.java -
Modify:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/arrearagereminder/ArrearageReminderService.java -
Modify:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/arrearagereminder/ArrearageReminderServiceImpl.java -
Modify:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/arrearagereminder/ArrearageReminderMapper.java -
Modify:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/arrearagereminder/ArrearageReminderController.java -
Step 1: 创建催缴记录统计 VO
Create ../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/arrearagereminder/vo/ArrearageReminderSummaryRespVO.java:
package cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - 催缴记录汇总 Response VO")
@Data
public class ArrearageReminderSummaryRespVO {
@Schema(description = "催缴记录数")
private Long reminderCount;
@Schema(description = "客户数")
private Long custCount;
@Schema(description = "欠费水量")
private BigDecimal totalBillWater;
@Schema(description = "账单金额")
private BigDecimal totalExtendedAmount;
@Schema(description = "违约金")
private BigDecimal totalLateFee;
@Schema(description = "预存余额")
private BigDecimal totalDeposit;
}
- Step 2: 扩展 Service 接口
Modify ArrearageReminderService.java imports:
import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderExportExcelVO;
import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderSummaryRespVO;
Add methods:
ArrearageReminderSummaryRespVO getSummary(ArrearageReminderPageReqVO reqVO);
List<ArrearageReminderExportExcelVO> getExportList(ArrearageReminderPageReqVO reqVO);
- Step 3: 扩展 Mapper
Modify ArrearageReminderMapper.java imports:
import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderSummaryRespVO;
import java.math.BigDecimal;
import java.util.List;
Add methods inside interface:
default List<ArrearageReminderDO> selectListForExport(ArrearageReminderPageReqVO reqVO) {
return selectList(new LambdaQueryWrapperX<ArrearageReminderDO>()
.eqIfPresent(ArrearageReminderDO::getCustId, reqVO.getCustId())
.eqIfPresent(ArrearageReminderDO::getReminderUser, reqVO.getReminderUser())
.eqIfPresent(ArrearageReminderDO::getReminderType, reqVO.getReminderType())
.eqIfPresent(ArrearageReminderDO::getReminderReason, reqVO.getReminderReason())
.eqIfPresent(ArrearageReminderDO::getReminderResult, reqVO.getReminderResult())
.orderByDesc(ArrearageReminderDO::getId));
}
default ArrearageReminderSummaryRespVO selectSummary(ArrearageReminderPageReqVO reqVO) {
List<ArrearageReminderDO> list = selectListForExport(reqVO);
ArrearageReminderSummaryRespVO summary = new ArrearageReminderSummaryRespVO();
summary.setReminderCount((long) list.size());
summary.setCustCount(list.stream()
.map(ArrearageReminderDO::getCustId)
.filter(java.util.Objects::nonNull)
.distinct()
.count());
summary.setTotalBillWater(list.stream()
.map(ArrearageReminderDO::getTotalBillWater)
.filter(java.util.Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add));
summary.setTotalExtendedAmount(list.stream()
.map(ArrearageReminderDO::getTotalExtendedAmount)
.filter(java.util.Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add));
summary.setTotalLateFee(list.stream()
.map(ArrearageReminderDO::getTotalLateFee)
.filter(java.util.Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add));
summary.setTotalDeposit(list.stream()
.map(ArrearageReminderDO::getDeposit)
.filter(java.util.Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add));
return summary;
}
- Step 4: ServiceImpl 实现 summary/export
Add imports:
import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderExportExcelVO;
import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderSummaryRespVO;
Add methods:
@Override
public ArrearageReminderSummaryRespVO getSummary(ArrearageReminderPageReqVO reqVO) {
ArrearageReminderSummaryRespVO summary = arrearageReminderMapper.selectSummary(reqVO);
if (summary.getReminderCount() == null) summary.setReminderCount(0L);
if (summary.getCustCount() == null) summary.setCustCount(0L);
summary.setTotalBillWater(nvl(summary.getTotalBillWater()));
summary.setTotalExtendedAmount(nvl(summary.getTotalExtendedAmount()));
summary.setTotalLateFee(nvl(summary.getTotalLateFee()));
summary.setTotalDeposit(nvl(summary.getTotalDeposit()));
return summary;
}
@Override
public List<ArrearageReminderExportExcelVO> getExportList(ArrearageReminderPageReqVO reqVO) {
List<ArrearageReminderDO> reminders = arrearageReminderMapper.selectListForExport(reqVO);
if (reminders == null || reminders.isEmpty()) {
return List.of();
}
List<Long> custIds = reminders.stream()
.map(ArrearageReminderDO::getCustId)
.filter(Objects::nonNull)
.distinct()
.toList();
Map<Long, CustDO> custMap = custService.getCustsByIds(custIds).stream()
.collect(Collectors.toMap(CustDO::getId, cust -> cust, (v1, v2) -> v1));
return reminders.stream()
.map(item -> toExportExcelVO(item, custMap.get(item.getCustId())))
.toList();
}
private ArrearageReminderExportExcelVO toExportExcelVO(ArrearageReminderDO item, CustDO cust) {
ArrearageReminderExportExcelVO vo = new ArrearageReminderExportExcelVO();
vo.setCustCode(cust != null ? cust.getCode() : null);
vo.setCustName(cust != null ? cust.getName() : null);
vo.setReminderType(item.getReminderType());
vo.setReminderReason(item.getReminderReason());
vo.setReminderUser(item.getReminderUser());
vo.setReminderResult(item.getReminderResult());
vo.setTotalBillWater(item.getTotalBillWater());
vo.setTotalExtendedAmount(item.getTotalExtendedAmount());
vo.setTotalLateFee(item.getTotalLateFee());
vo.setDeposit(item.getDeposit());
vo.setMobile(item.getMobile());
vo.setCreateTime(item.getCreateTime());
vo.setRemark(item.getRemark());
return vo;
}
- Step 5: Controller 新增 summary/export
Add imports:
import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderExportExcelVO;
import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderSummaryRespVO;
import cn.com.emsoft.sw.framework.excel.core.util.ExcelUtils;
import jakarta.servlet.http.HttpServletResponse;
Add endpoints:
@GetMapping("/summary")
@Operation(summary = "催缴记录汇总统计")
@PreAuthorize("@ss.hasPermission('business:charge:query')")
public CommonResult<ArrearageReminderSummaryRespVO> summary(@Valid ArrearageReminderPageReqVO reqVO) {
return success(arrearageReminderService.getSummary(reqVO));
}
@GetMapping("/export")
@Operation(summary = "催缴记录导出")
@PreAuthorize("@ss.hasPermission('business:charge:export')")
public void export(@Valid ArrearageReminderPageReqVO reqVO, HttpServletResponse response) throws Exception {
List<ArrearageReminderExportExcelVO> list = arrearageReminderService.getExportList(reqVO);
ExcelUtils.write(response, "催缴记录.xlsx", "数据", ArrearageReminderExportExcelVO.class, list);
}
- Step 6: 编译验证
Run:
cd ../water-backend
mvn compile -pl sw-business/sw-business-server -am -q
Expected: exit code 0
- Step 7: Commit
cd ../water-backend
git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/arrearagereminder \
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/arrearagereminder \
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/arrearagereminder
git commit -m "feat: add arrearage reminder record summary and export"
Task 2: 诊断并修复 pending-page / pending-export 500
Files:
-
Inspect/Modify:
../water-backend/sw-business/sw-business-server/src/main/resources/mapper/arrearagereminder/ArrearageReminderQueryMapper.xml -
Inspect/Modify:
../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/arrearagereminder/ArrearageReminderQueryServiceImpl.java -
Test:
../water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/arrearagereminder/ArrearageReminderQueryServiceImplTest.java -
Step 1: 用线上接口复现 500
Run:
TOKEN=$(curl -s -X POST https://sw-api.ingress.hwpc.1msoft.cn/admin-api/system/auth/login \
-H 'Content-Type: application/json' -H 'tenant-id: 1' \
-d '{"username":"admin","password":"admin123"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['accessToken'])")
curl -s "https://sw-api.ingress.hwpc.1msoft.cn/admin-api/business/arrearage-reminder/pending-page?pageNo=1&pageSize=1" \
-H "Authorization: Bearer $TOKEN" -H "tenant-id: 1"
curl -s -D /tmp/arrearage-export.headers \
"https://sw-api.ingress.hwpc.1msoft.cn/admin-api/business/arrearage-reminder/pending-export?pageNo=1&pageSize=1" \
-H "Authorization: Bearer $TOKEN" -H "tenant-id: 1" \
-o /tmp/arrearage-export.body
Expected before fix:
{"code":500,"data":null,"msg":"系统异常"}
- Step 2: 手工验证 SQL 主查询和明细查询
Run:
PGPASSWORD='Em@123456' psql -h 192.168.10.130 -p 5436 -U sw_system -d sw_system -c "
SELECT COUNT(*)
FROM (
SELECT c.cust_id
FROM biz_charge c
LEFT JOIN biz_cust cust ON cust.id = c.cust_id AND cust.deleted = 0
WHERE c.deleted = 0 AND c.pay_state = 0 AND cust.id IS NOT NULL
GROUP BY c.cust_id
) t;"
Expected: count is greater than 0.
Run:
PGPASSWORD='Em@123456' psql -h 192.168.10.130 -p 5436 -U sw_system -d sw_system -c "
SELECT c.cust_id, c.id, c.bill_month, c.last_reading, c.reading,
c.bill_water, c.extended_amount, c.late_fee, c.read_date
FROM biz_charge c
LEFT JOIN biz_cust cust ON cust.id = c.cust_id AND cust.deleted = 0
WHERE c.deleted = 0 AND c.pay_state = 0 AND cust.id IS NOT NULL
ORDER BY c.cust_id, c.bill_month, c.id
LIMIT 3;"
Expected: rows return without SQL error.
- Step 3: Add a regression test for pending page service
Modify ArrearageReminderQueryServiceImplTest.java to include a test that mocks mapper outputs and verifies service assembly does not throw.
@Test
void getPendingPage_shouldAttachSummaryFieldsAndDetails() {
ArrearagePendingPageReqVO reqVO = new ArrearagePendingPageReqVO();
reqVO.setPageNo(1);
reqVO.setPageSize(10);
ArrearagePendingPageRespVO row = new ArrearagePendingPageRespVO();
row.setCustId(990001L);
row.setCustCode("E2E_AR_001");
row.setCustName("催缴E2E客户A");
when(arrearageReminderQueryMapper.selectPendingCustomerPage(any(), any(), any()))
.thenReturn(new PageResult<>(List.of(row), 1L));
CustDO cust = CustDO.builder().id(990001L).code("E2E_AR_001").name("催缴E2E客户A").status(0).build();
when(custService.getCustsByIds(List.of(990001L))).thenReturn(List.of(cust));
AccountDO account = AccountDO.builder().custId(990001L).deposit(new BigDecimal("12.34")).build();
when(accountService.listByCustIds(List.of(990001L))).thenReturn(List.of(account));
CustContactDO contact = CustContactDO.builder().custId(990001L).mobile("13800000001").status(0).build();
when(custContactService.listByCustIds(List.of(990001L))).thenReturn(List.of(contact));
when(arrearageReminderQueryMapper.selectPendingChargeDetails(any(), eq(List.of(990001L)), any(), any()))
.thenReturn(Map.of(990001L, List.of(new ArrearagePendingChargeDetailRespVO())));
when(arrearageReminderQueryMapper.selectRemindedCustomerIdsThisMonth(eq(List.of(990001L)), any(), any()))
.thenReturn(Set.of());
PageResult<ArrearagePendingPageRespVO> result = service.getPendingPage(reqVO);
assertThat(result.getTotal()).isEqualTo(1L);
assertThat(result.getList()).hasSize(1);
assertThat(result.getList().get(0).getMobile()).isEqualTo("13800000001");
assertThat(result.getList().get(0).getPrestoreAmount()).isEqualByComparingTo("12.34");
}
- Step 4: Fix implementation based on failing point
If the service throws because custService.getCustsByIds(custIds) returns null in production, modify:
Map<Long, CustDO> custMap = nullSafe(custService.getCustsByIds(custIds)).stream()
.filter(cust -> cust.getId() != null)
.collect(Collectors.toMap(CustDO::getId, Function.identity(), (v1, v2) -> v1));
If mapper XML throws only on export/page but summary works, temporarily remove the agreementNo and contractNo subqueries from page list and add them back only after a local SQL test proves the subquery works on deployed schema. The page list must return stable customer rows before optional collection fields.
Replace the two subquery blocks in selectPendingCustomerPageList with:
NULL AS agreementNo,
NULL AS contractNo
This is acceptable because agreement/contract display is optional for arrearage reminder validation, while page availability is required.
- Step 5: Run focused backend tests
cd ../water-backend
mvn test -pl sw-business/sw-business-server \
-Dtest="cn.com.emsoft.sw.business.service.arrearagereminder.ArrearageReminderQueryServiceImplTest,cn.com.emsoft.sw.business.controller.admin.arrearagereminder.ArrearageReminderControllerTest" \
-Dsurefire.failIfNoSpecifiedTests=false
Expected: all arrearage tests pass.
- Step 6: Commit and push
cd ../water-backend
git add sw-business/sw-business-server/src/main/resources/mapper/arrearagereminder/ArrearageReminderQueryMapper.xml \
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/arrearagereminder/ArrearageReminderQueryServiceImpl.java \
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/arrearagereminder/ArrearageReminderQueryServiceImplTest.java
git commit -m "fix: stabilize arrearage pending page and export queries"
git push origin develop
Task 3: 写入测试库闭环 seed 数据
Files:
-
Create:
../water-docs/sql/e2e/arrearage_reminder_seed.sql -
Modify:
../water-docs/docs/evidence/arrearage-reminder-real-data-e2e.md -
Step 1: 创建 seed SQL
Create ../water-docs/sql/e2e/arrearage_reminder_seed.sql:
BEGIN;
DELETE FROM biz_arrearage_reminder_detail WHERE arrearage_reminder_id IN (990201, 990202);
DELETE FROM biz_arrearage_reminder WHERE id IN (990201, 990202);
DELETE FROM biz_charge WHERE id IN (990101, 990102, 990103, 990104, 990105);
DELETE FROM biz_cust_contact WHERE id IN (990011, 990012);
DELETE FROM biz_account WHERE id IN (990021, 990022);
DELETE FROM biz_cust WHERE id IN (990001, 990002);
INSERT INTO biz_cust (
id, code, name, population, address, price_template_code, dept_id,
cust_meter_id, cust_invoice_id, type, certificate_type, pay_method,
certificate_account, contract_date, is_over, is_preferential_scheme,
credit_rate, status, create_time, update_time, deleted, tenant_id
) VALUES
(
990001, 'E2E_AR_001', '催缴E2E客户A', 3, 'E2E测试地址A', 'E2E_PRICE',
1, 990001, 990001, 1, 1, 1, 'E2E_CERT_001', now(), 0, 0, 0, 0,
now(), now(), 0, 1
),
(
990002, 'E2E_AR_002', '催缴E2E客户B', 4, 'E2E测试地址B', 'E2E_PRICE',
1, 990002, 990002, 1, 1, 1, 'E2E_CERT_002', now(), 0, 0, 0, 0,
now(), now(), 0, 1
);
INSERT INTO biz_account (
id, cust_id, deposit, uncheck_money, overdraft, status,
create_time, update_time, deleted, tenant_id
) VALUES
(990021, 990001, 12.00, 0.00, 0.00, 0, now(), now(), 0, 1),
(990022, 990002, 23.00, 0.00, 0.00, 0, now(), now(), 0, 1);
INSERT INTO biz_cust_contact (
id, cust_id, contact_type, contact, mobile, status,
create_time, update_time, deleted, tenant_id
) VALUES
(990011, 990001, 1, 'E2E联系人A', '13800000001', 0, now(), now(), 0, 1),
(990012, 990002, 1, 'E2E联系人B', '13800000002', 0, now(), now(), 0, 1);
INSERT INTO biz_charge (
id, meter_id, record_id, bill_month, dept_id, book_id, book_sort_index,
cust_id, cust_code, cust_name, cust_address, last_reading, reading,
bill_water, bill_amount, extended_amount, late_fee, pay_state,
price_template_code, read_date, create_time, update_time, deleted, tenant_id
) VALUES
(990101, 990001, 990101, 202601, 1, 1, 1, 990001, 'E2E_AR_001', '催缴E2E客户A', 'E2E测试地址A', 0.000, 10.000, 10.000, 100.00, 100.00, 10.00, 0, 'E2E_PRICE', '2026-01-15', now(), now(), 0, 1),
(990102, 990001, 990102, 202602, 1, 1, 2, 990001, 'E2E_AR_001', '催缴E2E客户A', 'E2E测试地址A', 10.000, 30.000, 20.000, 200.00, 200.00, 20.00, 0, 'E2E_PRICE', '2026-02-15', now(), now(), 0, 1),
(990103, 990001, 990103, 202603, 1, 1, 3, 990001, 'E2E_AR_001', '催缴E2E客户A', 'E2E测试地址A', 30.000, 60.000, 30.000, 300.00, 300.00, 30.00, 0, 'E2E_PRICE', '2026-03-15', now(), now(), 0, 1),
(990104, 990002, 990104, 202602, 1, 1, 4, 990002, 'E2E_AR_002', '催缴E2E客户B', 'E2E测试地址B', 0.000, 40.000, 40.000, 400.00, 400.00, 40.00, 0, 'E2E_PRICE', '2026-02-16', now(), now(), 0, 1),
(990105, 990002, 990105, 202603, 1, 1, 5, 990002, 'E2E_AR_002', '催缴E2E客户B', 'E2E测试地址B', 40.000, 90.000, 50.000, 500.00, 500.00, 50.00, 0, 'E2E_PRICE', '2026-03-16', now(), now(), 0, 1);
INSERT INTO biz_arrearage_reminder (
id, cust_id, reminder_type, reminder_reason, reminder_user, remark,
reminder_template, complete_time, push_state, push_results,
reminder_result, batch_stamp, total_bill_water, total_extended_amount,
total_late_fee, deposit, mobile, create_time, update_time, deleted, tenant_id
) VALUES
(990201, 990001, 2, 1, 'E2E催缴员', 'E2E催缴记录A', NULL, now(), 0, NULL, 0, 'E2E_AR_BATCH', 60.000, 600.00, 60.00, 12.00, '13800000001', now(), now(), 0, 1),
(990202, 990002, 3, 1, 'E2E催缴员', 'E2E催缴记录B', NULL, now(), 0, NULL, 0, 'E2E_AR_BATCH', 90.000, 900.00, 90.00, 23.00, '13800000002', now(), now(), 0, 1);
INSERT INTO biz_arrearage_reminder_detail (
id, arrearage_reminder_id, charge_id, late_fee,
create_time, update_time, deleted, tenant_id
) VALUES
(990301, 990201, 990101, 10.00, now(), now(), 0, 1),
(990302, 990201, 990102, 20.00, now(), now(), 0, 1),
(990303, 990201, 990103, 30.00, now(), now(), 0, 1),
(990304, 990202, 990104, 40.00, now(), now(), 0, 1),
(990305, 990202, 990105, 50.00, now(), now(), 0, 1);
SELECT setval('biz_cust_seq', GREATEST((SELECT COALESCE(MAX(id), 1) FROM biz_cust), 990002));
SELECT setval('biz_account_seq', GREATEST((SELECT COALESCE(MAX(id), 1) FROM biz_account), 990022));
SELECT setval('biz_cust_contact_seq', GREATEST((SELECT COALESCE(MAX(id), 1) FROM biz_cust_contact), 990012));
SELECT setval('biz_charge_seq', GREATEST((SELECT COALESCE(MAX(id), 1) FROM biz_charge), 990105));
SELECT setval('biz_arrearage_reminder_seq', GREATEST((SELECT COALESCE(MAX(id), 1) FROM biz_arrearage_reminder), 990202));
SELECT setval('biz_arrearage_reminder_detail_seq', GREATEST((SELECT COALESCE(MAX(id), 1) FROM biz_arrearage_reminder_detail), 990305));
COMMIT;
- Step 2: 执行 seed SQL
PGPASSWORD='Em@123456' psql -h 192.168.10.130 -p 5436 -U sw_system -d sw_system \
-f ../water-docs/sql/e2e/arrearage_reminder_seed.sql
Expected: COMMIT
- Step 3: 验证 seed 聚合值
PGPASSWORD='Em@123456' psql -h 192.168.10.130 -p 5436 -U sw_system -d sw_system -c "
SELECT COUNT(DISTINCT cust_id) AS cust_count,
COUNT(*) AS charge_count,
SUM(bill_water) AS water,
SUM(extended_amount) AS bill,
SUM(late_fee) AS late_fee,
SUM(extended_amount + late_fee) AS total
FROM biz_charge
WHERE deleted = 0 AND pay_state = 0 AND cust_code LIKE 'E2E_AR%';"
Expected:
cust_count=2
charge_count=5
water=150.000
bill=1500.00
late_fee=150.00
total=1650.00
Run:
PGPASSWORD='Em@123456' psql -h 192.168.10.130 -p 5436 -U sw_system -d sw_system -c "
SELECT COUNT(*) AS reminder_count,
COUNT(DISTINCT cust_id) AS cust_count,
SUM(total_bill_water) AS water,
SUM(total_extended_amount) AS bill,
SUM(total_late_fee) AS late_fee,
SUM(deposit) AS deposit
FROM biz_arrearage_reminder
WHERE deleted = 0 AND reminder_user = 'E2E催缴员';"
Expected:
reminder_count=2
cust_count=2
water=150.000
bill=1500.00
late_fee=150.00
deposit=35.00
- Step 4: Commit docs artifacts
cd ../water-docs
git add sql/e2e/arrearage_reminder_seed.sql
git commit -m "testdata: add arrearage reminder e2e seed"
Task 4: 前端接入催缴记录真实统计与导出
Files:
-
Modify:
../water-frontend/src/api/collectionManage/arrears/index.ts -
Modify:
../water-frontend/src/views/collectionManage/collectionRecord/index.vue -
Inspect:
../water-frontend/src/views/collectionManage/arrears/index.vue -
Step 1: API 新增催缴记录 summary/export
Modify ../water-frontend/src/api/collectionManage/arrears/index.ts:
export interface ArrearageReminderSummaryRespVO {
reminderCount: number
custCount: number
totalBillWater: number
totalExtendedAmount: number
totalLateFee: number
totalDeposit: number
}
Add methods in ArrearsApi:
getReminderSummary: async (params: ArrearageReminderPageReqVO) => {
return await request.get<ArrearageReminderSummaryRespVO>({
url: '/business/arrearage-reminder/summary',
params
})
},
exportReminder: async (params: ArrearageReminderPageReqVO) => {
const res = await request.download({
url: '/business/arrearage-reminder/export',
params
})
download.response(res, '催缴记录.xlsx')
}
- Step 2: collectionRecord 增加 summary ref
In collectionRecord/index.vue script near const total = ref(0):
const summary = ref({
reminderCount: 0,
custCount: 0,
totalBillWater: 0,
totalExtendedAmount: 0,
totalLateFee: 0,
totalDeposit: 0
})
- Step 3: 建立记录查询参数方法
Add:
const buildReminderPageParams = () => ({
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
reminderUser: queryParams.remindUser || undefined,
reminderReason: queryParams.remindReason ? Number(queryParams.remindReason) : undefined,
reminderResult: queryParams.remindResult ? Number(queryParams.remindResult) : undefined
})
Modify getList to use:
const data = await ArrearsApi.getPage(buildReminderPageParams())
- Step 4: 获取真实 summary
Add:
const fetchSummary = async () => {
summary.value = await ArrearsApi.getReminderSummary(buildReminderPageParams())
}
Modify:
const handleQuery = () => {
queryParams.pageNo = 1
Promise.all([getList(), fetchSummary()])
}
onMounted(async () => {
await Promise.resolve(getSiteTree())
await Promise.all([getList(), fetchSummary()])
})
- Step 5: 删除统计硬编码并替换模板
Replace current <el-descriptions> block with:
<el-descriptions :column="5" class="mt-15px mb-8px" border label-width="120">
<el-descriptions-item label="记录数">
<span class="text-orange-500 font-bold">{{ summary.reminderCount }}</span>
</el-descriptions-item>
<el-descriptions-item label="客户数">
<span class="text-orange-500 font-bold">{{ summary.custCount }}</span>
</el-descriptions-item>
<el-descriptions-item label="欠费水量">
<span class="text-orange-500 font-bold">{{ summary.totalBillWater }}</span>
</el-descriptions-item>
<el-descriptions-item label="账单金额">
<span class="text-orange-500 font-bold">¥{{ summary.totalExtendedAmount }}</span>
</el-descriptions-item>
<el-descriptions-item label="违约金">
<span class="text-orange-500 font-bold">¥{{ summary.totalLateFee }}</span>
</el-descriptions-item>
<el-descriptions-item label="预存余额">
<span class="text-orange-500 font-bold">¥{{ summary.totalDeposit }}</span>
</el-descriptions-item>
</el-descriptions>
- Step 6: 导出改真实接口
Replace handleExport:
const handleExport = async () => {
try {
await ArrearsApi.exportReminder(buildReminderPageParams())
message.success('导出成功')
} catch {
message.error('导出失败')
}
}
- Step 7: 前端构建验证
cd ../water-frontend
pnpm run build:dev
Expected: build exits 0.
- Step 8: Commit and push
cd ../water-frontend
git add src/api/collectionManage/arrears/index.ts \
src/views/collectionManage/collectionRecord/index.vue \
src/views/collectionManage/arrears/index.vue
git commit -m "feat: wire arrearage reminder record summary and export to real APIs"
git push origin develop
Task 5: Playwright 页面级验证真实数据
Files:
-
Modify:
../water-frontend/tests/e2e/arrearageReminder.e2e.spec.ts -
Modify:
../water-docs/docs/evidence/arrearage-reminder-real-data-e2e.md -
Step 1: Playwright 测试中加入固定预期
Modify arrearageReminder.e2e.spec.ts:
const E2E_EXPECTED = {
pool: {
code: 'E2E_AR',
custCount: '2',
arrearsCount: '5',
water: '150',
bill: '1500',
lateFee: '150',
total: '1650'
},
records: {
reminderUser: 'E2E催缴员',
reminderCount: '2',
custCount: '2',
water: '150',
bill: '1500',
lateFee: '150',
deposit: '35',
customerA: 'E2E_AR_001',
customerB: 'E2E_AR_002',
phoneA: '13800000001',
phoneB: '13800000002'
}
}
- Step 2: 用菜单文本进入页面
Use menu click rather than direct route path:
async function openMenu(page, menuText: string) {
await page.goto('/', { waitUntil: 'domcontentloaded' })
await page.waitForTimeout(3000)
const menu = page.locator('.el-menu').getByText(menuText, { exact: true }).first()
await expect(menu).toBeVisible({ timeout: 15000 })
await menu.click()
await page.waitForTimeout(3000)
}
- Step 3: 新增欠费催缴池页面断言
test('欠费催缴池显示 seed 客户与真实统计', async ({ page }) => {
await loginAndSetupAuth(page)
await openMenu(page, '欠费催缴')
await page.getByPlaceholder('请输入客户编号').fill(E2E_EXPECTED.pool.code)
await page.getByRole('button', { name: /查询/ }).click()
await page.waitForTimeout(3000)
await expect(page.getByText('E2E_AR_001')).toBeVisible()
await expect(page.getByText('E2E_AR_002')).toBeVisible()
const summary = page.locator('.el-descriptions').first()
await expect(summary).toContainText(E2E_EXPECTED.pool.custCount)
await expect(summary).toContainText(E2E_EXPECTED.pool.arrearsCount)
await expect(summary).toContainText(E2E_EXPECTED.pool.water)
await expect(summary).toContainText(E2E_EXPECTED.pool.bill)
await expect(summary).toContainText(E2E_EXPECTED.pool.lateFee)
await expect(summary).toContainText(E2E_EXPECTED.pool.total)
await page.screenshot({ path: 'test-results/arrearage-pool-real-data.png', fullPage: true })
})
- Step 4: 新增催缴记录页面断言
test('催缴记录显示 seed 记录与真实统计', async ({ page }) => {
await loginAndSetupAuth(page)
await openMenu(page, '催缴记录')
await page.getByPlaceholder('请选择催缴员').click()
await page.getByText(E2E_EXPECTED.records.reminderUser).click()
await page.getByRole('button', { name: /查询/ }).click()
await page.waitForTimeout(3000)
await expect(page.getByText(E2E_EXPECTED.records.customerA)).toBeVisible()
await expect(page.getByText(E2E_EXPECTED.records.customerB)).toBeVisible()
await expect(page.getByText(E2E_EXPECTED.records.phoneA)).toBeVisible()
await expect(page.getByText(E2E_EXPECTED.records.phoneB)).toBeVisible()
const summary = page.locator('.el-descriptions').first()
await expect(summary).toContainText(E2E_EXPECTED.records.reminderCount)
await expect(summary).toContainText(E2E_EXPECTED.records.custCount)
await expect(summary).toContainText(E2E_EXPECTED.records.water)
await expect(summary).toContainText(E2E_EXPECTED.records.bill)
await expect(summary).toContainText(E2E_EXPECTED.records.lateFee)
await expect(summary).toContainText(E2E_EXPECTED.records.deposit)
await page.screenshot({ path: 'test-results/arrearage-record-real-data.png', fullPage: true })
})
- Step 5: 新增催缴记录展开明细断言
test('催缴记录展开后显示账单明细', async ({ page }) => {
await loginAndSetupAuth(page)
await openMenu(page, '催缴记录')
await page.getByPlaceholder('请选择催缴员').click()
await page.getByText(E2E_EXPECTED.records.reminderUser).click()
await page.getByRole('button', { name: /查询/ }).click()
await page.waitForTimeout(3000)
const expandButton = page.locator('button').filter({ has: page.locator('[class*=icon]') }).first()
await expandButton.click()
await page.waitForTimeout(2000)
await expect(page.getByText('990101')).toBeVisible()
await expect(page.getByText('10.00')).toBeVisible()
await page.screenshot({ path: 'test-results/arrearage-record-detail-real-data.png', fullPage: true })
})
- Step 6: Run Playwright
cd ../water-frontend
npx playwright test tests/e2e/arrearageReminder.e2e.spec.ts --reporter=list
Expected:
3 passed
- Step 7: Record evidence
Create ../water-docs/docs/evidence/arrearage-reminder-real-data-e2e.md:
# 催缴管理真实数据 E2E Evidence
Date: 2026-06-08
## Seed
Command:
```bash
PGPASSWORD='Em@123456' psql -h 192.168.10.130 -p 5436 -U sw_system -d sw_system -f ../water-docs/sql/e2e/arrearage_reminder_seed.sql
Expected seed values:
- pending pool: 2 customers, 5 bills, water 150, bill 1500, late fee 150, total 1650
- reminder records: 2 records, 2 customers, water 150, bill 1500, late fee 150, deposit 35
API Verification
GET /business/arrearage-reminder/pending-page?code=E2E_ARGET /business/arrearage-reminder/pending-summary?code=E2E_ARGET /business/arrearage-reminder/page?reminderUser=E2E催缴员GET /business/arrearage-reminder/summary?reminderUser=E2E催缴员GET /business/arrearage-reminder/detail-list?reminderId=990201
Playwright
Command:
npx playwright test tests/e2e/arrearageReminder.e2e.spec.ts --reporter=list
Expected: 3 passed
- [ ] **Step 8: Commit**
```bash
cd ../water-frontend
git add tests/e2e/arrearageReminder.e2e.spec.ts
git commit -m "test: verify arrearage reminder pages with seeded real data"
cd ../water-docs
git add docs/evidence/arrearage-reminder-real-data-e2e.md
git commit -m "docs: record arrearage reminder real data e2e evidence"
Final Verification
Run after all tasks:
cd ../water-backend
mvn test -pl sw-business/sw-business-server \
-Dtest="cn.com.emsoft.sw.business.service.arrearagereminder.*,cn.com.emsoft.sw.business.controller.admin.arrearagereminder.*" \
-Dsurefire.failIfNoSpecifiedTests=false
cd ../water-frontend
pnpm run build:dev
npx playwright test tests/e2e/arrearageReminder.e2e.spec.ts --reporter=list
Expected:
- Backend tests pass
- Frontend build passes
- Playwright page-level tests pass
rg -n "267595|1251256|模拟|mock|allMockData|detailsMock" src/views/collectionManage/arrears src/views/collectionManage/collectionRecordreturns no matches
Self-Review
Spec coverage:
- Fake data removed from arrears/collectionRecord scope: covered by Task 4 and final
rg - Seed real database data: covered by Task 3
- Playwright validates page values: covered by Task 5
- Backend endpoints required for real data: covered by Tasks 1 and 2
Placeholder scan:
- No placeholder wording remains in task instructions.
- Every code-changing step includes exact file and snippet.
Type consistency:
- Frontend
ArrearageReminderSummaryRespVOfields match backendArrearageReminderSummaryRespVO. - Seed expected values match SQL inserts and Playwright assertions.