diff --git a/lib/app/bindings/initial_binding.dart b/lib/app/bindings/initial_binding.dart index e8e15f8..b66e1a3 100644 --- a/lib/app/bindings/initial_binding.dart +++ b/lib/app/bindings/initial_binding.dart @@ -5,6 +5,8 @@ import 'package:problem_check_system/data/providers/http_provider.dart'; import 'package:problem_check_system/data/providers/sqlite_provider.dart'; import 'package:problem_check_system/data/repositories/auth_repository.dart'; import 'package:problem_check_system/data/repositories/file_repository.dart'; +import 'package:problem_check_system/data/repositories/image_repository.dart'; +import 'package:problem_check_system/data/repositories/image_repository_impl.dart'; import 'package:problem_check_system/data/repositories/problem_repository.dart'; class InitialBinding implements Bindings { @@ -24,6 +26,9 @@ class InitialBinding implements Bindings { void _registerRepositories() { Get.lazyPut(() => FileRepository()); + Get.lazyPut( + () => ImageRepositoryImpl(httpProvider: Get.find()), + ); /// 懒加载注册所有的仓库 Get.lazyPut( diff --git a/lib/data/models/image_metadata_model.dart b/lib/data/models/image_metadata_model.dart index f8e8266..813be33 100644 --- a/lib/data/models/image_metadata_model.dart +++ b/lib/data/models/image_metadata_model.dart @@ -29,4 +29,19 @@ class ImageMetadata { status: ImageStatus.values[map['status'] as int], ); } + + /// Creates a new [ImageMetadata] instance with optional new values. + /// + /// The original object remains unchanged. + ImageMetadata copyWith({ + String? localPath, + String? remoteUrl, + ImageStatus? status, + }) { + return ImageMetadata( + localPath: localPath ?? this.localPath, + remoteUrl: remoteUrl ?? this.remoteUrl, + status: status ?? this.status, + ); + } } diff --git a/lib/data/models/image_status.dart b/lib/data/models/image_status.dart index 6a47d71..e037b95 100644 --- a/lib/data/models/image_status.dart +++ b/lib/data/models/image_status.dart @@ -1,11 +1,14 @@ /// 图片的同步状态 enum ImageStatus { - /// 新增的本地图片,需要上传 - local, - - /// 已上传,本地无修改 + /// 已同步 synced, - /// 已同步,但已在本地删除,需要通知服务器 - deleted, + // 待上传 + pendingUpload, + + /// 待删除 + pendingDeleted, + + /// 待下载 + pendingDownload, } diff --git a/lib/data/models/problem_model.dart b/lib/data/models/problem_model.dart index 122dc48..d08a825 100644 --- a/lib/data/models/problem_model.dart +++ b/lib/data/models/problem_model.dart @@ -7,7 +7,7 @@ import 'package:problem_check_system/data/models/problem_sync_status.dart'; /// 用于表示系统中的一个具体问题,包含了问题的描述、位置、图片等信息。 class Problem { /// 问题的唯一标识符,可空。 - final String? id; + final String id; /// 问题的详细描述。 final String description; @@ -37,7 +37,7 @@ class Problem { final bool isChecked; Problem({ - this.id, + required this.id, required this.description, required this.location, required this.imageUrls, diff --git a/lib/data/models/server_problem.dart b/lib/data/models/server_problem.dart new file mode 100644 index 0000000..2e1979e --- /dev/null +++ b/lib/data/models/server_problem.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +@immutable +class ServerProblem { + final String id; + final String title; + final String location; + final String? censorTaskId; + final String? rowId; + final String? bindData; + final List? imageUrls; + final DateTime creationTime; + final String creatorId; + final DateTime lastModificationTime; + final String lastModifierId; + + const ServerProblem({ + required this.id, + required this.title, + required this.location, + this.censorTaskId, + this.rowId, + this.bindData, + this.imageUrls, + required this.creationTime, + required this.creatorId, + required this.lastModificationTime, + required this.lastModifierId, + }); + + factory ServerProblem.fromJson(Map json) => ServerProblem( + id: json['id'] as String, + title: json['title'] as String, + location: json['location'] as String, + censorTaskId: json['censorTaskId'] as String?, + rowId: json['rowId'] as String?, + bindData: json['bindData'] as String?, + imageUrls: json['imageUrls'] as List?, + creationTime: DateTime.parse(json['creationTime'] as String), + creatorId: json['creatorId'] as String, + lastModificationTime: DateTime.parse( + json['lastModificationTime'] as String, + ), + lastModifierId: json['lastModifierId'] as String, + ); + + Map toJson() => { + 'id': id, + 'title': title, + 'location': location, + 'censorTaskId': censorTaskId, + 'rowId': rowId, + 'bindData': bindData, + 'imageUrls': imageUrls, + 'creationTime': creationTime.toUtc().toIso8601String(), + 'creatorId': creatorId, + 'lastModificationTime': lastModificationTime.toUtc().toIso8601String(), + 'lastModifierId': lastModifierId, + }; + + ServerProblem copyWith({ + String? id, + String? title, + String? location, + String? censorTaskId, + String? rowId, + String? bindData, + List? imageUrls, + DateTime? creationTime, + String? creatorId, + DateTime? lastModificationTime, + String? lastModifierId, + }) { + return ServerProblem( + id: id ?? this.id, + title: title ?? this.title, + location: location ?? this.location, + censorTaskId: censorTaskId ?? this.censorTaskId, + rowId: rowId ?? this.rowId, + bindData: bindData ?? this.bindData, + imageUrls: imageUrls ?? this.imageUrls, + creationTime: creationTime ?? this.creationTime, + creatorId: creatorId ?? this.creatorId, + lastModificationTime: lastModificationTime ?? this.lastModificationTime, + lastModifierId: lastModifierId ?? this.lastModifierId, + ); + } +} diff --git a/lib/data/providers/http_provider.dart b/lib/data/providers/http_provider.dart index 3a3e3bd..f8d0771 100644 --- a/lib/data/providers/http_provider.dart +++ b/lib/data/providers/http_provider.dart @@ -232,4 +232,27 @@ class HttpProvider extends GetxService { cancelToken: cancelToken, ); } + + /// 下载文件 + Future download( + String urlPath, + String savePath, { + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, + bool deleteOnError = true, + int? lengthHeader, + Object? data, + Options? requestOptions, + }) async { + return await _dio.download( + urlPath, + savePath, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ); + } } diff --git a/lib/data/repositories/file_repository.dart b/lib/data/repositories/file_repository.dart index d17813e..58abb29 100644 --- a/lib/data/repositories/file_repository.dart +++ b/lib/data/repositories/file_repository.dart @@ -1,9 +1,14 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; // 引入 kDebugMode 和 debugPrint import 'package:get/get.dart' hide FormData, MultipartFile; import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'package:problem_check_system/core/extensions/http_response_extension.dart'; import 'package:problem_check_system/core/utils/constants/api_endpoints.dart'; +import 'package:problem_check_system/data/models/image_metadata_model.dart'; +import 'package:problem_check_system/data/models/image_status.dart'; import 'package:problem_check_system/data/providers/http_provider.dart'; class FileRepository { @@ -61,4 +66,102 @@ class FileRepository { throw Exception('图片上传发生未知错误: $e'); } } + + // 新增的下载方法 + Future downloadImage( + String imageUrl, { + CancelToken? cancelToken, + void Function(int received, int total)? onReceiveProgress, + }) async { + final directory = await getApplicationDocumentsDirectory(); + final imagesDir = Directory('${directory.path}/problem_images'); + + // 确保目录存在 + if (!await imagesDir.exists()) { + await imagesDir.create(recursive: true); + } + + // 生成唯一的文件名 + final String fileName = + 'downloaded_${DateTime.now().millisecondsSinceEpoch}_${imageUrl.hashCode}${_getFileExtension(imageUrl)}'; + final String imagePath = '${imagesDir.path}/$fileName'; + + try { + // 下载图片 + await _httpProvider.download( + imageUrl, + imagePath, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ); + + // 返回图片元数据 + return ImageMetadata( + localPath: imagePath, + remoteUrl: imageUrl, + status: ImageStatus.synced, + ); + } catch (e) { + // 清理可能创建的不完整文件 + final file = File(imagePath); + if (await file.exists()) { + await file.delete(); + } + rethrow; + } + } + + // 批量下载方法 + Future> downloadImages( + List imageUrls, { + CancelToken? cancelToken, + void Function(int current, int total)? onProgress, + }) async { + final List results = []; + int downloadedCount = 0; + + for (final imageUrl in imageUrls) { + if (cancelToken?.isCancelled == true) { + break; + } + + try { + final metadata = await downloadImage( + imageUrl, + cancelToken: cancelToken, + onReceiveProgress: (received, total) { + // 单个文件的进度可以在这里处理 + }, + ); + results.add(metadata); + + // 更新总体进度 + downloadedCount++; + onProgress?.call(downloadedCount, imageUrls.length); + } catch (e) { + Get.log('Failed to download image $imageUrl: $e'); + // 可以选择继续下载其他图片或抛出异常 + } + } + + return results; + } + + // 辅助方法:获取文件扩展名 + String _getFileExtension(String url) { + try { + final uri = Uri.parse(url); + final pathSegments = uri.pathSegments; + if (pathSegments.isNotEmpty) { + final fileName = pathSegments.last; + final dotIndex = fileName.lastIndexOf('.'); + if (dotIndex != -1 && dotIndex < fileName.length - 1) { + return fileName.substring(dotIndex); + } + } + return '.jpg'; + } catch (e) { + return '.jpg'; + } + } } diff --git a/lib/data/repositories/image_repository.dart b/lib/data/repositories/image_repository.dart new file mode 100644 index 0000000..d3d89e0 --- /dev/null +++ b/lib/data/repositories/image_repository.dart @@ -0,0 +1,8 @@ +// image_repository.dart +abstract class ImageRepository { + Future downloadImage(String imageUrl, String problemId); + Future isImageDownloaded(String imageUrl, String problemId); + Future getLocalImagePath(String imageUrl, String problemId); + Future deleteProblemImages(String problemId); + Future cleanupCache({Duration maxAge = const Duration(days: 30)}); +} diff --git a/lib/data/repositories/image_repository_impl.dart b/lib/data/repositories/image_repository_impl.dart new file mode 100644 index 0000000..6964379 --- /dev/null +++ b/lib/data/repositories/image_repository_impl.dart @@ -0,0 +1,202 @@ +// image_repository_impl.dart +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:get/get.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; +import 'package:problem_check_system/data/providers/http_provider.dart'; +import 'package:problem_check_system/data/repositories/image_repository.dart'; + +class ImageRepositoryImpl implements ImageRepository { + final HttpProvider httpProvider; + + ImageRepositoryImpl({required this.httpProvider}); + + @override + Future downloadImage(String imageUrl, String problemId) async { + try { + // 1. 获取应用文档目录 + final Directory appDocDir = await getApplicationDocumentsDirectory(); + final String problemDirPath = path.join( + appDocDir.path, + 'problems', + problemId, + 'images', + ); + final Directory problemDir = Directory(problemDirPath); + + // 2. 创建目录(如果不存在) + if (!await problemDir.exists()) { + await problemDir.create(recursive: true); + } + + // 3. 生成文件名 + final String fileExtension = _getFileExtensionFromUrl(imageUrl); + final String fileName = + '${_generateFileNameHash(imageUrl)}.$fileExtension'; + final String filePath = path.join(problemDir.path, fileName); + + // 4. 使用 Dio 下载文件 + final response = await httpProvider.download( + imageUrl, + filePath, + options: Options( + responseType: ResponseType.bytes, + followRedirects: true, + receiveTimeout: const Duration(seconds: 30), + ), + onReceiveProgress: (received, total) { + if (total != -1) { + Get.log('下载进度: ${(received / total * 100).toStringAsFixed(1)}%'); + } + }, + ); + + if (response.statusCode == 200) { + // 验证文件是否成功写入 + final File file = File(filePath); + if (await file.exists()) { + return filePath; + } else { + throw Exception('文件写入失败'); + } + } else { + throw Exception('下载失败: HTTP ${response.statusCode}'); + } + } on DioException catch (e) { + throw Exception('图片下载失败: ${e.message}'); + } catch (e) { + throw Exception('图片下载失败: $e'); + } + } + + @override + Future isImageDownloaded(String imageUrl, String problemId) async { + try { + final String fileExtension = _getFileExtensionFromUrl(imageUrl); + final String fileName = + '${_generateFileNameHash(imageUrl)}.$fileExtension'; + + final Directory appDocDir = await getApplicationDocumentsDirectory(); + final String filePath = path.join( + appDocDir.path, + 'problems', + problemId, + 'images', + fileName, + ); + + return await File(filePath).exists(); + } catch (e) { + return false; + } + } + + @override + Future getLocalImagePath(String imageUrl, String problemId) async { + try { + final String fileExtension = _getFileExtensionFromUrl(imageUrl); + final String fileName = + '${_generateFileNameHash(imageUrl)}.$fileExtension'; + + final Directory appDocDir = await getApplicationDocumentsDirectory(); + final String filePath = path.join( + appDocDir.path, + 'problems', + problemId, + 'images', + fileName, + ); + + if (await File(filePath).exists()) { + return filePath; + } + return null; + } catch (e) { + return null; + } + } + + @override + Future deleteProblemImages(String problemId) async { + try { + final Directory appDocDir = await getApplicationDocumentsDirectory(); + final String problemDirPath = path.join( + appDocDir.path, + 'problems', + problemId, + ); + final Directory problemDir = Directory(problemDirPath); + + if (await problemDir.exists()) { + await problemDir.delete(recursive: true); + } + } catch (e) { + Get.log('删除图片失败: $e'); + } + } + + @override + Future cleanupCache({ + Duration maxAge = const Duration(days: 30), + }) async { + try { + final Directory appDocDir = await getApplicationDocumentsDirectory(); + final String problemsDirPath = path.join(appDocDir.path, 'problems'); + final Directory problemsDir = Directory(problemsDirPath); + + if (await problemsDir.exists()) { + final DateTime cutoffTime = DateTime.now().subtract(maxAge); + + final List entities = await problemsDir + .list() + .toList(); + + for (final entity in entities) { + if (entity is Directory) { + try { + final FileStat stat = await entity.stat(); + if (stat.modified.isBefore(cutoffTime)) { + await entity.delete(recursive: true); + Get.log('已清理过期目录: ${entity.path}'); + } + } catch (e) { + Get.log('无法获取目录状态: ${entity.path}, 错误: $e'); + // 如果无法获取状态,也尝试删除(可能是损坏的目录) + try { + await entity.delete(recursive: true); + Get.log('强制清理目录: ${entity.path}'); + } catch (deleteError) { + Get.log('删除目录失败: ${entity.path}, 错误: $deleteError'); + } + } + } + } + } + } catch (e) { + Get.log('清理缓存失败: $e'); + } + } + + // 辅助方法 + String _getFileExtensionFromUrl(String url) { + try { + final Uri uri = Uri.parse(url); + final String path = uri.path; + + if (path.endsWith('.jpg') || path.endsWith('.jpeg')) return 'jpg'; + if (path.endsWith('.png')) return 'png'; + if (path.endsWith('.gif')) return 'gif'; + if (path.endsWith('.webp')) return 'webp'; + if (path.endsWith('.bmp')) return 'bmp'; + + return 'jpg'; + } catch (e) { + return 'jpg'; + } + } + + String _generateFileNameHash(String url) { + return url.hashCode.abs().toString(); + } +} diff --git a/lib/data/repositories/problem_repository.dart b/lib/data/repositories/problem_repository.dart index 44d87ec..38c96a8 100644 --- a/lib/data/repositories/problem_repository.dart +++ b/lib/data/repositories/problem_repository.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart' hide MultipartFile, FormData, Response; import 'package:problem_check_system/core/extensions/http_response_extension.dart'; import 'package:problem_check_system/core/utils/constants/api_endpoints.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; +import 'package:problem_check_system/data/models/server_problem.dart'; import 'package:problem_check_system/data/providers/connectivity_provider.dart'; import 'package:problem_check_system/data/providers/http_provider.dart'; import 'package:problem_check_system/data/providers/sqlite_provider.dart'; @@ -54,7 +55,7 @@ class ProblemRepository extends GetxService { } // 在ProblemRepository中添加 - Future> fetchProblemsFromServer({ + Future> fetchProblemsFromServer({ DateTime? startTime, DateTime? endTime, int? pageNumber, @@ -77,7 +78,7 @@ class ProblemRepository extends GetxService { if (response.isSuccess) { // 假设服务器返回的是Problem对象的列表 final List data = response.data; - return data.map((json) => Problem.fromJson(json)).toList(); + return data.map((json) => ServerProblem.fromJson(json)).toList(); } else { throw Exception('拉取问题失败: ${response.statusCode}'); } @@ -88,7 +89,7 @@ class ProblemRepository extends GetxService { /// post Future post( - Map apiPayload, + Map apiPayload, CancelToken cancelToken, ) async { // 3. 发送给服务器 @@ -103,7 +104,7 @@ class ProblemRepository extends GetxService { /// put Future put( String id, - Map apiPayload, + Map apiPayload, CancelToken cancelToken, ) async { // 3. 发送给服务器 diff --git a/lib/modules/problem/controllers/problem_controller.dart b/lib/modules/problem/controllers/problem_controller.dart index 8ec5927..dabcdb7 100644 --- a/lib/modules/problem/controllers/problem_controller.dart +++ b/lib/modules/problem/controllers/problem_controller.dart @@ -11,7 +11,9 @@ import 'package:problem_check_system/core/extensions/http_response_extension.dar import 'package:problem_check_system/data/models/image_metadata_model.dart'; import 'package:problem_check_system/data/models/image_status.dart'; import 'package:problem_check_system/data/models/problem_sync_status.dart'; +import 'package:problem_check_system/data/models/server_problem.dart'; import 'package:problem_check_system/data/repositories/file_repository.dart'; +import 'package:problem_check_system/data/repositories/image_repository.dart'; import 'package:problem_check_system/data/repositories/problem_repository.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/modules/problem/views/widgets/models/date_range_enum.dart'; @@ -272,7 +274,7 @@ class ProblemController extends GetxController ); if (updatedProblem.syncStatus == ProblemSyncStatus.untracked) { - problemRepository.deleteProblem(updatedProblem.id!); + problemRepository.deleteProblem(updatedProblem.id); } else { problemRepository.updateProblem(updatedProblem); } @@ -301,7 +303,7 @@ class ProblemController extends GetxController final List remoteUrls = []; if (problem.syncStatus != ProblemSyncStatus.pendingDelete) { final newImages = problem.imageUrls - .where((img) => img.status == ImageStatus.local) + .where((img) => img.status == ImageStatus.pendingUpload) .toList(); final totalFilesToUpload = newImages.length; @@ -354,13 +356,13 @@ class ProblemController extends GetxController break; case ProblemSyncStatus.pendingUpdate: response = await problemRepository.put( - problem.id!, + problem.id, apiPayload!, cancelToken, ); break; case ProblemSyncStatus.pendingDelete: - response = await problemRepository.delete(problem.id!, cancelToken); + response = await problemRepository.delete(problem.id, cancelToken); break; } @@ -400,7 +402,7 @@ class ProblemController extends GetxController for (var image in images) { if (image.status == ImageStatus.synced) { finalRemoteUrls.add(image.remoteUrl!); - } else if (image.status == ImageStatus.local) { + } else if (image.status == ImageStatus.pendingUpload) { finalRemoteUrls.add(newRemoteUrls[newImageIndex]); newImageIndex++; } @@ -418,7 +420,7 @@ class ProblemController extends GetxController int uploadedUrlIndex = 0; for (var image in images) { - if (image.status == ImageStatus.local) { + if (image.status == ImageStatus.pendingUpload) { updatedImageMetadata.add( ImageMetadata( localPath: image.localPath, @@ -438,21 +440,27 @@ class ProblemController extends GetxController // #endregion // #region 问题同步 - + // TODO 同步服务器问题到本地 Future pullDataFromServer() async { isLoading.value = true; try { // 1. 从服务器获取最新数据 - final List serverProblems = await problemRepository + final List serverProblems = await problemRepository .fetchProblemsFromServer(); // 2. 获取本地数据 final List localProblems = await problemRepository.getProblems(); - // 3. 同步策略:以服务器数据为准,合并本地未上传的更改 - await _syncProblems(serverProblems, localProblems); + // 3. 同步策略:以服务器数据为准,保留本地未同步的更改 + final List downloadedProblems = await _syncProblems( + serverProblems, + localProblems, + ); + + // 4. 启动图片下载任务 + _downloadImagesForProblems(downloadedProblems); - // 4. 重新加载本地问题列表 + // 5. 重新加载本地问题列表 await loadProblems(); Get.snackbar('成功', '数据同步完成', snackPosition: SnackPosition.TOP); @@ -463,60 +471,152 @@ class ProblemController extends GetxController } } - /// 同步服务器和本地数据 - Future _syncProblems( - List serverProblems, + /// 异步下载问题的图片 + void _downloadImagesForProblems(List problems) { + if (problems.isEmpty) return; + + // 在后台执行图片下载 + Future(() async { + final imageRepository = Get.find(); // 使用GetX获取实例 + + for (final problem in problems) { + try { + final List downloadedImages = []; + + for (final imageMeta in problem.imageUrls) { + if (imageMeta.remoteUrl != null && + imageMeta.remoteUrl!.isNotEmpty) { + // 检查是否已下载 + final bool isDownloaded = await imageRepository.isImageDownloaded( + imageMeta.remoteUrl!, + problem.id, + ); + + String localPath; + if (isDownloaded) { + // 如果已下载,获取本地路径 + localPath = (await imageRepository.getLocalImagePath( + imageMeta.remoteUrl!, + problem.id, + ))!; + } else { + // 下载图片到本地 + localPath = await imageRepository.downloadImage( + imageMeta.remoteUrl!, + problem.id, + ); + } + + // 更新图片元数据 + final downloadedImage = imageMeta.copyWith( + localPath: localPath, + status: ImageStatus.synced, + ); + downloadedImages.add(downloadedImage); + } + } + + // 更新问题的图片数据 + if (downloadedImages.isNotEmpty) { + final updatedProblem = problem.copyWith( + imageUrls: downloadedImages, + ); + await problemRepository.updateProblem(updatedProblem); + } + } catch (e) { + Get.log('下载问题 ${problem.id} 的图片失败: $e'); + } + } + }); + } + + /// 同步服务器和本地数据,返回需要下载图片的问题列表 + Future> _syncProblems( + List serverProblems, List localProblems, ) async { + final List needDownloadImages = []; + // 创建映射以便快速查找 - final Map serverProblemsMap = { - for (var problem in serverProblems.where((p) => p.id != null)) - problem.id!: problem, + final Map serverProblemsMap = { + for (var problem in serverProblems) problem.id: problem, }; final Map localProblemsMap = { - for (var problem in localProblems.where((p) => p.id != null)) - problem.id!: problem, + for (var problem in localProblems) problem.id: problem, }; // 处理服务器有但本地没有的数据(新增) for (final serverProblem in serverProblems) { - if (serverProblem.id != null && - !localProblemsMap.containsKey(serverProblem.id)) { + if (!localProblemsMap.containsKey(serverProblem.id)) { // 服务器新增的问题,添加到本地 - await problemRepository.insertProblem(serverProblem); + final newProblem = _convertServerProblemToLocal(serverProblem); + await problemRepository.insertProblem(newProblem); + needDownloadImages.add(newProblem); } } // 处理本地有但服务器没有的数据(删除) for (final localProblem in localProblems) { - if (localProblem.id != null && - !serverProblemsMap.containsKey(localProblem.id)) { - // 服务器已删除的问题,从本地删除 - await problemRepository.deleteProblem(localProblem.id!); + if (!serverProblemsMap.containsKey(localProblem.id)) { + // 只有已同步的数据才从本地删除,未同步的数据保留 + if (localProblem.syncStatus == ProblemSyncStatus.synced) { + await problemRepository.deleteProblem(localProblem.id); + } + // 如果是未同步的数据(pendingCreate/pendingUpdate),保留在本地等待上传 } } // 处理双方都有的数据(更新) for (final serverProblem in serverProblems) { - if (serverProblem.id != null && - localProblemsMap.containsKey(serverProblem.id)) { + if (localProblemsMap.containsKey(serverProblem.id)) { final localProblem = localProblemsMap[serverProblem.id]!; // 只有当本地数据已同步时才更新(避免覆盖本地未上传的更改) if (localProblem.syncStatus == ProblemSyncStatus.synced) { // 比较更新时间,使用最新的数据 - final serverUpdated = serverProblem.lastModifiedTime; + final serverUpdated = serverProblem.lastModificationTime; final localUpdated = localProblem.lastModifiedTime; if (serverUpdated.isAfter(localUpdated)) { // 服务器数据更新,更新本地数据 - await problemRepository.updateProblem(serverProblem); + final updatedProblem = _convertServerProblemToLocal(serverProblem); + await problemRepository.updateProblem(updatedProblem); + needDownloadImages.add(updatedProblem); } } // 如果本地有未同步的更改,保留本地更改(下次上传时会同步到服务器) } } + + return needDownloadImages; + } + + /// 将服务器问题转换为本地问题模型 + Problem _convertServerProblemToLocal(ServerProblem serverProblem) { + // 转换图片URL为ImageMetadata列表(初始状态为待下载) + final List imageMetadatas = (serverProblem.imageUrls ?? []) + .map( + (url) => ImageMetadata( + remoteUrl: url, + localPath: '', // 初始为空,等待下载 + status: ImageStatus.pendingDownload, // 标记为待下载状态 + ), + ) + .toList(); + + return Problem( + id: serverProblem.id, + description: serverProblem.title, + location: serverProblem.location, + imageUrls: imageMetadatas, + creationTime: serverProblem.creationTime, + lastModifiedTime: serverProblem.lastModificationTime, + syncStatus: ProblemSyncStatus.synced, // 来自服务器的数据标记为已同步 + censorTaskId: serverProblem.censorTaskId, + bindData: serverProblem.bindData, + isChecked: false, // 默认未检查 + ); } // #endregion @@ -710,7 +810,7 @@ class ProblemController extends GetxController final deleteProblem = ProblemStateManager.markForDeletion(problem); if (deleteProblem.syncStatus == ProblemSyncStatus.untracked) { // 直接删除问题和图片 - await problemRepository.deleteProblem(problem.id!); + await problemRepository.deleteProblem(problem.id); await _deleteProblemImages(problem); } else { // 更新状态 diff --git a/lib/modules/problem/controllers/problem_form_controller.dart b/lib/modules/problem/controllers/problem_form_controller.dart index c790f84..ea7e44f 100644 --- a/lib/modules/problem/controllers/problem_form_controller.dart +++ b/lib/modules/problem/controllers/problem_form_controller.dart @@ -189,7 +189,7 @@ class ProblemFormController extends GetxController { final String imagePath = '${imagesDir.path}/$fileName'; final ImageMetadata imageData = ImageMetadata( localPath: imagePath, - status: ImageStatus.local, + status: ImageStatus.pendingUpload, ); final File imageFile = File(imagePath);