diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java index 153c5532..1c4e13a4 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java @@ -335,7 +335,10 @@ public final class AihrPracticeDto { List dimensions, List latestRecords, List assignments, - List evidenceItems + List evidenceItems, + Integer humanAssessmentScore, + Integer finalAdoptedScore, + Integer calibrationCount ) { } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index ed215589..2b7fe9d4 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -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 latestRecords = mobileHistory(party, 5); List assignmentRows = assignments(party, 5); List 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 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) { } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java index f1174de2..3b61bba1 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java @@ -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); diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 1d8f1832..1d92e352 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -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 仍仅作训练参考,不直接生成绩效或晋升结论。 diff --git a/frontend/src/api/aihr/mobile-admin.ts b/frontend/src/api/aihr/mobile-admin.ts index fad5c7ce..cc18cf9f 100644 --- a/frontend/src/api/aihr/mobile-admin.ts +++ b/frontend/src/api/aihr/mobile-admin.ts @@ -95,6 +95,9 @@ export type PromotionEvidence = { latestRecords: ReviewRecord[]; assignments: PracticeAssignment[]; evidenceItems: string[]; + humanAssessmentScore?: number | null; + finalAdoptedScore?: number | null; + calibrationCount?: number; }; export type OrgSyncRequest = { diff --git a/frontend/src/views/competency/growth.vue b/frontend/src/views/competency/growth.vue index 0c8d3166..91984b94 100644 --- a/frontend/src/views/competency/growth.vue +++ b/frontend/src/views/competency/growth.vue @@ -92,6 +92,11 @@
  • {{ item }}
+
+ 人工校准分:{{ evidence?.humanAssessmentScore ?? '待采集' }} + 最终采用分:{{ evidence?.finalAdoptedScore ?? evidence?.assessmentScore ?? '-' }} + 校准样本:{{ evidence?.calibrationCount ?? 0 }} +
@@ -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; diff --git a/mobile-uni/src/pages/user/profile/index.vue b/mobile-uni/src/pages/user/profile/index.vue index e8ac6925..2e4b3dfe 100644 --- a/mobile-uni/src/pages/user/profile/index.vue +++ b/mobile-uni/src/pages/user/profile/index.vue @@ -101,7 +101,15 @@ {{ evidence.assessmentScore }} - 测评分 + AI测评分 + + + {{ evidence.humanAssessmentScore ?? '待采集' }} + 人工校准分 + + + {{ evidence.finalAdoptedScore ?? evidence.assessmentScore }} + 最终采用分 {{ evidence.trainingMinutes }} diff --git a/mobile-uni/src/types/api.ts b/mobile-uni/src/types/api.ts index ea9e4eb9..b7747fca 100644 --- a/mobile-uni/src/types/api.ts +++ b/mobile-uni/src/types/api.ts @@ -509,4 +509,7 @@ export interface PromotionEvidence { latestRecords: PracticeRecord[]; assignments: PracticeAssignment[]; evidenceItems: string[]; + humanAssessmentScore?: number | null; + finalAdoptedScore?: number | null; + calibrationCount?: number; }