Files
zyt/server/app/api/logic/tcm/DietMealValidator.php
T
2026-05-29 09:28:38 +08:00

209 lines
7.5 KiB
PHP

<?php
namespace app\api\logic\tcm;
/**
* 三餐推荐硬性规则校验(午餐高蛋白、淀粉不重复、禁高 GI)
*/
class DietMealValidator
{
/** 淀粉类关键词 → 分组(同组即视为同一种淀粉) */
private const STARCH_GROUPS = [
'porridge' => ['粥', '稀饭', '玉米碴', '棒子面', '高粱', '糊糊'],
'rice' => ['米饭', '糙米饭', '二米饭', '杂豆饭', '红米饭', '饭'],
'noodle' => ['挂面', '面条', '手擀面', '拉面', '刀削面'],
'bun' => ['馒头', '窝头', '贴饼', '玉米面饼', '杂面馒头', '杂面窝头', '杂面', '饼'],
'potato' => ['红薯', '地瓜', '番薯', '土豆', '洋芋', '芋头'],
'pumpkin' => ['南瓜'],
];
/** 蛋白质关键词(出现即计 1,同词不重复计) */
private const PROTEIN_KEYWORDS = [
'鲫鱼', '鲤鱼', '小鱼', '清蒸鱼', '炖鱼', '鱼',
'虾', '蟹',
'鸡肉', '鸡胸', '鸡', '鸭肉', '鸭',
'牛肉', '羊肉', '猪肉', '瘦肉', '肉',
'鸡蛋', '茶叶蛋', '蛋羹', '炒蛋', '蛋',
'豆腐脑', '豆干', '豆腐', '豆角', '芸豆', '扁豆',
];
/** 高 GI 禁用(出现在任一餐即违规) */
private const FORBIDDEN_HIGH_GI = [
'白馒头', '馒头', '油条', '粘豆包', '糯米饭', '西瓜', '荔枝', '龙眼',
'含糖饮料', '可乐', '汽水', '糖糕', '糕点', '白稀饭', '稀饭',
];
/**
* 校验并规范化三餐方案;不通过则 ok=false
*
* @param array{breakfast?:string,lunch?:string,dinner?:string,tips?:string,avoid?:array} $plan
* @return array{ok:bool,errors:list<string>,plan:array,meta:array}
*/
public static function validateAndNormalize(array $plan): array
{
$breakfast = trim((string) ($plan['breakfast'] ?? ''));
$lunch = trim((string) ($plan['lunch'] ?? ''));
$dinner = trim((string) ($plan['dinner'] ?? ''));
$errors = [];
if ($breakfast === '' || $lunch === '' || $dinner === '') {
$errors[] = '三餐内容不完整';
}
foreach (self::FORBIDDEN_HIGH_GI as $bad) {
foreach (['breakfast' => $breakfast, 'lunch' => $lunch, 'dinner' => $dinner] as $mealName => $text) {
if ($text !== '' && mb_strpos($text, $bad) !== false) {
// 「杂面馒头」含馒头但可接受;纯「馒头」才禁
if ($bad === '馒头' && (mb_strpos($text, '杂面') !== false || mb_strpos($text, '玉米面') !== false)) {
continue;
}
if ($bad === '稀饭' && mb_strpos($text, '玉米') !== false) {
continue;
}
$errors[] = "{$mealName}含高 GI 食物「{$bad}」";
}
}
}
$bfStarch = self::detectStarchGroups($breakfast);
$luStarch = self::detectStarchGroups($lunch);
$diStarch = self::detectStarchGroups($dinner);
if (count($bfStarch) > 1) {
$errors[] = '早餐淀粉种类超过 1 种';
}
if (count($luStarch) > 1) {
$errors[] = '午餐淀粉种类超过 1 种';
}
if (count($diStarch) > 1) {
$errors[] = '晚餐淀粉种类超过 1 种';
}
$dayGroups = array_values(array_filter([
$bfStarch[0] ?? null,
$luStarch[0] ?? null,
$diStarch[0] ?? null,
]));
if (count($dayGroups) !== count(array_unique($dayGroups))) {
$errors[] = '早中晚淀粉种类重复';
}
$bfProtein = self::countProteinHits($breakfast);
$luProtein = self::countProteinHits($lunch);
$diProtein = self::countProteinHits($dinner);
if ($luProtein < 2) {
$errors[] = '午餐高蛋白不足(至少 2 样:鱼/肉/蛋/豆腐)';
}
if ($luProtein < $bfProtein || $luProtein < $diProtein) {
$errors[] = '午餐蛋白质应多于早、晚餐';
}
$avoid = is_array($plan['avoid'] ?? null) ? $plan['avoid'] : [];
if (count($avoid) < 3) {
$avoid = array_values(array_unique(array_merge($avoid, [
'白面馒头', '油条', '粘豆包', '西瓜', '含糖饮料',
])));
$avoid = array_slice($avoid, 0, 5);
}
$tips = trim((string) ($plan['tips'] ?? ''));
if ($tips === '') {
$tips = '中午鱼蛋豆肉吃足;早中晚各一种主食,别重复。';
}
$normalized = [
'breakfast' => $breakfast,
'lunch' => $lunch,
'dinner' => $dinner,
'tips' => $tips,
'avoid' => $avoid,
];
$meta = [
'protein' => ['breakfast' => $bfProtein, 'lunch' => $luProtein, 'dinner' => $diProtein],
'starch' => ['breakfast' => $bfStarch[0] ?? '', 'lunch' => $luStarch[0] ?? '', 'dinner' => $diStarch[0] ?? ''],
'rules_ok' => empty($errors),
];
return [
'ok' => empty($errors),
'errors' => $errors,
'plan' => $normalized,
'meta' => $meta,
];
}
/**
* @return list<string> 淀粉分组 id,按命中顺序
*/
public static function detectStarchGroups(string $text): array
{
if ($text === '') {
return [];
}
$found = [];
foreach (self::STARCH_GROUPS as $groupId => $keywords) {
foreach ($keywords as $kw) {
if (mb_strpos($text, $kw) !== false) {
$found[$groupId] = true;
break;
}
}
}
return array_keys($found);
}
public static function countProteinHits(string $text): int
{
if ($text === '') {
return 0;
}
$count = 0;
$used = [];
foreach (self::PROTEIN_KEYWORDS as $kw) {
if (isset($used[$kw])) {
continue;
}
if (mb_strpos($text, $kw) !== false) {
// 「鱼」已命中则不再计「小鱼」「鲫鱼」
$skip = false;
foreach ($used as $u) {
if (mb_strpos($u, $kw) !== false || mb_strpos($kw, $u) !== false) {
$skip = true;
break;
}
}
if ($skip) {
continue;
}
$used[$kw] = true;
$count++;
}
}
return $count;
}
public static function metaSummary(array $meta): string
{
$p = $meta['protein'] ?? [];
$s = $meta['starch'] ?? [];
$starchLabels = [
'porridge' => '粥', 'rice' => '饭', 'noodle' => '面', 'bun' => '饼/窝头',
'potato' => '薯', 'pumpkin' => '南瓜',
];
$sBf = $starchLabels[$s['breakfast'] ?? ''] ?? ($s['breakfast'] ?: '—');
$sLu = $starchLabels[$s['lunch'] ?? ''] ?? ($s['lunch'] ?: '—');
$sDi = $starchLabels[$s['dinner'] ?? ''] ?? ($s['dinner'] ?: '—');
return sprintf(
'午餐 %d 样蛋白(早%d/晚%d);淀粉:早%s·午%s·晚%s',
(int) ($p['lunch'] ?? 0),
(int) ($p['breakfast'] ?? 0),
(int) ($p['dinner'] ?? 0),
$sBf,
$sLu,
$sDi
);
}
}