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

27 KiB
Raw Blame History

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:

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:

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:

@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:

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:

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:

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:

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(...):

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:

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:

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:

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:

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:

员工信息 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:

员工信息 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:

# 导入模板下拉框结构校验实施记录

## 修改内容

-`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:

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:

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:

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:

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:

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:

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:

git commit -m "新增导入模板下拉框校验"

Expected: commit succeeds without .DS_Store or unrelated docs/files.