This commit is contained in:
fuenmao
2026-09-23 18:19:40 +08:00
parent db7bd2231a
commit 6a622c9bd5
8 changed files with 534 additions and 247 deletions
@@ -0,0 +1,198 @@
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
}
};
}
}