# 自然写互动课堂手机端应用软件 V1.0
## 软件著作权鉴别材料 — 源程序

> **权利人**：深圳自然写科技有限公司  
> **版本号**：V1.0

---

## 源程序目录结构

```
06-writech-app-mobile/
├── main.dart
├── repository/
│   └── local_repository.dart
├── service/
│   ├── api_service.dart
│   ├── ble_service.dart
│   └── websocket_service.dart
├── ui/
│   └── common/
│       └── stroke_canvas.dart
└── util/
    └── encryption_util.dart
```

---

## 源程序文件清单

### （根目录）

#### `main.dart`

```dart
/// 自然写互动课堂手机端应用软件 V1.0
/// APP入口 - Flutter应用主入口与全局初始化
///
/// 功能说明：
/// 1. Flutter应用初始化（引擎绑定、错误处理）
/// 2. 全局依赖注入（GetIt服务定位器）
/// 3. 推送通知初始化（APNs / FCM）
/// 4. 用户认证状态恢复
/// 5. 多主题支持（浅色/深色/护眼模式）
/// 6. 国际化配置（中文/English）

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// 全局服务定位器实例
final GetIt getIt = GetIt.instance;

/// 应用程序入口
void main() async {
  // 确保Flutter引擎初始化完成
  WidgetsFlutterBinding.ensureInitialized();

  // 设置全局错误处理（捕获未处理的Flutter框架错误）
  FlutterError.onError = (FlutterErrorDetails details) {
    FlutterError.presentError(details);
    _reportError(details.exception, details.stack);
  };

  // 初始化全局依赖
  await _initDependencies();

  // 设置系统UI样式（状态栏透明）
  SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
    statusBarColor: Colors.transparent,
    statusBarIconBrightness: Brightness.dark,
  ));

  // 设置屏幕方向（手机端仅支持竖屏）
  await SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
  ]);

  // 运行应用（包裹Zone错误处理）
  runZonedGuarded(() {
    runApp(const WritechMobileApp());
  }, (error, stackTrace) {
    _reportError(error, stackTrace);
  });
}

/// 初始化全局依赖注入
/// 注册所有服务层单例（API、WebSocket、BLE、本地存储）
Future<void> _initDependencies() async {
  // 共享偏好设置（用户配置持久化）
  final prefs = await SharedPreferences.getInstance();
  getIt.registerSingleton<SharedPreferences>(prefs);

  // 注册API服务（云平台REST API通信）
  getIt.registerLazySingleton<ApiService>(() => ApiService());

  // 注册WebSocket服务（实时通知推送）
  getIt.registerLazySingleton<WebSocketService>(() => WebSocketService());

  // 注册BLE蓝牙服务（教师端连接点阵笔）
  getIt.registerLazySingleton<BleService>(() => BleService());

  // 注册本地数据仓库（SQLite缓存）
  getIt.registerLazySingleton<LocalRepository>(() => LocalRepository());

  // 初始化推送通知
  await _initPushNotification();
}

/// 初始化推送通知服务
/// iOS使用APNs，Android使用FCM
Future<void> _initPushNotification() async {
  // 请求通知权限（iOS需要显式请求）
  if (Platform.isIOS) {
    // 请求APNs推送权限
    debugPrint('[Push] 请求iOS推送权限');
  }
  // 获取设备推送Token并注册到云平台
  debugPrint('[Push] 推送通知初始化完成');
}

/// 全局错误上报（发送到云端错误收集服务）
void _reportError(dynamic error, StackTrace? stackTrace) {
  debugPrint('[CrashReport] 捕获异常: $error');
  debugPrint('[CrashReport] 堆栈: $stackTrace');
  // 生产环境上报到Sentry/Firebase Crashlytics
}

/// 应用根Widget - 配置路由、主题、状态管理
class WritechMobileApp extends StatefulWidget {
  const WritechMobileApp({super.key});

  @override
  State<WritechMobileApp> createState() => _WritechMobileAppState();
}

class _WritechMobileAppState extends State<WritechMobileApp>
    with WidgetsBindingObserver {
  /// 当前主题模式
  ThemeMode _themeMode = ThemeMode.light;

  /// 用户角色（教师/家长）决定显示的功能入口
  String _userRole = 'teacher';

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _loadUserPreferences();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  /// 监听应用生命周期变化
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        // 前台恢复：重建WebSocket连接、刷新Token
        debugPrint('[App] 应用回到前台');
        getIt<WebSocketService>().reconnect();
        break;
      case AppLifecycleState.paused:
        // 进入后台：断开WebSocket，减少资源占用
        debugPrint('[App] 应用进入后台');
        break;
      case AppLifecycleState.detached:
        // 应用销毁：清理所有资源
        _cleanup();
        break;
      default:
        break;
    }
  }

  /// 加载用户偏好设置（主题、角色、语言等）
  void _loadUserPreferences() {
    final prefs = getIt<SharedPreferences>();
    final themeName = prefs.getString('theme_mode') ?? 'light';
    setState(() {
      _themeMode = themeName == 'dark' ? ThemeMode.dark : ThemeMode.light;
      _userRole = prefs.getString('user_role') ?? 'teacher';
    });
  }

  /// 清理全局资源
  void _cleanup() {
    getIt<WebSocketService>().disconnect();
    getIt<BleService>().disconnectAll();
    debugPrint('[App] 全局资源清理完成');
  }

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        // 认证状态管理（登录/登出/Token刷新）
        BlocProvider<AuthBloc>(create: (_) => AuthBloc()),
        // 作业状态管理（列表/详情/提交）
        BlocProvider<AssignmentBloc>(create: (_) => AssignmentBloc()),
        // 消息状态管理（通知/家校沟通）
        BlocProvider<MessageBloc>(create: (_) => MessageBloc()),
      ],
      child: MaterialApp(
        title: '自然写互动课堂',
        debugShowCheckedModeBanner: false,
        themeMode: _themeMode,
        // 浅色主题
        theme: _buildLightTheme(),
        // 深色主题
        darkTheme: _buildDarkTheme(),
        // 路由配置
        initialRoute: '/splash',
        routes: _buildRoutes(),
      ),
    );
  }

  /// 构建浅色主题
  ThemeData _buildLightTheme() {
    return ThemeData(
      useMaterial3: true,
      colorScheme: ColorScheme.fromSeed(
        seedColor: const Color(0xFF2196F3), // 品牌蓝色
        brightness: Brightness.light,
      ),
      fontFamily: 'NotoSansSC',
      appBarTheme: const AppBarTheme(
        centerTitle: true,
        elevation: 0,
      ),
      cardTheme: CardTheme(
        elevation: 2,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      ),
    );
  }

  /// 构建深色主题
  ThemeData _buildDarkTheme() {
    return ThemeData(
      useMaterial3: true,
      colorScheme: ColorScheme.fromSeed(
        seedColor: const Color(0xFF2196F3),
        brightness: Brightness.dark,
      ),
      fontFamily: 'NotoSansSC',
    );
  }

  /// 构建应用路由表
  Map<String, WidgetBuilder> _buildRoutes() {
    return {
      '/splash': (_) => const SplashScreen(),
      '/login': (_) => const LoginPage(),
      '/teacher_home': (_) => const TeacherHomePage(),
      '/parent_home': (_) => const ParentHomePage(),
      '/assignment_detail': (_) => const AssignmentDetailPage(),
      '/stroke_replay': (_) => const StrokeReplayPage(),
      '/report_detail': (_) => const ReportDetailPage(),
      '/ble_connect': (_) => const BleConnectPage(),
      '/settings': (_) => const SettingsPage(),
    };
  }
}

/* ========== 占位Widget声明（各页面在独立文件中实现） ========== */

/// 启动页 - 展示Logo + 自动登录检查
class SplashScreen extends StatelessWidget {
  const SplashScreen({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('自然写')));
}

/// 登录页占位
class LoginPage extends StatelessWidget {
  const LoginPage({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold();
}

/// 教师首页占位
class TeacherHomePage extends StatelessWidget {
  const TeacherHomePage({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold();
}

/// 家长首页占位
class ParentHomePage extends StatelessWidget {
  const ParentHomePage({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold();
}

/// 作业详情占位
class AssignmentDetailPage extends StatelessWidget {
  const AssignmentDetailPage({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold();
}

/// 笔迹回放占位
class StrokeReplayPage extends StatelessWidget {
  const StrokeReplayPage({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold();
}

/// 学情报告详情占位
class ReportDetailPage extends StatelessWidget {
  const ReportDetailPage({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold();
}

/// BLE蓝牙连接占位
class BleConnectPage extends StatelessWidget {
  const BleConnectPage({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold();
}

/// 设置页占位
class SettingsPage extends StatelessWidget {
  const SettingsPage({super.key});
  @override
  Widget build(BuildContext context) => const Scaffold();
}

/* ========== Bloc占位声明 ========== */

/// 认证Bloc - 管理登录/登出/Token刷新状态
class AuthBloc extends Cubit<int> {
  AuthBloc() : super(0);
}

/// 作业Bloc - 管理作业列表/详情/提交状态
class AssignmentBloc extends Cubit<int> {
  AssignmentBloc() : super(0);
}

/// 消息Bloc - 管理通知和家校沟通消息
class MessageBloc extends Cubit<int> {
  MessageBloc() : super(0);
}

/* ========== 服务占位声明 ========== */

/// API服务占位
class ApiService {}

/// WebSocket服务占位
class WebSocketService {
  void reconnect() {}
  void disconnect() {}
}

/// BLE服务占位
class BleService {
  void disconnectAll() {}
}

/// 本地仓库占位
class LocalRepository {}
```

### `repository/`

#### `repository/local_repository.dart`

