# 员工导入结果跨页面持久化设计文档 **创建日期**: 2026-02-06 **设计者**: Claude Code **状态**: 已确认 **关联文档**: [员工信息异步导入功能设计文档](./2026-02-06-employee-async-import-design.md) --- ## 一、需求概述 ### 1.1 背景 当前员工信息异步导入功能存在问题: - 导入开始后,切换到其他菜单再返回,无法查看上一次的导入结果 - `showFailureButton`、`currentTaskId` 等状态变量存储在组件内存中,页面切换后丢失 ### 1.2 目标 - 实现导入结果的跨页面持久化 - 用户可以在切换菜单后仍然查看上一次的导入失败记录 - 仅保留最近一次导入记录,下次导入时自动清除旧数据 - 依赖Redis的7天TTL机制自动清理过期数据 ### 1.3 核心决策 - **存储方案**: localStorage(前端持久化) - **保留范围**: 仅最后一次导入记录 - **过期策略**: 依赖Redis TTL(7天),前端校验时间戳 - **清除时机**: 下次导入开始时自动清除旧数据 --- ## 二、技术方案 ### 2.1 整体设计 采用 **前端localStorage持久化** 方案: ``` 用户上传Excel ↓ 清除localStorage旧数据 → 保存新taskId ↓ 开始轮询查询状态 ↓ 导入完成 → 更新localStorage状态 ↓ 用户切换菜单 → 组件销毁 ↓ 用户返回页面 → created()钩子 ↓ 从localStorage读取 → 恢复按钮显示状态 ↓ 用户点击查看失败记录 → 正常查询 ``` **核心优势**: - 无需后端改动,完全前端实现 - 简单可靠,利用浏览器原生存储 - 用户体验流畅,状态不丢失 ### 2.2 数据结构设计 **localStorage存储格式**: ```javascript // key: 'employee_import_last_task' { taskId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', status: 'SUCCESS' | 'PARTIAL_SUCCESS' | 'FAILED' | 'PROCESSING', timestamp: 1707225900000, saveTime: 1707225900000, hasFailures: true, totalCount: 100, successCount: 95, failureCount: 5 } ``` **字段说明**: - `taskId`: 导入任务唯一标识 - `status`: 导入状态 - `timestamp`: 导入完成时间戳 - `saveTime`: 保存到localStorage的时间戳(用于过期校验) - `hasFailures`: 是否有失败记录 - `totalCount/successCount/failureCount`: 导入统计信息 --- ## 三、前端实现设计 ### 3.1 新增工具方法 **文件**: `ruoyi-ui/src/views/ccdiEmployee/index.vue` ```javascript methods: { /** * 保存导入任务到localStorage * @param {Object} taskData - 任务数据 */ saveImportTaskToStorage(taskData) { try { const data = { ...taskData, saveTime: Date.now() }; localStorage.setItem('employee_import_last_task', JSON.stringify(data)); } catch (error) { console.error('保存导入任务状态失败:', error); } }, /** * 从localStorage读取导入任务 * @returns {Object|null} 任务数据或null */ getImportTaskFromStorage() { try { const data = localStorage.getItem('employee_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('employee_import_last_task'); } catch (error) { console.error('清除导入任务状态失败:', error); } }, /** * 恢复导入状态 * 在created()钩子中调用 */ async 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; } }, /** * 获取上次导入的提示信息 * @returns {String} 提示文本 */ getLastImportTooltip() { const savedTask = this.getImportTaskFromStorage(); if (savedTask && savedTask.timestamp) { const date = new Date(savedTask.timestamp); const timeStr = this.parseTime(date, '{y}-{m}-{d} {h}:{i}'); return `上次导入: ${timeStr}`; } return ''; }, /** * 清除导入历史记录 * 用户手动触发 */ clearImportHistory() { this.$confirm('确认清除上次导入记录?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { this.clearImportTaskFromStorage(); this.showFailureButton = false; this.currentTaskId = null; this.failureDialogVisible = false; this.$message.success('已清除'); }).catch(() => {}); } } ``` ### 3.2 生命周期钩子修改 ```javascript created() { this.getList(); this.getDeptTree(); this.restoreImportState(); // 新增:恢复导入状态 } ``` ### 3.3 导入成功处理修改 ```javascript handleFileSuccess(response, file, fileList) { this.upload.isUploading = false; this.upload.open = false; if (response.code === 200) { const taskId = response.data.taskId; // 清除旧的导入记录(防止并发) if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer = 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); } } ``` ### 3.4 导入完成处理修改 ```javascript handleImportComplete(statusResult) { const hasFailures = statusResult.failureCount > 0; // 更新localStorage中的任务状态 this.saveImportTaskToStorage({ taskId: statusResult.taskId, status: statusResult.status, timestamp: Date.now(), hasFailures: hasFailures, totalCount: statusResult.totalCount, successCount: statusResult.successCount, failureCount: statusResult.failureCount }); if (statusResult.status === 'SUCCESS') { this.$notify({ title: '导入完成', message: `全部成功!共导入${statusResult.totalCount}条数据`, type: 'success', duration: 5000 }); this.getList(); } else if (hasFailures) { this.$notify({ title: '导入完成', message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`, type: 'warning', duration: 5000 }); // 显示查看失败记录按钮 this.showFailureButton = true; this.currentTaskId = statusResult.taskId; // 刷新列表 this.getList(); } } ``` ### 3.5 失败记录查询增强 ```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); } }); } ``` ### 3.6 新增计算属性 ```javascript computed: { /** * 上次导入信息摘要 */ lastImportInfo() { const savedTask = this.getImportTaskFromStorage(); if (savedTask && savedTask.totalCount) { return `导入时间: ${this.parseTime(savedTask.timestamp)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}条`; } return ''; } } ``` ### 3.7 模板修改 **失败记录按钮**: ```vue 查看上次导入失败记录 ``` **失败记录对话框**: ```vue ``` --- ## 四、用户体验流程 ### 4.1 典型场景 **场景1: 导入成功无失败** 1. 用户上传Excel文件 2. 导入成功,显示通知"全部成功!共导入100条数据" 3. 刷新页面或切换菜单后返回 4. **预期**: 不显示"查看导入失败记录"按钮 **场景2: 导入有失败记录** 1. 用户上传有错误数据的Excel文件 2. 导入完成,显示通知"成功95条,失败5条" 3. 显示"查看导入失败记录"按钮 4. 用户切换到其他菜单 5. 用户返回员工管理页面 6. **预期**: 按钮仍然存在,点击可查看失败记录 **场景3: 导入中切换页面** 1. 用户上传Excel文件 2. 后台开始处理,用户立即切换菜单 3. 用户返回员工管理页面 4. **预期**: 如有失败,显示按钮并可查看 **场景4: Redis数据过期** 1. 导入完成,有失败记录 2. 7天后用户点击"查看导入失败记录" 3. 后端返回404错误 4. **预期**: 前端提示"导入记录已过期,无法查看失败记录",并清除localStorage数据,隐藏按钮 **场景5: 新导入覆盖旧记录** 1. 已有上一次的导入失败记录 2. 用户上传新的Excel文件 3. **预期**: 旧记录被立即清除,新导入的结果覆盖localStorage --- ## 五、错误处理与边界情况 ### 5.1 localStorage异常 | 异常情况 | 处理方式 | |---------|---------| | localStorage被禁用 | try-catch捕获,console.error记录,功能降级但不报错 | | 数据损坏(非JSON格式) | try-catch捕获,清除损坏数据,返回null | | 数据格式不完整 | 校验必要字段,清除无效数据 | | 时间戳异常 | 校验类型,清除无效数据 | ### 5.2 API请求失败 | 错误类型 | HTTP状态码 | 处理方式 | |---------|-----------|---------| | 记录不存在或已过期 | 404 | 提示用户"记录已过期",清除localStorage,隐藏按钮 | | 服务器内部错误 | 500 | 提示"服务器错误,请稍后重试" | | 网络连接失败 | 无响应 | 提示"网络连接失败,请检查网络" | | 其他错误 | 其他 | 显示具体错误信息 | ### 5.3 并发导入处理 - 新导入开始时,立即清除旧的localStorage数据 - 清除旧的轮询定时器(如果有) - 防止状态混乱 ### 5.4 浏览器兼容性 localStorage在所有现代浏览器中都得到支持: - Chrome 4+ - Firefox 3.5+ - Safari 4+ - IE 8+ - Edge(所有版本) ### 5.5 存储空间限制 - localStorage通常有5-10MB限制 - 本功能仅存储一个JSON对象(约200字节),远低于限制 - 不需要考虑存储空间问题 --- ## 六、测试策略 ### 6.1 功能测试 | 测试用例 | 步骤 | 预期结果 | |---------|------|---------| | 导入成功无失败-刷新 | 上传正确Excel → 等待完成 → 刷新页面 | 不显示失败记录按钮 | | 导入有失败-刷新 | 上传有错误Excel → 等待完成 → 刷新页面 | 显示按钮,可查看失败记录 | | 导入有失败-切换菜单 | 上传有错误Excel → 等待完成 → 切换菜单 → 返回 | 显示按钮,可查看失败记录 | | 导入中切换页面 | 上传Excel → 立即切换菜单 → 返回 | 状态正常,如有失败显示按钮 | | 新导入覆盖 | 有旧记录 → 上传新Excel → 等待完成 | 显示新导入的按钮,旧记录清除 | | 手动清除记录 | 有失败记录 → 点击"清除历史记录" | 按钮隐藏,localStorage清空 | | Redis过期模拟 | 修改localStorage时间戳为8天前 → 打开页面 | 自动清除数据,不显示按钮 | | API 404处理 | 有失败记录 → Mock后端返回404 | 提示过期,清除数据,隐藏按钮 | ### 6.2 边界测试 | 测试用例 | 预期结果 | |---------|---------| | localStorage被禁用 | 功能正常,不报错,仅不持久化 | | localStorage数据手动篡改 | 自动检测并清除,恢复正常 | | 连续快速多次导入 | 最后一次导入的状态为准 | | 浏览器关闭后重新打开 | localStorage数据保留,状态恢复 | ### 6.3 浏览器兼容性测试 测试目标浏览器: - Chrome(最新版) - Firefox(最新版) - Edge(最新版) - Safari(如适用) ### 6.4 性能测试 | 指标 | 目标 | |------|------| | localStorage读取时间 | < 10ms | | localStorage写入时间 | < 10ms | | 页面加载恢复时间 | < 50ms | | 内存占用增加 | 可忽略(约200字节) | --- ## 七、实施检查清单 ### 7.1 代码实现 - [ ] 新增 `saveImportTaskToStorage()` 方法 - [ ] 新增 `getImportTaskFromStorage()` 方法 - [ ] 新增 `clearImportTaskFromStorage()` 方法 - [ ] 新增 `restoreImportState()` 方法 - [ ] 新增 `getLastImportTooltip()` 方法 - [ ] 新增 `clearImportHistory()` 方法 - [ ] 新增 `lastImportInfo` 计算属性 - [ ] 修改 `created()` 钩子,调用 `restoreImportState()` - [ ] 修改 `handleFileSuccess()` 方法 - [ ] 修改 `handleImportComplete()` 方法 - [ ] 修改 `getFailureList()` 方法 - [ ] 修改模板,添加tooltip和清除按钮 ### 7.2 测试 - [ ] 导入成功无失败-刷新页面测试 - [ ] 导入有失败-刷新页面测试 - [ ] 导入有失败-切换菜单测试 - [ ] 导入中切换页面测试 - [ ] 新导入覆盖旧记录测试 - [ ] 手动清除记录测试 - [ ] Redis过期处理测试 - [ ] API 404错误处理测试 - [ ] localStorage异常处理测试 - [ ] 浏览器兼容性测试 ### 7.3 文档 - [ ] 更新 `doc/api/ccdi-employee-import-api.md` (如有需要) - [ ] 更新用户手册(如需要) --- ## 八、风险与限制 ### 8.1 风险 | 风险 | 影响 | 缓解措施 | |------|------|---------| | localStorage被禁用 | 无法持久化 | 功能降级,不影响基本使用 | | 用户清除浏览器数据 | 记录丢失 | 符合预期,无负面影响 | | 多标签页并发导入 | 状态可能不一致 | 新导入会覆盖旧数据,可接受 | ### 8.2 限制 1. **仅保留最后一次导入记录** - 设计决策,符合用户需求 - 需要查看历史记录可考虑后续扩展 2. **依赖Redis TTL** - 7天后Redis数据自动删除 - 前端有7天时间戳校验,但以Redis为准 3. **单浏览器本地存储** - 不同浏览器不共享状态 - 换设备后无法查看(符合预期) --- ## 九、未来扩展方向 ### 9.1 可能的增强功能 1. **历史导入记录列表** - 后端新增导入记录表 - 支持查询所有历史导入 - 按时间倒序展示 2. **跨设备同步** - 使用后端存储导入记录 - 用户登录后同步导入状态 3. **导入结果导出** - 支持导出失败记录为Excel - 便于用户修正后重新导入 4. **导入统计可视化** - 展示导入成功率趋势 - 常见错误类型统计 --- ## 十、相关文件清单 ### 10.1 修改文件 - `ruoyi-ui/src/views/ccdiEmployee/index.vue` - 员工管理页面 ### 10.2 关联文档 - `doc/plans/2026-02-06-employee-async-import-design.md` - 员工信息异步导入功能设计文档 - `doc/api/ccdi-employee-import-api.md` - 员工导入API文档 --- ## 附录 ### A. localStorage Key命名规范 ``` employee_import_last_task // 员工导入最后一次任务 ``` 命名格式: `{模块}_{功能}_{用途}` ### B. 相关接口 | 接口 | 方法 | 说明 | |------|------|------| | /ccdi/employee/importData | POST | 提交导入任务 | | /ccdi/employee/importStatus/{taskId} | GET | 查询导入状态 | | /ccdi/employee/importFailures/{taskId} | GET | 查询失败记录 | --- **文档版本**: 1.0 **最后更新**: 2026-02-06