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 index 8c7faad2..7e4dc6dc 100644 --- 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 @@ -1,6 +1,7 @@ package org.qinan.safetyeval.app.support; import cn.hutool.core.lang.Pair; +import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdcardUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.excel.EasyExcel; @@ -11,11 +12,10 @@ import com.zcloud.gbscommon.utils.MD5; import com.zcloud.gbscommon.utils.Sm2Util; import org.qinan.safetyeval.client.co.OrgPersonnelImportErrorCO; import org.qinan.safetyeval.client.co.OrgPersonnelImportResultCO; -import org.qinan.safetyeval.domain.constant.QualificationLevelEnum; import org.qinan.safetyeval.domain.entity.OrgDepartmentEntity; +import org.qinan.safetyeval.domain.entity.OrgPersonnelCertEntity; 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; @@ -38,14 +38,15 @@ import javax.annotation.Resource; import javax.validation.ConstraintViolation; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; import java.util.*; import java.util.stream.Collectors; /** * 基于 EasyExcel 的人员信息批量导入器。 *

- * 解析 Excel -> 批量校验部门/岗位名称并回填ID -> 校验账号重复并收集异常 -> 逐行落库。 + * 模板包含两个 sheet:sheet1「专职人员基础信息」、sheet2「专职人员能力信息」, + * 解析后按 身份证号(为空时按姓名) 将能力信息关联到基础信息行; + * 再批量校验部门/岗位名称并回填ID -> 校验账号重复并收集异常 -> 逐行落库。 *

* * @author safety-eval @@ -55,10 +56,6 @@ public class OrgPersonnelExcelImporter { private static final Logger LOGGER = LoggerFactory.getLogger(OrgPersonnelExcelImporter.class); - private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - private static final DateTimeFormatter SLASH_DATE_FORMATTER = - new DateTimeFormatterBuilder().appendPattern("yyyy/M/d").toFormatter(); - @Resource private OrgDepartmentGateway orgDepartmentGateway; @@ -80,6 +77,8 @@ public class OrgPersonnelExcelImporter { @Resource private SystemRemote systemRemote; + public static final String YES = "是"; + @Value("${def.password:a123456}") private String defPassword; @Value("${def.publicKey:0402df2195296d4062ac85ad766994d73e871b887e18efb9a9a06b4cebc72372869b7da6c347c129dee2b46a0f279ff066b01c76208c2a052af75977c722a2ccee}") @@ -137,29 +136,32 @@ public class OrgPersonnelExcelImporter { // 5. 逐行转换、落库 int successCount = 0; + for (OrgPersonnelImportRow row : rows) { - String account = trim(row.getAccount()); + String account = trim(row.getBasic().getAccount()); // 账号在库中已存在 -> 跳过该行,记录异常 if (existingAccounts.contains(account)) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), account, trim(row.getUserName()), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), account, trim(row.getBasic().getUserName()), "账号已存在,跳过该行")); continue; } try { - OrgPersonnelEntity entity = buildEntity(row, orgId, errors); - if (entity == null) { + OrgPersonnelEntity personnelEntity = buildOrgPersonnel(row, orgId, errors); + if (personnelEntity == null) { // 行内枚举/数据校验未通过,已记录异常 continue; } - Long userId = addUserIfAbsentAndAppendRole(entity); - entity.setId(userId); - orgPersonnelDomainService.add(entity); + List personnelCertEntities = buildOrgPersonnelCert(row, orgId, errors, personnelEntity); + + Long userId = addUserIfAbsentAndAppendRole(personnelEntity); + personnelEntity.setId(userId); + orgPersonnelDomainService.add(personnelEntity); successCount++; } catch (Exception ex) { - LOGGER.warn("导入人员落库失败, rowIndex={}, account={}", row.getRowIndex(), account, ex); - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), account, trim(row.getUserName()), + LOGGER.warn("导入人员落库失败, rowIndex={}, account={}", row.getBasic().getRowIndex(), account, ex); + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), account, trim(row.getBasic().getUserName()), "保存失败:" + ex.getMessage())); } } @@ -169,6 +171,19 @@ public class OrgPersonnelExcelImporter { return result; } + /** + * + * @param row row + * @param orgId orgId + * @param errors errors + * @param personnelEntity personnelEntity + * @return OrgPersonnelCertEntity + */ + private List buildOrgPersonnelCert(OrgPersonnelImportRow row + , Long orgId, List errors, OrgPersonnelEntity personnelEntity) { + return null; + } + /** * addUserIfAbsentAndAppendRole * @@ -192,40 +207,56 @@ public class OrgPersonnelExcelImporter { } /** - * 解析 Excel,过滤空行,并回填物理行号(从1开始,含表头,首条数据为第2行)。 + * 解析 Excel:分别读取 sheet1「专职人员基础信息」与 sheet2「专职人员能力信息」, + * 能力信息按 身份证号(为空时按姓名) 关联到基础信息行; + * 过滤空行并回填物理行号(从1开始,含表头,首条数据为第2行)。 *

* 文件内手机号重复的行在此剔除,并生成提示放入 errors(空账号不参与去重)。 *

*/ private List parseRows(MultipartFile file, List errors) { - List raw; + List basicRaw; + List capabilityRaw; try { - raw = EasyExcel.read(file.getInputStream()) - .head(OrgPersonnelImportRow.class) - .sheet() + // sheet1:专职人员基础信息 + basicRaw = EasyExcel.read(file.getInputStream()) + .head(OrgPersonnelImportRow.BasicRow.class) + .sheet(0) + .headRowNumber(1) + .doReadSync(); + // sheet2:专职人员能力信息 + capabilityRaw = EasyExcel.read(file.getInputStream()) + .head(OrgPersonnelImportRow.CapabilityRow.class) + .sheet(1) .headRowNumber(1) .doReadSync(); } catch (Exception ex) { LOGGER.error("解析人员导入Excel失败", ex); throw new BizException(ErrorCode.ORG_PERSONNEL_IMPORT_PARSE_ERROR); } - if (raw == null || raw.isEmpty()) { + if (basicRaw == null || basicRaw.isEmpty()) { return new ArrayList<>(); } - List rows = new ArrayList<>(raw.size()); + + Map capabilityMap = indexCapabilityRows(capabilityRaw); + + List rows = new ArrayList<>(basicRaw.size()); Set seenAccounts = new HashSet<>(); - for (int i = 0; i < raw.size(); i++) { - OrgPersonnelImportRow row = raw.get(i); - if (row == null) { + for (int i = 0; i < basicRaw.size(); i++) { + OrgPersonnelImportRow.BasicRow basic = basicRaw.get(i); + if (basic == null) { continue; } // 空行:账号与姓名均为空 - if (!StringUtils.hasText(trim(row.getAccount())) && !StringUtils.hasText(trim(row.getUserName()))) { + if (!StringUtils.hasText(trim(basic.getAccount())) && !StringUtils.hasText(trim(basic.getUserName()))) { continue; } - row.setRowIndex(i + 2); + OrgPersonnelImportRow row = new OrgPersonnelImportRow(); + basic.setRowIndex(i + 2); + row.setBasic(basic); + row.setCapability(matchCapability(basic, capabilityMap)); - // 字段校验:JSR-303 注解(必填、长度、手机号格式)+ 身份证号 + 日期格式 + // 字段校验:JSR-303 注解(必填、长度、手机号格式)+ 身份证号 if (!validateRowFields(row, errors)) { continue; } @@ -238,13 +269,69 @@ public class OrgPersonnelExcelImporter { return rows; } + /** + * sheet2 能力信息建立索引:优先身份证号,身份证号为空时按姓名;重复时保留首条并告警。 + */ + private Map indexCapabilityRows( + List capabilityRaw) { + Map map = new LinkedHashMap<>(); + if (capabilityRaw == null || capabilityRaw.isEmpty()) { + return map; + } + for (int i = 0; i < capabilityRaw.size(); i++) { + OrgPersonnelImportRow.CapabilityRow capability = capabilityRaw.get(i); + if (capability == null) { + continue; + } + // 空行:姓名与身份证号均为空 + if (!StringUtils.hasText(trim(capability.getIdCardNo())) + && !StringUtils.hasText(trim(capability.getUserName()))) { + continue; + } + capability.setRowIndex(i + 2); + String key = capabilityKey(capability.getIdCardNo(), capability.getUserName()); + if (map.putIfAbsent(key, capability) != null) { + LOGGER.warn("人员导入能力信息重复,key={},sheet2行号={},仅保留首条", key, capability.getRowIndex()); + } + } + return map; + } + + /** + * 基础信息行匹配能力信息:优先身份证号,其次姓名;未匹配返回 null。 + */ + private OrgPersonnelImportRow.CapabilityRow matchCapability(OrgPersonnelImportRow.BasicRow basic, + Map capabilityMap) { + if (capabilityMap.isEmpty()) { + return null; + } + OrgPersonnelImportRow.CapabilityRow capability = + capabilityMap.get(capabilityKey(basic.getIdCardNo(), null)); + if (capability != null) { + return capability; + } + return capabilityMap.get(capabilityKey(null, basic.getUserName())); + } + + /** + * 能力信息索引键:身份证号优先(前缀 ID:),为空时使用姓名(前缀 NAME:)。 + */ + private String capabilityKey(String idCardNo, String userName) { + String idCard = trim(idCardNo); + if (StringUtils.hasText(idCard)) { + return "ID:" + idCard; + } + return "NAME:" + trim(userName); + } + /** * 字段级校验: *
    - *
  1. JSR-303 注解校验({@link OrgPersonnelImportRow} 字段上的 @NotBlank/@Size/@Pattern), + *
  2. JSR-303 注解校验({@link OrgPersonnelImportRow.BasicRow} 字段上的 @NotBlank/@Size/@Pattern), * 覆盖必填、长度、手机号格式;
  3. - *
  4. 身份证号:使用 {@link IdcardUtil} 校验校验位与出生日期合法性;
  5. - *
  6. 日期字段:兼容 yyyy-MM-dd / yyyy/M/d 格式校验。
  7. + *
  8. sheet2 能力信息({@link OrgPersonnelImportRow.CapabilityRow})JSR-303 注解校验 + + * 身份证号格式校验(未匹配到能力信息时跳过),行号取 sheet2 行号;
  9. + *
  10. 身份证号:使用 {@link IdcardUtil} 校验校验位与出生日期合法性。
  11. *
* * @param row 当前行 @@ -252,12 +339,12 @@ public class OrgPersonnelExcelImporter { * @return true 通过,false 失败(已记录异常) */ private boolean validateRowFields(OrgPersonnelImportRow row, List errors) { - Integer rowIndex = row.getRowIndex(); - String account = trim(row.getAccount()); - String userName = trim(row.getUserName()); + Integer rowIndex = row.getBasic().getRowIndex(); + String account = trim(row.getBasic().getAccount()); + String userName = trim(row.getBasic().getUserName()); - // 1. JSR-303 注解校验:每字段仅取第一条违规,避免 @NotBlank+@Pattern 重复报错 - Set> violations = validator.validate(row); + // 1. JSR-303 注解校验(sheet1 基础信息):每字段仅取第一条违规,避免 @NotBlank+@Pattern 重复报错 + Set> violations = validator.validate(row.getBasic()); if (!violations.isEmpty()) { violations.stream() .collect(Collectors.toMap( @@ -270,22 +357,40 @@ public class OrgPersonnelExcelImporter { return false; } - // 2. 身份证号格式校验(DB: id_card_no varchar(18)) - String idCardNo = trim(row.getIdCardNo()); + // 2. sheet2 能力信息校验(未匹配到能力信息时跳过),行号取 sheet2 行号,消息加前缀便于区分来源 + OrgPersonnelImportRow.CapabilityRow capability = row.getCapability(); + if (capability != null) { + Set> capabilityViolations = + validator.validate(capability); + if (!capabilityViolations.isEmpty()) { + capabilityViolations.stream() + .collect(Collectors.toMap( + v -> v.getPropertyPath().toString(), + v -> v, + (a, b) -> a)) + .values() + .forEach(v -> errors.add(new OrgPersonnelImportErrorCO(capability.getRowIndex(), + account, userName, "能力信息:" + v.getMessage()))); + return false; + } + + // sheet2 身份证号格式校验(DB: id_card_no varchar(18)) + String capabilityIdCardNo = trim(capability.getIdCardNo()); + if (StringUtils.hasText(capabilityIdCardNo) && !IdcardUtil.isValidCard(capabilityIdCardNo)) { + errors.add(new OrgPersonnelImportErrorCO(capability.getRowIndex(), account, userName, + "能力信息:身份证号格式不正确,应为15或18位有效身份证号")); + return false; + } + } + + // 3. 身份证号格式校验(DB: id_card_no varchar(18)) + String idCardNo = trim(row.getBasic().getIdCardNo()); if (StringUtils.hasText(idCardNo) && !IdcardUtil.isValidCard(idCardNo)) { errors.add(new OrgPersonnelImportErrorCO(rowIndex, account, userName, "身份证号格式不正确,应为15或18位有效身份证号")); return false; } - // 3. 日期格式校验(DB: join_work_date date;出生日期由身份证号解析,不做校验) - String joinWorkDate = trim(row.getJoinWorkDate()); - if (StringUtils.hasText(joinWorkDate) && parseDate(joinWorkDate) == null) { - errors.add(new OrgPersonnelImportErrorCO(rowIndex, account, userName, - "参加工作时间格式不正确,应为yyyy-MM-dd或yyyy/M/d")); - return false; - } - return true; } @@ -299,9 +404,9 @@ public class OrgPersonnelExcelImporter { */ private boolean checkAccountDuplicate(OrgPersonnelImportRow row, Set seenAccounts, List errors) { - String account = trim(row.getAccount()); + String account = trim(row.getBasic().getAccount()); if (StringUtils.hasText(account) && !seenAccounts.add(account)) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), account, trim(row.getUserName()), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), account, trim(row.getBasic().getUserName()), "账号在文件内重复,已剔除该行")); return false; } @@ -322,13 +427,14 @@ public class OrgPersonnelExcelImporter { // 收集所有层级中不为空的部门名称 Set deptNames = new LinkedHashSet<>(); for (OrgPersonnelImportRow row : rows) { - row.setDeptNameLevel3(StrUtil.trim(row.getDeptNameLevel3())); - row.setDeptNameLevel2(StrUtil.trim(row.getDeptNameLevel2())); - row.setDeptNameLevel1(StrUtil.trim(row.getDeptNameLevel1())); + OrgPersonnelImportRow.BasicRow basic = row.getBasic(); + basic.setDeptNameLevel3(StrUtil.trim(basic.getDeptNameLevel3())); + basic.setDeptNameLevel2(StrUtil.trim(basic.getDeptNameLevel2())); + basic.setDeptNameLevel1(StrUtil.trim(basic.getDeptNameLevel1())); - collectDeptName(deptNames, row.getDeptNameLevel1()); - collectDeptName(deptNames, row.getDeptNameLevel2()); - collectDeptName(deptNames, row.getDeptNameLevel3()); + collectDeptName(deptNames, basic.getDeptNameLevel1()); + collectDeptName(deptNames, basic.getDeptNameLevel2()); + collectDeptName(deptNames, basic.getDeptNameLevel3()); } List orgDepartmentList = orgDepartmentGateway.listByOrgIdAndNames(orgId, deptNames); @@ -360,7 +466,7 @@ public class OrgPersonnelExcelImporter { */ private Pair getL3(List errors, OrgPersonnelImportRow row , Map> nameMapList, OrgDepartmentEntity l2Dept) { - String l3 = row.getDeptNameLevel3(); + String l3 = row.getBasic().getDeptNameLevel3(); if (StrUtil.isBlank(l3)) { return Pair.of(true, null); } @@ -368,7 +474,7 @@ public class OrgPersonnelExcelImporter { // l3 部门查询 List departmentEntities = nameMapList.get(l3); if (CollectionUtils.isEmpty(departmentEntities)) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), ErrorCode.ORG_PERSONNEL_IMPORT_DEPT_L3_NOT_EXIST.getMessage())); row.setValidateFail(true); return Pair.of(false, null); @@ -383,7 +489,7 @@ public class OrgPersonnelExcelImporter { } } - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), l2Dept.getDeptName() + "下" + ErrorCode.ORG_PERSONNEL_IMPORT_DEPT_L3_NOT_EXIST.getMessage())); row.setValidateFail(true); @@ -401,7 +507,7 @@ public class OrgPersonnelExcelImporter { */ private Pair getL2(List errors , OrgPersonnelImportRow row, Map> nameMapList, OrgDepartmentEntity l1Dept) { - String l2 = row.getDeptNameLevel2(); + String l2 = row.getBasic().getDeptNameLevel2(); if (StrUtil.isBlank(l2)) { return Pair.of(true, null); } @@ -409,7 +515,7 @@ public class OrgPersonnelExcelImporter { // l2 部门查询 List departmentEntities = nameMapList.get(l2); if (CollectionUtils.isEmpty(departmentEntities)) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), ErrorCode.ORG_PERSONNEL_IMPORT_DEPT_L2_NOT_EXIST.getMessage())); row.setValidateFail(true); return Pair.of(false, null); @@ -424,7 +530,7 @@ public class OrgPersonnelExcelImporter { } } - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), l1Dept.getDeptName() + "下" + ErrorCode.ORG_PERSONNEL_IMPORT_DEPT_L2_NOT_EXIST.getMessage())); row.setValidateFail(true); return Pair.of(false, null); @@ -439,9 +545,9 @@ public class OrgPersonnelExcelImporter { */ private static Pair getL1(List errors, OrgPersonnelImportRow row , Map> nameMapList) { - String l1 = row.getDeptNameLevel1(); + String l1 = row.getBasic().getDeptNameLevel1(); if (StrUtil.isBlank(l1)) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), ErrorCode.ORG_PERSONNEL_IMPORT_DEPT_L1_NOT_EXIST.getMessage())); row.setValidateFail(true); return Pair.of(false, null); @@ -450,7 +556,7 @@ public class OrgPersonnelExcelImporter { // l1 部门查询 List departmentEntities = nameMapList.get(l1); if (CollectionUtils.isEmpty(departmentEntities)) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), ErrorCode.ORG_PERSONNEL_IMPORT_DEPT_L1_NOT_EXIST.getMessage())); row.setValidateFail(true); return Pair.of(false, null); @@ -458,7 +564,7 @@ public class OrgPersonnelExcelImporter { // 一级部门重复 if (departmentEntities.size() > 1) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), ErrorCode.ORG_PERSONNEL_IMPORT_DEPT_L1_REPEAT.getMessage())); row.setValidateFail(true); return Pair.of(false, null); @@ -484,7 +590,7 @@ public class OrgPersonnelExcelImporter { if (row.getDeptId() != null) { deptIds.add(row.getDeptId()); } - String posName = trim(row.getPositionName()); + String posName = trim(row.getBasic().getPositionName()); if (StringUtils.hasText(posName)) { positionNames.add(posName); } @@ -501,9 +607,9 @@ public class OrgPersonnelExcelImporter { // 逐行校验:岗位名称为空或部门下岗位不存在 -> 记录异常并标记;校验通过则回填岗位ID for (OrgPersonnelImportRow row : rows) { - String posName = trim(row.getPositionName()); + String posName = trim(row.getBasic().getPositionName()); if (!StringUtils.hasText(posName)) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), ErrorCode.ORG_PERSONNEL_IMPORT_POSITION_NOT_FOUND.getMessage() + "(部门:" + row.getDeptDisplayName() + ")")); row.setValidateFail(true); @@ -512,7 +618,7 @@ public class OrgPersonnelExcelImporter { String key = row.getDeptId() + "|" + posName; OrgPositionEntity pos = positionMap.get(key); if (pos == null) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), ErrorCode.ORG_PERSONNEL_IMPORT_POSITION_NOT_FOUND.getMessage() + ":" + posName + "(部门:" + row.getDeptDisplayName() + ")")); row.setValidateFail(true); @@ -529,7 +635,7 @@ public class OrgPersonnelExcelImporter { */ private void validateAndFillMajor(List rows, List errors) { Set majorNames = rows.stream() - .map(r -> trim(r.getMajor())) + .map(r -> r.getCapability() == null ? null : trim(r.getCapability().getMajor())) .filter(StringUtils::hasText) .collect(Collectors.toCollection(LinkedHashSet::new)); @@ -544,13 +650,13 @@ public class OrgPersonnelExcelImporter { // 逐行校验:专业名称为空的行跳过;对照表中不存在的行记录异常并标记;校验通过则回填专业ID for (OrgPersonnelImportRow row : rows) { - String name = trim(row.getMajor()); + String name = row.getCapability() == null ? null : trim(row.getCapability().getMajor()); if (!StringUtils.hasText(name)) { continue; } BasicDisciplineMajorE major = majorMap.get(name); if (major == null) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), row.getAccount(), row.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(row.getBasic().getRowIndex(), row.getBasic().getAccount(), row.getBasic().getUserName(), ErrorCode.ORG_PERSONNEL_IMPORT_MAJOR_NOT_FOUND.getMessage() + ":" + name)); row.setValidateFail(true); continue; @@ -561,7 +667,7 @@ public class OrgPersonnelExcelImporter { private Set batchQueryExistingAccounts(List rows, Long orgId) { Set accounts = rows.stream() - .map(r -> trim(r.getAccount())) + .map(r -> trim(r.getBasic().getAccount())) .filter(StringUtils::hasText) .collect(Collectors.toSet()); if (accounts.isEmpty()) { @@ -575,16 +681,20 @@ public class OrgPersonnelExcelImporter { /** * 行数据转换为实体;枚举无法识别等校验失败时记录异常并返回 null。 + *

基础信息取自 sheet1;毕业院校/所学专业/学历/注册安全工程师取自 sheet2 能力信息(可为空)。

*/ - private OrgPersonnelEntity buildEntity(OrgPersonnelImportRow row, Long orgId, List errors) { + private OrgPersonnelEntity buildOrgPersonnel(OrgPersonnelImportRow row, Long orgId, List errors) { + OrgPersonnelImportRow.BasicRow basic = row.getBasic(); + OrgPersonnelImportRow.CapabilityRow capability = row.getCapability(); OrgPersonnelEntity entity = new OrgPersonnelEntity(); + entity.setId(IdUtil.getSnowflakeNextId()); entity.setOrgId(orgId); entity.setDeptId(row.getDeptId()); entity.setPostId(row.getPostId()); entity.setBasicDisciplineMajorId(row.getBasicDisciplineMajorId()); - entity.setUserName(trim(row.getUserName())); - entity.setAccount(trim(row.getAccount())); - String idCardNo = trim(row.getIdCardNo()); + entity.setUserName(trim(basic.getUserName())); + entity.setAccount(trim(basic.getAccount())); + String idCardNo = trim(basic.getIdCardNo()); entity.setIdCardNo(idCardNo); // 出生日期:不再从Excel导入,直接通过身份证号(Hutool)解析 if (StringUtils.hasText(idCardNo)) { @@ -593,21 +703,31 @@ public class OrgPersonnelExcelImporter { entity.setBirthDate(LocalDate.parse(birth, DateTimeFormatter.BASIC_ISO_DATE)); } } - 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())); + entity.setCurrentAddress(trim(basic.getCurrentAddress())); + entity.setOfficeAddress(trim(basic.getOfficeAddress())); + entity.setWorkExperience(trim(basic.getWorkExperience())); + entity.setPublications(trim(basic.getPublications())); + entity.setAbilityDeclaration(trim(basic.getAbilityDeclaration())); + + // sheet2 能力信息(未匹配到能力信息的行为空) + if (capability != null) { + entity.setGraduateSchool(trim(capability.getGraduateSchool())); + entity.setMajor(trim(capability.getMajor())); + // 学历:名称取学历层次,类型/层次分别填入 + entity.setEducationCode(trim(capability.getEducationLevel())); + entity.setEducationName(trim(capability.getEducationLevel())); + entity.setEducationTypeName(trim(capability.getEducationType())); + entity.setEducationTypeCode(trim(capability.getEducationType())); + entity.setEducationLevelName(trim(capability.getEducationLevel())); + entity.setEducationLevelCode(trim(capability.getEducationLevel())); + } // 性别:严格转换 - String genderText = trim(row.getGender()); + String genderText = trim(basic.getGender()); if (StringUtils.hasText(genderText)) { ImportGenderEnum gender = ImportGenderEnum.fromText(genderText); if (gender == null) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), entity.getAccount(), entity.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(basic.getRowIndex(), entity.getAccount(), entity.getUserName(), "性别[" + genderText + "]无法识别,跳过该行")); row.setValidateFail(true); return null; @@ -616,12 +736,12 @@ public class OrgPersonnelExcelImporter { entity.setGenderName(gender.getName()); } - // 是否注册安全工程师:严格转换 - String registerText = trim(row.getRegisterEngineer()); + // 是否注册安全工程师:严格转换(sheet2) + String registerText = capability == null ? null : trim(capability.getRegisterEngineer()); if (StringUtils.hasText(registerText)) { ImportRegisterEngineerEnum register = ImportRegisterEngineerEnum.fromText(registerText); if (register == null) { - errors.add(new OrgPersonnelImportErrorCO(row.getRowIndex(), entity.getAccount(), entity.getUserName(), + errors.add(new OrgPersonnelImportErrorCO(basic.getRowIndex(), entity.getAccount(), entity.getUserName(), "是否注册安全工程师[" + registerText + "]无法识别,跳过该行")); row.setValidateFail(true); return null; @@ -629,64 +749,13 @@ public class OrgPersonnelExcelImporter { entity.setRegisterEngineerFlag(register.getCode()); } - // 学历:兜底字典映射,匹配不到则原样存名称 - String educationText = trim(row.getEducation()); - entity.setEducationName(educationText); - - // 职称:无字典,按分隔符拆分后原样存入(保留信息,待后续字典完善) - entity.setTitleCodeArr(parseTitles(trim(row.getTitle()))); + entity.setJoinWorkDate(LocalDate.now().minusYears(basic.getIndustryWorkYears())); + entity.setTechnicalDirectorFlag(YES.equals(basic.getTechnicalDirector())); + entity.setProcessControlLeaderFlag(YES.equals(basic.getProcessControlLeader())); + // 能力认证一/二、注安证书、评价师证书、高级工程师等 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(convertTitleToCode(t)); - return co; - }) - .collect(Collectors.toList()); - return list.isEmpty() ? null : list; - } - - /** - * 职称文本转 code:按 QualificationLevelEnum 名称包含匹配(如"高级工程师"→SENIOR、"中级工程师"→MIDDLE、"初级"→JUNIOR), - * 匹配不到则原样返回。 - */ - private String convertTitleToCode(String titleText) { - for (QualificationLevelEnum level : QualificationLevelEnum.values()) { - if (titleText.contains(level.getName())) { - return level.getCode(); - } - } - return titleText; - } - - /** - * 解析日期:兼容 yyyy-MM-dd 与 yyyy/M/d(如 1988-05-12、1988/5/12)等常见 Excel 格式。 - */ - private LocalDate parseDate(String text) { - if (!StringUtils.hasText(text)) { - return null; - } - String trimmed = text.trim(); - try { - return LocalDate.parse(trimmed, DATE_FORMATTER); - } catch (Exception ignore) { - // 继续尝试斜杠格式 - } - try { - return LocalDate.parse(trimmed, SLASH_DATE_FORMATTER); - } catch (Exception ignore) { - 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 index f19481a7..ef7e86ca 100644 --- 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 @@ -3,21 +3,24 @@ package org.qinan.safetyeval.app.support; import com.alibaba.excel.annotation.ExcelIgnore; import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; +import org.hibernate.validator.constraints.Range; import org.springframework.util.StringUtils; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; /** - * 人员导入 Excel 行模型(按列下标映射,避免表头存在重名列)。 + * 人员导入 Excel 行模型(按列下标映射,避免表头存在重名列,如 sheet2 两列「证书编号」)。 *

- * 列顺序:一级部门 / 二级部门 / 三级部门 / 岗位 / 账号(手机号) / 姓名 / 性别 / - * 身份证号 / 学历 / 毕业院校 / 所学专业 / 职称 / 参加工作时间 / - * 是否注册安全工程师 / 现住地址 / 办公地址 / 主要学习工作经历 / - * 出版学术专著、专利、获奖、发表学术论文等 / 自我申报的专业能力及认定方式。 - *

出生日期不再通过Excel导入,统一由身份证号解析。

- *

+ * 模板包含两个 sheet: + *
    + *
  • sheet1「专职人员基础信息」-> {@link BasicRow};
  • + *
  • sheet2「专职人员能力信息」-> {@link CapabilityRow},导入时按 + * 身份证号(为空时按姓名)关联到基础信息行,可能为空。
  • + *
+ *

出生日期不通过Excel导入,统一由身份证号解析。

*

* 部门按三级层级填写:一级部门为根,二级部门 parentId 指向一级部门, * 三级部门 parentId 指向二级部门;导入时校验层级关系,取最底层不为空的一级作为 deptId。 @@ -28,120 +31,253 @@ import javax.validation.constraints.Size; @Data public class OrgPersonnelImportRow { - @ExcelProperty(value = "一级部门", index = 0) - private String deptNameLevel1; - - @ExcelProperty(value = "二级部门", index = 1) - private String deptNameLevel2; - - @ExcelProperty(value = "三级部门", index = 2) - private String deptNameLevel3; - - @ExcelProperty(value = "岗位", index = 3) - private String positionName; - - @ExcelProperty(value = "账号", index = 4) - @NotBlank(message = "账号不能为空") - @Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确,应为11位数字且以1开头") - @Size(max = 50, message = "账号长度不能超过50个字符") - private String account; - - @ExcelProperty(value = "姓名", index = 5) - @NotBlank(message = "姓名不能为空") - @Size(max = 50, message = "姓名长度不能超过50个字符") - private String userName; - - @ExcelProperty(value = "性别", index = 6) - @Size(max = 10, message = "性别长度不能超过10个字符") - private String gender; - - @ExcelProperty(value = "身份证号", index = 7) - @Size(max = 18, message = "身份证号长度不能超过18个字符") - private String idCardNo; - - @ExcelProperty(value = "学历", index = 8) - @Size(max = 50, message = "学历长度不能超过50个字符") - private String education; - - @ExcelProperty(value = "毕业院校", index = 9) - @Size(max = 200, message = "毕业院校长度不能超过200个字符") - private String graduateSchool; - - @ExcelProperty(value = "所学专业", index = 10) - @Size(max = 100, message = "所学专业长度不能超过100个字符") - private String major; - - @ExcelProperty(value = "职称", index = 11) - private String title; - - @ExcelProperty(value = "参加工作时间", index = 12) - private String joinWorkDate; - - @ExcelProperty(value = "是否注册安全工程师", index = 13) - private String registerEngineer; - - @ExcelProperty(value = "现住地址", index = 14) - @Size(max = 500, message = "现住地址长度不能超过500个字符") - private String currentAddress; - - @ExcelProperty(value = "办公地址", index = 15) - @Size(max = 500, message = "办公地址长度不能超过500个字符") - private String officeAddress; - - @ExcelProperty(value = "主要学习工作经历", index = 16) - private String workExperience; - - @ExcelProperty(value = "出版学术专著、专利、获奖、发表学术论文等", index = 17) - @Size(max = 1000, message = "出版学术专著等信息长度不能超过1000个字符") - private String publications; - - @ExcelProperty(value = "自我申报的专业能力及认定方式", index = 18) - @Size(max = 1000, message = "自我申报的专业能力信息长度不能超过1000个字符") - private String abilityDeclaration; + /** + * sheet1「专职人员基础信息」行数据。 + */ + private BasicRow basic; /** - * EasyExcel 解析时记录的物理行号(从1开始,含表头),由导入器填充。 + * sheet2「专职人员能力信息」行数据(按身份证号/姓名关联,未匹配到时为空)。 */ - @ExcelIgnore - private Integer rowIndex; + private CapabilityRow capability; /** * 校验通过后回填的部门ID(非Excel列)。 */ - @ExcelIgnore private Long deptId; /** * 校验通过后回填的岗位ID(非Excel列)。 */ - @ExcelIgnore private Long postId; /** * 校验通过后回填的学科基础专业对照表ID(非Excel列)。 */ - @ExcelIgnore private Long basicDisciplineMajorId; /** * 校验失败 true 失败 ,false 未失败 */ - @ExcelIgnore private boolean validateFail; /** * 返回最底层(最深一级)不为空的部门名称,用于错误提示。 */ public String getDeptDisplayName() { - if (StringUtils.hasText(deptNameLevel3)) { - return deptNameLevel3.trim(); + if (basic == null) { + return null; } - if (StringUtils.hasText(deptNameLevel2)) { - return deptNameLevel2.trim(); + if (StringUtils.hasText(basic.getDeptNameLevel3())) { + return basic.getDeptNameLevel3().trim(); } - if (StringUtils.hasText(deptNameLevel1)) { - return deptNameLevel1.trim(); + if (StringUtils.hasText(basic.getDeptNameLevel2())) { + return basic.getDeptNameLevel2().trim(); + } + if (StringUtils.hasText(basic.getDeptNameLevel1())) { + return basic.getDeptNameLevel1().trim(); } return null; } + + /** + * Sheet1「专职人员基础信息」行模型。 + *

+ * 列顺序:一级部门 / 二级部门 / 三级部门 / 岗位 / 姓名 / 性别 / 本行业工作年限 / + * 账号(系统登录账号) / 身份证号 / 现住地址 / 办公地址 / 是否技术负责人 / + * 是否过程控制负责人 / 主要学习工作经历 / + * 出版学术专著、专利、获奖、发表学术论文等 / 自我申报的专业能力及认定方式。 + *

+ */ + @Data + public static class BasicRow { + + @ExcelProperty(value = "一级部门", index = 0) + @NotBlank(message = "一级部门不能为空") + @Size(max = 128, message = "一级部门名称最多120个字符") + private String deptNameLevel1; + + @ExcelProperty(value = "二级部门", index = 1) + @Size(max = 128, message = "二级部门名称最多120个字符") + private String deptNameLevel2; + + @ExcelProperty(value = "三级部门", index = 2) + @Size(max = 128, message = "三级部门最多120个字符") + private String deptNameLevel3; + + @ExcelProperty(value = "岗位", index = 3) + @Size(max = 128, message = "岗位最多120个字符") + private String positionName; + + @ExcelProperty(value = "姓名", index = 4) + @NotBlank(message = "姓名不能为空") + @Size(max = 50, message = "姓名长度不能超过50个字符") + private String userName; + + @ExcelProperty(value = "性别", index = 5) + @Size(max = 10, message = "性别长度不能超过10个字符") + @NotBlank(message = "性别不能为空") + private String gender; + + @ExcelProperty(value = "本行业工作年限", index = 6) + @NotNull(message = "本行业工作年限不能为空") + @Range(min = 1, max = 200, message = "本行业工作年限1-200之间") + private Integer industryWorkYears; + + @ExcelProperty(value = "账号(系统登录账号)", index = 7) + @NotBlank(message = "账号不能为空") + @Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确,应为11位数字且以1开头") + @Size(max = 50, message = "账号长度不能超过50个字符") + private String account; + + @ExcelProperty(value = "身份证号", index = 8) + @Size(max = 18, message = "身份证号长度不能超过18个字符") + @NotBlank(message = "身份证号不能为空") + private String idCardNo; + + @ExcelProperty(value = "现住地址", index = 9) + @Size(max = 500, message = "现住地址长度不能超过500个字符") + private String currentAddress; + + @ExcelProperty(value = "办公地址", index = 10) + @Size(max = 500, message = "办公地址长度不能超过500个字符") + private String officeAddress; + + @ExcelProperty(value = "是否技术负责人", index = 11) + @Pattern(regexp = "^([是否])?$", message = "是否技术负责人仅允许:是、否") + private String technicalDirector; + + @ExcelProperty(value = "是否过程控制负责人", index = 12) + @Pattern(regexp = "^([是否])?$", message = "是否过程控制负责人仅允许:是、否") + private String processControlLeader; + + @ExcelProperty(value = "主要学习工作经历", index = 13) + @Size(max = 1024, message = "办公地址长度不能超过1024个字符") + private String workExperience; + + @ExcelProperty(value = "出版学术专著、专利、获奖、发表学术论文等", index = 14) + @Size(max = 1000, message = "出版学术专著等信息长度不能超过1000个字符") + private String publications; + + @ExcelProperty(value = "自我申报的专业能力及认定方式", index = 15) + @Size(max = 1000, message = "自我申报的专业能力信息长度不能超过1000个字符") + private String abilityDeclaration; + + /** + * sheet1 的物理行号(从1开始,含表头),由导入器填充,用于错误提示。 + */ + @ExcelIgnore + private Integer rowIndex; + } + + /** + * Sheet2「专职人员能力信息」行模型。 + *

+ * 列顺序:姓名 / 身份证号 / 能力认证一(认证行业、专业能力、来源) / + * 能力认证二(认证行业、专业能力、来源) / 学历类型 / 毕业院校 / 学历层次 / 所学专业 / + * 是否注册安全工程师 / 注安专业类别 / 注安等级 / 证书编号 / 执业证书编号 / + * 是否评价师 / 评价师等级 / 证书编号 / 是否高级工程师 / 系列/专业。 + *

+ */ + @Data + public static class CapabilityRow { + + @ExcelProperty(value = "姓名", index = 0) + private String userName; + + @ExcelProperty(value = "身份证号", index = 1) + @Size(max = 18, message = "身份证号长度不能超过18个字符") + @NotBlank(message = "身份证号不能为空") + private String idCardNo; + + @ExcelProperty(value = "能力认证一-认证行业", index = 2) + @NotBlank(message = "能力认证一-认证行业不能为空") + @Size(max = 64, message = "能力认证一-认证行业最多64个字符") + private String cert1Industry; + + @ExcelProperty(value = "能力认证一-专业能力", index = 3) + @NotBlank(message = "能力认证一-专业能力不能为空") + @Size(max = 64, message = "能力认证一-专业能力最多64个字符") + private String cert1Ability; + + @ExcelProperty(value = "能力认证一-来源", index = 4) + @NotBlank(message = "能力认证一-来源不能为空") + @Size(max = 128, message = "能力认证一-来源最多64个字符") + private String cert1Source; + + @ExcelProperty(value = "能力认证二-认证行业", index = 5) + @Size(max = 64, message = "能力认证一-认证行业最多64个字符") + private String cert2Industry; + + @ExcelProperty(value = "能力认证二-专业能力", index = 6) + @Size(max = 64, message = "能力认证一-专业能力最多64个字符") + private String cert2Ability; + + @ExcelProperty(value = "能力认证二-来源", index = 7) + @Size(max = 128, message = "能力认证二-来源最多128个字符") + private String cert2Source; + + @ExcelProperty(value = "学历类型", index = 8) + @NotBlank(message = "学历类型不能为空") + @Size(max = 64, message ="学历类型最多64个字符") + private String educationType; + + @ExcelProperty(value = "毕业院校", index = 9) + @Size(max = 200, message = "毕业院校长度不能超过200个字符") + @NotBlank(message = "毕业院校不能为空") + private String graduateSchool; + + @ExcelProperty(value = "学历层次", index = 10) + @NotBlank(message = "学历层次不能为空") + private String educationLevel; + + @ExcelProperty(value = "所学专业", index = 11) + @Size(max = 100, message = "所学专业长度不能超过100个字符") + @NotBlank(message = "所学专业不能为空") + private String major; + + @ExcelProperty(value = "是否注册安全工程师", index = 12) + @Pattern(regexp = "^([是否])?$", message = "是否注册安全工程师仅允许:是、否") + @NotBlank(message = "是否注册安全工程师不能为空") + private String registerEngineer; + + @ExcelProperty(value = "注安专业类别", index = 13) + @Size(max = 100, message = "注安专业类别长度不能超过100个字符") + private String registerEngineerCategory; + + @ExcelProperty(value = "注安等级", index = 14) + @Size(max = 100, message = "注安等级长度不能超过100个字符") + private String registerEngineerLevel; + + @ExcelProperty(value = "注安证书编号", index = 15) + @Size(max = 64, message = "注安证书编号长度不能超过100个字符") + private String registerEngineerCertNo; + + @ExcelProperty(value = "执业证书编号", index = 16) + @Size(max = 64, message = "执业证书编号长度不能超过100个字符") + private String practiceCertNo; + + @ExcelProperty(value = "是否评价师", index = 17) + @Pattern(regexp = "^([是否])?$", message = "是否评价师仅允许:是、否") + @NotBlank(message = "是否是否评价师不能为空") + private String evaluator; + + @ExcelProperty(value = "评价师等级", index = 18) + @Size(max = 64, message = "评价师等级长度不能超过100个字符") + private String evaluatorLevel; + + @ExcelProperty(value = "评价师证书编号", index = 19) + @Size(max = 64, message = "评价师证书编号长度不能超过100个字符") + private String evaluatorCertNo; + + @ExcelProperty(value = "是否高级工程师", index = 20) + @Pattern(regexp = "^([是否])?$", message = "是否高级工程师仅允许:是、否") + private String seniorEngineer; + + @ExcelProperty(value = "高级工程师系列/专业", index = 21) + private String seniorEngineerSeries; + + @ExcelIgnore + private Integer rowIndex; + } }