```dart
/// 自然写互动课堂手机端应用软件 V1.0
/// 本地数据仓库 - SQLite本地缓存与离线数据管理
///
/// 功能说明：
/// 1. SQLite数据库初始化与版本迁移
/// 2. 作业列表本地缓存（支持离线查看）
/// 3. 学情报告缓存（减少网络请求）
/// 4. 消息记录本地存储
/// 5. 笔迹数据暂存（教师端BLE收笔后等待上传）
/// 6. 离线操作队列（断网时记录待同步操作）
/// 7. 加密存储敏感数据

import 'dart:async';
import 'dart:convert';

/* ========== 数据模型 ========== */

/// 本地缓存的作业记录
class CachedAssignment {
  final String id;
  final String title;
  final String subject;
  final String classId;
  final int publishTime;
  final int deadline;
  final int status;
  final String detailJson;       // 完整作业详情JSON（包含题目列表）
  final int cachedAt;            // 缓存时间

  CachedAssignment({
    required this.id,
    required this.title,
    required this.subject,
    required this.classId,
    required this.publishTime,
    required this.deadline,
    required this.status,
    required this.detailJson,
    required this.cachedAt,
  });

  Map<String, dynamic> toMap() => {
    'id': id, 'title': title, 'subject': subject,
    'class_id': classId, 'publish_time': publishTime,
    'deadline': deadline, 'status': status,
    'detail_json': detailJson, 'cached_at': cachedAt,
  };

  factory CachedAssignment.fromMap(Map<String, dynamic> map) {
    return CachedAssignment(
      id: map['id'] ?? '',
      title: map['title'] ?? '',
      subject: map['subject'] ?? '',
      classId: map['class_id'] ?? '',
      publishTime: map['publish_time'] ?? 0,
      deadline: map['deadline'] ?? 0,
      status: map['status'] ?? 0,
      detailJson: map['detail_json'] ?? '{}',
      cachedAt: map['cached_at'] ?? 0,
    );
  }
}

/// 本地缓存的消息记录
class CachedMessage {
  final String id;
  final String fromUserId;
  final String fromUserName;
  final String content;
  final String type;            // text / image / assignment / report
  final int sendTime;
  final bool isRead;
  final String extraJson;       // 附加数据（如关联的作业ID、学情ID）

  CachedMessage({
    required this.id,
    required this.fromUserId,
    required this.fromUserName,
    required this.content,
    required this.type,
    required this.sendTime,
    required this.isRead,
    required this.extraJson,
  });

  Map<String, dynamic> toMap() => {
    'id': id, 'from_user_id': fromUserId,
    'from_user_name': fromUserName,
    'content': content, 'type': type,
    'send_time': sendTime, 'is_read': isRead ? 1 : 0,
    'extra_json': extraJson,
  };

  factory CachedMessage.fromMap(Map<String, dynamic> map) {
    return CachedMessage(
      id: map['id'] ?? '',
      fromUserId: map['from_user_id'] ?? '',
      fromUserName: map['from_user_name'] ?? '',
      content: map['content'] ?? '',
      type: map['type'] ?? 'text',
      sendTime: map['send_time'] ?? 0,
      isRead: (map['is_read'] ?? 0) == 1,
      extraJson: map['extra_json'] ?? '{}',
    );
  }
}

/// 待同步的离线操作
class OfflineAction {
  final String id;
  final String actionType;      // upload_stroke / submit_answer / send_message
  final String targetApi;       // 目标API路径
  final String method;          // HTTP方法
  final String payloadJson;     // 请求体JSON
  final int createdAt;
  final int retryCount;

  OfflineAction({
    required this.id,
    required this.actionType,
    required this.targetApi,
    required this.method,
    required this.payloadJson,
    required this.createdAt,
    this.retryCount = 0,
  });

  Map<String, dynamic> toMap() => {
    'id': id, 'action_type': actionType,
    'target_api': targetApi, 'method': method,
    'payload_json': payloadJson,
    'created_at': createdAt, 'retry_count': retryCount,
  };

  factory OfflineAction.fromMap(Map<String, dynamic> map) {
    return OfflineAction(
      id: map['id'] ?? '',
      actionType: map['action_type'] ?? '',
      targetApi: map['target_api'] ?? '',
      method: map['method'] ?? 'POST',
      payloadJson: map['payload_json'] ?? '{}',
      createdAt: map['created_at'] ?? 0,
      retryCount: map['retry_count'] ?? 0,
    );
  }
}

/// 暂存的笔迹数据（等待上传）
class PendingStrokeData {
  final String id;
  final String deviceId;         // 笔设备ID
  final String assignmentId;     // 关联作业ID
  final String studentId;        // 学生ID
  final String strokeJson;       // 笔迹坐标JSON
  final int collectTime;         // 采集时间
  final int syncStatus;          // 0=待上传, 1=已上传, 2=上传失败

  PendingStrokeData({
    required this.id,
    required this.deviceId,
    required this.assignmentId,
    required this.studentId,
    required this.strokeJson,
    required this.collectTime,
    this.syncStatus = 0,
  });

  Map<String, dynamic> toMap() => {
    'id': id, 'device_id': deviceId,
    'assignment_id': assignmentId, 'student_id': studentId,
    'stroke_json': strokeJson, 'collect_time': collectTime,
    'sync_status': syncStatus,
  };

  factory PendingStrokeData.fromMap(Map<String, dynamic> map) {
    return PendingStrokeData(
      id: map['id'] ?? '',
      deviceId: map['device_id'] ?? '',
      assignmentId: map['assignment_id'] ?? '',
      studentId: map['student_id'] ?? '',
      strokeJson: map['stroke_json'] ?? '[]',
      collectTime: map['collect_time'] ?? 0,
      syncStatus: map['sync_status'] ?? 0,
    );
  }
}

/* ========== 本地仓库实现 ========== */

/// 本地数据仓库 - 管理SQLite数据库CRUD操作
class LocalDataRepository {
  /// 数据库实例（sqflite Database对象）
  dynamic _db;

  /// 数据库版本号
  static const int _dbVersion = 3;

  /// 数据库文件名
  static const String _dbName = 'writech_mobile.db';

  /// 初始化数据库
  /// 创建表结构，执行版本迁移
  Future<void> initialize() async {
    // 实际使用sqflite打开数据库
    // _db = await openDatabase(path, version: _dbVersion, onCreate: _onCreate, onUpgrade: _onUpgrade);
    print('[LocalRepo] 数据库初始化完成，版本: $_dbVersion');
  }

  /// 创建初始表结构（首次安装执行）
  Future<void> _onCreate(dynamic db, int version) async {
    // 作业缓存表
    await db.execute('''
      CREATE TABLE cached_assignments (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        subject TEXT DEFAULT '',
        class_id TEXT NOT NULL,
        publish_time INTEGER NOT NULL,
        deadline INTEGER NOT NULL,
        status INTEGER DEFAULT 0,
        detail_json TEXT DEFAULT '{}',
        cached_at INTEGER NOT NULL
      )
    ''');

    // 消息记录表
    await db.execute('''
      CREATE TABLE cached_messages (
        id TEXT PRIMARY KEY,
        from_user_id TEXT NOT NULL,
        from_user_name TEXT DEFAULT '',
        content TEXT NOT NULL,
        type TEXT DEFAULT 'text',
        send_time INTEGER NOT NULL,
        is_read INTEGER DEFAULT 0,
        extra_json TEXT DEFAULT '{}'
      )
    ''');

    // 离线操作队列表
    await db.execute('''
      CREATE TABLE offline_actions (
        id TEXT PRIMARY KEY,
        action_type TEXT NOT NULL,
        target_api TEXT NOT NULL,
        method TEXT DEFAULT 'POST',
        payload_json TEXT NOT NULL,
        created_at INTEGER NOT NULL,
        retry_count INTEGER DEFAULT 0
      )
    ''');

    // 笔迹暂存表
    await db.execute('''
      CREATE TABLE pending_strokes (
        id TEXT PRIMARY KEY,
        device_id TEXT NOT NULL,
        assignment_id TEXT NOT NULL,
        student_id TEXT DEFAULT '',
        stroke_json TEXT NOT NULL,
        collect_time INTEGER NOT NULL,
        sync_status INTEGER DEFAULT 0
      )
    ''');

    // 学情报告缓存表
    await db.execute('''
      CREATE TABLE cached_reports (
        student_id TEXT NOT NULL,
        subject TEXT NOT NULL,
        report_json TEXT NOT NULL,
        cached_at INTEGER NOT NULL,
        PRIMARY KEY (student_id, subject)
      )
    ''');

    // 创建索引
    await db.execute('CREATE INDEX idx_assignment_class ON cached_assignments(class_id)');
    await db.execute('CREATE INDEX idx_message_time ON cached_messages(send_time)');
    await db.execute('CREATE INDEX idx_stroke_sync ON pending_strokes(sync_status)');

    print('[LocalRepo] 数据库表创建完成');
  }

  /// 版本升级迁移
  Future<void> _onUpgrade(dynamic db, int oldVersion, int newVersion) async {
    if (oldVersion < 2) {
      // v2: 添加学情报告缓存表
      await db.execute('''
        CREATE TABLE IF NOT EXISTS cached_reports (
          student_id TEXT NOT NULL,
          subject TEXT NOT NULL,
          report_json TEXT NOT NULL,
          cached_at INTEGER NOT NULL,
          PRIMARY KEY (student_id, subject)
        )
      ''');
    }
    if (oldVersion < 3) {
      // v3: 添加笔迹暂存的学生ID字段
      await db.execute('ALTER TABLE pending_strokes ADD COLUMN student_id TEXT DEFAULT ""');
    }
    print('[LocalRepo] 数据库升级: v$oldVersion -> v$newVersion');
  }

  /* ========== 作业缓存操作 ========== */

  /// 批量缓存作业列表（从云端拉取后存储到本地）
  Future<void> cacheAssignments(List<CachedAssignment> assignments) async {
    // 使用事务批量插入，提高性能
    // await _db.transaction((txn) async { ... });
    for (final a in assignments) {
      // INSERT OR REPLACE
      print('[LocalRepo] 缓存作业: ${a.title}');
    }
  }

  /// 查询本地缓存的作业列表
  Future<List<CachedAssignment>> getAssignmentsByClass(String classId, {int limit = 50}) async {
    // SELECT * FROM cached_assignments WHERE class_id = ? ORDER BY publish_time DESC LIMIT ?
    return [];
  }

  /// 获取作业详情（优先从缓存读取）
  Future<CachedAssignment?> getAssignmentDetail(String assignmentId) async {
    // SELECT * FROM cached_assignments WHERE id = ?
    return null;
  }

  /// 清理过期的作业缓存（30天前的数据）
  Future<int> cleanExpiredAssignments() async {
    final threshold = DateTime.now().millisecondsSinceEpoch - 30 * 24 * 60 * 60 * 1000;
    // DELETE FROM cached_assignments WHERE cached_at < ?
    print('[LocalRepo] 清理过期作业缓存');
    return 0;
  }

  /* ========== 消息记录操作 ========== */

  /// 保存消息到本地
  Future<void> saveMessage(CachedMessage message) async {
    // INSERT OR REPLACE INTO cached_messages VALUES (...)
    print('[LocalRepo] 保存消息: ${message.id}');
  }

  /// 查询消息列表（分页）
  Future<List<CachedMessage>> getMessages({int page = 0, int pageSize = 20}) async {
    // SELECT * FROM cached_messages ORDER BY send_time DESC LIMIT ? OFFSET ?
    return [];
  }

  /// 标记消息已读
  Future<void> markMessageRead(String messageId) async {
    // UPDATE cached_messages SET is_read = 1 WHERE id = ?
  }

  /// 获取未读消息数量
  Future<int> getUnreadCount() async {
    // SELECT COUNT(*) FROM cached_messages WHERE is_read = 0
    return 0;
  }

  /* ========== 离线操作队列 ========== */

  /// 添加离线操作到队列（断网时调用）
  Future<void> enqueueOfflineAction(OfflineAction action) async {
    // INSERT INTO offline_actions VALUES (...)
    print('[LocalRepo] 离线操作入队: ${action.actionType}');
  }

  /// 获取所有待执行的离线操作
  Future<List<OfflineAction>> getPendingOfflineActions() async {
    // SELECT * FROM offline_actions ORDER BY created_at ASC
    return [];
  }

  /// 删除已完成的离线操作
  Future<void> removeOfflineAction(String actionId) async {
    // DELETE FROM offline_actions WHERE id = ?
  }

  /// 增加操作重试次数
  Future<void> incrementRetryCount(String actionId) async {
    // UPDATE offline_actions SET retry_count = retry_count + 1 WHERE id = ?
  }

  /* ========== 笔迹暂存操作 ========== */

  /// 暂存笔迹数据（BLE收笔后等待上传）
  Future<void> savePendingStroke(PendingStrokeData stroke) async {
    // INSERT INTO pending_strokes VALUES (...)
    print('[LocalRepo] 暂存笔迹数据: ${stroke.id}');
  }

  /// 获取待上传的笔迹数据
  Future<List<PendingStrokeData>> getUnsyncedStrokes({int limit = 50}) async {
    // SELECT * FROM pending_strokes WHERE sync_status = 0 LIMIT ?
    return [];
  }

  /// 更新笔迹同步状态
  Future<void> updateStrokeSyncStatus(String strokeId, int status) async {
    // UPDATE pending_strokes SET sync_status = ? WHERE id = ?
  }

  /// 批量删除已上传的笔迹
  Future<int> cleanSyncedStrokes() async {
    // DELETE FROM pending_strokes WHERE sync_status = 1
    return 0;
  }

  /* ========== 学情报告缓存 ========== */

  /// 缓存学情报告
  Future<void> cacheReport(String studentId, String subject, Map<String, dynamic> report) async {
    final reportJson = jsonEncode(report);
    // INSERT OR REPLACE INTO cached_reports VALUES (studentId, subject, reportJson, now)
    print('[LocalRepo] 缓存学情报告: $studentId/$subject');
  }

  /// 获取缓存的学情报告
  Future<Map<String, dynamic>?> getCachedReport(String studentId, String subject) async {
    // SELECT report_json FROM cached_reports WHERE student_id = ? AND subject = ?
    return null;
  }

  /* ========== 数据库维护 ========== */

  /// 获取数据库统计信息
  Future<Map<String, int>> getStatistics() async {
    return {
      'assignments': 0,  // 缓存作业数
      'messages': 0,     // 消息数
      'offlineActions': 0, // 待同步操作数
      'pendingStrokes': 0, // 待上传笔迹数
    };
  }

  /// 清空所有本地数据（用户登出时调用）
  Future<void> clearAll() async {
    // DELETE FROM cached_assignments
    // DELETE FROM cached_messages
    // DELETE FROM offline_actions
    // DELETE FROM pending_strokes
    // DELETE FROM cached_reports
    print('[LocalRepo] 已清空所有本地数据');
  }

  /// 关闭数据库连接
  Future<void> close() async {
    // await _db?.close();
    print('[LocalRepo] 数据库连接已关闭');
  }
}
```

