['粥', '稀饭', '玉米碴', '棒子面', '高粱', '糊糊'], '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,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, 'drinks' => trim((string) ($plan['drinks'] ?? '')), '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 淀粉分组 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 ); } }