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

604 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 银行流水实体类与数据转换设计文档
**日期:** 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