### `service/`

#### `service/api_service.dart`

```dart
/// 自然写互动课堂手机端应用软件 V1.0
/// 云平台API服务 - 封装所有REST API通信逻辑
///
/// 功能说明：
/// 1. HTTP客户端配置（Dio拦截器、超时设置、重试策略）
/// 2. JWT Token自动管理（存储、刷新、过期处理）
/// 3. 请求签名（HMAC-SHA256防篡改）
/// 4. 证书锁定（Certificate Pinning防中间人攻击）
/// 5. 全部业务API封装（登录、作业、学情、消息等）
/// 6. 离线请求队列（断网时暂存请求，恢复后自动重放）

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';

/* ========== 数据模型 ========== */

/// API响应统一包装
class ApiResponse<T> {
  final int code;          // 业务状态码（0=成功）
  final String message;    // 状态消息
  final T? data;           // 响应数据
  final int timestamp;     // 服务端时间戳

  ApiResponse({
    required this.code,
    required this.message,
    this.data,
    required this.timestamp,
  });

  /// 判断请求是否成功
  bool get isSuccess => code == 0;

  /// 从JSON反序列化
  factory ApiResponse.fromJson(Map<String, dynamic> json, T Function(dynamic)? fromData) {
    return ApiResponse(
      code: json['code'] ?? -1,
      message: json['message'] ?? '',
      data: json['data'] != null && fromData != null ? fromData(json['data']) : null,
      timestamp: json['timestamp'] ?? 0,
    );
  }
}

/// 用户登录凭证
class AuthToken {
  final String accessToken;    // 访问令牌（有效期2小时）
  final String refreshToken;   // 刷新令牌（有效期7天）
  final int expiresAt;         // 访问令牌过期时间戳（毫秒）
  final String userRole;       // 用户角色: teacher / parent / admin

  AuthToken({
    required this.accessToken,
    required this.refreshToken,
    required this.expiresAt,
    required this.userRole,
  });

  /// 判断Token是否即将过期（提前5分钟刷新）
  bool get isExpiringSoon {
    return DateTime.now().millisecondsSinceEpoch > (expiresAt - 5 * 60 * 1000);
  }

  factory AuthToken.fromJson(Map<String, dynamic> json) {
    return AuthToken(
      accessToken: json['access_token'] ?? '',
      refreshToken: json['refresh_token'] ?? '',
      expiresAt: json['expires_at'] ?? 0,
      userRole: json['user_role'] ?? '',
    );
  }

  Map<String, dynamic> toJson() => {
    'access_token': accessToken,
    'refresh_token': refreshToken,
    'expires_at': expiresAt,
    'user_role': userRole,
  };
}

/// 用户信息模型
class UserInfo {
  final String userId;
  final String name;
  final String avatar;
  final String role;
  final String phone;
  final List<String> classIds;   // 关联的班级ID列表

  UserInfo({
    required this.userId,
    required this.name,
    required this.avatar,
    required this.role,
    required this.phone,
    required this.classIds,
  });

  factory UserInfo.fromJson(Map<String, dynamic> json) {
    return UserInfo(
      userId: json['user_id'] ?? '',
      name: json['name'] ?? '',
      avatar: json['avatar'] ?? '',
      role: json['role'] ?? '',
      phone: json['phone'] ?? '',
      classIds: List<String>.from(json['class_ids'] ?? []),
    );
  }
}

/// 作业信息模型
class AssignmentInfo {
  final String id;
  final String title;
  final String subject;        // 科目
  final String type;           // 类型: homework / exam / practice
  final String classId;
  final int publishTime;       // 发布时间
  final int deadline;          // 截止时间
  final int submittedCount;    // 已提交人数
  final int totalCount;        // 应提交人数
  final int status;            // 0=进行中, 1=已截止, 2=已批改

  AssignmentInfo({
    required this.id,
    required this.title,
    required this.subject,
    required this.type,
    required this.classId,
    required this.publishTime,
    required this.deadline,
    required this.submittedCount,
    required this.totalCount,
    required this.status,
  });

  factory AssignmentInfo.fromJson(Map<String, dynamic> json) {
    return AssignmentInfo(
      id: json['id'] ?? '',
      title: json['title'] ?? '',
      subject: json['subject'] ?? '',
      type: json['type'] ?? '',
      classId: json['class_id'] ?? '',
      publishTime: json['publish_time'] ?? 0,
      deadline: json['deadline'] ?? 0,
      submittedCount: json['submitted_count'] ?? 0,
      totalCount: json['total_count'] ?? 0,
      status: json['status'] ?? 0,
    );
  }
}

/// 学情报告模型
class LearningReport {
  final String studentId;
  final String studentName;
  final String subject;
  final double overallScore;             // 综合评分（0-100）
  final Map<String, double> knowledgeMap; // 知识点掌握度
  final List<ErrorItem> topErrors;       // 高频错题
  final WritingGrowth writingGrowth;     // 书写成长数据

  LearningReport({
    required this.studentId,
    required this.studentName,
    required this.subject,
    required this.overallScore,
    required this.knowledgeMap,
    required this.topErrors,
    required this.writingGrowth,
  });

  factory LearningReport.fromJson(Map<String, dynamic> json) {
    return LearningReport(
      studentId: json['student_id'] ?? '',
      studentName: json['student_name'] ?? '',
      subject: json['subject'] ?? '',
      overallScore: (json['overall_score'] ?? 0).toDouble(),
      knowledgeMap: Map<String, double>.from(json['knowledge_map'] ?? {}),
      topErrors: (json['top_errors'] as List? ?? [])
          .map((e) => ErrorItem.fromJson(e))
          .toList(),
      writingGrowth: WritingGrowth.fromJson(json['writing_growth'] ?? {}),
    );
  }
}

/// 错题条目
class ErrorItem {
  final String questionId;
  final String content;
  final String knowledgePoint;
  final int errorCount;
  final String errorReason;

  ErrorItem({
    required this.questionId,
    required this.content,
    required this.knowledgePoint,
    required this.errorCount,
    required this.errorReason,
  });

  factory ErrorItem.fromJson(Map<String, dynamic> json) {
    return ErrorItem(
      questionId: json['question_id'] ?? '',
      content: json['content'] ?? '',
      knowledgePoint: json['knowledge_point'] ?? '',
      errorCount: json['error_count'] ?? 0,
      errorReason: json['error_reason'] ?? '',
    );
  }
}

/// 书写成长数据
class WritingGrowth {
  final List<double> scores;        // 历次书写评分
  final List<String> dates;         // 对应日期
  final double strokeAccuracy;      // 笔顺正确率
  final double writingNeatness;     // 书写规范性
  final String improvement;         // 进步趋势描述

  WritingGrowth({
    required this.scores,
    required this.dates,
    required this.strokeAccuracy,
    required this.writingNeatness,
    required this.improvement,
  });

  factory WritingGrowth.fromJson(Map<String, dynamic> json) {
    return WritingGrowth(
      scores: List<double>.from(json['scores'] ?? []),
      dates: List<String>.from(json['dates'] ?? []),
      strokeAccuracy: (json['stroke_accuracy'] ?? 0).toDouble(),
      writingNeatness: (json['writing_neatness'] ?? 0).toDouble(),
      improvement: json['improvement'] ?? '',
    );
  }
}

/* ========== API服务实现 ========== */

/// 云平台API服务 - 管理所有HTTP通信
/// 采用Dio作为HTTP客户端，支持拦截器链、证书锁定、自动重试
class CloudApiService {
  /// 云平台API基础地址
  static const String _baseUrl = 'https://api.writech.com/v1';

  /// HMAC签名密钥（从安全存储中加载）
  final String _hmacSecret;

  /// 当前认证令牌
  AuthToken? _authToken;

  /// Token刷新锁（防止并发刷新）
  bool _isRefreshing = false;
  final List<Function> _refreshQueue = [];

  /// HTTP客户端实例
  late final HttpClient _httpClient;

  /// 离线请求队列（断网时暂存）
  final List<Map<String, dynamic>> _offlineQueue = [];

  /// 最大重试次数
  static const int _maxRetries = 3;

  CloudApiService({String hmacSecret = ''}) : _hmacSecret = hmacSecret {
    _httpClient = HttpClient()
      ..connectionTimeout = const Duration(seconds: 15)
      ..idleTimeout = const Duration(seconds: 60);

    // 配置证书锁定（防止中间人攻击）
    _httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) {
      // 验证证书指纹是否匹配预置的服务器证书
      final fingerprint = sha256.convert(cert.der).toString();
      const expectedFingerprint = 'a1b2c3d4e5f6...'; // 预置证书指纹
      return fingerprint == expectedFingerprint;
    };
  }

  /// 设置认证令牌（登录成功后调用）
  void setAuthToken(AuthToken token) {
    _authToken = token;
  }

  /// 生成请求签名（HMAC-SHA256）
  /// 签名内容: METHOD + PATH + TIMESTAMP + BODY_HASH
  String _generateSignature(String method, String path, int timestamp, String body) {
    final bodyHash = sha256.convert(utf8.encode(body)).toString();
    final content = '$method\n$path\n$timestamp\n$bodyHash';
    final hmacSha256 = Hmac(sha256, utf8.encode(_hmacSecret));
    return hmacSha256.convert(utf8.encode(content)).toString();
  }

  /// 统一HTTP请求方法（带签名、Token、重试）
  Future<ApiResponse<T>> _request<T>({
    required String method,
    required String path,
    Map<String, dynamic>? queryParams,
    Map<String, dynamic>? body,
    T Function(dynamic)? fromData,
    int retryCount = 0,
  }) async {
    // 检查Token是否需要刷新
    if (_authToken != null && _authToken!.isExpiringSoon) {
      await _refreshToken();
    }

    final uri = Uri.parse('$_baseUrl$path').replace(queryParameters: 
      queryParams?.map((k, v) => MapEntry(k, v.toString())));
    final timestamp = DateTime.now().millisecondsSinceEpoch;
    final bodyStr = body != null ? jsonEncode(body) : '';
    final signature = _generateSignature(method, path, timestamp, bodyStr);

    try {
      final request = await _httpClient.openUrl(method, uri);

      // 设置请求头
      request.headers.set('Content-Type', 'application/json');
      request.headers.set('X-Timestamp', timestamp.toString());
      request.headers.set('X-Signature', signature);
      request.headers.set('X-Client', 'writech-mobile/1.0');
      if (_authToken != null) {
        request.headers.set('Authorization', 'Bearer ${_authToken!.accessToken}');
      }

      // 写入请求体
      if (body != null) {
        request.write(bodyStr);
      }

      // 发送请求并接收响应
      final response = await request.close();
      final responseBody = await response.transform(utf8.decoder).join();
      final jsonData = jsonDecode(responseBody) as Map<String, dynamic>;

      // 处理401未授权（Token过期）
      if (response.statusCode == 401 && retryCount < 1) {
        await _refreshToken();
        return _request(
          method: method, path: path, queryParams: queryParams,
          body: body, fromData: fromData, retryCount: retryCount + 1,
        );
      }

      return ApiResponse.fromJson(jsonData, fromData);
    } on SocketException {
      // 网络不可用，加入离线队列
      if (method == 'POST' || method == 'PUT') {
        _offlineQueue.add({
          'method': method, 'path': path,
          'body': body, 'timestamp': timestamp,
        });
      }
      return ApiResponse(code: -1, message: '网络连接不可用', timestamp: timestamp);
    } catch (e) {
      // 重试逻辑（指数退避）
      if (retryCount < _maxRetries) {
        await Future.delayed(Duration(seconds: 1 << retryCount));
        return _request(
          method: method, path: path, queryParams: queryParams,
          body: body, fromData: fromData, retryCount: retryCount + 1,
        );
      }
      return ApiResponse(code: -1, message: '请求失败: $e', timestamp: timestamp);
    }
  }

  /// 刷新Token（使用Refresh Token获取新的Access Token）
  Future<void> _refreshToken() async {
    if (_isRefreshing) {
      // 等待正在进行的刷新完成
      final completer = Completer<void>();
      _refreshQueue.add(() => completer.complete());
      return completer.future;
    }

    _isRefreshing = true;
    try {
      final response = await _request<AuthToken>(
        method: 'POST',
        path: '/auth/refresh',
        body: {'refresh_token': _authToken?.refreshToken ?? ''},
        fromData: (data) => AuthToken.fromJson(data),
      );

      if (response.isSuccess && response.data != null) {
        _authToken = response.data;
        // 持久化新Token到安全存储
        _persistToken(_authToken!);
      }
    } finally {
      _isRefreshing = false;
      // 通知所有等待的请求继续
      for (final callback in _refreshQueue) {
        callback();
      }
      _refreshQueue.clear();
    }
  }

  /// 持久化Token到Keychain/KeyStore
  void _persistToken(AuthToken token) {
    // 使用flutter_secure_storage存储到系统安全存储
    // iOS: Keychain  Android: KeyStore
  }

  /// 重放离线队列中的请求（网络恢复后调用）
  Future<int> replayOfflineQueue() async {
    int successCount = 0;
    final queue = List<Map<String, dynamic>>.from(_offlineQueue);
    _offlineQueue.clear();

    for (final item in queue) {
      final response = await _request(
        method: item['method'],
        path: item['path'],
        body: item['body'],
      );
      if (response.isSuccess) successCount++;
    }
    return successCount;
  }

  /* ========== 认证相关API ========== */

  /// 手机号+验证码登录
  Future<ApiResponse<AuthToken>> loginByPhone(String phone, String code) {
    return _request(
      method: 'POST',
      path: '/auth/login/phone',
      body: {'phone': phone, 'code': code},
      fromData: (data) => AuthToken.fromJson(data),
    );
  }

  /// 微信OAuth登录
  Future<ApiResponse<AuthToken>> loginByWechat(String wxCode) {
    return _request(
      method: 'POST',
      path: '/auth/login/wechat',
      body: {'wx_code': wxCode},
      fromData: (data) => AuthToken.fromJson(data),
    );
  }

  /// 获取当前用户信息
  Future<ApiResponse<UserInfo>> getUserInfo() {
    return _request(
      method: 'GET',
      path: '/user/profile',
      fromData: (data) => UserInfo.fromJson(data),
    );
  }

  /// 登出（撤销Token）
  Future<ApiResponse> logout() {
    return _request(method: 'POST', path: '/auth/logout');
  }

  /* ========== 作业相关API ========== */

  /// 获取作业列表（教师端）
  Future<ApiResponse<List<AssignmentInfo>>> getAssignmentList({
    required String classId,
    int page = 1,
    int pageSize = 20,
    String? status,
  }) {
    return _request(
      method: 'GET',
      path: '/assignment/list',
      queryParams: {
        'class_id': classId,
        'page': page,
        'page_size': pageSize,
        if (status != null) 'status': status,
      },
      fromData: (data) => (data as List)
          .map((e) => AssignmentInfo.fromJson(e))
          .toList(),
    );
  }

  /// 发布新作业（教师端）
  Future<ApiResponse<String>> publishAssignment({
    required String title,
    required String classId,
    required String subject,
    required int deadline,
    required List<Map<String, dynamic>> questions,
  }) {
    return _request(
      method: 'POST',
      path: '/assignment/publish',
      body: {
        'title': title,
        'class_id': classId,
        'subject': subject,
        'deadline': deadline,
        'questions': questions,
      },
    );
  }

  /* ========== 学情报告API ========== */

  /// 获取学生学情报告（家长端/教师端）
  Future<ApiResponse<LearningReport>> getStudentReport(String studentId, {String? subject}) {
    return _request(
      method: 'GET',
      path: '/report/student/$studentId',
      queryParams: subject != null ? {'subject': subject} : null,
      fromData: (data) => LearningReport.fromJson(data),
    );
  }

  /// 获取班级学情概览（教师端）
  Future<ApiResponse<Map<String, dynamic>>> getClassReport(String classId) {
    return _request(
      method: 'GET',
      path: '/report/class/$classId',
    );
  }

  /* ========== 消息通知API ========== */

  /// 获取消息列表
  Future<ApiResponse<List<Map<String, dynamic>>>> getMessageList({
    int page = 1,
    int pageSize = 20,
  }) {
    return _request(
      method: 'GET',
      path: '/message/list',
      queryParams: {'page': page, 'page_size': pageSize},
    );
  }

  /// 发送家校沟通消息（教师→家长）
  Future<ApiResponse> sendMessage({
    required String toUserId,
    required String content,
    String type = 'text',
  }) {
    return _request(
      method: 'POST',
      path: '/message/send',
      body: {'to_user_id': toUserId, 'content': content, 'type': type},
    );
  }

  /// 标记消息已读
  Future<ApiResponse> markMessageRead(List<String> messageIds) {
    return _request(
      method: 'PUT',
      path: '/message/read',
      body: {'message_ids': messageIds},
    );
  }

  /* ========== 笔迹数据API ========== */

  /// 上传笔迹数据（教师端蓝牙收笔后上传）
  Future<ApiResponse<String>> uploadStrokeData({
    required String assignmentId,
    required String studentId,
    required List<Map<String, dynamic>> strokes,
  }) {
    return _request(
      method: 'POST',
      path: '/stroke/upload',
      body: {
        'assignment_id': assignmentId,
        'student_id': studentId,
        'strokes': strokes,
        'client_time': DateTime.now().millisecondsSinceEpoch,
      },
    );
  }

  /// 获取笔迹回放数据
  Future<ApiResponse<List<Map<String, dynamic>>>> getStrokeReplay({
    required String assignmentId,
    required String studentId,
  }) {
    return _request(
      method: 'GET',
      path: '/stroke/replay',
      queryParams: {
        'assignment_id': assignmentId,
        'student_id': studentId,
      },
    );
  }

  /// 销毁HTTP客户端
  void dispose() {
    _httpClient.close();
    _offlineQueue.clear();
    _refreshQueue.clear();
  }
}
```

