# 银行流水实体类与数据转换设计文档

**日期：** 2026-03-04
**模块：** ccdi-project
**作者：** Claude

---

## 一、概述

### 1.1 目标

创建银行流水实体类 `CcdiBankStatement`，用于持久化从流水分析平台获取的流水数据，并提供数据转换方法。

### 1.2 背景

- 流水分析平台提供 `GetBankStatementResponse.BankStatementItem` 接口响应对象
- 需要将响应数据转换为本地数据库实体进行持久化
- 流水数据需要关联到具体项目（`ccdi_project` 表）

### 1.3 技术选型

| 技术点 | 选择 | 理由 |
|--------|------|------|
| ORM框架 | MyBatis Plus 3.5.10 | 项目已集成，简化CRUD操作 |
| 对象映射 | Spring BeanUtils | 无需额外依赖，简单易用 |
| 数据库 | MySQL 8.2.0 | 项目标准数据库 |
| 实体类注解 | Lombok @Data | 简化代码，提高可读性 |

---

## 二、架构设计

### 2.1 模块位置

**主模块：** `ccdi-project` （项目管理模块）

**依赖关系：**
```
ccdi-project (流水实体类所在模块)
    └── 依赖 ccdi-lsfx (访问流水分析响应类)
        └── 依赖 ruoyi-common (通用工具)
```

### 2.2 包结构

```
ccdi-project/
├── src/main/java/com/ruoyi/ccdi/project/
│   ├── domain/
│   │   └── entity/
│   │       └── CcdiBankStatement.java  (核心实体类)
│   ├── mapper/
│   │   └── CcdiBankStatementMapper.java (数据访问层)
│   └── service/
│       ├── IBankStatementService.java
│       └── impl/BankStatementServiceImpl.java
└── src/main/resources/
    └── mapper/ccdi/project/
        └── CcdiBankStatementMapper.xml
```

### 2.3 核心组件

**1. 实体类：** `CcdiBankStatement`
- 39个字段（38个原有字段 + 1个新增字段）
- 包含静态转换方法 `fromResponse()`
- 使用 MyBatis Plus 注解进行映射

**2. Mapper接口：** `CcdiBankStatementMapper`
- 继承 `BaseMapper<CcdiBankStatement>`
- 提供批量插入方法

**3. Service层：** 调用转换方法，设置业务字段

---

## 三、数据模型设计

### 3.1 数据库表结构修改

**表名：** `ccdi_bank_statement`

**新增字段：**
```sql
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`);
```

**说明：**
- `project_id` 关联 `ccdi_project` 表的主键
- `group_id` 字段保留，用于兼容流水分析平台的原始项目ID

### 3.2 字段映射关系

**总字段数：** 39个

**字段分类：**

| 分类 | 字段数 | 说明 |
|------|--------|------|
| 主键和关联 | 4 | bank_statement_id, project_id, le_id, group_id |
| 账号信息 | 5 | account_id, le_account_name, le_account_no, accounting_date_id, accounting_date |
| 交易信息 | 5 | trx_date, currency, amount_dr, amount_cr, amount_balance |
| 交易类型 | 5 | cash_type, trx_flag, trx_type, exception_type, internal_flag |
| 对手方信息 | 5 | customer_le_id, customer_account_name, customer_account_no, customer_bank, customer_reference |
| 摘要备注 | 4 | user_memo, bank_comments, bank_trx_number, bank |
| 批次上传 | 2 | batch_id, batch_sequence |
| 附加字段 | 7 | meta_json, no_balance, begin_balance, end_balance, override_bs_id, payment_method, cret_no |
| 审计字段 | 2 | create_date, created_by |

**特殊字段处理：**

| 数据库字段 | 响应字段 | 处理方式 |
|-----------|---------|---------|
| le_account_no | accountMaskNo | 手动映射 |
| customer_account_no | customerAccountMaskNo | 手动映射 |
| batch_sequence | uploadSequnceNumber | 手动映射 |
| meta_json | - | 强制设为 null |
| project_id | - | Service层设置 |

### 3.3 实体类字段类型

```java
// 数值类型
private Long bankStatementId;      // 主键
private Long projectId;            // 项目ID（新增）
private Long accountId;            // 账号ID
private Integer leId;              // 企业ID
private Integer groupId;           // 项目ID（原有）
private Integer accountingDateId;  // 账号日期ID
private Integer customerLeId;      // 对手方企业ID
private Integer trxType;           // 分类ID
private Integer internalFlag;      // 内部交易标志
private Integer batchId;           // 批次ID
private Integer batchSequence;     // 批次序号
private Integer noBalance;         // 是否包含余额
private Integer beginBalance;      // 初始余额
private Integer endBalance;        // 结束余额
private Long overrideBsId;         // 覆盖标识
private Long createdBy;            // 创建者

