diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index bafb3c0a..026c5703 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -535,9 +535,9 @@ public class AihrPracticeSeedService { ensureAssignmentTable(); ensureCalibrationTable(); int trainingCount = count("SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile'", TENANT_ID); - int pendingAssignments = count("SELECT COUNT(*) FROM aihr_practice_assignment WHERE tenant_id = ? AND status = '待训练'", TENANT_ID); - int assignmentTotal = trainingCount + pendingAssignments; - boolean completionReady = assignmentTotal > 0 && trainingCount * 100 >= assignmentTotal * 80; + int completedPeople = countCompletedPilotPeople(); + int pilotPeople = countPilotPeople(); + boolean completionReady = pilotPeople > 0 && completedPeople * 100 >= pilotPeople * 80; int reviewed = count("SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND status = '已复盘'", TENANT_ID); CalibrationStats calibrationStats = calibrationStats(); AihrSopSeedService.SopReviewStats sopStats = sopSeedService.sopReviewStats(); @@ -554,7 +554,9 @@ public class AihrPracticeSeedService { StringBuilder csv = new StringBuilder(); appendCsvRow(csv, "metric", "value"); appendCsvRow(csv, "training_count", trainingCount); - appendCsvRow(csv, "completion_rate", percent(trainingCount, assignmentTotal)); + appendCsvRow(csv, "completion_people_count", completedPeople); + appendCsvRow(csv, "completion_people_total", pilotPeople); + appendCsvRow(csv, "completion_rate", percent(completedPeople, pilotPeople)); appendCsvRow(csv, "completion_rate_target", "80.0%"); appendCsvRow(csv, "completion_rate_ready", completionReady); appendCsvRow(csv, "average_score", String.format(java.util.Locale.ROOT, "%.1f", averageScore == null ? 0 : averageScore)); @@ -597,6 +599,34 @@ public class AihrPracticeSeedService { return csv.toString(); } + private int countCompletedPilotPeople() { + return count(""" + SELECT COUNT(*) + FROM ( + SELECT DISTINCT ext_party_id + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' + ) completed_people + """, TENANT_ID); + } + + private int countPilotPeople() { + return count(""" + SELECT COUNT(*) + FROM ( + SELECT ext_party_id + FROM aihr_practice_session + WHERE tenant_id = ? AND mode = 'mobile' + UNION + SELECT ext_party_id + FROM aihr_practice_assignment + WHERE tenant_id = ? + AND status = '待训练' + AND (source <> 'daily' OR DATE(create_time) = CURRENT_DATE()) + ) pilot_people + """, TENANT_ID, TENANT_ID); + } + public ReviewDetailResponse reviewDetail(Long id) { ensurePracticeTable(); if (id == null) { diff --git a/docs/DEMO_ACCEPTANCE.md b/docs/DEMO_ACCEPTANCE.md index 03175f3f..7acd220a 100644 --- a/docs/DEMO_ACCEPTANCE.md +++ b/docs/DEMO_ACCEPTANCE.md @@ -32,7 +32,7 @@ | 4 | 管理端 `/train/practice` | 选择训练记录 → 人工校准评分 → 导出试点 CSV | 导出包含训练次数、完训率、平均分、预警数、主管复盘率、AI/人工分档一致率、SOP 可用率 | | 5 | 模型成本闸门 | 设置 `AIHR_AI_RUNTIME_ENABLED=false` 后重启后端 → 调 `/api/aihr/model/chat` | 返回 `cost-guard-fallback`,chat/asr/tts 走现有文本兜底,不影响训练闭环 | -试点正式验收前,用 `AIHR_PILOT_STRICT=true ./scripts/demo-check.sh` 检查 M5 样本闸门;完训率、校准样本、AI/人工分档一致率和 SOP 可用率未达标时应失败,不用静态演示 marker 代替真实试点数据。 +试点正式验收前,用 `AIHR_PILOT_STRICT=true ./scripts/demo-check.sh` 检查 M5 样本闸门;人员完训率、校准样本、AI/人工分档一致率和 SOP 可用率未达标时应失败,不用静态演示 marker 代替真实试点数据。 ## 录屏兜底 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 6403935b..6bcb244c 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -57,11 +57,22 @@ check_pilot_samples() { return fi - local scenario_count training_count pending_assignments assignment_total calibration_stats calibration_count calibration_matched sop_total sop_usable sop_pending + local scenario_count completed_people pilot_people calibration_stats calibration_count calibration_matched sop_total sop_usable sop_pending scenario_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_practice_scenario WHERE tenant_id = '000000' AND position = '生活顾问' AND enabled = 1")" - training_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = '000000' AND mode = 'mobile'")" - pending_assignments="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_practice_assignment WHERE tenant_id = '000000' AND status = '待训练'")" - assignment_total=$(( training_count + pending_assignments )) + completed_people="$(pilot_sql_scalar "SELECT COUNT(*) FROM (SELECT DISTINCT ext_party_id FROM aihr_practice_session WHERE tenant_id = '000000' AND mode = 'mobile') completed_people")" + pilot_people="$(pilot_sql_scalar " + SELECT COUNT(*) + FROM ( + SELECT ext_party_id + FROM aihr_practice_session + WHERE tenant_id = '000000' AND mode = 'mobile' + UNION + SELECT ext_party_id + FROM aihr_practice_assignment + WHERE tenant_id = '000000' + AND status = '待训练' + AND (source <> 'daily' OR DATE(create_time) = CURRENT_DATE()) + ) pilot_people")" calibration_stats="$(pilot_sql_scalar " SELECT COUNT(*) AS total, COALESCE(SUM(CASE @@ -80,12 +91,12 @@ check_pilot_samples() { sop_total="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_sop_answer_review WHERE tenant_id = '000000' AND status = '已评审'")" sop_usable="$(pilot_sql_scalar "SELECT COALESCE(SUM(CASE WHEN usable = 1 THEN 1 ELSE 0 END), 0) FROM aihr_sop_answer_review WHERE tenant_id = '000000' AND status = '已评审'")" sop_pending="$(pilot_sql_scalar "SELECT COUNT(*) FROM aihr_sop_answer_review WHERE tenant_id = '000000' AND status <> '已评审'")" - echo "OK: pilot gates scenarios=${scenario_count}/12 completion=${training_count}/${assignment_total} calibration=${calibration_count}/20 consistency=${calibration_matched}/${calibration_count} sop_usable=${sop_usable}/${sop_total} sop_pending=${sop_pending}" + echo "OK: pilot gates scenarios=${scenario_count}/12 completion=${completed_people}/${pilot_people} calibration=${calibration_count}/20 consistency=${calibration_matched}/${calibration_count} sop_usable=${sop_usable}/${sop_total} sop_pending=${sop_pending}" [[ "${AIHR_PILOT_STRICT:-false}" != "true" ]] || { (( scenario_count >= 12 )) || fail "pilot enabled scenarios ${scenario_count}/12" - (( assignment_total > 0 )) || fail "pilot completion samples missing" - (( training_count * 100 >= assignment_total * 80 )) || fail "pilot completion rate below 80% (${training_count}/${assignment_total})" + (( pilot_people > 0 )) || fail "pilot completion samples missing" + (( completed_people * 100 >= pilot_people * 80 )) || fail "pilot completion rate below 80% (${completed_people}/${pilot_people})" (( calibration_count >= 20 )) || fail "pilot calibration samples ${calibration_count}/20" (( calibration_matched * 100 >= calibration_count * 70 )) || fail "pilot AI/human band consistency below 70%" (( sop_total > 0 )) || fail "pilot SOP review samples missing"