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 接口
This commit is contained in:
213
app/lib/data/services/sync_engine_web.dart
Normal file
213
app/lib/data/services/sync_engine_web.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
// 同步引擎 — 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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user