Files
nj/app/lib/data/repositories/remote_journal_repository.dart
iven bb388ed8ff
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): 日记可见性修复 — 私密日记仅本地 + Web 端 ID 修复 + 分享按钮
问题修复:
1. Web端保存的日记看不到:createJournal 返回值未捕获,server ID 丢失导致
   后续元素保存用错 ID。现在使用 saved.id 贯穿全部操作。
2. 管理端看不到新建日记:后端 list_journals 添加 is_private 过滤,admin/teacher
   查看他人日记时排除私密日记。
3. RemoteJournalRepository 添加 onJournalChanged 变更通知流,HomeBloc 可自动刷新。
4. SyncEngine(native + web)enqueue 添加 is_private 防御性检查,私密日记不入队。
5. 编辑器 _persistState 条件入队:仅非私密日记同步到后端。
6. 分享流程改造:首次从私密变为公开时入队 create 操作上传。
7. 日记卡片添加可见性标签(仅自己可见/班级可见/公开),私密日记可点击分享。
8. 首页 _sharePrivateJournal 弹出 ShareBottomSheet 主动分享。
2026-06-04 12:03:24 +08:00

172 lines
5.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 远程日记仓库 — 通过 API 客户端连接后端
import 'dart:async';
import '../models/journal_element.dart';
import '../models/journal_entry.dart';
import '../remote/api_client.dart';
import 'journal_repository.dart';
/// 远程日记仓库 — 通过 HTTP API 操作后端数据
///
/// 所有操作需要网络连接。离线场景由 SyncEngine 协调 Isar 本地仓库处理。
class RemoteJournalRepository implements JournalRepository {
final ApiClient _api;
/// 变更通知流控制器 — 写操作成功后通知 UI 刷新
final StreamController<void> _changeController =
StreamController<void>.broadcast();
RemoteJournalRepository({required ApiClient api}) : _api = api;
@override
Future<List<JournalEntry>> getJournals({
DateTime? dateFrom,
DateTime? dateTo,
int? page,
int? pageSize,
String? mood,
String? tag,
String? classId,
}) async {
final queryParams = <String, dynamic>{};
// 后端 NaiveDateTime 格式: "2026-06-01T00:00:00"(不带毫秒)
if (dateFrom != null) {
queryParams['date_from'] = dateFrom.toIso8601String().replaceFirst(RegExp(r'\.\d+'), '');
}
if (dateTo != null) {
queryParams['date_to'] = dateTo.toIso8601String().replaceFirst(RegExp(r'\.\d+'), '');
}
if (page != null) queryParams['page'] = page;
if (pageSize != null) queryParams['page_size'] = pageSize;
if (mood != null) queryParams['mood'] = mood;
if (tag != null) queryParams['tag'] = tag;
if (classId != null) queryParams['class_id'] = classId;
final response = await _api.get('/diary/journals', queryParams: queryParams);
final body = response.data as Map<String, dynamic>;
// 后端信封格式: { success, data: { data: [...], total, page, ... }, message }
final envelope = body['data'] as Map<String, dynamic>? ?? {};
final items = envelope['data'] as List? ?? [];
return items
.map((json) => JournalEntry.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<int> getJournalCount() async {
final response = await _api.get('/diary/journals', queryParams: {
'page': 1,
'page_size': 1,
});
final body = response.data as Map<String, dynamic>;
// 后端信封格式: { success, data: { data: [...], total, ... }, message }
final envelope = body['data'] as Map<String, dynamic>? ?? {};
return (envelope['total'] as int?) ?? 0;
}
@override
Future<JournalEntry?> getJournal(String id) async {
try {
final response = await _api.get('/diary/journals/$id');
final body = response.data as Map<String, dynamic>;
return JournalEntry.fromJson(body['data'] as Map<String, dynamic>);
} on ApiException catch (e) {
if (e.statusCode == 404) return null;
rethrow;
}
}
@override
Future<JournalEntry> createJournal(JournalEntry entry) async {
// 后端 CreateJournalReq.date 是 NaiveDate只有日期需转换格式
final json = entry.toJson();
json['date'] = entry.date.toIso8601String().substring(0, 10);
final response = await _api.post('/diary/journals', data: json);
final body = response.data as Map<String, dynamic>;
final created = JournalEntry.fromJson(body['data'] as Map<String, dynamic>);
_changeController.add(null); // 通知 UI 刷新列表
return created;
}
@override
Future<JournalEntry> updateJournal(JournalEntry entry) async {
final response = await _api.put(
'/diary/journals/${entry.id}',
data: {
'title': entry.title,
'mood': entry.mood.value,
'weather': entry.weather.value,
'tags': entry.tags,
'is_private': entry.isPrivate,
'shared_to_class': entry.sharedToClass,
'version': entry.version,
},
);
final body = response.data as Map<String, dynamic>;
_changeController.add(null); // 通知 UI 刷新列表
return JournalEntry.fromJson(body['data'] as Map<String, dynamic>);
}
@override
Future<void> deleteJournal(String id) async {
await _api.delete('/diary/journals/$id');
_changeController.add(null); // 通知 UI 刷新列表
}
@override
Future<List<JournalElement>> getElements(String journalId) async {
final response = await _api.get('/diary/journals/$journalId/elements');
final body = response.data as Map<String, dynamic>;
final items = body['data'] as List? ?? [];
return items
.map((json) => JournalElement.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<JournalElement> addElement(JournalElement element) async {
final response = await _api.post(
'/diary/journals/${element.journalId}/elements',
data: element.toJson(),
);
final body = response.data as Map<String, dynamic>;
return JournalElement.fromJson(body['data'] as Map<String, dynamic>);
}
@override
Future<JournalElement> updateElement(JournalElement element) async {
final response = await _api.put(
'/diary/journals/${element.journalId}/elements/${element.id}',
data: element.toJson(),
);
final body = response.data as Map<String, dynamic>;
return JournalElement.fromJson(body['data'] as Map<String, dynamic>);
}
@override
Future<void> removeElement(String elementId) async {
await _api.delete('/diary/elements/$elementId');
}
/// 变更通知流 — 写操作后广播,供 HomeBloc 自动刷新
@override
Stream<void> get onJournalChanged => _changeController.stream;
}
/// API 异常封装 — 后端返回非 2xx 状态码时抛出
class ApiException implements Exception {
final String message;
final int statusCode;
final dynamic responseBody;
const ApiException({
required this.message,
required this.statusCode,
this.responseBody,
});
@override
String toString() => 'ApiException($statusCode): $message';
}