diff --git a/doc/test-reports/2026-02-11-cust-fmy-relation-import-alignment-test.md b/doc/test-reports/2026-02-11-cust-fmy-relation-import-alignment-test.md new file mode 100644 index 0000000..c0f323f --- /dev/null +++ b/doc/test-reports/2026-02-11-cust-fmy-relation-import-alignment-test.md @@ -0,0 +1,423 @@ +# 信贷客户家庭关系导入功能对齐测试报告 + +## 修改概述 + +本次修改将**信贷客户家庭关系**功能的导入实现完全对齐到**员工亲属关系**的成熟模式,提升了代码质量、性能和用户体验。 + +**修改日期**: 2026-02-11 +**参考模板**: `CcdiStaffEnterpriseRelationImportServiceImpl` +**修改对象**: `CcdiCustFmyRelationImportServiceImpl` + +--- + +## 修改文件清单 + +### 1. Mapper 层 +**文件**: `CcdiCustFmyRelationMapper.java` +- ✅ 新增 `batchExistsByCombinations` 方法接口 +- ✅ 支持批量查询已存在的关系组合 + +**文件**: `CcdiCustFmyRelationMapper.xml` +- ✅ 实现 `batchExistsByCombinations` SQL +- ✅ 优化:从 N 次查询减少到 1 次查询 + +```xml + +``` + +### 2. Service 层 + +**文件**: `CcdiCustFmyRelationImportServiceImpl.java` +- ✅ 完全重构,参考员工亲属关系实现 +- ✅ 引入 `ImportLogUtils` 统一日志记录 +- ✅ 实现 `getExistingCombinations` 批量查询 +- ✅ 添加 Excel 内部重复检查 +- ✅ 优化 Redis 状态管理(Hash 结构) +- ✅ 实现分批插入(每批500条) +- ✅ 添加 `getImportStatus` 方法 +- ✅ 优化失败记录存储(JSON 序列化,7天过期) + +**文件**: `CcdiCustFmyRelationServiceImpl.java` +- ✅ 更新 `importRelations` 方法,传递 userName 参数 +- ✅ 初始化 Redis 状态为 Hash 结构 +- ✅ 使用 `EasyExcelUtil` 进行导出和模板下载 +- ✅ 添加数据量校验 + +### 3. Controller 层 + +**文件**: `CcdiCustFmyRelationController.java` +- ✅ 导入接口返回 `ImportResultVO` 对象 +- ✅ 状态查询接口返回 `ImportStatusVO` 对象 +- ✅ 失败记录接口支持分页 +- ✅ 使用 `EasyExcelUtil` 工具类 + +### 4. VO 类 +- ✅ 复用 `ImportStatusVO.java` +- ✅ 复用 `ImportResultVO.java` +- ✅ 复用 `CustFmyRelationImportFailureVO.java` + +### 5. Excel 实体 +**文件**: `CcdiCustFmyRelationExcel.java` +- ✅ 已包含完整的 `@DictDropdown` 注解 + - `ccdi_relation_type` (关系类型) + - `ccdi_indiv_gender` (性别) + - `ccdi_certificate_type` (证件类型) + +--- + +## 核心改进点 + +### 1. 性能优化 +| 项目 | 优化前 | 优化后 | 提升 | +|------|--------|--------|------| +| 唯一性检查 | N 次数据库查询 | 1 次批量查询 | 约 90% | +| 批量插入 | 无分批控制 | 每批 500 条 | 更稳定 | +| 导入1000条 | 预计 30-50秒 | 预计 10-15秒 | 约 60% | + +### 2. Redis 状态管理升级 + +**优化前**: +``` +Key: import:custFmyRelation:{taskId} +Value: "COMPLETED:10:5" +TTL: 1 小时 +``` + +**优化后**: +``` +Key: import:custFmyRelation:{taskId} +Type: Hash +Fields: + - taskId: "uuid" + - status: "SUCCESS" | "PARTIAL_SUCCESS" | "PROCESSING" + - totalCount: 100 + - successCount: 95 + - failureCount: 5 + - progress: 100 + - startTime: 1234567890 + - endTime: 1234567900 + - message: "成功95条,失败5条" +TTL: 7 天 +``` + +### 3. 导入日志记录 +使用 `ImportLogUtils` 统一记录: +- ✅ 导入开始/结束 +- ✅ 批量查询日志 +- ✅ 进度跟踪 +- ✅ 验证错误详情 +- ✅ 批量操作日志 +- ✅ Redis 操作日志 + +### 4. 数据验证增强 +- ✅ 身份证号格式验证(18位) +- ✅ 字段长度验证 +- ✅ Excel 内部重复检查 +- ✅ 数据库唯一性检查(批量) + +--- + +## 测试指南 + +### 测试环境准备 +1. 启动后端服务 (`mvn spring-boot:run`) +2. 确保数据库连接正常 +3. 确保 Redis 服务运行 + +### 测试步骤 + +#### 1. 下载导入模板 +```bash +POST /ccdi/custFmyRelation/importTemplate +Headers: + Authorization: Bearer {token} +``` + +**预期结果**: +- 返回 Excel 文件 +- 包含字典下拉框(关系类型、性别、证件类型) + +#### 2. 准备测试数据 + +创建包含以下字段的测试数据: + +| 信贷客户身份证号 | 关系类型 | 关系人姓名 | 性别 | 关系人证件类型 | 关系人证件号码 | +|----------------|---------|-----------|------|-------------|-------------| +| 110101199001011234 | 配偶 | 张三 | 男 | 身份证 | 110101199001011235 | +| 110101199001011234 | 子女 | 李四 | 女 | 身份证 | 110101201001011236 | + +**测试场景**: +- ✅ 正常数据导入 +- ✅ 重复数据导入(应返回错误) +- ✅ Excel 内部重复(应检测并报错) +- ✅ 必填字段缺失(应返回详细错误) + +#### 3. 提交导入任务 +```bash +POST /ccdi/custFmyRelation/importData +Headers: + Authorization: Bearer {token} +Form Data: + file: 测试数据.xlsx +``` + +**预期响应**: +```json +{ + "code": 200, + "msg": "导入任务已提交,正在后台处理", + "data": { + "taskId": "uuid-string", + "status": "PROCESSING", + "message": "导入任务已提交,正在后台处理" + } +} +``` + +#### 4. 查询导入状态 +```bash +GET /ccdi/custFmyRelation/importStatus/{taskId} +Headers: + Authorization: Bearer {token} +``` + +**预期响应**: +```json +{ + "taskId": "uuid-string", + "status": "SUCCESS", + "totalCount": 2, + "successCount": 2, + "failureCount": 0, + "progress": 100, + "message": "全部成功!共导入2条数据" +} +``` + +#### 5. 查询失败记录 +```bash +GET /ccdi/custFmyRelation/importFailures/{taskId}?pageNum=1&pageSize=10 +Headers: + Authorization: Bearer {token} +``` + +**预期响应** (如果有失败): +```json +{ + "total": 1, + "rows": [ + { + "rowNum": 2, + "personId": "110101199001011234", + "relationType": "配偶", + "relationName": "张三", + "errorMessage": "该关系已存在,请勿重复导入" + } + ], + "code": 200, + "msg": "查询成功" +} +``` + +### 自动化测试脚本 + +使用提供的测试脚本: +```bash +doc\test-scripts\test-cust-fmy-relation-import.bat +``` + +**测试脚本功能**: +1. 登录获取 token +2. 下载导入模板 +3. 提交导入任务 +4. 查询导入状态 +5. 查询失败记录 +6. 测试查询接口 + +--- + +## 验证清单 + +### 功能验证 +- [ ] 导入模板下载正常 +- [ ] 导入任务提交成功 +- [ ] 导入状态查询正常 +- [ ] 导入成功数据正确插入数据库 +- [ ] 重复数据被正确拦截 +- [ ] Excel 内部重复被检测 +- [ ] 失败记录正确保存到 Redis +- [ ] 失败记录查询支持分页 +- [ ] 导入日志正常输出 + +### 性能验证 +- [ ] 导入 100 条数据 < 5 秒 +- [ ] 导入 1000 条数据 < 20 秒 +- [ ] 批量查询只执行 1 次 SQL +- [ ] Redis 状态更新及时 + +### 日志验证 +- [ ] 导入开始日志 +- [ ] 批量查询日志 +- [ ] 进度日志 +- [ ] 验证错误日志 +- [ ] 批量操作日志 +- [ ] 导入完成日志 + +--- + +## API 文档更新 + +### 导入相关接口 + +#### 1. 下载导入模板 +```http +POST /ccdi/custFmyRelation/importTemplate +Authorization: Bearer {token} +Content-Type: application/json + +Response: Excel 文件 +``` + +#### 2. 提交导入任务 +```http +POST /ccdi/custFmyRelation/importData +Authorization: Bearer {token} +Content-Type: multipart/form-data + +Form Data: + file: Excel 文件 + +Response: +{ + "code": 200, + "msg": "导入任务已提交,正在后台处理", + "data": { + "taskId": "uuid", + "status": "PROCESSING", + "message": "导入任务已提交,正在后台处理" + } +} +``` + +#### 3. 查询导入状态 +```http +GET /ccdi/custFmyRelation/importStatus/{taskId} +Authorization: Bearer {token} + +Response: +{ + "code": 200, + "data": { + "taskId": "uuid", + "status": "SUCCESS", + "totalCount": 100, + "successCount": 95, + "failureCount": 5, + "progress": 100, + "startTime": 1234567890, + "endTime": 1234567900, + "message": "成功95条,失败5条" + } +} +``` + +#### 4. 查询导入失败记录 +```http +GET /ccdi/custFmyRelation/importFailures/{taskId}?pageNum=1&pageSize=10 +Authorization: Bearer {token} + +Response: +{ + "code": 200, + "total": 5, + "rows": [...], + "msg": "查询成功" +} +``` + +--- + +## 回归测试建议 + +### 测试场景 +1. **正常数据导入**: 全部字段完整有效 +2. **必填字段缺失**: 缺少 personId、relationType 等 +3. **格式错误**: 身份证号格式不正确 +4. **数据重复**: + - 数据库中已存在 + - Excel 文件内重复 +5. **大数据量**: 导入 1000+ 条数据 +6. **并发导入**: 同时提交多个导入任务 +7. **边界情况**: 空文件、单条数据、最大字段长度 + +### 性能基准 +| 数据量 | 预期时间 | 最大内存 | +|--------|---------|---------| +| 10 条 | < 2 秒 | < 50MB | +| 100 条 | < 5 秒 | < 100MB | +| 1000 条 | < 20 秒 | < 200MB | +| 10000 条 | < 3 分钟 | < 500MB | + +--- + +## 注意事项 + +### 1. 字典配置 +确保以下字典数据已配置: +- `ccdi_relation_type` (关系类型) +- `ccdi_indiv_gender` (性别) +- `ccdi_certificate_type` (证件类型) + +### 2. Redis 配置 +- 确保 Redis 服务运行 +- 检查 Redis 过期策略 +- 监控 Redis 内存使用 + +### 3. 异步配置 +- 确保 `@EnableAsync` 已启用 +- 检查异步线程池配置 +- 监控异步任务执行情况 + +### 4. 日志级别 +- 生产环境: INFO +- 开发环境: DEBUG +- 测试环境: DEBUG + +--- + +## 后续优化建议 + +### 1. 导入进度实时推送 +考虑使用 WebSocket 实现导入进度实时推送,替代轮询查询。 + +### 2. 导入历史记录 +添加导入历史记录表,记录每次导入的详细信息,便于追溯。 + +### 3. 数据预校验 +在前端添加数据预校验,提前发现格式错误,减少无效提交。 + +### 4. 导入模板智能生成 +根据数据库字典动态生成导入模板,减少维护成本。 + +### 5. 批量操作优化 +考虑使用 MyBatis Plus 的 `SqlInjector` 实现真正的批量插入。 + +--- + +## 创建日期 + +2026-02-11 + +## 相关文档 + +- [设计方案](../plans/2026-02-11-cust-fmy-relation-import-alignment.md) +- [测试脚本](./test-cust-fmy-relation-import.bat) +- [API 文档](../../api/ccdi/cust-fmy-relation-api.md) diff --git a/doc/test-scripts/test-cust-fmy-relation-import.bat b/doc/test-scripts/test-cust-fmy-relation-import.bat new file mode 100644 index 0000000..c399ad5 --- /dev/null +++ b/doc/test-scripts/test-cust-fmy-relation-import.bat @@ -0,0 +1,107 @@ +@echo off +REM 信贷客户家庭关系导入功能测试脚本 +REM 测试对齐后的导入功能 + +echo ======================================== +echo 信贷客户家庭关系导入功能测试 +echo ======================================== +echo. + +REM 设置后端服务地址 +set BASE_URL=http://localhost:8080 + +REM 步骤1: 登录获取token +echo [1/6] 正在登录... +curl -s -X POST "%BASE_URL%/login/test" ^ + -H "Content-Type: application/json" ^ + -d "{\"username\":\"admin\",\"password\":\"admin123\"}" ^ + > login_response.json + +REM 提取token +for /f "tokens=2 delims=:\"" %%a in ('findstr /C:"\"token\"" login_response.json') do ( + set TOKEN=%%a + goto :token_found +) +:token_found + +echo 登录成功! Token: %TOKEN:~0,20%... +echo. + +REM 步骤2: 下载导入模板 +echo [2/6] 下载导入模板... +curl -s -X POST "%BASE_URL%/ccdi/custFmyRelation/importTemplate" ^ + -H "Authorization: Bearer %TOKEN%" ^ + --output 信贷客户家庭关系导入模板.xlsx +echo 模板已下载: 信贷客户家庭关系导入模板.xlsx +echo. + +REM 步骤3: 测试导入接口(使用测试数据) +echo [3/6] 测试导入接口... +echo 创建测试Excel文件... + +REM 步骤4: 提交导入任务 +echo [4/6] 提交导入任务... +curl -s -X POST "%BASE_URL%/ccdi/custFmyRelation/importData" ^ + -H "Authorization: Bearer %TOKEN%" ^ + -F "file=@测试数据_信贷客户家庭关系.xlsx" ^ + > import_response.json + +echo 导入响应: +type import_response.json +echo. + +REM 提取taskId +for /f "tokens=2 delims=:\"" %%a in ('findstr /C:"\"taskId\"" import_response.json') do ( + set TASK_ID=%%a + goto :task_found +) +:task_found +echo 任务ID: %TASK_ID% +echo. + +REM 步骤5: 查询导入状态 +echo [5/6] 查询导入状态(等待3秒)... +timeout /t 3 /nobreak >nul + +curl -s -X GET "%BASE_URL%/ccdi/custFmyRelation/importStatus/%TASK_ID%" ^ + -H "Authorization: Bearer %TOKEN%" ^ + > status_response.json + +echo 导入状态: +type status_response.json +echo. + +REM 步骤6: 查询导入失败记录 +echo [6/6] 查询导入失败记录... +curl -s -X GET "%BASE_URL%/ccdi/custFmyRelation/importFailures/%TASK_ID%?pageNum=1&pageSize=10" ^ + -H "Authorization: Bearer %TOKEN%" ^ + > failures_response.json + +echo 失败记录: +type failures_response.json +echo. + +REM 测试查询接口 +echo [额外] 测试查询接口... +curl -s -X GET "%BASE_URL%/ccdi/custFmyRelation/list?pageNum=1&pageSize=10" ^ + -H "Authorization: Bearer %TOKEN%" ^ + > list_response.json + +echo 查询结果: +type list_response.json +echo. + +echo ======================================== +echo 测试完成! +echo ======================================== +echo. +echo 生成的文件: +echo - login_response.json (登录响应) +echo - import_response.json (导入响应) +echo - status_response.json (状态响应) +echo - failures_response.json (失败记录) +echo - list_response.json (查询结果) +echo - 信贷客户家庭关系导入模板.xlsx (导入模板) +echo. + +pause diff --git a/docs/plans/2026-02-11-cust-fmy-relation-import-alignment.md b/docs/plans/2026-02-11-cust-fmy-relation-import-alignment.md new file mode 100644 index 0000000..269be46 --- /dev/null +++ b/docs/plans/2026-02-11-cust-fmy-relation-import-alignment.md @@ -0,0 +1,373 @@ +# 信贷客户家庭关系导入功能对齐方案 + +## 概述 + +本文档描述了如何将**信贷客户家庭关系**功能的导入实现完全对齐到**员工亲属关系**的成熟模式。 + +**参考模板**: `CcdiStaffEnterpriseRelationImportServiceImpl` +**修改对象**: `CcdiCustFmyRelationImportServiceImpl` + +## 设计目标 + +1. 提升代码质量和可维护性 +2. 优化性能,避免 N+1 查询问题 +3. 改善用户体验,提供详细的导入进度和状态反馈 +4. 统一日志记录和错误处理机制 + +## 架构调整 + +### 1. 引入导入工具类 + +复用 `ImportLogUtils` 进行统一的日志记录: +- 导入开始/结束日志 +- 批量查询日志 +- 进度跟踪日志 +- 验证错误日志 +- 批量操作日志 + +### 2. Redis 状态管理升级 + +**现状**: 简单 String 值存储状态 +``` +"COMPLETED:10:5" +``` + +**优化**: Hash 结构存储详细状态 +```java +{ + "taskId": "uuid", + "status": "SUCCESS" | "PARTIAL_SUCCESS" | "PROCESSING", + "totalCount": 100, + "successCount": 95, + "failureCount": 5, + "progress": 100, + "startTime": 1234567890, + "endTime": 1234567900, + "message": "成功95条,失败5条" +} +``` + +- 过期时间: 7 天 +- 失败记录: 单独 Key, JSON 序列化, 7 天过期 + +### 3. 批量查询优化 + +**实现 `batchExistsByCombinations` 方法**: +- 提取所有 `person_id + relation_type + relation_cert_no` 组合 +- 一次性批量查询已存在的组合 +- 避免循环查询导致的 N+1 问题 + +### 4. 导入结果封装 + +创建/复用统一的 VO: +- `ImportStatusVO`: 导入状态详情 +- `ImportResultVO`: 导入提交结果 +- `CustFmyRelationImportFailureVO`: 失败记录详情 + +## 数据验证逻辑 + +### 唯一性检查 + +**优化前**: 每条记录单独查询 +```java +for (excel : excels) { + CcdiCustFmyRelation existing = mapper.selectExistingRelations(...); + // N 次数据库查询 +} +``` + +**优化后**: 批量查询 +```java +Set existingCombinations = getExistingCombinations(excels); +// 1 次数据库查询 + +for (excel : excels) { + String combination = excel.getPersonId() + "|" + ...; + if (existingCombinations.contains(combination)) { + throw new RuntimeException("该关系已存在"); + } +} +``` + +### Excel 内部重复检查 + +```java +Set processedCombinations = new HashSet<>(); + +for (excel : excels) { + String combination = ...; + + if (processedCombinations.contains(combination)) { + throw new RuntimeException("该关系在导入文件中重复"); + } + + processedCombinations.add(combination); +} +``` + +### 验证规则 + +**必填字段**: +- 信贷客户身份证号 +- 关系类型 +- 关系人姓名 +- 关系人证件类型 +- 关系人证件号码 + +**格式验证**: +- 身份证号: 18位有效格式 +- 证件号码: 根据证件类型验证 + +**长度限制**: +- 关系人姓名: ≤ 50 +- 关系类型: ≤ 20 +- 证件号码: ≤ 50 + +## 批量操作优化 + +### 分批插入策略 + +```java +private void saveBatch(List list, int batchSize) { + for (int i = 0; i < list.size(); i += batchSize) { + int end = Math.min(i + batchSize, list.size()); + List subList = list.subList(i, end); + mapper.insertBatch(subList); + } +} + +// 调用: 每 500 条为一批 +saveBatch(newRecords, 500); +``` + +### 批量操作日志 + +``` +开始批量插入: 总批次数=5, 每批大小=500 +批量插入完成: 成功=2500, 耗时=1234ms +``` + +## 失败记录处理 + +### 失败记录数据结构 + +```java +public class CustFmyRelationImportFailureVO { + private Integer rowNum; // Excel 行号 + private String personId; // 信贷客户身份证号 + private String relationType; // 关系类型 + private String relationName; // 关系人姓名 + private String errorMessage; // 错误消息 +} +``` + +### Redis 存储优化 + +**Key**: `import:custFmyRelation:{taskId}:failures` +**序列化**: JSON +**过期时间**: 7 天 +**反序列化**: +```java +return JSON.parseArray( + JSON.toJSONString(failuresObj), + CustFmyRelationImportFailureVO.class +); +``` + +## Controller 层调整 + +### 导入接口 + +```java +@PostMapping("/importData") +public AjaxResult importData(@RequestParam("file") MultipartFile file) { + List excels = + EasyExcelUtil.importExcel(file.getInputStream(), ...); + + if (excels == null || excels.isEmpty()) { + return error("至少需要一条数据"); + } + + String taskId = relationService.importRelations(excels); + + ImportResultVO result = new ImportResultVO(); + result.setTaskId(taskId); + result.setStatus("PROCESSING"); + result.setMessage("导入任务已提交,正在后台处理"); + + return AjaxResult.success("导入任务已提交,正在后台处理", result); +} +``` + +### 导入状态查询 + +```java +@GetMapping("/importStatus/{taskId}") +public AjaxResult getImportStatus(@PathVariable String taskId) { + ImportStatusVO statusVO = relationImportService.getImportStatus(taskId); + return success(statusVO); +} +``` + +### 失败记录查询 + +```java +@GetMapping("/importFailures/{taskId}") +public TableDataInfo getImportFailures( + @PathVariable String taskId, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize +) { + List failures = + relationImportService.getImportFailures(taskId); + + // 手动分页 + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, failures.size()); + + if (fromIndex >= failures.size()) { + return getDataTable(new ArrayList<>(), failures.size()); + } + + List pageData = + failures.subList(fromIndex, toIndex); + + return getDataTable(pageData, failures.size()); +} +``` + +## 导入模板改进 + +### 使用字典下拉框 + +```java +@PostMapping("/importTemplate") +public void importTemplate(HttpServletResponse response) { + EasyExcelUtil.importTemplateWithDictDropdown( + response, + CcdiCustFmyRelationExcel.class, + "信贷客户家庭关系" + ); +} +``` + +### Excel 实体注解增强 + +```java +@DictDropdown(type = "ccdi_relation_type") +private String relationType; + +@DictDropdown(type = "ccdi_cert_type") +private String relationCertType; +``` + +## 修改文件清单 + +### 1. Service 层 +- `CcdiCustFmyRelationImportServiceImpl.java` - 核心导入逻辑重构 +- `CcdiCustFmyRelationServiceImpl.java` - 导入入口方法调整 + +### 2. Controller 层 +- `CcdiCustFmyRelationController.java` - 接口返回值优化 + +### 3. Mapper 层 +- `CcdiCustFmyRelationMapper.java` - 添加批量查询方法 +- Mapper XML - 实现批量查询 SQL + +### 4. VO 类 +- 检查/创建 `ImportStatusVO.java` +- 检查/创建 `ImportResultVO.java` +- 优化 `CustFmyRelationImportFailureVO.java` + +### 5. Excel 实体 +- `CcdiCustFmyRelationExcel.java` - 添加字典注解 + +### 6. 工具类 +- 复用 `ImportLogUtils.java` +- 复用 `EasyExcelUtil.java` + +## 关键代码片段 + +### Mapper 批量查询 + +```java +// Mapper 接口 +List batchExistsByCombinations( + @Param("combinations") List combinations +); + +// XML 实现 + +``` + +### 异步导入方法 + +```java +@Async +@Transactional(rollbackFor = Exception.class) +public void importRelationsAsync( + List excels, + String taskId, + String userName // 新增参数,用于审计 +) { + // 实现逻辑... +} +``` + +## 实施步骤 + +1. **添加 Mapper 批量查询方法** + - 在 Mapper 接口添加 `batchExistsByCombinations` + - 在 XML 实现 SQL + +2. **重构 ImportServiceImpl** + - 引入 `ImportLogUtils` + - 实现批量查询逻辑 + - 添加 Excel 内部重复检查 + - 优化 Redis 状态管理 + - 改进失败记录存储 + +3. **创建/优化 VO 类** + - 检查并复用已有的 `ImportStatusVO` + - 检查并复用已有的 `ImportResultVO` + - 优化失败记录 VO + +4. **调整 Controller** + - 修改导入接口返回值 + - 优化状态查询接口 + - 优化失败记录查询接口 + +5. **更新 Excel 实体** + - 添加 `@DictDropdown` 注解 + +6. **测试验证** + - 单元测试 + - 集成测试 + - 性能对比测试 + +## 预期效果 + +### 性能提升 +- 批量查询: 从 N 次减少到 1 次 +- 导入 1000 条数据预计提升 50-70% + +### 用户体验 +- 实时进度反馈 +- 详细的错误信息 +- 清晰的成功/失败统计 + +### 代码质量 +- 统一的日志记录 +- 完善的错误处理 +- 更好的可维护性 + +## 创建日期 + +2026-02-11