// 金额类型
private BigDecimal amountDr;       // 付款金额
private BigDecimal amountCr;       // 收款金额
private BigDecimal amountBalance;  // 余额

// 字符串类型
private String leAccountName;      // 企业账号名称
private String leAccountNo;        // 企业银行账号
private String accountingDate;     // 账号日期
private String trxDate;            // 交易日期
private String currency;           // 币种
private String cashType;           // 交易类型
private String trxFlag;            // 交易标志位
private String exceptionType;      // 异常类型
private String customerAccountName;// 对手方企业名称
private String customerAccountNo;  // 对手方账号
private String customerBank;       // 对手方银行
private String customerReference;  // 对手方备注
private String userMemo;           // 用户交易摘要
private String bankComments;       // 银行交易摘要
private String bankTrxNumber;      // 银行交易号
private String bank;               // 所属银行缩写
private String metaJson;           // meta json
private String paymentMethod;      // 交易方式
private String cretNo;             // 身份证号

// 日期类型
private Date createDate;           // 创建时间
```

---

## 四、转换方法设计

### 4.1 方法签名

```java
/**
 * 从流水分析接口响应转换为实体
 *
 * @param item 流水分析接口返回的流水项
 * @return 流水实体，如果 item 为 null 则返回 null
 */
public static CcdiBankStatement fromResponse(BankStatementItem item)
```

### 4.2 转换逻辑

```java
public static CcdiBankStatement fromResponse(BankStatementItem item) {
    // 1. 空值检查
    if (item == null) {
        return null;
    }

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

    // 6. 注意：project_id 需要在 Service 层设置

    return entity;
}
```

### 4.3 BeanUtils 行为说明

| 场景 | BeanUtils 行为 |
|------|---------------|
| 字段名相同且类型兼容 | 自动复制 |
| 字段名相同但类型不兼容 | 抛出异常 |
| 源对象中不存在目标字段 | 忽略，不抛异常 |
| 目标对象中不存在源字段 | 忽略，不抛异常 |
| 源字段为 null | 复制 null 值到目标字段 |

**注意事项：**
- BeanUtils 会忽略响应对象中额外的字段（如 `transAmount`, `attachments` 等）
- 需要手动处理字段名不一致的3个字段
- `meta_json` 字段强制设为 null

---

## 五、使用示例

### 5.1 Service层调用

```java
@Service
public class BankStatementServiceImpl implements IBankStatementService {

    @Resource
    private CcdiBankStatementMapper bankStatementMapper;

    @Resource
    private LsfxAnalysisClient lsfxClient;

