fix(aihr): bind SOP reviews to pilot identity
This commit is contained in:
+10
-1
@@ -31,6 +31,7 @@ import org.dromara.aihr.service.AihrUploadQueueService;
|
||||
import org.dromara.common.core.constant.TenantConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -82,7 +83,15 @@ public class AihrSopController {
|
||||
@SaCheckLogin
|
||||
@PostMapping("/search")
|
||||
public R<SearchResponse> search(@RequestBody SearchRequest request) {
|
||||
return R.ok(sopSeedService.search(restrictDemoVerificationSource(request)));
|
||||
return R.ok(sopSeedService.search(restrictDemoVerificationSource(request), currentRequesterExtPartyId()));
|
||||
}
|
||||
|
||||
private String currentRequesterExtPartyId() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser == null || !UserType.APP_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
return "";
|
||||
}
|
||||
return loginUser.getUsername();
|
||||
}
|
||||
|
||||
private SearchRequest restrictDemoVerificationSource(SearchRequest request) {
|
||||
|
||||
+44
-19
@@ -139,24 +139,29 @@ public class AihrSopSeedService {
|
||||
}
|
||||
|
||||
public SearchResponse search(SearchRequest request) {
|
||||
return search(request, "");
|
||||
}
|
||||
|
||||
public SearchResponse search(SearchRequest request, String requesterExtPartyId) {
|
||||
String requestedCategory = request == null ? "" : request.category();
|
||||
SopSeed seed = seeds.getOrDefault(requestedCategory, seeds.get("投诉处理 SOP"));
|
||||
String category = isBlank(requestedCategory) ? seed.category() : requestedCategory;
|
||||
String position = firstNonBlank(request == null ? null : request.position(), "生活顾问");
|
||||
String source = normalizeSearchSource(request == null ? null : request.source());
|
||||
String requester = isBlank(requesterExtPartyId) ? "" : requesterExtPartyId.trim();
|
||||
String queryText = request == null ? "" : Optional.ofNullable(request.queryText()).orElse("").trim();
|
||||
if (isBlank(queryText)) {
|
||||
return withReviewId(noEvidenceResponse("", category), source);
|
||||
return withReviewId(noEvidenceResponse("", category), source, requester);
|
||||
}
|
||||
SearchResponse dbResponse = dbSearch(dbCategory(category), queryText, request == null ? null : request.limit());
|
||||
if (dbResponse != null) {
|
||||
if (isNoEvidenceAnswer(dbResponse.answer())) {
|
||||
recordKnowledgeGap(queryText, category, position, source);
|
||||
}
|
||||
return withReviewId(dbResponse, source);
|
||||
return withReviewId(dbResponse, source, requester);
|
||||
}
|
||||
recordKnowledgeGap(queryText, category, position, source);
|
||||
return withReviewId(noEvidenceResponse(queryText, category), source);
|
||||
return withReviewId(noEvidenceResponse(queryText, category), source, requester);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1853,8 +1858,9 @@ public class AihrSopSeedService {
|
||||
}
|
||||
}
|
||||
|
||||
private SearchResponse withReviewId(SearchResponse response, String source) {
|
||||
Long reviewId = shouldCreateSopReview(response.queryText(), source) ? createSopReview(response, source) : null;
|
||||
private SearchResponse withReviewId(SearchResponse response, String source, String requesterExtPartyId) {
|
||||
Long reviewId = shouldCreateSopReview(response.queryText(), source)
|
||||
? createSopReview(response, source, requesterExtPartyId) : null;
|
||||
return new SearchResponse(
|
||||
response.queryText(),
|
||||
response.category(),
|
||||
@@ -1878,15 +1884,15 @@ public class AihrSopSeedService {
|
||||
static boolean shouldCreateSopReview(String queryText, String source) {
|
||||
return !isBlank(queryText) && !skipSopReview(source);
|
||||
}
|
||||
private Long createSopReview(SearchResponse response, String source) {
|
||||
private Long createSopReview(SearchResponse response, String source, String requesterExtPartyId) {
|
||||
try {
|
||||
ensureSopReviewTable();
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
jdbcTemplate.update(connection -> {
|
||||
PreparedStatement ps = connection.prepareStatement("""
|
||||
INSERT INTO aihr_sop_answer_review
|
||||
(tenant_id, query_text, category, answer_text, reference_text, snippet_count, source, prompt_version, status, create_time, update_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, '待评审', ?, ?)
|
||||
(tenant_id, query_text, category, answer_text, reference_text, snippet_count, source, requester_ext_party_id, prompt_version, status, create_time, update_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, '待评审', ?, ?)
|
||||
""", Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setString(1, tenantId());
|
||||
ps.setString(2, truncate(AihrSensitiveText.forModel(response.queryText()), 1000));
|
||||
@@ -1895,10 +1901,11 @@ public class AihrSopSeedService {
|
||||
ps.setString(5, truncate(AihrSensitiveText.forModel(response.reference()), 1000));
|
||||
ps.setInt(6, response.snippets() == null ? 0 : response.snippets().size());
|
||||
ps.setString(7, normalizeSearchSource(source));
|
||||
ps.setString(8, firstNonBlank(response.promptVersion(), "unknown"));
|
||||
ps.setString(8, requesterExtPartyId.isBlank() ? null : truncate(requesterExtPartyId, 100));
|
||||
ps.setString(9, firstNonBlank(response.promptVersion(), "unknown"));
|
||||
Timestamp now = Timestamp.valueOf(java.time.LocalDateTime.now());
|
||||
ps.setTimestamp(9, now);
|
||||
ps.setTimestamp(10, now);
|
||||
ps.setTimestamp(11, now);
|
||||
return ps;
|
||||
}, keyHolder);
|
||||
Number key = keyHolder.getKey();
|
||||
@@ -1930,10 +1937,18 @@ public class AihrSopSeedService {
|
||||
public SopReviewStats sopReviewStats() {
|
||||
ensureSopReviewTable();
|
||||
return jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*) AS total, COALESCE(SUM(CASE WHEN usable = 1 THEN 1 ELSE 0 END), 0) AS usable
|
||||
FROM aihr_sop_answer_review
|
||||
WHERE tenant_id = ? AND status = '已评审'
|
||||
AND (LOWER(TRIM(COALESCE(category, ''))) = 'sop' OR category LIKE '%SOP%')
|
||||
SELECT COUNT(*) AS total, COALESCE(SUM(CASE WHEN r.usable = 1 THEN 1 ELSE 0 END), 0) AS usable
|
||||
FROM aihr_sop_answer_review r
|
||||
WHERE r.tenant_id = ? AND r.status = '已评审'
|
||||
AND NULLIF(TRIM(r.requester_ext_party_id), '') IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM aihr_org_snapshot o
|
||||
WHERE o.tenant_id = r.tenant_id
|
||||
AND o.employment_status = 'active'
|
||||
AND (o.ext_party_id = r.requester_ext_party_id OR o.person_phone = r.requester_ext_party_id)
|
||||
)
|
||||
AND (LOWER(TRIM(COALESCE(r.category, ''))) = 'sop' OR r.category LIKE '%SOP%')
|
||||
""", (rs, rowNum) -> new SopReviewStats(rs.getInt("total"), rs.getInt("usable")), tenantId());
|
||||
}
|
||||
|
||||
@@ -1944,11 +1959,19 @@ public class AihrSopSeedService {
|
||||
public SopReviewStats sopReviewStats(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
ensureSopReviewTable();
|
||||
return jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*) AS total, COALESCE(SUM(CASE WHEN usable = 1 THEN 1 ELSE 0 END), 0) AS usable
|
||||
FROM aihr_sop_answer_review
|
||||
WHERE tenant_id = ? AND status = '已评审'
|
||||
AND (LOWER(TRIM(COALESCE(category, ''))) = 'sop' OR category LIKE '%SOP%')
|
||||
AND reviewed_time >= ? AND reviewed_time < ?
|
||||
SELECT COUNT(*) AS total, COALESCE(SUM(CASE WHEN r.usable = 1 THEN 1 ELSE 0 END), 0) AS usable
|
||||
FROM aihr_sop_answer_review r
|
||||
WHERE r.tenant_id = ? AND r.status = '已评审'
|
||||
AND NULLIF(TRIM(r.requester_ext_party_id), '') IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM aihr_org_snapshot o
|
||||
WHERE o.tenant_id = r.tenant_id
|
||||
AND o.employment_status = 'active'
|
||||
AND (o.ext_party_id = r.requester_ext_party_id OR o.person_phone = r.requester_ext_party_id)
|
||||
)
|
||||
AND (LOWER(TRIM(COALESCE(r.category, ''))) = 'sop' OR r.category LIKE '%SOP%')
|
||||
AND r.reviewed_time >= ? AND r.reviewed_time < ?
|
||||
""", (rs, rowNum) -> new SopReviewStats(rs.getInt("total"), rs.getInt("usable")),
|
||||
tenantId(), Timestamp.valueOf(startTime), Timestamp.valueOf(endTime));
|
||||
}
|
||||
@@ -1971,6 +1994,7 @@ public class AihrSopSeedService {
|
||||
`reference_text` varchar(1000) DEFAULT NULL COMMENT '引用',
|
||||
`snippet_count` int DEFAULT 0 COMMENT '命中片段数',
|
||||
`source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源',
|
||||
`requester_ext_party_id` varchar(100) DEFAULT NULL COMMENT '提问员工外部主体ID',
|
||||
`prompt_version` varchar(80) DEFAULT NULL COMMENT '答案生成提示词版本',
|
||||
`usable` tinyint DEFAULT NULL COMMENT '人工评审是否可用',
|
||||
`reviewer` varchar(100) DEFAULT NULL COMMENT '评审人',
|
||||
@@ -1984,6 +2008,7 @@ public class AihrSopSeedService {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR SOP问答人工评审';
|
||||
""");
|
||||
ensureColumn("aihr_sop_answer_review", "source", "ALTER TABLE aihr_sop_answer_review ADD COLUMN `source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源' AFTER `snippet_count`");
|
||||
ensureColumn("aihr_sop_answer_review", "requester_ext_party_id", "ALTER TABLE aihr_sop_answer_review ADD COLUMN `requester_ext_party_id` varchar(100) DEFAULT NULL COMMENT '提问员工外部主体ID' AFTER `source`");
|
||||
ensureColumn("aihr_sop_answer_review", "prompt_version", "ALTER TABLE aihr_sop_answer_review ADD COLUMN `prompt_version` varchar(80) DEFAULT NULL COMMENT '答案生成提示词版本' AFTER `source`");
|
||||
sopReviewTableReady = true;
|
||||
}
|
||||
|
||||
+24
-2
@@ -62,8 +62,8 @@ public class AihrSopSeedServiceTest {
|
||||
}
|
||||
String code = Files.readString(source);
|
||||
|
||||
assertTrue(code.contains("LOWER(TRIM(COALESCE(category, ''))) = 'sop'"));
|
||||
assertTrue(code.contains("category LIKE '%SOP%'"));
|
||||
assertTrue(code.contains("LOWER(TRIM(COALESCE(r.category, ''))) = 'sop'"));
|
||||
assertTrue(code.contains("r.category LIKE '%SOP%'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -139,6 +139,28 @@ public class AihrSopSeedServiceTest {
|
||||
assertTrue(code.contains("reviewSearch(id, request, operator)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
public void formalSopSamplesRequireAuthenticatedAppRequesterIdentity() throws Exception {
|
||||
Path controller = Path.of("src/main/java/org/dromara/aihr/controller/AihrSopController.java");
|
||||
if (!Files.exists(controller)) {
|
||||
controller = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java");
|
||||
}
|
||||
Path service = Path.of("src/main/java/org/dromara/aihr/service/AihrSopSeedService.java");
|
||||
if (!Files.exists(service)) {
|
||||
service = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java");
|
||||
}
|
||||
String controllerCode = Files.readString(controller);
|
||||
String serviceCode = Files.readString(service);
|
||||
|
||||
assertTrue(controllerCode.contains("UserType.APP_USER.getUserType()"));
|
||||
assertTrue(controllerCode.contains("currentRequesterExtPartyId()"));
|
||||
assertTrue(serviceCode.contains("requester_ext_party_id"));
|
||||
assertTrue(serviceCode.contains("NULLIF(TRIM(r.requester_ext_party_id), '') IS NOT NULL"));
|
||||
assertTrue(serviceCode.contains("FROM aihr_org_snapshot o"));
|
||||
assertTrue(serviceCode.contains("o.employment_status = 'active'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
public void demoVerificationSourceIsRestrictedToSuperAdmin() throws Exception {
|
||||
|
||||
@@ -237,6 +237,7 @@ CREATE TABLE IF NOT EXISTS `aihr_sop_answer_review` (
|
||||
`reference_text` varchar(1000) DEFAULT NULL COMMENT '引用',
|
||||
`snippet_count` int DEFAULT 0 COMMENT '命中片段数',
|
||||
`source` varchar(50) DEFAULT 'knowledge_search' COMMENT '来源',
|
||||
`requester_ext_party_id` varchar(100) DEFAULT NULL COMMENT '提问员工外部主体ID',
|
||||
`prompt_version` varchar(80) DEFAULT NULL COMMENT '答案生成提示词版本',
|
||||
`usable` tinyint DEFAULT NULL COMMENT '人工评审是否可用',
|
||||
`reviewer` varchar(100) DEFAULT NULL COMMENT '评审人',
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- AIHR SOP 问答提问员工主体迁移(MySQL 8.x)
|
||||
-- 仅补齐结构,不写入业务数据;可重复执行。
|
||||
|
||||
SET @aihr_sop_review_table_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'aihr_sop_answer_review'
|
||||
);
|
||||
SET @aihr_sop_requester_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'aihr_sop_answer_review'
|
||||
AND COLUMN_NAME = 'requester_ext_party_id'
|
||||
);
|
||||
SET @aihr_sop_requester_ddl := IF(
|
||||
@aihr_sop_review_table_exists = 1 AND @aihr_sop_requester_exists = 0,
|
||||
'ALTER TABLE aihr_sop_answer_review ADD COLUMN `requester_ext_party_id` varchar(100) DEFAULT NULL COMMENT ''提问员工外部主体ID'' AFTER `source`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE aihr_sop_requester_stmt FROM @aihr_sop_requester_ddl;
|
||||
EXECUTE aihr_sop_requester_stmt;
|
||||
DEALLOCATE PREPARE aihr_sop_requester_stmt;
|
||||
Reference in New Issue
Block a user