浏览代码

fix: 企业导入实现与优化

zhangying 3 周之前
父节点
当前提交
d13774e6f1

+ 683 - 0
.docs/250615-导入功能前后端代码实现与使用规范.md

@@ -0,0 +1,683 @@
+# Excel 导入功能前后端代码实现与使用规范
+
+> 本文档基于项目中"个人信息"和"企业信息"模块的导入功能实现进行总结,后续开发新模块的导入功能时,按照本文档的步骤和代码模板照写即可。
+
+---
+
+## 一、整体架构概览
+
+导入功能的整体流程如下:
+
+```
+前端上传 Excel → 后端 Controller 接收 → AutoPoi 解析 Excel → Service 逐行处理
+→ 字典反向翻译 → 唯一键去重 → 必填校验 → 批量保存 → 返回校验结果
+```
+
+涉及的文件分层:
+
+| 层级 | 文件 | 职责 |
+|------|------|------|
+| 前端 - 公共组件 | `src/components/CustomImportModal/src/CustomImportModal.vue` | 通用的导入弹窗组件 |
+| 前端 - API 层 | `src/views/{module}/{Entity}Info.api.ts` | 定义导入/模板下载接口URL |
+| 前端 - 列表页 | `src/views/{module}/{Entity}InfoList.vue` | 集成 CustomImportModal 组件 |
+| 后端 - DTO | `personal/dto/ImportResultItem.java` | 导入校验结果的数据结构(通用) |
+| 后端 - Controller | `{module}/controller/{Entity}InfoController.java` | 接收文件上传、提供模板下载 |
+| 后端 - Service接口 | `{module}/service/I{Entity}InfoService.java` | 声明 importExcelData 方法 |
+| 后端 - Service实现 | `{module}/service/impl/{Entity}InfoServiceImpl.java` | 核心导入逻辑 |
+
+---
+
+## 二、后端实现详解
+
+### 2.1 ImportResultItem — 导入校验结果通用 DTO
+
+**文件位置**: `personal/dto/ImportResultItem.java`
+
+```java
+package org.jeecg.modules.zjrs.personal.dto;
+
+import lombok.Data;
+import java.util.Map;
+
+/**
+ * 导入结果行数据:包含原始Excel列值 + 错误信息(如有)
+ */
+@Data
+public class ImportResultItem {
+    /** Excel行号 */
+    private int rowNum;
+    /** 该行各列的原始文本值:Excel列名 → 值 */
+    private Map<String, String> rowData;
+    /** 错误信息:null或空表示该行校验通过 */
+    private String errorMsg;
+}
+```
+
+> 此 DTO 是通用结构,所有模块共用。`rowData` 存放该行所有 Excel 列的原始值(key 为 Excel 列名 @Excel.name),`errorMsg` 为 null 表示该行校验通过。
+
+---
+
+### 2.2 Service 接口 — 声明导入方法
+
+**文件位置**: `{module}/service/I{Entity}InfoService.java`
+
+在接口中新增 `importExcelData` 方法声明:
+
+```java
+/**
+ * 批量导入Excel数据
+ * 对解析后的数据进行:字典文本→字典码反向翻译、唯一键去重、必填字段校验、批量保存
+ *
+ * @param list Excel解析后的数据列表
+ * @return 每行的导入结果(含原始列值 + 错误信息),当全部无错误时内部会执行批量保存
+ */
+List<ImportResultItem> importExcelData(List<{Entity}Info> list);
+```
+
+示例参考:
+- `IPersonalInfoService.java` ([service/IPersonalInfoService.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/service/IPersonalInfoService.java#L81))
+- `IEnterpriseInfoService.java` ([service/IEnterpriseInfoService.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprise/service/IEnterpriseInfoService.java#L33))
+
+---
+
+### 2.3 Service 实现 — 导入核心逻辑(最重要)
+
+**文件位置**: `{module}/service/impl/{Entity}InfoServiceImpl.java`
+
+核心方法 `importExcelData` 执行以下步骤:
+
+```
+步骤0:定义常量 → 步骤1:字典文本→字典码反向翻译映射 → 步骤2:唯一键去重(Excel内部+数据库)
+→ 步骤3:逐行校验(字典翻译 + 必填校验 + 补充字段)→ 步骤4:有错误则返回错误明细,无错误则批量保存
+```
+
+#### 2.3.1 必须定义的三个常量
+
+在 ServiceImpl 类中定义三个全局常量:
+
+```java
+// 常量1:导入时需要进行 文本→字典码 反向翻译的字段:entity字段名 → 字典编码
+// 只有字典字段(前端是下拉选择而非自由输入的字段)才需要配置此处
+private static final Map<String, String> IMPORT_DICT_FIELD_MAP = new LinkedHashMap<>();
+
+static {
+    IMPORT_DICT_FIELD_MAP.put("gender", "Gender");           // entity字段名 → 字典编码
+    IMPORT_DICT_FIELD_MAP.put("education", "Education");
+    // ... 根据实际业务字段添加
+}
+
+// 常量2:导入模板的字段名列表,顺序必须与 Controller 中 importTemplate 的 fieldNames 一致
+private static final List<String> IMPORT_FIELD_NAMES = Arrays.asList(
+    "idType", "idNumber", "fullName", "gender", "birthDate"
+    // ... 所有模板列字段(包括必填和非必填),顺序与模板导出时的列顺序严格一致
+);
+
+// 常量3:日期格式化器
+private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
+```
+
+> **注意**:`IMPORT_FIELD_NAMES` 的顺序必须与 Controller 中 `importTemplate` 方法里的 `fieldNames` 数组顺序完全一致,否则错误反馈时列名和列值会错位。
+
+#### 2.3.2 核心方法完整代码模板
+
+```java
+@Override
+public List<ImportResultItem> importExcelData(List<{Entity}Info> list) {
+    // ========== 步骤1:批量查询所有字典项,构建 文本→字典码 的反向映射表 ==========
+    Map<String, Map<String, String>> dictTextToCodeMap = new HashMap<>();
+    for (Map.Entry<String, String> entry : IMPORT_DICT_FIELD_MAP.entrySet()) {
+        String dictCode = entry.getValue();
+        if (!dictTextToCodeMap.containsKey(dictCode)) {
+            // 从字典表查询,构建 中文文本→字典码 映射
+            List<DictModel> items = dictionaryItemMapper.queryDictItemsByCode(dictCode);
+            Map<String, String> textToCode = new HashMap<>();
+            for (DictModel item : items) {
+                textToCode.put(item.getText(), item.getValue());
+            }
+            dictTextToCodeMap.put(dictCode, textToCode);
+        }
+    }
+
+    // ========== 步骤2:唯一键去重(Excel内部 + 数据库已存在)==========
+    // 以"统一社会信用代码"或"证件号码"作为唯一键,检查Excel中是否有重复、数据库中是否已存在
+    Map<String, List<Integer>> uniqueKeyPositions = new HashMap<>();
+    for (int i = 0; i < list.size(); i++) {
+        String uniqueKey = list.get(i).getUniqueKeyField(); // 替换为你的唯一键getter
+        if (oConvertUtils.isNotEmpty(uniqueKey)) {
+            uniqueKeyPositions.computeIfAbsent(uniqueKey, k -> new ArrayList<>()).add(i);
+        }
+    }
+
+    // 检查Excel内部重复
+    Map<String, String> uniqueKeyDuplicateMsg = new HashMap<>();
+    for (Map.Entry<String, List<Integer>> entry : uniqueKeyPositions.entrySet()) {
+        if (entry.getValue().size() > 1) {
+            String msg = "唯一键【" + entry.getKey() + "】在数据中多次出现(行:"
+                    + entry.getValue().stream().map(pos -> String.valueOf(pos + 3))
+                            .collect(Collectors.joining("、"))
+                    + ")";
+            for (Integer pos : entry.getValue()) {
+                uniqueKeyDuplicateMsg.put(String.valueOf(pos), msg);
+            }
+        }
+    }
+
+    // 检查数据库是否已存在
+    for (Map.Entry<String, List<Integer>> entry : uniqueKeyPositions.entrySet()) {
+        String uniqueKey = entry.getKey();
+        // 如果Excel内已经重复,不再重复报数据库重复
+        if (uniqueKeyDuplicateMsg.containsKey(String.valueOf(entry.getValue().get(0)))) {
+            continue;
+        }
+        LambdaQueryWrapper<{Entity}Info> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq({Entity}Info::getUniqueKeyField, uniqueKey); // 替换
+        long dbCount = this.count(queryWrapper);
+        if (dbCount > 0) {
+            for (Integer pos : entry.getValue()) {
+                uniqueKeyDuplicateMsg.put(String.valueOf(pos),
+                        "唯一键【" + uniqueKey + "】已在系统中存在");
+            }
+        }
+    }
+
+    // ========== 步骤3:逐行校验与字段处理 ==========
+    List<ImportResultItem> resultItems = new ArrayList<>();
+    boolean hasAnyError = false;
+
+    for (int i = 0; i < list.size(); i++) {
+        {Entity}Info item = list.get(i);
+        int rowNum = i + 3; // Excel行号:标题1行 + 表头1行 = 前2行为头,数据从第3行开始
+
+        // 翻译前捕获原始Excel列值,用于错误反馈时展示原数据
+        Map<String, String> rowData = captureOriginalValues(item);
+        List<String> rowErrors = new ArrayList<>();
+
+        // 唯一键重复校验
+        String dupMsg = uniqueKeyDuplicateMsg.get(String.valueOf(i));
+        if (dupMsg != null) {
+            rowErrors.add(dupMsg);
+        }
+
+        try {
+            // 3.1 字典文本→字典码 反向翻译
+            for (Map.Entry<String, String> fieldEntry : IMPORT_DICT_FIELD_MAP.entrySet()) {
+                String fieldName = fieldEntry.getKey();
+                String dictCode = fieldEntry.getValue();
+                String textValue = (String) getPropertyValue(item, fieldName);
+                if (oConvertUtils.isNotEmpty(textValue)) {
+                    Map<String, String> textToCode = dictTextToCodeMap.get(dictCode);
+                    String codeValue = textToCode.get(textValue.trim());
+                    if (codeValue != null) {
+                        setPropertyValue(item, fieldName, codeValue);
+                    } else {
+                        // 字典中找不到对应值 → 校验失败
+                        rowErrors.add("【" + getFieldExcelName(fieldName) + "】值\""
+                                + textValue + "\"在字典\"" + dictCode + "\"中不存在");
+                    }
+                }
+            }
+
+            // 3.2 校验必填字段(利用已有的 validateRequiredFields 方法)
+            if (rowErrors.isEmpty()) {
+                try {
+                    validateRequiredFields(item);
+                } catch (JeecgBootBizTipException e) {
+                    rowErrors.add(e.getMessage());
+                }
+            }
+
+            // 3.3 补充成对字段、设置默认值(根据业务需要)
+            if (rowErrors.isEmpty()) {
+                // 例如:设置数据来源为"导入"
+                item.setDataSource("2");
+                // 其他业务字段补充...
+            }
+        } catch (Exception e) {
+            rowErrors.add(e.getMessage());
+        }
+
+        // 构建该行的校验结果
+        ImportResultItem resultItem = new ImportResultItem();
+        resultItem.setRowNum(rowNum);
+        resultItem.setRowData(rowData); // 原始Excel列值
+        if (!rowErrors.isEmpty()) {
+            resultItem.setErrorMsg(String.join(";", rowErrors));
+            hasAnyError = true;
+            resultItems.add(resultItem);
+        }
+        // 没错误的不加入 resultItems(减少返回数据量)
+    }
+
+    // ========== 步骤4:有错误则不保存,全部通过则批量保存 ==========
+    if (hasAnyError) {
+        return resultItems;
+    }
+
+    long start = System.currentTimeMillis();
+    this.saveBatch(list);
+    log.info("导入消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
+    return resultItems;
+}
+```
+
+#### 2.3.3 必须实现的三个辅助方法
+
+```java
+/**
+ * 根据字段名获取对应的Excel列名(反射读取 @Excel 注解的 name 属性)
+ * 用于错误提示时展示用户能看懂的列名,而非Java驼峰字段名
+ */
+private String getFieldExcelName(String fieldName) {
+    try {
+        java.lang.reflect.Field field = {Entity}Info.class.getDeclaredField(fieldName);
+        org.jeecgframework.poi.excel.annotation.Excel excel =
+                field.getAnnotation(org.jeecgframework.poi.excel.annotation.Excel.class);
+        if (excel != null) {
+            return excel.name();
+        }
+    } catch (NoSuchFieldException e) {
+        // ignore
+    }
+    return fieldName; // 兜底:返回字段名
+}
+
+/**
+ * 捕获原始Excel列值,以Excel列名作为key
+ * 用于校验失败时在错误详情弹窗中展示用户填写的原始数据
+ */
+private Map<String, String> captureOriginalValues({Entity}Info item) {
+    Map<String, String> rowData = new LinkedHashMap<>();
+    for (String fieldName : IMPORT_FIELD_NAMES) {
+        String excelName = getFieldExcelName(fieldName);
+        try {
+            Object value = getPropertyValue(item, fieldName);
+            if (value == null) {
+                rowData.put(excelName, "");
+            } else if (value instanceof java.util.Date) {
+                rowData.put(excelName, DATE_FORMAT.format(value)); // 日期格式化
+            } else {
+                rowData.put(excelName, value.toString());
+            }
+        } catch (Exception e) {
+            rowData.put(excelName, "");
+        }
+    }
+    return rowData;
+}
+
+/**
+ * 通过 getter 方法名反射读取属性值,兼容 @Accessors(chain = true) 的实体
+ * 注意:这两个方法是通用的静态工具方法,可以抽取到公共工具类中复用
+ */
+private static Object getPropertyValue(Object bean, String propertyName) throws Exception {
+    String getterName = "get" + Character.toUpperCase(propertyName.charAt(0))
+            + propertyName.substring(1);
+    return bean.getClass().getMethod(getterName).invoke(bean);
+}
+
+/**
+ * 通过 setter 方法名反射设置属性值,兼容 @Accessors(chain = true) 的实体
+ */
+private static void setPropertyValue(Object bean, String propertyName, Object value)
+        throws Exception {
+    String setterName = "set" + Character.toUpperCase(propertyName.charAt(0))
+            + propertyName.substring(1);
+    bean.getClass().getMethod(setterName, value.getClass()).invoke(bean, value);
+}
+```
+
+---
+
+### 2.4 Controller — 接收导入请求与提供模板下载
+
+**文件位置**: `{module}/controller/{Entity}InfoController.java`
+
+#### 2.4.1 导入Excel数据接口
+
+```java
+@RequiresPermissions("{module}:{entityInfo}:importExcel")
+@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
+public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
+    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
+    Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
+
+    for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
+        MultipartFile file = entity.getValue();
+        ImportParams params = new ImportParams();
+        params.setTitleRows(1);   // 标题行数(模板第0行为标题行)
+        params.setHeadRows(1);    // 表头行数(模板第1行为表头行)
+        try {
+            // AutoPoi 将 Excel 解析为 Entity 列表
+            List<{Entity}Info> list = ExcelImportUtil.importExcel(
+                    file.getInputStream(), {Entity}Info.class, params);
+
+            // 调用 Service 进行校验与保存
+            List<ImportResultItem> resultItems = {entity}InfoService.importExcelData(list);
+
+            // 检查是否有行校验失败
+            boolean hasError = false;
+            for (ImportResultItem item : resultItems) {
+                if (item.getErrorMsg() != null && !item.getErrorMsg().isEmpty()) {
+                    hasError = true;
+                    break;
+                }
+            }
+            if (hasError) {
+                // 返回 code=500 + result=校验错误行列表,前端会展示错误详情弹窗
+                return Result.error("导入数据校验失败", resultItems);
+            }
+            return Result.ok("文件导入成功!数据行数:" + list.size());
+        } catch (Exception e) {
+            String msg = e.getMessage();
+            log.error(msg, e);
+            if (msg != null && msg.contains("Duplicate entry")) {
+                return Result.error("文件导入失败:有重复数据!");
+            } else {
+                return Result.error("文件导入失败:" + e.getMessage());
+            }
+        } finally {
+            try {
+                file.getInputStream().close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+    return Result.error("文件导入失败!");
+}
+```
+
+> **关键点**:
+> - 返回 `Result.error(msg, resultItems)` 时,前端检测到 `code=500` 且 `result` 是数组,就会打开错误详情弹窗
+> - `ImportParams.titleRows` 和 `headRows` 的值必须与导出的模板结构一致
+
+#### 2.4.2 下载导入模板接口
+
+```java
+@RequiresPermissions("{module}:{entityInfo}:importExcel")
+@RequestMapping(value = "/importTemplate")
+public void importTemplate(HttpServletRequest request, HttpServletResponse response)
+        throws Exception {
+    String title = "{业务名}导入模板";
+    ExportParams exportParams = new ExportParams(title, "导入模板", ExcelType.XSSF);
+
+    // 定义模板的列(必填在前,非必填在后)
+    String[] fieldNames = {"idType", "idNumber", "fullName", /* 所有列... */};
+
+    // 构建一条示例数据
+    List<{Entity}Info> dataList = new ArrayList<>(List.of(buildExampleData()));
+
+    // AutoPoi 生成完整 workbook(含列头、样式、示例数据)
+    Workbook workbook = ExcelExportUtil.exportExcel(
+            exportParams, {Entity}Info.class, dataList, fieldNames);
+
+    Sheet sheet = workbook.getSheetAt(0);
+    // AutoPoi 带 ExportParams(title,...) 时:第0行为标题行,第1行为表头行
+    Row headerRow = sheet.getRow(1);
+
+    // 定义必填字段集合(与 Service 中 validateRequiredFields 保持一致)
+    Set<String> requiredFields = new HashSet<>(Arrays.asList(
+            "idType", "idNumber", "fullName" /* 必填字段列表 */
+    ));
+
+    // 必填字段表头加红色加粗样式
+    CellStyle requiredStyle = workbook.createCellStyle();
+    Font requiredFont = workbook.createFont();
+    requiredFont.setColor(IndexedColors.RED.getIndex());
+    requiredFont.setBold(true);
+    requiredStyle.setFont(requiredFont);
+    // 复制 AutoPoi 默认的边框/对齐等样式
+    if (headerRow != null && headerRow.getCell(0) != null) {
+        requiredStyle.cloneStyleFrom(headerRow.getCell(0).getCellStyle());
+        requiredStyle.setFont(requiredFont);
+    }
+
+    for (int i = 0; i < fieldNames.length; i++) {
+        Cell cell = headerRow.getCell(i);
+        if (cell != null && requiredFields.contains(fieldNames[i])) {
+            cell.setCellStyle(requiredStyle); // 必填字段表头标红
+        }
+    }
+
+    // 写出响应
+    response.setContentType(
+            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+    response.setHeader("Content-disposition",
+            "attachment; filename=" + URLEncoder.encode(title + ".xlsx", "UTF-8"));
+    workbook.write(response.getOutputStream());
+    workbook.close();
+}
+
+/**
+ * 构建模板的示例数据行
+ * 字典字段填写中文文本(与字典表一致),导入时会自动反向翻译为字典码
+ */
+private {Entity}Info buildExampleData() {
+    {Entity}Info example = new {Entity}Info();
+    example.setFullName("张三");       // 自由文本字段
+    example.setGender("男");           // 字典字段 → 填写中文标签文本
+    example.setEducation("大学本科");   // 字典字段 → 填写中文标签文本
+    // ... 所有模板列的示例数据
+    return example;
+}
+```
+
+> **注意**:`fieldNames` 数组的顺序必须与 ServiceImpl 中 `IMPORT_FIELD_NAMES` 常量完全一致。必填字段集合 `requiredFields` 要与 `validateRequiredFields` 方法中的校验逻辑一致。
+
+---
+
+## 三、前端实现详解
+
+### 3.1 CustomImportModal 公共组件
+
+**文件位置**: `src/components/CustomImportModal/src/CustomImportModal.vue`
+
+该组件是通用的导入弹窗,所有模块可直接复用,无需修改组件源码。
+
+#### 组件功能
+
+1. **文件选择**:支持 `.xls`、`.xlsx` 格式,限制 2MB
+2. **模板下载**:可配置下载按钮(传入 `downloadUrl`)
+3. **上传导入**:调用 `importUrl` 上传文件
+4. **错误展示**:当后端返回 `code=500` + `result` 数组时,自动弹出错误详情表格,展示每行的原始 Excel 数据和错误原因
+
+#### Props 参数
+
+| 参数 | 类型 | 必填 | 默认值 | 说明 |
+|------|------|------|--------|------|
+| `auth` | `String` | 否 | `''` | 权限标识,用于 v-auth 控制按钮显示 |
+| `importUrl` | `String` | **是** | — | 导入上传的接口URL |
+| `downloadUrl` | `String` | 否 | `''` | 模板下载的接口URL |
+| `templateName` | `String` | 否 | `'下载导入模板'` | 模板下载按钮文本 |
+| `buttonText` | `String` | 否 | `'导入'` | 触发按钮文本 |
+| `buttonIcon` | `String` | 否 | `'ant-design:import-outlined'` | 触发按钮图标 |
+| `buttonType` | `String` | 否 | `'primary'` | 触发按钮类型 |
+| `success` | `Function` | 否 | — | 导入成功后的回调函数 |
+
+#### Events
+
+| 事件名 | 说明 | 回调参数 |
+|--------|------|----------|
+| `success` | 导入成功后触发 | 无 |
+
+---
+
+### 3.2 API 层 — 定义导入接口URL
+
+**文件位置**: `src/views/{module}/{Entity}Info.api.ts`
+
+在 API 枚举和导出函数中添加:
+
+```typescript
+enum Api {
+  // ... 其他接口
+  importExcel = '/enterprise/enterpriseInfo/importExcel',     // 导入上传URL
+  importTemplate = '/enterprise/enterpriseInfo/importTemplate', // 模板下载URL
+}
+
+/**
+ * 导入api(传给 CustomImportModal 的 importUrl)
+ */
+export const getImportUrl = Api.importExcel;
+
+/**
+ * 导入模板下载api(传给 CustomImportModal 的 downloadUrl)
+ */
+export const getImportTemplateUrl = Api.importTemplate;
+```
+
+---
+
+### 3.3 列表页 — 集成 CustomImportModal
+
+**文件位置**: `src/views/{module}/{Entity}InfoList.vue`
+
+#### 3.3.1 模板中引入组件
+
+在表格标题区域(`#tableTitle` 插槽)中添加:
+
+```html
+<CustomImportModal
+  auth="{module}:{entityInfo}:importExcel"
+  :importUrl="getImportUrl"
+  :downloadUrl="getImportTemplateUrl"
+  templateName="{业务名}导入模板"
+  buttonText="导入"
+  @success="handleSuccess"
+/>
+```
+
+#### 3.3.2 script 中导入
+
+```typescript
+import { CustomImportModal } from '/@/components/CustomImportModal';
+import { getImportUrl, getImportTemplateUrl } from './{Entity}Info.api';
+```
+
+不需要在 `components` 中注册(组件已通过 `withInstall` 全局注册)。
+
+---
+
+## 四、新模块接入步骤清单
+
+按以下步骤操作,即可为新模块添加完整的导入功能:
+
+### 后端(5步)
+
+1. **确认 Entity 的 `@Excel` 注解**:实体类中需要用 `@Excel(name = "列名")` 标注字段,这样 AutoPoi 才能按列名解析 Excel,错误提示也能展示中文列名
+
+2. **Service 接口添加方法**:
+   ```java
+   List<ImportResultItem> importExcelData(List<{Entity}Info> list);
+   ```
+
+3. **ServiceImpl 实现 importExcelData**:
+   - 定义 `IMPORT_DICT_FIELD_MAP`(字典字段映射)
+   - 定义 `IMPORT_FIELD_NAMES`(模板列顺序,与第5步模板列顺序一致)
+   - 定义 `DATE_FORMAT`
+   - 照抄 2.3.2 节的代码模板,替换实体类名、唯一键字段
+   - 照抄 2.3.3 节的三个辅助方法
+
+4. **Controller 新增两个接口**:
+   - `POST /importExcel` — 照抄 2.4.1 节代码模板
+   - `GET /importTemplate` — 照抄 2.4.2 节代码模板,编写 `buildExampleData()` 方法
+
+5. **核对一致性**:
+   - Controller 的 `fieldNames` 顺序 = ServiceImpl 的 `IMPORT_FIELD_NAMES` 顺序
+   - Controller 的 `requiredFields` 集合 = ServiceImpl 的 `validateRequiredFields` 逻辑
+
+### 前端(2步)
+
+6. **API 层添加接口URL**:
+   ```typescript
+   export const getImportUrl = Api.importExcel;
+   export const getImportTemplateUrl = Api.importTemplate;
+   ```
+
+7. **列表页引入 CustomImportModal**:
+   ```html
+   <CustomImportModal
+     auth="权限标识"
+     :importUrl="getImportUrl"
+     :downloadUrl="getImportTemplateUrl"
+     templateName="模板名称"
+     @success="handleSuccess"
+   />
+   ```
+
+---
+
+## 五、数据流转示意图
+
+```
+┌──────────────────────────────────────────────────────────────────────────┐
+│ 前端                                                                      │
+│                                                                           │
+│  [导入按钮] → [选择Excel文件] → [开始导入]                                  │
+│       │                              │                                    │
+│       ▼                              ▼                                    │
+│  [下载模板]                    POST /importExcel                          │
+│  GET /importTemplate           上传 MultipartFile                          │
+│                                                                           │
+├──────────────────────────────────────────────────────────────────────────┤
+│ 后端                                                                      │
+│                                                                           │
+│  Controller.importExcel()                                                 │
+│       │                                                                   │
+│       ▼                                                                   │
+│  ExcelImportUtil.importExcel()  →  List<Entity>(AutoPoi解析,字典字段仍是中文)│
+│       │                                                                   │
+│       ▼                                                                   │
+│  Service.importExcelData()                                                │
+│       │                                                                   │
+│       ├── 步骤1:批量查字典,构建 中文→字典码 映射                             │
+│       ├── 步骤2:唯一键去重(Excel内部 + 数据库存在)                          │
+│       ├── 步骤3:逐行校验                                                   │
+│       │    ├── 字典文本→字典码 反向翻译                                      │
+│       │    ├── 必填字段校验(调用 validateRequiredFields)                   │
+│       │    └── 补充默认值(如 dataSource = "2")                             │
+│       │                                                                   │
+│       └── 步骤4:                                                          │
+│            ├── 有错误 → return List<ImportResultItem>(不保存)              │
+│            │          前端展示错误详情表格                                    │
+│            └── 无错误 → saveBatch() 批量写入数据库                           │
+│                                                                           │
+└──────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 六、注意事项与常见问题
+
+1. **日期格式**:Excel 中日期列请填写 `yyyy-MM-dd` 格式(如 `1990-01-01`),模板示例数据也使用此格式
+
+2. **字典字段**:模板中字典列填写的是**中文标签文本**(如性别填"男"而不是字典码"1"),导入时会自动反向翻译为字典码存入数据库
+
+3. **Excel 行号计算**:数据从第3行开始(第0行标题 + 第1行表头),所以错误提示中的行号 = `列表索引 + 3`
+
+4. **唯一键去重**:如果业务的唯一键不止一个列,需要扩展步骤2的去重逻辑。如果业务没有唯一键约束,可以跳过步骤2
+
+5. **字段顺序一致性**:Controller 的 `fieldNames`、ServiceImpl 的 `IMPORT_FIELD_NAMES`、Entity 的 `@Excel` 注解,三者的列顺序必须完全一致
+
+6. **权限标识**:导入按钮和模板下载使用 `{module}:{entityInfo}:importExcel` 权限,Controller 方法也需要加 `@RequiresPermissions`
+
+7. **大量数据导入**:超过 1000 行数据时,建议使用 `saveBatch(list, 500)` 分批次保存
+
+---
+
+## 七、涉及的核心文件清单
+
+| 文件 | 作用 |
+|------|------|
+| [CustomImportModal.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/CustomImportModal/src/CustomImportModal.vue) | 前端公共导入弹窗组件 |
+| [ImportResultItem.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/dto/ImportResultItem.java) | 导入校验结果通用DTO |
+| [PersonalInfoController.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/controller/PersonalInfoController.java#L226-L328) | 个人信息导入Controller(参考实现) |
+| [EnterpriseInfoController.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprise/controller/EnterpriseInfoController.java#L216-L360) | 企业信息导入Controller(参考实现) |
+| [PersonalInfoServiceImpl.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/service/impl/PersonalInfoServiceImpl.java#L402-L648) | 个人信息导入Service实现(参考实现) |
+| [EnterpriseInfoServiceImpl.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprise/service/impl/EnterpriseInfoServiceImpl.java#L151-L351) | 企业信息导入Service实现(参考实现) |
+| [PersonalInfo.api.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/recruitment/personal/PersonalInfo.api.ts) | 个人信息API定义(参考实现) |
+| [EnterpriseInfo.api.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/recruitment/enterprise/EnterpriseInfo.api.ts) | 企业信息API定义(参考实现) |
+| [PersonalInfoList.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue) | 个人信息列表页(组件集成示例) |
+| [EnterpriseInfoList.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/views/recruitment/enterprise/EnterpriseInfoList.vue) | 企业信息列表页(组件集成示例) |
+| [IPersonalInfoService.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/service/IPersonalInfoService.java) | 个人信息Service接口(接口声明示例) |
+| [IEnterpriseInfoService.java](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprise/service/IEnterpriseInfoService.java) | 企业信息Service接口(接口声明示例) |

+ 0 - 271
.docs/260613-自定义导入组件使用文档.md

@@ -1,271 +0,0 @@
-# 自定义导入组件(CustomImportModal)使用文档
-
-## 一、组件概述
-
-`CustomImportModal` 是一个封装的 Excel 导入弹窗组件,提供以下能力:
-
-1. **文件上传**:选择 `.xls` / `.xlsx` 文件,内置前端格式和大小校验(最大 2MB)
-2. **模板下载**:支持从后端下载 Excel 导入模板(含表头 + 示例数据)
-3. **权限控制**:可通过 `auth` 属性配置导入按钮的权限标识
-4. **导入校验**:上传后由后端进行字典反向翻译、必填校验、去重校验
-5. **错误展示**:校验失败时以表格形式展示每行错误数据及具体错误原因
-6. **导入成功回调**:导入成功后触发 `success` 事件,通常用于刷新列表
-
-> 组件只会处理 `code === 201`(部分成功)、`code === 500`(有结构化校验结果 / 普通失败)、其他(成功)三种情况。业务后端需按约定格式返回数据。
-
----
-
-## 二、组件 Props
-
-| 属性 | 类型 | 必填 | 默认值 | 说明 |
-|------|------|------|--------|------|
-| `auth` | `String` | 否 | `''` | 按钮权限标识,为空时按钮不进行权限控制 |
-| `importUrl` | `String` | **是** | - | 后端 Excel 导入接口 URL |
-| `downloadUrl` | `String` | 否 | `''` | 后端模板下载接口 URL,为空时不显示下载模板区域 |
-| `templateName` | `String` | 否 | `'下载导入模板'` | 模板下载按钮显示的文字 |
-| `buttonText` | `String` | 否 | `'导入'` | 触发按钮显示文字 |
-| `buttonIcon` | `String` | 否 | `'ant-design:import-outlined'` | 触发按钮图标 |
-| `buttonType` | `String` | 否 | `'primary'` | 触发按钮类型(Ant Design Button type) |
-| `success` | `Function` | 否 | - | 导入成功后的回调函数(也可通过 `@success` 事件监听) |
-
----
-
-## 三、组件 Events
-
-| 事件名 | 参数 | 触发时机 |
-|--------|------|----------|
-| `success` | 无 | 文件导入成功且无错误;或部分成功(code=201)关闭弹窗后 |
-
----
-
-## 四、后端 API 约定
-
-### 4.1 导入接口 `POST {importUrl}`
-
-**请求方式**:`multipart/form-data`,字段名为 `file`
-
-**响应格式**(与组件逻辑的对应关系):
-
-| 响应 `code` | 组件行为 |
-|-------------|----------|
-| `200` 或 其他非 201/500 | 提示 `message`,关闭弹窗,触发 `success` |
-| `201` | 部分成功,弹出警告框,若 `result.fileUrl` 存在则提供错误详情下载链接,关闭弹窗,触发 `success` |
-| `500` 且 `result` 为 `ImportResultItem[]` 非空数组 | 弹出"导入校验结果"表格弹窗,展示每行错误详情,**不关闭导入手弹窗**,不触发 `success` |
-| `500` 且 `result` 不是数组或为空 | 仅提示 `message` 错误信息 |
-
-### 4.2 `ImportResultItem` 结构
-
-```java
-public class ImportResultItem {
-    private Integer rowNum;                    // Excel 行号
-    private Map<String, String> rowData;       // 该行的列名→列值映射
-    private String errorMsg;                   // 错误信息(多个错误用分号拼接)
-}
-```
-
-### 4.3 模板下载接口 `GET {downloadUrl}`
-
-返回 Excel 文件的二进制流(`Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`)。
-
----
-
-## 五、前端使用示例
-
-以"个人基本信息"页面的导入为例。
-
-### 5.1 API 定义(PersonalInfo.api.ts)
-
-```typescript
-// 文件:jeecgboot-vue3/src/views/recruitment/personal/PersonalInfo.api.ts
-
-enum Api {
-  importExcel = '/personal/personalInfo/importExcel',
-  importTemplate = '/personal/personalInfo/importTemplate',
-}
-
-// 导入接口URL
-export const getImportUrl = Api.importExcel;
-
-// 模板下载接口URL
-export const getImportTemplateUrl = Api.importTemplate;
-```
-
-### 5.2 Vue 模板中使用
-
-```vue
-<template>
-  <!-- 在表格标题区域内放置导入按钮 -->
-  <template #tableTitle>
-    <CustomImportModal
-      auth="personal:personal_info:importExcel"
-      :importUrl="getImportUrl"
-      :downloadUrl="getImportTemplateUrl"
-      templateName="个人基本信息导入模板"
-      @success="handleSuccess"
-    />
-  </template>
-</template>
-
-<script lang="ts" setup>
-  import { CustomImportModal } from '/@/components/CustomImportModal';
-  import { getImportUrl, getImportTemplateUrl } from './PersonalInfo.api';
-
-  // 导入成功后刷新表格
-  function handleSuccess() {
-    reload();
-  }
-</script>
-```
-
----
-
-## 六、后端实现参考
-
-### 6.1 Controller 层(PersonalInfoController.java)
-
-```java
-// 文件:jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/.../personal/controller/PersonalInfoController.java
-
-/**
- * 通过excel导入数据
- * 解析Excel → 调用Service进行字典反向翻译、校验、补充字段、批量保存
- */
-@RequiresPermissions("personal:personal_info:importExcel")
-@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
-public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
-    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
-    Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
-
-    for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
-        MultipartFile file = entity.getValue();
-        ImportParams params = new ImportParams();
-        params.setTitleRows(1);
-        params.setHeadRows(1);
-        params.setNeedSave(true);
-        try {
-            // 解析Excel为实体列表
-            List<PersonalInfo> list = ExcelImportUtil.importExcel(
-                file.getInputStream(), PersonalInfo.class, params);
-            // 调用Service进行校验和处理
-            List<ImportResultItem> resultItems = personalInfoService.importExcelData(list);
-            // 检查是否有校验错误
-            boolean hasError = false;
-            for (ImportResultItem item : resultItems) {
-                if (item.getErrorMsg() != null && !item.getErrorMsg().isEmpty()) {
-                    hasError = true;
-                    break;
-                }
-            }
-            if (hasError) {
-                return Result.error("导入数据校验失败", resultItems);
-            }
-            return Result.ok("文件导入成功!数据行数:" + list.size());
-        } catch (Exception e) {
-            String msg = e.getMessage();
-            if (msg != null && msg.contains("Duplicate entry")) {
-                return Result.error("文件导入失败:有重复数据!");
-            } else {
-                return Result.error("文件导入失败:" + e.getMessage());
-            }
-        }
-    }
-    return Result.error("文件导入失败!");
-}
-```
-
-### 6.2 Service 层核心逻辑(PersonalInfoServiceImpl.importExcelData)
-
-| 步骤 | 说明 |
-|------|------|
-| 1. 字典反向翻译 | 根据 `IMPORT_DICT_FIELD_MAP`(字段→字典编码映射),批量查询字典项,构建 `文本→字典码` 的反向映射表,将 Excel 中的字典中文文本转为字典码值 |
-| 2. 证件号码去重 | 检查 Excel 内部是否有重复证件号 + 数据库中是否已存在相同证件号,重复则标记错误 |
-| 3. 逐行校验 | 捕获原始列值 → 字典文本转码 → 必填字段校验 → 补充成对字段(如户口所在地和区域名称) → 设置数据来源为"2"(导入) |
-| 4. 批量保存或返回错误 | 若有任何错误行则**不保存**,直接返回 `ImportResultItem` 列表;全部通过则 `saveBatch` 批量插入 |
-
-**涉及的字典字段映射(IMPORT_DICT_FIELD_MAP)**:
-- 证件类型 → `IDType`
-- 性别 → `Gender`
-- 民族 → `Ethnicity`
-- 国籍 → `Nationality`
-- 婚姻状况 → `MaritalStatus`
-- 学历 → `Education`
-- 政治面貌 → `PoliticalStatus`
-- 工作经验 → `WorkExperience`
-- 户口类型 → `HouseholdType`
-- 求职人员类别 → `JobSeekerCategory`
-- 求职状态 → `JobSeekerStatus`
-
-### 6.3 模板下载接口
-
-```java
-/**
- * 下载导入模板
- * 包含指定字段的表头 + 一条示例数据,方便用户参照填写
- */
-@RequiresPermissions("personal:personal_info:importExcel")
-@RequestMapping(value = "/importTemplate")
-public ModelAndView importTemplate(HttpServletRequest request, HttpServletResponse response) {
-    ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
-    String title = "个人基本信息导入模板";
-    mv.addObject(NormalExcelConstants.FILE_NAME, title);
-    mv.addObject(NormalExcelConstants.CLASS, PersonalInfo.class);
-    ExportParams exportParams = new ExportParams(title, "导入模板", ExcelType.XSSF);
-    mv.addObject(NormalExcelConstants.PARAMS, exportParams);
-    // 指定导出字段
-    String exportFields = "idType,idNumber,fullName,gender,birthDate,...";
-    mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
-    // 一条示例数据
-    mv.addObject(NormalExcelConstants.DATA_LIST, new ArrayList<>(List.of(buildExampleData())));
-    return mv;
-}
-```
-
----
-
-## 七、完整数据流
-
-```
-用户点击"导入"按钮
-    │
-    ▼
-打开导入弹窗 → 用户选择Excel文件(前端校验格式、大小)
-    │
-    ├── 可选:点击下载模板按钮 → GET {downloadUrl} → 返回Excel二进制流 → 浏览器下载
-    │
-    ▼
-用户点击"开始导入"
-    │
-    ▼
-POST {importUrl} (multipart/form-data, file字段)
-    │
-    ▼
-后端 Controller 解析Excel → Service 处理
-    ├── 字典文本 → 字典码反向翻译
-    ├── 证件号码去重(Excel内部 + 数据库)
-    ├── 逐行必填校验
-    ├── 补充关联字段
-    │
-    ▼
-┌─ 全部通过 ───────────────────────────────┐
-│  saveBatch 批量保存 → Result.ok → 前端提示成功 → 关闭弹窗 → 触发 @success → 刷新列表
-│
-├─ 有校验错误 ─────────────────────────────┐
-│  Result.error(resultItems) → 前端弹出"导入校验结果"弹窗 → 表格展示每行错误 → 用户关闭 → 可重新选择文件再导入
-│
-└─ 数据库唯一键冲突 ────────────────────────┐
-   Result.error("有重复数据") → 前端提示错误
-```
-
----
-
-## 八、涉及文件清单
-
-| 文件 | 角色 |
-|------|------|
-| `jeecgboot-vue3/src/components/CustomImportModal/index.ts` | 组件入口,注册全局组件 |
-| `jeecgboot-vue3/src/components/CustomImportModal/src/CustomImportModal.vue` | 导入弹窗核心实现(上传、校验展示、模板下载) |
-| `jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue` | 使用示例:引入组件并配置属性 |
-| `jeecgboot-vue3/src/views/recruitment/personal/PersonalInfo.api.ts` | API 定义:`getImportUrl`、`getImportTemplateUrl` |
-| `jeecg-boot/.../personal/controller/PersonalInfoController.java` | 后端导入接口 `POST /importExcel`、模板下载 `GET /importTemplate` |
-| `jeecg-boot/.../personal/service/impl/PersonalInfoServiceImpl.java` | 导入核心逻辑:字典翻译、去重校验、逐行校验、批量保存 |
-| `jeecg-boot/.../personal/dto/ImportResultItem.java` | 导入结果行 DTO:rowNum、rowData、errorMsg |

+ 155 - 2
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprise/controller/EnterpriseInfoController.java

@@ -20,18 +20,31 @@ import org.jeecg.modules.zjrs.enterprise.dto.EnterpriseInfoDTO;
 import org.jeecg.modules.zjrs.enterprise.entity.EnterpriseInfo;
 import org.jeecg.modules.zjrs.enterprise.service.IEnterpriseInfoService;
 import org.jeecg.modules.zjrs.enterprise.vo.EnterpriseInfoVO;
+import org.jeecg.modules.zjrs.personal.dto.ImportResultItem;
+import org.jeecgframework.poi.excel.ExcelExportUtil;
+import org.jeecgframework.poi.excel.ExcelImportUtil;
 import org.jeecgframework.poi.excel.def.NormalExcelConstants;
 import org.jeecgframework.poi.excel.entity.ExportParams;
+import org.jeecgframework.poi.excel.entity.ImportParams;
 import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
 import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
+import org.apache.poi.ss.usermodel.*;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.MultipartHttpServletRequest;
 import org.springframework.web.servlet.ModelAndView;
 
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * @Description: 企业基本信息表
@@ -195,15 +208,155 @@ public class EnterpriseInfoController extends JeecgController<EnterpriseInfo, IE
 
     /**
      * 通过excel导入数据
+     * 解析Excel → 调用Service进行字典反向翻译、校验、补充字段、批量保存
      *
      * @param request
      * @param response
-     * @return
      */
     @RequiresPermissions("enterprise:enterprise_info:importExcel")
     @RequestMapping(value = "/importExcel", method = RequestMethod.POST)
     public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
-        return super.importExcel(request, response, EnterpriseInfo.class);
+        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
+        Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
+
+        for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
+            MultipartFile file = entry.getValue();
+            ImportParams params = new ImportParams();
+            params.setTitleRows(1);
+            params.setHeadRows(1);
+            try {
+                List<EnterpriseInfo> list = ExcelImportUtil.importExcel(file.getInputStream(), EnterpriseInfo.class, params);
+                List<ImportResultItem> resultItems = enterpriseInfoService.importExcelData(list);
+                boolean hasError = false;
+                for (ImportResultItem item : resultItems) {
+                    if (item.getErrorMsg() != null && !item.getErrorMsg().isEmpty()) {
+                        hasError = true;
+                        break;
+                    }
+                }
+                if (hasError) {
+                    return Result.error("导入数据校验失败", resultItems);
+                }
+                return Result.ok("文件导入成功!数据行数:" + list.size());
+            } catch (Exception e) {
+                String msg = e.getMessage();
+                log.error(msg, e);
+                if (msg != null && msg.contains("Duplicate entry")) {
+                    return Result.error("文件导入失败:有重复数据!");
+                } else {
+                    return Result.error("文件导入失败:" + e.getMessage());
+                }
+            } finally {
+                try {
+                    file.getInputStream().close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return Result.error("文件导入失败!");
+    }
+
+    /**
+     * 下载导入模板
+     * 包含指定字段的表头 + 一条示例数据,方便用户参照填写
+     *
+     * @param request
+     * @param response
+     */
+    @RequiresPermissions("enterprise:enterprise_info:importExcel")
+    @RequestMapping(value = "/importTemplate")
+    public void importTemplate(HttpServletRequest request, HttpServletResponse response) throws Exception {
+        String title = "企业基本信息导入模板";
+        ExportParams exportParams = new ExportParams(title, "导入模板", ExcelType.XSSF);
+        // 指定导出字段:必填在前,非必填在后
+        String[] fieldNames = {"companyName", "unifiedCreditCode", "regAddrDistrict", "companyType",
+                "economyType", "industry", "staffSize", "contactPerson",
+                "contactPhone", "acceptSmsPush", "openToJobSeeker", "allowAgentSignup",
+                "companyIntro", "officeAddrDistrict", "officeAddress",
+                "orgCode", "establishDate", "businessStatus", "registeredCapital",
+                "isListedCompany", "industryField", "industryAdminDept", "industryTag",
+                "companyPropertyTag", "provinceOrCity", "companyAttribute",
+                "isHeadEnterprise", "isKeyEnterprise", "isKeyInstitution", "isStrategicIndustry"};
+        List<EnterpriseInfo> dataList = new ArrayList<>(List.of(buildExampleData()));
+        // 通过 AutoPoi 生成完整 workbook(含列头、样式、示例数据)
+        Workbook workbook = ExcelExportUtil.exportExcel(exportParams, EnterpriseInfo.class, dataList, fieldNames);
+        Sheet sheet = workbook.getSheetAt(0);
+        // AutoPoi 带 ExportParams(title,...) 时:第0行为标题行,第1行为表头行
+        Row headerRow = sheet.getRow(1);
+
+        // 必填字段集合
+        Set<String> requiredFields = new HashSet<>(Arrays.asList(
+                "companyName", "unifiedCreditCode", "regAddrDistrict", "companyType",
+                "economyType", "industry", "staffSize", "contactPerson",
+                "contactPhone", "acceptSmsPush", "openToJobSeeker", "allowAgentSignup", 
+                "companyIntro", "officeAddrDistrict", "officeAddress"
+        ));
+        // 必填字段表头样式:红色 + 加粗
+        CellStyle requiredStyle = workbook.createCellStyle();
+        Font requiredFont = workbook.createFont();
+        requiredFont.setColor(IndexedColors.RED.getIndex());
+        requiredFont.setBold(true);
+        requiredStyle.setFont(requiredFont);
+        // 复制 AutoPoi 默认的边框/对齐等样式,避免表头风格与原来不一致
+        if (headerRow != null && headerRow.getCell(0) != null) {
+            requiredStyle.cloneStyleFrom(headerRow.getCell(0).getCellStyle());
+            requiredStyle.setFont(requiredFont);
+        }
+
+        for (int i = 0; i < fieldNames.length; i++) {
+            Cell cell = headerRow.getCell(i);
+            if (cell != null && requiredFields.contains(fieldNames[i])) {
+                cell.setCellStyle(requiredStyle);
+            }
+        }
+
+        // 写出响应
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setHeader("Content-disposition", "attachment; filename=" + URLEncoder.encode(title + ".xlsx", "UTF-8"));
+        workbook.write(response.getOutputStream());
+        workbook.close();
+    }
+
+    /**
+     * 构建导入模板的示例数据行
+     * 字典字段填写中文文本(与字典表一致),导入时会自动反向翻译为字典码
+     */
+    private EnterpriseInfo buildExampleData() {
+        EnterpriseInfo example = new EnterpriseInfo();
+        // === 必填字段 ===
+        example.setCompanyName("示例企业有限公司");
+        example.setUnifiedCreditCode("91440800MA4XXXXXXX");
+        example.setRegAddrDistrict("广东省湛江市赤坎区");
+        example.setCompanyType("企业");
+        example.setEconomyType("内资");
+        example.setIndustry("信息传输、软件和信息技术服务业");
+        example.setStaffSize("100-299人");
+        example.setContactPerson("李四");
+        example.setContactPhone("13800001111");
+        example.setAcceptSmsPush("是");
+        example.setOpenToJobSeeker("是");
+        example.setAllowAgentSignup("是");
+        example.setCompanyIntro("这是一家示例企业,用于展示导入模板的填写格式。");
+        example.setOfficeAddrDistrict("广东省湛江市赤坎区");
+        example.setOfficeAddress("湛江市赤坎区示范路1号");
+        // === 非必填字段 ===
+        example.setOrgCode("12345678-X");
+        example.setEstablishDate(new Date());
+        example.setBusinessStatus("登记在册");
+        example.setRegisteredCapital("1000万元");
+        example.setIsListedCompany("否");
+        example.setIndustryField("人工智能与机器人");
+        example.setIndustryAdminDept("湛江市工业和信息化局");
+        example.setIndustryTag("高新技术产业");
+        example.setCompanyPropertyTag("私营企业");
+        example.setProvinceOrCity("省属");
+        example.setCompanyAttribute("其他");
+        example.setIsHeadEnterprise("否");
+        example.setIsKeyEnterprise("是");
+        example.setIsKeyInstitution("否");
+        example.setIsStrategicIndustry("是");
+        return example;
     }
 
     /**

+ 1 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprise/entity/EnterpriseInfo.java

@@ -242,6 +242,7 @@ public class EnterpriseInfo implements Serializable {
     /**
      * 单位简介
      */
+    @Excel(name = "单位简介", width = 30)
     @Schema(description = "单位简介")
     private java.lang.String companyIntro;
     /**

+ 9 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprise/service/IEnterpriseInfoService.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import org.jeecg.modules.zjrs.enterprise.dto.EnterpriseInfoDTO;
 import org.jeecg.modules.zjrs.enterprise.entity.EnterpriseInfo;
 import org.jeecg.modules.zjrs.enterprise.vo.EnterpriseInfoVO;
+import org.jeecg.modules.zjrs.personal.dto.ImportResultItem;
 
 import java.util.List;
 import java.util.Map;
@@ -23,6 +24,14 @@ public interface IEnterpriseInfoService extends IService<EnterpriseInfo> {
 
     void validateRequiredFields(EnterpriseInfo enterpriseInfo);
 
+    /**
+     * 导入Excel数据:字典反向翻译、唯一键去重、逐行校验、批量保存
+     *
+     * @param list Excel解析后的企业信息列表
+     * @return 校验结果行列表(errorMsg非空表示该行校验失败)
+     */
+    List<ImportResultItem> importExcelData(List<EnterpriseInfo> list);
+
     /**
      * 分页查询企业信息(含当前岗位数)
      */

+ 215 - 1
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprise/service/impl/EnterpriseInfoServiceImpl.java

@@ -5,14 +5,18 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.common.system.vo.DictModel;
 import org.jeecg.common.util.AssertUtils;
 import org.jeecg.common.util.oConvertUtils;
+import org.jeecg.modules.zjrs.dictionary.mapper.DictionaryItemMapper;
 import org.jeecg.modules.zjrs.dictionary.service.IDictionaryItemService;
 import org.jeecg.modules.zjrs.enterprise.dto.EnterpriseInfoDTO;
 import org.jeecg.modules.zjrs.enterprise.entity.EnterpriseInfo;
 import org.jeecg.modules.zjrs.enterprise.mapper.EnterpriseInfoMapper;
 import org.jeecg.modules.zjrs.enterprise.service.IEnterpriseInfoService;
 import org.jeecg.modules.zjrs.enterprise.vo.EnterpriseInfoVO;
+import org.jeecg.modules.zjrs.personal.dto.ImportResultItem;
 import org.jeecg.modules.zjrs.tag.entity.EnterpriseTag;
 import org.jeecg.modules.zjrs.tag.entity.EnterpriseTagRelation;
 import org.jeecg.modules.zjrs.tag.mapper.EnterpriseTagMapper;
@@ -22,8 +26,11 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -34,12 +41,16 @@ import java.util.stream.Collectors;
  * @Date: 2026-06-02
  * @Version: V1.0
  */
+@Slf4j
 @Service
 public class EnterpriseInfoServiceImpl extends ServiceImpl<EnterpriseInfoMapper, EnterpriseInfo> implements IEnterpriseInfoService {
 
     @Autowired
     private IDictionaryItemService dictionaryItemService;
 
+    @Autowired
+    private DictionaryItemMapper dictionaryItemMapper;
+
     @Autowired
     private EnterpriseTagRelationMapper enterpriseTagRelationMapper;
 
@@ -67,7 +78,6 @@ public class EnterpriseInfoServiceImpl extends ServiceImpl<EnterpriseInfoMapper,
         AssertUtils.assertNotEmpty("单位类型不能为空", enterpriseInfo.getCompanyType());
         AssertUtils.assertNotEmpty("经济类型不能为空", enterpriseInfo.getEconomyType());
         AssertUtils.assertNotEmpty("所属行业不能为空", enterpriseInfo.getIndustry());
-        AssertUtils.assertNotEmpty("数据来源不能为空", enterpriseInfo.getDataSource());
         AssertUtils.assertNotEmpty("人员规模不能为空", enterpriseInfo.getStaffSize());
         AssertUtils.assertNotEmpty("联系人不能为空", enterpriseInfo.getContactPerson());
         AssertUtils.assertNotEmpty("联系方式不能为空", enterpriseInfo.getContactPhone());
@@ -136,6 +146,210 @@ public class EnterpriseInfoServiceImpl extends ServiceImpl<EnterpriseInfoMapper,
         return list;
     }
 
+    // ========== 导入相关 ==========
+
+    /**
+     * 导入时需要进行 文本→字典码 反向翻译的字段:字段名 → 字典编码
+     * 与导出时 EXPORT_DICT_FIELD_MAP 的映射一致
+     */
+    private static final Map<String, String> IMPORT_DICT_FIELD_MAP = new LinkedHashMap<>();
+
+    static {
+        IMPORT_DICT_FIELD_MAP.put("businessStatus", "BusinessStatus");
+        IMPORT_DICT_FIELD_MAP.put("companyType", "CompanyType");
+        IMPORT_DICT_FIELD_MAP.put("dataSource", "DataSource");
+        IMPORT_DICT_FIELD_MAP.put("staffSize", "CompanySize");
+    }
+
+    /**
+     * 导入模板的字段名列表,顺序必须与 Controller 中 importTemplate 的 EXPORT_FIELDS 一致
+     */
+    private static final List<String> IMPORT_FIELD_NAMES = Arrays.asList(
+            "companyName", "unifiedCreditCode", "regAddrDistrict", "companyType",
+            "economyType", "industry", "staffSize", "contactPerson",
+            "contactPhone", "acceptSmsPush", "openToJobSeeker", "allowAgentSignup",
+            "companyIntro", "officeAddrDistrict", "officeAddress",
+            "orgCode", "establishDate", "businessStatus", "registeredCapital",
+            "isListedCompany", "industryField", "industryAdminDept", "industryTag",
+            "companyPropertyTag", "provinceOrCity", "companyAttribute",
+            "isHeadEnterprise", "isKeyEnterprise", "isKeyInstitution", "isStrategicIndustry"
+    );
+
+    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
+
+    @Override
+    public List<ImportResultItem> importExcelData(List<EnterpriseInfo> list) {
+        // 步骤1:批量查询所有字典项,构建 文本→字典码 的反向映射表
+        Map<String, Map<String, String>> dictTextToCodeMap = new HashMap<>();
+        for (Map.Entry<String, String> entry : IMPORT_DICT_FIELD_MAP.entrySet()) {
+            String dictCode = entry.getValue();
+            if (!dictTextToCodeMap.containsKey(dictCode)) {
+                List<DictModel> items = dictionaryItemMapper.queryDictItemsByCode(dictCode);
+                Map<String, String> textToCode = new HashMap<>();
+                for (DictModel item : items) {
+                    textToCode.put(item.getText(), item.getValue());
+                }
+                dictTextToCodeMap.put(dictCode, textToCode);
+            }
+        }
+
+        // 步骤2:统一社会信用代码去重(Excel内部 + 数据库)
+        Map<String, List<Integer>> creditCodePositions = new HashMap<>();
+        for (int i = 0; i < list.size(); i++) {
+            String code = list.get(i).getUnifiedCreditCode();
+            if (oConvertUtils.isNotEmpty(code)) {
+                creditCodePositions.computeIfAbsent(code, k -> new ArrayList<>()).add(i);
+            }
+        }
+        // Excel内部去重
+        Map<String, String> creditCodeDuplicateMsg = new HashMap<>();
+        for (Map.Entry<String, List<Integer>> entry : creditCodePositions.entrySet()) {
+            if (entry.getValue().size() > 1) {
+                String msg = "统一社会信用代码【" + entry.getKey() + "】在数据中多次出现(行:"
+                        + entry.getValue().stream().map(pos -> String.valueOf(pos + 3)).collect(Collectors.joining("、"))
+                        + ")";
+                for (Integer pos : entry.getValue()) {
+                    creditCodeDuplicateMsg.put(String.valueOf(pos), msg);
+                }
+            }
+        }
+        // 数据库去重
+        for (Map.Entry<String, List<Integer>> entry : creditCodePositions.entrySet()) {
+            String code = entry.getKey();
+            if (creditCodeDuplicateMsg.containsKey(String.valueOf(entry.getValue().get(0)))) {
+                continue; // Excel内已重复,不再重复报数据库重复
+            }
+            LambdaQueryWrapper<EnterpriseInfo> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(EnterpriseInfo::getUnifiedCreditCode, code);
+            long dbCount = this.count(queryWrapper);
+            if (dbCount > 0) {
+                for (Integer pos : entry.getValue()) {
+                    creditCodeDuplicateMsg.put(String.valueOf(pos), "统一社会信用代码【" + code + "】已在系统中存在");
+                }
+            }
+        }
+
+        // 步骤3:逐行校验与字段处理
+        List<ImportResultItem> resultItems = new ArrayList<>();
+        boolean hasAnyError = false;
+
+        for (int i = 0; i < list.size(); i++) {
+            EnterpriseInfo item = list.get(i);
+            int rowNum = i + 3; // Excel行号(标题1行 + 表头1行 = 前2行为头,数据从第3行开始)
+
+            // 翻译前捕获原始Excel列值,用于错误反馈时展示原数据
+            Map<String, String> rowData = captureOriginalValues(item);
+
+            List<String> rowErrors = new ArrayList<>();
+            // 统一社会信用代码重复校验
+            String dupMsg = creditCodeDuplicateMsg.get(String.valueOf(i));
+            if (dupMsg != null) {
+                rowErrors.add(dupMsg);
+            }
+            try {
+                // 字典文本→字典码 反向翻译
+                for (Map.Entry<String, String> fieldEntry : IMPORT_DICT_FIELD_MAP.entrySet()) {
+                    String fieldName = fieldEntry.getKey();
+                    String dictCode = fieldEntry.getValue();
+                    String textValue = (String) getPropertyValue(item, fieldName);
+                    if (oConvertUtils.isNotEmpty(textValue)) {
+                        Map<String, String> textToCode = dictTextToCodeMap.get(dictCode);
+                        String codeValue = textToCode.get(textValue.trim());
+                        if (codeValue != null) {
+                            setPropertyValue(item, fieldName, codeValue);
+                        } else {
+                            rowErrors.add("【" + getFieldExcelName(fieldName) + "】值\"" + textValue + "\"在字典\"" + dictCode + "\"中不存在");
+                        }
+                    }
+                }
+                // 校验必填字段
+                if (rowErrors.isEmpty()) {
+                    validateRequiredFields(item);
+                }
+                // 设置数据来源为导入
+                if (rowErrors.isEmpty()) {
+                    item.setDataSource("2");
+                }
+            } catch (Exception e) {
+                rowErrors.add(e.getMessage());
+            }
+
+            ImportResultItem resultItem = new ImportResultItem();
+            resultItem.setRowNum(rowNum);
+            resultItem.setRowData(rowData);
+            if (!rowErrors.isEmpty()) {
+                resultItem.setErrorMsg(String.join(";", rowErrors));
+                hasAnyError = true;
+                resultItems.add(resultItem);
+            }
+        }
+
+        // 步骤4:有错误则不保存,全部通过则批量保存
+        if (hasAnyError) {
+            return resultItems;
+        }
+
+        long start = System.currentTimeMillis();
+        this.saveBatch(list);
+        log.info("导入消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
+        return resultItems;
+    }
+
+    /**
+     * 捕获原始Excel列值,以Excel列名作为key,用于校验失败时反馈原数据
+     */
+    private Map<String, String> captureOriginalValues(EnterpriseInfo item) {
+        Map<String, String> rowData = new LinkedHashMap<>();
+        for (String fieldName : IMPORT_FIELD_NAMES) {
+            String excelName = getFieldExcelName(fieldName);
+            try {
+                Object value = getPropertyValue(item, fieldName);
+                if (value == null) {
+                    rowData.put(excelName, "");
+                } else if (value instanceof java.util.Date) {
+                    rowData.put(excelName, DATE_FORMAT.format(value));
+                } else {
+                    rowData.put(excelName, value.toString());
+                }
+            } catch (Exception e) {
+                rowData.put(excelName, "");
+            }
+        }
+        return rowData;
+    }
+
+    /**
+     * 根据字段名获取对应的Excel列名(用于错误提示)
+     */
+    private String getFieldExcelName(String fieldName) {
+        try {
+            java.lang.reflect.Field field = EnterpriseInfo.class.getDeclaredField(fieldName);
+            org.jeecgframework.poi.excel.annotation.Excel excel = field.getAnnotation(org.jeecgframework.poi.excel.annotation.Excel.class);
+            if (excel != null) {
+                return excel.name();
+            }
+        } catch (NoSuchFieldException e) {
+            // ignore
+        }
+        return fieldName;
+    }
+
+    /**
+     * 通过 getter 方法名反射读取属性值,兼容 @Accessors(chain = true) 的实体
+     */
+    private static Object getPropertyValue(Object bean, String propertyName) throws Exception {
+        String getterName = "get" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
+        return bean.getClass().getMethod(getterName).invoke(bean);
+    }
+
+    /**
+     * 通过 setter 方法名反射设置属性值,兼容 @Accessors(chain = true) 的实体
+     */
+    private static void setPropertyValue(Object bean, String propertyName, Object value) throws Exception {
+        String setterName = "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
+        bean.getClass().getMethod(setterName, value.getClass()).invoke(bean, value);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void saveWithTags(EnterpriseInfoDTO enterpriseInfoDTO) {

+ 52 - 14
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/controller/PersonalInfoController.java

@@ -21,6 +21,7 @@ import org.jeecg.modules.zjrs.personal.service.IPersonalInfoService;
 import org.jeecg.modules.zjrs.personal.dto.ImportResultItem;
 import org.jeecg.modules.zjrs.personal.dto.PersonalInfoDTO;
 import org.jeecg.modules.zjrs.personal.vo.PersonalInfoVO;
+import org.jeecgframework.poi.excel.ExcelExportUtil;
 import org.jeecgframework.poi.excel.ExcelImportUtil;
 import org.jeecgframework.poi.excel.def.NormalExcelConstants;
 import org.jeecgframework.poi.excel.entity.ExportParams;
@@ -34,11 +35,16 @@ import org.springframework.web.multipart.MultipartHttpServletRequest;
 import org.springframework.web.servlet.ModelAndView;
 
 import java.io.IOException;
+import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+
+import org.apache.poi.ss.usermodel.*;
 
 /**
  * @Description: 个人基本信息表
@@ -271,22 +277,54 @@ public class PersonalInfoController extends JeecgController<PersonalInfo, IPerso
      */
     @RequiresPermissions("personal:personal_info:importExcel")
     @RequestMapping(value = "/importTemplate")
-    public ModelAndView importTemplate(HttpServletRequest request, HttpServletResponse response) {
-        ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
+    public void importTemplate(HttpServletRequest request, HttpServletResponse response) throws Exception {
         String title = "个人基本信息导入模板";
-        mv.addObject(NormalExcelConstants.FILE_NAME, title);
-        mv.addObject(NormalExcelConstants.CLASS, PersonalInfo.class);
         ExportParams exportParams = new ExportParams(title, "导入模板", ExcelType.XSSF);
-        mv.addObject(NormalExcelConstants.PARAMS, exportParams);
-        // 指定导出字段,按用户要求顺序排列
-        String exportFields = "idType,idNumber,fullName,gender,birthDate,nation,nationality,maritalStatus,"
-                + "education,graduationDate,graduateSchool,major,politicalStatus,workExperience,"
-                + "householdType,householdLocation,currentResidence,currentAddress,jobSeekerCategory,"
-                + "contactPhone,email,qqNumber,wechatId,isOverseasTalent,skillLevel,jobSearchStatus,acceptRecommend";
-        mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
-        // 一条示例数据(用 ArrayList 包裹,避免 AutoPoi 对不可修改列表做 remove 操作报 UnsupportedOperationException)
-        mv.addObject(NormalExcelConstants.DATA_LIST, new ArrayList<>(List.of(buildExampleData())));
-        return mv;
+        // 指定导出字段:必填在前,非必填在后
+        String[] fieldNames = {"idType", "idNumber", "fullName", "gender", "birthDate",
+                "nation", "nationality", "education", "graduationDate", "graduateSchool",
+                "workExperience", "householdLocation", "currentResidence", "jobSeekerCategory",
+                "contactPhone", "isOverseasTalent", "jobSearchStatus", "acceptRecommend",
+                "maritalStatus", "major", "politicalStatus", "householdType", "currentAddress",
+                "email", "qqNumber", "wechatId", "skillLevel"};
+        List<PersonalInfo> dataList = new ArrayList<>(List.of(buildExampleData()));
+        // 通过 AutoPoi 生成完整 workbook(含列头、样式、示例数据)
+        Workbook workbook = ExcelExportUtil.exportExcel(exportParams, PersonalInfo.class, dataList, fieldNames);
+        Sheet sheet = workbook.getSheetAt(0);
+        // AutoPoi 带 ExportParams(title,...) 时:第0行为标题行,第1行为表头行
+        Row headerRow = sheet.getRow(1);
+
+        // 必填字段集合
+        Set<String> requiredFields = new HashSet<>(Arrays.asList(
+                "idType", "idNumber", "fullName", "gender", "birthDate",
+                "nation", "nationality", "education", "graduationDate", "graduateSchool",
+                "workExperience", "householdLocation", "currentResidence", "jobSeekerCategory",
+                "contactPhone", "isOverseasTalent", "jobSearchStatus", "acceptRecommend"
+        ));
+        // 必填字段表头样式:红色 + 加粗
+        CellStyle requiredStyle = workbook.createCellStyle();
+        Font requiredFont = workbook.createFont();
+        requiredFont.setColor(IndexedColors.RED.getIndex());
+        requiredFont.setBold(true);
+        requiredStyle.setFont(requiredFont);
+        // 复制 AutoPoi 默认的边框/对齐等样式,避免表头风格与原来不一致
+        if (headerRow != null && headerRow.getCell(0) != null) {
+            requiredStyle.cloneStyleFrom(headerRow.getCell(0).getCellStyle());
+            requiredStyle.setFont(requiredFont);
+        }
+
+        for (int i = 0; i < fieldNames.length; i++) {
+            Cell cell = headerRow.getCell(i);
+            if (cell != null && requiredFields.contains(fieldNames[i])) {
+                cell.setCellStyle(requiredStyle);
+            }
+        }
+
+        // 写出响应
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setHeader("Content-disposition", "attachment; filename=" + URLEncoder.encode(title + ".xlsx", "UTF-8"));
+        workbook.write(response.getOutputStream());
+        workbook.close();
     }
 
     /**

文件差异内容过多而无法显示
+ 185520 - 0
jeecgboot-vue3/public/resource/data/xzqh.json


文件差异内容过多而无法显示
+ 15745 - 0
jeecgboot-vue3/public/resource/data/zyfl.json


+ 1 - 1
jeecgboot-vue3/src/components/CustomImportModal/src/CustomImportModal.vue

@@ -209,7 +209,7 @@
     let importSuccess = false;
     try {
       await defHttp.uploadFile(
-        { url: props.importUrl },
+        { url: props.importUrl, timeout: 120000 },
         { file: fileList.value[0], name: 'file' },
         {
           success: (res: any) => {

+ 6 - 0
jeecgboot-vue3/src/views/recruitment/enterprise/EnterpriseInfo.api.ts

@@ -11,6 +11,7 @@ enum Api {
   deleteOne = '/enterprise/enterpriseInfo/delete',
   deleteBatch = '/enterprise/enterpriseInfo/deleteBatch',
   importExcel = '/enterprise/enterpriseInfo/importExcel',
+  importTemplate = '/enterprise/enterpriseInfo/importTemplate',
   exportXls = '/enterprise/enterpriseInfo/exportXls',
   enterpriseTagListForSelect = '/tag/enterpriseTag/listForSelect',
 }
@@ -26,6 +27,11 @@ export const getExportUrl = Api.exportXls;
  */
 export const getImportUrl = Api.importExcel;
 
+/**
+ * 导入模板下载api
+ */
+export const getImportTemplateUrl = Api.importTemplate;
+
 /**
  * 列表接口
  * @param params

+ 11 - 11
jeecgboot-vue3/src/views/recruitment/enterprise/EnterpriseInfoList.vue

@@ -54,9 +54,14 @@
         <a-button v-auth="'enterprise:enterprise_info:exportXls'" preIcon="ant-design:export-outlined" type="primary" @click="onExportXls">
           导出</a-button
         >
-        <j-upload-button v-auth="'enterprise:enterprise_info:importExcel'" preIcon="ant-design:import-outlined" type="primary" @click="onImportXls"
-          >导入</j-upload-button
-        >
+        <CustomImportModal
+          auth="enterprise:enterprise_info:importExcel"
+          :importUrl="getImportUrl"
+          :downloadUrl="getImportTemplateUrl"
+          templateName="企业基本信息导入模板"
+          buttonText="导入"
+          @success="handleSuccess"
+        />
         <a-dropdown v-if="selectedRowKeys.length > 0">
           <template #overlay>
             <a-menu>
@@ -71,8 +76,6 @@
             <Icon icon="mdi:chevron-down"></Icon>
           </a-button>
         </a-dropdown>
-        <!-- 高级查询 -->
-        <!--        <super-query :config="superQueryConfig" @search="handleSuperQuery" />-->
       </template>
       <!--操作栏-->
       <template #action="{ record }">
@@ -120,7 +123,7 @@
   import { useModal } from '/@/components/Modal';
   import { useListPage } from '/@/hooks/system/useListPage';
   import { columns, superQuerySchema } from './EnterpriseInfo.data';
-  import { batchDelete, deleteOne, getExportUrl, getImportUrl, list, queryById } from './EnterpriseInfo.api';
+  import { batchDelete, deleteOne, getExportUrl, getImportUrl, getImportTemplateUrl, list, queryById } from './EnterpriseInfo.api';
   import EnterpriseInfoModal from './components/EnterpriseInfoModal.vue';
   import EnterpriseInfoDetailModal from './components/EnterpriseInfoDetailModal.vue';
   import EnterprisePostListModal from '/@/views/recruitment/post/components/EnterprisePostListModal.vue';
@@ -129,6 +132,7 @@
   import { getDateByPicker } from '/@/utils';
   import { useDict } from '/@/hooks/dictionary/useDict';
   import { defHttp } from '/@/utils/http/axios';
+  import { CustomImportModal } from '/@/components/CustomImportModal';
 
   const fieldPickers = reactive({});
 
@@ -218,7 +222,7 @@
   }
 
   //注册table数据
-  const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
+  const { prefixCls, tableContext, onExportXls } = useListPage({
     tableProps: {
       title: '企业信息-企业基本信息',
       api: list,
@@ -243,10 +247,6 @@
       url: getExportUrl,
       params: queryParam,
     },
-    importConfig: {
-      url: getImportUrl,
-      success: handleSuccess,
-    },
   });
   const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] =
     tableContext;

+ 1 - 1
jeecgboot-vue3/src/views/recruitment/enterprise/components/EnterpriseInfoDetailModal.vue

@@ -48,7 +48,7 @@
           <a-descriptions-item label="联系方式" :span="1">{{ detailData.contactPhone }}</a-descriptions-item>
           <a-descriptions-item label="接收平台短信推送" :span="1">{{ detailData.acceptSmsPush }}</a-descriptions-item>
           
-          <a-descriptions-item label="面向求职者会员开放" :span="1">{{ detailData.openToJobSeeker }}</a-descriptions-item>
+          <a-descriptions-item label="面向求职者会员开放" :span="1">{{ formatYesNo(detailData.openToJobSeeker) }}</a-descriptions-item>
           <a-descriptions-item label="是否允许人社部门代报名" :span="1">{{ formatYesNo(detailData.allowAgentSignup) }}</a-descriptions-item>
           <a-descriptions-item label="单位网站" :span="1">{{ detailData.website }}</a-descriptions-item>
           

+ 4 - 9
jeecgboot-vue3/src/views/recruitment/enterprise/components/EnterpriseInfoForm.vue

@@ -188,11 +188,6 @@
                 <a-select v-model:value="formData.isStrategicIndustry" :options="yesNoOptions" allow-clear placeholder="请选择是否属于20个战略性产业集群" />
               </a-form-item>
             </a-col>
-            <a-col :span="8">
-              <a-form-item id="EnterpriseInfoForm-dataSource" label="数据来源" name="dataSource" v-bind="validateInfos.dataSource">
-                <a-select v-model:value="formData.dataSource" allow-clear disabled placeholder="请选择数据来源" :options="dataSourceOptions"></a-select>
-              </a-form-item>
-            </a-col>
           </a-row>
           <a-divider orientation="left" orientation-margin="0px">单位详细信息</a-divider>
           <a-row>
@@ -228,7 +223,7 @@
             </a-col>
             <a-col :span="8">
               <a-form-item id="EnterpriseInfoForm-acceptSmsPush" label="接收平台短信推送" name="acceptSmsPush" v-bind="validateInfos.acceptSmsPush">
-                <a-input v-model:value="formData.acceptSmsPush" allow-clear placeholder="请输入接收平台短信推送"></a-input>
+                <a-select v-model:value="formData.acceptSmsPush" :options="yesNoOptions" allow-clear placeholder="请选择是否接收平台短信推送" />
               </a-form-item>
             </a-col>
             <a-col :span="8">
@@ -238,7 +233,7 @@
                 name="openToJobSeeker"
                 v-bind="validateInfos.openToJobSeeker"
               >
-                <a-input v-model:value="formData.openToJobSeeker" allow-clear placeholder="请输入面向求职者会员开放"></a-input>
+                <a-select v-model:value="formData.openToJobSeeker" :options="yesNoOptions" allow-clear placeholder="请选择是否面向求职者会员开放" />
               </a-form-item>
             </a-col>
             <a-col :span="8">
@@ -385,8 +380,8 @@
     postalCode: '',
     contactPerson: '',
     contactPhone: '',
-    acceptSmsPush: '',
-    openToJobSeeker: '',
+    acceptSmsPush: undefined,
+    openToJobSeeker: undefined,
     allowAgentSignup: undefined,
     website: '',
     faxNumber: '',