fix(aihr): align practice scoring with BRD five dimensions
This commit is contained in:
+2
@@ -161,6 +161,8 @@ public class AihrPracticeController {
|
|||||||
request == null ? null : request.correctedEmotion(),
|
request == null ? null : request.correctedEmotion(),
|
||||||
request == null ? null : request.correctedCommunication(),
|
request == null ? null : request.correctedCommunication(),
|
||||||
request == null ? null : request.correctedMarketing(),
|
request == null ? null : request.correctedMarketing(),
|
||||||
|
request == null ? null : request.correctedTaskCompletion(),
|
||||||
|
request == null ? null : request.correctedResponseTimeliness(),
|
||||||
request == null ? null : request.reason()
|
request == null ? null : request.reason()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -196,8 +196,16 @@ public final class AihrPracticeDto {
|
|||||||
Integer correctedEmotion,
|
Integer correctedEmotion,
|
||||||
Integer correctedCommunication,
|
Integer correctedCommunication,
|
||||||
Integer correctedMarketing,
|
Integer correctedMarketing,
|
||||||
|
Integer correctedTaskCompletion,
|
||||||
|
Integer correctedResponseTimeliness,
|
||||||
String reason
|
String reason
|
||||||
) {
|
) {
|
||||||
|
public CalibrationRequest(String reviewer, Integer correctedTotal, Integer correctedCompliance,
|
||||||
|
Integer correctedEmotion, Integer correctedCommunication, Integer correctedMarketing,
|
||||||
|
String reason) {
|
||||||
|
this(reviewer, correctedTotal, correctedCompliance, correctedEmotion, correctedCommunication,
|
||||||
|
correctedMarketing, null, null, reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record CalibrationResponse(Long id, String sessionId, Integer originalScore, Integer correctedScore, String reviewer, String reason, String status, String createTime) {
|
public record CalibrationResponse(Long id, String sessionId, Integer originalScore, Integer correctedScore, String reviewer, String reason, String status, String createTime) {
|
||||||
|
|||||||
+19
-1
@@ -33,9 +33,15 @@ public class AihrPracticeLlmService {
|
|||||||
int communication,
|
int communication,
|
||||||
int emotion,
|
int emotion,
|
||||||
int marketing,
|
int marketing,
|
||||||
|
int taskCompletion,
|
||||||
|
int responseTimeliness,
|
||||||
String rewrite,
|
String rewrite,
|
||||||
String summary
|
String summary
|
||||||
) {
|
) {
|
||||||
|
public PracticeScore(int total, int compliance, int communication, int emotion, int marketing,
|
||||||
|
String rewrite, String summary) {
|
||||||
|
this(total, compliance, communication, emotion, marketing, compliance, -1, rewrite, summary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PracticeTurn(
|
public record PracticeTurn(
|
||||||
@@ -140,7 +146,8 @@ public class AihrPracticeLlmService {
|
|||||||
评分锚点:60=及格线(有明显缺失);75=合格(覆盖主要要点);90=优秀(完整且超预期)。
|
评分锚点:60=及格线(有明显缺失);75=合格(覆盖主要要点);90=优秀(完整且超预期)。
|
||||||
约束:只根据"员工"实际说过的话评分,未提及的内容不得臆造加分。
|
约束:只根据"员工"实际说过的话评分,未提及的内容不得臆造加分。
|
||||||
只输出一个 JSON 对象,不要输出任何其他文字、解释或代码块标记,字段如下:
|
只输出一个 JSON 对象,不要输出任何其他文字、解释或代码块标记,字段如下:
|
||||||
{"total":0-100,"compliance":0-100,"communication":0-100,"emotion":0-100,"marketing":0-100,"rewrite":"给员工的示范话术改写,80字内","summary":"一句话点评,60字内"}
|
{"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);
|
""".formatted(PROMPT_VERSION);
|
||||||
String user = """
|
String user = """
|
||||||
训练场景:%s
|
训练场景:%s
|
||||||
@@ -166,6 +173,8 @@ public class AihrPracticeLlmService {
|
|||||||
dimension(root, "communication", total),
|
dimension(root, "communication", total),
|
||||||
dimension(root, "emotion", total),
|
dimension(root, "emotion", total),
|
||||||
dimension(root, "marketing", total),
|
dimension(root, "marketing", total),
|
||||||
|
dimension(root, "taskCompletion", dimension(root, "compliance", total)),
|
||||||
|
dimensionOrMissing(root, "responseTimeliness"),
|
||||||
truncateText(root.path("rewrite").asText(""), 400),
|
truncateText(root.path("rewrite").asText(""), 400),
|
||||||
truncateText(root.path("summary").asText(""), 300)
|
truncateText(root.path("summary").asText(""), 300)
|
||||||
));
|
));
|
||||||
@@ -204,6 +213,15 @@ public class AihrPracticeLlmService {
|
|||||||
return clampScore(node.asInt(fallback));
|
return clampScore(node.asInt(fallback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int dimensionOrMissing(JsonNode root, String field) {
|
||||||
|
JsonNode node = root.path(field);
|
||||||
|
if (!node.isNumber() && !node.isTextual()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int value = node.asInt(-1);
|
||||||
|
return value < 0 ? -1 : clampScore(value);
|
||||||
|
}
|
||||||
|
|
||||||
/** 落库列为 varchar(1000),模型输出截断兜底。 */
|
/** 落库列为 varchar(1000),模型输出截断兜底。 */
|
||||||
private static String truncateText(String value, int maxChars) {
|
private static String truncateText(String value, int maxChars) {
|
||||||
if (value == null || value.length() <= maxChars) {
|
if (value == null || value.length() <= maxChars) {
|
||||||
|
|||||||
+227
-56
@@ -90,6 +90,11 @@ public class AihrPracticeSeedService {
|
|||||||
"professional", "表达更专业,突出SOP依据、责任边界、留痕和闭环口径。"
|
"professional", "表达更专业,突出SOP依据、责任边界、留痕和闭环口径。"
|
||||||
);
|
);
|
||||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("MM-dd HH:mm");
|
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("MM-dd HH:mm");
|
||||||
|
// 阶段一只统计“员工收到业主话术后到提交下一轮”的陪练响应时长;正式首响/工单处理时效仍需业务系统数据。
|
||||||
|
private static final long RESPONSE_FAST_MS = 5_000L;
|
||||||
|
private static final long RESPONSE_ACCEPTABLE_MS = 10_000L;
|
||||||
|
private static final long RESPONSE_SLOW_MS = 20_000L;
|
||||||
|
private static final long RESPONSE_VERY_SLOW_MS = 30_000L;
|
||||||
private static final String PILOT_SCOPE_CTE = """
|
private static final String PILOT_SCOPE_CTE = """
|
||||||
WITH pilot_params AS (
|
WITH pilot_params AS (
|
||||||
SELECT ? AS tenant_id, ? AS start_time, ? AS end_time
|
SELECT ? AS tenant_id, ? AS start_time, ? AS end_time
|
||||||
@@ -489,7 +494,8 @@ public class AihrPracticeSeedService {
|
|||||||
String trainee = resolveTrainee(request, scenario);
|
String trainee = resolveTrainee(request, scenario);
|
||||||
List<String> customerLines = new ArrayList<>();
|
List<String> customerLines = new ArrayList<>();
|
||||||
customerLines.add(firstRound.customer());
|
customerLines.add(firstRound.customer());
|
||||||
activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), request == null ? null : request.assignmentId(), isMobile(request), LocalDateTime.now(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), customerLines, new ArrayList<>()));
|
long promptPresentedAt = System.currentTimeMillis();
|
||||||
|
activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), request == null ? null : request.assignmentId(), isMobile(request), LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(promptPresentedAt), java.time.ZoneId.systemDefault()), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), customerLines, new ArrayList<>(), new ArrayList<>(List.of(promptPresentedAt)), new ArrayList<>()));
|
||||||
return new StartResponse(
|
return new StartResponse(
|
||||||
sessionId,
|
sessionId,
|
||||||
scenario.id(),
|
scenario.id(),
|
||||||
@@ -518,6 +524,7 @@ public class AihrPracticeSeedService {
|
|||||||
if (request != null && Boolean.TRUE.equals(request.regenerate())) {
|
if (request != null && Boolean.TRUE.equals(request.regenerate())) {
|
||||||
return regenerateCoachHint(request, session, scenario, roundIndex, style);
|
return regenerateCoachHint(request, session, scenario, roundIndex, style);
|
||||||
}
|
}
|
||||||
|
rememberResponseLatency(session, roundIndex, System.currentTimeMillis());
|
||||||
rememberTraineeReply(request, roundIndex);
|
rememberTraineeReply(request, roundIndex);
|
||||||
RoundSeed currentRound = scenario.rounds().get(roundIndex);
|
RoundSeed currentRound = scenario.rounds().get(roundIndex);
|
||||||
int nextRoundIndex = roundIndex + 1;
|
int nextRoundIndex = roundIndex + 1;
|
||||||
@@ -529,6 +536,9 @@ public class AihrPracticeSeedService {
|
|||||||
boolean redFlag = localRedFlag || resolved.redFlag();
|
boolean redFlag = localRedFlag || resolved.redFlag();
|
||||||
String coachHint = redFlag ? redFlagCoachHint(resolved.coachHint()) : resolved.coachHint();
|
String coachHint = redFlag ? redFlagCoachHint(resolved.coachHint()) : resolved.coachHint();
|
||||||
rememberTurnEvidence(session, roundIndex, resolved.emotion(), resolved.trust(), redFlag, coachHint);
|
rememberTurnEvidence(session, roundIndex, resolved.emotion(), resolved.trust(), redFlag, coachHint);
|
||||||
|
if (!finished) {
|
||||||
|
rememberPromptPresentedAt(session, nextRoundIndex, System.currentTimeMillis());
|
||||||
|
}
|
||||||
return new TurnResponse(
|
return new TurnResponse(
|
||||||
resolved.customerText(),
|
resolved.customerText(),
|
||||||
"",
|
"",
|
||||||
@@ -681,25 +691,64 @@ public class AihrPracticeSeedService {
|
|||||||
* /finish 单次评分:配置了 chat 模型且学员有真实回复时走 LLM 结构化评分,否则用 seed 分。
|
* /finish 单次评分:配置了 chat 模型且学员有真实回复时走 LLM 结构化评分,否则用 seed 分。
|
||||||
*/
|
*/
|
||||||
private PracticeResult evaluate(ActiveSession activeSession, ScenarioSeed scenario) {
|
private PracticeResult evaluate(ActiveSession activeSession, ScenarioSeed scenario) {
|
||||||
PracticeResult seedResult = new PracticeResult(scenario.total(), scenario.scores(), scenario.rewrite(), scenario.summary());
|
PracticeResult seedResult = seedPracticeResult(activeSession, scenario);
|
||||||
if (activeSession == null) {
|
if (activeSession == null) {
|
||||||
return seedResult;
|
return seedResult;
|
||||||
}
|
}
|
||||||
return practiceLlmService.score(scenario.name(), scenario.goal(), scenario.strategy(), dialogueTurns(activeSession, scenario))
|
return practiceLlmService.score(scenario.name(), scenario.goal(), scenario.strategy(), dialogueTurns(activeSession, scenario))
|
||||||
.map(score -> new PracticeResult(
|
.map(score -> {
|
||||||
score.total(),
|
Integer measuredResponse = responseTimelinessScore(activeSession);
|
||||||
List.of(
|
Integer response = measuredResponse == null && score.responseTimeliness() >= 0
|
||||||
new DimensionResponse("合规", score.compliance(), "SOP关键点覆盖"),
|
? score.responseTimeliness() : measuredResponse;
|
||||||
new DimensionResponse("沟通", score.communication(), "承诺与表达清晰度"),
|
int standardization = average(score.compliance(), score.communication());
|
||||||
new DimensionResponse("情绪", score.emotion(), "安抚与承接能力"),
|
int total = response == null
|
||||||
new DimensionResponse("营销", score.marketing(), "增值转化意识")
|
? score.total()
|
||||||
),
|
: weightedPracticeScore(score.taskCompletion(), standardization, score.emotion(), response, score.marketing());
|
||||||
isBlank(score.rewrite()) ? scenario.rewrite() : score.rewrite(),
|
return new PracticeResult(
|
||||||
isBlank(score.summary()) ? scenario.summary() : score.summary()
|
total,
|
||||||
))
|
brdDimensions(score.taskCompletion(), standardization, score.emotion(), response, score.marketing()),
|
||||||
|
isBlank(score.rewrite()) ? scenario.rewrite() : score.rewrite(),
|
||||||
|
isBlank(score.summary()) ? scenario.summary() : score.summary(),
|
||||||
|
score.compliance(), score.communication(), score.taskCompletion(), response,
|
||||||
|
averageResponseLatencyMs(activeSession)
|
||||||
|
);
|
||||||
|
})
|
||||||
.orElse(seedResult);
|
.orElse(seedResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PracticeResult seedPracticeResult(ActiveSession activeSession, ScenarioSeed scenario) {
|
||||||
|
int compliance = scoreValue(scenario.scores(), "合规", scenario.total());
|
||||||
|
int communication = scoreValue(scenario.scores(), "沟通", scenario.total());
|
||||||
|
int emotion = scoreValue(scenario.scores(), "情绪", scenario.total());
|
||||||
|
int marketing = scoreValue(scenario.scores(), "营销", scenario.total());
|
||||||
|
Integer response = responseTimelinessScore(activeSession);
|
||||||
|
int total = response == null
|
||||||
|
? scenario.total()
|
||||||
|
: weightedPracticeScore(compliance, average(compliance, communication), emotion, response, marketing);
|
||||||
|
return new PracticeResult(
|
||||||
|
total,
|
||||||
|
brdDimensions(compliance, average(compliance, communication), emotion, response, marketing),
|
||||||
|
scenario.rewrite(), scenario.summary(), compliance, communication, compliance, response,
|
||||||
|
averageResponseLatencyMs(activeSession)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DimensionResponse> brdDimensions(Integer taskCompletion, Integer standardization, Integer emotion,
|
||||||
|
Integer responseTimeliness, Integer marketing) {
|
||||||
|
return List.of(
|
||||||
|
new DimensionResponse("任务完成度", taskCompletion, "权重40%;当前以SOP执行和问题推进结果代理"),
|
||||||
|
new DimensionResponse("话术规范性", standardization, "权重25%;合规与沟通训练维度综合"),
|
||||||
|
new DimensionResponse("情绪管理能力", emotion, "权重20%;共情承接与信任恢复"),
|
||||||
|
new DimensionResponse("响应时效", responseTimeliness == null ? "待采集" : responseTimeliness, "权重10%;阶段一按陪练回合响应时长统计"),
|
||||||
|
new DimensionResponse("增值转化潜力", marketing, "权重5%;增值引导与办理意识")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int weightedPracticeScore(int taskCompletion, int standardization, int emotion, int responseTimeliness, int marketing) {
|
||||||
|
return Math.round(taskCompletion * 0.40f + standardization * 0.25f + emotion * 0.20f
|
||||||
|
+ responseTimeliness * 0.10f + marketing * 0.05f);
|
||||||
|
}
|
||||||
|
|
||||||
public int mobileCompletedCount() {
|
public int mobileCompletedCount() {
|
||||||
return countMobileRecords("");
|
return countMobileRecords("");
|
||||||
}
|
}
|
||||||
@@ -1334,7 +1383,9 @@ public class AihrPracticeSeedService {
|
|||||||
}
|
}
|
||||||
List<ReviewDetailResponse> rows = jdbcTemplate.query("""
|
List<ReviewDetailResponse> rows = jdbcTemplate.query("""
|
||||||
SELECT id, session_id, finished_time, trainee_name, scenario_id, scenario_name, total_score, status,
|
SELECT id, session_id, finished_time, trainee_name, scenario_id, scenario_name, total_score, status,
|
||||||
summary, mentor_rewrite, ai_comment, review_advice, incentive_point, dim_compliance, dim_emotion, dim_communication, dim_marketing, dialogue_json, annotations_json
|
summary, mentor_rewrite, ai_comment, review_advice, incentive_point,
|
||||||
|
dim_task_completion, dim_response_timeliness, response_latency_ms,
|
||||||
|
dim_compliance, dim_emotion, dim_communication, dim_marketing, dialogue_json, annotations_json
|
||||||
FROM aihr_practice_session
|
FROM aihr_practice_session
|
||||||
WHERE tenant_id = ? AND mode = 'mobile' AND id = ?
|
WHERE tenant_id = ? AND mode = 'mobile' AND id = ?
|
||||||
""", this::mapReviewDetail, tenantId(), id);
|
""", this::mapReviewDetail, tenantId(), id);
|
||||||
@@ -1497,16 +1548,19 @@ public class AihrPracticeSeedService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
List<ScoreSnapshot> rows = jdbcTemplate.query("""
|
List<ScoreSnapshot> rows = jdbcTemplate.query("""
|
||||||
SELECT total_score, dim_compliance, dim_emotion, dim_communication, dim_marketing
|
SELECT total_score, dim_task_completion, dim_response_timeliness,
|
||||||
|
dim_compliance, dim_emotion, dim_communication, dim_marketing
|
||||||
FROM aihr_practice_session
|
FROM aihr_practice_session
|
||||||
WHERE tenant_id = ? AND session_id = ?
|
WHERE tenant_id = ? AND session_id = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""", (rs, rowNum) -> new ScoreSnapshot(
|
""", (rs, rowNum) -> new ScoreSnapshot(
|
||||||
rs.getInt("total_score"),
|
nullableInt(rs, "total_score"),
|
||||||
rs.getInt("dim_compliance"),
|
nullableInt(rs, "dim_task_completion"),
|
||||||
rs.getInt("dim_emotion"),
|
nullableInt(rs, "dim_response_timeliness"),
|
||||||
rs.getInt("dim_communication"),
|
nullableInt(rs, "dim_compliance"),
|
||||||
rs.getInt("dim_marketing")
|
nullableInt(rs, "dim_emotion"),
|
||||||
|
nullableInt(rs, "dim_communication"),
|
||||||
|
nullableInt(rs, "dim_marketing")
|
||||||
), tenantId(), sessionId.trim());
|
), tenantId(), sessionId.trim());
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1567,9 +1621,11 @@ public class AihrPracticeSeedService {
|
|||||||
COUNT(*) completed,
|
COUNT(*) completed,
|
||||||
COALESCE(ROUND(AVG(total_score)), 0) score,
|
COALESCE(ROUND(AVG(total_score)), 0) score,
|
||||||
SUM(CASE WHEN status = '待复盘' THEN 1 ELSE 0 END) pending_review,
|
SUM(CASE WHEN status = '待复盘' THEN 1 ELSE 0 END) pending_review,
|
||||||
|
COALESCE(ROUND(AVG(COALESCE(dim_task_completion, dim_compliance))), 0) task_completion,
|
||||||
COALESCE(ROUND(AVG(dim_compliance)), 0) compliance,
|
COALESCE(ROUND(AVG(dim_compliance)), 0) compliance,
|
||||||
COALESCE(ROUND(AVG(dim_communication)), 0) communication,
|
COALESCE(ROUND(AVG(dim_communication)), 0) communication,
|
||||||
COALESCE(ROUND(AVG(dim_emotion)), 0) emotion,
|
COALESCE(ROUND(AVG(dim_emotion)), 0) emotion,
|
||||||
|
ROUND(AVG(dim_response_timeliness)) response_timeliness,
|
||||||
COALESCE(ROUND(AVG(dim_marketing)), 0) marketing,
|
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
|
COALESCE(ROUND(SUM(CASE
|
||||||
@@ -1583,14 +1639,16 @@ public class AihrPracticeSeedService {
|
|||||||
rs.getInt("completed"),
|
rs.getInt("completed"),
|
||||||
rs.getInt("score"),
|
rs.getInt("score"),
|
||||||
rs.getInt("pending_review"),
|
rs.getInt("pending_review"),
|
||||||
|
nullableInt(rs, "task_completion"),
|
||||||
rs.getInt("compliance"),
|
rs.getInt("compliance"),
|
||||||
rs.getInt("communication"),
|
rs.getInt("communication"),
|
||||||
rs.getInt("emotion"),
|
rs.getInt("emotion"),
|
||||||
|
nullableInt(rs, "response_timeliness"),
|
||||||
rs.getInt("marketing"),
|
rs.getInt("marketing"),
|
||||||
rs.getInt("incentive_points"),
|
rs.getInt("incentive_points"),
|
||||||
rs.getInt("training_minutes")
|
rs.getInt("training_minutes")
|
||||||
), tenantId(), party, party);
|
), tenantId(), party, party);
|
||||||
CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 0, 0, 0, 0, 0, 0, 0, 0) : rows.get(0);
|
CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 0, 0, null, 0, 0, 0, null, 0, 0, 0) : rows.get(0);
|
||||||
int trainingMinutes = snapshot.trainingMinutes();
|
int trainingMinutes = snapshot.trainingMinutes();
|
||||||
int contribution = Math.min(100, Math.max(0, snapshot.incentivePoints()));
|
int contribution = Math.min(100, Math.max(0, snapshot.incentivePoints()));
|
||||||
int assessmentScore = snapshot.score();
|
int assessmentScore = snapshot.score();
|
||||||
@@ -1604,14 +1662,14 @@ public class AihrPracticeSeedService {
|
|||||||
assessmentScore,
|
assessmentScore,
|
||||||
aiLevel,
|
aiLevel,
|
||||||
List.of(
|
List.of(
|
||||||
new DimensionResponse("任务完成度", null,
|
new DimensionResponse("任务完成度", snapshot.completed() == 0 ? null : firstNonNull(snapshot.taskCompletion(), snapshot.compliance()),
|
||||||
"权重40%;需接入工单闭环率、首解率与处理结果,当前待采集"),
|
"权重40%;当前以SOP执行和问题推进结果代理,工单闭环率/首解率待接入"),
|
||||||
new DimensionResponse("话术规范性", snapshot.completed() == 0 ? null : average(snapshot.compliance(), snapshot.communication()),
|
new DimensionResponse("话术规范性", snapshot.completed() == 0 ? null : average(snapshot.compliance(), snapshot.communication()),
|
||||||
"权重25%;当前以合规+沟通训练维度代理,正式绩效口径需HR确认"),
|
"权重25%;当前以合规+沟通训练维度代理,正式绩效口径需HR确认"),
|
||||||
new DimensionResponse("情绪管理能力", snapshot.completed() == 0 ? null : snapshot.emotion(),
|
new DimensionResponse("情绪管理能力", snapshot.completed() == 0 ? null : snapshot.emotion(),
|
||||||
"权重20%;当前以陪练情绪维度代理"),
|
"权重20%;当前以陪练情绪维度代理"),
|
||||||
new DimensionResponse("响应时效", null,
|
new DimensionResponse("响应时效", snapshot.completed() == 0 ? null : snapshot.responseTimeliness(),
|
||||||
"权重10%;需接入首响时长、处理周期与跟进频率,当前待采集"),
|
"权重10%;当前按陪练回合响应时长统计,工单首响/处理周期待接入"),
|
||||||
new DimensionResponse("增值转化潜力", snapshot.completed() == 0 ? null : snapshot.marketing(),
|
new DimensionResponse("增值转化潜力", snapshot.completed() == 0 ? null : snapshot.marketing(),
|
||||||
"权重5%;当前以陪练营销维度代理,正式口径需HR确认")
|
"权重5%;当前以陪练营销维度代理,正式口径需HR确认")
|
||||||
),
|
),
|
||||||
@@ -2221,11 +2279,15 @@ public class AihrPracticeSeedService {
|
|||||||
jdbcTemplate.update("""
|
jdbcTemplate.update("""
|
||||||
INSERT INTO aihr_practice_session
|
INSERT INTO aihr_practice_session
|
||||||
(tenant_id, session_id, ext_party_id, trainee_name, scenario_id, scenario_name, mode,
|
(tenant_id, session_id, ext_party_id, trainee_name, scenario_id, scenario_name, mode,
|
||||||
total_score, dim_compliance, dim_emotion, dim_communication, dim_marketing,
|
total_score, dim_task_completion, dim_response_timeliness, response_latency_ms,
|
||||||
|
dim_compliance, dim_emotion, dim_communication, dim_marketing,
|
||||||
mentor_rewrite, ai_comment, summary, dialogue_json, annotations_json, trust_curve, status, started_time, finished_time, create_time, update_time)
|
mentor_rewrite, ai_comment, summary, dialogue_json, annotations_json, trust_curve, status, started_time, finished_time, create_time, update_time)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
total_score = VALUES(total_score),
|
total_score = VALUES(total_score),
|
||||||
|
dim_task_completion = VALUES(dim_task_completion),
|
||||||
|
dim_response_timeliness = VALUES(dim_response_timeliness),
|
||||||
|
response_latency_ms = VALUES(response_latency_ms),
|
||||||
dim_compliance = VALUES(dim_compliance),
|
dim_compliance = VALUES(dim_compliance),
|
||||||
dim_emotion = VALUES(dim_emotion),
|
dim_emotion = VALUES(dim_emotion),
|
||||||
dim_communication = VALUES(dim_communication),
|
dim_communication = VALUES(dim_communication),
|
||||||
@@ -2248,10 +2310,13 @@ public class AihrPracticeSeedService {
|
|||||||
scenario.name(),
|
scenario.name(),
|
||||||
mobile ? "mobile" : "text",
|
mobile ? "mobile" : "text",
|
||||||
result.total(),
|
result.total(),
|
||||||
scoreValue(result.scores(), "合规"),
|
result.taskCompletion(),
|
||||||
scoreValue(result.scores(), "情绪"),
|
result.responseTimeliness(),
|
||||||
scoreValue(result.scores(), "沟通"),
|
result.responseLatencyMs(),
|
||||||
scoreValue(result.scores(), "营销"),
|
result.compliance(),
|
||||||
|
scoreValue(result.scores(), "情绪管理能力"),
|
||||||
|
result.communication(),
|
||||||
|
scoreValue(result.scores(), "增值转化潜力"),
|
||||||
result.rewrite(),
|
result.rewrite(),
|
||||||
result.summary(),
|
result.summary(),
|
||||||
record.summary(),
|
record.summary(),
|
||||||
@@ -2771,17 +2836,27 @@ public class AihrPracticeSeedService {
|
|||||||
|
|
||||||
private List<DimensionResponse> scoreItems(ResultSet rs) throws SQLException {
|
private List<DimensionResponse> scoreItems(ResultSet rs) throws SQLException {
|
||||||
List<DimensionResponse> items = new ArrayList<>();
|
List<DimensionResponse> items = new ArrayList<>();
|
||||||
addScoreItem(items, "合规", rs.getObject("dim_compliance"), "SOP关键点覆盖");
|
Integer compliance = nullableInt(rs, "dim_compliance");
|
||||||
addScoreItem(items, "沟通", rs.getObject("dim_communication"), "承诺与表达清晰度");
|
Integer communication = nullableInt(rs, "dim_communication");
|
||||||
addScoreItem(items, "情绪", rs.getObject("dim_emotion"), "安抚与承接能力");
|
addScoreItem(items, "任务完成度", firstNonNull(nullableInt(rs, "dim_task_completion"), compliance), "权重40%;当前以SOP执行和问题推进结果代理");
|
||||||
addScoreItem(items, "营销", rs.getObject("dim_marketing"), "增值转化意识");
|
addScoreItem(items, "话术规范性", average(compliance, communication), "权重25%;合规与沟通训练维度综合");
|
||||||
|
addScoreItem(items, "情绪管理能力", nullableInt(rs, "dim_emotion"), "权重20%;共情承接与信任恢复");
|
||||||
|
addScoreItem(items, "响应时效", nullableInt(rs, "dim_response_timeliness"), "权重10%;阶段一按陪练回合响应时长统计");
|
||||||
|
addScoreItem(items, "增值转化潜力", nullableInt(rs, "dim_marketing"), "权重5%;增值引导与办理意识");
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addScoreItem(List<DimensionResponse> items, String label, Object value, String note) {
|
private void addScoreItem(List<DimensionResponse> items, String label, Object value, String note) {
|
||||||
if (value instanceof Number number) {
|
items.add(new DimensionResponse(label, value == null ? "待采集" : value, note));
|
||||||
items.add(new DimensionResponse(label, number.intValue(), note));
|
}
|
||||||
}
|
|
||||||
|
private Integer nullableInt(ResultSet rs, String column) throws SQLException {
|
||||||
|
Object value = rs.getObject(column);
|
||||||
|
return value instanceof Number number ? number.intValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer firstNonNull(Integer value, Integer fallback) {
|
||||||
|
return value == null ? fallback : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScenarioResponse mapScenarioResponse(ResultSet rs, int rowNum) throws SQLException {
|
private ScenarioResponse mapScenarioResponse(ResultSet rs, int rowNum) throws SQLException {
|
||||||
@@ -2892,10 +2967,11 @@ public class AihrPracticeSeedService {
|
|||||||
|
|
||||||
private List<RubricDimensionResponse> defaultRubricDimensions(boolean feeScenario) {
|
private List<RubricDimensionResponse> defaultRubricDimensions(boolean feeScenario) {
|
||||||
return List.of(
|
return List.of(
|
||||||
new RubricDimensionResponse("compliance", "合规", feeScenario ? 0.30 : 0.35, "SOP关键点、权限边界、承诺口径"),
|
new RubricDimensionResponse("task_completion", "任务完成度", 0.40, "问题解决有效性、工单闭环率、首解率、SOP关键点执行率"),
|
||||||
new RubricDimensionResponse("communication", "沟通", 0.25, "诉求确认、信息结构、反馈节点"),
|
new RubricDimensionResponse("standardization", "话术规范性", 0.25, "流程完整性、法规引用准确性、沟通合规、用语专业度"),
|
||||||
new RubricDimensionResponse("emotion", "情绪", 0.25, "共情承接、降温、信任恢复"),
|
new RubricDimensionResponse("emotion", "情绪管理能力", 0.20, "共情、语速语气控制、业主情绪引导安抚有效性"),
|
||||||
new RubricDimensionResponse("marketing", "营销", feeScenario ? 0.20 : 0.15, "收费季转化、政策解释、办理引导")
|
new RubricDimensionResponse("response_timeliness", "响应时效", 0.10, "首响时长、处理周期、跟进频率"),
|
||||||
|
new RubricDimensionResponse("marketing", "增值转化潜力", 0.05, "活动引导、产品推介时机与成功率")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2969,6 +3045,78 @@ public class AihrPracticeSeedService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void rememberResponseLatency(ActiveSession session, int roundIndex, long submittedAt) {
|
||||||
|
if (session == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Long> promptTimes = session.promptPresentedAtMillis();
|
||||||
|
List<Long> latencies = session.responseLatenciesMs();
|
||||||
|
synchronized (promptTimes) {
|
||||||
|
if (roundIndex < 0 || roundIndex >= promptTimes.size() || promptTimes.get(roundIndex) == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long latency = Math.max(0L, submittedAt - promptTimes.get(roundIndex));
|
||||||
|
synchronized (latencies) {
|
||||||
|
while (latencies.size() <= roundIndex) {
|
||||||
|
latencies.add(null);
|
||||||
|
}
|
||||||
|
latencies.set(roundIndex, latency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rememberPromptPresentedAt(ActiveSession session, int roundIndex, long presentedAt) {
|
||||||
|
if (session == null || roundIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Long> promptTimes = session.promptPresentedAtMillis();
|
||||||
|
synchronized (promptTimes) {
|
||||||
|
while (promptTimes.size() <= roundIndex) {
|
||||||
|
promptTimes.add(null);
|
||||||
|
}
|
||||||
|
promptTimes.set(roundIndex, presentedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer averageResponseLatencyMs(ActiveSession session) {
|
||||||
|
if (session == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Long> latencies = snapshotLong(session.responseLatenciesMs());
|
||||||
|
long total = 0L;
|
||||||
|
int count = 0;
|
||||||
|
for (Long latency : latencies) {
|
||||||
|
if (latency != null && latency >= 0) {
|
||||||
|
total += latency;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (int) Math.min(Integer.MAX_VALUE, Math.round(total / (double) count));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer responseTimelinessScore(ActiveSession session) {
|
||||||
|
Integer latency = averageResponseLatencyMs(session);
|
||||||
|
if (latency == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (latency <= RESPONSE_FAST_MS) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
if (latency <= RESPONSE_ACCEPTABLE_MS) {
|
||||||
|
return 85;
|
||||||
|
}
|
||||||
|
if (latency <= RESPONSE_SLOW_MS) {
|
||||||
|
return 70;
|
||||||
|
}
|
||||||
|
if (latency <= RESPONSE_VERY_SLOW_MS) {
|
||||||
|
return 55;
|
||||||
|
}
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
|
||||||
private void rememberTurnEvidence(ActiveSession session, int roundIndex, Integer emotion, Integer trust, boolean redFlag, String coachHint) {
|
private void rememberTurnEvidence(ActiveSession session, int roundIndex, Integer emotion, Integer trust, boolean redFlag, String coachHint) {
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
return;
|
return;
|
||||||
@@ -3163,18 +3311,24 @@ public class AihrPracticeSeedService {
|
|||||||
private String calibrationDimensionsJson(ScoreSnapshot score, CalibrationRequest request) {
|
private String calibrationDimensionsJson(ScoreSnapshot score, CalibrationRequest request) {
|
||||||
Map<String, Object> data = new LinkedHashMap<>();
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
data.put("original", Map.of(
|
data.put("original", Map.of(
|
||||||
"total", score.total(),
|
"total", firstNonNull(score.total(), 0),
|
||||||
"compliance", score.compliance(),
|
"taskCompletion", firstNonNull(firstNonNull(score.taskCompletion(), score.compliance()), 0),
|
||||||
"emotion", score.emotion(),
|
"standardization", firstNonNull(average(score.compliance(), score.communication()), 0),
|
||||||
"communication", score.communication(),
|
"emotion", firstNonNull(score.emotion(), 0),
|
||||||
"marketing", score.marketing()
|
"responseTimeliness", firstNonNull(score.responseTimeliness(), 0),
|
||||||
|
"conversion", firstNonNull(score.marketing(), 0)
|
||||||
));
|
));
|
||||||
Map<String, Object> corrected = new LinkedHashMap<>();
|
Map<String, Object> corrected = new LinkedHashMap<>();
|
||||||
corrected.put("total", request == null || request.correctedTotal() == null ? score.total() : clampScore(request.correctedTotal()));
|
corrected.put("total", request == null || request.correctedTotal() == null ? firstNonNull(score.total(), 0) : clampScore(request.correctedTotal()));
|
||||||
corrected.put("compliance", request == null || request.correctedCompliance() == null ? score.compliance() : clampScore(request.correctedCompliance()));
|
corrected.put("taskCompletion", request == null || request.correctedTaskCompletion() == null
|
||||||
corrected.put("emotion", request == null || request.correctedEmotion() == null ? score.emotion() : clampScore(request.correctedEmotion()));
|
? firstNonNull(firstNonNull(score.taskCompletion(), score.compliance()), 0) : clampScore(request.correctedTaskCompletion()));
|
||||||
corrected.put("communication", request == null || request.correctedCommunication() == null ? score.communication() : clampScore(request.correctedCommunication()));
|
corrected.put("standardization", request == null || (request.correctedCompliance() == null && request.correctedCommunication() == null)
|
||||||
corrected.put("marketing", request == null || request.correctedMarketing() == null ? score.marketing() : clampScore(request.correctedMarketing()));
|
? firstNonNull(average(score.compliance(), score.communication()), 0)
|
||||||
|
: firstNonNull(average(firstNonNull(request.correctedCompliance(), score.compliance()), firstNonNull(request.correctedCommunication(), score.communication())), 0));
|
||||||
|
corrected.put("emotion", request == null || request.correctedEmotion() == null ? firstNonNull(score.emotion(), 0) : clampScore(request.correctedEmotion()));
|
||||||
|
corrected.put("responseTimeliness", request == null || request.correctedResponseTimeliness() == null
|
||||||
|
? firstNonNull(score.responseTimeliness(), 0) : clampScore(request.correctedResponseTimeliness()));
|
||||||
|
corrected.put("conversion", request == null || request.correctedMarketing() == null ? firstNonNull(score.marketing(), 0) : clampScore(request.correctedMarketing()));
|
||||||
data.put("corrected", corrected);
|
data.put("corrected", corrected);
|
||||||
return writeJson(data);
|
return writeJson(data);
|
||||||
}
|
}
|
||||||
@@ -3321,6 +3475,11 @@ public class AihrPracticeSeedService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int scoreValue(List<DimensionResponse> scores, String label, int fallback) {
|
||||||
|
Integer value = scoreValue(scores, label);
|
||||||
|
return value == null ? fallback : value;
|
||||||
|
}
|
||||||
|
|
||||||
private int normalizeLimit(int limit) {
|
private int normalizeLimit(int limit) {
|
||||||
if (limit <= 0) {
|
if (limit <= 0) {
|
||||||
return 5;
|
return 5;
|
||||||
@@ -3355,6 +3514,9 @@ public class AihrPracticeSeedService {
|
|||||||
`scenario_name` varchar(100) DEFAULT NULL COMMENT '场景名称',
|
`scenario_name` varchar(100) DEFAULT NULL COMMENT '场景名称',
|
||||||
`mode` varchar(30) DEFAULT 'text' COMMENT '训练模式',
|
`mode` varchar(30) DEFAULT 'text' COMMENT '训练模式',
|
||||||
`total_score` int DEFAULT NULL COMMENT '总分',
|
`total_score` int DEFAULT NULL COMMENT '总分',
|
||||||
|
`dim_task_completion` int DEFAULT NULL COMMENT '任务完成度分',
|
||||||
|
`dim_response_timeliness` int DEFAULT NULL COMMENT '响应时效分',
|
||||||
|
`response_latency_ms` int DEFAULT NULL COMMENT '平均陪练响应时长毫秒',
|
||||||
`dim_compliance` int DEFAULT NULL COMMENT '合规分',
|
`dim_compliance` int DEFAULT NULL COMMENT '合规分',
|
||||||
`dim_emotion` int DEFAULT NULL COMMENT '情绪分',
|
`dim_emotion` int DEFAULT NULL COMMENT '情绪分',
|
||||||
`dim_communication` int DEFAULT NULL COMMENT '沟通分',
|
`dim_communication` int DEFAULT NULL COMMENT '沟通分',
|
||||||
@@ -3380,6 +3542,9 @@ public class AihrPracticeSeedService {
|
|||||||
KEY `idx_aihr_practice_review` (`tenant_id`, `mode`, `status`, `finished_time`)
|
KEY `idx_aihr_practice_review` (`tenant_id`, `mode`, `status`, `finished_time`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练记录';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练记录';
|
||||||
""");
|
""");
|
||||||
|
ensurePracticeColumn("dim_task_completion", "ALTER TABLE aihr_practice_session ADD COLUMN `dim_task_completion` int DEFAULT NULL COMMENT '任务完成度分' AFTER `total_score`");
|
||||||
|
ensurePracticeColumn("dim_response_timeliness", "ALTER TABLE aihr_practice_session ADD COLUMN `dim_response_timeliness` int DEFAULT NULL COMMENT '响应时效分' AFTER `dim_task_completion`");
|
||||||
|
ensurePracticeColumn("response_latency_ms", "ALTER TABLE aihr_practice_session ADD COLUMN `response_latency_ms` int DEFAULT NULL COMMENT '平均陪练响应时长毫秒' AFTER `dim_response_timeliness`");
|
||||||
ensurePracticeColumn("dialogue_json", "ALTER TABLE aihr_practice_session ADD COLUMN `dialogue_json` text DEFAULT NULL COMMENT '对练话术JSON' AFTER `summary`");
|
ensurePracticeColumn("dialogue_json", "ALTER TABLE aihr_practice_session ADD COLUMN `dialogue_json` text DEFAULT NULL COMMENT '对练话术JSON' AFTER `summary`");
|
||||||
ensurePracticeColumn("annotations_json", "ALTER TABLE aihr_practice_session ADD COLUMN `annotations_json` text DEFAULT NULL COMMENT '逐句标注JSON' AFTER `dialogue_json`");
|
ensurePracticeColumn("annotations_json", "ALTER TABLE aihr_practice_session ADD COLUMN `annotations_json` text DEFAULT NULL COMMENT '逐句标注JSON' AFTER `dialogue_json`");
|
||||||
ensurePracticeColumn("review_advice", "ALTER TABLE aihr_practice_session ADD COLUMN `review_advice` varchar(1000) DEFAULT NULL COMMENT '主管复盘建议' AFTER `ai_comment`");
|
ensurePracticeColumn("review_advice", "ALTER TABLE aihr_practice_session ADD COLUMN `review_advice` varchar(1000) DEFAULT NULL COMMENT '主管复盘建议' AFTER `ai_comment`");
|
||||||
@@ -3907,7 +4072,9 @@ public class AihrPracticeSeedService {
|
|||||||
return Math.round((left + right) / 2.0f);
|
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, Integer trainingMinutes) {
|
private record CompetencySnapshot(Integer completed, Integer score, Integer pendingReview, Integer taskCompletion,
|
||||||
|
Integer compliance, Integer communication, Integer emotion, Integer responseTimeliness,
|
||||||
|
Integer marketing, Integer incentivePoints, Integer trainingMinutes) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record EvidenceScoreSnapshot(Integer aiScore, Integer humanScore, Integer finalScore, Integer calibrationCount) {
|
private record EvidenceScoreSnapshot(Integer aiScore, Integer humanScore, Integer finalScore, Integer calibrationCount) {
|
||||||
@@ -3916,7 +4083,8 @@ public class AihrPracticeSeedService {
|
|||||||
private record SessionAnnotationSource(String scenarioId, String dialogueJson, String annotationsJson) {
|
private record SessionAnnotationSource(String scenarioId, String dialogueJson, String annotationsJson) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ScoreSnapshot(Integer total, Integer compliance, Integer emotion, Integer communication, Integer marketing) {
|
private record ScoreSnapshot(Integer total, Integer taskCompletion, Integer responseTimeliness,
|
||||||
|
Integer compliance, Integer emotion, Integer communication, Integer marketing) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record CalibrationPair(int original, int corrected) {
|
private record CalibrationPair(int original, int corrected) {
|
||||||
@@ -3972,9 +4140,12 @@ public class AihrPracticeSeedService {
|
|||||||
|
|
||||||
private record ActiveSession(String scenarioId, String trainee, String extPartyId, Long assignmentId, boolean mobile, LocalDateTime startedAt,
|
private record ActiveSession(String scenarioId, String trainee, String extPartyId, Long assignmentId, boolean mobile, LocalDateTime startedAt,
|
||||||
List<String> traineeReplies, List<String> traineeAudioUrls, List<Long> traineeAudioOssIds,
|
List<String> traineeReplies, List<String> traineeAudioUrls, List<Long> traineeAudioOssIds,
|
||||||
List<String> customerLines, List<TurnEvidence> turnEvidence) {
|
List<String> customerLines, List<TurnEvidence> turnEvidence,
|
||||||
|
List<Long> promptPresentedAtMillis, List<Long> responseLatenciesMs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record PracticeResult(Integer total, List<DimensionResponse> scores, String rewrite, String summary) {
|
private record PracticeResult(Integer total, List<DimensionResponse> scores, String rewrite, String summary,
|
||||||
|
Integer compliance, Integer communication, Integer taskCompletion,
|
||||||
|
Integer responseTimeliness, Integer responseLatencyMs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -756,7 +756,7 @@ public class AihrPracticeSeedServiceTest {
|
|||||||
assertEquals(81, profile.dimensions().get(1).value());
|
assertEquals(81, profile.dimensions().get(1).value());
|
||||||
assertEquals(78, profile.dimensions().get(2).value());
|
assertEquals(78, profile.dimensions().get(2).value());
|
||||||
assertEquals(74, profile.dimensions().get(4).value());
|
assertEquals(74, profile.dimensions().get(4).value());
|
||||||
assertNull(profile.dimensions().get(0).value());
|
assertEquals(82, profile.dimensions().get(0).value());
|
||||||
assertNull(profile.dimensions().get(3).value());
|
assertNull(profile.dimensions().get(3).value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_session` (
|
|||||||
`scenario_name` varchar(100) DEFAULT NULL COMMENT '场景名称',
|
`scenario_name` varchar(100) DEFAULT NULL COMMENT '场景名称',
|
||||||
`mode` varchar(30) DEFAULT 'text' COMMENT '训练模式',
|
`mode` varchar(30) DEFAULT 'text' COMMENT '训练模式',
|
||||||
`total_score` int DEFAULT NULL COMMENT '总分',
|
`total_score` int DEFAULT NULL COMMENT '总分',
|
||||||
|
`dim_task_completion` int DEFAULT NULL COMMENT '任务完成度分',
|
||||||
|
`dim_response_timeliness` int DEFAULT NULL COMMENT '响应时效分',
|
||||||
|
`response_latency_ms` int DEFAULT NULL COMMENT '平均陪练响应时长毫秒',
|
||||||
`dim_compliance` int DEFAULT NULL COMMENT '合规分',
|
`dim_compliance` int DEFAULT NULL COMMENT '合规分',
|
||||||
`dim_emotion` int DEFAULT NULL COMMENT '情绪分',
|
`dim_emotion` int DEFAULT NULL COMMENT '情绪分',
|
||||||
`dim_communication` int DEFAULT NULL COMMENT '沟通分',
|
`dim_communication` int DEFAULT NULL COMMENT '沟通分',
|
||||||
@@ -354,7 +357,7 @@ SELECT
|
|||||||
CONCAT(`scenario_name`, ' Rubric'),
|
CONCAT(`scenario_name`, ' Rubric'),
|
||||||
'v1',
|
'v1',
|
||||||
1,
|
1,
|
||||||
'生活顾问试点四维评分标准',
|
'生活顾问试点五维评分标准',
|
||||||
NOW(),
|
NOW(),
|
||||||
NOW()
|
NOW()
|
||||||
FROM `aihr_practice_scenario`
|
FROM `aihr_practice_scenario`
|
||||||
@@ -373,11 +376,7 @@ SELECT
|
|||||||
r.`rubric_code`,
|
r.`rubric_code`,
|
||||||
d.`dimension_code`,
|
d.`dimension_code`,
|
||||||
d.`dimension_name`,
|
d.`dimension_name`,
|
||||||
CASE
|
d.`weight`,
|
||||||
WHEN s.`scenario_type` = '催费/停车费' AND d.`dimension_code` = 'compliance' THEN 0.30
|
|
||||||
WHEN s.`scenario_type` = '催费/停车费' AND d.`dimension_code` = 'marketing' THEN 0.20
|
|
||||||
ELSE d.`weight`
|
|
||||||
END,
|
|
||||||
d.`description`,
|
d.`description`,
|
||||||
d.`sort_order`,
|
d.`sort_order`,
|
||||||
NOW(),
|
NOW(),
|
||||||
@@ -386,10 +385,11 @@ FROM `aihr_practice_rubric` r
|
|||||||
JOIN `aihr_practice_scenario` s
|
JOIN `aihr_practice_scenario` s
|
||||||
ON s.`tenant_id` = r.`tenant_id` AND s.`scenario_code` = r.`scenario_code`
|
ON s.`tenant_id` = r.`tenant_id` AND s.`scenario_code` = r.`scenario_code`
|
||||||
JOIN (
|
JOIN (
|
||||||
SELECT 'compliance' AS `dimension_code`, '合规' AS `dimension_name`, 0.35 AS `weight`, 'SOP关键点、权限边界、承诺口径' AS `description`, 10 AS `sort_order`
|
SELECT 'task_completion' AS `dimension_code`, '任务完成度' AS `dimension_name`, 0.40 AS `weight`, '问题解决有效性、工单闭环率、首解率、SOP关键点执行率' AS `description`, 10 AS `sort_order`
|
||||||
UNION ALL SELECT 'communication', '沟通', 0.25, '诉求确认、信息结构、反馈节点', 20
|
UNION ALL SELECT 'standardization', '话术规范性', 0.25, '流程完整性、法规引用准确性、沟通合规、用语专业度', 20
|
||||||
UNION ALL SELECT 'emotion', '情绪', 0.25, '共情承接、降温、信任恢复', 30
|
UNION ALL SELECT 'emotion', '情绪管理能力', 0.20, '共情、语速语气控制、业主情绪引导安抚有效性', 30
|
||||||
UNION ALL SELECT 'marketing', '营销', 0.15, '收费季转化、政策解释、办理引导', 40
|
UNION ALL SELECT 'response_timeliness', '响应时效', 0.10, '首响时长、处理周期、跟进频率', 40
|
||||||
|
UNION ALL SELECT 'marketing', '增值转化潜力', 0.05, '活动引导、产品推介时机与成功率', 50
|
||||||
) d
|
) d
|
||||||
WHERE r.`tenant_id` = '000000'
|
WHERE r.`tenant_id` = '000000'
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
-- AIHR 对练五维评分迁移(MySQL 8.x)
|
||||||
|
-- 补齐会话结构与生活顾问试点 Rubric,不写入训练业务数据;可重复执行。
|
||||||
|
|
||||||
|
SET @aihr_practice_table_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'aihr_practice_session'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @aihr_task_completion_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'aihr_practice_session'
|
||||||
|
AND COLUMN_NAME = 'dim_task_completion'
|
||||||
|
);
|
||||||
|
SET @aihr_task_completion_ddl := IF(
|
||||||
|
@aihr_practice_table_exists = 1 AND @aihr_task_completion_exists = 0,
|
||||||
|
'ALTER TABLE aihr_practice_session ADD COLUMN `dim_task_completion` int DEFAULT NULL COMMENT ''任务完成度分'' AFTER `total_score`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE aihr_task_completion_stmt FROM @aihr_task_completion_ddl;
|
||||||
|
EXECUTE aihr_task_completion_stmt;
|
||||||
|
DEALLOCATE PREPARE aihr_task_completion_stmt;
|
||||||
|
|
||||||
|
SET @aihr_response_timeliness_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'aihr_practice_session'
|
||||||
|
AND COLUMN_NAME = 'dim_response_timeliness'
|
||||||
|
);
|
||||||
|
SET @aihr_response_timeliness_ddl := IF(
|
||||||
|
@aihr_practice_table_exists = 1 AND @aihr_response_timeliness_exists = 0,
|
||||||
|
'ALTER TABLE aihr_practice_session ADD COLUMN `dim_response_timeliness` int DEFAULT NULL COMMENT ''响应时效分'' AFTER `dim_task_completion`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE aihr_response_timeliness_stmt FROM @aihr_response_timeliness_ddl;
|
||||||
|
EXECUTE aihr_response_timeliness_stmt;
|
||||||
|
DEALLOCATE PREPARE aihr_response_timeliness_stmt;
|
||||||
|
|
||||||
|
SET @aihr_response_latency_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'aihr_practice_session'
|
||||||
|
AND COLUMN_NAME = 'response_latency_ms'
|
||||||
|
);
|
||||||
|
SET @aihr_response_latency_ddl := IF(
|
||||||
|
@aihr_practice_table_exists = 1 AND @aihr_response_latency_exists = 0,
|
||||||
|
'ALTER TABLE aihr_practice_session ADD COLUMN `response_latency_ms` int DEFAULT NULL COMMENT ''平均陪练响应时长毫秒'' AFTER `dim_response_timeliness`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE aihr_response_latency_stmt FROM @aihr_response_latency_ddl;
|
||||||
|
EXECUTE aihr_response_latency_stmt;
|
||||||
|
DEALLOCATE PREPARE aihr_response_latency_stmt;
|
||||||
|
|
||||||
|
CREATE TEMPORARY TABLE IF NOT EXISTS aihr_brd_rubric_codes (
|
||||||
|
tenant_id varchar(20) NOT NULL,
|
||||||
|
rubric_code varchar(100) NOT NULL,
|
||||||
|
PRIMARY KEY (tenant_id, rubric_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
DELETE FROM aihr_brd_rubric_codes;
|
||||||
|
|
||||||
|
INSERT INTO aihr_brd_rubric_codes (tenant_id, rubric_code)
|
||||||
|
SELECT r.tenant_id, r.rubric_code
|
||||||
|
FROM aihr_practice_rubric r
|
||||||
|
JOIN aihr_practice_scenario s
|
||||||
|
ON s.tenant_id = r.tenant_id AND s.scenario_code = r.scenario_code
|
||||||
|
JOIN (
|
||||||
|
SELECT tenant_id, rubric_code
|
||||||
|
FROM aihr_practice_rubric_dimension
|
||||||
|
WHERE dimension_code IN ('compliance', 'communication', 'emotion', 'marketing')
|
||||||
|
GROUP BY tenant_id, rubric_code
|
||||||
|
HAVING COUNT(*) = 4
|
||||||
|
) old_dimensions
|
||||||
|
ON old_dimensions.tenant_id = r.tenant_id AND old_dimensions.rubric_code = r.rubric_code
|
||||||
|
WHERE r.tenant_id = '000000'
|
||||||
|
AND s.position = '生活顾问';
|
||||||
|
|
||||||
|
DELETE d
|
||||||
|
FROM aihr_practice_rubric_dimension d
|
||||||
|
JOIN aihr_brd_rubric_codes r
|
||||||
|
ON r.tenant_id = d.tenant_id AND r.rubric_code = d.rubric_code;
|
||||||
|
|
||||||
|
INSERT 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_brd_rubric_codes 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;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS aihr_brd_rubric_codes;
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
|
|
||||||
- `aihr_practice_assignment` 吸收 `daily_drill`(每日一练)与 `training_camp`(专项训练营)的语义,用 `source`(daily/camp/retry/manual)+ `reason` 字段区分来源,不再单独建这两张表。
|
- `aihr_practice_assignment` 吸收 `daily_drill`(每日一练)与 `training_camp`(专项训练营)的语义,用 `source`(daily/camp/retry/manual)+ `reason` 字段区分来源,不再单独建这两张表。
|
||||||
- `mistake_book`(错题本)语义由“低分维度 → 定向再练派发”承载(见 M4),`assignment.source='retry'` 即错题重练记录。
|
- `mistake_book`(错题本)语义由“低分维度 → 定向再练派发”承载(见 M4),`assignment.source='retry'` 即错题重练记录。
|
||||||
- 跨期五维画像仍走 TechSpec M6 的 `competency_assessment`,本计划只做单场四维分与四项画像聚合,勿与跨期评估混成一个数(BACKLOG B5 口径)。
|
- 跨期画像仍走 TechSpec M6 的 `competency_assessment`,本计划单场训练结果按 BRD 4.5.1 五维输出与聚合;任务完成度、响应时效若缺少工单/组织系统证据,只能标代理指标或待采集,勿与跨期正式评估混成一个数(BACKLOG B5 口径)。
|
||||||
|
|
||||||
### 3.2 复用现有能力
|
### 3.2 复用现有能力
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
功能:
|
功能:
|
||||||
|
|
||||||
- 员工画像扩展为四项:训练时长、贡献度、测评分、AI 等级(BACKLOG B5;贡献度二期用人工计分入口记 `incentive_point`,不做自动案例沉淀)。
|
- 员工画像扩展为四项:训练时长、贡献度、测评分、AI 等级(BACKLOG B5;贡献度二期用人工计分入口记 `incentive_point`,不做自动案例沉淀)。
|
||||||
- 员工端画像以雷达图 + 趋势呈现单场四维分聚合(BRD 4.5 可视化口径,数据留跨期 `competency_assessment` 的入口)。
|
- 员工端画像以雷达图 + 趋势呈现单场 BRD 五维分聚合(BRD 4.5 可视化口径,数据留跨期 `competency_assessment` 的入口)。
|
||||||
- 主管端预警:连续低分、超期未训、分数下滑。
|
- 主管端预警:连续低分、超期未训、分数下滑。
|
||||||
- 批量训练任务派发:主管按岗位/短板派发专项。
|
- 批量训练任务派发:主管按岗位/短板派发专项。
|
||||||
- 管理者“传帮带”工作台:主管从画像/预警进入员工详情,查看回放与标注,写复盘建议,派发下一次训练,形成“看见问题→带教→复训”的闭环(主管侧帮带承载形态,边界见 §2 暂缓表;定向师带徒已定不做,员工间帮带归阶段二开放问题榜)。
|
- 管理者“传帮带”工作台:主管从画像/预警进入员工详情,查看回放与标注,写复盘建议,派发下一次训练,形成“看见问题→带教→复训”的闭环(主管侧帮带承载形态,边界见 §2 暂缓表;定向师带徒已定不做,员工间帮带归阶段二开放问题榜)。
|
||||||
@@ -289,7 +289,7 @@
|
|||||||
| 雷点 | 被敷衍、先讲规定、推给业主自己协调 |
|
| 雷点 | 被敷衍、先讲规定、推给业主自己协调 |
|
||||||
| 爽点 | 先共情、给首次反馈时间、明确责任人 |
|
| 爽点 | 先共情、给首次反馈时间、明确责任人 |
|
||||||
| SOP 引用 | 对应知识库片段 ID |
|
| SOP 引用 | 对应知识库片段 ID |
|
||||||
| Rubric | 合规、沟通、情绪、营销四维权重 |
|
| Rubric | 任务完成度40%、话术规范性25%、情绪管理能力20%、响应时效10%、增值转化潜力5% |
|
||||||
| 标杆话术 | 1 条优秀版本、1 条反例 |
|
| 标杆话术 | 1 条优秀版本、1 条反例 |
|
||||||
|
|
||||||
## 7. 决策待确认
|
## 7. 决策待确认
|
||||||
|
|||||||
@@ -175,7 +175,7 @@
|
|||||||
- 2026-07-14 G3 案例读取权限复核:发现案例列表和详情对任意已登录后台系统用户返回当前租户数据,存在普通后台账号横向读取案例资产的风险;现收紧为 `superadmin`/`hr_operator`,APP 用户保留登录后的项目范围读取。新增源码契约测试,AIHR 全量测试 `57/57`、脚本语法和空白检查通过,修复提交为 `b0d24970`,尚未发布生产。
|
- 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 G3 驾驶舱权限复核:发现管理驾驶舱只要求登录,APP 用户或普通后台账号理论上可读取当前租户的组织、训练、知识库、案例和候选人汇总;现收紧为 `superadmin`/`hr_operator`,主管团队数据继续走移动端项目范围接口。新增源码契约测试,AIHR 全量测试 `58/58`、脚本语法和空白检查通过,修复提交为 `7522794d`,尚未发布生产。
|
||||||
- 2026-07-14 后续 BRD P1 复核:认证/晋升规则、岗位-SOP 资格 gating、方言样本、正式案例视频样片与组织项目范围仍依赖 HR/运维输入;当前不继续用硬编码、演示 seed 或新增页面伪造完成,下一批优先接正式规则与试点数据后再实现。
|
- 2026-07-14 后续 BRD P1 复核:认证/晋升规则、岗位-SOP 资格 gating、方言样本、正式案例视频样片与组织项目范围仍依赖 HR/运维输入;当前不继续用硬编码、演示 seed 或新增页面伪造完成,下一批优先接正式规则与试点数据后再实现。
|
||||||
- 2026-07-14 BRD 4.5.1 五维能力画像对齐:员工端与管理端画像现在统一返回“任务完成度、话术规范性、情绪管理能力、响应时效、增值转化潜力”五项;话术规范性由现有合规+沟通陪练分数作为训练代理,情绪和增值转化分别由陪练情绪/营销分代理。任务完成度、响应时效因尚未接入工单闭环、首解率、首响时长和处理周期,明确返回“待采集”,不再用训练次数或综合分伪造 BRD 绩效指标;正式权重和绩效口径仍需 HR 确认。
|
- 2026-07-14 BRD 4.5.1 五维能力画像对齐:员工端与管理端画像现在统一返回“任务完成度、话术规范性、情绪管理能力、响应时效、增值转化潜力”五项;话术规范性由现有合规+沟通陪练分数作为训练代理,情绪和增值转化分别由陪练情绪/营销分代理。任务完成度当前以 SOP 执行/问题推进代理,响应时效在有陪练回合耗时证据时按代理分返回,正式工单闭环率、首解率、首响时长和处理周期仍显示待采集,不把代理指标伪装成 BRD 正式绩效指标。
|
||||||
- 2026-07-14 BRD 五维画像展示语义收紧:员工端、管理端和证据包导出将平均对练分称为“训练综合分”,缺失的五维指标统一显示“待采集”,不再把空值导出为 `null` 或把训练综合分称为正式绩效总分。
|
- 2026-07-14 BRD 五维画像展示语义收紧:员工端、管理端和证据包导出将平均对练分称为“训练综合分”,缺失的五维指标统一显示“待采集”,不再把空值导出为 `null` 或把训练综合分称为正式绩效总分。
|
||||||
- 2026-07-14 BRD 动态难度复核:场景表已有 `difficulty` 字段,但开始训练仍按用户/主管传入的场景直接启动,没有基于连续高分自动升级复合高压场景的规则、候选场景映射、人工关闭开关或升级证据。该项需要 HR 确认连续高分阈值、升级范围和降级条件后再实现,当前不以固定阈值直接改变员工训练难度。
|
- 2026-07-14 BRD 动态难度复核:场景表已有 `difficulty` 字段,但开始训练仍按用户/主管传入的场景直接启动,没有基于连续高分自动升级复合高压场景的规则、候选场景映射、人工关闭开关或升级证据。该项需要 HR 确认连续高分阈值、升级范围和降级条件后再实现,当前不以固定阈值直接改变员工训练难度。
|
||||||
- 2026-07-14 线上资源复核:生产根站、`/h5/` 和租户接口均返回 `200`;生产管理端仍加载 `index-CJZ3Ax3Z.js`(SHA-256 `8b9278a26ccb760abce489b12a20b221748c64d4c4153613d2511777bb9677bc`),当前本地构建为 `index-DF7MSq-V.js`(SHA-256 `96969b2ae71192cb08b3b2f14edb84355dce6b00679f8237d508ab392c97d7d9`),管理端五维画像修复尚未发布;mobile-uni 线上与本地均为 `index-D4-NrEpb.js` 且 hash 一致。发布预检仍因两个既有 Figma 文档未提交而阻断,本轮未执行生产写入或重启。
|
- 2026-07-14 线上资源复核:生产根站、`/h5/` 和租户接口均返回 `200`;生产管理端仍加载 `index-CJZ3Ax3Z.js`(SHA-256 `8b9278a26ccb760abce489b12a20b221748c64d4c4153613d2511777bb9677bc`),当前本地构建为 `index-DF7MSq-V.js`(SHA-256 `96969b2ae71192cb08b3b2f14edb84355dce6b00679f8237d508ab392c97d7d9`),管理端五维画像修复尚未发布;mobile-uni 线上与本地均为 `index-D4-NrEpb.js` 且 hash 一致。发布预检仍因两个既有 Figma 文档未提交而阻断,本轮未执行生产写入或重启。
|
||||||
@@ -292,3 +292,4 @@
|
|||||||
- 2026-07-14 BRD 生产迁移资产补齐:`aihr_practice_audio`、`aihr_practice_calibration` 此前只有 reset SQL 和运行时懒建表,生产曾依赖人工建表;现新增可重复执行的 MySQL 8 版本化迁移脚本,仅补齐结构、不写入业务数据。仍需由运维在正式发布窗口执行并核对生产 schema,不能把脚本存在视为迁移已执行。
|
- 2026-07-14 BRD 生产迁移资产补齐:`aihr_practice_audio`、`aihr_practice_calibration` 此前只有 reset SQL 和运行时懒建表,生产曾依赖人工建表;现新增可重复执行的 MySQL 8 版本化迁移脚本,仅补齐结构、不写入业务数据。仍需由运维在正式发布窗口执行并核对生产 schema,不能把脚本存在视为迁移已执行。
|
||||||
- 2026-07-14 最新线上只读复核:生产根站、`/h5/` 和 `/prod-api/auth/tenant/list` 均返回 `200`;线上仍加载管理端 `assets/index-CJZ3Ax3Z.js`(SHA-256 `8b9278a26ccb760abce489b12a20b221748c64d4c4153613d2511777bb9677bc`)与 H5 `assets/index-D4-NrEpb.js`(SHA-256 `ab15bfd17cabe58e2f34b0ac61ee198a837128ed85dc6bb0ddf77bc3e22dcd9c`),本地最新提交 `e0da5f0a` 尚未发布;本轮未执行生产同步、后端重启或业务数据写入。
|
- 2026-07-14 最新线上只读复核:生产根站、`/h5/` 和 `/prod-api/auth/tenant/list` 均返回 `200`;线上仍加载管理端 `assets/index-CJZ3Ax3Z.js`(SHA-256 `8b9278a26ccb760abce489b12a20b221748c64d4c4153613d2511777bb9677bc`)与 H5 `assets/index-D4-NrEpb.js`(SHA-256 `ab15bfd17cabe58e2f34b0ac61ee198a837128ed85dc6bb0ddf77bc3e22dcd9c`),本地最新提交 `e0da5f0a` 尚未发布;本轮未执行生产同步、后端重启或业务数据写入。
|
||||||
- 2026-07-14 BRD 4.4 入职日期数据链路补齐:组织快照初始化 DDL、外部组织同步和旧库懒迁移现在统一保存 `hire_date`,每日三题在存在该字段但记录缺日期时仍只允许本地 `dev/local` 的显式训练次数兼容回退,生产不会用训练次数推断新员工;新增 MySQL 8 版本迁移与严格预检字段/身份门禁。未写入试点业务数据,正式环境仍需外部组织同步实际提供入职日期并执行迁移。
|
- 2026-07-14 BRD 4.4 入职日期数据链路补齐:组织快照初始化 DDL、外部组织同步和旧库懒迁移现在统一保存 `hire_date`,每日三题在存在该字段但记录缺日期时仍只允许本地 `dev/local` 的显式训练次数兼容回退,生产不会用训练次数推断新员工;新增 MySQL 8 版本迁移与严格预检字段/身份门禁。未写入试点业务数据,正式环境仍需外部组织同步实际提供入职日期并执行迁移。
|
||||||
|
- 2026-07-14 BRD 4.5.1 五维评分规则数据对齐:对练 `/finish`、复盘、校准和画像代码已补齐任务完成度、话术规范性、情绪管理能力、响应时效、增值转化潜力五项;本轮同步 reset SQL、运行时 seed fallback 和现有生活顾问 Rubric 迁移为 40%/25%/20%/10%/5%,严格预检新增五维 Rubric 数量与权重和检查。任务完成度当前以 SOP 执行/问题推进代理,响应时效当前以陪练回合响应耗时代理;工单闭环率、首解率、正式首响/处理周期仍待业务系统与 HR 确认,未把代理指标宣称为正式绩效口径。迁移只改评分规则结构,不写训练、校准或 SOP 业务样本。
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ export type PracticeCalibrationRequest = {
|
|||||||
correctedEmotion?: number;
|
correctedEmotion?: number;
|
||||||
correctedCommunication?: number;
|
correctedCommunication?: number;
|
||||||
correctedMarketing?: number;
|
correctedMarketing?: number;
|
||||||
|
correctedTaskCompletion?: number;
|
||||||
|
correctedResponseTimeliness?: number;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+17
-2
@@ -81,7 +81,7 @@ check_pilot_samples() {
|
|||||||
[[ "$(pilot_sql_scalar "SELECT DATE('$pilot_end') IS NOT NULL")" == "1" ]] || fail "invalid AIHR_PILOT_END_DATE: $pilot_end"
|
[[ "$(pilot_sql_scalar "SELECT DATE('$pilot_end') IS NOT NULL")" == "1" ]] || fail "invalid AIHR_PILOT_END_DATE: $pilot_end"
|
||||||
[[ "$pilot_start" > "$pilot_end" ]] && fail "pilot start date must not be after end date"
|
[[ "$pilot_start" > "$pilot_end" ]] && fail "pilot start date must not be after end date"
|
||||||
|
|
||||||
local scenario_count scenario_service_promotion_count scenario_daily_service_count completed_people pilot_people org_active org_phone_linked org_hire_date_linked org_pilot_people org_pilot_projects org_completed_people audio_table_count prompt_version_schema_count hire_date_schema_count sop_document_count case_stored_count case_unmasked_count
|
local scenario_count scenario_service_promotion_count scenario_daily_service_count rubric_expected_count rubric_five_dimension_count completed_people pilot_people org_active org_phone_linked org_hire_date_linked org_pilot_people org_pilot_projects org_completed_people audio_table_count prompt_version_schema_count hire_date_schema_count sop_document_count case_stored_count case_unmasked_count
|
||||||
local calibration_stats calibration_count calibration_matched sop_total sop_usable sop_pending satisfaction_count satisfaction_average
|
local calibration_stats calibration_count calibration_matched sop_total sop_usable sop_pending satisfaction_count satisfaction_average
|
||||||
local org_identity_cte="
|
local org_identity_cte="
|
||||||
WITH active_org AS (
|
WITH active_org AS (
|
||||||
@@ -127,6 +127,18 @@ check_pilot_samples() {
|
|||||||
# ponytail: presence-only core-flow gate; content quality and SOP applicability still require HR review.
|
# ponytail: presence-only core-flow gate; content quality and SOP applicability still require HR review.
|
||||||
scenario_service_promotion_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_practice_scenario WHERE tenant_id = '$pilot_tenant' AND position = '生活顾问' AND enabled = 1 AND (scenario_type LIKE '%推介%' OR scenario_type LIKE '%增值%' OR scenario_name LIKE '%推介%' OR scenario_name LIKE '%增值%')")"
|
scenario_service_promotion_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_practice_scenario WHERE tenant_id = '$pilot_tenant' AND position = '生活顾问' AND enabled = 1 AND (scenario_type LIKE '%推介%' OR scenario_type LIKE '%增值%' OR scenario_name LIKE '%推介%' OR scenario_name LIKE '%增值%')")"
|
||||||
scenario_daily_service_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_practice_scenario WHERE tenant_id = '$pilot_tenant' AND position = '生活顾问' AND enabled = 1 AND (scenario_type LIKE '%日常%' OR scenario_name LIKE '%日常%')")"
|
scenario_daily_service_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_practice_scenario WHERE tenant_id = '$pilot_tenant' AND position = '生活顾问' AND enabled = 1 AND (scenario_type LIKE '%日常%' OR scenario_name LIKE '%日常%')")"
|
||||||
|
rubric_expected_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_practice_rubric r JOIN aihr_practice_scenario s ON s.tenant_id = r.tenant_id AND s.scenario_code = r.scenario_code WHERE r.tenant_id = '$pilot_tenant' AND s.position = '生活顾问' AND s.enabled = 1 AND r.enabled = 1")"
|
||||||
|
rubric_five_dimension_count="$(pilot_sql_scalar "
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM (
|
||||||
|
SELECT r.rubric_code
|
||||||
|
FROM aihr_practice_rubric r
|
||||||
|
JOIN aihr_practice_scenario s ON s.tenant_id = r.tenant_id AND s.scenario_code = r.scenario_code
|
||||||
|
JOIN aihr_practice_rubric_dimension d ON d.tenant_id = r.tenant_id AND d.rubric_code = r.rubric_code
|
||||||
|
WHERE r.tenant_id = '$pilot_tenant' AND s.position = '生活顾问' AND s.enabled = 1 AND r.enabled = 1
|
||||||
|
GROUP BY r.rubric_code
|
||||||
|
HAVING COUNT(*) = 5 AND ABS(SUM(d.weight) - 1.00) < 0.001
|
||||||
|
) ready_rubrics")"
|
||||||
audio_table_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'aihr_practice_audio'")"
|
audio_table_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'aihr_practice_audio'")"
|
||||||
prompt_version_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_sop_answer_review' AND column_name = 'prompt_version'")"
|
prompt_version_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_sop_answer_review' AND column_name = 'prompt_version'")"
|
||||||
hire_date_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_org_snapshot' AND column_name = 'hire_date'")"
|
hire_date_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_org_snapshot' AND column_name = 'hire_date'")"
|
||||||
@@ -283,10 +295,11 @@ check_pilot_samples() {
|
|||||||
AND s.finished_time < DATE_ADD('${pilot_end}', INTERVAL 1 DAY)
|
AND s.finished_time < DATE_ADD('${pilot_end}', INTERVAL 1 DAY)
|
||||||
)
|
)
|
||||||
SELECT COALESCE(ROUND(AVG(satisfaction_score), 1), 0) FROM formal_sessions WHERE satisfaction_score IS NOT NULL")"
|
SELECT COALESCE(ROUND(AVG(satisfaction_score), 1), 0) FROM formal_sessions WHERE satisfaction_score IS NOT NULL")"
|
||||||
echo "OK: pilot gates window=${pilot_start}..${pilot_end} scenarios=${scenario_count}/12 core_flow=service_promotion:${scenario_service_promotion_count} daily_service:${scenario_daily_service_count} sop_docs=${sop_document_count}/5 stored_cases=${case_stored_count}/20 case_unmasked=${case_unmasked_count} ten_sessions=${completed_people}/${pilot_people} org_identity=${org_phone_linked}/${org_active} org_hire_date=${org_hire_date_linked}/${org_active} org_pilot=${org_pilot_people}/20 projects=${org_pilot_projects}/1-2 org_ten_sessions=${org_completed_people}/${org_pilot_people} calibration=${calibration_count}/20 consistency=${calibration_matched}/${calibration_count} sop_usable=${sop_usable}/${sop_total} sop_pending=${sop_pending} satisfaction=${satisfaction_average}/5 responses=${satisfaction_count} audio_schema=${audio_table_count} prompt_version_schema=${prompt_version_schema_count} hire_date_schema=${hire_date_schema_count}"
|
echo "OK: pilot gates window=${pilot_start}..${pilot_end} scenarios=${scenario_count}/12 rubric_five_dimensions=${rubric_five_dimension_count}/${rubric_expected_count} core_flow=service_promotion:${scenario_service_promotion_count} daily_service:${scenario_daily_service_count} sop_docs=${sop_document_count}/5 stored_cases=${case_stored_count}/20 case_unmasked=${case_unmasked_count} ten_sessions=${completed_people}/${pilot_people} org_identity=${org_phone_linked}/${org_active} org_hire_date=${org_hire_date_linked}/${org_active} org_pilot=${org_pilot_people}/20 projects=${org_pilot_projects}/1-2 org_ten_sessions=${org_completed_people}/${org_pilot_people} calibration=${calibration_count}/20 consistency=${calibration_matched}/${calibration_count} sop_usable=${sop_usable}/${sop_total} sop_pending=${sop_pending} satisfaction=${satisfaction_average}/5 responses=${satisfaction_count} audio_schema=${audio_table_count} prompt_version_schema=${prompt_version_schema_count} hire_date_schema=${hire_date_schema_count}"
|
||||||
|
|
||||||
[[ "${AIHR_PILOT_STRICT:-false}" != "true" ]] || {
|
[[ "${AIHR_PILOT_STRICT:-false}" != "true" ]] || {
|
||||||
(( scenario_count >= 12 )) || fail "pilot enabled scenarios ${scenario_count}/12"
|
(( scenario_count >= 12 )) || fail "pilot enabled scenarios ${scenario_count}/12"
|
||||||
|
(( rubric_expected_count >= scenario_count && rubric_five_dimension_count >= rubric_expected_count )) || fail "pilot five-dimension rubrics ${rubric_five_dimension_count}/${rubric_expected_count}"
|
||||||
(( scenario_service_promotion_count > 0 )) || fail "pilot service-promotion scenarios missing"
|
(( scenario_service_promotion_count > 0 )) || fail "pilot service-promotion scenarios missing"
|
||||||
(( scenario_daily_service_count > 0 )) || fail "pilot daily-service scenarios missing"
|
(( scenario_daily_service_count > 0 )) || fail "pilot daily-service scenarios missing"
|
||||||
(( sop_document_count >= 5 )) || fail "pilot residential SOP documents ${sop_document_count}/5"
|
(( sop_document_count >= 5 )) || fail "pilot residential SOP documents ${sop_document_count}/5"
|
||||||
@@ -484,6 +497,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/service/AihrPracticeSeedService.java "mobileCompletedCount"
|
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/service/AihrPracticeSeedService.java "响应时效"
|
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "响应时效"
|
||||||
|
contains backend/script/sql/aihr_practice_mysql8.sql "task_completion"
|
||||||
|
contains backend/script/sql/update/aihr_20260714_practice_five_dimensions_mysql8.sql "aihr_brd_rubric_codes"
|
||||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java "MistakeAggregateResponse"
|
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/pages/supervisor/team/index.vue "团队错题本"
|
||||||
contains mobile-uni/src/types/api.ts "PracticeMistakeAggregate"
|
contains mobile-uni/src/types/api.ts "PracticeMistakeAggregate"
|
||||||
|
|||||||
Reference in New Issue
Block a user