# 银行流水实体类实现计划

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**目标：** 创建 CcdiBankStatement 实体类，实现从流水分析接口响应到数据库实体的数据转换功能。

**架构：** 在 ccdi-project 模块中创建实体类，使用 Spring BeanUtils 进行对象映射，手动处理字段名不一致的情况。实体类包含静态转换方法 fromResponse()。

**技术栈：** MyBatis Plus 3.5.10, Spring BeanUtils, Lombok, MySQL 8.2.0

---

## 任务概览

| 任务 | 预估时间 | 文件 |
|------|---------|------|
| Task 1: 数据库表修改 | 5分钟 | SQL脚本 |
| Task 2: 创建实体类基础结构 | 10分钟 | CcdiBankStatement.java |
| Task 3: 编写转换方法测试 | 15分钟 | CcdiBankStatementTest.java |
| Task 4: 实现转换方法 | 10分钟 | CcdiBankStatement.java |
| Task 5: 创建 Mapper 接口 | 5分钟 | CcdiBankStatementMapper.java |
| Task 6: 创建 Mapper XML | 10分钟 | CcdiBankStatementMapper.xml |
| Task 7: 验证测试 | 5分钟 | - |

---

## Task 1: 数据库表修改

**目标：** 在 ccdi_bank_statement 表中添加 project_id 字段和索引。

**文件：**
- 创建: `sql/ccdi_bank_statement_add_project_id.sql`

**Step 1: 创建数据库修改脚本**

```sql
-- 为 ccdi_bank_statement 表添加 project_id 字段
ALTER TABLE `ccdi_bank_statement`
ADD COLUMN `project_id` bigint(20) DEFAULT NULL COMMENT '关联项目ID' AFTER `bank_statement_id`,
ADD INDEX `idx_project_id` (`project_id`);
```

**Step 2: 执行数据库修改脚本**

```bash
# 连接到数据库并执行脚本
mysql -h 116.62.17.81 -u root -p ccdi < sql/ccdi_bank_statement_add_project_id.sql
```

**预期输出：** 执行成功，无错误信息。

**Step 3: 验证字段已添加**

```sql
-- 查看表结构，确认 project_id 字段已添加
SHOW COLUMNS FROM ccdi_bank_statement LIKE 'project_id';
```

**预期输出：**
```
Field        | Type       | Null | Key | Default | Extra
-------------|------------|------|-----|---------|-------
project_id   | bigint(20) | YES  | MUL | NULL    |
```

**Step 4: 提交**

```bash
git add sql/ccdi_bank_statement_add_project_id.sql
git commit -m "feat: 为银行流水表添加 project_id 字段"
```

---

## Task 2: 创建实体类基础结构

**目标：** 创建 CcdiBankStatement 实体类的基础结构，包含所有字段定义。

**文件：**
- 创建: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java`

**Step 1: 创建实体类文件**

```java
package com.ruoyi.ccdi.project.domain.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;

/**
 * 银行流水对象 ccdi_bank_statement
 *
 * @author ruoyi
 * @date 2026-03-04
 */
