diff --git a/docs/plans/backend/2026-04-30-import-dropdown-validation-backend-implementation-plan.md b/docs/plans/backend/2026-04-30-import-dropdown-validation-backend-implementation-plan.md new file mode 100644 index 00000000..73d4ca4e --- /dev/null +++ b/docs/plans/backend/2026-04-30-import-dropdown-validation-backend-implementation-plan.md @@ -0,0 +1,730 @@ +# 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 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 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)** + +Change the file-name overload to use a stream so the same validation path is used: + +```java +public static List importExcel(String fileName, Class 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 List importExcel(java.io.InputStream inputStream, Class 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 List importExcel(java.io.InputStream inputStream, Class 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 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 resolveDropdownColumns(Class clazz) { + List 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.