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
39 KiB
预存余额前后端测试与部署计划
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: 为已完成的预存余额后端功能 (P0-1~P1-2) 补齐集成测试、前端端到端验证、以及部署到测试环境。
Architecture: 分三条并行轨道 — 后端集成测试 (Spring MockMvc + SQL seed)、前端 E2E (Playwright + route mocking)、前端手动冒烟 (Browser 直接操作 localhost)。全部完成后编译部署到测试环境。
Tech Stack: backend: Spring Boot 3, MyBatis-Plus, MockMvc, H2/MySQL; frontend: Vue 3, Element Plus, Playwright, TypeScript
预检清单
执行开始前逐项确认,全部就绪后再开始 Task 1。
- MySQL 本地库
ruoyi-vue-pro可连接 (127.0.0.1:3306, root/123456) biz_account_log和biz_cust_bill_type表已存在- 后端可编译:
cd /Volumes/Dpan/github/water-workspace/water-backend && mvn compile -pl sw-business/sw-business-server -am -o - 前端依赖已安装:
cd /Volumes/Dpan/github/water-workspace/water-frontend && pnpm install - 前端可启动:
cd /Volumes/Dpan/github/water-workspace/water-frontend && pnpm dev(端口应为 5173) - BPM 服务 (
sw-bpm) 已启动或在开发环境可访问
Task 1: 后端集成测试 — 预存充值流水 (counterTopup → account_log)
Files:
- Create:
sw-business/sw-business-server/src/test/resources/sql/prestore/01_counter_topup_log_seed.sql - Modify:
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/integration/countercharge/CounterChargeFullChainIntegrationTest.java
Purpose: 验证柜台预存充值时 biz_account_log 正确写入 accLogType=2 (预存), accInOut=1 (进)。
- Step 1: 创建 seed SQL
写入 sw-business/sw-business-server/src/test/resources/sql/prestore/01_counter_topup_log_seed.sql:
-- 预存充值流水测试种子数据
-- 客户 910001: 已有账户, preDeposit=100.00, 无欠费
INSERT INTO biz_account (id, cust_id, pre_deposit, total_pre_deposit, create_time, update_time, deleted, tenant_id)
VALUES (910001, 910001, 100.00, 0.00, NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE pre_deposit = 100.00;
-- 确保无欠费
DELETE FROM biz_charge WHERE cust_id = 910001 AND pay_state = 0;
- Step 2: 添加集成测试方法
在 CounterChargeFullChainIntegrationTest.java 末尾 (class closing brace 前) 添加:
@Test
@Sql(scripts = {
"classpath:sql/rev004/accountprocess/00_reset.sql",
"classpath:sql/rev004/accountprocess/01_dict_seed.sql",
"classpath:sql/prestore/01_counter_topup_log_seed.sql"
}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:sql/rev004/accountprocess/00_reset.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void counterTopup_shouldWriteAccountLogWithAccLogTypeTwo() throws Exception {
java.util.Map<String, Object> requestBody = new java.util.LinkedHashMap<>();
requestBody.put("custId", 910001);
requestBody.put("amount", new BigDecimal("50.00"));
requestBody.put("chargeWay", 1);
requestBody.put("cashierId", "1001");
requestBody.put("payTime", "2026-06-10T10:00:00");
requestBody.put("remark", "柜台预存流水测试");
mockMvc.perform(post("/admin-api/business/charge/counter-topup")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestBody)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.amount").value(50.00))
.andExpect(jsonPath("$.data.balanceAfter").value(150.00));
// 验证账户余额
assertEquals(new BigDecimal("150.00"),
jdbcTemplate.queryForObject(
"select pre_deposit from biz_account where cust_id = 910001",
BigDecimal.class));
// 验证流水表: acc_log_type=2 (预存), acc_in_out=1 (进)
assertEquals(1, jdbcTemplate.queryForObject(
"select count(1) from biz_account_log where cust_id = 910001 and acc_log_type = 2 and acc_in_out = 1",
Integer.class));
}
- Step 3: 运行测试验证通过
cd /Volumes/Dpan/github/water-workspace/water-backend
REV004_IT_DB_URL=jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro mvn test -pl sw-business/sw-business-server \
-Dtest=CounterChargeFullChainIntegrationTest#counterTopup_shouldWriteAccountLogWithAccLogTypeTwo -am -o
Expected: BUILD SUCCESS, 1 test passed.
- Step 4: Commit
cd /Volumes/Dpan/github/water-workspace/water-backend
git add sw-business/sw-business-server/src/test/resources/sql/prestore/01_counter_topup_log_seed.sql \
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/integration/countercharge/CounterChargeFullChainIntegrationTest.java
git commit -m "test: 预存充值流水集成测试 - counterTopup → account_log (accLogType=2)"
Task 2: 后端集成测试 — 多缴转预存流水 (updateCharge overpay → account_log)
Files:
- Create:
sw-business/sw-business-server/src/test/resources/sql/prestore/02_overpay_transfer_seed.sql - Modify:
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/integration/countercharge/CounterChargeFullChainIntegrationTest.java
Purpose: 验证收费时多缴部分自动转预存,且 biz_account_log 写入 accLogType=3 (转预存)。
- Step 1: 创建 seed SQL
写入 sw-business/sw-business-server/src/test/resources/sql/prestore/02_overpay_transfer_seed.sql:
-- 多缴转预存种子数据
-- 客户 910001: 账户 preDeposit=0, 有一笔欠费 18.80 的营业账
INSERT INTO biz_account (id, cust_id, pre_deposit, total_pre_deposit, create_time, update_time, deleted, tenant_id)
VALUES (910001, 910001, 0.00, 0.00, NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE pre_deposit = 0.00;
-- 欠费营业账: id=910002, custId=910001, billAmount=18.00, lateFee=0.80, extendedAmount=18.80, payState=0
INSERT INTO biz_charge (id, meter_id, record_id, bill_month, dept_id, cust_id, cust_code, cust_name, cust_address,
price_template_code, bill_water, bill_amount, extended_amount, late_fee, pay_state,
create_time, update_time, deleted, tenant_id)
VALUES (910002, 51, 1, 202605, 58, 910001, 'COUNTER_IT_CUST', '柜台收费集成测试客户', '柜台收费集成测试地址',
'TEMPLATE001', 12.000, 18.00, 18.80, 0.80, 0,
NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE pay_state = 0;
- Step 2: 添加集成测试方法
@Test
@Sql(scripts = {
"classpath:sql/rev004/accountprocess/00_reset.sql",
"classpath:sql/rev004/accountprocess/01_dict_seed.sql",
"classpath:sql/prestore/02_overpay_transfer_seed.sql"
}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:sql/rev004/accountprocess/00_reset.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void updateCharge_withOverpay_shouldTransferExcessToDepositAndWriteAccountLog() throws Exception {
LocalDateTime payTime = LocalDateTime.of(2026, 6, 10, 10, 0);
java.util.Map<String, Object> requestBody = new java.util.LinkedHashMap<>();
requestBody.put("id", 910002);
requestBody.put("meterId", 51);
requestBody.put("recordId", 1);
requestBody.put("billMonth", 202605);
requestBody.put("deptId", 58);
requestBody.put("custId", 910001);
requestBody.put("custCode", "COUNTER_IT_CUST");
requestBody.put("custName", "柜台收费集成测试客户");
requestBody.put("custAddress", "柜台收费集成测试地址");
requestBody.put("billWater", new BigDecimal("12.000"));
requestBody.put("billAmount", new BigDecimal("18.00"));
requestBody.put("extendedAmount", new BigDecimal("18.80"));
requestBody.put("lateFee", new BigDecimal("0.80"));
requestBody.put("payState", 1);
requestBody.put("payDate", payTime.toString());
requestBody.put("cashierId", "1001");
requestBody.put("chargeMethod", 1);
requestBody.put("chargeWay", 1);
// actualPayAmount = 50.00 (多缴 50.00 - 18.80 = 31.20)
requestBody.put("actualPayAmount", new BigDecimal("50.00"));
String body = objectMapper.writeValueAsString(requestBody);
mockMvc.perform(put("/admin-api/business/charge/update")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data").value(true));
// 验证营业账已销
assertEquals(1, jdbcTemplate.queryForObject(
"select pay_state from biz_charge where id = 910002", Integer.class));
// 验证多缴转入预存: 50.00 - 18.80 = 31.20
assertEquals(new BigDecimal("31.20"),
jdbcTemplate.queryForObject(
"select pre_deposit from biz_account where cust_id = 910001",
BigDecimal.class));
// 验证流水: acc_log_type=3 (转预存), acc_in_out=1 (进)
assertEquals(1, jdbcTemplate.queryForObject(
"select count(1) from biz_account_log where cust_id = 910001 and acc_log_type = 3 and acc_in_out = 1 and amount = 31.20",
Integer.class));
}
- Step 3: 运行测试验证通过
cd /Volumes/Dpan/github/water-workspace/water-backend
REV004_IT_DB_URL=jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro mvn test -pl sw-business/sw-business-server \
-Dtest=CounterChargeFullChainIntegrationTest#updateCharge_withOverpay_shouldTransferExcessToDepositAndWriteAccountLog -am -o
Expected: BUILD SUCCESS, 1 test passed.
- Step 4: Commit
git add sw-business/sw-business-server/src/test/resources/sql/prestore/02_overpay_transfer_seed.sql \
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/integration/countercharge/CounterChargeFullChainIntegrationTest.java
git commit -m "test: 多缴转预存集成测试 - updateCharge overpay → account_log (accLogType=3)"
Task 3: 后端集成测试 — 预存调整 BPM 审批执行 (execute → balance change)
Files:
- Create:
sw-business/sw-business-server/src/test/resources/sql/prestore/03_prestorage_execute_seed.sql - Create:
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/prestorage/PrestorageExecuteIntegrationTest.java
Purpose: 验证 PrestorageAdjustInternalApiImpl.execute() 在 APPROVE/REJECT 两种审批结果下正确变更余额和写入流水。
- Step 1: 创建 seed SQL
写入 sw-business/sw-business-server/src/test/resources/sql/prestore/03_prestorage_execute_seed.sql:
-- 预存调整执行测试种子
-- 客户 CUST_REFUND: 预存 200.00, 待退 50.00
INSERT INTO biz_cust (id, code, name, address, create_time, update_time, deleted, tenant_id)
VALUES (910001, 'CUST_REFUND', '退款测试客户', '测试地址A', NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE code = 'CUST_REFUND';
INSERT INTO biz_account (id, cust_id, pre_deposit, create_time, update_time, deleted, tenant_id)
VALUES (910001, 910001, 200.00, NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE pre_deposit = 200.00;
-- 客户 CUST_SRC: 预存 300.00 (转账源)
INSERT INTO biz_cust (id, code, name, address, create_time, update_time, deleted, tenant_id)
VALUES (910002, 'CUST_SRC', '转账源客户', '测试地址B', NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE code = 'CUST_SRC';
INSERT INTO biz_account (id, cust_id, pre_deposit, create_time, update_time, deleted, tenant_id)
VALUES (910002, 910002, 300.00, NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE pre_deposit = 300.00;
-- 客户 CUST_TGT: 预存 100.00 (转账目标)
INSERT INTO biz_cust (id, code, name, address, create_time, update_time, deleted, tenant_id)
VALUES (910003, 'CUST_TGT', '转账目标客户', '测试地址C', NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE code = 'CUST_TGT';
INSERT INTO biz_account (id, cust_id, pre_deposit, create_time, update_time, deleted, tenant_id)
VALUES (910003, 910003, 100.00, NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE pre_deposit = 100.00;
-- 预存调整单: REFUND, 待审批
INSERT INTO biz_prestorage_adjust (id, adjustment_no, adjust_type, source_cust_id, source_account_id, adjust_amount,
execution_status, create_time, update_time, deleted, tenant_id)
VALUES (910001, 'PRF-910001-20260610100000', 'REFUND', 910001, 910001, 50.00,
'PENDING', NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE execution_status = 'PENDING';
-- 预存调整单: TRANSFER, 待审批
INSERT INTO biz_prestorage_adjust (id, adjustment_no, adjust_type, source_cust_id, source_account_id,
target_cust_id, target_account_id, adjust_amount,
execution_status, create_time, update_time, deleted, tenant_id)
VALUES (910002, 'PTR-910002-20260610100000', 'TRANSFER', 910002, 910002, 910003, 910003, 80.00,
'PENDING', NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE execution_status = 'PENDING';
- Step 2: 创建集成测试类
写入 sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/prestorage/PrestorageExecuteIntegrationTest.java:
package cn.com.emsoft.sw.business.service.accountingadjust.prestorage;
import cn.com.emsoft.sw.business.integration.rev004.accountprocess.support.AbstractRev004AccountProcessIntegrationTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@EnabledIfEnvironmentVariable(named = "REV004_IT_DB_URL", matches = ".+")
class PrestorageExecuteIntegrationTest extends AbstractRev004AccountProcessIntegrationTest {
@BeforeEach
void stubDictApi() {
cn.com.emsoft.sw.framework.dict.core.DictFrameworkUtils.init(dictDataCommonApi);
org.mockito.Mockito.lenient()
.when(dictDataCommonApi.getDictDataList(org.mockito.ArgumentMatchers.any()))
.thenReturn(cn.com.emsoft.sw.framework.common.pojo.CommonResult.success(java.util.Collections.emptyList()));
}
@Test
@Sql(scripts = {
"classpath:sql/rev004/accountprocess/00_reset.sql",
"classpath:sql/prestore/03_prestorage_execute_seed.sql"
}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:sql/rev004/accountprocess/00_reset.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void executeRefundApproved_shouldDecreaseDepositAndWriteAccountLog() throws Exception {
Map<String, Object> body = new LinkedHashMap<>();
body.put("adjustmentNo", "PRF-910001-20260610100000");
body.put("approvalStatus", "APPROVE");
body.put("approvalComment", "审批通过");
mockMvc.perform(post("/internal/prestorage-adjust/execute")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.resultStatus").value("SUCCESS"));
// 验证余额: 200.00 - 50.00 = 150.00
assertEquals(new BigDecimal("150.00"),
jdbcTemplate.queryForObject(
"select pre_deposit from biz_account where cust_id = 910001",
BigDecimal.class));
// 验证流水: acc_log_type=1 (扣款), acc_in_out=2 (出)
assertEquals(1, jdbcTemplate.queryForObject(
"select count(1) from biz_account_log where cust_id = 910001 and acc_log_type = 1 and acc_in_out = 2 and amount = 50.00",
Integer.class));
// 验证执行状态
assertEquals("EXECUTED", jdbcTemplate.queryForObject(
"select execution_status from biz_prestorage_adjust where adjustment_no = 'PRF-910001-20260610100000'",
String.class));
}
@Test
@Sql(scripts = {
"classpath:sql/rev004/accountprocess/00_reset.sql",
"classpath:sql/prestore/03_prestorage_execute_seed.sql"
}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:sql/rev004/accountprocess/00_reset.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void executeTransferApproved_shouldMoveDepositAndWriteAccountLogs() throws Exception {
Map<String, Object> body = new LinkedHashMap<>();
body.put("adjustmentNo", "PTR-910002-20260610100000");
body.put("approvalStatus", "APPROVE");
body.put("approvalComment", "审批通过");
mockMvc.perform(post("/internal/prestorage-adjust/execute")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.resultStatus").value("SUCCESS"));
// 验证源余额: 300.00 - 80.00 = 220.00
assertEquals(new BigDecimal("220.00"),
jdbcTemplate.queryForObject(
"select pre_deposit from biz_account where cust_id = 910002",
BigDecimal.class));
// 验证目标余额: 100.00 + 80.00 = 180.00
assertEquals(new BigDecimal("180.00"),
jdbcTemplate.queryForObject(
"select pre_deposit from biz_account where cust_id = 910003",
BigDecimal.class));
// 源流水: acc_log_type=3, acc_in_out=2 (出)
assertEquals(1, jdbcTemplate.queryForObject(
"select count(1) from biz_account_log where cust_id = 910002 and acc_log_type = 3 and acc_in_out = 2",
Integer.class));
// 目标流水: acc_log_type=3, acc_in_out=1 (进)
assertEquals(1, jdbcTemplate.queryForObject(
"select count(1) from biz_account_log where cust_id = 910003 and acc_log_type = 3 and acc_in_out = 1",
Integer.class));
}
@Test
@Sql(scripts = {
"classpath:sql/rev004/accountprocess/00_reset.sql",
"classpath:sql/prestore/03_prestorage_execute_seed.sql"
}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:sql/rev004/accountprocess/00_reset.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void executeRejected_shouldMarkRejectedWithoutBalanceChange() throws Exception {
Map<String, Object> body = new LinkedHashMap<>();
body.put("adjustmentNo", "PRF-910001-20260610100000");
body.put("approvalStatus", "REJECT");
body.put("approvalComment", "不同意退款");
mockMvc.perform(post("/internal/prestorage-adjust/execute")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.resultStatus").value("SUCCESS"));
// 余额不变
assertEquals(new BigDecimal("200.00"),
jdbcTemplate.queryForObject(
"select pre_deposit from biz_account where cust_id = 910001",
BigDecimal.class));
// 无流水产生
assertEquals(0, jdbcTemplate.queryForObject(
"select count(1) from biz_account_log where cust_id = 910001",
Integer.class));
// 执行状态为 REJECTED
assertEquals("REJECTED", jdbcTemplate.queryForObject(
"select execution_status from biz_prestorage_adjust where adjustment_no = 'PRF-910001-20260610100000'",
String.class));
}
}
- Step 3: 运行全部测试验证通过
cd /Volumes/Dpan/github/water-workspace/water-backend
REV004_IT_DB_URL=jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro mvn test -pl sw-business/sw-business-server \
-Dtest=PrestorageExecuteIntegrationTest -am -o
Expected: BUILD SUCCESS, 3 tests passed.
- Step 4: Commit
git add sw-business/sw-business-server/src/test/resources/sql/prestore/03_prestorage_execute_seed.sql \
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/prestorage/PrestorageExecuteIntegrationTest.java
git commit -m "test: 预存调整BPM审批执行集成测试 - execute APPROVE/REJECT + account_log"
Task 4: 后端集成测试 — 主副卡资金归集流水 (sub-card → main card via cust_bill_type)
Files:
- Create:
sw-business/sw-business-server/src/test/resources/sql/prestore/04_main_sub_card_seed.sql - Modify:
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/integration/countercharge/CounterChargeFullChainIntegrationTest.java
Purpose: 验证副卡充值时资金归集到主卡,且 biz_account_log 正确记录 accLogType=3 (转预存)。
- Step 1: 创建 seed SQL
写入 sw-business/sw-business-server/src/test/resources/sql/prestore/04_main_sub_card_seed.sql:
-- 主副卡测试种子
-- 主卡: 910001, 副卡: 910010
-- 主卡账户 preDeposit=500.00
INSERT INTO biz_account (id, cust_id, pre_deposit, create_time, update_time, deleted, tenant_id)
VALUES (910001, 910001, 500.00, NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE pre_deposit = 500.00;
-- 副卡账户 preDeposit=0
INSERT INTO biz_cust (id, code, name, address, create_time, update_time, deleted, tenant_id)
VALUES (910010, 'SUB_CUST', '副卡客户', '副卡地址', NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE code = 'SUB_CUST';
INSERT INTO biz_account (id, cust_id, pre_deposit, create_time, update_time, deleted, tenant_id)
VALUES (910010, 910010, 0.00, NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE pre_deposit = 0.00;
-- 主副卡关系: 910010 → 910001 (主卡)
INSERT INTO biz_cust_bill_type (id, cust_id, payment_type, parent_id, create_time, update_time, deleted, tenant_id)
VALUES (1, 910010, 0, 910001, NOW(), NOW(), 0, 1)
ON DUPLICATE KEY UPDATE payment_type = 0, parent_id = 910001;
-- 清除副卡欠费
DELETE FROM biz_charge WHERE cust_id = 910010 AND pay_state = 0;
- Step 2: 添加集成测试方法
@Test
@Sql(scripts = {
"classpath:sql/rev004/accountprocess/00_reset.sql",
"classpath:sql/rev004/accountprocess/01_dict_seed.sql",
"classpath:sql/prestore/04_main_sub_card_seed.sql"
}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:sql/rev004/accountprocess/00_reset.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void subCardCounterTopup_shouldCollectToMainCardAndWriteAccountLog() throws Exception {
java.util.Map<String, Object> requestBody = new java.util.LinkedHashMap<>();
requestBody.put("custId", 910010); // 副卡
requestBody.put("amount", new BigDecimal("200.00"));
requestBody.put("chargeWay", 1);
requestBody.put("cashierId", "1001");
requestBody.put("payTime", "2026-06-10T10:00:00");
requestBody.put("remark", "副卡预存归集测试");
mockMvc.perform(post("/admin-api/business/charge/counter-topup")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestBody)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
// 副卡余额不变 (资金归集到主卡)
assertEquals(new BigDecimal("0.00"),
jdbcTemplate.queryForObject(
"select pre_deposit from biz_account where cust_id = 910010",
BigDecimal.class));
// 主卡余额增加: 500.00 + 200.00 = 700.00
assertEquals(new BigDecimal("700.00"),
jdbcTemplate.queryForObject(
"select pre_deposit from biz_account where cust_id = 910001",
BigDecimal.class));
// 主卡流水: acc_log_type=3 (转预存), acc_in_out=1 (进)
assertEquals(1, jdbcTemplate.queryForObject(
"select count(1) from biz_account_log where cust_id = 910001 and acc_log_type = 3 and acc_in_out = 1 and amount = 200.00",
Integer.class));
}
- Step 3: 运行测试验证通过
cd /Volumes/Dpan/github/water-workspace/water-backend
REV004_IT_DB_URL=jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro mvn test -pl sw-business/sw-business-server \
-Dtest=CounterChargeFullChainIntegrationTest#subCardCounterTopup_shouldCollectToMainCardAndWriteAccountLog -am -o
Expected: BUILD SUCCESS, 1 test passed.
- Step 4: Commit
git add sw-business/sw-business-server/src/test/resources/sql/prestore/04_main_sub_card_seed.sql \
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/integration/countercharge/CounterChargeFullChainIntegrationTest.java
git commit -m "test: 主副卡资金归集集成测试 - sub-card topup → main card + accLogType=3"
Task 5: 前端 E2E — 柜台预存充值完整流程
Files:
- Create:
water-frontend/tests/e2e/prestore/counterTopup.e2e.spec.ts
Purpose: 通过 Playwright 验证柜台收费页面预存充值前端完整流程 (查找客户 → 显示余额 → 输入金额 → 提交 → 成功反馈)。
- Step 1: 创建 E2E 测试文件
写入 water-frontend/tests/e2e/prestore/counterTopup.e2e.spec.ts:
import { test, expect } from '@playwright/test'
import type { Page } from '@playwright/test'
const ADMIN_API = '/admin-api'
async function mockLoginApi(page: Page) {
await page.route('**/system/auth/login**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: {
userId: 1,
accessToken: 'mock-token-e2e',
refreshToken: 'mock-refresh-e2e',
expiresTime: Date.now() + 86400000
}
})
})
})
}
async function mockGetCustomer(page: Page) {
await page.route('**/business/cust/get-by-code*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: {
id: 10001,
code: 'CUST-E2E-001',
name: 'E2E测试客户',
address: 'E2E测试地址',
mobile: '13800001111',
balance: 500.00,
predepositBalance: 500.00
}
})
})
})
}
async function mockCounterPreview(page: Page) {
await page.route('**/business/charge/counter-preview*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: {
custId: 10001,
custCode: 'CUST-E2E-001',
custName: 'E2E测试客户',
depositBalance: 500.00,
unpaidCharges: [],
unpaidCount: 0,
unpaidTotal: 0
}
})
})
})
}
async function mockCounterTopup(page: Page) {
await page.route('**/business/charge/counter-topup', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: {
paymentRecordId: 20001,
paymentNo: 'TOPUP-E2E-001',
custId: 10001,
amount: 100.00,
balanceAfter: 600.00,
payTime: '2026-06-10T10:00:00'
}
})
})
})
}
test.describe('柜台预存充值 E2E', () => {
test.beforeEach(async ({ page }) => {
await mockLoginApi(page)
await mockGetCustomer(page)
await mockCounterPreview(page)
await mockCounterTopup(page)
})
test('完整预存充值流程: 查找客户 → 查看余额 → 充值 → 成功反馈', async ({ page }) => {
// 1. 导航到柜台收费页面
await page.goto('/operating-charges/counter-charging')
await page.waitForLoadState('networkidle')
// 2. 输入客户编号并查找
const customerInput = page.locator('input[placeholder*="客户编号"]')
await customerInput.fill('CUST-E2E-001')
await customerInput.press('Enter')
await page.waitForTimeout(500)
// 3. 验证客户信息显示
await expect(page.locator('text=E2E测试客户')).toBeVisible()
await expect(page.locator('text=CUST-E2E-001')).toBeVisible()
// 4. 点击预存按钮
const topupButton = page.locator('button:has-text("预存")')
await topupButton.click()
await page.waitForTimeout(300)
// 5. 输入预存金额
const amountInput = page.locator('input[placeholder*="金额"]').first()
await amountInput.fill('100')
// 6. 确认预存
const confirmButton = page.locator('button:has-text("确认")')
await confirmButton.click()
await page.waitForTimeout(500)
// 7. 验证成功提示
await expect(page.locator('.el-message--success')).toBeVisible()
})
})
- Step 2: 运行 E2E 测试 (需前端服务运行)
cd /Volumes/Dpan/github/water-workspace/water-frontend
# 确保 pnpm dev 已在另一个终端运行
npx playwright test tests/e2e/prestore/counterTopup.e2e.spec.ts --project=chromium
Expected: 1 test passed.
- Step 3: Commit
cd /Volumes/Dpan/github/water-workspace/water-frontend
git add tests/e2e/prestore/counterTopup.e2e.spec.ts
git commit -m "test(e2e): 柜台预存充值完整流程 Playwright E2E"
Task 6: 前端 E2E — 预存调整提交到审批流程
Files:
- Create:
water-frontend/tests/e2e/prestore/prestorageAdjustBpm.e2e.spec.ts
Purpose: 验证预存调整页面提交 → BPM 创建流程的前端交互。
- Step 1: 创建 E2E 测试文件
写入 water-frontend/tests/e2e/prestore/prestorageAdjustBpm.e2e.spec.ts:
import { test, expect } from '@playwright/test'
import type { Page } from '@playwright/test'
async function setupMocks(page: Page) {
// Login
await page.route('**/system/auth/login**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: { userId: 1, accessToken: 'mock-token', refreshToken: 'mock-refresh', expiresTime: Date.now() + 86400000 }
})
})
})
// Prestorage adjustment page
await page.route('**/business/accounting-adjust/prestorage-page*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: {
list: [
{
id: 1,
adjustmentNo: 'PRF-10001-20260610083000',
custCode: 'CUST-E2E-001',
custName: 'E2E退款客户',
adjustType: 'REFUND',
adjustAmount: 50.00,
approvalStatus: 'APPROVED',
executionStatus: 'EXECUTED',
createTime: '2026-06-10 08:30:00'
}
],
total: 1
}
})
})
})
// Department tree
await page.route('**/system/dept/simple-list**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ code: 0, data: [{ id: 1, name: '总部', children: [] }] })
})
})
// BPM create
await page.route('**/bpm/prestorage-adjust/create', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: { adjustmentNo: 'PRF-10002-20260610100000', prestorageAdjustId: 2 },
msg: '预存调整工单创建成功'
})
})
})
}
test.describe('预存调整 BPM 流程 E2E', () => {
test.beforeEach(async ({ page }) => {
await setupMocks(page)
})
test('预存调整列表页加载并显示已审批记录', async ({ page }) => {
await page.goto('/account-process/prestorage-adjustment')
await page.waitForLoadState('networkidle')
// 列表中有数据
await expect(page.locator('text=PRF-10001-20260610083000')).toBeVisible()
await expect(page.locator('text=E2E退款客户')).toBeVisible()
})
test('新增预存调整 → 填写退款 → 提交 BPM 创建', async ({ page }) => {
await page.goto('/account-process/prestorage-adjustment')
await page.waitForLoadState('networkidle')
// 点击新增
await page.locator('button:has-text("新增")').click()
await page.waitForTimeout(500)
// 选择调整类型为退款
const adjustTypeSelect = page.locator('.el-form-item:has-text("调整类型") .el-select')
await adjustTypeSelect.click()
await page.locator('.el-select-dropdown__item:has-text("退款")').click()
// 输入源客户编号
await page.locator('input[placeholder*="客户编号"]').first().fill('CUST-E2E-001')
await page.waitForTimeout(300)
// 输入金额
await page.locator('input[placeholder*="金额"]').first().fill('50')
// 输入申请人
await page.locator('input[placeholder*="申请人"]').fill('测试员')
// 输入原因
await page.locator('textarea[placeholder*="原因"]').fill('E2E测试退款')
// 提交
await page.locator('button:has-text("提交")').click()
await page.waitForTimeout(500)
// 验证成功提示
await expect(page.locator('.el-message--success')).toBeVisible()
})
})
- Step 2: 运行 E2E 测试
cd /Volumes/Dpan/github/water-workspace/water-frontend
npx playwright test tests/e2e/prestore/prestorageAdjustBpm.e2e.spec.ts --project=chromium
Expected: 2 tests passed.
- Step 3: Commit
git add tests/e2e/prestore/prestorageAdjustBpm.e2e.spec.ts
git commit -m "test(e2e): 预存调整BPM提交流程 Playwright E2E"
Task 7: 前端手动冒烟 — Browser 验证所有预存功能页面
目的: 用 Codex Browser 直接操作 localhost,逐一验证所有预存相关页面的完整交互。
-
Step 1: 确保前后端均运行
-
后端: 确保
sw-business-server已启动 (默认 port 48080) -
前端:
cd /Volumes/Dpan/github/water-workspace/water-frontend && pnpm dev(port 5173) -
登录: 使用 admin/123456 或已有测试账号
-
Step 2: 冒烟清单逐一验证
打开 Browser 到 http://localhost:5173,逐项检查:
| # | 页面路径 | 验证点 | 预期 |
|---|---|---|---|
| 1 | /operating-charges/counter-charging |
预存余额显示在概览栏 | 紫色数字显示当前余额 |
| 2 | 同上 | 无欠费客户点击"预存"按钮 | 弹出预存金额输入框 |
| 3 | 同上 | 输入金额 100 确认 | 成功提示, 余额更新 |
| 4 | 同上 | 欠费客户收费时多缴 | 自动转预存, 查看账户余额增加 |
| 5 | /account-process/prestorage-adjustment |
列表页加载 | 有数据或无数据均正常 |
| 6 | 同上 | 点击新增 → 选择退款 | 填写表单后提交, BPM 创建成功 |
| 7 | /account-process/account-log |
账务日志列表 | 显示预存相关流水记录 |
| 8 | /cust-data/cust-info → 缴费记录 tab |
查看客户缴费记录 | 预存充值/抵扣记录可查 |
- Step 3: 记录冒烟结果
每项验证后标记 ✓ 或记录具体错误信息。如有失败项, 在 docs/superpowers/plans/ 下创建 2026-06-10-prestore-smoke-results.md。
Task 8: 全量后端测试回归
目的: 确保新增测试未破坏既有功能。
- Step 1: 运行所有后端集成测试
cd /Volumes/Dpan/github/water-workspace/water-backend
REV004_IT_DB_URL=jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro mvn test -pl sw-business/sw-business-server -am -o
Expected: BUILD SUCCESS, 所有测试通过 (包括新增的 6 个预存测试、以及已有测试)。
- Step 2: 运行单元测试
cd /Volumes/Dpan/github/water-workspace/water-backend
mvn test -pl sw-business/sw-business-server -Dtest="*Test,!*IntegrationTest" -am -o
Expected: BUILD SUCCESS.
- Step 3: Commit (如测试代码有微调)
git add -A
git commit -m "test: 预存完整测试套件 — 回归通过"
Task 9: 编译打包部署
目的: 将已验证的后端编译打包,部署到测试环境。
- Step 1: 后端编译打包
cd /Volumes/Dpan/github/water-workspace/water-backend
mvn clean package -pl sw-business/sw-business-server -am -o -DskipTests
核实 jar 包生成:
ls -la sw-business/sw-business-server/target/*.jar
- Step 2: 前端构建
cd /Volumes/Dpan/github/water-workspace/water-frontend
pnpm build
核实 dist 目录:
ls -la dist/
- Step 3: 部署到测试环境
根据部署方式执行:
- Docker:
docker-compose up -d(如项目提供) - 直接部署: 将 jar 包和 dist 目录复制到服务器
- Jenkins: 触发 CI pipeline
具体命令取决于项目部署配置。如需确认, 先检查 Jenkinsfile 或 docker-compose.yml。
- Step 4: 部署后冒烟
部署完成后, 在测试环境 URL 重复 Task 7 的冒烟清单。
- Step 5: Commit 部署配置变更 (如有)
git add -A
git commit -m "chore: 预存功能部署配置更新"
Task 10: 最终文档更新
目的: 将测试结果和部署信息归档到 water-docs。
- Step 1: 更新 specs 工件
在 ../water-docs/specs/ 中找到对应的 spec 目录 (如 001-rev004-accounting), 更新或创建 test-results.md:
# 预存余额后端功能 — 测试结果
## 后端集成测试
| 测试方法 | 状态 | 覆盖场景 |
|---------|------|---------|
| counterTopup_shouldWriteAccountLogWithAccLogTypeTwo | ✓ | counterTopup → accLogType=2 |
| updateCharge_withOverpay_shouldTransferExcessToDepositAndWriteAccountLog | ✓ | 多缴转预存 → accLogType=3 |
| executeRefundApproved_shouldDecreaseDepositAndWriteAccountLog | ✓ | BPM 审批通过退款 |
| executeTransferApproved_shouldMoveDepositAndWriteAccountLogs | ✓ | BPM 审批通过转账 |
| executeRejected_shouldMarkRejectedWithoutBalanceChange | ✓ | BPM 审批拒绝 |
| subCardCounterTopup_shouldCollectToMainCardAndWriteAccountLog | ✓ | 副卡充值 → 主卡归集 |
## 前端 E2E
| 测试文件 | 状态 |
|---------|------|
| counterTopup.e2e.spec.ts | ✓ |
| prestorageAdjustBpm.e2e.spec.ts | ✓ |
## 手动冒烟
| 页面 | 状态 |
|------|------|
| 柜台收费-预存显示 | ✓ |
| 柜台收费-预存充值 | ✓ |
| 柜台收费-多缴转预存 | ✓ |
| 预存调整列表 | ✓ |
| 预存调整新增 | ✓ |
| 账务日志 | ✓ |
| 客户缴费记录 | ✓ |
## 部署
- 测试环境: [URL]
- 部署时间: 2026-06-10
- Step 2: Commit
cd /Volumes/Dpan/github/water-workspace/water-docs
git add specs/
git commit -m "docs: 预存余额功能测试结果归档"
**File path:** `/Volumes/Dpan/github/water-workspace/water-docs/docs/superpowers/plans/2026-06-10-prestore-frontend-test-and-deploy.md`