@Data
@TableName("ccdi_bank_statement")
public class CcdiBankStatement implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    // ===== 主键和关联字段 =====

    /** 流水ID */
    @TableId(type = IdType.AUTO)
    private Long bankStatementId;

    /** 关联项目ID（业务字段） */
    private Long projectId;

    /** 企业ID */
    private Integer leId;

    /** 账号ID */
    private Long accountId;

    /** 项目id（保留原有字段） */
    private Integer groupId;

    // ===== 账号信息 =====

    /** 企业账号名称 */
    private String leAccountName;

    /** 企业银行账号 */
    private String leAccountNo;

    /** 账号日期ID */
    private Integer accountingDateId;

    /** 账号日期 */
    private String accountingDate;

    /** 交易日期 */
    private String trxDate;

    /** 币种 */
    private String currency;

    // ===== 交易金额 =====

    /** 付款金额 */
    private BigDecimal amountDr;

    /** 收款金额 */
    private BigDecimal amountCr;

    /** 余额 */
    private BigDecimal amountBalance;

    // ===== 交易类型和标志 =====

    /** 交易类型 */
    private String cashType;

    /** 交易标志位 */
    private String trxFlag;

    /** 分类ID */
    private Integer trxType;

    /** 异常类型 */
    private String exceptionType;

    /** 是否为内部交易 */
    private Integer internalFlag;

    // ===== 对手方信息 =====

    /** 对手方企业ID */
    private Integer customerLeId;

    /** 对手方企业名称 */
    private String customerAccountName;

    /** 对手方账号 */
    private String customerAccountNo;

    /** 对手方银行 */
    private String customerBank;

    /** 对手方备注 */
    private String customerReference;

    // ===== 摘要和备注 =====

    /** 用户交易摘要 */
    private String userMemo;

    /** 银行交易摘要 */
    private String bankComments;

    /** 银行交易号 */
    private String bankTrxNumber;

    // ===== 银行信息 =====

    /** 所属银行缩写 */
    private String bank;

    // ===== 批次和上传信息 =====

    /** 上传logId */
    private Integer batchId;

    /** 每次上传在文件中的line */
    private Integer batchSequence;

    // ===== 附加字段 =====

    /** meta json（固定为null） */
    private String metaJson;

    /** 是否包含余额 */
    private Integer noBalance;

    /** 初始余额 */
    private Integer beginBalance;

    /** 结束余额 */
    private Integer endBalance;

    /** 覆盖标识 */
    private Long overrideBsId;

    /** 交易方式 */
    private String paymentMethod;

    /** 身份证号 */
    private String cretNo;

    // ===== 审计字段 =====

    /** 创建时间 */
    private Date createDate;

    /** 创建者 */
    private Long createdBy;
}
```

**Step 2: 验证代码编译**

```bash
cd ccdi-project
mvn compile
```

**预期输出：** BUILD SUCCESS

**Step 3: 提交**

```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java
git commit -m "feat: 创建银行流水实体类基础结构"
```

---

## Task 3: 编写转换方法测试

**目标：** 使用 TDD 方法，先编写 fromResponse() 方法的单元测试。

**文件：**
- 创建: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatementTest.java`

**Step 1: 添加测试依赖（如果不存在）**

检查 `ccdi-project/pom.xml` 是否包含测试依赖：

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
```

如果没有，添加上述依赖。

**Step 2: 创建测试类**

```java
package com.ruoyi.ccdi.project.domain.entity;

import com.ruoyi.lsfx.domain.response.GetBankStatementResponse.BankStatementItem;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 银行流水实体类测试
 *
 * @author ruoyi
 * @date 2026-03-04
 */
class CcdiBankStatementTest {

    @Test
    void testFromResponse_Success() {
        // 准备测试数据
        BankStatementItem item = new BankStatementItem();
        item.setBankStatementId(123456L);
        item.setLeId(100);
        item.setAccountId(200L);
        item.setLeName("测试企业");
        item.setAccountMaskNo("6222****1234");
        item.setDrAmount(new BigDecimal("1000.00"));
        item.setCrAmount(new BigDecimal("500.00"));
        item.setBalanceAmount(new BigDecimal("5000.00"));
        item.setTrxDate("2026-03-04");
        item.setCustomerAccountMaskNo("6228****5678");
        item.setUploadSequnceNumber(1);

        // 执行转换
        CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);

        // 验证结果
        assertNotNull(entity, "转换结果不应为null");
        assertEquals(123456L, entity.getBankStatementId(), "流水ID应该匹配");
        assertEquals(100, entity.getLeId(), "企业ID应该匹配");
        assertEquals(200L, entity.getAccountId(), "账号ID应该匹配");
        assertEquals("测试企业", entity.getLeAccountName(), "企业名称应该匹配");

        // 验证手动映射的字段
        assertEquals("6222****1234", entity.getLeAccountNo(), "企业账号应该从 accountMaskNo 映射");
        assertEquals("6228****5678", entity.getCustomerAccountNo(), "对手方账号应该从 customerAccountMaskNo 映射");
        assertEquals(1, entity.getBatchSequence(), "批次序号应该从 uploadSequnceNumber 映射");

