Files
nj/app/lib/data/services/sync_engine_web.dart
iven 11d0971a67
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
feat(app): pnpm 一键启动 + Flutter Web 编译修复
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 接口
2026-06-03 09:50:19 +08:00

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 {};
}
}
}