From cd4e930832dc3dc6da2f765e03c95d406ef87b1d Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 05:16:42 +0800 Subject: [PATCH] fix(case): restrict mobile case writes --- .../aihr/controller/AihrCaseController.java | 19 +++++++++++ .../dromara/aihr/service/AihrCaseService.java | 32 +++++++++++++++++++ .../aihr/service/AihrCaseServiceTest.java | 10 ++++++ docs/BRD_IMPLEMENTATION_AUDIT.md | 2 +- mobile-uni/src/pages/user/cases/index.vue | 2 +- scripts/demo-check.sh | 2 ++ 6 files changed, 65 insertions(+), 2 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java index faabfc78..84892cff 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java @@ -38,16 +38,25 @@ public class AihrCaseController { @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public R upload(@RequestPart("file") MultipartFile file, @RequestParam(value = "projectExtOrgId", required = false) String projectExtOrgId) { + if (!canManageCases()) { + return R.fail("仅项目负责人或主管可提交案例素材"); + } return R.ok(caseService.upload(file, projectExtOrgId, currentProjectScopes())); } @PostMapping("/organize") public R organize(@RequestBody OrganizeRequest request) { + if (!canManageCases()) { + return R.fail("仅项目负责人或主管可整理案例素材"); + } return R.ok(caseService.organize(request, currentProjectScopes())); } @PostMapping("/curate") public R curate(@RequestBody CurateRequest request) { + if (!canManageCases()) { + return R.fail("仅项目负责人或主管可入库案例"); + } return R.ok(caseService.curate(request, currentProjectScopes())); } @@ -72,4 +81,14 @@ public class AihrCaseController { } return caseService.projectScopesForAppUser(loginUser.getUsername()); } + + private boolean canManageCases() { + LoginUser loginUser = LoginHelper.getLoginUser(); + if (loginUser != null && UserType.SYS_USER.getUserType().equals(loginUser.getUserType())) { + return true; + } + return loginUser != null + && UserType.APP_USER.getUserType().equals(loginUser.getUserType()) + && caseService.isCaseContributor(loginUser.getUsername()); + } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java index e9bbff45..29d9654a 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java @@ -181,6 +181,31 @@ public class AihrCaseService { } } + public boolean isCaseContributor(String identity) { + String username = clean(identity); + if (username.isBlank()) { + return false; + } + try { + Integer count = jdbcTemplate.queryForObject(""" + SELECT COUNT(*) + FROM aihr_org_snapshot + WHERE tenant_id = '000000' + AND employment_status = 'active' + AND (person_phone = ? OR ext_party_id = ?) + AND (position_level IN ('主管', '项目经理') + OR position_name LIKE '%主管%' + OR position_name LIKE '%经理%' + OR position_name LIKE '%负责人%' + OR position_name LIKE '%组长%') + """, Integer.class, username, username); + return count != null && count > 0; + } catch (DataAccessException e) { + log.warn("resolve case contributor failed: {}", e.getMessage()); + return false; + } + } + private void saveCase(CaseState state, String status, String title, String summary) { ensureCaseTable(); jdbcTemplate.update(""" @@ -305,6 +330,13 @@ public class AihrCaseService { || (!projectScopes.isEmpty() && projectScopes.contains(clean(projectExtOrgId))); } + static boolean isCaseContributorPosition(String positionName, String positionLevel) { + String name = clean(positionName); + String level = clean(positionLevel); + return "主管".equals(level) || "项目经理".equals(level) + || name.contains("主管") || name.contains("经理") || name.contains("负责人") || name.contains("组长"); + } + private String resolveUploadProject(String requestedProject, List projectScopes) { if (projectScopes == null) { return firstNonBlank(requestedProject, ""); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrCaseServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrCaseServiceTest.java index 785fc3e8..270dcf8a 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrCaseServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrCaseServiceTest.java @@ -82,6 +82,16 @@ class AihrCaseServiceTest { assertTrue(AihrCaseService.caseProjectAllowed("P1", null)); } + @Test + @Tag("dev") + void caseContributorPositionRecognizesProjectManagersAndLeads() { + assertTrue(AihrCaseService.isCaseContributorPosition("项目经理", "一线")); + assertTrue(AihrCaseService.isCaseContributorPosition("招商主管", "一线")); + assertTrue(AihrCaseService.isCaseContributorPosition("项目负责人", "一线")); + assertTrue(AihrCaseService.isCaseContributorPosition("普通员工", "主管")); + assertFalse(AihrCaseService.isCaseContributorPosition("普通员工", "一线")); + } + private static class CaseJdbcTemplate extends JdbcTemplate { private Object[] updateArgs = new Object[0]; diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 69ef5770..5785496a 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -18,7 +18,7 @@ | 候选人面试与 HR 复核 | `mobile-uni/src/pages/candidate/*`、`frontend/src/views/recruit/interview.vue` | 工程闭环已在位,需继续保留人工复核证据 | | SOP/RAG/资料处理 | `frontend/src/views/knowledge/*`、`backend/.../AihrSopSeedService.java` | 搜索、引用、反馈、异步处理和评审入口已在位 | | 对练/每日题/主管带教 | `backend/.../AihrPracticeSeedService.java`、`mobile-uni/src/pages/user/practice` | 主闭环已在位;低分再练、错题归因仍需更明确的用户可见闭环 | -| 案例上传/整理/筛选 | `backend/.../AihrCaseService.java`、`mobile-uni/src/pages/user/cases`、`frontend/src/views/knowledge/cases.vue` | 上传与整理已在位,案例状态在内存缺失时可从 `aihr_case_record` 回读恢复;APP 用户的上传、整理、入库和列表现在按 `aihr_org_snapshot` 项目范围校验,缺少正式组织映射时拒绝访问;当前仓库仍没有可用预渲染视频样片资产,也没有稳定的案例详情/学习点模型,不能用假播放器宣称案例学习闭环完成 | +| 案例上传/整理/筛选 | `backend/.../AihrCaseService.java`、`mobile-uni/src/pages/user/cases`、`frontend/src/views/knowledge/cases.vue` | 上传与整理已在位,案例状态在内存缺失时可从 `aihr_case_record` 回读恢复;APP 用户的读取按项目范围校验,写入接口已增加“主管/项目经理/系统用户”门禁,普通员工不能提交、整理或入库案例;当前仓库仍没有可用预渲染视频样片资产,也没有稳定的案例详情/学习点模型,不能用假播放器宣称案例学习闭环完成 | ## 3. 未完成项与优先级 diff --git a/mobile-uni/src/pages/user/cases/index.vue b/mobile-uni/src/pages/user/cases/index.vue index 69f4983b..58a5dc1b 100644 --- a/mobile-uni/src/pages/user/cases/index.vue +++ b/mobile-uni/src/pages/user/cases/index.vue @@ -11,7 +11,7 @@ 案例素材 - 把一线语音整理成可复用培训案例。上传后依次完成 ASR、AI 整理和入库。 + 项目负责人或主管可把一线语音整理成培训案例;其他员工可查看本项目已入库案例。 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 3bb3ffbb..0e90c43b 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -428,6 +428,8 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "仅支持 txt/md/markdown/pdf/doc/docx/xls/xlsx/ppt/pptx/图片文件" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "/embeddings" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "aihr_knowledge_gap" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "仅项目负责人或主管可提交案例素材" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "position_level IN ('主管', '项目经理')" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "prompt_version" contains backend/script/sql/aihr_practice_mysql8.sql "prompt_version" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "review_id"