新增招聘信息双Sheet导入实施计划

This commit is contained in:
wkc
2026-04-23 09:45:50 +08:00
parent ff9627d0d9
commit 110817abba
3 changed files with 603 additions and 0 deletions

View File

@@ -0,0 +1,320 @@
# Staff Recruitment Dual-Sheet Import Backend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将招聘信息管理后端导入链路收口为“招聘信息 + 历史工作经历”双 Sheet 单任务模式,并补齐失败 Sheet、失败行号、失败原因。
**Architecture:** 保留现有 `ccdi:staffRecruitment:*` 权限、`/ccdi/staffRecruitment/*` 路径和现有主从表结构,只收口控制器、服务接口和异步导入编排。导入任务先处理主信息 Sheet再按 `recruitId` 分组处理工作经历 Sheet工作经历匹配“本次主 Sheet 成功数据 + 数据库已有主信息”,若数据库中已存在旧工作经历则直接整组失败,不做覆盖。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis-Plus, EasyExcel, Redis, JUnit 5, Mockito
---
## File Map
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java`
- 收口双 Sheet 模板下载与统一导入入口,移除独立工作经历导入接口
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java`
- 暴露统一的双 Sheet 提交入口
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java`
- 暴露统一的双 Sheet 异步导入入口
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java`
- 初始化统一 Redis 任务状态并提交统一异步任务
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java`
- 实现主 Sheet 与工作经历 Sheet 两阶段编排、行号上下文、已有工作经历报错规则
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java`
- 新增 `sheetName``sheetRowNum`
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java`
- 锁定控制器与接口的双 Sheet 契约
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java`
- 锁定异步导入编排、已有工作经历报错和失败定位行为
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java`
- 补模板双 Sheet 约束回归
- Create: `docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md`
- 记录最终实施内容、验证结果与浏览器实测结论
### Task 1: 锁定双 Sheet 导入接口契约
**Files:**
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java`
- [ ] **Step 1: 写控制器与接口契约失败测试**
```java
@Test
void shouldExposeSingleDualSheetImportEntry() throws Exception {
String controller = Files.readString(
Path.of("src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java")
);
assertTrue(controller.contains("\"招聘信息\""));
assertTrue(controller.contains("\"历史工作经历\""));
assertFalse(controller.contains("workImportTemplate"));
assertFalse(controller.contains("importWorkData"));
}
```
- [ ] **Step 2: 运行测试确认失败**
Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest test`
Expected: FAIL提示控制器仍存在 `workImportTemplate` / `importWorkData` 或接口签名仍为双入口
- [ ] **Step 3: 最小化修改控制器与服务接口**
```java
String importRecruitment(
List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList
);
void importRecruitmentAsync(
List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList,
String taskId,
String userName
);
```
- [ ] **Step 4: 重跑契约测试**
Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest test`
Expected: PASS控制器仅保留双 Sheet 模板与单导入入口
- [ ] **Step 5: 提交这一小步**
```bash
git add \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java
git commit -m "收口招聘双Sheet导入接口"
```
### Task 2: 接入双 Sheet 模板与失败 VO 字段
**Files:**
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java`
- [ ] **Step 1: 先补失败 VO 与模板契约测试**
```java
assertHasField(
"com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO",
"sheetName"
);
assertHasField(
"com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO",
"sheetRowNum"
);
```
- [ ] **Step 2: 运行测试确认失败**
Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest,EasyExcelUtilTemplateTest test`
Expected: FAIL提示 `RecruitmentImportFailureVO` 缺字段或模板断言未通过
- [ ] **Step 3: 最小化实现字段与模板导出**
```java
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiStaffRecruitmentExcel.class,
"招聘信息",
CcdiStaffRecruitmentWorkExcel.class,
"历史工作经历",
"招聘信息管理导入模板"
);
```
- [ ] **Step 4: 重跑测试**
Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest,EasyExcelUtilTemplateTest test`
Expected: PASS模板输出双 Sheet失败 VO 具备新字段
- [ ] **Step 5: 提交这一小步**
```bash
git add \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java
git commit -m "补齐招聘双Sheet模板与失败字段"
```
### Task 3: 收口服务层任务初始化与统一状态统计
**Files:**
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java`
- [ ] **Step 1: 先写服务层统一任务初始化失败测试**
```java
@Test
void shouldInitializeSingleRedisTaskForTwoSheets() throws Exception {
String service = Files.readString(
Path.of("src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java")
);
assertTrue(service.contains("recruitmentList.size() + workList.size()"));
assertFalse(service.contains("importRecruitmentWork("));
}
```
- [ ] **Step 2: 运行测试确认失败**
Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest test`
Expected: FAIL提示服务层仍保留独立工作经历任务初始化
- [ ] **Step 3: 实现统一提交入口**
```java
public String importRecruitment(
List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList
) {
int totalCount = recruitmentList.size() + workList.size();
recruitmentImportService.importRecruitmentAsync(recruitmentList, workList, taskId, userName);
return taskId;
}
```
- [ ] **Step 4: 重跑契约测试**
Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest test`
Expected: PASS任务总数按双 Sheet 合并统计,接口只剩统一入口
- [ ] **Step 5: 提交这一小步**
```bash
git add \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java
git commit -m "统一招聘双Sheet任务初始化"
```
### Task 4: 实现异步导入两阶段编排与失败定位
**Files:**
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java`
- [ ] **Step 1: 先写异步导入失败测试**
```java
@Test
void shouldFailWholeWorkGroupWhenExistingHistoryExists() {
// arrange: recruitmentWorkMapper.countByRecruitId("RC001") -> 1
// act: importRecruitmentAsync(mainRows, workRows, taskId, "admin")
// assert: recruitmentWorkMapper.insert(...) not called
// assert: failure.sheetName == "历史工作经历"
// assert: failure.sheetRowNum == "2"
}
```
- [ ] **Step 2: 运行测试确认失败**
Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentImportServiceImplTest test`
Expected: FAIL提示当前实现仍会删除旧工作经历或未记录 `sheetName` / `sheetRowNum`
- [ ] **Step 3: 以最小改动实现两阶段编排**
```java
List<MainImportRow> indexedMainRows = buildMainImportRows(recruitmentList);
List<WorkImportRow> indexedWorkRows = buildWorkImportRows(workList);
Map<String, CcdiStaffRecruitment> importedRecruitmentMap = importMainSheet(indexedMainRows, failures, userName);
importWorkSheet(indexedWorkRows, importedRecruitmentMap, failures, userName);
```
- [ ] **Step 4: 补充工作经历“已有旧记录即失败”与行号上下文**
```java
if (hasExistingWorkHistory(recruitId)) {
throw buildValidationException(
"历史工作经历",
extractWorkRowNums(workRows),
String.format("招聘记录编号[%s]已存在历史工作经历,不允许重复导入", recruitId)
);
}
```
- [ ] **Step 5: 重跑测试**
Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentImportServiceImplTest test`
Expected: PASS已有工作经历不覆盖失败记录带 Sheet 与行号
- [ ] **Step 6: 提交这一小步**
```bash
git add \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java
git commit -m "实现招聘双Sheet异步导入编排"
```
### Task 5: 做后端回归、补实施记录并交付联调入口
**Files:**
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java`
- Create: `docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md`
- [ ] **Step 1: 运行后端定向测试**
Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest,CcdiStaffRecruitmentImportServiceImplTest,EasyExcelUtilTemplateTest test`
Expected: PASS双 Sheet 契约、异步编排、模板约束全部通过
- [ ] **Step 2: 运行模块编译**
Run: `mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile`
Expected: BUILD SUCCESS
- [ ] **Step 3: 启动后端供前端联调**
Run: `sh bin/restart_java_backend.sh`
Expected: 后端正常重启,`/ccdi/staffRecruitment/importTemplate``/importData` 可访问
- [ ] **Step 4: 补实施记录**
```md
- 导入入口收口为双 Sheet 单任务
- 工作经历导入改为“已有旧记录时报错”
- 失败记录补齐失败 Sheet、失败行号、失败原因
- 已完成后端编译与定向测试
```
- [ ] **Step 5: 提交后端收尾**
```bash
git add \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java \
docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md
git commit -m "完成招聘双Sheet导入后端改造"
```

View File

@@ -0,0 +1,264 @@
# Staff Recruitment Dual-Sheet Import Frontend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将招聘信息管理前端导入交互改为单入口双 Sheet 模式,统一任务轮询与失败弹窗,并在失败列表中展示失败 Sheet、失败行号、失败原因。
**Architecture:** 前端只保留一个导入按钮和一个上传弹窗,统一使用 `/ccdi/staffRecruitment/importTemplate``/importData`。页面本地状态从“按导入类型区分任务”收口为“按唯一任务 ID 轮询”,失败记录统一通过一个弹窗展示,并用 `sheetName``sheetRowNum` 区分失败来源。
**Tech Stack:** Vue 2, Element UI, axios request wrapper, Node 14.21.3 via nvm, source-inspection unit tests, Playwright browser validation
---
## File Map
- Modify: `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
- 删除独立“导入工作经历”入口,收口上传弹窗、轮询状态和失败列表
- Modify: `ruoyi-ui/src/api/ccdiStaffRecruitment.js`
- 去掉独立工作经历导入模板/上传调用,保留统一导入 API
- Create: `ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js`
- 锁定顶部工具栏已收口为单入口
- Create: `ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js`
- 锁定统一任务状态与轮询逻辑
- Create: `ruoyi-ui/tests/unit/staff-recruitment-import-failure-dialog.test.js`
- 锁定失败弹窗列定义与 `sheetRowNum` 展示格式
- Modify: `docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md`
- 追加前端改造与真实页面验证结果
### Task 1: 收口工具栏与上传 API
**Files:**
- Create: `ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js`
- Modify: `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
- Modify: `ruoyi-ui/src/api/ccdiStaffRecruitment.js`
- [ ] **Step 1: 先写工具栏与 API 契约失败测试**
```js
[
"handleImport()",
'"/ccdi/staffRecruitment/importData"',
"招聘信息管理导入模板"
].forEach((token) => {
assert(source.includes(token), `招聘导入入口缺少统一双Sheet能力: ${token}`)
})
[
"handleWorkImport",
"importWorkData",
"workImportTemplate"
].forEach((token) => {
assert(!source.includes(token), `招聘页不应继续保留独立工作经历导入: ${token}`)
})
```
- [ ] **Step 2: 运行测试确认失败**
Run: `node ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js`
Expected: FAIL提示页面仍保留“导入工作经历”按钮或 API 仍存在旧入口
- [ ] **Step 3: 最小化修改页面与 API**
```js
export function importTemplate() {
return request({
url: "/ccdi/staffRecruitment/importTemplate",
method: "post"
})
}
```
- [ ] **Step 4: 重跑测试**
Run: `node ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js`
Expected: PASS页面只剩一个导入入口API 只调用统一模板与上传接口
- [ ] **Step 5: 提交这一小步**
```bash
git add \
ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue \
ruoyi-ui/src/api/ccdiStaffRecruitment.js \
ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js
git commit -m "收口招聘双Sheet导入前端入口"
```
### Task 2: 收口上传弹窗文案与统一任务状态
**Files:**
- Create: `ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js`
- Modify: `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
- [ ] **Step 1: 先写统一状态失败测试**
```js
[
"模板包含“招聘信息”和“历史工作经历”两个 Sheet。",
"this.currentTaskId = taskId",
"this.showFailureButton = false",
"this.startImportStatusPolling(taskId)"
].forEach((token) => {
assert(source.includes(token), `招聘导入状态未统一到单任务: ${token}`)
})
[
"currentImportType",
"upload.importType",
"getImportTypeLabel"
].forEach((token) => {
assert(!source.includes(token), `招聘导入状态不应再按类型拆分: ${token}`)
})
```
- [ ] **Step 2: 运行测试确认失败**
Run: `node ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js`
Expected: FAIL提示页面仍保留类型切换状态
- [ ] **Step 3: 最小化实现统一轮询状态**
```js
this.saveImportTaskToStorage({
taskId,
status: "PROCESSING",
hasFailures: false
})
this.currentTaskId = taskId
this.startImportStatusPolling(taskId)
```
- [ ] **Step 4: 重跑测试**
Run: `node ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js`
Expected: PASS弹窗文案改为双 Sheet页面状态只围绕一个任务 ID 轮询
- [ ] **Step 5: 提交这一小步**
```bash
git add \
ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue \
ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js
git commit -m "统一招聘双Sheet导入轮询状态"
```
### Task 3: 调整统一失败弹窗列定义
**Files:**
- Create: `ruoyi-ui/tests/unit/staff-recruitment-import-failure-dialog.test.js`
- Modify: `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
- [ ] **Step 1: 先写失败弹窗失败测试**
```js
[
'label="失败Sheet"',
'label="失败行号"',
"scope.row.sheetName",
"scope.row.sheetRowNum",
"失败原因"
].forEach((token) => {
assert(source.includes(token), `招聘失败弹窗缺少双Sheet定位列: ${token}`)
})
```
- [ ] **Step 2: 运行测试确认失败**
Run: `node ruoyi-ui/tests/unit/staff-recruitment-import-failure-dialog.test.js`
Expected: FAIL提示弹窗仍按旧类型列展示
- [ ] **Step 3: 实现统一失败表格**
```vue
<el-table-column label="失败Sheet" prop="sheetName" align="center" width="140" />
<el-table-column label="失败行号" prop="sheetRowNum" align="center" width="120">
<template slot-scope="scope">
<span>{{ scope.row.sheetRowNum ? `${scope.row.sheetRowNum}` : "-" }}</span>
</template>
</el-table-column>
```
- [ ] **Step 4: 重跑测试**
Run: `node ruoyi-ui/tests/unit/staff-recruitment-import-failure-dialog.test.js`
Expected: PASS失败弹窗明确展示失败 Sheet、失败行号、失败原因
- [ ] **Step 5: 提交这一小步**
```bash
git add \
ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue \
ruoyi-ui/tests/unit/staff-recruitment-import-failure-dialog.test.js
git commit -m "完善招聘双Sheet失败弹窗展示"
```
### Task 4: 做前端构建、真实页面验证与实施记录
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
- Modify: `ruoyi-ui/src/api/ccdiStaffRecruitment.js`
- Modify: `docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md`
- [ ] **Step 1: 切换 Node 版本并执行前端静态回归**
Run: `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js && node ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js && node ruoyi-ui/tests/unit/staff-recruitment-import-failure-dialog.test.js`
Expected: PASS三个静态契约测试全部通过
- [ ] **Step 2: 执行前端构建**
Run: `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod`
Expected: BUILD SUCCESS
- [ ] **Step 3: 启动真实页面并做浏览器验证**
Run:
```bash
source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null
cd ruoyi-ui
npm run dev -- --port 8080
```
Expected: 前端开发服务启动成功,真实页面 `http://localhost:8080` 可访问
Playwright 验证最少覆盖:
- 进入真实 `招聘信息管理` 页面,不使用 prototype 页面
- 从页面下载双 Sheet 模板
- 只导 `招聘信息` Sheet
- 只导 `历史工作经历` Sheet
- 双 Sheet 同时导入
- 已存在工作经历时报错
- 失败弹窗显示 `失败Sheet / 失败行号 / 失败原因`
- [ ] **Step 4: 补前端实施记录**
```md
- 页面导入入口收口为一个按钮
- 上传弹窗提示调整为双 Sheet 文案
- 页面状态收口为单任务轮询
- 失败弹窗新增失败 Sheet、失败行号、失败原因
- 已完成真实页面 Playwright 验证
```
- [ ] **Step 5: 关闭测试进程并提交前端收尾**
Run: 关闭本轮 `npm run dev` 与后端联调用到的进程,确保无残留端口占用
```bash
git add \
ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue \
ruoyi-ui/src/api/ccdiStaffRecruitment.js \
ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js \
ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js \
ruoyi-ui/tests/unit/staff-recruitment-import-failure-dialog.test.js \
docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md
git commit -m "完成招聘双Sheet导入前端改造"
```