fix(aihr): align app case permissions with brd
This commit is contained in:
+6
@@ -9,6 +9,7 @@ import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CapabilitiesResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.DetailResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeResponse;
|
||||
@@ -83,6 +84,11 @@ public class AihrCaseController {
|
||||
return detail == null ? R.fail("案例不存在或无权访问") : R.ok(detail);
|
||||
}
|
||||
|
||||
@GetMapping("/capabilities")
|
||||
public R<CapabilitiesResponse> capabilities() {
|
||||
return R.ok(new CapabilitiesResponse(canManageCases(), canViewCases()));
|
||||
}
|
||||
|
||||
private List<String> currentProjectScopes() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser != null && UserType.SYS_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
|
||||
+3
@@ -26,6 +26,9 @@ public final class AihrCaseDto {
|
||||
public record CurateResponse(List<String> selectedCaseIds, List<RecordResponse> records, String sampleHint) {
|
||||
}
|
||||
|
||||
public record CapabilitiesResponse(boolean canSubmit, boolean canView) {
|
||||
}
|
||||
|
||||
public record SummaryResponse(String label, String text) {
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -163,6 +163,7 @@ class AihrCaseServiceTest {
|
||||
assertTrue(controllerSource.contains("StpUtil.hasRoleOr"));
|
||||
assertTrue(controllerSource.contains("HR_OPERATOR_ROLE = \"hr_operator\""));
|
||||
assertTrue(controllerSource.contains("private boolean canViewCases()"));
|
||||
assertTrue(controllerSource.contains("@GetMapping(\"/capabilities\")"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| 三角色对练 `/train/practice` | `POST /api/train/practice/start`、`/turn`、`/finish` | 已接入编排 API;数据库启用 chat 模型后,`/turn` 客户回复按人设走真 LLM 生成(seed 剧本作剧情锚点),`/finish` 走单次 temperature=0 结构化评分(4 维分+导师改写+点评);移动端 `turn/finish` 校验当前手机号与启动会话归属,未知或他人会话拒绝;模型未配置或调用失败自动回退 seed,契约不变 |
|
||||
| 正式试点数据导出 | `GET /api/train/practice/export?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD` | 起止日期必填且包含结束日;只统计窗口内能通过唯一手机号或外部 ID 映射到在职组织快照的正式会话,排除重复手机号和身份碰撞。完训定义为每人至少 10 次,校准必须关联同一窗口内正式会话;CSV 同时给出校准命中数、SOP 可用数、满意度响应数/平均分,以及明细级 AI 分、人工校准分、校准人、校准时间、最终采用分、满意度分和意见,避免用四舍五入后的比率反推门禁状态;汇总和明细均携带正式人员及项目口径,不混入历史 seed/开发身份 |
|
||||
| 对练语音 | `POST /api/ai/asr`(multipart 字段 `file`,≤5MB)、`POST /api/ai/tts`(JSON `{text≤300字, voice}`,返回 `{audioUrl}` base64 dataURL)、`GET /api/aihr/mobile/oss/{ossId}` | 走 OpenAI-compatible audio 接口(如硅基流动 SenseVoice/CosyVoice2);模型管理需启用 `category=asr/tts` 配置;移动端优先用浏览器录音,`getUserMedia/MediaRecorder` 不可用或麦克风权限失败时,用 `audio/*` file input 选择/录制音频后继续调同一 ASR 接口;ASR/TTS 未配置或失败返回 fail,前端降级文本;音频下载按员工本人或主管项目范围授权,系统管理端保持后台访问 |
|
||||
| 案例沉淀 `/knowledge/cases` | `POST /api/knowledge/case/upload`、`/organize`、`/curate`、`GET /records`、`GET /records/{caseId}` | `/upload` 改为 multipart 真实语音上传并走 ASR,服务端只接受 MP3/WAV/M4A/WebM/OGG/AAC/FLAC,成功后原始音频写入 `sys_oss`,案例记录保存 `mediaOssId/mediaUrl`;`/organize` 用真实转写调 chat 模型整理案例,未配置模型时按真实 transcript 本地结构化;APP 用户的项目范围从 `aihr_org_snapshot` 登录身份解析,上传、整理、入库、列表和详情均按项目范围校验,未完成正式组织映射时安全拒绝,不接受前端伪造项目范围;移动端和管理端案例详情均可回放原始音频;管理端系统用户保留全局运营视图;预渲染视频样片、学习点和主管点评仍未接入 |
|
||||
| 案例沉淀 `/knowledge/cases` | `GET /api/knowledge/case/capabilities`、`POST /api/knowledge/case/upload`、`/organize`、`/curate`、`GET /records`、`GET /records/{caseId}` | `/capabilities` 返回服务端判定的案例提交/查看能力,移动端不再向普通员工展示无权提交的素材表单;`/upload` 改为 multipart 真实语音上传并走 ASR,服务端只接受 MP3/WAV/M4A/WebM/OGG/AAC/FLAC,成功后原始音频写入 `sys_oss`,案例记录保存 `mediaOssId/mediaUrl`;`/organize` 用真实转写调 chat 模型整理案例,未配置模型时按真实 transcript 本地结构化;APP 用户的项目范围从 `aihr_org_snapshot` 登录身份解析,上传、整理、入库、列表和详情均按项目范围校验,未完成正式组织映射时安全拒绝,不接受前端伪造项目范围;员工列表/详情只返回 `已入库` 案例,管理端系统用户保留全局运营视图;移动端和管理端案例详情均可回放原始音频;预渲染视频样片、学习点和主管点评仍未接入 |
|
||||
| SOP知识库 `/knowledge/sop` | `POST /api/knowledge/search`、`POST /api/knowledge/answer-feedback`、`GET /api/knowledge/position-sop`、`POST /api/knowledge/doc/upload` | 已接入 MySQL Fulltext + Qdrant 混合召回、岗位学习适配摘要、OSS-first 文档上传、txt/md/PDF/Word/Excel/PPT 解析和 embedding 写入,失败回退 seed;搜索返回 `reviewId` 与 `promptVersion`,员工反馈回传并保存该评审批次,SOP 人工评审记录同时保留答案生成提示词版本,管理端可继续复核;`position-sop` 目前只返回一期生活顾问学习导航,不代表正式上岗资格 |
|
||||
| 资料处理 `/knowledge/processing` | `GET /api/knowledge/processing/overview`、`POST /api/knowledge/doc/upload-async`、`GET /api/knowledge/doc/upload-items`、`POST /api/knowledge/doc/upload-items/{id}/retry`、`POST /api/knowledge/doc/import-local-task`、`GET /api/knowledge/doc/import-tasks`、`POST /api/knowledge/doc/import-tasks/{id}/cancel` | 已接入解析任务状态聚合;**批量上传走异步队列**:接口只暂存+入队即秒回,后台 worker(并发 2)逐条解析/归类/向量化;ZIP 在 worker 内安全解压后把支持的子文件继续入同一批次队列,页面按批次轮询进度、失败可单文件重试;服务端目录导入、进度轮询和任务取消保留,失败回退 seed |
|
||||
| 组织人员同步 | `POST /api/aihr/org/sync` | 从开放组织同步系统的 `/api/open/v1/sync/snapshot` 拉取 `company/department/employee` 快照,分页参数使用 `limit`;员工手机号只落 `person_phone` 用于移动端身份映射,不在组织列表响应暴露;岗位识别 `position/job_title/post/job_name/role/title` 等字段。默认 `replaceExisting=true`,写入前必须先用 `{"dryRun":true}`;dry-run 不访问本地快照表、不执行 DDL/写库,返回 `phoneLinked/maskedPhone/suspectText/warnings` 且 `syncedCount=0`。非 dry-run 覆盖写入遇到员工被跳过、手机号不完整、脱敏手机号或疑似乱码时默认拒绝,只有确认 dry-run 结果后显式传 `allowPartialReplace=true` 才允许覆盖;重复 `ext_party_id` 始终拒绝写入,因为数据库唯一键会折叠重复身份;`replaceExisting=false` 不触发不完整快照覆盖闸门,但仍拒绝重复身份。2026-07-10 源接口实测 3474 人仅 1 个可用手机号、3473 个脱敏手机号、8 条疑似乱码,因此未执行覆盖同步,正式试点需上游先开放至少 20 名试点人员手机号 |
|
||||
|
||||
@@ -187,3 +187,4 @@
|
||||
- 2026-07-14 BRD SOP 人工评审结论完整性:`POST /api/knowledge/reviews/{id}` 现在要求明确提交 `usable=true/false`,空请求或缺少结论会拒绝,不再把未作答误记为“不可用”;`reviewer` 仍由当前登录操作人写入。已补服务层回归测试与 `demo-check` marker,但这只修正评审数据完整性,不代表住宅 SOP 可用率已达到正式试点门槛。
|
||||
- 2026-07-14 线上只读核对:生产根站点、`/h5/` 和 `/prod-api/auth/tenant/list` 均返回 `200`,当前线上仍加载管理端 `index-CJZ3Ax3Z.js` 与 H5 `index-D4-NrEpb.js`;本地 `8fcc58fe` 的 SOP 评审结论校验尚未发布。`release-preflight.sh` 仍因两份既有 Figma 文档未提交而阻断,本轮未执行生产写入、静态资源同步或后端重启。
|
||||
- 2026-07-14 BRD 4.6 案例发布状态复核:员工端案例列表和详情原先只按租户/项目范围读取,未限制案例状态,理论上可能看到仍处于“已转写/已整理”的管理流程半成品;现对 APP 项目范围读取增加 `status='已入库'`,管理端 HR/管理员仍可查看全部状态以继续处理。新增案例服务回归测试,AIHR 全量测试 `81/81` 通过;视频样片、学习点和主管点评仍需正式业务输入。
|
||||
- 2026-07-14 BRD 案例提交能力边界复核:移动端案例页原先对所有登录员工展示“提交素材”表单,但后端仅允许主管/项目负责人提交,普通员工点击后才得到权限错误;现新增只读 `GET /api/knowledge/case/capabilities`,复用服务端唯一贡献者判定,员工端仅在 `canSubmit=true` 时展示提交入口,其他员工显示案例学习说明。后端权限仍保留为最终防线。
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<button class="primary-button login-button" @click="goLogin">去登录</button>
|
||||
</view>
|
||||
|
||||
<view v-else class="panel">
|
||||
<view v-else-if="canSubmitCases" class="panel">
|
||||
<text class="panel-title">提交素材</text>
|
||||
<view class="file-box" :class="{ 'has-file': Boolean(selectedFile) }">
|
||||
<text class="file-title">{{ selectedFile?.name || '未选择音频' }}</text>
|
||||
@@ -39,6 +39,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="loggedIn" class="panel permission-panel">
|
||||
<text class="panel-title">案例学习</text>
|
||||
<text class="muted block">当前岗位可查看本项目已入库案例;提交案例素材需要主管或项目负责人权限。</text>
|
||||
</view>
|
||||
|
||||
<view v-if="uploadResult" class="panel">
|
||||
<text class="panel-title">ASR 转写</text>
|
||||
<view class="tag-row">
|
||||
@@ -117,7 +122,7 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import type { CaseCurateResponse, CaseDetailResponse, CaseOrganizeResponse, CaseRecord, CaseSelectedFile, CaseUploadResponse } from '@/types/api';
|
||||
import { chooseCaseAudio, curateCase, getCaseDetail, getCaseRecords, organizeCase, uploadCaseAudio } from '@/services/case';
|
||||
import { chooseCaseAudio, curateCase, getCaseCapabilities, getCaseDetail, getCaseRecords, organizeCase, uploadCaseAudio } from '@/services/case';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { navigateBackToToday, resetPageScroll } from '@/services/navigation';
|
||||
import { ensureEmployeePosition } from '@/services/position';
|
||||
@@ -129,6 +134,7 @@ const organizeResult = ref<CaseOrganizeResponse | null>(null);
|
||||
const curateResult = ref<CaseCurateResponse | null>(null);
|
||||
const records = ref<CaseRecord[]>([]);
|
||||
const caseDetail = ref<CaseDetailResponse | null>(null);
|
||||
const canSubmitCases = ref(false);
|
||||
const status = ref<'idle' | 'uploading' | 'organizing' | 'curating'>('idle');
|
||||
const recordsLoading = ref(false);
|
||||
const caseDetailLoading = ref(false);
|
||||
@@ -151,6 +157,7 @@ const clearCaseData = () => {
|
||||
selectedFile.value = null;
|
||||
records.value = [];
|
||||
caseDetail.value = null;
|
||||
canSubmitCases.value = false;
|
||||
resetResult();
|
||||
};
|
||||
|
||||
@@ -196,6 +203,20 @@ const loadRecords = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadCaseCapabilities = async () => {
|
||||
if (!loggedIn.value) {
|
||||
canSubmitCases.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const capabilities = await getCaseCapabilities();
|
||||
canSubmitCases.value = Boolean(capabilities.canSubmit);
|
||||
} catch (error) {
|
||||
canSubmitCases.value = false;
|
||||
handleRequestError(error, '案例权限加载失败', true);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshRecords = () => {
|
||||
if (!loggedIn.value) {
|
||||
goLogin();
|
||||
@@ -256,6 +277,7 @@ const handleShow = async () => {
|
||||
if (!(await ensureEmployeePosition('/pages/user/cases/index'))) {
|
||||
return;
|
||||
}
|
||||
void loadCaseCapabilities();
|
||||
void loadRecords();
|
||||
} else {
|
||||
clearCaseData();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
CaseCurateResponse,
|
||||
CaseCapabilities,
|
||||
CaseDetailResponse,
|
||||
CaseOrganizeResponse,
|
||||
CaseRecord,
|
||||
@@ -73,6 +74,13 @@ export const curateCase = (caseId: string) =>
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
export const getCaseCapabilities = () =>
|
||||
apiRequest<CaseCapabilities>({
|
||||
url: '/api/knowledge/case/capabilities',
|
||||
method: 'GET',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
export const getCaseRecords = () =>
|
||||
apiRequest<CaseRecord[]>({
|
||||
url: '/api/knowledge/case/records?limit=10',
|
||||
|
||||
@@ -105,6 +105,11 @@ export interface KnowledgeSearchResponse {
|
||||
reviewId?: number | null;
|
||||
}
|
||||
|
||||
export interface CaseCapabilities {
|
||||
canSubmit: boolean;
|
||||
canView: boolean;
|
||||
}
|
||||
|
||||
export interface PositionSopTopic {
|
||||
category: string;
|
||||
question: string;
|
||||
|
||||
@@ -355,6 +355,7 @@ contains mobile-uni/src/services/knowledge.ts "/api/knowledge/position-sop"
|
||||
contains mobile-uni/src/services/position.ts "removeStorageSync(roleKey)"
|
||||
contains mobile-uni/src/services/position.ts "/api/aihr/mobile/me"
|
||||
contains mobile-uni/src/services/case.ts "/api/knowledge/case/upload"
|
||||
contains mobile-uni/src/services/case.ts "/api/knowledge/case/capabilities"
|
||||
contains mobile-uni/src/services/case.ts "/api/knowledge/case/records/"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "/records/{caseId}"
|
||||
contains mobile-uni/src/services/candidate.ts "/api/recruit/interview/start"
|
||||
@@ -516,6 +517,7 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrVideoService.java "AihrSensitiveText.forModel(fileName)"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "外部响应体已隐藏"
|
||||
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/controller/AihrCaseController.java "@GetMapping(\"/capabilities\")"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "StpUtil.hasRoleOr"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "private boolean canViewCases()"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "position_level IN ('主管', '项目经理')"
|
||||
|
||||
Reference in New Issue
Block a user