        // 验证金额字段
        assertEquals(new BigDecimal("1000.00"), entity.getAmountDr(), "付款金额应该匹配");
        assertEquals(new BigDecimal("500.00"), entity.getAmountCr(), "收款金额应该匹配");
        assertEquals(new BigDecimal("5000.00"), entity.getAmountBalance(), "余额应该匹配");

        // 验证特殊字段
        assertNull(entity.getMetaJson(), "metaJson 应该强制为 null");
        assertNull(entity.getProjectId(), "projectId 应该为 null（需要 Service 层设置）");
    }

    @Test
    void testFromResponse_Null() {
        // 测试空值处理
        CcdiBankStatement entity = CcdiBankStatement.fromResponse(null);

        // 验证返回 null
        assertNull(entity, "传入 null 应该返回 null");
    }

    @Test
    void testFromResponse_EmptyObject() {
        // 测试空对象转换
        BankStatementItem item = new BankStatementItem();

        // 执行转换
        CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);

        // 验证不会抛出异常
        assertNotNull(entity, "空对象转换结果不应为 null");
        assertNull(entity.getMetaJson(), "metaJson 应该为 null");
    }

    @Test
    void testFromResponse_FieldTypeCompatibility() {
        // 测试字段类型兼容性
        BankStatementItem item = new BankStatementItem();
        item.setInternalFlag(1);  // Integer 类型
        item.setTransTypeId(100);  // Integer 类型

        // 执行转换
        CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);

        // 验证类型转换正确
        assertNotNull(entity, "转换结果不应为 null");
        assertEquals(1, entity.getInternalFlag(), "Integer 类型应该正确复制");
        assertEquals(100, entity.getTrxType(), "Integer 类型应该正确复制");
    }
}
```

**Step 3: 运行测试验证失败**

```bash
cd ccdi-project
mvn test -Dtest=CcdiBankStatementTest
```

**预期输出：** 编译失败，因为 `fromResponse()` 方法还不存在。

**Step 4: 提交**

```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatementTest.java
git commit -m "test: 添加银行流水转换方法的单元测试"
```

---

## Task 4: 实现转换方法

**目标：** 在 CcdiBankStatement 实体类中实现 fromResponse() 静态方法。

**文件：**
- 修改: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java`

**Step 1: 添加必要的导入**

在文件顶部添加：

```java
import com.ruoyi.lsfx.domain.response.GetBankStatementResponse.BankStatementItem;
import org.springframework.beans.BeanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
```

**Step 2: 添加日志常量**

在类的开头添加：

```java
private static final Logger log = LoggerFactory.getLogger(CcdiBankStatement.class);
```

**Step 3: 实现 fromResponse() 方法**

在类的末尾（createdBy 字段之后）添加转换方法：

```java
/**
 * 从流水分析接口响应转换为实体
 *
 * @param item 流水分析接口返回的流水项
 * @return 流水实体，如果 item 为 null 则返回 null
 */
public static CcdiBankStatement fromResponse(BankStatementItem item) {
    // 1. 空值检查
    if (item == null) {
        log.warn("流水项为空，无法转换");
        return null;
    }

    try {
        // 2. 创建实体对象
        CcdiBankStatement entity = new CcdiBankStatement();

        // 3. 使用 BeanUtils 复制同名字段
        BeanUtils.copyProperties(item, entity);

        // 4. 手动映射字段名不一致的情况
        entity.setLeAccountNo(item.getAccountMaskNo());
        entity.setCustomerAccountNo(item.getCustomerAccountMaskNo());
        entity.setBatchSequence(item.getUploadSequnceNumber());

        // 5. 特殊字段处理
        entity.setMetaJson(null);  // 根据文档要求强制设为 null

        // 注意：project_id 需要在 Service 层根据业务逻辑设置

        return entity;
    } catch (Exception e) {
        log.error("流水数据转换失败, bankStatementId={}", item.getBankStatementId(), e);
        throw new RuntimeException("流水数据转换失败", e);
    }
}
```

**Step 4: 运行测试验证通过**

