Files
Evergreen-Health/Evergreen.Health.Application/Health/Services/HealthAppService.cs
T

332 lines
13 KiB
C#
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.
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<string> SupportedModules = new(StringComparer.OrdinalIgnoreCase)
{
"sleep", "exercise", "diet"
};
private readonly SqlSugarRepository<HealthUser> _userRepository;
private readonly SqlSugarRepository<HealthProfile> _profileRepository;
private readonly SqlSugarRepository<HealthModuleSelection> _moduleRepository;
private readonly SqlSugarRepository<HealthPlan> _planRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
public HealthAppService(
SqlSugarRepository<HealthUser> userRepository,
SqlSugarRepository<HealthProfile> profileRepository,
SqlSugarRepository<HealthModuleSelection> moduleRepository,
SqlSugarRepository<HealthPlan> planRepository,
IHttpContextAccessor httpContextAccessor)
{
_userRepository = userRepository;
_profileRepository = profileRepository;
_moduleRepository = moduleRepository;
_planRepository = planRepository;
_httpContextAccessor = httpContextAccessor;
}
[AllowAnonymous]
[HttpPost("/api/health/auth/register")]
[UnitOfWork]
public async Task<HealthAuthOutput> 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<HealthModuleSelection>
{
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<HealthAuthOutput> 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<HealthDashboardOutput> 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<SaveHealthProfileInput> 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<List<string>> 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<HealthPlanOutput> 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<HealthPlan>()
.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<HealthUser> 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<HealthPlanItemOutput> BuildPlanItems(HealthProfile? profile, IEnumerable<string> modules)
{
var items = new List<HealthPlanItemOutput>();
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<string> 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<List<HealthPlanItemOutput>>(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<string, object>
{
[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)
};
}
}