fix(case): restrict mobile case writes

This commit is contained in:
2026-07-14 05:16:42 +08:00
parent 7175399f50
commit cd4e930832
6 changed files with 65 additions and 2 deletions
@@ -38,16 +38,25 @@ public class AihrCaseController {
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<UploadResponse> 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<OrganizeResponse> organize(@RequestBody OrganizeRequest request) {
if (!canManageCases()) {
return R.fail("仅项目负责人或主管可整理案例素材");
}
return R.ok(caseService.organize(request, currentProjectScopes()));
}
@PostMapping("/curate")
public R<CurateResponse> 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());
}
}
@@ -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<String> projectScopes) {
if (projectScopes == null) {
return firstNonBlank(requestedProject, "");
@@ -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];
+1 -1
View File
@@ -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. 未完成项与优先级
+1 -1
View File
@@ -11,7 +11,7 @@
</button>
<view class="subpage-heading">
<text class="page-title">案例素材</text>
<text class="page-subtitle">把一线语音整理成可复用培训案例。上传后依次完成 ASR、AI 整理和入库。</text>
<text class="page-subtitle">项目负责人或主管可把一线语音整理成培训案例;其他员工可查看本项目已入库案例。</text>
</view>
</view>
+2
View File
@@ -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"