From 26f516080286f31cd800fc30b82b27ea420a9e2f Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 16:40:41 +0800 Subject: [PATCH] fix(aihr): avoid duplicate inline tts audio --- .../org/dromara/aihr/controller/AihrSpeechController.java | 4 ++-- .../org/dromara/aihr/service/AihrSensitiveTextTest.java | 8 ++++++++ docs/API_INTEGRATION.md | 2 +- docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + scripts/demo-check.sh | 1 + 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java index ee0d4d67..599d9047 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java @@ -78,16 +78,16 @@ public class AihrSpeechController { } private R storeTtsAudio(byte[] audio) { - String inlineAudioUrl = "data:audio/mp3;base64," + Base64.getEncoder().encodeToString(audio); Path tempFile = null; try { tempFile = Files.createTempFile("aihr-tts-", ".mp3"); Files.write(tempFile, audio); File file = tempFile.toFile(); SysOssVo oss = ossService.upload(file); - return R.ok(new TtsResponse(oss.getUrl(), "openai-compatible", oss.getOssId(), inlineAudioUrl)); + return R.ok(new TtsResponse(oss.getUrl(), "openai-compatible", oss.getOssId(), "")); } catch (Exception e) { log.warn("tts audio upload failed(处理错误已隐藏)"); + String inlineAudioUrl = "data:audio/mp3;base64," + Base64.getEncoder().encodeToString(audio); return R.ok(new TtsResponse(inlineAudioUrl, "openai-compatible")); } finally { if (tempFile != null) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSensitiveTextTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSensitiveTextTest.java index fdc032ae..ead31fbf 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSensitiveTextTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSensitiveTextTest.java @@ -70,6 +70,14 @@ class AihrSensitiveTextTest { assertTrue(practiceLlmSource.contains("practice score parse failed, falls back to seed(处理错误已隐藏)")); } + @Test + void ttsUploadSuccessDoesNotDuplicateInlineAudio() throws Exception { + String speechControllerSource = Files.readString(controllerSource("AihrSpeechController.java")); + + assertTrue(speechControllerSource.contains("new TtsResponse(oss.getUrl(), \"openai-compatible\", oss.getOssId(), \"\")")); + assertTrue(speechControllerSource.contains("new TtsResponse(inlineAudioUrl, \"openai-compatible\")")); + } + private static Path source(String fileName) { Path direct = Path.of("src/main/java/org/dromara/aihr/service", fileName); return Files.exists(direct) diff --git a/docs/API_INTEGRATION.md b/docs/API_INTEGRATION.md index 6d3c08f3..d00d2f0f 100644 --- a/docs/API_INTEGRATION.md +++ b/docs/API_INTEGRATION.md @@ -10,7 +10,7 @@ | 候选人入职主体关联 `/recruit/interview` | `GET/POST /api/recruit/interview/candidate-links` | HR/管理员在当前租户范围内把本地候选人 ID 关联到已同步的在职 `ext_party_id`;只保存外部主体 ID,不复制姓名、部门等组织字段,重复关联同一主体幂等,候选人更换主体或同一主体已关联其他候选人会拒绝 | | 三角色对练 `/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,前端降级文本;音频下载按员工本人或主管项目范围授权,系统管理端保持后台访问 | +| 对练语音 | `POST /api/ai/asr`(multipart 字段 `file`,≤5MB)、`POST /api/ai/tts`(JSON `{text≤300字, voice}`,成功返回 OSS `audioUrl + ossId`,OSS 上传失败才回退 `inlineAudioUrl` dataURL)、`GET /api/aihr/mobile/oss/{ossId}` | 走 OpenAI-compatible audio 接口(如硅基流动 SenseVoice/CosyVoice2);模型管理需启用 `category=asr/tts` 配置;移动端优先用浏览器录音,`getUserMedia/MediaRecorder` 不可用或麦克风权限失败时,用 `audio/*` file input 选择/录制音频后继续调同一 ASR 接口;ASR/TTS 未配置或失败返回 fail,前端降级文本;音频下载按员工本人或主管项目范围授权,系统管理端保持后台访问;正常 TTS 响应不重复携带完整 base64 音频 | | 案例沉淀 `/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 | diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index ae74bc52..0f887951 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -2,6 +2,7 @@ 更新时间:2026-07-14 +- 2026-07-14 BRD G3/G6 TTS 响应边界收紧:TTS 成功写入 `sys_oss` 后原先仍在 JSON 中重复携带完整 base64 音频,增加浏览器缓存、日志和响应体暴露面;当前 `mobile-uni` 与管理端没有消费 `inlineAudioUrl`,现改为成功只返回 OSS `audioUrl/ossId`,仅在 OSS 上传失败时保留内联 data URL 降级。补充源码契约测试、`demo-check` marker 和 API 对接说明;未改变音频回放授权与 OSS 留存策略。 - 2026-07-14 BRD G3/G6 生产验证码安全边界修复:`CaptchaController` 原先允许通过 `aihr.sms.demo-fixed-code-enabled=true` 在 `prod` profile 放行固定短信验证码,和项目要求的“生产代码级禁用”冲突;现仅允许非 `prod` profile 使用 `aihr.sms.dev-fixed-code`,删除生产覆盖开关,并新增源码契约回归与 `demo-check` marker。未修改线上配置或生产数据。 - 2026-07-14 本地验证环境一致性修复:项目 Docker 编排使用 MySQL `13306`、Redis `16379`,但 `application-dev.yml` 原先仍默认连接 `3306/6379`,导致 admin Spring 全量测试误连其他本机容器;现 dev 数据源改为 `AIHR_MYSQL_*`、`AIHR_REDIS_*` 可覆盖配置,默认与本项目编排一致,生产配置不变。`ruoyi-admin` 全量测试已通过。 - 2026-07-14 BRD 5.4/G3 线上岗位身份映射修复:生产关闭组织姓名/部门展示时,`orgSnapshotWhere` 原先也排除了 `person_phone`,认证 APP 用户的手机号查询可能返回空结果并错误降级为员工/手动选岗;现隐藏展示字段模式仍允许按手机号过滤,但不返回手机号或姓名部门。新增回归测试与 `demo-check` marker;线上需重新发布后再用正式组织映射数据验证。 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 0a93cd38..125d0fd7 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -611,6 +611,7 @@ 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/AihrSpeechController.java "oss.getOssId(), \"\")" contains backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java '!environment.acceptsProfiles(Profiles.of("prod"))' contains backend/ruoyi-admin/src/test/java/org/dromara/web/controller/CaptchaControllerSecurityTest.java "CaptchaControllerSecurityTest" contains backend/ruoyi-admin/src/main/resources/application-dev.yml "AIHR_MYSQL_PORT:13306"