#### `service/ble_service.dart`

```dart
/// 自然写互动课堂手机端应用软件 V1.0
/// BLE蓝牙服务 - 教师端蓝牙连接点阵笔进行移动教学
///
/// 功能说明：
/// 1. BLE设备扫描与发现（按自然写笔设备UUID过滤）
/// 2. GATT连接与特征值订阅（实时接收笔迹坐标数据）
/// 3. 7字节紧凑坐标数据解码（x:16bit, y:16bit, pressure:8bit, timestamp:16bit）
/// 4. 多笔同时连接管理（教师端移动教学最多连接4支笔）
/// 5. 自动重连与连接状态监控
/// 6. 设备电量读取与低电量告警
/// 7. 蓝牙权限检查与引导
/// 8. 笔迹数据缓冲与批量回调

import 'dart:async';
import 'dart:typed_data';

/* ========== BLE协议常量定义 ========== */

/// 自然写点阵笔BLE服务UUID
class WritechBleUuids {
  /// 主服务UUID - 笔迹数据传输
  static const String strokeServiceUuid = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E';
  /// 笔迹数据特征值UUID（Notify模式，笔到手机）
  static const String strokeDataCharUuid = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E';
  /// 命令写入特征值UUID（Write模式，手机到笔）
  static const String commandCharUuid = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E';
  /// 设备信息服务UUID（标准BLE Device Information Service）
  static const String deviceInfoServiceUuid = '0000180A-0000-1000-8000-00805F9B34FB';
  /// 电池服务UUID（标准BLE Battery Service）
  static const String batteryServiceUuid = '0000180F-0000-1000-8000-00805F9B34FB';
  /// 电池电量特征值UUID
  static const String batteryLevelCharUuid = '00002A19-0000-1000-8000-00805F9B34FB';
}

/// BLE笔命令定义
class PenCommand {
  static const int cmdSetMode = 0x01;
  static const int cmdGetStatus = 0x02;
  static const int cmdSyncOffline = 0x03;
  static const int cmdSetName = 0x04;
  static const int cmdStartOta = 0x05;
  static const int cmdReset = 0xFF;
}

/* ========== 数据模型 ========== */

/// BLE笔设备信息
class PenDevice {
  final String deviceId;
  final String name;
  int rssi;
  int batteryLevel;
  String firmwareVersion;
  PenConnectionState state;
  DateTime? lastActiveTime;
  int offlineDataCount;

  PenDevice({
    required this.deviceId,
    required this.name,
    this.rssi = -100,
    this.batteryLevel = -1,
    this.firmwareVersion = '',
    this.state = PenConnectionState.disconnected,
    this.lastActiveTime,
    this.offlineDataCount = 0,
  });
}

/// 笔连接状态枚举
enum PenConnectionState {
  disconnected,
  connecting,
  connected,
  disconnecting,
}

/// 笔迹坐标点（从BLE数据解码后的结构化数据）
class StrokePoint {
  final double x;
  final double y;
  final double pressure;
  final int timestamp;
  final bool isPenDown;

  const StrokePoint({
    required this.x,
    required this.y,
    required this.pressure,
    required this.timestamp,
    required this.isPenDown,
  });

  Map<String, dynamic> toJson() => {
    'x': x, 'y': y,
    'pressure': pressure,
    'timestamp': timestamp,
    'pen_down': isPenDown,
  };
}

/// 笔迹数据回调事件
class StrokeDataEvent {
  final String deviceId;
  final List<StrokePoint> points;
  final int pageId;

  StrokeDataEvent({
    required this.deviceId,
    required this.points,
    required this.pageId,
  });
}

/* ========== BLE服务实现 ========== */

/// BLE蓝牙服务 - 管理点阵笔的蓝牙连接与数据传输
class BleConnectionService {
  /// 已连接或已发现的笔设备列表
  final Map<String, PenDevice> _devices = {};

  /// 笔迹数据流控制器（向上层广播解码后的笔迹坐标）
  final StreamController<StrokeDataEvent> _strokeStreamController =
      StreamController<StrokeDataEvent>.broadcast();

  /// 设备状态变化流
  final StreamController<PenDevice> _deviceStateController =
      StreamController<PenDevice>.broadcast();

  /// 扫描状态
  bool _isScanning = false;

  /// 最大同时连接数（教师移动教学最多4支笔）
  static const int maxConnections = 4;

  /// 自动重连间隔（秒）
  static const int reconnectIntervalSec = 5;

  /// 数据缓冲区大小（累积到一定量后批量回调）
  static const int batchSize = 10;

  /// 设备活跃超时时间（毫秒）
  static const int activeTimeoutMs = 30000;

  /// 低电量告警阈值
  static const int lowBatteryThreshold = 10;

  /// 重连计时器
  final Map<String, Timer> _reconnectTimers = {};

  /// 电量查询计时器
  Timer? _batteryCheckTimer;

  /// 笔迹数据缓冲区（按设备ID分组）
  final Map<String, List<StrokePoint>> _dataBuffers = {};

  /// 外部可订阅的笔迹数据流
  Stream<StrokeDataEvent> get strokeStream => _strokeStreamController.stream;

  /// 外部可订阅的设备状态流
  Stream<PenDevice> get deviceStateStream => _deviceStateController.stream;

  /// 获取当前已连接设备数量
  int get connectedCount =>
      _devices.values.where((d) => d.state == PenConnectionState.connected).length;

  /// 获取所有已发现设备列表
  List<PenDevice> get discoveredDevices => _devices.values.toList();

  /// 开始BLE扫描（发现周围的自然写点阵笔设备）
  /// 仅扫描包含自然写笔服务UUID的设备，过滤无关BLE设备
  Future<void> startScan({Duration timeout = const Duration(seconds: 10)}) async {
    if (_isScanning) {
      print('[BLE] 已在扫描中，忽略重复请求');
      return;
    }

    // 检查蓝牙权限和状态
    final hasPermission = await _checkBluetoothPermission();
    if (!hasPermission) {
      print('[BLE] 蓝牙权限未授予，无法扫描');
      return;
    }

    _isScanning = true;
    print('[BLE] 开始扫描自然写点阵笔设备...');

    // 使用flutter_blue扫描指定服务UUID的设备
    // 实际实现通过FlutterBluePlus.startScan()
    // 此处模拟扫描逻辑
    Timer(timeout, () {
      stopScan();
    });
  }

  /// 停止BLE扫描
  void stopScan() {
    if (!_isScanning) return;
    _isScanning = false;
    print('[BLE] 停止扫描');
  }

  /// 处理扫描到的设备广播数据
  /// 解析设备名称、信号强度、服务UUID
  void _onDeviceDiscovered(String deviceId, String name, int rssi, List<String> serviceUuids) {
    // 仅处理包含自然写笔服务UUID的设备
    if (!serviceUuids.contains(WritechBleUuids.strokeServiceUuid)) return;

    if (_devices.containsKey(deviceId)) {
      // 更新已知设备的RSSI
      _devices[deviceId]!.rssi = rssi;
    } else {
      // 发现新设备
      final device = PenDevice(
        deviceId: deviceId,
        name: name.isNotEmpty ? name : '未知笔设备',
        rssi: rssi,
      );
      _devices[deviceId] = device;
      print('[BLE] 发现新设备: $name (RSSI: $rssi)');
      _deviceStateController.add(device);
    }
  }

  /// 连接指定的点阵笔设备
  /// 建立GATT连接，发现服务，订阅笔迹数据特征值
  Future<bool> connectDevice(String deviceId) async {
    final device = _devices[deviceId];
    if (device == null) {
      print('[BLE] 未找到设备: $deviceId');
      return false;
    }

    // 检查连接数限制
    if (connectedCount >= maxConnections) {
      print('[BLE] 已达最大连接数限制 ($maxConnections)');
      return false;
    }

    device.state = PenConnectionState.connecting;
    _deviceStateController.add(device);
    print('[BLE] 正在连接: ${device.name}');

    try {
      // 步骤1: 建立BLE GATT连接
      // 实际调用: FlutterBluePlus.connect(device, autoConnect: false)
      await Future.delayed(const Duration(milliseconds: 500)); // 模拟连接耗时

      // 步骤2: 发现服务（查找笔迹数据服务和电池服务）
      await _discoverServices(deviceId);

      // 步骤3: 订阅笔迹数据Notify特征值
      await _subscribeStrokeData(deviceId);

      // 步骤4: 读取初始电量
      await _readBatteryLevel(deviceId);

      // 步骤5: 读取固件版本
      await _readFirmwareVersion(deviceId);

      device.state = PenConnectionState.connected;
      device.lastActiveTime = DateTime.now();
      _deviceStateController.add(device);

      // 初始化数据缓冲区
      _dataBuffers[deviceId] = [];

      // 启动电量定时检查（每60秒读取一次电量）
      _startBatteryCheck();

      print('[BLE] 连接成功: ${device.name}, 固件: ${device.firmwareVersion}, 电量: ${device.batteryLevel}%');
      return true;
    } catch (e) {
      device.state = PenConnectionState.disconnected;
      _deviceStateController.add(device);
      print('[BLE] 连接失败: ${device.name}, 错误: $e');

      // 设置自动重连计时器
      _scheduleReconnect(deviceId);
      return false;
    }
  }

  /// 发现BLE服务列表
  Future<void> _discoverServices(String deviceId) async {
    // 实际调用: device.discoverServices()
    // 验证是否包含笔迹数据服务UUID
    print('[BLE] 服务发现完成: $deviceId');
  }

  /// 订阅笔迹数据Notify特征值
  /// 设置MTU为247字节以支持最大数据包
  Future<void> _subscribeStrokeData(String deviceId) async {
    // 步骤1: 请求MTU协商（247字节，支持每包最多34个坐标点）
    // 实际调用: device.requestMtu(247)

    // 步骤2: 启用Notify
    // 实际调用: characteristic.setNotifyValue(true)

    // 步骤3: 监听Notify数据流
    // characteristic.onValueReceived.listen((data) => _onStrokeDataReceived(deviceId, data))
    print('[BLE] 笔迹数据订阅成功: $deviceId');
  }

  /// 处理接收到的BLE笔迹原始数据包
  /// 每个数据包包含1-34个7字节坐标点
  /// 7字节编码格式: [x_hi, x_lo, y_hi, y_lo, pressure, ts_hi, ts_lo]
  void _onStrokeDataReceived(String deviceId, Uint8List rawData) {
    final device = _devices[deviceId];
    if (device == null) return;

    // 更新设备活跃时间
    device.lastActiveTime = DateTime.now();

    // 数据包最小长度: 3字节头 + 7字节坐标 = 10字节
    if (rawData.length < 10) {
      print('[BLE] 数据包过短，丢弃: ${rawData.length}字节');
      return;
    }

    // 解析数据包头部（3字节）
    final packetType = rawData[0];    // 包类型: 0x01=实时数据, 0x02=离线数据
    final pageId = (rawData[1] << 8) | rawData[2]; // 点阵码页面ID
    final isPenDown = (packetType & 0x80) != 0;     // 最高位标识落笔状态

    // 验证CRC-16校验（数据包最后2字节）
    if (rawData.length > 5) {
      final payloadEnd = rawData.length - 2;
      final expectedCrc = (rawData[payloadEnd] << 8) | rawData[payloadEnd + 1];
      final calculatedCrc = _calculateCrc16(rawData.sublist(0, payloadEnd));
      if (expectedCrc != calculatedCrc) {
        print('[BLE] CRC校验失败，丢弃数据包');
        return;
      }
    }

    // 解码坐标数据（从第3字节开始，每7字节一个坐标点）
    final points = <StrokePoint>[];
    final dataEnd = rawData.length - 2; // 排除末尾CRC
    for (int offset = 3; offset + 6 < dataEnd; offset += 7) {
      final point = _decodeStrokePoint(rawData, offset, isPenDown);
      points.add(point);
    }

    if (points.isEmpty) return;

    // 添加到缓冲区
    final buffer = _dataBuffers[deviceId];
    if (buffer != null) {
      buffer.addAll(points);

      // 缓冲区达到批量大小时回调
      if (buffer.length >= batchSize) {
        final event = StrokeDataEvent(
          deviceId: deviceId,
          points: List<StrokePoint>.from(buffer),
          pageId: pageId,
        );
        _strokeStreamController.add(event);
        buffer.clear();
      }
    }
  }

  /// 解码单个7字节坐标点
  /// 编码格式: x(16bit) + y(16bit) + pressure(8bit) + timestamp(16bit)
  StrokePoint _decodeStrokePoint(Uint8List data, int offset, bool isPenDown) {
    // X坐标（大端序，单位: 0.01mm，范围: 0-65535 即 0-655.35mm）
    final rawX = (data[offset] << 8) | data[offset + 1];
    final x = rawX * 0.01;

    // Y坐标（同上）
    final rawY = (data[offset + 2] << 8) | data[offset + 3];
    final y = rawY * 0.01;

    // 压力值（0-255，归一化到0.0-1.0）
    final rawPressure = data[offset + 4];
    final pressure = rawPressure / 255.0;

    // 时间戳（毫秒增量，相对于笔迹起始）
    final timestamp = (data[offset + 5] << 8) | data[offset + 6];

    return StrokePoint(
      x: x, y: y,
      pressure: pressure,
      timestamp: timestamp,
      isPenDown: isPenDown,
    );
  }

  /// CRC-16 CCITT校验计算
  int _calculateCrc16(Uint8List data) {
    int crc = 0xFFFF;
    for (int i = 0; i < data.length; i++) {
      crc ^= (data[i] << 8);
      for (int j = 0; j < 8; j++) {
        if ((crc & 0x8000) != 0) {
          crc = ((crc << 1) ^ 0x1021) & 0xFFFF;
        } else {
          crc = (crc << 1) & 0xFFFF;
        }
      }
    }
    return crc;
  }

  /// 读取设备电量
  Future<void> _readBatteryLevel(String deviceId) async {
    final device = _devices[deviceId];
    if (device == null) return;

    // 实际调用: 读取Battery Service的Battery Level特征值
    // device.batteryLevel = characteristic.value[0];
    device.batteryLevel = 85; // 模拟值

    // 低电量告警
    if (device.batteryLevel > 0 && device.batteryLevel <= lowBatteryThreshold) {
      print('[BLE] 低电量告警: ${device.name} 电量 ${device.batteryLevel}%');
      _deviceStateController.add(device);
    }
  }

  /// 读取固件版本号
  Future<void> _readFirmwareVersion(String deviceId) async {
    final device = _devices[deviceId];
    if (device == null) return;
    // 读取Device Information Service的Firmware Revision特征值
    device.firmwareVersion = '1.2.0';
  }

  /// 启动电量定时检查
  void _startBatteryCheck() {
    _batteryCheckTimer?.cancel();
    _batteryCheckTimer = Timer.periodic(const Duration(seconds: 60), (_) {
      for (final entry in _devices.entries) {
        if (entry.value.state == PenConnectionState.connected) {
          _readBatteryLevel(entry.key);
        }
      }
    });
  }

  /// 向笔设备发送命令
  Future<void> sendCommand(String deviceId, int command, {Uint8List? payload}) async {
    final device = _devices[deviceId];
    if (device == null || device.state != PenConnectionState.connected) {
      print('[BLE] 设备未连接，无法发送命令');
      return;
    }

    // 构造命令数据包: [cmd, payload_len, ...payload, crc_hi, crc_lo]
    final totalLen = 2 + (payload?.length ?? 0) + 2;
    final packet = Uint8List(totalLen);
    packet[0] = command;
    packet[1] = payload?.length ?? 0;
    if (payload != null) {
      packet.setRange(2, 2 + payload.length, payload);
    }
    final crc = _calculateCrc16(packet.sublist(0, totalLen - 2));
    packet[totalLen - 2] = (crc >> 8) & 0xFF;
    packet[totalLen - 1] = crc & 0xFF;

    // 写入命令特征值
    // 实际调用: commandCharacteristic.write(packet)
    print('[BLE] 发送命令: 0x${command.toRadixString(16)} -> ${device.name}');
  }

  /// 请求同步离线数据（笔断线期间缓存的笔迹）
  Future<void> syncOfflineData(String deviceId) async {
    await sendCommand(deviceId, PenCommand.cmdSyncOffline);
    print('[BLE] 已请求同步离线数据: $deviceId');
  }

  /// 断开指定设备
  Future<void> disconnectDevice(String deviceId) async {
    final device = _devices[deviceId];
    if (device == null) return;

    // 取消重连计时器
    _reconnectTimers[deviceId]?.cancel();
    _reconnectTimers.remove(deviceId);

    device.state = PenConnectionState.disconnecting;
    _deviceStateController.add(device);

    // 清空缓冲区中的残余数据
    final buffer = _dataBuffers[deviceId];
    if (buffer != null && buffer.isNotEmpty) {
      _strokeStreamController.add(StrokeDataEvent(
        deviceId: deviceId, points: List.from(buffer), pageId: 0,
      ));
      buffer.clear();
    }

    // 断开GATT连接
    // 实际调用: device.disconnect()
    device.state = PenConnectionState.disconnected;
    _deviceStateController.add(device);
    _dataBuffers.remove(deviceId);
    print('[BLE] 已断开设备: ${device.name}');
  }

  /// 设置自动重连计时器
  void _scheduleReconnect(String deviceId) {
    _reconnectTimers[deviceId]?.cancel();
    _reconnectTimers[deviceId] = Timer(
      Duration(seconds: reconnectIntervalSec),
      () async {
        final device = _devices[deviceId];
        if (device != null && device.state == PenConnectionState.disconnected) {
          print('[BLE] 尝试自动重连: ${device.name}');
          await connectDevice(deviceId);
        }
      },
    );
  }

  /// 检查蓝牙权限（Android需要位置权限，iOS需要蓝牙使用描述）
  Future<bool> _checkBluetoothPermission() async {
    // Android: 检查 BLUETOOTH_SCAN, BLUETOOTH_CONNECT, ACCESS_FINE_LOCATION
    // iOS: 检查 CBManager authorization status
    return true;
  }

  /// 断开所有设备并释放资源
  void dispose() {
    // 停止扫描
    stopScan();

    // 取消所有重连计时器
    for (final timer in _reconnectTimers.values) {
      timer.cancel();
    }
    _reconnectTimers.clear();

    // 停止电量检查
    _batteryCheckTimer?.cancel();

    // 断开所有设备
    for (final deviceId in _devices.keys.toList()) {
      disconnectDevice(deviceId);
    }

    // 关闭流控制器
    _strokeStreamController.close();
    _deviceStateController.close();

    _devices.clear();
    _dataBuffers.clear();
    print('[BLE] BLE服务已销毁');
  }
}
```

