Преглед изворни кода

feat(hr): 新增年度绩效排名管理功能及相关枚举常量

- 新增系统内置常量类SystemQueryConstant- 新增奖项级别枚举AwardLevelEnum和积分项目枚举ScoreItemEnum
- 新增年度贡献综合评价相关常量类ContributionEvaluationConstant- 修改绩效排名管理常量类PerfRankMgmtConstant,优化字段命名及注释
- 新增职位结构相关常量MANAGERALLOWANCE_ENTITYID
- 完善FormConstant常量类,新增员工实体标识、操作标识及页面控件常量
-优化JobLevelCalculatorService服务类中的过滤条件
- 新增PositionStructureHelper工具类方法getNewestPersonPosFileByPerson- 完善UngradedPersonQueryListPlugin插件,增加在职人员过滤条件
- 新增PerfrankMgmtFormPlugin表单插件,实现年度绩效排名管理功能
-修复AnnualLockOrUnLockedOpPlugin插件中的逻辑判断问题
- 完善PosFileUnlockBillOpPlugin插件,新增属性准备事件支持
wyc пре 1 недеља
родитељ
комит
1b6c55e7e3
18 измењених фајлова са 1391 додато и 11 уклоњено
  1. 29 1
      code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/constant/FormConstant.java
  2. 13 0
      code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/constant/SystemQueryConstant.java
  3. 63 0
      code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/enums/psms/AwardLevelEnum.java
  4. 71 0
      code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/enums/psms/ScoreItemEnum.java
  5. 2 2
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/business/JobLevelCalculatorService.java
  6. 24 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/common/ContributionEvaluationConstant.java
  7. 17 2
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/common/PerfRankMgmtConstant.java
  8. 5 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/common/PositionStructureConstant.java
  9. 58 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/helper/PositionStructureHelper.java
  10. 0 1
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/form/initial/NewHireInitialBatchFormPlugin.java
  11. 6 4
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/form/initial/UngradedPersonQueryListPlugin.java
  12. 422 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/form/performance/PerfrankMgmtFormPlugin.java
  13. 2 1
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/annualadjust/AnnualLockOrUnLockedOpPlugin.java
  14. 5 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/annualadjust/PosFileUnlockBillOpPlugin.java
  15. 74 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/contribution/validate/ScoreItemConfValidate.java
  16. 21 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/performance/PerfRankMgmtSaveOpPlugin.java
  17. 540 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/performance/validate/PerfRankMgmtSaveValidate.java
  18. 39 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/performance/validate/RankRatioConfValidate.java

+ 29 - 1
code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/constant/FormConstant.java

