222 lines
10 KiB
C#
222 lines
10 KiB
C#
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,
|
||
EnableFragrance = fragrance && input.Answers["allergy"] != "often", FragranceLevel = 1,
|
||
EnableSpeakerSleepMode = true, SpeakerVolumePercent = 50
|
||
}
|
||
};
|
||
}
|
||
}
|