diff --git a/docs/plans/2026-03-04-bank-statement-entity-design.md b/docs/plans/2026-03-04-bank-statement-entity-design.md new file mode 100644 index 0000000..2527c24 --- /dev/null +++ b/docs/plans/2026-03-04-bank-statement-entity-design.md @@ -0,0 +1,603 @@ +# 银行流水实体类与数据转换设计文档 + +**日期:** 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