fix(aihr): expose reviewed promotion evidence scores

This commit is contained in:
2026-07-14 13:23:25 +08:00
parent b9cce62cf1
commit 1c0c47a757
8 changed files with 88 additions and 4 deletions
@@ -335,7 +335,10 @@ public final class AihrPracticeDto {
List<DimensionResponse> dimensions,
List<RecordResponse> latestRecords,
List<PracticeAssignmentResponse> assignments,
List<String> evidenceItems
List<String> evidenceItems,
Integer humanAssessmentScore,
Integer finalAdoptedScore,
Integer calibrationCount
) {
}
}
@@ -1605,11 +1605,14 @@ public class AihrPracticeSeedService {
public PromotionEvidenceResponse promotionEvidence(String extPartyId) {
String party = isBlank(extPartyId) ? "" : extPartyId.trim();
CompetencyResponse profile = competency(party);
EvidenceScoreSnapshot scores = evidenceScores(party);
List<RecordResponse> latestRecords = mobileHistory(party, 5);
List<PracticeAssignmentResponse> assignmentRows = assignments(party, 5);
List<String> evidenceItems = new ArrayList<>();
evidenceItems.add("累计完成 " + profile.completed() + " 次移动端AI陪练,训练时长约 " + profile.trainingMinutes() + " 分钟");
evidenceItems.add("AI测评分 " + profile.assessmentScore() + ",训练画像参考等级 " + profile.aiLevel() + "(需人工复核)");
evidenceItems.add("AI测评分 " + scores.aiScore() + ";人工校准分 "
+ (scores.humanScore() == null ? "未采集" : scores.humanScore())
+ ";最终采用分 " + scores.finalScore() + ";训练画像参考等级 " + profile.aiLevel() + "(需人工复核)");
evidenceItems.add("训练贡献度 " + profile.contribution() + ",待主管复盘 " + profile.pendingReview() + " 条");
if (!latestRecords.isEmpty()) {
RecordResponse latest = latestRecords.get(0);
@@ -1626,10 +1629,40 @@ public class AihrPracticeSeedService {
profile.dimensions(),
latestRecords,
assignmentRows,
evidenceItems
evidenceItems,
scores.humanScore(),
scores.finalScore(),
scores.calibrationCount()
);
}
private EvidenceScoreSnapshot evidenceScores(String extPartyId) {
ensureCalibrationTable();
String party = isBlank(extPartyId) ? "" : extPartyId.trim();
List<EvidenceScoreSnapshot> rows = jdbcTemplate.query("""
WITH latest_calibration AS (
SELECT tenant_id, session_id, corrected_score,
ROW_NUMBER() OVER (PARTITION BY tenant_id, session_id ORDER BY id DESC) AS row_num
FROM aihr_practice_calibration
WHERE tenant_id = ?
)
SELECT COALESCE(ROUND(AVG(s.total_score)), 0) AS ai_score,
CASE WHEN COUNT(lc.corrected_score) = 0 THEN NULL ELSE ROUND(AVG(lc.corrected_score)) END AS human_score,
COALESCE(ROUND(AVG(COALESCE(lc.corrected_score, s.total_score))), 0) AS final_score,
COUNT(lc.corrected_score) AS calibration_count
FROM aihr_practice_session s
LEFT JOIN latest_calibration lc
ON lc.tenant_id = s.tenant_id AND lc.session_id = s.session_id AND lc.row_num = 1
WHERE s.tenant_id = ? AND s.mode = 'mobile' AND (? = '' OR s.ext_party_id = ?)
""", (rs, rowNum) -> new EvidenceScoreSnapshot(
rs.getInt("ai_score"),
rs.getObject("human_score") == null ? null : rs.getInt("human_score"),
rs.getInt("final_score"),
rs.getInt("calibration_count")
), tenantId(), tenantId(), party, party);
return rows.isEmpty() ? new EvidenceScoreSnapshot(0, null, 0, 0) : rows.get(0);
}
private String aiLevel(int completed, int score) {
if (completed <= 0) {
return "未评级";
@@ -3814,6 +3847,9 @@ public class AihrPracticeSeedService {
private record CompetencySnapshot(Integer completed, Integer score, Integer pendingReview, Integer compliance, Integer communication, Integer emotion, Integer marketing, Integer incentivePoints) {
}
private record EvidenceScoreSnapshot(Integer aiScore, Integer humanScore, Integer finalScore, Integer calibrationCount) {
}
private record SessionAnnotationSource(String scenarioId, String dialogueJson, String annotationsJson) {
}
@@ -608,6 +608,19 @@ public class AihrPracticeSeedServiceTest {
assertTrue(profile.growthPath().stream().allMatch(item -> Integer.valueOf(0).equals(item.progress())));
}
@Test
public void promotionEvidenceIncludesHumanCalibrationAndFinalScore() throws Exception {
String source = Files.readString(Path.of("src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java"));
int methodStart = source.indexOf("public PromotionEvidenceResponse promotionEvidence");
int methodEnd = source.indexOf("private EvidenceScoreSnapshot evidenceScores", methodStart);
assertTrue(methodStart >= 0 && methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("scores.humanScore()"));
assertTrue(method.contains("scores.finalScore()"));
assertTrue(source.contains("ROW_NUMBER() OVER (PARTITION BY tenant_id, session_id ORDER BY id DESC)"));
assertTrue(source.contains("COALESCE(lc.corrected_score, s.total_score)"));
}
@Test
public void competencyGrowthProgressStopsAtWeakRequirement() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
+1
View File
@@ -260,3 +260,4 @@
- 2026-07-14 BRD G3 空身份路径复核:员工任务列表省略 `extPartyId` 时此前会进入空身份查询,普通系统用户可能看到当前租户任务集合;现与个人历史/画像共用同一身份归一化,APP 用户按登录手机号、HR/超级管理员按授权范围读取,普通系统用户直接拒绝;补回归断言,未改变数据库或生产数据。
- 2026-07-14 BRD 发布证据复核:当前 HEAD 的管理端、`mobile-uni` H5 和 `ruoyi-admin` jar 均已重新构建通过;只读线上根站与 `/h5/` 均返回 `200`,线上仍加载管理端 `index-CJZ3Ax3Z.js`、H5 `index-D4-NrEpb.js`,本地当前产物已刷新为管理端 `index-Cin-vnjm.js`、H5 `index-Byx3WtY1.js`。`RELEASE_VERIFY_REMOTE_MATCH=true` 的发布预检在产物新鲜度通过后,因两份用户未提交 Figma 文档停止;本轮未修改、暂存或提交这两份文档,也未执行生产同步、后端重启或业务数据写入。
- 2026-07-14 BRD 4.3.4 内容现状核对:本地开发租户当前启用生活顾问场景 `12` 条,但服务推介/增值场景 `0` 条、日常服务场景 `0` 条;已入库案例 `2` 条,知识附件处理数 `0` 条。严格预检继续把两类核心流程和正式内容数量作为事实门禁;本轮不自拟业务话术或伪造知识/案例数据,待内容负责人提供正式 SOP、成功条件和脱敏素材后再入库验收。
- 2026-07-14 BRD G1/4.5 证据包评分边界修复:成长/晋升证据包此前只展示 AI 测评分,人工校准结果仅存在于试点 CSV;现复用 `aihr_practice_calibration` 最新记录,向管理端和员工端同时返回人工校准分、最终采用分和校准样本数,并在导出证据包中保留三项字段。未校准时明确显示“待采集”,不把 AI 分伪装成人工结论;AI 仍仅作训练参考,不直接生成绩效或晋升结论。
+3
View File
@@ -95,6 +95,9 @@ export type PromotionEvidence = {
latestRecords: ReviewRecord[];
assignments: PracticeAssignment[];
evidenceItems: string[];
humanAssessmentScore?: number | null;
finalAdoptedScore?: number | null;
calibrationCount?: number;
};
export type OrgSyncRequest = {
+17
View File
@@ -92,6 +92,11 @@
<ul class="evidence-list">
<li v-for="item in evidence?.evidenceItems || []" :key="item">{{ item }}</li>
</ul>
<div class="evidence-score-row">
<span>人工校准分:{{ evidence?.humanAssessmentScore ?? '待采集' }}</span>
<span>最终采用分:{{ evidence?.finalAdoptedScore ?? evidence?.assessmentScore ?? '-' }}</span>
<span>校准样本:{{ evidence?.calibrationCount ?? 0 }}</span>
</div>
</article>
<article class="panel">
@@ -275,6 +280,9 @@ const exportEvidenceCsv = () => {
appendCsvRow(rows, 'summary', 'ext_party_id', party);
appendCsvRow(rows, 'summary', 'current_level', evidence.value.currentLevel);
appendCsvRow(rows, 'summary', 'assessment_score', evidence.value.assessmentScore);
appendCsvRow(rows, 'summary', 'human_assessment_score', evidence.value.humanAssessmentScore ?? '待采集');
appendCsvRow(rows, 'summary', 'final_adopted_score', evidence.value.finalAdoptedScore ?? evidence.value.assessmentScore);
appendCsvRow(rows, 'summary', 'calibration_count', evidence.value.calibrationCount ?? 0);
appendCsvRow(rows, 'summary', 'training_minutes', evidence.value.trainingMinutes);
appendCsvRow(rows, 'summary', 'contribution', evidence.value.contribution);
appendCsvRow(rows, 'summary', 'completed', evidence.value.completed);
@@ -447,6 +455,15 @@ onMounted(() => {
}
}
.evidence-score-row {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
margin-top: 14px;
color: #667085;
font-size: 13px;
}
.alert-item {
border-radius: 8px;
padding: 12px;
+9 -1
View File
@@ -101,7 +101,15 @@
</view>
<view class="evidence-metric">
<text>{{ evidence.assessmentScore }}</text>
<text>测评分</text>
<text>AI测评分</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.humanAssessmentScore ?? '待采集' }}</text>
<text>人工校准分</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.finalAdoptedScore ?? evidence.assessmentScore }}</text>
<text>最终采用分</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.trainingMinutes }}</text>
+3
View File
@@ -509,4 +509,7 @@ export interface PromotionEvidence {
latestRecords: PracticeRecord[];
assignments: PracticeAssignment[];
evidenceItems: string[];
humanAssessmentScore?: number | null;
finalAdoptedScore?: number | null;
calibrationCount?: number;
}