fix(aihr): expire unbound practice audio
This commit is contained in:
+43
@@ -3,12 +3,16 @@ package org.dromara.aihr.controller;
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.service.AihrPracticeSeedService;
|
||||
import org.dromara.aihr.domain.AihrSpeechDto.AsrResponse;
|
||||
import org.dromara.aihr.domain.AihrSpeechDto.TtsRequest;
|
||||
import org.dromara.aihr.domain.AihrSpeechDto.TtsResponse;
|
||||
import org.dromara.aihr.service.AihrMultipartFiles;
|
||||
import org.dromara.aihr.service.AihrSpeechService;
|
||||
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.domain.vo.SysOssVo;
|
||||
import org.dromara.system.service.ISysOssService;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -24,6 +28,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对练语音 API:ASR 语音输入 + TTS 客户语音播报。未配置语音模型时返回失败,前端降级为文本。
|
||||
@@ -40,6 +45,7 @@ public class AihrSpeechController {
|
||||
private static final int MAX_TTS_CHARS = 300;
|
||||
|
||||
private final AihrSpeechService speechService;
|
||||
private final AihrPracticeSeedService practiceSeedService;
|
||||
private final ISysOssService ossService;
|
||||
|
||||
@PostMapping(value = "/asr", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@@ -59,6 +65,16 @@ public class AihrSpeechController {
|
||||
.map(text -> {
|
||||
try {
|
||||
SysOssVo oss = ossService.upload(sanitizedFile);
|
||||
Long ownerUserId = currentAppUserId();
|
||||
if (ownerUserId != null) {
|
||||
try {
|
||||
practiceSeedService.stagePracticeAudio(oss.getOssId(), oss.getUrl(), currentAppUsername(), ownerUserId);
|
||||
} catch (RuntimeException ex) {
|
||||
deleteOssQuietly(oss.getOssId());
|
||||
log.warn("stage asr audio failed(处理错误已隐藏)");
|
||||
return R.ok(new AsrResponse(text, "openai-compatible"));
|
||||
}
|
||||
}
|
||||
return R.ok(new AsrResponse(text, "openai-compatible", oss.getOssId(), oss.getUrl()));
|
||||
} catch (Exception e) {
|
||||
log.warn("asr audio upload failed(处理错误已隐藏)");
|
||||
@@ -68,6 +84,33 @@ public class AihrSpeechController {
|
||||
.orElseGet(() -> R.fail("语音识别失败,请改用文字输入"));
|
||||
}
|
||||
|
||||
private void deleteOssQuietly(Long ossId) {
|
||||
if (ossId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ossService.deleteWithValidByIds(List.of(ossId), false);
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("delete staged asr audio failed(处理错误已隐藏)");
|
||||
}
|
||||
}
|
||||
|
||||
private static String currentAppUsername() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser == null || !UserType.APP_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
return "";
|
||||
}
|
||||
return loginUser.getUsername() == null ? "" : loginUser.getUsername().trim();
|
||||
}
|
||||
|
||||
private static Long currentAppUserId() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser == null || !UserType.APP_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
return null;
|
||||
}
|
||||
return loginUser.getUserId();
|
||||
}
|
||||
|
||||
@PostMapping("/tts")
|
||||
public R<TtsResponse> tts(@RequestBody TtsRequest request) {
|
||||
if (!speechService.ttsConfigured()) {
|
||||
|
||||
+125
-3
@@ -1,8 +1,10 @@
|
||||
package org.dromara.aihr.service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.AnnotationResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.AnnotationSaveRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.CalibrationRequest;
|
||||
@@ -48,6 +50,7 @@ import org.dromara.aihr.service.AihrPracticeLlmService.PracticeTurn;
|
||||
import org.dromara.aihr.service.AihrPracticeLlmService.DailyDrillAssessment;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.tenant.helper.TenantHelper;
|
||||
import org.dromara.system.service.ISysOssService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
@@ -73,12 +76,15 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 三角色对练编排器。seed 剧本承担剧情锚点与兜底;配置了 chat 模型时,
|
||||
* 客户回复与 /finish 评分走真 LLM(AihrPracticeLlmService),失败自动回退 seed。
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AihrPracticeSeedService {
|
||||
|
||||
private static final Set<String> LEGACY_DAILY_DRILL_PROFILES = Set.of("dev", "local");
|
||||
@@ -224,6 +230,8 @@ public class AihrPracticeSeedService {
|
||||
private final AihrPracticeLlmService practiceLlmService;
|
||||
private final AihrSopSeedService sopSeedService;
|
||||
private final TransactionTemplate pilotExportTransaction;
|
||||
private final ISysOssService ossService;
|
||||
private final ScheduledExecutorService scheduledExecutorService;
|
||||
@Value("${aihr.practice.allow-legacy-daily-drill-fallback:${AIHR_PRACTICE_ALLOW_LEGACY_DAILY_DRILL_FALLBACK:false}}")
|
||||
private boolean allowLegacyDailyDrillFallback;
|
||||
@Value("${spring.profiles.active:}")
|
||||
@@ -240,22 +248,39 @@ public class AihrPracticeSeedService {
|
||||
|
||||
@Autowired
|
||||
public AihrPracticeSeedService(ObjectMapper objectMapper, JdbcTemplate jdbcTemplate, AihrPracticeLlmService practiceLlmService,
|
||||
AihrSopSeedService sopSeedService, PlatformTransactionManager transactionManager) {
|
||||
this(objectMapper, jdbcTemplate, practiceLlmService, sopSeedService, new TransactionTemplate(transactionManager));
|
||||
AihrSopSeedService sopSeedService, PlatformTransactionManager transactionManager,
|
||||
ISysOssService ossService, ScheduledExecutorService scheduledExecutorService) {
|
||||
this(objectMapper, jdbcTemplate, practiceLlmService, sopSeedService, new TransactionTemplate(transactionManager),
|
||||
ossService, scheduledExecutorService);
|
||||
}
|
||||
|
||||
public AihrPracticeSeedService(ObjectMapper objectMapper, JdbcTemplate jdbcTemplate, AihrPracticeLlmService practiceLlmService,
|
||||
AihrSopSeedService sopSeedService) {
|
||||
this(objectMapper, jdbcTemplate, practiceLlmService, sopSeedService, (TransactionTemplate) null);
|
||||
this(objectMapper, jdbcTemplate, practiceLlmService, sopSeedService, (TransactionTemplate) null, null, null);
|
||||
}
|
||||
|
||||
AihrPracticeSeedService(ObjectMapper objectMapper, JdbcTemplate jdbcTemplate, AihrPracticeLlmService practiceLlmService,
|
||||
AihrSopSeedService sopSeedService, TransactionTemplate pilotExportTransaction) {
|
||||
this(objectMapper, jdbcTemplate, practiceLlmService, sopSeedService, pilotExportTransaction, null, null);
|
||||
}
|
||||
|
||||
AihrPracticeSeedService(ObjectMapper objectMapper, JdbcTemplate jdbcTemplate, AihrPracticeLlmService practiceLlmService,
|
||||
AihrSopSeedService sopSeedService, TransactionTemplate pilotExportTransaction,
|
||||
ISysOssService ossService, ScheduledExecutorService scheduledExecutorService) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.practiceLlmService = practiceLlmService;
|
||||
this.sopSeedService = sopSeedService;
|
||||
this.pilotExportTransaction = configurePilotExportTransaction(pilotExportTransaction);
|
||||
this.ossService = ossService;
|
||||
this.scheduledExecutorService = scheduledExecutorService;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
void schedulePracticeAudioCleanup() {
|
||||
if (scheduledExecutorService != null) {
|
||||
scheduledExecutorService.scheduleWithFixedDelay(this::cleanupExpiredPracticeAudioUploads, 10, 15, TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
|
||||
public List<ScenarioResponse> scenarios(String position, String scenarioType) {
|
||||
@@ -1497,6 +1522,81 @@ public class AihrPracticeSeedService {
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
public void stagePracticeAudio(Long ossId, String audioUrl, String ownerIdentity, Long ownerUserId) {
|
||||
if (ossId == null || isBlank(ownerIdentity) || ownerUserId == null || ownerUserId <= 0) {
|
||||
return;
|
||||
}
|
||||
ensureAudioUploadTable();
|
||||
cleanupExpiredPracticeAudioUploads();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO aihr_practice_audio_upload
|
||||
(tenant_id, oss_id, owner_user_id, owner_identity, audio_url, status, expires_time, create_time, update_time)
|
||||
VALUES (?, ?, ?, ?, ?, 'staged', ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
owner_user_id = VALUES(owner_user_id),
|
||||
owner_identity = VALUES(owner_identity),
|
||||
audio_url = VALUES(audio_url),
|
||||
status = 'staged',
|
||||
expires_time = VALUES(expires_time),
|
||||
update_time = VALUES(update_time)
|
||||
""",
|
||||
tenantId(),
|
||||
ossId,
|
||||
ownerUserId,
|
||||
ownerIdentity.trim(),
|
||||
firstNonBlank(audioUrl, ""),
|
||||
Timestamp.valueOf(now.plusHours(2)),
|
||||
Timestamp.valueOf(now),
|
||||
Timestamp.valueOf(now)
|
||||
);
|
||||
}
|
||||
|
||||
private void bindPracticeAudio(Long ossId) {
|
||||
if (ossId == null) {
|
||||
return;
|
||||
}
|
||||
ensureAudioUploadTable();
|
||||
jdbcTemplate.update("""
|
||||
UPDATE aihr_practice_audio_upload
|
||||
SET status = 'bound', update_time = ?
|
||||
WHERE tenant_id = ? AND oss_id = ? AND status = 'staged'
|
||||
""", Timestamp.valueOf(LocalDateTime.now()), tenantId(), ossId);
|
||||
}
|
||||
|
||||
private void cleanupExpiredPracticeAudioUploads() {
|
||||
if (jdbcTemplate == null || ossService == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ensureAudioUploadTable();
|
||||
List<Long> expiredIds = jdbcTemplate.query("""
|
||||
SELECT u.oss_id
|
||||
FROM aihr_practice_audio_upload u
|
||||
WHERE u.tenant_id = ? AND u.status = 'staged' AND u.expires_time < ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM aihr_practice_audio a
|
||||
WHERE a.tenant_id = u.tenant_id AND a.oss_id = u.oss_id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM aihr_practice_assignment d
|
||||
WHERE d.tenant_id = u.tenant_id AND d.answer_audio_oss_id = u.oss_id
|
||||
)
|
||||
""", (rs, rowNum) -> rs.getLong("oss_id"), tenantId(), Timestamp.valueOf(LocalDateTime.now()));
|
||||
for (Long ossId : expiredIds) {
|
||||
try {
|
||||
ossService.deleteWithValidByIds(List.of(ossId), false);
|
||||
jdbcTemplate.update("DELETE FROM aihr_practice_audio_upload WHERE tenant_id = ? AND oss_id = ? AND status = 'staged'", tenantId(), ossId);
|
||||
} catch (RuntimeException ex) {
|
||||
// Keep the staging row so the next sweep can retry object deletion.
|
||||
log.warn("practice audio cleanup failed(处理错误已隐藏)");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("practice audio cleanup sweep failed(处理错误已隐藏)");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAudioOwnership(Long ossId, String ownerIdentity, Long ownerUserId) {
|
||||
if (ossId == null) {
|
||||
return;
|
||||
@@ -2226,6 +2326,7 @@ public class AihrPracticeSeedService {
|
||||
if (updated == 0) {
|
||||
return null;
|
||||
}
|
||||
bindPracticeAudio(audioOssId);
|
||||
return new DailyDrillAnswerResponse(id, "已完成", score.correct(), score.score(), score.feedback(), parts.referenceAnswer(), score.mode(), audioUrl, audioOssId);
|
||||
}
|
||||
|
||||
@@ -3476,6 +3577,7 @@ public class AihrPracticeSeedService {
|
||||
Timestamp.valueOf(now),
|
||||
Timestamp.valueOf(now)
|
||||
);
|
||||
bindPracticeAudio(i < ossIds.size() ? ossIds.get(i) : null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3938,6 +4040,26 @@ public class AihrPracticeSeedService {
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureAudioUploadTable() {
|
||||
jdbcTemplate.execute("""
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_audio_upload` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`oss_id` bigint NOT NULL COMMENT 'OSS文件ID',
|
||||
`owner_user_id` bigint NOT NULL COMMENT '上传用户ID',
|
||||
`owner_identity` varchar(100) NOT NULL COMMENT '上传主体',
|
||||
`audio_url` varchar(1000) DEFAULT NULL COMMENT '音频URL',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'staged' COMMENT 'staged/bound',
|
||||
`expires_time` datetime NOT NULL COMMENT '过期时间',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_practice_audio_upload` (`tenant_id`, `oss_id`),
|
||||
KEY `idx_aihr_practice_audio_upload_expire` (`tenant_id`, `status`, `expires_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练音频上传暂存';
|
||||
""");
|
||||
}
|
||||
|
||||
private void ensurePromptTemplateTable() {
|
||||
if (promptTemplateTableReady) {
|
||||
return;
|
||||
|
||||
+2
@@ -315,6 +315,8 @@ public class AihrPracticeSeedServiceTest {
|
||||
assertTrue(service.contains("validateAudioOwnership(request == null ? null : request.traineeAudioOssId(), ownerIdentity, ownerUserId)"));
|
||||
assertTrue(service.contains("validateAudioOwnership(audioOssId, extPartyId, ownerUserId)"));
|
||||
assertTrue(service.contains("d.answer_audio_oss_id = ?"));
|
||||
assertTrue(service.contains("aihr_practice_audio_upload"));
|
||||
assertTrue(service.contains("cleanupExpiredPracticeAudioUploads"));
|
||||
assertTrue(controller.contains("currentAppUserId()"));
|
||||
assertTrue(mobileController.contains("answerDailyDrill(id, request, ownMobileExtPartyId(null), currentAppUserId())"));
|
||||
}
|
||||
|
||||
@@ -91,6 +91,22 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_audio` (
|
||||
KEY `idx_aihr_practice_audio_session` (`tenant_id`, `session_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练音频';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_audio_upload` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`oss_id` bigint NOT NULL COMMENT 'OSS文件ID',
|
||||
`owner_user_id` bigint NOT NULL COMMENT '上传用户ID',
|
||||
`owner_identity` varchar(100) NOT NULL COMMENT '上传主体',
|
||||
`audio_url` varchar(1000) DEFAULT NULL COMMENT '音频URL',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'staged' COMMENT 'staged/bound',
|
||||
`expires_time` datetime NOT NULL COMMENT '过期时间',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_practice_audio_upload` (`tenant_id`, `oss_id`),
|
||||
KEY `idx_aihr_practice_audio_upload_expire` (`tenant_id`, `status`, `expires_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练音频上传暂存';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_assignment` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
|
||||
@@ -16,6 +16,22 @@ CREATE TABLE IF NOT EXISTS `aihr_practice_audio` (
|
||||
KEY `idx_aihr_practice_audio_session` (`tenant_id`, `session_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练音频';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_audio_upload` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
`oss_id` bigint NOT NULL COMMENT 'OSS文件ID',
|
||||
`owner_user_id` bigint NOT NULL COMMENT '上传用户ID',
|
||||
`owner_identity` varchar(100) NOT NULL COMMENT '上传主体',
|
||||
`audio_url` varchar(1000) DEFAULT NULL COMMENT '音频URL',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'staged' COMMENT 'staged/bound',
|
||||
`expires_time` datetime NOT NULL COMMENT '过期时间',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_practice_audio_upload` (`tenant_id`, `oss_id`),
|
||||
KEY `idx_aihr_practice_audio_upload_expire` (`tenant_id`, `status`, `expires_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练音频上传暂存';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_practice_calibration` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# BRD 功能审查与未完成项记录
|
||||
|
||||
- 2026-07-14 BRD G3 训练音频上传者边界修复:陪练回合和每日三题此前接受客户端直接提交的 `audioOssId`,服务端只在下载时按训练记录判断归属,存在把其他对象绑定到当前训练/作业的风险;现要求移动端提交的 OSS 对象必须由当前 APP 用户创建,且每日三题音频下载授权同时覆盖 `aihr_practice_assignment.answer_audio_oss_id`。ASR 上传后尚未绑定会话的孤儿对象清理仍需独立的短期 staging/过期策略,不用本次归属校验冒充已解决。
|
||||
- 2026-07-14 BRD G3 训练音频生命周期修复:陪练回合和每日三题此前接受客户端直接提交的 `audioOssId`,服务端只在下载时按训练记录判断归属,存在把其他对象绑定到当前训练/作业的风险;现要求移动端提交的 OSS 对象必须由当前 APP 用户创建,且每日三题音频下载授权同时覆盖 `aihr_practice_assignment.answer_audio_oss_id`。ASR 成功上传后先写 `aihr_practice_audio_upload` 暂存记录,训练/作业绑定后转为 `bound`,超过两小时且没有业务引用的暂存对象由定时清理删除;清理失败保留暂存记录重试。正式环境仍需用真实 ASR 和 MinIO 对象抽查清理结果。
|
||||
- 2026-07-14 BRD G3 SOP 附件绑定补偿:同步/待处理图片文档此前在 `sys_oss` 上传成功后再写 `aihr_knowledge_attach`,附件写入失败时可能留下无业务引用对象;现仅对“附件尚未成功绑定”的异常路径做 OSS 补偿删除,解析失败后已存在的失败/待处理附件仍保留用于重试,不改变现有文档处理状态机。
|
||||
- 2026-07-14 BRD G3 OSS 生命周期补偿:候选资料和案例音频此前先写 `sys_oss`、再写业务记录,业务数据库写入失败时可能留下没有业务引用的对象;现对“本次上传新建且尚未绑定”的 `oss_id` 增加失败补偿删除,案例同时移除内存态,补充两条回归测试和 `demo-check` marker。该修复不触碰已有业务资源;SOP 分片/ASR 等复杂异步链路仍需按各自状态机单独核查,不能用本次补偿覆盖全部媒体生命周期。
|
||||
- 2026-07-14 BRD G3 文件名边界复核:候选资料、案例音频、同步 SOP 文档和 ASR 音频此前虽然分别校验或清理文件名,但仍有路径把原始 `MultipartFile` 直接交给 `sys_oss`,清理后的名字没有贯通到 OSS 元数据;现新增共用 `AihrMultipartFiles` 包装器,四条链路均以清理后的文件名上传/外发,并补单测与 `demo-check` marker。该修复不改变文件类型、大小、权限和业务状态,仍需正式环境抽查 OSS 原始文件名与下载响应头。
|
||||
|
||||
@@ -140,6 +140,7 @@ check_pilot_samples() {
|
||||
HAVING COUNT(*) = 5 AND ABS(SUM(d.weight) - 1.00) < 0.001
|
||||
) ready_rubrics")"
|
||||
audio_table_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'aihr_practice_audio'")"
|
||||
audio_upload_table_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'aihr_practice_audio_upload'")"
|
||||
prompt_version_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_sop_answer_review' AND column_name = 'prompt_version'")"
|
||||
requester_identity_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_sop_answer_review' AND column_name = 'requester_ext_party_id'")"
|
||||
hire_date_schema_count="$(pilot_sql_scalar "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'aihr_org_snapshot' AND column_name = 'hire_date'")"
|
||||
@@ -307,6 +308,7 @@ check_pilot_samples() {
|
||||
(( case_stored_count >= 20 )) || fail "pilot stored cases ${case_stored_count}/20"
|
||||
(( case_unmasked_count == 0 )) || fail "pilot stored cases contain unmasked known PII (${case_unmasked_count})"
|
||||
(( audio_table_count == 1 )) || fail "pilot audio schema unavailable (aihr_practice_audio missing)"
|
||||
(( audio_upload_table_count == 1 )) || fail "pilot audio staging schema unavailable (aihr_practice_audio_upload missing)"
|
||||
(( prompt_version_schema_count == 1 )) || fail "pilot RAG traceability schema unavailable (aihr_sop_answer_review.prompt_version missing)"
|
||||
(( requester_identity_schema_count == 1 )) || fail "pilot SOP requester identity schema unavailable (aihr_sop_answer_review.requester_ext_party_id missing)"
|
||||
(( hire_date_schema_count == 1 )) || fail "pilot org hire_date schema unavailable"
|
||||
@@ -542,6 +544,7 @@ contains frontend/src/api/aihr/practice.ts "/api/train/practice/export"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "final_adopted_score"
|
||||
contains backend/script/sql/aihr_practice_mysql8.sql "aihr_practice_calibration"
|
||||
contains backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql "aihr_practice_audio"
|
||||
contains backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql "aihr_practice_audio_upload"
|
||||
contains backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql "aihr_practice_calibration"
|
||||
contains backend/script/sql/aihr_org_snapshot_mysql8.sql "hire_date"
|
||||
contains backend/script/sql/update/aihr_20260714_org_hire_date_mysql8.sql "hire_date"
|
||||
@@ -602,6 +605,9 @@ 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/AihrPracticeSeedService.java "maskSensitiveText"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "validateAudioOwnership"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "d.answer_audio_oss_id = ?"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "stagePracticeAudio"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "cleanupExpiredPracticeAudioUploads"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java "stagePracticeAudio"
|
||||
contains mobile-uni/src/services/practice.ts "/api/aihr/mobile/practice/assignments"
|
||||
contains mobile-uni/src/services/practice.ts "/api/train/practice/scenarios?position="
|
||||
contains mobile-uni/src/pages/supervisor/assign/index.vue "scenarioOptions"
|
||||
|
||||
@@ -23,8 +23,8 @@ mysql -e "CREATE DATABASE \`$DB\` CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_
|
||||
|
||||
mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql"
|
||||
mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql"
|
||||
evidence_tables="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(table_name ORDER BY table_name) FROM information_schema.tables WHERE table_schema='$DB' AND table_name IN ('aihr_practice_audio','aihr_practice_calibration');")"
|
||||
test "$evidence_tables" = 'aihr_practice_audio,aihr_practice_calibration'
|
||||
evidence_tables="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(table_name ORDER BY table_name) FROM information_schema.tables WHERE table_schema='$DB' AND table_name IN ('aihr_practice_audio','aihr_practice_audio_upload','aihr_practice_calibration');")"
|
||||
test "$evidence_tables" = 'aihr_practice_audio,aihr_practice_audio_upload,aihr_practice_calibration'
|
||||
|
||||
mysql "$DB" <<'SQL'
|
||||
CREATE TABLE aihr_sop_answer_review (
|
||||
|
||||
Reference in New Issue
Block a user