111
This commit is contained in:
@@ -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}全套智能家具启动参数已完成个性化配置。";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user