From b7f3cd8e1047a02ca35bd8ee436aa9046b96144f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=8C=AF=E5=8D=87?= <359059686@qq.com> Date: Wed, 5 Nov 2025 16:27:43 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E5=90=8C=E6=AD=A5=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E9=97=AE=E9=A2=98=E5=88=B0=E6=9C=AC=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 67 +++++++++++++++++ .../enterprise_repository_impl.dart | 4 +- .../problem_local_data_source.dart | 27 +++++++ .../repositories/problem_repository_impl.dart | 74 +++++++++++++++++++ .../repositories/problem_repository.dart | 7 ++ .../usecases/sync_problems_usecase.dart | 11 +++ .../bindings/problem_list_binding.dart | 1 + .../bindings/problem_upload_binding.dart | 1 + 8 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 lib/app/features/problem/domain/usecases/sync_problems_usecase.dart diff --git a/README.md b/README.md index bb08a53..68baec5 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,70 @@ SQFlite: 本地数据库,用于离线数据存储。 Image Picker: 用于从图库或相机选择图片。 HTTP/Dio: 网络请求库。推荐使用 Dio,因为它提供了更强大的拦截器、表单数据处理和错误处理功能。 + +TODO// + +/// 定义了一个通用的 CRUD (Create, Read, Update, Delete) 仓库必须具备的核心能力 +abstract class ICrudRepository { + Future getById(String id); + Future> getAll(); + Future add(T entity); + Future update(T entity); + Future delete(String id); + Future syncToServer(T entity); +} + +新增 + +import 'package:problem_check_system/app/core/domain/repositories/i_crud_repository.dart'; + +class AddEntityUseCase { + final ICrudRepository repository; + + AddEntityUseCase({required this.repository}); + + Future call(T entity) { + return repository.add(entity); + } +} + +更新 + +import 'package:problem_check_system/app/core/domain/repositories/i_crud_repository.dart'; + +class UpdateEntityUseCase { + final ICrudRepository repository; + + UpdateEntityUseCase({required this.repository}); + + Future call(T entity) { + return repository.update(entity); + } +} + +删除 + +import 'package:problem_check_system/app/core/domain/repositories/i_crud_repository.dart'; + +class DeleteEntityUseCase { + final ICrudRepository repository; + + DeleteEntityUseCase({required this.repository}); + + Future call(String id) { + return repository.delete(id); + } +} + +根据id获取 +import 'package:problem_check_system/app/core/domain/repositories/i_crud_repository.dart'; + +class GetEntityByIdUseCase { + final ICrudRepository repository; + + GetEntityByIdUseCase({required this.repository}); + + Future call(String id) { + return repository.getById(id); + } +} diff --git a/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart b/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart index 604a0ac..867b698 100644 --- a/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart +++ b/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart @@ -5,9 +5,7 @@ import 'package:problem_check_system/app/features/enterprise/data/datasources/en import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_dto.dart'; import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_model.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; -import 'package:problem_check_system/app/core/domain/entities/data_conflict.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; -import 'package:problem_check_system/app/core/domain/entities/sync_result.dart'; import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; import 'package:uuid/uuid.dart'; @@ -181,7 +179,7 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository { } // 5. 返回同步结果,让上层处理冲突 - return SyncResult( + return EnterpriseSyncResult( newItemsFromServer: newEnterprises.length, conflicts: conflicts, ); diff --git a/lib/app/features/problem/data/datasources/problem_local_data_source.dart b/lib/app/features/problem/data/datasources/problem_local_data_source.dart index fb55f2b..6efd9b1 100644 --- a/lib/app/features/problem/data/datasources/problem_local_data_source.dart +++ b/lib/app/features/problem/data/datasources/problem_local_data_source.dart @@ -1,5 +1,7 @@ import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; import 'package:problem_check_system/app/core/services/database_service.dart'; +import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; +import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_filter_params.dart'; import 'package:sqflite/sqflite.dart'; // 导入你的 DatabaseService @@ -35,6 +37,8 @@ abstract class ProblemLocalDataSource { /// /// [id] - 要删除的问题的唯一标识符。 Future deleteProblem(String id); + + Future cacheProblems(List> newProblems); } // 假设 IProblemLocalDataSource 接口定义在同一个文件中或已导入 @@ -157,4 +161,27 @@ class ProblemLocalDataSourceImpl implements ProblemLocalDataSource { whereArgs: [problemMap['id']], ); } + + @override + Future cacheProblems(List> problemMaps) async { + if (problemMaps.isEmpty) { + return; // 如果列表为空,则直接返回 + } + final db = await _databaseService.database; + + // 1. 创建一个 Batch 对象 + final batch = db.batch(); + + // 2. 遍历列表,将每个 insert 操作添加到 batch 中 + for (final problem in problemMaps) { + batch.insert( + _tableName, + problem, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // 3. 提交 batch,所有操作在一次事务中完成 + await batch.commit(noResult: true); + } } diff --git a/lib/app/features/problem/data/repositories/problem_repository_impl.dart b/lib/app/features/problem/data/repositories/problem_repository_impl.dart index 920e78e..8923dbe 100644 --- a/lib/app/features/problem/data/repositories/problem_repository_impl.dart +++ b/lib/app/features/problem/data/repositories/problem_repository_impl.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; +import 'package:problem_check_system/app/core/services/network_status_service.dart'; import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_data_source.dart'; import 'package:problem_check_system/app/features/problem/data/datasources/problem_remote_data_source.dart'; import 'package:problem_check_system/app/features/problem/data/model/problem_dto.dart'; +import 'package:problem_check_system/app/features/problem/data/model/problem_filter_dto.dart'; import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_filter_params.dart'; @@ -15,10 +17,12 @@ import 'package:problem_check_system/app/features/problem/domain/repositories/pr class ProblemRepositoryImpl implements ProblemRepository { final ProblemLocalDataSource localDataSource; final ProblemRemoteDataSource remoteDataSource; + final NetworkStatusService networkStatusService; ProblemRepositoryImpl({ required this.localDataSource, required this.remoteDataSource, + required this.networkStatusService, }); @override @@ -161,4 +165,74 @@ class ProblemRepositoryImpl implements ProblemRepository { // 返回最终从服务器同步回来的、纯净的 Entity return syncedEntity; } + + /// 同步服务器数据 + @override + Future syncWithServer() async { + if (!networkStatusService.isOnline.value) { + // 如果离线,直接返回一个空的同步结果 + return const ProblemSyncResult(); + } + + // 从远程和本地获取所有数据 + final remoteProblemsDto = await remoteDataSource.getProblems( + ProblemFilterDto(), + ); + final localProblemsModel = await localDataSource.getAllProblems( + ProblemFilterParams(), + ); + + final remoteProblems = remoteProblemsDto + .map((dto) => dto.toEntity()) + .toList(); + final localProblems = localProblemsModel.map( + (map) => ProblemModel.fromMap(map), + ); + // 2. 使用 Map 进行高效查找 + final localProblemMap = {for (var e in localProblems) e.id: e.toEntity()}; + + final List newProblems = []; + final List conflicts = []; + + // 3. 遍历远程数据,进行比较 + for (final remoteProblem in remoteProblems) { + final localProblem = localProblemMap[remoteProblem.id]; + + if (localProblem == null) { + // 本地没有,是新数据 + + newProblems.add(remoteProblem); + } else { + // 将两个时间都转换为毫秒时间戳进行比较,以忽略微秒级别的精度差异。 + final remoteMillis = + remoteProblem.lastModifiedTime.millisecondsSinceEpoch; + final localMillis = + localProblem.lastModifiedTime.millisecondsSinceEpoch; + if (remoteMillis != localMillis) { + //产生冲突 + conflicts.add( + ProblemConflict( + localVersion: localProblem, + serverVersion: remoteProblem, + ), + ); + } + // 如果时间一致或本地更新(我们以服务器为准),则跳过 + } + } + + // 4. 将无冲突的新数据直接写入本地数据库 + if (newProblems.isNotEmpty) { + List> problemMaps = newProblems + .map((entity) => ProblemModel.fromEntity(entity).toMap()) + .toList(); + await localDataSource.cacheProblems(problemMaps); + } + + // 5. 返回同步结果,让上层处理冲突 + return ProblemSyncResult( + newItemsFromServer: newProblems.length, + conflicts: conflicts, + ); + } } diff --git a/lib/app/features/problem/domain/repositories/problem_repository.dart b/lib/app/features/problem/domain/repositories/problem_repository.dart index 481e723..9932e50 100644 --- a/lib/app/features/problem/domain/repositories/problem_repository.dart +++ b/lib/app/features/problem/domain/repositories/problem_repository.dart @@ -1,7 +1,12 @@ +import 'package:problem_check_system/app/core/domain/entities/data_conflict.dart'; +import 'package:problem_check_system/app/core/domain/entities/sync_result.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_filter_params.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart'; +typedef ProblemSyncResult = SyncResult; +typedef ProblemConflict = DataConflict; + /// Problem 仓库的抽象接口 /// 定义了业务逻辑层需要的数据操作。 abstract class ProblemRepository { @@ -14,4 +19,6 @@ abstract class ProblemRepository { Future deleteProblem(String id); // 同步问题到服务器 Future syncProblemToServer(ProblemEntity problem); + + Future syncWithServer(); } diff --git a/lib/app/features/problem/domain/usecases/sync_problems_usecase.dart b/lib/app/features/problem/domain/usecases/sync_problems_usecase.dart new file mode 100644 index 0000000..261d42d --- /dev/null +++ b/lib/app/features/problem/domain/usecases/sync_problems_usecase.dart @@ -0,0 +1,11 @@ +import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; + +class SyncProblemsUsecase { + final ProblemRepository repository; + + SyncProblemsUsecase({required this.repository}); + + Future call() async { + return repository.syncWithServer(); + } +} diff --git a/lib/app/features/problem/presentation/bindings/problem_list_binding.dart b/lib/app/features/problem/presentation/bindings/problem_list_binding.dart index eabbdf1..6c26562 100644 --- a/lib/app/features/problem/presentation/bindings/problem_list_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_list_binding.dart @@ -30,6 +30,7 @@ class ProblemListBinding extends BaseBindings { () => ProblemRepositoryImpl( localDataSource: Get.find(), remoteDataSource: Get.find(), + networkStatusService: Get.find(), ), ); } diff --git a/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart b/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart index 7509eaa..0a2db62 100644 --- a/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart @@ -38,6 +38,7 @@ class ProblemUploadBinding extends BaseBindings { () => ProblemRepositoryImpl( localDataSource: Get.find(), remoteDataSource: Get.find(), + networkStatusService: Get.find(), ), ); Get.lazyPut(