import 'dart:async'; import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import '../core/constants/storage_keys.dart'; import '../data/models/upload_task_model.dart'; import 'storage_service.dart'; import 'api_service.dart'; import '../core/utils/app_logger.dart'; /// 上传服务 - 单例模式 class UploadService extends ChangeNotifier { UploadService._internal() : super(); factory UploadService() => instance; static UploadService? _instance; static UploadService get instance { _instance ??= UploadService._internal(); return _instance!; } final Map _tasks = {}; /// 上传完成回调:参数为 (目标路径, 文件名) void Function(String targetPath, String fileName)? onUploadCompleted; final Map _cancelTokens = {}; final Map> _progressControllers = {}; final Map _speedTrackers = {}; /// 添加任务 void addTask(UploadTaskModel task) { _tasks[task.id] = task; if (!_progressControllers.containsKey(task.id)) { _progressControllers[task.id] = StreamController.broadcast(); } AppLogger.d('UploadTaskModel -> addTask > ${task.toJson()}'); _saveTasks(); notifyListeners(); } /// 更新任务 void updateTask(UploadTaskModel task) { if (_tasks.containsKey(task.id)) { _tasks[task.id] = task; _saveTasks(); notifyListeners(); } } /// 获取任务 UploadTaskModel? getTask(String id) => _tasks[id]; /// 获取所有任务 List get allTasks => _tasks.values.toList(); /// 获取进行中的任务 List get activeTasks => _tasks.values .where( (t) => t.status == UploadStatus.uploading || t.status == UploadStatus.waiting, ) .toList(); /// 移除任务 void removeTask(String id) { _tasks.remove(id); _cancelTokens.remove(id); final controller = _progressControllers.remove(id); controller?.close(); _saveTasks(); notifyListeners(); } /// 获取上传进度流 Stream getProgressStream(String taskId) { if (!_progressControllers.containsKey(taskId)) { _progressControllers[taskId] = StreamController.broadcast(); } return _progressControllers[taskId]!.stream; } /// 清除所有任务 void clearAllTasks() { for (final controller in _progressControllers.values) { controller.close(); } _tasks.clear(); _cancelTokens.clear(); _progressControllers.clear(); _saveTasks(); notifyListeners(); } /// 清除已完成的任务 void clearCompletedTasks() { final completedIds = _tasks.values .where( (t) => t.status == UploadStatus.completed || t.status == UploadStatus.cancelled, ) .map((t) => t.id) .toList(); for (final id in completedIds) { removeTask(id); } _saveTasks(); } /// 清除失败的任务 void clearFailedTasks() { final failedIds = _tasks.values .where((t) => t.status == UploadStatus.failed) .map((t) => t.id) .toList(); for (final id in failedIds) { removeTask(id); } _saveTasks(); } /// 初始化上传服务 Future initialize() async { await _loadTasks(); } /// 从本地存储加载上传任务 Future _loadTasks() async { try { final tasksJson = await StorageService.instance.getString( StorageKeys.uploadTasks, ); if (tasksJson == null || tasksJson.isEmpty) { AppLogger.d('没有保存的上传任务'); return; } final tasksList = jsonDecode(tasksJson) as List; final loadedTasks = []; final now = DateTime.now(); for (final taskJson in tasksList) { try { final task = UploadTaskModel.fromJson( taskJson as Map, ); // 检查文件是否存在 if (!await task.file.exists()) { AppLogger.d('上传任务文件不存在,跳过: ${task.fileName}'); continue; } // 过滤掉已取消的任务 if (task.status == UploadStatus.cancelled) { continue; } // 如果任务已完成,只保留配置天数内的记录 if (task.status == UploadStatus.completed) { if (task.completedAt == null) { continue; } final retentionDays = await StorageService.instance .getInt(StorageKeys.taskRetentionDays) ?? 7; if (retentionDays > 0) { final daysSinceCompletion = now .difference(task.completedAt!) .inDays; if (daysSinceCompletion > retentionDays) { AppLogger.d('跳过超过$retentionDays天的已完成任务: ${task.fileName}'); continue; } } } // 对于未完成的任务,重置状态为等待(因为应用关闭后上传已停止) if (task.status == UploadStatus.uploading || task.status == UploadStatus.waiting) { loadedTasks.add( task.copyWith( status: UploadStatus.waiting, uploadedBytes: 0, progress: 0, uploadedChunks: 0, errorMessage: null, ), ); } else { loadedTasks.add(task); } } catch (e) { AppLogger.d('解析上传任务失败: $e'); } } // 将加载的任务添加到当前任务列表 for (final task in loadedTasks) { _tasks[task.id] = task; if (!_progressControllers.containsKey(task.id)) { _progressControllers[task.id] = StreamController.broadcast(); } } AppLogger.d('从存储加载了 ${loadedTasks.length} 个上传任务'); // 通知 UI 更新 if (loadedTasks.isNotEmpty) { notifyListeners(); } } catch (e) { AppLogger.d('加载上传任务失败: $e'); } } /// 保存上传任务到本地存储 Future _saveTasks() async { try { final tasksList = _tasks.values.map((task) => task.toJson()).toList(); final tasksJson = jsonEncode(tasksList); await StorageService.instance.setString( StorageKeys.uploadTasks, tasksJson, ); AppLogger.d('已保存 ${_tasks.length} 个上传任务到存储'); } catch (e) { AppLogger.d('保存上传任务失败: $e'); } } /// 取消上传 void cancelUpload(String taskId) { final cancelToken = _cancelTokens[taskId]; if (cancelToken != null) { cancelToken.cancel('上传已取消'); } final task = _tasks[taskId]; if (task != null) { updateTask(task.copyWith(status: UploadStatus.cancelled, speed: 0)); _cleanSpeedTracker(taskId); } } /// 重试上传 Future retryUpload(String taskId) async { final task = _tasks[taskId]; if (task == null) return; // 重置任务状态 updateTask( task.copyWith( status: UploadStatus.waiting, uploadedBytes: 0, progress: 0, uploadedChunks: 0, errorMessage: null, ), ); // 开始上传 await startUpload(task); } /// 开始上传 Future startUpload(UploadTaskModel task) async { AppLogger.d('UploadService.startUpload: 开始上传任务 ${task.fileName}'); final cancelToken = CancelToken(); _cancelTokens[task.id] = cancelToken; try { // 步骤1:创建上传会话 AppLogger.d('UploadService.startUpload: 创建上传会话...'); final session = await _createUploadSession(task); AppLogger.d( 'UploadService.startUpload: 上传会话创建成功,sessionId=${session.sessionId}, chunkSize=${session.chunkSize}', ); // 更新任务,添加会话信息 final updatedTask = task.copyWith( session: session, totalChunks: task.calculateTotalChunks(session.chunkSize), status: UploadStatus.uploading, ); updateTask(updatedTask); // 步骤2:上传文件 await _uploadFile(updatedTask, cancelToken); // 上传完成 final completedTask = updatedTask.copyWith( status: UploadStatus.completed, progress: 1.0, uploadedBytes: task.fileSize, uploadedChunks: updatedTask.totalChunks, completedAt: DateTime.now(), speed: 0, ); updateTask(completedTask); _cleanSpeedTracker(task.id); onUploadCompleted?.call(task.targetPath, task.fileName); _emitProgress(task.id, 1.0); } catch (e) { AppLogger.d('Upload failed for ${task.fileName}: $e'); final isCancelled = e is DioException && e.type == DioExceptionType.cancel; updateTask( task.copyWith( status: isCancelled ? UploadStatus.cancelled : UploadStatus.failed, errorMessage: e.toString(), speed: 0, ), ); _cleanSpeedTracker(task.id); if (!isCancelled) { _emitProgress(task.id, task.progress); } } } /// 创建上传会话 Future _createUploadSession(UploadTaskModel task) async { final response = await ApiService.instance.put>( '/file/upload', data: { 'uri': task.targetPath.endsWith('/') ? '${task.targetPath}${task.fileName}' : '${task.targetPath}/${task.fileName}', 'size': task.fileSize, }, ); // API: /file/upload 响应格式: {code: 0, data: {...}, msg: ''} 但在 _parseResponse 已经解析过 data, 这里直接使用 response final Map sessionData = response; return UploadSessionModel.fromJson(sessionData); } /// 上传文件(支持分片上传) Future _uploadFile( UploadTaskModel task, CancelToken cancelToken, ) async { final session = task.session!; final file = task.file; AppLogger.d('开始上传 -> ${task.fileName}'); // 读取文件 final fileBytes = await file.readAsBytes(); // 检查是否需要分片:服务器返回了分片大小 或 文件大于20MB const largeFileThreshold = 20 * 1024 * 1024; // 20MB final shouldMultipart = session.isMultipartEnabled || task.fileSize > largeFileThreshold; if (shouldMultipart) { // 分片上传 final chunkSize = session.isMultipartEnabled ? session.chunkSize : largeFileThreshold; await _uploadMultipart(fileBytes, chunkSize, task, cancelToken); } else { // 单次上传 await _uploadSinglePart(fileBytes, session, task, cancelToken); } } /// 分片上传 Future _uploadMultipart( List fileBytes, int chunkSize, UploadTaskModel task, CancelToken cancelToken, ) async { final session = task.session!; final totalChunks = (fileBytes.length / chunkSize).ceil(); for (int i = 0; i < totalChunks; i++) { if (cancelToken.isCancelled) { throw DioException( requestOptions: RequestOptions(path: ''), type: DioExceptionType.cancel, error: '上传已取消', ); } // 计算当前分片的范围 final start = i * chunkSize; final end = (start + chunkSize).clamp(0, fileBytes.length); final chunkData = fileBytes.sublist(start, end); AppLogger.d('Uploading chunk ${i + 1}/$totalChunks for ${task.fileName}'); if (session.isRelayUpload) { // 上传到 Cloudreve 服务器 await _uploadChunkToRelay(chunkData, i, session.sessionId, cancelToken, null); } else { // 上传到远程存储 await _uploadChunkToRemote(chunkData, i, session, cancelToken, null); } // 更新进度 - 获取最新任务状态并计算正确进度 final currentTask = getTask(task.id) ?? task; final progress = (i + 1) / totalChunks; final speed = _computeSpeed(task.id, end); updateTask( currentTask.copyWith( uploadedBytes: end, progress: progress, uploadedChunks: i + 1, speed: speed, ), ); _emitProgress(task.id, progress); } // 完成上传(某些存储策略需要) if (session.completeUrl != null && session.completeUrl!.isNotEmpty) { await _completeMultipartUpload(session); } } /// 上传分片到中继服务器 Future _uploadChunkToRelay( List chunkData, int index, String sessionId, CancelToken cancelToken, void Function(int, int)? onProgress, ) async { await ApiService.instance.postWithProgress( '/file/upload/$sessionId/$index', data: Stream.fromIterable([chunkData]), headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': chunkData.length.toString(), }, onSendProgress: onProgress, ); } /// 上传分片到远程存储 Future _uploadChunkToRemote( List chunkData, int index, UploadSessionModel session, CancelToken cancelToken, void Function(int, int)? onProgress, ) async { final urls = session.uploadUrls ?? []; if (urls.isEmpty) { throw Exception('没有可用的上传 URL'); } // 大多数远程存储使用一个 URL,通过 query 参数指定分片索引 final url = urls.first; final uploadUrl = url.contains('?') ? '$url&chunk=$index' : '$url?chunk=$index'; final dio = Dio(BaseOptions()); await dio.post( uploadUrl, data: Stream.fromIterable([chunkData]), options: Options( contentType: 'application/octet-stream', headers: { 'Content-Length': chunkData.length.toString(), if (session.credential != null) 'Authorization': session.credential, }, ), cancelToken: cancelToken, onSendProgress: onProgress, ); } /// 完成多部分上传 Future _completeMultipartUpload(UploadSessionModel session) async { await ApiService.instance.post(session.completeUrl!, data: {}); } /// 单次上传 Future _uploadSinglePart( List fileBytes, UploadSessionModel session, UploadTaskModel task, CancelToken cancelToken, ) async { if (session.isRelayUpload) { // 上传到中继服务器 await _uploadChunkToRelay( fileBytes, 0, session.sessionId, cancelToken, (sent, total) { final progress = sent / total; final currentTask = getTask(task.id) ?? task; final speed = _computeSpeed(task.id, sent); updateTask( currentTask.copyWith( uploadedBytes: sent, progress: progress, speed: speed, ), ); _emitProgress(task.id, progress); }, ); } else { // 上传到远程存储 await _uploadChunkToRemote( fileBytes, 0, session, cancelToken, (sent, total) { final progress = sent / total; final currentTask = getTask(task.id) ?? task; final speed = _computeSpeed(task.id, sent); updateTask( currentTask.copyWith( uploadedBytes: sent, progress: progress, speed: speed, ), ); _emitProgress(task.id, progress); }, ); } } /// 发送进度更新 void _emitProgress(String taskId, double progress) { final controller = _progressControllers[taskId]; if (controller != null && !controller.isClosed) { controller.add(progress); } } /// 计算上传速度 int _computeSpeed(String taskId, int uploadedBytes) { final tracker = _speedTrackers[taskId]; if (tracker == null) { _speedTrackers[taskId] = _SpeedTracker(uploadedBytes); return 0; } return tracker.update(uploadedBytes); } /// 清理速度追踪器 void _cleanSpeedTracker(String taskId) { _speedTrackers.remove(taskId); } } /// 上传速度追踪器 class _SpeedTracker { int lastBytes; DateTime lastTime; _SpeedTracker(this.lastBytes) : lastTime = DateTime.now(); int update(int currentBytes) { final now = DateTime.now(); final elapsed = now.difference(lastTime).inMilliseconds; if (elapsed < 200) return 0; // 至少 200ms 间隔才计算 final bytesDelta = currentBytes - lastBytes; final speed = (bytesDelta * 1000 / elapsed).round(); lastBytes = currentBytes; lastTime = now; return speed > 0 ? speed : 0; } }