// 自然写互动课堂平板端应用软件 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 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 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 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 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 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 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 initialize() async { // 实际调用: openDatabase(path, version: padDbVersion, ...) // 以下为建表SQL // V1: 基础表 await _createTablesV1(); // V2: 增加学习计划表 await _createTablesV2(); // V3: 增加使用时长记录表 await _createTablesV3(); // V4: 增加练字记录表和索引优化 await _createTablesV4(); } /// V1建表:作业缓存、作答进度、错题本、离线操作队列 Future _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 _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 _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 _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 cacheHomework(CachedHomework homework) async { // await _db.insert( // PadDbTables.homework, // homework.toMap(), // conflictAlgorithm: ConflictAlgorithm.replace, // ); } /// 获取本地缓存的作业列表 Future> getCachedHomeworks({ String? status, int limit = 50, }) async { // String where = ''; // List 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 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>> getAnswerProgress( String homeworkId, ) async { // final maps = await _db.query( // PadDbTables.answerProgress, // where: 'homework_id = ?', // whereArgs: [homeworkId], // ); // final result = >{}; // for (final m in maps) { // result[m['question_id'] as String] = m; // } // return result; return {}; } // ============================================================ // 错题本 CRUD // ============================================================ /// 添加错题记录 Future addErrorEntry(ErrorBookEntry entry) async { // await _db.insert( // PadDbTables.errorBook, // entry.toMap(), // conflictAlgorithm: ConflictAlgorithm.replace, // ); } /// 获取错题列表(支持按科目/知识点筛选) Future> getErrorEntries({ String? subject, String? knowledgePoint, int limit = 50, int offset = 0, }) async { // final conditions = []; // final args = []; // 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 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> 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 saveStudyPlan(StudyPlanEntry plan) async { // await _db.insert( // PadDbTables.studyPlan, // plan.toMap(), // conflictAlgorithm: ConflictAlgorithm.replace, // ); } /// 获取活跃的学习计划列表 Future>> getActiveStudyPlans() async { // return await _db.query( // PadDbTables.studyPlan, // where: 'is_active = 1', // orderBy: 'start_date ASC', // ); return []; } /// 更新学习计划进度 Future updatePlanProgress( String planId, double progress, int completedTasks, ) async { // await _db.update( // PadDbTables.studyPlan, // {'progress': progress, 'completed_tasks': completedTasks}, // where: 'id = ?', // whereArgs: [planId], // ); } // ============================================================ // 练字记录 // ============================================================ /// 保存练字记录 Future savePracticeRecord(PracticeRecord record) async { // await _db.insert( // PadDbTables.practiceRecord, // record.toMap(), // conflictAlgorithm: ConflictAlgorithm.replace, // ); } /// 获取某字的练习历史(查看进步轨迹) Future>> 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 enqueueOfflineAction( String actionType, Map payload, ) async { // await _db.insert(PadDbTables.offlineAction, { // 'action_type': actionType, // 'payload': jsonEncode(payload), // 'created_at': DateTime.now().toIso8601String(), // 'status': 'pending', // }); } /// 获取待执行的离线操作 Future>> getPendingOfflineActions() async { // return await _db.query( // PadDbTables.offlineAction, // where: 'status = ? AND retry_count < 5', // whereArgs: ['pending'], // orderBy: 'created_at ASC', // ); return []; } /// 标记离线操作完成 Future markOfflineActionDone(int actionId) async { // await _db.update( // PadDbTables.offlineAction, // {'status': 'done'}, // where: 'id = ?', // whereArgs: [actionId], // ); } // ============================================================ // 使用时长记录 // ============================================================ /// 记录使用时长 Future 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 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 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 getDatabaseSize() async { // final dbPath = await getDatabasesPath(); // final file = File('$dbPath/writech_pad.db'); // return file.existsSync() ? file.lengthSync() : 0; return 0; } /// 关闭数据库 Future close() async { // await _db.close(); } }