diff --git a/lib/app/core/bindings/initial_binding.dart b/lib/app/core/bindings/initial_binding.dart index c78874d..0dd93a6 100644 --- a/lib/app/core/bindings/initial_binding.dart +++ b/lib/app/core/bindings/initial_binding.dart @@ -5,7 +5,6 @@ import 'package:problem_check_system/app/core/services/database_service.dart'; import 'package:problem_check_system/app/core/services/network_status_service.dart'; import 'package:problem_check_system/app/core/services/http_provider.dart'; import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; -import 'package:problem_check_system/app/core/repositories/file_repository.dart'; import 'package:problem_check_system/app/core/repositories/image_repository.dart'; import 'package:problem_check_system/app/core/repositories/image_repository_impl.dart'; import 'package:problem_check_system/app/core/services/upgrader_service.dart'; @@ -29,7 +28,6 @@ class InitialBinding implements Bindings { } void _registerRepositories() { - Get.lazyPut(() => FileRepository()); Get.lazyPut( () => ImageRepositoryImpl(httpProvider: Get.find()), ); diff --git a/lib/app/core/repositories/file_repository.dart b/lib/app/core/repositories/file_repository.dart deleted file mode 100644 index c96ba81..0000000 --- a/lib/app/core/repositories/file_repository.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; // 引入 kDebugMode 和 debugPrint -import 'package:get/get.dart' hide FormData, MultipartFile; -import 'package:path/path.dart' as p; -import 'package:problem_check_system/app/core/extensions/http_response_extension.dart'; -import 'package:problem_check_system/app/core/utils/constants/api_endpoints.dart'; -import 'package:problem_check_system/app/core/services/http_provider.dart'; - -class FileRepository extends GetxService { - final HttpProvider _httpProvider = Get.find(); - - /// @param imageFilePath 要上传的本地图片文件。 - /// @param cancelToken 用于取消上传任务的令牌。 - /// @param onSendProgress 上传进度回调,提供已发送和总大小。 - /// @return 上传成功后服务器返回的图片 URL。 - Future uploadImage( - String imageFilePath, { - required CancelToken cancelToken, - ProgressCallback? onSendProgress, - }) async { - try { - // 1. 创建 FormData 对象,用于构建 multipart/form-data 请求体 - final formData = FormData.fromMap({ - // 'file': 这通常是后端接口定义的文件字段名 - 'file': await MultipartFile.fromFile( - imageFilePath, - filename: p.basename(imageFilePath), - ), - }); - - // 2. 使用 HttpProvider 的 post 方法发送请求 - final response = await _httpProvider.post( - ApiEndpoints.postUploadFile, - data: formData, - cancelToken: cancelToken, // 将取消令牌传递给 post 请求 - onSendProgress: onSendProgress, // 将进度回调传递给 post 请求 - ); - - // --- 在这里打印服务器的完整响应结构 (仅在调试模式下) --- - if (kDebugMode) { - debugPrint('服务器返回的状态码: ${response.statusCode}'); - debugPrint('服务器返回的原始数据: ${response.data}'); - } - - // 3. 处理响应,并返回图片 URL - if (response.isSuccess) { - final Map data = response.data; - - // 假设服务器返回的图片 URL 字段名为 'url' - String imageUrl = - "${ApiEndpoints.baseUrl}${ApiEndpoints.postUploadFile}/${data['objectName']}"; - - return imageUrl; - } else { - throw Exception('上传失败,状态码: ${response.statusCode}'); - } - } on DioException catch (e) { - Get.log('图片上传发生未知错误: $e'); - throw Exception('图片上传失败: ${e.message}'); - } catch (e) { - Get.log('图片上传发生未知错误: $e'); - throw Exception('图片上传发生未知错误: $e'); - } - } -} diff --git a/lib/app/core/repositories/image_repository.dart b/lib/app/core/repositories/image_repository.dart index d3d89e0..0619d39 100644 --- a/lib/app/core/repositories/image_repository.dart +++ b/lib/app/core/repositories/image_repository.dart @@ -1,6 +1,13 @@ // image_repository.dart +import 'package:dio/dio.dart'; + abstract class ImageRepository { Future downloadImage(String imageUrl, String problemId); + Future uploadImage( + String imageFilePath, { + required CancelToken cancelToken, + ProgressCallback? onSendProgress, + }); Future isImageDownloaded(String imageUrl, String problemId); Future getLocalImagePath(String imageUrl, String problemId); Future deleteProblemImages(String problemId); diff --git a/lib/app/core/repositories/image_repository_impl.dart b/lib/app/core/repositories/image_repository_impl.dart index fe70baa..3146567 100644 --- a/lib/app/core/repositories/image_repository_impl.dart +++ b/lib/app/core/repositories/image_repository_impl.dart @@ -1,11 +1,15 @@ // image_repository_impl.dart import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:get/get.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart' hide FormData, MultipartFile; +import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; +import 'package:problem_check_system/app/core/extensions/http_response_extension.dart'; import 'package:problem_check_system/app/core/services/http_provider.dart'; import 'package:problem_check_system/app/core/repositories/image_repository.dart'; +import 'package:problem_check_system/app/core/utils/constants/api_endpoints.dart'; class ImageRepositoryImpl extends GetxService implements ImageRepository { final HttpProvider httpProvider; @@ -70,6 +74,61 @@ class ImageRepositoryImpl extends GetxService implements ImageRepository { } } + /// @param imageFilePath 要上传的本地图片文件。 + /// @param cancelToken 用于取消上传任务的令牌。 + /// @param onSendProgress 上传进度回调,提供已发送和总大小。 + /// @return 上传成功后服务器返回的图片 URL。 + @override + Future uploadImage( + String imageFilePath, { + required CancelToken cancelToken, + ProgressCallback? onSendProgress, + }) async { + try { + // 1. 创建 FormData 对象,用于构建 multipart/form-data 请求体 + final formData = FormData.fromMap({ + // 'file': 这通常是后端接口定义的文件字段名 + 'file': await MultipartFile.fromFile( + imageFilePath, + filename: p.basename(imageFilePath), + ), + }); + + // 2. 使用 HttpProvider 的 post 方法发送请求 + final response = await httpProvider.post( + ApiEndpoints.postUploadFile, + data: formData, + cancelToken: cancelToken, // 将取消令牌传递给 post 请求 + onSendProgress: onSendProgress, // 将进度回调传递给 post 请求 + ); + + // --- 在这里打印服务器的完整响应结构 (仅在调试模式下) --- + if (kDebugMode) { + debugPrint('服务器返回的状态码: ${response.statusCode}'); + debugPrint('服务器返回的原始数据: ${response.data}'); + } + + // 3. 处理响应,并返回图片 URL + if (response.isSuccess) { + final Map data = response.data; + + // 假设服务器返回的图片 URL 字段名为 'url' + String imageUrl = + "${ApiEndpoints.baseUrl}${ApiEndpoints.postUploadFile}/${data['objectName']}"; + + return imageUrl; + } else { + throw Exception('上传失败,状态码: ${response.statusCode}'); + } + } on DioException catch (e) { + Get.log('图片上传发生未知错误: $e'); + throw Exception('图片上传失败: ${e.message}'); + } catch (e) { + Get.log('图片上传发生未知错误: $e'); + throw Exception('图片上传发生未知错误: $e'); + } + } + @override Future isImageDownloaded(String imageUrl, String problemId) async { try { diff --git a/lib/app/features/navigation/presentation/bindings/navigation_binding.dart b/lib/app/features/navigation/presentation/bindings/navigation_binding.dart index ea7f9d1..a025deb 100644 --- a/lib/app/features/navigation/presentation/bindings/navigation_binding.dart +++ b/lib/app/features/navigation/presentation/bindings/navigation_binding.dart @@ -11,6 +11,7 @@ class NavigationBinding extends Bindings { () => NavigationController( networkStatusService: Get.find(), enterpriseListController: Get.find(), + problemListController: Get.find(), ), ); } diff --git a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart index 402e8ac..12bcc1d 100644 --- a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart +++ b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart @@ -7,6 +7,7 @@ import 'package:problem_check_system/app/features/enterprise/presentation/contro import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_list_page.dart'; import 'package:problem_check_system/app/features/home/pages/home_page.dart'; import 'package:problem_check_system/app/features/my/views/my_page.dart'; +import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_list_controller.dart'; import 'package:problem_check_system/app/features/problem/presentation/pages/problem_list_page.dart'; class NavigationController extends GetxController { @@ -19,9 +20,11 @@ class NavigationController extends GetxController { final fabUploadPosition = Offset(0, 0).obs; final NetworkStatusService networkStatusService; final EnterpriseListController enterpriseListController; + final ProblemListController problemListController; NavigationController({ required this.networkStatusService, required this.enterpriseListController, + required this.problemListController, }); /// get 选中的 @@ -108,8 +111,10 @@ class NavigationController extends GetxController { } break; case 2: // 问题列表页面 - Get.log("当前在问题页面,准备跳转到问题上传页..."); - Get.toNamed(AppRoutes.problemUpload); + final result = await Get.toNamed(AppRoutes.problemUpload); + if (result == true) { + problemListController.search(); + } break; default: final result = await Get.toNamed(AppRoutes.enterpriseUpload); 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 e939c7b..0f73e89 100644 --- a/lib/app/features/problem/data/repositories/problem_repository_impl.dart +++ b/lib/app/features/problem/data/repositories/problem_repository_impl.dart @@ -1,5 +1,8 @@ import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:get/get.dart'; import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; +import 'package:problem_check_system/app/core/repositories/image_repository.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'; @@ -18,11 +21,13 @@ class ProblemRepositoryImpl implements ProblemRepository { final ProblemLocalDataSource localDataSource; final ProblemRemoteDataSource remoteDataSource; final NetworkStatusService networkStatusService; + final ImageRepository imageRepository; ProblemRepositoryImpl({ required this.localDataSource, required this.remoteDataSource, required this.networkStatusService, + required this.imageRepository, }); @override @@ -127,13 +132,72 @@ class ProblemRepositoryImpl implements ProblemRepository { return problem; } + // @override + // Future syncProblemToServer(ProblemEntity problem) async { + // // 将 Entity 转换为 DTO,准备与服务器通信 + // final problemDto = ProblemDto.fromEntity(problem); + // ProblemDto syncedDto; + + // switch (problem.syncStatus) { + // case SyncStatus.pendingCreate: + // syncedDto = await remoteDataSource.createProblem(problemDto); + // break; + // case SyncStatus.pendingUpdate: + // syncedDto = await remoteDataSource.updateProblem(problemDto); + // break; + // case SyncStatus.pendingDelete: + // // 先从远程删除 + // await remoteDataSource.deleteProblem(problemDto.id); + // // 远程删除成功后,必须从本地也删除 + // await localDataSource.deleteProblem(problem.id); + // // 返回一个更新了状态的实体,表示操作完成 + // return problem.copyWith(syncStatus: SyncStatus.synced); + + // case SyncStatus.synced: + // case SyncStatus.untracked: + // return problem; + // } + + // // 将从服务器返回的最新 DTO 转换回 Entity + // // 这个 syncedEntity 现在包含了服务器生成的ID、时间戳等信息 + // final syncedEntity = syncedDto.toEntity(); + + // // 将这个最新的、已同步的 Entity 转换为 Model,并更新回本地数据库 + // // 这是确保本地状态与服务器一致的核心步骤! + // final modelToSave = ProblemModel.fromEntity(syncedEntity); + // await localDataSource.updateProblem(modelToSave.toMap()); + + // // 返回最终从服务器同步回来的、纯净的 Entity + // return syncedEntity; + // } @override Future syncProblemToServer(ProblemEntity problem) async { + // 2. 在方法开始时创建一个 CancelToken + // 这个 token 将用于本次同步操作中的所有图片上传 + final cancelToken = CancelToken(); + + ProblemEntity problemToSend = problem; + + // 仅在需要创建或更新时才处理图片上传 + if (problem.syncStatus == SyncStatus.pendingCreate || + problem.syncStatus == SyncStatus.pendingUpdate) { + // 3. 将 CancelToken 传递给辅助方法 + final processedImageUrls = await _uploadLocalImagesAndGetRemoteUrls( + problem.imageUrls, + cancelToken: cancelToken, // 传递 token + ); + + // 使用包含了远程URL的新实体进行后续操作 + problemToSend = problem.copyWith(imageUrls: processedImageUrls); + } + + // ---- 后续逻辑与你原来的一致,只是使用 problemToSend ---- + // 将 Entity 转换为 DTO,准备与服务器通信 - final problemDto = ProblemDto.fromEntity(problem); + final problemDto = ProblemDto.fromEntity(problemToSend); ProblemDto syncedDto; - switch (problem.syncStatus) { + switch (problemToSend.syncStatus) { case SyncStatus.pendingCreate: syncedDto = await remoteDataSource.createProblem(problemDto); break; @@ -141,11 +205,9 @@ class ProblemRepositoryImpl implements ProblemRepository { syncedDto = await remoteDataSource.updateProblem(problemDto); break; case SyncStatus.pendingDelete: - // 先从远程删除 await remoteDataSource.deleteProblem(problemDto.id); - // 远程删除成功后,必须从本地也删除 await localDataSource.deleteProblem(problem.id); - // 返回一个更新了状态的实体,表示操作完成 + await imageRepository.deleteProblemImages(problem.id); return problem.copyWith(syncStatus: SyncStatus.synced); case SyncStatus.synced: @@ -153,17 +215,47 @@ class ProblemRepositoryImpl implements ProblemRepository { return problem; } - // 将从服务器返回的最新 DTO 转换回 Entity - // 这个 syncedEntity 现在包含了服务器生成的ID、时间戳等信息 - final syncedEntity = syncedDto.toEntity(); - - // 将这个最新的、已同步的 Entity 转换为 Model,并更新回本地数据库 - // 这是确保本地状态与服务器一致的核心步骤! - final modelToSave = ProblemModel.fromEntity(syncedEntity); + final entityToSaveLocally = syncedDto.toEntity().copyWith( + imageUrls: problem.imageUrls, + ); + final modelToSave = ProblemModel.fromEntity(entityToSaveLocally); await localDataSource.updateProblem(modelToSave.toMap()); - // 返回最终从服务器同步回来的、纯净的 Entity - return syncedEntity; + return entityToSaveLocally; + } + + /// 辅助方法:处理图片URL列表,上传本地图片并返回包含远程URL的新列表 + Future> _uploadLocalImagesAndGetRemoteUrls( + List urls, { + required CancelToken cancelToken, // 接收 token + }) async { + // 创建所有上传任务的 Future 列表 + final uploadFutures = urls.map((url) async { + // 判断 URL 是否是本地文件路径 (简单判断,!url.startsWith('http') 更通用) + if (!url.startsWith('http')) { + try { + Get.log('准备上传本地图片: $url'); + // 将 CancelToken 传递给 imageRepository 的 uploadImage 方法 + final remoteUrl = await imageRepository.uploadImage( + url, + cancelToken: cancelToken, + ); + Get.log('图片上传成功: $url -> $remoteUrl'); + return remoteUrl; + } catch (e) { + Get.log('图片上传失败: $url, 错误: $e'); + // 如果上传失败,建议抛出异常,中断整个同步过程 + // 因为数据不完整可能会导致后续问题 + throw Exception('图片上传失败: $url'); + } + } else { + // 已经是远程 URL,直接返回,无需上传 + return url; + } + }).toList(); + + // 并行等待所有任务完成 + return Future.wait(uploadFutures); } /// 同步服务器数据 @@ -223,7 +315,44 @@ class ProblemRepositoryImpl implements ProblemRepository { // 4. 将无冲突的新数据直接写入本地数据库 if (newProblems.isNotEmpty) { - List> problemMaps = newProblems + final List problemsWithLocalImages = []; + + for (final problem in newProblems) { + // 创建一个新的列表来存储下载后的本地图片路径 + final List localImagePaths = []; + + // 遍历实体中的每一个远程图片 URL + for (final remoteUrl in problem.imageUrls) { + try { + Get.log('准备下载图片: $remoteUrl for problem ${problem.id}'); + + // 调用 ImageRepository 下载图片,它会返回本地文件路径 + final localPath = await imageRepository.downloadImage( + remoteUrl, + problem.id, + ); + + // 将获取到的本地路径添加到新列表中 + localImagePaths.add(localPath); + Get.log('图片下载并替换成功: $remoteUrl -> $localPath'); + } catch (e) { + // 如果下载失败,可以选择记录日志,然后跳过这张图片 + // 这样,失败的图片就不会被添加到本地路径列表中 + Get.log('图片下载失败: $remoteUrl, 错误: $e'); + // 在这种情况下,我们不把任何路径添加到 localImagePaths 中, + // 或者你也可以选择保留原始的 remoteUrl,但这可能会导致后续逻辑复杂化。 + // 目前的建议是:下载失败就直接跳过。 + } + } + + // 使用 copyWith 方法创建一个新的实体, + // 用包含本地路径的列表替换掉旧的远程 URL 列表 + final updatedProblem = problem.copyWith(imageUrls: localImagePaths); + + // 将更新后的实体添加到最终要存入数据库的列表中 + problemsWithLocalImages.add(updatedProblem); + } + List> problemMaps = problemsWithLocalImages .map((entity) => ProblemModel.fromEntity(entity).toMap()) .toList(); await localDataSource.cacheProblems(problemMaps); 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 c401c36..7a05bcd 100644 --- a/lib/app/features/problem/presentation/bindings/problem_list_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_list_binding.dart @@ -1,5 +1,7 @@ import 'package:get/get.dart'; import 'package:problem_check_system/app/core/bindings/base_bindings.dart'; +import 'package:problem_check_system/app/core/repositories/image_repository.dart'; +import 'package:problem_check_system/app/core/repositories/image_repository_impl.dart'; import 'package:problem_check_system/app/core/services/database_service.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/get_enterprises_usecase.dart'; import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_data_source.dart'; @@ -29,11 +31,15 @@ class ProblemListBinding extends BaseBindings { @override void register3Repositories() { + Get.lazyPut( + () => ImageRepositoryImpl(httpProvider: Get.find()), + ); Get.lazyPut( () => ProblemRepositoryImpl( localDataSource: Get.find(), remoteDataSource: Get.find(), networkStatusService: Get.find(), + imageRepository: 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 0a2db62..b0f107d 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,7 @@ import 'package:get/get.dart'; import 'package:problem_check_system/app/core/bindings/base_bindings.dart'; +import 'package:problem_check_system/app/core/repositories/image_repository.dart'; +import 'package:problem_check_system/app/core/repositories/image_repository_impl.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'; @@ -34,11 +36,15 @@ class ProblemUploadBinding extends BaseBindings { @override void register3Repositories() { + Get.lazyPut( + () => ImageRepositoryImpl(httpProvider: Get.find()), + ); Get.lazyPut( () => ProblemRepositoryImpl( localDataSource: Get.find(), remoteDataSource: Get.find(), networkStatusService: Get.find(), + imageRepository: Get.find(), ), ); Get.lazyPut( diff --git a/lib/app/features/problem/presentation/controllers/problem_list_controller.dart b/lib/app/features/problem/presentation/controllers/problem_list_controller.dart index 67931f8..5962aa4 100644 --- a/lib/app/features/problem/presentation/controllers/problem_list_controller.dart +++ b/lib/app/features/problem/presentation/controllers/problem_list_controller.dart @@ -15,8 +15,6 @@ import 'package:problem_check_system/app/features/problem/domain/usecases/get_al import 'package:problem_check_system/app/features/problem/domain/usecases/resolve_conflict_usecase.dart'; import 'package:problem_check_system/app/features/problem/domain/usecases/sync_problems_usecase.dart'; -// TODO 同步的时候需要保存图片到本地 - class ProblemListController extends GetxController { final GetAllProblemsUsecase getAllProblemsUsecase; final GetEnterprisesUsecase getEnterprisesUsecase; diff --git a/lib/app/features/problem/presentation/pages/problem_form_page.dart b/lib/app/features/problem/presentation/pages/problem_form_page.dart index 631e04d..6541acf 100644 --- a/lib/app/features/problem/presentation/pages/problem_form_page.dart +++ b/lib/app/features/problem/presentation/pages/problem_form_page.dart @@ -34,11 +34,6 @@ class ProblemFormPage extends GetView { ), ); } - - // 3. 列表为空状态 - if (controller.enterpriseList.isEmpty) { - return const Center(child: Text('没有可用的企业')); - } return Column( children: [ Expanded(