From 0c941ba5780ec541c08c72ccbd5e485de5a6eba9 Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 05:18:54 +0800 Subject: [PATCH] fix(aihr): protect model and knowledge admin APIs --- .../aihr/controller/AihrModelController.java | 14 +++++++++++++ .../aihr/controller/AihrSopController.java | 20 +++++++++++++++++++ docs/BRD_IMPLEMENTATION_AUDIT.md | 2 +- scripts/demo-check.sh | 2 ++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrModelController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrModelController.java index 4c377090..d86399cf 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrModelController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrModelController.java @@ -1,5 +1,7 @@ package org.dromara.aihr.controller; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaMode; import lombok.RequiredArgsConstructor; import org.dromara.aihr.domain.AihrModelDto.ChatRequest; import org.dromara.aihr.domain.AihrModelDto.ChatResponse; @@ -10,6 +12,7 @@ import org.dromara.aihr.domain.AihrModelDto.ProviderRequest; import org.dromara.aihr.domain.AihrModelDto.ProviderResponse; import org.dromara.aihr.domain.AihrModelDto.ProviderStatusRequest; import org.dromara.aihr.service.AihrModelSeedService; +import org.dromara.common.core.constant.TenantConstants; import org.dromara.common.core.domain.R; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -30,54 +33,65 @@ import java.util.List; @RequestMapping("/api/aihr/model") public class AihrModelController { + private static final String HR_OPERATOR_ROLE = "hr_operator"; + private final AihrModelSeedService modelSeedService; + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @GetMapping("/providers") public R> providers() { return R.ok(modelSeedService.providers()); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/providers") public R createProvider(@RequestBody ProviderRequest request) { modelSeedService.createProvider(request); return R.ok(); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PutMapping("/providers/{providerCode}") public R updateProvider(@PathVariable String providerCode, @RequestBody ProviderRequest request) { modelSeedService.updateProvider(providerCode, request); return R.ok(); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PatchMapping("/providers/{providerCode}/status") public R updateProviderStatus(@PathVariable String providerCode, @RequestBody ProviderStatusRequest request) { modelSeedService.updateProviderStatus(providerCode, request); return R.ok(); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @GetMapping("/configs") public R> configs() { return R.ok(modelSeedService.configs()); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/configs") public R createConfig(@RequestBody ConfigRequest request) { modelSeedService.createConfig(request); return R.ok(); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PutMapping("/configs/{id}") public R updateConfig(@PathVariable Long id, @RequestBody ConfigRequest request) { modelSeedService.updateConfig(id, request); return R.ok(); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PatchMapping("/configs/{id}/enabled") public R updateConfigEnabled(@PathVariable Long id, @RequestBody ConfigEnabledRequest request) { modelSeedService.updateConfigEnabled(id, request); return R.ok(); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/chat") public R chat(@RequestBody(required = false) ChatRequest request) { return R.ok(modelSeedService.chat(request)); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java index 86d025d9..8b202cec 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java @@ -1,5 +1,7 @@ package org.dromara.aihr.controller; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaMode; import lombok.RequiredArgsConstructor; import org.dromara.aihr.domain.AihrSopDto.AnswerFeedbackItemResponse; import org.dromara.aihr.domain.AihrSopDto.AnswerFeedbackRequest; @@ -23,6 +25,7 @@ import org.dromara.aihr.domain.AihrSopDto.VectorIndexStatusResponse; import org.dromara.aihr.domain.AihrSopDto.VectorizeResponse; import org.dromara.aihr.service.AihrSopSeedService; import org.dromara.aihr.service.AihrUploadQueueService; +import org.dromara.common.core.constant.TenantConstants; import org.dromara.common.core.domain.R; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -44,9 +47,12 @@ import java.util.List; @RequestMapping("/api/knowledge") public class AihrSopController { + private static final String HR_OPERATOR_ROLE = "hr_operator"; + private final AihrSopSeedService sopSeedService; private final AihrUploadQueueService uploadQueueService; + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/doc/upload-async") public R uploadAsync(@RequestPart("file") MultipartFile file, @RequestParam(value = "category", required = false) String category, @@ -54,12 +60,14 @@ public class AihrSopController { return R.ok(uploadQueueService.enqueue(file, category, batchId)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @GetMapping("/doc/upload-items") public R> uploadItems(@RequestParam(value = "batchId", required = false) String batchId, @RequestParam(value = "limit", required = false, defaultValue = "50") int limit) { return R.ok(uploadQueueService.items(batchId, limit)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/doc/upload-items/{id}/retry") public R retryUploadItem(@PathVariable Long id) { return R.ok(uploadQueueService.retry(id)); @@ -81,18 +89,21 @@ public class AihrSopController { return R.ok(sopSeedService.answerFeedback(request)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @GetMapping("/answer-feedback") public R> answerFeedbackList(@RequestParam(value = "status", required = false, defaultValue = "待复核") String status, @RequestParam(value = "limit", required = false, defaultValue = "50") int limit) { return R.ok(sopSeedService.answerFeedbackList(status, limit)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/answer-feedback/{id}/review") public R reviewAnswerFeedback(@PathVariable Long id) { AnswerFeedbackReviewResponse response = sopSeedService.reviewAnswerFeedback(id); return response == null ? R.fail("答案反馈记录不存在") : R.ok(response); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/reviews/{id}") public R review(@PathVariable Long id, @RequestBody(required = false) SopReviewRequest request) { SopReviewResponse response = sopSeedService.reviewSearch(id, request); @@ -104,46 +115,55 @@ public class AihrSopController { return R.ok(sopSeedService.summaryCard(request)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/doc/upload") public R uploadDoc(@RequestPart("file") MultipartFile file, @RequestParam("category") String category) { return R.ok(sopSeedService.uploadDoc(file, category)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/doc/import-local") public R importLocalDocs(@RequestBody LocalImportRequest request) { return R.ok(sopSeedService.importLocalDocs(request)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/doc/import-local-task") public R startLocalImportTask(@RequestBody LocalImportRequest request) { return R.ok(sopSeedService.startLocalImportTask(request)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/doc/vectorize-missing") public R vectorizeMissing() { return R.ok(sopSeedService.vectorizeMissing()); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @GetMapping("/doc/vector-index-status") public R vectorIndexStatus() { return R.ok(sopSeedService.vectorIndexStatus()); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/doc/rebuild-vector-index") public R rebuildVectorIndex() { return R.ok(sopSeedService.rebuildVectorIndex()); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @GetMapping("/doc/import-tasks") public R> localImportTasks() { return R.ok(sopSeedService.localImportTasks()); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/doc/import-tasks/{id}/cancel") public R cancelLocalImportTask(@PathVariable Long id) { return R.ok(sopSeedService.cancelLocalImportTask(id)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @GetMapping("/processing/overview") public R processingOverview() { return R.ok(sopSeedService.processingOverview()); diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 5785496a..14ea9da3 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -40,7 +40,7 @@ | 智能错题本 | 3.1、4.4、TechSpec P1 | 已补员工本人错题本和主管 `/api/aihr/mobile/practice/team` 内的 `mistakes` 聚合:按当前项目权限统计场景、归因、次数、影响人数、平均分和最近发生时间;员工端可跳转待训练 retry assignment,retry 已完成时改为显示“已完成”,不再重复引导进入已完成任务;不新增独立错题事实表 | 继续用正式组织快照和试点窗口验证主管只看到本项目数据;后续可在真实错题量上来后补趋势/导出,不把本地演示样本当正式指标 | | 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 已能从 `aihr_case_record` 恢复转写案例,且新增按项目范围校验的 `GET /api/knowledge/case/records/{caseId}`,员工端可查看持久化转写、摘要和状态;但仍没有稳定 media URL、学习点、主管点评和案例视频资产 | 先由业务提供样片或正式媒体 URL,并确定案例详情、学习点、主管点评字段,再扩持久化契约和权限;没有真实媒体 URL 时不造播放按钮 | | 案例库质量与数量 | L1-B、4.6/4.7 | 上传/整理/筛选路径已有;“至少20条脱敏案例”和每周每项目一条需要正式数据证明,当前也没有可用于一期样片验收的真实视频资产 | 先按项目窗口采集并脱敏检查,再补案例统计/学习详情导出;不使用空壳视频或演示 seed 冒充 | -| RAG 可用率与知识治理 | 4.7、L1-C、G3 | 搜索、引用、反馈、知识缺口和人工评审入口已有;员工反馈保存同一次搜索的 `reviewId`,SOP 评审记录和搜索响应现在保留答案生成 `promptVersion`;住宅核心流程覆盖率与可用率仍需正式评审数据,不能宣称完成逐问答全链路追溯 | 后续由业务确认模型/检索批次的正式版本口径,再补检索批次与模型版本;当前先用 `reviewId + promptVersion + query + fragment` 做可追溯统计,不伪造正式可用率 | +| RAG 可用率与知识治理 | 4.7、L1-C、G3 | 搜索、引用、反馈、知识缺口和人工评审入口已有;员工反馈保存同一次搜索的 `reviewId`,SOP 评审记录和搜索响应现在保留答案生成 `promptVersion`;知识库上传、导入、向量重建、评审队列和模型配置管理接口已收紧为 `superadmin/hr_operator`;住宅核心流程覆盖率与可用率仍需正式评审数据,不能宣称完成逐问答全链路追溯 | 后续由业务确认模型/检索批次的正式版本口径,再补检索批次与模型版本;当前先用 `reviewId + promptVersion + query + fragment` 做可追溯统计,不伪造正式可用率 | | 方言语音与多模态训练验收 | 4.3.2、4.3.3、L1-C | ASR/TTS、音频 OSS 留痕和文本降级链路已有;但当前没有四川话/粤语等方言供应商配置、样本集和准确率/响应时延证据,也没有把图像/视频训练输入纳入一期验收口径 | 先确定一期方言厂商和可接受指标,用真实短音频做至少一条方言路径回归;图像/视频输入另按场景与合规边界评审,不用通用文本链路冒充完成 | ### P2/P3:明确暂缓,不应混入当前修复 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 0e90c43b..e670065a 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -386,6 +386,8 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service contains backend/script/sql/aihr_practice_mysql8.sql "aihr_practice_calibration" contains frontend/src/api/aihr/practice.ts '/api/train/practice/scenarios/${id}/enabled' contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java "/scenarios/{id}/enabled" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrModelController.java "@SaCheckRole" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java "@SaCheckRole" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrSopDto.java "String position" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java "/position-sop" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java 'firstNonBlank(position, "生活顾问")'