wangxx недель назад: 4
Родитель
Сommit
19a73f2d09

+ 180 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/score/DeptPortraitController.java

@@ -0,0 +1,180 @@
1
+package com.sundot.airport.web.controller.score;
2
+
3
+import com.sundot.airport.common.core.controller.BaseController;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.ledger.dto.DeptMemberDistributionDTO;
6
+import com.sundot.airport.ledger.dto.DeptMemberDTO;
7
+import com.sundot.airport.ledger.dto.DeptPortraitQueryDTO;
8
+import com.sundot.airport.ledger.dto.GroupPortraitDTO;
9
+import com.sundot.airport.ledger.service.IDeptPortraitService;
10
+import com.sundot.airport.ledger.service.IGroupPortraitService;
11
+import io.swagger.annotations.Api;
12
+import io.swagger.annotations.ApiOperation;
13
+import org.springframework.beans.factory.annotation.Autowired;
14
+import org.springframework.web.bind.annotation.*;
15
+
16
+import java.util.List;
17
+
18
+/**
19
+ * <b>功能名:</b>DeptPortraitController<br>
20
+ * <b>说明:</b> 部门画像Controller(团队成员展示)<br>
21
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
22
+ *
23
+ * @author Claude
24
+ */
25
+@Api(tags = "部门画像-团队成员")
26
+@RestController
27
+@RequestMapping("/score/dept-portrait")
28
+public class DeptPortraitController extends BaseController {
29
+
30
+    @Autowired
31
+    private IDeptPortraitService deptPortraitService;
32
+
33
+    @Autowired
34
+    private IGroupPortraitService groupPortraitService;
35
+
36
+    /**
37
+     * 获取部门内所有成员列表(部门画像-团队成员展示)
38
+     * <p>
39
+     * 查询指定部门及其所有下级部门的成员信息,包括:
40
+     * 姓名、年龄、工龄、性别、民族、政治面貌、职务、职业资格等级、开机年限、综合得分
41
+     * </p>
42
+     *
43
+     * @param query 查询参数对象
44
+     * @return 部门成员列表
45
+     */
46
+    @ApiOperation("获取部门内所有成员列表")
47
+    @PostMapping("/members")
48
+    public AjaxResult getDeptMembers(@RequestBody DeptPortraitQueryDTO query) {
49
+        try {
50
+            // 参数校验
51
+            if (query == null || query.getDeptId() == null) {
52
+                return error("部门ID不能为空");
53
+            }
54
+
55
+            // 如果没有传日期范围,则默认为91天
56
+            if (query.getStartDate() == null || query.getEndDate() == null) {
57
+                java.time.LocalDate today = java.time.LocalDate.now();
58
+                query.setEndDate(today.toString());
59
+                query.setStartDate(today.minusDays(91).toString());
60
+            }
61
+
62
+            List<DeptMemberDTO> members = deptPortraitService.getDeptMembers(query);
63
+
64
+            return success(members);
65
+        } catch (Exception e) {
66
+            logger.error("获取部门成员列表失败", e);
67
+            return error("获取部门成员列表失败:" + e.getMessage());
68
+        }
69
+    }
70
+
71
+    /**
72
+     * 获取部门成员基本情况分布(用于饼图展示)
73
+     * <p>
74
+     * 统计指定部门及其下级部门成员的:
75
+     * 1. 性别分布
76
+     * 2. 民族分布
77
+     * 3. 政治面貌分布
78
+     * </p>
79
+     *
80
+     * @param query 查询参数对象
81
+     * @return 成员基本情况分布数据
82
+     */
83
+    @ApiOperation("获取部门成员基本情况分布")
84
+    @PostMapping("/distribution")
85
+    public AjaxResult getMemberDistribution(@RequestBody DeptPortraitQueryDTO query) {
86
+        try {
87
+            // 参数校验
88
+            if (query == null || query.getDeptId() == null) {
89
+                return error("部门ID不能为空");
90
+            }
91
+
92
+            // 如果没有传日期范围,则默认为91天
93
+            if (query.getStartDate() == null || query.getEndDate() == null) {
94
+                java.time.LocalDate today = java.time.LocalDate.now();
95
+                query.setEndDate(today.toString());
96
+                query.setStartDate(today.minusDays(91).toString());
97
+            }
98
+
99
+            DeptMemberDistributionDTO distribution = deptPortraitService.getMemberDistribution(query);
100
+
101
+            return success(distribution);
102
+        } catch (Exception e) {
103
+            logger.error("获取部门成员分布失败", e);
104
+            return error("获取部门成员分布失败:" + e.getMessage());
105
+        }
106
+    }
107
+
108
+    /**
109
+     * 获取部门成员职位情况分布(用于柱状图展示)
110
+     * <p>
111
+     * 统计指定部门及其下级部门成员的:
112
+     * 1. 职业资格等级分布(等级1-等级5)
113
+     * 2. 开机年限分布(0-3年、4-7年、8-11年、12-15年、15-18年)
114
+     * 3. 岗位资质分布(前传、操机、人身、验证、开箱包、开机)
115
+     * </p>
116
+     *
117
+     * @param query 查询参数对象
118
+     * @return 成员职位情况分布数据
119
+     */
120
+    @ApiOperation("获取部门成员职位情况分布")
121
+    @PostMapping("/position-distribution")
122
+    public AjaxResult getPositionDistribution(@RequestBody DeptPortraitQueryDTO query) {
123
+        try {
124
+            // 参数校验
125
+            if (query == null || query.getDeptId() == null) {
126
+                return error("部门ID不能为空");
127
+            }
128
+
129
+            // 如果没有传日期范围,则默认为91天
130
+            if (query.getStartDate() == null || query.getEndDate() == null) {
131
+                java.time.LocalDate today = java.time.LocalDate.now();
132
+                query.setEndDate(today.toString());
133
+                query.setStartDate(today.minusDays(91).toString());
134
+            }
135
+
136
+            DeptMemberDistributionDTO distribution = deptPortraitService.getPositionDistribution(query);
137
+
138
+            return success(distribution);
139
+        } catch (Exception e) {
140
+            logger.error("获取部门成员职位分布失败", e);
141
+            return error("获取部门成员职位分布失败:" + e.getMessage());
142
+        }
143
+    }
144
+
145
+    /**
146
+     * 维度得分一览
147
+     * 计算规则:
148
+     * 1. 根据员工配分表,计算小组六维度均值
149
+     * 2. 小组各维度最终分值 = 基础分 + 员工维度均值 + 小组特有合计分值
150
+     * 3. 综合评分 = Σ(各维度最终分值 × 权重 / 100)
151
+     * </p>
152
+     *
153
+     * @param query 查询参数对象
154
+     * @return 组织画像数据
155
+     */
156
+    @ApiOperation("维度得分一览")
157
+    @PostMapping("/group-portrait")
158
+    public AjaxResult getGroupPortrait(@RequestBody DeptPortraitQueryDTO query) {
159
+        try {
160
+            // 参数校验
161
+            if (query == null || query.getDeptId() == null) {
162
+                return error("部门ID不能为空");
163
+            }
164
+
165
+            // 如果没有传日期范围,则默认为91天
166
+            if (query.getStartDate() == null || query.getEndDate() == null) {
167
+                java.time.LocalDate today = java.time.LocalDate.now();
168
+                query.setEndDate(today.toString());
169
+                query.setStartDate(today.minusDays(91).toString());
170
+            }
171
+
172
+            GroupPortraitDTO portrait = groupPortraitService.getGroupPortrait(query);
173
+
174
+            return success(portrait);
175
+        } catch (Exception e) {
176
+            logger.error("获取组织画像失败", e);
177
+            return error("获取组织画像失败:" + e.getMessage());
178
+        }
179
+    }
180
+}

+ 6 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/domain/ScoreDimension.java

