Files
2026-03-22 15:24:40 +08:00

754 lines
21 KiB
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();
}
}