19 changed files with 642 additions and 353 deletions
@ -0,0 +1,22 @@
|
||||
enum SyncStatus { |
||||
/// 未同步到服务器 |
||||
notSynced, |
||||
|
||||
/// 已同步,本地无修改 |
||||
synced, |
||||
|
||||
/// 已同步,但本地有修改 |
||||
modified, |
||||
} |
||||
|
||||
// 图片的同步状态 |
||||
enum ImageStatus { |
||||
/// 新增的本地图片,需要上传 |
||||
local, |
||||
|
||||
/// 已上传,本地无修改 |
||||
synced, |
||||
|
||||
/// 已同步,但已在本地删除,需要通知服务器 |
||||
deleted, |
||||
} |
@ -0,0 +1,32 @@
|
||||
// image_metadata_model.dart |
||||
import 'package:problem_check_system/data/models/enum_model.dart'; |
||||
|
||||
class ImageMetadata { |
||||
final String localPath; |
||||
final String? remoteUrl; |
||||
final ImageStatus status; |
||||
|
||||
ImageMetadata({ |
||||
required this.localPath, |
||||
this.remoteUrl, |
||||
required this.status, |
||||
}); |
||||
|
||||
// For saving to SQL |
||||
Map<String, dynamic> toMap() { |
||||
return { |
||||
'localPath': localPath, |
||||
'remoteUrl': remoteUrl, |
||||
'status': status.index, |
||||
}; |
||||
} |
||||
|
||||
// For reading from SQL |
||||
factory ImageMetadata.fromMap(Map<String, dynamic> map) { |
||||
return ImageMetadata( |
||||
localPath: map['localPath'] as String, |
||||
remoteUrl: map['remoteUrl'] as String?, |
||||
status: ImageStatus.values[map['status'] as int], |
||||
); |
||||
} |
||||
} |
@ -1,148 +0,0 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:sqflite/sqflite.dart'; |
||||
import 'package:path/path.dart'; |
||||
import 'package:uuid/uuid.dart'; |
||||
import '../models/problem_model.dart'; |
||||
|
||||
/// LocalDatabase 是一个 GetxService,它负责管理本地 SQLite 数据库。 |
||||
/// GetxService 确保这个类在整个应用生命周期中都是一个单例,并且不会被自动销毁。 |
||||
class LocalDatabase extends GetxService { |
||||
static const String _dbName = 'problems.db'; |
||||
static const String _tableName = 'problems'; |
||||
|
||||
/// 私有的数据库实例,只能在 LocalDatabase 类内部访问。 |
||||
late Database _database; |
||||
|
||||
/// onInit 是 GetxService 的生命周期方法,在服务首次被创建时调用。 |
||||
/// 它是执行异步初始化的最佳位置,确保在使用服务前,数据库已经准备就绪。 |
||||
@override |
||||
void onInit() { |
||||
super.onInit(); |
||||
_initDatabase(); |
||||
} |
||||
|
||||
/// 异步初始化数据库。它会打开数据库连接,如果数据库不存在则创建。 |
||||
Future<void> _initDatabase() async { |
||||
final databasePath = await getDatabasesPath(); |
||||
final path = join(databasePath, _dbName); |
||||
|
||||
_database = await openDatabase(path, version: 1, onCreate: _onCreate); |
||||
} |
||||
|
||||
/// 数据库创建时的回调函数,用于执行建表语句。 |
||||
Future<void> _onCreate(Database db, int version) async { |
||||
await db.execute(''' |
||||
CREATE TABLE $_tableName( |
||||
id TEXT PRIMARY KEY, |
||||
description TEXT NOT NULL, |
||||
location TEXT NOT NULL, |
||||
imageUrls TEXT NOT NULL, |
||||
createdAt INTEGER NOT NULL, |
||||
isUploaded INTEGER NOT NULL, |
||||
censorTaskId TEXT, |
||||
bindData TEXT |
||||
) |
||||
'''); |
||||
} |
||||
|
||||
/// 插入一个新问题到数据库。在插入前,会为其生成一个唯一的 UUID。 |
||||
/// 返回插入的行数。 |
||||
Future<int> insertProblem(Problem problem) async { |
||||
problem.id = const Uuid().v4(); |
||||
return await _database.insert(_tableName, problem.toMap()); |
||||
} |
||||
|
||||
/// 根据 ID 删除数据库中的问题。 |
||||
/// 返回删除的行数。 |
||||
Future<int> deleteProblem(String id) async { |
||||
return await _database.delete(_tableName, where: 'id = ?', whereArgs: [id]); |
||||
} |
||||
|
||||
/// 更新数据库中已存在的问题。 |
||||
/// 返回更新的行数。 |
||||
Future<int> updateProblem(Problem problem) async { |
||||
return await _database.update( |
||||
_tableName, |
||||
problem.toMap(), |
||||
where: 'id = ?', |
||||
whereArgs: [problem.id], |
||||
); |
||||
} |
||||
|
||||
/// 根据 ID 获取单个问题。 |
||||
/// 如果找到问题,返回 Problem 对象;否则,返回 null。 |
||||
Future<Problem?> getProblemById(String id) async { |
||||
final List<Map<String, dynamic>> maps = await _database.query( |
||||
_tableName, |
||||
where: 'id = ?', |
||||
whereArgs: [id], |
||||
limit: 1, // 限制结果为1,因为ID是唯一的 |
||||
); |
||||
|
||||
if (maps.isNotEmpty) { |
||||
// 找到问题,将其转换为 Problem 对象 |
||||
return Problem.fromMap(maps.first); |
||||
} else { |
||||
// 未找到问题,返回 null |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/// 通用查询方法,根据可选的筛选条件获取问题列表。 |
||||
/// - `startDate`/`endDate`:筛选创建时间范围。 |
||||
/// - `uploadStatus`:筛选上传状态('已上传', '未上传', '全部')。 |
||||
/// - `bindStatus`:筛选绑定状态('已绑定', '未绑定', '全部')。 |
||||
Future<List<Problem>> getProblems({ |
||||
DateTime? startDate, |
||||
DateTime? endDate, |
||||
String uploadStatus = '全部', |
||||
String bindStatus = '全部', |
||||
}) async { |
||||
final List<String> whereClauses = []; |
||||
final List<dynamic> whereArgs = []; |
||||
|
||||
if (startDate != null) { |
||||
whereClauses.add('createdAt >= ?'); |
||||
whereArgs.add(startDate.millisecondsSinceEpoch); |
||||
} |
||||
|
||||
if (endDate != null) { |
||||
whereClauses.add('createdAt <= ?'); |
||||
whereArgs.add(endDate.millisecondsSinceEpoch); |
||||
} |
||||
|
||||
if (uploadStatus != '全部') { |
||||
whereClauses.add('isUploaded = ?'); |
||||
whereArgs.add(uploadStatus == '已上传' ? 1 : 0); |
||||
} |
||||
|
||||
if (bindStatus != '全部') { |
||||
if (bindStatus == '已绑定') { |
||||
whereClauses.add('censorTaskId IS NOT NULL'); |
||||
} else { |
||||
whereClauses.add('censorTaskId IS NULL'); |
||||
} |
||||
} |
||||
|
||||
final String whereString = whereClauses.join(' AND '); |
||||
|
||||
final List<Map<String, dynamic>> maps = await _database.query( |
||||
_tableName, |
||||
where: whereString.isEmpty ? null : whereString, |
||||
whereArgs: whereArgs.isEmpty ? null : whereArgs, |
||||
orderBy: 'createdAt DESC', |
||||
); |
||||
|
||||
return List.generate(maps.length, (i) { |
||||
return Problem.fromMap(maps[i]); |
||||
}); |
||||
} |
||||
|
||||
/// onClose 是 GetxService 的生命周期方法,在服务被销毁前调用。 |
||||
/// 虽然 GetxService 默认是永久的,但养成关闭数据库连接的习惯是很好的实践。 |
||||
@override |
||||
void onClose() { |
||||
_database.close(); |
||||
super.onClose(); |
||||
} |
||||
} |
@ -0,0 +1,147 @@
|
||||
// sqlite_provider.dart |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/data/models/enum_model.dart'; |
||||
import 'package:problem_check_system/data/models/problem_model.dart'; |
||||
import 'package:sqflite/sqflite.dart'; |
||||
import 'package:path/path.dart'; |
||||
import 'package:uuid/uuid.dart'; |
||||
|
||||
/// `SQLiteProvider` 是一个 GetxService,负责管理本地 SQLite 数据库。 |
||||
/// 作为一个单例服务,它在整个应用生命周期中只会被创建一次。 |
||||
class SQLiteProvider extends GetxService { |
||||
static const String _dbName = 'problems.db'; |
||||
static const String _tableName = 'problems'; |
||||
|
||||
/// 私有数据库实例,仅在服务内部访问。 |
||||
late Database _database; |
||||
|
||||
/// 在服务首次初始化时调用,用于执行异步设置。 |
||||
@override |
||||
void onInit() { |
||||
super.onInit(); |
||||
_initDatabase(); |
||||
} |
||||
|
||||
/// 异步初始化数据库连接。如果数据库不存在,则会创建它。 |
||||
Future<void> _initDatabase() async { |
||||
final databasePath = await getDatabasesPath(); |
||||
final path = join(databasePath, _dbName); |
||||
|
||||
_database = await openDatabase(path, version: 1, onCreate: _onCreate); |
||||
} |
||||
|
||||
/// 数据库创建时的回调函数,用于定义表结构。 |
||||
Future<void> _onCreate(Database db, int version) async { |
||||
await db.execute(''' |
||||
CREATE TABLE $_tableName( |
||||
id TEXT PRIMARY KEY, |
||||
description TEXT NOT NULL, |
||||
location TEXT NOT NULL, |
||||
imageUrls TEXT NOT NULL, |
||||
creationTime INTEGER NOT NULL, |
||||
syncStatus INTEGER NOT NULL, |
||||
censorTaskId TEXT, |
||||
bindData TEXT |
||||
) |
||||
'''); |
||||
} |
||||
|
||||
/// --- |
||||
|
||||
/// **数据操作 (CRUD) 方法** |
||||
|
||||
/// 向数据库中插入一个新问题。 |
||||
/// 如果 `problem` 没有 `id`,会自动生成一个唯一的 UUID。 |
||||
Future<int> insertProblem(Problem problem) async { |
||||
final problemToInsert = problem.copyWith( |
||||
id: problem.id ?? const Uuid().v4(), |
||||
); |
||||
return await _database.insert( |
||||
_tableName, |
||||
problemToInsert.toMap(), |
||||
conflictAlgorithm: ConflictAlgorithm.replace, |
||||
); |
||||
} |
||||
|
||||
/// 根据 ID 从数据库中删除一个问题。 |
||||
/// 返回被删除的行数。 |
||||
Future<int> deleteProblem(String id) async { |
||||
return await _database.delete(_tableName, where: 'id = ?', whereArgs: [id]); |
||||
} |
||||
|
||||
/// 更新数据库中已存在的问题。 |
||||
/// 返回被更新的行数。 |
||||
Future<int> updateProblem(Problem problem) async { |
||||
return await _database.update( |
||||
_tableName, |
||||
problem.toMap(), |
||||
where: 'id = ?', |
||||
whereArgs: [problem.id], |
||||
); |
||||
} |
||||
|
||||
/// 根据 ID 获取单个问题。 |
||||
/// 如果找到则返回 `Problem` 对象,否则返回 `null`。 |
||||
Future<Problem?> getProblemById(String id) async { |
||||
final List<Map<String, dynamic>> maps = await _database.query( |
||||
_tableName, |
||||
where: 'id = ?', |
||||
whereArgs: [id], |
||||
limit: 1, |
||||
); |
||||
|
||||
if (maps.isNotEmpty) { |
||||
return Problem.fromMap(maps.first); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/// 获取所有问题,支持按筛选条件查询。 |
||||
/// 可选参数用于筛选创建时间范围和同步状态。 |
||||
Future<List<Problem>> getProblems({ |
||||
DateTime? startDate, |
||||
DateTime? endDate, |
||||
SyncStatus? syncStatus, |
||||
}) async { |
||||
final List<String> whereClauses = []; |
||||
final List<dynamic> whereArgs = []; |
||||
|
||||
if (startDate != null) { |
||||
whereClauses.add('creationTime >= ?'); |
||||
whereArgs.add(startDate.millisecondsSinceEpoch); |
||||
} |
||||
|
||||
if (endDate != null) { |
||||
whereClauses.add('creationTime <= ?'); |
||||
whereArgs.add(endDate.millisecondsSinceEpoch); |
||||
} |
||||
|
||||
if (syncStatus != null) { |
||||
whereClauses.add('syncStatus = ?'); |
||||
whereArgs.add(syncStatus.index); |
||||
} |
||||
|
||||
final String? whereString = whereClauses.isNotEmpty |
||||
? whereClauses.join(' AND ') |
||||
: null; |
||||
|
||||
final List<Map<String, dynamic>> maps = await _database.query( |
||||
_tableName, |
||||
where: whereString, |
||||
whereArgs: whereArgs.isEmpty ? null : whereArgs, |
||||
orderBy: 'creationTime DESC', |
||||
); |
||||
|
||||
return maps.map((json) => Problem.fromMap(json)).toList(); |
||||
} |
||||
|
||||
/// --- |
||||
|
||||
/// `GetxService` 生命周期方法,在服务被销毁前调用, |
||||
/// 用于关闭数据库连接,防止资源泄漏。 |
||||
@override |
||||
void onClose() { |
||||
_database.close(); |
||||
super.onClose(); |
||||
} |
||||
} |
@ -0,0 +1,71 @@
|
||||
import 'dart:io'; |
||||
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/data/providers/http_provider.dart'; |
||||
|
||||
class FileRepository { |
||||
final HttpProvider _httpProvider = Get.find<HttpProvider>(); |
||||
|
||||
/// 上传图片文件到服务器。 |
||||
/// @param imageFile 要上传的本地图片文件。 |
||||
/// @param cancelToken 用于取消上传任务的令牌。 |
||||
/// @param onSendProgress 上传进度回调,提供已发送和总大小。 |
||||
/// @return 上传成功后服务器返回的图片 URL。 |
||||
Future<String> uploadImage( |
||||
File imageFile, { |
||||
required CancelToken cancelToken, |
||||
ProgressCallback? onSendProgress, |
||||
}) async { |
||||
try { |
||||
// 1. 创建 FormData 对象,用于构建 multipart/form-data 请求体 |
||||
final formData = FormData.fromMap({ |
||||
// 'file': 这通常是后端接口定义的文件字段名 |
||||
'file': await MultipartFile.fromFile( |
||||
imageFile.path, |
||||
filename: p.basename(imageFile.path), |
||||
), |
||||
}); |
||||
|
||||
// 2. 使用 HttpProvider 的 post 方法发送请求 |
||||
final response = await _httpProvider.post( |
||||
'/api/Objects/association/problem', |
||||
data: formData, |
||||
cancelToken: cancelToken, // 将取消令牌传递给 post 请求 |
||||
onSendProgress: onSendProgress, // 将进度回调传递给 post 请求 |
||||
); |
||||
|
||||
// --- 在这里打印服务器的完整响应结构 (仅在调试模式下) --- |
||||
if (kDebugMode) { |
||||
debugPrint('服务器返回的状态码: ${response.statusCode}'); |
||||
debugPrint('服务器返回的原始数据: ${response.data}'); |
||||
} |
||||
|
||||
// 3. 处理响应,并返回图片 URL |
||||
if (response.statusCode == 200) { |
||||
final Map<String, dynamic> data = response.data; |
||||
|
||||
// 假设服务器返回的图片 URL 字段名为 'url' |
||||
final imageUrl = data['url']; |
||||
|
||||
if (imageUrl is String && imageUrl.isNotEmpty) { |
||||
return imageUrl; |
||||
} else { |
||||
// 如果返回结构不符合预期,抛出自定义异常 |
||||
throw Exception('服务器响应中未找到有效的图片URL'); |
||||
} |
||||
} else { |
||||
// 对于非 200 状态码,抛出异常 |
||||
throw Exception('上传失败,状态码: ${response.statusCode}'); |
||||
} |
||||
} on DioException catch (e) { |
||||
// 捕获 Dio 异常并重新抛出,以便上层逻辑可以处理(如取消、超时等) |
||||
// 使用 rethrow 来保留原始的异常栈信息 |
||||
rethrow; |
||||
} catch (e) { |
||||
// 捕获其他未知错误 |
||||
throw Exception('图片上传发生未知错误: $e'); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue