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

feat: 实现员工画像后端接口

新增 EmployeePortraitVO、IEmployeePortraitService、EmployeePortraitServiceImpl
和 EmployeePortraitController,提供 /score/portrait/searchUsers 和
/score/portrait/employee 两个接口,聚合用户基本信息、六维度评分、
最新考试成绩、获奖记录等数据。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
simonlll пре 1 месец
родитељ
комит
a1b3d19713

+ 31 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/score/EmployeePortraitController.java

@@ -0,0 +1,31 @@
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.service.IEmployeePortraitService;
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.web.bind.annotation.GetMapping;
8
+import org.springframework.web.bind.annotation.RequestMapping;
9
+import org.springframework.web.bind.annotation.RequestParam;
10
+import org.springframework.web.bind.annotation.RestController;
11
+
12
+@RestController
13
+@RequestMapping("/score/portrait")
14
+public class EmployeePortraitController extends BaseController {
15
+
16
+    @Autowired
17
+    private IEmployeePortraitService portraitService;
18
+
19
+    @GetMapping("/searchUsers")
20
+    public AjaxResult searchUsers(@RequestParam(required = false, defaultValue = "") String keyword) {
21
+        return AjaxResult.success(portraitService.searchUsers(keyword));
22
+    }
23
+
24
+    @GetMapping("/employee")
25
+    public AjaxResult getEmployee(
26
+            @RequestParam String personName,
27
+            @RequestParam(required = false) String beginTime,
28
+            @RequestParam(required = false) String endTime) {
29
+        return AjaxResult.success(portraitService.getPortrait(personName, beginTime, endTime));
30
+    }
31
+}

+ 191 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/domain/vo/EmployeePortraitVO.java

