diff --git a/lib/data/services/meeting/meeting_upload_service.dart b/lib/data/services/meeting/meeting_upload_service.dart index af54033a2..2f8329cdb 100644 --- a/lib/data/services/meeting/meeting_upload_service.dart +++ b/lib/data/services/meeting/meeting_upload_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:async'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -21,6 +22,9 @@ class MeetingUploadService extends GetxService { bool _isUpload = false; int _handled = 0; //队列处理指针,表示当前循环语句处理的任务数量 + static const Duration _fileStableTimeout = Duration(seconds: 20); + static const Duration _fileStablePoll = Duration(milliseconds: 350); + static const int _fileStableNeededCount = 3; @override void onInit() { @@ -80,8 +84,26 @@ class MeetingUploadService extends GetxService { meetingId.value = element['id']; try { int totalProgress = 0; + final filePath = (element['filepath'] ?? '').toString(); + if (filePath.isEmpty) { + throw Exception('Empty upload file path'); + } + final fileSizeBytes = await _waitForStableFileSize( + filePath, + timeout: _fileStableTimeout, + pollInterval: _fileStablePoll, + stableNeededCount: _fileStableNeededCount, + ); + final isWav = filePath.toLowerCase().endsWith('.wav'); + if (isWav) { + final ok = await _isWavHeaderConsistent(filePath, fileSizeBytes); + if (!ok) { + throw Exception('WAV header is inconsistent with file size'); + } + } + element['fileSizeBytes'] = fileSizeBytes; final audiourl = await UploadOss.upload( - filepath: element['filepath'], + filepath: filePath, rootDir: element['rootdir'], onSendProgress: (int count, int total) { // uploadProgress.value = count / total; @@ -139,6 +161,87 @@ class MeetingUploadService extends GetxService { _isUpload = false; } + Future _waitForStableFileSize( + String filePath, { + Duration timeout = _fileStableTimeout, + Duration pollInterval = _fileStablePoll, + int stableNeededCount = _fileStableNeededCount, + }) async { + final file = File(filePath); + final deadline = DateTime.now().add(timeout); + int lastSize = -1; + int stableCount = 0; + while (DateTime.now().isBefore(deadline)) { + final exists = await file.exists(); + if (exists) { + int size = 0; + try { + size = await file.length(); + } catch (_) { + size = 0; + } + if (size > 0) { + if (size == lastSize) { + stableCount += 1; + } else { + stableCount = 0; + lastSize = size; + } + if (stableCount >= stableNeededCount) { + return size; + } + } + } + await Future.delayed(pollInterval); + } + throw TimeoutException('File not stable: $filePath', timeout); + } + + Future _isWavHeaderConsistent( + String filePath, int fileSizeBytes) async { + RandomAccessFile? raf; + try { + raf = await File(filePath).open(mode: FileMode.read); + final header = await raf.read(12); + if (header.length < 12) return false; + final riff = String.fromCharCodes(header.sublist(0, 4)); + final wave = String.fromCharCodes(header.sublist(8, 12)); + if (riff != 'RIFF' || wave != 'WAVE') return true; + + int offset = 12; + const int maxScanBytes = 64 * 1024; + while (offset + 8 <= fileSizeBytes && offset <= maxScanBytes) { + await raf.setPosition(offset); + final chunkHeader = await raf.read(8); + if (chunkHeader.length < 8) return false; + final chunkId = String.fromCharCodes(chunkHeader.sublist(0, 4)); + final chunkSize = _readUint32Le(chunkHeader, 4); + final chunkDataStart = offset + 8; + final expectedEnd = chunkDataStart + chunkSize; + if (chunkId == 'data') { + return fileSizeBytes >= expectedEnd; + } + final paddedSize = chunkSize + (chunkSize % 2); + offset = chunkDataStart + paddedSize; + } + return true; + } catch (_) { + return true; + } finally { + try { + await raf?.close(); + } catch (_) {} + } + } + + int _readUint32Le(List bytes, int offset) { + if (offset + 4 > bytes.length) return 0; + return (bytes[offset]) | + (bytes[offset + 1] << 8) | + (bytes[offset + 2] << 16) | + (bytes[offset + 3] << 24); + } + void removeUpload(int id, {bool isDeleteTask = false}) { Map uploadData = uploadList.firstWhere( (element) => element['id'] == id, diff --git a/lib/modules/meeting/controllers/meeting_home_controller.dart b/lib/modules/meeting/controllers/meeting_home_controller.dart index aa8806c5f..0d988cd88 100644 --- a/lib/modules/meeting/controllers/meeting_home_controller.dart +++ b/lib/modules/meeting/controllers/meeting_home_controller.dart @@ -260,8 +260,15 @@ class MeetingHomeController extends GetxController { final creationTime = fileStat.changed.millisecondsSinceEpoch; // 准备新路径 - final fileName = filePath.split('/').last; //获取文件名(包含扩展名) - final newPath = "${destDir.path}/$fileName"; //构建新路径,保持原文件名 + String fileName = filePath.split('/').last; //获取文件名(包含扩展名) + String newPath = "${destDir.path}/$fileName"; //构建新路径,保持原文件名 + if (await File(newPath).exists()) { + final ext = path.extension(fileName); + final base = path.basenameWithoutExtension(fileName); + final ts = DateTime.now().millisecondsSinceEpoch; + fileName = '${base}_$ts$ext'; + newPath = "${destDir.path}/$fileName"; + } // 构建要存入数据库的音频文件抽象对象 Map data = { @@ -286,6 +293,13 @@ class MeetingHomeController extends GetxController { ...data, }); if (meetingResult != null) { + // 移动文件:优先 rename,失败再 copy+delete,确保上传读取的是完整目标文件 + try { + await file.rename(newPath); + } catch (_) { + await file.copy(newPath); + await file.delete(); + } // 安排后台上传任务 meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio'); @@ -306,10 +320,6 @@ class MeetingHomeController extends GetxController { }); } - // 移动文件 - await file.copy(newPath); // 复制到新路径 - await file.delete(); // 删除原文件 - addUp++; } } catch (e) {