19 KiB
19 KiB
银行流水实体类与数据转换设计文档
日期: 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 在字段类型不匹配时会抛出异常
解决方案:
- 确保
BankStatementItem和CcdiBankStatement字段类型一致 - BigDecimal、Integer、Long 类型已验证兼容
- 添加异常捕获日志:
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条),可考虑:
- 使用 MapStruct(编译期生成代码,无反射)
- 异步批量处理
- 分批插入数据库
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 集成测试
测试场景:
- 完整流程:调用接口 → 转换数据 → 保存数据库
- 数据库查询:验证字段值正确性
- 关联查询:验证
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