@@ -0,0 +1,191 @@
1
+package com.sundot.airport.ledger.domain.vo;
2
+
3
+import com.fasterxml.jackson.annotation.JsonFormat;
4
+
5
+import java.math.BigDecimal;
6
+import java.util.Date;
7
+import java.util.List;
8
+
9
+/**
10
+ * 员工画像聚合 VO
11
+ */
12
+public class EmployeePortraitVO {
13
+
14
+    // ── 基本信息 ──────────────────────────────────────────────────────────────
15
+
16
+    private Long userId;
17
+    private String personName;
18
+    private String avatar;
19
+    private String sex;
20
+    private String sexText;
21
+    private String deptName;
22
+    private String parentDeptName;
23
+    private String deptPath;
24
+    private String qualificationLevelText;
25
+    private String schooling;
26
+    private String politicalStatusText;
27
+    private String characterCharacteristics;
28
+    private String workingStyle;
29
+    private String phonenumber;
30
+
31
+    @JsonFormat(pattern = "yyyy-MM-dd")
32
+    private Date startWorkingDate;
33
+
34
+    @JsonFormat(pattern = "yyyy-MM-dd")
35
+    private Date securityCheckStartDate;
36
+
37
+    private Integer workYears;
38
+    private Integer securityCheckYears;
39
+    private String securityInspectionPosition;
40
+
41
+    // ── 综合得分 ──────────────────────────────────────────────────────────────
42
+
43
+    private BigDecimal totalScore;
44
+
45
+    // ── 六维度雷达 ────────────────────────────────────────────────────────────
46
+
47
+    private List<DimScoreItem> dimensions;
48
+
49
+    // ── 最新考试成绩 ──────────────────────────────────────────────────────────
50
+
51
+    private BigDecimal theoryScore;
52
+    private BigDecimal imageScore;
53
+    private String examCategory;
54
+    private String examPeriod;
55
+
56
+    @JsonFormat(pattern = "yyyy-MM-dd")
57
+    private Date examDate;
58
+
59
+    // ── 获奖/加分记录 ─────────────────────────────────────────────────────────
60
+
61
+    private List<AwardRecord> awards;
62
+
63
+    // ── getter / setter ───────────────────────────────────────────────────────
64
+
65
+    public Long getUserId() { return userId; }
66
+    public void setUserId(Long userId) { this.userId = userId; }
67
+
68
+    public String getPersonName() { return personName; }
69
+    public void setPersonName(String personName) { this.personName = personName; }
70
+
71
+    public String getAvatar() { return avatar; }
72
+    public void setAvatar(String avatar) { this.avatar = avatar; }
73
+
74
+    public String getSex() { return sex; }
75
+    public void setSex(String sex) { this.sex = sex; }
76
+
77
+    public String getSexText() { return sexText; }
78
+    public void setSexText(String sexText) { this.sexText = sexText; }
79
+
80
+    public String getDeptName() { return deptName; }
81
+    public void setDeptName(String deptName) { this.deptName = deptName; }
82
+
83
+    public String getParentDeptName() { return parentDeptName; }
84
+    public void setParentDeptName(String parentDeptName) { this.parentDeptName = parentDeptName; }
85
+
86
+    public String getDeptPath() { return deptPath; }
87
+    public void setDeptPath(String deptPath) { this.deptPath = deptPath; }
88
+
89
+    public String getQualificationLevelText() { return qualificationLevelText; }
90
+    public void setQualificationLevelText(String qualificationLevelText) { this.qualificationLevelText = qualificationLevelText; }
91
+
92
+    public String getSchooling() { return schooling; }
93
+    public void setSchooling(String schooling) { this.schooling = schooling; }
94
+
95
+    public String getPoliticalStatusText() { return politicalStatusText; }
96
+    public void setPoliticalStatusText(String politicalStatusText) { this.politicalStatusText = politicalStatusText; }
97
+
98
+    public String getCharacterCharacteristics() { return characterCharacteristics; }
99
+    public void setCharacterCharacteristics(String characterCharacteristics) { this.characterCharacteristics = characterCharacteristics; }
100
+
101
+    public String getWorkingStyle() { return workingStyle; }
102
+    public void setWorkingStyle(String workingStyle) { this.workingStyle = workingStyle; }
103
+
104
+    public String getPhonenumber() { return phonenumber; }
105
+    public void setPhonenumber(String phonenumber) { this.phonenumber = phonenumber; }
106
+
107
+    public Date getStartWorkingDate() { return startWorkingDate; }
108
+    public void setStartWorkingDate(Date startWorkingDate) { this.startWorkingDate = startWorkingDate; }
109
+
110
+    public Date getSecurityCheckStartDate() { return securityCheckStartDate; }
111
+    public void setSecurityCheckStartDate(Date securityCheckStartDate) { this.securityCheckStartDate = securityCheckStartDate; }
112
+
113
+    public Integer getWorkYears() { return workYears; }
114
+    public void setWorkYears(Integer workYears) { this.workYears = workYears; }
115
+
116
+    public Integer getSecurityCheckYears() { return securityCheckYears; }
117
+    public void setSecurityCheckYears(Integer securityCheckYears) { this.securityCheckYears = securityCheckYears; }
118
+
119
+    public String getSecurityInspectionPosition() { return securityInspectionPosition; }
120
+    public void setSecurityInspectionPosition(String securityInspectionPosition) { this.securityInspectionPosition = securityInspectionPosition; }
121
+
122
+    public BigDecimal getTotalScore() { return totalScore; }
123
+    public void setTotalScore(BigDecimal totalScore) { this.totalScore = totalScore; }
124
+
125
+    public List<DimScoreItem> getDimensions() { return dimensions; }
126
+    public void setDimensions(List<DimScoreItem> dimensions) { this.dimensions = dimensions; }
127
+
128
+    public BigDecimal getTheoryScore() { return theoryScore; }
129
+    public void setTheoryScore(BigDecimal theoryScore) { this.theoryScore = theoryScore; }
130
+
131
+    public BigDecimal getImageScore() { return imageScore; }
132
+    public void setImageScore(BigDecimal imageScore) { this.imageScore = imageScore; }
133
+
134
+    public String getExamCategory() { return examCategory; }
135
+    public void setExamCategory(String examCategory) { this.examCategory = examCategory; }
136
+
137
+    public String getExamPeriod() { return examPeriod; }
138
+    public void setExamPeriod(String examPeriod) { this.examPeriod = examPeriod; }
139
+
140
+    public Date getExamDate() { return examDate; }
141
+    public void setExamDate(Date examDate) { this.examDate = examDate; }
142
+
143
+    public List<AwardRecord> getAwards() { return awards; }
144
+    public void setAwards(List<AwardRecord> awards) { this.awards = awards; }
145
+
146
+    // ── 内部类 ────────────────────────────────────────────────────────────────
147
+
148
+    public static class DimScoreItem {
149
+        private String name;
150
+        private BigDecimal score;
151
+        private BigDecimal weight;
152
+        private BigDecimal baseScore;
153
+        private BigDecimal eventScore;
154
+
155
+        public String getName() { return name; }
156
+        public void setName(String name) { this.name = name; }
157
+
158
+        public BigDecimal getScore() { return score; }
159
+        public void setScore(BigDecimal score) { this.score = score; }
160
+
161
+        public BigDecimal getWeight() { return weight; }
162
+        public void setWeight(BigDecimal weight) { this.weight = weight; }
163
+
164
+        public BigDecimal getBaseScore() { return baseScore; }
165
+        public void setBaseScore(BigDecimal baseScore) { this.baseScore = baseScore; }
166
+
167
+        public BigDecimal getEventScore() { return eventScore; }
168
+        public void setEventScore(BigDecimal eventScore) { this.eventScore = eventScore; }
169
+    }
170
+
171
+    public static class AwardRecord {
172
+        private String type;
173
+        private String content;
174
+        private BigDecimal score;
175
+
176
+        @JsonFormat(pattern = "yyyy-MM-dd")
177
+        private Date date;
178
+
179
+        public String getType() { return type; }
180
+        public void setType(String type) { this.type = type; }
181
+
182
+        public String getContent() { return content; }
183
+        public void setContent(String content) { this.content = content; }
184
+
185
+        public BigDecimal getScore() { return score; }
186
+        public void setScore(BigDecimal score) { this.score = score; }
187
+
188
+        public Date getDate() { return date; }
189
+        public void setDate(Date date) { this.date = date; }
190
+    }
191
+}

