package com.rj.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.entity.AudioManagementSegments; import com.rj.service.IAudioManagementSegmentsService; import com.rj.service.ITtsRequestLogService; import com.rj.tenant.TenantContextHolder; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; 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.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.io.File; import java.nio.file.Files; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; import com.rj.service.MinIOService; /** *

* 录音分段管理表 前端控制器 *

* * 对应表:audio_management_segments */ @RestController @RequestMapping("/api/audioManagementSegments") @Tag(name = "录音分段管理", description = "录音分段管理相关接口") @Slf4j public class AudioManagementSegmentsController { @Autowired private IAudioManagementSegmentsService audioManagementSegmentsService; /** * 新增录音分段 */ @PostMapping("/add") @Operation(summary = "新增录音分段", description = "添加新的录音分段信息") public ResponseEntity> addSegment( @Parameter(description = "录音分段信息", required = true) @RequestBody AudioManagementSegments segment) { Map result = new HashMap<>(); try { if (segment.getCreateTime() == null) { segment.setCreateTime(LocalDateTime.now()); } boolean success = audioManagementSegmentsService.save(segment); if (success) { result.put("success", true); result.put("message", "录音分段添加成功"); result.put("data", segment); return ResponseEntity.ok(result); } else { result.put("success", false); result.put("message", "录音分段添加失败"); return ResponseEntity.badRequest().body(result); } } catch (Exception e) { log.error("录音分段添加异常:{}", e.getMessage(), e); result.put("success", false); result.put("message", "录音分段添加异常:" + e.getMessage()); return ResponseEntity.internalServerError().body(result); } } /** * 根据ID查询录音分段 */ @GetMapping("/get/{id}") @Operation(summary = "根据ID查询录音分段", description = "根据分段ID获取录音分段详细信息") public ResponseEntity> getSegmentById( @Parameter(description = "分段ID", required = true) @PathVariable String id) { Map result = new HashMap<>(); try { AudioManagementSegments segment = audioManagementSegmentsService.getById(id); if (segment != null) { result.put("success", true); result.put("message", "查询成功"); result.put("data", segment); return ResponseEntity.ok(result); } else { result.put("success", false); result.put("message", "录音分段不存在"); return ResponseEntity.notFound().build(); } } catch (Exception e) { log.error("录音分段查询异常:{}", e.getMessage(), e); result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return ResponseEntity.internalServerError().body(result); } } /** * 根据ID播放录音分段音频文件 */ @PostMapping("/playSegmentById/{id}") @Operation(summary = "播放分段音频文件", description = "播放音频文件") public ResponseEntity playSegmentById( @Parameter(description = "分段ID", required = true) @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(); } 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(); } } @Autowired private ITtsRequestLogService ttsRequestLogService; @Autowired private MinIOService minIOService; /** * 转文本请求 */ @PostMapping("/transcribe/{id}") @Operation(summary = "转文本", description = "分段音频文件转文本") public ResponseEntity> transcribeSegmentById( @Parameter(description = "分段ID", required = true) @PathVariable String id) { Map result = new HashMap<>(); try { // 先查询记录是否存在 AudioManagementSegments segment = audioManagementSegmentsService.getById(id); if (segment == null) { result.put("success", false); result.put("message", "录音分段不存在,ID: " + id); return ResponseEntity.notFound().build(); } // 检查本地文件路径是否存在 String audioFilePath = segment.getAudioFilePath(); if (audioFilePath == null || audioFilePath.trim().isEmpty()) { result.put("success", false); result.put("message", "音频文件路径为空"); return ResponseEntity.badRequest().body(result); } File audioFile = new File(audioFilePath); if (!audioFile.exists() || !audioFile.isFile()) { result.put("success", false); result.put("message", "音频文件不存在: " + audioFilePath); return ResponseEntity.badRequest().body(result); } // TODO 1: 上传文件到MinIO String minioUrl = null; try { // 读取本地文件 byte[] fileBytes = Files.readAllBytes(audioFile.toPath()); String fileName = segment.getAudioFileOriginalName() != null ? segment.getAudioFileOriginalName() : audioFile.getName(); // 获取文件扩展名,确定contentType String contentType = getContentType(fileName); // 创建MultipartFile对象 MultipartFile multipartFile = new MockMultipartFile( "file", fileName, contentType, fileBytes ); // 上传到MinIO minioUrl = minIOService.uploadFile(multipartFile); log.info("文件上传到MinIO成功,ID: {}, minioUrl: {}", id, minioUrl); } catch (Exception e) { log.error("上传文件到MinIO失败,ID: {}, error: {}", id, e.getMessage(), e); result.put("success", false); result.put("message", "上传文件到MinIO失败: " + e.getMessage()); return ResponseEntity.internalServerError().body(result); } // 根据id修改audio_management_segments 表字段: needTranscribe为 1, transcribeRequestTime为当前时间 AudioManagementSegments updateEntity = new AudioManagementSegments(); updateEntity.setNeedTranscribe(true); updateEntity.setTranscribeRequestTime(LocalDateTime.now()); updateEntity.setAudioFileUrl(minioUrl); LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(AudioManagementSegments::getId, id); boolean success = audioManagementSegmentsService.updateNoTenant(updateEntity, updateWrapper); if (!success) { result.put("success", false); result.put("message", "更新转文本状态失败"); return ResponseEntity.badRequest().body(result); } // 从minioUrl中提取objectName,生成presignedUrl String presignedUrl = null; try { String objectName = extractObjectNameFromUrl(minioUrl); if (objectName != null) { // 生成7天有效期的预签名URL presignedUrl = minIOService.getPresignedUrl(objectName, 7 * 24 * 60 * 60); log.info("生成预签名URL成功,ID: {}, presignedUrl: {}", id, presignedUrl); } else { // 如果无法提取objectName,直接使用minioUrl presignedUrl = minioUrl; log.warn("无法从minioUrl提取objectName,使用原始URL: {}", minioUrl); } } catch (Exception e) { log.error("生成预签名URL失败,使用原始URL: {}", e.getMessage(), e); presignedUrl = minioUrl; } // TODO 2: 启用异步线程处理转文本 // 获取租户ID:优先从segment对象获取,如果没有则从当前线程的租户上下文获取 final String tenantId = segment.getTenantId() != null ? segment.getTenantId() : TenantContextHolder.getTenantId(); final String finalPresignedUrl = presignedUrl; final String segmentId = id; final String audioFileName = segment.getAudioFileOriginalName(); CompletableFuture.runAsync(() -> { try { // 在异步线程中设置租户上下文 if (tenantId != null && !tenantId.trim().isEmpty()) { TenantContextHolder.setTenantId(tenantId); log.info("异步线程中设置租户ID: {}, segmentId: {}", tenantId, segmentId); } else { log.warn("租户ID为空,segmentId: {}", segmentId); } log.info("开始异步转文本处理,ID: {}, tenantId: {}", segmentId, tenantId); AsrRequest asrRequest = new AsrRequest(); asrRequest.setAudioUrl(finalPresignedUrl); asrRequest.setAudioName(audioFileName); asrRequest.setModel("paraformer-v2"); // 使用默认模型 asrRequest.setFormat("wav"); // 默认格式 asrRequest.setSampleRate(16000); // 默认采样率 asrRequest.setEnablePunctuation(true); // 启用标点符号 asrRequest.setEnableNumberConversion(true); // 启用数字转换 asrRequest.setEnableSpeakerDiarization(false); // 不启用说话人分离 // 调用ASR服务进行语音识别 AsrResponse asrResponse = ttsRequestLogService.speechToText(asrRequest); if (asrResponse != null && asrResponse.isSuccess() && asrResponse.getText() != null) { // 更新转文本结果到数据库 AudioManagementSegments textUpdateEntity = new AudioManagementSegments(); textUpdateEntity.setRecordingText(asrResponse.getText()); textUpdateEntity.setTranscribeEndTime(LocalDateTime.now()); LambdaUpdateWrapper textUpdateWrapper = new LambdaUpdateWrapper<>(); textUpdateWrapper.eq(AudioManagementSegments::getId, segmentId); // 使用正常的update方法,因为已经设置了租户上下文 boolean updateSuccess = audioManagementSegmentsService.update(textUpdateEntity, textUpdateWrapper); if (updateSuccess) { log.info("转文本结果保存成功,ID: {}, tenantId: {}, 文本长度: {}", segmentId, tenantId, asrResponse.getText().length()); } else { log.error("转文本结果保存失败,ID: {}, tenantId: {}", segmentId, tenantId); } } else { String errorMsg = asrResponse != null ? asrResponse.getMessage() : "ASR响应为空"; log.error("转文本失败,ID: {}, tenantId: {}, error: {}", segmentId, tenantId, errorMsg); } } catch (Exception e) { log.error("异步转文本处理异常,ID: {}, tenantId: {}, error: {}", segmentId, tenantId, e.getMessage(), e); } finally { // 清理租户上下文,防止线程复用导致租户串用 TenantContextHolder.clear(); log.debug("清理异步线程租户上下文,segmentId: {}", segmentId); } }); result.put("success", true); result.put("message", "转文本请求已提交,正在后台处理"); result.put("data", updateEntity); return ResponseEntity.ok(result); } catch (Exception e) { log.error("转文本请求异常,ID: {}, error: {}", id, e.getMessage(), e); result.put("success", false); result.put("message", "转文本请求异常:" + e.getMessage()); return ResponseEntity.internalServerError().body(result); } } /** * 从MinIO URL中提取对象名 * @param url MinIO文件URL * @return 对象名 */ private String extractObjectNameFromUrl(String url) { if (url == null || url.trim().isEmpty()) { return null; } try { // 去掉查询参数 String urlWithoutParams = url; if (url.contains("?")) { urlWithoutParams = url.substring(0, url.indexOf("?")); } // 按"/"分割URL,获取最后一个部分作为对象名 String[] parts = urlWithoutParams.split("/"); if (parts.length > 0) { // 获取最后一个非空部分作为对象名 for (int i = parts.length - 1; i >= 0; i--) { if (parts[i] != null && !parts[i].trim().isEmpty()) { return parts[i]; } } } return null; } catch (Exception e) { log.error("从URL提取对象名失败: {}", url, e); return null; } } /** * 判断文件是否为音频文件 */ private boolean isAudioFile(String fileName) { if (fileName == null || fileName.isEmpty()) { return false; } String extension = getFileExtension(fileName).toLowerCase(); String[] audioExtensions = {"mp3", "wav", "m4a", "aac", "ogg", "flac", "wma"}; for (String ext : audioExtensions) { if (ext.equals(extension)) { return true; } } return false; } /** * 获取文件扩展名 */ private String getFileExtension(String fileName) { if (fileName == null || fileName.isEmpty()) { return ""; } int lastDotIndex = fileName.lastIndexOf('.'); if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) { return fileName.substring(lastDotIndex + 1); } return ""; } /** * 根据文件名获取Content-Type */ private String getContentType(String fileName) { String extension = getFileExtension(fileName).toLowerCase(); switch (extension) { case "mp3": return "audio/mpeg"; case "wav": return "audio/wav"; case "m4a": return "audio/mp4"; case "aac": return "audio/aac"; case "ogg": return "audio/ogg"; case "flac": return "audio/flac"; case "wma": return "audio/x-ms-wma"; default: return "application/octet-stream"; } } /** * 分页查询录音分段列表 */ @GetMapping("/list") @Operation(summary = "分页查询录音分段列表", description = "分页查询录音分段信息列表") public ResponseEntity> getSegmentList( @Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") Integer current, @Parameter(description = "每页大小", example = "10") @RequestParam(defaultValue = "10") Integer size, @Parameter(description = "父录音ID") @RequestParam(required = false) String parentId, @Parameter(description = "租户ID") @RequestParam(required = false) String tenantId, @Parameter(description = "设备编号(模糊查询)") @RequestParam(required = false) String deviceNo, @Parameter(description = "销售姓名(模糊查询)") @RequestParam(required = false) String salesName, @Parameter(description = "销售电话(模糊查询)") @RequestParam(required = false) String salesPhone, @Parameter(description = "意向级别") @RequestParam(required = false) String intentionLevel, @Parameter(description = "上传状态") @RequestParam(required = false) String uploadStatus, @Parameter(description = "同步状态") @RequestParam(required = false) String syncStatus) { Map result = new HashMap<>(); try { Page page = new Page<>(current, size); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); if (parentId != null && !parentId.trim().isEmpty()) { queryWrapper.eq(AudioManagementSegments::getParentId, parentId); } if (tenantId != null && !tenantId.trim().isEmpty()) { queryWrapper.eq(AudioManagementSegments::getTenantId, tenantId); } if (deviceNo != null && !deviceNo.trim().isEmpty()) { queryWrapper.like(AudioManagementSegments::getDeviceNo, deviceNo); } if (salesName != null && !salesName.trim().isEmpty()) { queryWrapper.like(AudioManagementSegments::getSalesName, salesName); } if (salesPhone != null && !salesPhone.trim().isEmpty()) { queryWrapper.like(AudioManagementSegments::getSalesPhone, salesPhone); } if (intentionLevel != null && !intentionLevel.trim().isEmpty()) { queryWrapper.eq(AudioManagementSegments::getIntentionLevel, intentionLevel); } if (uploadStatus != null && !uploadStatus.trim().isEmpty()) { queryWrapper.eq(AudioManagementSegments::getUploadStatus, uploadStatus); } if (syncStatus != null && !syncStatus.trim().isEmpty()) { queryWrapper.eq(AudioManagementSegments::getSyncStatus, syncStatus); } // 按创建时间倒序排列 queryWrapper.orderByDesc(AudioManagementSegments::getCreateTime); Page segmentPage = audioManagementSegmentsService.page(page, queryWrapper); result.put("success", true); result.put("message", "查询成功"); result.put("data", segmentPage.getRecords()); result.put("total", segmentPage.getTotal()); result.put("current", segmentPage.getCurrent()); result.put("size", segmentPage.getSize()); result.put("pages", segmentPage.getPages()); return ResponseEntity.ok(result); } catch (Exception e) { log.error("录音分段分页查询异常:{}", e.getMessage(), e); result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return ResponseEntity.internalServerError().body(result); } } /** * 根据父ID查询录音分段列表(不分页) */ @GetMapping("/listByParentId") @Operation(summary = "根据父ID查询录音分段列表(不分页)", description = "根据父录音ID查询所有分段列表,不分页") public ResponseEntity> listByParentId( @Parameter(description = "父录音ID", required = true) @RequestParam String parentId) { Map result = new HashMap<>(); try { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(AudioManagementSegments::getParentId, parentId); // 通常按开始时间或段索引排序 queryWrapper.orderByAsc(AudioManagementSegments::getStartTime); List list = audioManagementSegmentsService.list(queryWrapper); result.put("success", true); result.put("message", "查询成功"); result.put("data", list); result.put("count", list.size()); return ResponseEntity.ok(result); } catch (Exception e) { log.error("根据父ID查询录音分段列表异常:{}", e.getMessage(), e); result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return ResponseEntity.internalServerError().body(result); } } /** * 更新录音分段信息 */ @PutMapping("/update") @Operation(summary = "更新录音分段信息", description = "更新录音分段详细信息") public ResponseEntity> updateSegment( @Parameter(description = "录音分段信息", required = true) @RequestBody AudioManagementSegments segment) { Map result = new HashMap<>(); try { if (segment.getId() == null || segment.getId().trim().isEmpty()) { result.put("success", false); result.put("message", "分段ID不能为空"); return ResponseEntity.badRequest().body(result); } boolean success = audioManagementSegmentsService.updateById(segment); if (success) { result.put("success", true); result.put("message", "录音分段信息更新成功"); result.put("data", segment); return ResponseEntity.ok(result); } else { result.put("success", false); result.put("message", "录音分段信息更新失败"); return ResponseEntity.badRequest().body(result); } } catch (Exception e) { log.error("录音分段更新异常:{}", e.getMessage(), e); result.put("success", false); result.put("message", "更新异常:" + e.getMessage()); return ResponseEntity.internalServerError().body(result); } } /** * 根据ID删除录音分段 */ @DeleteMapping("/delete/{id}") @Operation(summary = "删除录音分段", description = "根据分段ID删除录音分段信息") public ResponseEntity> deleteSegment( @Parameter(description = "分段ID", required = true) @PathVariable String id) { Map result = new HashMap<>(); try { boolean success = audioManagementSegmentsService.removeById(id); if (success) { result.put("success", true); result.put("message", "录音分段删除成功"); return ResponseEntity.ok(result); } else { result.put("success", false); result.put("message", "录音分段删除失败"); return ResponseEntity.badRequest().body(result); } } catch (Exception e) { log.error("录音分段删除异常:{}", e.getMessage(), e); result.put("success", false); result.put("message", "删除异常:" + e.getMessage()); return ResponseEntity.internalServerError().body(result); } } /** * 批量删除录音分段 */ @DeleteMapping("/batchDelete") @Operation(summary = "批量删除录音分段", description = "根据分段ID列表批量删除录音分段信息") public ResponseEntity> batchDeleteSegments( @Parameter(description = "分段ID列表", required = true) @RequestBody List ids) { Map result = new HashMap<>(); try { if (ids == null || ids.isEmpty()) { result.put("success", false); result.put("message", "分段ID列表不能为空"); return ResponseEntity.badRequest().body(result); } boolean success = audioManagementSegmentsService.removeByIds(ids); if (success) { result.put("success", true); result.put("message", "批量删除成功,共删除 " + ids.size() + " 条记录"); return ResponseEntity.ok(result); } else { result.put("success", false); result.put("message", "批量删除失败"); return ResponseEntity.badRequest().body(result); } } catch (Exception e) { log.error("批量删除录音分段异常:{}", e.getMessage(), e); result.put("success", false); result.put("message", "批量删除异常:" + e.getMessage()); return ResponseEntity.internalServerError().body(result); } } }