feat(diary): 数据层 + 班级系统 (Phase F1 + B3)

Flutter 数据层 (Phase F1):
- journal_entry.dart: 日记数据模型 (Mood/Weather/tags/version)
- journal_element.dart: 元素模型 (text/image/sticker/handwriting_ref/tape)
- school_class.dart: 班级模型
- user_settings.dart: 用户设置 (主题/画笔/字号)
- isar_database.dart: Isar 初始化
- api_client.dart: Dio + JWT注入 + 离线感知 + 401处理
- journal_repository.dart: 抽象接口 + InMemory实现 (乐观锁)
- sync_engine.dart: WiFi同步 + 操作队列 + 重试(5次) + 快照持久化

Rust 班级系统 (Phase B3):
- class_service.rs: 创建班级(6位码) + 加入班级 + 成员管理
- topic_service.rs: 老师布置主题 + 主题列表
- comment_service.rs: 老师点评 + 评语列表
- class_handler.rs: 5个API端点 + 权限守卫
- topic_handler.rs: 2个API端点
- comment_handler.rs: 2个API端点
- dto.rs: 新增5个DTO (ClassMemberResp/CreateTopicReq/TopicResp/CreateCommentReq/CommentResp)
- 6条新路由注册

验证: cargo check 通过, 433测试全绿, flutter analyze 1 warning
This commit is contained in:
iven
2026-06-01 00:55:51 +08:00
parent d0653614e0
commit 5e6c6fdd62
18 changed files with 2205 additions and 1 deletions

View File

@@ -0,0 +1,184 @@
// 日记仓库 — 抽象接口 + 内存实现(开发测试用)
//
// 设计思路:
// - 抽象接口 [JournalRepository] 定义数据操作契约
// - 后续实现 IsarJournalRepository本地和 RemoteJournalRepository远程
// - SyncEngine 负责协调本地和远程仓库之间的数据同步
// - 内存实现 [InMemoryJournalRepository] 用于开发阶段快速迭代
import '../models/journal_entry.dart';
import '../models/journal_element.dart';
/// 日记仓库抽象接口 — 所有数据操作的统一契约
///
/// 查询参数说明:
/// - [dateFrom]/[dateTo]: 日期范围过滤(闭区间)
/// - [page]/[pageSize]: 分页参数,从 1 开始
abstract class JournalRepository {
/// 获取日记列表(支持日期范围过滤和分页)
Future<List<JournalEntry>> getJournals({
DateTime? dateFrom,
DateTime? dateTo,
int? page,
int? pageSize,
});
/// 获取单篇日记(返回 null 表示不存在)
Future<JournalEntry?> getJournal(String id);
/// 创建新日记
Future<JournalEntry> createJournal(JournalEntry entry);
/// 更新日记(使用 version 字段做乐观锁冲突检测)
Future<JournalEntry> updateJournal(JournalEntry entry);
/// 删除日记(软删除,设置 deleted_at
Future<void> deleteJournal(String id);
/// 获取指定日记的所有元素
Future<List<JournalElement>> getElements(String journalId);
/// 添加元素到日记
Future<JournalElement> addElement(JournalElement element);
/// 更新元素
Future<JournalElement> updateElement(JournalElement element);
/// 从日记中移除元素
Future<void> removeElement(String elementId);
}
/// 内存实现 — 用于开发阶段快速迭代和单元测试
///
/// 数据存储在内存中,应用重启后丢失。
/// 线程安全说明Flutter 是单线程模型,无需加锁。
class InMemoryJournalRepository implements JournalRepository {
final Map<String, JournalEntry> _journals = {};
final Map<String, JournalElement> _elements = {};
@override
Future<List<JournalEntry>> getJournals({
DateTime? dateFrom,
DateTime? dateTo,
int? page,
int? pageSize,
}) async {
var results = _journals.values.toList();
// 日期范围过滤
if (dateFrom != null) {
results = results.where((j) => j.date.isAfter(dateFrom)).toList();
}
if (dateTo != null) {
results = results.where((j) => j.date.isBefore(dateTo)).toList();
}
// 按日期降序排列(最新在前)
results.sort((a, b) => b.date.compareTo(a.date));
// 分页
if (page != null && pageSize != null) {
final start = (page - 1) * pageSize;
if (start >= results.length) return [];
final end = (start + pageSize).clamp(0, results.length);
results = results.sublist(start, end);
}
return results;
}
@override
Future<JournalEntry?> getJournal(String id) async {
return _journals[id];
}
@override
Future<JournalEntry> createJournal(JournalEntry entry) async {
_journals[entry.id] = entry;
return entry;
}
@override
Future<JournalEntry> updateJournal(JournalEntry entry) async {
final existing = _journals[entry.id];
if (existing == null) {
throw StateError('日记不存在: ${entry.id}');
}
// 乐观锁冲突检测:版本号必须匹配
if (existing.version != entry.version) {
throw StateError(
'版本冲突: 本地版本 ${entry.version}, 服务端版本 ${existing.version}',
);
}
// 版本号 +1更新时间戳
final updated = entry.copyWith(
version: entry.version + 1,
updatedAt: DateTime.now(),
);
_journals[entry.id] = updated;
return updated;
}
@override
Future<void> deleteJournal(String id) async {
// 内存实现:直接移除(软删除由 Isar 实现处理)
_journals.remove(id);
// 同时移除关联元素
_elements.removeWhere((_, e) => e.journalId == id);
}
@override
Future<List<JournalElement>> getElements(String journalId) async {
return _elements.values
.where((e) => e.journalId == journalId)
.toList()
..sort((a, b) => a.zIndex.compareTo(b.zIndex));
}
@override
Future<JournalElement> addElement(JournalElement element) async {
_elements[element.id] = element;
return element;
}
@override
Future<JournalElement> updateElement(JournalElement element) async {
final existing = _elements[element.id];
if (existing == null) {
throw StateError('元素不存在: ${element.id}');
}
// 乐观锁冲突检测
if (existing.version != element.version) {
throw StateError(
'版本冲突: 本地版本 ${element.version}, 服务端版本 ${existing.version}',
);
}
final updated = element.copyWith(
version: element.version + 1,
updatedAt: DateTime.now(),
);
_elements[element.id] = updated;
return updated;
}
@override
Future<void> removeElement(String elementId) async {
_elements.remove(elementId);
}
/// 清空所有数据(测试辅助方法)
void clearAll() {
_journals.clear();
_elements.clear();
}
/// 获取日记总数(测试辅助方法)
int get journalCount => _journals.length;
/// 获取元素总数(测试辅助方法)
int get elementCount => _elements.length;
}