From ac8cddbbeea8d910e1f35287c62eebc8a874f510 Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 09:11:45 +0800 Subject: [PATCH] feat(aihr): align competency profile with brd dimensions --- .../aihr/service/AihrPracticeSeedService.java | 26 +++++++++++++++---- .../service/AihrPracticeSeedServiceTest.java | 13 +++++++++- docs/API_INTEGRATION.md | 2 +- docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + frontend/src/api/aihr/mobile-admin.ts | 4 +-- frontend/src/views/competency/growth.vue | 14 +++++++++- mobile-uni/src/pages/user/profile/index.vue | 9 ++++++- mobile-uni/src/types/api.ts | 2 +- scripts/demo-check.sh | 2 ++ 9 files changed, 61 insertions(+), 12 deletions(-) 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 080639a7..6569ccab 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 @@ -1438,6 +1438,7 @@ public class AihrPracticeSeedService { COALESCE(ROUND(AVG(dim_compliance)), 0) compliance, COALESCE(ROUND(AVG(dim_communication)), 0) communication, COALESCE(ROUND(AVG(dim_emotion)), 0) emotion, + COALESCE(ROUND(AVG(dim_marketing)), 0) marketing, COALESCE(SUM(incentive_point), 0) incentive_points FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND (? = '' OR ext_party_id = ?) @@ -1448,9 +1449,10 @@ public class AihrPracticeSeedService { rs.getInt("compliance"), rs.getInt("communication"), rs.getInt("emotion"), + rs.getInt("marketing"), rs.getInt("incentive_points") ), tenantId(), party, party); - CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 0, 0, 0, 0, 0, 0) : rows.get(0); + CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 0, 0, 0, 0, 0, 0, 0) : rows.get(0); int trainingMinutes = snapshot.completed() * 8; int contribution = snapshot.completed() == 0 && snapshot.incentivePoints() == 0 ? 0 @@ -1466,9 +1468,16 @@ public class AihrPracticeSeedService { assessmentScore, aiLevel, List.of( - new DimensionResponse("合规", snapshot.compliance(), "SOP关键点覆盖"), - new DimensionResponse("沟通", snapshot.communication(), "承诺与表达清晰度"), - new DimensionResponse("情绪", snapshot.emotion(), "安抚与承接能力") + new DimensionResponse("任务完成度", null, + "权重40%;需接入工单闭环率、首解率与处理结果,当前待采集"), + new DimensionResponse("话术规范性", snapshot.completed() == 0 ? null : average(snapshot.compliance(), snapshot.communication()), + "权重25%;当前以合规+沟通训练维度代理,正式绩效口径需HR确认"), + new DimensionResponse("情绪管理能力", snapshot.completed() == 0 ? null : snapshot.emotion(), + "权重20%;当前以陪练情绪维度代理"), + new DimensionResponse("响应时效", null, + "权重10%;需接入首响时长、处理周期与跟进频率,当前待采集"), + new DimensionResponse("增值转化潜力", snapshot.completed() == 0 ? null : snapshot.marketing(), + "权重5%;当前以陪练营销维度代理,正式口径需HR确认") ), growthPath(snapshot.completed(), assessmentScore) ); @@ -3546,7 +3555,14 @@ public class AihrPracticeSeedService { private record RoundSeed(String customer, String coachHint, Integer emotion, Integer trust) { } - private record CompetencySnapshot(Integer completed, Integer score, Integer pendingReview, Integer compliance, Integer communication, Integer emotion, Integer incentivePoints) { + private Integer average(Integer left, Integer right) { + if (left == null || right == null) { + return null; + } + return Math.round((left + right) / 2.0f); + } + + private record CompetencySnapshot(Integer completed, Integer score, Integer pendingReview, Integer compliance, Integer communication, Integer emotion, Integer marketing, Integer incentivePoints) { } 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 85848fb4..2826942e 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 @@ -33,6 +33,7 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -464,6 +465,7 @@ public class AihrPracticeSeedServiceTest { "compliance", "0", "communication", "0", "emotion", "0", + "marketing", "0", "incentive_points", "0" ))); AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null); @@ -476,7 +478,10 @@ public class AihrPracticeSeedServiceTest { assertEquals(0, profile.trainingMinutes()); assertEquals(0, profile.contribution()); assertEquals("未评级", profile.aiLevel()); - assertTrue(profile.dimensions().stream().allMatch(item -> Integer.valueOf(0).equals(item.value()))); + assertEquals(5, profile.dimensions().size()); + assertTrue(profile.dimensions().stream().allMatch(item -> item.value() == null)); + assertEquals("任务完成度", profile.dimensions().get(0).label()); + assertEquals("响应时效", profile.dimensions().get(3).label()); assertTrue(profile.growthPath().stream().allMatch(item -> Integer.valueOf(0).equals(item.progress()))); } @@ -493,6 +498,7 @@ public class AihrPracticeSeedServiceTest { "compliance", "82", "communication", "80", "emotion", "78", + "marketing", "74", "incentive_points", "20" ))); AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null); @@ -506,6 +512,11 @@ public class AihrPracticeSeedServiceTest { assertEquals("待积累", profile.growthPath().get(1).status()); assertEquals(90, profile.growthPath().get(2).progress()); assertEquals("待积累", profile.growthPath().get(2).status()); + assertEquals(81, profile.dimensions().get(1).value()); + assertEquals(78, profile.dimensions().get(2).value()); + assertEquals(74, profile.dimensions().get(4).value()); + assertNull(profile.dimensions().get(0).value()); + assertNull(profile.dimensions().get(3).value()); } @SuppressWarnings({"unchecked", "rawtypes"}) diff --git a/docs/API_INTEGRATION.md b/docs/API_INTEGRATION.md index 5c350156..63bb2525 100644 --- a/docs/API_INTEGRATION.md +++ b/docs/API_INTEGRATION.md @@ -139,7 +139,7 @@ curl -fsS "$API_BASE/api/aihr/org/sync" \ - 模型配置 API 文件:`frontend/src/api/aihr/model.ts` - 模型配置页面:`frontend/src/views/system/model/index.vue` - 用户侧工程:`mobile-uni/`,路由为 `/h5/#/pages/user/today/index`、`/h5/#/pages/user/practice/index`、`/h5/#/pages/user/sop/index`、`/h5/#/pages/user/profile/index`、`/h5/#/pages/candidate/index/index`、`/h5/#/pages/supervisor/index/index`。未登录先走手机号短信登录,登录后 API 优先请求真实业务接口,不复用后台页面路由。语音输入不要新增移动端专用接口,浏览器录音和文件选择兜底都复用 `/api/ai/asr`。 -- 员工端训练:底部“练”进入 `/h5/#/pages/user/practice/index`,展示今日任务、每日三题、专项训练、训练前预习卡和训练对话;每日三题支持文字或录音转写,作答接口 `/api/aihr/mobile/practice/assignments/{id}/answer` 可携带 `answer/audioUrl/audioOssId`,语音地址会随作业记录保存;完成后记录写入 `aihr_practice_session`,员工端可提交 1-5 分训练体验和可选意见,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。 +- 员工端训练:底部“练”进入 `/h5/#/pages/user/practice/index`,展示今日任务、每日三题、专项训练、训练前预习卡和训练对话;每日三题支持文字或录音转写,作答接口 `/api/aihr/mobile/practice/assignments/{id}/answer` 可携带 `answer/audioUrl/audioOssId`,语音地址会随作业记录保存;完成后记录写入 `aihr_practice_session`,员工端可提交 1-5 分训练体验和可选意见,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。能力画像按 BRD 五维返回任务完成度、话术规范性、情绪管理能力、响应时效、增值转化潜力;暂无工单/时效数据的维度返回 `null`,前端显示“待采集”,不把训练次数当正式绩效指标。 - 候选人端:`/h5/#/pages/candidate/index/index` 的“开始面试/面试练习”复用 AI 面试 API;“补充资料”走候选资料上传接口,成功后展示资料类型和审核状态。管理端 `/recruit/interview` 底部展示“候选资料审核”,支持通过/驳回。 - 演示 fallback 只作为未配置模型或外部接口异常时的降级,不作为真实测试通过证据。 diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 5cfc0a80..a055a686 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -142,4 +142,5 @@ - 2026-07-14 G3 案例读取权限复核:发现案例列表和详情对任意已登录后台系统用户返回当前租户数据,存在普通后台账号横向读取案例资产的风险;现收紧为 `superadmin`/`hr_operator`,APP 用户保留登录后的项目范围读取。新增源码契约测试,AIHR 全量测试 `57/57`、脚本语法和空白检查通过,修复提交为 `b0d24970`,尚未发布生产。 - 2026-07-14 G3 驾驶舱权限复核:发现管理驾驶舱只要求登录,APP 用户或普通后台账号理论上可读取当前租户的组织、训练、知识库、案例和候选人汇总;现收紧为 `superadmin`/`hr_operator`,主管团队数据继续走移动端项目范围接口。新增源码契约测试,AIHR 全量测试 `58/58`、脚本语法和空白检查通过,修复提交为 `7522794d`,尚未发布生产。 - 2026-07-14 后续 BRD P1 复核:认证/晋升规则、岗位-SOP 资格 gating、方言样本、正式案例视频样片与组织项目范围仍依赖 HR/运维输入;当前不继续用硬编码、演示 seed 或新增页面伪造完成,下一批优先接正式规则与试点数据后再实现。 +- 2026-07-14 BRD 4.5.1 五维能力画像对齐:员工端与管理端画像现在统一返回“任务完成度、话术规范性、情绪管理能力、响应时效、增值转化潜力”五项;话术规范性由现有合规+沟通陪练分数作为训练代理,情绪和增值转化分别由陪练情绪/营销分代理。任务完成度、响应时效因尚未接入工单闭环、首解率、首响时长和处理周期,明确返回“待采集”,不再用训练次数或综合分伪造 BRD 绩效指标;正式权重和绩效口径仍需 HR 确认。 - 2026-07-14 只读发布预检:当前提交 `07557656` 工作区干净,本地管理端、`mobile-uni` H5 与后端 jar 均有产物;生产根站点与租户接口返回 200。本轮权限修复尚未执行生产同步、服务重启或业务数据写入,不能把本地提交视为线上已生效。 diff --git a/frontend/src/api/aihr/mobile-admin.ts b/frontend/src/api/aihr/mobile-admin.ts index cccb30a3..fad5c7ce 100644 --- a/frontend/src/api/aihr/mobile-admin.ts +++ b/frontend/src/api/aihr/mobile-admin.ts @@ -45,7 +45,7 @@ export type CompetencyProfile = { contribution: number; assessmentScore: number; aiLevel: string; - dimensions: Array<{ label: string; value: number | string; note: string }>; + dimensions: Array<{ label: string; value: number | string | null; note: string }>; growthPath: Array<{ level: string; progress: number; status: string; evidence: string }>; }; @@ -91,7 +91,7 @@ export type PromotionEvidence = { contribution: number; completed: number; pendingReview: number; - dimensions: Array<{ label: string; value: number | string; note: string }>; + dimensions: Array<{ label: string; value: number | string | null; note: string }>; latestRecords: ReviewRecord[]; assignments: PracticeAssignment[]; evidenceItems: string[]; diff --git a/frontend/src/views/competency/growth.vue b/frontend/src/views/competency/growth.vue index b5cb5bf6..cb30e1b8 100644 --- a/frontend/src/views/competency/growth.vue +++ b/frontend/src/views/competency/growth.vue @@ -56,7 +56,13 @@ {{ item.label }} {{ item.note }} - + + 待采集 @@ -422,6 +428,12 @@ onMounted(() => { color: #667085; } +.dimension-item .dimension-unavailable { + margin-bottom: 8px; + color: #98a2b3; + font-weight: 600; +} + .evidence-list { margin: 0; padding-left: 18px; diff --git a/mobile-uni/src/pages/user/profile/index.vue b/mobile-uni/src/pages/user/profile/index.vue index 1b01ed99..0190e82a 100644 --- a/mobile-uni/src/pages/user/profile/index.vue +++ b/mobile-uni/src/pages/user/profile/index.vue @@ -70,7 +70,9 @@ {{ item.label }} {{ item.note }} - {{ item.value }} + + {{ item.value === null || item.value === undefined ? '待采集' : item.value }} + @@ -594,6 +596,11 @@ onShow(() => { font-weight: 900; } +.dimension-score.pending { + color: var(--employee-muted); + font-size: 14px; +} + .refresh-button { min-height: 36px; padding: 0 12px; diff --git a/mobile-uni/src/types/api.ts b/mobile-uni/src/types/api.ts index 4cae38da..4af58c98 100644 --- a/mobile-uni/src/types/api.ts +++ b/mobile-uni/src/types/api.ts @@ -468,7 +468,7 @@ export interface InterviewFinishResponse { export interface CompetencyDimension { label: string; - value: number | string; + value: number | string | null; note: string; } diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 07bf4f73..450024b1 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -393,6 +393,8 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPromptTemplateController.java "/api/aihr/prompt-templates" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPromptTemplateController.java "public R> list" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "mobileCompletedCount" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "任务完成度" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "响应时效" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java "MistakeAggregateResponse" contains mobile-uni/src/pages/supervisor/team/index.vue "团队错题本" contains mobile-uni/src/types/api.ts "PracticeMistakeAggregate"