Files
ccdi/docs/plans/backend/2026-04-30-import-dropdown-validation-backend-implementation-plan.md

731 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Import Dropdown Validation 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:** 上传 Excel 导入文件时,所有 `@DictDropdown` 字段对应列必须保留模板下拉框;缺失下拉框立即报错并阻止导入。
**Architecture:**`EasyExcelUtil` 的公共读取入口统一增加模板结构校验,先用 POI 检查上传文件中对应 Sheet 的 `LIST` 数据验证覆盖情况,再交给 EasyExcel 执行现有数据读取。业务 Controller、异步导入、Redis 失败记录逻辑保持不变。
**Tech Stack:** Java 21, Spring Boot 3, EasyExcel, Apache POI 4.1.2, JUnit 5, Mockito.
---
## Project Notes
- 当前工作区已有未提交改动,包含 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java`。实施时必须先阅读当前 diff保留已有 `templateWriter(...).inMemory(Boolean.TRUE)` 改动,不要回滚用户或其他任务留下的内容。
- `.DS_Store` 忽略,不纳入任何暂存或提交。
- 本计划只涉及后端;不新增前端代码。
- 实现完成后必须新增实施记录:`docs/reports/implementation/2026-04-30-import-dropdown-validation-implementation.md`
## File Map
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java`
- 统一读取上传文件字节。
- 解析 `@DictDropdown` 字段。
- 使用 POI 校验 `LIST` 数据验证是否覆盖每个实际数据行。
- 校验通过后继续调用 EasyExcel 读取数据。
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilImportDropdownValidationTest.java`
- 覆盖缺失下拉框、非 LIST 验证、部分行覆盖、无字典字段绕过等工具层规则。
- Create: `docs/reports/implementation/2026-04-30-import-dropdown-validation-implementation.md`
- 记录本次实现、影响范围、测试命令、用户文件验证和真实页面验证结果。
- Reference only: `docs/plans/backend/2026-04-30-import-dropdown-validation-backend-design.md`
- 已审查通过的设计文档。
## Task 1: Add Failing Utility Tests
**Files:**
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilImportDropdownValidationTest.java`
- [ ] **Step 1: Create test class skeleton**
Add this file with package/imports and helper methods:
```java
package com.ruoyi.info.collection.utils;
import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.DataValidationHelper;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
```
Helper methods:
```java
private byte[] baseStaffWorkbook(boolean partyDropdown, boolean statusDropdown, boolean statusAsList, int statusLastRow) throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("员工信息");
Row header = sheet.createRow(0);
String[] headers = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)", "入职时间", "是否党员", "状态"};
for (int i = 0; i < headers.length; i++) {
header.createCell(i).setCellValue(headers[i]);
}
createBaseStaffRow(sheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1");
createBaseStaffRow(sheet, 2, "李四", 9020002L, "330106198603031022", "1", "1");
if (partyDropdown) {
addListValidation(sheet, 7, 1, 2, "0", "1");
}
if (statusDropdown) {
if (statusAsList) {
addListValidation(sheet, 8, 1, statusLastRow, "0", "1");
} else {
addIntegerValidation(sheet, 8, 1, 2);
}
}
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private byte[] baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown() throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Sheet staffSheet = workbook.createSheet("员工信息");
Row staffHeader = staffSheet.createRow(0);
String[] staffHeaders = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)", "入职时间", "是否党员", "状态"};
for (int i = 0; i < staffHeaders.length; i++) {
staffHeader.createCell(i).setCellValue(staffHeaders[i]);
}
createBaseStaffRow(staffSheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1");
addListValidation(staffSheet, 7, 1, 1, "0", "1");
addListValidation(staffSheet, 8, 1, 1, "0", "1");
Sheet assetSheet = workbook.createSheet("员工资产信息");
Row assetHeader = assetSheet.createRow(0);
String[] assetHeaders = {"员工身份证号*", "资产大类*", "资产小类*", "资产名称*", "产权占比", "购买/评估日期", "资产原值", "当前估值*", "估值截止日期", "资产状态*", "备注"};
for (int i = 0; i < assetHeaders.length; i++) {
assetHeader.createCell(i).setCellValue(assetHeaders[i]);
}
Row assetRow = assetSheet.createRow(1);
assetRow.createCell(0).setCellValue("33010619850202101X");
assetRow.createCell(1).setCellValue("房产");
assetRow.createCell(2).setCellValue("住宅");
assetRow.createCell(3).setCellValue("测试住宅");
assetRow.createCell(7).setCellValue(1000000D);
assetRow.createCell(9).setCellValue("正常");
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private void createBaseStaffRow(Sheet sheet, int rowIndex, String name, long staffId, String idCard, String partyMember, String status) {
Row row = sheet.createRow(rowIndex);
row.createCell(0).setCellValue(name);
row.createCell(1).setCellValue(staffId);
row.createCell(2).setCellValue(103L);
row.createCell(3, CellType.STRING).setCellValue(idCard);
row.createCell(4, CellType.STRING).setCellValue("13370000001");
row.createCell(5).setCellValue(new BigDecimal("180000").doubleValue());
row.createCell(6).setCellValue("2026-04-30");
row.createCell(7, CellType.STRING).setCellValue(partyMember);
row.createCell(8, CellType.STRING).setCellValue(status);
}
private void addListValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow, String... options) {
DataValidationHelper helper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = helper.createExplicitListConstraint(options);
DataValidation validation = helper.createValidation(
constraint,
new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex)
);
sheet.addValidationData(validation);
}
private void addIntegerValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow) {
DataValidationHelper helper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = helper.createIntegerConstraint(
DataValidationConstraint.OperatorType.BETWEEN,
"0",
"1"
);
DataValidation validation = helper.createValidation(
constraint,
new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex)
);
sheet.addValidationData(validation);
}
private byte[] plainWorkbookWithoutDropdown() throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("普通信息");
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("名称");
Row row = sheet.createRow(1);
row.createCell(0).setCellValue("张三");
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private static class PlainExcel {
@ExcelProperty(value = "名称", index = 0)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
```
- [ ] **Step 2: Add tests for accepted and rejected workbooks**
Add these tests:
```java
@Test
void importExcel_shouldPassWhenAllDictDropdownColumnsKeepListValidation() throws Exception {
byte[] bytes = baseStaffWorkbook(true, true, true, 2);
List<CcdiBaseStaffExcel> rows = EasyExcelUtil.importExcel(
new ByteArrayInputStream(bytes),
CcdiBaseStaffExcel.class,
"员工信息"
);
assertEquals(2, rows.size());
}
@Test
void importExcel_shouldFailWhenPartyMemberDropdownIsMissing() throws Exception {
byte[] bytes = baseStaffWorkbook(false, true, true, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertTrue(exception.getMessage().contains("是否党员 列缺少下拉框"));
}
@Test
void importExcel_shouldFailWhenStatusDropdownIsMissing() throws Exception {
byte[] bytes = baseStaffWorkbook(true, false, true, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertEquals("员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
}
@Test
void importExcel_shouldFailWhenValidationIsNotListType() throws Exception {
byte[] bytes = baseStaffWorkbook(true, true, false, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertTrue(exception.getMessage().contains("状态 列缺少下拉框"));
}
@Test
void importExcel_shouldFailWhenListValidationDoesNotCoverEveryActualDataRow() throws Exception {
byte[] bytes = baseStaffWorkbook(true, true, true, 1);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertTrue(exception.getMessage().contains("状态 列缺少下拉框"));
}
@Test
void importExcel_shouldFailWhenSecondSheetDropdownIsMissing() throws Exception {
byte[] bytes = baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown();
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffAssetInfoExcel.class, "员工资产信息")
);
assertEquals("员工资产信息 Sheet 的 资产状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
}
@Test
void importExcel_shouldSkipDropdownStructureValidationWhenClassHasNoDictDropdownFields() throws Exception {
byte[] bytes = plainWorkbookWithoutDropdown();
List<PlainExcel> rows = EasyExcelUtil.importExcel(
new ByteArrayInputStream(bytes),
PlainExcel.class,
"普通信息"
);
assertEquals(1, rows.size());
}
```
- [ ] **Step 3: Run the new tests and confirm they fail**
Run:
```bash
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilImportDropdownValidationTest test
```
Expected before implementation:
- Tests for missing dropdown, non-LIST validation, and partial coverage fail because no structure validation exists.
- The passing workbook test may pass already.
## Task 2: Implement Dropdown Structure Validation in EasyExcelUtil
**Files:**
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java`
- [ ] **Step 1: Add imports**
Add imports needed by the new helper methods:
```java
import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.exception.ServiceException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.util.CellRangeAddress;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Comparator;
```
Keep existing imports that are still used, including the current `ExcelWriterBuilder` import from the existing working tree.
- [ ] **Step 2: Update importExcel(String fileName, Class<T>)**
Change the file-name overload to use a stream so the same validation path is used:
```java
public static <T> List<T> importExcel(String fileName, Class<T> clazz) {
try (InputStream inputStream = java.nio.file.Files.newInputStream(java.nio.file.Path.of(fileName))) {
return importExcel(inputStream, clazz);
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
}
```
- [ ] **Step 3: Update InputStream import overloads**
Make both stream overloads read bytes once, validate, then pass a fresh `ByteArrayInputStream` to EasyExcel:
```java
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz) {
try {
byte[] bytes = inputStream.readAllBytes();
validateDictDropdownTemplate(bytes, clazz, null);
return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet().doReadSync();
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
}
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz, String sheetName) {
try {
byte[] bytes = inputStream.readAllBytes();
validateDictDropdownTemplate(bytes, clazz, sheetName);
return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet(sheetName).doReadSync();
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
}
```
- [ ] **Step 4: Add validation helpers**
Add these private helpers near the bottom of `EasyExcelUtil`, before `templateWriter(...)`:
```java
private static void validateDictDropdownTemplate(byte[] bytes, Class<?> clazz, String sheetName) {
List<DropdownColumn> dropdownColumns = resolveDropdownColumns(clazz);
if (dropdownColumns.isEmpty()) {
return;
}
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(bytes))) {
Sheet sheet = sheetName == null ? workbook.getSheetAt(0) : workbook.getSheet(sheetName);
if (sheet == null) {
return;
}
int lastDataRowIndex = findLastDataRowIndex(sheet);
if (lastDataRowIndex < 1) {
return;
}
for (DropdownColumn column : dropdownColumns) {
if (!isListValidationCovered(sheet, column.index(), lastDataRowIndex)) {
throw new ServiceException(sheet.getSheetName() + " Sheet 的 " + column.title()
+ " 列缺少下拉框,请下载最新导入模板填写后重新导入");
}
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
}
private static List<DropdownColumn> resolveDropdownColumns(Class<?> clazz) {
List<DropdownColumn> columns = new ArrayList<>();
Class<?> current = clazz;
while (current != null && current != Object.class) {
for (Field field : current.getDeclaredFields()) {
if (field.getAnnotation(DictDropdown.class) == null) {
continue;
}
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (excelProperty == null || excelProperty.index() < 0) {
continue;
}
columns.add(new DropdownColumn(excelProperty.index(), resolveColumnTitle(field, excelProperty)));
}
current = current.getSuperclass();
}
columns.sort(Comparator.comparingInt(DropdownColumn::index));
return columns;
}
private static String resolveColumnTitle(Field field, ExcelProperty excelProperty) {
if (excelProperty.value().length > 0 && excelProperty.value()[0] != null && !excelProperty.value()[0].isBlank()) {
return excelProperty.value()[0].replace("*", "");
}
return field.getName();
}
private static int findLastDataRowIndex(Sheet sheet) {
int lastDataRowIndex = -1;
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (hasData(row)) {
lastDataRowIndex = rowIndex;
}
}
return lastDataRowIndex;
}
private static boolean hasData(Row row) {
if (row == null || row.getLastCellNum() < 0) {
return false;
}
for (int cellIndex = row.getFirstCellNum(); cellIndex < row.getLastCellNum(); cellIndex++) {
if (cellIndex < 0) {
continue;
}
Cell cell = row.getCell(cellIndex);
if (cell != null && cell.toString() != null && !cell.toString().isBlank()) {
return true;
}
}
return false;
}
private static boolean isListValidationCovered(Sheet sheet, int columnIndex, int lastDataRowIndex) {
boolean[] coveredRows = new boolean[lastDataRowIndex + 1];
for (DataValidation validation : sheet.getDataValidations()) {
DataValidationConstraint constraint = validation.getValidationConstraint();
if (constraint == null || constraint.getValidationType() != DataValidationConstraint.ValidationType.LIST) {
continue;
}
for (CellRangeAddress address : validation.getRegions().getCellRangeAddresses()) {
if (address.getFirstColumn() > columnIndex || address.getLastColumn() < columnIndex) {
continue;
}
int firstRow = Math.max(1, address.getFirstRow());
int lastRow = Math.min(lastDataRowIndex, address.getLastRow());
for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
coveredRows[rowIndex] = true;
}
}
}
for (int rowIndex = 1; rowIndex <= lastDataRowIndex; rowIndex++) {
if (hasData(sheet.getRow(rowIndex)) && !coveredRows[rowIndex]) {
return false;
}
}
return true;
}
private record DropdownColumn(int index, String title) {}
```
Implementation notes:
- Do not add dictionary-value checks; this task only checks template dropdown structure.
- Do not make Controller-specific changes.
- Do not change existing async import status behavior.
- If `sheet == null`, preserve current EasyExcel failure path rather than inventing a new fallback.
- [ ] **Step 5: Run focused tests**
Run:
```bash
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilImportDropdownValidationTest test
```
Expected: all tests in the new class pass.
## Task 3: Regression Tests for Existing Template Generation
**Files:**
- Modify only if needed: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java`
- [ ] **Step 1: Run existing template tests**
Run:
```bash
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest test
```
Expected: existing template-generation tests pass. This confirms the validation change did not break dropdown generation and preserves the existing `inMemory(Boolean.TRUE)` template writer fix.
- [ ] **Step 2: Run controller tests that mock EasyExcelUtil**
Run:
```bash
mvn -pl ccdi-info-collection -am -Dtest=CcdiBaseStaffControllerTest,CcdiStaffFmyRelationControllerTest,CcdiAssetInfoControllerTest,CcdiBaseStaffAssetImportControllerTest test
```
Expected: controller tests pass. This confirms method signatures and import entry points did not change.
## Task 4: Verify User-Provided Broken Workbook
**Files:**
- No committed file changes.
- [ ] **Step 1: Confirm workbook structure manually**
Run:
```bash
python3 - <<'PY'
from openpyxl import load_workbook
path = '/Users/wkc/Desktop/ccdi/ccdi_bulk_20260430/员工信息维护导入模板_批量测试数据.xlsx'
wb = load_workbook(path)
for ws in wb.worksheets:
print(ws.title, len(ws.data_validations.dataValidation))
PY
```
Expected:
```text
员工信息 0
员工资产信息 0
```
- [ ] **Step 2: Validate through backend code path**
Use the real page upload in Task 6 as the authoritative backend-path validation. The expected page/API error is:
```text
员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入
```
Do not commit this user-provided workbook or any generated upload files.
## Task 5: Add Implementation Report
**Files:**
- Create: `docs/reports/implementation/2026-04-30-import-dropdown-validation-implementation.md`
- [ ] **Step 1: Write implementation report**
Create the report with this structure:
```markdown
# 导入模板下拉框结构校验实施记录
## 修改内容
-`EasyExcelUtil.importExcel(...)` 公共入口增加 `@DictDropdown` 列下拉框结构校验。
- 上传文件中对应 Sheet 的对应列必须由 `LIST` 类型数据验证覆盖每个实际数据行。
- 缺失下拉框时导入立即失败,不进入异步导入任务。
## 影响范围
- 影响所有使用 `EasyExcelUtil.importExcel(...)` 且导入对象含 `@DictDropdown` 字段的 Excel 导入。
- 不影响无 `@DictDropdown` 字段的导入。
- 不修改前端页面、业务字段校验、异步导入状态和失败记录逻辑。
## 验证结果
- `mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilImportDropdownValidationTest test`
- `mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest test`
- `mvn -pl ccdi-info-collection -am -Dtest=CcdiBaseStaffControllerTest,CcdiStaffFmyRelationControllerTest,CcdiAssetInfoControllerTest,CcdiBaseStaffAssetImportControllerTest test`
## 用户文件验证
- 文件:`/Users/wkc/Desktop/ccdi/ccdi_bulk_20260430/员工信息维护导入模板_批量测试数据.xlsx`
- 结果:上传后提示 `员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入`
## 真实页面验证
- 页面:员工信息维护真实业务页面。
- 结果:记录下载真实模板、上传缺下拉文件、上传保留下拉测试文件的页面验证结论。
```
- [ ] **Step 2: Fill actual verification output after tests**
Replace the verification bullets with actual pass/fail results after running commands and browser validation.
## Task 6: Real Page Validation With browser-use
**Files:**
- No source file changes unless verification uncovers a bug.
- Generated test files must stay under ignored output paths such as `output/browser-use/` or `output/spreadsheet/` and must not be committed.
- [ ] **Step 1: Use browser-use skill**
Before browser work, open `/Users/wkc/.codex/plugins/cache/openai-bundled/browser-use/0.1.0-alpha1/skills/browser/SKILL.md` and follow it.
- [ ] **Step 2: Start backend using project script if needed**
If no backend is running or code changes require restart, run:
```bash
sh bin/restart_java_backend.sh
```
Expected: backend available at `http://localhost:62318`.
- [ ] **Step 3: Start frontend with nvm if needed**
If no frontend is running, run:
```bash
cd ruoyi-ui
nvm use
npm run dev
```
Expected: frontend dev server URL printed by Vite/Vue CLI. Keep the process id/session so it can be stopped after testing.
- [ ] **Step 4: Test broken workbook on real page**
In the real employee information maintenance page:
1. Log in through the real app or `/login/test` shortcut if already used by the project.
2. Open employee information maintenance.
3. Upload `/Users/wkc/Desktop/ccdi/ccdi_bulk_20260430/员工信息维护导入模板_批量测试数据.xlsx`.
4. Confirm the page displays the backend error mentioning `状态 列缺少下拉框`.
Expected: no async import task is created for this upload.
- [ ] **Step 5: Test current real template still works**
1. Download the current import template from the real page.
2. Fill a small test workbook while preserving dropdown validations.
3. Upload it and confirm it enters the existing normal import chain.
4. Clean up any successfully imported test data.
Expected: dropdown validation does not block a valid template.
- [ ] **Step 6: Stop test processes**
Stop any backend/frontend process started during this task. Do not stop unrelated user-owned processes.
## Task 7: Final Verification and Commit Hygiene
**Files:**
- Modify: files from previous tasks only.
- [ ] **Step 1: Check worktree and staged state**
Run:
```bash
git status --short
git diff --cached --name-status
```
Expected: no staged unrelated files. `.DS_Store` remains ignored/uncommitted.
- [ ] **Step 2: Run final focused verification**
Run:
```bash
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilImportDropdownValidationTest,EasyExcelUtilTemplateTest,CcdiBaseStaffControllerTest,CcdiStaffFmyRelationControllerTest,CcdiAssetInfoControllerTest,CcdiBaseStaffAssetImportControllerTest test
```
Expected: all selected tests pass.
- [ ] **Step 3: Stage only this task's files**
Stage only these files:
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java
git add ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilImportDropdownValidationTest.java
git add docs/reports/implementation/2026-04-30-import-dropdown-validation-implementation.md
```
If `EasyExcelUtil.java` still contains unrelated pre-existing edits that should not be committed, use partial staging or stop and ask the user before committing.
- [ ] **Step 4: Review staged diff**
Run:
```bash
git diff --cached --name-status
git diff --cached --stat
```
Expected: only the implementation files and implementation report are staged.
- [ ] **Step 5: Commit if requested**
If the user wants a commit, use a Chinese message:
```bash
git commit -m "新增导入模板下拉框校验"
```
Expected: commit succeeds without `.DS_Store` or unrelated docs/files.