Files
system-design/software-copyright/10-writech-app-pad/自然写互动课堂平板端应用软件-源程序.md
2026-03-22 15:24:40 +08:00

96 KiB
Raw Permalink Blame History

自然写互动课堂平板端应用软件 V1.0

软件著作权鉴别材料 — 源程序

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


源程序目录结构

10-writech-app-pad/
├── main.dart
├── bloc/
│   └── homework_bloc.dart
├── eye_care/
│   └── eye_care_manager.dart
├── renderer/
│   └── stroke_painter.dart
├── repository/
│   └── local_repository.dart
└── service/
    ├── api_service.dart
    └── ble_service.dart

源程序文件清单

(根目录)

main.dart

/// 自然写互动课堂平板端应用软件 V1.0
/// APP入口 - Flutter平板端应用初始化
///
/// 功能说明:
/// 1. 平板端应用初始化(Pad自适应布局配置)
/// 2. 学生端/教师端双模式切换
/// 3. 护眼模式初始化(色温调节、使用时长监控)
/// 4. 全局Bloc状态管理注入
/// 5. 离线模式支持(断网时可继续作答)

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

/// 应用入口
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 全局错误处理
  FlutterError.onError = (FlutterErrorDetails details) {
    FlutterError.presentError(details);
    debugPrint('[CrashReport] ${details.exception}');
  };

  // 设置系统UI(平板端支持横屏+竖屏)
  await SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
    DeviceOrientation.landscapeLeft,
    DeviceOrientation.landscapeRight,
  ]);

  // 初始化全局服务
  await _initServices();

  runZonedGuarded(() {
    runApp(const WritechPadApp());
  }, (error, stack) {
    debugPrint('[CrashReport] $error\n$stack');
  });
}

/// 初始化全局服务
Future<void> _initServices() async {
  debugPrint('[App] 服务初始化开始');
  // 初始化数据库、网络、BLE、护眼模块
  debugPrint('[App] 服务初始化完成');
}

/// 平板端应用根Widget
class WritechPadApp extends StatefulWidget {
  const WritechPadApp({super.key});

  @override
  State<WritechPadApp> createState() => _WritechPadAppState();
}

class _WritechPadAppState extends State<WritechPadApp>
    with WidgetsBindingObserver {
  /// 当前用户模式(学生/教师)
  String _userMode = 'student';

  /// 护眼模式是否开启
  bool _eyeCareEnabled = false;

  /// 色温滤镜值(0.0=正常,1.0=最暖)
  double _colorTemperature = 0.0;

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

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

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      debugPrint('[App] 应用恢复前台');
    } else if (state == AppLifecycleState.paused) {
      debugPrint('[App] 应用进入后台');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '自然写互动课堂',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF4CAF50),
          brightness: Brightness.light,
        ),
        fontFamily: 'NotoSansSC',
      ),
      // 护眼色温滤镜叠加
      builder: (context, child) {
        if (_eyeCareEnabled && _colorTemperature > 0) {
          return ColorFiltered(
            colorFilter: ColorFilter.matrix(_buildWarmMatrix(_colorTemperature)),
            child: child,
          );
        }
        return child ?? const SizedBox();
      },
      initialRoute: '/splash',
      routes: {
        '/splash': (_) => const _SplashPage(),
        '/login': (_) => const _LoginPage(),
        '/student_home': (_) => const _StudentHomePage(),
        '/teacher_home': (_) => const _TeacherHomePage(),
        '/homework': (_) => const _HomeworkPage(),
        '/practice': (_) => const _PracticePage(),
        '/error_book': (_) => const _ErrorBookPage(),
        '/settings': (_) => const _SettingsPage(),
      },
    );
  }

  /// 构建暖色温矩阵(护眼模式)
  List<double> _buildWarmMatrix(double intensity) {
    final r = 1.0;
    final g = 1.0 - intensity * 0.1;
    final b = 1.0 - intensity * 0.3;
    return [
      r, 0, 0, 0, 0,
      0, g, 0, 0, 0,
      0, 0, b, 0, 0,
      0, 0, 0, 1, 0,
    ];
  }
}

// 占位页面声明
class _SplashPage extends StatelessWidget {
  const _SplashPage();
  @override
  Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('自然写')));
}
class _LoginPage extends StatelessWidget {
  const _LoginPage();
  @override
  Widget build(BuildContext context) => const Scaffold();
}
class _StudentHomePage extends StatelessWidget {
  const _StudentHomePage();
  @override
  Widget build(BuildContext context) => const Scaffold();
}
class _TeacherHomePage extends StatelessWidget {
  const _TeacherHomePage();
  @override
  Widget build(BuildContext context) => const Scaffold();
}
class _HomeworkPage extends StatelessWidget {
  const _HomeworkPage();
  @override
  Widget build(BuildContext context) => const Scaffold();
}
class _PracticePage extends StatelessWidget {
  const _PracticePage();
  @override
  Widget build(BuildContext context) => const Scaffold();
}
class _ErrorBookPage extends StatelessWidget {
  const _ErrorBookPage();
  @override
  Widget build(BuildContext context) => const Scaffold();
}
class _SettingsPage extends StatelessWidget {
  const _SettingsPage();
  @override
  Widget build(BuildContext context) => const Scaffold();
}

bloc/

bloc/homework_bloc.dart

// 自然写互动课堂平板端应用软件 V1.0
// bloc/homework_bloc.dart - 作业状态管理(Bloc模式)

import 'dart:async';

/// 作业状态枚举
enum HomeworkStatus {
  /// 待完成
  pending,

  /// 进行中(已开始作答)
  inProgress,

  /// 已提交
  submitted,

  /// 已批改
  graded,

  /// 已过期
  expired,
}

/// 作业数据模型
class HomeworkItem {
  final String id;
  final String title;
  final String subject;
  final String teacherName;
  final HomeworkStatus status;
  final DateTime? assignedAt;
  final DateTime? deadline;
  final DateTime? submittedAt;
  final int? score;
  final int totalQuestions;
  final int answeredQuestions;
  final String? coverImageUrl;

  HomeworkItem({
    required this.id,
    required this.title,
    required this.subject,
    required this.teacherName,
    this.status = HomeworkStatus.pending,
    this.assignedAt,
    this.deadline,
    this.submittedAt,
    this.score,
    this.totalQuestions = 0,
    this.answeredQuestions = 0,
    this.coverImageUrl,
  });

  /// 是否已过截止时间
  bool get isOverdue =>
      deadline != null && DateTime.now().isAfter(deadline!);

  /// 作答进度百分比
  double get progress => totalQuestions > 0
      ? answeredQuestions / totalQuestions
      : 0.0;

  /// 从JSON解析
  factory HomeworkItem.fromJson(Map<String, dynamic> json) {
    return HomeworkItem(
      id: json['id'] ?? '',
      title: json['title'] ?? '',
      subject: json['subject'] ?? '',
      teacherName: json['teacher_name'] ?? '',
      status: _parseStatus(json['status']),
      assignedAt: json['assigned_at'] != null
          ? DateTime.tryParse(json['assigned_at'])
          : null,
      deadline: json['deadline'] != null
          ? DateTime.tryParse(json['deadline'])
          : null,
      submittedAt: json['submitted_at'] != null
          ? DateTime.tryParse(json['submitted_at'])
          : null,
      score: json['score'],
      totalQuestions: json['total_questions'] ?? 0,
      answeredQuestions: json['answered_questions'] ?? 0,
      coverImageUrl: json['cover_image_url'],
    );
  }

  /// 解析状态字符串
  static HomeworkStatus _parseStatus(String? status) {
    switch (status) {
      case 'pending':
        return HomeworkStatus.pending;
      case 'in_progress':
        return HomeworkStatus.inProgress;
      case 'submitted':
        return HomeworkStatus.submitted;
      case 'graded':
        return HomeworkStatus.graded;
      case 'expired':
        return HomeworkStatus.expired;
      default:
        return HomeworkStatus.pending;
    }
  }
}

/// 作业详情中的题目数据
class HomeworkQuestion {
  final String id;
  final int index;
  final String type;
  final String content;
  final String? imageUrl;
  final List<String>? options;
  final String? correctAnswer;
  final String? studentAnswer;
  final List<Map<String, dynamic>>? studentStrokes;
  final int? questionScore;
  final int? earnedScore;
  final String? teacherComment;

  HomeworkQuestion({
    required this.id,
    required this.index,
    required this.type,
    required this.content,
    this.imageUrl,
    this.options,
    this.correctAnswer,
    this.studentAnswer,
    this.studentStrokes,
    this.questionScore,
    this.earnedScore,
    this.teacherComment,
  });

  /// 从JSON解析
  factory HomeworkQuestion.fromJson(Map<String, dynamic> json) {
    return HomeworkQuestion(
      id: json['id'] ?? '',
      index: json['index'] ?? 0,
      type: json['type'] ?? 'write',
      content: json['content'] ?? '',
      imageUrl: json['image_url'],
      options: json['options'] != null
          ? List<String>.from(json['options'])
          : null,
      correctAnswer: json['correct_answer'],
      studentAnswer: json['student_answer'],
      studentStrokes: json['student_strokes'] != null
          ? List<Map<String, dynamic>>.from(json['student_strokes'])
          : null,
      questionScore: json['question_score'],
      earnedScore: json['earned_score'],
      teacherComment: json['teacher_comment'],
    );
  }
}

// ============================================================
//  Bloc Events(作业相关事件定义)
// ============================================================

/// 作业事件基类
abstract class HomeworkEvent {}

/// 加载作业列表事件
class LoadHomeworkListEvent extends HomeworkEvent {
  final HomeworkStatus? filterStatus;
  final int page;
  final bool refresh;

  LoadHomeworkListEvent({
    this.filterStatus,
    this.page = 1,
    this.refresh = false,
  });
}

/// 下载作业详情事件(用于离线作答)
class DownloadHomeworkEvent extends HomeworkEvent {
  final String homeworkId;
  DownloadHomeworkEvent(this.homeworkId);
}

/// 保存作答进度事件(本地暂存)
class SaveAnswerProgressEvent extends HomeworkEvent {
  final String homeworkId;
  final String questionId;
  final String? textAnswer;
  final List<Map<String, dynamic>>? strokeData;

  SaveAnswerProgressEvent({
    required this.homeworkId,
    required this.questionId,
    this.textAnswer,
    this.strokeData,
  });
}

/// 提交作业事件
class SubmitHomeworkEvent extends HomeworkEvent {
  final String homeworkId;
  SubmitHomeworkEvent(this.homeworkId);
}

/// 查看批改结果事件
class ViewGradeResultEvent extends HomeworkEvent {
  final String homeworkId;
  ViewGradeResultEvent(this.homeworkId);
}

// ============================================================
//  Bloc States(作业相关状态定义)
// ============================================================

/// 作业状态基类
abstract class HomeworkState {}

/// 初始状态
class HomeworkInitialState extends HomeworkState {}

/// 加载中状态
class HomeworkLoadingState extends HomeworkState {
  final String? message;
  HomeworkLoadingState({this.message});
}

/// 作业列表加载成功状态
class HomeworkListLoadedState extends HomeworkState {
  final List<HomeworkItem> homeworks;
  final bool hasMore;
  final int currentPage;
  final HomeworkStatus? currentFilter;

  /// 各状态的作业计数统计
  final Map<HomeworkStatus, int> statusCounts;

  HomeworkListLoadedState({
    required this.homeworks,
    this.hasMore = false,
    this.currentPage = 1,
    this.currentFilter,
    this.statusCounts = const {},
  });
}

/// 作业详情加载成功状态
class HomeworkDetailLoadedState extends HomeworkState {
  final HomeworkItem homework;
  final List<HomeworkQuestion> questions;
  final bool isOfflineAvailable;

  HomeworkDetailLoadedState({
    required this.homework,
    required this.questions,
    this.isOfflineAvailable = false,
  });
}

/// 作答进度保存成功状态
class AnswerSavedState extends HomeworkState {
  final String homeworkId;
  final String questionId;
  final int answeredCount;
  final int totalCount;

