feat: 应用内通知系统 + 结构化手术史/用药等相关改动
- 新增用户通知 outbox 流水线(EfUserNotificationPipeline)与后台投递 worker - 通知中心页面及前端通知服务接入 - 健康指标异常、用药/运动提醒等事件统一产出站内通知 - 健康档案结构化手术史、用药提醒扫描、医生/用户端点等配套调整 - AppDbContext 注册通知相关实体
This commit is contained in:
@@ -3,8 +3,21 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'local_database.dart';
|
||||
|
||||
/// API 基础地址
|
||||
const String baseUrl = 'http://localhost:5000'; // adb reverse 或同WiFi直连时改为PC的IP
|
||||
/// API 基础地址。可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。
|
||||
const String baseUrl = String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'http://localhost:5000',
|
||||
);
|
||||
|
||||
class ApiException implements Exception {
|
||||
final int? code;
|
||||
final String message;
|
||||
|
||||
const ApiException(this.message, {this.code});
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
/// Dio HTTP 客户端封装——带 token 注入、401 自动刷新
|
||||
class ApiClient {
|
||||
@@ -40,22 +53,22 @@ class ApiClient {
|
||||
|
||||
/// 带 token 的 GET 请求
|
||||
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
|
||||
return _dio.get(path, queryParameters: queryParameters);
|
||||
return _request(_dio.get(path, queryParameters: queryParameters));
|
||||
}
|
||||
|
||||
/// 带 token 的 POST 请求
|
||||
Future<Response> post(String path, {dynamic data}) async {
|
||||
return _dio.post(path, data: data);
|
||||
return _request(_dio.post(path, data: data));
|
||||
}
|
||||
|
||||
/// 带 token 的 PUT 请求
|
||||
Future<Response> put(String path, {dynamic data}) async {
|
||||
return _dio.put(path, data: data);
|
||||
return _request(_dio.put(path, data: data));
|
||||
}
|
||||
|
||||
/// 带 token 的 DELETE 请求
|
||||
Future<Response> delete(String path) async {
|
||||
return _dio.delete(path);
|
||||
return _request(_dio.delete(path));
|
||||
}
|
||||
|
||||
/// 上传文件(multipart),返回文件 URL
|
||||
@@ -63,7 +76,7 @@ class ApiClient {
|
||||
final formData = FormData.fromMap({
|
||||
fieldName: await MultipartFile.fromFile(file.path, filename: file.path.split('/').last),
|
||||
});
|
||||
final res = await _dio.post(path, data: formData);
|
||||
final res = await _request(_dio.post(path, data: formData));
|
||||
final data = res.data;
|
||||
if (data is Map) {
|
||||
final topUrl = data['url']?.toString();
|
||||
@@ -82,6 +95,46 @@ class ApiClient {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Response _ensureBusinessSuccess(Response response) {
|
||||
final body = response.data;
|
||||
if (body is Map && body.containsKey('code')) {
|
||||
final rawCode = body['code'];
|
||||
final code = rawCode is int ? rawCode : int.tryParse('$rawCode');
|
||||
if (code != null && code != 0) {
|
||||
throw ApiException(
|
||||
body['message']?.toString().trim().isNotEmpty == true
|
||||
? body['message'].toString()
|
||||
: '请求失败',
|
||||
code: code,
|
||||
);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response> _request(Future<Response> request) async {
|
||||
try {
|
||||
return _ensureBusinessSuccess(await request);
|
||||
} on DioException catch (error) {
|
||||
final body = error.response?.data;
|
||||
final message = body is Map && body['message']?.toString().trim().isNotEmpty == true
|
||||
? body['message'].toString()
|
||||
: error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout
|
||||
? '请求超时,请稍后重试'
|
||||
: error.type == DioExceptionType.connectionError
|
||||
? '无法连接服务器,请检查网络或后端地址'
|
||||
: '请求失败,请稍后重试';
|
||||
final rawCode = body is Map ? body['code'] : null;
|
||||
throw ApiException(
|
||||
message,
|
||||
code: rawCode is int
|
||||
? rawCode
|
||||
: int.tryParse('$rawCode') ?? error.response?.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 认证拦截器:自动注入 token + 401 刷新
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../pages/report/ai_analysis_page.dart';
|
||||
import '../pages/consultation/consultation_pages.dart';
|
||||
import '../pages/settings/settings_pages.dart';
|
||||
import '../pages/settings/notification_prefs_page.dart';
|
||||
import '../pages/notifications/notification_center_page.dart';
|
||||
import '../pages/profile/profile_page.dart';
|
||||
import '../pages/diet/diet_capture_page.dart';
|
||||
import '../pages/device/device_scan_page.dart';
|
||||
@@ -97,6 +98,8 @@ Widget buildPage(RouteInfo route, WidgetRef ref) {
|
||||
return const SettingsPage();
|
||||
case 'notificationPrefs':
|
||||
return const NotificationPrefsPage();
|
||||
case 'notifications':
|
||||
return const NotificationCenterPage();
|
||||
case 'staticText':
|
||||
return StaticTextPage(type: params['type']!);
|
||||
case 'dietDetail':
|
||||
|
||||
Reference in New Issue
Block a user