324 lines
16 KiB
PHP
324 lines
16 KiB
PHP
<?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)优先;中 GI(56-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' => '中低强度 · 每天累计 40–50 分钟',
|
||
'note' => $note,
|
||
];
|
||
case 'good':
|
||
return [
|
||
'level' => 'good',
|
||
'level_label' => '控制不错 · 保持',
|
||
'headline' => '血糖控制得不错,保持规律活动就行。',
|
||
'items' => [
|
||
'餐后散步 15–20 分钟,雷打不动',
|
||
'每天一段八段锦、太极或柔和拉伸',
|
||
'天好多到院里、地里走动,别久坐',
|
||
],
|
||
'intensity' => '中低强度 · 每天累计 30–40 分钟',
|
||
'note' => $note,
|
||
];
|
||
default:
|
||
return [
|
||
'level' => 'unknown',
|
||
'level_label' => '先记血糖 · 再调运动',
|
||
'headline' => '血糖记录还少,先按基础来:饭后多走动。',
|
||
'items' => [
|
||
'三餐后都散步 15–20 分钟',
|
||
'每天一段八段锦或太极拳',
|
||
'少久坐,坐 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;
|
||
}
|
||
}
|