fix: 修复中介导入成功条数计算错误
问题: - 导入成功条数显示为负数 - 原因:成功数量计算使用 validRecords.size() - failures.size() - 但没有使用实际的数据库操作返回值 修复: - saveBatchWithUpsert 和 saveBatch 方法现在返回 int - 累加实际的数据库影响行数 - 使用 actualSuccessCount 变量跟踪真实成功数量 影响范围: - CcdiIntermediaryPersonImportServiceImpl - CcdiIntermediaryEntityImportServiceImpl
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
# 中介导入功能优化设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
本设计文档描述了如何使用 MySQL 的 `INSERT ... ON DUPLICATE KEY UPDATE` 语句优化中介信息导入功能,替代现有的"先删除再插入"更新模式,提升性能并简化代码逻辑。
|
||||
|
||||
**设计日期**: 2026-02-08
|
||||
**目标**: 优化个人中介和实体中介的批量导入性能
|
||||
**核心改进**: 使用 `ON DUPLICATE KEY UPDATE` 实现 Upsert 操作
|
||||
|
||||
---
|
||||
|
||||
## 一、整体架构设计
|
||||
|
||||
### 1.1 核心变更
|
||||
|
||||
**保持现有架构:**
|
||||
- Controller 层:`CcdiIntermediaryController` - 无需修改
|
||||
- Service 层:`CcdiIntermediaryServiceImpl` - 简化逻辑
|
||||
- Mapper 层:新增批量导入方法
|
||||
|
||||
**架构优化点:**
|
||||
|
||||
| 层级 | 现有方案 | 优化方案 | 改进点 |
|
||||
|------|----------|----------|--------|
|
||||
| Mapper | `insertBatch` + `delete` | `importBatch` (ON DUPLICATE KEY UPDATE) | 单次SQL完成插入或更新 |
|
||||
| Service | 查询→分类→删除→插入 | 验证→直接导入 | 减少50%代码量 |
|
||||
| 数据库 | 2-3次操作 | 1次操作 | 减少30-40%响应时间 |
|
||||
|
||||
### 1.2 数据流变化
|
||||
|
||||
**优化前流程:**
|
||||
```
|
||||
解析Excel → 验证数据 → 批量查询已存在记录 → 分类数据
|
||||
→ 批量删除已存在记录 → 批量插入新记录和更新记录
|
||||
```
|
||||
|
||||
**优化后流程:**
|
||||
```
|
||||
解析Excel → 验证数据 → 批量 INSERT ON DUPLICATE KEY UPDATE
|
||||
```
|
||||
|
||||
**简化关键点:**
|
||||
- 移除"批量查询已存在记录"步骤
|
||||
- 移除"分类新增/更新记录"步骤
|
||||
- 移除"批量删除已存在记录"步骤
|
||||
|
||||
---
|
||||
|
||||
## 二、SQL实现细节
|
||||
|
||||
### 2.1 个人中介批量导入SQL
|
||||
|
||||
**Mapper方法签名:**
|
||||
```java
|
||||
void importPersonBatch(@Param("list") List<CcdiBizIntermediary> list);
|
||||
```
|
||||
|
||||
**SQL实现 (CcdiBizIntermediaryMapper.xml):**
|
||||
```xml
|
||||
<insert id="importPersonBatch">
|
||||
INSERT INTO cdi_biz_intermediary (
|
||||
person_id, name, gender, phone, address,
|
||||
intermediary_type, data_source, created_by, updated_by
|
||||
) VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(
|
||||
#{item.personId}, #{item.name}, #{item.gender},
|
||||
#{item.phone}, #{item.address}, #{item.intermediaryType},
|
||||
#{item.dataSource}, #{item.createdBy}, #{item.updatedBy}
|
||||
)
|
||||
</foreach>
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = IF(#{item.name} IS NOT NULL AND #{item.name} != '', #{item.name}, name),
|
||||
gender = IF(#{item.gender} IS NOT NULL AND #{item.gender} != '', #{item.gender}, gender),
|
||||
phone = IF(#{item.phone} IS NOT NULL AND #{item.phone} != '', #{item.phone}, phone),
|
||||
address = IF(#{item.address} IS NOT NULL AND #{item.address} != '', #{item.address}, address),
|
||||
intermediary_type = IF(#{item.intermediaryType} IS NOT NULL AND #{item.intermediaryType} != '', #{item.intermediaryType}, intermediary_type),
|
||||
update_time = NOW(),
|
||||
update_by = #{item.updatedBy}
|
||||
</insert>
|
||||
```
|
||||
|
||||
### 2.2 实体中介批量导入SQL
|
||||
|
||||
**Mapper方法签名:**
|
||||
```java
|
||||
void importEntityBatch(@Param("list") List<CcdiEnterpriseBaseInfo> list);
|
||||
```
|
||||
|
||||
**SQL实现 (CcdiEnterpriseBaseInfoMapper.xml):**
|
||||
```xml
|
||||
<insert id="importEntityBatch">
|
||||
INSERT INTO cdi_enterprise_base_info (
|
||||
social_credit_code, enterprise_name, legal_representative,
|
||||
phone, address, risk_level, ent_source, data_source,
|
||||
created_by, updated_by
|
||||
) VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(
|
||||
#{item.socialCreditCode}, #{item.enterpriseName},
|
||||
#{item.legalRepresentative}, #{item.phone}, #{item.address},
|
||||
#{item.riskLevel}, #{item.entSource}, #{item.dataSource},
|
||||
#{item.createdBy}, #{item.updatedBy}
|
||||
)
|
||||
</foreach>
|
||||
ON DUPLICATE KEY UPDATE
|
||||
enterprise_name = IF(#{item.enterpriseName} IS NOT NULL AND #{item.enterpriseName} != '', #{item.enterpriseName}, enterprise_name),
|
||||
legal_representative = IF(#{item.legalRepresentative} IS NOT NULL AND #{item.legalRepresentative} != '', #{item.legalRepresentative}, legal_representative),
|
||||
phone = IF(#{item.phone} IS NOT NULL AND #{item.phone} != '', #{item.phone}, phone),
|
||||
address = IF(#{item.address} IS NOT NULL AND #{item.address} != '', #{item.address}, address),
|
||||
update_time = NOW(),
|
||||
update_by = #{item.updatedBy}
|
||||
</insert>
|
||||
```
|
||||
|
||||
### 2.3 关键设计要点
|
||||
|
||||
**1. 非空字段更新策略:**
|
||||
```sql
|
||||
field = IF(#{item.field} IS NOT NULL AND #{item.field} != '', #{item.field}, field)
|
||||
```
|
||||
- 只更新Excel中非空的字段
|
||||
- 保留数据库中的原有值
|
||||
- 避免误清空数据
|
||||
|
||||
**2. 审计字段处理:**
|
||||
| 字段 | INSERT时 | UPDATE时 |
|
||||
|------|----------|----------|
|
||||
| created_by | 设置当前用户 | 不更新 |
|
||||
| create_time | 数据库默认NOW() | 不更新 |
|
||||
| updated_by | NULL | 设置当前用户 |
|
||||
| update_time | 数据库默认NOW() | 更新为NOW() |
|
||||
|
||||
**3. 唯一键约束:**
|
||||
- 个人中介: `person_id` (证件号)
|
||||
- 实体中介: `social_credit_code` (统一社会信用代码)
|
||||
|
||||
**4. 批量操作优化:**
|
||||
- 每批最多500条记录
|
||||
- 避免SQL过长导致性能问题
|
||||
- 超过500条时分批处理
|
||||
|
||||
---
|
||||
|
||||
## 三、Service层实现
|
||||
|
||||
### 3.1 isUpdateSupport参数处理
|
||||
|
||||
采用**方案C: Service层预处理**
|
||||
|
||||
```java
|
||||
@Override
|
||||
@Async
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
|
||||
Boolean isUpdateSupport,
|
||||
String taskId,
|
||||
String userName) {
|
||||
List<CcdiBizIntermediary> validRecords = new ArrayList<>();
|
||||
List<IntermediaryPersonImportFailureVO> failures = new ArrayList<>();
|
||||
|
||||
// 1. 数据验证阶段
|
||||
for (CcdiIntermediaryPersonExcel excel : excelList) {
|
||||
try {
|
||||
validatePersonData(excel);
|
||||
CcdiBizIntermediary intermediary = new CcdiBizIntermediary();
|
||||
BeanUtils.copyProperties(excel, intermediary);
|
||||
intermediary.setDataSource("IMPORT");
|
||||
intermediary.setCreatedBy(userName);
|
||||
if (isUpdateSupport) {
|
||||
intermediary.setUpdatedBy(userName);
|
||||
}
|
||||
validRecords.add(intermediary);
|
||||
} catch (Exception e) {
|
||||
IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO();
|
||||
BeanUtils.copyProperties(excel, failure);
|
||||
failure.setErrorMessage(e.getMessage());
|
||||
failures.add(failure);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 根据isUpdateSupport选择处理方式
|
||||
if (isUpdateSupport) {
|
||||
// 更新模式:直接批量导入,数据库自动处理INSERT或UPDATE
|
||||
importBatchWithUpdateSupport(validRecords, 500);
|
||||
} else {
|
||||
// 仅新增模式:先查询已存在的记录,对冲突的抛出异常
|
||||
Set<String> existingIds = getExistingPersonIds(validRecords);
|
||||
for (CcdiBizIntermediary record : validRecords) {
|
||||
if (existingIds.contains(record.getPersonId())) {
|
||||
throw new RuntimeException("该证件号已存在");
|
||||
}
|
||||
}
|
||||
// 确认无冲突后,批量插入
|
||||
importBatchWithoutUpdateSupport(validRecords, 500);
|
||||
}
|
||||
|
||||
// 3. 更新导入状态
|
||||
ImportResult result = new ImportResult();
|
||||
result.setTotalCount(excelList.size());
|
||||
result.setSuccessCount(validRecords.size());
|
||||
result.setFailureCount(failures.size());
|
||||
|
||||
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
|
||||
updateImportStatus(taskId, finalStatus, result);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 代码简化对比
|
||||
|
||||
**优化前 (约120行):**
|
||||
```java
|
||||
// 1. 批量查询已存在记录
|
||||
Set<String> existingIds = getExistingPersonIds(excelList);
|
||||
|
||||
// 2. 分类数据
|
||||
for (excel : excelList) {
|
||||
if (existingIds.contains(excel.getPersonId())) {
|
||||
if (isUpdateSupport) {
|
||||
updateRecords.add(convert(excel));
|
||||
} else {
|
||||
throw new RuntimeException("已存在");
|
||||
}
|
||||
} else {
|
||||
newRecords.add(convert(excel));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 批量插入新数据
|
||||
if (!newRecords.isEmpty()) {
|
||||
saveBatch(newRecords, 500);
|
||||
}
|
||||
|
||||
// 4. 批量更新已有数据(先删除再插入)
|
||||
if (!updateRecords.isEmpty() && isUpdateSupport) {
|
||||
List<String> personIds = updateRecords.stream()
|
||||
.map(CcdiBizIntermediary::getPersonId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
LambdaQueryWrapper<CcdiBizIntermediary> deleteWrapper = new LambdaQueryWrapper<>();
|
||||
deleteWrapper.in(CcdiBizIntermediary::getPersonId, personIds);
|
||||
intermediaryMapper.delete(deleteWrapper);
|
||||
|
||||
intermediaryMapper.insertBatch(updateRecords);
|
||||
}
|
||||
```
|
||||
|
||||
**优化后 (约60行):**
|
||||
```java
|
||||
// 1. 验证数据并转换
|
||||
for (excel : excelList) {
|
||||
validatePersonData(excel);
|
||||
validRecords.add(convert(excel));
|
||||
}
|
||||
|
||||
// 2. 直接批量导入(数据库自动处理INSERT或UPDATE)
|
||||
if (isUpdateSupport) {
|
||||
intermediaryMapper.importPersonBatch(validRecords);
|
||||
} else {
|
||||
// 仅新增模式:检查唯一性
|
||||
checkUniqueAndInsert(validRecords);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、错误处理与边界情况
|
||||
|
||||
### 4.1 错误分类处理
|
||||
|
||||
| 错误类型 | 处理方式 | 状态标记 |
|
||||
|----------|----------|----------|
|
||||
| 数据验证错误 | 添加到失败列表,继续处理后续数据 | PARTIAL_SUCCESS |
|
||||
| 唯一性冲突 (isUpdateSupport=false) | 抛出异常,添加到失败列表 | PARTIAL_SUCCESS |
|
||||
| SQL执行错误 | 事务回滚,记录详细错误信息 | FAILED |
|
||||
| 所有记录失败 | 状态为FAILED | FAILED |
|
||||
| 部分成功 | 状态为PARTIAL_SUCCESS | PARTIAL_SUCCESS |
|
||||
|
||||
### 4.2 边界情况处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|----------|
|
||||
| Excel为空 | 返回"至少需要一条数据" |
|
||||
| 所有数据格式错误 | 成功数=0,失败数=总数,状态=FAILED |
|
||||
| 超大数据量(>5000条) | 分批处理,每批500条 |
|
||||
| 并发导入相同数据 | 依靠数据库唯一索引保证一致性 |
|
||||
| NULL字段更新 | 使用IF语句跳过,保留原值 |
|
||||
| 空字符串字段更新 | 视为NULL,不更新 |
|
||||
|
||||
### 4.3 事务处理
|
||||
|
||||
```java
|
||||
@Async
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void importPersonAsync(...) {
|
||||
try {
|
||||
// 数据验证
|
||||
// 批量导入
|
||||
// 更新状态
|
||||
} catch (Exception e) {
|
||||
// 事务自动回滚
|
||||
// 记录错误日志
|
||||
// 更新状态为FAILED
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、测试策略
|
||||
|
||||
### 5.1 单元测试
|
||||
|
||||
**Mapper层测试:**
|
||||
- ✅ 批量插入全新记录
|
||||
- ✅ 批量更新已存在记录
|
||||
- ✅ 混合场景(部分新记录+部分已存在)
|
||||
- ✅ NULL值字段不覆盖原值
|
||||
- ✅ 审计字段正确设置和更新
|
||||
- ✅ 唯一键冲突处理
|
||||
|
||||
**Service层测试:**
|
||||
- ✅ `isUpdateSupport=true` 的完整流程
|
||||
- ✅ `isUpdateSupport=false` 时重复数据抛异常
|
||||
- ✅ 数据验证逻辑(必填字段、格式校验)
|
||||
- ✅ 事务回滚机制
|
||||
- ✅ 失败记录保存到Redis
|
||||
|
||||
### 5.2 集成测试场景
|
||||
|
||||
| 测试场景 | 测试步骤 | 预期结果 |
|
||||
|----------|----------|----------|
|
||||
| 新增模式测试 | 导入100条全新记录 | 全部成功插入,审计字段正确 |
|
||||
| 更新模式测试 | 导入→修改→再导入 | 数据正确更新,NULL字段保留 |
|
||||
| 混合模式测试 | 50新+50已存在记录 | 新记录插入,旧记录更新 |
|
||||
| 仅新增冲突测试 | 导入已存在记录(isUpdateSupport=false) | 抛出异常,记录失败 |
|
||||
| 空文件测试 | 导入空Excel | 返回"至少需要一条数据" |
|
||||
| 全部失败测试 | 所有数据格式错误 | 状态=FAILED,失败数=总数 |
|
||||
| 大数据量测试 | 导入2000+条记录 | 分批处理,全部成功 |
|
||||
| 并发测试 | 同时导入相同数据 | 依靠唯一索引保证一致性 |
|
||||
|
||||
### 5.3 性能测试
|
||||
|
||||
**测试数据:**
|
||||
- 500条记录
|
||||
- 1000条记录
|
||||
- 2000条记录
|
||||
|
||||
**性能指标:**
|
||||
- 总响应时间
|
||||
- 数据库操作次数
|
||||
- 内存使用情况
|
||||
|
||||
**预期性能提升:**
|
||||
- 更新模式下性能提升 30-40%
|
||||
- 数据库操作次数减少 2次(查询+删除)
|
||||
|
||||
---
|
||||
|
||||
## 六、实施计划
|
||||
|
||||
### 6.1 实施步骤
|
||||
|
||||
1. **数据库准备**
|
||||
- 确认 `cdi_biz_intermediary.person_id` 有唯一索引
|
||||
- 确认 `cdi_enterprise_base_info.social_credit_code` 有唯一索引
|
||||
|
||||
2. **Mapper层实现**
|
||||
- 在 `CcdiBizIntermediaryMapper` 接口添加 `importPersonBatch` 方法
|
||||
- 在 `CcdiEnterpriseBaseInfoMapper` 接口添加 `importEntityBatch` 方法
|
||||
- 在对应的XML文件实现SQL语句
|
||||
|
||||
3. **Service层重构**
|
||||
- 修改 `CcdiIntermediaryPersonImportServiceImpl.importPersonAsync` 方法
|
||||
- 修改 `CcdiIntermediaryEntityImportServiceImpl.importEntityAsync` 方法
|
||||
- 简化逻辑,移除删除操作
|
||||
|
||||
4. **单元测试**
|
||||
- 编写Mapper层测试
|
||||
- 编写Service层测试
|
||||
|
||||
5. **集成测试**
|
||||
- 使用现有测试数据验证功能
|
||||
- 对比优化前后的性能
|
||||
|
||||
6. **文档更新**
|
||||
- 更新API文档
|
||||
- 记录性能优化结果
|
||||
|
||||
### 6.2 向后兼容性
|
||||
|
||||
- ✅ API接口保持不变,前端无需修改
|
||||
- ✅ 返回数据格式不变
|
||||
- ✅ 错误处理机制不变
|
||||
- ✅ Redis状态管理不变
|
||||
|
||||
### 6.3 风险评估
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 唯一索引缺失 | 功能失败 | 实施前检查索引存在性 |
|
||||
| 数据库版本兼容性 | SQL语法不支持 | 确认MySQL 5.7+ |
|
||||
| 并发冲突 | 数据不一致 | 依赖数据库唯一索引和事务 |
|
||||
| 性能回退 | 响应变慢 | 进行性能测试对比 |
|
||||
|
||||
---
|
||||
|
||||
## 七、预期收益
|
||||
|
||||
### 7.1 性能提升
|
||||
|
||||
| 指标 | 优化前 | 优化后 | 提升 |
|
||||
|------|--------|--------|------|
|
||||
| 数据库操作次数 | 3次(查询+删除+插入) | 1次(UPSERT) | -66% |
|
||||
| 代码行数 | ~120行 | ~60行 | -50% |
|
||||
| 响应时间(1000条更新) | 基准 | 减少30-40% | 30-40% |
|
||||
|
||||
### 7.2 代码质量
|
||||
|
||||
- ✅ 逻辑更清晰,易于维护
|
||||
- ✅ 减少出错可能性
|
||||
- ✅ 更好的事务一致性
|
||||
- ✅ 符合数据库最佳实践
|
||||
|
||||
### 7.3 可维护性
|
||||
|
||||
- SQL集中在XML文件,易于优化
|
||||
- 业务逻辑简化,降低认知负担
|
||||
- 错误处理更精确
|
||||
- 测试覆盖更全面
|
||||
|
||||
---
|
||||
|
||||
## 八、附录
|
||||
|
||||
### 8.1 相关文件
|
||||
|
||||
- Controller: `CcdiIntermediaryController.java`
|
||||
- Service接口: `ICcdiIntermediaryService.java`
|
||||
- Service实现: `CcdiIntermediaryServiceImpl.java`
|
||||
- Import Service: `CcdiIntermediaryPersonImportServiceImpl.java`
|
||||
- Mapper接口: `CcdiBizIntermediaryMapper.java`
|
||||
- Mapper XML: `CcdiBizIntermediaryMapper.xml`
|
||||
|
||||
### 8.2 数据库表结构
|
||||
|
||||
**个人中介表 (cdi_biz_intermediary):**
|
||||
```sql
|
||||
UNIQUE KEY `uk_person_id` (`person_id`)
|
||||
```
|
||||
|
||||
**实体中介表 (cdi_enterprise_base_info):**
|
||||
```sql
|
||||
PRIMARY KEY (`social_credit_code`)
|
||||
```
|
||||
|
||||
### 8.3 测试数据
|
||||
|
||||
- 测试文件: `doc/test-data/purchase_transaction/purchase_test_data_2000_final.xlsx`
|
||||
- 测试脚本: 待生成
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2026-02-08
|
||||
**状态**: 待评审
|
||||
209
doc/plans/2026-02-08-purchase-transaction-import-fixes.md
Normal file
209
doc/plans/2026-02-08-purchase-transaction-import-fixes.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# 采购交易导入功能问题修复总结
|
||||
|
||||
## 修复日期
|
||||
2026-02-08
|
||||
|
||||
## 问题描述
|
||||
|
||||
### 问题1: 导入全部失败,提示"采购数量不能为空"
|
||||
**现象**:
|
||||
- 使用 `purchase_test_data_2000.xlsx` 导入,2000条记录全部失败
|
||||
- 错误信息:`采购数量不能为空`
|
||||
- 查询失败记录接口返回2000条记录
|
||||
|
||||
**根本原因**:
|
||||
- Excel实体类 `CcdiPurchaseTransactionExcel` 中,数值字段(purchaseQty、budgetAmount等)类型为 **String**
|
||||
- AddDTO `CcdiPurchaseTransactionAddDTO` 中,对应字段类型为 **BigDecimal**
|
||||
- `BeanUtils.copyProperties()` 进行 String → BigDecimal 转换时,空字符串转换为null
|
||||
- 测试数据中这些列为空,导致验证失败
|
||||
|
||||
**修复方案**:
|
||||
修改 `CcdiPurchaseTransactionExcel.java`,将数值字段类型从 String 改为 BigDecimal
|
||||
|
||||
**修改文件**:
|
||||
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiPurchaseTransactionExcel.java:52-82`
|
||||
|
||||
**修改内容**:
|
||||
```java
|
||||
// 修改前
|
||||
private String purchaseQty;
|
||||
private String budgetAmount;
|
||||
private String bidAmount;
|
||||
// ... 其他金额字段
|
||||
|
||||
// 修改后
|
||||
private BigDecimal purchaseQty;
|
||||
private BigDecimal budgetAmount;
|
||||
private BigDecimal bidAmount;
|
||||
// ... 其他金额字段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题2: 查看失败记录弹窗显示"暂无数据"
|
||||
**现象**:
|
||||
- 导入失败后,点击"查看导入失败记录"按钮
|
||||
- 后端接口返回了失败记录数据
|
||||
- 前端页面显示"暂无数据"
|
||||
|
||||
**根本原因**:
|
||||
- 前端期望接口返回分页格式:`{rows: [...], total: N}`
|
||||
- 后端接口返回的是简单列表:`{data: [...]}`
|
||||
- 后端接口缺少分页参数和分页逻辑
|
||||
|
||||
**修复方案**:
|
||||
参照员工信息管理模块,修改采购交易管理的查询失败记录接口:
|
||||
1. 添加分页参数(pageNum、pageSize)
|
||||
2. 实现手动分页逻辑
|
||||
3. 返回类型从 `AjaxResult` 改为 `TableDataInfo`
|
||||
4. 使用 `getDataTable()` 方法返回分页格式
|
||||
|
||||
**修改文件**:
|
||||
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiPurchaseTransactionController.java:173-196`
|
||||
|
||||
**修改内容**:
|
||||
```java
|
||||
// 修改前
|
||||
@GetMapping("/importFailures/{taskId}")
|
||||
public AjaxResult getImportFailures(@PathVariable String taskId) {
|
||||
List<PurchaseTransactionImportFailureVO> failures = transactionImportService.getImportFailures(taskId);
|
||||
return success(failures); // 返回 {data: [...]}
|
||||
}
|
||||
|
||||
// 修改后
|
||||
@GetMapping("/importFailures/{taskId}")
|
||||
public TableDataInfo getImportFailures(
|
||||
@PathVariable String taskId,
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
|
||||
List<PurchaseTransactionImportFailureVO> failures = transactionImportService.getImportFailures(taskId);
|
||||
|
||||
// 手动分页
|
||||
int fromIndex = (pageNum - 1) * pageSize;
|
||||
int toIndex = Math.min(fromIndex + pageSize, failures.size());
|
||||
List<PurchaseTransactionImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
|
||||
|
||||
return getDataTable(pageData, failures.size()); // 返回 {rows: [...], total: N}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 修复后的完整流程
|
||||
|
||||
### 1. 正常导入场景(数据完整)
|
||||
1. 上传Excel文件
|
||||
2. 后端异步处理,验证数据
|
||||
3. 所有数据通过验证,成功插入数据库
|
||||
4. 前端收到完成通知:`导入完成!全部成功!共导入2000条数据`
|
||||
5. 列表刷新,显示新导入的数据
|
||||
|
||||
### 2. 部分失败场景(数据有误)
|
||||
1. 上传Excel文件
|
||||
2. 后端异步处理,验证数据
|
||||
3. 部分数据验证失败,失败记录保存到Redis
|
||||
4. 前端收到完成通知:`导入完成!成功1800条,失败200条`
|
||||
5. 操作栏显示"查看导入失败记录"按钮
|
||||
6. 点击按钮,打开失败记录对话框
|
||||
7. 对话框显示分页的失败记录列表:
|
||||
- 采购事项ID
|
||||
- 项目名称
|
||||
- 标的物名称
|
||||
- 失败原因
|
||||
8. 支持分页查询(每页10条)
|
||||
9. 支持清除历史记录
|
||||
|
||||
---
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 测试正常导入
|
||||
- 使用修复后的测试数据:`purchase_test_data_2000_fixed.xlsx`
|
||||
- 预期结果:全部成功,2000条记录导入成功
|
||||
|
||||
### 2. 测试失败记录查看
|
||||
- 使用有问题的测试数据(故意制造错误数据)
|
||||
- 预期结果:
|
||||
- 显示部分成功通知
|
||||
- 显示"查看导入失败记录"按钮
|
||||
- 点击后能看到失败记录列表
|
||||
- 分页功能正常
|
||||
|
||||
### 3. 测试状态持久化
|
||||
- 导入有失败的数据
|
||||
- 刷新页面
|
||||
- 预期结果:"查看导入失败记录"按钮仍然显示
|
||||
|
||||
---
|
||||
|
||||
## 修复验证清单
|
||||
|
||||
- [x] 修改Excel实体类字段类型
|
||||
- [x] 重新编译后端成功
|
||||
- [x] 修改查询失败记录接口
|
||||
- [x] 添加分页支持
|
||||
- [x] 重新编译后端成功
|
||||
- [ ] 重启后端服务
|
||||
- [ ] 测试正常导入
|
||||
- [ ] 测试失败记录查看
|
||||
- [ ] 验证前端显示正常
|
||||
|
||||
---
|
||||
|
||||
## 下一步操作
|
||||
|
||||
**需要手动执行**:
|
||||
1. 重启后端服务(加载新编译的代码)
|
||||
2. 使用修复后的测试数据进行导入测试
|
||||
3. 验证失败记录查看功能正常
|
||||
|
||||
---
|
||||
|
||||
## 技术说明
|
||||
|
||||
### Excel数值字段处理
|
||||
- **EasyExcel** 会根据Java字段类型自动转换
|
||||
- String类型 → 读取为字符串(空值可能为空字符串)
|
||||
- BigDecimal类型 → 读取为数值(空值为null)
|
||||
- BeanUtils.copyProperties() 会自动处理类型转换
|
||||
|
||||
### 分页数据格式
|
||||
```javascript
|
||||
// 前端期望的格式
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "查询成功",
|
||||
"rows": [...], // 当前页数据
|
||||
"total": 100 // 总记录数
|
||||
}
|
||||
```
|
||||
|
||||
### 若依框架分页方法
|
||||
```java
|
||||
// BaseController.getDataTable() 方法
|
||||
protected TableDataInfo getDataTable(List<?> list, long total) {
|
||||
TableDataInfo rspData = new TableDataInfo();
|
||||
rspData.setCode(200);
|
||||
rspData.setMsg("查询成功");
|
||||
rspData.setRows(list);
|
||||
rspData.setTotal(total);
|
||||
return rspData;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文件
|
||||
|
||||
### 修改的文件
|
||||
1. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiPurchaseTransactionExcel.java`
|
||||
2. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiPurchaseTransactionController.java`
|
||||
|
||||
### 参考文件
|
||||
1. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java` (员工信息管理,作为参考)
|
||||
|
||||
### 测试文件
|
||||
1. `doc/test-data/purchase_transaction/generate-test-data.js` (测试数据生成脚本)
|
||||
2. `doc/test-data/purchase_transaction/purchase_test_data_2000_fixed.xlsx` (修复后的测试数据)
|
||||
3. `doc/test-data/purchase_transaction/test-import-debug.js` (导入测试脚本)
|
||||
Reference in New Issue
Block a user