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