  AnswerSavedState({
    required this.homeworkId,
    required this.questionId,
    required this.answeredCount,
    required this.totalCount,
  });
}

/// 作业提交成功状态
class HomeworkSubmittedState extends HomeworkState {
  final String homeworkId;
  final DateTime submittedAt;

  HomeworkSubmittedState({
    required this.homeworkId,
    required this.submittedAt,
  });
}

/// 批改结果状态
class GradeResultState extends HomeworkState {
  final HomeworkItem homework;
  final List<HomeworkQuestion> questions;
  final int totalScore;
  final int earnedScore;
  final String? overallComment;

  GradeResultState({
    required this.homework,
    required this.questions,
    required this.totalScore,
    required this.earnedScore,
    this.overallComment,
  });
}

/// 错误状态
class HomeworkErrorState extends HomeworkState {
  final String message;
  final String? actionType;

  HomeworkErrorState({
    required this.message,
    this.actionType,
  });
}

// ============================================================
//  HomeworkBloc 实现
// ============================================================

/// 作业状态管理Bloc
/// 管理作业列表加载、下载、作答、提交、查看批改结果等完整流程
class HomeworkBloc {
  /// 当前状态
  HomeworkState _state = HomeworkInitialState();

  /// 状态流控制器
  final StreamController<HomeworkState> _stateController =
      StreamController<HomeworkState>.broadcast();

  /// 本地缓存的作业列表
  List<HomeworkItem> _cachedHomeworks = [];

  /// 本地缓存的作答进度 {homeworkId: {questionId: answerData}}
  final Map<String, Map<String, dynamic>> _answerCache = {};

  /// 获取当前状态
  HomeworkState get state => _state;

  /// 状态流
  Stream<HomeworkState> get stateStream => _stateController.stream;

  /// 发射新状态
  void _emit(HomeworkState newState) {
    _state = newState;
    _stateController.add(newState);
  }

  /// 处理事件分发
  void add(HomeworkEvent event) {
    if (event is LoadHomeworkListEvent) {
      _handleLoadList(event);
    } else if (event is DownloadHomeworkEvent) {
      _handleDownload(event);
    } else if (event is SaveAnswerProgressEvent) {
      _handleSaveAnswer(event);
    } else if (event is SubmitHomeworkEvent) {
      _handleSubmit(event);
    } else if (event is ViewGradeResultEvent) {
      _handleViewGrade(event);
    }
  }

  /// 处理加载作业列表
  Future<void> _handleLoadList(LoadHomeworkListEvent event) async {
    try {
      _emit(HomeworkLoadingState(message: '正在加载作业列表...'));

      // 调用API获取作业列表
      // final response = await PadApiService.instance.getHomeworkList(
      //   page: event.page,
      //   status: event.filterStatus?.name,
      // );

      // 模拟数据处理逻辑
      if (event.refresh) {
        _cachedHomeworks.clear();
      }

      // 统计各状态作业数量
      final statusCounts = <HomeworkStatus, int>{};
      for (final hw in _cachedHomeworks) {
        statusCounts[hw.status] = (statusCounts[hw.status] ?? 0) + 1;
      }

      // 根据筛选条件过滤
      List<HomeworkItem> filtered = _cachedHomeworks;
      if (event.filterStatus != null) {
        filtered = _cachedHomeworks
            .where((hw) => hw.status == event.filterStatus)
            .toList();
      }

      _emit(HomeworkListLoadedState(
        homeworks: filtered,
        hasMore: false,
        currentPage: event.page,
        currentFilter: event.filterStatus,
        statusCounts: statusCounts,
      ));
    } catch (e) {
      _emit(HomeworkErrorState(
        message: '加载作业列表失败: $e',
        actionType: 'load_list',
      ));
    }
  }

  /// 处理下载作业详情(支持离线作答)
  Future<void> _handleDownload(DownloadHomeworkEvent event) async {
    try {
      _emit(HomeworkLoadingState(message: '正在下载作业内容...'));

      // 调用API下载作业详情
      // final response = await PadApiService.instance.downloadHomework(
      //   event.homeworkId,
      // );

      // 将作业内容缓存到本地SQLite(支持离线作答)
      // await LocalRepository.instance.cacheHomework(...)

      // _emit(HomeworkDetailLoadedState(...));
    } catch (e) {
      _emit(HomeworkErrorState(
        message: '下载作业失败: $e',
        actionType: 'download',
      ));
    }
  }

  /// 处理保存作答进度(本地暂存,支持断点续答)
  Future<void> _handleSaveAnswer(SaveAnswerProgressEvent event) async {
    try {
      // 更新内存缓存
      _answerCache.putIfAbsent(event.homeworkId, () => {});
      _answerCache[event.homeworkId]![event.questionId] = {
        'text_answer': event.textAnswer,
        'stroke_data': event.strokeData,
        'saved_at': DateTime.now().toIso8601String(),
      };

      // 持久化到本地数据库
      // await LocalRepository.instance.saveAnswerProgress(...)

      // 计算已作答题目数
      final answeredCount = _answerCache[event.homeworkId]?.length ?? 0;

      _emit(AnswerSavedState(
        homeworkId: event.homeworkId,
        questionId: event.questionId,
        answeredCount: answeredCount,
        totalCount: 0, // 从缓存的作业详情中获取
      ));
    } catch (e) {
      _emit(HomeworkErrorState(
        message: '保存作答进度失败: $e',
        actionType: 'save_answer',
      ));
    }
  }

  /// 处理提交作业
  Future<void> _handleSubmit(SubmitHomeworkEvent event) async {
    try {
      _emit(HomeworkLoadingState(message: '正在提交作业...'));

      // 收集所有作答数据
      final answers = _answerCache[event.homeworkId] ?? {};

      // 构建提交数据(含笔迹页面数据)
      final strokePages = answers.entries.map((entry) {
        return {
          'question_id': entry.key,
          'answer': entry.value,
        };
      }).toList();

      // 调用API提交
      // final response = await PadApiService.instance.submitHomework(
      //   homeworkId: event.homeworkId,
      //   strokePages: strokePages,
      // );

      // 提交成功后清除本地缓存
      _answerCache.remove(event.homeworkId);

      _emit(HomeworkSubmittedState(
        homeworkId: event.homeworkId,
        submittedAt: DateTime.now(),
      ));
    } catch (e) {
      _emit(HomeworkErrorState(
        message: '提交作业失败: $e',
        actionType: 'submit',
      ));
    }
  }

  /// 处理查看批改结果
  Future<void> _handleViewGrade(ViewGradeResultEvent event) async {
    try {
      _emit(HomeworkLoadingState(message: '正在加载批改结果...'));

      // 调用API获取批改结果
      // final response = await PadApiService.instance.getHomeworkResult(
      //   event.homeworkId,
      // );

      // _emit(GradeResultState(...));
    } catch (e) {
      _emit(HomeworkErrorState(
        message: '加载批改结果失败: $e',
        actionType: 'view_grade',
      ));
    }
  }

  /// 释放资源
  void dispose() {
    _stateController.close();
    _cachedHomeworks.clear();
    _answerCache.clear();
  }
}

eye_care/

eye_care/eye_care_manager.dart

/// 自然写互动课堂平板端应用软件 V1.0
/// 护眼管理器 - 色温调节、使用时长监控、距离检测
///
/// 功能说明:
/// 1. 色温调节(暖色滤镜,减少蓝光对眼睛的刺激)
/// 2. 使用时长监控(按应用/科目统计,超时提醒休息)
/// 3. 距离检测(前置摄像头检测用眼距离,过近时提醒)
/// 4. 定时提醒(每30分钟提醒休息,远眺放松)
/// 5. 家长远程管控(接收家长设置的时段/时长限制)
/// 6. 护眼数据统计(每日使用时长报告)

import 'dart:async';

/// 护眼模式配置
class EyeCareConfig {
  /// 是否启用护眼模式
  bool enabled;

  /// 色温强度(0.0=关闭, 1.0=最暖)
  double colorTemperature;

  /// 连续使用提醒间隔(分钟)
  int reminderIntervalMinutes;

  /// 每日使用时长上限(分钟,0=不限制)
  int dailyLimitMinutes;

  /// 允许使用的时段(开始小时, 结束小时)
  int allowedStartHour;
  int allowedEndHour;

  /// 是否启用距离检测
  bool distanceDetectionEnabled;

  /// 安全用眼距离(厘米)
  int safeDistanceCm;

  /// 夜间模式自动开启时间(小时)
  int nightModeStartHour;
  int nightModeEndHour;

  EyeCareConfig({
    this.enabled = true,
    this.colorTemperature = 0.3,
    this.reminderIntervalMinutes = 30,
    this.dailyLimitMinutes = 120,
    this.allowedStartHour = 7,
    this.allowedEndHour = 21,
    this.distanceDetectionEnabled = false,
    this.safeDistanceCm = 30,
    this.nightModeStartHour = 20,
    this.nightModeEndHour = 7,
  });

  Map<String, dynamic> toJson() => {
    'enabled': enabled,
    'color_temperature': colorTemperature,
    'reminder_interval': reminderIntervalMinutes,
    'daily_limit': dailyLimitMinutes,
    'allowed_start': allowedStartHour,
    'allowed_end': allowedEndHour,
    'distance_enabled': distanceDetectionEnabled,
    'safe_distance': safeDistanceCm,
    'night_start': nightModeStartHour,
    'night_end': nightModeEndHour,
  };

  factory EyeCareConfig.fromJson(Map<String, dynamic> json) {
    return EyeCareConfig(
      enabled: json['enabled'] ?? true,
      colorTemperature: (json['color_temperature'] ?? 0.3).toDouble(),
      reminderIntervalMinutes: json['reminder_interval'] ?? 30,
      dailyLimitMinutes: json['daily_limit'] ?? 120,
      allowedStartHour: json['allowed_start'] ?? 7,
      allowedEndHour: json['allowed_end'] ?? 21,
      distanceDetectionEnabled: json['distance_enabled'] ?? false,
      safeDistanceCm: json['safe_distance'] ?? 30,
      nightModeStartHour: json['night_start'] ?? 20,
      nightModeEndHour: json['night_end'] ?? 7,
    );
  }
}

/// 使用时长记录
class UsageRecord {
  final String date;         // 日期 (yyyy-MM-dd)
  final String category;     // 分类 (homework/practice/reading)
  final int durationMinutes; // 使用时长(分钟)
  final int sessionCount;    // 使用次数

  UsageRecord({
    required this.date,
    required this.category,
    required this.durationMinutes,
    required this.sessionCount,
  });

  Map<String, dynamic> toJson() => {
    'date': date, 'category': category,
    'duration': durationMinutes, 'sessions': sessionCount,
  };
}

/// 护眼事件类型
enum EyeCareEvent {
  restReminder,          // 休息提醒
  dailyLimitReached,     // 每日时长上限
  outsideAllowedTime,    // 超出允许使用时段
  tooCloseWarning,       // 用眼距离过近
  nightModeOn,           // 夜间模式开启
  nightModeOff,          // 夜间模式关闭
}

/// 护眼事件回调
typedef EyeCareEventCallback = void Function(EyeCareEvent event, Map<String, dynamic> data);

/// 护眼管理器
class EyeCareManager {
  /// 护眼配置
  EyeCareConfig _config = EyeCareConfig();

  /// 事件回调列表
  final List<EyeCareEventCallback> _callbacks = [];

  /// 当前会话开始时间
  DateTime? _sessionStartTime;

  /// 今日累计使用时长(秒)
  int _todayUsageSeconds = 0;

  /// 当前连续使用时长(秒)
  int _continuousUsageSeconds = 0;

  /// 今日使用记录
  final Map<String, int> _categoryUsage = {};

  /// 计时器(每秒更新使用时长)
  Timer? _usageTimer;

  /// 距离检测计时器
  Timer? _distanceTimer;

  /// 夜间模式检查计时器
  Timer? _nightModeTimer;

  /// 当前是否在夜间模式
  bool _isNightMode = false;

