diff --git a/pom.xml b/pom.xml index b8e59db..25bce1e 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.cst AIDriverEEBackend - 1.26032801.1-SNAPSHOT + 1.26032906.1-SNAPSHOT Langchain4j-rj Langchain4j-rj20250803 diff --git a/src/main/java/com/rj/controller/AudioFileController.java b/src/main/java/com/rj/controller/AudioFileController.java index e795aba..1b5e4ee 100644 --- a/src/main/java/com/rj/controller/AudioFileController.java +++ b/src/main/java/com/rj/controller/AudioFileController.java @@ -293,6 +293,7 @@ public class AudioFileController { log.info("收到语音转写数据,设备号: {}", requestBody.get("deviceNo")); // 收到语音转写数据 , 不再处理: 保存数据库 ,用python 处理 // processResult = handleAudioTextDataType(requestBody); + log.info("---------------------AudioText不再保存数据库-----------------------------"); break; case "LoginLog": log.info("收到登录日志数据,设备号: {}", requestBody.get("deviceNo")); diff --git a/src/main/java/com/rj/controller/AudioManagementSegmentsController.java b/src/main/java/com/rj/controller/AudioManagementSegmentsController.java index a677cd8..19d6f73 100644 --- a/src/main/java/com/rj/controller/AudioManagementSegmentsController.java +++ b/src/main/java/com/rj/controller/AudioManagementSegmentsController.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.rj.dto.AsrRequest; import com.rj.dto.AsrResponse; +import com.rj.config.MinIOConfig; import com.rj.entity.AudioManagementSegments; import com.rj.service.IAudioManagementSegmentsService; import com.rj.service.ITtsRequestLogService; @@ -15,6 +16,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -22,8 +24,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.io.File; +import java.io.InputStream; import java.nio.file.Files; import java.time.LocalDateTime; +import java.util.Locale; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,52 +122,123 @@ public class AudioManagementSegmentsController { @PathVariable String id) { try { AudioManagementSegments segment = audioManagementSegmentsService.getById(id); - + if (segment == null) { log.warn("录音分段不存在,ID: {}", id); return ResponseEntity.notFound().build(); } - String audioFilePath = segment.getAudioFilePath(); - if (audioFilePath == null || audioFilePath.trim().isEmpty()) { - log.warn("录音分段文件路径为空,ID: {}", id); - return ResponseEntity.badRequest().build(); + if (isWindowsOs()) { + return playSegmentUsingAudioFileUrl(segment, id); } + return playSegmentUsingLocalFilePath(segment, id); - File file = new File(audioFilePath); - if (!file.exists() || !file.isFile()) { - log.warn("音频文件不存在或不是文件,路径: {}", audioFilePath); - return ResponseEntity.notFound().build(); - } - - // 检查文件是否为音频文件 - if (!isAudioFile(audioFilePath)) { - log.warn("文件不是音频文件,路径: {}", audioFilePath); - return ResponseEntity.badRequest().build(); - } - - Resource resource = new FileSystemResource(file); - - // 设置响应头 - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.parseMediaType(getContentType(audioFilePath))); - headers.setContentLength(file.length()); - headers.set("Accept-Ranges", "bytes"); - - return ResponseEntity.ok() - .headers(headers) - .body(resource); - } catch (Exception e) { log.error("播放录音分段音频文件异常,ID: {}, error: {}", id, e.getMessage(), e); return ResponseEntity.internalServerError().build(); } } + + /** + * Linux 等环境:按分段表中的本地路径 {@link AudioManagementSegments#getAudioFilePath()} 读取并返回音频流(与原实现一致)。 + */ + private ResponseEntity playSegmentUsingLocalFilePath(AudioManagementSegments segment, String id) { + String audioFilePath = segment.getAudioFilePath(); + if (audioFilePath == null || audioFilePath.trim().isEmpty()) { + log.warn("录音分段文件路径为空,ID: {}", id); + return ResponseEntity.badRequest().build(); + } + + File file = new File(audioFilePath); + if (!file.exists() || !file.isFile()) { + log.warn("音频文件不存在或不是文件,路径: {}", audioFilePath); + return ResponseEntity.notFound().build(); + } + + if (!isAudioFile(audioFilePath)) { + log.warn("文件不是音频文件,路径: {}", audioFilePath); + return ResponseEntity.badRequest().build(); + } + + Resource resource = new FileSystemResource(file); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(getContentType(audioFilePath))); + headers.setContentLength(file.length()); + headers.set("Accept-Ranges", "bytes"); + + return ResponseEntity.ok() + .headers(headers) + .body(resource); + } + + /** + * Windows:在已根据 ID 查到分段记录的前提下,使用 {@link AudioManagementSegments#getAudioFileUrl()}(MinIO 访问 URL) + * 解析对象名并从 MinIO 拉流播放,逻辑对齐 {@link MinIOController#downloadFile} 的 downloadFile 调用方式。 + */ + private ResponseEntity playSegmentUsingAudioFileUrl(AudioManagementSegments segment, String id) { + String audioFileUrl = segment.getAudioFileUrl(); + if (audioFileUrl == null || audioFileUrl.trim().isEmpty()) { + log.warn("录音分段音频 URL 为空,ID: {}", id); + return ResponseEntity.badRequest().build(); + } + + String loc = audioFileUrl.trim(); + String nameProbe = lastPathSegmentForAudioCheck(loc); + if (!isAudioFile(nameProbe)) { + log.warn("文件不是音频文件,location: {}", loc); + return ResponseEntity.badRequest().build(); + } + + String objectName = resolveMinioObjectName(loc); + if (objectName == null || objectName.isEmpty()) { + log.warn("无法从 URL 解析 MinIO 对象名,ID: {}, location: {}", id, loc); + return ResponseEntity.badRequest().build(); + } + + try { + InputStream inputStream = minIOService.downloadFile(objectName); + Resource resource = new InputStreamResource(inputStream); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(getContentType(nameProbe))); + headers.set("Accept-Ranges", "bytes"); + + return ResponseEntity.ok() + .headers(headers) + .body(resource); + } catch (Exception e) { + log.error("从 MinIO 读取分段音频失败,ID: {}, objectName: {}", id, objectName, e); + return ResponseEntity.notFound().build(); + } + } + + private static boolean isWindowsOs() { + String os = System.getProperty("os.name", ""); + return os != null && os.toLowerCase(Locale.ROOT).contains("win"); + } + + /** 去掉查询串后取路径最后一段,避免 URL 中带 IP 时扩展名判断错误。 */ + private static String lastPathSegmentForAudioCheck(String urlOrPath) { + if (urlOrPath == null) { + return ""; + } + String s = urlOrPath.trim(); + int q = s.indexOf('?'); + if (q >= 0) { + s = s.substring(0, q); + } + int slash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\')); + return slash >= 0 ? s.substring(slash + 1) : s; + } @Autowired private ITtsRequestLogService ttsRequestLogService; @Autowired private MinIOService minIOService; + + @Autowired + private MinIOConfig minioConfig; /** * 转文本请求 @@ -341,6 +416,27 @@ public class AudioManagementSegmentsController { } } + /** + * 从分段表中的 MinIO 访问 URL 解析对象键:优先按 {@link MinIOConfig#getFileUrlPrefix()} 去掉前缀得到完整 object 路径 + *(与 {@link MinIOService#getFileUrl(String)} 生成的 URL 一致),否则回退为 {@link #extractObjectNameFromUrl(String)}。 + */ + private String resolveMinioObjectName(String audioFileUrl) { + if (audioFileUrl == null || audioFileUrl.trim().isEmpty()) { + return null; + } + String trimmed = audioFileUrl.trim(); + String withoutQuery = trimmed; + int q = trimmed.indexOf('?'); + if (q >= 0) { + withoutQuery = trimmed.substring(0, q); + } + String prefix = minioConfig.getFileUrlPrefix(); + if (prefix != null && !prefix.isEmpty() && withoutQuery.startsWith(prefix)) { + return withoutQuery.substring(prefix.length()); + } + return extractObjectNameFromUrl(trimmed); + } + /** * 从MinIO URL中提取对象名 * @param url MinIO文件URL diff --git a/src/main/java/com/rj/mapper/AudioManagementSegmentsMapper.java b/src/main/java/com/rj/mapper/AudioManagementSegmentsMapper.java index 466045f..ea5cb85 100644 --- a/src/main/java/com/rj/mapper/AudioManagementSegmentsMapper.java +++ b/src/main/java/com/rj/mapper/AudioManagementSegmentsMapper.java @@ -1,7 +1,13 @@ package com.rj.mapper; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.rj.entity.AudioManagementSegments; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; /** *

@@ -12,5 +18,20 @@ import com.rj.entity.AudioManagementSegments; */ public interface AudioManagementSegmentsMapper extends BaseMapper { -} + /** + * 本地路径已填、MinIO URL 未填的分段。使用注解 SQL,避免 Lambda 条件生成 audio_file_url / audio_file_path 的多余占位符, + * 并在此方法上关闭租户行插件(否则会出现 tenant_id = null 等错误条件)。 + */ + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT * FROM audio_management_segments WHERE (audio_file_url IS NULL OR audio_file_url = '') " + + "AND audio_file_path IS NOT NULL AND audio_file_path <> '' " + + "ORDER BY create_time ASC LIMIT #{limit}") + List listSegmentsNeedingMinioUpload(@Param("limit") int limit); + /** + * 仅按主键更新 MinIO URL,不拼接 tenant_id(供调度任务等无租户上下文场景使用)。 + */ + @InterceptorIgnore(tenantLine = "true") + @Update("UPDATE audio_management_segments SET audio_file_url = #{audioFileUrl} WHERE id = #{id}") + int updateAudioFileUrlByIdIgnoreTenant(@Param("id") String id, @Param("audioFileUrl") String audioFileUrl); +} diff --git a/src/main/java/com/rj/scheduler/AudioStatisticsScheduler.java b/src/main/java/com/rj/scheduler/AudioStatisticsScheduler.java index ab6a053..6550937 100644 --- a/src/main/java/com/rj/scheduler/AudioStatisticsScheduler.java +++ b/src/main/java/com/rj/scheduler/AudioStatisticsScheduler.java @@ -1,13 +1,23 @@ package com.rj.scheduler; import com.rj.config.AppConfig; +import com.rj.entity.AudioManagementSegments; +import com.rj.service.IAudioManagementSegmentsService; import com.rj.service.IAudioManagementStatisticsService; +import com.rj.service.MinIOService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import java.io.FileInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.LocalDate; +import java.util.List; +import java.util.UUID; /** * 门店录音统计定时任务 @@ -25,6 +35,67 @@ public class AudioStatisticsScheduler { @Autowired private AppConfig appConfig; + @Autowired + private IAudioManagementSegmentsService audioManagementSegmentsService; + + @Autowired + private MinIOService minIOService; + + /** + * 每 5 秒:将仅有本地路径、尚未写入 MinIO URL 的录音分段补传到 Minio 并回写 audio_file_url。 + */ + @Scheduled(fixedRate = 50000) + public void uploadLocalSegmentFilesToMinio() { + if (!appConfig.getScheduler().isStart()) { + return; + } + try { + List pending = audioManagementSegmentsService.listSegmentsNeedingMinioUpload(50); + if (pending.isEmpty()) { + return; + } + for (AudioManagementSegments segment : pending) { + uploadOneSegmentFileToMinio(segment); + } + } catch (Exception e) { + log.error("录音分段补传 MinIO 定时任务执行失败", e); + } + } + + private void uploadOneSegmentFileToMinio(AudioManagementSegments segment) { + String pathStr = segment.getAudioFilePath(); + Path path = Paths.get(pathStr); + if (!Files.isRegularFile(path)) { + log.warn("分段本地文件不存在,跳过: id={}, path={}", segment.getId(), pathStr); + return; + } + String originalName = segment.getAudioFileOriginalName(); + if (!StringUtils.hasText(originalName)) { + originalName = path.getFileName().toString(); + } + String objectName = UUID.randomUUID().toString().replace("-", "") + originalName; + String contentType; + try { + contentType = Files.probeContentType(path); + } catch (Exception e) { + contentType = null; + } + if (!StringUtils.hasText(contentType)) { + contentType = "application/octet-stream"; + } + try (FileInputStream in = new FileInputStream(path.toFile())) { + String minioUrl = minIOService.uploadFile(in, objectName, contentType); + boolean ok = audioManagementSegmentsService.updateAudioFileUrlByIdIgnoreTenant(segment.getId(), minioUrl); + if (!ok) { + log.warn("补传成功但更新 audio_file_url 未生效: id={}", segment.getId()); + } else { + log.info("分段已补传 MinIO: id={}", segment.getId()); + } + } catch (Exception e) { + log.error("分段补传 MinIO 失败: id={}, path={}", segment.getId(), pathStr, e); + } + } + /** * 每天凌晨2点执行门店录音统计 * 统计昨天的录音数据 @@ -65,6 +136,7 @@ public class AudioStatisticsScheduler { throw e; } } + } diff --git a/src/main/java/com/rj/service/IAudioManagementSegmentsService.java b/src/main/java/com/rj/service/IAudioManagementSegmentsService.java index 7dbbcb7..9579836 100644 --- a/src/main/java/com/rj/service/IAudioManagementSegmentsService.java +++ b/src/main/java/com/rj/service/IAudioManagementSegmentsService.java @@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.IService; import com.rj.entity.AudioManagementSegments; +import java.util.List; + /** *

* 录音分段管理表 服务类 @@ -17,5 +19,15 @@ public interface IAudioManagementSegmentsService extends IService updateWrapper) ; + + /** + * 查询本地已有文件路径但未写入 MinIO URL 的分段(供定时补传任务使用;SQL 在 Mapper 注解中并忽略租户插件)。 + */ + List listSegmentsNeedingMinioUpload(int limit); + + /** + * 按主键更新 audio_file_url,不附加租户条件(与 {@link #updateNoTenant} 不同,此方法走 Mapper 注解 SQL,租户插件确定被忽略)。 + */ + boolean updateAudioFileUrlByIdIgnoreTenant(String id, String audioFileUrl); } diff --git a/src/main/java/com/rj/service/impl/AudioManagementSegmentsServiceImpl.java b/src/main/java/com/rj/service/impl/AudioManagementSegmentsServiceImpl.java index 1c7a640..11527c9 100644 --- a/src/main/java/com/rj/service/impl/AudioManagementSegmentsServiceImpl.java +++ b/src/main/java/com/rj/service/impl/AudioManagementSegmentsServiceImpl.java @@ -8,6 +8,8 @@ import com.rj.mapper.AudioManagementSegmentsMapper; import com.rj.service.IAudioManagementSegmentsService; import org.springframework.stereotype.Service; +import java.util.List; + /** *

* 录音分段管理表 服务实现类 @@ -26,5 +28,16 @@ public class AudioManagementSegmentsServiceImpl return super.update(entity, updateWrapper); } + @Override + public List listSegmentsNeedingMinioUpload(int limit) { + int safeLimit = Math.max(1, Math.min(limit, 500)); + return baseMapper.listSegmentsNeedingMinioUpload(safeLimit); + } + + @Override + public boolean updateAudioFileUrlByIdIgnoreTenant(String id, String audioFileUrl) { + return baseMapper.updateAudioFileUrlByIdIgnoreTenant(id, audioFileUrl) > 0; + } + }