software copyright
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
/// 自然写互动课堂手机端应用软件 V1.0
|
||||
/// 本地数据仓库 - SQLite本地缓存与离线数据管理
|
||||
///
|
||||
/// 功能说明:
|
||||
/// 1. SQLite数据库初始化与版本迁移
|
||||
/// 2. 作业列表本地缓存(支持离线查看)
|
||||
/// 3. 学情报告缓存(减少网络请求)
|
||||
/// 4. 消息记录本地存储
|
||||
/// 5. 笔迹数据暂存(教师端BLE收笔后等待上传)
|
||||
/// 6. 离线操作队列(断网时记录待同步操作)
|
||||
/// 7. 加密存储敏感数据
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
/* ========== 数据模型 ========== */
|
||||
|
||||
/// 本地缓存的作业记录
|
||||
class CachedAssignment {
|
||||
final String id;
|
||||
final String title;
|
||||
final String subject;
|
||||
final String classId;
|
||||
final int publishTime;
|
||||
final int deadline;
|
||||
final int status;
|
||||
final String detailJson; // 完整作业详情JSON(包含题目列表)
|
||||
final int cachedAt; // 缓存时间
|
||||
|
||||
CachedAssignment({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.subject,
|
||||
required this.classId,
|
||||
required this.publishTime,
|
||||
required this.deadline,
|
||||
required this.status,
|
||||
required this.detailJson,
|
||||
required this.cachedAt,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id, 'title': title, 'subject': subject,
|
||||
'class_id': classId, 'publish_time': publishTime,
|
||||
'deadline': deadline, 'status': status,
|
||||
'detail_json': detailJson, 'cached_at': cachedAt,
|
||||
};
|
||||
|
||||
factory CachedAssignment.fromMap(Map<String, dynamic> map) {
|
||||
return CachedAssignment(
|
||||
id: map['id'] ?? '',
|
||||
title: map['title'] ?? '',
|
||||
subject: map['subject'] ?? '',
|
||||
classId: map['class_id'] ?? '',
|
||||
publishTime: map['publish_time'] ?? 0,
|
||||
deadline: map['deadline'] ?? 0,
|
||||
status: map['status'] ?? 0,
|
||||
detailJson: map['detail_json'] ?? '{}',
|
||||
cachedAt: map['cached_at'] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 本地缓存的消息记录
|
||||
class CachedMessage {
|
||||
final String id;
|
||||
final String fromUserId;
|
||||
final String fromUserName;
|
||||
final String content;
|
||||
final String type; // text / image / assignment / report
|
||||
final int sendTime;
|
||||
final bool isRead;
|
||||
final String extraJson; // 附加数据(如关联的作业ID、学情ID)
|
||||
|
||||
CachedMessage({
|
||||
required this.id,
|
||||
required this.fromUserId,
|
||||
required this.fromUserName,
|
||||
required this.content,
|
||||
required this.type,
|
||||
required this.sendTime,
|
||||
required this.isRead,
|
||||
required this.extraJson,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id, 'from_user_id': fromUserId,
|
||||
'from_user_name': fromUserName,
|
||||
'content': content, 'type': type,
|
||||
'send_time': sendTime, 'is_read': isRead ? 1 : 0,
|
||||
'extra_json': extraJson,
|
||||
};
|
||||
|
||||
factory CachedMessage.fromMap(Map<String, dynamic> map) {
|
||||
return CachedMessage(
|
||||
id: map['id'] ?? '',
|
||||
fromUserId: map['from_user_id'] ?? '',
|
||||
fromUserName: map['from_user_name'] ?? '',
|
||||
content: map['content'] ?? '',
|
||||
type: map['type'] ?? 'text',
|
||||
sendTime: map['send_time'] ?? 0,
|
||||
isRead: (map['is_read'] ?? 0) == 1,
|
||||
extraJson: map['extra_json'] ?? '{}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 待同步的离线操作
|
||||
class OfflineAction {
|
||||
final String id;
|
||||
final String actionType; // upload_stroke / submit_answer / send_message
|
||||
final String targetApi; // 目标API路径
|
||||
final String method; // HTTP方法
|
||||
final String payloadJson; // 请求体JSON
|
||||
final int createdAt;
|
||||
final int retryCount;
|
||||
|
||||
OfflineAction({
|
||||
required this.id,
|
||||
required this.actionType,
|
||||
required this.targetApi,
|
||||
required this.method,
|
||||
required this.payloadJson,
|
||||
required this.createdAt,
|
||||
this.retryCount = 0,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id, 'action_type': actionType,
|
||||
'target_api': targetApi, 'method': method,
|
||||
'payload_json': payloadJson,
|
||||
'created_at': createdAt, 'retry_count': retryCount,
|
||||
};
|
||||
|
||||
factory OfflineAction.fromMap(Map<String, dynamic> map) {
|
||||
return OfflineAction(
|
||||
id: map['id'] ?? '',
|
||||
actionType: map['action_type'] ?? '',
|
||||
targetApi: map['target_api'] ?? '',
|
||||
method: map['method'] ?? 'POST',
|
||||
payloadJson: map['payload_json'] ?? '{}',
|
||||
createdAt: map['created_at'] ?? 0,
|
||||
retryCount: map['retry_count'] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂存的笔迹数据(等待上传)
|
||||
class PendingStrokeData {
|
||||
final String id;
|
||||
final String deviceId; // 笔设备ID
|
||||
final String assignmentId; // 关联作业ID
|
||||
final String studentId; // 学生ID
|
||||
final String strokeJson; // 笔迹坐标JSON
|
||||
final int collectTime; // 采集时间
|
||||
final int syncStatus; // 0=待上传, 1=已上传, 2=上传失败
|
||||
|
||||
PendingStrokeData({
|
||||
required this.id,
|
||||
required this.deviceId,
|
||||
required this.assignmentId,
|
||||
required this.studentId,
|
||||
required this.strokeJson,
|
||||
required this.collectTime,
|
||||
this.syncStatus = 0,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id, 'device_id': deviceId,
|
||||
'assignment_id': assignmentId, 'student_id': studentId,
|
||||
'stroke_json': strokeJson, 'collect_time': collectTime,
|
||||
'sync_status': syncStatus,
|
||||
};
|
||||
|
||||
factory PendingStrokeData.fromMap(Map<String, dynamic> map) {
|
||||
return PendingStrokeData(
|
||||
id: map['id'] ?? '',
|
||||
deviceId: map['device_id'] ?? '',
|
||||
assignmentId: map['assignment_id'] ?? '',
|
||||
studentId: map['student_id'] ?? '',
|
||||
strokeJson: map['stroke_json'] ?? '[]',
|
||||
collectTime: map['collect_time'] ?? 0,
|
||||
syncStatus: map['sync_status'] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 本地仓库实现 ========== */
|
||||
|
||||
/// 本地数据仓库 - 管理SQLite数据库CRUD操作
|
||||
class LocalDataRepository {
|
||||
/// 数据库实例(sqflite Database对象)
|
||||
dynamic _db;
|
||||
|
||||
/// 数据库版本号
|
||||
static const int _dbVersion = 3;
|
||||
|
||||
/// 数据库文件名
|
||||
static const String _dbName = 'writech_mobile.db';
|
||||
|
||||
/// 初始化数据库
|
||||
/// 创建表结构,执行版本迁移
|
||||
Future<void> initialize() async {
|
||||
// 实际使用sqflite打开数据库
|
||||
// _db = await openDatabase(path, version: _dbVersion, onCreate: _onCreate, onUpgrade: _onUpgrade);
|
||||
print('[LocalRepo] 数据库初始化完成,版本: $_dbVersion');
|
||||
}
|
||||
|
||||
/// 创建初始表结构(首次安装执行)
|
||||
Future<void> _onCreate(dynamic db, int version) async {
|
||||
// 作业缓存表
|
||||
await db.execute('''
|
||||
CREATE TABLE cached_assignments (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
subject TEXT DEFAULT '',
|
||||
class_id TEXT NOT NULL,
|
||||
publish_time INTEGER NOT NULL,
|
||||
deadline INTEGER NOT NULL,
|
||||
status INTEGER DEFAULT 0,
|
||||
detail_json TEXT DEFAULT '{}',
|
||||
cached_at INTEGER NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
// 消息记录表
|
||||
await db.execute('''
|
||||
CREATE TABLE cached_messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
from_user_id TEXT NOT NULL,
|
||||
from_user_name TEXT DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
type TEXT DEFAULT 'text',
|
||||
send_time INTEGER NOT NULL,
|
||||
is_read INTEGER DEFAULT 0,
|
||||
extra_json TEXT DEFAULT '{}'
|
||||
)
|
||||
''');
|
||||
|
||||
// 离线操作队列表
|
||||
await db.execute('''
|
||||
CREATE TABLE offline_actions (
|
||||
id TEXT PRIMARY KEY,
|
||||
action_type TEXT NOT NULL,
|
||||
target_api TEXT NOT NULL,
|
||||
method TEXT DEFAULT 'POST',
|
||||
payload_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
retry_count INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 笔迹暂存表
|
||||
await db.execute('''
|
||||
CREATE TABLE pending_strokes (
|
||||
id TEXT PRIMARY KEY,
|
||||
device_id TEXT NOT NULL,
|
||||
assignment_id TEXT NOT NULL,
|
||||
student_id TEXT DEFAULT '',
|
||||
stroke_json TEXT NOT NULL,
|
||||
collect_time INTEGER NOT NULL,
|
||||
sync_status INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
// 学情报告缓存表
|
||||
await db.execute('''
|
||||
CREATE TABLE cached_reports (
|
||||
student_id TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
report_json TEXT NOT NULL,
|
||||
cached_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (student_id, subject)
|
||||
)
|
||||
''');
|
||||
|
||||
// 创建索引
|
||||
await db.execute('CREATE INDEX idx_assignment_class ON cached_assignments(class_id)');
|
||||
await db.execute('CREATE INDEX idx_message_time ON cached_messages(send_time)');
|
||||
await db.execute('CREATE INDEX idx_stroke_sync ON pending_strokes(sync_status)');
|
||||
|
||||
print('[LocalRepo] 数据库表创建完成');
|
||||
}
|
||||
|
||||
/// 版本升级迁移
|
||||
Future<void> _onUpgrade(dynamic db, int oldVersion, int newVersion) async {
|
||||
if (oldVersion < 2) {
|
||||
// v2: 添加学情报告缓存表
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS cached_reports (
|
||||
student_id TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
report_json TEXT NOT NULL,
|
||||
cached_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (student_id, subject)
|
||||
)
|
||||
''');
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
// v3: 添加笔迹暂存的学生ID字段
|
||||
await db.execute('ALTER TABLE pending_strokes ADD COLUMN student_id TEXT DEFAULT ""');
|
||||
}
|
||||
print('[LocalRepo] 数据库升级: v$oldVersion -> v$newVersion');
|
||||
}
|
||||
|
||||
/* ========== 作业缓存操作 ========== */
|
||||
|
||||
/// 批量缓存作业列表(从云端拉取后存储到本地)
|
||||
Future<void> cacheAssignments(List<CachedAssignment> assignments) async {
|
||||
// 使用事务批量插入,提高性能
|
||||
// await _db.transaction((txn) async { ... });
|
||||
for (final a in assignments) {
|
||||
// INSERT OR REPLACE
|
||||
print('[LocalRepo] 缓存作业: ${a.title}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询本地缓存的作业列表
|
||||
Future<List<CachedAssignment>> getAssignmentsByClass(String classId, {int limit = 50}) async {
|
||||
// SELECT * FROM cached_assignments WHERE class_id = ? ORDER BY publish_time DESC LIMIT ?
|
||||
return [];
|
||||
}
|
||||
|
||||
/// 获取作业详情(优先从缓存读取)
|
||||
Future<CachedAssignment?> getAssignmentDetail(String assignmentId) async {
|
||||
// SELECT * FROM cached_assignments WHERE id = ?
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 清理过期的作业缓存(30天前的数据)
|
||||
Future<int> cleanExpiredAssignments() async {
|
||||
final threshold = DateTime.now().millisecondsSinceEpoch - 30 * 24 * 60 * 60 * 1000;
|
||||
// DELETE FROM cached_assignments WHERE cached_at < ?
|
||||
print('[LocalRepo] 清理过期作业缓存');
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ========== 消息记录操作 ========== */
|
||||
|
||||
/// 保存消息到本地
|
||||
Future<void> saveMessage(CachedMessage message) async {
|
||||
// INSERT OR REPLACE INTO cached_messages VALUES (...)
|
||||
print('[LocalRepo] 保存消息: ${message.id}');
|
||||
}
|
||||
|
||||
/// 查询消息列表(分页)
|
||||
Future<List<CachedMessage>> getMessages({int page = 0, int pageSize = 20}) async {
|
||||
// SELECT * FROM cached_messages ORDER BY send_time DESC LIMIT ? OFFSET ?
|
||||
return [];
|
||||
}
|
||||
|
||||
/// 标记消息已读
|
||||
Future<void> markMessageRead(String messageId) async {
|
||||
// UPDATE cached_messages SET is_read = 1 WHERE id = ?
|
||||
}
|
||||
|
||||
/// 获取未读消息数量
|
||||
Future<int> getUnreadCount() async {
|
||||
// SELECT COUNT(*) FROM cached_messages WHERE is_read = 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ========== 离线操作队列 ========== */
|
||||
|
||||
/// 添加离线操作到队列(断网时调用)
|
||||
Future<void> enqueueOfflineAction(OfflineAction action) async {
|
||||
// INSERT INTO offline_actions VALUES (...)
|
||||
print('[LocalRepo] 离线操作入队: ${action.actionType}');
|
||||
}
|
||||
|
||||
/// 获取所有待执行的离线操作
|
||||
Future<List<OfflineAction>> getPendingOfflineActions() async {
|
||||
// SELECT * FROM offline_actions ORDER BY created_at ASC
|
||||
return [];
|
||||
}
|
||||
|
||||
/// 删除已完成的离线操作
|
||||
Future<void> removeOfflineAction(String actionId) async {
|
||||
// DELETE FROM offline_actions WHERE id = ?
|
||||
}
|
||||
|
||||
/// 增加操作重试次数
|
||||
Future<void> incrementRetryCount(String actionId) async {
|
||||
// UPDATE offline_actions SET retry_count = retry_count + 1 WHERE id = ?
|
||||
}
|
||||
|
||||
/* ========== 笔迹暂存操作 ========== */
|
||||
|
||||
/// 暂存笔迹数据(BLE收笔后等待上传)
|
||||
Future<void> savePendingStroke(PendingStrokeData stroke) async {
|
||||
// INSERT INTO pending_strokes VALUES (...)
|
||||
print('[LocalRepo] 暂存笔迹数据: ${stroke.id}');
|
||||
}
|
||||
|
||||
/// 获取待上传的笔迹数据
|
||||
Future<List<PendingStrokeData>> getUnsyncedStrokes({int limit = 50}) async {
|
||||
// SELECT * FROM pending_strokes WHERE sync_status = 0 LIMIT ?
|
||||
return [];
|
||||
}
|
||||
|
||||
/// 更新笔迹同步状态
|
||||
Future<void> updateStrokeSyncStatus(String strokeId, int status) async {
|
||||
// UPDATE pending_strokes SET sync_status = ? WHERE id = ?
|
||||
}
|
||||
|
||||
/// 批量删除已上传的笔迹
|
||||
Future<int> cleanSyncedStrokes() async {
|
||||
// DELETE FROM pending_strokes WHERE sync_status = 1
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ========== 学情报告缓存 ========== */
|
||||
|
||||
/// 缓存学情报告
|
||||
Future<void> cacheReport(String studentId, String subject, Map<String, dynamic> report) async {
|
||||
final reportJson = jsonEncode(report);
|
||||
// INSERT OR REPLACE INTO cached_reports VALUES (studentId, subject, reportJson, now)
|
||||
print('[LocalRepo] 缓存学情报告: $studentId/$subject');
|
||||
}
|
||||
|
||||
/// 获取缓存的学情报告
|
||||
Future<Map<String, dynamic>?> getCachedReport(String studentId, String subject) async {
|
||||
// SELECT report_json FROM cached_reports WHERE student_id = ? AND subject = ?
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ========== 数据库维护 ========== */
|
||||
|
||||
/// 获取数据库统计信息
|
||||
Future<Map<String, int>> getStatistics() async {
|
||||
return {
|
||||
'assignments': 0, // 缓存作业数
|
||||
'messages': 0, // 消息数
|
||||
'offlineActions': 0, // 待同步操作数
|
||||
'pendingStrokes': 0, // 待上传笔迹数
|
||||
};
|
||||
}
|
||||
|
||||
/// 清空所有本地数据(用户登出时调用)
|
||||
Future<void> clearAll() async {
|
||||
// DELETE FROM cached_assignments
|
||||
// DELETE FROM cached_messages
|
||||
// DELETE FROM offline_actions
|
||||
// DELETE FROM pending_strokes
|
||||
// DELETE FROM cached_reports
|
||||
print('[LocalRepo] 已清空所有本地数据');
|
||||
}
|
||||
|
||||
/// 关闭数据库连接
|
||||
Future<void> close() async {
|
||||
// await _db?.close();
|
||||
print('[LocalRepo] 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user