fujian_water_biz_doc/docs/superpowers/plans/2026-06-10-prestore-frontend-test-and-deploy.md
tangweijie 3eccab2cf9 docs: 文档治理统一 — AGENTS.md 生命周期规则 + 模块归档 + DDL 修正
1. AGENTS.md 更新
   - water-docs: 新增 specs/ 与 docs/design/ 生命周期规则章节
   - water-backend: 更新协作引用(建设期/建成后、evidence 模块化)

2. specs/ 重复合并
   - 006-reminder-event-design 合并入 003-rev006-reminder-event-design
   - 001-rev004-accounting 删除冗余 data-model.md + contracts/
   - 002-rev005-invoice-flow 删除冗余 data-model.md + contracts/

3. evidence 按模块归档
   - 35 个 REV-004 文件归入 evidence/rev004-accounting/
   - 7 个通用 bugfix 文件归入 evidence/bugfix/ 和 bugfix/frontend/
   - 新建 rev005-invoice/、rev006-reminder/、rev007-statistics/ 目录

4. guides/ 清理
   - 14 个 REV004_*.md 移入 evidence/rev004-accounting/

5. 遗留文件处理
   - docs/research/ 归档到 Archive/06_Migration_Plans/
   - backend-check detached worktrees 清理

6. 交叉引用修复
   - 006-reminder-event-design → 003-rev006-reminder-event-design
   - docs/guides/REV004_ → docs/evidence/rev004-accounting/REV004_

7. DB 设计文档修正(01_Database_Design.md)
   - biz_invoice 明确为开票配置表,非发票记录表
   - 新增 biz_invoice_record 为发票申请/结果主表
   - 新增 biz_charge_invoice_rel 账单-发票关联说明
   - REV-005 承接口径表名全部修正

8. 发票审计证据
   - 新增 evidence/rev005-invoice/2026-06-16-invoice-document-audit.md
2026-06-16 11:47:16 +08:00

39 KiB
Raw Blame History

预存余额前后端测试与部署计划

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_logbiz_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

具体命令取决于项目部署配置。如需确认, 先检查 Jenkinsfiledocker-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`