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
1048 lines
37 KiB
Markdown
1048 lines
37 KiB
Markdown
# 催缴管理真实数据与 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<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.
|