// Isar 本地日记仓库 — 本地优先数据存储 // // 实现 JournalRepository 抽象接口,所有数据存储在 Isar 本地数据库。 // 核心逻辑参考 InMemoryJournalRepository,替换内存 Map 为 Isar 查询。 // // 转换层: // - JournalEntry ↔ JournalEntryCollection(通过 toCollection/fromCollection) // - JournalElement ↔ JournalElementCollection(通过 toCollection/fromCollection) import 'dart:convert'; import 'package:isar/isar.dart'; import '../local/isar_database.dart'; import '../local/collections/journal_entry_collection.dart'; import '../local/collections/journal_element_collection.dart'; import '../models/journal_entry.dart'; import '../models/journal_element.dart'; import 'journal_repository.dart'; /// Isar 本地日记仓库 — JournalRepository 的 Isar 实现 class IsarJournalRepository implements JournalRepository { Isar get _isar => IsarDatabase.instance!; // ============================================================ // 日记 CRUD // ============================================================ @override Future> getJournals({ DateTime? dateFrom, DateTime? dateTo, int? page, int? pageSize, String? mood, String? tag, String? classId, }) async { var query = _isar.journalEntryCollections .where() .filter() .isDeletedEqualTo(false); // 日期范围过滤 if (dateFrom != null) { query = query.and().dateEpochGreaterThan(dateFrom.millisecondsSinceEpoch); } if (dateTo != null) { query = query.and().dateEpochLessThan(dateTo.millisecondsSinceEpoch); } // 心情过滤 if (mood != null) { query = query.and().moodEqualTo(mood); } // 标签过滤:Isar tagsJson 字段存储 JSON 数组,用 contains 匹配 if (tag != null) { query = query.and().tagsJsonContains(tag); } // 班级过滤 if (classId != null) { query = query.and().classIdEqualTo(classId); } // 按日期降序排列 var results = await query .sortByDateEpochDesc() .findAll(); // 分页 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.map(_fromCollection).toList(); } @override Future getJournalCount() async { return _isar.journalEntryCollections .where() .filter() .isDeletedEqualTo(false) .count(); } @override Future getJournal(String id) async { final col = await _isar.journalEntryCollections .where() .filter() .idEqualTo(id) .and() .isDeletedEqualTo(false) .findFirst(); if (col == null) return null; return _fromCollection(col); } @override Future createJournal(JournalEntry entry) async { final col = _toEntryCollection(entry); await _isar.writeTxn(() async { await _isar.journalEntryCollections.put(col); }); return entry; } @override Future updateJournal(JournalEntry entry) async { final existing = await _isar.journalEntryCollections .where() .filter() .idEqualTo(entry.id) .and() .isDeletedEqualTo(false) .findFirst(); if (existing == null) { throw StateError('日记不存在: ${entry.id}'); } // 乐观锁冲突检测 if (existing.version != entry.version) { throw StateError( '版本冲突: 本地版本 ${entry.version}, 存储版本 ${existing.version}', ); } final updated = entry.copyWith( version: entry.version + 1, updatedAt: DateTime.now(), ); final col = _toEntryCollection(updated); col.isarId = existing.isarId; // 保留 Isar 主键 await _isar.writeTxn(() async { await _isar.journalEntryCollections.put(col); }); return updated; } @override Future deleteJournal(String id) async { final existing = await _isar.journalEntryCollections .where() .filter() .idEqualTo(id) .findFirst(); if (existing == null) return; // 软删除日记 existing.isDeleted = true; existing.updatedAtEpoch = DateTime.now().millisecondsSinceEpoch; // 软删除关联元素 final elements = await _isar.journalElementCollections .where() .filter() .journalIdEqualTo(id) .and() .isDeletedEqualTo(false) .findAll(); await _isar.writeTxn(() async { await _isar.journalEntryCollections.put(existing); for (final el in elements) { el.isDeleted = true; el.updatedAtEpoch = DateTime.now().millisecondsSinceEpoch; await _isar.journalElementCollections.put(el); } }); } // ============================================================ // 元素 CRUD // ============================================================ @override Future> getElements(String journalId) async { final results = await _isar.journalElementCollections .where() .filter() .journalIdEqualTo(journalId) .and() .isDeletedEqualTo(false) .sortByZIndex() .findAll(); return results.map(_fromElementCollection).toList(); } @override Future addElement(JournalElement element) async { final col = _toElementCollection(element); await _isar.writeTxn(() async { await _isar.journalElementCollections.put(col); }); return element; } @override Future updateElement(JournalElement element) async { final existing = await _isar.journalElementCollections .where() .filter() .idEqualTo(element.id) .and() .isDeletedEqualTo(false) .findFirst(); 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(), ); final col = _toElementCollection(updated); col.isarId = existing.isarId; await _isar.writeTxn(() async { await _isar.journalElementCollections.put(col); }); return updated; } @override Future removeElement(String elementId) async { final existing = await _isar.journalElementCollections .where() .filter() .idEqualTo(elementId) .findFirst(); if (existing == null) return; // 软删除 existing.isDeleted = true; existing.updatedAtEpoch = DateTime.now().millisecondsSinceEpoch; await _isar.writeTxn(() async { await _isar.journalElementCollections.put(existing); }); } // ============================================================ // 转换函数:JournalEntry ↔ JournalEntryCollection // ============================================================ /// JournalEntry → JournalEntryCollection JournalEntryCollection _toEntryCollection(JournalEntry entry) { return JournalEntryCollection() ..id = entry.id ..authorId = entry.authorId ..classId = entry.classId ..title = entry.title ..dateEpoch = entry.date.millisecondsSinceEpoch ..mood = entry.mood.value ..weather = entry.weather.value ..tagsJson = jsonEncode(entry.tags) ..isPrivate = entry.isPrivate ..sharedToClass = entry.sharedToClass ..assignedTopicId = entry.assignedTopicId ..contentExcerpt = entry.contentExcerpt ..version = entry.version ..createdAtEpoch = entry.createdAt.millisecondsSinceEpoch ..updatedAtEpoch = entry.updatedAt.millisecondsSinceEpoch ..isDeleted = false; } /// JournalEntryCollection → JournalEntry JournalEntry _fromCollection(JournalEntryCollection col) { return JournalEntry( id: col.id, authorId: col.authorId, classId: col.classId, title: col.title, date: DateTime.fromMillisecondsSinceEpoch(col.dateEpoch), mood: Mood.values.firstWhere( (m) => m.value == col.mood, orElse: () => Mood.calm, ), weather: Weather.values.firstWhere( (w) => w.value == col.weather, orElse: () => Weather.sunny, ), tags: List.from( jsonDecode(col.tagsJson) as List? ?? [], ), isPrivate: col.isPrivate, sharedToClass: col.sharedToClass, assignedTopicId: col.assignedTopicId, contentExcerpt: col.contentExcerpt, version: col.version, createdAt: DateTime.fromMillisecondsSinceEpoch(col.createdAtEpoch), updatedAt: DateTime.fromMillisecondsSinceEpoch(col.updatedAtEpoch), ); } // ============================================================ // 转换函数:JournalElement ↔ JournalElementCollection // ============================================================ /// JournalElement → JournalElementCollection JournalElementCollection _toElementCollection(JournalElement element) { return JournalElementCollection() ..id = element.id ..journalId = element.journalId ..elementType = element.elementType.value ..positionX = element.positionX ..positionY = element.positionY ..width = element.width ..height = element.height ..rotation = element.rotation ..zIndex = element.zIndex ..contentJson = jsonEncode(element.content) ..version = element.version ..createdAtEpoch = element.createdAt.millisecondsSinceEpoch ..updatedAtEpoch = element.updatedAt.millisecondsSinceEpoch ..isDeleted = false; } /// JournalElementCollection → JournalElement JournalElement _fromElementCollection(JournalElementCollection col) { return JournalElement( id: col.id, journalId: col.journalId, elementType: ElementType.values.firstWhere( (e) => e.value == col.elementType, orElse: () => ElementType.text, ), positionX: col.positionX, positionY: col.positionY, width: col.width, height: col.height, rotation: col.rotation, zIndex: col.zIndex, content: Map.from( jsonDecode(col.contentJson) as Map? ?? {}, ), version: col.version, createdAt: DateTime.fromMillisecondsSinceEpoch(col.createdAtEpoch), updatedAt: DateTime.fromMillisecondsSinceEpoch(col.updatedAtEpoch), ); } }