From 6e69f1085eaba1c0cabc8534df66df655055d179 Mon Sep 17 00:00:00 2001 From: MingNian <1281442923@qq.com> Date: Tue, 2 Jun 2026 12:41:06 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=85=A8=E9=9D=A2=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=EF=BC=8C=E9=81=B5=E5=BE=AA=20CLAUDE?= =?UTF-8?q?.md=20=E7=BC=96=E7=A0=81=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C# 文件命名改为 snake_case(28 个文件重命名) - C# 类转换为主构造函数(8 个类) - 空 catch 添加异常类型(2 处) - 新建 GlobalUsings.cs(Health.Infrastructure、Health.WebApi) - Flutter 移除 go_router,改用 Riverpod 路由栈 - Flutter 移除 flutter_secure_storage,改用 sqflite 持久化 - 修复 Flutter 构建路径(Flutter SDK 迁至 D 盘) - 后端端口改为 0.0.0.0:5000,支持局域网访问 --- .../{Consultation.cs => consultation.cs} | 0 .../{Conversation.cs => conversation.cs} | 0 .../{DietRecord.cs => diet_record.cs} | 0 .../{ExercisePlan.cs => exercise_plan.cs} | 0 .../{HealthRecord.cs => health_record.cs} | 0 .../Entities/{Medication.cs => medication.cs} | 0 .../Entities/{Report.cs => report.cs} | 0 ...SupportEntities.cs => support_entities.cs} | 0 .../Entities/{User.cs => user.cs} | 0 .../Enums/{HealthEnums.cs => health_enums.cs} | 0 .../AI/{AiClients.cs => ai_clients.cs} | 44 ++-- ...Client.cs => open_ai_compatible_client.cs} | 31 ++- .../{PromptManager.cs => prompt_manager.cs} | 2 - .../{AppDbContext.cs => app_db_context.cs} | 4 +- .../Data/{DataSeeder.cs => data_seeder.cs} | 2 - .../{DevDataSeeder.cs => dev_data_seeder.cs} | 2 - .../src/Health.Infrastructure/GlobalUsings.cs | 4 + .../{JwtProvider.cs => jwt_provider.cs} | 16 +- .../{SmsService.cs => sms_service.cs} | 0 .../{CleanupService.cs => cleanup_service.cs} | 15 +- ...vice.cs => medication_reminder_service.cs} | 17 +- ...iChatEndpoints.cs => ai_chat_endpoints.cs} | 9 +- .../{AuthEndpoints.cs => auth_endpoints.cs} | 4 - ...HealthEndpoints.cs => health_endpoints.cs} | 5 - ...ingEndpoints.cs => remaining_endpoints.cs} | 5 - .../{UserEndpoints.cs => user_endpoints.cs} | 3 - backend/src/Health.WebApi/GlobalUsings.cs | 5 + ...nMiddleware.cs => exception_middleware.cs} | 13 +- .../Properties/launchSettings.json | 4 +- .../{AuthTests.cs => auth_tests.cs} | 0 .../{EntityTests.cs => entity_tests.cs} | 0 .../{UnitTest1.cs => unit_test1.cs} | 0 health_app/android/settings.gradle.kts | 2 +- health_app/lib/app.dart | 29 ++- health_app/lib/core/api_client.dart | 20 +- health_app/lib/core/app_router.dart | 95 ++++---- health_app/lib/core/local_database.dart | 58 +++++ health_app/lib/core/navigation_provider.dart | 56 +++++ health_app/lib/core/secure_storage.dart | 35 --- health_app/lib/pages/auth/login_page.dart | 6 +- .../medication/medication_list_page.dart | 6 +- .../lib/pages/profile/profile_page.dart | 12 +- .../lib/pages/settings/settings_pages.dart | 12 +- health_app/lib/providers/auth_provider.dart | 18 +- health_app/lib/widgets/health_drawer.dart | 16 +- health_app/pubspec.lock | 212 +++++------------- health_app/pubspec.yaml | 8 +- 47 files changed, 342 insertions(+), 428 deletions(-) rename backend/src/Health.Domain/Entities/{Consultation.cs => consultation.cs} (100%) rename backend/src/Health.Domain/Entities/{Conversation.cs => conversation.cs} (100%) rename backend/src/Health.Domain/Entities/{DietRecord.cs => diet_record.cs} (100%) rename backend/src/Health.Domain/Entities/{ExercisePlan.cs => exercise_plan.cs} (100%) rename backend/src/Health.Domain/Entities/{HealthRecord.cs => health_record.cs} (100%) rename backend/src/Health.Domain/Entities/{Medication.cs => medication.cs} (100%) rename backend/src/Health.Domain/Entities/{Report.cs => report.cs} (100%) rename backend/src/Health.Domain/Entities/{SupportEntities.cs => support_entities.cs} (100%) rename backend/src/Health.Domain/Entities/{User.cs => user.cs} (100%) rename backend/src/Health.Domain/Enums/{HealthEnums.cs => health_enums.cs} (100%) rename backend/src/Health.Infrastructure/AI/{AiClients.cs => ai_clients.cs} (81%) rename backend/src/Health.Infrastructure/AI/{OpenAiCompatibleClient.cs => open_ai_compatible_client.cs} (89%) rename backend/src/Health.Infrastructure/AI/{PromptManager.cs => prompt_manager.cs} (99%) rename backend/src/Health.Infrastructure/Data/{AppDbContext.cs => app_db_context.cs} (96%) rename backend/src/Health.Infrastructure/Data/{DataSeeder.cs => data_seeder.cs} (97%) rename backend/src/Health.Infrastructure/Data/{DevDataSeeder.cs => dev_data_seeder.cs} (99%) create mode 100644 backend/src/Health.Infrastructure/GlobalUsings.cs rename backend/src/Health.Infrastructure/Services/{JwtProvider.cs => jwt_provider.cs} (81%) rename backend/src/Health.Infrastructure/Services/{SmsService.cs => sms_service.cs} (100%) rename backend/src/Health.WebApi/BackgroundServices/{CleanupService.cs => cleanup_service.cs} (80%) rename backend/src/Health.WebApi/BackgroundServices/{MedicationReminderService.cs => medication_reminder_service.cs} (80%) rename backend/src/Health.WebApi/Endpoints/{AiChatEndpoints.cs => ai_chat_endpoints.cs} (99%) rename backend/src/Health.WebApi/Endpoints/{AuthEndpoints.cs => auth_endpoints.cs} (98%) rename backend/src/Health.WebApi/Endpoints/{HealthEndpoints.cs => health_endpoints.cs} (97%) rename backend/src/Health.WebApi/Endpoints/{RemainingEndpoints.cs => remaining_endpoints.cs} (99%) rename backend/src/Health.WebApi/Endpoints/{UserEndpoints.cs => user_endpoints.cs} (98%) create mode 100644 backend/src/Health.WebApi/GlobalUsings.cs rename backend/src/Health.WebApi/Middleware/{ExceptionMiddleware.cs => exception_middleware.cs} (74%) rename backend/tests/Health.Tests/{AuthTests.cs => auth_tests.cs} (100%) rename backend/tests/Health.Tests/{EntityTests.cs => entity_tests.cs} (100%) rename backend/tests/Health.Tests/{UnitTest1.cs => unit_test1.cs} (100%) create mode 100644 health_app/lib/core/local_database.dart create mode 100644 health_app/lib/core/navigation_provider.dart delete mode 100644 health_app/lib/core/secure_storage.dart diff --git a/backend/src/Health.Domain/Entities/Consultation.cs b/backend/src/Health.Domain/Entities/consultation.cs similarity index 100% rename from backend/src/Health.Domain/Entities/Consultation.cs rename to backend/src/Health.Domain/Entities/consultation.cs diff --git a/backend/src/Health.Domain/Entities/Conversation.cs b/backend/src/Health.Domain/Entities/conversation.cs similarity index 100% rename from backend/src/Health.Domain/Entities/Conversation.cs rename to backend/src/Health.Domain/Entities/conversation.cs diff --git a/backend/src/Health.Domain/Entities/DietRecord.cs b/backend/src/Health.Domain/Entities/diet_record.cs similarity index 100% rename from backend/src/Health.Domain/Entities/DietRecord.cs rename to backend/src/Health.Domain/Entities/diet_record.cs diff --git a/backend/src/Health.Domain/Entities/ExercisePlan.cs b/backend/src/Health.Domain/Entities/exercise_plan.cs similarity index 100% rename from backend/src/Health.Domain/Entities/ExercisePlan.cs rename to backend/src/Health.Domain/Entities/exercise_plan.cs diff --git a/backend/src/Health.Domain/Entities/HealthRecord.cs b/backend/src/Health.Domain/Entities/health_record.cs similarity index 100% rename from backend/src/Health.Domain/Entities/HealthRecord.cs rename to backend/src/Health.Domain/Entities/health_record.cs diff --git a/backend/src/Health.Domain/Entities/Medication.cs b/backend/src/Health.Domain/Entities/medication.cs similarity index 100% rename from backend/src/Health.Domain/Entities/Medication.cs rename to backend/src/Health.Domain/Entities/medication.cs diff --git a/backend/src/Health.Domain/Entities/Report.cs b/backend/src/Health.Domain/Entities/report.cs similarity index 100% rename from backend/src/Health.Domain/Entities/Report.cs rename to backend/src/Health.Domain/Entities/report.cs diff --git a/backend/src/Health.Domain/Entities/SupportEntities.cs b/backend/src/Health.Domain/Entities/support_entities.cs similarity index 100% rename from backend/src/Health.Domain/Entities/SupportEntities.cs rename to backend/src/Health.Domain/Entities/support_entities.cs diff --git a/backend/src/Health.Domain/Entities/User.cs b/backend/src/Health.Domain/Entities/user.cs similarity index 100% rename from backend/src/Health.Domain/Entities/User.cs rename to backend/src/Health.Domain/Entities/user.cs diff --git a/backend/src/Health.Domain/Enums/HealthEnums.cs b/backend/src/Health.Domain/Enums/health_enums.cs similarity index 100% rename from backend/src/Health.Domain/Enums/HealthEnums.cs rename to backend/src/Health.Domain/Enums/health_enums.cs diff --git a/backend/src/Health.Infrastructure/AI/AiClients.cs b/backend/src/Health.Infrastructure/AI/ai_clients.cs similarity index 81% rename from backend/src/Health.Infrastructure/AI/AiClients.cs rename to backend/src/Health.Infrastructure/AI/ai_clients.cs index 0180f3d..dad8f3d 100644 --- a/backend/src/Health.Infrastructure/AI/AiClients.cs +++ b/backend/src/Health.Infrastructure/AI/ai_clients.cs @@ -1,29 +1,20 @@ using Microsoft.Extensions.Configuration; using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; namespace Health.Infrastructure.AI; /// /// DeepSeek LLM 客户端(对话 + Tool Calling) /// -public sealed class DeepSeekClient +public sealed class DeepSeekClient(HttpClient http, IConfiguration config) { - private readonly HttpClient _http; - private readonly string _model; - private readonly JsonSerializerOptions _jsonOptions; - - public DeepSeekClient(HttpClient http, IConfiguration config) + private readonly HttpClient _http = http; + private readonly string _model = config["DEEPSEEK_MODEL"] ?? "deepseek-chat"; + private readonly JsonSerializerOptions _jsonOptions = new() { - _http = http; - _model = config["DEEPSEEK_MODEL"] ?? "deepseek-chat"; - _jsonOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - PropertyNameCaseInsensitive = true - }; - } + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + PropertyNameCaseInsensitive = true + }; /// /// 流式 Chat Completions @@ -96,22 +87,15 @@ public sealed class DeepSeekClient /// /// 千问 VL 视觉客户端(食物识别 + 报告解读) /// -public sealed class QwenVisionClient +public sealed class QwenVisionClient(HttpClient http, IConfiguration config) { - private readonly HttpClient _http; - private readonly string _model; - private readonly JsonSerializerOptions _jsonOptions; - - public QwenVisionClient(HttpClient http, IConfiguration config) + private readonly HttpClient _http = http; + private readonly string _model = config["QWEN_VISION_MODEL"] ?? "qwen-vl-max"; + private readonly JsonSerializerOptions _jsonOptions = new() { - _http = http; - _model = config["QWEN_VISION_MODEL"] ?? "qwen-vl-max"; - _jsonOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - PropertyNameCaseInsensitive = true - }; - } + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + PropertyNameCaseInsensitive = true + }; public async Task VisionAsync( string systemPrompt, diff --git a/backend/src/Health.Infrastructure/AI/OpenAiCompatibleClient.cs b/backend/src/Health.Infrastructure/AI/open_ai_compatible_client.cs similarity index 89% rename from backend/src/Health.Infrastructure/AI/OpenAiCompatibleClient.cs rename to backend/src/Health.Infrastructure/AI/open_ai_compatible_client.cs index dd8f479..6f4dc21 100644 --- a/backend/src/Health.Infrastructure/AI/OpenAiCompatibleClient.cs +++ b/backend/src/Health.Infrastructure/AI/open_ai_compatible_client.cs @@ -1,33 +1,30 @@ using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; namespace Health.Infrastructure.AI; /// /// OpenAI 兼容协议 HTTP 客户端,统一调用 DeepSeek / 千问 VL /// -public sealed class OpenAiCompatibleClient +public sealed class OpenAiCompatibleClient(string baseUrl, string apiKey, string model) { - private readonly HttpClient _http; - private readonly string _model; - private readonly JsonSerializerOptions _jsonOptions; - - public OpenAiCompatibleClient(string baseUrl, string apiKey, string model) + private readonly HttpClient _http = CreateHttpClient(baseUrl, apiKey); + private readonly string _model = model; + private readonly JsonSerializerOptions _jsonOptions = new() { - _http = new HttpClient + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + PropertyNameCaseInsensitive = true + }; + + private static HttpClient CreateHttpClient(string baseUrl, string apiKey) + { + var client = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"), Timeout = TimeSpan.FromSeconds(60) }; - _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); - _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _model = model; - _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - PropertyNameCaseInsensitive = true - }; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return client; } /// diff --git a/backend/src/Health.Infrastructure/AI/PromptManager.cs b/backend/src/Health.Infrastructure/AI/prompt_manager.cs similarity index 99% rename from backend/src/Health.Infrastructure/AI/PromptManager.cs rename to backend/src/Health.Infrastructure/AI/prompt_manager.cs index cf27012..26f7217 100644 --- a/backend/src/Health.Infrastructure/AI/PromptManager.cs +++ b/backend/src/Health.Infrastructure/AI/prompt_manager.cs @@ -1,5 +1,3 @@ -using Health.Domain.Enums; - namespace Health.Infrastructure.AI; /// diff --git a/backend/src/Health.Infrastructure/Data/AppDbContext.cs b/backend/src/Health.Infrastructure/Data/app_db_context.cs similarity index 96% rename from backend/src/Health.Infrastructure/Data/AppDbContext.cs rename to backend/src/Health.Infrastructure/Data/app_db_context.cs index fa93333..09d8420 100644 --- a/backend/src/Health.Infrastructure/Data/AppDbContext.cs +++ b/backend/src/Health.Infrastructure/Data/app_db_context.cs @@ -1,4 +1,3 @@ -using Health.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -7,9 +6,8 @@ namespace Health.Infrastructure.Data; /// /// 应用程序数据库上下文 /// -public sealed class AppDbContext : DbContext +public sealed class AppDbContext(DbContextOptions options) : DbContext(options) { - public AppDbContext(DbContextOptions options) : base(options) { } // 核心业务表 public DbSet Users => Set(); diff --git a/backend/src/Health.Infrastructure/Data/DataSeeder.cs b/backend/src/Health.Infrastructure/Data/data_seeder.cs similarity index 97% rename from backend/src/Health.Infrastructure/Data/DataSeeder.cs rename to backend/src/Health.Infrastructure/Data/data_seeder.cs index 2a07977..23215b3 100644 --- a/backend/src/Health.Infrastructure/Data/DataSeeder.cs +++ b/backend/src/Health.Infrastructure/Data/data_seeder.cs @@ -1,5 +1,3 @@ -using Health.Domain.Entities; - namespace Health.Infrastructure.Data; /// diff --git a/backend/src/Health.Infrastructure/Data/DevDataSeeder.cs b/backend/src/Health.Infrastructure/Data/dev_data_seeder.cs similarity index 99% rename from backend/src/Health.Infrastructure/Data/DevDataSeeder.cs rename to backend/src/Health.Infrastructure/Data/dev_data_seeder.cs index 3ffb16f..720fa25 100644 --- a/backend/src/Health.Infrastructure/Data/DevDataSeeder.cs +++ b/backend/src/Health.Infrastructure/Data/dev_data_seeder.cs @@ -1,5 +1,3 @@ -using Health.Domain.Entities; -using Health.Domain.Enums; using Microsoft.Extensions.Configuration; namespace Health.Infrastructure.Data; diff --git a/backend/src/Health.Infrastructure/GlobalUsings.cs b/backend/src/Health.Infrastructure/GlobalUsings.cs new file mode 100644 index 0000000..01a2b12 --- /dev/null +++ b/backend/src/Health.Infrastructure/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using Health.Domain.Entities; +global using Health.Domain.Enums; +global using System.Text; +global using System.Text.Json; diff --git a/backend/src/Health.Infrastructure/Services/JwtProvider.cs b/backend/src/Health.Infrastructure/Services/jwt_provider.cs similarity index 81% rename from backend/src/Health.Infrastructure/Services/JwtProvider.cs rename to backend/src/Health.Infrastructure/Services/jwt_provider.cs index 3896e3f..5000567 100644 --- a/backend/src/Health.Infrastructure/Services/JwtProvider.cs +++ b/backend/src/Health.Infrastructure/Services/jwt_provider.cs @@ -3,25 +3,17 @@ using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; -using System.Text; namespace Health.Infrastructure.Services; /// /// JWT Token 生成与验证服务 /// -public sealed class JwtProvider +public sealed class JwtProvider(IConfiguration configuration) { - private readonly string _secret; - private readonly string _issuer; - private readonly string _audience; - - public JwtProvider(IConfiguration configuration) - { - _secret = configuration["JWT_SECRET"] ?? "dev-secret-key-change-in-production-min-32-chars!!"; - _issuer = configuration["JWT_ISSUER"] ?? "health-manager"; - _audience = configuration["JWT_AUDIENCE"] ?? "health-manager-app"; - } + private readonly string _secret = configuration["JWT_SECRET"] ?? "dev-secret-key-change-in-production-min-32-chars!!"; + private readonly string _issuer = configuration["JWT_ISSUER"] ?? "health-manager"; + private readonly string _audience = configuration["JWT_AUDIENCE"] ?? "health-manager-app"; /// /// 生成 access_token(30 分钟有效) diff --git a/backend/src/Health.Infrastructure/Services/SmsService.cs b/backend/src/Health.Infrastructure/Services/sms_service.cs similarity index 100% rename from backend/src/Health.Infrastructure/Services/SmsService.cs rename to backend/src/Health.Infrastructure/Services/sms_service.cs diff --git a/backend/src/Health.WebApi/BackgroundServices/CleanupService.cs b/backend/src/Health.WebApi/BackgroundServices/cleanup_service.cs similarity index 80% rename from backend/src/Health.WebApi/BackgroundServices/CleanupService.cs rename to backend/src/Health.WebApi/BackgroundServices/cleanup_service.cs index 783b44b..4694329 100644 --- a/backend/src/Health.WebApi/BackgroundServices/CleanupService.cs +++ b/backend/src/Health.WebApi/BackgroundServices/cleanup_service.cs @@ -1,21 +1,12 @@ -using Health.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; - namespace Health.WebApi.BackgroundServices; /// /// 数据清理后台服务(每小时检查一次) /// -public sealed class CleanupService : BackgroundService +public sealed class CleanupService(IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService { - private readonly IServiceScopeFactory _scopeFactory; - private readonly ILogger _logger; - - public CleanupService(IServiceScopeFactory scopeFactory, ILogger logger) - { - _scopeFactory = scopeFactory; - _logger = logger; - } + private readonly IServiceScopeFactory _scopeFactory = scopeFactory; + private readonly ILogger _logger = logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/backend/src/Health.WebApi/BackgroundServices/MedicationReminderService.cs b/backend/src/Health.WebApi/BackgroundServices/medication_reminder_service.cs similarity index 80% rename from backend/src/Health.WebApi/BackgroundServices/MedicationReminderService.cs rename to backend/src/Health.WebApi/BackgroundServices/medication_reminder_service.cs index 6db9727..6e0c0fe 100644 --- a/backend/src/Health.WebApi/BackgroundServices/MedicationReminderService.cs +++ b/backend/src/Health.WebApi/BackgroundServices/medication_reminder_service.cs @@ -1,23 +1,12 @@ -using Health.Domain.Entities; -using Health.Domain.Enums; -using Health.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; - namespace Health.WebApi.BackgroundServices; /// /// 用药提醒定时扫描服务(每分钟检查一次) /// -public sealed class MedicationReminderService : BackgroundService +public sealed class MedicationReminderService(IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService { - private readonly IServiceScopeFactory _scopeFactory; - private readonly ILogger _logger; - - public MedicationReminderService(IServiceScopeFactory scopeFactory, ILogger logger) - { - _scopeFactory = scopeFactory; - _logger = logger; - } + private readonly IServiceScopeFactory _scopeFactory = scopeFactory; + private readonly ILogger _logger = logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/backend/src/Health.WebApi/Endpoints/AiChatEndpoints.cs b/backend/src/Health.WebApi/Endpoints/ai_chat_endpoints.cs similarity index 99% rename from backend/src/Health.WebApi/Endpoints/AiChatEndpoints.cs rename to backend/src/Health.WebApi/Endpoints/ai_chat_endpoints.cs index b8a4f1d..fe45e24 100644 --- a/backend/src/Health.WebApi/Endpoints/AiChatEndpoints.cs +++ b/backend/src/Health.WebApi/Endpoints/ai_chat_endpoints.cs @@ -1,9 +1,4 @@ -using System.Text.Json; -using Health.Domain.Entities; -using Health.Domain.Enums; using Health.Infrastructure.AI; -using Health.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; namespace Health.WebApi.Endpoints; @@ -135,7 +130,7 @@ public static class AiChatEndpoints await SseWriteAsync(http, new { action = "answer", data = content }, ct); } } - catch { /* 跳过解析失败的 chunk */ } + catch (JsonException) { /* 跳过解析失败的 chunk */ } } completedNormally = true; break; @@ -311,7 +306,7 @@ public static class AiChatEndpoints var sub = jwt.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; return sub != null && Guid.TryParse(sub, out var id) ? id : null; } - catch { return null; } + catch (Exception) { return null; } } private static List GetToolsForAgent(AgentType agentType) => agentType switch diff --git a/backend/src/Health.WebApi/Endpoints/AuthEndpoints.cs b/backend/src/Health.WebApi/Endpoints/auth_endpoints.cs similarity index 98% rename from backend/src/Health.WebApi/Endpoints/AuthEndpoints.cs rename to backend/src/Health.WebApi/Endpoints/auth_endpoints.cs index b81684f..c97f049 100644 --- a/backend/src/Health.WebApi/Endpoints/AuthEndpoints.cs +++ b/backend/src/Health.WebApi/Endpoints/auth_endpoints.cs @@ -1,8 +1,4 @@ -using System.Text.Json; -using Health.Domain.Entities; -using Health.Infrastructure.Data; using Health.Infrastructure.Services; -using Microsoft.EntityFrameworkCore; namespace Health.WebApi.Endpoints; diff --git a/backend/src/Health.WebApi/Endpoints/HealthEndpoints.cs b/backend/src/Health.WebApi/Endpoints/health_endpoints.cs similarity index 97% rename from backend/src/Health.WebApi/Endpoints/HealthEndpoints.cs rename to backend/src/Health.WebApi/Endpoints/health_endpoints.cs index ca52c7e..f0ab047 100644 --- a/backend/src/Health.WebApi/Endpoints/HealthEndpoints.cs +++ b/backend/src/Health.WebApi/Endpoints/health_endpoints.cs @@ -1,8 +1,3 @@ -using Health.Domain.Entities; -using Health.Domain.Enums; -using Health.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; - namespace Health.WebApi.Endpoints; /// diff --git a/backend/src/Health.WebApi/Endpoints/RemainingEndpoints.cs b/backend/src/Health.WebApi/Endpoints/remaining_endpoints.cs similarity index 99% rename from backend/src/Health.WebApi/Endpoints/RemainingEndpoints.cs rename to backend/src/Health.WebApi/Endpoints/remaining_endpoints.cs index 70fcb6b..68c295c 100644 --- a/backend/src/Health.WebApi/Endpoints/RemainingEndpoints.cs +++ b/backend/src/Health.WebApi/Endpoints/remaining_endpoints.cs @@ -1,8 +1,3 @@ -using Health.Domain.Entities; -using Health.Domain.Enums; -using Health.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; - namespace Health.WebApi.Endpoints; /// diff --git a/backend/src/Health.WebApi/Endpoints/UserEndpoints.cs b/backend/src/Health.WebApi/Endpoints/user_endpoints.cs similarity index 98% rename from backend/src/Health.WebApi/Endpoints/UserEndpoints.cs rename to backend/src/Health.WebApi/Endpoints/user_endpoints.cs index 24ac71b..4c09227 100644 --- a/backend/src/Health.WebApi/Endpoints/UserEndpoints.cs +++ b/backend/src/Health.WebApi/Endpoints/user_endpoints.cs @@ -1,6 +1,3 @@ -using Health.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; - namespace Health.WebApi.Endpoints; /// diff --git a/backend/src/Health.WebApi/GlobalUsings.cs b/backend/src/Health.WebApi/GlobalUsings.cs new file mode 100644 index 0000000..ee3bdb6 --- /dev/null +++ b/backend/src/Health.WebApi/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using Health.Domain.Entities; +global using Health.Domain.Enums; +global using Health.Infrastructure.Data; +global using Microsoft.EntityFrameworkCore; +global using System.Text.Json; diff --git a/backend/src/Health.WebApi/Middleware/ExceptionMiddleware.cs b/backend/src/Health.WebApi/Middleware/exception_middleware.cs similarity index 74% rename from backend/src/Health.WebApi/Middleware/ExceptionMiddleware.cs rename to backend/src/Health.WebApi/Middleware/exception_middleware.cs index 59aa416..e8b5ba9 100644 --- a/backend/src/Health.WebApi/Middleware/ExceptionMiddleware.cs +++ b/backend/src/Health.WebApi/Middleware/exception_middleware.cs @@ -1,21 +1,14 @@ using System.Net; -using System.Text.Json; namespace Health.WebApi.Middleware; /// /// 全局异常处理中间件——统一返回 {code, data, message} 格式 /// -public sealed class ExceptionMiddleware +public sealed class ExceptionMiddleware(RequestDelegate next, ILogger logger) { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public ExceptionMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } + private readonly RequestDelegate _next = next; + private readonly ILogger _logger = logger; public async Task InvokeAsync(HttpContext context) { diff --git a/backend/src/Health.WebApi/Properties/launchSettings.json b/backend/src/Health.WebApi/Properties/launchSettings.json index 76eddb6..a88d34c 100644 --- a/backend/src/Health.WebApi/Properties/launchSettings.json +++ b/backend/src/Health.WebApi/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5277", + "applicationUrl": "http://0.0.0.0:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -14,7 +14,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7102;http://localhost:5277", + "applicationUrl": "https://localhost:7102;http://0.0.0.0:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/backend/tests/Health.Tests/AuthTests.cs b/backend/tests/Health.Tests/auth_tests.cs similarity index 100% rename from backend/tests/Health.Tests/AuthTests.cs rename to backend/tests/Health.Tests/auth_tests.cs diff --git a/backend/tests/Health.Tests/EntityTests.cs b/backend/tests/Health.Tests/entity_tests.cs similarity index 100% rename from backend/tests/Health.Tests/EntityTests.cs rename to backend/tests/Health.Tests/entity_tests.cs diff --git a/backend/tests/Health.Tests/UnitTest1.cs b/backend/tests/Health.Tests/unit_test1.cs similarity index 100% rename from backend/tests/Health.Tests/UnitTest1.cs rename to backend/tests/Health.Tests/unit_test1.cs diff --git a/health_app/android/settings.gradle.kts b/health_app/android/settings.gradle.kts index 2a693b9..638b5a9 100644 --- a/health_app/android/settings.gradle.kts +++ b/health_app/android/settings.gradle.kts @@ -1,5 +1,5 @@ pluginManagement { - val flutterSdkPath = "C:/flutter_sdk" + val flutterSdkPath = "D:/flutter" includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") diff --git a/health_app/lib/app.dart b/health_app/lib/app.dart index 0d7d077..55a3bec 100644 --- a/health_app/lib/app.dart +++ b/health_app/lib/app.dart @@ -1,18 +1,39 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/app_router.dart'; import 'core/app_theme.dart'; +import 'core/navigation_provider.dart'; /// 健康管家 App 根组件 -class HealthApp extends StatelessWidget { +class HealthApp extends ConsumerWidget { const HealthApp({super.key}); @override - Widget build(BuildContext context) { - return MaterialApp.router( + Widget build(BuildContext context, WidgetRef ref) { + return const MaterialApp( title: '健康管家', debugShowCheckedModeBanner: false, theme: AppTheme.lightTheme, - routerConfig: AppRouter.router, + home: _RootNavigator(), + ); + } +} + +/// 根导航——根据 Riverpod 路由状态切换页面 +class _RootNavigator extends ConsumerWidget { + const _RootNavigator(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final stack = ref.watch(routeStackProvider); + final current = stack.last; + + return PopScope( + canPop: stack.length <= 1, + onPopInvokedWithResult: (didPop, result) { + if (!didPop) popRoute(ref); + }, + child: buildPage(current), ); } } diff --git a/health_app/lib/core/api_client.dart b/health_app/lib/core/api_client.dart index beb228d..152b106 100644 --- a/health_app/lib/core/api_client.dart +++ b/health_app/lib/core/api_client.dart @@ -1,16 +1,16 @@ import 'package:dio/dio.dart'; -import 'secure_storage.dart'; +import 'local_database.dart'; /// API 基础地址 -const String baseUrl = 'http://10.4.172.93:5000'; +const String baseUrl = 'http://10.4.185.103:5000'; /// Dio HTTP 客户端封装——带 token 注入、401 自动刷新 class ApiClient { final Dio _dio; - final SecureStorage _storage; + final LocalDatabase _db; - ApiClient({required SecureStorage storage}) - : _storage = storage, + ApiClient({required LocalDatabase db}) + : _db = db, _dio = Dio(BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 15), @@ -23,16 +23,16 @@ class ApiClient { Dio get dio => _dio; - Future get accessToken => _storage.readAccessToken(); - Future get refreshToken => _storage.readRefreshToken(); + Future get accessToken => _db.read('access_token'); + Future get refreshToken => _db.read('refresh_token'); Future saveTokens(String access, String refresh) async { - await _storage.writeAccessToken(access); - await _storage.writeRefreshToken(refresh); + await _db.write('access_token', access); + await _db.write('refresh_token', refresh); } Future clearTokens() async { - await _storage.deleteAll(); + await _db.deleteAll(); } /// 带 token 的 GET 请求 diff --git a/health_app/lib/core/app_router.dart b/health_app/lib/core/app_router.dart index 3126c2b..35f8b30 100644 --- a/health_app/lib/core/app_router.dart +++ b/health_app/lib/core/app_router.dart @@ -1,4 +1,5 @@ -import 'package:go_router/go_router.dart'; +import 'package:flutter/material.dart'; +import 'navigation_provider.dart'; import '../pages/auth/login_page.dart'; import '../pages/home/home_page.dart'; import '../pages/chart/trend_page.dart'; @@ -9,49 +10,51 @@ import '../pages/settings/settings_pages.dart'; import '../pages/profile/profile_page.dart'; import '../pages/remaining_pages.dart'; -/// 应用路由配置 -class AppRouter { - AppRouter._(); - - static final GoRouter router = GoRouter( - initialLocation: '/login', - routes: [ - GoRoute(path: '/login', builder: (_, _) => const LoginPage()), - GoRoute(path: '/home', builder: (_, _) => const HomePage()), - GoRoute(path: '/trend/:type', builder: (_, state) => TrendPage(metricType: state.pathParameters['type']!)), - GoRoute(path: '/calendar', builder: (_, _) => const HealthCalendarPage()), - - // 用药 - GoRoute(path: '/medications', builder: (_, _) => const MedicationListPage()), - GoRoute(path: '/medications/add', builder: (_, _) => const MedicationEditPage()), - GoRoute(path: '/medications/:id/edit', builder: (_, state) => MedicationEditPage(id: state.pathParameters['id'])), - - // 报告 - GoRoute(path: '/reports', builder: (_, _) => const ReportListPage()), - GoRoute(path: '/reports/:id', builder: (_, state) => ReportDetailPage(id: state.pathParameters['id']!)), - - // 问诊 - GoRoute(path: '/doctors', builder: (_, _) => const DoctorListPage()), - GoRoute(path: '/consultation/:id', builder: (_, state) => DoctorChatPage(id: state.pathParameters['id']!)), - - // 运动 - GoRoute(path: '/exercise-plan', builder: (_, _) => const ExercisePlanPage()), - - // 饮食 - GoRoute(path: '/diet-records', builder: (_, _) => const DietRecordListPage()), - - // 个人中心 - GoRoute(path: '/profile', builder: (_, _) => const ProfilePage()), - GoRoute(path: '/profile/edit', builder: (_, _) => const EditProfilePage()), - GoRoute(path: '/health-archive', builder: (_, _) => const HealthArchivePage()), - - // 复查 - GoRoute(path: '/followups', builder: (_, _) => const FollowUpListPage()), - - // 设置 - GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()), - GoRoute(path: '/settings/notifications', builder: (_, _) => const NotificationPrefsPage()), - GoRoute(path: '/page/:type', builder: (_, state) => StaticTextPage(type: state.pathParameters['type']!)), - ], - ); +/// 根据路由信息返回对应页面 +Widget buildPage(RouteInfo route) { + final params = route.params; + switch (route.name) { + case 'login': + return const LoginPage(); + case 'home': + return const HomePage(); + case 'trend': + return TrendPage(metricType: params['type'] ?? ''); + case 'calendar': + return const HealthCalendarPage(); + case 'medications': + return const MedicationListPage(); + case 'medicationAdd': + return const MedicationEditPage(); + case 'medicationEdit': + return MedicationEditPage(id: params['id']); + case 'reports': + return const ReportListPage(); + case 'reportDetail': + return ReportDetailPage(id: params['id']!); + case 'doctors': + return const DoctorListPage(); + case 'consultation': + return DoctorChatPage(id: params['id']!); + case 'exercisePlan': + return const ExercisePlanPage(); + case 'dietRecords': + return const DietRecordListPage(); + case 'profile': + return const ProfilePage(); + case 'profileEdit': + return const EditProfilePage(); + case 'healthArchive': + return const HealthArchivePage(); + case 'followups': + return const FollowUpListPage(); + case 'settings': + return const SettingsPage(); + case 'notificationPrefs': + return const NotificationPrefsPage(); + case 'staticText': + return StaticTextPage(type: params['type']!); + default: + return const LoginPage(); + } } diff --git a/health_app/lib/core/local_database.dart b/health_app/lib/core/local_database.dart new file mode 100644 index 0000000..3d5fe29 --- /dev/null +++ b/health_app/lib/core/local_database.dart @@ -0,0 +1,58 @@ +import 'package:sqflite/sqflite.dart'; +import 'package:path/path.dart'; + +/// SQLite 本地数据库——存储 token 等关键信息 +class LocalDatabase { + static LocalDatabase? _instance; + Database? _db; + + LocalDatabase._(); + + static LocalDatabase get instance => _instance ??= LocalDatabase._(); + + Future get database async { + _db ??= await _initDb(); + return _db!; + } + + Future _initDb() async { + final dbPath = await getDatabasesPath(); + final path = join(dbPath, 'health_app.db'); + return openDatabase( + path, + version: 1, + onCreate: (db, version) async { + await db.execute( + 'CREATE TABLE kv_store (key TEXT PRIMARY KEY, value TEXT)', + ); + }, + ); + } + + Future write(String key, String value) async { + final db = await database; + await db.insert( + 'kv_store', + {'key': key, 'value': value}, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future read(String key) async { + final db = await database; + final result = + await db.query('kv_store', where: 'key = ?', whereArgs: [key]); + if (result.isEmpty) return null; + return result.first['value'] as String?; + } + + Future delete(String key) async { + final db = await database; + await db.delete('kv_store', where: 'key = ?', whereArgs: [key]); + } + + Future deleteAll() async { + final db = await database; + await db.delete('kv_store'); + } +} diff --git a/health_app/lib/core/navigation_provider.dart b/health_app/lib/core/navigation_provider.dart new file mode 100644 index 0000000..b8043f3 --- /dev/null +++ b/health_app/lib/core/navigation_provider.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// 路由信息 +class RouteInfo { + final String name; + final Map params; + const RouteInfo(this.name, {this.params = const {}}); + + String param(String key) => params[key] ?? ''; +} + +/// 路由栈 Notifier +class RouteStackNotifier extends Notifier> { + @override + List build() => [const RouteInfo('login')]; + + void replace(String name, {Map params = const {}}) { + state = [RouteInfo(name, params: params)]; + } + + void push(String name, {Map params = const {}}) { + state = [...state, RouteInfo(name, params: params)]; + } + + void pop() { + if (state.length > 1) { + state = state.sublist(0, state.length - 1); + } + } +} + +/// 路由栈 Provider +final routeStackProvider = + NotifierProvider>(RouteStackNotifier.new); + +/// 当前路由 +final currentRouteProvider = Provider((ref) { + final stack = ref.watch(routeStackProvider); + return stack.last; +}); + +/// 跳转(替换整个栈) +void goRoute(WidgetRef ref, String name, {Map params = const {}}) { + ref.read(routeStackProvider.notifier).replace(name, params: params); +} + +/// 推入新页面 +void pushRoute(WidgetRef ref, String name, {Map params = const {}}) { + ref.read(routeStackProvider.notifier).push(name, params: params); +} + +/// 返回上一页 +void popRoute(WidgetRef ref) { + ref.read(routeStackProvider.notifier).pop(); +} diff --git a/health_app/lib/core/secure_storage.dart b/health_app/lib/core/secure_storage.dart deleted file mode 100644 index 5ff2259..0000000 --- a/health_app/lib/core/secure_storage.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; - -/// Token 安全存储(iOS Keychain / Android EncryptedSharedPreferences / Web 内存) -class SecureStorage { - final FlutterSecureStorage _storage; - static final Map _webFallback = {}; - - SecureStorage() : _storage = const FlutterSecureStorage(); - static const _access = 'access_token'; - static const _refresh = 'refresh_token'; - - bool get _isWeb => kIsWeb; - - Future writeAccessToken(String t) async { - if (_isWeb) { _webFallback[_access] = t; return; } - await _storage.write(key: _access, value: t); - } - Future readAccessToken() async { - if (_isWeb) return _webFallback[_access]; - return _storage.read(key: _access); - } - Future writeRefreshToken(String t) async { - if (_isWeb) { _webFallback[_refresh] = t; return; } - await _storage.write(key: _refresh, value: t); - } - Future readRefreshToken() async { - if (_isWeb) return _webFallback[_refresh]; - return _storage.read(key: _refresh); - } - Future deleteAll() async { - if (_isWeb) { _webFallback.clear(); return; } - await _storage.deleteAll(); - } -} diff --git a/health_app/lib/pages/auth/login_page.dart b/health_app/lib/pages/auth/login_page.dart index 36c6be0..6deb925 100644 --- a/health_app/lib/pages/auth/login_page.dart +++ b/health_app/lib/pages/auth/login_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; +import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; /// 登录页——手机号 + 验证码 @@ -71,7 +71,7 @@ class _LoginPageState extends ConsumerState { setState(() => _error = err); return; } - if (mounted) context.go('/home'); + goRoute(ref, 'home'); } @override @@ -80,7 +80,7 @@ class _LoginPageState extends ConsumerState { // 已登录直接跳转 if (authState.isLoggedIn && !authState.isLoading) { - WidgetsBinding.instance.addPostFrameCallback((_) => context.go('/home')); + WidgetsBinding.instance.addPostFrameCallback((_) => goRoute(ref, 'home')); } return Scaffold( diff --git a/health_app/lib/pages/medication/medication_list_page.dart b/health_app/lib/pages/medication/medication_list_page.dart index 35bf7bc..a3daf69 100644 --- a/health_app/lib/pages/medication/medication_list_page.dart +++ b/health_app/lib/pages/medication/medication_list_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; +import '../../core/navigation_provider.dart'; import '../../providers/data_providers.dart'; /// 用药列表页 @@ -37,7 +37,7 @@ class MedicationListPage extends ConsumerWidget { error: (_, _) => _empty(context), ), floatingActionButton: FloatingActionButton.extended( - onPressed: () => context.push('/medications/add').then((_) => ref.invalidate(medicationListProvider)), + onPressed: () { pushRoute(ref, 'medicationAdd'); ref.invalidate(medicationListProvider); }, icon: const Icon(Icons.add), label: const Text('添加药品'), ), ); @@ -65,7 +65,7 @@ class _MedicationEditPageState extends ConsumerState { 'frequency': 'Daily', 'timeOfDay': [if (_timeCtrl.text.isNotEmpty) _timeCtrl.text], 'source': 'Manual', 'startDate': DateTime.now().toIso8601String().substring(0, 10), }); - if (mounted) context.pop(); + popRoute(ref); } @override Widget build(BuildContext context) => Scaffold( diff --git a/health_app/lib/pages/profile/profile_page.dart b/health_app/lib/pages/profile/profile_page.dart index d54e73e..0fe8be7 100644 --- a/health_app/lib/pages/profile/profile_page.dart +++ b/health_app/lib/pages/profile/profile_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; +import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; /// 个人中心页面 @@ -38,12 +38,12 @@ class ProfilePage extends ConsumerWidget { ), ), const SizedBox(height: 8), - _MenuItem(icon: Icons.person, title: '编辑资料', onTap: () => context.push('/profile/edit')), - _MenuItem(icon: Icons.folder, title: '健康档案', onTap: () => context.push('/health-archive')), + _MenuItem(icon: Icons.person, title: '编辑资料', onTap: () => pushRoute(ref, 'profileEdit')), + _MenuItem(icon: Icons.folder, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')), _MenuItem(icon: Icons.devices, title: '设备管理', onTap: () {}), const Divider(), - _MenuItem(icon: Icons.settings, title: '设置', onTap: () => context.push('/settings')), - _MenuItem(icon: Icons.info, title: '关于', onTap: () => context.push('/page/about')), + _MenuItem(icon: Icons.settings, title: '设置', onTap: () => pushRoute(ref, 'settings')), + _MenuItem(icon: Icons.info, title: '关于', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})), const Divider(), _MenuItem( icon: Icons.logout, title: '退出登录', textColor: const Color(0xFFE53935), @@ -52,7 +52,7 @@ class ProfilePage extends ConsumerWidget { title: const Text('退出登录'), content: const Text('确定退出?'), actions: [TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定'))])); - if (ok == true) { await ref.read(authProvider.notifier).logout(); if (context.mounted) context.go('/login'); } + if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); } }, ), ], diff --git a/health_app/lib/pages/settings/settings_pages.dart b/health_app/lib/pages/settings/settings_pages.dart index 1ed4ee3..a8ac35a 100644 --- a/health_app/lib/pages/settings/settings_pages.dart +++ b/health_app/lib/pages/settings/settings_pages.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; +import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; /// 设置页 @@ -10,17 +10,17 @@ class SettingsPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) => Scaffold( appBar: AppBar(title: const Text('设置')), body: ListView(children: [ - _SetItem(icon: Icons.shield, title: '隐私保护中心', onTap: () => context.push('/page/privacy')), - _SetItem(icon: Icons.notifications, title: '通知偏好', onTap: () => context.push('/settings/notifications')), + _SetItem(icon: Icons.shield, title: '隐私保护中心', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'privacy'})), + _SetItem(icon: Icons.notifications, title: '通知偏好', onTap: () => pushRoute(ref, 'notificationPrefs')), _SetItem(icon: Icons.text_fields, title: '字体大小', trailing: _FontSlider()), - _SetItem(icon: Icons.article, title: '协议与公告', onTap: () => context.push('/page/terms')), - _SetItem(icon: Icons.info, title: '关于', onTap: () => context.push('/page/about')), + _SetItem(icon: Icons.article, title: '协议与公告', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'terms'})), + _SetItem(icon: Icons.info, title: '关于', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})), const Divider(), _SetItem(icon: Icons.logout, title: '退出登录', textColor: const Color(0xFFE53935), onTap: () async { final ok = await showDialog(context: context, builder: (ctx) => AlertDialog( title: const Text('退出登录'), content: const Text('确定退出?'), actions: [TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定'))])); - if (ok == true) { await ref.read(authProvider.notifier).logout(); if (context.mounted) context.go('/login'); } + if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); } }), ]), ); diff --git a/health_app/lib/providers/auth_provider.dart b/health_app/lib/providers/auth_provider.dart index deb8148..af2568d 100644 --- a/health_app/lib/providers/auth_provider.dart +++ b/health_app/lib/providers/auth_provider.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; import '../core/api_client.dart'; -import '../core/secure_storage.dart'; +import '../core/local_database.dart'; /// 用户简要信息 class UserInfo { @@ -25,10 +25,10 @@ class AuthState { /// 认证 Provider final authProvider = NotifierProvider(AuthNotifier.new); -final secureStorageProvider = Provider((ref) => SecureStorage()); +final localDbProvider = Provider((ref) => LocalDatabase.instance); final apiClientProvider = Provider((ref) { - return ApiClient(storage: ref.watch(secureStorageProvider)); + return ApiClient(db: ref.watch(localDbProvider)); }); class AuthNotifier extends Notifier { @@ -39,8 +39,8 @@ class AuthNotifier extends Notifier { } Future _checkAuth() async { - final storage = ref.read(secureStorageProvider); - final refresh = await storage.readRefreshToken(); + final db = ref.read(localDbProvider); + final refresh = await db.read('refresh_token'); if (refresh == null) { state = const AuthState(isLoggedIn: false, isLoading: false); return; @@ -51,8 +51,8 @@ class AuthNotifier extends Notifier { .post('/api/auth/refresh', data: {'refreshToken': refresh}); final data = response.data['data']; if (data != null) { - await storage.writeAccessToken(data['accessToken']); - await storage.writeRefreshToken(data['refreshToken']); + await db.write('access_token', data['accessToken']); + await db.write('refresh_token', data['refreshToken']); state = AuthState( isLoggedIn: true, isLoading: false, @@ -128,8 +128,8 @@ class AuthNotifier extends Notifier { /// 登出 Future logout() async { final api = ref.read(apiClientProvider); - final storage = ref.read(secureStorageProvider); - final refresh = await storage.readRefreshToken(); + final db = ref.read(localDbProvider); + final refresh = await db.read('refresh_token'); if (refresh != null) { try { await api.post('/api/auth/logout', data: {'refreshToken': refresh}); } catch (_) {} } diff --git a/health_app/lib/widgets/health_drawer.dart b/health_app/lib/widgets/health_drawer.dart index d3a0015..edf4779 100644 --- a/health_app/lib/widgets/health_drawer.dart +++ b/health_app/lib/widgets/health_drawer.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; +import '../core/navigation_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/data_providers.dart'; @@ -26,7 +26,7 @@ class HealthDrawer extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( - onTap: () => context.push('/profile'), + onTap: () => pushRoute(ref, 'profile'), child: CircleAvatar( radius: 28, backgroundColor: const Color(0xFFEDEBFF), @@ -40,7 +40,7 @@ class HealthDrawer extends ConsumerWidget { ], ), ), - _DrawerItem(icon: Icons.settings, label: '设置', onTap: () => context.push('/settings')), + _DrawerItem(icon: Icons.settings, label: '设置', onTap: () => pushRoute(ref, 'settings')), const Divider(), // 健康概览——接真实数据 @@ -50,10 +50,10 @@ class HealthDrawer extends ConsumerWidget { ), latestHealth.when( data: (data) => Column(children: [ - _HealthMetric(icon: Icons.favorite, label: '血压', value: _bpText(data['BloodPressure']), onTap: () => context.push('/trend/blood_pressure')), - _HealthMetric(icon: Icons.monitor_heart, label: '心率', value: _metricText(data['HeartRate'], '次/分'), onTap: () => context.push('/trend/heart_rate')), - _HealthMetric(icon: Icons.bloodtype, label: '血糖', value: _metricText(data['Glucose'], 'mmol/L'), onTap: () => context.push('/trend/glucose')), - _HealthMetric(icon: Icons.air, label: '血氧', value: _metricText(data['SpO2'], '%'), onTap: () => context.push('/trend/spo2')), + _HealthMetric(icon: Icons.favorite, label: '血压', value: _bpText(data['BloodPressure']), onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'})), + _HealthMetric(icon: Icons.monitor_heart, label: '心率', value: _metricText(data['HeartRate'], '次/分'), onTap: () => pushRoute(ref, 'trend', params: {'type': 'heart_rate'})), + _HealthMetric(icon: Icons.bloodtype, label: '血糖', value: _metricText(data['Glucose'], 'mmol/L'), onTap: () => pushRoute(ref, 'trend', params: {'type': 'glucose'})), + _HealthMetric(icon: Icons.air, label: '血氧', value: _metricText(data['SpO2'], '%'), onTap: () => pushRoute(ref, 'trend', params: {'type': 'spo2'})), ]), loading: () => const Padding(padding: EdgeInsets.all(16), child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))), error: (_, _) => Column(children: [ @@ -76,7 +76,7 @@ class HealthDrawer extends ConsumerWidget { final ok = await showDialog(context: context, builder: (ctx) => AlertDialog( title: const Text('退出登录'), content: const Text('确定退出?'), actions: [TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定'))])); - if (ok == true) { await ref.read(authProvider.notifier).logout(); if (context.mounted) context.go('/login'); } + if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); } }), ], ), diff --git a/health_app/pubspec.lock b/health_app/pubspec.lock index c0f78e8..a0dac9d 100644 --- a/health_app/pubspec.lock +++ b/health_app/pubspec.lock @@ -65,14 +65,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85" - url: "https://pub.dev" - source: hosted - version: "1.2.0" collection: dependency: transitive description: @@ -254,54 +246,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" - flutter_secure_storage: - dependency: "direct main" - description: - name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" - url: "https://pub.dev" - source: hosted - version: "9.2.4" - flutter_secure_storage_linux: - dependency: transitive - description: - name: flutter_secure_storage_linux - sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 - url: "https://pub.dev" - source: hosted - version: "1.2.3" - flutter_secure_storage_macos: - dependency: transitive - description: - name: flutter_secure_storage_macos - sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" - url: "https://pub.dev" - source: hosted - version: "3.1.3" - flutter_secure_storage_platform_interface: - dependency: transitive - description: - name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_secure_storage_web: - dependency: transitive - description: - name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - flutter_secure_storage_windows: - dependency: transitive - description: - name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 - url: "https://pub.dev" - source: hosted - version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -328,22 +272,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" - go_router: - dependency: "direct main" - description: - name: go_router - sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 - url: "https://pub.dev" - source: hosted - version: "14.8.1" - hooks: - dependency: transitive - description: - name: hooks - sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31 - url: "https://pub.dev" - source: hosted - version: "2.0.0" http: dependency: transitive description: @@ -440,22 +368,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - jni: - dependency: transitive - description: - name: jni - sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f - url: "https://pub.dev" - source: hosted - version: "1.0.0" - jni_flutter: - dependency: transitive - description: - name: jni_flutter - sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" js: dependency: transitive description: @@ -544,14 +456,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" - url: "https://pub.dev" - source: hosted - version: "9.4.1" package_config: dependency: transitive description: @@ -561,61 +465,13 @@ packages: source: hosted version: "2.2.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" petitparser: dependency: transitive description: @@ -656,14 +512,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - record_use: - dependency: transitive - description: - name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" - url: "https://pub.dev" - source: hosted - version: "0.6.0" riverpod: dependency: transitive description: @@ -733,6 +581,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a" + url: "https://pub.dev" + source: hosted + version: "2.4.2+1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" + url: "https://pub.dev" + source: hosted + version: "2.4.2+3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465" + url: "https://pub.dev" + source: hosted + version: "2.5.8" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -765,6 +653,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -869,14 +765,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" xml: dependency: transitive description: @@ -895,4 +783,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" + flutter: ">=3.38.0" diff --git a/health_app/pubspec.yaml b/health_app/pubspec.yaml index 3295c83..571c214 100644 --- a/health_app/pubspec.yaml +++ b/health_app/pubspec.yaml @@ -16,11 +16,9 @@ dependencies: # HTTP 网络 dio: ^5.4.0 - # 安全存储 - flutter_secure_storage: ^9.2.0 - - # 路由 - go_router: ^14.0.0 + # 本地数据库 + sqflite: ^2.4.0 + path: ^1.9.0 # 图表 fl_chart: ^0.68.0