From ea8958e186967c815d6214e60463441d531a5e53 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 14:58:19 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E4=B8=8A=E4=BC=A0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/controllers/i_upload_controller.dart | 34 ++++ .../pages/widgets/upload_progress_dialog.dart | 8 +- .../enterprise_local_data_source.dart | 26 ++++ .../enterprise_repository_impl.dart | 29 +++- .../repositories/enterprise_repository.dart | 4 +- .../usecases/upload_enterprises_usecase.dart | 13 +- .../enterprise_upload_controller.dart | 15 +- .../problem_local_data_source.dart | 16 +- .../problem_remote_data_source.dart | 118 +++++++++++++- .../problem/data/model/problem_dto.dart | 145 +++++++++--------- .../data/model/problem_filter_dto.dart | 3 + .../repositories/problem_repository_impl.dart | 70 ++++++--- .../entities/problem_filter_params.dart | 6 +- .../repositories/problem_repository.dart | 2 +- .../domain/usecases/add_problem_usecase.dart | 2 +- .../domain/usecases/delete_problem.dart | 2 +- .../usecases/get_all_problems_usecase.dart | 2 +- .../usecases/get_problem_by_id_usecase.dart | 2 +- .../usecases/update_problem_usecase.dart | 10 +- .../usecases/upload_problems_usecase.dart | 131 ++++++++++++---- .../bindings/problem_form_binding.dart | 2 +- .../bindings/problem_list_binding.dart | 51 ++++-- .../bindings/problem_upload_binding.dart | 55 ++++++- .../controllers/problem_form_controller.dart | 43 ++++-- .../problem_upload_controller.dart | 58 ++++--- .../pages/problem_upload_page.dart | 24 +-- 26 files changed, 620 insertions(+), 251 deletions(-) create mode 100644 lib/app/core/controllers/i_upload_controller.dart rename lib/app/{features/enterprise/presentation => core}/pages/widgets/upload_progress_dialog.dart (88%) create mode 100644 lib/app/features/problem/data/model/problem_filter_dto.dart diff --git a/lib/app/core/controllers/i_upload_controller.dart b/lib/app/core/controllers/i_upload_controller.dart new file mode 100644 index 0000000..7a5718c --- /dev/null +++ b/lib/app/core/controllers/i_upload_controller.dart @@ -0,0 +1,34 @@ +import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/domain/entities/upload_result.dart'; + +/// 上传控制器接口 +/// +/// 定义了所有上传流程控制器必须提供的通用属性和方法。 +/// 这使得 UI 组件 (如 UploadProgressDialog) 可以与任何实现了此接口的控制器协作, +/// 而无需知道其具体类型(是企业上传还是问题上传)。 +abstract class IUploadController { + // --- 状态属性 (State Properties) --- + + /// 是否正在上传 + RxBool get isUploading; + + /// 上传进度 (0.0 to 1.0) + RxDouble get uploadProgress; + + /// 已上传的数量 + RxInt get uploadedCount; + + /// 总共需要上传的数量 + RxInt get totalToUpload; + + /// 上传结果 + Rx get uploadResult; + + // --- 操作方法 (Action Methods) --- + + /// 取消上传 + void cancelUpload(); + + /// 关闭上传对话框 (通常在上传完成后调用) + void closeUploadDialog(); +} diff --git a/lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart b/lib/app/core/pages/widgets/upload_progress_dialog.dart similarity index 88% rename from lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart rename to lib/app/core/pages/widgets/upload_progress_dialog.dart index 7d74904..ab7471b 100644 --- a/lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart +++ b/lib/app/core/pages/widgets/upload_progress_dialog.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/controllers/i_upload_controller.dart'; import 'package:problem_check_system/app/core/domain/entities/upload_result.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart'; -import 'package:problem_check_system/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart'; -class UploadProgressDialog extends GetView { - const UploadProgressDialog({super.key}); +class UploadProgressDialog extends StatelessWidget { + final IUploadController controller; + const UploadProgressDialog({super.key, required this.controller}); @override Widget build(BuildContext context) { 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 dff25d9..1aa8306 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 @@ -23,6 +23,10 @@ abstract class EnterpriseLocalDataSource { // [新增] 更新或插入单个企业 Future upsertEnterprise(EnterpriseModel enterprise); Future cacheEnterprises(List enterprises); + + Future getEnterpriseById(String enterpriseId); + + Future deleteEnterprise(String id) async {} } class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource { @@ -210,4 +214,26 @@ class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource { conflictAlgorithm: ConflictAlgorithm.replace, // replace 会覆盖已有记录 ); } + + @override + Future getEnterpriseById(String enterpriseId) async { + final db = await _databaseService.database; + final List> maps = await db.query( + _tableName, + where: 'id = ?', + whereArgs: [enterpriseId], + limit: 1, // 限制只返回一条记录 + ); + + if (maps.isNotEmpty) { + return maps.first; + } + return null; + } + + @override + Future deleteEnterprise(String id) { + //TODO 添加删除逻辑 + throw UnimplementedError(); + } } 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 882291b..a36456e 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 @@ -98,14 +98,23 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository { syncedDto = await remoteDataSource.updateEnterprise(enterpriseModel); break; case SyncStatus.pendingDelete: + // 远程删除 await remoteDataSource.deleteEnterprise(enterprise.id); - return enterprise; - // 对于其他状态,此方法不执行任何网络操作 + // 远程成功后,立即从本地删除 + await localDataSource.deleteEnterprise(enterprise.id); + // 返回一个更新了状态的实体 + return enterprise.copyWith(syncStatus: SyncStatus.synced); case SyncStatus.synced: case SyncStatus.untracked: return enterprise; } - return syncedDto.toModel().toEntity(); + // [核心修复] 将从服务器返回的最新 DTO 转换为本地 Model,并更新回数据库 + // 这确保了服务器生成的ID、时间戳和'synced'状态被持久化 + final syncedModel = syncedDto.toModel(); // 假设有这个转换 + await localDataSource.updateEnterprise(syncedModel); + + // 最后,将最新的 Model 转换回纯净的 Entity 并返回 + return syncedModel.toEntity(); } @override @@ -192,4 +201,18 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository { final enterprises = models.map((model) => model.toEntity()).toList(); return enterprises; } + + @override + Future getEnterpriseById(String enterpriseId) async { + // 1. 调用数据源通过 ID 获取问题的原始数据 (Map) + final problemMap = await localDataSource.getEnterpriseById(enterpriseId); + + // 2. 如果数据不存在,则返回 null + if (problemMap == null) { + return null; + } + + // 3. 如果数据存在,则先转换为 DTO,再转换为领域实体并返回 + return EnterpriseModel.fromMap(problemMap).toEntity(); + } } diff --git a/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart b/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart index 92cd1ef..009d083 100644 --- a/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart +++ b/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart @@ -23,7 +23,7 @@ abstract class EnterpriseRepository implements SyncableRepository { Future syncEnterpriseToServer(Enterprise enterprise); - Future updateEnterpriseSyncStatus(String id, SyncStatus synced) async {} + Future updateEnterpriseSyncStatus(String id, SyncStatus synced); // [新增] 执行同步并返回结果 Future syncWithServer(); @@ -31,4 +31,6 @@ abstract class EnterpriseRepository implements SyncableRepository { Future resolveConflictAndUpdate(Enterprise chosenEnterprise); // 获取所有企业信息 Future> getAllEnterprises(); + // 根据企业ID获取企业信息 + Future getEnterpriseById(String enterpriseId); } diff --git a/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart b/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart index ee719f4..77b958c 100644 --- a/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart +++ b/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart @@ -20,7 +20,7 @@ class UploadEnterprisesUseCase { } /// 执行上传操作 - /// todo 正确的架构模式:请求-确认-更新 (Request-Acknowledge-Update) 需要更新本地的最后修改时间为服务器的返回数据 + /// TODO 您应该将“同步到服务器后更新本地数据库”的逻辑放在 Repository 中。参考最近的问题同步用例 /// [onProgress] 是一个回调函数,用于将进度实时报告给调用者 (Controller) Future call({ required List enterprisesToUpload, @@ -45,16 +45,7 @@ class UploadEnterprisesUseCase { final enterprise = enterprisesToUpload[i].enterprise; try { - // 1. 调用通用的同步方法 - final syncedEnterprise = await repository.syncEnterpriseToServer( - enterprise, - ); - // 2. 同步成功后,更新本地状态 - if (enterprise.syncStatus == SyncStatus.pendingDelete) { - // repository.deleteLocalEnterprise(enterprise.id); - } else { - await repository.updateEnterprise(syncedEnterprise); - } + await repository.syncEnterpriseToServer(enterprise); successCount++; } catch (e) { // 记录失败,但继续上传下一个 diff --git a/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart b/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart index 4f3b9b8..6591753 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart @@ -1,13 +1,15 @@ // lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/controllers/i_upload_controller.dart'; import 'package:problem_check_system/app/core/domain/entities/upload_result.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/upload_enterprises_usecase.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart'; +import 'package:problem_check_system/app/core/pages/widgets/upload_progress_dialog.dart'; -class EnterpriseUploadController extends GetxController { +class EnterpriseUploadController extends GetxController + implements IUploadController { final GetEnterpriseListUsecase getEnterpriseListUsecase; final UploadEnterprisesUseCase uploadEnterprisesUseCase; @@ -21,10 +23,15 @@ class EnterpriseUploadController extends GetxController { final enterprises = [].obs; final selectedEnterprises = {}.obs; // --- [新增] 上传状态管理的属性 --- + @override final isUploading = false.obs; + @override final uploadProgress = 0.0.obs; // 进度,0.0 到 1.0 + @override final uploadedCount = 0.obs; + @override final totalToUpload = 0.obs; + @override final uploadResult = Rxn(); // 用于存储上传结果 bool get allSelected => @@ -74,7 +81,7 @@ class EnterpriseUploadController extends GetxController { } // 1. 显示上传对话框 Get.dialog( - const UploadProgressDialog(), + UploadProgressDialog(controller: this), barrierDismissible: false, // 禁止点击外部关闭 ); // 2. 开始执行上传 @@ -106,11 +113,13 @@ class EnterpriseUploadController extends GetxController { } /// [新增] 取消上传 + @override void cancelUpload() { uploadEnterprisesUseCase.cancel(); } /// [新增] 关闭对话框并处理后续事宜 + @override void closeUploadDialog() { Get.back(); // 关闭对话框 // 如果上传不是被取消的,并且有成功项,则认为操作成功,返回 true 通知列表刷新 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 8f38733..63423a3 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,4 @@ -import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; // 导入 SyncStatus +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/domain/entities/problem_filter_params.dart'; import 'package:sqflite/sqflite.dart'; // 导入你的 DatabaseService @@ -7,7 +6,7 @@ import 'package:sqflite/sqflite.dart'; // 导入你的 DatabaseService /// IProblemLocalDataSource 定义了问题本地数据源的接口。 /// 它抽象了所有与本地数据库中 'problems' 表相关的 CRUD (创建, 读取, 更新, 删除) 操作。 /// 仓库层 (Repository) 将通过这个接口与数据源交互,而无需关心底层的数据库实现 (如 Sqflite)。 -abstract class IProblemLocalDataSource { +abstract class ProblemLocalDataSource { /// 根据 ID 从数据库获取一个问题。 /// /// [id] - 要查询的问题的唯一标识符。 @@ -40,11 +39,11 @@ abstract class IProblemLocalDataSource { // 假设 IProblemLocalDataSource 接口定义在同一个文件中或已导入 -class ProblemLocalDataSource implements IProblemLocalDataSource { +class ProblemLocalDataSourceImpl implements ProblemLocalDataSource { final DatabaseService _databaseService; final String _tableName = 'problems'; - ProblemLocalDataSource({required DatabaseService databaseService}) + ProblemLocalDataSourceImpl({required DatabaseService databaseService}) : _databaseService = databaseService; @override @@ -102,10 +101,11 @@ class ProblemLocalDataSource implements IProblemLocalDataSource { } // 4. 同步状态筛选 - if (filter.syncStatus != null) { - whereClauses.add('p.syncStatus = ?'); + if (filter.isUploaded != null) { + final operator = filter.isUploaded! ? '=' : '!='; + whereClauses.add('p.syncStatus $operator ?'); // 假设 SyncStatus 枚举在数据库中存储为字符串 - arguments.add(filter.syncStatus!.name); + arguments.add(SyncStatus.synced.name); } // 5. 绑定状态筛选 diff --git a/lib/app/features/problem/data/datasources/problem_remote_data_source.dart b/lib/app/features/problem/data/datasources/problem_remote_data_source.dart index 5aa6760..fb87b0b 100644 --- a/lib/app/features/problem/data/datasources/problem_remote_data_source.dart +++ b/lib/app/features/problem/data/datasources/problem_remote_data_source.dart @@ -1 +1,117 @@ -class ProblemRemoteDataSource {} +import 'package:dio/dio.dart'; +import 'package:problem_check_system/app/core/services/http_provider.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'; + +/// 问题远程数据源接口 +/// +/// 定义了与问题相关的远程 API 的所有操作。 +/// 这个接口处理的是 DTO (Data Transfer Objects),与服务器直接交互。 +abstract class ProblemRemoteDataSource { + // --- 查 (Read/Query) --- + + /// 根据筛选条件获取问题列表 + /// + /// [filter]: 包含所有筛选条件的 DTO + /// 返回一个 ProblemDto 列表的 Future + Future> getProblems(ProblemFilterDto filter); + + /// 根据 ID 获取单个问题的详细信息 + /// + /// [problemId]: 问题的唯一标识符 + /// 如果找到,返回一个 ProblemDto 的 Future;否则可能抛出异常 + Future getProblemById(String problemId); + + // --- 增 (Create) --- + + /// 创建一个新问题 + /// + /// [problem]: 包含新问题信息的 DTO + /// 服务器通常会返回创建成功后的完整问题对象(包含服务器生成的ID和时间戳) + Future createProblem(ProblemDto problem); + + // --- 改 (Update) --- + + /// 更新一个已存在的问题 + /// + /// [problem]: 包含要更新的问题信息的 DTO + /// 服务器通常会返回更新成功后的完整问题对象 + Future updateProblem(ProblemDto problem); + + // --- 删 (Delete) --- + + /// 根据 ID 删除一个问题 + /// + /// [problemId]: 要删除的问题的唯一标识符 + /// 删除操作通常成功后没有返回内容,所以使用 Future + Future deleteProblem(String problemId); +} + +class ProblemRemoteDataSourceImpl implements ProblemRemoteDataSource { + final HttpProvider http; + static const String problemsEndpoint = '/api/Memorandum'; + // 通过依赖注入传入 Dio 实例 + ProblemRemoteDataSourceImpl({required this.http}); + + @override + Future> getProblems(ProblemFilterDto filter) async { + try { + final response = await http.get( + problemsEndpoint, // 假设这是获取列表的端点 + queryParameters: filter.toJson(), + ); + // 假设服务器返回的数据在 'data' 字段中,且是一个列表 + final List problemListJson = response.data['data']; + return problemListJson.map((json) => ProblemDto.fromJson(json)).toList(); + } on DioException catch (e) { + // 在这里处理网络错误,可以抛出一个自定义的异常 + throw Exception('获取问题列表失败: $e'); + } + } + + @override + Future getProblemById(String problemId) async { + try { + final response = await http.get('$problemsEndpoint/$problemId'); + return ProblemDto.fromJson(response.data); + } on DioException catch (e) { + throw Exception('获取问题失败: $e'); + } + } + + @override + Future createProblem(ProblemDto problem) async { + try { + final response = await http.post( + problemsEndpoint, + data: problem.toJson(), // 将 DTO 转换为 Map 发送 + ); + return ProblemDto.fromJson(response.data); + } on DioException catch (e) { + throw Exception('创建问题失败: $e'); + } + } + + @override + Future updateProblem(ProblemDto problem) async { + try { + final response = await http.put( + '$problemsEndpoint/${problem.id}', // 通常 PUT 请求需要 ID 在 URL 中 + data: problem.toJson(), + ); + return ProblemDto.fromJson(response.data); + } on DioException catch (e) { + throw Exception('更新问题失败: $e'); + } + } + + @override + Future deleteProblem(String problemId) async { + try { + // 删除成功通常返回 204 No Content,Dio 会默认处理 + await http.delete('$problemsEndpoint/$problemId'); + } on DioException catch (e) { + throw Exception('删除问题失败: $e'); + } + } +} diff --git a/lib/app/features/problem/data/model/problem_dto.dart b/lib/app/features/problem/data/model/problem_dto.dart index 4a9a228..7c1667b 100644 --- a/lib/app/features/problem/data/model/problem_dto.dart +++ b/lib/app/features/problem/data/model/problem_dto.dart @@ -1,115 +1,108 @@ import 'dart:convert'; - -import 'package:problem_check_system/app/core/extensions/map_extensions.dart'; import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; -import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_model.dart'; -import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; +import 'package:problem_check_system/app/core/extensions/map_extensions.dart'; +import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; -/// ProblemDto (Data Transfer Object) -/// -/// 这个模型专门用于与远程 API 进行数据交互。 -/// 它的字段和结构【严格匹配】服务器 API 定义的 JSON 格式。 class ProblemDto { - final String? id; + final String id; final String? title; final String? location; final String? censorTaskId; final String? rowId; final String? bindData; - final List? imageUrls; - final String? creationTime; + final List? imageUrls; + final String creatorId; + final String creationTime; + final String? lastModifierId; + final String? lastModificationTime; final String? companyId; ProblemDto({ - this.id, + required this.id, this.title, this.location, this.censorTaskId, this.rowId, this.bindData, this.imageUrls, - this.creationTime, + required this.creatorId, + required this.creationTime, + this.lastModifierId, + this.lastModificationTime, this.companyId, }); - - factory ProblemDto.fromModel(ProblemModel model) { + factory ProblemDto.fromEntity(ProblemEntity entity) { return ProblemDto( - id: model.id, - title: model.description, - location: model.location, - bindData: model.bindData, - imageUrls: List.from(jsonDecode(model.imageUrls)), - creationTime: model.creationTime, - companyId: model.enterpriseId, + id: entity.id, + title: entity.description, + location: entity.location, + bindData: entity.bindData, + imageUrls: entity.imageUrls, + creationTime: entity.creationTime.toIso8601String(), + creatorId: entity.creatorId, + companyId: entity.enterpriseId, ); } - ProblemModel toModel() { - return ProblemModel( - id: + ProblemEntity toEntity() { + return ProblemEntity( + id: id, + description: title ?? '', + location: location ?? '', + imageUrls: imageUrls ?? [], + enterpriseId: companyId ?? '', + creatorId: creatorId, + creationTime: DateTime.tryParse(creationTime) ?? DateTime.now(), + lastModifierId: lastModifierId ?? creatorId, + lastModifiedTime: + DateTime.tryParse(lastModificationTime ?? '') ?? DateTime.now(), + syncStatus: SyncStatus.synced, + bindData: bindData, ); } - // ======================================================================= - // 新增方法 - // ======================================================================= - - /// [新增] fromJson 工厂构造函数:从 JSON Map 创建 EnterpriseDto 实例。 - /// - /// 当从服务器接收到 JSON 数据时,调用此方法将其转换为 Dart 对象。 + // ✅ FIX: fromJson - 对所有字段进行安全转换 factory ProblemDto.fromJson(Map json) { - final creationTime = DateTime.parse(json['creationTime'] as String); - final creatorId = json['creatorId'] as String; - final lastModTimeStr = json['lastModificationTime'] as String?; - return ProblemDto( - // 必须存在的字段 - id: json['id'] 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? ?? "生产", + final creatorIdData = json['creatorId'] as String? ?? ''; + final creationTimeData = json['creationTime'] as String? ?? ''; - // 可选字段 - companyScope: json['companyScope'] as String?, - mainPrincipalName: json['mainPrincipalName'] as String?, - mainPrincipalPhone: json['mainPrincipalPhone'] as String?, - securityPrincipalName: json['securityPrincipalName'] as String?, - securityPrincipalPhone: json['securityPrincipalPhone'] as String?, - companyAddress: json['companyAddress'] as String?, - majorHazard: json['majorHazard'] as String?, + return ProblemDto( + id: json['id'] as String? ?? '', + creationTime: creationTimeData, + creatorId: creatorIdData, + lastModificationTime: + json['lastModificationTime'] as String? ?? creationTimeData, + lastModifierId: json['lastModifierId'] as String? ?? creatorIdData, + title: json['title'] as String?, + location: json['location'] as String?, + bindData: json['bindData'] as String?, + imageUrls: (json['imageUrls'] as List? ?? []) + .whereType() + .toList(), + companyId: json['companyId'] as String?, + censorTaskId: json['censorTaskId'] as String?, + rowId: json['rowId'] as String?, ); } - /// [新增] toJson 方法:将 EnterpriseDto 实例转换为 JSON Map。 - /// - /// 当需要将数据发送到服务器时,调用此方法将其转换为 JSON 格式。 + // 🟡 提醒: 请确认是否需要包含所有字段 Map toJson() { final jsonMap = { - 'id': id, - // 使用 toIso8601String() 是将 DateTime 转换为标准化字符串的最佳实践 - 'creationTime': creationTime.toIso8601String(), - 'creatorId': creatorId, - 'lastModificationTime': lastModificationTime?.toIso8601String(), - 'lastModifierId': lastModifierId, - 'companyName': companyName, - 'companyType': companyType, - 'companyScope': companyScope, - 'mainPrincipalName': mainPrincipalName, - 'mainPrincipalPhone': mainPrincipalPhone, - 'securityPrincipalName': securityPrincipalName, - 'securityPrincipalPhone': securityPrincipalPhone, - 'companyAddress': companyAddress, - 'majorHazard': majorHazard, + // 建议包含所有需要发送给服务器的字段 + "id": id, + "title": title, + "location": location, + "bindData": bindData, + "imageUrls": imageUrls, + "creatorId": creatorId, + "creationTime": creationTime, + "companyId": companyId, + "censorTaskId": censorTaskId, + "rowId": rowId, + "lastModifierId": lastModifierId, + "lastModificationTime": lastModificationTime, }; return jsonMap.withoutNullOrEmptyValues; } - - /// [核心] 将 DTO (网络数据) 转换为 Model (本地/业务模型)。 - /// - /// 这个转换是数据流入应用内部的关键步骤。 } diff --git a/lib/app/features/problem/data/model/problem_filter_dto.dart b/lib/app/features/problem/data/model/problem_filter_dto.dart new file mode 100644 index 0000000..83bb05e --- /dev/null +++ b/lib/app/features/problem/data/model/problem_filter_dto.dart @@ -0,0 +1,3 @@ +class ProblemFilterDto { + toJson() {} +} 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 c6a7fb0..47ecc44 100644 --- a/lib/app/features/problem/data/repositories/problem_repository_impl.dart +++ b/lib/app/features/problem/data/repositories/problem_repository_impl.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:problem_check_system/app/core/domain/entities/sync_status.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_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'; @@ -10,18 +12,22 @@ import 'package:problem_check_system/app/features/problem/domain/repositories/pr /// 问题仓库,负责处理问题数据的本地持久化。 /// 它封装了底层数据库操作,为业务逻辑层提供一个简洁的接口。 /// 它的核心工作是在领域实体 (ProblemEntity) 和数据传输对象 (ProblemModel) 之间进行转换。 -class ProblemRepository implements IProblemRepository { - final IProblemLocalDataSource problemLocalDataSource; // 2. 依赖于数据源的抽象 +class ProblemRepositoryImpl implements ProblemRepository { + final ProblemLocalDataSource localDataSource; + final ProblemRemoteDataSource remoteDataSource; - ProblemRepository(this.problemLocalDataSource); + ProblemRepositoryImpl({ + required this.localDataSource, + required this.remoteDataSource, + }); @override Future addProblem(ProblemEntity problem) async { // 1. 将领域实体 (Entity) 转换为本地数据传输对象 (DTO) - final problemDto = ProblemModel.fromEntity(problem); + final problemModel = ProblemModel.fromEntity(problem); // 2. 调用数据源的方法,将 DTO 转换为 Map 进行存储 - await problemLocalDataSource.addProblem(problemDto.toMap()); + await localDataSource.addProblem(problemModel.toMap()); // 3. 操作成功后,返回传入的实体,确认操作完成 return problem; @@ -30,14 +36,14 @@ class ProblemRepository implements IProblemRepository { @override Future deleteProblem(String id) async { // 直接将 ID 传递给数据源进行删除操作 - await problemLocalDataSource.deleteProblem(id); + await localDataSource.deleteProblem(id); } @override Future> getAllProblemListItem({ required ProblemFilterParams filter, }) async { - final problemDataMaps = await problemLocalDataSource.getAllProblems(filter); + final problemDataMaps = await localDataSource.getAllProblems(filter); return problemDataMaps.map((map) { // 步骤 1: 从 Map 中解析并构建完整的 ProblemEntity @@ -94,7 +100,7 @@ class ProblemRepository implements IProblemRepository { @override Future getProblemById(String id) async { // 1. 调用数据源通过 ID 获取问题的原始数据 (Map) - final problemMap = await problemLocalDataSource.getProblemById(id); + final problemMap = await localDataSource.getProblemById(id); // 2. 如果数据不存在,则返回 null if (problemMap == null) { @@ -108,37 +114,51 @@ class ProblemRepository implements IProblemRepository { @override Future updateProblem(ProblemEntity problem) async { // 1. 将领域实体 (Entity) 转换为本地数据传输对象 (DTO) - final problemDto = ProblemModel.fromEntity(problem); + final problemModel = ProblemModel.fromEntity(problem); // 2. 调用数据源的方法,将 DTO 转换为 Map 进行更新 - await problemLocalDataSource.updateProblem(problemDto.toMap()); + await localDataSource.updateProblem(problemModel.toMap()); // 3. 操作成功后,返回传入的实体,确认操作完成 return problem; } - // TODO: 上传问题时必须检查企业是否已经上传 + @override - Future syncProblemToServer(ProblemEntity problem) { - // 将 Domain 层的纯净实体转换为 Data 层的 Model - final problemModel = ProblemModel.fromEntity(problem); - EnterpriseDto syncedDto; - // **核心调度逻辑** - // Repository 像一个指挥官,根据情况向不同的士兵(DataSource)下达命令 - switch (enterprise.syncStatus) { + Future syncProblemToServer(ProblemEntity problem) async { + // 将 Entity 转换为 DTO,准备与服务器通信 + final problemDto = ProblemDto.fromEntity(problem); + ProblemDto syncedDto; + + switch (problem.syncStatus) { case SyncStatus.pendingCreate: - syncedDto = await remoteDataSource.createEnterprise(enterpriseModel); + syncedDto = await remoteDataSource.createProblem(problemDto); break; case SyncStatus.pendingUpdate: - syncedDto = await remoteDataSource.updateEnterprise(enterpriseModel); + syncedDto = await remoteDataSource.updateProblem(problemDto); break; case SyncStatus.pendingDelete: - await remoteDataSource.deleteEnterprise(enterprise.id); - return enterprise; - // 对于其他状态,此方法不执行任何网络操作 + // 先从远程删除 + await remoteDataSource.deleteProblem(problemDto.id); + // 远程删除成功后,必须从本地也删除 + await localDataSource.deleteProblem(problem.id); + // 返回一个更新了状态的实体,表示操作完成 + return problem.copyWith(syncStatus: SyncStatus.synced); + case SyncStatus.synced: case SyncStatus.untracked: - return enterprise; + return problem; } - return syncedDto.toModel().toEntity(); + + // 将从服务器返回的最新 DTO 转换回 Entity + // 这个 syncedEntity 现在包含了服务器生成的ID、时间戳等信息 + final syncedEntity = syncedDto.toEntity(); + + // 将这个最新的、已同步的 Entity 转换为 Model,并更新回本地数据库 + // 这是确保本地状态与服务器一致的核心步骤! + final modelToSave = ProblemModel.fromEntity(syncedEntity); + await localDataSource.updateProblem(modelToSave.toMap()); + + // 返回最终从服务器同步回来的、纯净的 Entity + return syncedEntity; } } diff --git a/lib/app/features/problem/domain/entities/problem_filter_params.dart b/lib/app/features/problem/domain/entities/problem_filter_params.dart index c007807..2decbfc 100644 --- a/lib/app/features/problem/domain/entities/problem_filter_params.dart +++ b/lib/app/features/problem/domain/entities/problem_filter_params.dart @@ -1,5 +1,3 @@ -import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; - /// 封装问题列表的所有筛选条件 class ProblemFilterParams { /// 企业名称 (用于模糊查询) @@ -12,7 +10,7 @@ class ProblemFilterParams { final DateTime? endTime; /// 上传状态 (同步状态) - final SyncStatus? syncStatus; + final bool? isUploaded; /// 绑定状态 (true: 已绑定, false: 未绑定, null: 全部) final bool? isBound; @@ -21,7 +19,7 @@ class ProblemFilterParams { this.enterpriseName, this.startTime, this.endTime, - this.syncStatus, + this.isUploaded, this.isBound, }); diff --git a/lib/app/features/problem/domain/repositories/problem_repository.dart b/lib/app/features/problem/domain/repositories/problem_repository.dart index e2b7c86..481e723 100644 --- a/lib/app/features/problem/domain/repositories/problem_repository.dart +++ b/lib/app/features/problem/domain/repositories/problem_repository.dart @@ -4,7 +4,7 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble /// Problem 仓库的抽象接口 /// 定义了业务逻辑层需要的数据操作。 -abstract class IProblemRepository { +abstract class ProblemRepository { Future> getAllProblemListItem({ required ProblemFilterParams filter, }); diff --git a/lib/app/features/problem/domain/usecases/add_problem_usecase.dart b/lib/app/features/problem/domain/usecases/add_problem_usecase.dart index 9f1daf1..a018505 100644 --- a/lib/app/features/problem/domain/usecases/add_problem_usecase.dart +++ b/lib/app/features/problem/domain/usecases/add_problem_usecase.dart @@ -6,7 +6,7 @@ import 'package:problem_check_system/app/features/problem/domain/repositories/pr import 'package:uuid/uuid.dart'; class AddProblemUsecase { - final IProblemRepository problemRepository; + final ProblemRepository problemRepository; final AuthRepository authRepository; final Uuid uuid; diff --git a/lib/app/features/problem/domain/usecases/delete_problem.dart b/lib/app/features/problem/domain/usecases/delete_problem.dart index 391d95f..bd41655 100644 --- a/lib/app/features/problem/domain/usecases/delete_problem.dart +++ b/lib/app/features/problem/domain/usecases/delete_problem.dart @@ -1,7 +1,7 @@ import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; class DeleteProblem { - final IProblemRepository problemRepository; + final ProblemRepository problemRepository; DeleteProblem({required this.problemRepository}); Future call(String id) async { diff --git a/lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart b/lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart index 9579ad3..21f8179 100644 --- a/lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart +++ b/lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart @@ -3,7 +3,7 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; class GetAllProblemsUsecase { - final IProblemRepository problemRepository; + final ProblemRepository problemRepository; GetAllProblemsUsecase({required this.problemRepository}); diff --git a/lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart b/lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart index 459a3db..5c2b5c8 100644 --- a/lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart +++ b/lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart @@ -2,7 +2,7 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; class GetProblemByIdUsecase { - final IProblemRepository problemRepository; + final ProblemRepository problemRepository; GetProblemByIdUsecase({required this.problemRepository}); Future call(String id) async { diff --git a/lib/app/features/problem/domain/usecases/update_problem_usecase.dart b/lib/app/features/problem/domain/usecases/update_problem_usecase.dart index 76f575d..eeff4a9 100644 --- a/lib/app/features/problem/domain/usecases/update_problem_usecase.dart +++ b/lib/app/features/problem/domain/usecases/update_problem_usecase.dart @@ -4,11 +4,11 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; class UpdateProblemUsecase { - final IProblemRepository repository; + final ProblemRepository problemRepository; final AuthRepository authRepository; UpdateProblemUsecase({ - required this.repository, + required this.problemRepository, required this.authRepository, }); @@ -18,8 +18,10 @@ class UpdateProblemUsecase { final newProblem = entity.copyWith( lastModifiedTime: nowUtc, lastModifierId: userId, - syncStatus: SyncStatus.pendingUpdate, + syncStatus: entity.syncStatus == SyncStatus.pendingCreate + ? SyncStatus.pendingCreate + : SyncStatus.pendingUpdate, ); - return await repository.updateProblem(newProblem); + return await problemRepository.updateProblem(newProblem); } } diff --git a/lib/app/features/problem/domain/usecases/upload_problems_usecase.dart b/lib/app/features/problem/domain/usecases/upload_problems_usecase.dart index 1b35e69..e55ddfc 100644 --- a/lib/app/features/problem/domain/usecases/upload_problems_usecase.dart +++ b/lib/app/features/problem/domain/usecases/upload_problems_usecase.dart @@ -1,42 +1,57 @@ import 'dart:async'; -import 'package:get/get.dart'; // 引入 GetX 用于创建可观察对象 +import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; import 'package:problem_check_system/app/core/domain/entities/upload_result.dart'; -import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; +import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; +import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; -import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; -import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository_impl.dart'; -import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart'; -/// 用例:上传一个或多个问题实体,支持进度回调和取消 +/// 用例:上传一个或多个问题实体。 +/// +/// 特性: +/// - 支持进度回调,用于更新 UI。 +/// - 支持取消操作,可以中断上传流程。 +/// - 自动处理并优先同步前置依赖(如企业)。 +/// - 提供详细的失败回调,便于 UI 展示错误详情。 +/// - 优化性能,避免在单次批量上传中重复同步同一个依赖项。 class UploadProblemsUsecase { - final ProblemRepository repository; - UploadProblemsUsecase({required this.repository}); + final ProblemRepository problemRepository; + final EnterpriseRepository enterpriseRepository; + + UploadProblemsUsecase({ + required this.problemRepository, + required this.enterpriseRepository, + }); - // 使用 GetX 的 RxBool 来创建一个可观察的取消标志 - // 这样 Controller 可以改变它,UseCase 可以监听它 + // 私有的取消标志 final RxBool _isCancelled = false.obs; - /// [核心] 提供一个公开的方法来触发取消 + /// [公开API] 触发取消操作 void cancel() { _isCancelled.value = true; } - /// 执行上传操作 - /// [onProgress] 是一个回调函数,用于将进度实时报告给调用者 (Controller) + /// [公开API] 执行批量上传流程 + /// + /// [problemsToUpload]: 需要上传的核心业务实体列表。 + /// [onProgress]: 进度回调,返回 (已处理数量, 总数)。 + /// [onFailure]: (可选) 单项失败回调,返回 (失败的实体, 错误信息字符串)。 Future call({ - required List problemsToUpload, + required List problemsToUpload, required void Function(int uploaded, int total) onProgress, + void Function(ProblemEntity failedProblem, String error)? onFailure, }) async { - // 重置取消标志,以便用例可以被复用 - _isCancelled.value = false; + _isCancelled.value = false; // 重置取消状态以便复用 int successCount = 0; int failureCount = 0; final total = problemsToUpload.length; + // [优化2] 跟踪本次会话中已成功同步的企业,避免重复网络请求 + final Set syncedEnterpriseIdsInThisSession = {}; + for (int i = 0; i < total; i++) { - // 在每次循环开始前,检查是否已被取消 + // 检查是否已取消 if (_isCancelled.value) { - // 如果已取消,立即中断并返回当前结果 return UploadResult( successCount: successCount, failureCount: failureCount, @@ -44,28 +59,80 @@ class UploadProblemsUsecase { ); } - final problem = problemsToUpload[i].problemEntity; + final problem = problemsToUpload[i]; + try { - // 1. 调用通用的同步方法 - final syncedProblem = await repository.syncProblemToServer(problem); - // 2. 同步成功后,更新本地状态 - if (problem.syncStatus == SyncStatus.pendingDelete) { - // repository.deleteLocalEnterprise(enterprise.id); - } else { - await repository.updateProblem(syncedProblem); - } + // [优化3] 将核心逻辑委托给私有方法,使循环体更清晰 + await _syncSingleProblemWithDependencies( + problem, + syncedEnterpriseIdsInThisSession, + ); successCount++; } catch (e) { - // 记录失败,但继续上传下一个 failureCount++; - Get.log('上传失败: ${problem.description}, 错误: $e'); + final errorMessage = '问题 [${problem.description}] 上传失败'; + Get.log('$errorMessage, 错误: $e'); + + // [优化1] 调用失败回调,向 Controller 报告具体错误 + onFailure?.call(problem, e.toString()); } - // 3. 通过回调报告进度 + // 报告总体进度 onProgress(i + 1, total); } - // 所有任务完成 - return UploadResult(successCount: successCount, failureCount: failureCount); + return UploadResult( + successCount: successCount, + failureCount: failureCount, + wasCancelled: false, + ); + } + + /// [私有方法] 处理单个问题的同步,包括其企业依赖 + Future _syncSingleProblemWithDependencies( + ProblemEntity problem, + Set syncedEnterpriseIds, + ) async { + // --- 步骤 A: 检查并同步企业依赖 --- + final enterprise = await enterpriseRepository.getEnterpriseById( + problem.enterpriseId, + ); + + // 防御性编程:确保企业存在于本地 + if (enterprise == null) { + throw Exception('数据不一致:关联的企业 (ID: ${problem.enterpriseId}) 在本地数据库中不存在。'); + } + + // 检查企业是否需要同步,并且尚未在本次会话中同步过 + final bool needsEnterpriseSync = + (enterprise.syncStatus != SyncStatus.synced && + enterprise.syncStatus != SyncStatus.untracked); + + if (needsEnterpriseSync && !syncedEnterpriseIds.contains(enterprise.id)) { + Get.log('优先同步依赖的企业 [${enterprise.name}]...'); + try { + final syncedEnterprise = await enterpriseRepository + .syncEnterpriseToServer(enterprise); + Get.log('企业 [${syncedEnterprise.name}] 同步成功!'); + + // 将成功同步的企业 ID 加入集合,避免重复同步 + syncedEnterpriseIds.add(syncedEnterprise.id); + } catch (e) { + // 如果企业同步失败,则此问题也无法继续,向上抛出异常 + throw Exception('其依赖的企业 [${enterprise.name}] 上传失败: $e'); + } + } + + // --- 步骤 B: 同步问题本身 --- + // 此时可以确保企业已同步。 + // 为确保 enterpriseId 是最新的(以防服务器返回新ID),我们最好重新从数据库获取一次问题实体。 + // 这是最安全、最能保证数据一致性的做法。 + final refreshedProblem = await problemRepository.getProblemById(problem.id); + if (refreshedProblem == null) { + throw Exception('数据不一致:在企业同步后,问题 (ID: ${problem.id}) 从本地数据库消失。'); + } + + Get.log('正在同步问题 [${refreshedProblem.description}]...'); + await problemRepository.syncProblemToServer(refreshedProblem); } } diff --git a/lib/app/features/problem/presentation/bindings/problem_form_binding.dart b/lib/app/features/problem/presentation/bindings/problem_form_binding.dart index dfae13b..f4666a7 100644 --- a/lib/app/features/problem/presentation/bindings/problem_form_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_form_binding.dart @@ -57,7 +57,7 @@ class ProblemFormBinding extends BaseBindings { ); Get.lazyPut( () => UpdateProblemUsecase( - repository: Get.find(), + problemRepository: Get.find(), authRepository: Get.find(), ), ); 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 2ad2e47..8fe69e9 100644 --- a/lib/app/features/problem/presentation/bindings/problem_list_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_list_binding.dart @@ -1,35 +1,52 @@ import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/bindings/base_bindings.dart'; import 'package:problem_check_system/app/core/services/database_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/repositories/problem_repository_impl.dart'; import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_list_controller.dart'; -class ProblemListBinding extends Bindings { +class ProblemListBinding extends BaseBindings { @override - void dependencies() { - // 数据 - Get.lazyPut( - () => - ProblemLocalDataSource(databaseService: Get.find()), + void register1Services() { + // TODO: implement register1Services + } + + @override + void register2DataSource() { + Get.lazyPut( + () => ProblemLocalDataSourceImpl( + databaseService: Get.find(), + ), ); - // 仓库 - Get.lazyPut( - () => ProblemRepository(Get.find()), + Get.lazyPut( + () => ProblemRemoteDataSourceImpl(http: Get.find()), ); - // 用例 - Get.lazyPut( - () => GetAllProblemsUsecase( - problemRepository: Get.find(), + } + + @override + void register3Repositories() { + Get.lazyPut( + () => ProblemRepositoryImpl( + localDataSource: Get.find(), + remoteDataSource: Get.find(), ), ); + } + + @override + void register4Usecases() { + Get.lazyPut( + () => GetAllProblemsUsecase(problemRepository: Get.find()), + ); + } - /// 控制器 + @override + void register5Controllers() { Get.lazyPut( - () => ProblemListController( - getAllProblemsUsecase: Get.find(), - ), + () => ProblemListController(getAllProblemsUsecase: 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 c7493a4..3a412ff 100644 --- a/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart @@ -1,5 +1,15 @@ import 'package:get/get.dart'; import 'package:problem_check_system/app/core/bindings/base_bindings.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/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/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/repositories/problem_repository_impl.dart'; +import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; +import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart'; +import 'package:problem_check_system/app/features/problem/domain/usecases/upload_problems_usecase.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart'; class ProblemUploadBinding extends BaseBindings { @@ -10,21 +20,58 @@ class ProblemUploadBinding extends BaseBindings { @override void register2DataSource() { - // TODO: implement register2DataSource + Get.lazyPut( + () => ProblemLocalDataSourceImpl(databaseService: Get.find()), + ); + Get.lazyPut( + () => ProblemRemoteDataSourceImpl(http: Get.find()), + ); + Get.lazyPut( + () => EnterpriseLocalDataSourceImpl(databaseService: Get.find()), + ); + Get.lazyPut( + () => EnterpriseRemoteDataSourceImpl(http: Get.find()), + ); } @override void register3Repositories() { - // TODO: implement register3Repositories + Get.lazyPut( + () => ProblemRepositoryImpl( + localDataSource: Get.find(), + remoteDataSource: Get.find(), + ), + ); + Get.lazyPut( + () => EnterpriseRepositoryImpl( + localDataSource: Get.find(), + remoteDataSource: Get.find(), + networkStatusService: Get.find(), + uuid: Get.find(), + ), + ); } @override void register4Usecases() { - // TODO: implement register4Usecases + Get.lazyPut( + () => GetAllProblemsUsecase(problemRepository: Get.find()), + ); + Get.lazyPut( + () => UploadProblemsUsecase( + problemRepository: Get.find(), + enterpriseRepository: Get.find(), + ), + ); } @override void register5Controllers() { - // Get.lazyPut(() => ProblemUploadController()); + Get.lazyPut( + () => ProblemUploadController( + getAllProblemsUsecase: Get.find(), + uploadProblemsUsecase: Get.find(), + ), + ); } } diff --git a/lib/app/features/problem/presentation/controllers/problem_form_controller.dart b/lib/app/features/problem/presentation/controllers/problem_form_controller.dart index 3963665..ecc5c12 100644 --- a/lib/app/features/problem/presentation/controllers/problem_form_controller.dart +++ b/lib/app/features/problem/presentation/controllers/problem_form_controller.dart @@ -251,40 +251,49 @@ class ProblemFormController extends GetxController { } Get.back(result: true); // 返回成功结果 } catch (e) { + Get.log('$e'); Get.snackbar('错误', '保存问题失败: $e'); } finally { isLoading.value = false; } } - // 保存图片到本地存储 Future> _saveImagesToLocal() async { - final List imagePaths = []; + final List finalImagePaths = []; final directory = await getApplicationDocumentsDirectory(); final imagesDir = Directory('${directory.path}/problem_images'); - // 确保目录存在 if (!await imagesDir.exists()) { await imagesDir.create(recursive: true); } - // 遍历选择图片 + for (var image in selectedImages) { - try { - final String fileName = '${Uuid().v4()}_${path.basename(image.name)}'; - final String imagePath = '${imagesDir.path}/$fileName'; - final File imageFile = File(imagePath); - - // 读取图片字节并写入文件 - final imageBytes = await image.readAsBytes(); - await imageFile.writeAsBytes(imageBytes); - - imagePaths.add(imagePath); - } catch (e) { - throw Exception(e); + // ✅ 关键检查: 判断这个 XFile 指向的是否是已经保存过的文件 + // 我们通过检查路径是否已经包含了应用的文档目录来判断 + if (image.path.startsWith(directory.path)) { + // 如果是已经保存的图片,直接使用它的旧路径 + finalImagePaths.add(image.path); + } else { + // 如果是新图片(来自相册或相机),为其生成一个全新的、唯一的名字并保存 + try { + // 从原始路径获取文件扩展名 (例如 .jpg) + final String extension = path.extension(image.path); + // 创建一个全新的、唯一的文件名 + final String newFileName = '${const Uuid().v4()}$extension'; + final String newImagePath = path.join(imagesDir.path, newFileName); + + // 读取图片字节并写入新文件 + await image.saveTo(newImagePath); // XFile.saveTo() 是更推荐的方法 + + finalImagePaths.add(newImagePath); + } catch (e) { + // 重新抛出更具体的错误信息 + throw Exception('保存新图片失败: ${image.path}. 错误: $e'); + } } } - return imagePaths; + return finalImagePaths; } // 取消编辑/新增 diff --git a/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart b/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart index da7caeb..a11f18d 100644 --- a/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart +++ b/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart @@ -1,14 +1,16 @@ // lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/controllers/i_upload_controller.dart'; import 'package:problem_check_system/app/core/domain/entities/upload_result.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/upload_enterprises_usecase.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart'; +import 'package:problem_check_system/app/core/pages/widgets/upload_progress_dialog.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'; import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart'; import 'package:problem_check_system/app/features/problem/domain/usecases/upload_problems_usecase.dart'; -class ProblemUploadController extends GetxController { +class ProblemUploadController extends GetxController + implements IUploadController { final GetAllProblemsUsecase getAllProblemsUsecase; final UploadProblemsUsecase uploadProblemsUsecase; @@ -19,18 +21,22 @@ class ProblemUploadController extends GetxController { // --- 实现基类中定义的属性 --- final isLoading = false.obs; - final enterprises = [].obs; - final selectedEnterprises = {}.obs; + final problems = [].obs; + final selectedProblems = {}.obs; // --- [新增] 上传状态管理的属性 --- + @override final isUploading = false.obs; + @override final uploadProgress = 0.0.obs; // 进度,0.0 到 1.0 + @override final uploadedCount = 0.obs; + @override final totalToUpload = 0.obs; + @override final uploadResult = Rxn(); // 用于存储上传结果 bool get allSelected => - enterprises.isNotEmpty && - enterprises.length == selectedEnterprises.length; + problems.isNotEmpty && problems.length == selectedProblems.length; @override void onInit() { @@ -38,20 +44,20 @@ class ProblemUploadController extends GetxController { fetchPendingUploads(); } - void onSelectionChanged(EnterpriseListItem enterprise) { - if (selectedEnterprises.contains(enterprise)) { - selectedEnterprises.remove(enterprise); + void onSelectionChanged(ProblemListItemEntity problem) { + if (selectedProblems.contains(problem)) { + selectedProblems.remove(problem); } else { - selectedEnterprises.add(enterprise); + selectedProblems.add(problem); } } void toggleSelectAll() { if (allSelected) { - selectedEnterprises.clear(); + selectedProblems.clear(); } else { // 如果是取消全选,则选择所有 - selectedEnterprises.addAll(enterprises); + selectedProblems.addAll(problems); } } @@ -59,8 +65,10 @@ class ProblemUploadController extends GetxController { Future fetchPendingUploads() async { isLoading.value = true; try { - final data = await getEnterpriseListUsecase(isUploaded: false); - enterprises.assignAll(data); + final data = await getAllProblemsUsecase( + filter: ProblemFilterParams(isUploaded: false), + ); + problems.assignAll(data); } catch (e) { Get.snackbar('错误', '加载待上传列表失败: $e'); } finally { @@ -69,13 +77,13 @@ class ProblemUploadController extends GetxController { } void confirmUpload() { - if (selectedEnterprises.isEmpty) { - Get.snackbar('提示', '请至少选择一个企业进行上传'); + if (selectedProblems.isEmpty) { + Get.snackbar('提示', '请至少选择一个问题进行上传'); return; } // 1. 显示上传对话框 Get.dialog( - const UploadProgressDialog(), + UploadProgressDialog(controller: this), barrierDismissible: false, // 禁止点击外部关闭 ); // 2. 开始执行上传 @@ -88,12 +96,14 @@ class ProblemUploadController extends GetxController { isUploading.value = true; uploadProgress.value = 0.0; uploadResult.value = null; - totalToUpload.value = selectedEnterprises.length; + totalToUpload.value = selectedProblems.length; uploadedCount.value = 0; // 调用 UseCase,并传入 onProgress 回调 - final result = await uploadEnterprisesUseCase.call( - enterprisesToUpload: selectedEnterprises.toList(), + final result = await uploadProblemsUsecase( + problemsToUpload: selectedProblems + .map((item) => item.problemEntity) + .toList(), onProgress: (uploaded, total) { // 更新进度状态 uploadedCount.value = uploaded; @@ -107,11 +117,13 @@ class ProblemUploadController extends GetxController { } /// [新增] 取消上传 + @override void cancelUpload() { - uploadEnterprisesUseCase.cancel(); + uploadProblemsUsecase.cancel(); } /// [新增] 关闭对话框并处理后续事宜 + @override void closeUploadDialog() { Get.back(); // 关闭对话框 // 如果上传不是被取消的,并且有成功项,则认为操作成功,返回 true 通知列表刷新 diff --git a/lib/app/features/problem/presentation/pages/problem_upload_page.dart b/lib/app/features/problem/presentation/pages/problem_upload_page.dart index 2b53699..03ee9cb 100644 --- a/lib/app/features/problem/presentation/pages/problem_upload_page.dart +++ b/lib/app/features/problem/presentation/pages/problem_upload_page.dart @@ -3,8 +3,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:get/get_state_manager/src/simple/get_view.dart'; import 'package:problem_check_system/app/core/pages/widgets/upload_app_bar.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart'; +import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/problem_card.dart'; class ProblemUploadPage extends GetView { const ProblemUploadPage({super.key}); @@ -16,9 +16,9 @@ class ProblemUploadPage extends GetView { preferredSize: const Size.fromHeight(kToolbarHeight), child: Obx( () => UploadAppBar( - selectedCount: controller.selectedEnterprises.length, + selectedCount: controller.selectedProblems.length, allSelected: controller.allSelected, - buttonVisible: controller.enterprises.isNotEmpty, + buttonVisible: controller.problems.isNotEmpty, onButtonPressed: () => controller.toggleSelectAll(), ), ), @@ -32,7 +32,7 @@ class ProblemUploadPage extends GetView { ), child: Obx( () => ElevatedButton( - onPressed: controller.selectedEnterprises.isNotEmpty + onPressed: controller.selectedProblems.isNotEmpty ? controller.confirmUpload : null, style: ElevatedButton.styleFrom( @@ -43,7 +43,7 @@ class ProblemUploadPage extends GetView { ), minimumSize: Size(double.infinity, 48.h), ), - child: Text('确认上传 (${controller.selectedEnterprises.length})'), + child: Text('确认上传 (${controller.selectedProblems.length})'), ), ), ), @@ -54,11 +54,11 @@ class ProblemUploadPage extends GetView { // 使用 Obx 包裹以监听 controller 中所有 Rx 变量的变化 return Obx(() { // 在列表为空且仍在加载时显示加载指示器 - if (controller.isLoading.value && controller.enterprises.isEmpty) { + if (controller.isLoading.value && controller.problems.isEmpty) { return const Center(child: CircularProgressIndicator()); } // 在加载完成但列表为空时显示提示信息 - if (controller.enterprises.isEmpty) { + if (controller.problems.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -83,18 +83,18 @@ class ProblemUploadPage extends GetView { onRefresh: () async => controller.fetchPendingUploads(), child: ListView.builder( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), - itemCount: controller.enterprises.length, + itemCount: controller.problems.length, itemBuilder: (context, index) { - final item = controller.enterprises[index]; + final item = controller.problems[index]; // 核心: 从 base controller 获取选中状态 return Obx(() { - final isSelected = controller.selectedEnterprises.contains(item); + final isSelected = controller.selectedProblems.contains(item); return Padding( padding: EdgeInsets.only(bottom: 12.h), - child: UnifiedEnterpriseCard( - enterpriseListItem: item, + child: ProblemCard( + problemListItem: item, isSelected: isSelected, // 根据外部传入的 itemMode 决定卡片内部的 mode // --- [核心] 将卡片的所有交互事件转发给 controller 的抽象方法 ---