docs: 添加采购交易导入功能优化设计文档
设计目标: - 采用后台异步处理+通知提示,避免弹窗阻塞用户操作 - 完全复用员工信息维护的导入逻辑 - 支持查看导入失败记录 - 实现状态持久化 主要设计内容: - 整体架构和用户交互流程 - 前端组件结构和状态管理 - UI组件修改方案 - 核心方法实现(10个方法) - 完整修改清单和测试要点 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
839
doc/plans/2026-02-08-purchase-transaction-import-design.md
Normal file
839
doc/plans/2026-02-08-purchase-transaction-import-design.md
Normal file
@@ -0,0 +1,839 @@
|
|||||||
|
# 采购交易管理导入功能优化设计文档
|
||||||
|
|
||||||
|
## 文档信息
|
||||||
|
- **创建日期**: 2026-02-08
|
||||||
|
- **模块**: 采购交易管理
|
||||||
|
- **设计目标**: 优化导入功能,采用后台异步处理+通知提示,避免弹窗阻塞用户操作
|
||||||
|
- **参考方案**: 员工信息维护导入功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [需求概述](#需求概述)
|
||||||
|
2. [整体架构](#整体架构)
|
||||||
|
3. [前端组件结构](#前端组件结构)
|
||||||
|
4. [UI组件修改](#ui组件修改)
|
||||||
|
5. [核心方法实现](#核心方法实现)
|
||||||
|
6. [完整修改清单](#完整修改清单)
|
||||||
|
7. [测试要点](#测试要点)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
采购交易管理的导入功能采用同步处理方式,上传文件后需要等待导入完成,使用弹窗显示结果,阻塞用户操作。
|
||||||
|
|
||||||
|
### 优化目标
|
||||||
|
1. ✅ 采用后台异步处理,上传后立即关闭对话框
|
||||||
|
2. ✅ 使用右上角通知提示,不使用弹窗
|
||||||
|
3. ✅ 自动轮询导入状态,完成后通知用户
|
||||||
|
4. ✅ 支持查看导入失败记录
|
||||||
|
5. ✅ 状态持久化,刷新页面后仍可查看上次导入结果
|
||||||
|
|
||||||
|
### 设计原则
|
||||||
|
- **完全复用**员工信息维护的导入逻辑
|
||||||
|
- 保持一致的交互体验
|
||||||
|
- 最小化代码修改,复用已有组件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 整体架构
|
||||||
|
|
||||||
|
### 用户交互流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户点击"导入"按钮
|
||||||
|
↓
|
||||||
|
打开导入对话框
|
||||||
|
↓
|
||||||
|
选择Excel文件,点击"确定"
|
||||||
|
↓
|
||||||
|
上传文件到后端
|
||||||
|
↓
|
||||||
|
立即关闭导入对话框
|
||||||
|
↓
|
||||||
|
右上角显示通知:"导入任务已提交,正在后台处理中,处理完成后将通知您"
|
||||||
|
↓
|
||||||
|
系统后台每2秒轮询一次导入状态
|
||||||
|
↓
|
||||||
|
导入完成后,右上角显示结果通知:
|
||||||
|
- 全部成功: "导入完成!全部成功!共导入N条数据"
|
||||||
|
- 部分失败: "导入完成!成功N条,失败M条"
|
||||||
|
↓
|
||||||
|
如果有失败记录:
|
||||||
|
- 在页面操作栏显示"查看导入失败记录"按钮
|
||||||
|
- 带tooltip显示上次导入信息
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据存储策略
|
||||||
|
|
||||||
|
使用localStorage存储导入任务状态,实现状态持久化:
|
||||||
|
|
||||||
|
**存储Key**: `purchase_transaction_import_last_task`
|
||||||
|
|
||||||
|
**存储内容**:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
taskId: "task-20250206-123456789",
|
||||||
|
status: "SUCCESS", // PROCESSING/SUCCESS/FAILED
|
||||||
|
saveTime: 1707225600000,
|
||||||
|
hasFailures: true,
|
||||||
|
totalCount: 1000,
|
||||||
|
successCount: 980,
|
||||||
|
failureCount: 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**数据保留时间**: 7天(过期自动清除)
|
||||||
|
|
||||||
|
### 轮询机制
|
||||||
|
|
||||||
|
- **轮询间隔**: 2秒
|
||||||
|
- **最大轮询次数**: 150次(5分钟)
|
||||||
|
- **超时处理**: 显示"导入任务处理超时,请联系管理员"
|
||||||
|
- **状态检查**: 当status !== 'PROCESSING'时停止轮询
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端组件结构
|
||||||
|
|
||||||
|
### 新增data属性
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// ... 现有属性
|
||||||
|
|
||||||
|
// 导入轮询定时器
|
||||||
|
importPollingTimer: null,
|
||||||
|
|
||||||
|
// 是否显示查看失败记录按钮
|
||||||
|
showFailureButton: false,
|
||||||
|
|
||||||
|
// 当前导入任务ID
|
||||||
|
currentTaskId: null,
|
||||||
|
|
||||||
|
// 失败记录对话框
|
||||||
|
failureDialogVisible: false,
|
||||||
|
failureList: [],
|
||||||
|
failureLoading: false,
|
||||||
|
failureTotal: 0,
|
||||||
|
failureQueryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新增computed属性
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* 上次导入信息摘要
|
||||||
|
*/
|
||||||
|
lastImportInfo() {
|
||||||
|
const savedTask = this.getImportTaskFromStorage();
|
||||||
|
if (savedTask && savedTask.totalCount) {
|
||||||
|
return `导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}条`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生命周期钩子修改
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
created() {
|
||||||
|
this.getList();
|
||||||
|
this.restoreImportState(); // 新增:恢复导入状态
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
// 清理定时器
|
||||||
|
if (this.importPollingTimer) {
|
||||||
|
clearInterval(this.importPollingTimer);
|
||||||
|
this.importPollingTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 需要新增/修改的方法
|
||||||
|
|
||||||
|
| 方法名 | 类型 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| saveImportTaskToStorage | 新增 | 保存导入状态到localStorage |
|
||||||
|
| getImportTaskFromStorage | 新增 | 读取导入状态 |
|
||||||
|
| clearImportTaskFromStorage | 新增 | 清除导入状态 |
|
||||||
|
| restoreImportState | 新增 | 页面加载时恢复导入状态 |
|
||||||
|
| getLastImportTooltip | 新增 | 获取上次导入提示信息 |
|
||||||
|
| handleFileSuccess | 修改 | 上传成功后不弹窗,开始轮询 |
|
||||||
|
| startImportStatusPolling | 新增 | 开始轮询导入状态 |
|
||||||
|
| handleImportComplete | 新增 | 处理导入完成 |
|
||||||
|
| viewImportFailures | 新增 | 查看导入失败记录 |
|
||||||
|
| getFailureList | 新增 | 查询失败记录列表 |
|
||||||
|
| clearImportHistory | 新增 | 清除导入历史记录 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI组件修改
|
||||||
|
|
||||||
|
### 1. 修改导入对话框
|
||||||
|
|
||||||
|
**移除loading相关属性:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 修改前 -->
|
||||||
|
<el-dialog
|
||||||
|
:title="upload.title"
|
||||||
|
:visible.sync="upload.open"
|
||||||
|
width="400px"
|
||||||
|
append-to-body
|
||||||
|
@close="handleImportDialogClose"
|
||||||
|
v-loading="upload.isUploading"
|
||||||
|
element-loading-text="正在导入数据,请稍候..."
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(0, 0, 0, 0.7)"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 修改后 -->
|
||||||
|
<el-dialog
|
||||||
|
:title="upload.title"
|
||||||
|
:visible.sync="upload.open"
|
||||||
|
width="400px"
|
||||||
|
append-to-body
|
||||||
|
@close="handleImportDialogClose"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因**: 导入改为后台异步处理,不需要在对话框中显示loading。
|
||||||
|
|
||||||
|
### 2. 操作栏添加"查看导入失败记录"按钮
|
||||||
|
|
||||||
|
**位置**: 导入按钮和导出按钮之后
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-col :span="1.5" v-if="showFailureButton">
|
||||||
|
<el-tooltip
|
||||||
|
:content="getLastImportTooltip()"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-warning"
|
||||||
|
size="mini"
|
||||||
|
@click="viewImportFailures"
|
||||||
|
>查看导入失败记录</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-col>
|
||||||
|
```
|
||||||
|
|
||||||
|
**条件显示**: `v-if="showFailureButton"` - 仅当有失败记录时显示
|
||||||
|
|
||||||
|
### 3. 新增导入失败记录对话框
|
||||||
|
|
||||||
|
**位置**: 导入结果对话框之后
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-dialog
|
||||||
|
title="导入失败记录"
|
||||||
|
:visible.sync="failureDialogVisible"
|
||||||
|
width="1200px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
v-if="lastImportInfo"
|
||||||
|
:title="lastImportInfo"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 15px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table :data="failureList" v-loading="failureLoading">
|
||||||
|
<el-table-column label="采购事项ID" prop="purchaseId" align="center" />
|
||||||
|
<el-table-column label="项目名称" prop="projectName" align="center" :show-overflow-tooltip="true"/>
|
||||||
|
<el-table-column label="标的物名称" prop="subjectName" align="center" :show-overflow-tooltip="true"/>
|
||||||
|
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination
|
||||||
|
v-show="failureTotal > 0"
|
||||||
|
:total="failureTotal"
|
||||||
|
:page.sync="failureQueryParams.pageNum"
|
||||||
|
:limit.sync="failureQueryParams.pageSize"
|
||||||
|
@pagination="getFailureList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="failureDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button type="danger" plain @click="clearImportHistory">清除历史记录</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
**显示字段**:
|
||||||
|
- 采购事项ID (purchaseId)
|
||||||
|
- 项目名称 (projectName)
|
||||||
|
- 标的物名称 (subjectName)
|
||||||
|
- 失败原因 (errorMessage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心方法实现
|
||||||
|
|
||||||
|
### 1. handleFileSuccess - 上传成功处理
|
||||||
|
|
||||||
|
**修改说明**: 移除弹窗提示,改为通知+轮询
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
handleFileSuccess(response, file, fileList) {
|
||||||
|
this.upload.isUploading = false;
|
||||||
|
this.upload.open = false;
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
// 验证响应数据完整性
|
||||||
|
if (!response.data || !response.data.taskId) {
|
||||||
|
this.$modal.msgError('导入任务创建失败:缺少任务ID');
|
||||||
|
this.upload.isUploading = false;
|
||||||
|
this.upload.open = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskId = response.data.taskId;
|
||||||
|
|
||||||
|
// 清除旧的轮询定时器
|
||||||
|
if (this.importPollingTimer) {
|
||||||
|
clearInterval(this.importPollingTimer);
|
||||||
|
this.importPollingTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearImportTaskFromStorage();
|
||||||
|
|
||||||
|
// 保存新任务的初始状态
|
||||||
|
this.saveImportTaskToStorage({
|
||||||
|
taskId: taskId,
|
||||||
|
status: 'PROCESSING',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
hasFailures: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
this.showFailureButton = false;
|
||||||
|
this.currentTaskId = taskId;
|
||||||
|
|
||||||
|
// 显示后台处理提示(不是弹窗,是通知)
|
||||||
|
this.$notify({
|
||||||
|
title: '导入任务已提交',
|
||||||
|
message: '正在后台处理中,处理完成后将通知您',
|
||||||
|
type: 'info',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开始轮询检查状态
|
||||||
|
this.startImportStatusPolling(taskId);
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError(response.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. startImportStatusPolling - 轮询导入状态
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
startImportStatusPolling(taskId) {
|
||||||
|
let pollCount = 0;
|
||||||
|
const maxPolls = 150; // 最多轮询150次(5分钟)
|
||||||
|
|
||||||
|
this.importPollingTimer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
pollCount++;
|
||||||
|
|
||||||
|
// 超时检查
|
||||||
|
if (pollCount > maxPolls) {
|
||||||
|
clearInterval(this.importPollingTimer);
|
||||||
|
this.$modal.msgWarning('导入任务处理超时,请联系管理员');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getImportStatus(taskId);
|
||||||
|
|
||||||
|
if (response.data && response.data.status !== 'PROCESSING') {
|
||||||
|
clearInterval(this.importPollingTimer);
|
||||||
|
this.handleImportComplete(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearInterval(this.importPollingTimer);
|
||||||
|
this.$modal.msgError('查询导入状态失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}, 2000); // 每2秒轮询一次
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. handleImportComplete - 处理导入完成
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
handleImportComplete(statusResult) {
|
||||||
|
// 更新localStorage中的任务状态
|
||||||
|
this.saveImportTaskToStorage({
|
||||||
|
taskId: statusResult.taskId,
|
||||||
|
status: statusResult.status,
|
||||||
|
hasFailures: statusResult.failureCount > 0,
|
||||||
|
totalCount: statusResult.totalCount,
|
||||||
|
successCount: statusResult.successCount,
|
||||||
|
failureCount: statusResult.failureCount
|
||||||
|
});
|
||||||
|
|
||||||
|
if (statusResult.status === 'SUCCESS') {
|
||||||
|
// 全部成功
|
||||||
|
this.$notify({
|
||||||
|
title: '导入完成',
|
||||||
|
message: `全部成功!共导入${statusResult.totalCount}条数据`,
|
||||||
|
type: 'success',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
this.showFailureButton = false; // 成功时清除失败按钮显示
|
||||||
|
this.getList(); // 刷新列表
|
||||||
|
} else if (statusResult.failureCount > 0) {
|
||||||
|
// 部分失败
|
||||||
|
this.$notify({
|
||||||
|
title: '导入完成',
|
||||||
|
message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`,
|
||||||
|
type: 'warning',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示查看失败记录按钮
|
||||||
|
this.showFailureButton = true;
|
||||||
|
this.currentTaskId = statusResult.taskId;
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
this.getList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. localStorage状态管理
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 保存导入任务到localStorage
|
||||||
|
*/
|
||||||
|
saveImportTaskToStorage(taskData) {
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
...taskData,
|
||||||
|
saveTime: Date.now()
|
||||||
|
};
|
||||||
|
localStorage.setItem('purchase_transaction_import_last_task', JSON.stringify(data));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存导入任务状态失败:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从localStorage读取导入任务
|
||||||
|
*/
|
||||||
|
getImportTaskFromStorage() {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem('purchase_transaction_import_last_task');
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const task = JSON.parse(data);
|
||||||
|
|
||||||
|
// 数据格式校验
|
||||||
|
if (!task || !task.taskId) {
|
||||||
|
this.clearImportTaskFromStorage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间戳校验
|
||||||
|
if (task.saveTime && typeof task.saveTime !== 'number') {
|
||||||
|
this.clearImportTaskFromStorage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过期检查(7天)
|
||||||
|
const sevenDays = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
if (Date.now() - task.saveTime > sevenDays) {
|
||||||
|
this.clearImportTaskFromStorage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return task;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取导入任务状态失败:', error);
|
||||||
|
this.clearImportTaskFromStorage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除localStorage中的导入任务
|
||||||
|
*/
|
||||||
|
clearImportTaskFromStorage() {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('purchase_transaction_import_last_task');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除导入任务状态失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. restoreImportState - 恢复导入状态
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 恢复导入状态
|
||||||
|
* 在created()钩子中调用
|
||||||
|
*/
|
||||||
|
restoreImportState() {
|
||||||
|
const savedTask = this.getImportTaskFromStorage();
|
||||||
|
|
||||||
|
if (!savedTask) {
|
||||||
|
this.showFailureButton = false;
|
||||||
|
this.currentTaskId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有失败记录,恢复按钮显示
|
||||||
|
if (savedTask.hasFailures && savedTask.taskId) {
|
||||||
|
this.currentTaskId = savedTask.taskId;
|
||||||
|
this.showFailureButton = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. getLastImportTooltip - 获取导入提示
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 获取上次导入的提示信息
|
||||||
|
*/
|
||||||
|
getLastImportTooltip() {
|
||||||
|
const savedTask = this.getImportTaskFromStorage();
|
||||||
|
if (savedTask && savedTask.saveTime) {
|
||||||
|
const date = new Date(savedTask.saveTime);
|
||||||
|
const timeStr = this.parseTime(date, '{y}-{m}-{d} {h}:{i}');
|
||||||
|
return `上次导入: ${timeStr}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. viewImportFailures - 查看失败记录
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 查看导入失败记录
|
||||||
|
*/
|
||||||
|
viewImportFailures() {
|
||||||
|
this.failureDialogVisible = true;
|
||||||
|
this.getFailureList();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. getFailureList - 查询失败记录列表
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 查询失败记录列表
|
||||||
|
*/
|
||||||
|
getFailureList() {
|
||||||
|
this.failureLoading = true;
|
||||||
|
getImportFailures(
|
||||||
|
this.currentTaskId,
|
||||||
|
this.failureQueryParams.pageNum,
|
||||||
|
this.failureQueryParams.pageSize
|
||||||
|
).then(response => {
|
||||||
|
this.failureList = response.rows;
|
||||||
|
this.failureTotal = response.total;
|
||||||
|
this.failureLoading = false;
|
||||||
|
}).catch(error => {
|
||||||
|
this.failureLoading = false;
|
||||||
|
|
||||||
|
// 处理不同类型的错误
|
||||||
|
if (error.response) {
|
||||||
|
const status = error.response.status;
|
||||||
|
|
||||||
|
if (status === 404) {
|
||||||
|
// 记录不存在或已过期
|
||||||
|
this.$modal.msgWarning('导入记录已过期,无法查看失败记录');
|
||||||
|
this.clearImportTaskFromStorage();
|
||||||
|
this.showFailureButton = false;
|
||||||
|
this.currentTaskId = null;
|
||||||
|
this.failureDialogVisible = false;
|
||||||
|
} else if (status === 500) {
|
||||||
|
this.$modal.msgError('服务器错误,请稍后重试');
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError(`查询失败: ${error.response.data.msg || '未知错误'}`);
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求发送了但没有收到响应
|
||||||
|
this.$modal.msgError('网络连接失败,请检查网络');
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError('查询失败记录失败: ' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. clearImportHistory - 清除历史记录
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 清除导入历史记录
|
||||||
|
* 用户手动触发
|
||||||
|
*/
|
||||||
|
clearImportHistory() {
|
||||||
|
this.$confirm('确认清除上次导入记录?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.clearImportTaskFromStorage();
|
||||||
|
this.showFailureButton = false;
|
||||||
|
this.currentTaskId = null;
|
||||||
|
this.failureDialogVisible = false;
|
||||||
|
this.$message.success('已清除');
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整修改清单
|
||||||
|
|
||||||
|
### 需要修改的文件
|
||||||
|
|
||||||
|
**文件路径**: `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
|
||||||
|
|
||||||
|
### 具体修改项
|
||||||
|
|
||||||
|
#### 1. data()中新增属性
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在data()返回对象中添加:
|
||||||
|
importPollingTimer: null,
|
||||||
|
showFailureButton: false,
|
||||||
|
currentTaskId: null,
|
||||||
|
failureDialogVisible: false,
|
||||||
|
failureList: [],
|
||||||
|
failureLoading: false,
|
||||||
|
failureTotal: 0,
|
||||||
|
failureQueryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. computed中新增属性
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在computed中添加:
|
||||||
|
lastImportInfo() {
|
||||||
|
const savedTask = this.getImportTaskFromStorage();
|
||||||
|
if (savedTask && savedTask.totalCount) {
|
||||||
|
return `导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}条`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. created钩子
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在created()中添加:
|
||||||
|
this.restoreImportState();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. beforeDestroy钩子
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在beforeDestroy()中添加:
|
||||||
|
if (this.importPollingTimer) {
|
||||||
|
clearInterval(this.importPollingTimer);
|
||||||
|
this.importPollingTimer = null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. methods中新增方法
|
||||||
|
|
||||||
|
需要新增10个方法(见上文"核心方法实现"部分)
|
||||||
|
|
||||||
|
#### 6. 模板修改
|
||||||
|
|
||||||
|
- 导入对话框: 移除v-loading和element-loading-*属性
|
||||||
|
- 操作栏: 添加"查看导入失败记录"按钮
|
||||||
|
- 新增导入失败记录对话框
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试要点
|
||||||
|
|
||||||
|
### 1. 正常导入流程测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 点击"导入"按钮
|
||||||
|
2. 选择有效的Excel文件
|
||||||
|
3. 点击"确定"上传
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 导入对话框立即关闭
|
||||||
|
- ✅ 右上角显示通知:"导入任务已提交,正在后台处理中,处理完成后将通知您"
|
||||||
|
- ✅ 后台开始轮询状态(每2秒一次)
|
||||||
|
- ✅ 导入完成后,右上角显示结果通知
|
||||||
|
- ✅ 列表自动刷新,显示新导入的数据
|
||||||
|
|
||||||
|
### 2. 全部成功场景测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 上传包含100条有效数据的Excel文件
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 显示成功通知:"导入完成!全部成功!共导入100条数据"
|
||||||
|
- ✅ 不显示"查看导入失败记录"按钮
|
||||||
|
- ✅ 列表中显示100条新数据
|
||||||
|
|
||||||
|
### 3. 部分失败场景测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 上传包含部分错误数据的Excel文件
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 显示警告通知:"导入完成!成功80条,失败20条"
|
||||||
|
- ✅ 显示"查看导入失败记录"按钮
|
||||||
|
- ✅ 按钮tooltip显示上次导入信息
|
||||||
|
- ✅ 列表中显示80条成功导入的数据
|
||||||
|
|
||||||
|
### 4. 失败记录查看测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 导入有失败的数据
|
||||||
|
2. 点击"查看导入失败记录"按钮
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 打开失败记录对话框
|
||||||
|
- ✅ 顶部显示导入信息提示(总数、成功、失败)
|
||||||
|
- ✅ 表格显示失败记录,包含:
|
||||||
|
- 采购事项ID
|
||||||
|
- 项目名称
|
||||||
|
- 标的物名称
|
||||||
|
- 失败原因
|
||||||
|
- ✅ 支持分页查询
|
||||||
|
|
||||||
|
### 5. 状态持久化测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 导入有失败的数据
|
||||||
|
2. 刷新页面
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ "查看导入失败记录"按钮仍然显示
|
||||||
|
- ✅ tooltip显示正确的导入时间
|
||||||
|
- ✅ 点击按钮可以正常查看失败记录
|
||||||
|
|
||||||
|
### 6. 清除历史记录测试
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 打开失败记录对话框
|
||||||
|
2. 点击"清除历史记录"按钮
|
||||||
|
3. 确认清除
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ localStorage中的导入状态被清除
|
||||||
|
- ✅ "查看导入失败记录"按钮消失
|
||||||
|
- ✅ 失败记录对话框关闭
|
||||||
|
|
||||||
|
### 7. 边界情况测试
|
||||||
|
|
||||||
|
**测试场景**:
|
||||||
|
|
||||||
|
**a. 轮询超时**
|
||||||
|
- 测试方法: 模拟导入任务超过5分钟未完成
|
||||||
|
- 预期结果: 显示"导入任务处理超时,请联系管理员"
|
||||||
|
|
||||||
|
**b. 记录过期**
|
||||||
|
- 测试方法: 修改localStorage中的saveTime为8天前
|
||||||
|
- 预期结果: 自动清除过期记录,不显示"查看导入失败记录"按钮
|
||||||
|
|
||||||
|
**c. 网络错误**
|
||||||
|
- 测试方法: 断网后查询失败记录
|
||||||
|
- 预期结果: 显示"网络连接失败,请检查网络"
|
||||||
|
|
||||||
|
**d. 服务器错误(404)**
|
||||||
|
- 测试方法: 查询不存在的taskId的失败记录
|
||||||
|
- 预期结果: 显示"导入记录已过期,无法查看失败记录",自动清除状态
|
||||||
|
|
||||||
|
**e. 服务器错误(500)**
|
||||||
|
- 测试方法: 后端返回500错误
|
||||||
|
- 预期结果: 显示"服务器错误,请稍后重试"
|
||||||
|
|
||||||
|
### 8. 用户体验测试
|
||||||
|
|
||||||
|
**测试要点**:
|
||||||
|
- ✅ 导入过程中用户可以继续操作页面(不被阻塞)
|
||||||
|
- ✅ 通知消息清晰易懂
|
||||||
|
- ✅ 失败记录对话框字段对齐,支持长文本省略
|
||||||
|
- ✅ tooltip提示信息准确
|
||||||
|
- ✅ 分页功能正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录
|
||||||
|
|
||||||
|
### A. 与员工信息维护的差异对比
|
||||||
|
|
||||||
|
| 对比项 | 员工信息维护 | 采购交易管理 |
|
||||||
|
|--------|-------------|-------------|
|
||||||
|
| localStorage Key | `employee_import_last_task` | `purchase_transaction_import_last_task` |
|
||||||
|
| API路径 | `/ccdi/employee/importData` | `/ccdi/purchaseTransaction/importData` |
|
||||||
|
| 失败记录字段 | name, employeeId, idCard, phone, errorMessage | purchaseId, projectName, subjectName, errorMessage |
|
||||||
|
| 轮询超时时间 | 5分钟(150次×2秒) | 5分钟(150次×2秒) |
|
||||||
|
|
||||||
|
### B. 后端API依赖
|
||||||
|
|
||||||
|
本设计依赖以下后端API(已实现):
|
||||||
|
|
||||||
|
1. **导入数据**:
|
||||||
|
- 路径: `POST /ccdi/purchaseTransaction/importData`
|
||||||
|
- 参数: `updateSupport` (是否更新已存在数据)
|
||||||
|
- 响应: `{code: 200, data: {taskId: "task-xxx"}}`
|
||||||
|
|
||||||
|
2. **查询导入状态**:
|
||||||
|
- 路径: `GET /ccdi/purchaseTransaction/importStatus/{taskId}`
|
||||||
|
- 响应: `{code: 200, data: {taskId, status, totalCount, successCount, failureCount}}`
|
||||||
|
|
||||||
|
3. **查询导入失败记录**:
|
||||||
|
- 路径: `GET /ccdi/purchaseTransaction/importFailures/{taskId}`
|
||||||
|
- 参数: `pageNum`, `pageSize`
|
||||||
|
- 响应: `{code: 200, rows: [...], total: N}`
|
||||||
|
|
||||||
|
### C. 技术栈
|
||||||
|
|
||||||
|
- **前端框架**: Vue 2.6.12
|
||||||
|
- **UI组件库**: Element UI 2.15.14
|
||||||
|
- **HTTP客户端**: Axios 0.28.1
|
||||||
|
- **状态管理**: localStorage (浏览器原生API)
|
||||||
|
|
||||||
|
### D. 参考文档
|
||||||
|
|
||||||
|
- 员工信息维护导入功能: `ruoyi-ui/src/views/ccdiEmployee/index.vue`
|
||||||
|
- 采购交易API文档: `doc/api/ccdi_purchase_transaction_api.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 说明 | 作者 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 1.0.0 | 2026-02-08 | 初始设计文档 | Claude |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结语
|
||||||
|
|
||||||
|
本设计完全复用了员工信息维护的导入逻辑,实现了采购交易管理的后台异步导入功能。通过采用通知提示替代弹窗,避免了阻塞用户操作,提供了更好的用户体验。所有设计均已详细说明,可直接进入实施阶段。
|
||||||
BIN
doc/test-data/purchase_transaction/purchase_1770454949058.xlsx
Normal file
BIN
doc/test-data/purchase_transaction/purchase_1770454949058.xlsx
Normal file
Binary file not shown.
BIN
doc/test-data/purchase_transaction/purchase_test_data_2000.xlsx
Normal file
BIN
doc/test-data/purchase_transaction/purchase_test_data_2000.xlsx
Normal file
Binary file not shown.
@@ -0,0 +1,43 @@
|
|||||||
|
package com.ruoyi.ccdi.domain.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实体中介导入失败记录VO
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-02-06
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "实体中介导入失败记录")
|
||||||
|
public class IntermediaryEntityImportFailureVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "机构名称")
|
||||||
|
private String enterpriseName;
|
||||||
|
|
||||||
|
@Schema(description = "统一社会信用代码")
|
||||||
|
private String socialCreditCode;
|
||||||
|
|
||||||
|
@Schema(description = "主体类型")
|
||||||
|
private String enterpriseType;
|
||||||
|
|
||||||
|
@Schema(description = "企业性质")
|
||||||
|
private String enterpriseNature;
|
||||||
|
|
||||||
|
@Schema(description = "法定代表人")
|
||||||
|
private String legalRepresentative;
|
||||||
|
|
||||||
|
@Schema(description = "成立日期")
|
||||||
|
private Date establishDate;
|
||||||
|
|
||||||
|
@Schema(description = "错误信息")
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.ruoyi.ccdi.domain.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 个人中介导入失败记录VO
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-02-06
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "个人中介导入失败记录")
|
||||||
|
public class IntermediaryPersonImportFailureVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "姓名")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "证件号码")
|
||||||
|
private String personId;
|
||||||
|
|
||||||
|
@Schema(description = "人员类型")
|
||||||
|
private String personType;
|
||||||
|
|
||||||
|
@Schema(description = "性别")
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
@Schema(description = "手机号码")
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
@Schema(description = "所在公司")
|
||||||
|
private String company;
|
||||||
|
|
||||||
|
@Schema(description = "错误信息")
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.ruoyi.ccdi.service;
|
||||||
|
|
||||||
|
import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryEntityExcel;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.IntermediaryEntityImportFailureVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实体中介异步导入Service接口
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-02-06
|
||||||
|
*/
|
||||||
|
public interface ICcdiIntermediaryEntityImportService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步导入实体中介数据
|
||||||
|
*
|
||||||
|
* @param excelList Excel数据列表
|
||||||
|
* @param isUpdateSupport 是否更新已存在的数据
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param userName 当前用户名(用于审计字段)
|
||||||
|
*/
|
||||||
|
void importEntityAsync(List<CcdiIntermediaryEntityExcel> excelList,
|
||||||
|
Boolean isUpdateSupport,
|
||||||
|
String taskId,
|
||||||
|
String userName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询导入状态
|
||||||
|
*
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @return 导入状态信息
|
||||||
|
*/
|
||||||
|
ImportStatusVO getImportStatus(String taskId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导入失败记录
|
||||||
|
*
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @return 失败记录列表
|
||||||
|
*/
|
||||||
|
List<IntermediaryEntityImportFailureVO> getImportFailures(String taskId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.ruoyi.ccdi.service;
|
||||||
|
|
||||||
|
import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryPersonExcel;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.IntermediaryPersonImportFailureVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 个人中介异步导入Service接口
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-02-06
|
||||||
|
*/
|
||||||
|
public interface ICcdiIntermediaryPersonImportService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步导入个人中介数据
|
||||||
|
*
|
||||||
|
* @param excelList Excel数据列表
|
||||||
|
* @param isUpdateSupport 是否更新已存在的数据
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param userName 当前用户名(用于审计字段)
|
||||||
|
*/
|
||||||
|
void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
|
||||||
|
Boolean isUpdateSupport,
|
||||||
|
String taskId,
|
||||||
|
String userName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询导入状态
|
||||||
|
*
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @return 导入状态信息
|
||||||
|
*/
|
||||||
|
ImportStatusVO getImportStatus(String taskId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导入失败记录
|
||||||
|
*
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @return 失败记录列表
|
||||||
|
*/
|
||||||
|
List<IntermediaryPersonImportFailureVO> getImportFailures(String taskId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package com.ruoyi.ccdi.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.ruoyi.ccdi.domain.CcdiEnterpriseBaseInfo;
|
||||||
|
import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryEntityExcel;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.ImportResult;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.IntermediaryEntityImportFailureVO;
|
||||||
|
import com.ruoyi.ccdi.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||||
|
import com.ruoyi.ccdi.service.ICcdiIntermediaryEntityImportService;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实体中介异步导入Service实现
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-02-06
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@EnableAsync
|
||||||
|
public class CcdiIntermediaryEntityImportServiceImpl implements ICcdiIntermediaryEntityImportService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiEnterpriseBaseInfoMapper entityMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Async
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void importEntityAsync(List<CcdiIntermediaryEntityExcel> excelList,
|
||||||
|
Boolean isUpdateSupport,
|
||||||
|
String taskId,
|
||||||
|
String userName) {
|
||||||
|
List<CcdiEnterpriseBaseInfo> newRecords = new ArrayList<>();
|
||||||
|
List<CcdiEnterpriseBaseInfo> updateRecords = new ArrayList<>();
|
||||||
|
List<IntermediaryEntityImportFailureVO> failures = new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. 批量查询已存在的统一社会信用代码
|
||||||
|
Set<String> existingCreditCodes = getExistingCreditCodes(excelList);
|
||||||
|
|
||||||
|
// 2. 分类数据
|
||||||
|
for (int i = 0; i < excelList.size(); i++) {
|
||||||
|
CcdiIntermediaryEntityExcel excel = excelList.get(i);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证数据
|
||||||
|
validateEntityData(excel, isUpdateSupport, existingCreditCodes);
|
||||||
|
|
||||||
|
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
|
||||||
|
BeanUtils.copyProperties(excel, entity);
|
||||||
|
|
||||||
|
// 设置数据来源和审计字段
|
||||||
|
entity.setDataSource("IMPORT");
|
||||||
|
entity.setEntSource("INTERMEDIARY");
|
||||||
|
entity.setCreatedBy(userName);
|
||||||
|
|
||||||
|
if (existingCreditCodes.contains(excel.getSocialCreditCode())) {
|
||||||
|
if (isUpdateSupport) {
|
||||||
|
// 更新模式:设置更新人
|
||||||
|
entity.setUpdatedBy(userName);
|
||||||
|
updateRecords.add(entity);
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("该统一社会信用代码已存在");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newRecords.add(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
IntermediaryEntityImportFailureVO failure = new IntermediaryEntityImportFailureVO();
|
||||||
|
BeanUtils.copyProperties(excel, failure);
|
||||||
|
failure.setErrorMessage(e.getMessage());
|
||||||
|
failures.add(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 批量插入新数据
|
||||||
|
if (!newRecords.isEmpty()) {
|
||||||
|
saveBatch(newRecords, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 批量更新已有数据(先删除再插入)
|
||||||
|
if (!updateRecords.isEmpty() && isUpdateSupport) {
|
||||||
|
// 先批量删除已存在的记录
|
||||||
|
List<String> creditCodes = updateRecords.stream()
|
||||||
|
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> deleteWrapper = new LambdaQueryWrapper<>();
|
||||||
|
deleteWrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, creditCodes);
|
||||||
|
entityMapper.delete(deleteWrapper);
|
||||||
|
|
||||||
|
// 批量插入更新后的数据
|
||||||
|
entityMapper.insertBatch(updateRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 保存失败记录到Redis
|
||||||
|
if (!failures.isEmpty()) {
|
||||||
|
String failuresKey = "import:intermediary-entity:" + taskId + ":failures";
|
||||||
|
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 更新最终状态
|
||||||
|
ImportResult result = new ImportResult();
|
||||||
|
result.setTotalCount(excelList.size());
|
||||||
|
result.setSuccessCount(newRecords.size() + updateRecords.size());
|
||||||
|
result.setFailureCount(failures.size());
|
||||||
|
|
||||||
|
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
|
||||||
|
updateImportStatus(taskId, finalStatus, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImportStatusVO getImportStatus(String taskId) {
|
||||||
|
String key = "import:intermediary-entity:" + taskId;
|
||||||
|
Boolean hasKey = redisTemplate.hasKey(key);
|
||||||
|
|
||||||
|
if (Boolean.FALSE.equals(hasKey)) {
|
||||||
|
throw new RuntimeException("任务不存在或已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Object, Object> statusMap = redisTemplate.opsForHash().entries(key);
|
||||||
|
|
||||||
|
ImportStatusVO statusVO = new ImportStatusVO();
|
||||||
|
statusVO.setTaskId((String) statusMap.get("taskId"));
|
||||||
|
statusVO.setStatus((String) statusMap.get("status"));
|
||||||
|
statusVO.setTotalCount((Integer) statusMap.get("totalCount"));
|
||||||
|
statusVO.setSuccessCount((Integer) statusMap.get("successCount"));
|
||||||
|
statusVO.setFailureCount((Integer) statusMap.get("failureCount"));
|
||||||
|
statusVO.setProgress((Integer) statusMap.get("progress"));
|
||||||
|
statusVO.setStartTime((Long) statusMap.get("startTime"));
|
||||||
|
statusVO.setEndTime((Long) statusMap.get("endTime"));
|
||||||
|
statusVO.setMessage((String) statusMap.get("message"));
|
||||||
|
|
||||||
|
return statusVO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IntermediaryEntityImportFailureVO> getImportFailures(String taskId) {
|
||||||
|
String key = "import:intermediary-entity:" + taskId + ":failures";
|
||||||
|
Object failuresObj = redisTemplate.opsForValue().get(key);
|
||||||
|
|
||||||
|
if (failuresObj == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parseArray(JSON.toJSONString(failuresObj), IntermediaryEntityImportFailureVO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询已存在的统一社会信用代码
|
||||||
|
*/
|
||||||
|
private Set<String> getExistingCreditCodes(List<CcdiIntermediaryEntityExcel> excelList) {
|
||||||
|
List<String> creditCodes = excelList.stream()
|
||||||
|
.map(CcdiIntermediaryEntityExcel::getSocialCreditCode)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (creditCodes.isEmpty()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, creditCodes);
|
||||||
|
List<CcdiEnterpriseBaseInfo> existingEntities = entityMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
return existingEntities.stream()
|
||||||
|
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量保存
|
||||||
|
*/
|
||||||
|
private void saveBatch(List<CcdiEnterpriseBaseInfo> list, int batchSize) {
|
||||||
|
// 使用真正的批量插入,分批次执行以提高性能
|
||||||
|
for (int i = 0; i < list.size(); i += batchSize) {
|
||||||
|
int end = Math.min(i + batchSize, list.size());
|
||||||
|
List<CcdiEnterpriseBaseInfo> subList = list.subList(i, end);
|
||||||
|
entityMapper.insertBatch(subList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新导入状态
|
||||||
|
*/
|
||||||
|
private void updateImportStatus(String taskId, String status, ImportResult result) {
|
||||||
|
String key = "import:intermediary-entity:" + taskId;
|
||||||
|
Map<String, Object> statusData = new HashMap<>();
|
||||||
|
statusData.put("status", status);
|
||||||
|
statusData.put("successCount", result.getSuccessCount());
|
||||||
|
statusData.put("failureCount", result.getFailureCount());
|
||||||
|
statusData.put("progress", 100);
|
||||||
|
statusData.put("endTime", System.currentTimeMillis());
|
||||||
|
|
||||||
|
if ("SUCCESS".equals(status)) {
|
||||||
|
statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据");
|
||||||
|
} else {
|
||||||
|
statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "条");
|
||||||
|
}
|
||||||
|
|
||||||
|
redisTemplate.opsForHash().putAll(key, statusData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证实体中介数据
|
||||||
|
*
|
||||||
|
* @param excel Excel数据
|
||||||
|
* @param isUpdateSupport 是否支持更新
|
||||||
|
* @param existingCreditCodes 已存在的统一社会信用代码集合
|
||||||
|
*/
|
||||||
|
private void validateEntityData(CcdiIntermediaryEntityExcel excel,
|
||||||
|
Boolean isUpdateSupport,
|
||||||
|
Set<String> existingCreditCodes) {
|
||||||
|
// 验证必填字段:机构名称
|
||||||
|
if (StringUtils.isEmpty(excel.getEnterpriseName())) {
|
||||||
|
throw new RuntimeException("机构名称不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必填字段:统一社会信用代码
|
||||||
|
if (StringUtils.isEmpty(excel.getSocialCreditCode())) {
|
||||||
|
throw new RuntimeException("统一社会信用代码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果统一社会信用代码已存在但未启用更新支持,抛出异常
|
||||||
|
if (existingCreditCodes.contains(excel.getSocialCreditCode()) && !isUpdateSupport) {
|
||||||
|
throw new RuntimeException("该统一社会信用代码已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果统一社会信用代码不存在,检查唯一性
|
||||||
|
if (!existingCreditCodes.contains(excel.getSocialCreditCode())) {
|
||||||
|
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(CcdiEnterpriseBaseInfo::getSocialCreditCode, excel.getSocialCreditCode());
|
||||||
|
if (entityMapper.selectCount(wrapper) > 0) {
|
||||||
|
throw new RuntimeException("该统一社会信用代码已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package com.ruoyi.ccdi.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.ruoyi.ccdi.domain.CcdiBizIntermediary;
|
||||||
|
import com.ruoyi.ccdi.domain.excel.CcdiIntermediaryPersonExcel;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.ImportResult;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.IntermediaryPersonImportFailureVO;
|
||||||
|
import com.ruoyi.ccdi.mapper.CcdiBizIntermediaryMapper;
|
||||||
|
import com.ruoyi.ccdi.service.ICcdiIntermediaryPersonImportService;
|
||||||
|
import com.ruoyi.common.utils.IdCardUtil;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 个人中介异步导入Service实现
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
* @date 2026-02-06
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@EnableAsync
|
||||||
|
public class CcdiIntermediaryPersonImportServiceImpl implements ICcdiIntermediaryPersonImportService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiBizIntermediaryMapper intermediaryMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Async
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
|
||||||
|
Boolean isUpdateSupport,
|
||||||
|
String taskId,
|
||||||
|
String userName) {
|
||||||
|
List<CcdiBizIntermediary> newRecords = new ArrayList<>();
|
||||||
|
List<CcdiBizIntermediary> updateRecords = new ArrayList<>();
|
||||||
|
List<IntermediaryPersonImportFailureVO> failures = new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. 批量查询已存在的证件号
|
||||||
|
Set<String> existingPersonIds = getExistingPersonIds(excelList);
|
||||||
|
|
||||||
|
// 2. 分类数据
|
||||||
|
for (int i = 0; i < excelList.size(); i++) {
|
||||||
|
CcdiIntermediaryPersonExcel excel = excelList.get(i);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证数据
|
||||||
|
validatePersonData(excel, isUpdateSupport, existingPersonIds);
|
||||||
|
|
||||||
|
CcdiBizIntermediary intermediary = new CcdiBizIntermediary();
|
||||||
|
BeanUtils.copyProperties(excel, intermediary);
|
||||||
|
|
||||||
|
// 设置数据来源和审计字段
|
||||||
|
intermediary.setDataSource("IMPORT");
|
||||||
|
intermediary.setCreatedBy(userName);
|
||||||
|
|
||||||
|
if (existingPersonIds.contains(excel.getPersonId())) {
|
||||||
|
if (isUpdateSupport) {
|
||||||
|
// 更新模式:设置更新人
|
||||||
|
intermediary.setUpdatedBy(userName);
|
||||||
|
updateRecords.add(intermediary);
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("该证件号码已存在");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newRecords.add(intermediary);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO();
|
||||||
|
BeanUtils.copyProperties(excel, failure);
|
||||||
|
failure.setErrorMessage(e.getMessage());
|
||||||
|
failures.add(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 保存失败记录到Redis
|
||||||
|
if (!failures.isEmpty()) {
|
||||||
|
String failuresKey = "import:intermediary:" + taskId + ":failures";
|
||||||
|
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 更新最终状态
|
||||||
|
ImportResult result = new ImportResult();
|
||||||
|
result.setTotalCount(excelList.size());
|
||||||
|
result.setSuccessCount(newRecords.size() + updateRecords.size());
|
||||||
|
result.setFailureCount(failures.size());
|
||||||
|
|
||||||
|
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
|
||||||
|
updateImportStatus(taskId, finalStatus, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImportStatusVO getImportStatus(String taskId) {
|
||||||
|
String key = "import:intermediary:" + taskId;
|
||||||
|
Boolean hasKey = redisTemplate.hasKey(key);
|
||||||
|
|
||||||
|
if (Boolean.FALSE.equals(hasKey)) {
|
||||||
|
throw new RuntimeException("任务不存在或已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Object, Object> statusMap = redisTemplate.opsForHash().entries(key);
|
||||||
|
|
||||||
|
ImportStatusVO statusVO = new ImportStatusVO();
|
||||||
|
statusVO.setTaskId((String) statusMap.get("taskId"));
|
||||||
|
statusVO.setStatus((String) statusMap.get("status"));
|
||||||
|
statusVO.setTotalCount((Integer) statusMap.get("totalCount"));
|
||||||
|
statusVO.setSuccessCount((Integer) statusMap.get("successCount"));
|
||||||
|
statusVO.setFailureCount((Integer) statusMap.get("failureCount"));
|
||||||
|
statusVO.setProgress((Integer) statusMap.get("progress"));
|
||||||
|
statusVO.setStartTime((Long) statusMap.get("startTime"));
|
||||||
|
statusVO.setEndTime((Long) statusMap.get("endTime"));
|
||||||
|
statusVO.setMessage((String) statusMap.get("message"));
|
||||||
|
|
||||||
|
return statusVO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IntermediaryPersonImportFailureVO> getImportFailures(String taskId) {
|
||||||
|
String key = "import:intermediary:" + taskId + ":failures";
|
||||||
|
Object failuresObj = redisTemplate.opsForValue().get(key);
|
||||||
|
|
||||||
|
if (failuresObj == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parseArray(JSON.toJSONString(failuresObj), IntermediaryPersonImportFailureVO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询已存在的证件号
|
||||||
|
*/
|
||||||
|
private Set<String> getExistingPersonIds(List<CcdiIntermediaryPersonExcel> excelList) {
|
||||||
|
List<String> personIds = excelList.stream()
|
||||||
|
.map(CcdiIntermediaryPersonExcel::getPersonId)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (personIds.isEmpty()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.in(CcdiBizIntermediary::getPersonId, personIds);
|
||||||
|
List<CcdiBizIntermediary> existingIntermediaries = intermediaryMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
return existingIntermediaries.stream()
|
||||||
|
.map(CcdiBizIntermediary::getPersonId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量保存
|
||||||
|
*/
|
||||||
|
private void saveBatch(List<CcdiBizIntermediary> list, int batchSize) {
|
||||||
|
// 使用真正的批量插入,分批次执行以提高性能
|
||||||
|
for (int i = 0; i < list.size(); i += batchSize) {
|
||||||
|
int end = Math.min(i + batchSize, list.size());
|
||||||
|
List<CcdiBizIntermediary> subList = list.subList(i, end);
|
||||||
|
intermediaryMapper.insertBatch(subList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新导入状态
|
||||||
|
*/
|
||||||
|
private void updateImportStatus(String taskId, String status, ImportResult result) {
|
||||||
|
String key = "import:intermediary:" + taskId;
|
||||||
|
Map<String, Object> statusData = new HashMap<>();
|
||||||
|
statusData.put("status", status);
|
||||||
|
statusData.put("successCount", result.getSuccessCount());
|
||||||
|
statusData.put("failureCount", result.getFailureCount());
|
||||||
|
statusData.put("progress", 100);
|
||||||
|
statusData.put("endTime", System.currentTimeMillis());
|
||||||
|
|
||||||
|
if ("SUCCESS".equals(status)) {
|
||||||
|
statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据");
|
||||||
|
} else {
|
||||||
|
statusData.put("message", "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "条");
|
||||||
|
}
|
||||||
|
|
||||||
|
redisTemplate.opsForHash().putAll(key, statusData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证个人中介数据
|
||||||
|
*
|
||||||
|
* @param excel Excel数据
|
||||||
|
* @param isUpdateSupport 是否支持更新
|
||||||
|
* @param existingPersonIds 已存在的证件号集合
|
||||||
|
*/
|
||||||
|
private void validatePersonData(CcdiIntermediaryPersonExcel excel,
|
||||||
|
Boolean isUpdateSupport,
|
||||||
|
Set<String> existingPersonIds) {
|
||||||
|
// 验证必填字段:姓名
|
||||||
|
if (StringUtils.isEmpty(excel.getName())) {
|
||||||
|
throw new RuntimeException("姓名不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必填字段:证件号码
|
||||||
|
if (StringUtils.isEmpty(excel.getPersonId())) {
|
||||||
|
throw new RuntimeException("证件号码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证证件号码格式
|
||||||
|
String idCardError = IdCardUtil.getErrorMessage(excel.getPersonId());
|
||||||
|
if (idCardError != null) {
|
||||||
|
throw new RuntimeException("证件号码" + idCardError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果证件号已存在但未启用更新支持,抛出异常
|
||||||
|
if (existingPersonIds.contains(excel.getPersonId()) && !isUpdateSupport) {
|
||||||
|
throw new RuntimeException("该证件号码已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果证件号不存在,检查唯一性
|
||||||
|
if (!existingPersonIds.contains(excel.getPersonId())) {
|
||||||
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(CcdiBizIntermediary::getPersonId, excel.getPersonId());
|
||||||
|
if (intermediaryMapper.selectCount(wrapper) > 0) {
|
||||||
|
throw new RuntimeException("该证件号码已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user