feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化

- 核心业务拆分为 Endpoint → Application Service → Repository 三层
- AI写入操作必须用户确认后才写库(确认卡片机制)
- 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复)
- 运动计划修复: 连续真实日期替代周模板
- 用药提醒去重 + 通知Outbox预留
- 认证收拢到AuthService, 管理员收拢到AdminService
- AI会话加用户归属校验防串号
- 提示词调整为患者视角
- 开发假数据已关闭
- 21/21测试通过, 0警告0错误
This commit is contained in:
MingNian
2026-06-20 20:41:42 +08:00
parent c610417e29
commit 4d213b5a44
132 changed files with 6733 additions and 2856 deletions

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'local_database.dart';
@@ -19,7 +20,7 @@ class ApiClient {
headers: {'Content-Type': 'application/json'},
)) {
_dio.interceptors.add(_AuthInterceptor(this));
_dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: false));
_dio.interceptors.add(LogInterceptor(requestBody: false, responseBody: false));
}
Dio get dio => _dio;
@@ -33,7 +34,8 @@ class ApiClient {
}
Future<void> clearTokens() async {
await _db.deleteAll();
await _db.delete('access_token');
await _db.delete('refresh_token');
}
/// 带 token 的 GET 请求
@@ -64,7 +66,19 @@ class ApiClient {
final res = await _dio.post(path, data: formData);
final data = res.data;
if (data is Map) {
return data['url']?.toString() ?? data['data']?['url']?.toString();
final topUrl = data['url']?.toString();
if (topUrl != null && topUrl.isNotEmpty) return topUrl;
final payload = data['data'];
if (payload is Map) {
final url = payload['url']?.toString();
return url != null && url.isNotEmpty ? url : null;
}
if (payload is List && payload.isNotEmpty && payload.first is Map) {
final first = payload.first as Map;
final url = first['url']?.toString();
return url != null && url.isNotEmpty ? url : null;
}
}
return null;
}
@@ -104,7 +118,7 @@ class _AuthInterceptor extends Interceptor {
final retryResponse = await Dio(BaseOptions(baseUrl: baseUrl)).fetch(opts);
return handler.resolve(retryResponse);
}
} catch (e) { print('[ApiClient] token刷新失败: $e'); }
} catch (e) { log('[ApiClient] token刷新失败: $e'); }
}
await _client.clearTokens();
}

View File

@@ -58,6 +58,11 @@ Widget buildPage(RouteInfo route, WidgetRef ref) {
return const ReportListPage();
case 'aiAnalysis':
return AiAnalysisPage(id: params['id']!);
case 'reportOriginal':
return ReportOriginalPage(
url: params['url'] ?? '',
title: params['title'] ?? '原始报告',
);
case 'exercisePlan':
return const ExercisePlanPage();
case 'exerciseCreate':

View File

@@ -1,7 +1,10 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
/// SQLite 本地数据库——存储 token 等关键信息
/// SQLite 本地键值缓存。
///
/// 约定:后端数据库是健康业务数据的唯一事实源。这里仅保存会话、偏好、
/// 草稿和临时缓存不保存健康记录、报告、用药、饮食、AI 对话等核心业务事实。
class LocalDatabase {
static LocalDatabase? _instance;
Database? _db;