# 预存余额前后端测试与部署计划 > **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`: ```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 前) 添加: ```java @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 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: 运行测试验证通过** ```bash 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** ```bash 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`: ```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: 添加集成测试方法** ```java @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 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: 运行测试验证通过** ```bash 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** ```bash 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`: ```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`: ```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 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 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 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: 运行全部测试验证通过** ```bash 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** ```bash 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`: ```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: 添加集成测试方法** ```java @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 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: 运行测试验证通过** ```bash 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** ```bash 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`: ```typescript 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 测试 (需前端服务运行)** ```bash 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** ```bash 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`: ```typescript 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 测试** ```bash 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** ```bash 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: 运行所有后端集成测试** ```bash 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: 运行单元测试** ```bash 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 (如测试代码有微调)** ```bash git add -A git commit -m "test: 预存完整测试套件 — 回归通过" ``` --- ## Task 9: 编译打包部署 **目的:** 将已验证的后端编译打包,部署到测试环境。 - [ ] **Step 1: 后端编译打包** ```bash cd /Volumes/Dpan/github/water-workspace/water-backend mvn clean package -pl sw-business/sw-business-server -am -o -DskipTests ``` 核实 jar 包生成: ```bash ls -la sw-business/sw-business-server/target/*.jar ``` - [ ] **Step 2: 前端构建** ```bash cd /Volumes/Dpan/github/water-workspace/water-frontend pnpm build ``` 核实 dist 目录: ```bash 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 部署配置变更 (如有)** ```bash git add -A git commit -m "chore: 预存功能部署配置更新" ``` --- ## Task 10: 最终文档更新 **目的:** 将测试结果和部署信息归档到 water-docs。 - [ ] **Step 1: 更新 specs 工件** 在 `../water-docs/specs/` 中找到对应的 spec 目录 (如 `001-rev004-accounting`), 更新或创建 `test-results.md`: ```markdown # 预存余额后端功能 — 测试结果 ## 后端集成测试 | 测试方法 | 状态 | 覆盖场景 | |---------|------|---------| | 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** ```bash 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`