feat(aihr): add pilot satisfaction evidence loop

This commit is contained in:
2026-07-14 04:13:02 +08:00
parent 22bb2cab58
commit d3552cc22d
10 changed files with 201 additions and 14 deletions
@@ -16,6 +16,8 @@ import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
import org.dromara.aihr.domain.AihrPracticeDto.MistakeResponse;
import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest;
import org.dromara.aihr.domain.AihrPracticeDto.ReviewDetailResponse;
import org.dromara.aihr.domain.AihrPracticeDto.SatisfactionRequest;
import org.dromara.aihr.domain.AihrPracticeDto.SatisfactionResponse;
import org.dromara.aihr.domain.vo.AihrMobileDto.HomeResponse;
import org.dromara.aihr.domain.vo.AihrMobileDto.IdentityResponse;
import org.dromara.aihr.service.AihrOrgSyncService;
@@ -156,6 +158,12 @@ public class AihrMobileController {
return response == null ? R.fail("每日三题不存在") : R.ok(response);
}
@PostMapping("/practice/satisfaction")
public R<SatisfactionResponse> submitSatisfaction(@RequestBody SatisfactionRequest request) {
SatisfactionResponse response = mobileSeedService.submitSatisfaction(request, ownMobileExtPartyId(null));
return response == null ? R.fail("满意度提交失败,请确认训练已完成且评分为1-5分") : R.ok(response);
}
private static String ownMobileExtPartyId(String requested) {
String username = currentAppUsername();
return username.isBlank() ? requested : username;
@@ -136,6 +136,12 @@ public final class AihrPracticeDto {
) {
}
public record SatisfactionRequest(String sessionId, Integer score, String comment) {
}
public record SatisfactionResponse(String sessionId, Integer score, String comment, String submittedAt) {
}
public record DimensionResponse(String label, Object value, String note) {
}
@@ -12,6 +12,8 @@ import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
import org.dromara.aihr.domain.AihrPracticeDto.MistakeResponse;
import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest;
import org.dromara.aihr.domain.AihrPracticeDto.ReviewDetailResponse;
import org.dromara.aihr.domain.AihrPracticeDto.SatisfactionRequest;
import org.dromara.aihr.domain.AihrPracticeDto.SatisfactionResponse;
import org.dromara.aihr.domain.vo.AihrMobileDto.HomeResponse;
import org.dromara.aihr.domain.vo.AihrMobileDto.MetricResponse;
import org.dromara.aihr.domain.vo.AihrMobileDto.MiniStatResponse;
@@ -98,6 +100,10 @@ public class AihrMobileSeedService {
return practiceSeedService.markReviewed(id, request, supervisorExtPartyId);
}
public SatisfactionResponse submitSatisfaction(SatisfactionRequest request, String extPartyId) {
return practiceSeedService.submitSatisfaction(request, extPartyId);
}
public CompetencyResponse profile(String extPartyId) {
return practiceSeedService.competency(extPartyId);
}
@@ -36,6 +36,8 @@ import org.dromara.aihr.domain.AihrPracticeDto.RubricSaveRequest;
import org.dromara.aihr.domain.AihrPracticeDto.ScenarioEnabledRequest;
import org.dromara.aihr.domain.AihrPracticeDto.ScenarioResponse;
import org.dromara.aihr.domain.AihrPracticeDto.ScenarioSaveRequest;
import org.dromara.aihr.domain.AihrPracticeDto.SatisfactionRequest;
import org.dromara.aihr.domain.AihrPracticeDto.SatisfactionResponse;
import org.dromara.aihr.domain.AihrPracticeDto.StartRequest;
import org.dromara.aihr.domain.AihrPracticeDto.StartResponse;
import org.dromara.aihr.domain.AihrPracticeDto.TurnRequest;
@@ -176,6 +178,8 @@ public class AihrPracticeSeedService {
(SELECT COUNT(*) FROM formal_sessions WHERE status = '已复盘') AS reviewed_count,
(SELECT COUNT(*) FROM formal_sessions WHERE total_score < 80 OR status = '待复盘') AS alert_count,
COALESCE((SELECT AVG(total_score) FROM formal_sessions), 0) AS average_score,
(SELECT COUNT(*) FROM formal_sessions WHERE satisfaction_score IS NOT NULL) AS satisfaction_count,
COALESCE((SELECT AVG(satisfaction_score) FROM formal_sessions WHERE satisfaction_score IS NOT NULL), 0) AS satisfaction_average,
(SELECT COUNT(*) FROM calibration_rows) AS calibration_count,
COALESCE((SELECT SUM(CASE
WHEN (CASE WHEN original_score < 60 THEN 0 WHEN original_score < 75 THEN 1 WHEN original_score < 90 THEN 2 ELSE 3 END)
@@ -188,6 +192,7 @@ public class AihrPracticeSeedService {
FROM safe_org o WHERE o.ext_party_id = s.formal_ext_party_id) AS project_codes,
s.session_id, s.trainee_name, s.scenario_name, s.total_score, s.status,
s.finished_time, s.summary, s.review_advice, s.incentive_point,
s.satisfaction_score, s.satisfaction_comment,
s.total_score AS ai_score,
c.original_score AS calibration_original_score,
c.corrected_score AS human_score,
@@ -1005,6 +1010,8 @@ public class AihrPracticeSeedService {
rs.getInt("reviewed_count"),
rs.getInt("alert_count"),
rs.getDouble("average_score"),
rs.getInt("satisfaction_count"),
rs.getDouble("satisfaction_average"),
rs.getInt("calibration_count"),
rs.getInt("calibration_matched")
), TENANT_ID, startTime, endTime);
@@ -1027,6 +1034,7 @@ public class AihrPracticeSeedService {
boolean calibrationReady = metrics.calibrationCount() >= 20
&& metrics.calibrationMatched() * 100 >= metrics.calibrationCount() * 70;
boolean sopReady = sopStats.total() > 0 && sopStats.usable() * 100 >= sopStats.total() * 80;
boolean satisfactionReady = metrics.satisfactionCount() > 0 && metrics.satisfactionAverage() >= 4.0D;
StringBuilder csv = new StringBuilder();
appendCsvRow(csv, "metric", "value");
appendCsvRow(csv, "pilot_start_date", startDate);
@@ -1062,9 +1070,13 @@ public class AihrPracticeSeedService {
appendCsvRow(csv, "sop_answer_usable_rate", percent(sopStats.usable(), sopStats.total()));
appendCsvRow(csv, "sop_answer_usable_target", "80.0%");
appendCsvRow(csv, "sop_answer_usable_ready", sopReady);
appendCsvRow(csv, "pilot_metric_ready", metrics.scenarioCount() >= 12 && completionReady && calibrationReady && sopReady);
appendCsvRow(csv, "satisfaction_response_count", metrics.satisfactionCount());
appendCsvRow(csv, "satisfaction_average", String.format(java.util.Locale.ROOT, "%.1f", metrics.satisfactionAverage()));
appendCsvRow(csv, "satisfaction_target", ">=4.0/5");
appendCsvRow(csv, "satisfaction_ready", satisfactionReady);
appendCsvRow(csv, "pilot_metric_ready", metrics.scenarioCount() >= 12 && completionReady && calibrationReady && sopReady && satisfactionReady);
csv.append('\n');
appendCsvRow(csv, "formal_ext_party_id", "project_codes", "session_id", "trainee", "scene", "score", "status", "finished_time", "summary", "review_advice", "incentive_point", "ai_score", "calibration_original_score", "human_score", "calibration_reviewer", "calibration_time", "final_adopted_score");
appendCsvRow(csv, "formal_ext_party_id", "project_codes", "session_id", "trainee", "scene", "score", "status", "finished_time", "summary", "review_advice", "incentive_point", "satisfaction_score", "satisfaction_comment", "ai_score", "calibration_original_score", "human_score", "calibration_reviewer", "calibration_time", "final_adopted_score");
sessionRows.forEach(row -> appendCsvRow(
csv,
row.get("formal_ext_party_id"),
@@ -1078,6 +1090,8 @@ public class AihrPracticeSeedService {
row.get("summary"),
row.get("review_advice"),
row.get("incentive_point"),
row.get("satisfaction_score"),
row.get("satisfaction_comment"),
row.get("ai_score"),
row.get("calibration_original_score"),
row.get("human_score"),
@@ -1216,6 +1230,31 @@ public class AihrPracticeSeedService {
return markReviewed(id, request);
}
@Transactional(rollbackFor = Exception.class)
public SatisfactionResponse submitSatisfaction(SatisfactionRequest request, String extPartyId) {
ensurePracticeTable();
if (request == null || isBlank(request.sessionId()) || isBlank(extPartyId)
|| request.score() == null || request.score() < 1 || request.score() > 5) {
return null;
}
String comment = request.comment() == null ? "" : request.comment().trim();
if (comment.length() > 500) {
comment = comment.substring(0, 500);
}
comment = maskSensitiveText(comment);
Timestamp submittedAt = Timestamp.valueOf(LocalDateTime.now());
int updated = jdbcTemplate.update("""
UPDATE aihr_practice_session
SET satisfaction_score = ?, satisfaction_comment = ?, update_time = ?
WHERE tenant_id = ? AND mode = 'mobile' AND session_id = ? AND ext_party_id = ? AND finished_time IS NOT NULL
""",
request.score(), comment, submittedAt, TENANT_ID, request.sessionId().trim(), extPartyId.trim());
if (updated == 0) {
return null;
}
return new SatisfactionResponse(request.sessionId().trim(), request.score(), comment, submittedAt.toString());
}
public List<AnnotationResponse> saveAnnotations(String sessionId, AnnotationSaveRequest request) {
ensurePracticeTable();
if (isBlank(sessionId)) {
@@ -2902,6 +2941,8 @@ public class AihrPracticeSeedService {
`ai_comment` varchar(1000) DEFAULT NULL COMMENT 'AI点评',
`review_advice` varchar(1000) DEFAULT NULL COMMENT '主管复盘建议',
`incentive_point` int DEFAULT NULL COMMENT '主管人工贡献分',
`satisfaction_score` tinyint DEFAULT NULL COMMENT '员工训练满意度1-5分',
`satisfaction_comment` varchar(500) DEFAULT NULL COMMENT '员工训练满意度意见',
`summary` varchar(1000) DEFAULT NULL COMMENT '摘要',
`dialogue_json` text DEFAULT NULL COMMENT '对练话术JSON',
`annotations_json` text DEFAULT NULL COMMENT '逐句标注JSON',
@@ -2921,6 +2962,8 @@ public class AihrPracticeSeedService {
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("incentive_point", "ALTER TABLE aihr_practice_session ADD COLUMN `incentive_point` int DEFAULT NULL COMMENT '主管人工贡献分' AFTER `review_advice`");
ensurePracticeColumn("satisfaction_score", "ALTER TABLE aihr_practice_session ADD COLUMN `satisfaction_score` tinyint DEFAULT NULL COMMENT '员工训练满意度1-5分' AFTER `incentive_point`");
ensurePracticeColumn("satisfaction_comment", "ALTER TABLE aihr_practice_session ADD COLUMN `satisfaction_comment` varchar(500) DEFAULT NULL COMMENT '员工训练满意度意见' AFTER `satisfaction_score`");
practiceTableReady = true;
}
}
@@ -3450,6 +3493,8 @@ public class AihrPracticeSeedService {
int reviewedCount,
int alertCount,
double averageScore,
int satisfactionCount,
double satisfactionAverage,
int calibrationCount,
int calibrationMatched
) {
@@ -19,6 +19,8 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_session` (
`ai_comment` varchar(1000) DEFAULT NULL COMMENT 'AI点评',
`review_advice` varchar(1000) DEFAULT NULL COMMENT '主管复盘建议',
`incentive_point` int DEFAULT NULL COMMENT '主管人工贡献分',
`satisfaction_score` tinyint DEFAULT NULL COMMENT '员工训练满意度1-5分',
`satisfaction_comment` varchar(500) DEFAULT NULL COMMENT '员工训练满意度意见',
`summary` varchar(1000) DEFAULT NULL COMMENT '摘要',
`dialogue_json` text DEFAULT NULL COMMENT '对练话术JSON',
`annotations_json` text DEFAULT NULL COMMENT '逐句标注JSON',