using Evergreen.Health.Application.Health.Entities; using Evergreen.Health.Application.Health.Models; 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 = "HealthTcmAssessment", Order = 52)] public sealed class HealthTcmAssessmentService : IDynamicApiController, ITransient { private readonly SqlSugarRepository _profiles; private readonly IHttpContextAccessor _http; public HealthTcmAssessmentService(SqlSugarRepository profiles, IHttpContextAccessor http) { _profiles = profiles; _http = http; } [Authorize] [HttpGet("/api/health/tcm-assessment")] public async Task GetAssessment() { var userId = CurrentUserId(); var profile = await _profiles.GetFirstAsync(p => p.UserId == userId); return TcmConstitutionRules.Read(profile); } [Authorize] [HttpPost("/api/health/tcm-assessment")] [UnitOfWork] public async Task SaveAssessment(TcmAssessmentInput input) { var result = TcmConstitutionRules.Evaluate(input); var userId = CurrentUserId(); var profile = await _profiles.GetFirstAsync(p => p.UserId == userId); if (profile == null) { profile = new HealthProfile { UserId = userId }; profile.TcmAssessmentJson = JsonSerializer.Serialize(input); profile.TcmResultJson = JsonSerializer.Serialize(result); await _profiles.InsertAsync(profile); } else { profile.TcmAssessmentJson = JsonSerializer.Serialize(input); profile.TcmResultJson = JsonSerializer.Serialize(result); await _profiles.AsUpdateable(profile) .UpdateColumns(p => new { p.TcmAssessmentJson, p.TcmResultJson }) .ExecuteCommandAsync(); } return result; } private long CurrentUserId() { var claim = _http.HttpContext?.User.FindFirstValue("health_user_id"); if (!long.TryParse(claim, out var userId)) throw Oops.Oh("登录状态已失效").StatusCode(401); return userId; } } public static class TcmConstitutionRules { private static readonly string[] Types = [ "balanced", "qi_deficient", "yang_deficient", "yin_deficient", "phlegm_damp", "damp_heat", "blood_stasis", "qi_stagnation", "special" ]; private static readonly Dictionary Names = new() { ["balanced"] = "平和质", ["qi_deficient"] = "气虚质", ["yang_deficient"] = "阳虚质", ["yin_deficient"] = "阴虚质", ["phlegm_damp"] = "痰湿质", ["damp_heat"] = "湿热质", ["blood_stasis"] = "血瘀质", ["qi_stagnation"] = "气郁质", ["special"] = "特禀质" }; private static readonly Dictionary Choices = new() { ["face"] = ["rosy", "pale", "sallow", "flushed", "dusky"], ["body"] = ["balanced", "heavy", "thin", "abdominal"], ["spirit"] = ["energetic", "low", "tired"], ["voice"] = ["clear", "normal", "weak"], ["coldHeat"] = ["neutral", "cold", "hot"], ["sweating"] = ["normal", "easy", "little"], ["mouth"] = ["normal", "bland", "bitter", "dry"], ["stool"] = ["normal", "dry", "loose", "sticky"], ["sleep"] = ["good", "difficult", "waking", "dreams"], ["emotion"] = ["steady", "anxious", "depressed", "irritable"], ["pulse"] = ["normal", "wiry", "slippery", "thin", "rough", "deep", "floating", "weak", "unknown"], ["allergy"] = ["none", "often"] }; private static readonly Dictionary Weights = new() { ["face:pale"] = [("qi_deficient", 2), ("yang_deficient", 1)], ["face:sallow"] = [("qi_deficient", 2), ("phlegm_damp", 1)], ["face:flushed"] = [("yin_deficient", 2), ("damp_heat", 1)], ["face:dusky"] = [("blood_stasis", 3)], ["body:heavy"] = [("phlegm_damp", 3)], ["body:thin"] = [("yin_deficient", 2)], ["body:abdominal"] = [("phlegm_damp", 3), ("damp_heat", 1)], ["spirit:low"] = [("qi_deficient", 2)], ["spirit:tired"] = [("qi_deficient", 2), ("yang_deficient", 1)], ["voice:weak"] = [("qi_deficient", 2)], ["coldHeat:cold"] = [("yang_deficient", 4)], ["coldHeat:hot"] = [("yin_deficient", 2), ("damp_heat", 2)], ["sweating:easy"] = [("qi_deficient", 2)], ["sweating:little"] = [("yin_deficient", 1)], ["mouth:bland"] = [("yang_deficient", 1), ("phlegm_damp", 1)], ["mouth:bitter"] = [("damp_heat", 3)], ["mouth:dry"] = [("yin_deficient", 3)], ["stool:dry"] = [("yin_deficient", 2), ("damp_heat", 1)], ["stool:loose"] = [("yang_deficient", 2), ("qi_deficient", 1)], ["stool:sticky"] = [("damp_heat", 3), ("phlegm_damp", 2)], ["sleep:difficult"] = [("qi_stagnation", 2), ("yin_deficient", 1)], ["sleep:waking"] = [("qi_deficient", 1), ("yin_deficient", 1)], ["sleep:dreams"] = [("qi_stagnation", 2)], ["emotion:anxious"] = [("qi_stagnation", 3)], ["emotion:depressed"] = [("qi_stagnation", 3)], ["emotion:irritable"] = [("qi_stagnation", 2), ("damp_heat", 1)], ["pulse:wiry"] = [("qi_stagnation", 2)], ["pulse:slippery"] = [("phlegm_damp", 2), ("damp_heat", 1)], ["pulse:thin"] = [("yin_deficient", 2)], ["pulse:rough"] = [("blood_stasis", 3)], ["pulse:deep"] = [("yang_deficient", 1)], ["pulse:floating"] = [("qi_deficient", 1)], ["pulse:weak"] = [("qi_deficient", 2)], ["allergy:often"] = [("special", 6)] }; public static TcmAssessmentResult Read(HealthProfile? profile) { if (string.IsNullOrWhiteSpace(profile?.TcmResultJson)) return new TcmAssessmentResult(); try { return JsonSerializer.Deserialize(profile.TcmResultJson) ?? new TcmAssessmentResult(); } catch { return new TcmAssessmentResult(); } } public static TcmAssessmentResult Evaluate(TcmAssessmentInput input) { if (input?.Answers == null) throw Oops.Oh("请完成四诊问卷"); foreach (var (field, choices) in Choices) if (!input.Answers.TryGetValue(field, out var answer) || !choices.Contains(answer)) throw Oops.Oh($"四诊项目 {field} 未填写或选项无效"); var scores = Types.ToDictionary(type => type, _ => 0); foreach (var (field, answer) in input.Answers) if (Weights.TryGetValue($"{field}:{answer}", out var weights)) foreach (var (type, points) in weights) scores[type] += points; // 无明显偏颇时才判平和;同分时按固定顺序取值,结果可复现。 var ranked = Types.Skip(1).OrderByDescending(type => scores[type]).ToList(); var primary = scores[ranked[0]] < 3 ? "balanced" : ranked[0]; var notes = (input.Notes ?? new Dictionary()) .Where(item => new[] { "tongue", "odor", "breath", "pulseNote" }.Contains(item.Key)) .ToDictionary(item => item.Key, item => (item.Value ?? "").Trim()[..Math.Min((item.Value ?? "").Trim().Length, 300)]); var (aroma, music, room, humidity, kelvin, fragrance, volume) = primary switch { "qi_deficient" => ("清淡木香", "舒缓自然声", "22–25℃", 55, 3000, true, 14), "yang_deficient" => ("温暖木质香", "轻柔慢节奏", "24–26℃", 52, 2700, true, 14), "yin_deficient" => ("淡菊花香", "轻柔海浪声", "21–24℃", 48, 4000, true, 12), "phlegm_damp" => ("清新柑橘香", "舒缓雨声", "22–24℃", 45, 3500, true, 15), "damp_heat" => ("清淡薄荷香", "流水白噪音", "21–24℃", 45, 4000, true, 12), "blood_stasis" => ("淡花木香", "低频舒缓音乐", "23–25℃", 52, 3000, true, 14), "qi_stagnation" => ("清新柑橘香", "轻柔吉他曲", "22–25℃", 51, 3000, true, 14), "special" => ("建议不使用香氛,避免过敏原", "自然风声", "22–25℃", 50, 3500, false, 12), _ => ("淡雅薰衣草香", "自然雨声", "22–25℃", 50, 3000, true, 15) }; return new TcmAssessmentResult { HasCompleted = true, ConstitutionCode = primary, ConstitutionName = Names[primary], Summary = $"根据本次望闻问切选择,演示规则提示您偏向{Names[primary]}。仅作健康生活方式参考,不作为诊断。", AromaRecommendation = input.Answers["allergy"] == "often" ? "建议不使用香氛,避免过敏原" : aroma, MusicRecommendation = music, RoomTemperatureRecommendation = room, Scores = scores, Answers = Choices.Keys.ToDictionary(key => key, key => input.Answers[key]), Notes = notes, AssessedAt = DateTime.Now, SceneConfig = new PersonalizedSceneConfig { TargetHumidity = humidity, DehumidifierMode = "睡眠", FanLevel = "1档", LightBrightnessPercent = 20, LightColorTemperatureKelvin = kelvin, MattressHeatingLevel = 0, MattressLeftFirmness = 0, MattressRightFirmness = 0, MattressMode = "智能适配", EnableFragrance = fragrance && input.Answers["allergy"] != "often", FragranceLevel = 1, EnableSpeakerSleepMode = true, SpeakerVolumePercent = volume } }; } }