架构治理: - Feature Flag 落地: Cargo.toml [features] default=["diary"] + main.rs cfg 条件编译 - 环境配置统一: AppConfig 类 + --dart-define 注入 + SSE 端口 8080→3000 修复 搜索替代方案 (无 FTS): - SearchBloc + 标签/心情筛选接入后端 API - JournalRepository 扩展 mood/tag 筛选参数 - 搜索页 UI 接入实际数据(替换占位文本) 家长中心最小集 (PIPL 合规): - 后端: parent_service (绑定/查看/导出/删除/解绑) + parent_handler (6 个 API 端点) - 前端: ParentBloc + ParentPage 功能完整实现 - 绑定孩子、只读查看日记、导出数据、删除数据、解绑 Docker 部署: - verify.sh 健康检查脚本 (Axum/PG/Redis/OpenAPI 四项检查) 测试修复: - home_bloc_test / calendar_bloc_test 适配 JournalRepository 新参数 验证: flutter test 84/84 pass, cargo test 76/76 pass, cargo check pass
199 lines
5.6 KiB
Dart
199 lines
5.6 KiB
Dart
// 日记仓库 — 抽象接口 + 内存实现(开发测试用)
|
||
//
|
||
// 设计思路:
|
||
// - 抽象接口 [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,
|
||
String? mood,
|
||
String? tag,
|
||
});
|
||
|
||
/// 获取单篇日记(返回 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,
|
||
String? mood,
|
||
String? tag,
|
||
}) 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();
|
||
}
|
||
|
||
// 心情过滤
|
||
if (mood != null) {
|
||
results = results.where((j) => j.mood.value == mood).toList();
|
||
}
|
||
|
||
// 标签过滤(日记 tags 列表包含指定标签)
|
||
if (tag != null) {
|
||
results = results.where((j) => j.tags.contains(tag)).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;
|
||
}
|