fix(aihr): scope sop data to tenant

This commit is contained in:
2026-07-14 06:40:54 +08:00
parent 20ed2e9431
commit 5c893e832e
2 changed files with 56 additions and 50 deletions
@@ -37,6 +37,7 @@ import org.dromara.aihr.domain.AihrSopDto.UploadResponse;
import org.dromara.aihr.domain.AihrSopDto.VectorIndexStatusResponse;
import org.dromara.aihr.domain.AihrSopDto.VectorizeResponse;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.satoken.utils.LoginHelper;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.AutoDetectParser;
@@ -205,7 +206,7 @@ public class AihrSopSeedService {
firstNonBlank(request == null ? null : request.note(), ""),
Timestamp.valueOf(java.time.LocalDateTime.now()),
Timestamp.valueOf(java.time.LocalDateTime.now()),
TENANT_ID,
tenantId(),
id
);
return updated == 0 ? null : sopReview(id);
@@ -244,7 +245,7 @@ public class AihrSopSeedService {
review_status = VALUES(review_status),
update_time = VALUES(update_time)
""",
TENANT_ID,
tenantId(),
truncate(queryText, 1000),
queryNorm,
fragmentId,
@@ -285,7 +286,7 @@ public class AihrSopSeedService {
rs.getString("review_status"),
formatTimestamp(rs.getTimestamp("create_time")),
formatTimestamp(rs.getTimestamp("update_time"))
), TENANT_ID, reviewStatus, reviewStatus, limitValue);
), tenantId(), reviewStatus, reviewStatus, limitValue);
}
public AnswerFeedbackReviewResponse reviewAnswerFeedback(Long id) {
@@ -299,7 +300,7 @@ public class AihrSopSeedService {
WHERE tenant_id = ? AND id = ?
""",
Timestamp.valueOf(java.time.LocalDateTime.now()),
TENANT_ID,
tenantId(),
id
);
return updated == 0 ? null : new AnswerFeedbackReviewResponse(id, "已复核");
@@ -397,7 +398,7 @@ public class AihrSopSeedService {
rs.getLong("knowledge_id"),
rs.getString("category"),
rs.getString("doc_id")
), TENANT_ID);
), tenantId());
int documents = 0;
int fragments = 0;
@@ -408,7 +409,7 @@ public class AihrSopSeedService {
from aihr_knowledge_fragment
where tenant_id = ? and knowledge_id = ? and doc_id = ?
order by idx asc
""", String.class, TENANT_ID, target.knowledgeId(), target.docId());
""", String.class, tenantId(), target.knowledgeId(), target.docId());
EmbeddingBatch batch = embedFragments(target.knowledgeId(), target.category(), target.docId(), rows);
if (batch.count() > 0) {
documents++;
@@ -452,7 +453,7 @@ public class AihrSopSeedService {
update aihr_knowledge_fragment
set embedding_json = null, embedding_model = null, embedding_time = null, update_time = now()
where tenant_id = ?
""", TENANT_ID);
""", tenantId());
deleteQdrantCollection();
return vectorizeMissing();
}
@@ -492,7 +493,7 @@ public class AihrSopSeedService {
rs.getTimestamp("create_time"),
rs.getTimestamp("update_time"),
rs.getString("remark")
), TENANT_ID);
), tenantId());
} catch (DataAccessException e) {
return List.of();
}
@@ -506,7 +507,7 @@ public class AihrSopSeedService {
update aihr_knowledge_import_task
set status = 4, current_file = '', finished_time = now(), update_time = now(), remark = '用户已取消'
where tenant_id = ? and id = ? and status = 1
""", TENANT_ID, taskId);
""", tenantId(), taskId);
return localImportTask(taskId).orElseThrow(() -> new IllegalArgumentException("导入任务不存在"));
}
@@ -605,7 +606,7 @@ public class AihrSopSeedService {
(tenant_id, directory, category, status, total_files, imported_files, failed_files, current_file, started_time, create_time, update_time, remark)
values (?, ?, ?, 1, ?, 0, 0, '', now(), now(), now(), '任务已启动')
""", Statement.RETURN_GENERATED_KEYS);
ps.setString(1, TENANT_ID);
ps.setString(1, tenantId());
ps.setString(2, plan.directory());
ps.setString(3, plan.category());
ps.setInt(4, plan.files().size());
@@ -633,7 +634,7 @@ public class AihrSopSeedService {
rs.getTimestamp("create_time"),
rs.getTimestamp("update_time"),
rs.getString("remark")
), TENANT_ID, taskId);
), tenantId(), taskId);
return rows.stream().findFirst();
}
@@ -642,7 +643,7 @@ public class AihrSopSeedService {
update aihr_knowledge_import_task
set status = ?, imported_files = ?, failed_files = ?, current_file = ?, update_time = now(), remark = ?
where tenant_id = ? and id = ? and status <> 4
""", status, imported, failed, currentFile, limitRemark(remark), TENANT_ID, taskId);
""", status, imported, failed, currentFile, limitRemark(remark), tenantId(), taskId);
}
private void finishLocalImportTask(long taskId, int status, int imported, int failed, String remark) {
@@ -650,7 +651,7 @@ public class AihrSopSeedService {
update aihr_knowledge_import_task
set status = ?, imported_files = ?, failed_files = ?, current_file = '', finished_time = now(), update_time = now(), remark = ?
where tenant_id = ? and id = ? and status <> 4
""", status, imported, failed, limitRemark(remark), TENANT_ID, taskId);
""", status, imported, failed, limitRemark(remark), tenantId(), taskId);
}
private boolean isLocalImportTaskCanceled(long taskId) {
@@ -659,7 +660,7 @@ public class AihrSopSeedService {
select count(1)
from aihr_knowledge_import_task
where tenant_id = ? and id = ? and status = 4
""", Integer.class, TENANT_ID, taskId);
""", Integer.class, tenantId(), taskId);
return count != null && count > 0;
} catch (DataAccessException e) {
return false;
@@ -740,13 +741,13 @@ public class AihrSopSeedService {
}
deleteQdrantDoc(config.knowledgeId(), docId);
jdbcTemplate.update("delete from aihr_knowledge_fragment where tenant_id = ? and knowledge_id = ? and doc_id = ?", TENANT_ID, config.knowledgeId(), docId);
jdbcTemplate.update("delete from aihr_knowledge_fragment where tenant_id = ? and knowledge_id = ? and doc_id = ?", tenantId(), config.knowledgeId(), docId);
for (int i = 0; i < fragments.size(); i++) {
jdbcTemplate.update("""
insert into aihr_knowledge_fragment
(tenant_id, knowledge_id, idx, doc_id, content, create_time, update_time, remark)
values (?, ?, ?, ?, ?, now(), now(), ?)
""", TENANT_ID, config.knowledgeId(), i + 1, docId, fragments.get(i), "upload:" + fileName);
""", tenantId(), config.knowledgeId(), i + 1, docId, fragments.get(i), "upload:" + fileName);
}
embedFragments(config.knowledgeId(), knowledgeName, docId, fragments);
updateOssInsight(ossId, insight, fingerprint);
@@ -883,7 +884,7 @@ public class AihrSopSeedService {
where tenant_id = ?
order by update_time desc, id desc
limit 50
""", (rs, rowNum) -> new CategoryOption(rs.getString("name"), rs.getString("description")), TENANT_ID);
""", (rs, rowNum) -> new CategoryOption(rs.getString("name"), rs.getString("description")), tenantId());
} catch (DataAccessException e) {
return List.of();
}
@@ -908,7 +909,7 @@ public class AihrSopSeedService {
rs.getString("model_name"),
rs.getString("resolved_api_host"),
rs.getString("resolved_api_key")
), TENANT_ID);
), tenantId());
return rows.stream()
.filter(runtime -> !isBlank(runtime.modelName()) && !isBlank(runtime.baseUrl()))
.findFirst();
@@ -938,7 +939,7 @@ public class AihrSopSeedService {
rs.getString("model_name"),
rs.getString("resolved_api_host"),
rs.getString("resolved_api_key")
), TENANT_ID);
), tenantId());
return rows.stream()
.filter(runtime -> !isBlank(runtime.modelName()) && !isBlank(runtime.baseUrl()))
.findFirst();
@@ -1015,7 +1016,7 @@ public class AihrSopSeedService {
where a.tenant_id = ? and a.id = ?
limit 1
""", (rs, rowNum) -> insightFromExt(rs.getString("ext1"), firstNonBlank(rs.getString("category"), fallbackCategory)),
TENANT_ID, hit.attachId()));
tenantId(), hit.attachId()));
} catch (DataAccessException e) {
return Optional.empty();
}
@@ -1061,7 +1062,7 @@ public class AihrSopSeedService {
rs.getString("doc_id"),
rs.getLong("oss_id"),
rs.getString("match_type")
), fingerprint.fileSha256(), fingerprint.textSha256(), TENANT_ID, fingerprint.fileSha256(), fingerprint.textSha256(), fileName, fingerprint.sizeBytes(), fingerprint.fileSha256(), fingerprint.textSha256());
), fingerprint.fileSha256(), fingerprint.textSha256(), tenantId(), fingerprint.fileSha256(), fingerprint.textSha256(), fileName, fingerprint.sizeBytes(), fingerprint.fileSha256(), fingerprint.textSha256());
} catch (DataAccessException e) {
return List.of();
}
@@ -1071,13 +1072,13 @@ public class AihrSopSeedService {
String docId = isBlank(hit.docId()) ? UUID.randomUUID().toString().replace("-", "") : hit.docId();
if (hit.knowledgeId() != targetKnowledgeId && !isBlank(hit.docId())) {
deleteQdrantDoc(hit.knowledgeId(), hit.docId());
jdbcTemplate.update("delete from aihr_knowledge_fragment where tenant_id = ? and knowledge_id = ? and doc_id = ?", TENANT_ID, hit.knowledgeId(), hit.docId());
jdbcTemplate.update("delete from aihr_knowledge_fragment where tenant_id = ? and knowledge_id = ? and doc_id = ?", tenantId(), hit.knowledgeId(), hit.docId());
}
jdbcTemplate.update("""
update aihr_knowledge_attach
set knowledge_id = ?, oss_id = ?, doc_id = ?, type = ?, status = 1, update_time = now(), remark = 'duplicate:reusing'
where tenant_id = ? and id = ?
""", targetKnowledgeId, hit.ossId(), docId, fileType(fileName), TENANT_ID, hit.attachId());
""", targetKnowledgeId, hit.ossId(), docId, fileType(fileName), tenantId(), hit.attachId());
return docId;
}
@@ -1088,9 +1089,9 @@ public class AihrSopSeedService {
}
if (!isBlank(hit.docId())) {
deleteQdrantDoc(hit.knowledgeId(), hit.docId());
jdbcTemplate.update("delete from aihr_knowledge_fragment where tenant_id = ? and knowledge_id = ? and doc_id = ?", TENANT_ID, hit.knowledgeId(), hit.docId());
jdbcTemplate.update("delete from aihr_knowledge_fragment where tenant_id = ? and knowledge_id = ? and doc_id = ?", tenantId(), hit.knowledgeId(), hit.docId());
}
jdbcTemplate.update("delete from aihr_knowledge_attach where tenant_id = ? and id = ?", TENANT_ID, hit.attachId());
jdbcTemplate.update("delete from aihr_knowledge_attach where tenant_id = ? and id = ?", tenantId(), hit.attachId());
}
}
@@ -1203,7 +1204,7 @@ public class AihrSopSeedService {
insight.tags(),
insight.reason()
);
}, TENANT_ID);
}, tenantId());
} catch (DataAccessException e) {
return List.of();
}
@@ -1578,7 +1579,7 @@ public class AihrSopSeedService {
update aihr_knowledge_fragment
set embedding_json = ?, embedding_model = ?, embedding_time = now(), update_time = now()
where tenant_id = ? and knowledge_id = ? and doc_id = ? and idx = ?
""", embeddings.get(i), runtime.modelName(), TENANT_ID, knowledgeId, docId, i + 1);
""", embeddings.get(i), runtime.modelName(), tenantId(), knowledgeId, docId, i + 1);
}
try {
upsertQdrant(knowledgeId, category, docId, fragments, embeddings, runtime.modelName());
@@ -1612,7 +1613,7 @@ public class AihrSopSeedService {
rs.getString("resolved_api_host"),
rs.getString("resolved_api_key"),
rs.getInt("model_dimension")
), TENANT_ID).stream()
), tenantId()).stream()
.filter(runtime -> configured(runtime.baseUrl(), runtime.modelName(), runtime.apiKey()))
.toList());
} catch (DataAccessException ignored) {
@@ -1722,7 +1723,7 @@ public class AihrSopSeedService {
rs.getString("model_name"),
rs.getString("resolved_api_host"),
rs.getString("resolved_api_key")
), TENANT_ID);
), tenantId());
return rows.stream()
.filter(runtime -> !isBlank(runtime.modelName()) && !isBlank(runtime.baseUrl()) && !isBlank(runtime.apiKey()))
.findFirst();
@@ -1814,7 +1815,7 @@ public class AihrSopSeedService {
(tenant_id, query_text, category, position, source, status, create_time)
VALUES (?, ?, ?, ?, ?, '待补充', ?)
""",
TENANT_ID,
tenantId(),
queryText.trim(),
firstNonBlank(category, "sop"),
firstNonBlank(position, "生活顾问"),
@@ -1858,7 +1859,7 @@ public class AihrSopSeedService {
(tenant_id, query_text, category, answer_text, reference_text, snippet_count, source, prompt_version, status, create_time, update_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, '待评审', ?, ?)
""", Statement.RETURN_GENERATED_KEYS);
ps.setString(1, TENANT_ID);
ps.setString(1, tenantId());
ps.setString(2, truncate(response.queryText(), 1000));
ps.setString(3, firstNonBlank(response.category(), "sop"));
ps.setString(4, truncate(response.answer(), 2000));
@@ -1893,7 +1894,7 @@ public class AihrSopSeedService {
reviewedTime == null ? "" : reviewedTime.toLocalDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")),
firstNonBlank(rs.getString("prompt_version"), "unknown")
);
}, TENANT_ID, id);
}, tenantId(), id);
return rows.isEmpty() ? null : rows.get(0);
}
@@ -1903,7 +1904,7 @@ public class AihrSopSeedService {
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 = '已评审'
""", (rs, rowNum) -> new SopReviewStats(rs.getInt("total"), rs.getInt("usable")), TENANT_ID);
""", (rs, rowNum) -> new SopReviewStats(rs.getInt("total"), rs.getInt("usable")), tenantId());
}
public void prepareSopReviewStats() {
@@ -1918,7 +1919,7 @@ public class AihrSopSeedService {
WHERE tenant_id = ? AND status = '已评审'
AND reviewed_time >= ? AND reviewed_time < ?
""", (rs, rowNum) -> new SopReviewStats(rs.getInt("total"), rs.getInt("usable")),
TENANT_ID, Timestamp.valueOf(startTime), Timestamp.valueOf(endTime));
tenantId(), Timestamp.valueOf(startTime), Timestamp.valueOf(endTime));
}
private void ensureSopReviewTable() {
@@ -2105,7 +2106,7 @@ public class AihrSopSeedService {
WHERE tenant_id = ?
AND query_norm = ?
AND verdict = 'down'
""", Long.class, TENANT_ID, queryNorm));
""", Long.class, tenantId(), queryNorm));
} catch (DataAccessException ignored) {
return Set.of();
}
@@ -2160,7 +2161,7 @@ public class AihrSopSeedService {
rs.getString("content"),
rs.getInt("idx"),
rs.getDouble("score")
), queryText, TENANT_ID, category, category, queryText, limit);
), queryText, tenantId(), category, category, queryText, limit);
}
private List<KnowledgeHit> likeHits(String category, String queryText, int limit) {
@@ -2194,7 +2195,7 @@ public class AihrSopSeedService {
""".formatted(String.join(" + ", scoreParts), String.join(" or ", whereParts));
List<Object> args = new ArrayList<>();
terms.stream().map(AihrSopSeedService::likePattern).forEach(args::add);
args.add(TENANT_ID);
args.add(tenantId());
args.add(category);
args.add(category);
terms.stream().map(AihrSopSeedService::likePattern).forEach(args::add);
@@ -2236,7 +2237,7 @@ public class AihrSopSeedService {
rs.getString("content"),
rs.getInt("idx"),
match.score()
), TENANT_ID, match.docId(), match.idx());
), tenantId(), match.docId(), match.idx());
return rows.stream().findFirst();
}
@@ -2453,7 +2454,7 @@ public class AihrSopSeedService {
firstNonBlank(rs.getString("system_prompt"), SOP_ANSWER_SYSTEM_PROMPT),
firstNonBlank(rs.getString("template"), SOP_ANSWER_USER_TEMPLATE),
"sop_answer:" + firstNonBlank(rs.getString("version"), "v1")
), TENANT_ID);
), tenantId());
if (!rows.isEmpty()) {
return rows.get(0);
}
@@ -2682,7 +2683,7 @@ public class AihrSopSeedService {
rs.getLong("id"),
Math.max(200, rs.getInt("block_size")),
Math.max(0, Math.min(rs.getInt("overlap_char"), 200))
), TENANT_ID, category);
), tenantId(), category);
if (!rows.isEmpty()) {
return rows.get(0);
}
@@ -2695,7 +2696,7 @@ public class AihrSopSeedService {
text_block_size, enable_hybrid, hybrid_alpha, system_prompt, create_time, update_time, remark)
values (?, ?, ?, ?, 120, 5, 0.5, 800, 1, 1.0, ?, now(), now(), 'upload')
""", Statement.RETURN_GENERATED_KEYS);
ps.setString(1, TENANT_ID);
ps.setString(1, tenantId());
ps.setString(2, category);
ps.setString(3, category + " 上传知识库");
ps.setString(4, "\n\n");
@@ -2712,20 +2713,20 @@ public class AihrSopSeedService {
from aihr_knowledge_attach
where tenant_id = ? and knowledge_id = ? and name = ?
limit 1
""", String.class, TENANT_ID, knowledgeId, fileName);
""", String.class, tenantId(), knowledgeId, fileName);
String docId = rows.isEmpty() || rows.get(0) == null ? UUID.randomUUID().toString().replace("-", "") : rows.get(0);
if (rows.isEmpty()) {
jdbcTemplate.update("""
insert into aihr_knowledge_attach
(tenant_id, knowledge_id, oss_id, doc_id, name, type, status, create_time, update_time, remark)
values (?, ?, ?, ?, ?, ?, 1, now(), now(), 'oss-upload:parsing')
""", TENANT_ID, knowledgeId, ossId, docId, fileName, fileType(fileName));
""", tenantId(), knowledgeId, ossId, docId, fileName, fileType(fileName));
} else {
jdbcTemplate.update("""
update aihr_knowledge_attach
set oss_id = ?, doc_id = ?, type = ?, status = 1, update_time = now(), remark = 'oss-upload:parsing'
where tenant_id = ? and knowledge_id = ? and name = ?
""", ossId, docId, fileType(fileName), TENANT_ID, knowledgeId, fileName);
""", ossId, docId, fileType(fileName), tenantId(), knowledgeId, fileName);
}
return docId;
}
@@ -2735,7 +2736,7 @@ public class AihrSopSeedService {
update aihr_knowledge_attach
set status = ?, update_time = now(), remark = ?
where tenant_id = ? and knowledge_id = ? and doc_id = ?
""", status, limitRemark(remark), TENANT_ID, knowledgeId, docId);
""", status, limitRemark(remark), tenantId(), knowledgeId, docId);
}
private void upsertQdrant(long knowledgeId, String category, String docId, List<String> fragments, List<String> embeddings, String modelName) throws Exception {
@@ -2756,7 +2757,7 @@ public class AihrSopSeedService {
continue;
}
ObjectNode payload = objectMapper.createObjectNode();
payload.put("tenant_id", TENANT_ID);
payload.put("tenant_id", tenantId());
payload.put("knowledge_id", knowledgeId);
payload.put("category", category);
payload.put("doc_id", docId);
@@ -2849,7 +2850,7 @@ public class AihrSopSeedService {
model,
dimension
);
}, TENANT_ID);
}, tenantId());
} catch (DataAccessException e) {
return new VectorDbStats(0, 0, "", null);
}
@@ -2922,7 +2923,7 @@ public class AihrSopSeedService {
private ObjectNode qdrantFilter(Long knowledgeId, String docId, String category) {
ObjectNode filter = objectMapper.createObjectNode();
ArrayNode must = filter.putArray("must");
must.add(qdrantMatch("tenant_id", TENANT_ID));
must.add(qdrantMatch("tenant_id", tenantId()));
if (knowledgeId != null) {
must.add(qdrantMatch("knowledge_id", knowledgeId));
}
@@ -3243,6 +3244,10 @@ public class AihrSopSeedService {
return value.substring(0, maxLength);
}
private String tenantId() {
return firstNonBlank(LoginHelper.getTenantId(), TENANT_ID);
}
private static String normalizeBaseUrl(String baseUrl) {
String normalized = baseUrl == null ? "" : baseUrl.trim();
while (normalized.endsWith("/")) {
@@ -3263,8 +3268,8 @@ public class AihrSopSeedService {
return firstNonBlank(System.getProperty("aihr.qdrant.apiKey"), System.getenv("AIHR_QDRANT_API_KEY"), "");
}
private static String qdrantPointId(long knowledgeId, String docId, int idx) {
return UUID.nameUUIDFromBytes((TENANT_ID + ":" + knowledgeId + ":" + docId + ":" + idx).getBytes(StandardCharsets.UTF_8)).toString();
private String qdrantPointId(long knowledgeId, String docId, int idx) {
return UUID.nameUUIDFromBytes((tenantId() + ":" + knowledgeId + ":" + docId + ":" + idx).getBytes(StandardCharsets.UTF_8)).toString();
}
private static boolean ok(int statusCode) {
+1
View File
@@ -94,3 +94,4 @@
- 2026-07-14 门禁合同复核:`scripts/tests/demo-check-pilot-gates.test.sh` 通过,确认严格预检覆盖批次日期、唯一在职身份、1–2 个项目、每人至少 10 次训练、80% 完训、20 条校准、AI/人工分档一致率、SOP 可用率、满意度和音频/RAG schema;`scripts/tests/pilot-export-contract.test.mjs` 5/5 通过,AIHR 案例/对练定向测试 31/31 通过,`mobile-uni` 类型检查与 49 个单测通过。当前没有新增可安全实现的门禁缺口,剩余阻塞仍是正式组织映射、正式试点数据、视频样片和方言样本等外部输入。
- 2026-07-14 租户边界复核:AI 面试结果、候选人入职主体关联、候选人补充资料、模型配置和案例记录的查询、写入、组织身份解析均改为使用当前登录租户;无登录上下文时仅保留本地开发默认租户 `000000` 回退。该修复已覆盖案例服务的列表、详情、项目范围、贡献者校验、保存和持久化回读路径,仍需在正式多租户环境做隔离回归。
- 2026-07-14 仪表盘租户边界复核:管理端首页的面试、训练、知识库、案例、候选资料、组织和模型统计改为按当前登录租户查询;组织同步写入和组织快照查询也改为当前登录租户;无登录上下文时保留本地开发默认租户回退。尚未把批量知识处理队列和完整 SOP 服务的全部租户调用统一替换,后续继续按数据敏感度分批审查。
- 2026-07-14 SOP/RAG 租户边界复核:知识库搜索、知识缺口、SOP 问答评审、答案反馈、异步导入任务、文档/片段/向量写入及 Qdrant tenant filter 改为当前登录租户;无登录上下文时回退本地开发租户 `000000`。定向 AIHR 测试 48/48 通过;正式多租户检索隔离仍需在测试环境做跨租户回归,批量上传队列仍待单独审查。