importPersonnel(MultipartFile file) {
+ return SingleResponse.success(orgPersonnelExcelImporter.importPersonnel(file));
+ }
}
diff --git a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/ImportEducationEnum.java b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/ImportEducationEnum.java
new file mode 100644
index 0000000..8bc4bde
--- /dev/null
+++ b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/ImportEducationEnum.java
@@ -0,0 +1,70 @@
+package org.qinan.safetyeval.app.support;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * 导入学历枚举转换。
+ *
+ * 学历为前端字典,此处提供常见学历名称的兜底映射;如与实际字典不一致可在此调整。
+ *
+ *
+ * @author safety-eval
+ */
+public enum ImportEducationEnum {
+
+ DOCTOR(new String[]{"博士"}, "06", "博士研究生", "04", "博士"),
+ MASTER(new String[]{"硕士", "研究生"}, "05", "硕士研究生", "03", "硕士"),
+ BACHELOR(new String[]{"本科"}, "04", "大学本科", "02", "本科"),
+ COLLEGE(new String[]{"专科", "大专", "高职"}, "03", "大学专科", "01", "专科"),
+ SECONDARY(new String[]{"中专", "中技", "技校"}, "02", "中专", "00", "中专"),
+ HIGH_SCHOOL(new String[]{"高中"}, "01", "高中", "00", "高中");
+
+ private final String[] keywords;
+ private final String educationCode;
+ private final String educationName;
+ private final String educationLevelCode;
+ private final String educationLevelName;
+
+ ImportEducationEnum(String[] keywords, String educationCode, String educationName,
+ String educationLevelCode, String educationLevelName) {
+ this.keywords = keywords;
+ this.educationCode = educationCode;
+ this.educationName = educationName;
+ this.educationLevelCode = educationLevelCode;
+ this.educationLevelName = educationLevelName;
+ }
+
+ public String getEducationCode() {
+ return educationCode;
+ }
+
+ public String getEducationName() {
+ return educationName;
+ }
+
+ public String getEducationLevelCode() {
+ return educationLevelCode;
+ }
+
+ public String getEducationLevelName() {
+ return educationLevelName;
+ }
+
+ /**
+ * 按包含关键字匹配学历枚举,无法识别返回 null。
+ */
+ public static ImportEducationEnum match(String text) {
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ String trimmed = text.trim();
+ for (ImportEducationEnum e : values()) {
+ for (String kw : e.keywords) {
+ if (trimmed.contains(kw)) {
+ return e;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/ImportGenderEnum.java b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/ImportGenderEnum.java
new file mode 100644
index 0000000..c5ea7d0
--- /dev/null
+++ b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/ImportGenderEnum.java
@@ -0,0 +1,50 @@
+package org.qinan.safetyeval.app.support;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * 导入性别枚举转换(性别编码 1男 2女)。
+ *
+ * @author safety-eval
+ */
+public enum ImportGenderEnum {
+
+ MALE(1, "男", new String[]{"男", "男性"}),
+ FEMALE(2, "女", new String[]{"女", "女性"});
+
+ private final Integer code;
+ private final String name;
+ private final String[] aliases;
+
+ ImportGenderEnum(Integer code, String name, String[] aliases) {
+ this.code = code;
+ this.name = name;
+ this.aliases = aliases;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * 按文本匹配性别枚举,无法识别返回 null。
+ */
+ public static ImportGenderEnum fromText(String text) {
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ String trimmed = text.trim();
+ for (ImportGenderEnum e : values()) {
+ for (String alias : e.aliases) {
+ if (alias.equals(trimmed)) {
+ return e;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/ImportRegisterEngineerEnum.java b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/ImportRegisterEngineerEnum.java
new file mode 100644
index 0000000..bb08d96
--- /dev/null
+++ b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/ImportRegisterEngineerEnum.java
@@ -0,0 +1,44 @@
+package org.qinan.safetyeval.app.support;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * 导入“是否注册安全工程师”枚举转换(1是 2否)。
+ *
+ * @author safety-eval
+ */
+public enum ImportRegisterEngineerEnum {
+
+ YES(1, new String[]{"是", "Y", "y", "yes", "true"}),
+ NO(2, new String[]{"否", "N", "n", "no", "false"});
+
+ private final Integer code;
+ private final String[] aliases;
+
+ ImportRegisterEngineerEnum(Integer code, String[] aliases) {
+ this.code = code;
+ this.aliases = aliases;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ /**
+ * 按文本匹配枚举,无法识别返回 null。
+ */
+ public static ImportRegisterEngineerEnum fromText(String text) {
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ String trimmed = text.trim();
+ for (ImportRegisterEngineerEnum e : values()) {
+ for (String alias : e.aliases) {
+ if (alias.equalsIgnoreCase(trimmed)) {
+ return e;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/OrgPersonnelExcelImporter.java b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/OrgPersonnelExcelImporter.java
new file mode 100644
index 0000000..08f8b2d
--- /dev/null
+++ b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/OrgPersonnelExcelImporter.java
@@ -0,0 +1,379 @@
+package org.qinan.safetyeval.app.support;
+
+import com.alibaba.excel.EasyExcel;
+import org.qinan.safetyeval.client.co.OrgPersonnelImportErrorCO;
+import org.qinan.safetyeval.client.co.OrgPersonnelImportResultCO;
+import org.qinan.safetyeval.domain.entity.OrgDepartmentEntity;
+import org.qinan.safetyeval.domain.entity.OrgPersonnelEntity;
+import org.qinan.safetyeval.domain.entity.OrgPositionEntity;
+import org.qinan.safetyeval.domain.entity.TitleCodeCo;
+import org.qinan.safetyeval.domain.exception.BizException;
+import org.qinan.safetyeval.domain.exception.ErrorCode;
+import org.qinan.safetyeval.domain.gateway.OrgDepartmentGateway;
+import org.qinan.safetyeval.domain.gateway.OrgPersonnelGateway;
+import org.qinan.safetyeval.domain.gateway.OrgPositionGateway;
+import org.qinan.safetyeval.domain.service.OrgPersonnelDomainService;
+import org.qinan.safetyeval.infrastructure.adapter.ThreadLocalUserInfoAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 基于 EasyExcel 的人员信息批量导入器。
+ *
+ * 解析 Excel -> 批量校验部门/岗位名称并回填ID -> 校验账号重复并收集异常 -> 逐行落库。
+ *
+ *
+ * @author safety-eval
+ */
+@Component
+public class OrgPersonnelExcelImporter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OrgPersonnelExcelImporter.class);
+
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+ @Resource
+ private OrgDepartmentGateway orgDepartmentGateway;
+
+ @Resource
+ private OrgPositionGateway orgPositionGateway;
+
+ @Resource
+ private OrgPersonnelGateway orgPersonnelGateway;
+
+ @Resource
+ private OrgPersonnelDomainService orgPersonnelDomainService;
+
+ public OrgPersonnelImportResultCO importPersonnel(MultipartFile file) {
+ if (file == null || file.isEmpty()) {
+ throw new BizException(ErrorCode.FILE_UPLOAD_EMPTY);
+ }
+
+ List errors = new ArrayList<>();
+ List rows = parseRows(file, errors);
+ if (rows.isEmpty()) {
+ throw new BizException(ErrorCode.ORG_PERSONNEL_IMPORT_EMPTY);
+ }
+
+ Long orgId = ThreadLocalUserInfoAdapter.getOrgId();
+
+ OrgPersonnelImportResultCO result = new OrgPersonnelImportResultCO();
+ result.setSuccessCount(0);
+ result.setFailCount(0);
+
+ // 1. 批量校验并回填部门ID
+ validateAndFillDept(rows, orgId);
+
+ // 2. 批量校验并回填岗位ID
+ validateAndFillPosition(rows, orgId);
+
+ // 3. 批量查询库中已存在的账号(未删除)
+ Set existingAccounts = batchQueryExistingAccounts(rows, orgId);
+
+ // 4. 逐行转换、落库
+ int successCount = 0;
+
+ for (OrgPersonnelImportRow row : rows) {
+ String account = trim(row.getAccount());
+
+ // 账号在库中已存在 -> 跳过该行,记录异常
+ if (existingAccounts.contains(account)) {
+ errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), account, trim(row.getUserName()),
+ "账号已存在,跳过该行"));
+ continue;
+ }
+
+ try {
+ OrgPersonnelEntity entity = buildEntity(row, orgId, errors);
+ if (entity == null) {
+ // 行内枚举/数据校验未通过,已记录异常
+ continue;
+ }
+ orgPersonnelDomainService.add(entity);
+ successCount++;
+ } catch (Exception ex) {
+ LOGGER.warn("导入人员落库失败, rowIndex={}, account={}", row.getRowIndex(), account, ex);
+ errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), account, trim(row.getUserName()),
+ "保存失败:" + ex.getMessage()));
+ }
+ }
+
+ result.setSuccessCount(successCount);
+ result.setFailCount(errors.size());
+ result.setErrors(errors);
+ return result;
+ }
+
+ /**
+ * 解析 Excel,过滤空行,并回填物理行号(从1开始,含表头,首条数据为第2行)。
+ *
+ * 文件内手机号重复的行在此剔除,并生成提示放入 errors(空账号不参与去重)。
+ *
+ */
+ private List parseRows(MultipartFile file, List errors) {
+ List raw;
+ try {
+ raw = EasyExcel.read(file.getInputStream())
+ .head(OrgPersonnelImportRow.class)
+ .sheet()
+ .headRowNumber(1)
+ .doReadSync();
+ } catch (Exception ex) {
+ LOGGER.error("解析人员导入Excel失败", ex);
+ throw new BizException(ErrorCode.ORG_PERSONNEL_IMPORT_PARSE_ERROR);
+ }
+ if (raw == null || raw.isEmpty()) {
+ return new ArrayList<>();
+ }
+ List rows = new ArrayList<>(raw.size());
+ Set seenAccounts = new HashSet<>();
+ for (int i = 0; i < raw.size(); i++) {
+ OrgPersonnelImportRow row = raw.get(i);
+ if (row == null) {
+ continue;
+ }
+ // 空行:账号与姓名均为空
+ if (!StringUtils.hasText(trim(row.getAccount())) && !StringUtils.hasText(trim(row.getUserName()))) {
+ continue;
+ }
+ row.setRowIndex(i + 2);
+
+ // 文件内手机号重复:剔除该行并记录提示(空账号不参与去重)
+ String account = trim(row.getAccount());
+ if (StringUtils.hasText(account) && !seenAccounts.add(account)) {
+ errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), account, trim(row.getUserName()),
+ "账号在文件内重复,已剔除该行"));
+ continue;
+ }
+ rows.add(row);
+ }
+ return rows;
+ }
+
+ /**
+ * 批量校验部门名称是否在当前机构下存在,校验通过的同时回填 deptId;不存在则抛出异常。
+ */
+ private void validateAndFillDept(List rows, Long orgId) {
+ Set deptNames = rows.stream()
+ .map(r -> trim(r.getDeptName()))
+ .filter(StringUtils::hasText)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+
+ Map deptMap = new HashMap<>();
+ if (!deptNames.isEmpty()) {
+ for (OrgDepartmentEntity dept : orgDepartmentGateway.listByOrgIdAndNames(orgId, deptNames)) {
+ if (dept.getDeptName() != null) {
+ deptMap.putIfAbsent(dept.getDeptName().trim(), dept);
+ }
+ }
+ }
+
+ // 收集所有缺失部门(保留出现顺序,附带首个出现行号)
+ Map missingDeptRows = new LinkedHashMap<>();
+ for (OrgPersonnelImportRow row : rows) {
+ String name = trim(row.getDeptName());
+ if (!StringUtils.hasText(name)) {
+ missingDeptRows.putIfAbsent("(空)", row.getRowIndex());
+ continue;
+ }
+ if (!deptMap.containsKey(name)) {
+ missingDeptRows.putIfAbsent(name, row.getRowIndex());
+ }
+ }
+ if (!missingDeptRows.isEmpty()) {
+ String detail = missingDeptRows.entrySet().stream()
+ .map(e -> e.getKey() + "(第" + e.getValue() + "行)")
+ .collect(Collectors.joining("; "));
+ throw new BizException(ErrorCode.ORG_PERSONNEL_IMPORT_DEPT_NOT_FOUND,
+ ErrorCode.ORG_PERSONNEL_IMPORT_DEPT_NOT_FOUND.getMessage() + ":" + detail);
+ }
+
+ // 校验通过,回填部门ID
+ for (OrgPersonnelImportRow row : rows) {
+ row.setDeptId(deptMap.get(trim(row.getDeptName())).getId());
+ }
+ }
+
+ /**
+ * 批量校验岗位名称在所属部门下是否存在(dept_id + org_id),校验通过的同时回填 postId;不存在则抛出异常。
+ */
+ private void validateAndFillPosition(List rows, Long orgId) {
+ Set deptIds = new HashSet<>();
+ Set positionNames = new HashSet<>();
+ for (OrgPersonnelImportRow row : rows) {
+ if (row.getDeptId() != null) {
+ deptIds.add(row.getDeptId());
+ }
+ String posName = trim(row.getPositionName());
+ if (StringUtils.hasText(posName)) {
+ positionNames.add(posName);
+ }
+ }
+
+ // key: deptId|positionName -> position
+ Map positionMap = new HashMap<>();
+ if (!deptIds.isEmpty() || !positionNames.isEmpty()) {
+ for (OrgPositionEntity pos : orgPositionGateway.listByOrgIdAndDeptIdsAndNames(orgId, deptIds, positionNames)) {
+ String key = pos.getDeptId() + "|" + (pos.getPositionName() == null ? "" : pos.getPositionName().trim());
+ positionMap.putIfAbsent(key, pos);
+ }
+ }
+
+ // 收集缺失岗位
+ Map missingPositionRows = new LinkedHashMap<>();
+ Map missingDeptName = new LinkedHashMap<>();
+ for (OrgPersonnelImportRow row : rows) {
+ String posName = trim(row.getPositionName());
+ if (!StringUtils.hasText(posName)) {
+ missingPositionRows.putIfAbsent("(空)", row.getRowIndex());
+ missingDeptName.putIfAbsent("(空)", trim(row.getDeptName()));
+ continue;
+ }
+ String key = row.getDeptId() + "|" + posName;
+ if (!positionMap.containsKey(key)) {
+ missingPositionRows.putIfAbsent(posName, row.getRowIndex());
+ missingDeptName.putIfAbsent(posName, trim(row.getDeptName()));
+ }
+ }
+ if (!missingPositionRows.isEmpty()) {
+ String detail = missingPositionRows.entrySet().stream()
+ .map(e -> e.getKey() + "(部门:" + missingDeptName.get(e.getKey()) + ",第" + e.getValue() + "行)")
+ .collect(Collectors.joining("; "));
+ throw new BizException(ErrorCode.ORG_PERSONNEL_IMPORT_POSITION_NOT_FOUND,
+ ErrorCode.ORG_PERSONNEL_IMPORT_POSITION_NOT_FOUND.getMessage() + ":" + detail);
+ }
+
+ // 校验通过,回填岗位ID
+ for (OrgPersonnelImportRow row : rows) {
+ String key = row.getDeptId() + "|" + trim(row.getPositionName());
+ row.setPostId(positionMap.get(key).getId());
+ }
+ }
+
+ private Set batchQueryExistingAccounts(List rows, Long orgId) {
+ Set accounts = rows.stream()
+ .map(r -> trim(r.getAccount()))
+ .filter(StringUtils::hasText)
+ .collect(Collectors.toSet());
+ if (accounts.isEmpty()) {
+ return new HashSet<>();
+ }
+ return orgPersonnelGateway.listByOrgIdAndAccounts(orgId, accounts).stream()
+ .map(OrgPersonnelEntity::getAccount)
+ .filter(StringUtils::hasText)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * 行数据转换为实体;枚举无法识别等校验失败时记录异常并返回 null。
+ */
+ private OrgPersonnelEntity buildEntity(OrgPersonnelImportRow row, Long orgId, List errors) {
+ OrgPersonnelEntity entity = new OrgPersonnelEntity();
+ entity.setOrgId(orgId);
+ entity.setDeptId(row.getDeptId());
+ entity.setPostId(row.getPostId());
+ entity.setUserName(trim(row.getUserName()));
+ entity.setAccount(trim(row.getAccount()));
+ entity.setIdCardNo(trim(row.getIdCardNo()));
+ entity.setBirthDate(parseDate(trim(row.getBirthDate())));
+ entity.setJoinWorkDate(parseDate(trim(row.getJoinWorkDate())));
+ entity.setGraduateSchool(trim(row.getGraduateSchool()));
+ entity.setMajor(trim(row.getMajor()));
+ entity.setCurrentAddress(trim(row.getCurrentAddress()));
+ entity.setOfficeAddress(trim(row.getOfficeAddress()));
+ entity.setWorkExperience(trim(row.getWorkExperience()));
+ entity.setPublications(trim(row.getPublications()));
+ entity.setAbilityDeclaration(trim(row.getAbilityDeclaration()));
+
+ // 性别:严格转换
+ String genderText = trim(row.getGender());
+ if (StringUtils.hasText(genderText)) {
+ ImportGenderEnum gender = ImportGenderEnum.fromText(genderText);
+ if (gender == null) {
+ errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), entity.getAccount(), entity.getUserName(),
+ "性别[" + genderText + "]无法识别,跳过该行"));
+ return null;
+ }
+ entity.setGenderCode(gender.getCode());
+ entity.setGenderName(gender.getName());
+ }
+
+ // 是否注册安全工程师:严格转换
+ String registerText = trim(row.getRegisterEngineer());
+ if (StringUtils.hasText(registerText)) {
+ ImportRegisterEngineerEnum register = ImportRegisterEngineerEnum.fromText(registerText);
+ if (register == null) {
+ errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), entity.getAccount(), entity.getUserName(),
+ "是否注册安全工程师[" + registerText + "]无法识别,跳过该行"));
+ return null;
+ }
+ entity.setRegisterEngineerFlag(register.getCode());
+ }
+
+ // 学历:兜底字典映射,匹配不到则原样存名称
+ String educationText = trim(row.getEducation());
+ if (StringUtils.hasText(educationText)) {
+ ImportEducationEnum education = ImportEducationEnum.match(educationText);
+ if (education != null) {
+ entity.setEducationCode(education.getEducationCode());
+ entity.setEducationName(education.getEducationName());
+ entity.setEducationLevelCode(education.getEducationLevelCode());
+ entity.setEducationLevelName(education.getEducationLevelName());
+ } else {
+ entity.setEducationName(educationText);
+ }
+ }
+
+ // 职称:无字典,按分隔符拆分后原样存入(保留信息,待后续字典完善)
+ entity.setTitleCodeArr(parseTitles(trim(row.getTitle())));
+ return entity;
+ }
+
+ private List parseTitles(String title) {
+ if (!StringUtils.hasText(title)) {
+ return null;
+ }
+ List list = Arrays.stream(title.split("[,,;;、]"))
+ .map(String::trim)
+ .filter(StringUtils::hasText)
+ .map(t -> {
+ TitleCodeCo co = new TitleCodeCo();
+ co.setTitleCode(t);
+ return co;
+ })
+ .collect(Collectors.toList());
+ return list.isEmpty() ? null : list;
+ }
+
+ private LocalDate parseDate(String text) {
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ try {
+ return LocalDate.parse(text, DATE_FORMATTER);
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ private String trim(String text) {
+ return text == null ? null : text.trim();
+ }
+}
diff --git a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/OrgPersonnelImportRow.java b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/OrgPersonnelImportRow.java
new file mode 100644
index 0000000..ab453f9
--- /dev/null
+++ b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/support/OrgPersonnelImportRow.java
@@ -0,0 +1,82 @@
+package org.qinan.safetyeval.app.support;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+/**
+ * 人员导入 Excel 行模型(按列下标映射,避免表头存在重名列)。
+ *
+ * 列顺序:部门 / 岗位 / 账号(手机号) / 姓名 / 性别 / 出生日期 / 身份证号 /
+ * 学历 / 毕业院校 / 所学专业 / 职称 / 参加工作时间 / 是否注册安全工程师 /
+ * 现住地址 / 办公地址 / 主要学习工作经历 / 出版学术专著、专利、获奖、发表学术论文等 /
+ * 自我申报的专业能力及认定方式。
+ *
+ *
+ * @author safety-eval
+ */
+@Data
+public class OrgPersonnelImportRow {
+
+ @ExcelProperty(value = "部门", index = 0)
+ private String deptName;
+
+ @ExcelProperty(value = "岗位", index = 1)
+ private String positionName;
+
+ @ExcelProperty(value = "账号", index = 2)
+ private String account;
+
+ @ExcelProperty(value = "姓名", index = 3)
+ private String userName;
+
+ @ExcelProperty(value = "性别", index = 4)
+ private String gender;
+
+ @ExcelProperty(value = "出生日期", index = 5)
+ private String birthDate;
+
+ @ExcelProperty(value = "身份证号", index = 6)
+ private String idCardNo;
+
+ @ExcelProperty(value = "学历", index = 7)
+ private String education;
+
+ @ExcelProperty(value = "毕业院校", index = 8)
+ private String graduateSchool;
+
+ @ExcelProperty(value = "所学专业", index = 9)
+ private String major;
+
+ @ExcelProperty(value = "职称", index = 10)
+ private String title;
+
+ @ExcelProperty(value = "参加工作时间", index = 11)
+ private String joinWorkDate;
+
+ @ExcelProperty(value = "是否注册安全工程师", index = 12)
+ private String registerEngineer;
+
+ @ExcelProperty(value = "现住地址", index = 13)
+ private String currentAddress;
+
+ @ExcelProperty(value = "办公地址", index = 14)
+ private String officeAddress;
+
+ @ExcelProperty(value = "主要学习工作经历", index = 15)
+ private String workExperience;
+
+ @ExcelProperty(value = "出版学术专著、专利、获奖、发表学术论文等", index = 16)
+ private String publications;
+
+ @ExcelProperty(value = "自我申报的专业能力及认定方式", index = 17)
+ private String abilityDeclaration;
+
+ /** EasyExcel 解析时记录的物理行号(从1开始,含表头),由导入器填充。 */
+ private Integer rowIndex;
+
+ /** 校验通过后回填的部门ID(非Excel列)。 */
+ private Long deptId;
+
+ /** 校验通过后回填的岗位ID(非Excel列)。 */
+ private Long postId;
+}
diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/OrgPersonnelApi.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/OrgPersonnelApi.java
index d7830b7..929b855 100644
--- a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/OrgPersonnelApi.java
+++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/OrgPersonnelApi.java
@@ -1,6 +1,7 @@
package org.qinan.safetyeval.client.api;
import org.qinan.safetyeval.client.co.OrgPersonnelCO;
+import org.qinan.safetyeval.client.co.OrgPersonnelImportResultCO;
import org.qinan.safetyeval.client.dto.PageResponse;
import org.qinan.safetyeval.client.dto.SingleResponse;
import org.qinan.safetyeval.client.dto.OrgPersonnelAddCmd;
@@ -8,6 +9,7 @@ import org.qinan.safetyeval.client.dto.OrgPersonnelCompareFaceCmd;
import org.qinan.safetyeval.client.dto.OrgPersonnelModifyCmd;
import org.qinan.safetyeval.client.dto.OrgPersonnelPageQuery;
import org.qinan.safetyeval.client.dto.OrgPersonnelUpdateOriginFaceCmd;
+import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
@@ -33,4 +35,12 @@ public interface OrgPersonnelApi {
SingleResponse updateOriginFace(OrgPersonnelUpdateOriginFaceCmd cmd);
SingleResponse compareFace(OrgPersonnelCompareFaceCmd cmd);
+
+ /**
+ * 通过 EasyExcel 批量导入人员信息。
+ *
+ * @param file Excel 文件
+ * @return 导入结果(成功条数与异常明细)
+ */
+ SingleResponse importPersonnel(MultipartFile file);
}
diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/OrgPersonnelImportErrorCO.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/OrgPersonnelImportErrorCO.java
new file mode 100644
index 0000000..09a13df
--- /dev/null
+++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/OrgPersonnelImportErrorCO.java
@@ -0,0 +1,35 @@
+package org.qinan.safetyeval.client.co;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 人员导入异常消息
+ *
+ * @author safety-eval
+ */
+@Data
+public class OrgPersonnelImportErrorCO {
+
+ @ApiModelProperty(value = "Excel行号(从1开始的物理行号)")
+ private Integer rowIndex;
+
+ @ApiModelProperty(value = "账号(手机号)")
+ private String account;
+
+ @ApiModelProperty(value = "姓名")
+ private String userName;
+
+ @ApiModelProperty(value = "异常原因")
+ private String errorMessage;
+
+ public OrgPersonnelImportErrorCO() {
+ }
+
+ public OrgPersonnelImportErrorCO(Integer rowIndex, String account, String userName, String errorMessage) {
+ this.rowIndex = rowIndex;
+ this.account = account;
+ this.userName = userName;
+ this.errorMessage = errorMessage;
+ }
+}
diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/OrgPersonnelImportResultCO.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/OrgPersonnelImportResultCO.java
new file mode 100644
index 0000000..b89d77b
--- /dev/null
+++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/OrgPersonnelImportResultCO.java
@@ -0,0 +1,25 @@
+package org.qinan.safetyeval.client.co;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 人员导入结果
+ *
+ * @author safety-eval
+ */
+@Data
+public class OrgPersonnelImportResultCO {
+
+ @ApiModelProperty(value = "成功导入条数")
+ private Integer successCount;
+
+ @ApiModelProperty(value = "失败/跳过条数")
+ private Integer failCount;
+
+ @ApiModelProperty(value = "异常明细")
+ private List errors = new ArrayList<>();
+}
diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/major/request/BasicDisciplineMajorPageQry.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/major/request/BasicDisciplineMajorPageQry.java
index 65432de..a928f01 100644
--- a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/major/request/BasicDisciplineMajorPageQry.java
+++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/major/request/BasicDisciplineMajorPageQry.java
@@ -40,10 +40,4 @@ public class BasicDisciplineMajorPageQry extends PageQuery {
*/
@ApiModelProperty(value = "专业名称")
private String professionalName;
- /**
- * 环境
- */
- @ApiModelProperty(value = "环境")
- private String env;
-
}
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/exception/ErrorCode.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/exception/ErrorCode.java
index fcb1599..e84c413 100644
--- a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/exception/ErrorCode.java
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/exception/ErrorCode.java
@@ -29,6 +29,11 @@ public enum ErrorCode {
ORG_PERSONNEL_FACE_ORIGIN_EMPTY("01-05-004", "人员未配置原始人像,无法比对"),
ORG_PERSONNEL_FACE_COMPARE_EMPTY("01-05-005", "待比对图片不能为空"),
ORG_PERSONNEL_FACE_COMPARE_FAILED("01-05-006", "人脸比对失败"),
+ // ---- 人员导入 ----
+ ORG_PERSONNEL_IMPORT_EMPTY("01-05-007", "导入文件没有有效数据"),
+ ORG_PERSONNEL_IMPORT_PARSE_ERROR("01-05-008", "导入文件解析失败"),
+ ORG_PERSONNEL_IMPORT_DEPT_NOT_FOUND("01-05-009", "导入文件中存在不存在的部门"),
+ ORG_PERSONNEL_IMPORT_POSITION_NOT_FOUND("01-05-010", "导入文件中存在不存在的岗位"),
// ---- 人员证书 ----
ORG_PERSONNEL_CERT_NOT_FOUND("01-06-001", "人员证书不存在"),
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgDepartmentGateway.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgDepartmentGateway.java
index c59f59a..f810d7f 100644
--- a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgDepartmentGateway.java
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgDepartmentGateway.java
@@ -4,6 +4,7 @@ import org.qinan.safetyeval.domain.entity.OrgDepartmentEntity;
import org.qinan.safetyeval.domain.query.OrgDepartmentQuery;
import org.qinan.safetyeval.domain.query.PageResult;
+import java.util.Collection;
import java.util.List;
/**
@@ -24,4 +25,9 @@ public interface OrgDepartmentGateway {
PageResult page(OrgDepartmentQuery query);
List list(OrgDepartmentQuery query);
+
+ /**
+ * 按机构ID + 部门名称集合批量精确查询未删除部门(用于导入名称解析)。
+ */
+ List listByOrgIdAndNames(Long orgId, Collection deptNames);
}
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgPersonnelGateway.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgPersonnelGateway.java
index 4fb37cd..48bff96 100644
--- a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgPersonnelGateway.java
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgPersonnelGateway.java
@@ -4,6 +4,8 @@ import org.qinan.safetyeval.domain.entity.OrgPersonnelEntity;
import org.qinan.safetyeval.domain.query.OrgPersonnelQuery;
import org.qinan.safetyeval.domain.query.PageResult;
+import java.util.Collection;
+
/**
* 人员信息Gateway接口(Domain层定义)
*
@@ -35,4 +37,9 @@ public interface OrgPersonnelGateway {
* 查询岗位下未删除人员(post_id = positionId)。
*/
java.util.List listByPostId(Long postId);
+
+ /**
+ * 按机构ID + 账号集合批量查询未删除人员(用于导入手机号重复校验)。
+ */
+ java.util.List listByOrgIdAndAccounts(Long orgId, Collection accounts);
}
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgPositionGateway.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgPositionGateway.java
index 030d2d5..841c685 100644
--- a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgPositionGateway.java
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/OrgPositionGateway.java
@@ -4,6 +4,9 @@ import org.qinan.safetyeval.domain.entity.OrgPositionEntity;
import org.qinan.safetyeval.domain.query.OrgPositionQuery;
import org.qinan.safetyeval.domain.query.PageResult;
+import java.util.Collection;
+import java.util.List;
+
/**
* 岗位Gateway接口(Domain层定义)
*
@@ -20,4 +23,9 @@ public interface OrgPositionGateway {
void delete(Long id);
PageResult page(OrgPositionQuery query);
+
+ /**
+ * 按机构ID + 部门ID集合 + 岗位名称集合批量精确查询未删除岗位(用于导入名称解析)。
+ */
+ List listByOrgIdAndDeptIdsAndNames(Long orgId, Collection deptIds, Collection positionNames);
}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgDepartmentGatewayImpl.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgDepartmentGatewayImpl.java
index 238aa77..9cf38af 100644
--- a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgDepartmentGatewayImpl.java
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgDepartmentGatewayImpl.java
@@ -15,6 +15,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
+import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@@ -80,6 +81,22 @@ public class OrgDepartmentGatewayImpl implements OrgDepartmentGateway {
.collect(Collectors.toList());
}
+ @Override
+ public List listByOrgIdAndNames(Long orgId, Collection deptNames) {
+ if (deptNames == null || deptNames.isEmpty()) {
+ return java.util.Collections.emptyList();
+ }
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(OrgDepartmentDO::getDeleteEnum, "false");
+ if (orgId != null) {
+ wrapper.eq(OrgDepartmentDO::getOrgId, orgId);
+ }
+ wrapper.in(OrgDepartmentDO::getDeptName, deptNames);
+ return orgDepartmentMapper.selectList(wrapper).stream()
+ .map(this::toEntity)
+ .collect(Collectors.toList());
+ }
+
private LambdaQueryWrapper buildQueryWrapper(OrgDepartmentQuery query) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrgDepartmentDO::getDeleteEnum, "false");
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgPersonnelGatewayImpl.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgPersonnelGatewayImpl.java
index 25ee078..49c940d 100644
--- a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgPersonnelGatewayImpl.java
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgPersonnelGatewayImpl.java
@@ -18,6 +18,8 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
/**
@@ -188,5 +190,19 @@ public class OrgPersonnelGatewayImpl implements OrgPersonnelGateway {
return orgPersonnelDoConvertor.converDosToEes(orgPersonnelMapper.selectList(wrapper));
}
+ @Override
+ public List listByOrgIdAndAccounts(Long orgId, Collection accounts) {
+ if (accounts == null || accounts.isEmpty()) {
+ return Collections.emptyList();
+ }
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(OrgPersonnelDO::getDeleteEnum, "false");
+ if (orgId != null) {
+ wrapper.eq(OrgPersonnelDO::getOrgId, orgId);
+ }
+ wrapper.in(OrgPersonnelDO::getAccount, accounts);
+ return orgPersonnelDoConvertor.converDosToEes(orgPersonnelMapper.selectList(wrapper));
+ }
+
}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgPositionGatewayImpl.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgPositionGatewayImpl.java
index d12a570..dd5ea1d 100644
--- a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgPositionGatewayImpl.java
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/OrgPositionGatewayImpl.java
@@ -15,6 +15,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
+import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@@ -99,6 +100,27 @@ public class OrgPositionGatewayImpl implements OrgPositionGateway {
return PageResult.of(entities, result.getTotal(), result.getCurrent(), result.getSize());
}
+ @Override
+ public List listByOrgIdAndDeptIdsAndNames(Long orgId, Collection deptIds, Collection positionNames) {
+ if ((deptIds == null || deptIds.isEmpty()) && (positionNames == null || positionNames.isEmpty())) {
+ return java.util.Collections.emptyList();
+ }
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(OrgPositionDO::getDeleteEnum, "false");
+ if (orgId != null) {
+ wrapper.eq(OrgPositionDO::getOrgId, orgId);
+ }
+ if (deptIds != null && !deptIds.isEmpty()) {
+ wrapper.in(OrgPositionDO::getDeptId, deptIds);
+ }
+ if (positionNames != null && !positionNames.isEmpty()) {
+ wrapper.in(OrgPositionDO::getPositionName, positionNames);
+ }
+ return orgPositionMapper.selectList(wrapper).stream()
+ .map(this::toEntity)
+ .collect(Collectors.toList());
+ }
+
private OrgPositionDO toDO(OrgPositionEntity entity) {
OrgPositionDO dataObject = new OrgPositionDO();
dataObject.setOrgId(orgContextResolver.resolveOrgId(entity.getOrgId()));
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/persistence/queryservice/impl/BasicDisciplineMajorQueryServiceImpl.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/persistence/queryservice/impl/BasicDisciplineMajorQueryServiceImpl.java
index 1f07290..2ec0d8d 100644
--- a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/persistence/queryservice/impl/BasicDisciplineMajorQueryServiceImpl.java
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/persistence/queryservice/impl/BasicDisciplineMajorQueryServiceImpl.java
@@ -34,7 +34,8 @@ public class BasicDisciplineMajorQueryServiceImpl implements BasicDisciplineMajo
@Override
public PageResponse listBasicDisciplineMajorPage(BasicDisciplineMajorPageQry basicDisciplineMajorPageQry) {
- IPage basicDisciplineMajorDoPage = basicDisciplineMajorRepository.listBasicDisciplineMajorPage(PageHelper.queryToPage(basicDisciplineMajorPageQry), basicDisciplineMajorPageQry);
+ IPage basicDisciplineMajorDoPage = basicDisciplineMajorRepository.listBasicDisciplineMajorPage(
+ PageHelper.queryToPage(basicDisciplineMajorPageQry), basicDisciplineMajorPageQry);
List basicDisciplineMajorCoS = basicDisciplineMajorDoConvertor.converDosToCos(basicDisciplineMajorDoPage.getRecords());
return PageHelper.pageToResponse(basicDisciplineMajorDoPage, basicDisciplineMajorCoS);
}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/persistence/repository/impl/BasicDisciplineMajorRepositoryImpl.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/persistence/repository/impl/BasicDisciplineMajorRepositoryImpl.java
index 876e968..d5e1151 100644
--- a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/persistence/repository/impl/BasicDisciplineMajorRepositoryImpl.java
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/persistence/repository/impl/BasicDisciplineMajorRepositoryImpl.java
@@ -1,12 +1,13 @@
package org.qinan.safetyeval.infrastructure.persistence.repository.impl;
import cn.hutool.core.util.ObjectUtil;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.jjb.saas.framework.enums.enums.BooleanEnum;
import com.jjb.saas.framework.repository.repo.impl.BaseRepositoryImpl;
import lombok.AllArgsConstructor;
import org.qinan.safetyeval.client.major.request.BasicDisciplineMajorPageQry;
+import org.qinan.safetyeval.domain.constant.BooleanQAEnum;
import org.qinan.safetyeval.infrastructure.persistence.domainobject.BasicDisciplineMajorDO;
import org.qinan.safetyeval.infrastructure.persistence.mapper.BasicDisciplineMajorMapper;
import org.qinan.safetyeval.infrastructure.persistence.repository.BasicDisciplineMajorRepository;
@@ -33,24 +34,22 @@ public class BasicDisciplineMajorRepositoryImpl extends BaseRepositoryImpl listBasicDisciplineMajorPage(IPage iPage, BasicDisciplineMajorPageQry basicDisciplineMajorPageQry) {
- QueryWrapper basicDisciplineMajorDoQueryWrapper = new QueryWrapper<>();
- if (ObjectUtil.isNotEmpty(basicDisciplineMajorPageQry.getIndustryCode())) {
- basicDisciplineMajorDoQueryWrapper.eq("industry_code", basicDisciplineMajorPageQry.getIndustryCode());
- }
- if (ObjectUtil.isNotEmpty(basicDisciplineMajorPageQry.getProfessionalCapabilityCode())) {
- basicDisciplineMajorDoQueryWrapper.eq("professional_capability_code", basicDisciplineMajorPageQry.getProfessionalCapabilityCode());
- }
- if (ObjectUtil.isNotEmpty(basicDisciplineMajorPageQry.getProfessionalCode())) {
- basicDisciplineMajorDoQueryWrapper.eq("professional_code", basicDisciplineMajorPageQry.getProfessionalCode());
- }
- if (ObjectUtil.isNotEmpty(basicDisciplineMajorPageQry.getProfessionalName())) {
- basicDisciplineMajorDoQueryWrapper.like("professional_name", basicDisciplineMajorPageQry.getProfessionalName());
- }
- if (ObjectUtil.isNotEmpty(basicDisciplineMajorPageQry.getEnv())) {
- basicDisciplineMajorDoQueryWrapper.eq("env", basicDisciplineMajorPageQry.getEnv());
- }
- basicDisciplineMajorDoQueryWrapper.eq("delete_enum", BooleanEnum.FALSE.getValue());
- return basicDisciplineMajorMapper.selectPage(iPage, basicDisciplineMajorDoQueryWrapper);
+ LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
+ queryWrapper.select(BasicDisciplineMajorDO::getId,
+ BasicDisciplineMajorDO::getIndustryCode,
+ BasicDisciplineMajorDO::getProfessionalCapabilityCode,
+ BasicDisciplineMajorDO::getProfessionalCode,
+ BasicDisciplineMajorDO::getProfessionalName)
+ .like(ObjectUtil.isNotEmpty(basicDisciplineMajorPageQry.getIndustryCode()),
+ BasicDisciplineMajorDO::getIndustryCode, basicDisciplineMajorPageQry.getIndustryCode())
+ .like(ObjectUtil.isNotEmpty(basicDisciplineMajorPageQry.getProfessionalCapabilityCode()),
+ BasicDisciplineMajorDO::getProfessionalCapabilityCode, basicDisciplineMajorPageQry.getProfessionalCapabilityCode())
+ .eq(ObjectUtil.isNotEmpty(basicDisciplineMajorPageQry.getProfessionalCode()),
+ BasicDisciplineMajorDO::getProfessionalCode, basicDisciplineMajorPageQry.getProfessionalCode())
+ .like(ObjectUtil.isNotEmpty(basicDisciplineMajorPageQry.getProfessionalName()),
+ BasicDisciplineMajorDO::getProfessionalName, basicDisciplineMajorPageQry.getProfessionalName())
+ .eq(BasicDisciplineMajorDO::getDeleteEnum, BooleanQAEnum.FALSE.getValue());
+ return basicDisciplineMajorMapper.selectPage(iPage, queryWrapper);
}
@Override