```bash
cd ccdi-project
mvn test -Dtest=CcdiBankStatementTest
```

**预期输出：**
```
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
```

**Step 5: 提交**

```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java
git commit -m "feat: 实现银行流水转换方法 fromResponse()"
```

---

## Task 5: 创建 Mapper 接口

**目标：** 创建 MyBatis Mapper 接口，继承 BaseMapper 并提供批量插入方法。

**文件：**
- 创建: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java`

**Step 1: 创建 Mapper 接口**

```java
package com.ruoyi.ccdi.project.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 银行流水Mapper接口
 *
 * @author ruoyi
 * @date 2026-03-04
 */
public interface CcdiBankStatementMapper extends BaseMapper<CcdiBankStatement> {

    /**
     * 批量插入银行流水
     *
     * @param list 银行流水列表
     * @return 插入记录数
     */
    int insertBatch(@Param("list") List<CcdiBankStatement> list);
}
```

**Step 2: 验证代码编译**

```bash
cd ccdi-project
mvn compile
```

**预期输出：** BUILD SUCCESS

**Step 3: 提交**

```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java
git commit -m "feat: 创建银行流水 Mapper 接口"
```

---

## Task 6: 创建 Mapper XML

**目标：** 创建 MyBatis XML 映射文件，实现批量插入 SQL。

**文件：**
- 创建: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml`

**Step 1: 创建 XML 文件**

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper">

    <resultMap type="com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement" id="CcdiBankStatementResult">
        <id     property="bankStatementId"    column="bank_statement_id"    />
        <result property="projectId"          column="project_id"           />
        <result property="leId"               column="LE_ID"                />
        <result property="accountId"          column="ACCOUNT_ID"           />
        <result property="groupId"            column="group_id"             />
        <result property="leAccountName"      column="LE_ACCOUNT_NAME"      />
        <result property="leAccountNo"        column="LE_ACCOUNT_NO"        />
        <result property="accountingDateId"   column="ACCOUNTING_DATE_ID"   />
        <result property="accountingDate"     column="ACCOUNTING_DATE"      />
        <result property="trxDate"            column="TRX_DATE"             />
        <result property="currency"           column="CURRENCY"             />
        <result property="amountDr"           column="AMOUNT_DR"            />
        <result property="amountCr"           column="AMOUNT_CR"            />
        <result property="amountBalance"      column="AMOUNT_BALANCE"       />
        <result property="cashType"           column="CASH_TYPE"            />
        <result property="customerLeId"       column="CUSTOMER_LE_ID"       />
        <result property="customerAccountName" column="CUSTOMER_ACCOUNT_NAME" />
        <result property="customerAccountNo"  column="CUSTOMER_ACCOUNT_NO"  />
        <result property="customerBank"       column="customer_bank"        />
        <result property="customerReference"  column="customer_reference"   />
        <result property="userMemo"           column="USER_MEMO"            />
        <result property="bankComments"       column="BANK_COMMENTS"        />
        <result property="bankTrxNumber"      column="BANK_TRX_NUMBER"      />
        <result property="bank"               column="BANK"                 />
        <result property="trxFlag"            column="TRX_FLAG"             />
        <result property="trxType"            column="TRX_TYPE"             />
        <result property="exceptionType"      column="EXCEPTION_TYPE"       />
        <result property="internalFlag"       column="internal_flag"        />
        <result property="batchId"            column="batch_id"             />
        <result property="batchSequence"      column="batch_sequence"       />
        <result property="createDate"         column="CREATE_DATE"          />
        <result property="createdBy"          column="created_by"           />
        <result property="metaJson"           column="meta_json"            />
        <result property="noBalance"          column="no_balance"           />
        <result property="beginBalance"       column="begin_balance"        />
        <result property="endBalance"         column="end_balance"          />
        <result property="overrideBsId"       column="override_bs_id"       />
        <result property="paymentMethod"      column="payment_method"       />
        <result property="cretNo"             column="cret_no"              />
    </resultMap>

    <sql id="selectCcdiBankStatementVo">
        select bank_statement_id, project_id, LE_ID, ACCOUNT_ID, group_id,
               LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE,
               TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE,
               CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO,
               customer_bank, customer_reference, USER_MEMO, BANK_COMMENTS,
               BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE,
               internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by,
               meta_json, no_balance, begin_balance, end_balance,
               override_bs_id, payment_method, cret_no
        from ccdi_bank_statement
    </sql>

    <insert id="insertBatch" parameterType="java.util.List">
        insert into ccdi_bank_statement (
            project_id, LE_ID, ACCOUNT_ID, group_id,
            LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE,
            TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE,
            CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO,
            customer_bank, customer_reference, USER_MEMO, BANK_COMMENTS,
            BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE,
            internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by,
            meta_json, no_balance, begin_balance, end_balance,
            override_bs_id, payment_method, cret_no
        ) values
        <foreach collection="list" item="item" separator=",">
            (
                #{item.projectId}, #{item.leId}, #{item.accountId}, #{item.groupId},
                #{item.leAccountName}, #{item.leAccountNo}, #{item.accountingDateId}, #{item.accountingDate},
                #{item.trxDate}, #{item.currency}, #{item.amountDr}, #{item.amountCr}, #{item.amountBalance},
                #{item.cashType}, #{item.customerLeId}, #{item.customerAccountName}, #{item.customerAccountNo},
                #{item.customerBank}, #{item.customerReference}, #{item.userMemo}, #{item.bankComments},
                #{item.bankTrxNumber}, #{item.bank}, #{item.trxFlag}, #{item.trxType}, #{item.exceptionType},
                #{item.internalFlag}, #{item.batchId}, #{item.batchSequence}, #{item.createDate}, #{item.createdBy},
                #{item.metaJson}, #{item.noBalance}, #{item.beginBalance}, #{item.endBalance},
                #{item.overrideBsId}, #{item.paymentMethod}, #{item.cretNo}
            )
        </foreach>
    </insert>