@@ -43,6 +43,8 @@ public class FormConstant {
     public static final String HRPI_PERSERLEN = "hrpi_perserlen";
     /** 雇佣信息-实体标识*/
     public static final String HRPI_EMPENTREL = "hrpi_empentrel";
+    /** 员工-实体标识*/
+    public static final String HRPI_EMPLOYEE = "hrpi_employee";
 
 
     //====================================== 标品op ======================================
@@ -70,6 +72,10 @@ public class FormConstant {
     public static final String OP_BAR_AUDIT = "bar_audit";
     /** 反审核操作标识*/
     public static final String OP_BAR_UNAUDIT = "bar_unaudit";
+    /** 新增分录操作*/
+    public static final String NEWENTRY_OP = "newentry";
+    /** 删除分录操作*/
+    public static final String DELETEENTRY_OP = "deleteentry";
 
 
     //====================================== 标品页面控件 ======================================
@@ -211,6 +217,28 @@ public class FormConstant {
     /** 职层-资格级别*/
     public static final String NCKD_JOBLEVELNUMBER = "NCKD_JOBLEVELNUMBER";
     /** 备注*/
-    public static final String NCKD_REMARK = "NCKD_REMARK";
+    public static final String NCKD_REMARK = "nckd_remark";
+    /** 组织*/
+    public static final String NCKD_ADMINORG = "nckd_adminorg";
+    /** 用工关系状态*/
+    public static final String LABOR_REL_STATUS = "laborrelstatus";
+    /** 是否在职*/
+    public static final String IS_HIRED = "ishired";
+    /** 是否试用*/
+    public static final String IS_TRIAL = "istrial";
+    /** 任职状态*/
+    public static final String POS_STATUS = "posstatus";
+    /** 任职状态分类*/
+    public static final String POST_STATE_CLS = "poststatecls";
+    /** 党政职务变更履历*/
+    public static final String NCKD_HRPI_PARTYPOSH = "nckd_hrpi_partyposh";
+    /** 职位级别*/
+    public static final String NCKD_POSGRADE = "nckd_posgrade";
+    /** 排序号*/
+    public static final String NCKD_SORTNUM = "nckd_sortnum";
+    /** 开始日期*/
+    public static final String NCKD_STARTDATE = "nckd_startdate";
+    /** 结束日期*/
+    public static final String NCKD_ENDDATE = "nckd_enddate";
 
 }

+ 13 - 0
code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/constant/SystemQueryConstant.java

@@ -0,0 +1,13 @@
+package nckd.jxccl.base.common.constant;
+
+/**
+ * 系统内置常量
+ * @author W.Y.C
+ * @date 2025/10/20 16:05
+ * @version 1.0
+ */
+public class SystemQueryConstant {
+
+    /**人员列表*/
+    public static final String HSPM_ASSIGNMENTQUERY = "hspm_assignmentquery";
+}

+ 63 - 0
code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/enums/psms/AwardLevelEnum.java

@@ -0,0 +1,63 @@
+package nckd.jxccl.base.common.enums.psms;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+* 奖项级别
+* @author W.Y.C
+* @date 2025/10/22 20:35
+* @version 1.0
+*/
+public enum AwardLevelEnum {
+
+    /**国家级*/
+    NATIONAL_LEVEL("1", "国家级"),
+    /**省部级*/
+    PROVINCIAL_LEVEL("2", "省部级"),
+    /**公司级*/
+    COMPANY_LEVEL("3", "公司级"),
+    /**矿级*/
+    MINE_LEVEL("4", "厂矿级"),
+    /**车间级*/
+    WORKSHOP_LEVEL("5", "车间级");
+
+
+    private static final Map<String, AwardLevelEnum> CODE_MAP = new HashMap<>();
+    static {
+        for (AwardLevelEnum value : AwardLevelEnum.values()) {
+            CODE_MAP.put(value.code, value);
+        }
+    }
+
+    private final String code;
+    private final String name;
+
+    AwardLevelEnum(String code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    /**
+     * 根据编码获取枚举
+     * @param code
+     * @return: AwardLevelEnum
+     * @author W.Y.C
+     * @date: 2025/09/12 14:33
+     */
+    public static AwardLevelEnum getByCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        return CODE_MAP.get(code);
+    }
+
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 71 - 0
code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/enums/psms/ScoreItemEnum.java

@@ -0,0 +1,71 @@
+package nckd.jxccl.base.common.enums.psms;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 员工年度贡献综合评价积分-项目
+ * @author W.Y.C
+ * @date 2025/10/14 10:35
+ * @version 1.0
+ */
+public enum ScoreItemEnum {
+
+    /**科研与创新积分*/
+    RESEARCH_SCORE("1", "科研与创新积分"),
+    /**专利申报积分*/
+    PATENT_SCORE("2", "专利申报积分"),
+    /**论文发表积分*/
+    PAPER_SCORE("3", "论文发表积分"),
+    /**技能竞赛积分*/
+    SKILL_CONTEST_SCORE("4", "技能竞赛积分"),
+    /**培训教材积分*/
+    TRAINING_MATERIAL_SCORE("5", "培训教材积分"),
+    /**技术标准积分*/
+    TECH_STANDARD_SCORE("6", "技术标准积分"),
+    /**管理规范积分*/
+    MGMT_SPEC_SCORE("7", "管理规范积分"),
+    /**培训授课积分*/
+    TRAINING_SCORE("8", "培训授课积分"),
+    /**师带徒积分*/
+    MENTOR_SCORE("9", "师带徒积分");
+
+    private static final Map<String, ScoreItemEnum> CODE_MAP = new HashMap<>();
+
+    static {
+        for (ScoreItemEnum value : ScoreItemEnum.values()) {
+            CODE_MAP.put(value.code, value);
+        }
+    }
+
+    private final String code;
+    private final String name;
+
+    ScoreItemEnum(String code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    /**
+     * 根据编码获取枚举
+     * @param code
+     * @return: AdjustTypeEnum
+     * @author W.Y.C
+     * @date: 2025/09/12 14:33
+     */
+    public static ScoreItemEnum getByCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        return CODE_MAP.get(code);
+    }
+
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 2 - 2
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/business/JobLevelCalculatorService.java

@@ -879,13 +879,13 @@ public class JobLevelCalculatorService {
         LocalDateTime lastYearDateTime = DateUtil.minusYears(DateUtil.toLocalDateTime(date), 1);
         //取上年度绩效排名
         QFilter groupFilter = new QFilter(PerfRankMgmtConstant.NCKD_THEYEAR,QCP.equals,lastYearDateTime.getYear());
-        groupFilter.and(PerfRankMgmtConstant.BILL_STATUS_KEY,QCP.equals, StatusEnum.C.toString());
+        groupFilter.and(QFilterCommonHelper.getStatusFilter());
         groupFilter.and(String.join(".",PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY,PerfRankMgmtConstant.NCKD_PERSON),QCP.equals, personId);
         groupFilter.and(String.join(".",PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY,PerfRankMgmtConstant.NCKD_ISRANKING),QCP.equals,EnableEnum.YES.getCode());
         //获取该员工上年度所在的排名分组
         DynamicObject[] groupArray = BusinessDataServiceHelper.load(PerfRankMgmtConstant.NCKD_PERFRANKMGMT_ENTITYID, FormConstant.ID_KEY+","+PerfRankMgmtConstant.NCKD_GROUPNAME,
                 new QFilter[]{groupFilter},
-                FormConstant.CREATE_TIME_KEY + " desc," + FormConstant.AUDIT_DATE_KEY + " desc," + FormConstant.MODIFY_TIME_KEY + " desc");
+                FormConstant.CREATE_TIME_KEY + " desc," + FormConstant.MODIFY_TIME_KEY + " desc");
         //避免一个员工存在多个排名分组中,只取最新排名分组
         Long groupId = null;
         String groupName = null;

+ 24 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/common/ContributionEvaluationConstant.java

@@ -0,0 +1,24 @@
+package nckd.jxccl.hr.psms.common;
+
+import nckd.jxccl.base.common.constant.FormConstant;
+
+/**
+ * 动态表单插件
+ */
+/**
+* 年度贡献综合评价-常量类
+* @author W.Y.C
+* @date 2025/10/22 21:53
+* @version 1.0
+*/
+public class ContributionEvaluationConstant extends FormConstant {
+
+    /** 积分项目配置-实体标识 */
+    public static final String SCOREITEMCONF_ENTITYID = "nckd_scoreitemconf";
+    /** 积分项目 */
+    public static final String NCKD_SCOREITEM = "NCKD_SCOREITEM";
+    /** 最高分数 */
+    public static final String NCKD_MAXSCORE = "NCKD_MAXSCORE";
+    /** 级别 */
+    public static final String NCKD_AWARDLEVEL = "NCKD_AWARDLEVEL";
+}

+ 17 - 2
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/common/PerfRankMgmtConstant.java

@@ -13,7 +13,7 @@ public class PerfRankMgmtConstant extends FormConstant {
     /** 年度绩效排名管理-实体标识 */
     public static final String NCKD_PERFRANKMGMT_ENTITYID = "nckd_perfrankmgmt";
     /** 单据体 */
-    public static final String NCKD_PERFRANKMGMTENTRY = "NCKD_PERFRANKMGMTENTRY";
+    public static final String NCKD_PERFRANKMGMTENTRY = "nckd_perfrankmgmtentry";
     /** 员工 */
     public static final String NCKD_PERSON = "NCKD_PERSON";
     /** 排名 */
@@ -28,7 +28,7 @@ public class PerfRankMgmtConstant extends FormConstant {
     public static final String NCKD_ISRANKING = "NCKD_ISRANKING";
     /** 不参与排名原因 */
     public static final String NCKD_EXCLUDERANKREASON = "NCKD_EXCLUDERANKREASON";
-    /** 排名来源 */
+    /** 排名来源(0:系统生成,1:手动添加) */
     public static final String NCKD_RANKSOURCE = "NCKD_RANKSOURCE";
     /** 组名 */
     public static final String NCKD_GROUPNAME = "NCKD_GROUPNAME";
@@ -44,4 +44,19 @@ public class PerfRankMgmtConstant extends FormConstant {
     public static final String NCKD_BASICS = "NCKD_BASICS";
     /** 优秀人数 */
     public static final String NCKD_EXCELLENTS = "NCKD_EXCELLENTS";
+    /** 行政组织 */
+    public static final String NCKD_ADMINORG = "nckd_adminorg";
+    /** 获取排名名单-按钮 */
+    public static final String NCKD_GETRANKLIST = "nckd_getranklist";
+    /** 获取排名名单-OP */
+    public static final String GETRANKLIST_OP = "getranklist";
+
+
+
+    /*-------------------------------------- 排名考核结果比例配置 begin --------------------------------------*/
+    /** 新建动态调整(弹窗)-实体标识 */
+    public static final String RANKRATIOCONF_ENTITYID = "nckd_rankratioconf";
+    /** 比例 */
+    public static final String NCKD_RATIO = "nckd_ratio";
+    /*-------------------------------------- 排名考核结果比例配置 end --------------------------------------*/
 }

+ 5 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/common/PositionStructureConstant.java

@@ -250,6 +250,11 @@ public class PositionStructureConstant extends FormConstant {
 
     /*-------------------------------------- 年度调整解锁单据 end --------------------------------------*/
 
+    /*-------------------------------------- 配置有职位津贴的管理人员 begin --------------------------------------*/
+    /** 配置有职位津贴的管理人员-实体标识 */
+    public static final String MANAGERALLOWANCE_ENTITYID = "nckd_managerallowance";
+    /*-------------------------------------- 配置有职位津贴的管理人员 end --------------------------------------*/
+
 
     /**职位序列对应职级查询*/
     public static final String JOBSEQTOJOBLEVEL_QUERY = "jobseqtojoblevelquery";

+ 58 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/helper/PositionStructureHelper.java

@@ -43,6 +43,56 @@ public class PositionStructureHelper {
         return QueryServiceHelper.exists(PositionStructureConstant.PERSONPOSFILE_ENTITYID, new QFilter[]{filter});
     }
 
+    /**
+     * 获取最新人员档案
+     * @param personId 人员ID
+     * @return: kd.bos.dataentity.entity.DynamicObject[]
+     * @author W.Y.C
+     * @date: 2025/10/20 21:56
+     */
+    public static DynamicObject[] getNewestPersonPosFileByPerson(Long... personId) {
+        QFilter filer = new QFilter(String.join(".", FormConstant.NCKD_PERSON, FormConstant.ID_KEY), QCP.in, personId)
+                .and(PositionStructureConstant.NCKD_DISABLE, QCP.equals, EnableEnum.NO.getCode())
+                .and(PositionStructureConstant.NCKD_ISCURRENTNEWEST, QCP.equals, EnableEnum.YES.getCode());
+        QueryFieldBuilder queryFieldBuilder = QueryFieldBuilder.create()
+                .add(FormConstant.ID_KEY)
+                .add(PositionStructureConstant.NCKD_BEGINDATE)
+                .add(PositionStructureConstant.NCKD_EXECUTEYEAR)
+                .add(PositionStructureConstant.NCKD_TYPESTATE)
+                .add(PositionStructureConstant.NCKD_FIRSTRANK)
+                .add(PositionStructureConstant.NCKD_DIPLOMASCORE)
+                .add(PositionStructureConstant.NCKD_RANKSCORE)
+                .add(PositionStructureConstant.NCKD_JOBSTATUSSCORE)
+                .addIdNumberName(PositionStructureConstant.NCKD_APPRAISALRESULT)
+                .addIdNumberName(PositionStructureConstant.NCKD_PERSON)
+                .add(PositionStructureConstant.NCKD_ALLOWANCERANKPCT)
+                .add(PositionStructureConstant.NCKD_ALLOWANCERANKSEL)
+                .add(PositionStructureConstant.NCKD_ALLOWANCERANKMARK)
+                .add(PositionStructureConstant.NCKD_ALLSUMSCORE)
+                .add(PositionStructureConstant.NCKD_SUMSCORE)
+                .add(PositionStructureConstant.NCKD_JOBLEVELHR)
+                .add(PositionStructureConstant.NCKD_RANKNAME)
+                .addIdNumberName(PositionStructureConstant.NCKD_PROTITLELEVEL)
+                .add(PositionStructureConstant.NCKD_JOBSTATUSNAME)
+                .addIdNumberName(PositionStructureConstant.NCKD_OCPQUALLEVEL)
+                .addIdNumberNameWithExtras(
+                        new String[]{PositionStructureConstant.NCKD_JOBLEVELHR},
+                        FormConstant.JOBLEVELSEQ
+                )
+                .addIdNumberNameWithExtras(
+                        new String[]{PositionStructureConstant.NCKD_DIPLOMA},
+                        FormConstant.NCKD_SCORE
+                )
+                .addIdNumberName(PositionStructureConstant.NCKD_JOBSEQHR);
+        DynamicObject[] load = BusinessDataServiceHelper.load(
+                PositionStructureConstant.PERSONPOSFILE_ENTITYID,
+                queryFieldBuilder.buildSelect(),
+                new QFilter[]{filer},
+                queryFieldBuilder.buildOrder()
+        );
+        return load;
+    }
+
     /**
      * 根据人员和状态过滤职位档案
      * @param personId 人员ID
@@ -291,11 +341,15 @@ public class PositionStructureHelper {
                 .add(PositionStructureConstant.NCKD_EXECUTEYEAR)
                 .add(PositionStructureConstant.NCKD_TYPESTATE)
                 .add(PositionStructureConstant.NCKD_FIRSTRANK)
+                .add(PositionStructureConstant.NCKD_DIPLOMASCORE)
+                .add(PositionStructureConstant.NCKD_RANKSCORE)
+                .add(PositionStructureConstant.NCKD_JOBSTATUSSCORE)
                 .addIdNumberName(PositionStructureConstant.NCKD_APPRAISALRESULT)
                 .add(PositionStructureConstant.NCKD_ALLOWANCERANKPCT)
                 .add(PositionStructureConstant.NCKD_ALLOWANCERANKSEL)
                 .add(PositionStructureConstant.NCKD_ALLOWANCERANKMARK)
                 .add(PositionStructureConstant.NCKD_ALLSUMSCORE)
+                .add(PositionStructureConstant.NCKD_SUMSCORE)
                 .add(PositionStructureConstant.NCKD_JOBLEVELHR)
                 .add(PositionStructureConstant.NCKD_RANKNAME)
                 .addIdNumberName(PositionStructureConstant.NCKD_PROTITLELEVEL)
@@ -305,6 +359,10 @@ public class PositionStructureHelper {
                         new String[]{PositionStructureConstant.NCKD_JOBLEVELHR},
                         FormConstant.JOBLEVELSEQ
                 )
+                .addIdNumberNameWithExtras(
+                        new String[]{PositionStructureConstant.NCKD_DIPLOMA},
+                        FormConstant.NCKD_SCORE
+                )
                 .addIdNumberName(PositionStructureConstant.NCKD_JOBSEQHR)
                 .orderDesc(PositionStructureConstant.NCKD_BEGINDATE);
 

+ 0 - 1
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/form/initial/NewHireInitialBatchFormPlugin.java

@@ -10,7 +10,6 @@ import kd.bos.form.plugin.AbstractFormPlugin;
 import kd.bos.orm.query.QCP;
 import kd.bos.orm.query.QFilter;
 import kd.bos.servicehelper.QueryServiceHelper;
-import kd.drp.pos.common.util.StringJoin;
 import kd.sdk.plugin.Plugin;
 import nckd.jxccl.base.common.constant.FormConstant;
 import nckd.jxccl.base.common.utils.ConvertUtil;

+ 6 - 4
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/form/initial/UngradedPersonQueryListPlugin.java

@@ -41,10 +41,12 @@ public class UngradedPersonQueryListPlugin extends AbstractListPlugin implements
         //只查询没有初定的人员
         QFilter filter = new QFilter(String.join(".", PositionStructureConstant.PERSONPOSFILE_ENTITYID, FormConstant.ID_KEY), QCP.is_null, null)
                 //组织分配为主组织分配的人员
-                .and(new QFilter(String.join(".",FormConstant.ASSIGNMENT_ENTITYID, FormConstant.IS_PRIMARY), QCP.equals,EnableEnum.YES.getCode()))
-                .and(new QFilter(FormConstant.IS_SEQLATESTRECORD, QCP.equals,EnableEnum.YES.getCode()))
-                .and(new QFilter(FormConstant.IS_DELETED, QCP.equals,EnableEnum.NO.getCode()))
-                .and(new QFilter(FormConstant.IS_PRIMARY, QCP.equals,EnableEnum.YES.getCode()));
+                .and(String.join(".",FormConstant.ASSIGNMENT_ENTITYID, FormConstant.IS_PRIMARY), QCP.equals,EnableEnum.YES.getCode())
+                .and(FormConstant.IS_SEQLATESTRECORD, QCP.equals,EnableEnum.YES.getCode())
+                .and(FormConstant.IS_DELETED, QCP.equals,EnableEnum.NO.getCode())
+                .and(FormConstant.IS_PRIMARY, QCP.equals,EnableEnum.YES.getCode())
+                //只查询在职人员
+                .and(String.join(".",FormConstant.HRPI_EMPENTREL, FormConstant.LABOR_REL_STATUS,FormConstant.IS_HIRED), QCP.equals,EnableEnum.YES.getCode());
         setFilterEvent.addCustomQFilter(filter);
     }
 

+ 422 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/form/performance/PerfrankMgmtFormPlugin.java

@@ -0,0 +1,422 @@
+package nckd.jxccl.hr.psms.plugin.form.performance;
+
+import kd.bos.bill.IBillView;
+import kd.bos.common.enums.EnableEnum;
+import kd.bos.dataentity.OperateOption;
+import kd.bos.dataentity.RefObject;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.dataentity.metadata.dynamicobject.DynamicObjectType;
+import kd.bos.entity.EntityMetadataCache;
+import kd.bos.entity.QueryEntityType;
+import kd.bos.entity.datamodel.IDataModel;
+import kd.bos.entity.datamodel.RowDataEntity;
+import kd.bos.entity.datamodel.events.AfterAddRowEventArgs;
+import kd.bos.entity.datamodel.events.AfterDeleteRowEventArgs;
+import kd.bos.entity.datamodel.events.PropertyChangedArgs;
+import kd.bos.ext.hr.service.query.QueryEntityHelper;
+import kd.bos.form.ConfirmCallBackListener;
+import kd.bos.form.ConfirmTypes;
+import kd.bos.form.IClientViewProxy;
+import kd.bos.form.MessageBoxOptions;
+import kd.bos.form.MessageBoxResult;
+import kd.bos.form.control.EntryGrid;
+import kd.bos.form.control.Toolbar;
+import kd.bos.form.control.events.BeforeClickEvent;
+import kd.bos.form.control.events.ItemClickEvent;
+import kd.bos.form.control.events.ItemClickListener;
+import kd.bos.form.events.AfterDoOperationEventArgs;
+import kd.bos.form.events.BeforeDoOperationEventArgs;
+import kd.bos.form.events.MessageBoxClosedEvent;
+import kd.bos.form.operate.FormOperate;
+import kd.bos.form.plugin.AbstractFormPlugin;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.servicehelper.QueryServiceHelper;
+import kd.sdk.plugin.Plugin;
+import nckd.jxccl.base.common.constant.FormConstant;
+import nckd.jxccl.base.common.constant.SystemQueryConstant;
+import nckd.jxccl.base.common.enums.AppraisalResultEnum;
+import nckd.jxccl.base.common.utils.ConvertUtil;
+import nckd.jxccl.base.common.utils.DateUtil;
+import nckd.jxccl.base.common.utils.QueryFieldBuilder;
+import nckd.jxccl.hr.hstu.business.CreateEvalQuestService;
+import nckd.jxccl.hr.psms.common.PerfRankMgmtConstant;
+import nckd.jxccl.hr.psms.common.PositionStructureConstant;
+import nckd.jxccl.hr.psms.helper.PositionStructureHelper;
+import org.apache.commons.lang3.StringUtils;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.EventObject;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+
+/**
+* 年度绩效排名管理表单
+* @author W.Y.C
+* @date 2025/10/20 15:11
+* @version 1.0
+*/
+public class PerfrankMgmtFormPlugin extends AbstractFormPlugin implements Plugin {
+
+    private static final Log logger = LogFactory.getLog(PerfrankMgmtFormPlugin.class);
+
+    private final static String CALLBACK_ID = "interrupt_confirm";
+
+    @Override
+    public void afterBindData(EventObject e) {
+        sortEntry();
+    }
+
+    @Override
+    public void afterAddRow(AfterAddRowEventArgs e) {
+        try {
+            // 获取新增行信息并设置焦点
+            for (RowDataEntity rowDataEntity : e.getRowDataEntities()) {
+                int rowIndex = rowDataEntity.getRowIndex();
+                //获取分录焦点
+                IClientViewProxy viewProxy = this.getView().getService(IClientViewProxy.class);
+                viewProxy.focusCell(PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY, rowIndex, FormConstant.NCKD_PERSON);
+            }
+            this.getView().updateView(PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY);
+        } catch (Exception ex) {
+            logger.error("设置新增行焦点失败", ex);
+        }
+    }
+
+
+    @Override
+    public void propertyChanged(PropertyChangedArgs e) {
+        String name = e.getProperty().getName();
+        if(Stream.of(PerfRankMgmtConstant.NCKD_ISRANKING, PerfRankMgmtConstant.NCKD_APPRAISALRESULT,
+                        PerfRankMgmtConstant.NCKD_ALLOWANCERANK, PerfRankMgmtConstant.NCKD_POSTALLOWANCE,
+                        PerfRankMgmtConstant.NCKD_TOPRANK)
+                .anyMatch(prop -> prop.equalsIgnoreCase(name))){
+            calcRankCount();
+        }
+
+    }
+
+
+    @Override
+    public void beforeDoOperation(BeforeDoOperationEventArgs args) {
+        FormOperate operate = (FormOperate) args.getSource();
+        String itemKey = operate.getOperateKey();
+        if(PerfRankMgmtConstant.GETRANKLIST_OP.equalsIgnoreCase(itemKey)) {
+            RefObject<String> afterConfirm = new RefObject<>();
+            if (!operate.getOption().tryGetVariableValue("afterConfirm", afterConfirm)) {
+                //判断是否已获取过名单,如获取过再获取提示用户数据会丢失
+                DynamicObjectCollection entry = this.getModel().getEntryEntity(PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY);
+                boolean isGetRank = false;
+                if (!entry.isEmpty()) {
+                    for (DynamicObject dynamicObject : entry) {
+                        String rankSource = dynamicObject.getString(PerfRankMgmtConstant.NCKD_RANKSOURCE);
+                        if (EnableEnum.NO.getCode().equals(rankSource)) {
+                            isGetRank = true;
+                            break;
+                        }
+                    }
+                }
+                if (isGetRank) {
+                    // 取消当前操作,等待用户确认
+                    args.setCancel(true);
+                    //询问对话
+                    this.getView().showConfirm(
+                            "已获取过排名名单了,重新获取将会覆盖(系统生成部分)现有人员名单是否继续操作?",
+                            MessageBoxOptions.OKCancel,
+                            ConfirmTypes.Default,
+                            new ConfirmCallBackListener(CALLBACK_ID, this)
+                    );
+                }
+            }
+        }else if(Arrays.asList(FormConstant.SAVE_OP, FormConstant.SUBMIT_OP).contains(itemKey)){
+            //保存或提交先计算一遍
+            // calcRankCount();
+        }
+    }
+
+    @Override
+    public void confirmCallBack(MessageBoxClosedEvent evt) {
+        super.confirmCallBack(evt);
+        if (CALLBACK_ID.equals(evt.getCallBackId())) {
+            if (evt.getResult() == MessageBoxResult.Yes) {
+                // 用户选择“是”,继续执行业务逻辑
+                OperateOption option = OperateOption.create();
+                option.setVariableValue("afterConfirm", "true");
+                this.getView().invokeOperation(PerfRankMgmtConstant.GETRANKLIST_OP,option);
+            } else {
+                // 用户选择“否”,中断操作
+                this.getView().showTipNotification("操作已取消");
+            }
+        }
+    }
+
+    @Override
+    public void afterDoOperation(AfterDoOperationEventArgs afterDoOperationEventArgs) {
+        String itemKey = afterDoOperationEventArgs.getOperateKey();
+        if(PerfRankMgmtConstant.GETRANKLIST_OP.equalsIgnoreCase(itemKey)){
+            IBillView billView = (IBillView)this.getView();
+            boolean isFormDb = billView.getModel().getDataEntity().getDataEntityState().getFromDatabase();
+            if (!isFormDb) {
+                billView.showTipNotification("请保存后再获取排名名单");
+            }else{
+                IDataModel model = this.getModel();
+                DynamicObject adminOrg = ConvertUtil.toDynamicObject(model.getValue(FormConstant.NCKD_ADMINORG));
+                String structLongNumber = adminOrg.getString(FormConstant.STRUCTLONGNUMBER);
+                //查询所选组织下的人员
+                QFilter qFilter = new QFilter(String.join(".", FormConstant.HRPI_EMPPOSORGREL, FormConstant.ADMINORG, FormConstant.STRUCTLONGNUMBER), QCP.like, structLongNumber + "%");
+                //在职人员
+                qFilter.and(String.join(".", FormConstant.HRPI_EMPENTREL, FormConstant.LABOR_REL_STATUS,FormConstant.IS_HIRED), QCP.equals, EnableEnum.YES.getCode());
+                QueryFieldBuilder queryFieldBuilder = QueryFieldBuilder.create()
+                        //用工关系状态.编码(1005_S	在职-试用中、1010_S	在职、1020_S	离职、1030_S	已退休),istrial:是否试用,ishired:是否在职
+                        .addGroup(new String[]{FormConstant.HRPI_EMPENTREL, FormConstant.LABOR_REL_STATUS}, FormConstant.NUMBER_KEY, FormConstant.NAME_KEY, FormConstant.IS_TRIAL, FormConstant.IS_HIRED)
+                        //任职状态分类(1010_S:在岗,1030_S:不在岗)
+                        .addGroup(new String[]{FormConstant.HRPI_EMPPOSORGREL, FormConstant.POS_STATUS, FormConstant.POST_STATE_CLS}, FormConstant.NUMBER_KEY)
+                        //入职日期
+                        .addGroup(new String[]{FormConstant.HRPI_EMPENTREL}, FormConstant.ENTRYDATE)
+                        //职务级别
+                        .addGroup(new String[]{FormConstant.NCKD_HRPI_PARTYPOSH, FormConstant.NCKD_POSGRADE}, FormConstant.NAME_KEY, FormConstant.NUMBER_KEY, FormConstant.NCKD_SORTNUM)
+                        .addGroup(new String[]{FormConstant.EMPLOYEE_KEY}, FormConstant.ID_KEY, FormConstant.NAME_KEY, FormConstant.EMP_NUMBER_KEY);
+
+                // -------------------------------- 1、查询组织下的在职人员 --------------------------------
+                QueryEntityType queryEntityType = (QueryEntityType) EntityMetadataCache.getDataEntityType(SystemQueryConstant.HSPM_ASSIGNMENTQUERY);
+                DynamicObjectCollection personList = QueryEntityHelper.getInstance().getQueryDyoColl(queryEntityType, queryFieldBuilder.buildSelect(), new QFilter[]{qFilter},queryFieldBuilder.buildOrder());
+                //添加分录
+                DynamicObjectCollection entryEntityCols = this.getModel().getDataEntity(true).getDynamicObjectCollection(PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY);
+                //根据条件删除分录
+
+                // -------------------------------- 2、删除系统生成部分人员 --------------------------------
+                //删除系统生成部分人员
+                entryEntityCols.removeIf(obj -> EnableEnum.NO.getCode().equals(obj.getString(PerfRankMgmtConstant.NCKD_RANKSOURCE)));
+
+                List<Long> personIds = personList.stream()
+                        .map(person -> person.getLong(String.join(".", FormConstant.EMPLOYEE_KEY, FormConstant.ID_KEY)))
+                        .collect(Collectors.toList());
+                if(!personIds.isEmpty()) {
+
+                    // -------------------------------- 3、获取人员对应的职级 --------------------------------
+                    //获取职位积分档案,用于判断是否有职级;职级大于0默认勾选“职位津贴”
+                    DynamicObject[] newestPersonPosFileByPerson = PositionStructureHelper.getNewestPersonPosFileByPerson(personIds.toArray(new Long[0]));
+                    Map<Long, Long> personJobLevelMap = Arrays.stream(newestPersonPosFileByPerson)
+                            .collect(Collectors.toMap(
+                                    obj -> obj.getLong(String.join(".", FormConstant.NCKD_PERSON, FormConstant.ID_KEY)),
+                                    obj -> obj.getLong(String.join(".", PositionStructureConstant.NCKD_JOBLEVELHR, FormConstant.JOBLEVELSEQ))
+                            ));
+
+                    // -------------------------------- 4、获取管理人员职位津贴配置 --------------------------------
+                    //获取《管理人员职位津贴》,存在则默认勾选“职位津贴”
+                    //查询在当前时间范围内的数据。NCKD_STARTDATE,NCKD_ENDDATE
+                    Date now = new Date();
+                        QFilter managerAllowanceFilter = new QFilter(FormConstant.NCKD_STARTDATE, QCP.less_equals, now)
+                            .and(FormConstant.NCKD_ENDDATE, QCP.large_equals,now)
+                            .and(FormConstant.NCKD_PERSON, QCP.in, personIds);
+                    DynamicObjectCollection managerAllowancQuery = QueryServiceHelper.query(PositionStructureConstant.MANAGERALLOWANCE_ENTITYID, String.join(".", FormConstant.NCKD_PERSON, FormConstant.ID_KEY), new QFilter[]{managerAllowanceFilter});
+                    Map<Long, Boolean> managerAllowanceMap = managerAllowancQuery.stream()
+                            .collect(Collectors.toMap(
+                                    obj -> obj.getLong(String.join(".", FormConstant.NCKD_PERSON, FormConstant.ID_KEY)),
+                                    obj -> Boolean.TRUE
+                            ));
+
+                    // -------------------------------- 4、加载数据到分录 --------------------------------
+                    for (DynamicObject person : personList) {
+                        DynamicObject entryCol = entryEntityCols.addNew();
+                        long personId = person.getLong(String.join(".", FormConstant.EMPLOYEE_KEY, FormConstant.ID_KEY));
+                        String personName = person.getString(String.join(".", FormConstant.EMPLOYEE_KEY, FormConstant.NAME_KEY));
+                        String personNumber = person.getString(String.join(".", FormConstant.EMPLOYEE_KEY, FormConstant.EMP_NUMBER_KEY));
+
+                        DynamicObjectType type = EntityMetadataCache.getDataEntityType(FormConstant.HRPI_EMPLOYEE);
+                        DynamicObject personObj = (DynamicObject)type.createInstance();
+                        personObj.set(FormConstant.ID_KEY, personId);
+                        personObj.set(FormConstant.NAME_KEY, personName);
+                        personObj.set(FormConstant.EMP_NUMBER_KEY, personNumber);
+                        entryCol.set(FormConstant.NCKD_PERSON, personObj);
+                        //是否试用
+                        boolean isTrial = person.getBoolean(String.join(".", FormConstant.HRPI_EMPENTREL, FormConstant.LABOR_REL_STATUS, FormConstant.IS_TRIAL));
+                        //入职日期
+                        Date entryDate = person.getDate(String.join(".", FormConstant.HRPI_EMPENTREL, FormConstant.ENTRYDATE));
+                        //职务级别编码
+                        Integer posGradeSort = null;
+                        if (person.containsProperty(FormConstant.NCKD_HRPI_PARTYPOSH)
+                                && person.containsProperty(String.join(".", FormConstant.NCKD_HRPI_PARTYPOSH, FormConstant.NCKD_POSGRADE))
+                                && person.containsProperty(String.join(".", FormConstant.NCKD_HRPI_PARTYPOSH, FormConstant.NCKD_POSGRADE, FormConstant.NCKD_SORTNUM))) {
+                            posGradeSort = person.getInt(String.join(".", FormConstant.NCKD_HRPI_PARTYPOSH, FormConstant.NCKD_POSGRADE, FormConstant.NCKD_SORTNUM));
+                        }
+                        StringBuilder reason = new StringBuilder();
+                        boolean participateRank = isParticipateRank(isTrial, entryDate, posGradeSort, reason);
+                        entryCol.set(PerfRankMgmtConstant.NCKD_ISRANKING, participateRank);
+                        entryCol.set(PerfRankMgmtConstant.NCKD_EXCLUDERANKREASON, reason.toString());
+                        entryCol.set(PerfRankMgmtConstant.NCKD_RANKSOURCE, EnableEnum.NO.getCode());
+                        //判断是否有职位津贴
+                        boolean hasAllowance = personJobLevelMap.containsKey(personId) && personJobLevelMap.get(personId) > 0 || managerAllowanceMap.containsKey(personId);
+                        entryCol.set(PerfRankMgmtConstant.NCKD_POSTALLOWANCE, hasAllowance);
+
+                    }
+                    this.getView().showSuccessNotification("名单获取完成");
+                }else{
+                    this.getView().showTipNotification("未获取到人员");
+                }
+            }
+        }
+        if(Arrays.asList(FormConstant.DELETEENTRY_OP, PerfRankMgmtConstant.GETRANKLIST_OP).contains(itemKey)){
+            sortEntry();
+            calcRankCount();
+        }
+    }
+
+
+
+    /**
+     * 判断是否参与排名
+     * @param isTrial 是否试用
+     * @param entryDate 入职日期
+     * @param posGradeSort 职务级别排序
+     * @return: boolean
+     * @author W.Y.C
+     * @date: 2025/10/20 20:17
+     */
+    private boolean isParticipateRank(boolean isTrial,Date entryDate,Integer posGradeSort,StringBuilder reason) {
+        //获取入职天数
+        long between = DateUtil.between(DateUtil.toLocalDateTime(entryDate), DateUtil.now(), ChronoUnit.DAYS);
+        if(between < 183){
+            //本所属组织未待满183天
+            reason.append("本所属组织未待满183天");
+            return false;
+        }
+        if(isTrial){
+            reason.append("试用");
+            return false;
+        }
+        if(posGradeSort != null && posGradeSort >= 115){
+            reason.append("副科级及以上不参与");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 分录排序
+     * @param
+     * @return: void
+     * @author W.Y.C
+     * @date: 2025/10/21 16:00
+     */
+    private void sortEntry(){
+        DynamicObjectCollection entryEntityCols = this.getModel().getDataEntity(true).getDynamicObjectCollection(PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY);
+        int allowanceRank = entryEntityCols.get(0).getInt(PerfRankMgmtConstant.NCKD_ALLOWANCERANK);
+        entryEntityCols.sort((o1, o2) -> {
+            // 先按 NCKD_TOPRANK 排序,空值或 0 视为最大值(排在最后)
+            Integer topRank1 = o1.getInt(PerfRankMgmtConstant.NCKD_TOPRANK);
+            Integer topRank2 = o2.getInt(PerfRankMgmtConstant.NCKD_TOPRANK);
+            if (topRank1 == 0) {
+                topRank1 = Integer.MAX_VALUE;
+            }
+            if (topRank2 == 0) {
+                topRank2 = Integer.MAX_VALUE;
+            }
+
+            int topRankCompare = topRank1.compareTo(topRank2);
+            if (topRankCompare != 0) {
+                return topRankCompare;
+            }
+
+            // 再按 NCKD_ALLOWANCERANK 排序,空值或 0 视为最大值(排在最后)
+            Integer allowanceRank1 = o1.getInt(PerfRankMgmtConstant.NCKD_ALLOWANCERANK);
+            Integer allowanceRank2 = o2.getInt(PerfRankMgmtConstant.NCKD_ALLOWANCERANK);
+            if (allowanceRank1 == 0) {
+                allowanceRank1 = Integer.MAX_VALUE;
+            }
+            if (allowanceRank2 == 0) {
+                allowanceRank2 = Integer.MAX_VALUE;
+            }
+
+            int allowanceRankCompare = allowanceRank1.compareTo(allowanceRank2);
+            if (allowanceRankCompare != 0) {
+                return allowanceRankCompare;
+            }
+
+            // 最后按 NCKD_ISRANKING 排序,true 在前,false 在后
+            Boolean isRanking1 = o1.getBoolean(PerfRankMgmtConstant.NCKD_ISRANKING);
+            Boolean isRanking2 = o2.getBoolean(PerfRankMgmtConstant.NCKD_ISRANKING);
+            return Boolean.compare(isRanking2, isRanking1);
+        });
+        this.getModel().updateEntryCache(entryEntityCols);
+        this.getView().updateView(PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY);
+    }
+
+    /**
+     * 计算排名数
+     * @return: void
+     * @author W.Y.C
+     * @date: 2025/10/21 16:30
+     */
+    private void calcRankCount(){
+        DynamicObjectCollection entryEntity = this.getModel().getEntryEntity(PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY);
+
+        int totalRankCount = 0;
+        int allowanceRankCount = 0;
+        int failCount = 0;
+        int basicCount = 0;
+        int excellentCount = 0;
+
+        for (DynamicObject entry : entryEntity) {
+            DynamicObject person = entry.getDynamicObject(FormConstant.NCKD_PERSON);
+            if(person != null) {
+                //是否排名
+                boolean isRanking = entry.getBoolean(PerfRankMgmtConstant.NCKD_ISRANKING);
+                //是否R排名
+                boolean postAllowance = entry.getBoolean(PerfRankMgmtConstant.NCKD_POSTALLOWANCE);
+                //考核结果
+                DynamicObject appraisalResult = entry.getDynamicObject(PerfRankMgmtConstant.NCKD_APPRAISALRESULT);
+
+                if (isRanking) {
+                    totalRankCount++;  // 统计参加排名人数
+
+                    if (postAllowance) {
+                        allowanceRankCount++;  // 统计参加R排名人数
+                    }
+                }
+
+                if (appraisalResult != null) {
+                    String appraisalResultNumber = appraisalResult.getString(FormConstant.NUMBER_KEY);
+                    AppraisalResultEnum appraisalResultEnum = AppraisalResultEnum.getByCode(appraisalResultNumber);
+
+                    if (appraisalResultEnum != null) {
+                        switch (appraisalResultEnum) {
+                            case EXCELLENT:
+                                excellentCount++;
+                                break;
+                            case BASICALLY_QUALIFIED:
+                                basicCount++;
+                                break;
+                            case UN_QUALIFIED:
+                                failCount++;
+                                break;
+                            default:
+                                break;
+                        }
+                    }
+                }
+            }
+        }
+        //参加排名总人数
+        this.getModel().setValue(PerfRankMgmtConstant.NCKD_TOPRANKS, totalRankCount);
+        //参加R排名人数
+        this.getModel().setValue(PerfRankMgmtConstant.NCKD_ALLOWANCERANKS, allowanceRankCount);
+        //不合格人数
+        this.getModel().setValue(PerfRankMgmtConstant.NCKD_FAILS, failCount);
+        //基本合格人数
+        this.getModel().setValue(PerfRankMgmtConstant.NCKD_BASICS, basicCount);
+        //优秀人数
+        this.getModel().setValue(PerfRankMgmtConstant.NCKD_EXCELLENTS, excellentCount);
+    }
+}

+ 2 - 1
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/annualadjust/AnnualLockOrUnLockedOpPlugin.java

@@ -105,8 +105,9 @@ public class AnnualLockOrUnLockedOpPlugin extends AbstractOperationServicePlugIn
                     } else if(PositionStructureConstant.UNLOCKED_OP.equals(operateKey)) {
                         if(!lockStatus) {
                             message.add(StrFormatter.format("【{}】的年度调整已是“解锁”状态,忽略此条处理。", person.getString("name")));
+                        }else{
+                            ids.add(id);
                         }
-                        ids.add(id);
                     }
                 }
             }

+ 5 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/annualadjust/PosFileUnlockBillOpPlugin.java

@@ -5,6 +5,7 @@ import kd.bos.context.RequestContext;
 import kd.bos.dataentity.entity.DynamicObject;
 import kd.bos.dataentity.entity.DynamicObjectCollection;
 import kd.bos.entity.plugin.AbstractOperationServicePlugIn;
+import kd.bos.entity.plugin.PreparePropertysEventArgs;
 import kd.bos.entity.plugin.args.BeginOperationTransactionArgs;
 import kd.bos.orm.query.QFilter;
 import kd.bos.servicehelper.BusinessDataServiceHelper;
@@ -27,6 +28,10 @@ import java.util.List;
 */
 public class PosFileUnlockBillOpPlugin extends AbstractOperationServicePlugIn implements Plugin {
 
+    @Override
+    public void onPreparePropertys(PreparePropertysEventArgs e) {
+        e.getFieldKeys().add(PositionStructureConstant.NCKD_PERSONPOSFILE);
+    }
 
     @Override
     public void beginOperationTransaction(BeginOperationTransactionArgs e) {

+ 74 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/contribution/validate/ScoreItemConfValidate.java

@@ -0,0 +1,74 @@
+package nckd.jxccl.hr.psms.plugin.operate.contribution.validate;
+
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.entity.ExtendedDataEntity;
+import kd.bos.entity.validate.AbstractValidator;
+import nckd.jxccl.base.common.constant.FormConstant;
+import nckd.jxccl.base.common.enums.psms.AwardLevelEnum;
+import nckd.jxccl.base.common.enums.psms.ScoreItemEnum;
+import nckd.jxccl.hr.psms.common.ContributionEvaluationConstant;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 积分项目配置-校验器
+ * 实体标识:nckd_scoreitemconf
+ * @author W.Y.C
+ * @date 2025/10/22 21:50
+ * @version 1.0
+ */
+public class ScoreItemConfValidate extends AbstractValidator {
+
+    @Override
+    public void validate() {
+        for (ExtendedDataEntity dataEntity : getDataEntities()) {
+            DynamicObject data = dataEntity.getDataEntity();
+            DynamicObjectCollection entrys = data.getDynamicObjectCollection(FormConstant.NCKD_ENTRYENTITY);
+            if(entrys.isEmpty()){
+                addMessage(dataEntity, "请添加积分项目");
+                return;
+            }
+            Set<String> uniqueCombinations = new HashSet<>();
+            for (DynamicObject entry : entrys) {
+                //级别
+                String awardLevel = entry.getString(ContributionEvaluationConstant.NCKD_AWARDLEVEL);
+                //积分项目
+                String scoreItem = entry.getString(ContributionEvaluationConstant.NCKD_SCOREITEM);
+
+                // 检查是否已存在相同的组合
+                if (awardLevel != null && !awardLevel.isEmpty() && scoreItem != null && !scoreItem.isEmpty()) {
+                    String[] levels = awardLevel.split(",");
+                    for (String level : levels) {
+                        String trimmedLevel = level.trim();
+                        // 过滤空字符串
+                        if (!trimmedLevel.isEmpty()) {
+                            String combinationKey = scoreItem + "_" + trimmedLevel;
+                            if (!uniqueCombinations.add(combinationKey)) {
+                                addMessage(dataEntity, "同一积分项目在同一级别不能重复配置:" +
+                                        ScoreItemEnum.getByCode(scoreItem).getName() + " - " +
+                                        AwardLevelEnum.getByCode(trimmedLevel).getName());
+                            }
+                        }
+                    }
+                } else if (awardLevel == null || awardLevel.isEmpty()) {
+                    // 处理awardLevel为空的情况
+                    if (scoreItem != null && !scoreItem.isEmpty()) {
+                        String combinationKey = scoreItem + "_EMPTY";
+                        if (!uniqueCombinations.add(combinationKey)) {
+                            addMessage(dataEntity, "同一积分项目在未指定级别时不能重复配置:" +
+                                    ScoreItemEnum.getByCode(scoreItem).getName());
+                        }
+                    } else {
+                        // 如果都为空,则也认为是重复
+                        if (!uniqueCombinations.add("_")) {
+                            addMessage(dataEntity, "存在多条未指定级别的积分项目配置");
+                        }
+                    }
+                }
+            }
+
+        }
+    }
+}

+ 21 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/performance/PerfRankMgmtSaveOpPlugin.java

@@ -0,0 +1,21 @@
+package nckd.jxccl.hr.psms.plugin.operate.performance;
+
+import kd.bos.entity.plugin.AbstractOperationServicePlugIn;
+import kd.bos.entity.plugin.AddValidatorsEventArgs;
+import kd.sdk.plugin.Plugin;
+import nckd.jxccl.hr.psms.plugin.operate.performance.validate.PerfRankMgmtSaveValidate;
+
+/**
+* 年度绩效排名管理-保存插件
+* @author W.Y.C
+* @date 2025/10/21 17:05
+* @version 1.0
+*/
+public class PerfRankMgmtSaveOpPlugin extends AbstractOperationServicePlugIn implements Plugin {
+
+    @Override
+    public void onAddValidators(AddValidatorsEventArgs e) {
+        e.addValidator(new PerfRankMgmtSaveValidate());
+    }
+
+}

+ 540 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/performance/validate/PerfRankMgmtSaveValidate.java

@@ -0,0 +1,540 @@
+package nckd.jxccl.hr.psms.plugin.operate.performance.validate;
+
+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.QFilter;
+import kd.bos.servicehelper.QueryServiceHelper;
+import nckd.jxccl.base.common.constant.FormConstant;
+import nckd.jxccl.base.common.enums.AppraisalResultEnum;
+import nckd.jxccl.base.common.utils.DateUtil;
+import nckd.jxccl.base.common.utils.QueryFieldBuilder;
+import nckd.jxccl.base.common.utils.StrFormatter;
+import nckd.jxccl.base.orm.helper.QFilterCommonHelper;
+import nckd.jxccl.hr.psms.common.PerfRankMgmtConstant;
+import nckd.jxccl.hr.psms.common.PositionStructureConstant;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 动态表单插件
+ */
+/**
+* 绩效排名管理保存验证插件
+* @author W.Y.C
+* @date 2025/10/21 22:39
+* @version 1.0
+*/
+public class PerfRankMgmtSaveValidate extends AbstractValidator {
+
+    private Map<String, BigDecimal> appraisalResultRatioMap = new HashMap<>();
+    @Override
+    public void validate() {
+        QueryFieldBuilder queryFieldBuilder = QueryFieldBuilder.create()
+                .addGroup(new String[]{FormConstant.NCKD_ENTRYENTITY}, PerfRankMgmtConstant.NCKD_RATIO)
+                .addGroup(new String[]{FormConstant.NCKD_ENTRYENTITY, PositionStructureConstant.NCKD_APPRAISALRESULT}, FormConstant.NUMBER_KEY);
+        DynamicObjectCollection query = QueryServiceHelper.query(PerfRankMgmtConstant.RANKRATIOCONF_ENTITYID, queryFieldBuilder.buildSelect(), new QFilter[]{QFilterCommonHelper.getEnableFilter()});
+
+        query.forEach(dynamicObject -> {
+            String key = dynamicObject.getString(String.join(".",FormConstant.NCKD_ENTRYENTITY, PositionStructureConstant.NCKD_APPRAISALRESULT, FormConstant.NUMBER_KEY));
+            BigDecimal value = dynamicObject.getBigDecimal(String.join(".",FormConstant.NCKD_ENTRYENTITY, PerfRankMgmtConstant.NCKD_RATIO));
+            appraisalResultRatioMap.put(key, value);
+        });
+        for (ExtendedDataEntity rowDataEntity : getDataEntities()) {
+            if(appraisalResultRatioMap.isEmpty()){
+                this.addMessage(rowDataEntity, "没有配置【排名考核结果比例】");
+                return;
+            }
+            DynamicObject data = rowDataEntity.getDataEntity();
+            // 年份校验
+            if (validateYear(data, rowDataEntity)) {
+                // 获取明细数据并验证是否为空
+                DynamicObjectCollection entries = data.getDynamicObjectCollection(PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY);
+                if (entries.isEmpty()) {
+                    this.addMessage(rowDataEntity, "请添加排名名单");
+                    continue;
+                }
+
+                // 验证明细数据
+                ValidationContext context = validateEntries(entries, rowDataEntity);
+
+                // 验证统计数据一致性
+                validateDataConsistency(data, context, rowDataEntity);
+            }
+        }
+    }
+
+    //校验考核结果人数比例
+    private void validateRatio(ValidationContext context, ExtendedDataEntity rowDataEntity) {
+        // 总人数
+        int totalRankCount = context.totalRankCount;
+        // 优秀人数
+        int excellentCount = context.excellentCount;
+        // 合格人数
+        int qualifiedCount = context.qualifiedCount;
+        // 基本合格人数
+        int basicCount = context.basicCount;
+        // 不合格人数
+        int failCount = context.failCount;
+
+        // 获取配置比例
+        BigDecimal excellentRatio = appraisalResultRatioMap.get(AppraisalResultEnum.EXCELLENT.getCode());
+        BigDecimal qualifiedRatio = appraisalResultRatioMap.get(AppraisalResultEnum.QUALIFIED.getCode());
+        BigDecimal basicallyQualifiedRatio = appraisalResultRatioMap.get(AppraisalResultEnum.BASICALLY_QUALIFIED.getCode());
+        BigDecimal unQualifiedRatio = appraisalResultRatioMap.get(AppraisalResultEnum.UN_QUALIFIED.getCode());
+
+        // 计算各等级的理论人数(四舍五入)
+        int expectedExcellent = calculateExpectedCount(totalRankCount, excellentRatio);
+        int expectedQualified = calculateExpectedCount(totalRankCount, qualifiedRatio);
+        int expectedBasic = calculateExpectedCount(totalRankCount, basicallyQualifiedRatio);
+        int expectedFail = calculateExpectedCount(totalRankCount, unQualifiedRatio);
+
+
+
+        // 校验各等级人数是否在合理范围内(允许±1的差异)
+        validateCountRange(AppraisalResultEnum.EXCELLENT.getName(), excellentCount, expectedExcellent, totalRankCount,rowDataEntity);
+        validateCountRange(AppraisalResultEnum.QUALIFIED.getName(), qualifiedCount, expectedQualified, totalRankCount,rowDataEntity);
+        validateCountRange(AppraisalResultEnum.BASICALLY_QUALIFIED.getName(), basicCount, expectedBasic, totalRankCount,rowDataEntity);
+        validateCountRange(AppraisalResultEnum.UN_QUALIFIED.getName(), failCount, expectedFail, totalRankCount,rowDataEntity);
+
+    }
+
+    /**
+     * 计算理论人数(四舍五入)
+     */
+    private int calculateExpectedCount(int totalCount, BigDecimal ratio) {
+        // 将百分比转换为小数进行计算
+        BigDecimal decimalRatio = ratio.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
+        BigDecimal expected = decimalRatio.multiply(BigDecimal.valueOf(totalCount));
+        return expected.setScale(0, RoundingMode.HALF_UP).intValue();
+    }
+
+    /**
+     * 校验人数范围(允许±1的差异)
+     */
+    private void validateCountRange(String levelName, int actualCount, int expectedCount, int totalCount,ExtendedDataEntity rowDataEntity) {
+        int difference = Math.abs(actualCount - expectedCount);
+        // 允许的差异阈值,可以根据总人数动态调整
+        int allowedDifference = Math.max(1, totalCount / 100); // 至少允许1人差异,或总人数的1%
+
+        if (difference > allowedDifference) {
+
+            this.addMessage(rowDataEntity,
+                    StrFormatter.format("{}人数与配置比例不符,应为{}人,实际为{}人",
+                            levelName, expectedCount,actualCount));
+        }
+    }
+
+
+    /**
+     * 验证年份字段有效性
+     * @param data 数据对象
+     * @param rowDataEntity 行数据实体
+     * @return: void
+     * @author W.Y.C
+     * @date: 2025/10/21 22:40
+     */
+    private boolean validateYear(DynamicObject data, ExtendedDataEntity rowDataEntity) {
+        int theYear = data.getInt(PerfRankMgmtConstant.NCKD_THEYEAR);
+        int currentYear = DateUtil.now().getYear();
+        if (theYear < 1900 || theYear > currentYear) {
+            this.addMessage(rowDataEntity, "年份不合法或超过当前年份");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 验证明细数据
+     * @param entries 分录
+     * @param rowDataEntity 行数据实体
+     * @return: nckd.jxccl.hr.psms.plugin.operate.performance.validate.PerfRankMgmtSaveValidate.ValidationContext
+     * @author W.Y.C
+     * @date: 2025/10/21 22:40
+     */
+    private ValidationContext validateEntries(DynamicObjectCollection entries, ExtendedDataEntity rowDataEntity) {
+        ValidationContext context = new ValidationContext();
+        Set<Long> personIds = new HashSet<>();
+
+        for (int i = 0; i < entries.size(); i++) {
+            DynamicObject entry = entries.get(i);
+            validateEntry(entry, rowDataEntity, i + 1, personIds, context);
+        }
+
+        return context;
+    }
+
+    /**
+     * 验证单条明细数据
+     * @param entry 分录
+     * @param rowDataEntity 行数据实体
+     * @param rowIndex 行索引
+     * @param personIds 已处理人员ID集合(用于去重检查)
+     * @param context 验证上下文
+     * @return: void
+     * @author W.Y.C
+     * @date: 2025/10/21 22:41
+     */
+    private void validateEntry(DynamicObject entry, ExtendedDataEntity rowDataEntity,
+                               int rowIndex, Set<Long> personIds, ValidationContext context) {
+        DynamicObject person = entry.getDynamicObject(PerfRankMgmtConstant.NCKD_PERSON);
+        if (person == null) {
+            this.addMessage(rowDataEntity, StrFormatter.format("第{}行,请选择人员\n", rowIndex));
+        } else {
+            // 人员重复校验
+            long personId = person.getLong(FormConstant.ID_KEY);
+            if (personIds.contains(personId)) {
+                this.addMessage(rowDataEntity, StrFormatter.format("第{}行,人员【{}】重复;\n", rowIndex, person.getString(FormConstant.NAME_KEY)));
+            } else {
+                personIds.add(personId);
+            }
+
+            // 统计各类人数
+            boolean isRanking = entry.getBoolean(PerfRankMgmtConstant.NCKD_ISRANKING);
+            boolean postAllowance = entry.getBoolean(PerfRankMgmtConstant.NCKD_POSTALLOWANCE);
+            DynamicObject appraisalResult = entry.getDynamicObject(PerfRankMgmtConstant.NCKD_APPRAISALRESULT);
+
+            if (isRanking) {
+                context.totalRankCount++;
+
+                if (postAllowance) {
+                    context.allowanceRankCount++;
+                }
+            }
+
+            if (appraisalResult != null) {
+                String appraisalResultNumber = appraisalResult.getString(FormConstant.NUMBER_KEY);
+                AppraisalResultEnum appraisalResultEnum = AppraisalResultEnum.getByCode(appraisalResultNumber);
+
+                if (appraisalResultEnum != null) {
+                    switch (appraisalResultEnum) {
+                        case EXCELLENT:
+                            context.excellentCount++;
+                            break;
+                        case BASICALLY_QUALIFIED:
+                            context.basicCount++;
+                            break;
+                        case UN_QUALIFIED:
+                            context.failCount++;
+                            break;
+                        case QUALIFIED:
+                            context.qualifiedCount++;
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 验证数据一致性
+     * @param data 数据对象
+     * @param context 验证上下文
+     * @param rowDataEntity 行数据实体
+     * @return: void
+     * @author W.Y.C
+     * @date: 2025/10/21 22:41
+     */
+    private void validateDataConsistency(DynamicObject data, ValidationContext context,
+                                         ExtendedDataEntity rowDataEntity) {
+        // 各类人数一致性校验
+        checkCountConsistency(data, context, rowDataEntity);
+
+        // 排名范围校验
+        validateRankRanges(data.getDynamicObjectCollection(PerfRankMgmtConstant.NCKD_PERFRANKMGMTENTRY),
+                data.getInt(PerfRankMgmtConstant.NCKD_TOPRANKS), rowDataEntity);
+
+        validateRatio(context, rowDataEntity);
+    }
+
+    /**
+     * 检查统计数据一致性
+     * @param data 数据对象
+     * @param context 验证上下文
+     * @param rowDataEntity 行数据实体
+     * @return: void
+     * @author W.Y.C
+     * @date: 2025/10/21 22:42
+     */
+    private void checkCountConsistency(DynamicObject data, ValidationContext context,
+                                       ExtendedDataEntity rowDataEntity) {
+        int topRanks = data.getInt(PerfRankMgmtConstant.NCKD_TOPRANKS);
+        int allowanceRanks = data.getInt(PerfRankMgmtConstant.NCKD_ALLOWANCERANKS);
+        int fails = data.getInt(PerfRankMgmtConstant.NCKD_FAILS);
+        int basics = data.getInt(PerfRankMgmtConstant.NCKD_BASICS);
+        int excellents = data.getInt(PerfRankMgmtConstant.NCKD_EXCELLENTS);
+
+        if (topRanks != context.totalRankCount) {
+            this.addMessage(rowDataEntity, "总排名人数与实际录入数据不一致;\n");
+        }
+        if (allowanceRanks != context.allowanceRankCount) {
+            this.addMessage(rowDataEntity, "R排名人数与实际录入数据不一致;\n");
+        }
+        if (fails != context.failCount) {
+            this.addMessage(rowDataEntity, "不合格人数与实际录入数据不一致;\n");
+        }
+        if (basics != context.basicCount) {
+            this.addMessage(rowDataEntity, "基本人数与实际录入数据不一致;\n");
+        }
+        if (excellents != context.excellentCount) {
+            this.addMessage(rowDataEntity, "优秀人数与实际录入数据不一致;\n");
+        }
+    }
+
+    /**
+     * 验证排名范围和重复情况
+     * @param entries 分录集合
+     * @param totalRankCount 总排名数
+     * @param rowDataEntity 行数据实体
+     * @return: void
+     * @author W.Y.C
+     * @date: 2025/10/21 22:42
+     */
+    private void validateRankRanges(DynamicObjectCollection entries, int totalRankCount, ExtendedDataEntity rowDataEntity) {
+        ValidationContext context = new ValidationContext();
+
+        // 统计每个排名出现的次数
+        for (int i = 0; i < entries.size(); i++) {
+            DynamicObject entry = entries.get(i);
+            DynamicObject person = entry.getDynamicObject(PerfRankMgmtConstant.NCKD_PERSON);
+
+            if (person != null) {
+                String personName = person.getString(FormConstant.NAME_KEY);
+                boolean isRanking = entry.getBoolean(PerfRankMgmtConstant.NCKD_ISRANKING);
+                boolean postAllowance = entry.getBoolean(PerfRankMgmtConstant.NCKD_POSTALLOWANCE);
+
+                if (isRanking) {
+                    validateAndRecordRank(entry, PerfRankMgmtConstant.NCKD_TOPRANK, "参与排名","排名", personName, rowDataEntity, context.rankCountMap);
+
+                    if (postAllowance) {
+                        validateAndRecordRank(entry, PerfRankMgmtConstant.NCKD_ALLOWANCERANK, "享受职位津贴","R排名", personName, rowDataEntity, context.rankRCountMap);
+                    }
+                }
+
+
+            }
+        }
+
+        // 找出重复的排名
+        findDuplicateRanks(context.rankCountMap, context.duplicateRanks);
+        findDuplicateRanks(context.rankRCountMap, context.duplicateRRanks);
+
+        // 检查排名范围和重复率
+       for (int i = 0; i < entries.size(); i++) {
+            DynamicObject entry = entries.get(i);
+            boolean isRanking = entry.getBoolean(PerfRankMgmtConstant.NCKD_ISRANKING);
+            boolean postAllowance = entry.getBoolean(PerfRankMgmtConstant.NCKD_POSTALLOWANCE);
+
+            if (isRanking) {
+                // 检查排名字段是否在有效范围内
+                int topRank = entry.getInt(PerfRankMgmtConstant.NCKD_TOPRANK);
+                if (topRank < 1 || topRank > totalRankCount) {
+                    this.addMessage(rowDataEntity,
+                            StrFormatter.format("第{}行,排名{}超出有效范围[1-{}]\n", i + 1, topRank, totalRankCount));
+                }
+                if (postAllowance) {
+                    // 检查R排名字段是否在有效范围内
+                    int allowanceRank = entry.getInt(PerfRankMgmtConstant.NCKD_ALLOWANCERANK);
+                    if (allowanceRank < 1 || allowanceRank > totalRankCount) {
+                        this.addMessage(rowDataEntity,
+                                StrFormatter.format("第{}行,R排名{}超出有效范围[1-{}]\n", i + 1, allowanceRank, totalRankCount));
+                    }
+                }
+            }
+        }
+
+        // 验证排名重复率
+        validateRankDuplicateRate(context, totalRankCount, rowDataEntity);
+
+        // 特殊排名组合校验
+        validateSpecialRankCombination(context, rowDataEntity);
+    }
+
+    /**
+     * 验证并记录排名值
+     * @param entry 分录对象
+     * @param rankField 排名字段名
+     * @param rankType 排名类型描述
+     * @param personName 人员姓名
+     * @param rowDataEntity 行数据实体
+     * @param rankCountMap 排名计数映射
+     */
+    private void validateAndRecordRank(DynamicObject entry, String rankField,String type, String rankType,
+                                       String personName, ExtendedDataEntity rowDataEntity,
+                                       Map<Integer, Integer> rankCountMap) {
+        int rankValue = entry.getInt(rankField);
+        if (rankValue < 1) {
+            this.addMessage(rowDataEntity, StrFormatter.format("人员【{}】已勾选{},{}数不能小于1或为空",
+                    personName, type, rankType));
+        } else {
+            rankCountMap.put(rankValue, rankCountMap.getOrDefault(rankValue, 0) + 1);
+        }
+    }
+
+    /**
+     * 查找重复的排名
+     * @param rankCountMap 排名计数映射
+     * @param duplicateRanks 重复排名集合
+     */
+    private void findDuplicateRanks(Map<Integer, Integer> rankCountMap, Set<Integer> duplicateRanks) {
+        for (Map.Entry<Integer, Integer> entry : rankCountMap.entrySet()) {
+            if (entry.getValue() > 1) {
+                duplicateRanks.add(entry.getKey());
+            }
+        }
+    }
+
+    /**
+     * 特殊排名组合校验
+     * 当相同排名重复出现时,其连续序列结束位置不能被其他排名占用
+     * @param context 上下文
+     * @param rowDataEntity 行数据实体
+     * @return: void
+     * @author W.Y.C
+     * @date: 2025/10/21 22:43
+     */
+    private void validateSpecialRankCombination(ValidationContext context, ExtendedDataEntity rowDataEntity) {
+        // 校验普通排名的特殊组合
+        List<String> invalidCombinations = new ArrayList<>();
+
+        // 校验普通排名
+        validateSpecialRankCombinationForType(context.rankCountMap, context.duplicateRanks, "排名", invalidCombinations);
+
+        // 校验R排名
+        validateSpecialRankCombinationForType(context.rankRCountMap, context.duplicateRRanks, "R排名", invalidCombinations);
+
+        // 如果有不合规的组合,添加详细的错误信息
+        if (!invalidCombinations.isEmpty()) {
+            StringBuilder errorMsg = new StringBuilder("排名设置不符合规范!\n");
+            errorMsg.append("【校验规则】\n");
+            errorMsg.append("当相同排名重复出现时,其连续序列结束位置不能被其他排名占用。\n");
+            errorMsg.append("【问题详情】\n");
+            for (int i = 0; i < invalidCombinations.size(); i++) {
+                errorMsg.append(StrFormatter.format("{}. {}\n", i + 1, invalidCombinations.get(i)));
+            }
+            errorMsg.append("【示例说明】\n");
+            errorMsg.append("❌ 不合规示例:[1,2,2,2,4] - 排名2重复3次,结束位置应为2+3-1=4,但4已在列表中\n");
+            errorMsg.append("✅ 合规示例:[1,1,1,4] - 排名1重复3次,结束位置应为1+3-1=3,3不在列表中\n");
+            errorMsg.append("【解决建议】\n");
+            errorMsg.append("请调整重复排名的数值或移除冲突的排名,确保连续序列结束位置未被占用;\n\n");
+            this.addMessage(rowDataEntity, errorMsg.toString());
+        }
+    }
+
+    /**
+     * 针对特定类型排名校验特殊组合规则
+     * @param rankCountMap 排名计数映射
+     * @param duplicateRanks 重复排名集合
+     * @param rankType 排名类型(如"排名"、"R排名")
+     * @param invalidCombinations 不合规组合列表
+     */
+    private void validateSpecialRankCombinationForType(Map<Integer, Integer> rankCountMap,
+                                                       Set<Integer> duplicateRanks,
+                                                       String rankType,
+                                                       List<String> invalidCombinations) {
+        // 遍历所有重复的排名
+        for (Integer rank : duplicateRanks) {
+            int count = rankCountMap.get(rank);
+            // 计算连续序列结束值
+            int mark = rank + count - 1;
+
+            // 检查结束值是否在列表中存在
+            if (rankCountMap.containsKey(mark) && mark != rank) {
+                invalidCombinations.add(StrFormatter.format(
+                        "{}{}重复{}次,结束位置{}被占用",
+                        rankType, rank, count, mark));
+            }
+        }
+    }
+
+    /**
+     * 验证排名重复率是否超过阈值
+     * @param context 上下文
+     * @param totalRankCount 总排名数
+     * @param rowDataEntity 行数据实体
+     * @return: void
+     * @author W.Y.C
+     * @date: 2025/10/21 22:44
+     */
+    private void validateRankDuplicateRate(ValidationContext context, int totalRankCount, ExtendedDataEntity rowDataEntity) {
+        // 验证普通排名重复率
+        validateRankDuplicateRateForType(context.rankCountMap, context.duplicateRanks, totalRankCount,
+                "排名", rowDataEntity);
+
+        // 验证R排名重复率
+        validateRankDuplicateRateForType(context.rankRCountMap, context.duplicateRRanks, totalRankCount,
+                "R排名", rowDataEntity);
+    }
+
+    /**
+     * 针对特定类型排名验证重复率
+     * @param rankCountMap 排名计数映射
+     * @param duplicateRanks 重复排名集合
+     * @param totalRankCount 总排名数
+     * @param rankType 排名类型
+     * @param rowDataEntity 行数据实体
+     */
+    private void validateRankDuplicateRateForType(Map<Integer, Integer> rankCountMap,
+                                                  Set<Integer> duplicateRanks,
+                                                  int totalRankCount,
+                                                  String rankType,
+                                                  ExtendedDataEntity rowDataEntity) {
+        if (totalRankCount > 0) {
+            double duplicateRate = (double) duplicateRanks.size() / totalRankCount;
+            if (duplicateRate > 0.3) {
+                // 构建重复排名详情信息
+                StringBuilder detailMessage = new StringBuilder();
+                detailMessage.append(rankType).append("重复率超过30%,当前重复率: ");
+                detailMessage.append(String.format("%.1f", duplicateRate * 100)).append("%,");
+                detailMessage.append("具体重复的排名有: ");
+
+                List<Integer> sortedDuplicateRanks = new ArrayList<>(duplicateRanks);
+                Collections.sort(sortedDuplicateRanks);
+
+                for (int i = 0; i < sortedDuplicateRanks.size(); i++) {
+                    int rank = sortedDuplicateRanks.get(i);
+                    int count = rankCountMap.get(rank);
+                    detailMessage.append(StrFormatter.format("{}【{}】出现{}次;", rankType, rank, count));
+                }
+                detailMessage.append("\n");
+                this.addMessage(rowDataEntity, detailMessage.toString());
+            }
+        }
+    }
+
+    private void validateUserUniqueAcrossUnits() {
+
+    }
+
+    /**
+     * 验证上下文对象,用于存储验证过程中的统计数据
+     * @author W.Y.C
+     * @date 2025/10/21 22:45
+     * @version 1.0
+     */
+    private static class ValidationContext {
+        int totalRankCount = 0;
+        int allowanceRankCount = 0;
+        int failCount = 0;
+        int basicCount = 0;
+        int excellentCount = 0;
+        int qualifiedCount = 0;
+        Set<Integer> rankSet = new HashSet<>();  // 记录已出现的排名
+        Set<Integer> duplicateRanks = new HashSet<>();  // 记录普通排名中重复的排名
+        Set<Integer> duplicateRRanks = new HashSet<>();  // 记录R排名中重复的排名
+        Map<Integer, Integer> rankCountMap = new HashMap<>();  // 记录每个普通排名出现的次数
+        Map<Integer, Integer> rankRCountMap = new HashMap<>();  // 记录每个R排名出现的次数
+    }
+
+}

+ 39 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/psms/plugin/operate/performance/validate/RankRatioConfValidate.java

@@ -0,0 +1,39 @@
+package nckd.jxccl.hr.psms.plugin.operate.performance.validate;
+
+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.form.plugin.AbstractFormPlugin;
+import kd.sdk.plugin.Plugin;
+import nckd.jxccl.base.common.constant.FormConstant;
+import nckd.jxccl.hr.psms.common.PerfRankMgmtConstant;
+
+import java.math.BigDecimal;
+
+/**
+* 排名考核结果比例配置-校验器
+* @author W.Y.C
+* @date 2025/10/22 12:59
+* @version 1.0
+*/
+public class RankRatioConfValidate extends AbstractValidator {
+
+    @Override
+    public void validate() {
+        for (ExtendedDataEntity dataEntity : this.getDataEntities()) {
+            DynamicObject data = dataEntity.getDataEntity();
+            DynamicObjectCollection entrys = data.getDynamicObjectCollection(FormConstant.NCKD_ENTRYENTITY);
+            BigDecimal ratio = BigDecimal.ZERO;
+            for (DynamicObject entry : entrys) {
+                BigDecimal bigDecimal = entry.getBigDecimal(PerfRankMgmtConstant.NCKD_RATIO);
+                ratio = ratio.add(bigDecimal);
+            }
+            //判断是否等于100
+            if (ratio.compareTo(new BigDecimal(100)) != 0) {
+                addMessage(dataEntity, "比例总和必须等于100");
+            }
+
+        }
+    }
+}