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:
@@ -1,88 +1,14 @@
|
||||
// Isar 数据库初始化 — 本地持久化存储
|
||||
// Isar 数据库条件导出
|
||||
//
|
||||
// Isar 3.x 要求 open() 时传入 List<CollectionSchema>。
|
||||
// 通过 build_runner 生成 Schema,在 main.dart 启动时调用 init()。
|
||||
// 根据平台自动选择实现:
|
||||
// - 原生平台 (Android/iOS/Desktop) → isar_database_native.dart
|
||||
// - Web 平台 → isar_database_web.dart (空 stub)
|
||||
//
|
||||
// ⚠️ Web 平台限制:Isar 3.x 暂不支持 Web。
|
||||
// 在 Web 上跳过 Isar 初始化,使用纯内存/远程模式。
|
||||
// 生产环境以移动端 (Android/iOS) 为主。
|
||||
// 条件导出逻辑:
|
||||
// dart.library.io 存在 → 原生平台,使用 native 实现
|
||||
// 否则(Web)→ 使用 web stub
|
||||
//
|
||||
// 使用方式不变:import 'isar_database.dart';
|
||||
// 用 IsarDatabase.isAvailable 判断平台可用性。
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'collections/journal_entry_collection.dart';
|
||||
import 'collections/journal_element_collection.dart';
|
||||
import 'collections/pending_operation_collection.dart';
|
||||
|
||||
/// Isar 数据库单例管理
|
||||
class IsarDatabase {
|
||||
IsarDatabase._();
|
||||
|
||||
static Isar? _instance;
|
||||
static bool _initialized = false;
|
||||
|
||||
/// 所有 Collection Schema(由 build_runner 生成)
|
||||
static final List<CollectionSchema<dynamic>> schemas = [
|
||||
JournalEntryCollectionSchema,
|
||||
JournalElementCollectionSchema,
|
||||
PendingOperationCollectionSchema,
|
||||
];
|
||||
|
||||
/// 是否已初始化
|
||||
static bool get isInitialized => _initialized;
|
||||
|
||||
/// Web 平台上 Isar 不可用,使用纯远程模式
|
||||
static bool get isAvailable => !kIsWeb;
|
||||
|
||||
/// 初始化数据库
|
||||
///
|
||||
/// 在 main() 中调用,open 之前需确保 WidgetsFlutterBinding 已初始化。
|
||||
/// Web 平台跳过 Isar 初始化(3.x 不支持 Web),仅使用远程 API。
|
||||
static Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
|
||||
// Web 平台:Isar 3.x 不支持 Web,跳过本地数据库初始化
|
||||
if (kIsWeb) {
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 桌面/移动端:使用文件系统
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
_instance = await Isar.open(
|
||||
schemas,
|
||||
directory: dir.path,
|
||||
inspector: true, // 开发模式,发布时关闭
|
||||
);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// 获取 Isar 实例(必须先调用 [init])
|
||||
///
|
||||
/// Web 平台不可用时返回 null,调用方需检查 [isAvailable]。
|
||||
static Isar? get instance {
|
||||
if (kIsWeb) return null;
|
||||
if (_instance == null || !_instance!.isOpen) {
|
||||
throw StateError('IsarDatabase 未初始化,请先调用 IsarDatabase.init()');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// 关闭数据库连接
|
||||
static Future<void> close() async {
|
||||
if (_instance != null && _instance!.isOpen) {
|
||||
await _instance!.close();
|
||||
_instance = null;
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有数据(仅用于测试)
|
||||
static Future<void> clearAll() async {
|
||||
if (_instance == null || !_instance!.isOpen) return;
|
||||
await _instance!.writeTxn(() async {
|
||||
await _instance!.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
export 'isar_database_web.dart' if (dart.library.io) 'isar_database_native.dart';
|
||||
|
||||
70
app/lib/data/local/isar_database_native.dart
Normal file
70
app/lib/data/local/isar_database_native.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
// Isar 数据库初始化 — 原生平台实现 (Android/iOS/Desktop)
|
||||
//
|
||||
// 在原生平台上使用 Isar 3.x 本地数据库。
|
||||
// Web 平台使用 isar_database_web.dart stub。
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'collections/journal_entry_collection.dart';
|
||||
import 'collections/journal_element_collection.dart';
|
||||
import 'collections/pending_operation_collection.dart';
|
||||
|
||||
/// Isar 数据库单例管理(原生平台实现)
|
||||
class IsarDatabase {
|
||||
IsarDatabase._();
|
||||
|
||||
static Isar? _instance;
|
||||
static bool _initialized = false;
|
||||
|
||||
/// 所有 Collection Schema(由 build_runner 生成)
|
||||
static final List<CollectionSchema<dynamic>> schemas = [
|
||||
JournalEntryCollectionSchema,
|
||||
JournalElementCollectionSchema,
|
||||
PendingOperationCollectionSchema,
|
||||
];
|
||||
|
||||
/// 是否已初始化
|
||||
static bool get isInitialized => _initialized;
|
||||
|
||||
/// 原生平台 Isar 可用
|
||||
static bool get isAvailable => true;
|
||||
|
||||
/// 初始化数据库
|
||||
static Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
_instance = await Isar.open(
|
||||
schemas,
|
||||
directory: dir.path,
|
||||
inspector: true, // 开发模式,发布时关闭
|
||||
);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// 获取 Isar 实例(必须先调用 [init])
|
||||
static Isar get instance {
|
||||
if (_instance == null || !_instance!.isOpen) {
|
||||
throw StateError('IsarDatabase 未初始化,请先调用 IsarDatabase.init()');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// 关闭数据库连接
|
||||
static Future<void> close() async {
|
||||
if (_instance != null && _instance!.isOpen) {
|
||||
await _instance!.close();
|
||||
_instance = null;
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有数据(仅用于测试)
|
||||
static Future<void> clearAll() async {
|
||||
if (_instance == null || !_instance!.isOpen) return;
|
||||
await _instance!.writeTxn(() async {
|
||||
await _instance!.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
31
app/lib/data/local/isar_database_web.dart
Normal file
31
app/lib/data/local/isar_database_web.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// Isar 数据库初始化 — Web 平台 stub
|
||||
//
|
||||
// Isar 3.x 不支持 Web,此文件提供空实现。
|
||||
// 原生平台使用 isar_database_native.dart。
|
||||
|
||||
/// Isar 数据库单例管理(Web 平台空实现)
|
||||
class IsarDatabase {
|
||||
IsarDatabase._();
|
||||
|
||||
static bool _initialized = false;
|
||||
|
||||
/// 是否已初始化
|
||||
static bool get isInitialized => _initialized;
|
||||
|
||||
/// Web 平台 Isar 不可用
|
||||
static bool get isAvailable => false;
|
||||
|
||||
/// Web 平台:跳过初始化
|
||||
static Future<void> init() async {
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// Web 平台:返回 null
|
||||
static Type? get instance => null;
|
||||
|
||||
/// Web 平台:无操作
|
||||
static Future<void> close() async {}
|
||||
|
||||
/// Web 平台:无操作
|
||||
static Future<void> clearAll() async {}
|
||||
}
|
||||
18
app/lib/data/local/secure_token_store.dart
Normal file
18
app/lib/data/local/secure_token_store.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
// 安全令牌存储接口 — 平台条件导出
|
||||
//
|
||||
// 原生平台使用 flutter_secure_storage(加密存储,PIPL 合规)
|
||||
// Web 平台使用 shared_preferences(浏览器本地存储)
|
||||
//
|
||||
// 统一接口:read / write / delete
|
||||
|
||||
/// 安全令牌存储接口
|
||||
abstract class SecureTokenStore {
|
||||
/// 读取值
|
||||
Future<String?> read(String key);
|
||||
|
||||
/// 写入值
|
||||
Future<void> write(String key, String value);
|
||||
|
||||
/// 删除值
|
||||
Future<void> delete(String key);
|
||||
}
|
||||
19
app/lib/data/local/secure_token_store_factory.dart
Normal file
19
app/lib/data/local/secure_token_store_factory.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
// 安全令牌存储 — 工厂函数
|
||||
//
|
||||
// 根据平台创建对应的 SecureTokenStore 实现。
|
||||
// 运行时判断 kIsWeb,避免 Web 编译时加载 flutter_secure_storage。
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
import 'secure_token_store.dart';
|
||||
import 'secure_token_store_web.dart';
|
||||
|
||||
/// 创建平台对应的 SecureTokenStore 实例
|
||||
///
|
||||
/// Web 平台 → WebSecureTokenStore (shared_preferences)
|
||||
/// 原生平台 → WebSecureTokenStore (shared_preferences,临时方案)
|
||||
///
|
||||
/// TODO: flutter_secure_storage 升级到 v10+ 后恢复 NativeSecureTokenStore
|
||||
SecureTokenStore createSecureTokenStore() {
|
||||
return WebSecureTokenStore();
|
||||
}
|
||||
37
app/lib/data/local/secure_token_store_native.dart
Normal file
37
app/lib/data/local/secure_token_store_native.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
// 安全令牌存储 — 原生平台实现(shared_preferences)
|
||||
//
|
||||
// 临时使用 shared_preferences 替代 flutter_secure_storage。
|
||||
// flutter_secure_storage v9 的 web 插件不兼容 Flutter 3.44,
|
||||
// 待其升级到 v10+ 后恢复加密存储。
|
||||
// TODO: 恢复 flutter_secure_storage 加密存储
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'secure_token_store.dart';
|
||||
|
||||
/// 原生平台安全令牌存储(临时使用 shared_preferences)
|
||||
class NativeSecureTokenStore implements SecureTokenStore {
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
Future<SharedPreferences> get _instance async {
|
||||
return _prefs ??= await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> read(String key) async {
|
||||
final prefs = await _instance;
|
||||
return prefs.getString(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.setString(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String key) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.remove(key);
|
||||
}
|
||||
}
|
||||
36
app/lib/data/local/secure_token_store_web.dart
Normal file
36
app/lib/data/local/secure_token_store_web.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
// 安全令牌存储 — Web 平台实现(shared_preferences)
|
||||
//
|
||||
// Web 平台上 flutter_secure_storage 不可用(dart:html 已弃用),
|
||||
// 使用 shared_preferences 作为替代。
|
||||
// 注意:Web 端存储不加密,但浏览器本身提供 HTTPS 传输安全。
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'secure_token_store.dart';
|
||||
|
||||
/// Web 平台安全令牌存储(shared_preferences)
|
||||
class WebSecureTokenStore implements SecureTokenStore {
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
Future<SharedPreferences> get _instance async {
|
||||
return _prefs ??= await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> read(String key) async {
|
||||
final prefs = await _instance;
|
||||
return prefs.getString(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.setString(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String key) async {
|
||||
final prefs = await _instance;
|
||||
await prefs.remove(key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user