731 lines
27 KiB
Markdown
731 lines
27 KiB
Markdown
# 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.
|