fujian_water_biz_doc/docs/superpowers/plans/2026-06-08-arrearage-reminder-real-data-e2e.md
tangweijie 3eccab2cf9 docs: 文档治理统一 — AGENTS.md 生命周期规则 + 模块归档 + DDL 修正
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
2026-06-16 11:47:16 +08:00

1048 lines
37 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.

# 催缴管理真实数据与 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 + 固定客户编号写入最小闭环数据;前端两个页面只消费真实 APIPlaywright 登录后进入真实页面,断言 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<ArrearageReminderExportExcelVO> 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<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:
```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<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:
```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<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:
```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<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:
```java
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:
```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<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)`:
```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 `<el-descriptions>` block with:
```html
<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`:
```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.