feat: initialize Evergreen Health backend
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
using SqlSugar;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Evergreen.Health.Application.Health.Entities;
|
||||
|
||||
public enum HealthUserStatus { Disabled = 0, Enabled = 1 }
|
||||
|
||||
[SugarTable("health_user", "健康端用户")]
|
||||
[SugarIndex("idx_health_user_phone", nameof(Phone), OrderByType.Asc, IsUnique = true)]
|
||||
public sealed class HealthUser : EntityBaseDel
|
||||
{
|
||||
[SugarColumn(ColumnDescription = "手机号", Length = 20)]
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "密码摘要", Length = 256)]
|
||||
[JsonIgnore, Newtonsoft.Json.JsonIgnore]
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "姓名或昵称", Length = 50)]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "头像", Length = 500, IsNullable = true)]
|
||||
public string? AvatarUrl { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "状态")]
|
||||
public HealthUserStatus Status { get; set; } = HealthUserStatus.Enabled;
|
||||
|
||||
[SugarColumn(ColumnDescription = "最后登录时间", IsNullable = true)]
|
||||
public DateTime? LastLoginTime { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "最后登录IP", Length = 64, IsNullable = true)]
|
||||
public string? LastLoginIp { get; set; }
|
||||
}
|
||||
|
||||
[SugarTable("health_profile", "用户健康档案")]
|
||||
[SugarIndex("idx_health_profile_user", nameof(UserId), OrderByType.Asc, IsUnique = true)]
|
||||
public sealed class HealthProfile : EntityBase
|
||||
{
|
||||
[SugarColumn(ColumnDescription = "健康用户Id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "出生日期", IsNullable = true)]
|
||||
public DateTime? Birthday { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "性别", Length = 20, IsNullable = true)]
|
||||
public string? Gender { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "身高厘米", DecimalDigits = 1, IsNullable = true)]
|
||||
public decimal? HeightCm { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "体重千克", DecimalDigits = 1, IsNullable = true)]
|
||||
public decimal? WeightKg { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "平均睡眠小时", DecimalDigits = 1, IsNullable = true)]
|
||||
public decimal? AverageSleepHours { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "每日运动分钟", IsNullable = true)]
|
||||
public int? DailyExerciseMinutes { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "饮食偏好", Length = 500, IsNullable = true)]
|
||||
public string? DietPreference { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "健康目标", Length = 500, IsNullable = true)]
|
||||
public string? HealthGoal { get; set; }
|
||||
}
|
||||
|
||||
[SugarTable("health_module_selection", "用户健康板块选择")]
|
||||
[SugarIndex("idx_health_module_user_code", nameof(UserId), OrderByType.Asc, nameof(ModuleCode), OrderByType.Asc, IsUnique = true)]
|
||||
public sealed class HealthModuleSelection : EntityBase
|
||||
{
|
||||
[SugarColumn(ColumnDescription = "健康用户Id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "板块编码", Length = 32)]
|
||||
public string ModuleCode { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "是否启用")]
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
}
|
||||
|
||||
[SugarTable("health_plan", "个性化健康方案")]
|
||||
[SugarIndex("idx_health_plan_user", nameof(UserId), OrderByType.Asc)]
|
||||
public sealed class HealthPlan : EntityBase
|
||||
{
|
||||
[SugarColumn(ColumnDescription = "健康用户Id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "方案版本")]
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
[SugarColumn(ColumnDescription = "方案标题", Length = 100)]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "方案摘要", Length = 1000)]
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "参数JSON", ColumnDataType = "text")]
|
||||
public string ParametersJson { get; set; } = "[]";
|
||||
|
||||
[SugarColumn(ColumnDescription = "当前方案")]
|
||||
public bool IsCurrent { get; set; } = true;
|
||||
}
|
||||
|
||||
[SugarTable("health_device_binding", "健康用户设备绑定")]
|
||||
[SugarIndex("idx_health_device_user_entity", nameof(UserId), OrderByType.Asc, nameof(EntityId), OrderByType.Asc, IsUnique = true)]
|
||||
public sealed class HealthDeviceBinding : EntityBase
|
||||
{
|
||||
[SugarColumn(ColumnDescription = "健康用户Id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "Home Assistant实体Id", Length = 200)]
|
||||
public string EntityId { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "显示名称", Length = 100)]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "设备类型", Length = 50, IsNullable = true)]
|
||||
public string? DeviceType { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "是否启用")]
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
}
|
||||
|
||||
[SugarTable("health_scene_execution", "健康场景执行记录")]
|
||||
[SugarIndex("idx_health_scene_user", nameof(UserId), OrderByType.Asc)]
|
||||
public sealed class HealthSceneExecution : EntityBase
|
||||
{
|
||||
[SugarColumn(ColumnDescription = "健康用户Id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDescription = "场景编码", Length = 50)]
|
||||
public string SceneCode { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "场景名称", Length = 100)]
|
||||
public string SceneName { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "执行状态", Length = 32)]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDescription = "执行详情", ColumnDataType = "text", IsNullable = true)]
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Evergreen.Health.Application.Health.Models;
|
||||
|
||||
public sealed class HealthRegisterInput
|
||||
{
|
||||
[Required, RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "请输入正确的手机号码")]
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
|
||||
[Required, MinLength(8), MaxLength(32)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[Required, MaxLength(50)]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class HealthLoginInput
|
||||
{
|
||||
[Required]
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class HealthAuthOutput
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
public HealthUserOutput User { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class HealthUserOutput
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string? AvatarUrl { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SaveHealthProfileInput
|
||||
{
|
||||
public DateTime? Birthday { get; set; }
|
||||
|
||||
[MaxLength(20)]
|
||||
public string? Gender { get; set; }
|
||||
|
||||
[Range(80, 250)]
|
||||
public decimal? HeightCm { get; set; }
|
||||
|
||||
[Range(20, 300)]
|
||||
public decimal? WeightKg { get; set; }
|
||||
|
||||
[Range(0, 24)]
|
||||
public decimal? AverageSleepHours { get; set; }
|
||||
|
||||
[Range(0, 1440)]
|
||||
public int? DailyExerciseMinutes { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? DietPreference { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? HealthGoal { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SaveHealthModulesInput
|
||||
{
|
||||
[Required, MinLength(1)]
|
||||
public List<string> ModuleCodes { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class HealthPlanItemOutput
|
||||
{
|
||||
public string ModuleCode { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Target { get; set; } = string.Empty;
|
||||
public string Advice { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class HealthPlanOutput
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int Version { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
public DateTime CreateTime { get; set; }
|
||||
public List<HealthPlanItemOutput> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class HealthDashboardOutput
|
||||
{
|
||||
public HealthUserOutput User { get; set; } = new();
|
||||
public SaveHealthProfileInput? Profile { get; set; }
|
||||
public List<string> SelectedModules { get; set; } = [];
|
||||
public HealthPlanOutput? CurrentPlan { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Evergreen.Health.Application.Health.Security;
|
||||
|
||||
internal static class HealthPasswordHasher
|
||||
{
|
||||
private const int Iterations = 120_000;
|
||||
private const int SaltSize = 16;
|
||||
private const int KeySize = 32;
|
||||
private const string Algorithm = "pbkdf2-sha256";
|
||||
|
||||
public static string Hash(string password)
|
||||
{
|
||||
var salt = RandomNumberGenerator.GetBytes(SaltSize);
|
||||
var key = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, KeySize);
|
||||
return "$" + Algorithm + "$" + Iterations + "$" + Convert.ToBase64String(salt) + "$" + Convert.ToBase64String(key);
|
||||
}
|
||||
|
||||
public static bool Verify(string password, string encodedHash)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = encodedHash.Split('$', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 4 || parts[0] != Algorithm || !int.TryParse(parts[1], out var iterations))
|
||||
return false;
|
||||
|
||||
var salt = Convert.FromBase64String(parts[2]);
|
||||
var expected = Convert.FromBase64String(parts[3]);
|
||||
var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, expected.Length);
|
||||
return CryptographicOperations.FixedTimeEquals(actual, expected);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user