feat(practice): streamline pilot content operations

This commit is contained in:
2026-07-25 12:51:39 +08:00
parent 35b37b07f7
commit fa9d6da975
9 changed files with 380 additions and 24 deletions
@@ -98,7 +98,9 @@ public final class AihrPracticeDto {
String strategy,
String goal,
Boolean enabled,
List<ScenarioRoundResponse> rounds
List<ScenarioRoundResponse> rounds,
String expectedContentVersion,
String expectedContentHash
) {
}
@@ -124,8 +124,10 @@ public class AihrPracticeSeedService {
private static final Set<String> SCENARIO_REVIEW_STATUSES = Set.of("草稿", "待业务审核", "已发布", "已下线");
private static final Set<String> SCENARIO_RISK_LEVELS = Set.of("待评估", "常规", "高风险");
private static final List<String> MANDATORY_DUAL_REVIEW_TERMS = List.of(
"投诉", "客诉", "纠纷", "收费", "催费", "费用", "停车费", "退款", "减免",
"安全", "事故", "消防", "燃气", "漏电", "电梯", "高空", "受伤", "人身伤害", "舆情"
"事故", "消防", "火灾", "起火", "爆炸", "燃气", "漏电", "触电", "带电",
"高空", "困人", "受伤", "人身伤害", "摔倒", "暴力", "肢体冲突",
"危险物品", "化学品", "医疗废弃", "针具", "锐器", "有限空间", "中毒",
"舆情", "赔偿", "退款", "退费", "减免", "免单", "豁免"
);
private static final String UNCLASSIFIED_COMPETENCY = "待归类";
private static final String DEFAULT_CURRICULUM_VERSION = "住宅五岗-v1";
@@ -429,6 +431,7 @@ public class AihrPracticeSeedService {
throw new ServiceException("训练场景不存在或内容未发布");
}
@Transactional(rollbackFor = Exception.class)
public ScenarioResponse saveScenario(ScenarioSaveRequest request) {
ensureScenarioTable();
LocalDateTime now = LocalDateTime.now();
@@ -437,6 +440,20 @@ public class AihrPracticeSeedService {
String position = canonicalPracticePosition(request == null ? null : request.position());
String scenarioType = firstNonBlank(request == null ? null : request.scenarioType(), "投诉处理");
String projectType = firstNonBlank(request == null ? null : request.projectType(), "住宅");
if (request != null && (!isBlank(request.expectedContentVersion()) || !isBlank(request.expectedContentHash()))) {
List<String[]> current = jdbcTemplate.query("""
SELECT content_version, content_hash
FROM aihr_practice_scenario
WHERE tenant_id = ? AND scenario_code = ?
FOR UPDATE
""", (rs, rowNum) -> new String[] {rs.getString("content_version"), rs.getString("content_hash")},
tenantId(), code);
if (current.isEmpty()
|| !Objects.equals(current.get(0)[0], request.expectedContentVersion())
|| !Objects.equals(current.get(0)[1], request.expectedContentHash())) {
throw new ServiceException("训练场景已发生变更,请刷新后重新编辑");
}
}
ScenarioCurriculumMetadata existingCurriculum = existingScenarioCurriculum(code);
String growthLevel = normalizeGrowthLevel(firstNonBlank(request == null ? null : request.growthLevel(),
existingCurriculum == null ? null : existingCurriculum.growthLevel()));
@@ -7653,13 +7670,13 @@ public class AihrPracticeSeedService {
return requiresMandatoryDualReview(content) ? "高风险" : riskLevel;
}
private boolean requiresMandatoryDualReview(String... content) {
static boolean requiresMandatoryDualReviewContent(String... content) {
for (String value : Optional.ofNullable(content).orElse(new String[0])) {
if (isBlank(value)) {
if (value == null || value.isBlank()) {
continue;
}
String normalized = value.trim();
if ("高风险".equals(normalized)) {
if (normalized.contains("高风险")) {
return true;
}
for (String term : MANDATORY_DUAL_REVIEW_TERMS) {
@@ -7671,6 +7688,10 @@ public class AihrPracticeSeedService {
return false;
}
private boolean requiresMandatoryDualReview(String... content) {
return requiresMandatoryDualReviewContent(content);
}
private String normalizeReviewStatus(String value) {
String status = firstNonBlank(value, "草稿");
if (!SCENARIO_REVIEW_STATUSES.contains(status)) {
@@ -513,6 +513,8 @@ public class AihrPracticeSeedServiceTest {
assertFalse(source.contains("WHEN enabled = 1 THEN '已发布' ELSE '待业务审核' END"));
assertTrue(source.contains("content_hash <=> ?"));
assertTrue(source.contains("训练场景已发生变更,请刷新后重新审核"));
assertTrue(source.contains("expectedContentVersion"));
assertTrue(source.contains("训练场景已发生变更,请刷新后重新编辑"));
assertTrue(source.contains("boolean reviewDecision = \"待业务审核\".equals(current)"));
assertTrue(source.contains("SCENARIO_RISK_LEVELS"));
assertTrue(source.contains("approveHighRiskScenario"));
@@ -577,6 +579,16 @@ public class AihrPracticeSeedServiceTest {
assertEquals("已发布", jdbcTemplate.reviewStatus);
}
@Test
public void genericServiceTopicsDoNotRequireDualReviewWithoutMaterialRisk() {
assertFalse(AihrPracticeSeedService.requiresMandatoryDualReviewContent(
"投诉信息完整登记", "费用明细查询", "电梯轿厢日常保洁", "安全确认"
));
assertTrue(AihrPracticeSeedService.requiresMandatoryDualReviewContent("电梯困人信息收集与升级"));
assertTrue(AihrPracticeSeedService.requiresMandatoryDualReviewContent("物业费减免申请"));
assertTrue(AihrPracticeSeedService.requiresMandatoryDualReviewContent("高风险作业"));
}
@Test
public void scenarioCannotPublishUntilContentRiskIsExplicitlyClassified() throws Exception {
HighRiskScenarioJdbcTemplate jdbcTemplate = new HighRiskScenarioJdbcTemplate("待评估");
@@ -800,7 +812,7 @@ public class AihrPracticeSeedServiceTest {
));
assertEquals("待业务审核", jdbcTemplate.reviewStatus);
assertEquals("高风险", jdbcTemplate.riskLevel);
assertEquals("常规", jdbcTemplate.riskLevel);
assertNull(jdbcTemplate.reviewer);
assertNull(jdbcTemplate.reviewerUserId);
assertNull(jdbcTemplate.secondReviewer);