using Evergreen.Health.Application.Health.Entities; using Evergreen.Health.Application.Health.Models; using Evergreen.Health.Application.Health.Security; using Furion.DataEncryption; using Furion.DatabaseAccessor; using Furion.FriendlyException; using Microsoft.AspNetCore.Http; using SqlSugar; using System.Security.Claims; using System.Text.Json; namespace Evergreen.Health.Application.Health.Services; [ApiDescriptionSettings("健康管理", Name = "Health", Order = 50)] public sealed class HealthAppService : IDynamicApiController, ITransient { private const string HealthUserIdClaim = "health_user_id"; private static readonly HashSet SupportedModules = new(StringComparer.OrdinalIgnoreCase) { "sleep", "exercise", "diet" }; private readonly SqlSugarRepository _userRepository; private readonly SqlSugarRepository _profileRepository; private readonly SqlSugarRepository _moduleRepository; private readonly SqlSugarRepository _planRepository; private readonly IHttpContextAccessor _httpContextAccessor; public HealthAppService( SqlSugarRepository userRepository, SqlSugarRepository profileRepository, SqlSugarRepository moduleRepository, SqlSugarRepository planRepository, IHttpContextAccessor httpContextAccessor) { _userRepository = userRepository; _profileRepository = profileRepository; _moduleRepository = moduleRepository; _planRepository = planRepository; _httpContextAccessor = httpContextAccessor; } [AllowAnonymous] [HttpPost("/api/health/auth/register")] [UnitOfWork] public async Task Register(HealthRegisterInput input) { var phone = input.Phone.Trim(); if (await _userRepository.IsAnyAsync(user => user.Phone == phone && !user.IsDelete)) throw Oops.Oh("该手机号已经注册"); var user = await _userRepository.AsInsertable(new HealthUser { Phone = phone, DisplayName = input.DisplayName.Trim(), PasswordHash = HealthPasswordHasher.Hash(input.Password), Status = HealthUserStatus.Enabled }).ExecuteReturnEntityAsync(); await _profileRepository.InsertAsync(new HealthProfile { UserId = user.Id }); await _moduleRepository.InsertRangeAsync(new List { new HealthModuleSelection { UserId = user.Id, ModuleCode = "sleep", IsEnabled = true }, new HealthModuleSelection { UserId = user.Id, ModuleCode = "exercise", IsEnabled = true }, new HealthModuleSelection { UserId = user.Id, ModuleCode = "diet", IsEnabled = true } }); return CreateAuthOutput(user); } [AllowAnonymous] [HttpPost("/api/health/auth/login")] public async Task Login(HealthLoginInput input) { var phone = input.Phone.Trim(); var user = await _userRepository.GetFirstAsync(item => item.Phone == phone && !item.IsDelete); if (user is null || !HealthPasswordHasher.Verify(input.Password, user.PasswordHash)) throw Oops.Oh("手机号或密码错误").StatusCode(401); if (user.Status != HealthUserStatus.Enabled) throw Oops.Oh("账号已停用").StatusCode(403); user.LastLoginTime = DateTime.Now; user.LastLoginIp = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(); await _userRepository.AsUpdateable(user) .UpdateColumns(item => new { item.LastLoginTime, item.LastLoginIp }) .ExecuteCommandAsync(); return CreateAuthOutput(user); } [Authorize] [HttpGet("/api/health/dashboard")] public async Task GetDashboard() { var userId = GetCurrentUserId(); var user = await GetCurrentUser(userId); var profile = await _profileRepository.GetFirstAsync(item => item.UserId == userId); var modules = await _moduleRepository.AsQueryable() .Where(item => item.UserId == userId && item.IsEnabled) .Select(item => item.ModuleCode) .ToListAsync(); var plan = await _planRepository.AsQueryable() .Where(item => item.UserId == userId && item.IsCurrent) .OrderByDescending(item => item.Version) .FirstAsync(); return new HealthDashboardOutput { User = MapUser(user), Profile = profile is null ? null : MapProfile(profile), SelectedModules = modules, CurrentPlan = plan is null ? null : MapPlan(plan) }; } [Authorize] [HttpPut("/api/health/profile")] public async Task SaveProfile(SaveHealthProfileInput input) { var userId = GetCurrentUserId(); var profile = await _profileRepository.GetFirstAsync(item => item.UserId == userId); if (profile is null) { profile = new HealthProfile { UserId = userId }; ApplyProfile(profile, input); await _profileRepository.InsertAsync(profile); } else { ApplyProfile(profile, input); await _profileRepository.UpdateAsync(profile); } return MapProfile(profile); } [Authorize] [HttpPut("/api/health/modules")] [UnitOfWork] public async Task> SaveModules(SaveHealthModulesInput input) { var userId = GetCurrentUserId(); var modules = input.ModuleCodes .Select(item => item.Trim().ToLowerInvariant()) .Where(SupportedModules.Contains) .Distinct() .ToList(); if (modules.Count == 0) throw Oops.Oh("请至少选择一个健康板块"); await _moduleRepository.DeleteAsync(item => item.UserId == userId); await _moduleRepository.InsertRangeAsync(modules.Select(module => new HealthModuleSelection { UserId = userId, ModuleCode = module, IsEnabled = true }).ToList()); return modules; } [Authorize] [HttpPost("/api/health/plans/generate")] [UnitOfWork] public async Task GeneratePlan() { var userId = GetCurrentUserId(); var profile = await _profileRepository.GetFirstAsync(item => item.UserId == userId); var modules = await _moduleRepository.AsQueryable() .Where(item => item.UserId == userId && item.IsEnabled) .Select(item => item.ModuleCode) .ToListAsync(); if (modules.Count == 0) throw Oops.Oh("请先选择健康板块"); var items = BuildPlanItems(profile, modules); var currentVersion = await _planRepository.AsQueryable() .Where(item => item.UserId == userId) .MaxAsync(item => (int?)item.Version) ?? 0; await _planRepository.Context.Updateable() .SetColumns(item => item.IsCurrent == false) .Where(item => item.UserId == userId && item.IsCurrent) .ExecuteCommandAsync(); var plan = await _planRepository.AsInsertable(new HealthPlan { UserId = userId, Version = currentVersion + 1, Title = "您的专属健康方案", Summary = BuildSummary(profile, modules), ParametersJson = JsonSerializer.Serialize(items), IsCurrent = true }).ExecuteReturnEntityAsync(); return MapPlan(plan); } private long GetCurrentUserId() { var value = _httpContextAccessor.HttpContext?.User.FindFirstValue(HealthUserIdClaim); if (!long.TryParse(value, out var userId)) throw Oops.Oh("登录状态已失效").StatusCode(401); return userId; } private async Task GetCurrentUser(long userId) { return await _userRepository.GetFirstAsync(item => item.Id == userId && !item.IsDelete) ?? throw Oops.Oh("用户不存在").StatusCode(401); } private static void ApplyProfile(HealthProfile profile, SaveHealthProfileInput input) { profile.Birthday = input.Birthday; profile.Gender = input.Gender?.Trim(); profile.HeightCm = input.HeightCm; profile.WeightKg = input.WeightKg; profile.AverageSleepHours = input.AverageSleepHours; profile.DailyExerciseMinutes = input.DailyExerciseMinutes; profile.DietPreference = input.DietPreference?.Trim(); profile.HealthGoal = input.HealthGoal?.Trim(); } private static SaveHealthProfileInput MapProfile(HealthProfile profile) => new() { Birthday = profile.Birthday, Gender = profile.Gender, HeightCm = profile.HeightCm, WeightKg = profile.WeightKg, AverageSleepHours = profile.AverageSleepHours, DailyExerciseMinutes = profile.DailyExerciseMinutes, DietPreference = profile.DietPreference, HealthGoal = profile.HealthGoal }; private static HealthUserOutput MapUser(HealthUser user) => new() { Id = user.Id, Phone = user.Phone, DisplayName = user.DisplayName, AvatarUrl = user.AvatarUrl }; private static List BuildPlanItems(HealthProfile? profile, IEnumerable modules) { var items = new List(); foreach (var module in modules) { switch (module) { case "sleep": items.Add(new HealthPlanItemOutput { ModuleCode = module, Title = "睡眠质量", Target = profile?.AverageSleepHours is < 7 ? "逐步达到每晚 7–8 小时" : "保持每晚 7–8 小时", Advice = "固定入睡时间,睡前一小时降低灯光亮度,并使用卧室舒睡场景。" }); break; case "exercise": items.Add(new HealthPlanItemOutput { ModuleCode = module, Title = "运动情况", Target = profile?.DailyExerciseMinutes is >= 30 ? "保持每日 30 分钟以上活动" : "从每日 20 分钟步行开始", Advice = "优先安排低到中等强度运动,每坐一小时起身活动 3–5 分钟。" }); break; case "diet": items.Add(new HealthPlanItemOutput { ModuleCode = module, Title = "饮食情况", Target = "三餐规律,晚餐不过量", Advice = string.IsNullOrWhiteSpace(profile?.DietPreference) ? "记录三餐与饮水情况,优先增加蔬菜和优质蛋白。" : $"结合“{profile.DietPreference}”偏好控制总量,并保持食物多样性。" }); break; } } return items; } private static string BuildSummary(HealthProfile? profile, IReadOnlyCollection modules) { var bmiText = string.Empty; if (profile?.HeightCm > 0 && profile.WeightKg > 0) { var height = profile.HeightCm.Value / 100m; var bmi = Math.Round(profile.WeightKg.Value / (height * height), 1); bmiText = $" 当前 BMI 约为 {bmi}。"; } return $"方案依据您选择的 {modules.Count} 个健康板块生成。{bmiText}第一版参数用于健康管理演示,不替代专业医疗诊断。"; } private static HealthPlanOutput MapPlan(HealthPlan plan) => new() { Id = plan.Id, Version = plan.Version, Title = plan.Title, Summary = plan.Summary, CreateTime = plan.CreateTime, Items = JsonSerializer.Deserialize>(plan.ParametersJson) ?? [] }; private static HealthAuthOutput CreateAuthOutput(HealthUser user) { const int accessTokenMinutes = 24 * 60; const int refreshTokenMinutes = 7 * 24 * 60; var accessToken = JWTEncryption.Encrypt(new Dictionary { [HealthUserIdClaim] = user.Id, [ClaimTypes.NameIdentifier] = user.Id, [ClaimTypes.Name] = user.DisplayName, [ClaimTypes.MobilePhone] = user.Phone, ["account_type"] = "health_user" }, accessTokenMinutes); return new HealthAuthOutput { AccessToken = accessToken, RefreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenMinutes), User = MapUser(user) }; } }