Files
MaliangAINovalWriter/AINoval/lib/services/sync_service.dart
2025-09-10 00:07:52 +08:00

913 lines
33 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:ainoval/config/app_config.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/base/api_exception.dart';
import 'package:ainoval/services/local_storage_service.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
/// 数据同步服务
///
/// 负责在本地数据和远程API之间同步数据支持离线模式和冲突解决
class SyncService {
SyncService({
required this.apiService,
required this.localStorageService,
});
final ApiClient apiService;
final LocalStorageService localStorageService;
// 同步状态流
final _syncStateController = StreamController<SyncState>.broadcast();
Stream<SyncState> get syncStateStream => _syncStateController.stream;
// 当前同步状态
SyncState _currentState = SyncState.idle();
SyncState get currentState => _currentState;
// 网络连接监听器
StreamSubscription? _connectivitySubscription;
// 自动同步定时器
Timer? _autoSyncTimer;
// 服务是否已关闭
bool _isDisposed = false;
bool get isDisposed => _isDisposed;
/// 初始化同步服务
Future<void> init() async {
AppLogger.i('SyncService', '初始化同步服务');
// 监听网络连接状态
_connectivitySubscription =
Connectivity().onConnectivityChanged.listen((result) {
final isOnline = result != ConnectivityResult.none;
AppLogger.d('SyncService', '网络连接状态变化: ${isOnline ? "在线" : "离线"}');
_handleConnectivityChange(isOnline);
});
// 检查当前网络状态
final connectivityResult = await Connectivity().checkConnectivity();
final isOnline = connectivityResult != ConnectivityResult.none;
AppLogger.d('SyncService', '当前网络状态: ${isOnline ? "在线" : "离线"}');
_updateSyncState(isOnline: isOnline);
// 设置自动同步定时器
_setupAutoSync();
}
/// 设置自动同步
void _setupAutoSync() {
AppLogger.i('SyncService', '设置自动同步定时器每5分钟同步一次');
_autoSyncTimer?.cancel();
_autoSyncTimer = Timer.periodic(const Duration(minutes: 5), (_) async {
if (_currentState.isOnline) {
// 检查当前小说ID是否设置
final currentNovelId = await localStorageService.getCurrentNovelId();
if (currentNovelId == null) {
AppLogger.w('SyncService', '自动同步触发但无当前小说ID跳过');
return;
}
AppLogger.d('SyncService', '自动同步触发当前小说ID: $currentNovelId');
syncAll();
}
});
}
/// 处理网络连接变化
void _handleConnectivityChange(bool isOnline) {
// Store the previous online state before updating
final wasOffline = !_currentState.isOnline;
_updateSyncState(isOnline: isOnline);
// Trigger sync only when coming back online
if (isOnline && wasOffline) {
// 检查当前小说ID是否设置后再同步
localStorageService.getCurrentNovelId().then((currentNovelId) {
if (currentNovelId != null) {
AppLogger.i('SyncService', '网络恢复开始同步数据当前小说ID: $currentNovelId');
syncAll(); // syncAll will now also handle pending messages
} else {
AppLogger.w('SyncService', '网络恢复但无当前小说ID不执行自动同步');
}
});
}
}
/// 更新同步状态
void _updateSyncState({
bool? isOnline,
bool? isSyncing,
String? error,
double? progress,
}) {
// 如果服务已关闭,则不更新状态
if (_isDisposed) {
AppLogger.w('SyncService', '服务已关闭,忽略状态更新');
return;
}
_currentState = SyncState(
isOnline: isOnline ?? _currentState.isOnline,
isSyncing: isSyncing ?? _currentState.isSyncing,
error: error,
progress: progress ?? _currentState.progress,
);
_syncStateController.add(_currentState);
AppLogger.v('SyncService', '同步状态更新: $_currentState');
}
/// 同步所有数据
Future<bool> syncAll() async {
// 如果服务已关闭,直接返回
if (_isDisposed) {
AppLogger.w('SyncService', '服务已关闭,无法执行同步');
return false;
}
if (_currentState.isSyncing) {
AppLogger.w('SyncService', '同步已在进行中,跳过本次同步');
return false;
}
if (!_currentState.isOnline) {
AppLogger.w('SyncService', '无网络连接,无法同步');
_updateSyncState(error: '无网络连接,无法同步');
return false;
}
try {
AppLogger.i('SyncService', '开始全量数据同步');
_updateSyncState(
isSyncing: true, progress: 0.0, error: null); // Clear previous error
// --- Sync Pending Messages First ---
AppLogger.d('SyncService', '开始同步待发送消息');
await _syncPendingMessages();
_updateSyncState(progress: 0.1); // Adjust progress steps
// --- Sync other data types ---
AppLogger.d('SyncService', '开始同步小说数据');
await _syncNovels();
_updateSyncState(progress: 0.3);
AppLogger.d('SyncService', '开始同步场景内容');
await _syncScenes();
_updateSyncState(progress: 0.5);
AppLogger.d('SyncService', '开始同步编辑器内容');
await _syncEditorContents();
_updateSyncState(progress: 0.7);
// Sync Chat Session METADATA (title, etc.)
AppLogger.d('SyncService', '开始同步聊天会话元数据');
await _syncChatSessions(); // This now only syncs metadata
_updateSyncState(progress: 0.9); // Example progress
// --- Final step, maybe sync user profile or other settings ---
_updateSyncState(progress: 1.0); // Example finish
AppLogger.i('SyncService', '全量数据同步完成');
_updateSyncState(isSyncing: false, error: null); // Clear error on success
return true;
} catch (e, stackTrace) {
AppLogger.e('SyncService', '同步失败', e, stackTrace);
// Preserve isOnline state, set error
_updateSyncState(isSyncing: false, error: '同步失败: $e');
return false;
}
}
/// 同步小说数据
Future<void> _syncNovels() async {
try {
// 获取当前正在编辑的小说ID
final currentNovelId = await localStorageService.getCurrentNovelId();
if (currentNovelId == null) {
AppLogger.w('SyncService', '无当前小说ID跳过小说同步');
return;
}
final syncList = await localStorageService.getSyncList('novel');
AppLogger.d('SyncService', '需要同步的小说数量: ${syncList.length}');
// 筛选出当前小说
final novelIdsToSync = syncList.where((novelId) => novelId == currentNovelId).toList();
AppLogger.d('SyncService', '当前小说需要同步: ${novelIdsToSync.length} (当前小说ID: $currentNovelId)');
if (novelIdsToSync.isEmpty) {
AppLogger.i('SyncService', '当前小说不需要同步,跳过');
return;
}
for (final novelId in novelIdsToSync) {
final localNovel = await localStorageService.getNovel(novelId);
if (localNovel == null) {
AppLogger.w('SyncService', '本地小说不存在: $novelId');
continue;
}
AppLogger.i('SyncService', '同步小说: ${localNovel.title}($novelId)');
// 构建后端所需的小说数据结构
final backendNovelJson = {
'id': localNovel.id,
'title': localNovel.title,
'coverImage': localNovel.coverUrl,
// 确保包含作者信息
'author': localNovel.author?.toJson() ??
{
'id': AppConfig.userId ?? '',
'username': AppConfig.username ?? 'user'
},
'lastEditedChapterId': localNovel.lastEditedChapterId,
'createdAt': localNovel.createdAt.toIso8601String(),
'updatedAt': DateTime.now().toIso8601String(),
'structure': {
'acts': localNovel.acts
.map((act) => {
'id': act.id,
'title': act.title,
'order': act.order,
'chapters': act.chapters
.map((chapter) => {
'id': chapter.id,
'title': chapter.title,
'order': chapter.order,
// 注意章节中只需包含ID场景内容通过scenesByChapter单独提供
'sceneIds': chapter.scenes.map((scene) => scene.id).toList(),
})
.toList(),
})
.toList(),
},
};
// 组织场景数据,按章节分组
Map<String, List<Map<String, dynamic>>> scenesByChapter = {};
for (final act in localNovel.acts) {
for (final chapter in act.chapters) {
if (chapter.scenes.isNotEmpty) {
scenesByChapter[chapter.id] = chapter.scenes
.map((scene) => {
'id': scene.id,
'novelId': localNovel.id,
'chapterId': chapter.id,
'content': scene.content,
'summary': scene.summary.content,
'updatedAt': scene.lastEdited.toIso8601String(),
'version': scene.version,
'title': '',
'sequence': 0,
'sceneType': 'NORMAL',
})
.toList();
}
}
}
// 组装完整的请求数据
final novelWithScenesJson = {
'novel': backendNovelJson,
'scenesByChapter': scenesByChapter,
};
// 调用updateNovelWithScenes接口
await apiService.updateNovelWithScenes(novelWithScenesJson);
await localStorageService.clearSyncFlagByType('novel', novelId);
AppLogger.d('SyncService', '小说同步完成: $novelId');
}
} catch (e, stackTrace) {
AppLogger.e('SyncService', '同步小说数据失败', e, stackTrace);
throw SyncException('同步小说数据失败: $e');
}
}
/// 同步场景内容
Future<void> _syncScenes() async {
try {
// 获取当前正在编辑的小说ID
final currentNovelId = await localStorageService.getCurrentNovelId();
if (currentNovelId == null) {
AppLogger.w('SyncService', '无当前小说ID跳过场景同步');
return;
}
final syncList = await localStorageService.getSyncList('scene');
AppLogger.d('SyncService', '需要同步的场景数量: ${syncList.length}');
// 筛选出当前小说的场景
final scenesToSync = syncList.where((sceneKey) {
final parts = sceneKey.split('_');
return parts.length == 4 && parts[0] == currentNovelId;
}).toList();
AppLogger.d('SyncService', '当前小说的场景需要同步: ${scenesToSync.length} (当前小说ID: $currentNovelId)');
if (scenesToSync.isEmpty) {
AppLogger.i('SyncService', '当前小说没有场景需要同步,跳过');
return;
}
for (final sceneKey in scenesToSync) {
final parts = sceneKey.split('_');
if (parts.length != 4) {
AppLogger.w('SyncService', '无效的场景键格式: $sceneKey');
continue;
}
final novelId = parts[0];
final actId = parts[1];
final chapterId = parts[2];
final sceneId = parts[3];
final localScene = await localStorageService.getSceneContent(
novelId, actId, chapterId, sceneId);
if (localScene == null) {
AppLogger.w('SyncService', '本地场景不存在: $sceneKey');
continue;
}
AppLogger.i('SyncService', '同步场景: $sceneKey');
final sceneData = localScene.toJson();
await apiService.updateScene(sceneData);
await localStorageService.clearSyncFlagByType('scene', sceneKey);
AppLogger.d('SyncService', '场景同步完成: $sceneKey');
}
} catch (e, stackTrace) {
AppLogger.e('SyncService', '同步场景内容失败', e, stackTrace);
throw SyncException('同步场景内容失败: $e');
}
}
/// 同步编辑器内容
Future<void> _syncEditorContents() async {
try {
// 获取当前正在编辑的小说ID
final currentNovelId = await localStorageService.getCurrentNovelId();
if (currentNovelId == null) {
AppLogger.w('SyncService', '无当前小说ID跳过编辑器内容同步');
return;
}
final syncList = await localStorageService.getSyncList('editor');
AppLogger.d('SyncService', '需要同步的编辑器内容数量: ${syncList.length}');
// 筛选出当前小说的编辑器内容
final contentsToSync = syncList.where((contentKey) {
final parts = contentKey.split('_');
return parts.length >= 2 && parts[0] == currentNovelId;
}).toList();
AppLogger.d('SyncService', '当前小说的编辑器内容需要同步: ${contentsToSync.length} (当前小说ID: $currentNovelId)');
if (contentsToSync.isEmpty) {
AppLogger.i('SyncService', '当前小说没有编辑器内容需要同步,跳过');
return;
}
for (final contentKey in contentsToSync) {
final parts = contentKey.split('_');
if (parts.length < 2) {
AppLogger.w('SyncService', '无效的编辑器内容键格式: $contentKey');
continue;
}
final novelId = parts[0];
final chapterId = parts[1];
final localContent =
await localStorageService.getEditorContent(novelId, chapterId, '');
if (localContent == null) {
AppLogger.w('SyncService', '本地编辑器内容不存在: $contentKey');
continue;
}
AppLogger.i('SyncService', '同步编辑器内容: $contentKey');
await apiService.saveEditorContent(
novelId, chapterId, localContent.toJson());
await localStorageService.clearSyncFlagByType('editor', contentKey);
AppLogger.d('SyncService', '编辑器内容同步完成: $contentKey');
}
} catch (e, stackTrace) {
AppLogger.e('SyncService', '同步编辑器内容失败', e, stackTrace);
throw SyncException('同步编辑器内容失败: $e');
}
}
/// 同步聊天会话元数据 (No longer sends full message history)
Future<void> _syncChatSessions() async {
// This method now only syncs session metadata like title, updatedAt
try {
// 获取当前正在编辑的小说ID
final currentNovelId = await localStorageService.getCurrentNovelId();
if (currentNovelId == null) {
AppLogger.w('SyncService', '无当前小说ID跳过聊天会话同步');
return;
}
final sessions = await localStorageService.getSessionsToSync();
AppLogger.d('SyncService', '需要同步的聊天会话元数据数量: ${sessions.length}');
// 筛选出当前小说的聊天会话
// 注意:这里假设 ChatSession 模型有 novelId 属性,如果没有,需要调整过滤逻辑
final sessionsToSync = sessions.where((session) =>
session.metadata != null &&
session.metadata!['novelId'] == currentNovelId).toList();
AppLogger.d('SyncService', '当前小说的聊天会话需要同步: ${sessionsToSync.length} (当前小说ID: $currentNovelId)');
if (sessionsToSync.isEmpty) {
AppLogger.i('SyncService', '当前小说没有聊天会话需要同步,跳过');
return;
}
// No need for userId here if updateSession API only updates metadata
// If updateSession *requires* userId, get it once:
// final String currentUserId = await _getCurrentUserId();
for (final session in sessionsToSync) {
AppLogger.i('SyncService', '同步聊天会话元数据: ${session.id}');
// Construct updates payload - only include fields managed locally
// that need syncing (like title if user can rename offline)
final Map<String, dynamic> updates = {
'title': session.title,
// Include lastUpdatedAt from local session to inform server?
// 'updatedAt': session.lastUpdatedAt.toIso8601String(),
// Or maybe server handles updatedAt automatically on update?
};
// Only call update if there are actual updates to send
if (updates.isNotEmpty) {
// If updateSession requires userId, pass it: userId: currentUserId,
await apiService.updateAiChatSession(
userId: await _getCurrentUserId(), // Get userId if needed by API
sessionId: session.id,
updates: updates,
);
} // Else: Session might be marked for sync without local changes, skip API call?
// REMOVED: Loop sending messages using getMessagesForSession
await localStorageService.clearSyncFlagByType(
'chat_session', session.id);
AppLogger.d('SyncService', '聊天会话元数据同步完成: ${session.id}');
}
} catch (e, stackTrace) {
// Don't throw - allow other sync tasks to proceed if possible
AppLogger.e('SyncService', '同步聊天会话元数据失败', e, stackTrace);
// Optionally update state with a non-fatal error?
// _updateSyncState(error: '部分同步失败: 聊天会话元数据'); // Be careful not to overwrite fatal errors
}
}
/// --- New Method: Sync Pending Chat Messages ---
Future<void> _syncPendingMessages() async {
try {
// 如果服务已关闭,直接返回
if (_isDisposed) {
AppLogger.w('SyncService', '服务已关闭,无法同步待发送消息');
return;
}
// 获取当前正在编辑的小说ID
final currentNovelId = await localStorageService.getCurrentNovelId();
if (currentNovelId == null) {
AppLogger.w('SyncService', '无当前小说ID跳过待发送消息同步');
return;
}
final pendingMessages = await localStorageService.getPendingMessages();
if (pendingMessages.isEmpty) {
AppLogger.d('SyncService', '没有待发送的消息。');
return;
}
// 筛选出当前小说的待发送消息
final messagesToSync = pendingMessages.where((message) {
// 检查消息元数据中是否包含小说ID
final metadata = message['metadata'] as Map<String, dynamic>?;
return metadata != null && metadata['novelId'] == currentNovelId;
}).toList();
if (messagesToSync.isEmpty) {
AppLogger.i('SyncService', '当前小说没有待发送消息需要同步,跳过');
return;
}
AppLogger.i('SyncService', '开始处理 ${messagesToSync.length} 条当前小说的待发送消息。 (当前小说ID: $currentNovelId)');
final String currentUserId =
await _getCurrentUserId(); // Get User ID once
for (final messageData in messagesToSync) {
final localId = messageData['localId'] as String?;
final sessionId = messageData['sessionId'] as String?;
final content = messageData['content'] as String?;
final metadata = messageData['metadata'] as Map<String, dynamic>?;
// Important: Use the userId stored with the message if available,
// otherwise use currentUserId. This handles cases where sync might
// happen after user logout/login, though ideally pending messages
// should be cleared on logout.
final userIdToSend = messageData['userId'] as String? ?? currentUserId;
if (localId == null || sessionId == null || content == null) {
AppLogger.e('SyncService', '待发送消息数据不完整,跳过: $messageData');
// Optionally remove corrupted data
// if (localId != null) await localStorageService.removePendingMessage(localId);
continue;
}
try {
AppLogger.d(
'SyncService', '尝试发送消息: localId=$localId, sessionId=$sessionId');
// Call the actual API to send the message
await apiService.sendAiChatMessage(
userId: userIdToSend,
sessionId: sessionId,
content: content,
metadata: metadata,
);
// If sendMessage succeeds, remove from local pending queue
await localStorageService.removePendingMessage(localId);
AppLogger.i('SyncService', '成功发送并移除待发送消息: localId=$localId');
// OPTIONAL: Add the successfully sent message to the local history cache
// This requires constructing a proper ChatMessage object from the response
// or assuming success and creating one locally. This is complex.
// It might be simpler to rely on fetching history later.
} on ApiException catch (apiError, stack) {
// Catch specific API errors
AppLogger.e(
'SyncService',
'发送待处理消息失败 (API Error $localId): ${apiError.message}',
apiError,
stack);
// Decide if error is temporary or permanent.
// For now, leave in queue and retry later.
// If 4xx error, maybe remove from queue?
if (apiError.statusCode >= 400 &&
apiError.statusCode < 500 &&
apiError.statusCode != 401 &&
apiError.statusCode != 429) {
AppLogger.w('SyncService',
'接收到客户端错误 (${apiError.statusCode}),可能移除待发送消息 $localId');
// Consider removing permanently failed message
// await localStorageService.removePendingMessage(localId);
}
} catch (e, stackTrace) {
// Catch other errors
AppLogger.e(
'SyncService', '发送待处理消息时发生未知错误 ($localId)', e, stackTrace);
// Leave in queue for retry
}
}
AppLogger.i('SyncService', '处理待发送消息完成。');
} catch (e, stackTrace) {
// Error fetching or processing the queue itself
AppLogger.e('SyncService', '处理待发送消息队列时出错', e, stackTrace);
// Don't throw, allow other sync tasks. Update state?
// _updateSyncState(error: '部分同步失败: 待发送消息');
}
}
/// 同步单个小说
Future<bool> syncNovel(String novelId) async {
// 如果服务已关闭,直接返回
if (_isDisposed) {
AppLogger.w('SyncService', '服务已关闭,无法同步小说');
return false;
}
if (!_currentState.isOnline) {
_updateSyncState(error: '无网络连接,无法同步');
return false;
}
try {
// 获取本地小说
final localNovel = await localStorageService.getNovel(novelId);
if (localNovel == null) return false;
// 构建后端所需的小说数据结构
final backendNovelJson = {
'id': localNovel.id,
'title': localNovel.title,
'coverImage': localNovel.coverUrl,
// 确保包含作者信息
'author': localNovel.author?.toJson() ??
{
'id': AppConfig.userId ?? '',
'username': AppConfig.username ?? 'user'
},
'lastEditedChapterId': localNovel.lastEditedChapterId,
'createdAt': localNovel.createdAt.toIso8601String(),
'updatedAt': DateTime.now().toIso8601String(),
'structure': {
'acts': localNovel.acts
.map((act) => {
'id': act.id,
'title': act.title,
'order': act.order,
'chapters': act.chapters
.map((chapter) => {
'id': chapter.id,
'title': chapter.title,
'order': chapter.order,
// 注意章节中只需包含ID场景内容通过scenesByChapter单独提供
'sceneIds': chapter.scenes.map((scene) => scene.id).toList(),
})
.toList(),
})
.toList(),
},
};
// 组织场景数据,按章节分组
Map<String, List<Map<String, dynamic>>> scenesByChapter = {};
for (final act in localNovel.acts) {
for (final chapter in act.chapters) {
if (chapter.scenes.isNotEmpty) {
scenesByChapter[chapter.id] = chapter.scenes
.map((scene) => {
'id': scene.id,
'novelId': localNovel.id,
'chapterId': chapter.id,
'content': scene.content,
'summary': scene.summary.content,
'updatedAt': scene.lastEdited.toIso8601String(),
'version': scene.version,
'title': '',
'sequence': 0,
'sceneType': 'NORMAL',
})
.toList();
}
}
}
// 组装完整的请求数据
final novelWithScenesJson = {
'novel': backendNovelJson,
'scenesByChapter': scenesByChapter,
};
// 调用updateNovelWithScenes接口
await apiService.updateNovelWithScenes(novelWithScenesJson);
// 标记为已同步
await localStorageService.clearSyncFlagByType('novel', novelId);
return true;
} catch (e) {
AppLogger.e('Services/sync_service', '同步小说失败', e);
_updateSyncState(error: '同步小说失败: $e');
return false;
}
}
/// 同步单个场景
Future<bool> syncScene(
String novelId, String actId, String chapterId, String sceneId) async {
// 如果服务已关闭,直接返回
if (_isDisposed) {
AppLogger.w('SyncService', '服务已关闭,无法同步场景');
return false;
}
if (!_currentState.isOnline) {
_updateSyncState(error: '无网络连接,无法同步');
return false;
}
try {
// 获取本地场景
final localScene = await localStorageService.getSceneContent(
novelId, actId, chapterId, sceneId);
if (localScene == null) return false;
// 上传到服务器
final sceneData = localScene.toJson();
await apiService.updateScene(sceneData);
// 标记为已同步
final sceneKey = '${novelId}_${actId}_${chapterId}_$sceneId';
await localStorageService.clearSyncFlagByType('scene', sceneKey);
return true;
} catch (e) {
AppLogger.e('Services/sync_service', '同步场景失败', e);
_updateSyncState(error: '同步场景失败: $e');
return false;
}
}
/// 同步单个编辑器内容
Future<bool> syncEditorContent(String novelId, String chapterId,
String sceneId) async {
// 如果服务已关闭,直接返回
if (_isDisposed) {
AppLogger.w('SyncService', '服务已关闭,无法同步编辑器内容');
return false;
}
if (!_currentState.isOnline) {
_updateSyncState(error: '无网络连接,无法同步');
return false;
}
try {
// 获取本地编辑器内容
final localContent = await localStorageService.getEditorContent(
novelId, chapterId, ''); // 传递空的 sceneId 或适配
if (localContent == null) return false;
// 上传到服务器
await apiService.saveEditorContent(
novelId, chapterId, localContent.toJson());
// 标记为已同步
final contentKey = '${novelId}_$chapterId'; // 调整 key
await localStorageService.clearSyncFlagByType('editor', contentKey);
return true;
} catch (e) {
AppLogger.e('Services/sync_service', '同步编辑器内容失败', e);
_updateSyncState(error: '同步编辑器内容失败: $e');
return false;
}
}
/// 同步单个聊天会话元数据 (不再发送消息历史)
Future<bool> syncChatSession(String sessionId) async {
// 如果服务已关闭,直接返回
if (_isDisposed) {
AppLogger.w('SyncService', '服务已关闭,无法同步聊天会话');
return false;
}
if (!_currentState.isOnline) {
_updateSyncState(error: '无网络连接,无法同步');
return false;
}
try {
final session = await localStorageService.getChatSession(sessionId);
if (session == null) {
AppLogger.w('SyncService', '尝试同步单个会话元数据,但本地未找到: $sessionId');
await localStorageService.clearSyncFlagByType(
'chat_session', sessionId); // 清除无效标记
return false; // 无法同步不存在的会话
}
// 同步元数据 (例如: title)
final Map<String, dynamic> updates = {'title': session.title};
if (updates.isNotEmpty) {
await apiService.updateAiChatSession(
userId: await _getCurrentUserId(), // 如果 API 需要,获取 userId
sessionId: session.id,
updates: updates,
);
AppLogger.d('SyncService', '单个聊天会话元数据 API 更新调用完成: $sessionId');
} else {
AppLogger.d('SyncService', '单个聊天会话 ${session.id} 没有需要同步的元数据更新');
}
// ======== 移除: 不再通过 getMessagesForSession 循环发送消息 ========
// 清除此会话的元数据同步标记
await localStorageService.clearSyncFlagByType('chat_session', sessionId);
AppLogger.i('SyncService', '单个聊天会话元数据同步处理完成: $sessionId');
return true;
} catch (e, stackTrace) {
// 捕获所有可能的错误
AppLogger.e('SyncService', '同步单个聊天会话元数据失败 ($sessionId)', e, stackTrace);
_updateSyncState(error: '同步单个聊天会话元数据失败: $e');
// 同步失败,暂时不清除标记,留待下次重试
return false;
}
}
/// 解决冲突 (需要根据聊天数据的具体冲突场景来完善)
/// 关闭服务,释放资源
void dispose() {
// 设置已关闭标志
_isDisposed = true;
// 取消网络监听和定时器
_connectivitySubscription?.cancel();
_autoSyncTimer?.cancel();
// 关闭状态流
if (!_syncStateController.isClosed) {
_syncStateController.close();
}
// 清除当前小说ID避免后续同步错误
localStorageService.setCurrentNovelId('').then((_) {
AppLogger.i('SyncService', '同步服务已关闭清除当前小说ID');
});
AppLogger.i('SyncService', '同步服务已关闭');
}
/// 获取当前用户ID (使用 AppConfig 实现)
Future<String> _getCurrentUserId() async {
final userId = AppConfig.userId; // 从 AppConfig 获取用户ID
if (userId == null || userId.isEmpty) {
AppLogger.e('SyncService', '无法获取当前用户ID同步操作可能失败或无法执行。');
// 根据需求,可以抛出异常或返回占位符/空字符串
// 如果 userId 是必需的,抛出异常更安全
throw SyncException('无法获取当前用户ID无法执行需要用户ID的同步操作。');
}
return userId;
}
/// 直接设置当前小说ID
Future<void> setCurrentNovelId(String novelId) async {
// 即使服务已关闭也允许设置,但记录警告
if (_isDisposed) {
AppLogger.w('SyncService', '尝试在服务已关闭状态下设置当前小说ID: $novelId');
// 考虑到可能在关闭过程中调用此方法,仍然允许操作继续
}
await localStorageService.setCurrentNovelId(novelId);
AppLogger.i('SyncService', '同步服务已设置当前小说ID: $novelId');
}
}
/// 同步状态类
class SyncState {
SyncState({
required this.isOnline, // 网络是否连接
required this.isSyncing, // 是否正在同步中
this.error, // 同步错误信息null表示无错误
this.progress = 0.0, // 同步进度 (0.0 到 1.0)
});
/// 空闲状态 (默认在线)
factory SyncState.idle({bool online = true}) {
return SyncState(
isOnline: online,
isSyncing: false,
);
}
/// 同步中状态
factory SyncState.syncing({double progress = 0.0}) {
return SyncState(
isOnline: true, // 同步时必须在线
isSyncing: true,
progress: progress,
);
}
/// 离线状态
factory SyncState.offline() {
return SyncState(
isOnline: false,
isSyncing: false, // 离线时不能同步
);
}
/// 错误状态 (允许指定当时的网络状态)
factory SyncState.error(String errorMessage, {bool online = true}) {
return SyncState(
isOnline: online, // 错误可能在线或离线时发生
isSyncing: false, // 出错时停止同步
error: errorMessage,
);
}
final bool isOnline;
final bool isSyncing;
final String? error;
final double progress;
@override
String toString() {
// 提供更清晰的状态描述
return 'SyncState(在线: $isOnline, 同步中: $isSyncing, 进度: ${progress.toStringAsFixed(2)}, 错误: ${error ?? ""})';
}
}
/// 同步异常类
class SyncException implements Exception {
SyncException(this.message);
final String message; // 异常信息
@override
String toString() => 'SyncException: $message';
}