Procházet zdrojové kódy

feat(hr): 添加职称技能聘任功能并优化合同类别处理

- 新增职称技能聘任相关常量定义
- 实现职称技能聘任导入插件和验证器
- 添加聘任单员工重复性校验和开始日期校验
- 移除合同类别和岗位类型的枚举依赖
- 使用硬编码值替换原有的枚举引用
- 实现聘任单生效操作插件和数据映射功能
jtd před 2 dny
rodič
revize
75b922be56

+ 4 - 6
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/hdm/plugin/operate/transfer/CancelContractAfterEffectOpPlugin.java

@@ -23,8 +23,6 @@ import kd.hr.hbp.business.servicehelper.HRMServiceHelper;
 import kd.hr.hbp.common.api.HrApiResponse;
 import kd.hr.hbp.common.util.HRObjectUtils;
 import kd.hr.hbp.common.util.HRStringUtils;
-import kd.hr.hlcm.common.enums.ContractCategoryEnum;
-import kd.hr.hlcm.common.enums.PostypeEnum;
 import kd.sdk.hr.hdm.common.enums.reg.RegBillStatusEnum;
 import kd.sdk.hr.hlcm.common.enums.BusinessTypeEnum;
 import nckd.jxccl.base.common.utils.QueryFieldBuilder;
