1. 新增 pnpm start:dev / pnpm start:stop 命令 - scripts/dev.mjs: 跨平台启动脚本(后端+管理端+学生端) - scripts/stop.mjs: 端口清理停止脚本 - 根 package.json 定义 pnpm 脚本 2. 修复 Flutter Web 编译(Isar 3.x + flutter_secure_storage 不兼容) - isar_database: 条件导出,Web 用空 stub - isar_journal_repository: 条件导出,Web 用空 stub - sync_engine: 条件导出,Web 用内存队列(无 Isar 持久化) - 移除 flutter_secure_storage(v9 web 插件用 dart:html) - 新增 SecureTokenStore 接口 + shared_preferences 实现 - auth_repository 改用 SecureTokenStore 接口
214 lines
5.5 KiB
Dart
214 lines
5.5 KiB
Dart
// 同步引擎 — Web 平台实现(无 Isar 持久化)
|
|
//
|
|
// Web 平台上 Isar 不可用,操作队列仅保存在内存中。
|
|
// 核心同步逻辑与原生版一致,仅持久化部分为空实现。
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:collection';
|
|
|
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../remote/api_client.dart';
|
|
|
|
/// 同步操作类型
|
|
enum SyncOperationType {
|
|
create('POST'),
|
|
update('PUT'),
|
|
delete('DELETE');
|
|
|
|
const SyncOperationType(this.httpMethod);
|
|
final String httpMethod;
|
|
}
|
|
|
|
/// 同步状态
|
|
enum SyncStatus {
|
|
idle,
|
|
syncing,
|
|
paused,
|
|
error,
|
|
}
|
|
|
|
/// 待同步操作
|
|
class PendingOperation {
|
|
final String id;
|
|
final SyncOperationType type;
|
|
final String endpoint;
|
|
final Map<String, dynamic> data;
|
|
final int version;
|
|
final DateTime createdAt;
|
|
final int retryCount;
|
|
|
|
static const int maxRetryCount = 5;
|
|
|
|
const PendingOperation({
|
|
required this.id,
|
|
required this.type,
|
|
required this.endpoint,
|
|
required this.data,
|
|
required this.version,
|
|
required this.createdAt,
|
|
this.retryCount = 0,
|
|
});
|
|
|
|
PendingOperation copyWith({
|
|
String? id,
|
|
SyncOperationType? type,
|
|
String? endpoint,
|
|
Map<String, dynamic>? data,
|
|
int? version,
|
|
DateTime? createdAt,
|
|
int? retryCount,
|
|
}) =>
|
|
PendingOperation(
|
|
id: id ?? this.id,
|
|
type: type ?? this.type,
|
|
endpoint: endpoint ?? this.endpoint,
|
|
data: data ?? this.data,
|
|
version: version ?? this.version,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
retryCount: retryCount ?? this.retryCount,
|
|
);
|
|
|
|
bool get isExhausted => retryCount >= maxRetryCount;
|
|
}
|
|
|
|
/// 同步引擎 — Web 版(内存队列,无持久化)
|
|
class SyncEngine {
|
|
final ApiClient _apiClient;
|
|
final Queue<PendingOperation> _pendingQueue = Queue();
|
|
StreamSubscription<List<ConnectivityResult>>? _connectivitySub;
|
|
|
|
SyncStatus _status = SyncStatus.idle;
|
|
String? _lastError;
|
|
|
|
SyncEngine({required ApiClient apiClient}) : _apiClient = apiClient;
|
|
|
|
SyncStatus get status => _status;
|
|
String? get lastError => _lastError;
|
|
int get pendingCount => _pendingQueue.length;
|
|
bool get isSyncing => _status == SyncStatus.syncing;
|
|
|
|
void enqueue(PendingOperation operation) {
|
|
_pendingQueue.add(operation);
|
|
if (_status == SyncStatus.idle) {
|
|
_status = SyncStatus.paused;
|
|
}
|
|
}
|
|
|
|
void enqueueAll(List<PendingOperation> operations) {
|
|
for (final op in operations) {
|
|
_pendingQueue.add(op);
|
|
}
|
|
if (_status == SyncStatus.idle && _pendingQueue.isNotEmpty) {
|
|
_status = SyncStatus.paused;
|
|
}
|
|
}
|
|
|
|
Future<void> trySync() async {
|
|
if (_status == SyncStatus.syncing) return;
|
|
if (_pendingQueue.isEmpty) {
|
|
_status = SyncStatus.idle;
|
|
return;
|
|
}
|
|
|
|
final connectivity = Connectivity();
|
|
final result = await connectivity.checkConnectivity();
|
|
final isOnline = result.any((r) => r != ConnectivityResult.none);
|
|
if (!isOnline) {
|
|
_status = SyncStatus.paused;
|
|
_lastError = '网络不可用';
|
|
return;
|
|
}
|
|
|
|
_status = SyncStatus.syncing;
|
|
_lastError = null;
|
|
|
|
while (_pendingQueue.isNotEmpty) {
|
|
final operation = _pendingQueue.removeFirst();
|
|
|
|
try {
|
|
await _executeOperation(operation);
|
|
} on OfflineException {
|
|
_pendingQueue.addFirst(operation);
|
|
_status = SyncStatus.paused;
|
|
_lastError = '同步中断:网络不可用';
|
|
return;
|
|
} catch (e) {
|
|
debugPrint('SyncEngine.trySync 操作失败: $e');
|
|
final retried = operation.copyWith(retryCount: operation.retryCount + 1);
|
|
|
|
if (retried.isExhausted) {
|
|
_lastError = '操作同步失败(已耗尽重试次数): ${operation.endpoint}';
|
|
continue;
|
|
}
|
|
|
|
_pendingQueue.addFirst(retried);
|
|
_status = SyncStatus.error;
|
|
_lastError = '同步失败: $e';
|
|
return;
|
|
}
|
|
}
|
|
|
|
_status = SyncStatus.idle;
|
|
_lastError = null;
|
|
}
|
|
|
|
Future<void> _executeOperation(PendingOperation operation) async {
|
|
switch (operation.type) {
|
|
case SyncOperationType.create:
|
|
await _apiClient.post(operation.endpoint, data: operation.data);
|
|
case SyncOperationType.update:
|
|
await _apiClient.put(operation.endpoint, data: operation.data);
|
|
case SyncOperationType.delete:
|
|
await _apiClient.delete(operation.endpoint);
|
|
}
|
|
}
|
|
|
|
void clear() {
|
|
_pendingQueue.clear();
|
|
_status = SyncStatus.idle;
|
|
_lastError = null;
|
|
}
|
|
|
|
List<PendingOperation> get snapshot => _pendingQueue.toList();
|
|
|
|
/// Web 平台:持久化为空操作(队列仅保存在内存中)
|
|
Future<void> persistPendingQueue() async {}
|
|
|
|
/// Web 平台:恢复队列为空操作(无持久化数据)
|
|
Future<void> restorePendingQueue() async {}
|
|
|
|
void startAutoSync() {
|
|
_connectivitySub = Connectivity().onConnectivityChanged.listen((result) {
|
|
final isOnline = result.any((r) => r != ConnectivityResult.none);
|
|
if (isOnline && _pendingQueue.isNotEmpty && _status != SyncStatus.syncing) {
|
|
debugPrint('SyncEngine: 网络恢复,开始同步 ${_pendingQueue.length} 个操作');
|
|
trySync();
|
|
}
|
|
});
|
|
}
|
|
|
|
void dispose() {
|
|
_connectivitySub?.cancel();
|
|
_connectivitySub = null;
|
|
}
|
|
|
|
String _encodeJson(Map<String, dynamic> data) {
|
|
try {
|
|
return jsonEncode(data);
|
|
} catch (_) {
|
|
return '{}';
|
|
}
|
|
}
|
|
|
|
Map<String, dynamic> _decodeJson(String json) {
|
|
try {
|
|
return jsonDecode(json) as Map<String, dynamic>;
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
}
|