  /// 当前色温值(供外部读取)
  double get currentColorTemperature {
    if (!_config.enabled) return 0.0;
    if (_isNightMode) return _config.colorTemperature * 1.5; // 夜间加强
    return _config.colorTemperature;
  }

  /// 今日总使用时长(分钟)
  int get todayUsageMinutes => _todayUsageSeconds ~/ 60;

  /// 剩余可用时长(分钟,-1表示不限制)
  int get remainingMinutes {
    if (_config.dailyLimitMinutes <= 0) return -1;
    return _config.dailyLimitMinutes - todayUsageMinutes;
  }

  /// 注册事件回调
  void addCallback(EyeCareEventCallback callback) {
    _callbacks.add(callback);
  }

  /// 移除事件回调
  void removeCallback(EyeCareEventCallback callback) {
    _callbacks.remove(callback);
  }

  /// 更新配置(家长远程设置后调用)
  void updateConfig(EyeCareConfig newConfig) {
    _config = newConfig;
    if (_config.enabled) {
      _startMonitoring();
    } else {
      _stopMonitoring();
    }
  }

  /// 开始使用(进入学习功能时调用)
  void startSession({String category = 'default'}) {
    _sessionStartTime = DateTime.now();
    _continuousUsageSeconds = 0;

    // 检查是否在允许时段内
    final now = DateTime.now();
    if (_config.enabled && !_isWithinAllowedTime(now)) {
      _notifyEvent(EyeCareEvent.outsideAllowedTime, {
        'allowed_start': _config.allowedStartHour,
        'allowed_end': _config.allowedEndHour,
      });
    }

    // 启动使用时长计时器
    _usageTimer?.cancel();
    _usageTimer = Timer.periodic(const Duration(seconds: 1), (_) {
      _todayUsageSeconds++;
      _continuousUsageSeconds++;

      // 检查连续使用时长提醒
      if (_config.reminderIntervalMinutes > 0 &&
          _continuousUsageSeconds > 0 &&
          _continuousUsageSeconds % (_config.reminderIntervalMinutes * 60) == 0) {
        _notifyEvent(EyeCareEvent.restReminder, {
          'continuous_minutes': _continuousUsageSeconds ~/ 60,
          'total_minutes': todayUsageMinutes,
        });
      }

      // 检查每日使用上限
      if (_config.dailyLimitMinutes > 0 &&
          todayUsageMinutes >= _config.dailyLimitMinutes) {
        _notifyEvent(EyeCareEvent.dailyLimitReached, {
          'limit_minutes': _config.dailyLimitMinutes,
          'used_minutes': todayUsageMinutes,
        });
      }
    });

    // 启动距离检测
    if (_config.distanceDetectionEnabled) {
      _startDistanceDetection();
    }

    // 启动夜间模式检查
    _startNightModeCheck();
  }

  /// 结束使用(退出学习功能时调用)
  void endSession({String category = 'default'}) {
    _usageTimer?.cancel();
    _usageTimer = null;

    if (_sessionStartTime != null) {
      final duration = DateTime.now().difference(_sessionStartTime!).inMinutes;
      _categoryUsage[category] = (_categoryUsage[category] ?? 0) + duration;
    }

    _sessionStartTime = null;
    _continuousUsageSeconds = 0;

    _distanceTimer?.cancel();
    _distanceTimer = null;
  }

  /// 用户休息后重置连续使用计时
  void acknowledgeRest() {
    _continuousUsageSeconds = 0;
  }

  /// 检查当前时间是否在允许使用时段内
  bool _isWithinAllowedTime(DateTime time) {
    final hour = time.hour;
    if (_config.allowedStartHour <= _config.allowedEndHour) {
      return hour >= _config.allowedStartHour && hour < _config.allowedEndHour;
    } else {
      // 跨午夜的情况
      return hour >= _config.allowedStartHour || hour < _config.allowedEndHour;
    }
  }

  /// 启动监控
  void _startMonitoring() {
    _startNightModeCheck();
  }

  /// 停止监控
  void _stopMonitoring() {
    _usageTimer?.cancel();
    _distanceTimer?.cancel();
    _nightModeTimer?.cancel();
  }

  /// 启动距离检测(通过前置摄像头估算用眼距离)
  void _startDistanceDetection() {
    _distanceTimer?.cancel();
    _distanceTimer = Timer.periodic(const Duration(seconds: 10), (_) {
      // 调用前置摄像头进行人脸检测
      // 通过人脸框大小估算距离(人脸越大=距离越近)
      _checkEyeDistance();
    });
  }

  /// 检查用眼距离(基于前置摄像头人脸检测)
  void _checkEyeDistance() {
    // 实际实现:
    // 1. 使用CameraController获取前置摄像头预览帧
    // 2. 使用MLKit/TFLite进行人脸检测
    // 3. 根据人脸框宽度估算距离: distance = (focal_length * real_face_width) / face_width_in_pixels
    // 4. 本地处理,不上传图像数据(隐私保护)

    // 模拟距离检测结果
    final estimatedDistanceCm = 35; // 实际从摄像头计算

    if (estimatedDistanceCm < _config.safeDistanceCm) {
      _notifyEvent(EyeCareEvent.tooCloseWarning, {
        'current_distance': estimatedDistanceCm,
        'safe_distance': _config.safeDistanceCm,
      });
    }
  }

  /// 启动夜间模式检查
  void _startNightModeCheck() {
    _nightModeTimer?.cancel();
    _nightModeTimer = Timer.periodic(const Duration(minutes: 1), (_) {
      final hour = DateTime.now().hour;
      final shouldBeNightMode = _isNightTimeHour(hour);

      if (shouldBeNightMode && !_isNightMode) {
        _isNightMode = true;
        _notifyEvent(EyeCareEvent.nightModeOn, {});
      } else if (!shouldBeNightMode && _isNightMode) {
        _isNightMode = false;
        _notifyEvent(EyeCareEvent.nightModeOff, {});
      }
    });

    // 立即检查一次
    final hour = DateTime.now().hour;
    _isNightMode = _isNightTimeHour(hour);
  }

  /// 判断是否为夜间时段
  bool _isNightTimeHour(int hour) {
    if (_config.nightModeStartHour <= _config.nightModeEndHour) {
      return hour >= _config.nightModeStartHour && hour < _config.nightModeEndHour;
    } else {
      return hour >= _config.nightModeStartHour || hour < _config.nightModeEndHour;
    }
  }

  /// 获取今日使用统计
  List<UsageRecord> getTodayUsageRecords() {
    final today = DateTime.now().toString().substring(0, 10);
    return _categoryUsage.entries.map((e) => UsageRecord(
      date: today,
      category: e.key,
      durationMinutes: e.value,
      sessionCount: 1,
    )).toList();
  }

  /// 通知事件到所有回调
  void _notifyEvent(EyeCareEvent event, Map<String, dynamic> data) {
    for (final callback in _callbacks) {
      try {
        callback(event, data);
      } catch (e) {
        // 忽略回调异常
      }
    }
  }

  /// 释放资源
  void dispose() {
    _usageTimer?.cancel();
    _distanceTimer?.cancel();
    _nightModeTimer?.cancel();
    _callbacks.clear();
  }
}

renderer/

renderer/stroke_painter.dart

/// 自然写互动课堂平板端应用软件 V1.0
/// Skia笔迹渲染器 - CustomPainter实现触屏直写与点阵笔笔迹渲染
///
/// 功能说明:
/// 1. CustomPainter高性能笔迹绘制(Skia引擎)
/// 2. 触屏直写支持(手指/触控笔Pointer事件处理)
/// 3. 点阵笔BLE数据渲染(从BLE服务接收坐标数据)
/// 4. 压力感应笔锋效果(触控笔ActiveStylus压力数据)
/// 5. 贝塞尔曲线平滑算法
/// 6. 字帖练习辅助线(田字格/米字格/四线三格)
/// 7. 撤销/重做操作栈
/// 8. 笔迹导出(SVG/PNG格式)

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

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

/// 笔迹点
class PadStrokePoint {
  final double x;
  final double y;
  final double pressure;
  final int timestamp;

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

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

/// 笔画
class PadStroke {
  final List<PadStrokePoint> points;
  final Color color;
  final double baseWidth;
  final String source; // 'touch'=触屏, 'ble'=点阵笔

  PadStroke({
    List<PadStrokePoint>? points,
    this.color = Colors.black,
    this.baseWidth = 2.5,
    this.source = 'touch',
  }) : points = points ?? [];

  void addPoint(PadStrokePoint point) => points.add(point);
}

/// 辅助线类型
enum GuideLineType {
  none,           // 无辅助线
  tianZiGe,       // 田字格
  miZiGe,         // 米字格
  siXianSanGe,    // 四线三格(英文/拼音)
}

/// 撤销/重做操作
sealed class CanvasAction {}
class AddStrokeAction extends CanvasAction {
  final PadStroke stroke;
  AddStrokeAction(this.stroke);
}
class ClearAction extends CanvasAction {
  final List<PadStroke> clearedStrokes;
  ClearAction(this.clearedStrokes);
}

/* ========== 笔迹画布Widget ========== */

/// 平板端笔迹渲染画布
/// 支持触屏直写和BLE点阵笔两种输入方式
class PadStrokeCanvas extends StatefulWidget {
  /// 初始笔画数据(如加载已有作业笔迹)
  final List<PadStroke>? initialStrokes;

  /// 辅助线类型
  final GuideLineType guideLineType;

  /// 是否只读模式(查看已提交的作业)
  final bool readOnly;

  /// 笔迹颜色
  final Color strokeColor;

  /// 笔画宽度
  final double strokeWidth;

  /// 笔迹变化回调
  final Function(List<PadStroke>)? onStrokesChanged;

  const PadStrokeCanvas({
    super.key,
    this.initialStrokes,
    this.guideLineType = GuideLineType.none,
    this.readOnly = false,
    this.strokeColor = Colors.black,
    this.strokeWidth = 2.5,
    this.onStrokesChanged,
  });

  @override
  State<PadStrokeCanvas> createState() => _PadStrokeCanvasState();
}

class _PadStrokeCanvasState extends State<PadStrokeCanvas> {
  /// 已完成的笔画列表
  final List<PadStroke> _strokes = [];

  /// 当前正在绘制的笔画
  PadStroke? _currentStroke;

  /// 撤销栈
  final List<CanvasAction> _undoStack = [];

  /// 重做栈
  final List<CanvasAction> _redoStack = [];

  /// 最大撤销步数
  static const int maxUndoSteps = 50;

  @override
  void initState() {
    super.initState();
    if (widget.initialStrokes != null) {
      _strokes.addAll(widget.initialStrokes!);
    }
  }

  /// 撤销最后一个操作
  void undo() {
    if (_undoStack.isEmpty) return;
    final action = _undoStack.removeLast();
    if (action is AddStrokeAction) {
      _strokes.remove(action.stroke);
      _redoStack.add(action);
    } else if (action is ClearAction) {
      _strokes.addAll(action.clearedStrokes);
      _redoStack.add(action);
    }
    setState(() {});
    widget.onStrokesChanged?.call(_strokes);
  }

  /// 重做上一个撤销的操作
  void redo() {
    if (_redoStack.isEmpty) return;
    final action = _redoStack.removeLast();
    if (action is AddStrokeAction) {
      _strokes.add(action.stroke);
      _undoStack.add(action);
    } else if (action is ClearAction) {
      _strokes.clear();
      _undoStack.add(action);
    }
    setState(() {});
    widget.onStrokesChanged?.call(_strokes);
  }

  /// 清除所有笔迹
  void clearAll() {
    if (_strokes.isEmpty) return;
    final cleared = List<PadStroke>.from(_strokes);
    _undoStack.add(ClearAction(cleared));
    _strokes.clear();
    _redoStack.clear();
    setState(() {});
    widget.onStrokesChanged?.call(_strokes);
  }

  /// 从BLE点阵笔添加笔画(外部调用)
  void addBleStroke(PadStroke stroke) {
    _strokes.add(stroke);
    _undoStack.add(AddStrokeAction(stroke));
    _redoStack.clear();
    setState(() {});
    widget.onStrokesChanged?.call(_strokes);
  }