    /**
     * 获取并保存流水数据
     *
     * @param projectId 项目ID
     * @param request 查询请求
     * @return 保存的记录数
     */
    public int fetchAndSaveBankStatements(Long projectId, GetBankStatementRequest request) {
        // 1. 调用流水分析接口
        GetBankStatementResponse response = lsfxClient.getBankStatement(request);

        // 2. 校验响应
        if (response == null || !Boolean.TRUE.equals(response.getSuccessResponse())) {
            throw new ServiceException("获取流水数据失败");
        }

        List<BankStatementItem> items = response.getData().getBankStatementList();
        if (items == null || items.isEmpty()) {
            return 0;
        }

        // 3. 转换并设置项目ID
        List<CcdiBankStatement> entities = items.stream()
            .map(item -> {
                CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
                if (entity != null) {
                    entity.setProjectId(projectId);  // 设置关联项目ID
                }
                return entity;
            })
            .filter(Objects::nonNull)
            .collect(Collectors.toList());

        // 4. 批量插入数据库
        return bankStatementMapper.insertBatch(entities);
    }
}
```

### 5.2 单条数据转换

```java
// 从接口响应转换单条流水
BankStatementItem item = response.getData().getBankStatementList().get(0);
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);

// 设置业务字段
entity.setProjectId(1001L);

// 保存到数据库
bankStatementMapper.insert(entity);
```

### 5.3 批量数据转换

```java
List<CcdiBankStatement> entities = response.getData().getBankStatementList()
    .stream()
    .map(CcdiBankStatement::fromResponse)
    .peek(entity -> entity.setProjectId(projectId))
    .collect(Collectors.toList());

bankStatementMapper.insertBatch(entities);
```

---

## 六、错误处理

### 6.1 空指针异常防护

**问题：** 接口响应可能为 null 或数据列表为空

**解决方案：**
```java
// 在 fromResponse 方法中
if (item == null) {
    log.warn("流水项为空，无法转换");
    return null;
}

// 在 Service 层
if (response == null || !Boolean.TRUE.equals(response.getSuccessResponse())) {
    throw new ServiceException("获取流水数据失败");
}

List<BankStatementItem> items = response.getData().getBankStatementList();
if (items == null || items.isEmpty()) {
    return 0;  // 正常返回，不是异常情况
}
```

### 6.2 类型转换异常

**问题：** BeanUtils 在字段类型不匹配时会抛出异常

**解决方案：**
1. 确保 `BankStatementItem` 和 `CcdiBankStatement` 字段类型一致
2. BigDecimal、Integer、Long 类型已验证兼容
3. 添加异常捕获日志：

```java
public static CcdiBankStatement fromResponse(BankStatementItem item) {
    if (item == null) {
        return null;
    }

    try {
        CcdiBankStatement entity = new CcdiBankStatement();
        BeanUtils.copyProperties(item, entity);
        entity.setLeAccountNo(item.getAccountMaskNo());
        entity.setCustomerAccountNo(item.getCustomerAccountMaskNo());
        entity.setBatchSequence(item.getUploadSequnceNumber());
        entity.setMetaJson(null);
        return entity;
    } catch (Exception e) {
        log.error("流水数据转换失败, bankStatementId={}", item.getBankStatementId(), e);
        throw new RuntimeException("流水数据转换失败", e);
    }
}
```

### 6.3 数据验证

**必填字段验证：**
```java
// 在 Service 层验证业务字段
if (entity.getProjectId() == null) {
    throw new IllegalArgumentException("项目ID不能为空");
}
```

**数据库约束：**
- `bank_statement_id` 自增主键，无需验证
- 其他字段根据业务需求设置数据库约束（NOT NULL、DEFAULT等）

---

## 七、性能考虑

### 7.1 BeanUtils 性能

**特点：**
- 使用 Java 反射机制
- 单次转换性能影响可忽略（< 1ms）
- 批量转换时累计开销需要考虑

**性能数据（参考）：**
| 操作 | 耗时 |
|------|------|
| 单次 BeanUtils.copyProperties() | < 1ms |
| 100次转换 | ~50ms |
| 1000次转换 | ~200ms |

**优化建议：**
- 对于单次或小批量转换（<100条），直接使用 BeanUtils
- 对于大批量转换（>1000条），可考虑：
  1. 使用 MapStruct（编译期生成代码，无反射）
  2. 异步批量处理
  3. 分批插入数据库

### 7.2 数据库批量插入

**推荐方式：**
```java
// MyBatis Plus 批量插入
@Service
public class BankStatementServiceImpl {

