# 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.