  /// 获取所有笔画数据(用于提交)
  List<PadStroke> getStrokes() => List.unmodifiable(_strokes);

  @override
  Widget build(BuildContext context) {
    return Listener(
      // 使用Listener而非GestureDetector,以获取精确的Pointer事件
      onPointerDown: widget.readOnly ? null : _onPointerDown,
      onPointerMove: widget.readOnly ? null : _onPointerMove,
      onPointerUp: widget.readOnly ? null : _onPointerUp,
      child: ClipRect(
        child: CustomPaint(
          painter: _PadStrokePainter(
            strokes: _strokes,
            currentStroke: _currentStroke,
            guideLineType: widget.guideLineType,
          ),
          size: Size.infinite,
        ),
      ),
    );
  }

  /// 触屏落笔
  void _onPointerDown(PointerDownEvent event) {
    final pressure = event.pressure > 0 ? event.pressure : 0.5;
    _currentStroke = PadStroke(
      color: widget.strokeColor,
      baseWidth: widget.strokeWidth,
      source: event.kind == PointerDeviceKind.stylus ? 'stylus' : 'touch',
    );
    _currentStroke!.addPoint(PadStrokePoint(
      x: event.localPosition.dx,
      y: event.localPosition.dy,
      pressure: pressure,
      timestamp: DateTime.now().millisecondsSinceEpoch,
    ));
    setState(() {});
  }

  /// 触屏移动
  void _onPointerMove(PointerMoveEvent event) {
    if (_currentStroke == null) return;
    final pressure = event.pressure > 0 ? event.pressure : 0.5;
    _currentStroke!.addPoint(PadStrokePoint(
      x: event.localPosition.dx,
      y: event.localPosition.dy,
      pressure: pressure,
      timestamp: DateTime.now().millisecondsSinceEpoch,
    ));
    setState(() {});
  }

  /// 触屏抬笔
  void _onPointerUp(PointerUpEvent event) {
    if (_currentStroke == null) return;
    if (_currentStroke!.points.length >= 2) {
      _strokes.add(_currentStroke!);
      _undoStack.add(AddStrokeAction(_currentStroke!));
      _redoStack.clear();
      // 限制撤销栈大小
      if (_undoStack.length > maxUndoSteps) {
        _undoStack.removeAt(0);
      }
      widget.onStrokesChanged?.call(_strokes);
    }
    _currentStroke = null;
    setState(() {});
  }
}

/* ========== Painter实现 ========== */

/// 笔迹绘制Painter
class _PadStrokePainter extends CustomPainter {
  final List<PadStroke> strokes;
  final PadStroke? currentStroke;
  final GuideLineType guideLineType;

  _PadStrokePainter({
    required this.strokes,
    this.currentStroke,
    this.guideLineType = GuideLineType.none,
  });

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

    // 绘制辅助线
    if (guideLineType != GuideLineType.none) {
      _drawGuideLines(canvas, size);
    }

    // 绘制已完成的笔画
    for (final stroke in strokes) {
      _drawStroke(canvas, stroke);
    }

    // 绘制当前活跃笔画
    if (currentStroke != null) {
      _drawStroke(canvas, currentStroke!);
    }
  }

  /// 绘制辅助线
  void _drawGuideLines(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 0.5;

    switch (guideLineType) {
      case GuideLineType.tianZiGe:
        _drawTianZiGe(canvas, size, paint);
        break;
      case GuideLineType.miZiGe:
        _drawMiZiGe(canvas, size, paint);
        break;
      case GuideLineType.siXianSanGe:
        _drawSiXianSanGe(canvas, size, paint);
        break;
      default:
        break;
    }
  }

  /// 绘制田字格
  void _drawTianZiGe(Canvas canvas, Size size, Paint paint) {
    const cellSize = 80.0;
    paint.color = Colors.red.withValues(alpha: 0.3);

    // 外框(实线)
    for (double x = 0; x <= size.width; x += cellSize) {
      canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
    }
    for (double y = 0; y <= size.height; y += cellSize) {
      canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
    }

    // 中心十字线(虚线效果用半透明)
    paint.color = Colors.red.withValues(alpha: 0.15);
    final halfCell = cellSize / 2;
    for (double x = halfCell; x < size.width; x += cellSize) {
      canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
    }
    for (double y = halfCell; y < size.height; y += cellSize) {
      canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
    }
  }

  /// 绘制米字格
  void _drawMiZiGe(Canvas canvas, Size size, Paint paint) {
    const cellSize = 80.0;
    paint.color = Colors.red.withValues(alpha: 0.3);

    for (double x = 0; x <= size.width; x += cellSize) {
      canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
    }
    for (double y = 0; y <= size.height; y += cellSize) {
      canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
    }

    // 对角线 + 十字线
    paint.color = Colors.red.withValues(alpha: 0.15);
    for (double x = 0; x < size.width; x += cellSize) {
      for (double y = 0; y < size.height; y += cellSize) {
        // 对角线
        canvas.drawLine(Offset(x, y), Offset(x + cellSize, y + cellSize), paint);
        canvas.drawLine(Offset(x + cellSize, y), Offset(x, y + cellSize), paint);
        // 十字线
        canvas.drawLine(Offset(x + cellSize / 2, y), Offset(x + cellSize / 2, y + cellSize), paint);
        canvas.drawLine(Offset(x, y + cellSize / 2), Offset(x + cellSize, y + cellSize / 2), paint);
      }
    }
  }

  /// 绘制四线三格(拼音/英文)
  void _drawSiXianSanGe(Canvas canvas, Size size, Paint paint) {
    const lineSpacing = 15.0;
    const groupHeight = lineSpacing * 3;
    const groupGap = 20.0;

    paint.color = Colors.green.withValues(alpha: 0.3);

    double y = 20;
    while (y < size.height - groupHeight) {
      // 四条横线
      for (int i = 0; i < 4; i++) {
        final lineY = y + i * lineSpacing;
        // 第二条线(中线)用虚线表示
        if (i == 1 || i == 2) {
          paint.color = Colors.green.withValues(alpha: 0.15);
        } else {
          paint.color = Colors.green.withValues(alpha: 0.3);
        }
        canvas.drawLine(Offset(0, lineY), Offset(size.width, lineY), paint);
      }
      y += groupHeight + groupGap;
    }
  }

  /// 绘制单个笔画(贝塞尔平滑 + 压力笔锋)
  void _drawStroke(Canvas canvas, PadStroke stroke) {
    if (stroke.points.length < 2) return;

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

    for (int i = 1; i < stroke.points.length; i++) {
      final prev = stroke.points[i - 1];
      final curr = stroke.points[i];

      // 压力笔锋宽度计算
      final avgPressure = (prev.pressure + curr.pressure) / 2.0;
      var width = stroke.baseWidth * (0.3 + avgPressure * 1.7);

      // 落笔过渡
      if (i < 5) width *= (i / 5.0);
      // 抬笔过渡
      final remaining = stroke.points.length - i;
      if (remaining < 5) width *= (remaining / 5.0);
      width = max(width, 0.5);

      paint.strokeWidth = width;

      if (i >= 2) {
        // 贝塞尔曲线平滑
        final pp = stroke.points[i - 2];
        final cp1x = prev.x + (curr.x - pp.x) * 0.2;
        final cp1y = prev.y + (curr.y - pp.y) * 0.2;
        final cp2x = curr.x - (curr.x - prev.x) * 0.2;
        final cp2y = curr.y - (curr.y - prev.y) * 0.2;

        final path = Path()
          ..moveTo(prev.x, prev.y)
          ..cubicTo(cp1x, cp1y, cp2x, cp2y, curr.x, curr.y);
        canvas.drawPath(path, paint);
      } else {
        canvas.drawLine(Offset(prev.x, prev.y), Offset(curr.x, curr.y), paint);
      }
    }
  }

  @override
  bool shouldRepaint(covariant _PadStrokePainter oldDelegate) {
    return oldDelegate.strokes.length != strokes.length ||
        oldDelegate.currentStroke != currentStroke;
  }
}

repository/

repository/local_repository.dart

// 自然写互动课堂平板端应用软件 V1.0
// repository/local_repository.dart - SQLite + Hive本地数据存储

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

/// 数据库表名常量
class PadDbTables {
  static const String homework = 'pad_homework';
  static const String homeworkQuestion = 'pad_homework_question';
  static const String answerProgress = 'pad_answer_progress';
  static const String errorBook = 'pad_error_book';
  static const String studyPlan = 'pad_study_plan';
  static const String studyTask = 'pad_study_task';
  static const String practiceRecord = 'pad_practice_record';
  static const String strokeCache = 'pad_stroke_cache';
  static const String offlineAction = 'pad_offline_action';
  static const String usageRecord = 'pad_usage_record';
}

/// 数据库版本
const int padDbVersion = 4;

/// 作业缓存模型
class CachedHomework {
  final String id;
  final String title;
  final String subject;
  final String teacherName;
  final String status;
  final String? deadline;
  final String? content;
  final int totalQuestions;
  final int answeredQuestions;
  final DateTime cachedAt;

  CachedHomework({
    required this.id,
    required this.title,
    required this.subject,
    required this.teacherName,
    required this.status,
    this.deadline,
    this.content,
    this.totalQuestions = 0,
    this.answeredQuestions = 0,
    required this.cachedAt,
  });

  Map<String, dynamic> toMap() => {
    'id': id,
    'title': title,
    'subject': subject,
    'teacher_name': teacherName,
    'status': status,
    'deadline': deadline,
    'content': content,
    'total_questions': totalQuestions,
    'answered_questions': answeredQuestions,
    'cached_at': cachedAt.toIso8601String(),
  };

  factory CachedHomework.fromMap(Map<String, dynamic> map) {
    return CachedHomework(
      id: map['id'],
      title: map['title'] ?? '',
      subject: map['subject'] ?? '',
      teacherName: map['teacher_name'] ?? '',
      status: map['status'] ?? 'pending',
      deadline: map['deadline'],
      content: map['content'],
      totalQuestions: map['total_questions'] ?? 0,
      answeredQuestions: map['answered_questions'] ?? 0,
      cachedAt: DateTime.parse(map['cached_at']),
    );
  }
}

/// 错题记录模型
class ErrorBookEntry {
  final String id;
  final String homeworkId;
  final String questionId;
  final String subject;
  final String? knowledgePoint;
  final String questionContent;
  final String? questionImageUrl;
  final String? studentAnswer;
  final String? correctAnswer;
  final String? errorReason;
  final int reviewCount;
  final DateTime createdAt;
  final DateTime? lastReviewAt;

  ErrorBookEntry({
    required this.id,
    required this.homeworkId,
    required this.questionId,
    required this.subject,
    this.knowledgePoint,
    required this.questionContent,
    this.questionImageUrl,
    this.studentAnswer,
    this.correctAnswer,
    this.errorReason,
    this.reviewCount = 0,
    required this.createdAt,
    this.lastReviewAt,
  });

  Map<String, dynamic> toMap() => {
    'id': id,
    'homework_id': homeworkId,
    'question_id': questionId,
    'subject': subject,
    'knowledge_point': knowledgePoint,
    'question_content': questionContent,
    'question_image_url': questionImageUrl,
    'student_answer': studentAnswer,
    'correct_answer': correctAnswer,
    'error_reason': errorReason,
    'review_count': reviewCount,
    'created_at': createdAt.toIso8601String(),
    'last_review_at': lastReviewAt?.toIso8601String(),
  };

  factory ErrorBookEntry.fromMap(Map<String, dynamic> map) {
    return ErrorBookEntry(
      id: map['id'],
      homeworkId: map['homework_id'] ?? '',
      questionId: map['question_id'] ?? '',
      subject: map['subject'] ?? '',
      knowledgePoint: map['knowledge_point'],
      questionContent: map['question_content'] ?? '',
      questionImageUrl: map['question_image_url'],
      studentAnswer: map['student_answer'],
      correctAnswer: map['correct_answer'],
      errorReason: map['error_reason'],
      reviewCount: map['review_count'] ?? 0,
      createdAt: DateTime.parse(map['created_at']),
      lastReviewAt: map['last_review_at'] != null
          ? DateTime.parse(map['last_review_at'])
          : null,
    );
  }
}

