This commit is contained in:
fuenmao
2026-09-22 15:01:29 +08:00
parent a063be61cd
commit c1c86b56b6
5 changed files with 797 additions and 45 deletions
@@ -104,15 +104,151 @@ public sealed class HealthAppService : IDynamicApiController, ITransient
.OrderByDescending(item => item.Version)
.FirstAsync();
var hasCompletedAssessment = profile != null && !string.IsNullOrWhiteSpace(profile.AssessmentJson);
decimal? bmi = null;
var bmiStatus = string.Empty;
if (profile?.HeightCm > 0 && profile.WeightKg > 0)
{
var h = profile.HeightCm.Value / 100m;
bmi = Math.Round(profile.WeightKg.Value / (h * h), 1);
bmiStatus = bmi < 18.5m ? "偏瘦" : bmi <= 23.9m ? "正常" : bmi <= 27.9m ? "超重" : "肥胖";
}
PersonalizedSceneConfig? sceneConfig = null;
if (!string.IsNullOrWhiteSpace(profile?.CustomSceneConfigJson))
{
try
{
sceneConfig = JsonSerializer.Deserialize<PersonalizedSceneConfig>(profile.CustomSceneConfigJson);
}
catch
{
}
}
if (sceneConfig == null && hasCompletedAssessment)
{
var answers = JsonSerializer.Deserialize<HealthAssessmentAnswers>(profile!.AssessmentJson!) ?? new HealthAssessmentAnswers();
sceneConfig = CalculatePersonalizedSceneConfig(answers, MapProfile(profile));
}
return new HealthDashboardOutput
{
User = MapUser(user),
Profile = profile is null ? null : MapProfile(profile),
SelectedModules = modules,
CurrentPlan = plan is null ? null : MapPlan(plan)
CurrentPlan = plan is null ? null : MapPlan(plan),
HasCompletedAssessment = hasCompletedAssessment,
Bmi = bmi,
BmiStatus = bmiStatus,
SceneConfig = sceneConfig
};
}
[Authorize]
[HttpGet("/api/health/assessment")]
public async Task<HealthAssessmentOutput> GetAssessment()
{
var userId = GetCurrentUserId();
var profile = await _profileRepository.GetFirstAsync(p => p.UserId == userId);
if (profile == null)
{
return new HealthAssessmentOutput
{
HasCompletedAssessment = false,
BasicInfo = new SaveHealthProfileInput(),
Answers = new HealthAssessmentAnswers(),
SceneConfig = new PersonalizedSceneConfig()
};
}
var answers = string.IsNullOrWhiteSpace(profile.AssessmentJson)
? new HealthAssessmentAnswers()
: JsonSerializer.Deserialize<HealthAssessmentAnswers>(profile.AssessmentJson) ?? new HealthAssessmentAnswers();
var hasCompleted = !string.IsNullOrWhiteSpace(profile.AssessmentJson);
var sceneConfig = string.IsNullOrWhiteSpace(profile.CustomSceneConfigJson)
? CalculatePersonalizedSceneConfig(answers, MapProfile(profile))
: JsonSerializer.Deserialize<PersonalizedSceneConfig>(profile.CustomSceneConfigJson) ?? new PersonalizedSceneConfig();
decimal? bmi = null;
var bmiStatus = "正常";
if (profile.HeightCm > 0 && profile.WeightKg > 0)
{
var h = profile.HeightCm.Value / 100m;
bmi = Math.Round(profile.WeightKg.Value / (h * h), 1);
bmiStatus = bmi < 18.5m ? "偏瘦" : bmi <= 23.9m ? "正常" : bmi <= 27.9m ? "超重" : "肥胖";
}
return new HealthAssessmentOutput
{
HasCompletedAssessment = hasCompleted,
BasicInfo = MapProfile(profile),
Answers = answers,
SceneConfig = sceneConfig,
Bmi = bmi,
BmiStatus = bmiStatus
};
}
[Authorize]
[HttpPost("/api/health/assessment")]
[UnitOfWork]
public async Task<HealthAssessmentOutput> SaveAssessment(SaveHealthAssessmentInput input)
{
ValidateAssessment(input);
var userId = GetCurrentUserId();
var profile = await _profileRepository.GetFirstAsync(p => p.UserId == userId);
var calculatedConfig = CalculatePersonalizedSceneConfig(input.Answers, input.BasicInfo);
if (profile == null)
{
profile = new HealthProfile { UserId = userId };
ApplyProfile(profile, input.BasicInfo);
profile.AssessmentJson = JsonSerializer.Serialize(input.Answers);
profile.CustomSceneConfigJson = JsonSerializer.Serialize(calculatedConfig);
await _profileRepository.InsertAsync(profile);
}
else
{
ApplyProfile(profile, input.BasicInfo);
profile.AssessmentJson = JsonSerializer.Serialize(input.Answers);
profile.CustomSceneConfigJson = JsonSerializer.Serialize(calculatedConfig);
await _profileRepository.UpdateAsync(profile);
}
var modules = new List<string> { "sleep", "exercise", "diet" };
var existingModules = await _moduleRepository.AsQueryable().Where(m => m.UserId == userId).ToListAsync();
foreach (var mod in modules)
{
var existing = existingModules.FirstOrDefault(m => m.ModuleCode == mod);
if (existing == null)
{
await _moduleRepository.InsertAsync(new HealthModuleSelection { UserId = userId, ModuleCode = mod, IsEnabled = true });
}
else if (!existing.IsEnabled)
{
existing.IsEnabled = true;
await _moduleRepository.UpdateAsync(existing);
}
}
var maxVersion = await _planRepository.AsQueryable().Where(p => p.UserId == userId).MaxAsync(p => (int?)p.Version) ?? 0;
await _planRepository.UpdateAsync(p => new HealthPlan { IsCurrent = false }, p => p.UserId == userId && p.IsCurrent);
var planItems = BuildPlanItems(profile, modules);
var plan = new HealthPlan
{
UserId = userId,
Version = maxVersion + 1,
Title = $"个性化健康方案 v{maxVersion + 1}.0",
Summary = BuildAssessmentPlanSummary(profile, input.Answers),
ParametersJson = JsonSerializer.Serialize(planItems),
IsCurrent = true
};
await _planRepository.InsertAsync(plan);
return await GetAssessment();
}
[Authorize]
[HttpPut("/api/health/profile")]
public async Task<SaveHealthProfileInput> SaveProfile(SaveHealthProfileInput input)
@@ -328,4 +464,200 @@ public sealed class HealthAppService : IDynamicApiController, ITransient
User = MapUser(user)
};
}
private static void ValidateAssessment(SaveHealthAssessmentInput input)
{
var info = input.BasicInfo ?? throw Oops.Oh("请填写个人基本信息");
var answers = input.Answers ?? throw Oops.Oh("请完成健康评估问卷");
if (info.Gender is not ("男" or "女")) throw Oops.Oh("请选择性别");
if (info.Birthday is null) throw Oops.Oh("请填写年龄");
var age = DateTime.Today.Year - info.Birthday.Value.Year;
if (info.Birthday.Value.Date > DateTime.Today.AddYears(-age)) age--;
if (age is < 1 or > 120) throw Oops.Oh("年龄应在 1 到 120 岁之间");
if (info.HeightCm is null or < 80 or > 250) throw Oops.Oh("身高应在 80 到 250cm 之间");
if (info.WeightKg is null or < 20 or > 300) throw Oops.Oh("体重应在 20 到 300kg 之间");
RequireChoice(answers.SleepDuration, "每晚平均睡眠时长", "<6", "6-7", "7-8", ">8");
RequireChoice(answers.FallingAsleepSpeed, "入睡耗时", "hard", "normal", "fast");
RequireChoice(answers.TemperaturePreference, "温度偏好", "heat_fear", "cold_fear", "normal");
RequireChoice(answers.NightAwakening, "夜醒情况", "frequent", "light", "none");
RequireChoice(answers.MattressPreference, "床垫偏好", "firm", "medium", "soft");
RequireChoice(answers.ExerciseFrequency, "运动频率", "none", "1-2", "3-5", "daily");
RequireChoice(answers.MuscleFatigueArea, "疲劳部位", "back_neck", "waist_back", "legs", "none");
RequireChoice(answers.ActiveTimeOfDay, "活跃时段", "morning", "evening", "night", "irregular");
RequireChoice(answers.MealRegularity, "三餐规律", "regular", "skip_breakfast", "irregular");
RequireChoice(answers.FlavorPreference, "口味偏好", "light", "moderate", "heavy");
RequireChoice(answers.LateNightEating, "夜宵频率", "often", "sometimes", "rarely");
RequireChoice(answers.DailyWaterIntake, "饮水情况", "abundant", "moderate", "scarce");
}
private static void RequireChoice(string value, string label, params string[] allowed)
{
if (string.IsNullOrWhiteSpace(value) || !allowed.Contains(value, StringComparer.Ordinal))
throw Oops.Oh($"请完成“{label}”问题");
}
private static PersonalizedSceneConfig CalculatePersonalizedSceneConfig(HealthAssessmentAnswers answers, SaveHealthProfileInput basicInfo)
{
var config = new PersonalizedSceneConfig();
switch (answers.TemperaturePreference)
{
case "heat_fear":
config.TargetHumidity = 48;
config.FanLevel = "2挡";
config.MattressTargetTemp = 32m;
config.MattressHeatingLevel = 1;
break;
case "cold_fear":
config.TargetHumidity = 58;
config.FanLevel = "1挡";
config.MattressTargetTemp = 37.5m;
config.MattressHeatingLevel = 3;
break;
default:
config.TargetHumidity = 52;
config.FanLevel = "1挡";
config.MattressTargetTemp = 35.5m;
config.MattressHeatingLevel = 2;
break;
}
switch (answers.FallingAsleepSpeed)
{
case "hard":
config.LightBrightnessPercent = 8;
config.LightColorTemperatureKelvin = 2200;
config.FragranceLevel = 2;
config.EnableSpeakerSleepMode = true;
config.SpeakerAudioMode = "助眠白噪音";
config.SpeakerVolumePercent = 12;
break;
case "fast":
config.LightBrightnessPercent = 25;
config.LightColorTemperatureKelvin = 3000;
config.EnableSpeakerSleepMode = false;
config.SpeakerAudioMode = "关闭助眠音频";
config.SpeakerVolumePercent = 10;
break;
default:
config.LightBrightnessPercent = 18;
config.LightColorTemperatureKelvin = 2700;
config.FragranceLevel = 1;
config.EnableSpeakerSleepMode = true;
config.SpeakerAudioMode = "舒缓轻音乐";
config.SpeakerVolumePercent = 15;
break;
}
switch (answers.SleepDuration)
{
case "<6":
config.LightBrightnessPercent -= 3;
config.SpeakerVolumePercent += 3;
config.FragranceLevel++;
break;
case "6-7":
config.LightBrightnessPercent -= 1;
config.SpeakerVolumePercent += 1;
break;
case ">8":
config.LightBrightnessPercent += 2;
config.SpeakerVolumePercent -= 2;
break;
}
if (answers.MuscleFatigueArea is "waist_back" or "back_neck" || answers.MattressPreference == "firm")
{
config.MattressLeftFirmness = 7;
config.MattressRightFirmness = 7;
config.MattressMode = "智能腰托";
}
else if (answers.MattressPreference == "soft")
{
config.MattressLeftFirmness = 4;
config.MattressRightFirmness = 4;
config.MattressMode = "瑜伽助眠";
}
else
{
config.MattressLeftFirmness = answers.MuscleFatigueArea == "legs" ? 6 : 5;
config.MattressRightFirmness = answers.MuscleFatigueArea == "legs" ? 6 : 5;
config.MattressMode = "智能适配";
}
config.DehumidifierMode = answers.NightAwakening == "none" ? "智能" : "睡眠";
if (answers.NightAwakening == "frequent")
{
config.FanLevel = "1挡";
config.LightBrightnessPercent -= 2;
}
switch (answers.ExerciseFrequency)
{
case "none": config.MattressHeatingLevel++; break;
case "daily": config.MattressHeatingLevel--; break;
}
switch (answers.ActiveTimeOfDay)
{
case "morning": config.LightColorTemperatureKelvin += 150; break;
case "night":
config.LightColorTemperatureKelvin -= 250;
config.LightBrightnessPercent -= 2;
break;
case "irregular":
config.EnableSpeakerSleepMode = true;
config.SpeakerVolumePercent += 2;
break;
}
config.EnableUv = answers.LateNightEating is "often" or "sometimes" || answers.FlavorPreference == "heavy";
if (answers.MealRegularity == "irregular")
{
config.EnableUv = true;
config.FragranceLevel++;
}
else if (answers.MealRegularity == "skip_breakfast")
{
config.FragranceLevel++;
}
if (answers.DailyWaterIntake == "scarce") config.TargetHumidity += 3;
if (answers.DailyWaterIntake == "abundant") config.TargetHumidity -= 2;
if (basicInfo.HeightCm > 0 && basicInfo.WeightKg > 0)
{
var height = basicInfo.HeightCm.Value / 100m;
var bmi = basicInfo.WeightKg.Value / (height * height);
if (bmi < 18.5m) config.MattressHeatingLevel++;
if (bmi >= 28m) config.TargetHumidity -= 2;
}
if (basicInfo.Birthday.HasValue)
{
var age = DateTime.Today.Year - basicInfo.Birthday.Value.Year;
if (age >= 60)
{
config.LightBrightnessPercent += 3;
config.SpeakerVolumePercent = Math.Max(8, config.SpeakerVolumePercent - 2);
}
}
config.TargetHumidity = Math.Clamp(config.TargetHumidity, 40, 70);
config.LightBrightnessPercent = Math.Clamp(config.LightBrightnessPercent, 1, 100);
config.LightColorTemperatureKelvin = Math.Clamp(config.LightColorTemperatureKelvin, 2200, 5000);
config.SpeakerVolumePercent = Math.Clamp(config.SpeakerVolumePercent, 1, 50);
config.FragranceLevel = Math.Clamp(config.FragranceLevel, 1, 3);
config.MattressHeatingLevel = Math.Clamp(config.MattressHeatingLevel, 0, 4);
return config;
}
private static string BuildAssessmentPlanSummary(HealthProfile profile, HealthAssessmentAnswers answers)
{
var sleepNote = answers.FallingAsleepSpeed == "hard" ? "针对入睡耗时较长启用了音箱睡眠模式与极暗暖光;" : "";
var tempNote = answers.TemperaturePreference == "heat_fear" ? "依据体感偏热降低了床垫加热档位与室内湿度;" : answers.TemperaturePreference == "cold_fear" ? "依据体感偏冷提高了床垫保温档位;" : "";
var supportNote = answers.MuscleFatigueArea is "waist_back" or "back_neck" ? "针对腰背与颈部疲劳启用了床垫智能腰托模式;" : "";
return $"已完成全面健康评估,{sleepNote}{tempNote}{supportNote}全套智能家具启动参数已完成个性化配置。";
}
}