#### `service/websocket_service.dart`

```dart
/// 自然写互动课堂手机端应用软件 V1.0
/// WebSocket实时通信服务 - 接收云端实时推送通知
///
/// 功能说明：
/// 1. WebSocket长连接管理（建立、维持、重连）
/// 2. 心跳机制（30秒间隔，检测连接存活性）
/// 3. 消息类型分发（新作业、批改完成、课堂互动、家校消息）
/// 4. 指数退避重连策略（断线后自动重连，逐步增加间隔）
/// 5. 消息ACK确认（确保重要消息不丢失）
/// 6. 离线消息补发（重连后请求离线期间的消息）

import 'dart:async';
import 'dart:convert';

/* ========== 消息类型定义 ========== */

/// WebSocket消息类型枚举
enum WsMessageType {
  heartbeat,           // 心跳包
  heartbeatAck,        // 心跳响应
  newAssignment,       // 新作业通知
  gradeComplete,       // 批改完成通知
  classroomEvent,      // 课堂互动事件（发题/收卷等）
  parentMessage,       // 家校沟通消息
  systemNotice,        // 系统公告
  strokeRealtime,      // 实时笔迹数据（课堂模式）
  offlineSync,         // 离线消息同步
  ack,                 // 消息确认
}

/// WebSocket消息模型
class WsMessage {
  final String id;                   // 消息唯一ID
  final WsMessageType type;         // 消息类型
  final Map<String, dynamic> data;  // 消息内容
  final int timestamp;              // 服务端时间戳
  final bool requireAck;            // 是否需要ACK确认

  WsMessage({
    required this.id,
    required this.type,
    required this.data,
    required this.timestamp,
    this.requireAck = false,
  });

  /// 从JSON反序列化
  factory WsMessage.fromJson(Map<String, dynamic> json) {
    return WsMessage(
      id: json['id'] ?? '',
      type: _parseMessageType(json['type'] ?? ''),
      data: Map<String, dynamic>.from(json['data'] ?? {}),
      timestamp: json['timestamp'] ?? 0,
      requireAck: json['require_ack'] ?? false,
    );
  }

  /// 序列化为JSON
  Map<String, dynamic> toJson() => {
    'id': id,
    'type': type.name,
    'data': data,
    'timestamp': timestamp,
  };

  /// 解析消息类型字符串
  static WsMessageType _parseMessageType(String typeStr) {
    switch (typeStr) {
      case 'heartbeat': return WsMessageType.heartbeat;
      case 'heartbeat_ack': return WsMessageType.heartbeatAck;
      case 'new_assignment': return WsMessageType.newAssignment;
      case 'grade_complete': return WsMessageType.gradeComplete;
      case 'classroom_event': return WsMessageType.classroomEvent;
      case 'parent_message': return WsMessageType.parentMessage;
      case 'system_notice': return WsMessageType.systemNotice;
      case 'stroke_realtime': return WsMessageType.strokeRealtime;
      case 'offline_sync': return WsMessageType.offlineSync;
      case 'ack': return WsMessageType.ack;
      default: return WsMessageType.systemNotice;
    }
  }
}

/* ========== WebSocket连接状态 ========== */

/// 连接状态枚举
enum WsConnectionState {
  disconnected,     // 未连接
  connecting,       // 正在连接
  connected,        // 已连接
  reconnecting,     // 重连中
}

/* ========== WebSocket服务实现 ========== */

/// WebSocket实时通信服务
/// 维护与云平台的长连接，接收实时推送通知
class WebSocketService {
  /// WebSocket服务器地址
  static const String _wsUrl = 'wss://ws.writech.com/v1/notify';

  /// 心跳间隔（秒）
  static const int heartbeatIntervalSec = 30;

  /// 心跳超时时间（秒，超过此时间未收到心跳响应则认为连接断开）
  static const int heartbeatTimeoutSec = 45;

  /// 最大重连间隔（秒，指数退避上限）
  static const int maxReconnectIntervalSec = 60;

  /// WebSocket实例
  dynamic _webSocket;  // WebSocket

  /// 连接状态
  WsConnectionState _state = WsConnectionState.disconnected;

  /// 当前认证Token
  String _authToken = '';

  /// 心跳定时器
  Timer? _heartbeatTimer;

  /// 心跳超时定时器
  Timer? _heartbeatTimeoutTimer;

  /// 重连定时器
  Timer? _reconnectTimer;

  /// 当前重连尝试次数（用于指数退避计算）
  int _reconnectAttempts = 0;

  /// 最后收到消息的时间戳（用于离线消息补发）
  int _lastMessageTimestamp = 0;

  /// 消息分发回调注册表
  final Map<WsMessageType, List<Function(WsMessage)>> _handlers = {};

  /// 连接状态变化回调
  final List<Function(WsConnectionState)> _stateListeners = [];

  /// 待ACK的消息队列（消息ID -> 超时Timer）
  final Map<String, Timer> _pendingAcks = {};

  /// 获取当前连接状态
  WsConnectionState get state => _state;

  /// 设置认证Token（登录成功后调用）
  void setAuthToken(String token) {
    _authToken = token;
  }

  /// 注册消息处理器
  /// 同一类型可注册多个处理器，按注册顺序依次执行
  void on(WsMessageType type, Function(WsMessage) handler) {
    _handlers.putIfAbsent(type, () => []);
    _handlers[type]!.add(handler);
  }

  /// 移除消息处理器
  void off(WsMessageType type, Function(WsMessage) handler) {
    _handlers[type]?.remove(handler);
  }

  /// 监听连接状态变化
  void onStateChange(Function(WsConnectionState) listener) {
    _stateListeners.add(listener);
  }

  /// 建立WebSocket连接
  /// 附带认证Token和最后消息时间戳（用于离线消息补发）
  Future<void> connect() async {
    if (_state == WsConnectionState.connected || _state == WsConnectionState.connecting) {
      return;
    }

    _updateState(WsConnectionState.connecting);

    try {
      // 构造带认证参数的WebSocket URL
      final url = '$_wsUrl?token=$_authToken&last_ts=$_lastMessageTimestamp';

      // 建立WebSocket连接
      // 实际实现: _webSocket = await WebSocket.connect(url);
      print('[WebSocket] 正在连接: $_wsUrl');

      // 模拟连接成功
      await Future.delayed(const Duration(milliseconds: 300));

      _updateState(WsConnectionState.connected);
      _reconnectAttempts = 0; // 重置重连计数

      // 启动心跳机制
      _startHeartbeat();

      // 监听消息流
      // _webSocket.listen(_onMessage, onDone: _onDisconnected, onError: _onError);

      print('[WebSocket] 连接成功');
    } catch (e) {
      print('[WebSocket] 连接失败: $e');
      _updateState(WsConnectionState.disconnected);
      _scheduleReconnect();
    }
  }

  /// 处理接收到的WebSocket消息
  void _onMessage(dynamic rawData) {
    try {
      final json = jsonDecode(rawData as String) as Map<String, dynamic>;
      final message = WsMessage.fromJson(json);

      // 更新最后消息时间戳
      if (message.timestamp > _lastMessageTimestamp) {
        _lastMessageTimestamp = message.timestamp;
      }

      // 处理心跳响应
      if (message.type == WsMessageType.heartbeatAck) {
        _onHeartbeatAck();
        return;
      }

      // 处理ACK确认
      if (message.type == WsMessageType.ack) {
        _onAckReceived(message.data['ack_id'] ?? '');
        return;
      }

      // 如果消息需要ACK，发送确认
      if (message.requireAck) {
        _sendAck(message.id);
      }

      // 分发消息到注册的处理器
      _dispatchMessage(message);
    } catch (e) {
      print('[WebSocket] 消息解析失败: $e');
    }
  }

  /// 分发消息到对应类型的处理器
  void _dispatchMessage(WsMessage message) {
    final handlers = _handlers[message.type];
    if (handlers != null && handlers.isNotEmpty) {
      for (final handler in handlers) {
        try {
          handler(message);
        } catch (e) {
          print('[WebSocket] 消息处理器异常: $e');
        }
      }
    } else {
      print('[WebSocket] 未注册的消息类型: ${message.type}');
    }
  }

  /// 发送消息确认（ACK）
  void _sendAck(String messageId) {
    _send({
      'type': 'ack',
      'data': {'ack_id': messageId},
      'timestamp': DateTime.now().millisecondsSinceEpoch,
    });
  }

  /// 处理收到的ACK确认
  void _onAckReceived(String messageId) {
    _pendingAcks[messageId]?.cancel();
    _pendingAcks.remove(messageId);
  }

  /// 启动心跳机制
  /// 每30秒发送一次心跳包，45秒内未收到响应则断开重连
  void _startHeartbeat() {
    _stopHeartbeat();
    _heartbeatTimer = Timer.periodic(
      Duration(seconds: heartbeatIntervalSec),
      (_) => _sendHeartbeat(),
    );
  }

  /// 发送心跳包
  void _sendHeartbeat() {
    _send({
      'type': 'heartbeat',
      'timestamp': DateTime.now().millisecondsSinceEpoch,
    });

    // 设置心跳超时检测
    _heartbeatTimeoutTimer?.cancel();
    _heartbeatTimeoutTimer = Timer(
      Duration(seconds: heartbeatTimeoutSec),
      () {
        print('[WebSocket] 心跳超时，断开连接');
        _onDisconnected();
      },
    );
  }

  /// 收到心跳响应，取消超时计时器
  void _onHeartbeatAck() {
    _heartbeatTimeoutTimer?.cancel();
  }

  /// 停止心跳
  void _stopHeartbeat() {
    _heartbeatTimer?.cancel();
    _heartbeatTimer = null;
    _heartbeatTimeoutTimer?.cancel();
    _heartbeatTimeoutTimer = null;
  }

  /// 发送JSON数据
  void _send(Map<String, dynamic> data) {
    if (_state != WsConnectionState.connected) return;
    try {
      final jsonStr = jsonEncode(data);
      // 实际调用: _webSocket.add(jsonStr);
      print('[WebSocket] 发送: ${data['type']}');
    } catch (e) {
      print('[WebSocket] 发送失败: $e');
    }
  }

  /// 连接断开处理
  void _onDisconnected() {
    _stopHeartbeat();
    _updateState(WsConnectionState.disconnected);
    print('[WebSocket] 连接已断开');
    _scheduleReconnect();
  }

  /// 连接错误处理
  void _onError(dynamic error) {
    print('[WebSocket] 连接错误: $error');
    _onDisconnected();
  }

  /// 安排自动重连（指数退避策略）
  /// 间隔: 1s, 2s, 4s, 8s, 16s, 32s, 60s(上限)
  void _scheduleReconnect() {
    _reconnectTimer?.cancel();

    final interval = _calculateReconnectInterval();
    _updateState(WsConnectionState.reconnecting);
    print('[WebSocket] ${interval}秒后尝试重连 (第${_reconnectAttempts + 1}次)');

    _reconnectTimer = Timer(Duration(seconds: interval), () {
      _reconnectAttempts++;
      connect();
    });
  }

  /// 计算重连间隔（指数退避，上限60秒）
  int _calculateReconnectInterval() {
    final interval = 1 << _reconnectAttempts; // 2^n
    return interval > maxReconnectIntervalSec ? maxReconnectIntervalSec : interval;
  }

  /// 更新连接状态并通知监听器
  void _updateState(WsConnectionState newState) {
    if (_state == newState) return;
    _state = newState;
    for (final listener in _stateListeners) {
      try {
        listener(newState);
      } catch (e) {
        print('[WebSocket] 状态监听器异常: $e');
      }
    }
  }

  /// 主动重连（应用前台恢复时调用）
  void reconnect() {
    if (_state == WsConnectionState.connected) return;
    _reconnectAttempts = 0;
    connect();
  }

  /// 断开连接并释放资源
  void disconnect() {
    _reconnectTimer?.cancel();
    _reconnectTimer = null;
    _stopHeartbeat();

    // 取消所有待ACK的超时计时器
    for (final timer in _pendingAcks.values) {
      timer.cancel();
    }
    _pendingAcks.clear();

    // 关闭WebSocket连接
    // 实际调用: _webSocket?.close();
    _webSocket = null;

    _updateState(WsConnectionState.disconnected);
    print('[WebSocket] 已主动断开连接');
  }

  /// 销毁服务（释放所有资源和回调）
  void dispose() {
    disconnect();
    _handlers.clear();
    _stateListeners.clear();
  }
}
```