/// 学习计划模型
class StudyPlanEntry {
  final String id;
  final String title;
  final String type;
  final String? subject;
  final DateTime startDate;
  final DateTime endDate;
  final double progress;
  final int totalTasks;
  final int completedTasks;
  final bool isActive;

  StudyPlanEntry({
    required this.id,
    required this.title,
    required this.type,
    this.subject,
    required this.startDate,
    required this.endDate,
    this.progress = 0.0,
    this.totalTasks = 0,
    this.completedTasks = 0,
    this.isActive = true,
  });

  Map<String, dynamic> toMap() => {
    'id': id,
    'title': title,
    'type': type,
    'subject': subject,
    'start_date': startDate.toIso8601String(),
    'end_date': endDate.toIso8601String(),
    'progress': progress,
    'total_tasks': totalTasks,
    'completed_tasks': completedTasks,
    'is_active': isActive ? 1 : 0,
  };
}

/// 练字练习记录模型
class PracticeRecord {
  final String id;
  final String templateId;
  final String character;
  final int strokeScore;
  final int structureScore;
  final int overallScore;
  final String? strokeDataJson;
  final DateTime practiceAt;

  PracticeRecord({
    required this.id,
    required this.templateId,
    required this.character,
    this.strokeScore = 0,
    this.structureScore = 0,
    this.overallScore = 0,
    this.strokeDataJson,
    required this.practiceAt,
  });

  Map<String, dynamic> toMap() => {
    'id': id,
    'template_id': templateId,
    'character': character,
    'stroke_score': strokeScore,
    'structure_score': structureScore,
    'overall_score': overallScore,
    'stroke_data_json': strokeDataJson,
    'practice_at': practiceAt.toIso8601String(),
  };
}

/// 平板端本地数据存储仓库
/// 使用SQLite持久化存储 + Hive内存级KV缓存
/// 支持:作业缓存、错题本、学习计划、练字记录、离线操作队列、使用时长记录
class PadLocalRepository {
  /// 数据库实例(实际使用sqflite库)
  // late final Database _db;

  /// 单例
  static PadLocalRepository? _instance;
  static PadLocalRepository get instance {
    _instance ??= PadLocalRepository._internal();
    return _instance!;
  }

  PadLocalRepository._internal();

  /// 初始化数据库,创建表结构并执行版本迁移
  Future<void> initialize() async {
    // 实际调用: openDatabase(path, version: padDbVersion, ...)
    // 以下为建表SQL

    // V1: 基础表
    await _createTablesV1();

    // V2: 增加学习计划表
    await _createTablesV2();

    // V3: 增加使用时长记录表
    await _createTablesV3();

    // V4: 增加练字记录表和索引优化
    await _createTablesV4();
  }

  /// V1建表:作业缓存、作答进度、错题本、离线操作队列
  Future<void> _createTablesV1() async {
    // 作业缓存表
    const createHomework = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.homework} (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        subject TEXT NOT NULL,
        teacher_name TEXT,
        status TEXT DEFAULT 'pending',
        deadline TEXT,
        content TEXT,
        total_questions INTEGER DEFAULT 0,
        answered_questions INTEGER DEFAULT 0,
        cached_at TEXT NOT NULL
      )
    ''';

    // 作业题目缓存表
    const createQuestion = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.homeworkQuestion} (
        id TEXT PRIMARY KEY,
        homework_id TEXT NOT NULL,
        question_index INTEGER,
        type TEXT DEFAULT 'write',
        content TEXT,
        image_url TEXT,
        options TEXT,
        correct_answer TEXT,
        FOREIGN KEY (homework_id) REFERENCES ${PadDbTables.homework}(id)
      )
    ''';

    // 作答进度暂存表
    const createProgress = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.answerProgress} (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        homework_id TEXT NOT NULL,
        question_id TEXT NOT NULL,
        text_answer TEXT,
        stroke_data TEXT,
        saved_at TEXT NOT NULL,
        UNIQUE(homework_id, question_id)
      )
    ''';

    // 错题本表
    const createErrorBook = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.errorBook} (
        id TEXT PRIMARY KEY,
        homework_id TEXT,
        question_id TEXT,
        subject TEXT NOT NULL,
        knowledge_point TEXT,
        question_content TEXT NOT NULL,
        question_image_url TEXT,
        student_answer TEXT,
        correct_answer TEXT,
        error_reason TEXT,
        review_count INTEGER DEFAULT 0,
        created_at TEXT NOT NULL,
        last_review_at TEXT
      )
    ''';

    // 离线操作队列表
    const createOffline = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.offlineAction} (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        action_type TEXT NOT NULL,
        payload TEXT NOT NULL,
        retry_count INTEGER DEFAULT 0,
        created_at TEXT NOT NULL,
        status TEXT DEFAULT 'pending'
      )
    ''';

    // 笔迹暂存表
    const createStroke = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.strokeCache} (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        homework_id TEXT,
        question_id TEXT,
        page_id TEXT,
        stroke_json TEXT NOT NULL,
        pen_mac TEXT,
        created_at TEXT NOT NULL
      )
    ''';

    // 实际执行建表SQL
    // await _db.execute(createHomework);
    // await _db.execute(createQuestion);
    // await _db.execute(createProgress);
    // await _db.execute(createErrorBook);
    // await _db.execute(createOffline);
    // await _db.execute(createStroke);
  }

  /// V2建表:学习计划与任务
  Future<void> _createTablesV2() async {
    const createPlan = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.studyPlan} (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        type TEXT NOT NULL,
        subject TEXT,
        start_date TEXT NOT NULL,
        end_date TEXT NOT NULL,
        progress REAL DEFAULT 0.0,
        total_tasks INTEGER DEFAULT 0,
        completed_tasks INTEGER DEFAULT 0,
        is_active INTEGER DEFAULT 1
      )
    ''';

    const createTask = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.studyTask} (
        id TEXT PRIMARY KEY,
        plan_id TEXT NOT NULL,
        title TEXT NOT NULL,
        description TEXT,
        target_date TEXT,
        is_completed INTEGER DEFAULT 0,
        completed_at TEXT,
        FOREIGN KEY (plan_id) REFERENCES ${PadDbTables.studyPlan}(id)
      )
    ''';

    // await _db.execute(createPlan);
    // await _db.execute(createTask);
  }

  /// V3建表:使用时长记录
  Future<void> _createTablesV3() async {
    const createUsage = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.usageRecord} (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        date TEXT NOT NULL,
        app_name TEXT DEFAULT 'writech',
        subject TEXT,
        duration_seconds INTEGER DEFAULT 0,
        start_time TEXT NOT NULL,
        end_time TEXT
      )
    ''';

    // await _db.execute(createUsage);
  }

  /// V4建表:练字记录 + 索引
  Future<void> _createTablesV4() async {
    const createPractice = '''
      CREATE TABLE IF NOT EXISTS ${PadDbTables.practiceRecord} (
        id TEXT PRIMARY KEY,
        template_id TEXT NOT NULL,
        character TEXT NOT NULL,
        stroke_score INTEGER DEFAULT 0,
        structure_score INTEGER DEFAULT 0,
        overall_score INTEGER DEFAULT 0,
        stroke_data_json TEXT,
        practice_at TEXT NOT NULL
      )
    ''';

    // 索引优化
    const indexHomeworkStatus = '''
      CREATE INDEX IF NOT EXISTS idx_homework_status 
      ON ${PadDbTables.homework}(status)
    ''';
    const indexErrorSubject = '''
      CREATE INDEX IF NOT EXISTS idx_error_subject 
      ON ${PadDbTables.errorBook}(subject)
    ''';
    const indexPracticeChar = '''
      CREATE INDEX IF NOT EXISTS idx_practice_char 
      ON ${PadDbTables.practiceRecord}(character)
    ''';

    // await _db.execute(createPractice);
    // await _db.execute(indexHomeworkStatus);
    // await _db.execute(indexErrorSubject);
    // await _db.execute(indexPracticeChar);
  }

  // ============================================================
  //  作业缓存 CRUD
  // ============================================================

  /// 缓存作业到本地(用于离线作答)
  Future<void> cacheHomework(CachedHomework homework) async {
    // await _db.insert(
    //   PadDbTables.homework,
    //   homework.toMap(),
    //   conflictAlgorithm: ConflictAlgorithm.replace,
    // );
  }

  /// 获取本地缓存的作业列表
  Future<List<CachedHomework>> getCachedHomeworks({
    String? status,
    int limit = 50,
  }) async {
    // String where = '';
    // List<dynamic> whereArgs = [];
    // if (status != null) {
    //   where = 'status = ?';
    //   whereArgs = [status];
    // }
    // final maps = await _db.query(
    //   PadDbTables.homework,
    //   where: where.isNotEmpty ? where : null,
    //   whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
    //   orderBy: 'cached_at DESC',
    //   limit: limit,
    // );
    // return maps.map((m) => CachedHomework.fromMap(m)).toList();
    return [];
  }

  /// 保存作答进度到本地
  Future<void> saveAnswerProgress({
    required String homeworkId,
    required String questionId,
    String? textAnswer,
    String? strokeDataJson,
  }) async {
    // await _db.insert(
    //   PadDbTables.answerProgress,
    //   {
    //     'homework_id': homeworkId,
    //     'question_id': questionId,
    //     'text_answer': textAnswer,
    //     'stroke_data': strokeDataJson,
    //     'saved_at': DateTime.now().toIso8601String(),
    //   },
    //   conflictAlgorithm: ConflictAlgorithm.replace,
    // );
  }

  /// 获取某作业的所有作答进度
  Future<Map<String, Map<String, dynamic>>> getAnswerProgress(
    String homeworkId,
  ) async {
    // final maps = await _db.query(
    //   PadDbTables.answerProgress,
    //   where: 'homework_id = ?',
    //   whereArgs: [homeworkId],
    // );
    // final result = <String, Map<String, dynamic>>{};
    // for (final m in maps) {
    //   result[m['question_id'] as String] = m;
    // }
    // return result;
    return {};
  }

  // ============================================================
  //  错题本 CRUD
  // ============================================================

  /// 添加错题记录
  Future<void> addErrorEntry(ErrorBookEntry entry) async {
    // await _db.insert(
    //   PadDbTables.errorBook,
    //   entry.toMap(),
    //   conflictAlgorithm: ConflictAlgorithm.replace,
    // );
  }

  /// 获取错题列表(支持按科目/知识点筛选)
  Future<List<ErrorBookEntry>> getErrorEntries({
    String? subject,
    String? knowledgePoint,
    int limit = 50,
    int offset = 0,
  }) async {
    // final conditions = <String>[];
    // final args = <dynamic>[];
    // if (subject != null) {
    //   conditions.add('subject = ?');
    //   args.add(subject);
    // }
    // if (knowledgePoint != null) {
    //   conditions.add('knowledge_point = ?');
    //   args.add(knowledgePoint);
    // }
    // final maps = await _db.query(
    //   PadDbTables.errorBook,
    //   where: conditions.isNotEmpty ? conditions.join(' AND ') : null,
    //   whereArgs: args.isNotEmpty ? args : null,
    //   orderBy: 'created_at DESC',
    //   limit: limit,
    //   offset: offset,
    // );
    // return maps.map((m) => ErrorBookEntry.fromMap(m)).toList();
    return [];
  }

  /// 更新错题复习次数
  Future<void> updateErrorReviewCount(String entryId) async {
    // await _db.rawUpdate('''
    //   UPDATE ${PadDbTables.errorBook}
    //   SET review_count = review_count + 1,
    //       last_review_at = ?
    //   WHERE id = ?
    // ''', [DateTime.now().toIso8601String(), entryId]);
  }

  /// 获取错题统计(按科目分组计数)
  Future<Map<String, int>> getErrorStatsBySubject() async {
    // final maps = await _db.rawQuery('''
    //   SELECT subject, COUNT(*) as count
    //   FROM ${PadDbTables.errorBook}
    //   GROUP BY subject
    // ''');
    // return {for (var m in maps) m['subject'] as String: m['count'] as int};
    return {};
  }

  // ============================================================
  //  学习计划 CRUD
  // ============================================================

  /// 保存学习计划
  Future<void> saveStudyPlan(StudyPlanEntry plan) async {
    // await _db.insert(
    //   PadDbTables.studyPlan,
    //   plan.toMap(),
    //   conflictAlgorithm: ConflictAlgorithm.replace,
    // );
  }

  /// 获取活跃的学习计划列表
  Future<List<Map<String, dynamic>>> getActiveStudyPlans() async {
    // return await _db.query(
    //   PadDbTables.studyPlan,
    //   where: 'is_active = 1',
    //   orderBy: 'start_date ASC',
    // );
    return [];
  }

  /// 更新学习计划进度
  Future<void> updatePlanProgress(
    String planId,
    double progress,
    int completedTasks,
  ) async {
    // await _db.update(
    //   PadDbTables.studyPlan,
    //   {'progress': progress, 'completed_tasks': completedTasks},
    //   where: 'id = ?',
    //   whereArgs: [planId],
    // );
  }

  // ============================================================
  //  练字记录
  // ============================================================

  /// 保存练字记录
  Future<void> savePracticeRecord(PracticeRecord record) async {
    // await _db.insert(
    //   PadDbTables.practiceRecord,
    //   record.toMap(),
    //   conflictAlgorithm: ConflictAlgorithm.replace,
    // );
  }

  /// 获取某字的练习历史(查看进步轨迹)
  Future<List<Map<String, dynamic>>> getPracticeHistory(
    String character, {
    int limit = 20,
  }) async {
    // return await _db.query(
    //   PadDbTables.practiceRecord,
    //   where: 'character = ?',
    //   whereArgs: [character],
    //   orderBy: 'practice_at DESC',
    //   limit: limit,
    // );
    return [];
  }

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

  /// 添加离线操作到队列
  Future<void> enqueueOfflineAction(
    String actionType,
    Map<String, dynamic> payload,
  ) async {
    // await _db.insert(PadDbTables.offlineAction, {
    //   'action_type': actionType,
    //   'payload': jsonEncode(payload),
    //   'created_at': DateTime.now().toIso8601String(),
    //   'status': 'pending',
    // });
  }

  /// 获取待执行的离线操作
  Future<List<Map<String, dynamic>>> getPendingOfflineActions() async {
    // return await _db.query(
    //   PadDbTables.offlineAction,
    //   where: 'status = ? AND retry_count < 5',
    //   whereArgs: ['pending'],
    //   orderBy: 'created_at ASC',
    // );
    return [];
  }

  /// 标记离线操作完成
  Future<void> markOfflineActionDone(int actionId) async {
    // await _db.update(
    //   PadDbTables.offlineAction,
    //   {'status': 'done'},
    //   where: 'id = ?',
    //   whereArgs: [actionId],
    // );
  }

  // ============================================================
  //  使用时长记录
  // ============================================================

  /// 记录使用时长
  Future<void> recordUsage({
    required String date,
    required int durationSeconds,
    required String startTime,
    String? endTime,
    String? subject,
  }) async {
    // await _db.insert(PadDbTables.usageRecord, {
    //   'date': date,
    //   'duration_seconds': durationSeconds,
    //   'start_time': startTime,
    //   'end_time': endTime,
    //   'subject': subject,
    // });
  }

  /// 获取某日使用总时长(秒)
  Future<int> getDailyUsage(String date) async {
    // final result = await _db.rawQuery('''
    //   SELECT COALESCE(SUM(duration_seconds), 0) as total
    //   FROM ${PadDbTables.usageRecord}
    //   WHERE date = ?
    // ''', [date]);
    // return result.first['total'] as int? ?? 0;
    return 0;
  }

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

  /// 清理过期缓存数据(30天前的作业缓存、90天前的笔迹暂存)
  Future<void> cleanExpiredData() async {
    final thirtyDaysAgo = DateTime.now()
        .subtract(const Duration(days: 30))
        .toIso8601String();
    final ninetyDaysAgo = DateTime.now()
        .subtract(const Duration(days: 90))
        .toIso8601String();

    // await _db.delete(
    //   PadDbTables.homework,
    //   where: 'cached_at < ? AND status IN (?, ?)',
    //   whereArgs: [thirtyDaysAgo, 'graded', 'expired'],
    // );
    // await _db.delete(
    //   PadDbTables.strokeCache,
    //   where: 'created_at < ?',
    //   whereArgs: [ninetyDaysAgo],
    // );
    // await _db.delete(
    //   PadDbTables.offlineAction,
    //   where: 'status = ? AND created_at < ?',
    //   whereArgs: ['done', thirtyDaysAgo],
    // );
  }

  /// 获取本地数据库存储大小(字节)
  Future<int> getDatabaseSize() async {
    // final dbPath = await getDatabasesPath();
    // final file = File('$dbPath/writech_pad.db');
    // return file.existsSync() ? file.lengthSync() : 0;
    return 0;
  }

  /// 关闭数据库
  Future<void> close() async {
    // await _db.close();
  }
}

