diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java index 45ebe50d..249df00c 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java @@ -1,6 +1,7 @@ package org.dromara.aihr.controller; import cn.dev33.satoken.annotation.SaIgnore; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.dromara.aihr.domain.AihrPracticeDto.AnnotationResponse; import org.dromara.aihr.domain.AihrPracticeDto.CompetencyResponse; @@ -19,6 +20,7 @@ import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.enums.UserType; import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.system.service.ISysOssService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.io.IOException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -40,6 +43,7 @@ import java.util.Map; public class AihrMobileController { private final AihrMobileSeedService mobileSeedService; + private final ISysOssService ossService; @SaIgnore @GetMapping("/home/{role}") @@ -65,6 +69,11 @@ public class AihrMobileController { return detail == null ? R.fail("复盘记录不存在") : R.ok(reviewDetailPayload(detail)); } + @GetMapping("/oss/{ossId}") + public void oss(@PathVariable Long ossId, HttpServletResponse response) throws IOException { + ossService.download(ossId, response); + } + @PostMapping("/practice/reviews/{id}/reviewed") public R markReviewed(@PathVariable Long id, @RequestBody(required = false) ReviewAdviceRequest request) { return mobileSeedService.markReviewed(id, request, currentAppUsername()) ? R.ok() : R.fail("复盘记录不存在"); @@ -142,12 +151,15 @@ public class AihrMobileController { "value", item.value(), "note", item.note() )).toList()); - data.put("dialogue", detail.dialogue().stream().map(turn -> Map.of( - "role", turn.role(), - "label", turn.label(), - "text", turn.text(), - "audioUrl", turn.audioUrl() - )).toList()); + data.put("dialogue", detail.dialogue().stream().map(turn -> { + Map item = new LinkedHashMap<>(); + item.put("role", turn.role()); + item.put("label", turn.label()); + item.put("text", turn.text()); + item.put("audioUrl", turn.audioUrl()); + item.put("audioOssId", turn.audioOssId()); + return item; + }).toList()); data.put("annotations", detail.annotations().stream().map(AihrMobileController::annotationPayload).toList()); return data; } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java index f6685dfd..669c15cd 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java @@ -116,13 +116,13 @@ public final class AihrPracticeDto { public record RecordResponse(Long id, String sessionId, String time, String trainee, String scene, Integer score, String status, String summary, String extPartyId) { } - public record DialogueResponse(String role, String label, String text, String audioUrl, Integer emotion, Integer trust, Boolean redFlag, String coachHint) { + public record DialogueResponse(String role, String label, String text, String audioUrl, Long audioOssId, Integer emotion, Integer trust, Boolean redFlag, String coachHint) { public DialogueResponse(String role, String label, String text) { - this(role, label, text, "", null, null, null, ""); + this(role, label, text, "", null, null, null, null, ""); } public DialogueResponse(String role, String label, String text, String audioUrl) { - this(role, label, text, audioUrl, null, null, null, ""); + this(role, label, text, audioUrl, null, null, null, null, ""); } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index 18ab34c2..9d2f60b0 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -2111,6 +2111,7 @@ public class AihrPracticeSeedService { item.path("label").asText(), item.path("text").asText(), item.path("audioUrl").asText(""), + item.hasNonNull("audioOssId") ? item.path("audioOssId").asLong() : null, item.hasNonNull("emotion") ? item.path("emotion").asInt() : null, item.hasNonNull("trust") ? item.path("trust").asInt() : null, item.hasNonNull("redFlag") ? item.path("redFlag").asBoolean() : null, @@ -2132,6 +2133,7 @@ public class AihrPracticeSeedService { List turns = new ArrayList<>(); List replies = snapshot(activeSession == null ? null : activeSession.traineeReplies()); List audioUrls = snapshot(activeSession == null ? null : activeSession.traineeAudioUrls()); + List audioOssIds = snapshotLong(activeSession == null ? null : activeSession.traineeAudioOssIds()); List customerLines = snapshot(activeSession == null ? null : activeSession.customerLines()); List evidenceList = snapshotEvidence(activeSession == null ? null : activeSession.turnEvidence()); for (int i = 0; i < scenario.rounds().size(); i++) { @@ -2145,6 +2147,7 @@ public class AihrPracticeSeedService { "员工话术", replies.get(i), i < audioUrls.size() ? audioUrls.get(i) : "", + i < audioOssIds.size() ? audioOssIds.get(i) : null, evidence == null ? null : evidence.emotion(), evidence == null ? null : evidence.trust(), evidence == null ? null : evidence.redFlag(), @@ -2157,6 +2160,7 @@ public class AihrPracticeSeedService { "AI教练", coachHint, "", + null, evidence == null ? null : evidence.emotion(), evidence == null ? null : evidence.trust(), evidence == null ? null : evidence.redFlag(), @@ -2201,6 +2205,7 @@ public class AihrPracticeSeedService { turn.label(), maskSensitiveText(turn.text()), firstNonBlank(turn.audioUrl(), ""), + turn.audioOssId(), turn.emotion(), turn.trust(), turn.redFlag(), diff --git a/docs/20260708/uni-app用户侧前端重建计划.md b/docs/20260708/uni-app用户侧前端重建计划.md index 65bb14ec..948e4fc4 100644 --- a/docs/20260708/uni-app用户侧前端重建计划.md +++ b/docs/20260708/uni-app用户侧前端重建计划.md @@ -227,6 +227,7 @@ mobile-uni/ - 员工今日页到练习页已补任务上下文传递:首页点“开始”会把当前 assignment 带到练习页,daily 任务高亮并提示填写,主管专项可直接启动,减少跳页后的二次选择。 - 移动端 hash 路由滚动复位已补:员工/候选人/主管/登录/建设中页面激活时回到顶部,真实浏览器验证从长页切回今日页后 `scrollY=0`,避免首屏被旧滚动位置截断。 - 员工练习页已补语音输入入口:回复区支持浏览器录音转写和选择音频转写,ASR 返回文本进入原提交框且音频 URL/OssId 随 turn 提交供主管复盘回放;真实浏览器验证按钮在 390px 手机视口不换行,文本 turn 回归可进入第 2 轮。 +- 主管复盘音频回放已补同源下载链路:移动端新增 `/api/aihr/mobile/oss/{ossId}` 下载代理,复盘详情返回 `audioOssId`,主管页用当前 `apiUrl` 生成音频地址;portless 验证 ASR 生成的 WAV 可经 `wygj-api.localhost` 下载,新训练记录详情可回读 `audioOssId`。 ## 7. 风险 diff --git a/mobile-uni/src/pages/supervisor/index/index.vue b/mobile-uni/src/pages/supervisor/index/index.vue index a8f20831..42a23430 100644 --- a/mobile-uni/src/pages/supervisor/index/index.vue +++ b/mobile-uni/src/pages/supervisor/index/index.vue @@ -173,7 +173,7 @@ {{ turn.label }} {{ turn.text }} - + @@ -240,6 +240,7 @@ import { onShow } from '@dcloudio/uni-app'; import type { PracticeAlert, PracticeAssignment, PracticeRecord, ReviewDetail } from '@/types/api'; import { isLoggedIn, rememberLoginRedirect } from '@/services/auth'; import { resetPageScroll } from '@/services/navigation'; +import { apiUrl } from '@/services/api'; import { createPracticeAssignment, getPracticeAlerts, @@ -274,6 +275,9 @@ const scenarioOptions = [ { id: 'repair-elevator-delay', name: '电梯故障' } ]; +const audioSrc = (turn: { audioOssId?: number | string; audioUrl?: string }) => + turn.audioOssId ? apiUrl(`/api/aihr/mobile/oss/${turn.audioOssId}`) : turn.audioUrl || ''; + const scenarioIdForText = (text: string) => { if (/电梯/.test(text)) return 'repair-elevator-delay'; if (/投诉|报修|漏水|噪音|摔倒/.test(text)) return 'complaint-water'; diff --git a/mobile-uni/src/types/api.ts b/mobile-uni/src/types/api.ts index ee335b5f..a22c206f 100644 --- a/mobile-uni/src/types/api.ts +++ b/mobile-uni/src/types/api.ts @@ -168,6 +168,7 @@ export interface ReviewDialogue { role: string; label: string; text: string; + audioOssId?: number | string; audioUrl?: string; }