fix(aihr): align task labels and training duration
This commit is contained in:
+11
-5
@@ -1568,7 +1568,12 @@ public class AihrPracticeSeedService {
|
||||
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
|
||||
COALESCE(SUM(incentive_point), 0) incentive_points,
|
||||
COALESCE(ROUND(SUM(CASE
|
||||
WHEN started_time IS NOT NULL AND finished_time IS NOT NULL
|
||||
THEN GREATEST(TIMESTAMPDIFF(SECOND, started_time, finished_time), 0)
|
||||
ELSE 0
|
||||
END) / 60.0), 0) training_minutes
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND mode = 'mobile' AND (? = '' OR ext_party_id = ?)
|
||||
""", (rs, rowNum) -> new CompetencySnapshot(
|
||||
@@ -1579,10 +1584,11 @@ public class AihrPracticeSeedService {
|
||||
rs.getInt("communication"),
|
||||
rs.getInt("emotion"),
|
||||
rs.getInt("marketing"),
|
||||
rs.getInt("incentive_points")
|
||||
rs.getInt("incentive_points"),
|
||||
rs.getInt("training_minutes")
|
||||
), tenantId(), party, party);
|
||||
CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 0, 0, 0, 0, 0, 0, 0) : rows.get(0);
|
||||
int trainingMinutes = snapshot.completed() * 8;
|
||||
CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 0, 0, 0, 0, 0, 0, 0, 0) : rows.get(0);
|
||||
int trainingMinutes = snapshot.trainingMinutes();
|
||||
int contribution = Math.min(100, Math.max(0, snapshot.incentivePoints()));
|
||||
int assessmentScore = snapshot.score();
|
||||
String aiLevel = aiLevel(snapshot.completed(), assessmentScore);
|
||||
@@ -3894,7 +3900,7 @@ public class AihrPracticeSeedService {
|
||||
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 CompetencySnapshot(Integer completed, Integer score, Integer pendingReview, Integer compliance, Integer communication, Integer emotion, Integer marketing, Integer incentivePoints, Integer trainingMinutes) {
|
||||
}
|
||||
|
||||
private record EvidenceScoreSnapshot(Integer aiScore, Integer humanScore, Integer finalScore, Integer calibrationCount) {
|
||||
|
||||
+5
-2
@@ -677,7 +677,8 @@ public class AihrPracticeSeedServiceTest {
|
||||
"communication", "0",
|
||||
"emotion", "0",
|
||||
"marketing", "0",
|
||||
"incentive_points", "0"
|
||||
"incentive_points", "0",
|
||||
"training_minutes", "0"
|
||||
)));
|
||||
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
|
||||
|
||||
@@ -724,13 +725,15 @@ public class AihrPracticeSeedServiceTest {
|
||||
"communication", "80",
|
||||
"emotion", "78",
|
||||
"marketing", "74",
|
||||
"incentive_points", "20"
|
||||
"incentive_points", "20",
|
||||
"training_minutes", "42"
|
||||
)));
|
||||
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
|
||||
|
||||
CompetencyResponse profile = service.competency("13900001001");
|
||||
|
||||
assertEquals("初级", profile.aiLevel());
|
||||
assertEquals(42, profile.trainingMinutes());
|
||||
assertEquals(20, profile.contribution());
|
||||
assertEquals(100, profile.growthPath().get(0).progress());
|
||||
assertEquals("已达成", profile.growthPath().get(0).status());
|
||||
|
||||
@@ -277,3 +277,5 @@
|
||||
- 2026-07-14 BRD M1 派发场景状态与展示一致性复核:修复停用场景仍可通过直接派发接口进入待训练,以及客户端传入场景名称可与场景 ID 不一致的问题;主管派发现在复用启停校验,记录名称始终取服务端场景定义。历史任务读取不受影响,未修改生产数据。
|
||||
- 2026-07-14 BRD M1 场景筛选回退语义复核:场景表查询成功但岗位/类型没有启用场景时,旧逻辑仍回退到内置场景,可能把不匹配或已停用场景展示给员工/主管;现仅在数据库异常时保留 seed fallback,真实空结果保持为空,并补岗位筛选回归测试。未修改生产数据。
|
||||
- 2026-07-14 BRD M1 主管场景入口回退复核:后端已启用场景 API 优先,但主管指派页模板仍循环三条静态场景,且 API 正常空结果会继续显示静态选项;现改为循环 API 场景,真实空结果显示“暂无可派发的已启用场景”,只有请求失败才使用本地兜底。复盘后再练入口同步保持真实空结果为空,未修改生产数据。
|
||||
- 2026-07-14 BRD 4.4 任务来源展示补齐:首页“今日安排”和练习页“最近练过”此前只把非每日任务显示为场景名/“专项训练”,会遮蔽主管派发的错题再练与训练营来源;现统一显示“每日三题/错题再练/训练营/专项训练”标签。该修复只补来源可见性,不把 `source='camp'` 扩展为尚未确认的训练营计划、进度或完成规则。
|
||||
- 2026-07-14 BRD 4.5/B5 训练时长口径修复:能力画像此前用“完成次数 × 8 分钟”估算训练时长,无法作为真实时长证据;现复用 `started_time/finished_time` 按实际秒数聚合并换算分钟,无时间证据的历史记录按 0 计。未新增表字段;正式试点仍需核对时间来源、异常时长和课程学习时长是否纳入统一口径。
|
||||
|
||||
@@ -62,6 +62,9 @@
|
||||
<view v-for="item in recentPracticeItems" :key="item.id" class="assignment-row recent-row" :class="{ highlighted: item.assignment && highlightedAssignmentId === item.assignment.id }" @click="openRecentItem(item)">
|
||||
<view class="recent-icon" aria-hidden="true">{{ item.icon }}</view>
|
||||
<view class="assignment-copy">
|
||||
<text v-if="item.sourceLabel" class="assignment-badge" :class="item.assignment?.source || 'manual'">
|
||||
{{ item.sourceLabel }}
|
||||
</text>
|
||||
<text class="assignment-title">{{ item.title }}</text>
|
||||
<text class="assignment-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
@@ -271,6 +274,7 @@ type RecentPracticeItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
sourceLabel?: string;
|
||||
status: string;
|
||||
icon: string;
|
||||
assignment?: PracticeAssignment;
|
||||
@@ -334,6 +338,7 @@ const recentPracticeItems = computed<RecentPracticeItem[]>(() => {
|
||||
id: `assignment-${item.id}`,
|
||||
title: item.question || item.scenarioName || '专项训练',
|
||||
desc: item.status === '已完成' ? '已完成' : '待复盘',
|
||||
sourceLabel: assignmentSourceLabel(item.source),
|
||||
status: item.status,
|
||||
icon: item.scenarioName?.includes('催费') ? '⌕' : item.scenarioName?.includes('报修') ? '⌁' : '◉',
|
||||
assignment: item
|
||||
@@ -370,6 +375,12 @@ const assignmentDesc = (item: PracticeAssignment) => {
|
||||
return item.feedback || item.reason;
|
||||
};
|
||||
|
||||
const assignmentSourceLabel = (source?: string) => {
|
||||
if (source === 'retry') return '错题再练';
|
||||
if (source === 'camp') return '训练营';
|
||||
return '专项训练';
|
||||
};
|
||||
|
||||
const goLogin = () => {
|
||||
rememberLoginRedirect('/pages/user/practice/index');
|
||||
uni.navigateTo({ url: '/pages/auth/login/index' });
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
<uni-icons class="schedule-icon" type="compose" size="26" color="#2f6bff" />
|
||||
</view>
|
||||
<view class="schedule-copy">
|
||||
<text v-if="primaryAssignmentSourceLabel" class="schedule-source" :class="primaryAssignmentSourceClass">
|
||||
{{ primaryAssignmentSourceLabel }}
|
||||
</text>
|
||||
<text class="schedule-title">{{ primaryScheduleTitle }}</text>
|
||||
<view class="schedule-meta">
|
||||
<text class="meta-item"><uni-icons type="calendar" size="15" color="#8a8d93" /> {{ primaryScheduleTime }}</text>
|
||||
@@ -247,6 +250,21 @@ const primaryScheduleTitle = computed(() => {
|
||||
return '';
|
||||
});
|
||||
|
||||
const assignmentSourceLabel = (source?: string) => {
|
||||
if (source === 'daily') return '每日三题';
|
||||
if (source === 'retry') return '错题再练';
|
||||
if (source === 'camp') return '训练营';
|
||||
return '专项训练';
|
||||
};
|
||||
|
||||
const primaryAssignmentSourceLabel = computed(() =>
|
||||
primaryAssignment.value ? assignmentSourceLabel(primaryAssignment.value.source) : ''
|
||||
);
|
||||
|
||||
const primaryAssignmentSourceClass = computed(() =>
|
||||
primaryAssignment.value?.source || 'manual'
|
||||
);
|
||||
|
||||
const primaryScheduleDesc = computed(() => {
|
||||
const item = primaryAssignment.value;
|
||||
if (item) {
|
||||
@@ -861,6 +879,33 @@ onShow(() => {
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.schedule-source {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #edf3ff;
|
||||
color: #2f6bff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.schedule-source.retry {
|
||||
background: #fff1f2;
|
||||
color: #d51b2b;
|
||||
}
|
||||
|
||||
.schedule-source.camp {
|
||||
background: #fff7eb;
|
||||
color: #b86b00;
|
||||
}
|
||||
|
||||
.schedule-source.daily {
|
||||
background: #eef8f2;
|
||||
color: #258652;
|
||||
}
|
||||
|
||||
.schedule-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -119,6 +119,15 @@ test('员工首页高保真入口继续使用真实图标和业务跳转', async
|
||||
assert.match(source, /src="\/static\/mentor-hifi\.png"/);
|
||||
});
|
||||
|
||||
test('员工首页今日安排保留派发任务来源', async () => {
|
||||
const source = await pageSource('../src/pages/user/today/index.vue');
|
||||
|
||||
assert.match(source, /primaryAssignmentSourceLabel/);
|
||||
assert.match(source, /source === 'retry'[\s\S]*错题再练/);
|
||||
assert.match(source, /source === 'camp'[\s\S]*训练营/);
|
||||
assert.match(source, /class="schedule-source"/);
|
||||
});
|
||||
|
||||
test('问师傅页保留搜索框、答案区和总结卡入口', async () => {
|
||||
const source = await pageSource('../src/pages/user/sop/index.vue');
|
||||
|
||||
@@ -137,6 +146,15 @@ test('训练页保留任务卡、对话区、回复区和结果区', async () =>
|
||||
assert.match(source, /result-card/);
|
||||
});
|
||||
|
||||
test('训练页最近练过保留派发任务来源', async () => {
|
||||
const source = await pageSource('../src/pages/user/practice/index.vue');
|
||||
|
||||
assert.match(source, /sourceLabel: assignmentSourceLabel\(item\.source\)/);
|
||||
assert.match(source, /class="assignment-badge"/);
|
||||
assert.match(source, /source === 'retry'[\s\S]*错题再练/);
|
||||
assert.match(source, /source === 'camp'[\s\S]*训练营/);
|
||||
});
|
||||
|
||||
test('学习页展示真实错题本并可进入已有再练任务', async () => {
|
||||
const [source, practiceService] = await Promise.all([
|
||||
pageSource('../src/pages/user/learning/index.vue'),
|
||||
|
||||
Reference in New Issue
Block a user