更新
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
<?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,lunch:string,dinner:string,tips:string}> */
|
||||
private static array $mealTemplates = [
|
||||
[
|
||||
'breakfast' => '玉米碴子粥、煮鸡蛋、拌黄瓜',
|
||||
'lunch' => '清炖鲤鱼、豆腐炖白菜、清炒豆角、米饭小半碗',
|
||||
'dinner' => '玉米面饼一个、瘦肉丝、凉拌茄子',
|
||||
'tips' => '中午鱼豆肉吃足;早中晚淀粉别重复,一顿一种主食就够。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '小米粥、煮鸡蛋、小碟咸菜(少盐)',
|
||||
'lunch' => '去皮炖鸡肉、芹菜炒豆干、木耳炒鸡蛋',
|
||||
'dinner' => '蒸红薯(小半块)、清炒菠菜、豆腐汤',
|
||||
'tips' => '午餐最讲究蛋白;晚上才吃薯,别跟中午再配大米饭。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '豆腐脑(少卤)、鸡蛋、一个苹果',
|
||||
'lunch' => '清炖鸡肉、炖芸豆、番茄炒蛋、贴饼子一个',
|
||||
'dinner' => '挂面一小碗、清蒸小鱼、拌海带丝',
|
||||
'tips' => '中午饼配肉豆,蛋白管够;面条放晚上,别三顿都是粥饭。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '高粱米粥、茶叶蛋、拌萝卜丝',
|
||||
'lunch' => '炖牛肉(瘦)、豆腐炒韭菜、清炒油菜、杂豆饭小半碗',
|
||||
'dinner' => '蒸南瓜(小半碗)、鸡蛋羹、拍黄瓜',
|
||||
'tips' => '中午肉蛋豆要多吃;南瓜算晚饭主食,别再加馒头稀饭。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '棒子面粥、水煮蛋、大拌菜(少油)',
|
||||
'lunch' => '清炖鲫鱼、番茄炒蛋、炖扁豆、糙米饭小半碗',
|
||||
'dinner' => '玉米面饼一个、清炒豆芽、豆腐脑',
|
||||
'tips' => '午饭鱼蛋豆齐上;玉米面放晚上,跟中午米饭分开。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '小米粥、煮鸡蛋、拌生菜',
|
||||
'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", [
|
||||
'【午餐】蛋白质要最高:至少 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'],
|
||||
'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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user