    @Resource
    private CcdiBankStatementMapper bankStatementMapper;

    public int insertBatch(List<CcdiBankStatement> entities) {
        if (entities == null || entities.isEmpty()) {
            return 0;
        }

        // 分批插入，每批 1000 条
        int batchSize = 1000;
        int totalInserted = 0;

        for (int i = 0; i < entities.size(); i += batchSize) {
            int end = Math.min(i + batchSize, entities.size());
            List<CcdiBankStatement> batch = entities.subList(i, end);
            bankStatementMapper.insertBatch(batch);
            totalInserted += batch.size();
        }

        return totalInserted;
    }
}
```

### 7.3 内存考虑

**对象占用空间估算：**
- 单个 `CcdiBankStatement` 对象约 1KB（包含所有字段）
- 1000条流水数据约占用 1MB 内存
- 10000条流水数据约占用 10MB 内存

**建议：**
- 对于超大数据量（>10000条），使用流式处理：
  ```java
  response.getData().getBankStatementList()
      .stream()
      .map(CcdiBankStatement::fromResponse)
      .forEach(entity -> {
          // 立即处理，不保留在内存中
          bankStatementMapper.insert(entity);
      });
  ```

---

## 八、测试策略

### 8.1 单元测试

**测试类：** `CcdiBankStatementTest`

**测试用例：**

| 测试场景 | 测试方法 | 验证点 |
|---------|---------|--------|
| 正常转换 | `testFromResponse_Success` | 所有字段正确映射 |
| 空值处理 | `testFromResponse_Null` | 返回 null |
| 字段名映射 | `testFromResponse_FieldMapping` | 3个特殊字段正确映射 |
| meta_json | `testFromResponse_MetaJson` | 强制为 null |

**测试代码示例：**
```java
@Test
public void testFromResponse_Success() {
    // 准备测试数据
    BankStatementItem item = new BankStatementItem();
    item.setBankStatementId(123456L);
    item.setLeId(100);
    item.setAccountMaskNo("6222****1234");
    item.setDrAmount(new BigDecimal("1000.00"));

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

    // 验证结果
    assertNotNull(entity);
    assertEquals(123456L, entity.getBankStatementId());
    assertEquals(100, entity.getLeId());
    assertEquals("6222****1234", entity.getLeAccountNo());
    assertEquals(new BigDecimal("1000.00"), entity.getAmountDr());
    assertNull(entity.getMetaJson());
}
```

### 8.2 集成测试

**测试场景：**
1. 完整流程：调用接口 → 转换数据 → 保存数据库
2. 数据库查询：验证字段值正确性
3. 关联查询：验证 `project_id` 关联有效

### 8.3 性能测试

**测试指标：**
- 单次转换耗时
- 1000次批量转换耗时
- 数据库批量插入耗时

---

## 九、部署检查清单

### 9.1 数据库修改

- [ ] 执行 ALTER TABLE 添加 `project_id` 字段
- [ ] 创建索引 `idx_project_id`
- [ ] 验证字段类型和长度

### 9.2 代码检查

- [ ] `ccdi-project` 模块已依赖 `ccdi-lsfx`
- [ ] 实体类字段类型与数据库一致
- [ ] 转换方法处理所有特殊字段
- [ ] Service 层正确设置 `project_id`

### 9.3 测试验证

- [ ] 单元测试通过
- [ ] 集成测试通过
- [ ] 性能测试达标

### 9.4 文档更新

- [ ] 更新 CLAUDE.md 文档
- [ ] 更新数据库设计文档
- [ ] 添加 API 文档说明

---

## 十、附录

### 10.1 完整字段映射表

| 序号 | 数据库字段 | Java字段 | Java类型 | 响应字段 | 说明 |
|------|-----------|---------|---------|---------|------|
| 1 | bank_statement_id | bankStatementId | Long | bankStatementId | 主键自增 |
| 2 | project_id | projectId | Long | - | **新增字段** |
| 3 | LE_ID | leId | Integer | leId | 企业ID |
| 4 | ACCOUNT_ID | accountId | Long | accountId | 账号ID |
| 5 | LE_ACCOUNT_NAME | leAccountName | String | leName | 企业账号名称 |
| 6 | LE_ACCOUNT_NO | leAccountNo | String | accountMaskNo | **手动映射** |
| 7 | ACCOUNTING_DATE_ID | accountingDateId | Integer | accountingDateId | 账号日期ID |
| 8 | ACCOUNTING_DATE | accountingDate | String | accountingDate | 账号日期 |
| 9 | TRX_DATE | trxDate | String | trxDate | 交易日期 |
| 10 | CURRENCY | currency | String | currency | 币种 |
| 11 | AMOUNT_DR | amountDr | BigDecimal | drAmount | 付款金额 |
| 12 | AMOUNT_CR | amountCr | BigDecimal | crAmount | 收款金额 |
| 13 | AMOUNT_BALANCE | amountBalance | BigDecimal | balanceAmount | 余额 |
| 14 | CASH_TYPE | cashType | String | cashType | 交易类型 |
| 15 | CUSTOMER_LE_ID | customerLeId | Integer | customerId | 对手方企业ID |
| 16 | CUSTOMER_ACCOUNT_NAME | customerAccountName | String | customerName | 对手方企业名称 |
| 17 | CUSTOMER_ACCOUNT_NO | customerAccountNo | String | customerAccountMaskNo | **手动映射** |
| 18 | customer_bank | customerBank | String | customerBank | 对手方银行 |
| 19 | customer_reference | customerReference | String | customerReference | 对手方备注 |
| 20 | USER_MEMO | userMemo | String | userMemo | 用户交易摘要 |
| 21 | BANK_COMMENTS | bankComments | String | bankComments | 银行交易摘要 |
| 22 | BANK_TRX_NUMBER | bankTrxNumber | String | bankTrxNumber | 银行交易号 |
| 23 | BANK | bank | String | bank | 所属银行缩写 |
| 24 | TRX_FLAG | trxFlag | String | transFlag | 交易标志位 |
| 25 | TRX_TYPE | trxType | Integer | transTypeId | 分类ID |
| 26 | EXCEPTION_TYPE | exceptionType | String | exceptionType | 异常类型 |
| 27 | internal_flag | internalFlag | Integer | internalFlag | 是否为内部交易 |
| 28 | batch_id | batchId | Integer | batchId | 上传logId |
| 29 | batch_sequence | batchSequence | Integer | uploadSequnceNumber | **手动映射** |
| 30 | CREATE_DATE | createDate | Date | createDate | 创建时间 |
| 31 | created_by | createdBy | Long | createdBy | 创建者 |
| 32 | meta_json | metaJson | String | - | **强制null** |
| 33 | no_balance | noBalance | Integer | isNoBalance | 是否包含余额 |
| 34 | begin_balance | beginBalance | Integer | isBeginBalance | 初始余额 |
| 35 | end_balance | endBalance | Integer | isEndBalance | 结束余额 |
| 36 | override_bs_id | overrideBsId | Long | overrideBsId | 覆盖标识 |
| 37 | payment_method | paymentMethod | String | paymentMethod | 交易方式 |
| 38 | cret_no | cretNo | String | cretNo | 身份证号 |
| 39 | group_id | groupId | Integer | groupId | 项目id |

### 10.2 参考文档

- [ccdi_bank_statement.md](../../assets/对接流水分析/ccdi_bank_statement.md)
- [MyBatis Plus 官方文档](https://baomidou.com/)
- [Spring BeanUtils 文档](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html)

---

**文档版本：** 1.0
**最后更新：** 2026-03-04
