Files
nj/app/lib/data/services/sse_notification_service.dart
iven 749ef55b89
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
feat: Week 4 收尾 + 架构治理 — 搜索/家长中心/Feature Flag/Docker/环境配置
架构治理:
- 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
2026-06-01 23:53:34 +08:00

147 lines
3.5 KiB
Dart
Raw 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.
// 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();
}
}