Files
zyt/server/app/api/logic/tcm/GiKnowledge.php
T
2026-06-03 14:20:51 +08:00

388 lines
20 KiB
PHP
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.
<?php
namespace app\api\logic\tcm;
/**
* 霍大夫升糖指数(GI)知识库
*/
class GiKnowledge
{
public const LEVEL_LOW = 'low';
public const LEVEL_MEDIUM = 'medium';
public const LEVEL_HIGH = 'high';
/** @var array<string, array<string, list<string>>> */
private static array $foods = [
self::LEVEL_LOW => [
'五谷类' => ['全蛋面', '荞麦面', '粉丝', '黑米', '黑米粥', '通心粉', '藕粉'],
'蔬菜类' => ['魔芋', '粟米', '大白菜', '黄瓜', '芹菜', '茄子', '青椒', '海带', '金针菇', '香菇', '菠菜', '番茄', '豆芽', '芦笋', '花椰菜', '洋葱', '生菜'],
'豆类' => ['黄豆', '眉豆', '鸡心豆', '豆腐', '豆角', '绿豆', '扁豆', '四季豆'],
'水果类' => ['苹果', '水梨', '橙子', '桃', '提子', '沙田柚', '雪梨', '车厘子', '柚子', '草莓', '樱桃', '金桔', '葡萄'],
'奶类' => ['牛奶', '低脂奶', '脱脂奶', '驼奶粉', '驼奶', '低脂乳酪', '红茶', '优格', '无糖豆浆'],
],
self::LEVEL_MEDIUM => [
'主食类' => ['红米饭', '糙米饭', '西米', '乌冬面', '面包', '麦片', '番薯', '芋头'],
'蔬菜类' => ['薯片', '番茄', '莲藕', '牛蒡'],
'肉类' => ['鱼肉', '鸡肉', '鸭肉', '猪肉', '羊肉', '牛肉', '虾子', '蟹'],
'水果类' => ['木瓜', '提子干', '菠萝', '香蕉', '芒果', '哈密瓜', '奇异果', '柳丁'],
'其他' => ['蔗糖', '蜂蜜', '红酒', '啤酒', '可乐', '咖啡'],
],
self::LEVEL_HIGH => [
'主食类' => ['白饭', '馒头', '油条', '糯米饭', '白面包', '燕麦片', '拉面', '炒饭', '爆米花'],
'肉类加工品' => ['贡丸', '肥肠', '蛋饺'],
'蔬菜类' => ['薯蓉', '南瓜', '焗薯'],
'水果类' => ['西瓜', '荔枝', '龙眼', '凤梨', '枣'],
'其他' => ['葡萄糖', '砂糖', '麦芽糖', '汽水', '柳橙汁', '蜂蜜'],
],
];
/** @var list<array{breakfast:string,drinks:string,lunch:string,dinner:string,tips:string}> */
private static array $mealTemplates = [
[
'breakfast' => '驼奶粉(温水冲服)、玉米碴子粥、煮鸡蛋、拌黄瓜',
'drinks' => '白天多喝温开水;早餐冲驼奶粉,别喝甜饮料、果汁。',
'lunch' => '清炖鲤鱼、豆腐炖白菜、清炒豆角、米饭小半碗',
'dinner' => '玉米面饼一个、瘦肉丝、凉拌茄子',
'tips' => '中午鱼豆肉吃足;早中晚淀粉别重复,一顿一种主食就够。',
],
[
'breakfast' => '驼奶粉(温水冲服)、小米粥、煮鸡蛋、小碟咸菜(少盐)',
'drinks' => '温开水为主;驼奶粉跟早餐一起吃,下午也记得喝水。',
'lunch' => '去皮炖鸡肉、芹菜炒豆干、木耳炒鸡蛋',
'dinner' => '蒸红薯(小半块)、清炒菠菜、豆腐汤',
'tips' => '午餐最讲究蛋白;晚上才吃薯,别跟中午再配大米饭。',
],
[
'breakfast' => '驼奶粉(温水冲服)、豆腐脑(少卤)、煮鸡蛋',
'drinks' => '早餐饮驼奶粉;白天喝白开水,别碰含糖饮料。',
'lunch' => '清炖鸡肉、炖芸豆、番茄炒蛋、贴饼子一个',
'dinner' => '挂面一小碗、清蒸小鱼、拌海带丝',
'tips' => '中午饼配肉豆,蛋白管够;面条放晚上,别三顿都是粥饭。',
],
[
'breakfast' => '驼奶粉(温水冲服)、高粱米粥、茶叶蛋、拌萝卜丝',
'drinks' => '温开水;早餐驼奶粉按说明冲,别加糖。',
'lunch' => '炖牛肉(瘦)、豆腐炒韭菜、清炒油菜、杂豆饭小半碗',
'dinner' => '蒸南瓜(小半碗)、鸡蛋羹、拍黄瓜',
'tips' => '中午肉蛋豆要多吃;南瓜算晚饭主食,别再加馒头稀饭。',
],
[
'breakfast' => '驼奶粉(温水冲服)、棒子面粥、水煮蛋、大拌菜(少油)',
'drinks' => '多喝温开水;驼奶粉放早餐,忌可乐汽水果汁。',
'lunch' => '清炖鲫鱼、番茄炒蛋、炖扁豆、糙米饭小半碗',
'dinner' => '玉米面饼一个、清炒豆芽、豆腐脑',
'tips' => '午饭鱼蛋豆齐上;玉米面放晚上,跟中午米饭分开。',
],
[
'breakfast' => '驼奶粉(温水冲服)、小米粥、煮鸡蛋、拌生菜',
'drinks' => '白天小口多次喝温水;早餐冲好驼奶粉再吃饭。',
'lunch' => '白切鸡(去皮)、炖豆腐、清炒豆角、二米饭小半碗',
'dinner' => '杂面馒头一个、瘦肉炒芹菜、凉拌黄瓜',
'tips' => '中午蛋白打主力;三顿别都喝粥吃面,一顿一种淀粉。',
],
];
public static function summaryText(): string
{
$path = root_path() . 'app/api/logic/tcm/data/gi_huo_summary.txt';
if (is_file($path)) {
$text = trim((string) file_get_contents($path));
if ($text !== '') {
return $text;
}
}
return '低 GI(≤55)优先;中 GI56-69)适量;高 GI(≥70)尽量避免。多选全谷物、蔬菜、豆类,少加工、少精制糖。';
}
/** 农村/口语别名 → 标准名(便于「能不能吃」匹配) */
private static array $aliases = [
'棒子面' => '荞麦面', '玉米碴' => '粟米', '地瓜' => '番薯', '红薯' => '番薯',
'洋芋' => '芋头', '土豆' => '芋头', '窝头' => '通心粉', '贴饼子' => '通心粉',
'二米饭' => '红米饭', '稀饭' => '白饭', '白稀饭' => '白饭', '糖糕' => '麦芽糖',
'驼奶' => '驼奶粉',
];
/**
* @return array{level:string,level_label:string,category:string,advice:string}|null
*/
public static function lookupFood(string $name): ?array
{
$name = trim($name);
if ($name === '') {
return null;
}
$canonical = self::$aliases[$name] ?? $name;
foreach ([self::LEVEL_LOW, self::LEVEL_MEDIUM, self::LEVEL_HIGH] as $level) {
foreach (self::$foods[$level] as $category => $items) {
foreach ($items as $item) {
if ($canonical === $item || $name === $item
|| mb_strpos($item, $canonical) !== false || mb_strpos($canonical, $item) !== false
|| mb_strpos($item, $name) !== false || mb_strpos($name, $item) !== false) {
return [
'level' => $level,
'level_label' => self::levelLabel($level),
'category' => $category,
'food' => $item,
'advice' => self::adviceForLevel($level, $name),
];
}
}
}
}
return null;
}
/** 供 AI 参考:老农民家常、集市易得食材示例 */
public static function ruralMealHints(): string
{
return implode("\n", [
'【早餐】须安排驼奶粉温水冲服(低 GI 奶类,稳血糖),可与粥、蛋、菜同餐。',
'【喝的】单独说明:温开水为主;早餐驼奶粉;严禁含糖饮料、果汁、酒。',
'【午餐】蛋白质要最高:至少 2 样高蛋白(鱼/鸡鸭/瘦肉/蛋/豆腐/豆干),肉蛋豆是午饭主角。',
'【淀粉不重复】粥/饭/面/饼/窝头/红薯/土豆/玉米/南瓜等算淀粉;早中晚各最多 1 种,且三顿不能同一种、不要顿顿大主食。',
'推荐蛋白:鸡蛋、豆腐、豆干、自家鸡鸭蛋、瘦肉、鲫鱼鲤鱼;',
'推荐蔬菜:白菜、萝卜、黄瓜、豆角、茄子、菠菜、芹菜;',
'淀粉示例(全天选 3 种不同的):玉米碴粥、二米饭小半碗、贴饼子、杂面窝头、挂面小碗、蒸红薯;',
'少吃:白馒头、油条、粘豆包、糯米饭、西瓜、含糖饮料、糕点。',
'说话要土话、短句,像村里大夫叮嘱。',
]);
}
public static function levelLabel(string $level): string
{
return match ($level) {
self::LEVEL_LOW => '低升糖',
self::LEVEL_MEDIUM => '中升糖',
default => '高升糖',
};
}
public static function adviceForLevel(string $level, string $food): string
{
return match ($level) {
self::LEVEL_LOW => "「{$food}」升糖慢,老农民家常能吃,当菜当饭都行,有助于稳血糖。",
self::LEVEL_MEDIUM => "「{$food}」升糖中等,可以吃但要少搁点,别当主食猛吃,配着蔬菜鸡蛋更好。",
default => "「{$food}」升糖快,血糖高时尽量别吃或少吃,尤其别空腹猛吃。",
};
}
/**
* @param int|null $templateIndex 指定模板下标(换一换时轮换)
* @return array{breakfast:string,lunch:string,dinner:string,tips:string,avoid:list<string>,source:string}
*/
public static function fallbackDailyMeals(int $diagnosisId, ?int $templateIndex = null): array
{
$templates = self::$mealTemplates;
$count = count($templates);
if ($templateIndex !== null) {
$idx = (($templateIndex % $count) + $count) % $count;
} else {
$idx = abs(crc32(date('Y-m-d') . ':' . $diagnosisId)) % $count;
}
$meal = $templates[$idx];
$plan = [
'breakfast' => $meal['breakfast'],
'drinks' => $meal['drinks'] ?? '白天多喝温开水;早餐冲驼奶粉,别喝甜饮。',
'lunch' => $meal['lunch'],
'dinner' => $meal['dinner'],
'tips' => $meal['tips'],
'avoid' => ['白面馒头', '油条', '粘豆包', '西瓜', '含糖饮料', '稀饭配糖'],
'source' => 'rule',
];
$checked = DietMealValidator::validateAndNormalize($plan);
$out = $checked['plan'];
$out['source'] = 'rule';
$out['rules_summary'] = DietMealValidator::metaSummary($checked['meta']);
return $out;
}
/**
* 根据血糖统计给出「运动降糖」建议(规则化,按控制好坏调整强度)
*
* @param array<string,mixed> $stats7 近7天统计
* @param array<string,mixed> $stats30 近30天统计
* @return array{level:string,level_label:string,headline:string,items:list<string>,intensity:string,note:string}
*/
public static function exercisePlan(array $stats7 = [], array $stats30 = []): array
{
// 优先用记录更全的口径判断;都没记录则用近7天
$primary = ((int) ($stats30['record_days'] ?? 0) > 0) ? $stats30 : $stats7;
$recordDays = (int) ($primary['record_days'] ?? 0);
$compliance = (int) ($primary['compliance'] ?? 0);
$highDays = (int) ($primary['high_days'] ?? 0);
if ($recordDays <= 0) {
$level = 'unknown';
} elseif ($compliance < 50 || $highDays * 2 >= $recordDays) {
$level = 'high'; // 偏高多、达标率低 = 控制不佳
} elseif ($compliance < 80) {
$level = 'medium';
} else {
$level = 'good';
}
$note = '餐后 1 小时内开始动,别空腹剧烈运动;身上带几块糖,头晕、心慌、出虚汗就马上停下歇着。';
switch ($level) {
case 'high':
return [
'level' => 'high',
'level_label' => '血糖偏高 · 多动',
'headline' => '近期血糖偏高,三顿饭后都动一动,最能帮着把糖降下来。',
'items' => [
'早饭后快走 15 分钟,走到微微出汗',
'午饭后快走或原地踏步 20–30 分钟,降餐后血糖最管用',
'晚饭后慢走 30 分钟,吃完别马上坐下、躺下',
],
'intensity' => '中等强度 · 每天累计约 60 分钟',
'note' => $note,
];
case 'medium':
return [
'level' => 'medium',
'level_label' => '基本平稳 · 再稳稳',
'headline' => '血糖大体平稳,坚持餐后活动,把它稳住。',
'items' => [
'三餐后各散步 20 分钟,午饭后可以走快些',
'每天加一段八段锦或太极拳,10–15 分钟',
'能走楼梯少坐电梯,多干点家务、地里活',
],
'intensity' => '中低强度 · 每天累计 4050 分钟',
'note' => $note,
];
case 'good':
return [
'level' => 'good',
'level_label' => '控制不错 · 保持',
'headline' => '血糖控制得不错,保持规律活动就行。',
'items' => [
'餐后散步 1520 分钟,雷打不动',
'每天一段八段锦、太极或柔和拉伸',
'天好多到院里、地里走动,别久坐',
],
'intensity' => '中低强度 · 每天累计 3040 分钟',
'note' => $note,
];
default:
return [
'level' => 'unknown',
'level_label' => '先记血糖 · 再调运动',
'headline' => '血糖记录还少,先按基础来:饭后多走动。',
'items' => [
'三餐后都散步 1520 分钟',
'每天一段八段锦或太极拳',
'少久坐,坐 1 小时就起来活动几分钟',
],
'intensity' => '中低强度 · 每天累计约 30 分钟',
'note' => $note,
];
}
}
/**
* @return array{food:string,level:string,level_label:string,advice:string,source:string}
*/
public static function fallbackAsk(string $question): array
{
$food = self::extractFoodName($question);
$hit = self::lookupFood($food);
if ($hit) {
return [
'food' => $hit['food'],
'level' => $hit['level'],
'level_label' => $hit['level_label'],
'advice' => $hit['advice'],
'source' => 'rule',
];
}
return [
'food' => $food,
'level' => '',
'level_label' => '未知',
'advice' => '库里没查到「' . $food . '」。一般多吃小米玉米、白菜豆角豆腐鸡蛋,少吃白馒头油条甜口;拿不准问村医。',
'source' => 'rule',
];
}
public static function extractFoodName(string $question): string
{
$q = trim($question);
$q = preg_replace('/^(能不能吃|可以吃|能吃|可不可以吃|请问|我想问)/u', '', $q) ?? $q;
$q = preg_replace('/(吗|呢||\?)+$/u', '', $q) ?? $q;
$q = trim($q);
return $q !== '' ? $q : $question;
}
/**
* 血糖照护 AI 关闭时的规则化方案(监测 + 中医调理方向)
*
* @param array<string,mixed> $stats7
* @param array<string,mixed> $stats30
* @param array<string,mixed> $extra syndrome_type, symptoms 等
* @return array<string,mixed>
*/
public static function fallbackBloodCarePlan(array $stats7 = [], array $stats30 = [], array $extra = []): array
{
$exercise = self::exercisePlan($stats7, $stats30);
$level = (string) ($exercise['level'] ?? 'unknown');
$compliance = (int) (($stats30['record_days'] ?? 0) > 0 ? ($stats30['compliance'] ?? 0) : ($stats7['compliance'] ?? 0));
$syndrome = trim((string) ($extra['syndrome_type'] ?? ''));
$symptoms = trim((string) ($extra['symptoms'] ?? ''));
$summary = match ($level) {
'high' => '近阶段血糖偏高天数偏多,要把监测、饮食和运动一起抓紧,别只靠感觉。',
'medium' => '血糖大体可控,但还有波动,坚持记录和餐后活动,能更稳。',
'good' => '近期血糖控制不错,保持现在的节奏,定期复查别松懈。',
default => '先坚持记血糖、看趋势,再按下面几条慢慢调整生活方式。',
};
$bloodAdvice = match ($level) {
'high' => '空腹、餐后都测,偏高那天记下吃了啥、动了没。主食换杂粮、每餐先吃菜和蛋白,甜饮糕点先停。三餐后按上面运动建议动起来,头晕心慌马上停、吃点糖。',
'medium' => '每周至少 4 天空腹+餐后配对记录。主食定量、晚餐别过饱,少喝粥配咸菜。餐后散步 20 分钟,睡眠规律,情绪紧张也会抬糖。',
'good' => '保持每周 3–4 次监测即可,注意节假日和串门聚餐别放松。继续低 GI 家常饭,运动别停,体重稳一点更好。',
default => '先把血糖记满 7 天再看趋势。吃饭定时定量,多喝白开水,少甜少油,饭后多走动。',
};
$tcmBase = '消渴病多与肺脾肾气虚、阴虚燥热或痰湿瘀血相关,需结合面诊脉象。';
if ($syndrome !== '') {
$tcmBase = '就诊辨证倾向「' . $syndrome . '」。';
} elseif ($symptoms !== '') {
$tcmBase = '结合您登记的症状(' . mb_substr($symptoms, 0, 40) . '…),宜辨证论治。';
}
$tcmPlan = match ($level) {
'high' => $tcmBase . '调理思路:益气养阴、清热生津为主,可配合健脾化湿;食疗如山药、麦冬、桑叶代茶饮方向;艾灸可选足三里、胰俞等穴(需医师指导)。勿自行加大西药或乱用偏方,务必复诊调方。',
'medium' => $tcmBase . '调理思路:健脾益气、滋阴润燥,少食肥甘,宜清淡易消化;可配合八段锦、太极拳;中药汤剂需医师面诊后开具,此处不给具体方名剂量。',
'good' => $tcmBase . '调理思路:顾护脾胃、养阴防燥,季节更替注意保暖;维持规律作息与情志舒畅,按医嘱定期复诊即可。',
default => $tcmBase . '建议到中医科面诊,明确证型后再定汤药、针灸方案;平时可饮食清淡、作息规律,配合记录血糖供医师参考。',
};
$watchPoints = match ($level) {
'high' => ['空腹≥7 或餐后明显升高要联系村医', '脚麻脚痛、视力模糊及时就诊', '用药时间饭点别乱改'],
'medium' => ['外出走亲也别忘了测糖', '感冒发烧会让血糖乱,多监测', '睡前别加餐太多'],
'good' => ['每 3–6 个月复查糖化血红蛋白', '血压血脂一并管', '家人提醒别擅自停药'],
default => ['坚持每日或隔日记录', '记录与饮食运动一起给大夫看', '不适及时线下就诊'],
};
return [
'summary' => $summary,
'blood_advice' => $bloodAdvice,
'tcm_plan' => $tcmPlan,
'watch_points' => $watchPoints,
'next_steps' => '下周继续记血糖;饮食可参考本页「AI 饮食建议」;若连续 3 天偏高或出现胸闷脚肿,请尽快线下就诊或视频问诊。',
'control_level' => $level,
'control_label' => (string) ($exercise['level_label'] ?? ''),
'disclaimer' => '本建议供日常参考,不能替代医师面诊、开方与调药。',
'source' => 'rule',
];
}
}