feat(practice): complete five-position training operations
This commit is contained in:
+58
-2
@@ -3,10 +3,12 @@ package org.dromara.aihr.controller;
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.AnnotationResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.AnnotationSaveRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.AssignmentCancelRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.CalibrationRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.CalibrationResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.FinishRequest;
|
||||
@@ -14,12 +16,18 @@ import org.dromara.aihr.domain.AihrPracticeDto.FinishResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.HelpEventResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.HelpRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PrepCardResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentBatchRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentBatchResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RealtimeSdpRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RealtimeSdpResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RubricResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RubricAdminResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RubricEnabledRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RubricSaveRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeQuestionEnabledRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeQuestionResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeQuestionSaveRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ScenarioEnabledRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ScenarioResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ScenarioSaveRequest;
|
||||
@@ -66,8 +74,9 @@ public class AihrPracticeController {
|
||||
|
||||
@GetMapping("/scenarios")
|
||||
public R<List<ScenarioResponse>> scenarios(@RequestParam(required = false) String position,
|
||||
@RequestParam(required = false) String scenarioType) {
|
||||
return R.ok(practiceSeedService.scenarios(position, scenarioType));
|
||||
@RequestParam(required = false) String scenarioType,
|
||||
@RequestParam(required = false, defaultValue = "false") boolean includeDisabled) {
|
||||
return R.ok(practiceSeedService.scenarios(position, scenarioType, includeDisabled && currentOperatorCanManageContent()));
|
||||
}
|
||||
|
||||
@GetMapping("/scenarios/{id}")
|
||||
@@ -87,6 +96,49 @@ public class AihrPracticeController {
|
||||
return practiceSeedService.updateScenarioEnabled(id, request) ? R.ok() : R.fail("训练场景不存在");
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@GetMapping("/questions")
|
||||
public R<List<PracticeQuestionResponse>> questions(@RequestParam(required = false) String position,
|
||||
@RequestParam(required = false) String type,
|
||||
@RequestParam(required = false, defaultValue = "true") boolean includeDisabled) {
|
||||
return R.ok(practiceSeedService.questions(position, type, includeDisabled));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PostMapping("/questions")
|
||||
public R<PracticeQuestionResponse> saveQuestion(@RequestBody PracticeQuestionSaveRequest request) {
|
||||
return R.ok(practiceSeedService.saveQuestion(request));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PatchMapping("/questions/{code}/enabled")
|
||||
public R<Void> updateQuestionEnabled(@PathVariable String code, @RequestBody PracticeQuestionEnabledRequest request) {
|
||||
return practiceSeedService.updateQuestionEnabled(code, request) ? R.ok() : R.fail("训练题目不存在");
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@GetMapping("/assignments/manage")
|
||||
public R<List<PracticeAssignmentResponse>> manageAssignments(@RequestParam(required = false) String position,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String source,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false, defaultValue = "100") Integer limit) {
|
||||
return R.ok(practiceSeedService.manageAssignments(position, status, source, keyword, limit == null ? 100 : limit));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PostMapping("/assignments/manage/batch")
|
||||
public R<PracticeAssignmentBatchResponse> createManagedAssignments(@RequestBody PracticeAssignmentBatchRequest request) {
|
||||
return R.ok(practiceSeedService.createAssignments(request, ""));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PatchMapping("/assignments/manage/{id}/cancel")
|
||||
public R<Void> cancelAssignment(@PathVariable Long id, @RequestBody(required = false) AssignmentCancelRequest request) {
|
||||
return practiceSeedService.cancelAssignment(id, request == null ? null : request.reason())
|
||||
? R.ok() : R.fail("任务不存在、已完成或不能撤回");
|
||||
}
|
||||
|
||||
@GetMapping("/scenarios/{id}/prep-card")
|
||||
public R<PrepCardResponse> prepCard(@PathVariable String id) {
|
||||
return R.ok(practiceSeedService.prepCard(id));
|
||||
@@ -229,4 +281,8 @@ public class AihrPracticeController {
|
||||
}
|
||||
return loginUser.getUsername().trim();
|
||||
}
|
||||
|
||||
private static boolean currentOperatorCanManageContent() {
|
||||
return StpUtil.hasRoleOr(TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE);
|
||||
}
|
||||
}
|
||||
|
||||
+48
-2
@@ -18,6 +18,7 @@ public final class AihrPracticeDto {
|
||||
String customerText,
|
||||
Integer emotion,
|
||||
Integer trust,
|
||||
Integer roundCount,
|
||||
String customerAudioUrl,
|
||||
PersonaResponse persona
|
||||
) {
|
||||
@@ -44,10 +45,21 @@ public final class AihrPracticeDto {
|
||||
String strategy,
|
||||
String goal,
|
||||
Boolean enabled,
|
||||
RubricResponse rubric
|
||||
RubricResponse rubric,
|
||||
List<ScenarioRoundResponse> rounds,
|
||||
String contentVersion,
|
||||
String contentHash
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A fixed, auditable training beat. The product only supports a small
|
||||
* dialogue script today, so keeping the four runtime fields explicit is
|
||||
* safer than accepting arbitrary JSON from the operations console.
|
||||
*/
|
||||
public record ScenarioRoundResponse(String customer, String coachHint, Integer emotion, Integer trust) {
|
||||
}
|
||||
|
||||
public record ScenarioSaveRequest(
|
||||
String id,
|
||||
String name,
|
||||
@@ -65,7 +77,8 @@ public final class AihrPracticeDto {
|
||||
String successCriteria,
|
||||
String strategy,
|
||||
String goal,
|
||||
Boolean enabled
|
||||
Boolean enabled,
|
||||
List<ScenarioRoundResponse> rounds
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -104,6 +117,39 @@ public final class AihrPracticeDto {
|
||||
public record RubricEnabledRequest(Boolean enabled) {
|
||||
}
|
||||
|
||||
public record PracticeQuestionResponse(
|
||||
String code,
|
||||
String scenarioCode,
|
||||
String scenarioName,
|
||||
String positionCode,
|
||||
String type,
|
||||
String question,
|
||||
String referenceAnswer,
|
||||
String contentVersion,
|
||||
String contentHash,
|
||||
Boolean enabled,
|
||||
Integer sortOrder
|
||||
) {
|
||||
}
|
||||
|
||||
public record PracticeQuestionSaveRequest(
|
||||
String code,
|
||||
String scenarioCode,
|
||||
String positionCode,
|
||||
String type,
|
||||
String question,
|
||||
String referenceAnswer,
|
||||
Boolean enabled,
|
||||
Integer sortOrder
|
||||
) {
|
||||
}
|
||||
|
||||
public record PracticeQuestionEnabledRequest(Boolean enabled) {
|
||||
}
|
||||
|
||||
public record AssignmentCancelRequest(String reason) {
|
||||
}
|
||||
|
||||
public record PrepCardResponse(List<String> keyPoints, List<String> redLines, List<String> scripts, String promptVersion) {
|
||||
}
|
||||
|
||||
|
||||
+24
-1
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.DialogueResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RubricDimensionResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -162,9 +163,21 @@ public class AihrPracticeLlmService {
|
||||
* /finish 单 LLM 结构化评分:temperature=0 + 固定 prompt 版本 + 档位锚点 60/75/90。
|
||||
*/
|
||||
public Optional<PracticeScore> score(String scenarioName, String goal, String strategy, List<DialogueResponse> dialogue) {
|
||||
return score(scenarioName, goal, strategy, dialogue, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* The model still returns the stable five machine-readable metrics, while
|
||||
* operations can tune the descriptions and weights of those metrics per
|
||||
* scenario. This preserves analytics compatibility and makes the rubric
|
||||
* used for a session auditable by the caller.
|
||||
*/
|
||||
public Optional<PracticeScore> score(String scenarioName, String goal, String strategy, List<DialogueResponse> dialogue,
|
||||
List<RubricDimensionResponse> rubric) {
|
||||
if (dialogue.stream().noneMatch(turn -> "trainee".equals(turn.role()) && !turn.text().isBlank())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String rubricText = renderRubric(rubric);
|
||||
String system = """
|
||||
你是物业行业培训考官(prompt版本 %s)。根据对练记录给员工话术评分。
|
||||
评分锚点:60=及格线(有明显缺失);75=合格(覆盖主要要点);90=优秀(完整且超预期)。
|
||||
@@ -172,7 +185,8 @@ public class AihrPracticeLlmService {
|
||||
只输出一个 JSON 对象,不要输出任何其他文字、解释或代码块标记,字段如下:
|
||||
{"total":0-100,"taskCompletion":0-100,"compliance":0-100,"communication":0-100,"emotion":0-100,"responseTimeliness":0-100或-1,"marketing":0-100,"rewrite":"给员工的示范话术改写,80字内","summary":"一句话点评,60字内"}
|
||||
taskCompletion 评估问题是否真正被推进到可执行闭环;responseTimeliness 只有对话中存在明确响应时效证据时填写,否则填 -1。
|
||||
""".formatted(PROMPT_VERSION);
|
||||
本场评分规则:%s
|
||||
""".formatted(PROMPT_VERSION, rubricText);
|
||||
String user = """
|
||||
训练场景:%s
|
||||
训练目标:%s
|
||||
@@ -183,6 +197,15 @@ public class AihrPracticeLlmService {
|
||||
return modelService.tryChat(system, user, SCORE_TEMPERATURE).flatMap(this::parseScore);
|
||||
}
|
||||
|
||||
private String renderRubric(List<RubricDimensionResponse> rubric) {
|
||||
if (rubric == null || rubric.isEmpty()) {
|
||||
return "任务完成40%、话术规范25%、情绪20%、响应时效10%、增值转化5%。";
|
||||
}
|
||||
return rubric.stream()
|
||||
.map(item -> item.name() + Math.round((item.weight() == null ? 0D : item.weight()) * 100) + "%(" + item.description() + ")")
|
||||
.collect(java.util.stream.Collectors.joining(";"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日三题的语义评估。模型未配置、成本闸门关闭或结构化结果解析失败时由调用方回退规则评分。
|
||||
*/
|
||||
|
||||
+641
-58
File diff suppressed because it is too large
Load Diff
+42
@@ -158,6 +158,42 @@ public class AihrPracticeSeedServiceTest {
|
||||
assertEquals("截止日期格式应为YYYY-MM-DD", malformed.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canonicalPracticePositionUsesOneFivePositionVocabulary() {
|
||||
assertEquals("生活顾问", AihrPracticeSeedService.canonicalPracticePosition("客服管家"));
|
||||
assertEquals("保安", AihrPracticeSeedService.canonicalPracticePosition("秩序维护员"));
|
||||
assertEquals("保洁", AihrPracticeSeedService.canonicalPracticePosition("环境保洁"));
|
||||
assertEquals("保修", AihrPracticeSeedService.canonicalPracticePosition("工程维修"));
|
||||
assertEquals("客服", AihrPracticeSeedService.canonicalPracticePosition("前台客服"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dailyDrillSeedCarriesAStableContentSnapshot() {
|
||||
AihrPracticeSeedService.DailyDrillSeed drill = new AihrPracticeSeedService.DailyDrillSeed(
|
||||
"repair-leak-followup", "漏水报修跟进要先确认什么?", "确认点位、责任人和反馈时间。"
|
||||
);
|
||||
|
||||
assertEquals("daily-v1", drill.contentVersion());
|
||||
assertNotNull(drill.contentHash());
|
||||
assertEquals(64, drill.contentHash().length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentOperationsUseCatalogSelectionAndPersistRubricSnapshots() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java"));
|
||||
String controller = Files.readString(Path.of("src/main/java/org/dromara/aihr/controller/AihrPracticeController.java"));
|
||||
String migration = Files.readString(Path.of("../../script/sql/update/aihr_20260723_practice_content_operations_mysql8.sql"));
|
||||
|
||||
assertTrue(source.contains("dailyDrillsFromCatalog(today, dailyDrillPosition(extPartyId), extPartyId)"));
|
||||
assertTrue(source.contains("@Scheduled(cron = \"${aihr.practice.daily-drill-cron:0 5 0 * * *}\""));
|
||||
assertTrue(source.contains("rubric_snapshot_json"));
|
||||
assertTrue(source.contains("validateRubricDimensions"));
|
||||
assertTrue(controller.contains("@GetMapping(\"/questions\")"));
|
||||
assertTrue(controller.contains("@GetMapping(\"/assignments/manage\")"));
|
||||
assertTrue(migration.contains("rubric_snapshot_json"));
|
||||
assertTrue(migration.contains("repair-leak-followup"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void batchAssignmentUsesPerEmployeeTransactionsAndDatabaseIdempotency() throws Exception {
|
||||
String serviceSource = Files.readString(Path.of("src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java"));
|
||||
@@ -167,6 +203,11 @@ public class AihrPracticeSeedServiceTest {
|
||||
assertTrue(serviceSource.contains("() -> createAssignmentInternal(itemRequest, batchScenario, storedExtPartyId)"));
|
||||
assertTrue(serviceSource.contains("TeamScope scope = teamScope(supervisorExtPartyId)"));
|
||||
assertTrue(serviceSource.contains("AssignmentStorageIdentityResolution storageResolution = assignmentStorageIdentities(requestedExtPartyIds)"));
|
||||
assertTrue(serviceSource.contains("AssignmentPositionPolicy positionPolicy = assignmentPositionPolicy(scenarioId)"));
|
||||
assertTrue(serviceSource.contains("requireAssignmentTargetPosition(extPartyId, positionPolicy)"));
|
||||
assertTrue(serviceSource.contains("assignmentScenarioPosition(scenarioId)"));
|
||||
assertTrue(serviceSource.contains("与训练场景岗位("));
|
||||
assertTrue(serviceSource.contains("requireAssignmentTargetPosition(extPartyId, assignmentPositionPolicy("));
|
||||
assertTrue(serviceSource.contains("phone_owner_count"));
|
||||
assertTrue(serviceSource.contains("ambiguousExtPartyIds().contains"));
|
||||
assertTrue(serviceSource.contains("uk_aihr_assignment_request"));
|
||||
@@ -628,6 +669,7 @@ public class AihrPracticeSeedServiceTest {
|
||||
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, llm, null);
|
||||
var session = service.start(new StartRequest("employee-a", "complaint-water", "mobile", null));
|
||||
|
||||
assertEquals(2, session.roundCount());
|
||||
ServiceException skipped = assertThrows(ServiceException.class,
|
||||
() -> service.turn(new TurnRequest(session.sessionId(), 2, "我先确认现场情况", null, null, false, null), "employee-a"));
|
||||
assertEquals("训练回合已变化,请查看最新对话后再继续", skipped.getMessage());
|
||||
|
||||
@@ -41,6 +41,9 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_session` (
|
||||
`dim_emotion` int DEFAULT NULL COMMENT '情绪分',
|
||||
`dim_communication` int DEFAULT NULL COMMENT '沟通分',
|
||||
`dim_marketing` int DEFAULT NULL COMMENT '营销分',
|
||||
`rubric_code` varchar(100) DEFAULT NULL COMMENT '训练开始时评分规则编码',
|
||||
`rubric_version` varchar(30) DEFAULT NULL COMMENT '训练开始时评分规则版本',
|
||||
`rubric_snapshot_json` text DEFAULT NULL COMMENT '训练开始时评分规则快照',
|
||||
`mentor_rewrite` varchar(1000) DEFAULT NULL COMMENT '导师改写',
|
||||
`ai_comment` varchar(1000) DEFAULT NULL COMMENT 'AI点评',
|
||||
`review_advice` varchar(1000) DEFAULT NULL COMMENT '主管复盘建议',
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
-- 训练内容运营闭环:会话评分规则快照、五岗位 Rubric 补齐、旧每日任务场景修正。
|
||||
-- 依赖:aihr_20260717_practice_five_position_catalog_mysql8.sql 已执行。
|
||||
-- 可重复执行;仅补结构和内置规则,不生成员工业务任务。
|
||||
|
||||
SET @aihr_schema = DATABASE();
|
||||
|
||||
SET @rubric_code_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = @aihr_schema AND table_name = 'aihr_practice_session' AND column_name = 'rubric_code'
|
||||
);
|
||||
SET @rubric_code_ddl = IF(@rubric_code_exists = 0,
|
||||
'ALTER TABLE aihr_practice_session ADD COLUMN rubric_code varchar(100) DEFAULT NULL COMMENT ''训练开始时评分规则编码'' AFTER dim_marketing',
|
||||
'SELECT 1');
|
||||
PREPARE aihr_rubric_code_stmt FROM @rubric_code_ddl;
|
||||
EXECUTE aihr_rubric_code_stmt;
|
||||
DEALLOCATE PREPARE aihr_rubric_code_stmt;
|
||||
|
||||
SET @rubric_version_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = @aihr_schema AND table_name = 'aihr_practice_session' AND column_name = 'rubric_version'
|
||||
);
|
||||
SET @rubric_version_ddl = IF(@rubric_version_exists = 0,
|
||||
'ALTER TABLE aihr_practice_session ADD COLUMN rubric_version varchar(30) DEFAULT NULL COMMENT ''训练开始时评分规则版本'' AFTER rubric_code',
|
||||
'SELECT 1');
|
||||
PREPARE aihr_rubric_version_stmt FROM @rubric_version_ddl;
|
||||
EXECUTE aihr_rubric_version_stmt;
|
||||
DEALLOCATE PREPARE aihr_rubric_version_stmt;
|
||||
|
||||
SET @rubric_snapshot_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = @aihr_schema AND table_name = 'aihr_practice_session' AND column_name = 'rubric_snapshot_json'
|
||||
);
|
||||
SET @rubric_snapshot_ddl = IF(@rubric_snapshot_exists = 0,
|
||||
'ALTER TABLE aihr_practice_session ADD COLUMN rubric_snapshot_json text DEFAULT NULL COMMENT ''训练开始时评分规则快照'' AFTER rubric_version',
|
||||
'SELECT 1');
|
||||
PREPARE aihr_rubric_snapshot_stmt FROM @rubric_snapshot_ddl;
|
||||
EXECUTE aihr_rubric_snapshot_stmt;
|
||||
DEALLOCATE PREPARE aihr_rubric_snapshot_stmt;
|
||||
|
||||
-- 对五岗位目录中没有 Rubric 的场景创建初始 v1.0 规则;已有版本不改写。
|
||||
INSERT INTO aihr_practice_rubric
|
||||
(tenant_id, rubric_code, scenario_code, rubric_name, version, enabled, description, create_time, update_time)
|
||||
SELECT s.tenant_id, CONCAT('rubric-', s.scenario_code), s.scenario_code,
|
||||
CONCAT(s.position, '基础评分规则'), 'v1.0', 1,
|
||||
'五岗位内容目录补齐的初始版本;后续由训练运营维护。', NOW(), NOW()
|
||||
FROM aihr_practice_scenario s
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM aihr_practice_rubric r
|
||||
WHERE r.tenant_id = s.tenant_id AND r.scenario_code = s.scenario_code AND r.enabled = 1
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE update_time = VALUES(update_time);
|
||||
|
||||
-- 一个场景仅保留最新的已启用版本,避免运行时随机取到历史 Rubric。
|
||||
UPDATE aihr_practice_rubric r
|
||||
JOIN (
|
||||
SELECT tenant_id, scenario_code, MAX(id) AS keep_id
|
||||
FROM aihr_practice_rubric
|
||||
WHERE enabled = 1
|
||||
GROUP BY tenant_id, scenario_code
|
||||
) current_rule
|
||||
ON current_rule.tenant_id = r.tenant_id AND current_rule.scenario_code = r.scenario_code
|
||||
SET r.enabled = CASE WHEN r.id = current_rule.keep_id THEN 1 ELSE 0 END,
|
||||
r.update_time = NOW()
|
||||
WHERE r.enabled = 1;
|
||||
|
||||
-- 每个生效 Rubric 具备稳定的五个机器可评分维度,前端只能调整名称、描述和权重。
|
||||
INSERT IGNORE INTO aihr_practice_rubric_dimension
|
||||
(tenant_id, rubric_code, dimension_code, dimension_name, weight, description, sort_order, create_time, update_time)
|
||||
SELECT r.tenant_id, r.rubric_code, d.dimension_code, d.dimension_name, d.weight, d.description, d.sort_order, NOW(), NOW()
|
||||
FROM aihr_practice_rubric r
|
||||
JOIN (
|
||||
SELECT 'task_completion' AS dimension_code, '任务完成度' AS dimension_name, 0.40 AS weight,
|
||||
'问题是否被推进到可执行闭环及 SOP 关键动作覆盖。' AS description, 10 AS sort_order
|
||||
UNION ALL SELECT 'standardization', '话术规范性', 0.25, '流程完整、合规表达和专业沟通。', 20
|
||||
UNION ALL SELECT 'emotion', '情绪管理能力', 0.20, '共情承接、情绪降级和信任恢复。', 30
|
||||
UNION ALL SELECT 'response_timeliness', '响应时效', 0.10, '阶段一仅按陪练回合响应时长统计。', 40
|
||||
UNION ALL SELECT 'marketing', '增值转化潜力', 0.05, '合规的服务引导和办理意识。', 50
|
||||
) d
|
||||
WHERE r.enabled = 1;
|
||||
|
||||
-- 旧硬编码每日题把 repair-leak 写成不存在编码;仅在目标场景已存在时修正历史未完成任务。
|
||||
UPDATE aihr_practice_assignment a
|
||||
JOIN aihr_practice_scenario s
|
||||
ON s.tenant_id = a.tenant_id AND s.scenario_code = 'repair-leak-followup'
|
||||
SET a.scenario_id = s.scenario_code,
|
||||
a.scenario_name = s.scenario_name,
|
||||
a.update_time = NOW()
|
||||
WHERE a.source = 'daily' AND a.scenario_id = 'repair-leak' AND a.status = '待训练';
|
||||
Reference in New Issue
Block a user