From bdfc6778f7b07af9c0afc089f73ca630b1000316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=8C=AF=E5=8D=87?= <359059686@qq.com> Date: Thu, 11 Sep 2025 11:55:16 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E6=B7=BB=E5=8A=A0=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E4=BA=BAid=E4=B8=8E=E4=BF=AE=E6=94=B9=E6=97=B6=E9=97=B4utc?= =?UTF-8?q?=E6=AF=AB=E7=A7=92=E6=AF=94=E5=AF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/bindings/initial_binding.dart | 3 + lib/data/models/problem_model.dart | 14 +- lib/data/models/problem_sync_status.dart | 36 +-- lib/data/providers/sqlite_provider.dart | 1 + lib/data/repositories/auth_repository.dart | 9 + lib/data/repositories/problem_repository.dart | 5 +- .../auth/controllers/login_controller.dart | 7 +- lib/modules/home/bindings/home_binding.dart | 7 +- .../bindings/problem_form_binding.dart | 4 + .../controllers/problem_controller.dart | 210 +++++++++++++----- .../controllers/problem_form_controller.dart | 6 +- 11 files changed, 224 insertions(+), 78 deletions(-) diff --git a/lib/app/bindings/initial_binding.dart b/lib/app/bindings/initial_binding.dart index b66e1a3..54dad48 100644 --- a/lib/app/bindings/initial_binding.dart +++ b/lib/app/bindings/initial_binding.dart @@ -8,6 +8,7 @@ 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'; +import 'package:uuid/uuid.dart'; class InitialBinding implements Bindings { @override @@ -19,6 +20,7 @@ class InitialBinding implements Bindings { void _registerCoreServices() { /// 立即注册所有的适配器 Get.put(GetStorage(), permanent: true); + Get.put(Uuid(), permanent: true); Get.put(HttpProvider()); Get.put(SQLiteProvider()); Get.put(ConnectivityProvider()); @@ -43,6 +45,7 @@ class InitialBinding implements Bindings { sqliteProvider: Get.find(), httpProvider: Get.find(), connectivityProvider: Get.find(), + authRepository: Get.find(), ), ); } diff --git a/lib/data/models/problem_model.dart b/lib/data/models/problem_model.dart index d08a825..7feee47 100644 --- a/lib/data/models/problem_model.dart +++ b/lib/data/models/problem_model.dart @@ -21,6 +21,9 @@ class Problem { /// 问题创建的时间。 final DateTime creationTime; + /// 问题创建id + final String creatorId; + /// 问题的同步状态,默认为未同步。 final ProblemSyncStatus syncStatus; @@ -42,6 +45,7 @@ class Problem { required this.location, required this.imageUrls, required this.creationTime, + required this.creatorId, required this.lastModifiedTime, this.syncStatus = ProblemSyncStatus.pendingCreate, this.censorTaskId, @@ -56,6 +60,7 @@ class Problem { String? location, List? imageUrls, DateTime? creationTime, + String? creatorId, DateTime? lastModifiedTime, ProblemSyncStatus? syncStatus, bool? isDeleted, @@ -69,6 +74,7 @@ class Problem { location: location ?? this.location, imageUrls: imageUrls ?? this.imageUrls, creationTime: creationTime ?? this.creationTime, + creatorId: creatorId ?? this.creatorId, lastModifiedTime: lastModifiedTime ?? this.lastModifiedTime, syncStatus: syncStatus ?? this.syncStatus, censorTaskId: censorTaskId ?? this.censorTaskId, @@ -85,6 +91,7 @@ class Problem { 'location': location, 'imageUrls': json.encode(imageUrls.map((e) => e.toMap()).toList()), 'creationTime': creationTime.millisecondsSinceEpoch, + 'creatorId': creatorId, 'lastModifiedTime': lastModifiedTime.millisecondsSinceEpoch, 'syncStatus': syncStatus.index, 'censorTaskId': censorTaskId, @@ -112,9 +119,14 @@ class Problem { description: map['description'], location: map['location'], imageUrls: imageUrlsList, - creationTime: DateTime.fromMillisecondsSinceEpoch(map['creationTime']), + creationTime: DateTime.fromMillisecondsSinceEpoch( + map['creationTime'], + isUtc: true, + ), + creatorId: map['creatorId'], lastModifiedTime: DateTime.fromMillisecondsSinceEpoch( map['lastModifiedTime'], + isUtc: true, ), syncStatus: ProblemSyncStatus.values[map['syncStatus']], censorTaskId: map['censorTaskId'], diff --git a/lib/data/models/problem_sync_status.dart b/lib/data/models/problem_sync_status.dart index a37252e..5c57560 100644 --- a/lib/data/models/problem_sync_status.dart +++ b/lib/data/models/problem_sync_status.dart @@ -1,5 +1,7 @@ +import 'package:get/get.dart'; import 'package:problem_check_system/data/models/image_metadata_model.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; +import 'package:problem_check_system/data/repositories/auth_repository.dart'; import 'package:uuid/uuid.dart'; enum ProblemSyncStatus { @@ -20,29 +22,33 @@ enum ProblemSyncStatus { } /// 问题状态管理器 - 类似 git add/git commit -class ProblemStateManager { +class ProblemStateManager extends GetxController { /// 静态对象uuid - static final Uuid _uuid = Uuid(); + final Uuid uuid; + final AuthRepository authRepository; + + ProblemStateManager({required this.uuid, required this.authRepository}); /// 创建新问题(类似创建新文件) - static Problem createNewProblem({ + Problem createNewProblem({ required String description, required String location, required List imageUrls, }) { return Problem( - id: _uuid.v4(), + id: uuid.v4(), description: description, location: location, imageUrls: imageUrls, - creationTime: DateTime.now(), - lastModifiedTime: DateTime.now(), + creationTime: DateTime.now().toUtc(), + creatorId: authRepository.getUserId()!, + lastModifiedTime: DateTime.now().toUtc(), syncStatus: ProblemSyncStatus.pendingCreate, ); } /// 修改问题内容(类似编辑文件) - static Problem modifyProblem(Problem problem) { + Problem modifyProblem(Problem problem) { final newStatus = problem.syncStatus == ProblemSyncStatus.synced ? ProblemSyncStatus .pendingUpdate // 已同步的改为待更新 @@ -50,25 +56,25 @@ class ProblemStateManager { return problem.copyWith( syncStatus: newStatus, - lastModifiedTime: DateTime.now(), + lastModifiedTime: DateTime.now().toUtc(), ); } /// 标记问题为删除 - static Problem markForDeletion(Problem problem) { + Problem markForDeletion(Problem problem) { switch (problem.syncStatus) { case ProblemSyncStatus.pendingCreate: // 待创建的问题 → 未跟踪(直接移除) return problem.copyWith( syncStatus: ProblemSyncStatus.untracked, - lastModifiedTime: DateTime.now(), + lastModifiedTime: DateTime.now().toUtc(), ); case ProblemSyncStatus.synced: case ProblemSyncStatus.pendingUpdate: // 已同步或待更新的问题 → 待删除(需要服务器操作) return problem.copyWith( syncStatus: ProblemSyncStatus.pendingDelete, - lastModifiedTime: DateTime.now(), + lastModifiedTime: DateTime.now().toUtc(), ); case ProblemSyncStatus.untracked: case ProblemSyncStatus.pendingDelete: @@ -78,21 +84,21 @@ class ProblemStateManager { } /// 撤销删除(类似 git reset) - static Problem undoDeletion(Problem problem) { + Problem undoDeletion(Problem problem) { if (problem.syncStatus == ProblemSyncStatus.pendingDelete) { return problem.copyWith( syncStatus: ProblemSyncStatus.pendingUpdate, - lastModifiedTime: DateTime.now(), + lastModifiedTime: DateTime.now().toUtc(), ); } return problem; } /// 同步成功后的状态更新(类似 git commit 成功) - static Problem markAsSynced(Problem problem) { + Problem markAsSynced(Problem problem) { return problem.copyWith( syncStatus: ProblemSyncStatus.synced, - lastModifiedTime: DateTime.now(), + lastModifiedTime: DateTime.now().toUtc(), ); } } diff --git a/lib/data/providers/sqlite_provider.dart b/lib/data/providers/sqlite_provider.dart index 463189c..7f3d7b0 100644 --- a/lib/data/providers/sqlite_provider.dart +++ b/lib/data/providers/sqlite_provider.dart @@ -50,6 +50,7 @@ class SQLiteProvider extends GetxService { location TEXT NOT NULL, imageUrls TEXT NOT NULL, creationTime INTEGER NOT NULL, + creatorId TEXT NOT NULL, lastModifiedTime INTEGER NOT NULL, syncStatus INTEGER NOT NULL, censorTaskId TEXT, diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index e96283e..d9d5484 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -21,6 +21,7 @@ class AuthRepository extends GetxService { static const String _refreshTokenKey = 'refresh_token'; static const String _loginKey = 'user'; static const String _rememberMe = 'remember_me'; + static const String _userId = 'userId'; void saveToken(String token) { storage.write(_tokenKey, token); @@ -38,6 +39,14 @@ class AuthRepository extends GetxService { return storage.read(_refreshTokenKey); } + void saveUserId(String id) { + storage.write(_userId, id); + } + + String? getUserId() { + return storage.read(_userId); + } + void addLoginKey(LoginRequest login) { storage.write(_loginKey, login.toJson()); } diff --git a/lib/data/repositories/problem_repository.dart b/lib/data/repositories/problem_repository.dart index a3db85f..7713c80 100644 --- a/lib/data/repositories/problem_repository.dart +++ b/lib/data/repositories/problem_repository.dart @@ -7,6 +7,7 @@ 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'; +import 'package:problem_check_system/data/repositories/auth_repository.dart'; /// 问题仓库,负责处理问题数据的本地持久化。 /// 它封装了底层数据库操作,为业务逻辑层提供一个简洁的接口。 @@ -14,6 +15,7 @@ class ProblemRepository extends GetxService { final SQLiteProvider sqliteProvider; final HttpProvider httpProvider; final ConnectivityProvider connectivityProvider; + final AuthRepository authRepository; RxBool get isOnline => connectivityProvider.isOnline; @@ -21,6 +23,7 @@ class ProblemRepository extends GetxService { required this.sqliteProvider, required this.httpProvider, required this.connectivityProvider, + required this.authRepository, }); /// 更新本地数据库中的一个问题。 @@ -54,7 +57,6 @@ class ProblemRepository extends GetxService { await sqliteProvider.deleteProblem(problemId); } - // TODO 添加创建者id,绑定信息显示 // 在ProblemRepository中添加 Future> fetchProblemsFromServer({ DateTime? startTime, @@ -67,6 +69,7 @@ class ProblemRepository extends GetxService { final response = await httpProvider.get( ApiEndpoints.getProblems, queryParameters: { + 'creatorId': authRepository.getUserId(), if (startTime != null) 'StartTime': startTime.toUtc().toIso8601String(), if (endTime != null) 'EndTime': endTime.toUtc().toIso8601String(), diff --git a/lib/modules/auth/controllers/login_controller.dart b/lib/modules/auth/controllers/login_controller.dart index d2ac4f2..2187c6c 100644 --- a/lib/modules/auth/controllers/login_controller.dart +++ b/lib/modules/auth/controllers/login_controller.dart @@ -73,8 +73,11 @@ class LoginController extends GetxController { _authRepository.removeLoginKey(); } Get.offAllNamed(AppRoutes.home); - // 登录成功,处理响应 - debugPrint('登录成功: $loginResponse'); + // 登录成功,访问用户详细信息 + var user = await _authRepository.getUserProfile(); + if (user.id != null) { + _authRepository.saveUserId(user.id!); + } } on DioException catch (e) { // 捕获由拦截器处理后抛出的 DioException // 拦截器已经显示了 Snackbar,这里你可以做其他业务处理,例如清空表单等。 diff --git a/lib/modules/home/bindings/home_binding.dart b/lib/modules/home/bindings/home_binding.dart index ecbcf37..c441c3b 100644 --- a/lib/modules/home/bindings/home_binding.dart +++ b/lib/modules/home/bindings/home_binding.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'package:problem_check_system/data/models/problem_sync_status.dart'; import 'package:problem_check_system/data/repositories/auth_repository.dart'; import 'package:problem_check_system/data/repositories/problem_repository.dart'; import 'package:problem_check_system/modules/home/controllers/home_controller.dart'; @@ -10,10 +11,14 @@ class HomeBinding implements Bindings { void dependencies() { /// 注册主页控制器 Get.lazyPut(() => HomeController()); + Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); /// 注册问题控制器 Get.lazyPut( - () => ProblemController(problemRepository: Get.find()), + () => ProblemController( + problemRepository: Get.find(), + problemStateManager: Get.find(), + ), fenix: true, ); diff --git a/lib/modules/problem/bindings/problem_form_binding.dart b/lib/modules/problem/bindings/problem_form_binding.dart index d357bc2..2dd892c 100644 --- a/lib/modules/problem/bindings/problem_form_binding.dart +++ b/lib/modules/problem/bindings/problem_form_binding.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; +import 'package:problem_check_system/data/models/problem_sync_status.dart'; import 'package:problem_check_system/modules/problem/controllers/problem_form_controller.dart'; class ProblemFormBinding extends Bindings { @@ -12,9 +13,12 @@ class ProblemFormBinding extends Bindings { if (arguments != null && arguments is Problem) { problem = arguments; } + + Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); Get.lazyPut( () => ProblemFormController( problemRepository: Get.find(), + problemStateManager: Get.find(), problem: problem, isReadOnly: readOnly, ), diff --git a/lib/modules/problem/controllers/problem_controller.dart b/lib/modules/problem/controllers/problem_controller.dart index 3202b97..e8390d3 100644 --- a/lib/modules/problem/controllers/problem_controller.dart +++ b/lib/modules/problem/controllers/problem_controller.dart @@ -25,6 +25,7 @@ class ProblemController extends GetxController with GetSingleTickerProviderStateMixin { /// 依赖问题数据 final ProblemRepository problemRepository; + final ProblemStateManager problemStateManager; final FileRepository fileRepository = Get.find(); /// 最近问题列表 @@ -103,7 +104,10 @@ class ProblemController extends GetxController /// get 选中的 RxBool get isOnline => problemRepository.isOnline; - ProblemController({required this.problemRepository}); + ProblemController({ + required this.problemRepository, + required this.problemStateManager, + }); @override void onInit() { @@ -457,7 +461,7 @@ class ProblemController extends GetxController ); try { - const int totalSteps = 4; + const int totalSteps = 5; syncProgress.startSync(totalSteps); // 1. 从服务器获取最新数据 @@ -477,10 +481,11 @@ class ProblemController extends GetxController ); // 4. 启动图片下载任务 - _downloadImagesForProblems(downloadedProblems); + syncProgress.updateProgress('正在下载图片...', 4); + await downloadImagesForProblems(downloadedProblems); // 5. 重新加载本地问题列表 - syncProgress.updateProgress('正在重新加载数据...', 4); + syncProgress.updateProgress('正在重新加载数据...', 5); await loadProblems(); syncProgress.completeSync(); @@ -500,64 +505,154 @@ class ProblemController extends GetxController } } - /// 异步下载问题的图片 - void _downloadImagesForProblems(List problems) { + /// 异步下载问题的图片,返回下载完成的任务 + Future downloadImagesForProblems(List problems) async { 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, - ); + final imageRepository = Get.find(); + final List> downloadFutures = []; + + for (final problem in problems) { + // 为每个问题创建下载任务 + final downloadFuture = _downloadProblemImages(problem, imageRepository); + downloadFutures.add(downloadFuture); + } + + // 等待所有问题图片下载完成 + await Future.wait(downloadFutures); + } - 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, + /// 下载单个问题的所有图片 + Future _downloadProblemImages( + Problem problem, + ImageRepository imageRepository, + ) async { + try { + final List downloadedImages = []; + final List> imageFutures = []; + + for (final imageMeta in problem.imageUrls) { + if (imageMeta.remoteUrl != null && imageMeta.remoteUrl!.isNotEmpty) { + // 为每张图片创建下载任务 + final imageFuture = + _downloadSingleImage(imageMeta, problem.id, imageRepository).then( + (downloadedImage) { + if (downloadedImage != null) { + downloadedImages.add(downloadedImage); + } + }, ); - downloadedImages.add(downloadedImage); - } - } - // 更新问题的图片数据 - if (downloadedImages.isNotEmpty) { - final updatedProblem = problem.copyWith( - imageUrls: downloadedImages, - ); - await problemRepository.updateProblem(updatedProblem); - } - } catch (e) { - Get.log('下载问题 ${problem.id} 的图片失败: $e'); + imageFutures.add(imageFuture); } } - }); + + // 等待当前问题的所有图片下载完成 + await Future.wait(imageFutures); + + // 更新问题的图片数据 + if (downloadedImages.isNotEmpty) { + final updatedProblem = problem.copyWith(imageUrls: downloadedImages); + await problemRepository.updateProblem(updatedProblem); + } + } catch (e) { + Get.log('下载问题 ${problem.id} 的图片失败: $e'); + rethrow; // 重新抛出异常,让调用方知道失败 + } + } + + /// 下载单张图片 + Future _downloadSingleImage( + ImageMetadata imageMeta, + String problemId, + ImageRepository imageRepository, + ) async { + try { + final bool isDownloaded = await imageRepository.isImageDownloaded( + imageMeta.remoteUrl!, + problemId, + ); + + String localPath; + if (isDownloaded) { + localPath = (await imageRepository.getLocalImagePath( + imageMeta.remoteUrl!, + problemId, + ))!; + } else { + localPath = await imageRepository.downloadImage( + imageMeta.remoteUrl!, + problemId, + ); + } + + return imageMeta.copyWith( + localPath: localPath, + status: ImageStatus.synced, + ); + } catch (e) { + Get.log('下载图片 ${imageMeta.remoteUrl} 失败: $e'); + return null; // 单张图片失败不影响其他图片 + } } + // /// 异步下载问题的图片 + // 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( @@ -607,7 +702,9 @@ class ProblemController extends GetxController final serverUpdated = serverProblem.lastModificationTime; final localUpdated = localProblem.lastModifiedTime; - if (serverUpdated != null && serverUpdated.isAfter(localUpdated)) { + if (serverUpdated != null && + serverUpdated.millisecondsSinceEpoch > + localUpdated.millisecondsSinceEpoch) { // 服务器数据更新,更新本地数据 final updatedProblem = _convertServerProblemToLocal(serverProblem); await problemRepository.updateProblem(updatedProblem); @@ -640,6 +737,7 @@ class ProblemController extends GetxController location: serverProblem.location, imageUrls: imageMetadatas, creationTime: serverProblem.creationTime, + creatorId: serverProblem.creatorId, lastModifiedTime: serverProblem.lastModificationTime ?? DateTime.now(), syncStatus: ProblemSyncStatus.synced, // 来自服务器的数据标记为已同步 censorTaskId: serverProblem.censorTaskId, @@ -836,7 +934,7 @@ class ProblemController extends GetxController /// 控制器中可以添加逻辑 Future deleteProblem(Problem problem) async { try { - final deleteProblem = ProblemStateManager.markForDeletion(problem); + final deleteProblem = problemStateManager.markForDeletion(problem); if (deleteProblem.syncStatus == ProblemSyncStatus.untracked) { // 直接删除问题和图片 await problemRepository.deleteProblem(problem.id); diff --git a/lib/modules/problem/controllers/problem_form_controller.dart b/lib/modules/problem/controllers/problem_form_controller.dart index 2c0d8d7..06d9269 100644 --- a/lib/modules/problem/controllers/problem_form_controller.dart +++ b/lib/modules/problem/controllers/problem_form_controller.dart @@ -20,6 +20,7 @@ class ProblemFormController extends GetxController { final bool isReadOnly; final ProblemRepository problemRepository; + final ProblemStateManager problemStateManager; final TextEditingController descriptionController = TextEditingController(); final TextEditingController locationController = TextEditingController(); final RxList selectedImages = [].obs; @@ -28,6 +29,7 @@ class ProblemFormController extends GetxController { // 使用依赖注入,便于测试 ProblemFormController({ required this.problemRepository, + required this.problemStateManager, this.problem, this.isReadOnly = false, }) { @@ -162,12 +164,12 @@ class ProblemFormController extends GetxController { imageUrls: imagePaths, ); // 如果原问题是待创建的,修改后仍然应该是创建操作 - final modifyProblem = ProblemStateManager.modifyProblem(updatedProblem); + final modifyProblem = problemStateManager.modifyProblem(updatedProblem); await problemRepository.updateProblem(modifyProblem); } else { // 创建新问题 - final newProblem = ProblemStateManager.createNewProblem( + final newProblem = problemStateManager.createNewProblem( description: descriptionController.text, location: locationController.text, imageUrls: imagePaths,