feat(aihr): align competency profile with brd dimensions
This commit is contained in:
+21
-5
@@ -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) {
|
||||
|
||||
+12
-1
@@ -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"})
|
||||
|
||||
@@ -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 只作为未配置模型或外部接口异常时的降级,不作为真实测试通过证据。
|
||||
|
||||
|
||||
@@ -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。本轮权限修复尚未执行生产同步、服务重启或业务数据写入,不能把本地提交视为线上已生效。
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -468,7 +468,7 @@ export interface InterviewFinishResponse {
|
||||
|
||||
export interface CompetencyDimension {
|
||||
label: string;
|
||||
value: number | string;
|
||||
value: number | string | null;
|
||||
note: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user