### `ui/common/`

#### `ui/common/stroke_canvas.dart`

```dart
/// 自然写互动课堂手机端应用软件 V1.0
/// 笔迹渲染组件 - CustomPainter实现高性能笔迹绘制与回放
///
/// 功能说明：
/// 1. 自定义CustomPainter实现60fps笔迹渲染
/// 2. 贝塞尔曲线平滑算法（消除锯齿）
/// 3. 压力感应笔锋效果（笔画粗细随压力变化）
/// 4. 笔迹回放动画（逐点重放书写过程）
/// 5. 多种笔迹颜色和宽度支持
/// 6. 笔迹缩放与平移（手势操作）
/// 7. 双缓冲渲染优化（离屏缓存已绘制内容）

import 'dart:async';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';

/* ========== 笔迹数据结构 ========== */

/// 笔迹点数据
class StrokePointData {
  final double x;
  final double y;
  final double pressure;
  final int timestamp;

  const StrokePointData({
    required this.x,
    required this.y,
    this.pressure = 0.5,
    required this.timestamp,
  });
}

/// 笔画数据（一次落笔到抬笔的完整路径）
class StrokeData {
  final List<StrokePointData> points;
  final Color color;
  final double baseWidth;

  StrokeData({
    required this.points,
    this.color = Colors.black,
    this.baseWidth = 2.0,
  });
}

/* ========== 笔迹渲染Widget ========== */

/// 笔迹画布Widget - 展示笔迹渲染与回放
class StrokeCanvasWidget extends StatefulWidget {
  /// 笔迹数据列表
  final List<StrokeData> strokes;

  /// 是否启用回放模式
  final bool enableReplay;

  /// 回放速度倍率（1.0=原速，2.0=两倍速）
  final double replaySpeed;

  /// 画布背景色
  final Color backgroundColor;

  /// 是否显示坐标网格
  final bool showGrid;

  const StrokeCanvasWidget({
    super.key,
    required this.strokes,
    this.enableReplay = false,
    this.replaySpeed = 1.0,
    this.backgroundColor = Colors.white,
    this.showGrid = false,
  });

  @override
  State<StrokeCanvasWidget> createState() => _StrokeCanvasWidgetState();
}

class _StrokeCanvasWidgetState extends State<StrokeCanvasWidget>
    with SingleTickerProviderStateMixin {
  /// 回放动画控制器
  AnimationController? _replayController;

  /// 当前回放进度（0.0-1.0）
  double _replayProgress = 0.0;

  /// 缩放比例
  double _scale = 1.0;

  /// 平移偏移量
  Offset _offset = Offset.zero;

  /// 缩放手势起始比例
  double _previousScale = 1.0;

  /// 离屏缓存（已绘制的静态笔迹）
  ui.Image? _cachedImage;

  /// 是否需要重建缓存
  bool _needsRebuildCache = true;

  @override
  void initState() {
    super.initState();
    if (widget.enableReplay) {
      _startReplay();
    }
  }

  @override
  void didUpdateWidget(covariant StrokeCanvasWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.strokes != oldWidget.strokes) {
      _needsRebuildCache = true;
    }
    if (widget.enableReplay && !oldWidget.enableReplay) {
      _startReplay();
    }
  }

  @override
  void dispose() {
    _replayController?.dispose();
    _cachedImage?.dispose();
    super.dispose();
  }

  /// 启动笔迹回放动画
  void _startReplay() {
    // 计算总回放时长（基于笔迹时间跨度）
    if (widget.strokes.isEmpty) return;

    int totalDuration = 0;
    for (final stroke in widget.strokes) {
      if (stroke.points.isNotEmpty) {
        totalDuration = max(totalDuration,
            stroke.points.last.timestamp - stroke.points.first.timestamp);
      }
    }

    // 根据回放速度调整时长
    final durationMs = (totalDuration / widget.replaySpeed).round();

    _replayController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: max(durationMs, 1000)),
    );

    _replayController!.addListener(() {
      setState(() {
        _replayProgress = _replayController!.value;
      });
    });

    _replayController!.forward();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      // 缩放手势
      onScaleStart: (details) {
        _previousScale = _scale;
      },
      onScaleUpdate: (details) {
        setState(() {
          _scale = (_previousScale * details.scale).clamp(0.5, 5.0);
          _offset += details.focalPointDelta;
        });
      },
      // 双击重置缩放
      onDoubleTap: () {
        setState(() {
          _scale = 1.0;
          _offset = Offset.zero;
        });
      },
      child: ClipRect(
        child: CustomPaint(
          painter: StrokePainter(
            strokes: widget.strokes,
            replayProgress: widget.enableReplay ? _replayProgress : 1.0,
            scale: _scale,
            offset: _offset,
            backgroundColor: widget.backgroundColor,
            showGrid: widget.showGrid,
          ),
          size: Size.infinite,
        ),
      ),
    );
  }
}

/* ========== 笔迹渲染Painter ========== */

/// CustomPainter实现 - 高性能笔迹绘制
class StrokePainter extends CustomPainter {
  final List<StrokeData> strokes;
  final double replayProgress;
  final double scale;
  final Offset offset;
  final Color backgroundColor;
  final bool showGrid;

  StrokePainter({
    required this.strokes,
    this.replayProgress = 1.0,
    this.scale = 1.0,
    this.offset = Offset.zero,
    this.backgroundColor = Colors.white,
    this.showGrid = false,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // 绘制背景
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      Paint()..color = backgroundColor,
    );

    // 绘制网格（可选）
    if (showGrid) {
      _drawGrid(canvas, size);
    }

    // 保存画布状态，应用变换
    canvas.save();
    canvas.translate(offset.dx, offset.dy);
    canvas.scale(scale);

    // 计算当前回放应显示的总点数
    int totalPoints = 0;
    for (final stroke in strokes) {
      totalPoints += stroke.points.length;
    }
    final visiblePoints = (totalPoints * replayProgress).round();

    // 逐笔画渲染
    int pointCounter = 0;
    for (final stroke in strokes) {
      if (pointCounter >= visiblePoints) break;

      final strokeVisibleCount = min(
        stroke.points.length,
        visiblePoints - pointCounter,
      );

      if (strokeVisibleCount > 1) {
        _drawStroke(canvas, stroke, strokeVisibleCount);
      }

      pointCounter += stroke.points.length;
    }

    canvas.restore();
  }

  /// 绘制单个笔画（贝塞尔曲线平滑 + 压力笔锋）
  void _drawStroke(Canvas canvas, StrokeData stroke, int visibleCount) {
    if (visibleCount < 2) return;

    final paint = Paint()
      ..color = stroke.color
      ..strokeCap = StrokeCap.round
      ..strokeJoin = StrokeJoin.round
      ..style = PaintingStyle.stroke
      ..isAntiAlias = true;

    // 使用压力感应笔锋渲染
    for (int i = 1; i < visibleCount; i++) {
      final prev = stroke.points[i - 1];
      final curr = stroke.points[i];

      // 根据压力值计算笔画宽度
      // 压力越大，笔画越粗；落笔和抬笔时笔画变细（模拟笔锋效果）
      final pressureWidth = _calculatePressureWidth(
        stroke.baseWidth, prev.pressure, curr.pressure,
        i, visibleCount,
      );

      paint.strokeWidth = pressureWidth;

      if (i >= 2 && i < visibleCount) {
        // 三次贝塞尔曲线平滑（消除折线锯齿）
        final prevPrev = stroke.points[i - 2];
        final cp1x = prev.x + (curr.x - prevPrev.x) / 6.0;
        final cp1y = prev.y + (curr.y - prevPrev.y) / 6.0;
        final cp2x = curr.x - (curr.x - prev.x) / 6.0;
        final cp2y = curr.y - (curr.y - prev.y) / 6.0;

        final path = Path()
          ..moveTo(prev.x, prev.y)
          ..cubicTo(cp1x, cp1y, cp2x, cp2y, curr.x, curr.y);

        canvas.drawPath(path, paint);
      } else {
        // 前两个点使用直线连接
        canvas.drawLine(
          ui.Offset(prev.x, prev.y),
          ui.Offset(curr.x, curr.y),
          paint,
        );
      }
    }
  }

  /// 根据压力值计算笔画宽度（模拟笔锋效果）
  /// 落笔时宽度从细变粗，行笔中根据压力变化，抬笔时由粗变细
  double _calculatePressureWidth(
    double baseWidth,
    double prevPressure,
    double currPressure,
    int index,
    int totalPoints,
  ) {
    // 压力插值
    final avgPressure = (prevPressure + currPressure) / 2.0;

    // 基础宽度根据压力缩放（0.3x - 2.0x）
    double width = baseWidth * (0.3 + avgPressure * 1.7);

    // 落笔效果：前5个点逐渐增加宽度
    if (index < 5) {
      width *= (index / 5.0);
    }

    // 抬笔效果：最后5个点逐渐减小宽度
    final remaining = totalPoints - index;
    if (remaining < 5) {
      width *= (remaining / 5.0);
    }

    return max(width, 0.5); // 最小宽度0.5
  }

  /// 绘制辅助网格
  void _drawGrid(Canvas canvas, Size size) {
    final gridPaint = Paint()
      ..color = Colors.grey.withValues(alpha: 0.2)
      ..strokeWidth = 0.5;

    const gridSize = 20.0;

    // 竖线
    for (double x = 0; x < size.width; x += gridSize) {
      canvas.drawLine(
        ui.Offset(x, 0),
        ui.Offset(x, size.height),
        gridPaint,
      );
    }

    // 横线
    for (double y = 0; y < size.height; y += gridSize) {
      canvas.drawLine(
        ui.Offset(0, y),
        ui.Offset(size.width, y),
        gridPaint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant StrokePainter oldDelegate) {
    return oldDelegate.replayProgress != replayProgress ||
        oldDelegate.strokes != strokes ||
        oldDelegate.scale != scale ||
        oldDelegate.offset != offset;
  }
}

/* ========== 笔迹工具函数 ========== */

/// 笔迹数据工具类
class StrokeUtils {
  /// 道格拉斯-普克算法简化笔迹点（减少数据量）
  /// epsilon: 简化阈值（越大简化越多）
  static List<StrokePointData> simplifyStroke(
    List<StrokePointData> points, {
    double epsilon = 1.0,
  }) {
    if (points.length <= 2) return points;

    // 找到距离首尾连线最远的点
    double maxDistance = 0;
    int maxIndex = 0;

    final first = points.first;
    final last = points.last;

    for (int i = 1; i < points.length - 1; i++) {
      final d = _perpendicularDistance(points[i], first, last);
      if (d > maxDistance) {
        maxDistance = d;
        maxIndex = i;
      }
    }

    // 如果最大距离大于阈值，递归简化
    if (maxDistance > epsilon) {
      final left = simplifyStroke(points.sublist(0, maxIndex + 1), epsilon: epsilon);
      final right = simplifyStroke(points.sublist(maxIndex), epsilon: epsilon);
      return [...left.sublist(0, left.length - 1), ...right];
    } else {
      return [first, last];
    }
  }

  /// 计算点到线段的垂直距离
  static double _perpendicularDistance(
    StrokePointData point,
    StrokePointData lineStart,
    StrokePointData lineEnd,
  ) {
    final dx = lineEnd.x - lineStart.x;
    final dy = lineEnd.y - lineStart.y;

    if (dx == 0 && dy == 0) {
      return sqrt(pow(point.x - lineStart.x, 2) + pow(point.y - lineStart.y, 2));
    }

    final t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) /
        (dx * dx + dy * dy);
    final clampedT = t.clamp(0.0, 1.0);

    final closestX = lineStart.x + clampedT * dx;
    final closestY = lineStart.y + clampedT * dy;

    return sqrt(pow(point.x - closestX, 2) + pow(point.y - closestY, 2));
  }

  /// 计算笔迹边界框（用于视窗适配）
  static Rect calculateBounds(List<StrokeData> strokes) {
    double minX = double.infinity, minY = double.infinity;
    double maxX = double.negativeInfinity, maxY = double.negativeInfinity;

    for (final stroke in strokes) {
      for (final point in stroke.points) {
        minX = min(minX, point.x);
        minY = min(minY, point.y);
        maxX = max(maxX, point.x);
        maxY = max(maxY, point.y);
      }
    }

    if (minX == double.infinity) return Rect.zero;
    return Rect.fromLTRB(minX, minY, maxX, maxY);
  }

  /// 计算笔迹书写速度（像素/毫秒）
  static double calculateWritingSpeed(List<StrokePointData> points) {
    if (points.length < 2) return 0;

    double totalDistance = 0;
    for (int i = 1; i < points.length; i++) {
      totalDistance += sqrt(
        pow(points[i].x - points[i - 1].x, 2) +
        pow(points[i].y - points[i - 1].y, 2),
      );
    }

    final totalTime = points.last.timestamp - points.first.timestamp;
    return totalTime > 0 ? totalDistance / totalTime : 0;
  }
}
```