service/

service/api_service.dart

// 自然写互动课堂平板端应用软件 V1.0
// service/api_service.dart - 云平台API服务(Dio HTTP客户端)

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

/// 云平台API基础路径配置
class ApiConfig {
  /// 生产环境API地址
  static const String productionBaseUrl = 'https://api.writech.com/v1';

  /// 测试环境API地址
  static const String stagingBaseUrl = 'https://staging-api.writech.com/v1';

  /// 连接超时时间(毫秒)
  static const int connectTimeout = 15000;

  /// 接收超时时间(毫秒)
  static const int receiveTimeout = 30000;

  /// Token刷新路径
  static const String refreshTokenPath = '/auth/refresh';

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

  /// HMAC签名密钥标识
  static const String hmacKeyId = 'writech-pad-v1';
}

/// API响应数据统一封装
class ApiResponse<T> {
  final int code;
  final String message;
  final T? data;
  final String? requestId;

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

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

  /// 从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'])
          : json['data'] as T?,
      requestId: json['request_id'],
    );
  }
}

/// 离线请求队列项
class OfflineRequest {
  final String id;
  final String method;
  final String path;
  final Map<String, dynamic>? data;
  final DateTime createdAt;
  int retryCount;

  OfflineRequest({
    required this.id,
    required this.method,
    required this.path,
    this.data,
    required this.createdAt,
    this.retryCount = 0,
  });

  /// 序列化为JSON用于本地持久化
  Map<String, dynamic> toJson() => {
    'id': id,
    'method': method,
    'path': path,
    'data': data,
    'created_at': createdAt.toIso8601String(),
    'retry_count': retryCount,
  };

  /// 从JSON反序列化
  factory OfflineRequest.fromJson(Map<String, dynamic> json) {
    return OfflineRequest(
      id: json['id'],
      method: json['method'],
      path: json['path'],
      data: json['data'],
      createdAt: DateTime.parse(json['created_at']),
      retryCount: json['retry_count'] ?? 0,
    );
  }
}

/// 平板端云平台API服务
/// 负责与云平台的所有HTTP通信,包括:
/// - JWT双令牌认证与自动刷新
/// - HMAC-SHA256请求签名
/// - 离线请求队列暂存
/// - 学生简化登录(班级+姓名/学号)
class PadApiService {
  late final Dio _dio;
  final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();

  /// 当前访问令牌
  String? _accessToken;

  /// 刷新令牌
  String? _refreshToken;

  /// Token刷新锁,防止并发刷新
  Completer<bool>? _refreshCompleter;

  /// 离线请求队列
  final List<OfflineRequest> _offlineQueue = [];

  /// 网络状态标志
  bool _isOnline = true;

  /// API事件流控制器(登录状态变化等)
  final StreamController<String> _eventController =
      StreamController<String>.broadcast();

  /// API事件流
  Stream<String> get eventStream => _eventController.stream;

  /// 单例实例
  static PadApiService? _instance;

  /// 获取单例
  static PadApiService get instance {
    _instance ??= PadApiService._internal();
    return _instance!;
  }