@@ -36,6 +36,9 @@ public class ScoreDimension extends BaseEntity {
36 36
     @Excel(name = "层级维度", readConverterExp = "1=部门,2=队室,3=通道,4=员工", combo = "部门,队室,通道,员工")
37 37
     private String org;
38 38
 
39
+    /** 维度对照关系(用于关联不同org层级的维度) */
40
+    private String dimRelationship;
41
+
39 42
     public Long getId() { return id; }
40 43
     public void setId(Long id) { this.id = id; }
41 44
 
@@ -56,4 +59,7 @@ public class ScoreDimension extends BaseEntity {
56 59
 
57 60
     public String getOrg() { return org; }
58 61
     public void setOrg(String org) { this.org = org; }
62
+
63
+    public String getDimRelationship() { return dimRelationship; }
64
+    public void setDimRelationship(String dimRelationship) { this.dimRelationship = dimRelationship; }
59 65
 }

+ 159 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/dto/DeptMemberDTO.java

@@ -0,0 +1,159 @@
1
+package com.sundot.airport.ledger.dto;
2
+
3
+import java.io.Serializable;
4
+import java.math.BigDecimal;
5
+
6
+/**
7
+ * <b>功能名:</b>DeptMemberDTO<br>
8
+ * <b>说明:</b> 部门成员信息DTO(用于部门画像展示)<br>
9
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
10
+ *
11
+ * @author Claude
12
+ */
13
+public class DeptMemberDTO implements Serializable {
14
+
15
+    private static final long serialVersionUID = 1L;
16
+
17
+    /**
18
+     * 用户ID
19
+     */
20
+    private Long userId;
21
+
22
+    /**
23
+     * 姓名
24
+     */
25
+    private String personName;
26
+
27
+    /**
28
+     * 年龄
29
+     */
30
+    private Integer age;
31
+
32
+    /**
33
+     * 工龄(年)
34
+     */
35
+    private Integer workYears;
36
+
37
+    /**
38
+     * 性别
39
+     */
40
+    private String sex;
41
+
42
+    /**
43
+     * 民族
44
+     */
45
+    private String nation;
46
+
47
+    /**
48
+     * 政治面貌
49
+     */
50
+    private String politicalStatus;
51
+
52
+    /**
53
+     * 职务
54
+     */
55
+    private String roleNames;
56
+
57
+    /**
58
+     * 职业资格等级
59
+     */
60
+    private String qualificationLevel;
61
+
62
+    /**
63
+     * 开机年限(年)
64
+     */
65
+    private Integer xrayOperatorYears;
66
+
67
+    /**
68
+     * 综合得分
69
+     */
70
+    private BigDecimal totalScore;
71
+
72
+    public Long getUserId() {
73
+        return userId;
74
+    }
75
+
76
+    public void setUserId(Long userId) {
77
+        this.userId = userId;
78
+    }
79
+
80
+    public String getPersonName() {
81
+        return personName;
82
+    }
83
+
84
+    public void setPersonName(String personName) {
85
+        this.personName = personName;
86
+    }
87
+
88
+    public Integer getAge() {
89
+        return age;
90
+    }
91
+
92
+    public void setAge(Integer age) {
93
+        this.age = age;
94
+    }
95
+
96
+    public Integer getWorkYears() {
97
+        return workYears;
98
+    }
99
+
100
+    public void setWorkYears(Integer workYears) {
101
+        this.workYears = workYears;
102
+    }
103
+
104
+    public String getSex() {
105
+        return sex;
106
+    }
107
+
108
+    public void setSex(String sex) {
109
+        this.sex = sex;
110
+    }
111
+
112
+    public String getNation() {
113
+        return nation;
114
+    }
115
+
116
+    public void setNation(String nation) {
117
+        this.nation = nation;
118
+    }
119
+
120
+    public String getPoliticalStatus() {
121
+        return politicalStatus;
122
+    }
123
+
124
+    public void setPoliticalStatus(String politicalStatus) {
125
+        this.politicalStatus = politicalStatus;
126
+    }
127
+
128
+    public String getRoleNames() {
129
+        return roleNames;
130
+    }
131
+
132
+    public void setRoleNames(String roleNames) {
133
+        this.roleNames = roleNames;
134
+    }
135
+
136
+    public String getQualificationLevel() {
137
+        return qualificationLevel;
138
+    }
139
+
140
+    public void setQualificationLevel(String qualificationLevel) {
141
+        this.qualificationLevel = qualificationLevel;
142
+    }
143
+
144
+    public Integer getXrayOperatorYears() {
145
+        return xrayOperatorYears;
146
+    }
147
+
148
+    public void setXrayOperatorYears(Integer xrayOperatorYears) {
149
+        this.xrayOperatorYears = xrayOperatorYears;
150
+    }
151
+
152
+    public BigDecimal getTotalScore() {
153
+        return totalScore;
154
+    }
155
+
156
+    public void setTotalScore(BigDecimal totalScore) {
157
+        this.totalScore = totalScore;
158
+    }
159
+}

+ 135 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/dto/DeptMemberDistributionDTO.java

@@ -0,0 +1,135 @@
1
+package com.sundot.airport.ledger.dto;
2
+
3
+import java.io.Serializable;
4
+import java.util.List;
5
+
6
+/**
7
+ * <b>功能名:</b>DeptMemberDistributionDTO<br>
8
+ * <b>说明:</b> 部门成员基本情况分布DTO(用于饼图展示)<br>
9
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
10
+ *
11
+ * @author Claude
12
+ */
13
+public class DeptMemberDistributionDTO implements Serializable {
14
+
15
+    private static final long serialVersionUID = 1L;
16
+
17
+    /**
18
+     * 性别分布
19
+     */
20
+    private List<DistributionItem> sexDistribution;
21
+
22
+    /**
23
+     * 民族分布
24
+     */
25
+    private List<DistributionItem> nationDistribution;
26
+
27
+    /**
28
+     * 政治面貌分布
29
+     */
30
+    private List<DistributionItem> politicalDistribution;
31
+
32
+    /**
33
+     * 职业资格等级分布
34
+     */
35
+    private List<DistributionItem> qualificationDistribution;
36
+
37
+    /**
38
+     * 开机年限分布
39
+     */
40
+    private List<DistributionItem> xrayYearDistribution;
41
+
42
+    /**
43
+     * 岗位资质分布
44
+     */
45
+    private List<DistributionItem> positionDistribution;
46
+
47
+    /**
48
+     * 分布项内部类
49
+     */
50
+    public static class DistributionItem implements Serializable {
51
+        private static final long serialVersionUID = 1L;
52
+
53
+        /**
54
+         * 名称(如:男、女、汉族等)
55
+         */
56
+        private String name;
57
+
58
+        /**
59
+         * 数量
60
+         */
61
+        private Integer count;
62
+
63
+        public DistributionItem() {
64
+        }
65
+
66
+        public DistributionItem(String name, Integer count) {
67
+            this.name = name;
68
+            this.count = count;
69
+        }
70
+
71
+        public String getName() {
72
+            return name;
73
+        }
74
+
75
+        public void setName(String name) {
76
+            this.name = name;
77
+        }
78
+
79
+        public Integer getCount() {
80
+            return count;
81
+        }
82
+
83
+        public void setCount(Integer count) {
84
+            this.count = count;
85
+        }
86
+    }
87
+
88
+    public List<DistributionItem> getSexDistribution() {
89
+        return sexDistribution;
90
+    }
91
+
92
+    public void setSexDistribution(List<DistributionItem> sexDistribution) {
93
+        this.sexDistribution = sexDistribution;
94
+    }
95
+
96
+    public List<DistributionItem> getNationDistribution() {
97
+        return nationDistribution;
98
+    }
99
+
100
+    public void setNationDistribution(List<DistributionItem> nationDistribution) {
101
+        this.nationDistribution = nationDistribution;
102
+    }
103
+
104
+    public List<DistributionItem> getPoliticalDistribution() {
105
+        return politicalDistribution;
106
+    }
107
+
108
+    public void setPoliticalDistribution(List<DistributionItem> politicalDistribution) {
109
+        this.politicalDistribution = politicalDistribution;
110
+    }
111
+
112
+    public List<DistributionItem> getQualificationDistribution() {
113
+        return qualificationDistribution;
114
+    }
115
+
116
+    public void setQualificationDistribution(List<DistributionItem> qualificationDistribution) {
117
+        this.qualificationDistribution = qualificationDistribution;
118
+    }
119
+
120
+    public List<DistributionItem> getXrayYearDistribution() {
121
+        return xrayYearDistribution;
122
+    }
123
+
124
+    public void setXrayYearDistribution(List<DistributionItem> xrayYearDistribution) {
125
+        this.xrayYearDistribution = xrayYearDistribution;
126
+    }
127
+
128
+    public List<DistributionItem> getPositionDistribution() {
129
+        return positionDistribution;
130
+    }
131
+
132
+    public void setPositionDistribution(List<DistributionItem> positionDistribution) {
133
+        this.positionDistribution = positionDistribution;
134
+    }
135
+}

+ 64 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/dto/DeptPortraitQueryDTO.java

@@ -0,0 +1,64 @@
1
+package com.sundot.airport.ledger.dto;
2
+
3
+
4
+import java.io.Serializable;
5
+
6
+/**
7
+ * <b>功能名:</b>DeptPortraitQueryDTO<br>
8
+ * <b>说明:</b> 部门画像查询参数DTO <br>
9
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
10
+ *
11
+ * @author Claude
12
+ */
13
+public class DeptPortraitQueryDTO implements Serializable {
14
+
15
+    private static final long serialVersionUID = 1L;
16
+
17
+    /**
18
+     * 部门ID (必填)
19
+     */
20
+    private Long deptId;
21
+
22
+    /**
23
+     * 开始日期 YYYY-MM-DD (可选)
24
+     */
25
+    private String startDate;
26
+
27
+    /**
28
+     * 结束日期 YYYY-MM-DD (可选)
29
+     */
30
+    private String endDate;
31
+
32
+    public Long getDeptId() {
33
+        return deptId;
34
+    }
35
+
36
+    public void setDeptId(Long deptId) {
37
+        this.deptId = deptId;
38
+    }
39
+
40
+    public String getStartDate() {
41
+        return startDate;
42
+    }
43
+
44
+    public void setStartDate(String startDate) {
45
+        this.startDate = startDate;
46
+    }
47
+
48
+    public String getEndDate() {
49
+        return endDate;
50
+    }
51
+
52
+    public void setEndDate(String endDate) {
53
+        this.endDate = endDate;
54
+    }
55
+
56
+    @Override
57
+    public String toString() {
58
+        return "DeptPortraitQueryDTO{" +
59
+                "deptId=" + deptId +
60
+                ", startDate='" + startDate + '\'' +
61
+                ", endDate='" + endDate + '\'' +
62
+                '}';
63
+    }
64
+}

+ 108 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/dto/GroupPortraitDTO.java

@@ -0,0 +1,108 @@
1
+package com.sundot.airport.ledger.dto;
2
+
3
+import lombok.Data;
4
+
5
+import java.io.Serializable;
6
+import java.math.BigDecimal;
7
+import java.util.List;
8
+
9
+/**
10
+ * <b>功能名:</b>GroupPortraitDTO<br>
11
+ * <b>说明:</b> 组织画像(小组/通道维度)DTO<br>
12
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
13
+ *
14
+ * @author Claude
15
+ */
16
+@Data
17
+public class GroupPortraitDTO implements Serializable {
18
+
19
+    private static final long serialVersionUID = 1L;
20
+
21
+    /**
22
+     * 部门ID
23
+     */
24
+    private Long deptId;
25
+
26
+    /**
27
+     * 部门名称
28
+     */
29
+    private String deptName;
30
+
31
+    /**
32
+     * 部门类型(班组/通道/科室等)
33
+     */
34
+    private String deptType;
35
+
36
+    /**
37
+     * 查询周期
38
+     */
39
+    private String period;
40
+
41
+    /**
42
+     * 小组成员数
43
+     */
44
+    private Integer memberCount;
45
+
46
+    /**
47
+     * 各维度得分列表
48
+     */
49
+    private List<DimensionScore> dimensions;
50
+
51
+    /**
52
+     * 综合评分
53
+     */
54
+    private BigDecimal totalScore;
55
+
56
+    /**
57
+     * 维度得分项
58
+     */
59
+    @Data
60
+    public static class DimensionScore implements Serializable {
61
+        private static final long serialVersionUID = 1L;
62
+
63
+        /**
64
+         * 维度名称
65
+         */
66
+        private String name;
67
+
68
+        /**
69
+         * 小组六维度均值(员工平均)
70
+         */
71
+        private BigDecimal groupAverageScore;
72
+
73
+        /**
74
+         * 基础分
75
+         */
76
+        private BigDecimal baseScore;
77
+
78
+        /**
79
+         * 小组维度特有加分
80
+         */
81
+        private BigDecimal addScore;
82
+
83
+        /**
84
+         * 小组维度特有减分
85
+         */
86
+        private BigDecimal subtractScore;
87
+
88
+        /**
89
+         * 特有合计(加分-减分)
90
+         */
91
+        private BigDecimal specialTotal;
92
+
93
+        /**
94
+         * 最终分值(基础分+均值+特有合计)
95
+         */
96
+        private BigDecimal finalScore;
97
+
98
+        /**
99
+         * 权重
100
+         */
101
+        private BigDecimal weight;
102
+
103
+        /**
104
+         * 贡献分值(最终分值*权重/100)
105
+         */
106
+        private BigDecimal contribution;
107
+    }
108
+}

+ 57 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/service/IDeptPortraitService.java

@@ -0,0 +1,57 @@
1
+package com.sundot.airport.ledger.service;
2
+
3
+import com.sundot.airport.ledger.dto.DeptMemberDistributionDTO;
4
+import com.sundot.airport.ledger.dto.DeptMemberDTO;
5
+import com.sundot.airport.ledger.dto.DeptPortraitQueryDTO;
6
+
7
+import java.util.List;
8
+
9
+/**
10
+ * <b>功能名:</b>IDeptPortraitService<br>
11
+ * <b>说明:</b> 部门画像Service接口 <br>
12
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
13
+ *
14
+ * @author Claude
15
+ */
16
+public interface IDeptPortraitService {
17
+
18
+    /**
19
+     * 获取部门内所有成员列表(部门画像-团队成员)
20
+     * <p>
21
+     * 查询指定部门及其所有下级部门的成员信息,包括:
22
+     * 姓名、年龄、工龄、性别、民族、政治面貌、职务、职业资格等级、开机年限、综合得分
23
+     * </p>
24
+     *
25
+     * @param query 查询参数对象(deptId, startDate, endDate)
26
+     * @return 部门成员列表
27
+     */
28
+    List<DeptMemberDTO> getDeptMembers(DeptPortraitQueryDTO query);
29
+
30
+    /**
31
+     * 获取部门成员基本情况分布(用于饼图展示)
32
+     * <p>
33
+     * 统计指定部门及其下级部门成员的:
34
+     * 1. 性别分布
35
+     * 2. 民族分布
36
+     * 3. 政治面貌分布
37
+     * </p>
38
+     *
39
+     * @param query 查询参数对象(deptId, startDate, endDate)
40
+     * @return 成员基本情况分布数据
41
+     */
42
+    DeptMemberDistributionDTO getMemberDistribution(DeptPortraitQueryDTO query);
43
+
44
+    /**
45
+     * 获取部门成员职位情况分布(用于柱状图展示)
46
+     * <p>
47
+     * 统计指定部门及其下级部门成员的:
48
+     * 1. 职业资格等级分布(等级1-等级5)
49
+     * 2. 开机年限分布(0-3年、4-7年、8-11年、12-15年、15-18年)
50
+     * 3. 岗位资质分布(前传、操机、人身、验证、开箱包、开机)
51
+     * </p>
52
+     *
53
+     * @param query 查询参数对象(deptId, startDate, endDate)
54
+     * @return 成员职位情况分布数据
55
+     */
56
+    DeptMemberDistributionDTO getPositionDistribution(DeptPortraitQueryDTO query);
57
+}

+ 31 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/service/IGroupPortraitService.java

@@ -0,0 +1,31 @@
1
+package com.sundot.airport.ledger.service;
2
+
3
+import com.sundot.airport.ledger.dto.DeptPortraitQueryDTO;
4
+import com.sundot.airport.ledger.dto.GroupPortraitDTO;
5
+
6
+/**
7
+ * <b>功能名:</b>IGroupPortraitService<br>
8
+ * <b>说明:</b> 组织画像Service接口(小组/通道维度)<br>
9
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
10
+ *
11
+ * @author Claude
12
+ */
13
+public interface IGroupPortraitService {
14
+
15
+    /**
16
+     * 获取组织画像(小组/通道维度)
17
+     * <p>
18
+     * 计算步骤:
19
+     * 1. 根据员工配分表,计算小组六维度均值
20
+     * 2. 小组各维度最终分值计分:
21
+     *    ①将小组特有维度及其他维度特有指标项分值累计,得出小组各维度特有合计分值
22
+     *    ②将各维度基础分、特有合计计分、员工维度均值三项内容相加得到小组各维度最终分值
23
+     * 3. 加权平均法:将小组各维度最终分值乘以其相应权重后的总和除以权重的总和
24
+     * 4. 小组综合评分:小组各维度贡献分值合计后四舍五入,保留两位小数
25
+     * </p>
26
+     *
27
+     * @param query 查询参数(deptId, startDate, endDate)
28
+     * @return 组织画像数据
29
+     */
30
+    GroupPortraitDTO getGroupPortrait(DeptPortraitQueryDTO query);
31
+}

+ 408 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/service/impl/DeptPortraitServiceImpl.java

@@ -0,0 +1,408 @@
1
+package com.sundot.airport.ledger.service.impl;
2
+
3
+import com.sundot.airport.common.core.domain.entity.SysRole;
4
+import com.sundot.airport.common.core.domain.entity.SysUser;
5
+import com.sundot.airport.ledger.domain.ScoreDimension;
6
+import com.sundot.airport.ledger.domain.ScoreEvent;
7
+import com.sundot.airport.ledger.dto.DeptMemberDistributionDTO;
8
+import com.sundot.airport.ledger.dto.DeptMemberDTO;
9
+import com.sundot.airport.ledger.dto.DeptPortraitQueryDTO;
10
+import com.sundot.airport.ledger.mapper.ScoreDimensionMapper;
11
+import com.sundot.airport.ledger.mapper.ScoreEventMapper;
12
+import com.sundot.airport.ledger.service.IDeptPortraitService;
13
+import com.sundot.airport.system.domain.BasePosition;
14
+import com.sundot.airport.system.mapper.BasePositionMapper;
15
+import com.sundot.airport.system.mapper.SysRoleMapper;
16
+import com.sundot.airport.system.mapper.SysUserMapper;
17
+import org.slf4j.Logger;
18
+import org.slf4j.LoggerFactory;
19
+import org.springframework.beans.factory.annotation.Autowired;
20
+import org.springframework.stereotype.Service;
21
+
22
+import java.math.BigDecimal;
23
+import java.math.RoundingMode;
24
+import java.time.LocalDate;
25
+import java.time.Period;
26
+import java.time.format.DateTimeFormatter;
27
+import java.util.*;
28
+import java.util.function.Function;
29
+import java.util.stream.Collectors;
30
+
31
+/**
32
+ * <b>功能名:</b>DeptPortraitServiceImpl<br>
33
+ * <b>说明:</b> 部门画像Service实现(团队成员查询)<br>
34
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
35
+ *
36
+ * @author Claude
37
+ */
38
+@Service
39
+public class DeptPortraitServiceImpl implements IDeptPortraitService {
40
+
41
+    private static final Logger log = LoggerFactory.getLogger(DeptPortraitServiceImpl.class);
42
+
43
+    @Autowired
44
+    private SysUserMapper sysUserMapper;
45
+
46
+    @Autowired
47
+    private SysRoleMapper sysRoleMapper;
48
+
49
+    @Autowired
50
+    private ScoreEventMapper scoreEventMapper;
51
+
52
+    @Autowired
53
+    private ScoreDimensionMapper scoreDimensionMapper;
54
+
55
+    @Autowired
56
+    private BasePositionMapper basePositionMapper;
57
+
58
+    @Override
59
+    public List<DeptMemberDTO> getDeptMembers(DeptPortraitQueryDTO query) {
60
+        // 递归查询所有下级部门的用户
61
+        SysUser userQuery = new SysUser();
62
+        userQuery.setDeptId(query.getDeptId());
63
+        userQuery.setStatus("0");
64
+        
65
+        List<SysUser> allUsers = sysUserMapper.selectUserList(userQuery);
66
+        
67
+        if (allUsers == null || allUsers.isEmpty()) {
68
+            return Collections.emptyList();
69
+        }
70
+
71
+        List<DeptMemberDTO> result = allUsers.stream()
72
+                .map(user -> convertToDeptMemberDTO(user, query.getStartDate(), query.getEndDate()))
73
+                .collect(Collectors.toList());
74
+
75
+        return result;
76
+    }
77
+
78
+    @Override
79
+    public DeptMemberDistributionDTO getMemberDistribution(DeptPortraitQueryDTO query) {
80
+        SysUser userQuery = new SysUser();
81
+        userQuery.setDeptId(query.getDeptId());
82
+        userQuery.setStatus("0");
83
+        
84
+        List<SysUser> allUsers = sysUserMapper.selectUserList(userQuery);
85
+        
86
+        if (allUsers == null || allUsers.isEmpty()) {
87
+            return new DeptMemberDistributionDTO();
88
+        }
89
+
90
+        DeptMemberDistributionDTO distribution = new DeptMemberDistributionDTO();
91
+        distribution.setSexDistribution(calculateDistribution(allUsers, SysUser::getSex, this::decodeSex));
92
+        distribution.setNationDistribution(calculateDistribution(allUsers, SysUser::getNation, this::decodeNation));
93
+        distribution.setPoliticalDistribution(calculateDistribution(allUsers, SysUser::getPoliticalStatus, this::decodePoliticalStatusForDistribution));
94
+        
95
+        return distribution;
96
+    }
97
+
98
+    @Override
99
+    public DeptMemberDistributionDTO getPositionDistribution(DeptPortraitQueryDTO query) {
100
+        SysUser userQuery = new SysUser();
101
+        userQuery.setDeptId(query.getDeptId());
102
+        userQuery.setStatus("0");
103
+        
104
+        List<SysUser> allUsers = sysUserMapper.selectUserList(userQuery);
105
+        
106
+        if (allUsers == null || allUsers.isEmpty()) {
107
+            return new DeptMemberDistributionDTO();
108
+        }
109
+
110
+        DeptMemberDistributionDTO distribution = new DeptMemberDistributionDTO();
111
+        
112
+        // 统计职业资格等级分布
113
+        distribution.setQualificationDistribution(calculateQualificationDistribution(allUsers));
114
+        
115
+        // 统计开机年限分布
116
+        distribution.setXrayYearDistribution(calculateXrayYearDistribution(allUsers));
117
+        
118
+        // 统计岗位资质分布
119
+        distribution.setPositionDistribution(calculatePositionDistribution(allUsers));
120
+        
121
+        return distribution;
122
+    }
123
+
124
+    /**
125
+     * 统计职业资格等级分布
126
+     */
127
+    private List<DeptMemberDistributionDTO.DistributionItem> calculateQualificationDistribution(List<SysUser> users) {
128
+        return users.stream()
129
+                .map(SysUser::getQualificationLevel)
130
+                .filter(Objects::nonNull)
131
+                .filter(s -> !s.isEmpty())
132
+                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
133
+                .entrySet().stream()
134
+                .map(entry -> new DeptMemberDistributionDTO.DistributionItem(
135
+                        decodeQualificationLevelForChart(entry.getKey()),
136
+                        entry.getValue().intValue()
137
+                ))
138
+                .sorted((a, b) -> {
139
+                    // 按等级排序
140
+                    String[] order = {"等级1", "等级2", "等级3", "等级4", "等级5"};
141
+                    return Arrays.asList(order).indexOf(a.getName()) - Arrays.asList(order).indexOf(b.getName());
142
+                })
143
+                .collect(Collectors.toList());
144
+    }
145
+
146
+    /**
147
+     * 统计开机年限分布
148
+     */
149
+    private List<DeptMemberDistributionDTO.DistributionItem> calculateXrayYearDistribution(List<SysUser> users) {
150
+        Map<String, Integer> yearRanges = new LinkedHashMap<>();
151
+        yearRanges.put("0-3", 0);
152
+        yearRanges.put("4-7", 0);
153
+        yearRanges.put("8-11", 0);
154
+        yearRanges.put("12-15", 0);
155
+        yearRanges.put("15-18", 0);
156
+
157
+        for (SysUser user : users) {
158
+            if (user.getXrayOperatorStarttime() != null) {
159
+                int years = calculateYears(user.getXrayOperatorStarttime());
160
+                if (years >= 0 && years <= 3) {
161
+                    yearRanges.put("0-3", yearRanges.get("0-3") + 1);
162
+                } else if (years >= 4 && years <= 7) {
163
+                    yearRanges.put("4-7", yearRanges.get("4-7") + 1);
164
+                } else if (years >= 8 && years <= 11) {
165
+                    yearRanges.put("8-11", yearRanges.get("8-11") + 1);
166
+                } else if (years >= 12 && years <= 15) {
167
+                    yearRanges.put("12-15", yearRanges.get("12-15") + 1);
168
+                } else if (years >= 16) {
169
+                    yearRanges.put("15-18", yearRanges.get("15-18") + 1);
170
+                }
171
+            }
172
+        }
173
+
174
+        return yearRanges.entrySet().stream()
175
+                .map(entry -> new DeptMemberDistributionDTO.DistributionItem(
176
+                        entry.getKey() + "年",
177
+                        entry.getValue()
178
+                ))
179
+                .collect(Collectors.toList());
180
+    }
181
+
182
+    /**
183
+     * 统计岗位资质分布
184
+     * 从base_position表查询岗位资质,按position_type='SECURITY_POSITION'筛选
185
+     */
186
+    private List<DeptMemberDistributionDTO.DistributionItem> calculatePositionDistribution(List<SysUser> users) {
187
+        // 查询所有安检岗位
188
+        BasePosition query = new BasePosition();
189
+        query.setPositionType("SECURITY_POSITION");
190
+        List<BasePosition> allPositions = basePositionMapper.selectBasePositionList(query);
191
+        
192
+        // 初始化岗位统计Map,按岗位名称统计
193
+        Map<String, Integer> positionMap = new LinkedHashMap<>();
194
+        if (allPositions != null) {
195
+            for (BasePosition pos : allPositions) {
196
+                positionMap.put(pos.getName(), 0);
197
+            }
198
+        }
199
+        
200
+        // 统计每个用户的岗位
201
+        for (SysUser user : users) {
202
+            String positions = user.getSecurityInspectionPosition();
203
+            if (positions != null && !positions.isEmpty()) {
204
+                // 岗位可能用逗号分隔
205
+                String[] positionArray = positions.split("[,,]");
206
+                for (String pos : positionArray) {
207
+                    String trimmed = pos.trim();
208
+                    if (positionMap.containsKey(trimmed)) {
209
+                        positionMap.put(trimmed, positionMap.get(trimmed) + 1);
210
+                    }
211
+                }
212
+            }
213
+        }
214
+        
215
+        return positionMap.entrySet().stream()
216
+                .map(entry -> new DeptMemberDistributionDTO.DistributionItem(
217
+                        entry.getKey(),
218
+                        entry.getValue()
219
+                ))
220
+                .collect(Collectors.toList());
221
+    }
222
+
223
+    /**
224
+     * 解码职业资格等级(用于图表显示)
225
+     */
226
+    private String decodeQualificationLevelForChart(String v) {
227
+        if (v == null) return "未知";
228
+        switch (v) {
229
+            case "LEVEL_ONE": return "等级1";
230
+            case "LEVEL_TWO": return "等级2";
231
+            case "LEVEL_THREE": return "等级3";
232
+            case "LEVEL_FOUR": return "等级4";
233
+            case "LEVEL_FIVE": return "等级5";
234
+            default: return v;
235
+        }
236
+    }
237
+
238
+    private List<DeptMemberDistributionDTO.DistributionItem> calculateDistribution(
239
+            List<SysUser> users,
240
+            Function<SysUser, String> extractor,
241
+            Function<String, String> decoder) {
242
+        
243
+        return users.stream()
244
+                .map(extractor)
245
+                .filter(Objects::nonNull)
246
+                .filter(s -> !s.isEmpty())
247
+                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
248
+                .entrySet().stream()
249
+                .map(entry -> new DeptMemberDistributionDTO.DistributionItem(
250
+                        decoder.apply(entry.getKey()),
251
+                        entry.getValue().intValue()
252
+                ))
253
+                .sorted((a, b) -> b.getCount() - a.getCount())
254
+                .collect(Collectors.toList());
255
+    }
256
+
257
+    private String decodeSex(String sex) {
258
+        if (sex == null || sex.isEmpty()) return "未知";
259
+        return "0".equals(sex) ? "男" : "1".equals(sex) ? "女" : "其他";
260
+    }
261
+
262
+    private String decodeNation(String nation) {
263
+        if (nation == null || nation.isEmpty()) return "未知";
264
+        return nation;
265
+    }
266
+
267
+    private String decodePoliticalStatusForDistribution(String politicalStatus) {
268
+        if (politicalStatus == null || politicalStatus.isEmpty()) return "未知";
269
+        switch (politicalStatus) {
270
+            case "COMMUNIST_PARTY_MEMBER": return "中共党员";
271
+            case "PROBATIONARY_COMMUNIST_PARTY_MEMBER": return "中共预备党员";
272
+            case "COMMUNIST_YOUTH_LEAGUE_MEMBER": return "共青团员";
273
+            case "MASS_PUBLIC": return "群众";
274
+            default: return politicalStatus;
275
+        }
276
+    }
277
+
278
+    private DeptMemberDTO convertToDeptMemberDTO(SysUser user, String startDate, String endDate) {
279
+        DeptMemberDTO dto = new DeptMemberDTO();
280
+        dto.setUserId(user.getUserId());
281
+        dto.setPersonName(user.getNickName());
282
+        dto.setSex("0".equals(user.getSex()) ? "男" : "1".equals(user.getSex()) ? "女" : "");
283
+        dto.setNation(user.getNation());
284
+        dto.setPoliticalStatus(decodePoliticalStatus(user.getPoliticalStatus()));
285
+        dto.setQualificationLevel(decodeQualificationLevel(user.getQualificationLevel()));
286
+        dto.setAge(calculateAgeFromIdCard(user.getCardNumber()));
287
+        
288
+        if (user.getStartWorkingDate() != null) {
289
+            dto.setWorkYears(calculateYears(user.getStartWorkingDate()));
290
+        }
291
+        if (user.getXrayOperatorStarttime() != null) {
292
+            dto.setXrayOperatorYears(calculateYears(user.getXrayOperatorStarttime()));
293
+        }
294
+        
295
+        List<SysRole> roles = sysRoleMapper.selectRolesByUserName(user.getUserName());
296
+        if (roles != null && !roles.isEmpty()) {
297
+            String roleNames = roles.stream()
298
+                    .map(SysRole::getRoleName)
299
+                    .filter(s -> s != null && !s.isEmpty())
300
+                    .collect(Collectors.joining("、"));
301
+            dto.setRoleNames(roleNames);
302
+        }
303
+        
304
+        BigDecimal totalScore = calculateTotalScore(user.getNickName(), startDate, endDate);
305
+        dto.setTotalScore(totalScore);
306
+
307
+        return dto;
308
+    }
309
+
310
+    private BigDecimal calculateTotalScore(String personName, String beginTime, String endTime) {
311
+        ScoreEvent eventQuery = new ScoreEvent();
312
+        eventQuery.setPersonName(personName);
313
+        if (beginTime != null && !beginTime.isEmpty()) {
314
+            eventQuery.getParams().put("beginTime", beginTime);
315
+        }
316
+        if (endTime != null && !endTime.isEmpty()) {
317
+            eventQuery.getParams().put("endTime", endTime);
318
+        }
319
+        List<ScoreEvent> events = scoreEventMapper.selectList(eventQuery);
320
+
321
+        if (events == null || events.isEmpty()) {
322
+            return BigDecimal.valueOf(80);
323
+        }
324
+
325
+        Map<Long, BigDecimal> dimMap = new HashMap<>();
326
+        for (ScoreEvent e : events) {
327
+            Long dimId = e.getDimensionId();
328
+            if (dimId == null) continue;
329
+            String raw = e.getPersonName();
330
+            if (raw == null) continue;
331
+            boolean matched = false;
332
+            for (String n : raw.split("[,,]")) {
333
+                if (personName.equals(n.trim())) {
334
+                    matched = true;
335
+                    break;
336
+                }
337
+            }
338
+            if (!matched) continue;
339
+            BigDecimal val = e.getTotalScore() != null ? e.getTotalScore() : BigDecimal.ZERO;
340
+            dimMap.merge(dimId, val, BigDecimal::add);
341
+        }
342
+
343
+        ScoreDimension dq = new ScoreDimension();
344
+        dq.setStatus("0");
345
+        List<ScoreDimension> dims = scoreDimensionMapper.selectList(dq);
346
+        dims.sort(Comparator.comparing(d -> d.getSortOrder() == null ? 999 : d.getSortOrder()));
347
+
348
+        BigDecimal total = BigDecimal.ZERO;
349
+        for (ScoreDimension dim : dims) {
350
+            BigDecimal eventScore = dimMap.getOrDefault(dim.getId(), BigDecimal.ZERO);
351
+            BigDecimal base = dim.getBaseScore() != null ? dim.getBaseScore() : BigDecimal.valueOf(80);
352
+            BigDecimal dimScore = base.add(eventScore);
353
+            BigDecimal weight = dim.getWeight() != null ? dim.getWeight() : BigDecimal.ZERO;
354
+            BigDecimal contribution = dimScore.multiply(weight)
355
+                    .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
356
+            total = total.add(contribution);
357
+        }
358
+
359
+        return total.setScale(1, RoundingMode.HALF_UP);
360
+    }
361
+
362
+    private Integer calculateAgeFromIdCard(String idCard) {
363
+        if (idCard == null || idCard.length() < 14) {
364
+            return null;
365
+        }
366
+        try {
367
+            String birthDateStr = idCard.substring(6, 14);
368
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
369
+            LocalDate birthDate = LocalDate.parse(birthDateStr, formatter);
370
+            LocalDate now = LocalDate.now();
371
+            return Period.between(birthDate, now).getYears();
372
+        } catch (Exception e) {
373
+            log.warn("从身份证计算年龄失败: {}", idCard, e);
374
+            return null;
375
+        }
376
+    }
377
+
378
+    private Integer calculateYears(Date startDate) {
379
+        if (startDate == null) {
380
+            return null;
381
+        }
382
+        long nowMs = System.currentTimeMillis();
383
+        return (int) ((nowMs - startDate.getTime()) / (365L * 24 * 3600 * 1000));
384
+    }
385
+
386
+    private String decodePoliticalStatus(String v) {
387
+        if (v == null) return "";
388
+        switch (v) {
389
+            case "COMMUNIST_PARTY_MEMBER": return "中共党员";
390
+            case "PROBATIONARY_COMMUNIST_PARTY_MEMBER": return "中共预备党员";
391
+            case "COMMUNIST_YOUTH_LEAGUE_MEMBER": return "共青团员";
392
+            case "MASS_PUBLIC": return "群众";
393
+            default: return v;
394
+        }
395
+    }
396
+
397
+    private String decodeQualificationLevel(String v) {
398
+        if (v == null) return "";
399
+        switch (v) {
400
+            case "LEVEL_ONE": return "一级";
401
+            case "LEVEL_TWO": return "二级";
402
+            case "LEVEL_THREE": return "三级";
403
+            case "LEVEL_FOUR": return "四级";
404
+            case "LEVEL_FIVE": return "五级";
405
+            default: return v;
406
+        }
407
+    }
408
+}

+ 352 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/service/impl/GroupPortraitServiceImpl.java

@@ -0,0 +1,352 @@
1
+package com.sundot.airport.ledger.service.impl;
2
+
3
+import com.sundot.airport.common.core.domain.entity.SysDept;
4
+import com.sundot.airport.common.core.domain.entity.SysUser;
5
+import com.sundot.airport.ledger.domain.ScoreDimension;
6
+import com.sundot.airport.ledger.domain.ScoreEvent;
7
+import com.sundot.airport.ledger.domain.vo.EmployeePortraitVO;
8
+import com.sundot.airport.ledger.dto.DeptPortraitQueryDTO;
9
+import com.sundot.airport.ledger.dto.GroupPortraitDTO;
10
+import com.sundot.airport.ledger.mapper.ScoreDimensionMapper;
11
+import com.sundot.airport.ledger.mapper.ScoreEventMapper;
12
+import com.sundot.airport.ledger.service.IGroupPortraitService;
13
+import com.sundot.airport.system.mapper.SysDeptMapper;
14
+import com.sundot.airport.system.mapper.SysUserMapper;
15
+import org.slf4j.Logger;
16
+import org.slf4j.LoggerFactory;
17
+import org.springframework.beans.factory.annotation.Autowired;
18
+import org.springframework.stereotype.Service;
19
+
20
+import java.math.BigDecimal;
21
+import java.math.RoundingMode;
22
+import java.util.*;
23
+import java.util.stream.Collectors;
24
+
25
+/**
26
+ * <b>功能名:</b>GroupPortraitServiceImpl<br>
27
+ * <b>说明:</b> 组织画像Service实现(小组/通道维度)<br>
28
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
29
+ *
30
+ * @author Claude
31
+ */
32
+@Service
33
+public class GroupPortraitServiceImpl implements IGroupPortraitService {
34
+
35
+    private static final Logger log = LoggerFactory.getLogger(GroupPortraitServiceImpl.class);
36
+
37
+    @Autowired
38
+    private SysUserMapper sysUserMapper;
39
+
40
+    @Autowired
41
+    private SysDeptMapper sysDeptMapper;
42
+
43
+    @Autowired
44
+    private ScoreEventMapper scoreEventMapper;
45
+
46
+    @Autowired
47
+    private ScoreDimensionMapper scoreDimensionMapper;
48
+
49
+    @Override
50
+    public GroupPortraitDTO getGroupPortrait(DeptPortraitQueryDTO query) {
51
+        // 1. 获取部门信息
52
+        SysDept dept = sysDeptMapper.selectDeptById(query.getDeptId());
53
+        if (dept == null) {
54
+            throw new RuntimeException("部门不存在");
55
+        }
56
+
57
+        // 2. 查询部门所有成员
58
+        SysUser userQuery = new SysUser();
59
+        userQuery.setDeptId(query.getDeptId());
60
+        userQuery.setStatus("0");
61
+        List<SysUser> members = sysUserMapper.selectUserList(userQuery);
62
+
63
+        if (members == null || members.isEmpty()) {
64
+            throw new RuntimeException("该部门暂无成员");
65
+        }
66
+
67
+        // 3. 构建组织画像DTO
68
+        GroupPortraitDTO portrait = new GroupPortraitDTO();
69
+        portrait.setDeptId(dept.getDeptId());
70
+        portrait.setDeptName(dept.getDeptName());
71
+        portrait.setDeptType(dept.getDeptType());
72
+        portrait.setPeriod(query.getStartDate() + " ~ " + query.getEndDate());
73
+        portrait.setMemberCount(members.size());
74
+
75
+        // 4. 计算各维度得分
76
+        List<GroupPortraitDTO.DimensionScore> dimensions = calculateDimensions( dept, query);
77
+        portrait.setDimensions(dimensions);
78
+
79
+        // 5. 计算综合评分(加权平均)
80
+        BigDecimal totalScore = calculateTotalScore(dimensions);
81
+        portrait.setTotalScore(totalScore);
82
+
83
+        return portrait;
84
+    }
85
+
86
+    /**
87
+     * 计算各维度得分
88
+     * 核心逻辑:
89
+     * 1. 根据当前部门层级查询对应维度
90
+     * 2. 通过dimRelationship找到下级维度
91
+     * 3. 如果下级有值,用下级平均值;否则用基础分
92
+     * 4. 加上当前层级特有分值
93
+     */
94
+    private List<GroupPortraitDTO.DimensionScore> calculateDimensions( SysDept dept, DeptPortraitQueryDTO query) {
95
+        // 1. 根据部门类型确定org值
96
+        String org = determineOrgByDeptType(dept.getDeptType());
97
+
98
+        // 2. 查询当前层级的所有维度定义
99
+        ScoreDimension dq = new ScoreDimension();
100
+        dq.setOrg(org);
101
+        dq.setStatus("0");
102
+        List<ScoreDimension> dims = scoreDimensionMapper.selectList(dq);
103
+        dims.sort(Comparator.comparing(d -> d.getSortOrder() == null ? 999 : d.getSortOrder()));
104
+
105
+        List<GroupPortraitDTO.DimensionScore> result = new ArrayList<>();
106
+
107
+        for (ScoreDimension dim : dims) {
108
+            // 3. 通过dimRelationship找到下级维度,计算下级平均值
109
+            BigDecimal lowerLevelAverage = calculateLowerLevelAverage(dim, query.getStartDate(), query.getEndDate());
110
+            
111
+            // 4. 获取基础分(如果下级有值就不用基础分)
112
+            BigDecimal baseScore = dim.getBaseScore() != null ? dim.getBaseScore() : BigDecimal.ZERO;
113
+            
114
+            // 5. 决定使用下级平均值还是基础分
115
+            BigDecimal averageScore = lowerLevelAverage.compareTo(BigDecimal.ZERO) > 0 
116
+                    ? lowerLevelAverage 
117
+                    : baseScore;
118
+
119
+            // 6. 计算当前层级特有加减分
120
+            Map<String, BigDecimal> specialScores = calculateSpecialScores(dim.getId(), query.getDeptId(), 
121
+                    query.getStartDate(), query.getEndDate(),org);
122
+            BigDecimal addScore = specialScores.getOrDefault("add", BigDecimal.ZERO);
123
+            BigDecimal subtractScore = specialScores.getOrDefault("subtract", BigDecimal.ZERO);
124
+            BigDecimal specialTotal = addScore.subtract(subtractScore);
125
+
126
+            // 7. 最终分值 = 下级平均值(或基础分) + 特有合计
127
+            BigDecimal finalScore = averageScore.add(specialTotal);
128
+
129
+            // 8. 计算贡献分值
130
+            BigDecimal weight = dim.getWeight() != null ? dim.getWeight() : BigDecimal.ZERO;
131
+            BigDecimal contribution = finalScore.multiply(weight)
132
+                    .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
133
+
134
+            // 9. 组装维度得分项
135
+            GroupPortraitDTO.DimensionScore item = new GroupPortraitDTO.DimensionScore();
136
+            item.setName(dim.getName());
137
+            item.setGroupAverageScore(lowerLevelAverage.setScale(2, RoundingMode.HALF_UP));
138
+            item.setBaseScore(baseScore.setScale(2, RoundingMode.HALF_UP));
139
+            item.setAddScore(addScore.setScale(2, RoundingMode.HALF_UP));
140
+            item.setSubtractScore(subtractScore.setScale(2, RoundingMode.HALF_UP));
141
+            item.setSpecialTotal(specialTotal.setScale(2, RoundingMode.HALF_UP));
142
+            item.setFinalScore(finalScore.setScale(2, RoundingMode.HALF_UP));
143
+            item.setWeight(weight);
144
+            item.setContribution(contribution);
145
+
146
+            result.add(item);
147
+        }
148
+
149
+        return result;
150
+    }
151
+
152
+    /**
153
+     * 根据部门类型确定org值
154
+     * TEAMS(班组) -> org='2' (小组维度)
155
+     * MANAGER(主管/队) -> org='1' (队维度)
156
+     * BRIGADE(大队/部门) -> org='1' (部门维度)
157
+     * STATION(站) -> org='1' (部门维度)
158
+     */
159
+    private String determineOrgByDeptType(String deptType) {
160
+        if (deptType == null) {
161
+            return "2"; // 默认小组维度
162
+        }
163
+        switch (deptType) {
164
+            case "TEAMS":
165
+                return "2"; // 小组维度
166
+            case "MANAGER":
167
+            case "BRIGADE":
168
+            case "STATION":
169
+                return "1"; // 队/部门维度
170
+            default:
171
+                return "2";
172
+        }
173
+    }
174
+
175
+    /**
176
+     * 计算下级维度平均值(递归查找)
177
+     * 层级关系:部门(1) → 队/班组(2) → 小组(3) → 人员(4)
178
+     * 递进逻辑:
179
+     * - org=1(部门) → 查询org=2(队)的维度 → 继续递归找org=3 → 继续递归找org=4
180
+     * - org=2(队) → 查询org=3(小组)的维度 → 继续递归找org=4
181
+     * - org=3(小组) → 查询org=4(人员)的维度 → 计算人员事件平均
182
+     * - org=4(人员) → 最底层,直接返回0
183
+     */
184
+    private BigDecimal calculateLowerLevelAverage(ScoreDimension currentDim, String beginTime, String endTime) {
185
+        String dimRelationship = currentDim.getDimRelationship();
186
+        String currentOrg = currentDim.getOrg();
187
+        
188
+        // 1. 如果没有对照关系或已是最底层(org=4),返回0
189
+        if (dimRelationship == null || dimRelationship.isEmpty() || "4".equals(currentOrg)) {
190
+            return BigDecimal.ZERO;
191
+        }
192
+        
193
+        // 2. 查找下一层级的org值
194
+        String lowerOrg = getLowerLevelOrg(currentOrg);
195
+        if (lowerOrg == null) {
196
+            return BigDecimal.ZERO;
197
+        }
198
+
199
+        
200
+        // 3. 查询下一级维度(通过dimRelationship匹配)
201
+        ScoreDimension query = new ScoreDimension();
202
+        query.setDimRelationship(dimRelationship);
203
+        query.setOrg(lowerOrg);
204
+        query.setStatus("0");
205
+        List<ScoreDimension> lowerDims = scoreDimensionMapper.selectList(query);
206
+        
207
+        if (lowerDims == null || lowerDims.isEmpty()) {
208
+            log.warn("未找到下级维度,对照关系: {}, 目标org: {}", dimRelationship, lowerOrg);
209
+            return BigDecimal.ZERO;
210
+        }
211
+        
212
+        // 取第一个匹配的维度
213
+        ScoreDimension lowerDim = lowerDims.get(0);
214
+
215
+        // 4. 根据下一级的org决定如何处理
216
+        if ("4".equals(lowerOrg)) {
217
+            // 下一级是人员维度,计算人员事件平均值
218
+            return calculatePersonDimensionAverage(lowerDim, beginTime, endTime);
219
+        } else {
220
+            // 下一级是组织维度(2或3),需要继续递归查找更下一级
221
+            // 例如:部门(1)找队(2),队(2)还要继续找小组(3)
222
+            return calculateLowerLevelAverage(lowerDim, beginTime, endTime);
223
+        }
224
+    }
225
+
226
+    /**
227
+     * 计算人员维度平均值
228
+     * 最底层(org=4),查询该维度下的所有人员事件,计算平均分数
229
+     */
230
+    private BigDecimal calculatePersonDimensionAverage(ScoreDimension dimension, String beginTime, String endTime) {
231
+
232
+        // 查询该维度下的所有事件
233
+        ScoreEvent eventQuery = new ScoreEvent();
234
+        eventQuery.setDimensionId(dimension.getId());
235
+        if (beginTime != null && !beginTime.isEmpty()) {
236
+            eventQuery.getParams().put("beginTime", beginTime);
237
+        }
238
+        if (endTime != null && !endTime.isEmpty()) {
239
+            eventQuery.getParams().put("endTime", endTime);
240
+        }
241
+        List<ScoreEvent> events = scoreEventMapper.selectList(eventQuery);
242
+
243
+        if (events == null || events.isEmpty()) {
244
+            log.info("人员维度无事件数据");
245
+            return BigDecimal.ZERO;
246
+        }
247
+
248
+        // 按人员分组统计总分
249
+        Map<String, BigDecimal> personScores = new HashMap<>();
250
+        for (ScoreEvent e : events) {
251
+            String personName = e.getPersonName();
252
+            if (personName == null || personName.trim().isEmpty()) continue;
253
+
254
+            // 支持逗号分隔多人
255
+            String[] names = personName.split("[,,]");
256
+            for (String name : names) {
257
+                String trimmedName = name.trim();
258
+                if (trimmedName.isEmpty()) continue;
259
+
260
+                BigDecimal score = e.getTotalScore() != null ? e.getTotalScore() : BigDecimal.ZERO;
261
+                personScores.merge(trimmedName, score, BigDecimal::add);
262
+            }
263
+        }
264
+
265
+        if (personScores.isEmpty()) {
266
+            return BigDecimal.ZERO;
267
+        }
268
+        BigDecimal sum = personScores.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
269
+
270
+        BigDecimal base = dimension.getBaseScore() != null ? dimension.getBaseScore() : BigDecimal.valueOf(80);
271
+        BigDecimal dimScore = base.add(sum);
272
+        BigDecimal weight = dimension.getWeight() != null ? dimension.getWeight() : BigDecimal.ZERO;
273
+        BigDecimal contribution = dimScore.multiply(weight)
274
+                .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
275
+        return contribution;
276
+    }
277
+
278
+    /**
279
+     * 获取下级层级的org值
280
+     * 层级关系:部门(1) -> 队(2) -> 小组(3) -> 员工(4)
281
+     */
282
+    private String getLowerLevelOrg(String currentOrg) {
283
+        if (currentOrg == null) {
284
+            return null;
285
+        }
286
+        switch (currentOrg) {
287
+            case "1":
288
+                return "2";
289
+            case "2":
290
+                return "3";
291
+            case "3":
292
+                return "4";
293
+            default:
294
+                return null;
295
+        }
296
+    }
297
+
298
+    /**
299
+     * 计算小组特有加减分
300
+     * 注:小组特有事件通过org=1(部门级别)筛选
301
+     */
302
+    private Map<String, BigDecimal> calculateSpecialScores(Long dimensionId, Long deptId, String beginTime, String endTime,String org) {
303
+        Map<String, BigDecimal> result = new HashMap<>();
304
+        result.put("add", BigDecimal.ZERO);
305
+        result.put("subtract", BigDecimal.ZERO);
306
+
307
+        // 查询该部门在该维度的特有事件(org=1表示部门级)
308
+        ScoreEvent eventQuery = new ScoreEvent();
309
+        eventQuery.setDimensionId(dimensionId);
310
+        eventQuery.setDeptId(deptId);
311
+        eventQuery.setOrg("1"); // 部门级别事件
312
+        if (beginTime != null && !beginTime.isEmpty()) {
313
+            eventQuery.getParams().put("beginTime", beginTime);
314
+        }
315
+        if (endTime != null && !endTime.isEmpty()) {
316
+            eventQuery.getParams().put("endTime", endTime);
317
+        }
318
+        List<ScoreEvent> events = scoreEventMapper.selectList(eventQuery);
319
+
320
+        if (events == null || events.isEmpty()) {
321
+            return result;
322
+        }
323
+
324
+        // 分别累计加分和减分
325
+        BigDecimal addTotal = BigDecimal.ZERO;
326
+        BigDecimal subtractTotal = BigDecimal.ZERO;
327
+
328
+        for (ScoreEvent e : events) {
329
+            BigDecimal score = e.getTotalScore() != null ? e.getTotalScore() : BigDecimal.ZERO;
330
+            if (score.compareTo(BigDecimal.ZERO) > 0) {
331
+                addTotal = addTotal.add(score);
332
+            } else {
333
+                subtractTotal = subtractTotal.add(score.abs());
334
+            }
335
+        }
336
+
337
+        result.put("add", addTotal);
338
+        result.put("subtract", subtractTotal);
339
+
340
+        return result;
341
+    }
342
+
343
+    /**
344
+     * 计算综合评分(各维度贡献分值之和)
345
+     */
346
+    private BigDecimal calculateTotalScore(List<GroupPortraitDTO.DimensionScore> dimensions) {
347
+        BigDecimal total = dimensions.stream()
348
+                .map(GroupPortraitDTO.DimensionScore::getContribution)
349
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
350
+        return total.setScale(2, RoundingMode.HALF_UP);
351
+    }
352
+}

+ 15 - 13
airport-ledger/src/main/resources/mapper/ledger/ScoreDimensionMapper.xml

@@ -3,22 +3,24 @@
3 3
 <mapper namespace="com.sundot.airport.ledger.mapper.ScoreDimensionMapper">
4 4
 
5 5
     <resultMap id="ScoreDimensionResult" type="com.sundot.airport.ledger.domain.ScoreDimension">
6
-        <id     property="id"         column="id"/>
7
-        <result property="name"       column="name"/>
8
-        <result property="weight"     column="weight"/>
9
-        <result property="baseScore"  column="base_score"/>
10
-        <result property="sortOrder"  column="sort_order"/>
11
-        <result property="status"     column="status"/>
12
-        <result property="org"     column="org"/>
13
-        <result property="remark"     column="remark"/>
14
-        <result property="createBy"   column="create_by"/>
15
-        <result property="createTime" column="create_time"/>
16
-        <result property="updateBy"   column="update_by"/>
17
-        <result property="updateTime" column="update_time"/>
6
+        <id     property="id"             column="id"/>
7
+        <result property="name"           column="name"/>
8
+        <result property="weight"         column="weight"/>
9
+        <result property="baseScore"      column="base_score"/>
10
+        <result property="sortOrder"      column="sort_order"/>
11
+        <result property="status"         column="status"/>
12
+        <result property="org"            column="org"/>
13
+        <result property="dimRelationship" column="dim_relationship"/>
14
+        <result property="remark"         column="remark"/>
15
+        <result property="createBy"       column="create_by"/>
16
+        <result property="createTime"     column="create_time"/>
17
+        <result property="updateBy"       column="update_by"/>
18
+        <result property="updateTime"     column="update_time"/>
18 19
     </resultMap>
19 20
 
20 21
     <sql id="selectVo">
21
-        SELECT id, name, weight, base_score, sort_order, status, remark,
22
+        SELECT id, name, weight, base_score, sort_order, status, org,
23
+               dim_relationship, remark,
22 24
                create_by, create_time, update_by, update_time
23 25
         FROM score_dimension
24 26
         WHERE del_flag = '0'

+ 5 - 5
airport-ledger/src/main/resources/mapper/ledger/ScoreEventMapper.xml

@@ -55,11 +55,11 @@
55 55
             resultMap="ScoreEventResult">
56 56
         <include refid="selectVo"/>
57 57
         <if test="dimensionId != null">AND dimension_id = #{dimensionId}</if>
58
-        <if test="personId != null">person_id = #{personId}</if>
59
-        <if test="deptId != null">dept_id = #{deptId}</if>
60
-        <if test="teamId != null">team_id = #{teamId}</if>
61
-        <if test="groupId != null">group_id = #{groupId}</if>
62
-        <if test="org != null and org != ''">org = #{org}</if>
58
+        <if test="personId != null">AND person_id = #{personId}</if>
59
+        <if test="deptId != null">AND dept_id = #{deptId}</if>
60
+        <if test="teamId != null">AND team_id = #{teamId}</if>
61
+        <if test="groupId != null">AND group_id = #{groupId}</if>
62
+        <if test="org != null and org != ''">AND org = #{org}</if>
63 63
         <if test="personName != null and personName != ''">AND person_name LIKE CONCAT('%', #{personName}, '%')</if>
64 64
         <if test="deptName != null and deptName != ''">AND dept_name LIKE CONCAT('%', #{deptName}, '%')</if>
65 65
         <if test="teamName != null and teamName != ''">AND team_name LIKE CONCAT('%', #{teamName}, '%')</if>