Files
xianyan/lib/features/file_transfer/services/offline_queue_service.dart
Developer 228095f80a chore: 完成v6.7.0版本迭代更新
本次更新涵盖多个功能模块的优化与新增:
1. 新增Wi-Fi直连配对方式与协作画布模块
2. 完成设备管理重命名API与前端适配
3. 优化日签卡片空数据保护与UI细节
4. 新增剪贴板工具与每日运势会话
5. 修复应用锁恢复、语音消息录制等已知问题
6. 完善文件传输统计与配对逻辑
7. 更新安卓权限配置与iOS隐私描述
8. 新增自动化测试脚本与文档
9. 清理旧版审计报告与测试文件
2026-05-14 05:35:18 +08:00

259 lines
7.6 KiB
Dart

// ============================================================
// 闲言APP — 离线队列服务
// 创建时间: 2026-05-14
// 更新时间: 2026-05-14
// 作用: 离线暂存+自动重发+状态监控 — 消息/文件离线队列管理
// 上次更新: v6.4.0 初始创建
// ============================================================
import 'dart:async';
import 'package:uuid/uuid.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/features/file_transfer/database/transfer_database.dart';
import 'package:xianyan/features/file_transfer/models/offline_queue_item.dart';
typedef OnResendCallback = Future<bool> Function(OfflineQueueItem item);
class OfflineQueueService {
OfflineQueueService({required TransferDatabase db}) : _db = db;
final TransferDatabase _db;
final _uuid = const Uuid();
final _statusController = StreamController<OfflineQueueStatus>.broadcast();
OnResendCallback? _onResend;
Stream<OfflineQueueStatus> get onStatusChanged => _statusController.stream;
void setResendCallback(OnResendCallback callback) {
_onResend = callback;
}
Future<OfflineQueueItem> enqueueMessage({
required String targetDeviceId,
required String content,
String? messageId,
String? deviceAlias,
}) async {
final item = OfflineQueueItem(
id: _uuid.v4(),
targetDeviceId: targetDeviceId,
type: OfflineQueueItemType.text,
content: content,
status: OfflineQueueItemStatus.pending,
createdAt: DateTime.now(),
messageId: messageId,
deviceAlias: deviceAlias,
);
await _db.insertOfflineQueueItem(item);
Log.i('OfflineQueue: Enqueued text message for $targetDeviceId');
_notifyStatus();
return item;
}
Future<OfflineQueueItem> enqueueFile({
required String targetDeviceId,
required String filePath,
required String fileName,
int? fileSize,
String? deviceAlias,
}) async {
final item = OfflineQueueItem(
id: _uuid.v4(),
targetDeviceId: targetDeviceId,
type: OfflineQueueItemType.file,
filePath: filePath,
fileName: fileName,
fileSize: fileSize,
status: OfflineQueueItemStatus.pending,
createdAt: DateTime.now(),
deviceAlias: deviceAlias,
);
await _db.insertOfflineQueueItem(item);
Log.i('OfflineQueue: Enqueued file "$fileName" for $targetDeviceId');
_notifyStatus();
return item;
}
Future<OfflineQueueItem> enqueueVoice({
required String targetDeviceId,
required String filePath,
String? deviceAlias,
}) async {
final item = OfflineQueueItem(
id: _uuid.v4(),
targetDeviceId: targetDeviceId,
type: OfflineQueueItemType.voice,
filePath: filePath,
fileName: 'voice_message.m4a',
status: OfflineQueueItemStatus.pending,
createdAt: DateTime.now(),
deviceAlias: deviceAlias,
);
await _db.insertOfflineQueueItem(item);
Log.i('OfflineQueue: Enqueued voice message for $targetDeviceId');
_notifyStatus();
return item;
}
Future<List<OfflineQueueItem>> getPendingItems(String deviceId) async {
return _db.getPendingOfflineItems(deviceId);
}
Future<List<OfflineQueueItem>> getAllPendingItems() async {
return _db.getAllPendingOfflineItems();
}
Future<int> getPendingCount(String deviceId) async {
return _db.getPendingOfflineCount(deviceId);
}
Future<int> getTotalPendingCount() async {
return _db.getTotalPendingCount();
}
Future<void> markAsSent(String id) async {
await _db.updateOfflineQueueItemStatus(id, OfflineQueueItemStatus.sent);
Log.i('OfflineQueue: Item $id marked as sent');
_notifyStatus();
}
Future<void> markAsFailed(String id, String error) async {
final items = await _db.getAllOfflineItems();
final item = items.where((i) => i.id == id).firstOrNull;
final newRetryCount = (item?.retryCount ?? 0) + 1;
if (newRetryCount >= OfflineQueueItem.maxRetryCount) {
await _db.updateOfflineQueueItemStatus(
id,
OfflineQueueItemStatus.failed,
errorMessage: '$error (已达最大重试次数)',
retryCount: newRetryCount,
);
Log.w('OfflineQueue: Item $id failed permanently after $newRetryCount retries');
} else {
await _db.updateOfflineQueueItemStatus(
id,
OfflineQueueItemStatus.failed,
errorMessage: error,
retryCount: newRetryCount,
);
Log.w('OfflineQueue: Item $id failed (retry $newRetryCount/${OfflineQueueItem.maxRetryCount}): $error');
}
_notifyStatus();
}
Future<void> markAsSending(String id) async {
await _db.updateOfflineQueueItemStatus(id, OfflineQueueItemStatus.sending);
_notifyStatus();
}
Future<void> deleteItem(String id) async {
await _db.deleteOfflineQueueItem(id);
_notifyStatus();
}
Future<void> clearPendingForDevice(String deviceId) async {
await _db.deleteOfflineItemsByDevice(deviceId);
Log.i('OfflineQueue: Cleared pending items for $deviceId');
_notifyStatus();
}
Future<void> cleanOldItems({int maxAgeDays = 7}) async {
await _db.cleanOldOfflineItems(maxAgeDays: maxAgeDays);
Log.i('OfflineQueue: Cleaned items older than $maxAgeDays days');
_notifyStatus();
}
Future<void> onDeviceOnline(String deviceId) async {
Log.i('OfflineQueue: Device $deviceId came online, processing pending items');
final pendingItems = await _db.getPendingOfflineItems(deviceId);
if (pendingItems.isEmpty) {
Log.i('OfflineQueue: No pending items for $deviceId');
return;
}
Log.i('OfflineQueue: Processing ${pendingItems.length} pending items for $deviceId');
for (final item in pendingItems) {
if (!item.status.isPending && !item.status.isFailed) continue;
if (item.status.isFailed && !item.canRetry) continue;
await _resendItem(item);
}
}
Future<bool> _resendItem(OfflineQueueItem item) async {
if (_onResend == null) {
Log.w('OfflineQueue: No resend callback set, skipping ${item.id}');
return false;
}
await markAsSending(item.id);
try {
final success = await _onResend!(item);
if (success) {
await markAsSent(item.id);
return true;
} else {
await markAsFailed(item.id, '重发返回失败');
return false;
}
} catch (e) {
await markAsFailed(item.id, e.toString());
return false;
}
}
Future<OfflineQueueStatus> getCurrentStatus() async {
final allItems = await _db.getAllOfflineItems();
final byDevice = <String, List<OfflineQueueItem>>{};
for (final item in allItems) {
byDevice.putIfAbsent(item.targetDeviceId, () => []).add(item);
}
final totalPending = allItems.where((i) => !i.status.isTerminal).length;
return OfflineQueueStatus(
itemsByDevice: byDevice,
totalPendingCount: totalPending,
);
}
void _notifyStatus() {
getCurrentStatus().then((status) {
if (!_statusController.isClosed) {
_statusController.add(status);
}
}).catchError((Object e) {
Log.w('OfflineQueue: Failed to notify status: $e');
});
}
Future<void> dispose() async {
await _statusController.close();
}
}
class OfflineQueueStatus {
const OfflineQueueStatus({
required this.itemsByDevice,
required this.totalPendingCount,
});
final Map<String, List<OfflineQueueItem>> itemsByDevice;
final int totalPendingCount;
List<OfflineQueueItem> pendingItemsForDevice(String deviceId) {
return itemsByDevice[deviceId]
?.where((i) => !i.status.isTerminal)
.toList() ??
[];
}
int pendingCountForDevice(String deviceId) {
return pendingItemsForDevice(deviceId).length;
}
}