  /// 私有构造函数,初始化Dio客户端
  PadApiService._internal() {
    _dio = Dio(BaseOptions(
      baseUrl: ApiConfig.productionBaseUrl,
      connectTimeout: Duration(milliseconds: ApiConfig.connectTimeout),
      receiveTimeout: Duration(milliseconds: ApiConfig.receiveTimeout),
      headers: {
        'Content-Type': 'application/json',
        'X-Client-Platform': 'pad',
        'X-Client-Version': '1.0.0',
      },
    ));

    // 添加请求拦截器:自动附加Token和HMAC签名
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: _onRequest,
      onResponse: _onResponse,
      onError: _onError,
    ));

    // 从安全存储恢复令牌
    _restoreTokens();
  }

  /// 从安全存储恢复上次保存的令牌
  Future<void> _restoreTokens() async {
    _accessToken = await _secureStorage.read(key: 'access_token');
    _refreshToken = await _secureStorage.read(key: 'refresh_token');
  }

  /// 请求拦截器:附加Authorization头和HMAC签名
  void _onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    // 附加JWT访问令牌
    if (_accessToken != null) {
      options.headers['Authorization'] = 'Bearer $_accessToken';
    }

    // 生成HMAC-SHA256请求签名
    final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
    options.headers['X-Timestamp'] = timestamp;
    final signature = _generateSignature(
      options.method,
      options.path,
      timestamp,
      options.data,
    );
    options.headers['X-Signature'] = signature;

    handler.next(options);
  }

  /// 响应拦截器:统一处理响应
  void _onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) {
    handler.next(response);
  }

  /// 错误拦截器:处理401自动刷新Token、离线暂存等
  Future<void> _onError(
    DioException error,
    ErrorInterceptorHandler handler,
  ) async {
    // 网络不可用时,将请求加入离线队列
    if (error.type == DioExceptionType.connectionError ||
        error.type == DioExceptionType.connectionTimeout) {
      _isOnline = false;
      _enqueueOfflineRequest(error.requestOptions);
      handler.reject(error);
      return;
    }

    // 401未授权:尝试刷新Token后重试
    if (error.response?.statusCode == 401) {
      final refreshSuccess = await _refreshAccessToken();
      if (refreshSuccess) {
        // Token刷新成功,使用新Token重试原请求
        final retryOptions = error.requestOptions;
        retryOptions.headers['Authorization'] = 'Bearer $_accessToken';
        try {
          final response = await _dio.fetch(retryOptions);
          handler.resolve(response);
          return;
        } catch (retryError) {
          // 重试也失败了
        }
      } else {
        // Token刷新失败,通知登出
        _eventController.add('token_expired');
      }
    }

    handler.reject(error);
  }

  /// 生成HMAC-SHA256请求签名
  /// 签名内容: METHOD\nPATH\nTIMESTAMP\nBODY_SHA256
  String _generateSignature(
    String method,
    String path,
    String timestamp,
    dynamic body,
  ) {
    // 计算请求体SHA256哈希
    String bodyHash = '';
    if (body != null) {
      final bodyStr = body is String ? body : jsonEncode(body);
      bodyHash = sha256.convert(utf8.encode(bodyStr)).toString();
    }

    // 拼接签名原文
    final signContent = '$method\n$path\n$timestamp\n$bodyHash';
    final hmacKey = utf8.encode(ApiConfig.hmacKeyId);
    final hmac = Hmac(sha256, hmacKey);
    final digest = hmac.convert(utf8.encode(signContent));

    return digest.toString();
  }

  /// 刷新访问令牌
  /// 使用Completer防止并发多次刷新
  Future<bool> _refreshAccessToken() async {
    // 如果已经在刷新中,等待结果
    if (_refreshCompleter != null) {
      return _refreshCompleter!.future;
    }

    _refreshCompleter = Completer<bool>();

    try {
      if (_refreshToken == null) {
        _refreshCompleter!.complete(false);
        return false;
      }

      // 发送刷新请求(不经过拦截器避免死循环)
      final response = await Dio().post(
        '${ApiConfig.productionBaseUrl}${ApiConfig.refreshTokenPath}',
        data: {'refresh_token': _refreshToken},
      );

      if (response.statusCode == 200 && response.data['code'] == 0) {
        _accessToken = response.data['data']['access_token'];
        _refreshToken = response.data['data']['refresh_token'];

        // 持久化新令牌到安全存储
        await _secureStorage.write(
          key: 'access_token',
          value: _accessToken,
        );
        await _secureStorage.write(
          key: 'refresh_token',
          value: _refreshToken,
        );

        _refreshCompleter!.complete(true);
        return true;
      }

      _refreshCompleter!.complete(false);
      return false;
    } catch (e) {
      _refreshCompleter!.complete(false);
      return false;
    } finally {
      _refreshCompleter = null;
    }
  }

  /// 将失败的请求加入离线队列
  void _enqueueOfflineRequest(RequestOptions options) {
    final offlineReq = OfflineRequest(
      id: DateTime.now().microsecondsSinceEpoch.toString(),
      method: options.method,
      path: options.path,
      data: options.data is Map ? options.data : null,
      createdAt: DateTime.now(),
    );
    _offlineQueue.add(offlineReq);
  }

  /// 网络恢复后,批量重发离线队列中的请求
  Future<void> flushOfflineQueue() async {
    if (_offlineQueue.isEmpty) return;

    _isOnline = true;
    final pendingRequests = List<OfflineRequest>.from(_offlineQueue);
    _offlineQueue.clear();

    for (final req in pendingRequests) {
      try {
        if (req.retryCount >= ApiConfig.maxRetryCount) continue;
        req.retryCount++;

        switch (req.method.toUpperCase()) {
          case 'POST':
            await _dio.post(req.path, data: req.data);
            break;
          case 'PUT':
            await _dio.put(req.path, data: req.data);
            break;
          case 'DELETE':
            await _dio.delete(req.path);
            break;
          default:
            await _dio.get(req.path);
        }
      } catch (e) {
        // 重发失败的请求重新加入队列
        if (req.retryCount < ApiConfig.maxRetryCount) {
          _offlineQueue.add(req);
        }
      }
    }
  }

  // ============================================================
  //  学生登录接口(简化登录,班级+姓名/学号)
  // ============================================================

  /// 学生简化登录(无需手机号,仅班级+姓名或学号)
  Future<ApiResponse<Map<String, dynamic>>> studentLogin({
    required String schoolCode,
    required String classId,
    required String studentName,
    String? studentNo,
  }) async {
    try {
      final response = await _dio.post('/auth/student/login', data: {
        'school_code': schoolCode,
        'class_id': classId,
        'student_name': studentName,
        'student_no': studentNo,
        'device_type': 'pad',
      });

      final result = ApiResponse<Map<String, dynamic>>.fromJson(
        response.data,
        (data) => data as Map<String, dynamic>,
      );

      // 保存登录令牌
      if (result.isSuccess && result.data != null) {
        _accessToken = result.data!['access_token'];
        _refreshToken = result.data!['refresh_token'];
        await _secureStorage.write(
          key: 'access_token',
          value: _accessToken,
        );
        await _secureStorage.write(
          key: 'refresh_token',
          value: _refreshToken,
        );
      }

      return result;
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '网络请求失败');
    }
  }

  /// 教师登录(手机号+验证码)
  Future<ApiResponse<Map<String, dynamic>>> teacherLogin({
    required String phone,
    required String verifyCode,
  }) async {
    try {
      final response = await _dio.post('/auth/teacher/login', data: {
        'phone': phone,
        'verify_code': verifyCode,
        'device_type': 'pad',
      });

      final result = ApiResponse<Map<String, dynamic>>.fromJson(
        response.data,
        (data) => data as Map<String, dynamic>,
      );

      if (result.isSuccess && result.data != null) {
        _accessToken = result.data!['access_token'];
        _refreshToken = result.data!['refresh_token'];
        await _secureStorage.write(
          key: 'access_token',
          value: _accessToken,
        );
        await _secureStorage.write(
          key: 'refresh_token',
          value: _refreshToken,
        );
      }

      return result;
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '网络请求失败');
    }
  }

  // ============================================================
  //  作业相关接口
  // ============================================================

  /// 获取学生待完成作业列表
  Future<ApiResponse<List<dynamic>>> getHomeworkList({
    int page = 1,
    int pageSize = 20,
    String? status,
  }) async {
    try {
      final response = await _dio.get('/homework/list', queryParameters: {
        'page': page,
        'page_size': pageSize,
        if (status != null) 'status': status,
      });
      return ApiResponse<List<dynamic>>.fromJson(
        response.data,
        (data) => data as List<dynamic>,
      );
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '获取作业列表失败');
    }
  }

  /// 下载作业详情(含题目内容,支持离线作答)
  Future<ApiResponse<Map<String, dynamic>>> downloadHomework(
    String homeworkId,
  ) async {
    try {
      final response = await _dio.get('/homework/detail/$homeworkId');
      return ApiResponse<Map<String, dynamic>>.fromJson(
        response.data,
        (data) => data as Map<String, dynamic>,
      );
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '下载作业失败');
    }
  }

  /// 提交作业(含笔迹数据)
  Future<ApiResponse<Map<String, dynamic>>> submitHomework({
    required String homeworkId,
    required List<Map<String, dynamic>> strokePages,
    int? timeCostSeconds,
  }) async {
    try {
      final response = await _dio.post('/homework/submit', data: {
        'homework_id': homeworkId,
        'stroke_pages': strokePages,
        'time_cost': timeCostSeconds,
        'submit_time': DateTime.now().toIso8601String(),
      });
      return ApiResponse<Map<String, dynamic>>.fromJson(
        response.data,
        (data) => data as Map<String, dynamic>,
      );
    } on DioException catch (e) {
      // 离线时暂存提交请求
      if (!_isOnline) {
        _enqueueOfflineRequest(e.requestOptions);
      }
      return ApiResponse(code: -1, message: e.message ?? '提交作业失败');
    }
  }

  /// 获取作业批改结果
  Future<ApiResponse<Map<String, dynamic>>> getHomeworkResult(
    String homeworkId,
  ) async {
    try {
      final response = await _dio.get('/homework/result/$homeworkId');
      return ApiResponse<Map<String, dynamic>>.fromJson(
        response.data,
        (data) => data as Map<String, dynamic>,
      );
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '获取批改结果失败');
    }
  }

  // ============================================================
  //  字帖练习接口
  // ============================================================

  /// 获取字帖模板列表(按年级/学科分类)
  Future<ApiResponse<List<dynamic>>> getCopybookTemplates({
    required String grade,
    String? subject,
    int page = 1,
  }) async {
    try {
      final response = await _dio.get('/copybook/templates', queryParameters: {
        'grade': grade,
        'subject': subject,
        'page': page,
      });
      return ApiResponse<List<dynamic>>.fromJson(
        response.data,
        (data) => data as List<dynamic>,
      );
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '获取字帖失败');
    }
  }

  /// 上传练字笔迹评分
  Future<ApiResponse<Map<String, dynamic>>> submitPracticeStroke({
    required String templateId,
    required String character,
    required List<Map<String, dynamic>> strokes,
  }) async {
    try {
      final response = await _dio.post('/copybook/evaluate', data: {
        'template_id': templateId,
        'character': character,
        'strokes': strokes,
      });
      return ApiResponse<Map<String, dynamic>>.fromJson(
        response.data,
        (data) => data as Map<String, dynamic>,
      );
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '提交练字评分失败');
    }
  }

  // ============================================================
  //  错题本接口
  // ============================================================

  /// 获取错题列表(按知识点/科目分类)
  Future<ApiResponse<List<dynamic>>> getErrorBookList({
    String? subject,
    String? knowledgePoint,
    int page = 1,
    int pageSize = 20,
  }) async {
    try {
      final response = await _dio.get('/error-book/list', queryParameters: {
        if (subject != null) 'subject': subject,
        if (knowledgePoint != null) 'knowledge_point': knowledgePoint,
        'page': page,
        'page_size': pageSize,
      });
      return ApiResponse<List<dynamic>>.fromJson(
        response.data,
        (data) => data as List<dynamic>,
      );
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '获取错题本失败');
    }
  }

  // ============================================================
  //  学情与学习计划接口
  // ============================================================

  /// 获取学生个人学情概览
  Future<ApiResponse<Map<String, dynamic>>> getStudentProfile() async {
    try {
      final response = await _dio.get('/profile/student/overview');
      return ApiResponse<Map<String, dynamic>>.fromJson(
        response.data,
        (data) => data as Map<String, dynamic>,
      );
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '获取学情失败');
    }
  }

  /// 获取学习计划列表
  Future<ApiResponse<List<dynamic>>> getStudyPlans() async {
    try {
      final response = await _dio.get('/study-plan/list');
      return ApiResponse<List<dynamic>>.fromJson(
        response.data,
        (data) => data as List<dynamic>,
      );
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '获取学习计划失败');
    }
  }

  /// 更新学习计划进度
  Future<ApiResponse<void>> updateStudyPlanProgress({
    required String planId,
    required String taskId,
    required double progress,
  }) async {
    try {
      final response = await _dio.put('/study-plan/progress', data: {
        'plan_id': planId,
        'task_id': taskId,
        'progress': progress,
        'update_time': DateTime.now().toIso8601String(),
      });
      return ApiResponse<void>.fromJson(response.data, null);
    } on DioException catch (e) {
      return ApiResponse(code: -1, message: e.message ?? '更新进度失败');
    }
  }

  /// 登出,清除本地令牌
  Future<void> logout() async {
    try {
      await _dio.post('/auth/logout');
    } catch (_) {
      // 忽略登出请求失败
    }
    _accessToken = null;
    _refreshToken = null;
    await _secureStorage.delete(key: 'access_token');
    await _secureStorage.delete(key: 'refresh_token');
    _eventController.add('logged_out');
  }

  /// 释放资源
  void dispose() {
    _eventController.close();
    _dio.close();
  }
}

service/ble_service.dart

// 自然写互动课堂平板端应用软件 V1.0
// service/ble_service.dart - BLE蓝牙点阵笔连接服务

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

/// BLE服务UUID常量定义
/// 基于自然写点阵笔自定义GATT Service规范
class PadBleConstants {
  /// 点阵笔主服务UUID
  static const String penServiceUuid = '0000ffe0-0000-1000-8000-00805f9b34fb';

  /// 笔迹坐标数据特征值UUIDNotify)
  static const String strokeCharUuid = '0000ffe1-0000-1000-8000-00805f9b34fb';

  /// 笔控制指令特征值UUIDWrite
  static const String controlCharUuid = '0000ffe2-0000-1000-8000-00805f9b34fb';

  /// 电量信息特征值UUIDRead/Notify
  static const String batteryCharUuid = '0000ffe3-0000-1000-8000-00805f9b34fb';

  /// 设备信息服务UUID
  static const String deviceInfoUuid = '0000180a-0000-1000-8000-00805f9b34fb';

  /// 扫描超时时间(秒)
  static const int scanTimeoutSeconds = 15;

  /// 自动重连延迟(秒)
  static const int reconnectDelaySeconds = 3;

  /// 最大重连次数
  static const int maxReconnectAttempts = 10;

  /// MTU协商大小
  static const int requestedMtu = 247;

  /// 笔迹数据缓冲批量回调阈值
  static const int strokeBatchSize = 8;

  /// 电量读取间隔(秒)
  static const int batteryReadInterval = 60;
}

/// 单个笔迹坐标点数据
class PadPenPoint {
  /// X坐标(0.01mm精度,16位无符号)
  final double x;

  /// Y坐标(0.01mm精度,16位无符号)
  final double y;

  /// 压力值(0-255,8位无符号)
  final int pressure;

  /// 时间戳(相对值,16位无符号,单位ms)
  final int timestamp;

  /// 是否为落笔点
  final bool isPenDown;

