From afc5d191881f646556b07929a7cf83f715c85a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=8C=AF=E5=8D=87?= <359059686@qq.com> Date: Tue, 28 Oct 2025 16:52:21 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E5=90=8C=E6=AD=A5=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/extensions/datetime_extension.dart | 4 + lib/app/core/services/database_service.dart | 46 +++++-- .../enterprise_local_data_source.dart | 75 +++++++++-- .../enterprise_remote_data_source.dart | 13 +- .../enterprise/data/model/enterprise_dto.dart | 60 ++++++--- .../enterprise_repository_impl.dart | 81 +++++++++++- .../domain/entities/enterprise_conflict.dart | 16 +++ .../domain/entities/sync_result.dart | 18 +++ .../repositories/enterprise_repository.dart | 6 + .../usecases/editor_enterprise_usecase.dart | 2 +- .../usecases/resolve_conflict_usecase.dart | 12 ++ .../usecases/sync_enterprises_usecase.dart | 12 ++ .../bindings/enterprise_form_binding.dart | 2 + .../bindings/enterprise_list_binding.dart | 13 ++ .../bindings/enterprise_upload_binding.dart | 2 + .../enterprise_list_controller.dart | 116 +++++++++++++++--- 16 files changed, 411 insertions(+), 67 deletions(-) create mode 100644 lib/app/features/enterprise/domain/entities/enterprise_conflict.dart create mode 100644 lib/app/features/enterprise/domain/entities/sync_result.dart create mode 100644 lib/app/features/enterprise/domain/usecases/resolve_conflict_usecase.dart create mode 100644 lib/app/features/enterprise/domain/usecases/sync_enterprises_usecase.dart diff --git a/lib/app/core/extensions/datetime_extension.dart b/lib/app/core/extensions/datetime_extension.dart index e6b2493..c32cc9c 100644 --- a/lib/app/core/extensions/datetime_extension.dart +++ b/lib/app/core/extensions/datetime_extension.dart @@ -7,6 +7,10 @@ extension DateTimeFormatting on DateTime { return DateFormat('yyyy-MM-dd HH:mm:ss').format(this); } + String toDateTimeString2() { + return DateFormat("yyyy-MM-dd HH:mm:ss.SSS'Z'").format(toLocal()); + } + /// 格式化为 'yyyy-MM-dd' String toDateString() { return DateFormat('yyyy-MM-dd').format(this); diff --git a/lib/app/core/services/database_service.dart b/lib/app/core/services/database_service.dart index fe388aa..a77d7af 100644 --- a/lib/app/core/services/database_service.dart +++ b/lib/app/core/services/database_service.dart @@ -26,7 +26,9 @@ const String _createEnterprisesTable = ''' id TEXT PRIMARY KEY, syncStatus INTEGER NOT NULL, lastModifiedTime INTEGER NOT NULL, + lastModifierId TEXT NOT NULL, creationTime INTEGER NOT NULL, + creatorId TEXT NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL, address TEXT, @@ -41,7 +43,7 @@ const String _createEnterprisesTable = ''' /// 这是一个核心服务,只处理连接、创建和升级,不关心具体的数据操作。 class DatabaseService extends GetxService { static const String _dbName = 'database.db'; - static const int _dbVersion = 1; + static const int _dbVersion = 3; Database? _database; @@ -88,15 +90,41 @@ class DatabaseService extends GetxService { /// 数据库版本升级处理 Future _onUpgrade(Database db, int oldVersion, int newVersion) async { Get.log('正在将数据库从版本 $oldVersion 升级到 $newVersion...'); - // 实际项目中,这里应包含每个版本的迁移逻辑 - if (oldVersion < 2) { - // 从版本 1 升级到版本 2 的逻辑 - // 为 problems 表添加 enterpriseId 字段 - await db.execute(''' - ALTER TABLE problems ADD COLUMN enterpriseId TEXT + + // 使用事务来确保升级的原子性 + await db.transaction((txn) async { + // 检查是否需要从版本 1 升级 + if (oldVersion < 2) { + Get.log('数据库升级 V1 -> V2:'); + + // 第 1 步:先添加新的列 + await txn.execute('ALTER TABLE enterprises ADD COLUMN creatorId TEXT'); + Get.log(' - `enterprises` 表已成功添加 `creatorId` 字段'); + + await txn.execute( + 'ALTER TABLE enterprises ADD COLUMN lastModifierId TEXT', + ); + Get.log(' - `enterprises` 表已成功添加 `lastModifierId` 字段'); + } + + // 如果还有后续版本升级,可以在这里添加 + if (oldVersion < 3) { + // 从版本 2 升级到版本 3 的逻辑 + await txn.execute(''' + UPDATE enterprises + SET + creatorId = '65a8dfa687a982050c224135', + lastModifierId = '65a8dfa687a982050c224135' + WHERE + creatorId IS NULL OR lastModifierId IS NULL; '''); - Get.log('`problems` 表已成功添加 `enterpriseId` 字段'); - } + Get.log( + ' - `enterprises` 表中旧数据的 `creatorId` 和 `lastModifierId` 字段已填充默认值', + ); + } + }); + + Get.log('数据库升级完成'); } @override diff --git a/lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart b/lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart index 79f3919..38a3290 100644 --- a/lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart +++ b/lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart @@ -6,7 +6,7 @@ import 'package:sqflite/sqflite.dart'; abstract class EnterpriseLocalDataSource { /// 新增企业 - Future addEnterprise(EnterpriseModel enterprise); + // Future addEnterprise(EnterpriseModel enterprise); Future updateEnterprise(EnterpriseModel enterprise); Future> getEnterpriseListItems({ String? name, @@ -17,6 +17,12 @@ abstract class EnterpriseLocalDataSource { }); Future updateSyncStatus(String enterpriseId, SyncStatus newStatus); + + // [新增] 获取所有企业用于比较 + Future> getAllEnterprises(); + // [新增] 更新或插入单个企业 + Future upsertEnterprise(EnterpriseModel enterprise); + Future cacheEnterprises(List enterprises); } class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource { @@ -28,18 +34,18 @@ class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource { static const String _tableName = 'enterprises'; - @override - Future addEnterprise(EnterpriseModel enterprise) async { - final db = await _databaseService.database; - await db.insert( - _tableName, - enterprise.toMap(), - // conflictAlgorithm 用于定义当主键 (id) 冲突时的处理策略。 - // ConflictAlgorithm.replace 表示如果 ID 已存在,则用新数据替换旧数据。 - // 这在同步场景下通常比较有用。 - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } + // @override + // Future addEnterprise(EnterpriseModel enterprise) async { + // final db = await _databaseService.database; + // await db.insert( + // _tableName, + // enterprise.toMap(), + // // conflictAlgorithm 用于定义当主键 (id) 冲突时的处理策略。 + // // ConflictAlgorithm.replace 表示如果 ID 已存在,则用新数据替换旧数据。 + // // 这在同步场景下通常比较有用。 + // conflictAlgorithm: ConflictAlgorithm.replace, + // ); + // } @override Future updateEnterprise(EnterpriseModel enterprise) async { @@ -161,4 +167,47 @@ class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource { whereArgs: [enterpriseId], ); } + + /// [正确实现] 批量缓存(插入)企业列表 + /// 这个方法专门用于高效地将一个列表的数据写入数据库 + @override + Future cacheEnterprises(List enterprises) async { + if (enterprises.isEmpty) { + return; // 如果列表为空,则直接返回 + } + final db = await _databaseService.database; + + // 1. 创建一个 Batch 对象 + final batch = db.batch(); + + // 2. 遍历列表,将每个 insert 操作添加到 batch 中 + for (final enterprise in enterprises) { + // 使用 insert 方法,ConflictAlgorithm.replace 可以在 id 冲突时覆盖旧数据,实现 "upsert" 效果 + batch.insert( + _tableName, + enterprise.toMap(), // 假设你的 EnterpriseModel 有一个 toMap() 方法 + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + // 3. 提交 batch,所有操作在一次事务中完成 + await batch.commit(noResult: true); + } + + @override + Future> getAllEnterprises() async { + final db = await _databaseService.database; + final maps = await db.query('enterprises'); + return maps.map((json) => EnterpriseModel.fromMap(json)).toList(); + } + + @override + Future upsertEnterprise(EnterpriseModel enterprise) async { + final db = await _databaseService.database; + await db.insert( + 'enterprises', + enterprise.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, // replace 会覆盖已有记录 + ); + } } diff --git a/lib/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart b/lib/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart index 6729df4..a6025ea 100644 --- a/lib/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart +++ b/lib/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart @@ -1,3 +1,4 @@ +import 'package:get/get.dart'; import 'package:problem_check_system/app/core/services/http_provider.dart'; 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'; @@ -28,7 +29,10 @@ class EnterpriseRemoteDataSourceImpl implements EnterpriseRemoteDataSource { try { final enterpriseDto = EnterpriseDto.fromModel(enterprise); final data = enterpriseDto.toJson(); - await http.post(enterprisesEndpoint, data: data); + await http.post( + '$enterprisesEndpoint/CreateWithId?Id=${enterprise.id}', + data: data, + ); } catch (e) { rethrow; } @@ -59,10 +63,11 @@ class EnterpriseRemoteDataSourceImpl implements EnterpriseRemoteDataSource { Future> getEnterprises() async { try { final response = await http.get(enterprisesEndpoint); - return (response.data as List) - .map((json) => EnterpriseDto.fromJson(json)) - .toList(); + final Map data = response.data; + final List enterprises = data['items']; + return enterprises.map((json) => EnterpriseDto.fromJson(json)).toList(); } catch (e) { + Get.log('$e'); rethrow; } } diff --git a/lib/app/features/enterprise/data/model/enterprise_dto.dart b/lib/app/features/enterprise/data/model/enterprise_dto.dart index 37333cf..7596b02 100644 --- a/lib/app/features/enterprise/data/model/enterprise_dto.dart +++ b/lib/app/features/enterprise/data/model/enterprise_dto.dart @@ -1,4 +1,5 @@ import 'package:problem_check_system/app/core/extensions/map_extensions.dart'; +import 'package:problem_check_system/app/core/models/sync_status.dart'; import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_model.dart'; /// EnterpriseDto (Data Transfer Object) @@ -9,8 +10,8 @@ class EnterpriseDto { final String id; final DateTime creationTime; final String creatorId; - final DateTime lastModificationTime; - final String lastModifierId; + final DateTime? lastModificationTime; + final String? lastModifierId; final String companyName; final String companyType; final String? companyScope; @@ -19,7 +20,7 @@ class EnterpriseDto { final String? securityPrincipalName; final String? securityPrincipalPhone; final String? companyAddress; - final String? detail; + final String? majorHazard; const EnterpriseDto({ required this.id, @@ -35,7 +36,7 @@ class EnterpriseDto { this.securityPrincipalName, this.securityPrincipalPhone, this.companyAddress, - this.detail, + this.majorHazard, }); /// [核心] 工厂构造函数:从您的内部模型 `EnterpriseModel` 创建 DTO。 @@ -45,16 +46,16 @@ class EnterpriseDto { return EnterpriseDto( id: model.id, creationTime: model.creationTime, - creatorId: "", //todo 需要在企业模型中添加创建时间 + creatorId: model.creatorId, lastModificationTime: model.lastModifiedTime, - lastModifierId: "", // todo需要在企业模型中添修改用户id + lastModifierId: model.lastModifierId, companyName: model.name, companyType: model.type, companyScope: model.scale, mainPrincipalName: model.contactPerson, mainPrincipalPhone: model.contactPhone, companyAddress: model.address, - detail: model.majorHazardsDescription, + majorHazard: model.majorHazardsDescription, ); } @@ -66,17 +67,20 @@ class EnterpriseDto { /// /// 当从服务器接收到 JSON 数据时,调用此方法将其转换为 Dart 对象。 factory EnterpriseDto.fromJson(Map json) { + final creationTime = DateTime.parse(json['creationTime'] as String); + final creatorId = json['creatorId'] as String; + final lastModTimeStr = json['lastModificationTime'] as String?; return EnterpriseDto( // 必须存在的字段 id: json['id'] as String, - creationTime: DateTime.parse(json['creationTime'] as String), - creatorId: json['creatorId'] as String, - lastModificationTime: DateTime.parse( - json['lastModificationTime'] as String, - ), - lastModifierId: json['lastModifierId'] as String, + creationTime: creationTime, + creatorId: creatorId, + lastModificationTime: lastModTimeStr != null + ? DateTime.parse(lastModTimeStr) + : creationTime, + lastModifierId: json['lastModifierId'] as String? ?? creatorId, companyName: json['companyName'] as String, - companyType: json['companyType'] as String, + companyType: json['companyType'] as String? ?? "生产", // 可选字段 companyScope: json['companyScope'] as String?, @@ -85,7 +89,7 @@ class EnterpriseDto { securityPrincipalName: json['securityPrincipalName'] as String?, securityPrincipalPhone: json['securityPrincipalPhone'] as String?, companyAddress: json['companyAddress'] as String?, - detail: json['detail'] as String?, + majorHazard: json['majorHazard'] as String?, ); } @@ -98,7 +102,7 @@ class EnterpriseDto { // 使用 toIso8601String() 是将 DateTime 转换为标准化字符串的最佳实践 'creationTime': creationTime.toIso8601String(), 'creatorId': creatorId, - 'lastModificationTime': lastModificationTime.toIso8601String(), + 'lastModificationTime': lastModificationTime?.toIso8601String(), 'lastModifierId': lastModifierId, 'companyName': companyName, 'companyType': companyType, @@ -108,8 +112,30 @@ class EnterpriseDto { 'securityPrincipalName': securityPrincipalName, 'securityPrincipalPhone': securityPrincipalPhone, 'companyAddress': companyAddress, - 'detail': detail, + 'majorHazard': majorHazard, }; return jsonMap.withoutNullOrEmptyValues; } + + /// [核心] 将 DTO (网络数据) 转换为 Model (本地/业务模型)。 + /// + /// 这个转换是数据流入应用内部的关键步骤。 + EnterpriseModel toModel() { + return EnterpriseModel( + id: id, + // DTO 中没有 syncStatus,因为从服务器来的数据我们默认为已同步 + syncStatus: SyncStatus.synced, + lastModifiedTime: lastModificationTime ?? creationTime, + lastModifierId: lastModifierId ?? creatorId, + creationTime: creationTime, + creatorId: creatorId, + name: companyName, + type: companyType, + address: companyAddress, + scale: companyScope, + contactPerson: mainPrincipalName, + contactPhone: mainPrincipalPhone, + majorHazardsDescription: majorHazard, + ); + } } 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 aac44db..54eb2a5 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 @@ -1,27 +1,33 @@ import 'package:problem_check_system/app/core/models/sync_status.dart'; +import 'package:problem_check_system/app/core/services/network_status_service.dart'; import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_local_data_source.dart'; import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_remote_data_source.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/features/enterprise/domain/entities/enterprise_conflict.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/entities/sync_result.dart'; import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; import 'package:uuid/uuid.dart'; class EnterpriseRepositoryImpl implements EnterpriseRepository { final EnterpriseLocalDataSource localDataSource; - final EnterpriseRemoteDataSource remoteDataSource; // 注入远程数据源 - final Uuid uuid = Uuid(); + final EnterpriseRemoteDataSource remoteDataSource; + final NetworkStatusService networkStatusService; + final Uuid uuid; EnterpriseRepositoryImpl({ required this.localDataSource, required this.remoteDataSource, + required this.networkStatusService, + required this.uuid, }); @override Future addEnterprise(Enterprise enterprise) async { final enterpriseModel = EnterpriseModel.fromEntity(enterprise); - await localDataSource.addEnterprise(enterpriseModel); + await localDataSource.upsertEnterprise(enterpriseModel); } @override @@ -108,4 +114,73 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository { // 将指令直接转发给本地数据源 await localDataSource.updateSyncStatus(enterpriseId, newStatus); } + + // [新增] 实现同步逻辑 + @override + Future syncWithServer() async { + if (!networkStatusService.isOnline.value) { + // 如果离线,直接返回一个空的同步结果 + return const SyncResult(); + } + + // 1. 从远程和本地获取所有数据 + final remoteEnterpriseDtos = await remoteDataSource.getEnterprises(); + // [使用 toModel] 将 DTO 列表转换为 Model 列表 + final remoteEnterprises = remoteEnterpriseDtos + .map((dto) => dto.toModel()) + .toList(); + final localEnterprises = await localDataSource.getAllEnterprises(); + + // 2. 使用 Map 进行高效查找 + final localEnterpriseMap = {for (var e in localEnterprises) e.id: e}; + + final List newEnterprises = []; + final List conflicts = []; + + // 3. 遍历远程数据,进行比较 + for (final remoteEnterprise in remoteEnterprises) { + final localEnterprise = localEnterpriseMap[remoteEnterprise.id]; + + if (localEnterprise == null) { + // 本地没有,是新数据 + + newEnterprises.add(remoteEnterprise); + } else { + // 将两个时间都转换为毫秒时间戳进行比较,以忽略微秒级别的精度差异。 + final remoteMillis = + remoteEnterprise.lastModifiedTime.millisecondsSinceEpoch; + final localMillis = + localEnterprise.lastModifiedTime.millisecondsSinceEpoch; + if (remoteMillis != localMillis) { + //产生冲突 + conflicts.add( + EnterpriseConflict( + localVersion: localEnterprise.toEntity(), // 假设 Model 可以转为 Entity + serverVersion: remoteEnterprise.toEntity(), + ), + ); + } + // 如果时间一致或本地更新(我们以服务器为准),则跳过 + } + } + + // 4. 将无冲突的新数据直接写入本地数据库 + if (newEnterprises.isNotEmpty) { + await localDataSource.cacheEnterprises(newEnterprises); + } + + // 5. 返回同步结果,让上层处理冲突 + return SyncResult( + newItemsFromServer: newEnterprises.length, + conflicts: conflicts, + ); + } + + // [新增] 实现解决冲突的逻辑 + @override + Future resolveConflictAndUpdate(Enterprise chosenEnterprise) { + // 将实体转换为模型,然后更新到数据库 + final enterpriseModel = EnterpriseModel.fromEntity(chosenEnterprise); + return localDataSource.upsertEnterprise(enterpriseModel); + } } diff --git a/lib/app/features/enterprise/domain/entities/enterprise_conflict.dart b/lib/app/features/enterprise/domain/entities/enterprise_conflict.dart new file mode 100644 index 0000000..0552526 --- /dev/null +++ b/lib/app/features/enterprise/domain/entities/enterprise_conflict.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; +import 'enterprise.dart'; // 假设 Enterprise 是你的核心实体 + +/// 表示本地版本和服务器版本之间的数据冲突 +class EnterpriseConflict extends Equatable { + final Enterprise localVersion; + final Enterprise serverVersion; + + const EnterpriseConflict({ + required this.localVersion, + required this.serverVersion, + }); + + @override + List get props => [localVersion, serverVersion]; +} diff --git a/lib/app/features/enterprise/domain/entities/sync_result.dart b/lib/app/features/enterprise/domain/entities/sync_result.dart new file mode 100644 index 0000000..cf59c4a --- /dev/null +++ b/lib/app/features/enterprise/domain/entities/sync_result.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; +import 'enterprise_conflict.dart'; + +class SyncResult extends Equatable { + /// 存在于服务器但本地没有的数据条目数量 + final int newItemsFromServer; + + /// 本地和服务器都存在,但内容有冲突的条目列表 + final List conflicts; + + const SyncResult({this.newItemsFromServer = 0, this.conflicts = const []}); + + /// 判断本次同步是否存在需要用户处理的事项 + bool get hasConflicts => conflicts.isNotEmpty; + + @override + List get props => [newItemsFromServer, conflicts]; +} diff --git a/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart b/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart index 4f29afb..8f07718 100644 --- a/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart +++ b/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart @@ -1,6 +1,7 @@ import 'package:problem_check_system/app/core/models/sync_status.dart'; import 'package:problem_check_system/app/core/repositories/syncable_repository.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/entities/sync_result.dart'; import '../entities/enterprise.dart'; @@ -23,4 +24,9 @@ abstract class EnterpriseRepository implements SyncableRepository { Future syncEnterpriseToServer(Enterprise enterprise) async {} Future updateEnterpriseSyncStatus(String id, SyncStatus synced) async {} + + // [新增] 执行同步并返回结果 + Future syncWithServer(); + // [新增] 用户选择后,用一个版本覆盖另一个版本 + Future resolveConflictAndUpdate(Enterprise chosenEnterprise); } diff --git a/lib/app/features/enterprise/domain/usecases/editor_enterprise_usecase.dart b/lib/app/features/enterprise/domain/usecases/editor_enterprise_usecase.dart index 9bff134..b6c8484 100644 --- a/lib/app/features/enterprise/domain/usecases/editor_enterprise_usecase.dart +++ b/lib/app/features/enterprise/domain/usecases/editor_enterprise_usecase.dart @@ -16,7 +16,7 @@ class EditEnterpriseUsecase { Future call(Enterprise updatedEnterprise) async { // 1. 业务规则:更新“最后修改时间” final enterpriseToSave = updatedEnterprise.copyWith( - lastModifiedTime: DateTime.now(), + lastModifiedTime: DateTime.now().toUtc(), lastModifierId: authRepository.getUserId(), // 2. 业务规则:将同步状态标记为“待更新” // 我们需要先判断它原始的状态,避免覆盖“待创建” diff --git a/lib/app/features/enterprise/domain/usecases/resolve_conflict_usecase.dart b/lib/app/features/enterprise/domain/usecases/resolve_conflict_usecase.dart new file mode 100644 index 0000000..b970afb --- /dev/null +++ b/lib/app/features/enterprise/domain/usecases/resolve_conflict_usecase.dart @@ -0,0 +1,12 @@ +import '../entities/enterprise.dart'; +import '../repositories/enterprise_repository.dart'; + +class ResolveConflictUsecase { + final EnterpriseRepository repository; + + ResolveConflictUsecase({required this.repository}); + + Future call(Enterprise chosenEnterprise) async { + return repository.resolveConflictAndUpdate(chosenEnterprise); + } +} diff --git a/lib/app/features/enterprise/domain/usecases/sync_enterprises_usecase.dart b/lib/app/features/enterprise/domain/usecases/sync_enterprises_usecase.dart new file mode 100644 index 0000000..17079e9 --- /dev/null +++ b/lib/app/features/enterprise/domain/usecases/sync_enterprises_usecase.dart @@ -0,0 +1,12 @@ +import '../entities/sync_result.dart'; +import '../repositories/enterprise_repository.dart'; + +class SyncEnterprisesUsecase { + final EnterpriseRepository repository; + + SyncEnterprisesUsecase({required this.repository}); + + Future call() async { + return repository.syncWithServer(); + } +} diff --git a/lib/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart b/lib/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart index df0952b..5654708 100644 --- a/lib/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart @@ -22,6 +22,8 @@ class EnterpriseFormBinding extends Bindings { (() => EnterpriseRepositoryImpl( localDataSource: Get.find(), remoteDataSource: Get.find(), + networkStatusService: Get.find(), + uuid: Get.find(), )), ); Get.lazyPut( diff --git a/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart b/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart index 8900e2d..4030a9a 100644 --- a/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart @@ -6,6 +6,8 @@ import 'package:problem_check_system/app/features/enterprise/data/datasources/en import 'package:problem_check_system/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart'; import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/get_enterprise_list_usecase.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/usecases/resolve_conflict_usecase.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/usecases/sync_enterprises_usecase.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart'; class EnterpriseListBinding extends Bindings { @@ -23,14 +25,25 @@ class EnterpriseListBinding extends Bindings { EnterpriseRepositoryImpl( localDataSource: Get.find(), remoteDataSource: Get.find(), + networkStatusService: Get.find(), + uuid: Get.find(), ), ); + Get.put( + SyncEnterprisesUsecase(repository: Get.find()), + ); + Get.put( + ResolveConflictUsecase(repository: Get.find()), + ); Get.put( GetEnterpriseListUsecase(repository: Get.find()), ); + Get.lazyPut( () => EnterpriseListController( getEnterpriseListUsecase: Get.find(), + syncEnterprisesUsecase: Get.find(), + resolveConflictUsecase: Get.find(), ), ); } diff --git a/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart b/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart index 6092c9c..53d262f 100644 --- a/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart @@ -25,6 +25,8 @@ class EnterpriseUploadBinding extends Bindings { EnterpriseRepositoryImpl( localDataSource: Get.find(), remoteDataSource: Get.find(), + networkStatusService: Get.find(), + uuid: Get.find(), ), ); diff --git a/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart b/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart index 705a788..326662c 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart @@ -2,11 +2,15 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/extensions/datetime_extension.dart'; import 'package:problem_check_system/app/core/models/company_enum.dart'; import 'package:problem_check_system/app/core/routes/app_routes.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_conflict.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/get_enterprise_list_usecase.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/usecases/resolve_conflict_usecase.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/usecases/sync_enterprises_usecase.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_form_controller.dart'; /// ----------------------------------------------------------------------------- @@ -23,12 +27,20 @@ import 'package:problem_check_system/app/features/enterprise/presentation/contro /// class EnterpriseListController extends GetxController { final GetEnterpriseListUsecase getEnterpriseListUsecase; + final SyncEnterprisesUsecase syncEnterprisesUsecase; // 新增 + final ResolveConflictUsecase resolveConflictUsecase; // 新增 - EnterpriseListController({required this.getEnterpriseListUsecase}); + EnterpriseListController({ + required this.getEnterpriseListUsecase, + required this.syncEnterprisesUsecase, + required this.resolveConflictUsecase, + }); // --- 实现基类中定义的属性 --- final enterpriseList = [].obs; final isLoading = false.obs; + final isSyncing = false.obs; + final nameController = TextEditingController(); final selectedType = Rx(null); final startDate = Rx(null); @@ -38,8 +50,9 @@ class EnterpriseListController extends GetxController { @override void onInit() { + // 页面初始化时,启动完整的“同步-再加载”流程 + loadAndSyncEnterprises(); super.onInit(); - fetchEnterprises(); // 页面初始化时加载数据 } @override @@ -50,26 +63,30 @@ class EnterpriseListController extends GetxController { } // --- 实现基类中定义的方法 --- + // 核心流程方法 + Future loadAndSyncEnterprises() async { + try { + isLoading(true); + isSyncing(true); - void search() { - fetchEnterprises(); - } + // 步骤 1: 执行同步 + final syncResult = await syncEnterprisesUsecase(); - void clearFilters() { - nameController.clear(); - selectedType.value = null; - startDate.value = null; - endDate.value = null; - fetchEnterprises(); - } + // 步骤 2: 处理冲突 + if (syncResult.hasConflicts) { + // 如果有冲突,则逐个弹窗让用户选择 + for (final conflict in syncResult.conflicts) { + final chosenVersion = await _showConflictDialog(conflict); + if (chosenVersion != null) { + // 用户做出了选择,更新本地数据 + await resolveConflictUsecase(chosenVersion); + } + } + } - // --- EnterpriseListController 特有的方法 --- + isSyncing(false); - /// 核心方法:获取企业列表 - Future fetchEnterprises() async { - expansibleController.collapse(); - isLoading.value = true; - try { + // 步骤 3: 所有同步和冲突解决完毕后,从本地加载最终数据 final result = await getEnterpriseListUsecase.call( name: nameController.text, type: selectedType.value?.displayText, @@ -78,12 +95,71 @@ class EnterpriseListController extends GetxController { ); enterpriseList.assignAll(result); } catch (e) { - Get.snackbar('错误', '加载企业列表失败: $e'); + Get.snackbar('错误', '操作失败: $e'); } finally { - isLoading.value = false; + isLoading(false); + isSyncing(false); } } + // 弹出冲突选择对话框的辅助方法 + Future _showConflictDialog(EnterpriseConflict conflict) { + return Get.dialog( + AlertDialog( + title: Text('数据冲突: ${conflict.localVersion.name}'), + + // [修改 1] 将所有内容(包括按钮)都放在 content 中 + content: Column( + // mainAxisSize.min 让 Column 的高度自适应内容,防止它无限高 + mainAxisSize: MainAxisSize.min, + // crossAxisAlignment.stretch 让所有子元素水平撑满 + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('服务器上的数据与本地数据不一致,请选择要保留的版本。'), + const SizedBox(height: 24), // 在文本和按钮之间添加一些间距 + // 第一个按钮 + ElevatedButton( + child: Text( + '使用客户端版本\n(修改于: ${conflict.localVersion.lastModifiedTime.toUtc().toDateTimeString2()})', + textAlign: TextAlign.center, + ), + onPressed: () => Get.back(result: conflict.localVersion), + ), + const SizedBox(height: 8), + + // 第二个按钮 + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Get.theme.colorScheme.primary, + foregroundColor: Get.theme.colorScheme.onPrimary, + ), + child: Text( + '使用服务器版本\n(修改于: ${conflict.serverVersion.lastModifiedTime.toUtc().toDateTimeString2()})', + textAlign: TextAlign.center, + ), + onPressed: () => Get.back(result: conflict.serverVersion), + ), + ], + ), + + // [修改 2] 不再需要 actions 属性 + // actions: [ ... ], + ), + ); + } + + void search() { + loadAndSyncEnterprises(); + } + + void clearFilters() { + nameController.clear(); + selectedType.value = null; + startDate.value = null; + endDate.value = null; + loadAndSyncEnterprises(); + } + /// 导航到编辑表单页面 Future navigateToEditForm(Enterprise enterprise) async { final result = await Get.toNamed(