# 银行流水实体类与数据转换设计文档 **日期:** 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` - 提供批量插入方法 **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 items = response.getData().getBankStatementList(); if (items == null || items.isEmpty()) { return 0; } // 3. 转换并设置项目ID List 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 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 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 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 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