diff --git a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java index 510a24d..b5c242f 100644 --- a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java @@ -248,8 +248,8 @@ public class AndroidMeetingController { command == null ? null : command.getTotalChunks(), authContext ); - if (uploadResult == null || uploadResult.getAudioUrl() == null || uploadResult.getAudioUrl().isBlank()) { - throw new RuntimeException("分片上传完成后未生成 audio_url"); + if (uploadResult == null) { + throw new RuntimeException("分片上传完成后未生成结果"); } } meetingCommandService.finishOfflineMeeting(meeting.getId(), command == null ? null : command.getFinishStage()); diff --git a/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyUploadAudioResponse.java b/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyUploadAudioResponse.java index 30f506c..5b48574 100644 --- a/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyUploadAudioResponse.java +++ b/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyUploadAudioResponse.java @@ -18,4 +18,12 @@ public class LegacyUploadAudioResponse { @JsonProperty("audio_url") @Schema(description = "上传后的音频访问地址") private String audioUrl; + + @JsonProperty("message") + @Schema(description = "结果提示") + private String message; + + public LegacyUploadAudioResponse(Long meetingId, String audioUrl) { + this(meetingId, audioUrl, null); + } } diff --git a/backend/src/main/java/com/imeeting/service/android/impl/AndroidChunkUploadServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/impl/AndroidChunkUploadServiceImpl.java index 6e6de3f..2b9544c 100644 --- a/backend/src/main/java/com/imeeting/service/android/impl/AndroidChunkUploadServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/impl/AndroidChunkUploadServiceImpl.java @@ -5,8 +5,8 @@ import com.imeeting.dto.android.AndroidChunkUploadSessionState; import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse; import com.imeeting.service.android.AndroidChunkUploadService; import com.imeeting.service.android.legacy.LegacyMeetingAdapterService; +import com.imeeting.service.biz.MeetingCommandService; import com.imeeting.support.redis.AndroidChunkUploadSessionCache; -import com.unisbase.security.LoginUser; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -32,10 +32,13 @@ import java.util.regex.Pattern; @Service @RequiredArgsConstructor public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService { - private static final Pattern CHUNK_FILE_NAME_PATTERN = Pattern.compile("^chunk-(\\d+)(\\..+)?$"); + private static final Pattern LEGACY_CHUNK_FILE_NAME_PATTERN = Pattern.compile("^chunk-(\\d+)(\\..+)?$"); + private static final Pattern CHUNK_DIR_NAME_PATTERN = Pattern.compile("^chunk-(\\d+)$"); + private static final String CHUNK_ROOT_DIR = "chunks"; private final AndroidChunkUploadSessionCache sessionCache; private final LegacyMeetingAdapterService legacyMeetingAdapterService; + private final MeetingCommandService meetingCommandService; @Value("${unisbase.app.upload-path}") private String uploadPath; @@ -54,7 +57,7 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService if (chunkIndex == null || chunkIndex < 0) { throw new RuntimeException("分片参数无效"); } - if (chunkFile == null) { + if (chunkFile == null) { throw new RuntimeException("chunk_file不能为空"); } @@ -63,19 +66,19 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService throw new RuntimeException("分片上传会话与当前设备或会议不匹配"); } - String chunkFileName = buildStoredChunkFileName(chunkIndex, chunkFile.getOriginalFilename()); - String previousFileName = state.getChunkFileNames().get(chunkIndex); - Path meetingDir = sessionDir(meetingId); - Files.createDirectories(meetingDir); + String originalFileName = resolveOriginalFileName(chunkFile.getOriginalFilename()); + Path chunkFilePath = resolveChunkFilePath(meetingId, chunkIndex, originalFileName); + Path chunkDir = chunkFilePath.getParent(); + clearChunkDirectory(chunkDir); + Files.createDirectories(chunkDir); - if (previousFileName != null && !previousFileName.equals(chunkFileName)) { - deleteQuietly(meetingDir.resolve(previousFileName)); + Files.write(chunkFilePath, chunkFile.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + String previousFileName = state.getChunkFileNames().put(chunkIndex, originalFileName); + if (previousFileName != null && !previousFileName.equals(originalFileName)) { state.getUploadedChunkFileNames().remove(previousFileName); } - - Files.write(meetingDir.resolve(chunkFileName), chunkFile.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - state.getUploadedChunkFileNames().add(chunkFileName); - state.getChunkFileNames().put(chunkIndex, chunkFileName); + state.getUploadedChunkFileNames().add(originalFileName); state.getReceivedChunks().add(chunkIndex); saveState(meetingId, state); } @@ -91,47 +94,39 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService throw new RuntimeException("total_chunks不能为空且必须大于0"); } - AndroidChunkUploadSessionState state = loadStateForCompletion(meetingId, authContext); + AndroidChunkUploadSessionState state = loadStateForCompletion(meetingId, authContext); if (!Objects.equals(state.getMeetingId(), meetingId) || !Objects.equals(state.getDeviceId(), authContext.getDeviceId())) { throw new RuntimeException("分片上传会话与当前设备或会议不匹配"); } - Path meetingDir = sessionDir(meetingId); - Files.createDirectories(meetingDir); + Path meetingDir = sessionDir(meetingId); + Files.createDirectories(meetingDir); state.setTotalChunks(totalChunks); - List orderedChunkPaths = rebuildChunkStateFromDisk(state, meetingDir, totalChunks); + List orderedChunkPaths = rebuildChunkStateFromDisk(state, meetingDir, totalChunks); saveState(meetingId, state); - Path mergedFile = mergeChunks(state, orderedChunkPaths); + Path mergedFile = mergeChunks(state, orderedChunkPaths); + if (mergedFile == null) { + meetingCommandService.failOfflineTranscription(meetingId, "安卓上传文件为空"); + cleanup(meetingId); + return new LegacyUploadAudioResponse(meetingId, null, "无可合并音频"); + } + MultipartFile mergedMultipart = new LocalMultipartFile( - buildMergedOriginalFilename(state, mergedFile), + resolveMergedOriginalFilename(state, orderedChunkPaths, mergedFile), state.getContentType(), Files.readAllBytes(mergedFile) ); - LegacyUploadAudioResponse response; - if (authContext.isAnonymous()) { - response = legacyMeetingAdapterService.uploadAndTriggerOfflineProcessForPublicDevice( - meetingId, - null, - null, - false, - mergedMultipart, - authContext - ); - } else { - LoginUser loginUser = toLoginUser(authContext); - response = legacyMeetingAdapterService.uploadAndTriggerOfflineProcess( - meetingId, - null, - null, - false, - mergedMultipart, - authContext, - loginUser - ); - } + LegacyUploadAudioResponse response = legacyMeetingAdapterService.uploadAndTriggerOfflineProcessForPublicDevice( + meetingId, + null, + null, + false, + mergedMultipart, + authContext + ); if (response != null) { cleanup(meetingId); } @@ -148,155 +143,170 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService AndroidChunkUploadSessionState state = new AndroidChunkUploadSessionState(); state.setMeetingId(meetingId); state.setDeviceId(authContext.getDeviceId()); - state.setFileName(normalizeChunkSourceFileName(chunkFile.getOriginalFilename())); + state.setFileName(resolveOriginalFileName(chunkFile.getOriginalFilename())); state.setContentType(chunkFile.getContentType()); saveState(meetingId, state); return state; } - private AndroidChunkUploadSessionState loadStateForCompletion(Long meetingId, AndroidAuthContext authContext) { - AndroidChunkUploadSessionState state = getState(meetingId); - if (state != null) { - return state; - } - AndroidChunkUploadSessionState rebuiltState = new AndroidChunkUploadSessionState(); - rebuiltState.setMeetingId(meetingId); - rebuiltState.setDeviceId(authContext.getDeviceId()); - return rebuiltState; - } - - private List rebuildChunkStateFromDisk(AndroidChunkUploadSessionState state, - Path meetingDir, - int totalChunks) throws IOException { - Map validatedChunkFiles = scanChunkFiles(meetingDir, true); - Map mergeChunkFiles = scanChunkFiles(meetingDir, false); - state.getReceivedChunks().clear(); - state.getUploadedChunkFileNames().clear(); - state.getChunkFileNames().clear(); - - for (Map.Entry entry : validatedChunkFiles.entrySet()) { - Integer chunkIndex = entry.getKey(); - Path stateChunkPath = mergeChunkFiles.getOrDefault(chunkIndex, entry.getValue()); - String fileName = stateChunkPath.getFileName().toString(); - state.getReceivedChunks().add(chunkIndex); - state.getUploadedChunkFileNames().add(fileName); - state.getChunkFileNames().put(chunkIndex, fileName); + private AndroidChunkUploadSessionState loadStateForCompletion(Long meetingId, AndroidAuthContext authContext) { + AndroidChunkUploadSessionState state = getState(meetingId); + if (state != null) { + return state; + } + AndroidChunkUploadSessionState rebuiltState = new AndroidChunkUploadSessionState(); + rebuiltState.setMeetingId(meetingId); + rebuiltState.setDeviceId(authContext.getDeviceId()); + return rebuiltState; } - List orderedChunkPaths = new ArrayList<>(totalChunks); - for (int i = 0; i < totalChunks; i++) { - Path validatedChunkPath = validatedChunkFiles.get(i); - if (validatedChunkPath == null) { - throw new RuntimeException("分片未上传完整"); - } - Path chunkPath = mergeChunkFiles.get(i); - if (chunkPath != null) { - orderedChunkPaths.add(chunkPath); - } - } - return orderedChunkPaths; - } + private List rebuildChunkStateFromDisk(AndroidChunkUploadSessionState state, + Path meetingDir, + int totalChunks) throws IOException { + Map chunkFiles = scanChunkFiles(meetingDir, true); + state.getReceivedChunks().clear(); + state.getUploadedChunkFileNames().clear(); + state.getChunkFileNames().clear(); - private Map scanChunkFiles(Path meetingDir, boolean includePending) throws IOException { - Map chunkFiles = new TreeMap<>(); - if (!Files.exists(meetingDir)) { - return chunkFiles; - } - try (var paths = Files.list(meetingDir)) { - paths.filter(Files::isRegularFile) - .filter(path -> isUsableChunkFile(path, includePending)) - .forEach(path -> { - Integer chunkIndex = parseChunkIndex(path.getFileName().toString(), includePending); - if (chunkIndex == null) { - return; - } - Path existing = chunkFiles.get(chunkIndex); - if (existing == null || isPreferredChunkFile(path, existing)) { - chunkFiles.put(chunkIndex, path); - } - }); - } - return chunkFiles; - } + for (Map.Entry entry : chunkFiles.entrySet()) { + Integer chunkIndex = entry.getKey(); + Path chunkPath = entry.getValue(); + String fileName = chunkPath.getFileName().toString(); + state.getReceivedChunks().add(chunkIndex); + state.getUploadedChunkFileNames().add(fileName); + state.getChunkFileNames().put(chunkIndex, fileName); + } - private boolean isUsableChunkFile(Path path, boolean includePending) { - if (path == null || path.getFileName() == null) { - return false; + List orderedChunkPaths = new ArrayList<>(totalChunks); + for (int i = 0; i < totalChunks; i++) { + Path chunkPath = chunkFiles.get(i); + if (chunkPath == null) { + throw new RuntimeException("分片未上传完整"); + } + orderedChunkPaths.add(chunkPath); + } + return orderedChunkPaths; } - String fileName = normalizeChunkStorageFileName(path.getFileName().toString(), includePending); - if (fileName == null) { - return false; - } - return CHUNK_FILE_NAME_PATTERN.matcher(fileName).matches(); - } - private Integer parseChunkIndex(String fileName, boolean includePending) { - String normalizedFileName = normalizeChunkStorageFileName(fileName, includePending); - if (normalizedFileName == null) { - return null; - } - Matcher matcher = CHUNK_FILE_NAME_PATTERN.matcher(normalizedFileName); - if (!matcher.matches()) { - return null; - } - return Integer.parseInt(matcher.group(1)); - } + private Map scanChunkFiles(Path meetingDir, boolean includePending) throws IOException { + Map chunkFiles = new TreeMap<>(); + if (!Files.exists(meetingDir)) { + return chunkFiles; + } - private boolean isPreferredChunkFile(Path candidate, Path existing) { - boolean candidatePending = isPendingChunkFile(candidate); - boolean existingPending = isPendingChunkFile(existing); - if (candidatePending != existingPending) { - return !candidatePending; - } - try { - return Files.getLastModifiedTime(candidate).compareTo(Files.getLastModifiedTime(existing)) >= 0; - } catch (IOException ex) { - return candidate.getFileName().toString().compareTo(existing.getFileName().toString()) >= 0; - } - } + Path chunkRoot = meetingDir.resolve(CHUNK_ROOT_DIR); + if (Files.exists(chunkRoot)) { + try (var dirs = Files.list(chunkRoot)) { + dirs.filter(Files::isDirectory).forEach(dir -> { + Integer chunkIndex = parseChunkDirIndex(dir.getFileName().toString()); + if (chunkIndex == null || chunkFiles.containsKey(chunkIndex)) { + return; + } + Path chunkPath = pickChunkFile(dir, includePending); + if (chunkPath != null) { + chunkFiles.put(chunkIndex, chunkPath); + } + }); + } + if (!chunkFiles.isEmpty()) { + return chunkFiles; + } + } - private String normalizeChunkStorageFileName(String fileName, boolean includePending) { - if (fileName == null) { - return null; + try (var paths = Files.list(meetingDir)) { + paths.filter(Files::isRegularFile) + .forEach(path -> { + Integer chunkIndex = parseLegacyChunkIndex(path.getFileName().toString()); + if (chunkIndex != null) { + chunkFiles.put(chunkIndex, path); + } + }); + } + return chunkFiles; } - if (fileName.endsWith(".pending")) { - return includePending ? fileName.substring(0, fileName.length() - ".pending".length()) : null; + + private Integer parseChunkDirIndex(String directoryName) { + if (directoryName == null) { + return null; + } + Matcher matcher = CHUNK_DIR_NAME_PATTERN.matcher(directoryName); + if (!matcher.matches()) { + return null; + } + return Integer.parseInt(matcher.group(1)); } - return fileName; - } - private boolean isPendingChunkFile(Path path) { - return path != null && path.getFileName() != null && path.getFileName().toString().endsWith(".pending"); - } + private Integer parseLegacyChunkIndex(String fileName) { + if (fileName == null) { + return null; + } + Matcher matcher = LEGACY_CHUNK_FILE_NAME_PATTERN.matcher(fileName); + if (!matcher.matches()) { + return null; + } + return Integer.parseInt(matcher.group(1)); + } - private Path mergeChunks(AndroidChunkUploadSessionState state, List chunkPaths) throws IOException { + private Path pickChunkFile(Path chunkDir, boolean includePending) { + if (chunkDir == null || !Files.isDirectory(chunkDir)) { + return null; + } + try (var files = Files.list(chunkDir)) { + Path preferred = files + .filter(Files::isRegularFile) + .filter(path -> includePending || !isPendingChunkFile(path)) + .findFirst() + .orElse(null); + if (preferred != null) { + return preferred; + } + } catch (IOException ex) { + return null; + } + try (var files = Files.list(chunkDir)) { + return files.filter(Files::isRegularFile).findFirst().orElse(null); + } catch (IOException ex) { + return null; + } + } + + private Path mergeChunks(AndroidChunkUploadSessionState state, List chunkPaths) throws IOException { + List mergeableChunkPaths = filterMergeableChunkPaths(chunkPaths); Path meetingDir = sessionDir(state.getMeetingId()); - String mergedExtension = resolveMergedExtension(state, chunkPaths); + String mergedExtension = resolveMergedExtension(state, mergeableChunkPaths); Path mergedOutput = meetingDir.resolve("merged" + mergedExtension); Path concatList = meetingDir.resolve("concat-inputs.txt"); Files.deleteIfExists(mergedOutput); - if (chunkPaths.isEmpty() || allChunkFilesEmpty(chunkPaths)) { - Files.write(mergedOutput, new byte[0], StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - return mergedOutput; - } + if (mergeableChunkPaths.isEmpty() || allChunkFilesEmpty(mergeableChunkPaths)) { + return null; + } - if (chunkPaths.size() == 1) { - return chunkPaths.get(0); - } + if (mergeableChunkPaths.size() == 1) { + return mergeableChunkPaths.get(0); + } - writeConcatListFile(concatList, chunkPaths); + writeConcatListFile(concatList, mergeableChunkPaths); executeFfmpegConcat(concatList, mergedOutput); return mergedOutput; } - private boolean allChunkFilesEmpty(List chunkPaths) throws IOException { - for (Path chunkPath : chunkPaths) { - if (Files.size(chunkPath) > 0) { - return false; - } + private boolean allChunkFilesEmpty(List chunkPaths) throws IOException { + for (Path chunkPath : chunkPaths) { + if (Files.size(chunkPath) > 0) { + return false; + } + } + return true; } - return true; + + private List filterMergeableChunkPaths(List chunkPaths) { + if (chunkPaths == null || chunkPaths.isEmpty()) { + return List.of(); + } + return chunkPaths.stream() + .filter(path -> path != null && !isPendingChunkFile(path)) + .toList(); } private AndroidChunkUploadSessionState getState(Long meetingId) { @@ -329,22 +339,19 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService return Paths.get(normalizedBasePath, "android-chunks", String.valueOf(meetingId)); } - private String buildStoredChunkFileName(Integer chunkIndex, String originalFileName) { - String normalizedSourceName = normalizeChunkSourceFileName(originalFileName); - int extensionIndex = normalizedSourceName.lastIndexOf('.'); - String extension = extensionIndex >= 0 ? normalizedSourceName.substring(extensionIndex) : ""; - String safeExtension = extension.isBlank() ? ".bin" : extension; - return "chunk-" + (chunkIndex == null ? "unknown" : chunkIndex) + safeExtension; + private Path resolveChunkFilePath(Long meetingId, Integer chunkIndex, String originalFileName) { + return sessionDir(meetingId) + .resolve(CHUNK_ROOT_DIR) + .resolve("chunk-" + chunkIndex) + .resolve(originalFileName); } - private String buildMergedOriginalFilename(AndroidChunkUploadSessionState state, Path mergedFile) { - if (state.getFileName() != null && !state.getFileName().isBlank()) { - String normalizedSourceName = normalizeChunkSourceFileName(state.getFileName()); - int extensionIndex = normalizedSourceName.lastIndexOf('.'); - if (extensionIndex >= 0) { - return "merged" + normalizedSourceName.substring(extensionIndex); - } - return normalizedSourceName; + private String resolveMergedOriginalFilename(AndroidChunkUploadSessionState state, List chunkPaths, Path mergedFile) { + if (state != null && state.getFileName() != null && !state.getFileName().isBlank()) { + return state.getFileName(); + } + if (chunkPaths != null && !chunkPaths.isEmpty() && chunkPaths.get(0).getFileName() != null) { + return chunkPaths.get(0).getFileName().toString(); } return mergedFile == null || mergedFile.getFileName() == null ? "meeting-audio.bin" : mergedFile.getFileName().toString(); } @@ -353,41 +360,56 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService if (chunkPath == null || chunkPath.getFileName() == null) { return ".bin"; } - String fileName = normalizeChunkSourceFileName(chunkPath.getFileName().toString()); + String fileName = chunkPath.getFileName().toString(); int extensionIndex = fileName.lastIndexOf('.'); return extensionIndex >= 0 ? fileName.substring(extensionIndex) : ".bin"; } - private String resolveMergedExtension(AndroidChunkUploadSessionState state, List chunkPaths) { - if (chunkPaths != null && !chunkPaths.isEmpty()) { - return resolveMergedExtension(chunkPaths.get(0)); - } - if (state != null && state.getFileName() != null && !state.getFileName().isBlank()) { - int extensionIndex = state.getFileName().lastIndexOf('.'); - if (extensionIndex >= 0) { - return state.getFileName().substring(extensionIndex); - } - } - return ".bin"; + private String resolveMergedExtension(AndroidChunkUploadSessionState state, List chunkPaths) { + if (chunkPaths != null && !chunkPaths.isEmpty()) { + return resolveMergedExtension(chunkPaths.get(0)); + } + if (state != null && state.getFileName() != null && !state.getFileName().isBlank()) { + int extensionIndex = state.getFileName().lastIndexOf('.'); + if (extensionIndex >= 0) { + return state.getFileName().substring(extensionIndex); + } + } + return ".bin"; } - private String normalizeChunkSourceFileName(String fileName) { - if (fileName == null) { - return ""; + private boolean isPendingChunkFile(Path path) { + return path != null && path.getFileName() != null && path.getFileName().toString().endsWith(".pending"); + } + + private String resolveOriginalFileName(String originalFileName) { + if (originalFileName == null || originalFileName.trim().isEmpty()) { + throw new RuntimeException("chunk_file原始文件名不能为空"); } - String normalized = Paths.get(fileName.trim()).getFileName().toString(); - if (normalized.endsWith(".pending")) { - return normalized.substring(0, normalized.length() - ".pending".length()); + if (originalFileName.contains("/") || originalFileName.contains("\\")) { + throw new RuntimeException("chunk_file文件名不合法"); + } + return originalFileName; + } + + private void clearChunkDirectory(Path chunkDir) throws IOException { + if (chunkDir == null || !Files.exists(chunkDir)) { + return; + } + try (var paths = Files.walk(chunkDir)) { + paths.sorted((left, right) -> right.compareTo(left)).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }); } - return normalized; } private void writeConcatListFile(Path concatList, List chunkPaths) throws IOException { List lines = new ArrayList<>(chunkPaths.size()); for (Path chunkPath : chunkPaths) { - if (chunkPath.getFileName() != null && chunkPath.getFileName().toString().endsWith(".pending")) { - continue; - } String normalizedPath = chunkPath.toAbsolutePath().toString().replace("'", "'\\''"); lines.add("file '" + normalizedPath + "'"); } @@ -424,37 +446,11 @@ public class AndroidChunkUploadServiceImpl implements AndroidChunkUploadService if (process.exitValue() != 0) { throw new IOException("音频分片合并失败: " + new String(output, StandardCharsets.UTF_8)); } - if (!Files.exists(mergedOutput)) { + if (!Files.exists(mergedOutput)) { throw new IOException("音频分片合并结果为空"); } } - private void deleteQuietly(Path path) { - if (path == null) { - return; - } - try { - Files.deleteIfExists(path); - } catch (IOException ignored) { - } - } - - private LoginUser toLoginUser(AndroidAuthContext authContext) { - if (authContext == null || authContext.isAnonymous() || authContext.getUserId() == null || authContext.getTenantId() == null) { - throw new RuntimeException("安卓用户未登录或认证无效"); - } - LoginUser loginUser = new LoginUser( - authContext.getUserId(), - authContext.getTenantId(), - authContext.getUsername(), - authContext.getPlatformAdmin(), - authContext.getTenantAdmin(), - authContext.getPermissions() - ); - loginUser.setDisplayName(authContext.getDisplayName()); - return loginUser; - } - private static final class LocalMultipartFile implements MultipartFile { private final String originalFilename; private final String contentType; diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java b/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java index 124b3c3..6ed8965 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingCommandService.java @@ -36,6 +36,8 @@ public interface MeetingCommandService { void finishOfflineMeeting(Long meetingId, String finishStage); + void failOfflineTranscription(Long meetingId, String failureMessage); + void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label); void updateMeetingTranscript(UpdateMeetingTranscriptCommand command); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index 3681ece..c43a309 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -278,6 +278,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme failPendingSummaryTask(sumTask, "没有可用于总结的转录内容"); updateMeetingStatus(meetingId, 4); updateProgress(meetingId, -1, "未识别到可用于总结的转录内容", 0); + androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.FAILED_TRANSCRIBING.getCode()); return; } if (!asrText.isBlank()) { @@ -1488,6 +1489,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme if ("SUMMARY".equals(task.getTaskType())) { meetingPointsService.markSummaryChargeFailed(task.getId(), error); } + androidMeetingPushService.pushMeetingStatusChanged(task.getMeetingId(), UnifiedMeetingStatusStage.FAILED_SUMMARIZING.getCode()); } private String buildAsrFailureMessage(Exception ex) { diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java index 7aa55c0..95631b8 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingCommandServiceImpl.java @@ -27,6 +27,7 @@ import com.imeeting.entity.biz.HotWord; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.entity.biz.MeetingTranscriptChapterVersion; +import com.imeeting.enums.BusinessErrorCodeEnum; import com.imeeting.enums.MeetingStatusEnum; import com.imeeting.service.android.AndroidPendingMeetingDraftService; import com.imeeting.service.android.AndroidPushMessageService; @@ -45,6 +46,8 @@ import com.imeeting.service.biz.RealtimeMeetingSessionStateService; import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService; import com.imeeting.support.redis.MeetingAsrPermitCache; import com.imeeting.support.redis.MeetingLockCache; +import com.unisbase.common.exception.BusinessException; +import com.unisbase.common.exception.ErrorCodeEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -522,6 +525,22 @@ public class MeetingCommandServiceImpl implements MeetingCommandService { meetingService.updateById(meeting); } + @Override + @Transactional(rollbackFor = Exception.class) + public void failOfflineTranscription(Long meetingId, String failureMessage) { + Meeting meeting = meetingService.getById(meetingId); + if (meeting == null) { + throw new BusinessException(BusinessErrorCodeEnum.MEETING_NOT_FOUND.getCode(),"会议不存在"); + } + if (!MeetingConstants.TYPE_OFFLINE.equals(meeting.getMeetingType())) { + throw new RuntimeException("会议不是离线会议"); + } + meeting.setStatus(MeetingStatusEnum.FAILED.getCode()); + meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED); + markAudioSaveFailure(meeting, failureMessage); + meetingService.updateById(meeting); + } + private void applyRealtimeAudioFinalizeResult(Meeting meeting, RealtimeMeetingAudioStorageService.FinalizeResult result) { if (result == null) { markAudioSaveFailure(meeting, RealtimeMeetingAudioStorageService.DEFAULT_FAILURE_MESSAGE); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingUnifiedStatusServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingUnifiedStatusServiceImpl.java index 4a64add..f175662 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingUnifiedStatusServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingUnifiedStatusServiceImpl.java @@ -121,6 +121,9 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ if (meeting == null || !MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.FAILED)) { return null; } + if (isAndroidOfflineEmptyUploadFailure(meeting)) { + return UnifiedMeetingStatusStage.FAILED_TRANSCRIBING; + } AiTask asrTask = findLatestTask(meeting.getId(), "ASR"); if (isTaskFailed(asrTask)) { return UnifiedMeetingStatusStage.FAILED_TRANSCRIBING; @@ -140,6 +143,13 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ return UnifiedMeetingStatusStage.FAILED_INITIALIZING; } + private boolean isAndroidOfflineEmptyUploadFailure(MeetingVO meeting) { + return meeting != null + && MeetingConstants.TYPE_OFFLINE.equalsIgnoreCase(meeting.getMeetingType()) + && MeetingConstants.SOURCE_ANDROID.equalsIgnoreCase(meeting.getMeetingSource()) + && "FAILED".equalsIgnoreCase(meeting.getAudioSaveStatus()); + } + private boolean isAndroidOfflineMeetingWaitingUpload(MeetingVO meeting) { return meeting != null && MeetingConstants.TYPE_OFFLINE.equalsIgnoreCase(meeting.getMeetingType()) @@ -162,6 +172,9 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ } private Integer resolvePercent(MeetingProgressSnapshot snapshot, UnifiedMeetingStatusStage stage) { + if (stage != null && stage.getCode().startsWith("FAILED_")) { + return -1; + } if (snapshot != null && snapshot.getPercent() != null) { return snapshot.getPercent(); } @@ -203,6 +216,9 @@ public class MeetingUnifiedStatusServiceImpl implements MeetingUnifiedStatusServ if (meeting == null) { return "处理失败"; } + if (meeting.getAudioSaveMessage() != null && !meeting.getAudioSaveMessage().isBlank()) { + return meeting.getAudioSaveMessage(); + } if (meeting.getLatestSummaryAttemptErrorMsg() != null && !meeting.getLatestSummaryAttemptErrorMsg().isBlank()) { return meeting.getLatestSummaryAttemptErrorMsg(); }