fix(case): require project scope for uploads
This commit is contained in:
+5
-1
@@ -339,7 +339,11 @@ public class AihrCaseService {
|
|||||||
|
|
||||||
private String resolveUploadProject(String requestedProject, List<String> projectScopes) {
|
private String resolveUploadProject(String requestedProject, List<String> projectScopes) {
|
||||||
if (projectScopes == null) {
|
if (projectScopes == null) {
|
||||||
return firstNonBlank(requestedProject, "");
|
String project = clean(requestedProject);
|
||||||
|
if (project.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("系统用户上传案例必须明确所属项目");
|
||||||
|
}
|
||||||
|
return project;
|
||||||
}
|
}
|
||||||
if (projectScopes.isEmpty()) {
|
if (projectScopes.isEmpty()) {
|
||||||
throw new IllegalArgumentException("当前账号没有可用项目身份,暂不能提交案例");
|
throw new IllegalArgumentException("当前账号没有可用项目身份,暂不能提交案例");
|
||||||
|
|||||||
+11
@@ -15,6 +15,7 @@ import java.util.Optional;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
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.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -82,6 +83,16 @@ class AihrCaseServiceTest {
|
|||||||
assertTrue(AihrCaseService.caseProjectAllowed("P1", null));
|
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
|
@Test
|
||||||
@Tag("dev")
|
@Tag("dev")
|
||||||
void caseContributorPositionRecognizesProjectManagersAndLeads() {
|
void caseContributorPositionRecognizesProjectManagersAndLeads() {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
| 候选人面试与 HR 复核 | `mobile-uni/src/pages/candidate/*`、`frontend/src/views/recruit/interview.vue` | 工程闭环已在位;候选人资料移动端上传/列表现在只接受 APP 候选人本人身份,HR 继续通过独立审核接口处理;需继续保留人工复核证据 |
|
| 候选人面试与 HR 复核 | `mobile-uni/src/pages/candidate/*`、`frontend/src/views/recruit/interview.vue` | 工程闭环已在位;候选人资料移动端上传/列表现在只接受 APP 候选人本人身份,HR 继续通过独立审核接口处理;需继续保留人工复核证据 |
|
||||||
| SOP/RAG/资料处理 | `frontend/src/views/knowledge/*`、`backend/.../AihrSopSeedService.java` | 搜索、引用、反馈、异步处理和评审入口已在位 |
|
| SOP/RAG/资料处理 | `frontend/src/views/knowledge/*`、`backend/.../AihrSopSeedService.java` | 搜索、引用、反馈、异步处理和评审入口已在位 |
|
||||||
| 对练/每日题/主管带教 | `backend/.../AihrPracticeSeedService.java`、`mobile-uni/src/pages/user/practice` | 主闭环已在位;低分再练、错题归因仍需更明确的用户可见闭环 |
|
| 对练/每日题/主管带教 | `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. 未完成项与优先级
|
## 3. 未完成项与优先级
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<span class="status-pill" :class="apiPill.type">{{ apiPill.label }}</span>
|
<span class="status-pill" :class="apiPill.type">{{ apiPill.label }}</span>
|
||||||
<span class="status-pill" :class="flowStatus.type">{{ flowStatus.label }}</span>
|
<span class="status-pill" :class="flowStatus.type">{{ flowStatus.label }}</span>
|
||||||
<el-button @click="router.push('/index')">返回首页</el-button>
|
<el-button @click="router.push('/index')">返回首页</el-button>
|
||||||
<el-button type="danger" @click="audioInput?.click()">上传语音</el-button>
|
<el-button type="danger" :disabled="!projectExtOrgId" @click="openAudioPicker">上传语音</el-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -20,10 +20,39 @@
|
|||||||
<el-icon><Microphone /></el-icon>
|
<el-icon><Microphone /></el-icon>
|
||||||
<h2>上传与转写</h2>
|
<h2>上传与转写</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-box" @click="audioInput?.click()">
|
<div class="project-selector">
|
||||||
|
<label for="case-project">案例所属项目</label>
|
||||||
|
<el-select
|
||||||
|
id="case-project"
|
||||||
|
v-model="projectExtOrgId"
|
||||||
|
class="project-select"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
:loading="projectsLoading"
|
||||||
|
placeholder="先选择项目,再上传案例"
|
||||||
|
>
|
||||||
|
<el-option v-for="project in projectOptions" :key="project.projectCode" :label="project.projectName" :value="project.projectCode" />
|
||||||
|
</el-select>
|
||||||
|
<el-alert
|
||||||
|
v-if="!projectsLoading && !projectOptions.length"
|
||||||
|
title="暂无可用项目,请先在“组织与权限”完成组织同步预检"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="upload-box" :class="{ disabled: !projectExtOrgId }" @click="openAudioPicker">
|
||||||
<el-icon><FolderChecked /></el-icon>
|
<el-icon><FolderChecked /></el-icon>
|
||||||
<strong>{{ audioUploaded ? displayFileName : '选择语音文件' }}</strong>
|
<strong>{{ audioUploaded ? displayFileName : '选择语音文件' }}</strong>
|
||||||
<span>{{ audioUploaded ? '真实语音已完成 ASR 转写。' : '支持 mp3/wav/m4a/webm/ogg,需先配置 ASR 模型。' }}</span>
|
<span>
|
||||||
|
{{
|
||||||
|
audioUploaded
|
||||||
|
? '真实语音已完成 ASR 转写。'
|
||||||
|
: projectExtOrgId
|
||||||
|
? '支持 mp3/wav/m4a/webm/ogg,需先配置 ASR 模型。'
|
||||||
|
: '请先选择案例所属项目。'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="audioUploaded" class="transcript-card">
|
<div v-if="audioUploaded" class="transcript-card">
|
||||||
<span>ASR 转写结果</span>
|
<span>ASR 转写结果</span>
|
||||||
@@ -129,6 +158,7 @@ import {
|
|||||||
type CaseSummaryItem,
|
type CaseSummaryItem,
|
||||||
type CaseTag
|
type CaseTag
|
||||||
} from '@/api/aihr/case';
|
} from '@/api/aihr/case';
|
||||||
|
import { getOrgSnapshot, type OrgProjectOption } from '@/api/aihr/mobile-admin';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -148,6 +178,9 @@ const apiSampleHint = ref('');
|
|||||||
const recordsLoading = ref(false);
|
const recordsLoading = ref(false);
|
||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
const caseDetail = ref<CaseDetailResponse | null>(null);
|
const caseDetail = ref<CaseDetailResponse | null>(null);
|
||||||
|
const projectExtOrgId = ref('');
|
||||||
|
const projectOptions = ref<OrgProjectOption[]>([]);
|
||||||
|
const projectsLoading = ref(false);
|
||||||
|
|
||||||
const displayFileName = computed(() => apiFileName.value || '未选择文件');
|
const displayFileName = computed(() => apiFileName.value || '未选择文件');
|
||||||
const displayTranscript = computed(() => apiTranscript.value || '暂无转写结果');
|
const displayTranscript = computed(() => apiTranscript.value || '暂无转写结果');
|
||||||
@@ -266,7 +299,35 @@ async function handleAudioPicked(event: Event) {
|
|||||||
await uploadAudio(file);
|
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) {
|
async function uploadAudio(file: File) {
|
||||||
|
if (!projectExtOrgId.value) {
|
||||||
|
ElMessage.warning('请先选择案例所属项目');
|
||||||
|
return;
|
||||||
|
}
|
||||||
audioUploaded.value = false;
|
audioUploaded.value = false;
|
||||||
organized.value = false;
|
organized.value = false;
|
||||||
reviewed.value = false;
|
reviewed.value = false;
|
||||||
@@ -279,6 +340,7 @@ async function uploadAudio(file: File) {
|
|||||||
try {
|
try {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('file', file);
|
form.append('file', file);
|
||||||
|
form.append('projectExtOrgId', projectExtOrgId.value);
|
||||||
const { data } = await uploadCaseAudio(form);
|
const { data } = await uploadCaseAudio(form);
|
||||||
caseId.value = data.caseId;
|
caseId.value = data.caseId;
|
||||||
apiMode.value = 'api';
|
apiMode.value = 'api';
|
||||||
@@ -353,6 +415,7 @@ async function storeCase() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadCaseRecords();
|
void loadCaseRecords();
|
||||||
|
void loadProjectOptions();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -444,6 +507,11 @@ onMounted(() => {
|
|||||||
color: #657184;
|
color: #657184;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.el-icon {
|
.el-icon {
|
||||||
color: #d70f20;
|
color: #d70f20;
|
||||||
font-size: 38px;
|
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,
|
.transcript-card,
|
||||||
.sample-card {
|
.sample-card {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|||||||
@@ -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/recruit/interview.vue "建议复试"
|
||||||
contains frontend/src/views/train/practice.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 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/knowledge/sop.vue "训练题已生成"
|
||||||
contains frontend/src/views/system/model/index.vue "调用优先级"
|
contains frontend/src/views/system/model/index.vue "调用优先级"
|
||||||
contains frontend/src/views/system/model/index.vue "提示词模板"
|
contains frontend/src/views/system/model/index.vue "提示词模板"
|
||||||
|
|||||||
Reference in New Issue
Block a user