feat(app): pnpm 一键启动 + Flutter Web 编译修复
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled

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:
iven
2026-06-03 09:50:19 +08:00
parent b81a972245
commit 11d0971a67
23 changed files with 2034 additions and 888 deletions

View File

@@ -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';

View 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();
});
}
}

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

View 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);
}

View 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();
}

View 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);
}
}

View 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);
}
}