# 催缴管理真实数据与 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=433917` - `pending-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` --- ## 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` - 手机号: - `13800000001` - `13800000002` --- ### 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`: ```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: ```java import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderExportExcelVO; import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderSummaryRespVO; ``` Add methods: ```java ArrearageReminderSummaryRespVO getSummary(ArrearageReminderPageReqVO reqVO); List getExportList(ArrearageReminderPageReqVO reqVO); ``` - [ ] **Step 3: 扩展 Mapper** Modify `ArrearageReminderMapper.java` imports: ```java import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderSummaryRespVO; import java.math.BigDecimal; import java.util.List; ``` Add methods inside interface: ```java default List selectListForExport(ArrearageReminderPageReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .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 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: ```java import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderExportExcelVO; import cn.com.emsoft.sw.business.controller.admin.arrearagereminder.vo.ArrearageReminderSummaryRespVO; ``` Add methods: ```java @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 getExportList(ArrearageReminderPageReqVO reqVO) { List reminders = arrearageReminderMapper.selectListForExport(reqVO); if (reminders == null || reminders.isEmpty()) { return List.of(); } List custIds = reminders.stream() .map(ArrearageReminderDO::getCustId) .filter(Objects::nonNull) .distinct() .toList(); Map 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: ```java 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: ```java @GetMapping("/summary") @Operation(summary = "催缴记录汇总统计") @PreAuthorize("@ss.hasPermission('business:charge:query')") public CommonResult 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 list = arrearageReminderService.getExportList(reqVO); ExcelUtils.write(response, "催缴记录.xlsx", "数据", ArrearageReminderExportExcelVO.class, list); } ``` - [ ] **Step 6: 编译验证** Run: ```bash cd ../water-backend mvn compile -pl sw-business/sw-business-server -am -q ``` Expected: exit code `0` - [ ] **Step 7: Commit** ```bash 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: ```bash 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: ```json {"code":500,"data":null,"msg":"系统异常"} ``` - [ ] **Step 2: 手工验证 SQL 主查询和明细查询** Run: ```bash 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: ```bash 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. ```java @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 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: ```java Map 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: ```xml 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** ```bash 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** ```bash 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`: ```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** ```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: `COMMIT` - [ ] **Step 3: 验证 seed 聚合值** ```bash 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: ```text cust_count=2 charge_count=5 water=150.000 bill=1500.00 late_fee=150.00 total=1650.00 ``` Run: ```bash 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: ```text reminder_count=2 cust_count=2 water=150.000 bill=1500.00 late_fee=150.00 deposit=35.00 ``` - [ ] **Step 4: Commit docs artifacts** ```bash 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`: ```typescript export interface ArrearageReminderSummaryRespVO { reminderCount: number custCount: number totalBillWater: number totalExtendedAmount: number totalLateFee: number totalDeposit: number } ``` Add methods in `ArrearsApi`: ```typescript getReminderSummary: async (params: ArrearageReminderPageReqVO) => { return await request.get({ 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)`: ```typescript const summary = ref({ reminderCount: 0, custCount: 0, totalBillWater: 0, totalExtendedAmount: 0, totalLateFee: 0, totalDeposit: 0 }) ``` - [ ] **Step 3: 建立记录查询参数方法** Add: ```typescript 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: ```typescript const data = await ArrearsApi.getPage(buildReminderPageParams()) ``` - [ ] **Step 4: 获取真实 summary** Add: ```typescript const fetchSummary = async () => { summary.value = await ArrearsApi.getReminderSummary(buildReminderPageParams()) } ``` Modify: ```typescript const handleQuery = () => { queryParams.pageNo = 1 Promise.all([getList(), fetchSummary()]) } onMounted(async () => { await Promise.resolve(getSiteTree()) await Promise.all([getList(), fetchSummary()]) }) ``` - [ ] **Step 5: 删除统计硬编码并替换模板** Replace current `` block with: ```html {{ summary.reminderCount }} {{ summary.custCount }} {{ summary.totalBillWater }} ¥{{ summary.totalExtendedAmount }} ¥{{ summary.totalLateFee }} ¥{{ summary.totalDeposit }} ``` - [ ] **Step 6: 导出改真实接口** Replace `handleExport`: ```typescript const handleExport = async () => { try { await ArrearsApi.exportReminder(buildReminderPageParams()) message.success('导出成功') } catch { message.error('导出失败') } } ``` - [ ] **Step 7: 前端构建验证** ```bash cd ../water-frontend pnpm run build:dev ``` Expected: build exits `0`. - [ ] **Step 8: Commit and push** ```bash 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`: ```typescript 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: ```typescript 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: 新增欠费催缴池页面断言** ```typescript 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: 新增催缴记录页面断言** ```typescript 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: 新增催缴记录展开明细断言** ```typescript 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** ```bash cd ../water-frontend npx playwright test tests/e2e/arrearageReminder.e2e.spec.ts --reporter=list ``` Expected: ```text 3 passed ``` - [ ] **Step 7: Record evidence** Create `../water-docs/docs/evidence/arrearage-reminder-real-data-e2e.md`: ```markdown # 催缴管理真实数据 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_AR` - `GET /business/arrearage-reminder/pending-summary?code=E2E_AR` - `GET /business/arrearage-reminder/page?reminderUser=E2E催缴员` - `GET /business/arrearage-reminder/summary?reminderUser=E2E催缴员` - `GET /business/arrearage-reminder/detail-list?reminderId=990201` ## Playwright Command: ```bash 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: ```bash 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/collectionRecord` returns 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 `ArrearageReminderSummaryRespVO` fields match backend `ArrearageReminderSummaryRespVO`. - Seed expected values match SQL inserts and Playwright assertions.