架构治理: - 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
147 lines
3.5 KiB
Dart
147 lines
3.5 KiB
Dart
// SSE 通知服务 — 监听服务端推送事件
|
||
|
||
import 'dart:async';
|
||
import 'dart:convert';
|
||
|
||
import 'package:dio/dio.dart';
|
||
|
||
/// SSE 通知事件
|
||
class NotificationEvent {
|
||
final String type;
|
||
final Map<String, dynamic> payload;
|
||
final DateTime receivedAt;
|
||
|
||
const NotificationEvent({
|
||
required this.type,
|
||
required this.payload,
|
||
required this.receivedAt,
|
||
});
|
||
}
|
||
|
||
/// SSE 通知服务 — 监听后端 Server-Sent Events 推送
|
||
///
|
||
/// 使用方式:
|
||
/// ```dart
|
||
/// final service = SseNotificationService(token: 'jwt-token');
|
||
/// service.events.listen((event) {
|
||
/// // 处理通知
|
||
/// });
|
||
/// await service.connect();
|
||
/// ```
|
||
class SseNotificationService {
|
||
final String _baseUrl;
|
||
final String? _token;
|
||
|
||
Dio? _dio;
|
||
Response<ResponseBody>? _response;
|
||
StreamController<NotificationEvent>? _controller;
|
||
bool _disposed = false;
|
||
|
||
SseNotificationService({
|
||
required String token,
|
||
String baseUrl = 'http://localhost:3000/api/v1',
|
||
}) : _token = token,
|
||
_baseUrl = baseUrl;
|
||
|
||
/// 通知事件流
|
||
Stream<NotificationEvent> get events {
|
||
_controller ??= StreamController<NotificationEvent>.broadcast();
|
||
return _controller!.stream;
|
||
}
|
||
|
||
/// 连接到 SSE 端点
|
||
Future<void> connect() async {
|
||
if (_disposed) return;
|
||
|
||
_dio = Dio(BaseOptions(
|
||
baseUrl: _baseUrl,
|
||
headers: {
|
||
'Accept': 'text/event-stream',
|
||
'Cache-Control': 'no-cache',
|
||
if (_token != null) 'Authorization': 'Bearer $_token',
|
||
},
|
||
responseType: ResponseType.stream,
|
||
));
|
||
|
||
try {
|
||
_response = await _dio!.get<ResponseBody>('/message/stream');
|
||
|
||
if (_response?.data == null) return;
|
||
|
||
_response!.data!.stream.listen(
|
||
(data) {
|
||
_parseSseData(data);
|
||
},
|
||
onError: (error) {
|
||
if (!_disposed) {
|
||
// 自动重连逻辑(3秒延迟)
|
||
Future.delayed(const Duration(seconds: 3), () {
|
||
if (!_disposed) connect();
|
||
});
|
||
}
|
||
},
|
||
onDone: () {
|
||
if (!_disposed) {
|
||
Future.delayed(const Duration(seconds: 3), () {
|
||
if (!_disposed) connect();
|
||
});
|
||
}
|
||
},
|
||
);
|
||
} catch (e) {
|
||
// 连接失败,延迟重连
|
||
if (!_disposed) {
|
||
Future.delayed(const Duration(seconds: 5), () {
|
||
if (!_disposed) connect();
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 解析 SSE 数据帧
|
||
void _parseSseData(List<int> data) {
|
||
final text = utf8.decode(data);
|
||
final lines = text.split('\n');
|
||
|
||
String? eventType;
|
||
String? eventData;
|
||
|
||
for (final line in lines) {
|
||
if (line.startsWith('event:')) {
|
||
eventType = line.substring(6).trim();
|
||
} else if (line.startsWith('data:')) {
|
||
eventData = line.substring(5).trim();
|
||
} else if (line.isEmpty && eventType != null && eventData != null) {
|
||
// 空行 = 事件结束
|
||
_emitEvent(eventType, eventData);
|
||
eventType = null;
|
||
eventData = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 发射通知事件到流
|
||
void _emitEvent(String type, String data) {
|
||
if (_disposed || _controller == null) return;
|
||
|
||
try {
|
||
final payload = jsonDecode(data) as Map<String, dynamic>;
|
||
_controller!.add(NotificationEvent(
|
||
type: type,
|
||
payload: payload,
|
||
receivedAt: DateTime.now(),
|
||
));
|
||
} catch (_) {
|
||
// 忽略解析错误
|
||
}
|
||
}
|
||
|
||
/// 断开连接并释放资源
|
||
void dispose() {
|
||
_disposed = true;
|
||
_response?.data?.stream.listen((_) {});
|
||
_controller?.close();
|
||
_dio?.close();
|
||
}
|
||
}
|