Load(); require_once 'cache.php'; require_once 'response.php'; header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } $method = $_SERVER['REQUEST_METHOD']; if ($method === 'GET') { $act = strtolower(trim($_GET['act'] ?? 'index')); $params = $_GET; } elseif ($method === 'POST') { $input = file_get_contents('php://input'); $jsonData = json_decode($input, true); if (is_array($jsonData)) { $params = $jsonData; } else { $params = $_POST; } $act = strtolower(trim($params['act'] ?? 'index')); } else { $act = 'index'; $params = array(); } $result = array(); switch ($act) { case 'filter_steps': $result = get_filter_steps(); break; case 'filter_apply': $result = apply_filter(); break; case 'detail': $result = get_recipe_detail(); break; case 'index': default: $result = get_index(); break; } $result['_query_time'] = round((microtime(true) - $startTime) * 1000, 2) . 'ms'; echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; /** * 接口索引 * @return array */ function get_index() { return array( 'code' => 200, 'message' => 'success', 'data' => array( 'description' => '🍽️ 今天吃什么 - 智能筛选接口', 'version' => '1.25.0', 'methods' => array('GET', 'POST'), 'endpoints' => array( 'filter_steps' => '?act=filter_steps', 'filter_steps_with_category' => '?act=filter_steps&category=13', 'filter_apply' => '?act=filter_apply&category=13&tag=2&count=5', 'detail_by_id' => '?act=detail&id=1234', 'detail_by_title' => '?act=detail&title=宫保鸡丁', 'detail_by_code' => '?act=detail&code=CP001234', 'detail_fuzzy' => '?act=detail&title=鸡丁&fuzzy=1' ), 'features' => array( 'dynamic_filter' => '逐步筛选,越选越精准', 'random_recommend' => '随机推荐,每次不同', 'multi_lookup' => '支持ID/标题/编码查询' ) ) ); } /** * 获取筛选步骤 * @return array */ function get_filter_steps() { global $zbp, $params; $selectedCategories = isset($params['category']) ? array_map('intval', array_filter(explode(',', $params['category']))) : array(); $selectedTags = isset($params['tag']) ? array_map('intval', array_filter(explode(',', $params['tag']))) : array(); $cached = ApiCache::get('filter_steps', $params, 300); if ($cached !== null) { return $cached; } $tablePost = $zbp->db->dbpre . 'post'; $tableCategory = $zbp->db->dbpre . 'category'; $tableTag = $zbp->db->dbpre . 'tag'; $expandedCategories = array(); if (!empty($selectedCategories)) { foreach ($selectedCategories as $catId) { $expandedCategories[] = $catId; $subSql = "SELECT cate_ID FROM $tableCategory WHERE cate_ParentID = $catId"; $subResults = $zbp->db->Query($subSql); foreach ($subResults as $subRow) { $expandedCategories[] = (int) $subRow['cate_ID']; $sub2Sql = "SELECT cate_ID FROM $tableCategory WHERE cate_ParentID = " . (int) $subRow['cate_ID']; $sub2Results = $zbp->db->Query($sub2Sql); foreach ($sub2Results as $sub2Row) { $expandedCategories[] = (int) $sub2Row['cate_ID']; } } } $expandedCategories = array_unique($expandedCategories); } $whereClauses = array("p.log_Type = 0", "p.log_Status = 0"); if (!empty($expandedCategories)) { $cateList = implode(',', $expandedCategories); $whereClauses[] = "p.log_CateID IN ($cateList)"; } if (!empty($selectedTags)) { $tagConditions = array(); foreach ($selectedTags as $tagId) { $tagConditions[] = "p.log_Tag LIKE '%{{$tagId}}%'"; } if (!empty($tagConditions)) { $whereClauses[] = "(" . implode(' OR ', $tagConditions) . ")"; } } $whereSql = implode(' AND ', $whereClauses); $countSql = "SELECT COUNT(*) as cnt FROM $tablePost p WHERE $whereSql"; $countResult = $zbp->db->Query($countSql); $matchedCount = (int) ($countResult[0]['cnt'] ?? 0); $steps = array( array( 'step' => 1, 'name' => '菜系分类', 'field' => 'category', 'description' => '选择你喜欢的菜系', 'required' => false, 'completed' => !empty($selectedCategories), 'selected' => $selectedCategories ), array( 'step' => 2, 'name' => '烹饪方式', 'field' => 'tag', 'description' => '选择烹饪方法', 'required' => false, 'completed' => !empty($selectedTags), 'selected' => $selectedTags ), array( 'step' => 3, 'name' => '口味偏好', 'field' => 'taste', 'description' => '选择口味类型', 'required' => false, 'completed' => false, 'selected' => array() ), array( 'step' => 4, 'name' => '功效需求', 'field' => 'effect', 'description' => '选择功效类型', 'required' => false, 'completed' => false, 'selected' => array() ) ); $currentStep = 1; foreach ($steps as $i => $step) { if (!$step['completed']) { $currentStep = $step['step']; break; } } $availableTags = array(); $tagSql = "SELECT t.tag_ID, t.tag_Name, COUNT(p.log_ID) as recipe_count FROM $tableTag t LEFT JOIN $tablePost p ON p.log_Tag LIKE CONCAT('%{', t.tag_ID, '}%') AND p.log_Type = 0 AND p.log_Status = 0"; if (!empty($expandedCategories)) { $cateList = implode(',', $expandedCategories); $tagSql .= " AND p.log_CateID IN ($cateList)"; } $tagSql .= " WHERE t.tag_ID IS NOT NULL GROUP BY t.tag_ID, t.tag_Name HAVING recipe_count > 0 ORDER BY recipe_count DESC LIMIT 50"; $tagResults = $zbp->db->Query($tagSql); foreach ($tagResults as $tagRow) { $availableTags[] = array( 'id' => (int) $tagRow['tag_ID'], 'name' => $tagRow['tag_Name'], 'count' => (int) $tagRow['recipe_count'] ); } $result = array( 'code' => 200, 'message' => 'success', 'data' => array( 'current_step' => $currentStep, 'total_steps' => count($steps), 'steps' => $steps, 'available_tags' => $availableTags, 'matched_count' => $matchedCount, 'can_skip' => true, 'can_apply' => $matchedCount > 0 ) ); ApiCache::set('filter_steps', $params, $result, 300); return $result; } /** * 应用筛选条件 * @return array */ function apply_filter() { global $zbp, $params; $categories = isset($params['category']) ? array_map('intval', array_filter(explode(',', $params['category']))) : array(); $tags = isset($params['tag']) ? array_map('intval', array_filter(explode(',', $params['tag']))) : array(); $count = isset($params['count']) ? min((int) $params['count'], 20) : 5; $count = max($count, 1); $tablePost = $zbp->db->dbpre . 'post'; $tableCategory = $zbp->db->dbpre . 'category'; $tableTag = $zbp->db->dbpre . 'tag'; $tablePostStat = $zbp->db->dbpre . 'post_stat'; $expandedCategories = array(); if (!empty($categories)) { foreach ($categories as $catId) { $expandedCategories[] = $catId; $subSql = "SELECT cate_ID FROM $tableCategory WHERE cate_ParentID = $catId"; $subResults = $zbp->db->Query($subSql); foreach ($subResults as $subRow) { $expandedCategories[] = (int) $subRow['cate_ID']; $sub2Sql = "SELECT cate_ID FROM $tableCategory WHERE cate_ParentID = " . (int) $subRow['cate_ID']; $sub2Results = $zbp->db->Query($sub2Sql); foreach ($sub2Results as $sub2Row) { $expandedCategories[] = (int) $sub2Row['cate_ID']; } } } $expandedCategories = array_unique($expandedCategories); } $whereClauses = array("p.log_Type = 0", "p.log_Status = 0"); if (!empty($expandedCategories)) { $cateList = implode(',', $expandedCategories); $whereClauses[] = "p.log_CateID IN ($cateList)"; } if (!empty($tags)) { $tagConditions = array(); foreach ($tags as $tagId) { $tagConditions[] = "p.log_Tag LIKE '%{{$tagId}}%'"; } if (!empty($tagConditions)) { $whereClauses[] = "(" . implode(' OR ', $tagConditions) . ")"; } } $whereSql = implode(' AND ', $whereClauses); $countSql = "SELECT COUNT(*) as cnt FROM $tablePost p WHERE $whereSql"; $countResult = $zbp->db->Query($countSql); $totalMatched = (int) ($countResult[0]['cnt'] ?? 0); if ($totalMatched === 0) { return array( 'code' => 404, 'message' => '没有找到符合条件的菜谱', 'data' => array( 'recipes' => array(), 'total_matched' => 0, 'returned_count' => 0, 'filters_applied' => array( 'category' => $categories, 'tag' => $tags ), 'suggestions' => array( '尝试减少筛选条件', '更换分类或标签' ) ) ); } $sql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_Tag, p.log_ViewNums, c.cate_Name, COALESCE(s.like_nums, 0) as like_nums, COALESCE(s.rate_nums, 0) as rate_nums, COALESCE(s.rate_score, 0) as rate_score FROM $tablePost p LEFT JOIN $tableCategory c ON p.log_CateID = c.cate_ID LEFT JOIN $tablePostStat s ON p.log_ID = s.log_id WHERE $whereSql ORDER BY RAND() LIMIT $count"; $results = $zbp->db->Query($sql); $recipes = array(); foreach ($results as $row) { $recipeId = (int) $row['log_ID']; $tagIds = array_filter(array_map('intval', explode(',', $row['log_Tag'] ?? ''))); $tagsList = array(); if (!empty($tagIds)) { $tagIdList = implode(',', $tagIds); $tagSql = "SELECT tag_ID, tag_Name FROM $tableTag WHERE tag_ID IN ($tagIdList)"; $tagResults = $zbp->db->Query($tagSql); foreach ($tagResults as $tagRow) { $tagsList[] = array( 'id' => (int) $tagRow['tag_ID'], 'name' => $tagRow['tag_Name'] ); } } $recipes[] = array( 'id' => $recipeId, 'code' => 'CP' . str_pad($recipeId, 6, '0', STR_PAD_LEFT), 'title' => $row['log_Title'], 'intro' => mb_substr(strip_tags($row['log_Intro'] ?? ''), 0, 100), 'category' => array( 'id' => (int) ($row['log_CateID'] ?? 0), 'name' => $row['cate_Name'] ?? '' ), 'tags' => $tagsList, 'statistics' => array( 'view' => (int) ($row['log_ViewNums'] ?? 0), 'like' => (int) ($row['like_nums'] ?? 0), 'rate_count' => (int) ($row['rate_nums'] ?? 0), 'rate_score' => (float) ($row['rate_score'] ?? 0) ), 'rating' => ApiResponse::getRatingSummary( (float) ($row['rate_score'] ?? 0), (int) ($row['rate_nums'] ?? 0) ) ); } $refreshParams = array('act' => 'filter_apply', 'count' => $count); if (!empty($categories)) { $refreshParams['category'] = implode(',', $categories); } if (!empty($tags)) { $refreshParams['tag'] = implode(',', $tags); } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'recipes' => $recipes, 'total_matched' => $totalMatched, 'returned_count' => count($recipes), 'filters_applied' => array( 'category' => $categories, 'tag' => $tags ), 'can_refresh' => $totalMatched > $count, 'refresh_url' => '?' . http_build_query($refreshParams) ) ); } /** * 获取菜谱详情 * @return array */ function get_recipe_detail() { global $zbp, $params; $id = isset($params['id']) ? (int) $params['id'] : 0; $title = isset($params['title']) ? trim($params['title']) : ''; $code = isset($params['code']) ? trim($params['code']) : ''; $fuzzy = isset($params['fuzzy']) ? (int) $params['fuzzy'] : 0; if ($id <= 0 && empty($title) && empty($code)) { return array( 'code' => 400, 'message' => '请提供 id、title 或 code 参数', 'data' => null ); } $tablePost = $zbp->db->dbpre . 'post'; $tableCategory = $zbp->db->dbpre . 'category'; $tableTag = $zbp->db->dbpre . 'tag'; $tablePostStat = $zbp->db->dbpre . 'post_stat'; $tableRecipeIngredient = $zbp->db->dbpre . 'recipe_ingredient'; $tableIngredientDetail = $zbp->db->dbpre . 'ingredient_detail'; $tableRecipeNutrition = $zbp->db->dbpre . 'recipe_nutrition'; $tableRecipeIdMap = $zbp->db->dbpre . 'recipe_id_map'; $whereClauses = array("p.log_Type = 0", "p.log_Status = 0"); if ($id > 0) { $whereClauses[] = "p.log_ID = $id"; } elseif (!empty($code)) { $codeId = (int) str_replace('CP', '', $code); $whereClauses[] = "p.log_ID = $codeId"; } elseif (!empty($title)) { $escapedTitle = $zbp->db->EscapeString($title); if ($fuzzy === 1) { $whereClauses[] = "p.log_Title LIKE '%$escapedTitle%'"; } else { $whereClauses[] = "p.log_Title = '$escapedTitle'"; } } $whereSql = implode(' AND ', $whereClauses); $sql = "SELECT p.log_ID, p.log_Title, p.log_Content, p.log_Intro, p.log_CateID, p.log_PostTime, p.log_ViewNums, p.log_Tag, p.log_AuthorID, c.cate_Name, c.cate_ParentID, COALESCE(s.like_nums, 0) as like_nums, COALESCE(s.rate_nums, 0) as rate_nums, COALESCE(s.rate_score, 0) as rate_score FROM $tablePost p LEFT JOIN $tableCategory c ON p.log_CateID = c.cate_ID LEFT JOIN $tablePostStat s ON p.log_ID = s.log_id WHERE $whereSql LIMIT 1"; $results = $zbp->db->Query($sql); if (empty($results)) { return array( 'code' => 404, 'message' => '菜谱不存在', 'data' => null ); } $row = $results[0]; $recipeId = (int) $row['log_ID']; $picId = null; $idMapSql = "SELECT old_id FROM $tableRecipeIdMap WHERE new_log_id = $recipeId LIMIT 1"; $idMapResult = $zbp->db->Query($idMapSql); if (!empty($idMapResult)) { $picId = (int) $idMapResult[0]['old_id']; } $tagIds = array_filter(array_map('intval', explode(',', $row['log_Tag'] ?? ''))); $tags = array(); if (!empty($tagIds)) { $tagIdList = implode(',', $tagIds); $tagSql = "SELECT tag_ID, tag_Name FROM $tableTag WHERE tag_ID IN ($tagIdList)"; $tagResults = $zbp->db->Query($tagSql); foreach ($tagResults as $tagRow) { $tags[] = array( 'id' => (int) $tagRow['tag_ID'], 'name' => $tagRow['tag_Name'] ); } } $parentCategory = null; $parentId = (int) ($row['cate_ParentID'] ?? 0); if ($parentId > 0) { $parentSql = "SELECT cate_ID, cate_Name FROM $tableCategory WHERE cate_ID = $parentId"; $parentResult = $zbp->db->Query($parentSql); if (!empty($parentResult)) { $parentCategory = array( 'id' => (int) $parentResult[0]['cate_ID'], 'name' => $parentResult[0]['cate_Name'] ); } } $ingredientSql = "SELECT ri.name as ingredient_name, ri.amount, ri.type, id.ingredient_id, id.alias, id.suitable_crowd, id.unsuitable_crowd, id.intro as ingredient_intro, id.efficacy, id.cooking_tips, id.nutrition as ingredient_nutrition, id.allergen_type, id.category_names FROM $tableRecipeIngredient ri LEFT JOIN $tableIngredientDetail id ON ri.name = id.name WHERE ri.log_id = $recipeId ORDER BY ri.sort ASC"; $ingredientResults = $zbp->db->Query($ingredientSql); $ingredients = array( 'main' => array(), 'auxiliary' => array(), 'seasoning' => array() ); foreach ($ingredientResults as $ingRow) { $type = $ingRow['type'] ?? 'main'; $ingredientDetail = array( 'name' => $ingRow['ingredient_name'], 'amount' => $ingRow['amount'] ?? '' ); if (!empty($ingRow['ingredient_id'])) { $ingredientDetail['detail'] = array( 'id' => (int) $ingRow['ingredient_id'], 'alias' => json_decode($ingRow['alias'] ?? '[]', true), 'suitable_crowd' => json_decode($ingRow['suitable_crowd'] ?? '[]', true), 'unsuitable_crowd' => json_decode($ingRow['unsuitable_crowd'] ?? '[]', true), 'intro' => $ingRow['ingredient_intro'] ?? '', 'efficacy' => $ingRow['efficacy'] ?? '', 'cooking_tips' => $ingRow['cooking_tips'] ?? '', 'nutrition' => json_decode($ingRow['ingredient_nutrition'] ?? '[]', true), 'allergen_type' => json_decode($ingRow['allergen_type'] ?? '[]', true), 'category_names' => json_decode($ingRow['category_names'] ?? '[]', true) ); } $ingredients[$type][] = $ingredientDetail; } $nutrition = array( 'calories' => null, 'protein' => null, 'fat' => null, 'carbohydrate' => null, 'fiber' => null, 'sodium' => null ); $nutritionSql = "SELECT * FROM $tableRecipeNutrition WHERE log_id = $recipeId LIMIT 1"; $nutritionResult = $zbp->db->Query($nutritionSql); if (!empty($nutritionResult)) { $nutRow = $nutritionResult[0]; $nutrition = array( 'calories' => isset($nutRow['calories']) ? (float) $nutRow['calories'] : null, 'protein' => isset($nutRow['protein']) ? (float) $nutRow['protein'] : null, 'fat' => isset($nutRow['fat']) ? (float) $nutRow['fat'] : null, 'carbohydrate' => isset($nutRow['carbohydrate']) ? (float) $nutRow['carbohydrate'] : null, 'fiber' => isset($nutRow['fiber']) ? (float) $nutRow['fiber'] : null, 'sodium' => isset($nutRow['sodium']) ? (float) $nutRow['sodium'] : null ); } $cover = ''; if (preg_match('/]+src=["\']([^"\']+)["\']/i', $row['log_Content'] ?? '', $matches)) { $cover = $matches[1]; } $result = array( 'code' => 200, 'message' => 'success', 'data' => array( 'id' => $recipeId, 'code' => 'CP' . str_pad($recipeId, 6, '0', STR_PAD_LEFT), 'pic_id' => $picId, 'title' => $row['log_Title'], 'cover' => $cover, 'intro' => mb_substr(strip_tags($row['log_Intro'] ?? ''), 0, 200), 'content' => $row['log_Content'] ?? '', 'category' => array( 'id' => (int) ($row['log_CateID'] ?? 0), 'name' => $row['cate_Name'] ?? '', 'parent' => $parentCategory ), 'tags' => $tags, 'ingredients' => $ingredients, 'nutrition' => $nutrition, 'statistics' => array( 'view' => (int) ($row['log_ViewNums'] ?? 0), 'like' => (int) ($row['like_nums'] ?? 0), 'rate_count' => (int) ($row['rate_nums'] ?? 0), 'rate_score' => (float) ($row['rate_score'] ?? 0) ), 'rating' => ApiResponse::formatRating( (float) ($row['rate_score'] ?? 0), (int) ($row['rate_nums'] ?? 0) ), 'author' => array( 'id' => (int) ($row['log_AuthorID'] ?? 0), 'name' => '管理员' ), 'publish_time' => $row['log_PostTime'] ?? '', 'url' => '?act=detail&id=' . $recipeId ) ); return $result; }