Files
AutoJudge-Flutter/lib/services/storage_service.dart
2025-11-13 09:14:49 +08:00

326 lines
9.7 KiB
Dart

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/evaluation_history.dart';
/// Service for managing local storage using SharedPreferences
///
/// Handles storage of evaluation history, cache management,
/// and other persistent data (excluding secure credentials)
///
/// Usage example:
/// ```dart
/// final storageService = StorageService();
///
/// // Save evaluation history
/// await storageService.saveEvaluationHistory(history);
///
/// // Load evaluation history
/// final histories = await storageService.loadEvaluationHistory();
///
/// // Clear all data
/// await storageService.clearAllData();
/// ```
class StorageService {
static const String _evaluationHistoryKey = 'evaluation_history';
static const String _lastSyncTimeKey = 'last_sync_time';
static const String _cacheVersionKey = 'cache_version';
static const int _maxHistoryItems = 100; // Maximum history items to keep
/// Save evaluation history to local storage
///
/// Appends new history item to existing list
/// Automatically trims list if it exceeds max items
///
/// [history] - The evaluation history to save
Future<void> saveEvaluationHistory(EvaluationHistory history) async {
try {
final prefs = await SharedPreferences.getInstance();
// Load existing history
final histories = await loadEvaluationHistory();
// Add new history at the beginning
histories.insert(0, history);
// Trim if exceeds max items
if (histories.length > _maxHistoryItems) {
histories.removeRange(_maxHistoryItems, histories.length);
}
// Convert to JSON and save
final jsonList = histories.map((h) => h.toJson()).toList();
final jsonString = jsonEncode(jsonList);
await prefs.setString(_evaluationHistoryKey, jsonString);
} catch (e) {
throw StorageException('Failed to save evaluation history: $e');
}
}
/// Save multiple evaluation histories at once
///
/// Useful for batch operations
///
/// [histories] - List of evaluation histories to save
Future<void> saveEvaluationHistories(
List<EvaluationHistory> histories,
) async {
try {
final prefs = await SharedPreferences.getInstance();
// Load existing history
final existingHistories = await loadEvaluationHistory();
// Merge new histories at the beginning
final mergedHistories = [...histories, ...existingHistories];
// Trim if exceeds max items
final trimmedHistories = mergedHistories.length > _maxHistoryItems
? mergedHistories.sublist(0, _maxHistoryItems)
: mergedHistories;
// Convert to JSON and save
final jsonList = trimmedHistories.map((h) => h.toJson()).toList();
final jsonString = jsonEncode(jsonList);
await prefs.setString(_evaluationHistoryKey, jsonString);
} catch (e) {
throw StorageException('Failed to save evaluation histories: $e');
}
}
/// Load evaluation history from local storage
///
/// Returns list of evaluation histories sorted by timestamp (newest first)
/// Returns empty list if no history exists
Future<List<EvaluationHistory>> loadEvaluationHistory() async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_evaluationHistoryKey);
if (jsonString == null || jsonString.isEmpty) {
return [];
}
final jsonList = jsonDecode(jsonString) as List<dynamic>;
final histories = jsonList
.map(
(json) => EvaluationHistory.fromJson(json as Map<String, dynamic>),
)
.toList();
// Sort by timestamp (newest first)
histories.sort((a, b) => b.timestamp.compareTo(a.timestamp));
return histories;
} catch (e) {
throw StorageException('Failed to load evaluation history: $e');
}
}
/// Get evaluation history for a specific course
///
/// [courseId] - The course ID to filter by
/// Returns list of histories for the specified course
Future<List<EvaluationHistory>> getHistoryByCourse(String courseId) async {
try {
final allHistories = await loadEvaluationHistory();
return allHistories.where((h) => h.course.id == courseId).toList();
} catch (e) {
throw StorageException('Failed to get history by course: $e');
}
}
/// Get successful evaluation count
///
/// Returns the total number of successful evaluations
Future<int> getSuccessfulEvaluationCount() async {
try {
final histories = await loadEvaluationHistory();
return histories.where((h) => h.success).length;
} catch (e) {
throw StorageException('Failed to get successful evaluation count: $e');
}
}
/// Get failed evaluation count
///
/// Returns the total number of failed evaluations
Future<int> getFailedEvaluationCount() async {
try {
final histories = await loadEvaluationHistory();
return histories.where((h) => !h.success).length;
} catch (e) {
throw StorageException('Failed to get failed evaluation count: $e');
}
}
/// Clear evaluation history
///
/// Removes all stored evaluation history
Future<void> clearEvaluationHistory() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_evaluationHistoryKey);
} catch (e) {
throw StorageException('Failed to clear evaluation history: $e');
}
}
/// Save last sync time
///
/// Records when the last data synchronization occurred
///
/// [time] - The timestamp to save
Future<void> saveLastSyncTime(DateTime time) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_lastSyncTimeKey, time.toIso8601String());
} catch (e) {
throw StorageException('Failed to save last sync time: $e');
}
}
/// Load last sync time
///
/// Returns the timestamp of the last synchronization
/// Returns null if no sync has occurred
Future<DateTime?> loadLastSyncTime() async {
try {
final prefs = await SharedPreferences.getInstance();
final timeString = prefs.getString(_lastSyncTimeKey);
if (timeString == null) {
return null;
}
return DateTime.parse(timeString);
} catch (e) {
throw StorageException('Failed to load last sync time: $e');
}
}
/// Get cache version
///
/// Returns the current cache version number
/// Used for cache invalidation when data structure changes
Future<int> getCacheVersion() async {
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt(_cacheVersionKey) ?? 1;
} catch (e) {
throw StorageException('Failed to get cache version: $e');
}
}
/// Set cache version
///
/// Updates the cache version number
///
/// [version] - The new version number
Future<void> setCacheVersion(int version) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_cacheVersionKey, version);
} catch (e) {
throw StorageException('Failed to set cache version: $e');
}
}
/// Clear cache if version mismatch
///
/// Compares current cache version with expected version
/// Clears cache if they don't match
///
/// [expectedVersion] - The expected cache version
/// Returns true if cache was cleared, false otherwise
Future<bool> clearCacheIfVersionMismatch(int expectedVersion) async {
try {
final currentVersion = await getCacheVersion();
if (currentVersion != expectedVersion) {
await clearEvaluationHistory();
await setCacheVersion(expectedVersion);
return true;
}
return false;
} catch (e) {
throw StorageException('Failed to check cache version: $e');
}
}
/// Clear all local data
///
/// Removes all data stored by this service
/// Does NOT clear secure storage (credentials)
/// Does NOT clear theme preferences
Future<void> clearAllData() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_evaluationHistoryKey);
await prefs.remove(_lastSyncTimeKey);
// Keep cache version to maintain compatibility
} catch (e) {
throw StorageException('Failed to clear all data: $e');
}
}
/// Get storage statistics
///
/// Returns information about stored data
Future<StorageStats> getStorageStats() async {
try {
final histories = await loadEvaluationHistory();
final lastSync = await loadLastSyncTime();
final cacheVersion = await getCacheVersion();
return StorageStats(
totalHistoryItems: histories.length,
successfulEvaluations: histories.where((h) => h.success).length,
failedEvaluations: histories.where((h) => !h.success).length,
lastSyncTime: lastSync,
cacheVersion: cacheVersion,
);
} catch (e) {
throw StorageException('Failed to get storage stats: $e');
}
}
}
/// Storage statistics data class
class StorageStats {
final int totalHistoryItems;
final int successfulEvaluations;
final int failedEvaluations;
final DateTime? lastSyncTime;
final int cacheVersion;
StorageStats({
required this.totalHistoryItems,
required this.successfulEvaluations,
required this.failedEvaluations,
this.lastSyncTime,
required this.cacheVersion,
});
@override
String toString() {
return 'StorageStats('
'total: $totalHistoryItems, '
'success: $successfulEvaluations, '
'failed: $failedEvaluations, '
'lastSync: $lastSyncTime, '
'cacheVersion: $cacheVersion'
')';
}
}
/// Exception thrown when storage operations fail
class StorageException implements Exception {
final String message;
StorageException(this.message);
@override
String toString() => 'StorageException: $message';
}