+ 26 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/service/IEmployeePortraitService.java

@@ -0,0 +1,26 @@
1
+package com.sundot.airport.ledger.service;
2
+
3
+import com.sundot.airport.ledger.domain.vo.EmployeePortraitVO;
4
+
5
+import java.util.List;
6
+import java.util.Map;
7
+
8
+/**
9
+ * 员工画像服务
10
+ */
11
+public interface IEmployeePortraitService {
12
+
13
+    /**
14
+     * 按姓名模糊搜索员工(用于前端下拉联想)
15
+     */
16
+    List<Map<String, Object>> searchUsers(String keyword);
17
+
18
+    /**
19
+     * 获取指定员工的完整画像数据
20
+     *
21
+     * @param personName 员工姓名(精确)
22
+     * @param beginTime  起始时间 yyyy-MM-dd,null 则不限
23
+     * @param endTime    结束时间 yyyy-MM-dd,null 则不限
24
+     */
25
+    EmployeePortraitVO getPortrait(String personName, String beginTime, String endTime);
26
+}

+ 314 - 0
airport-ledger/src/main/java/com/sundot/airport/ledger/service/impl/EmployeePortraitServiceImpl.java

@@ -0,0 +1,314 @@
1
+package com.sundot.airport.ledger.service.impl;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
4
+import com.sundot.airport.common.core.domain.entity.SysDept;
5
+import com.sundot.airport.common.core.domain.entity.SysUser;
6
+import com.sundot.airport.ledger.domain.LedgerBannerLetter;
7
+import com.sundot.airport.ledger.domain.LedgerExamScore;
8
+import com.sundot.airport.ledger.domain.LedgerTerminalBonus;
9
+import com.sundot.airport.ledger.domain.ScoreDimension;
10
+import com.sundot.airport.ledger.domain.ScoreEvent;
11
+import com.sundot.airport.ledger.domain.vo.EmployeePortraitVO;
12
+import com.sundot.airport.ledger.mapper.LedgerBannerLetterMapper;
13
+import com.sundot.airport.ledger.mapper.LedgerExamScoreMapper;
14
+import com.sundot.airport.ledger.mapper.LedgerTerminalBonusMapper;
15
+import com.sundot.airport.ledger.mapper.ScoreDimensionMapper;
16
+import com.sundot.airport.ledger.mapper.ScoreEventMapper;
17
+import com.sundot.airport.ledger.service.IEmployeePortraitService;
18
+import com.sundot.airport.system.mapper.SysDeptMapper;
19
+import com.sundot.airport.system.mapper.SysUserMapper;
20
+import org.springframework.beans.factory.annotation.Autowired;
21
+import org.springframework.stereotype.Service;
22
+
23
+import java.math.BigDecimal;
24
+import java.math.RoundingMode;
25
+import java.util.*;
26
+import java.util.stream.Collectors;
27
+
28
+@Service
29
+public class EmployeePortraitServiceImpl implements IEmployeePortraitService {
30
+
31
+    @Autowired
32
+    private SysUserMapper sysUserMapper;
33
+
34
+    @Autowired
35
+    private SysDeptMapper sysDeptMapper;
36
+
37
+    @Autowired
38
+    private ScoreEventMapper scoreEventMapper;
39
+
40
+    @Autowired
41
+    private ScoreDimensionMapper scoreDimensionMapper;
42
+
43
+    @Autowired
44
+    private LedgerExamScoreMapper ledgerExamScoreMapper;
45
+
46
+    @Autowired
47
+    private LedgerBannerLetterMapper ledgerBannerLetterMapper;
48
+
49
+    @Autowired
50
+    private LedgerTerminalBonusMapper ledgerTerminalBonusMapper;
51
+
52
+    // ── 搜索员工 ─────────────────────────────────────────────────────────────
53
+
54
+    @Override
55
+    public List<Map<String, Object>> searchUsers(String keyword) {
56
+        SysUser query = new SysUser();
57
+        query.setNickName(keyword);
58
+        List<SysUser> users = sysUserMapper.selectUserList(query);
59
+        if (users == null) return Collections.emptyList();
60
+        return users.stream()
61
+                .filter(u -> "0".equals(u.getStatus()) && "0".equals(u.getDelFlag()))
62
+                .limit(20)
63
+                .map(u -> {
64
+                    Map<String, Object> m = new LinkedHashMap<>();
65
+                    m.put("userId", u.getUserId());
66
+                    m.put("nickName", u.getNickName());
67
+                    m.put("deptName", u.getDept() != null ? u.getDept().getDeptName() : "");
68
+                    return m;
69
+                })
70
+                .collect(Collectors.toList());
71
+    }
72
+
73
+    // ── 获取员工完整画像 ──────────────────────────────────────────────────────
74
+
75
+    @Override
76
+    public EmployeePortraitVO getPortrait(String personName, String beginTime, String endTime) {
77
+        EmployeePortraitVO vo = new EmployeePortraitVO();
78
+        vo.setPersonName(personName);
79
+
80
+        // 1. 用户基本信息
81
+        fillUserInfo(vo, personName);
82
+
83
+        // 2. 六维度评分(从 score_event 聚合)
84
+        fillScores(vo, personName, beginTime, endTime);
85
+
86
+        // 3. 最新考试成绩
87
+        fillExamScore(vo, personName);
88
+
89
+        // 4. 获奖记录(锦旗/感谢信 + 航站楼加分)
90
+        fillAwards(vo, personName);
91
+
92
+        return vo;
93
+    }
94
+
95
+    // ── 用户基本信息 ─────────────────────────────────────────────────────────
96
+
97
+    private void fillUserInfo(EmployeePortraitVO vo, String personName) {
98
+        SysUser query = new SysUser();
99
+        query.setNickName(personName);
100
+        List<SysUser> users = sysUserMapper.selectUserList(query);
101
+        if (users == null || users.isEmpty()) return;
102
+
103
+        SysUser user = users.stream()
104
+                .filter(u -> personName.equals(u.getNickName()))
105
+                .findFirst()
106
+                .orElse(users.get(0));
107
+
108
+        vo.setUserId(user.getUserId());
109
+        vo.setAvatar(user.getAvatar());
110
+        vo.setPhonenumber(user.getPhonenumber());
111
+        vo.setSex(user.getSex());
112
+        vo.setSexText("0".equals(user.getSex()) ? "男" : "1".equals(user.getSex()) ? "女" : "");
113
+        vo.setSchooling(user.getSchooling());
114
+        vo.setQualificationLevelText(decodeQualLevel(user.getQualificationLevel()));
115
+        vo.setPoliticalStatusText(decodePolitical(user.getPoliticalStatus()));
116
+        vo.setCharacterCharacteristics(decodeChar(user.getCharacterCharacteristics()));
117
+        vo.setWorkingStyle(decodeWorkStyle(user.getWorkingStyle()));
118
+        vo.setStartWorkingDate(user.getStartWorkingDate());
119
+        vo.setSecurityCheckStartDate(user.getSecurityCheckStartDate());
120
+        vo.setSecurityInspectionPosition(user.getSecurityInspectionPosition());
121
+
122
+        // 司龄、开机年限
123
+        long nowMs = System.currentTimeMillis();
124
+        if (user.getStartWorkingDate() != null) {
125
+            vo.setWorkYears((int) ((nowMs - user.getStartWorkingDate().getTime()) / (365L * 24 * 3600 * 1000)));
126
+        }
127
+        if (user.getSecurityCheckStartDate() != null) {
128
+            vo.setSecurityCheckYears((int) ((nowMs - user.getSecurityCheckStartDate().getTime()) / (365L * 24 * 3600 * 1000)));
129
+        }
130
+
131
+        // 部门路径
132
+        if (user.getDept() != null) {
133
+            String deptName = user.getDept().getDeptName();
134
+            String parentDeptName = "";
135
+            Long parentId = user.getDept().getParentId();
136
+            if (parentId != null && parentId > 0) {
137
+                SysDept parent = sysDeptMapper.selectDeptById(parentId);
138
+                if (parent != null) parentDeptName = parent.getDeptName();
139
+            }
140
+            vo.setDeptName(deptName);
141
+            vo.setParentDeptName(parentDeptName);
142
+            vo.setDeptPath(parentDeptName.isEmpty() ? deptName : parentDeptName + "/" + deptName);
143
+        }
144
+    }
145
+
146
+    // ── 六维度评分 ────────────────────────────────────────────────────────────
147
+
148
+    private void fillScores(EmployeePortraitVO vo, String personName, String beginTime, String endTime) {
149
+        // 加载全部事件,按姓名 LIKE 过滤(支持逗号分隔多人)
150
+        ScoreEvent eventQuery = new ScoreEvent();
151
+        eventQuery.setPersonName(personName);
152
+        if (beginTime != null && !beginTime.isEmpty()) {
153
+            eventQuery.getParams().put("beginTime", beginTime);
154
+        }
155
+        if (endTime != null && !endTime.isEmpty()) {
156
+            eventQuery.getParams().put("endTime", endTime);
157
+        }
158
+        List<ScoreEvent> events = scoreEventMapper.selectList(eventQuery);
159
+
160
+        // 精确匹配(防止逗号多人字段的误匹配)
161
+        Map<Long, BigDecimal> dimMap = new HashMap<>();
162
+        for (ScoreEvent e : events) {
163
+            Long dimId = e.getDimensionId();
164
+            if (dimId == null) continue;
165
+            String raw = e.getPersonName();
166
+            if (raw == null) continue;
167
+            boolean matched = false;
168
+            for (String n : raw.split("[,,]")) {
169
+                if (personName.equals(n.trim())) { matched = true; break; }
170
+            }
171
+            if (!matched) continue;
172
+            BigDecimal val = e.getTotalScore() != null ? e.getTotalScore() : BigDecimal.ZERO;
173
+            dimMap.merge(dimId, val, BigDecimal::add);
174
+        }
175
+
176
+        // 加载维度定义
177
+        ScoreDimension dq = new ScoreDimension();
178
+        dq.setStatus("0");
179
+        List<ScoreDimension> dims = scoreDimensionMapper.selectList(dq);
180
+        dims.sort(Comparator.comparing(d -> d.getSortOrder() == null ? 999 : d.getSortOrder()));
181
+
182
+        List<EmployeePortraitVO.DimScoreItem> items = new ArrayList<>();
183
+        BigDecimal total = BigDecimal.ZERO;
184
+
185
+        for (ScoreDimension dim : dims) {
186
+            BigDecimal eventScore = dimMap.getOrDefault(dim.getId(), BigDecimal.ZERO);
187
+            BigDecimal base = dim.getBaseScore() != null ? dim.getBaseScore() : BigDecimal.valueOf(80);
188
+            BigDecimal dimScore = base.add(eventScore);
189
+            BigDecimal weight = dim.getWeight() != null ? dim.getWeight() : BigDecimal.ZERO;
190
+            BigDecimal contribution = dimScore.multiply(weight)
191
+                    .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
192
+            total = total.add(contribution);
193
+
194
+            EmployeePortraitVO.DimScoreItem item = new EmployeePortraitVO.DimScoreItem();
195
+            item.setName(dim.getName());
196
+            item.setScore(dimScore.setScale(1, RoundingMode.HALF_UP));
197
+            item.setWeight(weight);
198
+            item.setBaseScore(base);
199
+            item.setEventScore(eventScore.setScale(2, RoundingMode.HALF_UP));
200
+            items.add(item);
201
+        }
202
+
203
+        vo.setDimensions(items);
204
+        vo.setTotalScore(total.setScale(1, RoundingMode.HALF_UP));
205
+    }
206
+
207
+    // ── 考试成绩 ──────────────────────────────────────────────────────────────
208
+
209
+    private void fillExamScore(EmployeePortraitVO vo, String personName) {
210
+        QueryWrapper<LedgerExamScore> qw = new QueryWrapper<>();
211
+        qw.like("person_name", personName)
212
+          .eq("del_flag", "0")
213
+          .orderByDesc("exam_date")
214
+          .last("LIMIT 1");
215
+        List<LedgerExamScore> list = ledgerExamScoreMapper.selectList(qw);
216
+        if (list == null || list.isEmpty()) return;
217
+        LedgerExamScore exam = list.get(0);
218
+        vo.setTheoryScore(exam.getTheoryScore());
219
+        vo.setImageScore(exam.getImageScore());
220
+        vo.setExamDate(exam.getExamDate());
221
+        vo.setExamCategory(exam.getExamCategory());
222
+        vo.setExamPeriod(exam.getExamPeriod());
223
+    }
224
+
225
+    // ── 获奖记录 ──────────────────────────────────────────────────────────────
226
+
227
+    private void fillAwards(EmployeePortraitVO vo, String personName) {
228
+        List<EmployeePortraitVO.AwardRecord> awards = new ArrayList<>();
229
+
230
+        // 锦旗及感谢信
231
+        QueryWrapper<LedgerBannerLetter> bq = new QueryWrapper<>();
232
+        bq.like("person_name", personName).eq("del_flag", "0").orderByDesc("record_date");
233
+        for (LedgerBannerLetter b : ledgerBannerLetterMapper.selectList(bq)) {
234
+            EmployeePortraitVO.AwardRecord ar = new EmployeePortraitVO.AwardRecord();
235
+            ar.setType("1".equals(b.getType()) ? "锦旗" : "感谢信");
236
+            ar.setContent(b.getContentDesc());
237
+            ar.setScore(b.getAddScore());
238
+            ar.setDate(b.getRecordDate());
239
+            awards.add(ar);
240
+        }
241
+
242
+        // 航站楼加分
243
+        QueryWrapper<LedgerTerminalBonus> tq = new QueryWrapper<>();
244
+        tq.like("person_name", personName).eq("del_flag", "0").orderByDesc("approve_date");
245
+        for (LedgerTerminalBonus t : ledgerTerminalBonusMapper.selectList(tq)) {
246
+            EmployeePortraitVO.AwardRecord ar = new EmployeePortraitVO.AwardRecord();
247
+            ar.setType("航站楼加分");
248
+            ar.setContent(t.getBonusType());
249
+            ar.setScore(t.getAddScore());
250
+            ar.setDate(t.getApproveDate());
251
+            awards.add(ar);
252
+        }
253
+
254
+        // 按日期降序
255
+        awards.sort((a, b) -> {
256
+            if (a.getDate() == null) return 1;
257
+            if (b.getDate() == null) return -1;
258
+            return b.getDate().compareTo(a.getDate());
259
+        });
260
+
261
+        vo.setAwards(awards);
262
+    }
263
+
264
+    // ── 枚举解码工具 ─────────────────────────────────────────────────────────
265
+
266
+    private static String decodeQualLevel(String v) {
267
+        if (v == null) return "";
268
+        switch (v) {
269
+            case "LEVEL_ONE":   return "一级";
270
+            case "LEVEL_TWO":   return "二级";
271
+            case "LEVEL_THREE": return "三级";
272
+            case "LEVEL_FOUR":  return "四级";
273
+            case "LEVEL_FIVE":  return "五级";
274
+            default:            return v;
275
+        }
276
+    }
277
+
278
+    private static String decodePolitical(String v) {
279
+        if (v == null) return "";
280
+        switch (v) {
281
+            case "COMMUNIST_PARTY_MEMBER":             return "中共党员";
282
+            case "PROBATIONARY_COMMUNIST_PARTY_MEMBER":return "中共预备党员";
283
+            case "COMMUNIST_YOUTH_LEAGUE_MEMBER":      return "共青团员";
284
+            case "MASS_PUBLIC":                        return "群众";
285
+            default:                                   return v;
286
+        }
287
+    }
288
+
289
+    private static String decodeChar(String v) {
290
+        if (v == null) return "";
291
+        // 值格式如 "ISTJ",映射为 "ISTJ(检查员型)"
292
+        Map<String, String> m = new LinkedHashMap<>();
293
+        m.put("ISTJ", "ISTJ(检查员型)"); m.put("ISFJ", "ISFJ(守护者型)");
294
+        m.put("INFJ", "INFJ(咨询师型)"); m.put("INTJ", "INTJ(战略家型)");
295
+        m.put("ISTP", "ISTP(手艺人型)"); m.put("ISFP", "ISFP(艺术家型)");
296
+        m.put("INFP", "INFP(调停者型)"); m.put("INTP", "INTP(逻辑学家型)");
297
+        m.put("ESTP", "ESTP(企业家型)"); m.put("ESFP", "ESFP(表演者型)");
298
+        m.put("ENFP", "ENFP(活动家型)"); m.put("ENTP", "ENTP(辩论家型)");
299
+        m.put("ESTJ", "ESTJ(总经理型)"); m.put("ESFJ", "ESFJ(执政官型)");
300
+        m.put("ENFJ", "ENFJ(教育家型)"); m.put("ENTJ", "ENTJ(指挥官型)");
301
+        return m.getOrDefault(v, v);
302
+    }
303
+
304
+    private static String decodeWorkStyle(String v) {
305
+        if (v == null) return "";
306
+        switch (v) {
307
+            case "D": return "D型(支配型)";
308
+            case "I": return "I型(影响型)";
309
+            case "S": return "S型(稳健型)";
310
+            case "C": return "C型(尽责型)";
311
+            default:  return v;
312
+        }
313
+    }
314
+}