@@ -262,10 +260,10 @@ public class CancelContractAfterEffectOpPlugin extends AbstractOperationServiceP
             DynamicObject posType = signBill.getDynamicObject(TransferApplyBillConstant.POSTYPE);
             if (!HRObjectUtils.isEmpty(posType)) {
                 Object posTypeId = posType.getPkValue();
-                if (Objects.equals(posTypeId, PostypeEnum.FULL.getCombKey())) {
-                    signBill.set(TransferApplyBillConstant.CONTRACTCATEGORY_KEY, ContractCategoryEnum.MAIN.getCombKey());
-                } else if (Objects.equals(posTypeId, PostypeEnum.EXPATRIATE.getCombKey())) {
-                    signBill.set(TransferApplyBillConstant.CONTRACTCATEGORY_KEY, ContractCategoryEnum.OUTER.getCombKey());
+                if (Objects.equals(posTypeId, 1010L)) {
+                    signBill.set(TransferApplyBillConstant.CONTRACTCATEGORY_KEY, "1");
+                } else if (Objects.equals(posTypeId, 1040L)) {
+                    signBill.set(TransferApplyBillConstant.CONTRACTCATEGORY_KEY, "2");
                 } else {
                     log.warn("posTypeId|{} is not 1010 or 1040", posTypeId);
                 }

+ 37 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/hspm/common/TitleSkillAppointConstant.java

@@ -10,6 +10,43 @@ import nckd.jxccl.base.common.constant.FormConstant;
 public class TitleSkillAppointConstant extends FormConstant {
     /** 员工职称技能聘任-实体标识 */
     public static final String NCKD_HRPI_TITLSKLAPP_ENTITY = "nckd_hrpi_titlsklapp";
+    /** 职称技能聘任单-实体标识 */
+    public static final String NCKD_JOB_TITLE_SKILL_APPT_ENTITY = "nckd_job_title_skill_appt";
+    /** 职称技能聘任键值映射 */
+    public static final String NCKD_TS_APPOINT_KVM_ENTITY = "nckd_ts_appoint_kvm";
+    /** 员工职称信息-实体标识 */
+    public static final String NCKD_HRPI_NTITLE_ENTITY = "nckd_hrpi_ntitle";
+    /** 员工技能信息-实体标识 */
+    public static final String NCKD_HRPI_EMPSKILL_ENTITY = "nckd_hrpi_empskill";
+    /** 职称资格级别技能等级-实体标识 */
+    public static final String NCKD_HBSS_TITLESKLRK_ENTITY = "nckd_hbss_titlesklrk";
+
+    /** 聘任类型 */
+    public static final String NCKD_TYPE_KEY = "nckd_type";
+    /** 职称信息 */
+    public static final String NCKD_NTITLE_KEY = "nckd_ntitle";
+    /** 技能信息 */
+    public static final String NCKD_EMPSKILL_KEY = "nckd_empskill";
+    /** 聘任职称/技能等级 */
+    public static final String NCKD_A_TITLESKLRK_KEY = "nckd_a_titlesklrk";
+    /** 职务名称 */
+    public static final String NCKD_POSTNM_KEY = "nckd_postnm";
+    /** 是否虚拟 */
+    public static final String NCKD_ISVIRTUAL_KEY = "nckd_isvirtual";
+    /** 虚拟聘任职称/技能等级 */
+    public static final String NCKD_VIRTITLESKLRK_KEY = "nckd_virtitlesklrk";
+    /** 虚拟聘任职务名称 */
+    public static final String NCKD_VIRTPOSTNM_KEY = "nckd_virtpostnm";
+
+    /** 职称名称(资格名称) */
+    public static final String NCKD_TITLE_KEY = "nckd_title";
+    /** 资格级别 */
+    public static final String NCKD_QUALLEVEL_KEY = "nckd_quallevel";
+
+    /** 资格名称 */
+    public static final String NCKD_QUALINAME_KEY = "nckd_qualiname";
+    /** 职业技能等级 */
+    public static final String NCKD_SKILLLEVEL_KEY = "nckd_skilllevel";
 
 
 }

+ 101 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/hspm/plugin/form/tsapp/impt/TitleSkillAppointEntryImportPlugin.java

@@ -0,0 +1,101 @@
+package nckd.jxccl.hr.hspm.plugin.form.tsapp.impt;
+
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.metadata.IDataEntityProperty;
+import kd.bos.entity.EntityMetadataCache;
+import kd.bos.entity.EntryType;
+import kd.bos.entity.ValueMapItem;
+import kd.bos.entity.property.ComboProp;
+import kd.bos.entity.property.EntryProp;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.hr.hbp.common.util.HRObjectUtils;
+import kd.hrmp.hies.multientry.common.dto.EntryImptBillData;
+import kd.hrmp.hies.multientry.common.enu.EntryValidatorEnum;
+import kd.hrmp.hies.multientry.common.plugin.impt.BeforeBackFillDataEventArgs;
+import kd.hrmp.hies.multientry.common.plugin.impt.BeforeInitValidatorEventArgs;
+import kd.hrmp.hies.multientry.common.plugin.impt.BeforeValidateEventArgs;
+import kd.hrmp.hies.multientry.common.plugin.impt.HREntryImportPlugin;
+import nckd.jxccl.base.hrpi.helper.EmpPosOrgRelHelper;
+import nckd.jxccl.hr.hspm.common.TitleSkillAppointConstant;
+import nckd.jxccl.hr.hspm.plugin.form.tsapp.validator.TitleSkillAppointEntryImportValidator;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @entity:
+ * @author: jtd
+ * @date: 2026/1/14 18:36
+ */
+public class TitleSkillAppointEntryImportPlugin implements HREntryImportPlugin {
+    private static final Log log = LogFactory.getLog(TitleSkillAppointEntryImportPlugin.class);
+
+    @Override
+    public void beforeInitValidator(BeforeInitValidatorEventArgs args) {
+        HREntryImportPlugin.super.beforeInitValidator(args);
+
+        args.setValidator(EntryValidatorEnum.CUSTOM_VALIDATOR, new TitleSkillAppointEntryImportValidator());
+    }
+
+    @Override
+    public void beforeValidate(BeforeValidateEventArgs args) {
+        HREntryImportPlugin.super.beforeValidate(args);
+
+        // 获取分录属性名称
+        Map<String, IDataEntityProperty> entryFields = ((EntryType) ((EntryProp) args.getDataModel().getDataEntityType().getProperty(TitleSkillAppointConstant.NCKD_ENTRYENTITY)).getItemType()).getFields();
+        Map<String, String> entryFieldNameMap = entryFields.values().stream().collect(Collectors.toMap(IDataEntityProperty::getName, entryProp -> entryProp.getDisplayName().getLocaleValue()));
+        args.getCustomParams().put("entryFieldNameMap", entryFieldNameMap);
+
+        // 获取聘任类型下拉值
+        ComboProp typeComboProp = (ComboProp) entryFields.get(TitleSkillAppointConstant.NCKD_TYPE_KEY);
+        Map<String, String> typeItemMap = typeComboProp.getComboItems().stream().collect(Collectors.toMap(ValueMapItem::getValue, comboItem -> comboItem.getName().getLocaleValue()));
+        args.getCustomParams().put(TitleSkillAppointConstant.NCKD_TYPE_KEY, typeItemMap);
+
+        // 获取员工职称信息属性名称
+        Map<String, IDataEntityProperty> empNtileFields = EntityMetadataCache.getDataEntityType(TitleSkillAppointConstant.NCKD_HRPI_NTITLE_ENTITY).getAllFields();
+        Map<String, String> empNtitleFieldNameMap = empNtileFields.values().stream().collect(Collectors.toMap(IDataEntityProperty::getName, entryProp -> entryProp.getDisplayName().getLocaleValue()));
+        args.getCustomParams().put("empNtitleFieldNameMap", empNtitleFieldNameMap);
+
+        // 获取员工技能信息属性名称
+        Map<String, IDataEntityProperty> empSkillFields = EntityMetadataCache.getDataEntityType(TitleSkillAppointConstant.NCKD_HRPI_EMPSKILL_ENTITY).getAllFields();
+        Map<String, String> empSkillFieldNameMap = empSkillFields.values().stream().collect(Collectors.toMap(IDataEntityProperty::getName, entryProp -> entryProp.getDisplayName().getLocaleValue()));
+        args.getCustomParams().put("empSkillFieldNameMap", empSkillFieldNameMap);
+    }
+
+    @Override
+    public void beforeBackFillData(BeforeBackFillDataEventArgs args) {
+        HREntryImportPlugin.super.beforeBackFillData(args);
+
+        Set<Long> employeeIds = args.getRowData().stream().map(billData -> billData.getData().getJSONObject(billData.getEntryKey()).getJSONObject(TitleSkillAppointConstant.NCKD_EMPLOYEE).getLongValue(TitleSkillAppointConstant.ID_KEY)).collect(Collectors.toSet());
+        Map<Long, DynamicObject> empPosOrgRel = EmpPosOrgRelHelper.queryEmpPosOrgRelByEmployeesMap(employeeIds);
+
+        for (EntryImptBillData billData : args.getRowData()) {
+            JSONObject entryData = billData.getData().getJSONObject(billData.getEntryKey());
+            Long employeeId = entryData.getJSONObject(TitleSkillAppointConstant.NCKD_EMPLOYEE).getLongValue(TitleSkillAppointConstant.ID_KEY);
+
+            DynamicObject empPosOrgRelDy = empPosOrgRel.get(employeeId);
+            if (!HRObjectUtils.isEmpty(empPosOrgRelDy)) {
+                JSONObject adminOrg = new JSONObject();
+                adminOrg.put(TitleSkillAppointConstant.ID_KEY, empPosOrgRelDy.getLong(String.join(".", TitleSkillAppointConstant.ADMINORG, TitleSkillAppointConstant.ID_KEY)));
+                entryData.put(TitleSkillAppointConstant.NCKD_ADMINORG, adminOrg);
+
+                JSONObject position = new JSONObject();
+                position.put(TitleSkillAppointConstant.ID_KEY, empPosOrgRelDy.getLong(String.join(".", TitleSkillAppointConstant.POSITION_KEY, TitleSkillAppointConstant.ID_KEY)));
+                entryData.put(TitleSkillAppointConstant.NCKD_POSITION_KEY, position);
+            }
+
+            if (!Objects.isNull(entryData.get(TitleSkillAppointConstant.NCKD_VIRTITLESKLRK_KEY)) || !Objects.isNull(entryData.get(TitleSkillAppointConstant.NCKD_VIRTPOSTNM_KEY))) {
+                entryData.put(TitleSkillAppointConstant.NCKD_ISVIRTUAL_KEY, true);
+            } else {
+                entryData.put(TitleSkillAppointConstant.NCKD_ISVIRTUAL_KEY, false);
+                entryData.put(TitleSkillAppointConstant.NCKD_VIRTITLESKLRK_KEY, null);
+                entryData.put(TitleSkillAppointConstant.NCKD_VIRTPOSTNM_KEY, null);
+            }
+        }
+    }
+}

+ 204 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/hspm/plugin/form/tsapp/validator/TitleSkillAppointEntryImportValidator.java

@@ -0,0 +1,204 @@
+package nckd.jxccl.hr.hspm.plugin.form.tsapp.validator;
+
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.entity.EntityMetadataCache;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.bos.util.CollectionUtils;
+import kd.hr.hbp.business.servicehelper.HRBaseServiceHelper;
+import kd.hr.hbp.common.util.HRStringUtils;
+import kd.hr.impt.common.dto.ImportLog;
+import kd.hr.impt.common.dto.ImportRowErrorLog;
+import kd.hr.impt.common.enu.ValidatorOrderEnum;
+import kd.hrmp.hies.multientry.common.dto.EntryImptBillData;
+import kd.hrmp.hies.multientry.core.validate.AbstractEntryValidateHandler;
+import nckd.jxccl.base.common.utils.QueryFieldBuilder;
+import nckd.jxccl.hr.hspm.common.TitleSkillAppointConstant;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @entity:
+ * @author: jtd
+ * @date: 2026/1/14 22:20
+ */
+public class TitleSkillAppointEntryImportValidator extends AbstractEntryValidateHandler {
+    /** 员工职称信息基础服务 */
+    private static final HRBaseServiceHelper empNtitleHelper = new HRBaseServiceHelper(TitleSkillAppointConstant.NCKD_HRPI_NTITLE_ENTITY);
+    /** 员工技能信息-基础服务 */
+    private static final HRBaseServiceHelper empSkillHelper = new HRBaseServiceHelper(TitleSkillAppointConstant.NCKD_HRPI_EMPSKILL_ENTITY);
+    /** 职称资格级别技能等级-基础服务 */
+    private static final HRBaseServiceHelper titleSkillLevelHelper = new HRBaseServiceHelper(TitleSkillAppointConstant.NCKD_HBSS_TITLESKLRK_ENTITY);
+
+    @Override
+    public ValidatorOrderEnum setValidatorRole() {
+        return ValidatorOrderEnum.AFTER;
+    }
+
+    @Override
+    public void validate(List<EntryImptBillData> list, ImportLog importLog) {
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+
+        // 获取错误信息
+        ConcurrentHashMap<String, ConcurrentHashMap<Integer, ImportRowErrorLog>> rowErrors = importLog.getRowErrors();
+        List<EntryImptBillData> billDataList = list.stream().filter(billData -> {
+            ConcurrentHashMap<Integer, ImportRowErrorLog> rowError = rowErrors.getOrDefault(billData.getSheetName(), null);
+            if (Objects.isNull(rowError) || rowError.isEmpty()) {
+                return true;
+            }
+
+            return !rowError.containsKey(billData.getStartIndex());
+        }).collect(Collectors.toList());
+
+        if (billDataList.isEmpty()) {
+            return;
+        }
+
+        Map<String, String> typeItemMap = (Map<String, String>) getCustomParams().get(TitleSkillAppointConstant.NCKD_TYPE_KEY);
+        Set<Long> employeeIds = billDataList.stream().map(billData -> billData.getData().getJSONObject(billData.getEntryKey()).getJSONObject(TitleSkillAppointConstant.NCKD_EMPLOYEE).getLongValue(TitleSkillAppointConstant.ID_KEY)).collect(Collectors.toSet());
+
+        // 获取职称技能等级
+        Map<String, Long> titleSkillLevelMap = titleSkillLevelHelper.queryOriginalCollection(String.join(",", TitleSkillAppointConstant.ID_KEY, TitleSkillAppointConstant.NAME_KEY), null, TitleSkillAppointConstant.MODIFY_TIME_KEY)
+                .stream().collect(Collectors.toMap(
+                        dyo -> dyo.getString(TitleSkillAppointConstant.NAME_KEY),
+                        dyo -> dyo.getLong(TitleSkillAppointConstant.ID_KEY),
+                        (oldValue, newValue) -> newValue)
+                );
+
+        String selectFields = QueryFieldBuilder.create()
+                .add(TitleSkillAppointConstant.ID_KEY)
+                .add(TitleSkillAppointConstant.EMPLOYEE_KEY, TitleSkillAppointConstant.ID_KEY)
+                .add(TitleSkillAppointConstant.NCKD_TITLE_KEY)
+                .add(TitleSkillAppointConstant.NCKD_QUALLEVEL_KEY, TitleSkillAppointConstant.NAME_KEY)
+                .buildSelect();
+        QFilter[] filters = new QFilter[]{new QFilter(TitleSkillAppointConstant.EMPLOYEE_KEY, QCP.in, employeeIds)};
+        // 查询员工职称信息
+        DynamicObject[] empNtitleDyos = empNtitleHelper.queryOriginalArray(selectFields, filters, TitleSkillAppointConstant.MODIFY_TIME_KEY);
+        Map<String, Long> empNtitleMap = Arrays.stream(empNtitleDyos).collect(Collectors.toMap(
+                ntitleDyo -> String.join("##",
+                        ntitleDyo.getString(String.join(".", TitleSkillAppointConstant.EMPLOYEE_KEY, TitleSkillAppointConstant.ID_KEY)),
+                        ntitleDyo.getString(TitleSkillAppointConstant.NCKD_TITLE_KEY),
+                        ntitleDyo.getString(String.join(".", TitleSkillAppointConstant.NCKD_QUALLEVEL_KEY, TitleSkillAppointConstant.NAME_KEY))
+                ),
+                ntitleDyo -> ntitleDyo.getLong(TitleSkillAppointConstant.ID_KEY),
+                (oldValue, newValue) -> newValue
+            )
+        );
+
+        // 查询员工技能信息
+        selectFields = QueryFieldBuilder.create()
+                .add(TitleSkillAppointConstant.ID_KEY)
+                .add(TitleSkillAppointConstant.EMPLOYEE_KEY, TitleSkillAppointConstant.ID_KEY)
+                .add(TitleSkillAppointConstant.NCKD_QUALINAME_KEY)
+                .add(TitleSkillAppointConstant.NCKD_SKILLLEVEL_KEY, TitleSkillAppointConstant.NAME_KEY)
+                .buildSelect();
+        DynamicObject[] empSkillDyos = empSkillHelper.queryOriginalArray(selectFields, filters, TitleSkillAppointConstant.MODIFY_TIME_KEY);
+        Map<String, Long> empSkillMap = Arrays.stream(empSkillDyos).collect(Collectors.toMap(
+                skillDyo -> String.join("##",
+                    skillDyo.getString(String.join(".", TitleSkillAppointConstant.EMPLOYEE_KEY, TitleSkillAppointConstant.ID_KEY)),
+                    skillDyo.getString(TitleSkillAppointConstant.NCKD_QUALINAME_KEY),
+                    skillDyo.getString(String.join(".", TitleSkillAppointConstant.NCKD_SKILLLEVEL_KEY, TitleSkillAppointConstant.NAME_KEY))
+                ),
+                skillDyo -> skillDyo.getLong(TitleSkillAppointConstant.ID_KEY),
+                (oldValue, newValue) -> newValue
+            )
+        );
+
+        // 获取字段名称
+        Map<String, String> entryFieldNameMap = (Map<String, String>) getCustomParams().get("entryFieldNameMap");
+        // 员工职称信息属性名称
+        Map<String, String> empNtitleFieldNameMap = (Map<String, String>) getCustomParams().get("empNtitleFieldNameMap");
+        // 员工技能信息属性名称
+        Map<String, String> empSkillFieldNameMap = (Map<String, String>) getCustomParams().get("empSkillFieldNameMap");
+        for (EntryImptBillData billData : billDataList) {
+            JSONObject entryData = billData.getData().getJSONObject(billData.getEntryKey());
+            String employeeId = entryData.getJSONObject(TitleSkillAppointConstant.NCKD_EMPLOYEE).getString(TitleSkillAppointConstant.ID_KEY);
+
+            // 判断聘任类型是否正确
+            String typeItemName = entryData.getString(TitleSkillAppointConstant.NCKD_TYPE_KEY);
+            String typeItemValue = null;
+            for (Map.Entry<String, String> item : typeItemMap.entrySet()) {
+                if (item.getValue().equals(typeItemName)) {
+                    typeItemValue = item.getKey();
+                }
+            }
+            if (HRStringUtils.isBlank(typeItemValue)) {
+                importLog.writeRowLog(billData.getSheetName(), billData.getStartIndex(), billData.getEndIndex(), HRStringUtils.format("“{}”录入有误,请重新录入。", entryFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_TYPE_KEY, "")));
+                continue;
+            }
+
+            String[] temp = new String[0];
+            // 根据聘任类型判断对应列是否有值
+            if ("1".equals(typeItemValue)) {
+                String ntitleTemp = entryData.getString(String.join("_", TitleSkillAppointConstant.NCKD_NTITLE_KEY, "temp"));
+                if (HRStringUtils.isBlank(ntitleTemp)) {
+                    importLog.writeRowLog(billData.getSheetName(), billData.getStartIndex(), billData.getEndIndex(), HRStringUtils.format("“{}”为[{}]时,必须录入“{}”。", new Object[]{entryFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_TYPE_KEY, ""), typeItemName, entryFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_NTITLE_KEY, "")}));
+                    continue;
+                }
+
+                temp = ntitleTemp.split("##");
+                if (temp.length != 2) {
+                    importLog.writeRowLog(billData.getSheetName(), billData.getStartIndex(), billData.getEndIndex(), HRStringUtils.format("“{}”录入有误,请重新录入。", entryFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_NTITLE_KEY, "")));
+                    continue;
+                }
+
+                Long empNtitleId = empNtitleMap.get(String.join("##", employeeId, ntitleTemp));
+                if (empNtitleId == null || !(empNtitleId > 0)) {
+                    importLog.writeRowLog(billData.getSheetName(), billData.getStartIndex(), billData.getEndIndex(), HRStringUtils.format("该员工的“{}”中不存在“{}”为[{}],“{}”为[{}]的记录,请重新录入。", new Object[]{EntityMetadataCache.getDataEntityType(empNtitleHelper.getEntityName()).getDisplayName().getLocaleValue(), empNtitleFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_TITLE_KEY, ""), temp[0], empNtitleFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_QUALLEVEL_KEY, ""), temp[1]}));
+                    continue;
+                }
+
+                JSONObject title = new JSONObject(){{put(TitleSkillAppointConstant.ID_KEY, empNtitleId);}};
+                entryData.put(TitleSkillAppointConstant.NCKD_NTITLE_KEY, title);
+
+                JSONObject level = new JSONObject();
+                level.put(TitleSkillAppointConstant.ID_KEY, titleSkillLevelMap.getOrDefault(temp[1], 0L));
+                entryData.put(TitleSkillAppointConstant.NCKD_A_TITLESKLRK_KEY, level);
+
+                entryData.put(TitleSkillAppointConstant.NCKD_POSTNM_KEY, temp[0]);
+                entryData.put(TitleSkillAppointConstant.NCKD_EMPSKILL_KEY, null);
+            } else if ("2".equals(typeItemValue)) {
+                String skillTemp = entryData.getString(String.join("_", TitleSkillAppointConstant.NCKD_EMPSKILL_KEY, "temp"));
+                if (HRStringUtils.isBlank(skillTemp)) {
+                    importLog.writeRowLog(billData.getSheetName(), billData.getStartIndex(), billData.getEndIndex(), HRStringUtils.format("“{}”为[{}]时,必须录入“{}”。", new Object[]{entryFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_TYPE_KEY, ""), typeItemName, entryFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_EMPSKILL_KEY, "")}));
+                    continue;
+                }
+
+                temp = skillTemp.split("##");
+                if (temp.length != 2) {
+                    importLog.writeRowLog(billData.getSheetName(), billData.getStartIndex(), billData.getEndIndex(), HRStringUtils.format("“{}”录入有误,请重新录入。", entryFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_EMPSKILL_KEY, "")));
+                    continue;
+                }
+
+                Long empSkillId = empSkillMap.get(String.join("##", employeeId, skillTemp));
+                if (empSkillId == null || !(empSkillId > 0)) {
+                    importLog.writeRowLog(billData.getSheetName(), billData.getStartIndex(), billData.getEndIndex(), HRStringUtils.format("该员工的“{}”中不存在“{}”为[{}],“{}”为[{}]的记录,请重新录入。", new Object[]{EntityMetadataCache.getDataEntityType(empSkillHelper.getEntityName()).getDisplayName().getLocaleValue(), empSkillFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_QUALINAME_KEY, ""), temp[0], empSkillFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_SKILLLEVEL_KEY, ""), temp[1]}));
+                    continue;
+                }
+
+                JSONObject skill = new JSONObject(){{put(TitleSkillAppointConstant.ID_KEY, empSkillId);}};
+                entryData.put(TitleSkillAppointConstant.NCKD_EMPSKILL_KEY, skill);
+
+                JSONObject level = new JSONObject();
+                level.put(TitleSkillAppointConstant.ID_KEY, titleSkillLevelMap.getOrDefault(temp[1], 0L));
+                entryData.put(TitleSkillAppointConstant.NCKD_A_TITLESKLRK_KEY, level);
+
+                entryData.put(TitleSkillAppointConstant.NCKD_POSTNM_KEY, temp[0]);
+                entryData.put(TitleSkillAppointConstant.NCKD_NTITLE_KEY, null);
+            } else {
+                importLog.writeRowLog(billData.getSheetName(), billData.getStartIndex(), billData.getEndIndex(), HRStringUtils.format("“{}”录入有误,请联系管理员处理。", new Object[]{entryFieldNameMap.getOrDefault(TitleSkillAppointConstant.NCKD_TYPE_KEY, "")}));
+            }
+        }
+    }
+
+}

