Files
Evergreen-Health/Evergreen.Health.Application/Health/Services/HealthTcmAssessmentService.cs
T
2026-09-23 18:19:40 +08:00

199 lines
9.7 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 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 { return JsonSerializer.Deserialize<TcmAssessmentResult>(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<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, 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
}
};
}
}