Files
ccdi/docs/plans/2026-03-04-bank-statement-entity-design.md

19 KiB
Raw Blame History

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

日期: 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

新增字段:

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 实体类字段类型

// 数值类型
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 方法签名

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

4.2 转换逻辑

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层调用

@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 单条数据转换

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

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

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

5.3 批量数据转换

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

bankStatementMapper.insertBatch(entities);

六、错误处理

6.1 空指针异常防护

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

解决方案:

// 在 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. 确保 BankStatementItemCcdiBankStatement 字段类型一致
  2. BigDecimal、Integer、Long 类型已验证兼容
  3. 添加异常捕获日志:
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 数据验证

必填字段验证:

// 在 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 数据库批量插入

推荐方式:

// 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条使用流式处理
    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

测试代码示例:

@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 参考文档


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