  PadPenPoint({
    required this.x,
    required this.y,
    required this.pressure,
    required this.timestamp,
    this.isPenDown = false,
  });

  @override
  String toString() =>
      'PadPenPoint(x: ${x.toStringAsFixed(2)}, y: ${y.toStringAsFixed(2)}, '
      'p: $pressure, t: $timestamp)';
}

/// 点阵笔设备信息
class PadPenDevice {
  /// 设备蓝牙MAC地址
  final String macAddress;

  /// 设备名称
  final String name;

  /// 信号强度(RSSI
  int rssi;

  /// 当前连接状态
  PenConnectionState connectionState;

  /// 电量百分比(0-100
  int batteryLevel;

  /// 固件版本号
  String? firmwareVersion;

  /// 当前所在点阵码页面ID
  String? currentPageId;

  PadPenDevice({
    required this.macAddress,
    required this.name,
    this.rssi = -100,
    this.connectionState = PenConnectionState.disconnected,
    this.batteryLevel = -1,
    this.firmwareVersion,
    this.currentPageId,
  });
}

/// 笔连接状态枚举
enum PenConnectionState {
  /// 未连接
  disconnected,

  /// 正在扫描
  scanning,

  /// 正在连接
  connecting,

  /// 已连接
  connected,

  /// 正在断开
  disconnecting,

  /// 自动重连中
  reconnecting,
}

/// 笔迹数据事件(批量坐标点回调)
class PenStrokeEvent {
  /// 来源笔的MAC地址
  final String penMac;

  /// 坐标点列表
  final List<PadPenPoint> points;

  /// 所在页面ID(点阵码识别)
  final String? pageId;

  PenStrokeEvent({
    required this.penMac,
    required this.points,
    this.pageId,
  });
}

/// BLE蓝牙点阵笔连接服务
/// 负责扫描、连接、数据接收、电量监控、自动重连等功能
/// 平板端支持同时连接1支笔(学生个人使用场景)
class PadBleService {
  /// 已发现的设备列表
  final List<PadPenDevice> _discoveredDevices = [];

  /// 当前已连接的笔
  PadPenDevice? _connectedPen;

  /// 笔迹数据缓冲区(累积到阈值后批量回调)
  final List<PadPenPoint> _strokeBuffer = [];

  /// 扫描结果流
  final StreamController<List<PadPenDevice>> _scanController =
      StreamController<List<PadPenDevice>>.broadcast();

  /// 笔迹数据事件流
  final StreamController<PenStrokeEvent> _strokeController =
      StreamController<PenStrokeEvent>.broadcast();

  /// 连接状态变化流
  final StreamController<PenConnectionState> _connectionController =
      StreamController<PenConnectionState>.broadcast();

  /// 电量变化流
  final StreamController<int> _batteryController =
      StreamController<int>.broadcast();

  /// 自动重连计数器
  int _reconnectAttempts = 0;

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

  /// 电量读取定时器
  Timer? _batteryTimer;

  /// 是否正在扫描
  bool _isScanning = false;

  /// 公开的流
  Stream<List<PadPenDevice>> get scanStream => _scanController.stream;
  Stream<PenStrokeEvent> get strokeStream => _strokeController.stream;
  Stream<PenConnectionState> get connectionStream =>
      _connectionController.stream;
  Stream<int> get batteryStream => _batteryController.stream;

  /// 获取当前连接的笔
  PadPenDevice? get connectedPen => _connectedPen;

  /// 开始扫描附近的点阵笔设备
  /// 按服务UUID过滤,仅发现自然写点阵笔
  Future<void> startScan() async {
    if (_isScanning) return;
    _isScanning = true;
    _discoveredDevices.clear();

    // 通知扫描状态
    _connectionController.add(PenConnectionState.scanning);

    // 模拟BLE扫描(实际使用flutter_blue_plus库)
    // 过滤条件:仅发现包含pen_service_uuid的设备
    // scanFilters: [ScanFilter(serviceUuid: PadBleConstants.penServiceUuid)]

    // 设置扫描超时
    Timer(Duration(seconds: PadBleConstants.scanTimeoutSeconds), () {
      stopScan();
    });
  }

  /// 停止扫描
  Future<void> stopScan() async {
    _isScanning = false;
    // 实际调用: FlutterBluePlus.stopScan()
  }

  /// 处理扫描结果回调
  void _onScanResult(String mac, String name, int rssi) {
    // 检查是否已发现过
    final existingIndex = _discoveredDevices.indexWhere(
      (d) => d.macAddress == mac,
    );

    if (existingIndex >= 0) {
      // 更新已有设备的RSSI
      _discoveredDevices[existingIndex].rssi = rssi;
    } else {
      // 添加新发现的设备
      _discoveredDevices.add(PadPenDevice(
        macAddress: mac,
        name: name,
        rssi: rssi,
      ));
    }

    // 按信号强度降序排列
    _discoveredDevices.sort((a, b) => b.rssi.compareTo(a.rssi));
    _scanController.add(List.from(_discoveredDevices));
  }

  /// 连接指定的点阵笔
  /// [device] 要连接的笔设备信息
  Future<bool> connectPen(PadPenDevice device) async {
    // 先断开已有连接
    if (_connectedPen != null) {
      await disconnectPen();
    }

    device.connectionState = PenConnectionState.connecting;
    _connectionController.add(PenConnectionState.connecting);

    try {
      // 停止扫描
      await stopScan();

      // 执行BLE连接
      // 实际调用: device.connect(timeout: Duration(seconds: 10))
      // 协商MTU
      // await device.requestMtu(PadBleConstants.requestedMtu);

      // 发现服务和特征值
      // final services = await device.discoverServices();
      // 查找笔迹数据特征值并订阅Notify

      // 设置连接成功状态
      device.connectionState = PenConnectionState.connected;
      _connectedPen = device;
      _reconnectAttempts = 0;
      _connectionController.add(PenConnectionState.connected);

      // 启动电量定时读取
      _startBatteryMonitor();

      // 订阅笔迹数据特征值
      _subscribeStrokeData();

      return true;
    } catch (e) {
      device.connectionState = PenConnectionState.disconnected;
      _connectionController.add(PenConnectionState.disconnected);
      return false;
    }
  }

  /// 订阅笔迹坐标数据Notify特征值
  void _subscribeStrokeData() {
    // 实际调用:
    // characteristic.setNotifyValue(true);
    // characteristic.onValueReceived.listen(_onStrokeDataReceived);
  }

  /// 处理接收到的笔迹原始数据(7字节紧凑编码)
  /// 数据格式:[X_H, X_L, Y_H, Y_L, Pressure, TS_H, TS_L]
  /// X: 16位无符号(0.01mm精度)
  /// Y: 16位无符号(0.01mm精度)
  /// Pressure: 8位无符号(0-255
  /// Timestamp: 16位无符号(相对毫秒)
  void _onStrokeDataReceived(Uint8List rawData) {
    if (rawData.length < 7) return;

    // 可能包含多个坐标点(每7字节一个)
    int offset = 0;
    while (offset + 7 <= rawData.length) {
      // 解码X坐标(大端序16位)
      final int rawX = (rawData[offset] << 8) | rawData[offset + 1];
      final double x = rawX * 0.01; // 转换为毫米

      // 解码Y坐标
      final int rawY = (rawData[offset + 2] << 8) | rawData[offset + 3];
      final double y = rawY * 0.01;

      // 解码压力值
      final int pressure = rawData[offset + 4];

      // 解码时间戳
      final int timestamp =
          (rawData[offset + 5] << 8) | rawData[offset + 6];

      // 判断落笔/抬笔(压力值>0为落笔)
      final bool isPenDown = pressure > 0;

      final point = PadPenPoint(
        x: x,
        y: y,
        pressure: pressure,
        timestamp: timestamp,
        isPenDown: isPenDown,
      );

      _strokeBuffer.add(point);
      offset += 7;
    }

    // CRC-16 CCITT校验(如果数据包尾部有2字节CRC)
    if (rawData.length > offset + 1) {
      final int receivedCrc = (rawData[offset] << 8) | rawData[offset + 1];
      final int calculatedCrc = _calculateCrc16(
        rawData.sublist(0, offset),
      );
      if (receivedCrc != calculatedCrc) {
        // CRC校验失败,丢弃本批数据
        _strokeBuffer.clear();
        return;
      }
    }

    // 达到批量阈值后回调
    if (_strokeBuffer.length >= PadBleConstants.strokeBatchSize) {
      _flushStrokeBuffer();
    }
  }

  /// 将缓冲区中的笔迹数据批量回调
  void _flushStrokeBuffer() {
    if (_strokeBuffer.isEmpty || _connectedPen == null) return;

    final event = PenStrokeEvent(
      penMac: _connectedPen!.macAddress,
      points: List.from(_strokeBuffer),
      pageId: _connectedPen!.currentPageId,
    );

    _strokeController.add(event);
    _strokeBuffer.clear();
  }

  /// CRC-16 CCITT校验算法
  /// 多项式: 0x1021, 初始值: 0xFFFF
  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;
  }

  /// 启动电量定时读取
  void _startBatteryMonitor() {
    _batteryTimer?.cancel();
    _batteryTimer = Timer.periodic(
      Duration(seconds: PadBleConstants.batteryReadInterval),
      (_) => _readBatteryLevel(),
    );
    // 立即读取一次
    _readBatteryLevel();
  }

  /// 读取笔电量
  Future<void> _readBatteryLevel() async {
    if (_connectedPen == null) return;

    try {
      // 实际调用: 读取battery特征值
      // final value = await batteryCharacteristic.read();
      // _connectedPen!.batteryLevel = value[0];
      // _batteryController.add(_connectedPen!.batteryLevel);
    } catch (e) {
      // 读取失败,忽略
    }
  }

  /// 向笔发送控制指令
  /// [command] 指令类型(如:LED闪烁、蜂鸣提示、固件信息查询)
  Future<void> sendCommand(int command, [Uint8List? payload]) async {
    if (_connectedPen == null) return;

    // 构建指令包:[CMD, LEN, PAYLOAD..., CRC_H, CRC_L]
    final List<int> packet = [command];
    if (payload != null) {
      packet.add(payload.length);
      packet.addAll(payload);
    } else {
      packet.add(0);
    }

    // 追加CRC校验
    final crc = _calculateCrc16(Uint8List.fromList(packet));
    packet.add((crc >> 8) & 0xFF);
    packet.add(crc & 0xFF);

    // 实际调用: controlCharacteristic.write(Uint8List.fromList(packet));
  }

  /// 断开当前笔连接
  Future<void> disconnectPen() async {
    _batteryTimer?.cancel();
    _reconnectTimer?.cancel();

    if (_connectedPen != null) {
      _connectedPen!.connectionState = PenConnectionState.disconnecting;
      _connectionController.add(PenConnectionState.disconnecting);

      // 实际调用: device.disconnect();
      _connectedPen!.connectionState = PenConnectionState.disconnected;
      _connectedPen = null;
      _connectionController.add(PenConnectionState.disconnected);
    }

    // 清空缓冲区
    _flushStrokeBuffer();
  }

  /// 处理连接意外断开,启动自动重连
  void _onDisconnected(PadPenDevice device) {
    if (_reconnectAttempts >= PadBleConstants.maxReconnectAttempts) {
      // 超过最大重连次数,放弃重连
      _connectionController.add(PenConnectionState.disconnected);
      return;
    }

    _connectionController.add(PenConnectionState.reconnecting);
    _reconnectAttempts++;

    // 指数退避延迟重连
    final delay = PadBleConstants.reconnectDelaySeconds * _reconnectAttempts;
    final clampedDelay = delay > 30 ? 30 : delay;

    _reconnectTimer = Timer(Duration(seconds: clampedDelay), () async {
      final success = await connectPen(device);
      if (!success) {
        _onDisconnected(device);
      }
    });
  }

  /// 释放所有资源
  void dispose() {
    _batteryTimer?.cancel();
    _reconnectTimer?.cancel();
    _scanController.close();
    _strokeController.close();
    _connectionController.close();
    _batteryController.close();
    _strokeBuffer.clear();
  }
}