+ 57 - 1
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/hspm/plugin/operate/tsapp/TitleSkillAppointEffectOpPlugin.java

@@ -2,10 +2,24 @@ package nckd.jxccl.hr.hspm.plugin.operate.tsapp;
 
 import kd.bos.dataentity.entity.DynamicObject;
 import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.entity.operate.result.IOperateInfo;
+import kd.bos.entity.operate.result.OperationResult;
 import kd.bos.entity.plugin.AbstractOperationServicePlugIn;
+import kd.bos.entity.plugin.AddValidatorsEventArgs;
 import kd.bos.entity.plugin.PreparePropertysEventArgs;
 import kd.bos.entity.plugin.args.BeginOperationTransactionArgs;
+import kd.bos.exception.KDBizException;
+import kd.bos.servicehelper.operation.SaveServiceHelper;
+import kd.hr.hbp.business.servicehelper.HRBaseServiceHelper;
+import kd.hr.hbp.common.util.HRDateTimeUtils;
+import kd.hr.hbp.common.util.HRObjectUtils;
 import nckd.jxccl.hr.hspm.common.TitleSkillAppointConstant;
+import nckd.jxccl.hr.hspm.plugin.operate.tsapp.validator.TitleSkillAppointEmployeeValidator;
+import nckd.jxccl.hr.hspm.plugin.operate.tsapp.validator.TitleSkillAppointStartDateValidator;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * 职称技能聘任单生效操作插件
@@ -14,6 +28,12 @@ import nckd.jxccl.hr.hspm.common.TitleSkillAppointConstant;
  * @date: 2026/1/13 21:11
  */
 public class TitleSkillAppointEffectOpPlugin extends AbstractOperationServicePlugIn {
+    /** 员工职称技能聘任基础服务 */
+    private static final HRBaseServiceHelper tsappHelper = HRBaseServiceHelper.create(TitleSkillAppointConstant.NCKD_HRPI_TITLSKLAPP_ENTITY);
+    /** 职称技能聘任键值映射基础服务 */
+    private static final HRBaseServiceHelper appointKvmHelper = HRBaseServiceHelper.create(TitleSkillAppointConstant.NCKD_TS_APPOINT_KVM_ENTITY);
+    private Map<String, String> keyMapping;
+
 
     @Override
     public void onPreparePropertys(PreparePropertysEventArgs e) {
@@ -22,20 +42,56 @@ public class TitleSkillAppointEffectOpPlugin extends AbstractOperationServicePlu
         e.getFieldKeys().addAll(billEntityType.getAllFields().keySet());
     }
 
+    @Override
+    public void onAddValidators(AddValidatorsEventArgs e) {
+        super.onAddValidators(e);
+
+        e.addValidator(new TitleSkillAppointStartDateValidator());
+        e.addValidator(new TitleSkillAppointEmployeeValidator());
+    }
+
     @Override
     public void beginOperationTransaction(BeginOperationTransactionArgs e) {
         super.beginOperationTransaction(e);
 
+        if (HRObjectUtils.isEmpty(keyMapping)) {
+            keyMapping = Arrays.stream(appointKvmHelper.loadDynamicObjectArray(null)).collect(Collectors.toMap(dyo -> dyo.getString(TitleSkillAppointConstant.NUMBER_KEY), dyo -> dyo.getString(TitleSkillAppointConstant.NAME_KEY), (oldValue, newValue) -> newValue));
+        }
+
+        DynamicObjectCollection tkappCol = new DynamicObjectCollection();
         for (DynamicObject dataEntite : e.getDataEntities()) {
             DynamicObjectCollection entryEntity = dataEntite.getDynamicObjectCollection(TitleSkillAppointConstant.NCKD_ENTRYENTITY);
             for (DynamicObject entry : entryEntity) {
+                tkappCol.add(buildAppointRecordDy(entry));
+            }
+        }
+
+        if (!tkappCol.isEmpty()) {
+            OperationResult operationResult = SaveServiceHelper.saveOperate(TitleSkillAppointConstant.NCKD_HRPI_TITLSKLAPP_ENTITY, tkappCol.toArray(new DynamicObject[0]));
+            if (!operationResult.isSuccess()) {
+                StringBuilder errorMsg = new StringBuilder();
+
+                for(IOperateInfo operateInfo : operationResult.getAllErrorOrValidateInfo()) {
+                    errorMsg.append(operateInfo.getMessage());
+                }
 
+                throw new KDBizException(errorMsg.toString());
             }
         }
     }
 
-    private void doEffect(DynamicObject data) {
+    private DynamicObject buildAppointRecordDy(DynamicObject data) {
+        DynamicObject dyo = tsappHelper.generateEmptyDynamicObject();
 
+        for (Map.Entry<String, String> entry : keyMapping.entrySet()) {
+            dyo.set(entry.getKey(), data.get(entry.getValue()));
+        }
+
+        if (HRObjectUtils.isEmpty(dyo.get(TitleSkillAppointConstant.ENDDATE))) {
+            // 结束日期
+            dyo.set(TitleSkillAppointConstant.ENDDATE, HRDateTimeUtils.getSysMaxDate());
+        }
 
+        return dyo;
     }
 }

+ 53 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/hspm/plugin/operate/tsapp/validator/TitleSkillAppointEmployeeValidator.java

@@ -0,0 +1,53 @@
+package nckd.jxccl.hr.hspm.plugin.operate.tsapp.validator;
+
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.entity.ExtendedDataEntity;
+import kd.bos.entity.validate.AbstractValidator;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.hr.hbp.business.servicehelper.HRBaseServiceHelper;
+import kd.hr.hbp.common.util.HRStringUtils;
+import kd.sdk.hr.hdm.common.enums.reg.RegBillStatusEnum;
+import nckd.jxccl.base.common.utils.QueryFieldBuilder;
+import nckd.jxccl.hr.hspm.common.TitleSkillAppointConstant;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * 职称技能聘任单员工校验器
+ * @author: jtd
+ * @date: 2026/1/14 10:47
+ */
+public class TitleSkillAppointEmployeeValidator extends AbstractValidator {
+    /** 职称技能聘任单基础服务 */
+    private static final HRBaseServiceHelper tsapptHelper = HRBaseServiceHelper.create(TitleSkillAppointConstant.NCKD_JOB_TITLE_SKILL_APPT_ENTITY);
+
+    @Override
+    public void validate() {
+        String selectFields = QueryFieldBuilder.create().add(TitleSkillAppointConstant.BILL_NO_KEY).add(TitleSkillAppointConstant.NCKD_ENTRYENTITY, TitleSkillAppointConstant.NCKD_EMPLOYEE, TitleSkillAppointConstant.ID_KEY).buildSelect();
+        // 查询所有在途聘任单中的员工ID
+        Map<Long, String> employeeIds = tsapptHelper.queryOriginalCollection(selectFields, new QFilter[]{new QFilter(TitleSkillAppointConstant.BILL_STATUS_KEY, QCP.in, new Object[]{RegBillStatusEnum.ALREADYSUBMIT.getCode(), RegBillStatusEnum.APPROVING.getCode()})})
+                .stream().collect(Collectors.toMap(
+                        dyo -> dyo.getLong(String.join(".", TitleSkillAppointConstant.NCKD_EMPLOYEE, TitleSkillAppointConstant.ID_KEY)),
+                        dyo -> dyo.getString(String.join(".",TitleSkillAppointConstant.NCKD_ENTRYENTITY, TitleSkillAppointConstant.NCKD_EMPLOYEE, TitleSkillAppointConstant.ID_KEY)),
+                        (oldValue, newValue) -> newValue
+                ));
+        for (ExtendedDataEntity dataEntity : getDataEntities()) {
+            DynamicObjectCollection entryEntity = dataEntity.getDataEntity().getDynamicObjectCollection(TitleSkillAppointConstant.NCKD_ENTRYENTITY);
+            for (int i = 0; i < entryEntity.size(); i++) {
+                DynamicObject entry = entryEntity.get(i);
+                long employeeId = entry.getLong(String.join(".", TitleSkillAppointConstant.NCKD_EMPLOYEE, TitleSkillAppointConstant.ID_KEY));
+                if (employeeIds.containsKey(employeeId)) {
+                    String employeeDisplayname = entry.getDynamicObjectType().getProperty(TitleSkillAppointConstant.NCKD_EMPLOYEE).getDisplayName().getLocaleValue();
+                    String empNumber = entry.getString(String.join(".", TitleSkillAppointConstant.NCKD_EMPLOYEE, TitleSkillAppointConstant.EMP_NUMBER_KEY));
+                    String name = entry.getString(String.join(".", TitleSkillAppointConstant.NCKD_EMPLOYEE, TitleSkillAppointConstant.NAME_KEY));
+                    String billDisplayName = entryEntity.getDynamicObjectType().getDisplayName().getLocaleValue();
+                    addErrorMessage(dataEntity, HRStringUtils.format("第{}行{}[{}/{}]在{}[{}]中已存在,不允许重复聘任!", new Object[]{i+1, employeeDisplayname, empNumber, name, billDisplayName}));
+                }
+            }
+        }
+    }
+}

+ 51 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/hspm/plugin/operate/tsapp/validator/TitleSkillAppointStartDateValidator.java

@@ -0,0 +1,51 @@
+package nckd.jxccl.hr.hspm.plugin.operate.tsapp.validator;
+
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.entity.ExtendedDataEntity;
+import kd.bos.entity.validate.AbstractValidator;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.hr.hbp.business.servicehelper.HRBaseServiceHelper;
+import kd.hr.hbp.common.util.HRStringUtils;
+import nckd.jxccl.hr.hspm.common.TitleSkillAppointConstant;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 职称技能聘任单开始日期校验器
+ * @author: jtd
+ * @date: 2026/1/14 09:30
+ */
+public class TitleSkillAppointStartDateValidator extends AbstractValidator {
+    /** 员工职称技能聘任基础服务 */
+    private static final HRBaseServiceHelper tkappHelper = HRBaseServiceHelper.create(TitleSkillAppointConstant.NCKD_HRPI_TITLSKLAPP_ENTITY);
+
+    @Override
+    public void validate() {
+        for (ExtendedDataEntity dataEntity : getDataEntities()) {
+            DynamicObjectCollection entryEntity = dataEntity.getDataEntity().getDynamicObjectCollection(TitleSkillAppointConstant.NCKD_ENTRYENTITY);
+            Set<Long> employeeIds = entryEntity.stream().map(entry -> entry.getLong(String.join(".", TitleSkillAppointConstant.NCKD_EMPLOYEE, TitleSkillAppointConstant.ID_KEY))).collect(Collectors.toSet());
+            // 获取每个员工最近一笔聘任记录
+            Map<Long, Date> appointRecord = tkappHelper.queryOriginalCollection(TitleSkillAppointConstant.STARTDATE, new QFilter[]{new QFilter(TitleSkillAppointConstant.EMPLOYEE_KEY, QCP.in, employeeIds)}, String.format("%s desc", TitleSkillAppointConstant.STARTDATE))
+                    .stream().collect(Collectors.groupingBy(dyo -> dyo.getLong(String.join(".", TitleSkillAppointConstant.NCKD_EMPLOYEE, TitleSkillAppointConstant.ID_KEY)),
+                    Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0).getDate(TitleSkillAppointConstant.STARTDATE))
+            ));
+
+            // 校验最近一次聘任记录的开始时间
+            for (int i = 0; i < entryEntity.size(); i++) {
+                DynamicObject entry = entryEntity.get(i);
+                Long employeeId = entry.getLong(String.join(".", TitleSkillAppointConstant.NCKD_EMPLOYEE, TitleSkillAppointConstant.ID_KEY));
+                Date appointStartDate = entry.getDate(TitleSkillAppointConstant.NCKD_STARTDATE);
+                Date recordStartDate = appointRecord.get(employeeId);
+                if (recordStartDate != null && recordStartDate.after(appointStartDate)) {
+                    String displayName = entry.getDynamicObjectType().getProperty(TitleSkillAppointConstant.NCKD_STARTDATE).getDisplayName().getLocaleValue();
+                    addErrorMessage(dataEntity, HRStringUtils.format("第{}行${}不能早于最近一次聘任记录的开始日期[{}]!", new Object[]{i+1, displayName, recordStartDate}));
+                }
+            }
+        }
+    }
+}