</mapper>
```

**Step 2: 验证 XML 语法**

```bash
cd ccdi-project
mvn compile
```

**预期输出：** BUILD SUCCESS，无 XML 解析错误

**Step 3: 提交**

```bash
git add ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml
git commit -m "feat: 创建银行流水 Mapper XML 映射文件"
```

---

## Task 7: 验证测试

**目标：** 运行所有测试，确保功能正常。

**Step 1: 运行单元测试**

```bash
cd ccdi-project
mvn test
```

**预期输出：**
```
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
```

**Step 2: 运行集成编译**

```bash
mvn clean compile
```

**预期输出：** BUILD SUCCESS

**Step 3: 检查依赖关系**

确认 `ccdi-project` 模块的 `pom.xml` 中已依赖 `ccdi-lsfx` 模块：

```xml
<dependency>
    <groupId>com.ruoyi</groupId>
    <artifactId>ccdi-lsfx</artifactId>
</dependency>
```

如果没有，添加上述依赖并重新编译。

**Step 4: 提交所有更改**

```bash
git add .
git commit -m "test: 完成银行流水实体类功能验证"
```

---

## 完成检查清单

- [ ] 数据库已添加 `project_id` 字段和索引
- [ ] 实体类包含 39 个字段，类型正确
- [ ] `fromResponse()` 方法正确处理 3 个字段名映射
- [ ] `fromResponse()` 方法强制设置 `metaJson` 为 null
- [ ] 单元测试覆盖正常转换、空值处理、字段映射等场景
- [ ] Mapper 接口继承 `BaseMapper`
- [ ] Mapper XML 包含批量插入 SQL
- [ ] 所有测试通过

---

## 后续工作

本实施计划完成后，可以进行以下扩展：

1. **创建 Service 层** - 实现 `IBankStatementService` 接口和实现类
2. **创建 Controller 层** - 提供 REST API 接口
3. **编写集成测试** - 测试完整的数据库插入流程
4. **添加业务逻辑** - 在 Service 层设置 `projectId` 等业务字段
5. **性能优化** - 根据实际数据量调整批量插入大小

---

**计划版本：** 1.0
**创建日期：** 2026-03-04