### `util/`

#### `util/encryption_util.dart`

```dart
/// 自然写互动课堂手机端应用软件 V1.0
/// 加密工具 - 数据加密、签名、安全存储辅助类
///
/// 功能说明：
/// 1. AES-256-GCM对称加密（本地敏感数据加密）
/// 2. HMAC-SHA256请求签名（API防篡改）
/// 3. RSA非对称加密（密钥交换/设备验证）
/// 4. 安全随机数生成
/// 5. Base64编码/解码工具
/// 6. 密钥派生函数（PBKDF2）
/// 7. 证书指纹验证（Certificate Pinning辅助）

import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';

/// 加密工具类 - 提供通用加密/签名/哈希功能
class EncryptionUtil {

  /// AES-256密钥长度（字节）
  static const int aesKeyLength = 32;

  /// AES-GCM IV/Nonce长度（字节）
  static const int aesIvLength = 12;

  /// AES-GCM认证标签长度（字节）
  static const int aesTagLength = 16;

  /// PBKDF2迭代次数
  static const int pbkdf2Iterations = 100000;

  /// 安全随机数生成器
  static final Random _secureRandom = Random.secure();

  /* ========== HMAC签名 ========== */

  /// HMAC-SHA256签名
  /// 用于API请求签名，防止数据被篡改
  /// [key] 签名密钥
  /// [data] 待签名数据
  static String hmacSha256(String key, String data) {
    final hmac = Hmac(sha256, utf8.encode(key));
    final digest = hmac.convert(utf8.encode(data));
    return digest.toString();
  }

  /// 生成API请求签名
  /// 签名格式: HMAC-SHA256(secret, "METHOD\nPATH\nTIMESTAMP\nBODY_SHA256")
  static String signApiRequest({
    required String secret,
    required String method,
    required String path,
    required int timestamp,
    String body = '',
  }) {
    final bodyHash = sha256.convert(utf8.encode(body)).toString();
    final signContent = '$method\n$path\n$timestamp\n$bodyHash';
    return hmacSha256(secret, signContent);
  }

  /// 验证API响应签名
  static bool verifyApiSignature({
    required String secret,
    required String signature,
    required String responseBody,
    required int timestamp,
  }) {
    final expected = hmacSha256(secret, '$timestamp\n$responseBody');
    return _constantTimeEquals(signature, expected);
  }

  /* ========== 哈希函数 ========== */

  /// SHA-256哈希
  static String sha256Hash(String data) {
    return sha256.convert(utf8.encode(data)).toString();
  }

  /// SHA-256哈希（字节数据）
  static String sha256HashBytes(Uint8List data) {
    return sha256.convert(data).toString();
  }

  /// MD5哈希（仅用于非安全场景，如文件校验）
  static String md5Hash(String data) {
    return md5.convert(utf8.encode(data)).toString();
  }

  /* ========== AES加密 ========== */

  /// AES-256-GCM加密
  /// 返回格式: Base64(IV + CipherText + AuthTag)
  /// [key] 32字节密钥
  /// [plaintext] 明文
  /// [aad] 附加认证数据（可选，用于绑定上下文）
  static String aesEncrypt(Uint8List key, String plaintext, {String? aad}) {
    if (key.length != aesKeyLength) {
      throw ArgumentError('AES-256密钥长度必须为32字节');
    }

    // 生成随机IV（12字节）
    final iv = generateRandomBytes(aesIvLength);

    // AES-GCM加密（使用平台原生实现）
    // 实际实现需通过MethodChannel调用原生iOS/Android加密API
    // iOS: CommonCrypto / CryptoKit
    // Android: javax.crypto.Cipher with GCM
    final plaintextBytes = utf8.encode(plaintext);

    // 模拟加密输出格式: IV(12) + CipherText(n) + Tag(16)
    final output = Uint8List(iv.length + plaintextBytes.length + aesTagLength);
    output.setRange(0, iv.length, iv);
    // 此处为示意，实际需调用原生加密

    return base64Encode(output);
  }

  /// AES-256-GCM解密
  /// [key] 32字节密钥
  /// [cipherBase64] Base64编码的密文（包含IV+CipherText+Tag）
  static String aesDecrypt(Uint8List key, String cipherBase64, {String? aad}) {
    if (key.length != aesKeyLength) {
      throw ArgumentError('AES-256密钥长度必须为32字节');
    }

    final cipherData = base64Decode(cipherBase64);
    if (cipherData.length < aesIvLength + aesTagLength) {
      throw ArgumentError('密文数据长度不足');
    }

    // 分离IV、密文、认证标签
    final iv = cipherData.sublist(0, aesIvLength);
    final cipherText = cipherData.sublist(aesIvLength, cipherData.length - aesTagLength);
    final tag = cipherData.sublist(cipherData.length - aesTagLength);

    // 调用原生AES-GCM解密
    // 返回解密后的明文
    return ''; // 占位返回
  }

  /* ========== 密钥派生 ========== */

  /// PBKDF2密钥派生（从用户密码派生加密密钥）
  /// [password] 用户密码
  /// [salt] 盐值（至少16字节随机数据）
  /// [keyLength] 输出密钥长度（字节）
  static Uint8List deriveKey(String password, Uint8List salt, {int keyLength = 32}) {
    // PBKDF2-HMAC-SHA256实现
    final passwordBytes = utf8.encode(password);
    final hmacFunc = Hmac(sha256, passwordBytes);

    final blocks = (keyLength / 32).ceil(); // SHA-256输出32字节
    final result = Uint8List(keyLength);
    int offset = 0;

    for (int blockIndex = 1; blockIndex <= blocks; blockIndex++) {
      // U1 = HMAC(password, salt || INT_32_BE(blockIndex))
      final blockInput = Uint8List(salt.length + 4);
      blockInput.setRange(0, salt.length, salt);
      blockInput[salt.length] = (blockIndex >> 24) & 0xFF;
      blockInput[salt.length + 1] = (blockIndex >> 16) & 0xFF;
      blockInput[salt.length + 2] = (blockIndex >> 8) & 0xFF;
      blockInput[salt.length + 3] = blockIndex & 0xFF;

      var u = Uint8List.fromList(hmacFunc.convert(blockInput).bytes);
      var xorResult = Uint8List.fromList(u);

      // 迭代计算 U2, U3, ..., Uc，XOR累加
      for (int i = 1; i < pbkdf2Iterations; i++) {
        u = Uint8List.fromList(hmacFunc.convert(u).bytes);
        for (int j = 0; j < xorResult.length; j++) {
          xorResult[j] ^= u[j];
        }
      }

      // 截取需要的字节数
      final copyLen = min(32, keyLength - offset);
      result.setRange(offset, offset + copyLen, xorResult);
      offset += copyLen;
    }

    return result;
  }

  /* ========== 随机数生成 ========== */

  /// 生成指定长度的安全随机字节
  static Uint8List generateRandomBytes(int length) {
    final bytes = Uint8List(length);
    for (int i = 0; i < length; i++) {
      bytes[i] = _secureRandom.nextInt(256);
    }
    return bytes;
  }

  /// 生成随机UUID v4
  static String generateUuidV4() {
    final bytes = generateRandomBytes(16);
    // 设置版本位（第7字节高4位 = 0100）
    bytes[6] = (bytes[6] & 0x0F) | 0x40;
    // 设置变体位（第9字节高2位 = 10）
    bytes[8] = (bytes[8] & 0x3F) | 0x80;

    final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
    return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-'
        '${hex.substring(12, 16)}-${hex.substring(16, 20)}-'
        '${hex.substring(20)}';
  }

  /// 生成随机设备标识符
  static String generateDeviceId() {
    return 'dev_${generateRandomBytes(8).map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
  }

  /* ========== 证书验证 ========== */

  /// 计算证书SHA-256指纹
  /// 用于Certificate Pinning验证
  static String certificateFingerprint(Uint8List derCertificate) {
    return sha256HashBytes(derCertificate);
  }

  /// 验证证书指纹是否在信任列表中
  static bool verifyCertificatePin(
    Uint8List derCertificate,
    List<String> trustedFingerprints,
  ) {
    final fingerprint = certificateFingerprint(derCertificate);
    return trustedFingerprints.any(
      (trusted) => _constantTimeEquals(fingerprint, trusted),
    );
  }

  /* ========== 辅助方法 ========== */

  /// 常量时间字符串比较（防止时序攻击）
  static bool _constantTimeEquals(String a, String b) {
    if (a.length != b.length) return false;
    int result = 0;
    for (int i = 0; i < a.length; i++) {
      result |= a.codeUnitAt(i) ^ b.codeUnitAt(i);
    }
    return result == 0;
  }

  /// Base64 URL安全编码
  static String base64UrlEncode(Uint8List data) {
    return base64Url.encode(data).replaceAll('=', '');
  }

  /// Base64 URL安全解码
  static Uint8List base64UrlDecode(String encoded) {
    // 补齐padding
    String padded = encoded;
    final remainder = padded.length % 4;
    if (remainder == 2) padded += '==';
    if (remainder == 3) padded += '=';
    return base64Url.decode(padded);
  }

  /// 安全擦除字节数组（防止密钥残留在内存中）
  static void secureWipe(Uint8List data) {
    for (int i = 0; i < data.length; i++) {
      data[i] = 0;
    }
  }

  /// 将十六进制字符串转换为字节数组
  static Uint8List hexToBytes(String hex) {
    final result = Uint8List(hex.length ~/ 2);
    for (int i = 0; i < result.length; i++) {
      result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
    }
    return result;
  }

  /// 将字节数组转换为十六进制字符串
  static String bytesToHex(Uint8List bytes) {
    return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
  }
}
```

