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 29d9654a..3df83103 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 @@ -339,7 +339,11 @@ public class AihrCaseService { private String resolveUploadProject(String requestedProject, List projectScopes) { if (projectScopes == null) { - return firstNonBlank(requestedProject, ""); + String project = clean(requestedProject); + if (project.isBlank()) { + throw new IllegalArgumentException("系统用户上传案例必须明确所属项目"); + } + return project; } if (projectScopes.isEmpty()) { throw new IllegalArgumentException("当前账号没有可用项目身份,暂不能提交案例"); 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 270dcf8a..31e03eb4 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 @@ -15,6 +15,7 @@ import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -82,6 +83,16 @@ class AihrCaseServiceTest { assertTrue(AihrCaseService.caseProjectAllowed("P1", null)); } + @Test + @Tag("dev") + void systemUploadRequiresExplicitProject() { + AihrCaseService service = new AihrCaseService(null, null, new ObjectMapper(), new CaseJdbcTemplate()); + + assertThrows(IllegalArgumentException.class, () -> service.upload( + new MockMultipartFile("file", "case.webm", "audio/webm", new byte[]{1}), "", null + )); + } + @Test @Tag("dev") void caseContributorPositionRecognizesProjectManagersAndLeads() { diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 81778338..cdfe6395 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` | 工程闭环已在位;候选人资料移动端上传/列表现在只接受 APP 候选人本人身份,HR 继续通过独立审核接口处理;需继续保留人工复核证据 | | 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 用户的读取按项目范围校验,写入接口已增加“主管/项目经理/系统用户”门禁,普通员工不能提交、整理或入库案例;当前仓库仍没有可用预渲染视频样片资产,也没有稳定的案例详情/学习点模型,不能用假播放器宣称案例学习闭环完成 | +| 案例上传/整理/筛选 | `backend/.../AihrCaseService.java`、`mobile-uni/src/pages/user/cases`、`frontend/src/views/knowledge/cases.vue` | 上传与整理已在位,案例状态在内存缺失时可从 `aihr_case_record` 回读恢复;APP 用户的读取按项目范围校验,写入接口已增加“主管/项目经理/系统用户”门禁,普通员工不能提交、整理或入库案例;本轮补上管理端上传的项目选择与后端无项目拒绝,避免案例落成员工不可见的无范围记录;当前仓库仍没有可用预渲染视频样片资产,也没有稳定的案例详情/学习点模型,不能用假播放器宣称案例学习闭环完成 | ## 3. 未完成项与优先级 diff --git a/frontend/src/views/knowledge/cases.vue b/frontend/src/views/knowledge/cases.vue index cc25d587..d64c4786 100644 --- a/frontend/src/views/knowledge/cases.vue +++ b/frontend/src/views/knowledge/cases.vue @@ -10,7 +10,7 @@ {{ apiPill.label }} {{ flowStatus.label }} 返回首页 - 上传语音 + 上传语音 @@ -20,10 +20,39 @@

上传与转写

-
+
+ + + + + +
+
{{ audioUploaded ? displayFileName : '选择语音文件' }} - {{ audioUploaded ? '真实语音已完成 ASR 转写。' : '支持 mp3/wav/m4a/webm/ogg,需先配置 ASR 模型。' }} + + {{ + audioUploaded + ? '真实语音已完成 ASR 转写。' + : projectExtOrgId + ? '支持 mp3/wav/m4a/webm/ogg,需先配置 ASR 模型。' + : '请先选择案例所属项目。' + }} +
ASR 转写结果 @@ -129,6 +158,7 @@ import { type CaseSummaryItem, type CaseTag } from '@/api/aihr/case'; +import { getOrgSnapshot, type OrgProjectOption } from '@/api/aihr/mobile-admin'; const router = useRouter(); @@ -148,6 +178,9 @@ const apiSampleHint = ref(''); const recordsLoading = ref(false); const detailVisible = ref(false); const caseDetail = ref(null); +const projectExtOrgId = ref(''); +const projectOptions = ref([]); +const projectsLoading = ref(false); const displayFileName = computed(() => apiFileName.value || '未选择文件'); const displayTranscript = computed(() => apiTranscript.value || '暂无转写结果'); @@ -266,7 +299,35 @@ async function handleAudioPicked(event: Event) { await uploadAudio(file); } +function openAudioPicker() { + if (!projectExtOrgId.value) { + ElMessage.warning('请先选择案例所属项目'); + return; + } + audioInput.value?.click(); +} + +async function loadProjectOptions() { + projectsLoading.value = true; + try { + const { data } = await getOrgSnapshot({ status: 'active', pageNum: 1, pageSize: 1 }); + projectOptions.value = data?.projects ?? []; + if (projectOptions.value.length === 1) { + projectExtOrgId.value = projectOptions.value[0].projectCode; + } + } catch { + projectOptions.value = []; + ElMessage.warning('项目列表加载失败,请先检查组织同步配置'); + } finally { + projectsLoading.value = false; + } +} + async function uploadAudio(file: File) { + if (!projectExtOrgId.value) { + ElMessage.warning('请先选择案例所属项目'); + return; + } audioUploaded.value = false; organized.value = false; reviewed.value = false; @@ -279,6 +340,7 @@ async function uploadAudio(file: File) { try { const form = new FormData(); form.append('file', file); + form.append('projectExtOrgId', projectExtOrgId.value); const { data } = await uploadCaseAudio(form); caseId.value = data.caseId; apiMode.value = 'api'; @@ -353,6 +415,7 @@ async function storeCase() { onMounted(() => { void loadCaseRecords(); + void loadProjectOptions(); }); @@ -444,6 +507,11 @@ onMounted(() => { color: #657184; cursor: pointer; + &.disabled { + cursor: not-allowed; + opacity: 0.7; + } + .el-icon { color: #d70f20; font-size: 38px; @@ -455,6 +523,22 @@ onMounted(() => { } } +.project-selector { + display: grid; + gap: 8px; + margin-bottom: 12px; + + label { + color: #344255; + font-size: 13px; + font-weight: 600; + } +} + +.project-select { + width: 100%; +} + .transcript-card, .sample-card { margin-top: 12px; diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 9c1f25b3..7aa4604d 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -357,6 +357,9 @@ contains mobile-uni/src/pages/user/learning/index.vue "getPracticePrepCard" contains frontend/src/views/recruit/interview.vue "建议复试" contains frontend/src/views/train/practice.vue "导师改写" contains frontend/src/views/knowledge/cases.vue "真实语音" +contains frontend/src/views/knowledge/cases.vue "案例所属项目" +contains frontend/src/views/knowledge/cases.vue "请先选择案例所属项目" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "系统用户上传案例必须明确所属项目" contains frontend/src/views/knowledge/sop.vue "训练题已生成" contains frontend/src/views/system/model/index.vue "调用优先级" contains frontend/src/views/system/model/index.vue "提示词模板"