feat(aihr): align competency profile with brd dimensions

This commit is contained in:
2026-07-14 09:11:45 +08:00
parent 1b0f737ed7
commit ac8cddbbee
9 changed files with 61 additions and 12 deletions
@@ -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) {
@@ -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"})
+1 -1
View File
@@ -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 只作为未配置模型或外部接口异常时的降级,不作为真实测试通过证据。
+1
View File
@@ -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。本轮权限修复尚未执行生产同步、服务重启或业务数据写入,不能把本地提交视为线上已生效。
+2 -2
View File
@@ -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[];
+13 -1
View File
@@ -56,7 +56,13 @@
<strong>{{ item.label }}</strong>
<span>{{ item.note }}</span>
</div>
<el-progress :percentage="Number(item.value) || 0" :stroke-width="10" color="#e60012" />
<el-progress
v-if="item.value !== null && item.value !== undefined"
:percentage="Number(item.value) || 0"
:stroke-width="10"
color="#e60012"
/>
<span v-else class="dimension-unavailable">待采集</span>
</div>
</div>
</article>
@@ -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;
+8 -1
View File
@@ -70,7 +70,9 @@
<text class="dimension-title">{{ item.label }}</text>
<text class="dimension-note">{{ item.note }}</text>
</view>
<text class="dimension-score">{{ item.value }}</text>
<text class="dimension-score" :class="{ pending: item.value === null || item.value === undefined }">
{{ item.value === null || item.value === undefined ? '待采集' : item.value }}
</text>
</view>
</view>
<view v-if="profile.growthPath?.length" class="growth-section">
@@ -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;
+1 -1
View File
@@ -468,7 +468,7 @@ export interface InterviewFinishResponse {
export interface CompetencyDimension {
label: string;
value: number | string;
value: number | string | null;
note: string;
}
+2
View File
@@ -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<PromptTemplateResponse>> 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"