Files
Evergreen-Health/Evergreen.Health.Application/Health/Services/HealthTcmAssessmentService.cs
T
2026-10-09 16:46:30 +08:00

224 lines
11 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.Policies;
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<HealthProfile> _profiles;
private readonly IHttpContextAccessor _http;
public HealthTcmAssessmentService(SqlSugarRepository<HealthProfile> profiles, IHttpContextAccessor http)
{
_profiles = profiles;
_http = http;
}
[Authorize]
[HttpGet("/api/health/tcm-assessment")]
public async Task<TcmAssessmentResult> 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<TcmAssessmentResult> 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<string, string> Names = new()
{
["balanced"] = "平和质", ["qi_deficient"] = "气虚质", ["yang_deficient"] = "阳虚质",
["yin_deficient"] = "阴虚质", ["phlegm_damp"] = "痰湿质", ["damp_heat"] = "湿热质",
["blood_stasis"] = "血瘀质", ["qi_stagnation"] = "气郁质", ["special"] = "特禀质"
};
private static readonly Dictionary<string, string[]> 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<string, (string Type, int Points)[]> 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
{
var result = JsonSerializer.Deserialize<TcmAssessmentResult>(profile.TcmResultJson) ?? new TcmAssessmentResult();
if (result.HasCompleted)
{
result.MusicRecommendation = MusicFor(result.ConstitutionCode);
result.SceneConfig ??= new PersonalizedSceneConfig();
result.SceneConfig.SpeakerVolumePercent = 50;
result.RoomTemperatureRecommendation = RecommendedRoomRange.ForConstitution(result.ConstitutionCode).Display;
}
return result;
}
catch { return new TcmAssessmentResult(); }
}
private static string MusicFor(string constitutionCode) => constitutionCode switch
{
"balanced" or "yang_deficient" => "自然雨声",
"qi_deficient" => "轻音乐",
"yin_deficient" => "海浪白噪音",
"phlegm_damp" => "规律鸟鸣白噪音",
"damp_heat" => "自然流水声",
"blood_stasis" => "低频舒缓音乐",
"qi_stagnation" => "冥想古琴曲",
"special" => "田野风声",
_ => "自然雨声"
};
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<string, string>())
.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) = primary switch
{
"qi_deficient" => ("清淡木香", "轻音乐", "22–25℃", 55, 3000, true),
"yang_deficient" => ("温暖木质香", "自然雨声", "24–26℃", 52, 2700, true),
"yin_deficient" => ("淡菊花香", "海浪白噪音", "21–24℃", 48, 4000, true),
"phlegm_damp" => ("清新柑橘香", "规律鸟鸣白噪音", "22–24℃", 45, 3500, true),
"damp_heat" => ("清淡薄荷香", "自然流水声", "21–24℃", 45, 4000, true),
"blood_stasis" => ("淡花木香", "低频舒缓音乐", "23–25℃", 52, 3000, true),
"qi_stagnation" => ("清新柑橘香", "冥想古琴曲", "22–25℃", 51, 3000, true),
"special" => ("建议不使用香氛,避免过敏原", "田野风声", "22–25℃", 50, 3500, false),
_ => ("淡雅薰衣草香", "自然雨声", "22–25℃", 50, 3000, true)
};
return new TcmAssessmentResult
{
HasCompleted = true,
ConstitutionCode = primary,
ConstitutionName = Names[primary],
Summary = $"根据本次望闻问切选择,演示规则提示您偏向{Names[primary]}。仅作健康生活方式参考,不作为诊断。",
AromaRecommendation = input.Answers["allergy"] == "often"
? "建议不使用香氛,避免过敏原" : aroma,
MusicRecommendation = music,
RoomTemperatureRecommendation = RecommendedRoomRange.ForConstitution(primary).Display,
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 = 50
}
};
}
}