Load(); require_once 'cache.php'; require_once 'response.php'; header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Cache-Control: public, max-age=300'); header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 300) . ' GMT'); $act = strtolower(trim($_GET['act'] ?? 'index')); $type = strtolower(trim($_GET['type'] ?? 'recipe')); $format = ApiResponse::getFormat(); $allowedTypes = array('recipe', 'ingredient'); if (!in_array($type, $allowedTypes)) { $type = 'recipe'; } increment_api_request($act); if (rand(1, 200) === 1) { ApiCache::cleanExpired(); } function increment_api_request($api) { $cacheDir = dirname(__FILE__) . '/cache/stats/'; if (!is_dir($cacheDir)) { @mkdir($cacheDir, 0755, true); } $file = $cacheDir . 'request_stats.json'; $data = array( 'total' => 0, 'today' => 0, 'today_date' => date('Y-m-d'), 'apis' => array(), 'hourly' => array(), 'start_date' => date('Y-m-d') ); if (file_exists($file)) { $content = file_get_contents($file); $loaded = json_decode($content, true); if (is_array($loaded)) { $data = $loaded; } } $today = date('Y-m-d'); $hour = date('H'); if ($data['today_date'] !== $today) { $data['today'] = 0; $data['today_date'] = $today; $data['hourly'] = array(); } $data['total']++; $data['today']++; $data['apis'][$api] = isset($data['apis'][$api]) ? $data['apis'][$api] + 1 : 1; $data['hourly'][$hour] = isset($data['hourly'][$hour]) ? $data['hourly'][$hour] + 1 : 1; file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE)); } $result = array(); $cacheableActs = array('list', 'detail', 'full', 'ingredients', 'ingredient_detail', 'search', 'categories', 'tags', 'stats', 'query', 'filter', 'unified_list', 'unified_detail', 'unified_search', 'unified_hot'); $forceRefresh = isset($_GET['_refresh']) && $_GET['_refresh'] === '1'; $staleMode = isset($_GET['_stale']) && $_GET['_stale'] === '1'; if (in_array($act, $cacheableActs) && !$forceRefresh) { $cacheParams = $_GET; unset($cacheParams['act']); unset($cacheParams['_refresh']); unset($cacheParams['_stale']); unset($cacheParams['_format']); unset($cacheParams['_pretty']); $cachedResult = ApiCache::get($act, $cacheParams); if ($cachedResult !== null) { header('X-Cache: HIT'); header('X-Cache-TTL: ' . ApiCache::getTTL($act)); $cachedResult['_cached'] = true; $cachedResult['_cache_age'] = ApiCache::getCacheAge($act, $cacheParams); $cachedResult['_query_time'] = round((microtime(true) - $startTime) * 1000, 2) . 'ms'; ApiResponse::output($cachedResult, $format); exit; } if ($staleMode || isset($_SERVER['HTTP_X_STALE_CACHE'])) { $staleResult = ApiCache::getStale($act, $cacheParams); if ($staleResult !== null) { header('X-Cache: STALE'); header('X-Cache-Expired: true'); $staleResult['_cached'] = true; $staleResult['_stale'] = true; $staleResult['_cache_age'] = ApiCache::getCacheAge($act, $cacheParams); $staleResult['_query_time'] = round((microtime(true) - $startTime) * 1000, 2) . 'ms'; ApiResponse::output($staleResult, $format); exit; } } } header('X-Cache: MISS'); switch ($act) { case 'list': $result = recipe_list(); break; case 'detail': $result = recipe_detail(); break; case 'full': $result = recipe_full(); break; case 'ingredients': $result = ingredient_list(); break; case 'ingredient_detail': $result = ingredient_detail(); break; case 'search': $result = recipe_search(); break; case 'categories': $result = category_list(); break; case 'tags': $result = tag_list(); break; case 'stats': $result = site_stats(); break; case 'query': $result = advanced_query(); break; case 'filter': $result = field_filter(); break; case 'unified_list': $result = get_unified_list($type); break; case 'unified_detail': $result = get_unified_detail($type); break; case 'unified_search': $result = get_unified_search($type); break; case 'unified_hot': $result = get_unified_hot($type); break; case 'index': default: $result = array( 'code' => 200, 'message' => '🍳 菜谱API服务正常运行', 'data' => array( 'version' => '1.26.0', 'endpoints' => array( 'list' => '?act=list', 'detail' => '?act=detail&id=1', 'full' => '?act=full&id=1', 'ingredients' => '?act=ingredients', 'ingredient_detail' => '?act=ingredient_detail&id=1', 'search' => '?act=search&keyword=苹果', 'categories' => '?act=categories', 'tags' => '?act=tags', 'stats' => '?act=stats' ), 'related_apis' => array( 'what_to_eat' => 'api_what_to_eat.php', 'action' => 'api_action.php', 'feed' => 'api_feed.php', 'unified' => 'api_unified.php' ), 'doc_url' => $zbp->host . 'api/' ) ); break; } if (in_array($act, $cacheableActs) && $result['code'] === 200) { $cacheParams = $_GET; unset($cacheParams['act']); unset($cacheParams['_format']); unset($cacheParams['_pretty']); ApiCache::set($act, $cacheParams, $result); } $result['_cached'] = false; $result['_query_time'] = round((microtime(true) - $startTime) * 1000, 2) . 'ms'; ApiResponse::output($result, $format); exit; // ==================== API函数 ==================== /** * 获取菜谱列表 */ function recipe_list() { global $zbp; $page = (int) ($_GET['page'] ?? 1); $limit = (int) ($_GET['limit'] ?? 20); $cateId = (int) ($_GET['cate_id'] ?? 0); $tagId = (int) ($_GET['tag_id'] ?? 0); $search = trim($_GET['search'] ?? ''); $userId = trim($_GET['user_id'] ?? ''); $usePreference = isset($_GET['use_preference']) && $_GET['use_preference'] === 'true'; if ($limit > 100) $limit = 100; if ($limit < 1) $limit = 20; if ($page < 1) $page = 1; $preferredTags = array(); $preferredCategories = array(); $blockedAllergens = array(); if ($usePreference && !empty($userId)) { $preference = load_user_preference_data($userId); $preferredTags = $preference['preferred_tags'] ?? array(); $preferredCategories = $preference['preferred_categories'] ?? array(); $blockedAllergens = $preference['blocked_allergens'] ?? array(); } if (isset($_GET['preferred_tags'])) { $preferredTags = array_map('intval', explode(',', $_GET['preferred_tags'])); } if (isset($_GET['preferred_categories'])) { $preferredCategories = array_map('intval', explode(',', $_GET['preferred_categories'])); } $tablePost = $zbp->db->dbpre . 'post'; $tableCategory = $zbp->db->dbpre . 'category'; $offset = ($page - 1) * $limit; $whereSql = "WHERE p.log_Type = 0 AND p.log_Status = 0"; if ($cateId > 0) { $whereSql .= " AND p.log_CateID = $cateId"; } elseif (!empty($preferredCategories)) { $cateList = implode(',', $preferredCategories); $whereSql .= " AND p.log_CateID IN ($cateList)"; } if ($tagId > 0) { $whereSql .= " AND p.log_Tag LIKE '%{$tagId}%'"; } elseif (!empty($preferredTags)) { $tagConditions = array(); foreach ($preferredTags as $pt) { $tagConditions[] = "p.log_Tag LIKE '%{$pt}%'"; } $whereSql .= " AND (" . implode(' OR ', $tagConditions) . ")"; } if (!empty($search)) { $search = htmlspecialchars($search); $whereSql .= " AND (p.log_Content LIKE '%$search%' OR p.log_Intro LIKE '%$search%' OR p.log_Title LIKE '%$search%')"; } $countSql = "SELECT COUNT(*) as total FROM $tablePost p $whereSql"; $countResult = $zbp->db->Query($countSql); $total = (int) ($countResult[0]['total'] ?? 0); $selectFields = "p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_Tag, p.log_PostTime, p.log_ViewNums, p.log_CommNums, p.log_Meta, p.log_Content, c.cate_Name"; $sql = "SELECT $selectFields FROM $tablePost p LEFT JOIN $tableCategory c ON p.log_CateID = c.cate_ID $whereSql ORDER BY p.log_PostTime DESC LIMIT $offset, $limit"; $results = $zbp->db->Query($sql); $list = array(); foreach ($results as $row) { $meta = json_decode($row['log_Meta'] ?? '', true) ?: array(); $list[] = array( 'id' => (int) $row['log_ID'], 'title' => $row['log_Title'], 'intro' => $row['log_Intro'] ?? '', 'category_id' => (int) ($row['log_CateID'] ?? 0), 'category_name' => $row['cate_Name'] ?? '', 'tags' => parse_tags($row['log_Tag'] ?? ''), 'create_time' => strtotime($row['log_PostTime'] ?? 'now'), 'view_count' => (int) ($row['log_ViewNums'] ?? 0), 'comment_count' => (int) ($row['log_CommNums'] ?? 0), 'meta' => $meta, 'url' => '?id=' . $row['log_ID'], 'cover' => extract_cover_from_content($row['log_Content'] ?? '') ); } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'list' => $list, 'page' => $page, 'limit' => $limit, 'total' => $total, 'filter' => array( 'preferred_tags' => $preferredTags, 'preferred_categories' => $preferredCategories ) ) ); } function load_user_preference_data($userId) { $cacheDir = dirname(__FILE__) . '/cache/preference/'; $file = $cacheDir . 'pref_' . md5($userId) . '.json'; if (file_exists($file)) { $content = file_get_contents($file); $data = json_decode($content, true); return is_array($data) ? $data : array( 'preferred_tags' => array(), 'preferred_categories' => array(), 'blocked_allergens' => array() ); } return array( 'preferred_tags' => array(), 'preferred_categories' => array(), 'blocked_allergens' => array() ); } /** * 获取菜谱详情 */ function recipe_detail() { global $zbp; $id = (int) ($_GET['id'] ?? 0); if ($id <= 0) { return array('code' => 400, 'message' => '❌ 缺少菜谱ID参数'); } $post = $zbp->GetPostByID($id); if (!$post || $post->ID == 0) { return array('code' => 404, 'message' => '❌ 菜谱不存在'); } // 增加浏览量 if (isset($_GET['viewnums']) && $_GET['viewnums'] === 'true') { $post->ViewNums += 1; $sql = $zbp->db->sql->Update($zbp->table['Post'], array('log_ViewNums' => $post->ViewNums), array(array('=', 'log_ID', $post->ID))); $zbp->db->Update($sql); } $meta = json_decode($post->Meta, true) ?: array(); $ingredients = get_recipe_ingredients($id); return array( 'code' => 200, 'message' => 'success', 'data' => array( 'id' => $post->ID, 'title' => $post->Title, 'content' => $post->Content, 'intro' => $post->Intro, 'category_id' => $post->CategoryID, 'category_name' => $post->Category ? $post->Category->Name : '', 'tags' => parse_tags($post->Tag), 'create_time' => strtotime($post->PostTime), 'update_time' => strtotime($post->UpdateTime), 'view_count' => $post->ViewNums, 'comment_count' => $post->CommNums, 'meta' => $meta, 'ingredients' => $ingredients, 'url' => '?' . parse_url($post->Url, PHP_URL_QUERY), 'cover' => extract_cover($post), 'author' => $post->Author ? array( 'id' => $post->Author->ID, 'name' => $post->Author->Name, 'avatar' => $post->Author->Avatar ) : null ) ); } /** * 获取菜谱完整信息 * 包含:基本信息、食材详情、营养、统计、标签、分类层级等所有信息 * @return array 完整菜谱信息 */ function recipe_full() { global $zbp; $id = (int) ($_GET['id'] ?? 0); if ($id <= 0) { return array('code' => 400, 'message' => '缺少菜谱ID参数'); } $tablePost = $zbp->db->dbpre . 'post'; $tableCategory = $zbp->db->dbpre . 'category'; $tableMember = $zbp->db->dbpre . 'member'; $tablePostStat = $zbp->db->dbpre . 'post_stat'; $tableRecipeIngredient = $zbp->db->dbpre . 'recipe_ingredient'; $tableIngredientDetail = $zbp->db->dbpre . 'ingredient_detail'; $tableRecipeNutrition = $zbp->db->dbpre . 'recipe_nutrition'; $tableTag = $zbp->db->dbpre . 'tag'; $tableRecipeIdMap = $zbp->db->dbpre . 'recipe_id_map'; $sql = "SELECT p.log_ID, p.log_Title, p.log_Content, p.log_Intro, p.log_Tag, p.log_CateID, p.log_AuthorID, p.log_PostTime, p.log_UpdateTime, p.log_ViewNums, p.log_CommNums, p.log_Meta, p.log_Status, c.cate_ID as cate_id, c.cate_Name as cate_name, c.cate_Alias as cate_alias, c.cate_ParentID as cate_parent_id, m.mem_ID as author_id, m.mem_Name as author_name, m.mem_Alias as author_alias, m.mem_Email as author_email, m.mem_HomePage as author_homepage, COALESCE(s.like_nums, 0) as like_nums, COALESCE(s.recommend_nums, 0) as recommend_nums, COALESCE(s.recommend_score, 0) as recommend_score FROM $tablePost p LEFT JOIN $tableCategory c ON p.log_CateID = c.cate_ID LEFT JOIN $tableMember m ON p.log_AuthorID = m.mem_ID LEFT JOIN $tablePostStat s ON p.log_ID = s.log_id WHERE p.log_ID = $id AND p.log_Type = 0"; $result = $zbp->db->Query($sql); if (empty($result)) { return array('code' => 404, 'message' => '菜谱不存在'); } $row = $result[0]; $picId = null; $idMapSql = "SELECT old_id FROM $tableRecipeIdMap WHERE new_log_id = $id LIMIT 1"; $idMapResult = $zbp->db->Query($idMapSql); if (!empty($idMapResult)) { $picId = (int) $idMapResult[0]['old_id']; } $meta = json_decode($row['log_Meta'] ?? '', true) ?: array(); $tagIds = parse_tags($row['log_Tag'] ?? ''); $tags = array(); if (!empty($tagIds)) { $tagIdList = implode(',', array_filter($tagIds, 'is_numeric')); if (!empty($tagIdList)) { $tagSql = "SELECT tag_ID, tag_Name, tag_Alias, tag_Count 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'], 'alias' => $tagRow['tag_Alias'] ?? '', 'count' => (int) ($tagRow['tag_Count'] ?? 0) ); } } } $ingredientSql = "SELECT ri.ingredient_id, ri.log_id, ri.type, ri.name, ri.amount, ri.sort, ri.detail_id, id.alias, id.usage_tip, id.introduction, id.nutrition, id.guidance, id.effect, id.other, id.allergen, id.allergen_type FROM $tableRecipeIngredient ri LEFT JOIN $tableIngredientDetail id ON ri.detail_id = id.ingredient_id WHERE ri.log_id = $id ORDER BY ri.sort ASC"; $ingredientResults = $zbp->db->Query($ingredientSql); $ingredients = array( 'main' => array(), 'auxiliary' => array(), 'seasoning' => array() ); $allergens = array(); foreach ($ingredientResults as $ingRow) { $ingType = $ingRow['type'] ?? 'main'; $ingredientData = array( 'ingredient_id' => (int) ($ingRow['ingredient_id'] ?? 0), 'name' => $ingRow['name'], 'amount' => $ingRow['amount'] ?? '', 'sort' => (int) ($ingRow['sort'] ?? 0), 'detail_id' => (int) ($ingRow['detail_id'] ?? 0), 'detail' => null ); if (!empty($ingRow['detail_id'])) { $ingredientData['detail'] = array( 'alias' => json_decode($ingRow['alias'] ?? '[]', true), 'usage_tip' => json_decode($ingRow['usage_tip'] ?? '[]', true), 'introduction' => $ingRow['introduction'] ?? '', 'nutrition' => $ingRow['nutrition'] ?? '', 'guidance' => $ingRow['guidance'] ?? '', 'effect' => $ingRow['effect'] ?? '', 'other' => $ingRow['other'] ?? '', 'allergen' => json_decode($ingRow['allergen'] ?? '[]', true), 'allergen_type' => json_decode($ingRow['allergen_type'] ?? '[]', true) ); if (!empty($ingRow['allergen'])) { $allergenList = json_decode($ingRow['allergen'], true); if (is_array($allergenList)) { $allergens = array_merge($allergens, $allergenList); } } } if (isset($ingredients[$ingType])) { $ingredients[$ingType][] = $ingredientData; } else { $ingredients['main'][] = $ingredientData; } } $allergens = array_values(array_unique($allergens)); $nutritionSql = "SELECT name, value, unit FROM $tableRecipeNutrition WHERE log_id = $id"; $nutritionResults = $zbp->db->Query($nutritionSql); $nutrition = array(); foreach ($nutritionResults as $nutRow) { $nutrition[] = array( 'name' => $nutRow['name'], 'value' => (float) $nutRow['value'], 'unit' => $nutRow['unit'] ); } $categoryHierarchy = array(); if (!empty($row['cate_id'])) { $categoryHierarchy[] = array( 'id' => (int) $row['cate_id'], 'name' => $row['cate_name'] ?? '', 'alias' => $row['cate_alias'] ?? '', 'level' => 1 ); if (!empty($row['cate_parent_id'])) { $parentSql = "SELECT cate_ID, cate_Name, cate_Alias, cate_ParentID FROM $tableCategory WHERE cate_ID = " . (int) $row['cate_parent_id']; $parentResult = $zbp->db->Query($parentSql); if (!empty($parentResult)) { $parent = $parentResult[0]; $categoryHierarchy[] = array( 'id' => (int) $parent['cate_ID'], 'name' => $parent['cate_Name'] ?? '', 'alias' => $parent['cate_Alias'] ?? '', 'level' => 2 ); if (!empty($parent['cate_ParentID'])) { $grandparentSql = "SELECT cate_ID, cate_Name, cate_Alias FROM $tableCategory WHERE cate_ID = " . (int) $parent['cate_ParentID']; $grandparentResult = $zbp->db->Query($grandparentSql); if (!empty($grandparentResult)) { $grandparent = $grandparentResult[0]; $categoryHierarchy[] = array( 'id' => (int) $grandparent['cate_ID'], 'name' => $grandparent['cate_Name'] ?? '', 'alias' => $grandparent['cate_Alias'] ?? '', 'level' => 3 ); } } } } } $cover = ''; if (preg_match('/]+src=["\']([^"\']+)["\']/i', $row['log_Content'] ?? '', $matches)) { $cover = $matches[1]; } if (isset($_GET['viewnums']) && $_GET['viewnums'] === 'true') { $newViews = ((int) $row['log_ViewNums']) + 1; $updateSql = "UPDATE $tablePost SET log_ViewNums = $newViews WHERE log_ID = $id"; $zbp->db->Query($updateSql); $row['log_ViewNums'] = $newViews; } $recipeCode = 'CP' . str_pad($id, 6, '0', STR_PAD_LEFT); return array( 'code' => 200, 'message' => 'success', 'data' => array( 'id' => (int) $row['log_ID'], 'code' => $recipeCode, 'pic_id' => $picId, 'title' => $row['log_Title'], 'intro' => $row['log_Intro'] ?? '', 'content' => $row['log_Content'], 'cover' => $cover, 'status' => (int) ($row['log_Status'] ?? 0), 'create_time' => strtotime($row['log_PostTime']), 'update_time' => strtotime($row['log_UpdateTime']), 'category' => array( 'id' => (int) ($row['cate_id'] ?? 0), 'name' => $row['cate_name'] ?? '', 'alias' => $row['cate_alias'] ?? '', 'hierarchy' => array_reverse($categoryHierarchy) ), 'author' => array( 'id' => (int) ($row['author_id'] ?? 0), 'name' => $row['author_name'] ?? '', 'alias' => $row['author_alias'] ?? '', 'email' => $row['author_email'] ?? '', 'homepage' => $row['author_homepage'] ?? '' ), 'tags' => $tags, 'ingredients' => $ingredients, 'allergens' => $allergens, 'nutrition' => $nutrition, 'statistics' => array( 'view_count' => (int) ($row['log_ViewNums'] ?? 0), 'comment_count' => (int) ($row['log_CommNums'] ?? 0), 'like_count' => (int) ($row['like_nums'] ?? 0), 'recommend_count' => (int) ($row['recommend_nums'] ?? 0), 'recommend_score' => (float) ($row['recommend_score'] ?? 0) ), 'meta' => array( 'indices' => $meta['indices'] ?? array(), 'process' => $meta['process'] ?? '', 'taste' => $meta['taste'] ?? '', 'eating_time' => $meta['eating_time'] ?? array() ) ) ); } /** * 获取食材列表 */ function ingredient_list() { global $zbp; $page = (int) ($_GET['page'] ?? 1); $limit = (int) ($_GET['limit'] ?? 20); $cateId = (int) ($_GET['cate_id'] ?? 0); $search = trim($_GET['search'] ?? ''); $author = trim($_GET['author'] ?? ''); $userId = trim($_GET['user_id'] ?? ''); $usePreference = isset($_GET['use_preference']) && $_GET['use_preference'] === 'true'; if ($limit > 100) $limit = 100; if ($limit < 1) $limit = 20; if ($page < 1) $page = 1; $blockedAllergens = array(); if (isset($_GET['blocked_allergens'])) { $blockedAllergens = array_filter(explode(',', $_GET['blocked_allergens'])); } elseif ($usePreference && !empty($userId)) { $preference = load_user_preference_data($userId); $blockedAllergens = $preference['blocked_allergens'] ?? array(); } $table = $zbp->db->dbpre . 'ingredient_detail'; $offset = ($page - 1) * $limit; $whereClauses = array(); if (!empty($search)) { $search = htmlspecialchars($search); $whereClauses[] = "name LIKE '%$search%'"; } if ($cateId > 0) { $whereClauses[] = "cate_ID = $cateId"; } if (!empty($author)) { $author = htmlspecialchars($author); $whereClauses[] = "author = '$author'"; } $whereSql = empty($whereClauses) ? '' : 'WHERE ' . implode(' AND ', $whereClauses); $countSql = "SELECT COUNT(*) as total FROM $table $whereSql"; $countResult = $zbp->db->Query($countSql); $total = (int) ($countResult[0]['total'] ?? 0); $sql = "SELECT ingredient_id, name, view_count, allergen, allergen_type FROM $table $whereSql ORDER BY ingredient_id ASC LIMIT $offset, $limit"; $results = $zbp->db->Query($sql); $list = array(); $filtered = 0; foreach ($results as $row) { $allergenType = json_decode($row['allergen_type'] ?? '[]', true); if (!empty($blockedAllergens) && !empty($allergenType)) { $hasBlocked = false; foreach ($allergenType as $type) { if (in_array($type, $blockedAllergens)) { $hasBlocked = true; $filtered++; break; } } if ($hasBlocked) { continue; } } $item = array( 'id' => (int) $row['ingredient_id'], 'name' => $row['name'], 'view_count' => (int) ($row['view_count'] ?? 0), 'create_time' => time() ); $allergen = json_decode($row['allergen'] ?? '[]', true); if (!empty($allergen)) { $item['allergen'] = $allergen; $item['allergen_type'] = $allergenType; } $list[] = $item; } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'list' => $list, 'page' => $page, 'limit' => $limit, 'total' => $total, 'filtered' => $filtered, 'blocked_allergens' => $blockedAllergens ) ); } /** * 获取食材详情 */ function ingredient_detail() { global $zbp; $id = (int) ($_GET['id'] ?? 0); if ($id <= 0) { return array('code' => 400, 'message' => '❌ 缺少食材ID参数'); } $table = $zbp->db->dbpre . 'ingredient_detail'; $sql = "SELECT * FROM $table WHERE ingredient_id = $id LIMIT 1"; $result = $zbp->db->Query($sql); if (empty($result)) { return array('code' => 404, 'message' => '❌ 食材不存在'); } $row = $result[0]; $statTable = $zbp->db->dbpre . 'ingredient_stat'; $statSql = "SELECT * FROM $statTable WHERE ingredient_id = $id LIMIT 1"; $statResult = $zbp->db->Query($statSql); $likeCount = 0; $recommendCount = 0; $recommendScore = 0; if (!empty($statResult)) { $likeCount = (int) ($statResult[0]['like_count'] ?? 0); $recommendCount = (int) ($statResult[0]['recommend_count'] ?? 0); $recommendScore = (float) ($statResult[0]['recommend_score'] ?? 0); } $relatedRecipes = array(); $riTable = $zbp->db->dbpre . 'recipe_ingredient'; $recipeSql = "SELECT DISTINCT log_id FROM $riTable WHERE detail_id = $id LIMIT 10"; $recipeResults = $zbp->db->Query($recipeSql); foreach ($recipeResults as $recipeRow) { $logId = (int) $recipeRow['log_id']; $post = $zbp->GetPostByID($logId); if ($post && $post->ID > 0) { $relatedRecipes[] = array( 'id' => $post->ID, 'title' => $post->Title, 'url' => '?' . parse_url($post->Url, PHP_URL_QUERY) ); } } $allergen = json_decode($row['allergen'] ?? '[]', true); $allergenType = json_decode($row['allergen_type'] ?? '[]', true); $data = array( 'id' => (int) $row['ingredient_id'], 'name' => $row['name'], 'alias' => json_decode($row['alias'] ?? '[]', true), 'usage_tip' => json_decode($row['usage_tip'] ?? '[]', true), 'introduction' => $row['introduction'] ?? '', 'nutrition' => $row['nutrition'] ?? '', 'guidance' => $row['guidance'] ?? '', 'effect' => $row['effect'] ?? '', 'other' => $row['other'] ?? '', 'nutrients' => json_decode($row['nutrients'] ?? '[]', true), 'view_count' => (int) ($row['view_count'] ?? 0), 'like_count' => $likeCount, 'recommend_count' => $recommendCount, 'recommend_score' => $recommendScore, 'author' => $row['author'] ?? '系统', 'cate_id' => (int) ($row['cate_ID'] ?? 0), 'create_time' => (int) ($row['create_time'] ?? 0), 'update_time' => (int) ($row['update_time'] ?? 0), 'related_recipes' => $relatedRecipes ); if (!empty($allergen)) { $data['allergen'] = $allergen; $data['allergen_type'] = $allergenType; } return array( 'code' => 200, 'message' => 'success', 'data' => $data ); } /** * 搜索功能 */ function recipe_search() { global $zbp; $keyword = trim($_GET['keyword'] ?? ''); $type = $_GET['type'] ?? 'all'; $page = (int) ($_GET['page'] ?? 1); $limit = (int) ($_GET['limit'] ?? 20); if (empty($keyword)) { return array('code' => 400, 'message' => '❌ 请输入搜索关键词'); } if ($limit > 50) $limit = 50; if ($page < 1) $page = 1; $result = array('recipes' => array(), 'ingredients' => array()); $offset = ($page - 1) * $limit; $keyword = htmlspecialchars($keyword); if ($type == 'all' || $type == 'recipe') { $tablePost = $zbp->db->dbpre . 'post'; $searchSql = "SELECT * FROM $tablePost WHERE log_Type = 0 AND log_Status = 0 AND (log_Content LIKE '%$keyword%' OR log_Intro LIKE '%$keyword%' OR log_Title LIKE '%$keyword%') ORDER BY log_ViewNums DESC LIMIT $offset, $limit"; $posts = $zbp->db->Query($searchSql); foreach ($posts as $row) { $meta = json_decode($row['log_Meta'] ?? '', true) ?: array(); $categoryId = (int) ($row['log_CateID'] ?? 0); $categoryName = ''; $cate = $zbp->GetCategoryByID($categoryId); if ($cate) { $categoryName = $cate->Name; } $result['recipes'][] = array( 'id' => (int) $row['log_ID'], 'title' => $row['log_Title'], 'intro' => $row['log_Intro'] ?? '', 'category_name' => $categoryName, 'view_count' => (int) ($row['log_ViewNums'] ?? 0), 'url' => '?id=' . $row['log_ID'], 'cover' => extract_cover_from_content($row['log_Content'] ?? ''), 'meta' => $meta ); } } if ($type == 'all' || $type == 'ingredient') { $table = $zbp->db->dbpre . 'recipe_ingredient'; $ingredientSql = "SELECT DISTINCT name, ingredient_id FROM $table WHERE name LIKE '%$keyword%' ORDER BY ingredient_id ASC LIMIT $offset, $limit"; $ingredients = $zbp->db->Query($ingredientSql); foreach ($ingredients as $row) { $result['ingredients'][] = array( 'id' => (int) $row['ingredient_id'], 'name' => $row['name'] ); } } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'keyword' => $keyword, 'type' => $type, 'page' => $page, 'result' => $result ) ); } /** * 获取分类列表 */ function category_list() { global $zbp; $type = $_GET['type'] ?? 'recipe'; $list = array(); if ($type == 'recipe') { foreach ($zbp->categories as $cate) { if ($cate->ParentID == 0) { $children = array(); foreach ($cate->ChildrenCategories as $child) { $children[] = array( 'id' => $child->ID, 'name' => $child->Name, 'alias' => $child->Alias, 'count' => $child->Count ); } $list[] = array( 'id' => $cate->ID, 'name' => $cate->Name, 'alias' => $cate->Alias, 'count' => $cate->AllCount, 'children' => $children ); } } } else { $table = $zbp->db->dbpre . 'ingredient_detail'; $sql = "SELECT cate_ID, COUNT(*) as count FROM $table WHERE cate_ID > 0 GROUP BY cate_ID"; $results = $zbp->db->Query($sql); foreach ($results as $row) { $cate = $zbp->GetCategoryByID($row['cate_ID']); if ($cate) { $list[] = array( 'id' => $cate->ID, 'name' => $cate->Name, 'alias' => $cate->Alias, 'count' => (int) $row['count'] ); } } } return array( 'code' => 200, 'message' => 'success', 'data' => $list ); } /** * 获取标签列表 */ function tag_list() { global $zbp; $limit = (int) ($_GET['limit'] ?? 50); $list = array(); $tags = $zbp->GetTagList('*', array(array('>', 'tag_Count', 0)), array('tag_Count' => 'DESC'), $limit); foreach ($tags as $tag) { $list[] = array( 'id' => $tag->ID, 'name' => $tag->Name, 'count' => $tag->Count, 'url' => '?' . parse_url($tag->Url, PHP_URL_QUERY) ); } return array( 'code' => 200, 'message' => 'success', 'data' => $list ); } /** * 获取统计数据 */ function site_stats() { global $zbp; $tablePost = $zbp->db->dbpre . 'post'; $tableRecipeIngredient = $zbp->db->dbpre . 'recipe_ingredient'; $tableTag = $zbp->db->dbpre . 'tag'; $tableIngredientDetail = $zbp->db->dbpre . 'ingredient_detail'; $recipeCountResult = $zbp->db->Query("SELECT COUNT(*) as total FROM $tablePost WHERE log_Type = 0 AND log_Status = 0"); $recipeCount = (int) ($recipeCountResult[0]['total'] ?? 0); $ingredientCountResult = $zbp->db->Query("SELECT COUNT(*) as total FROM $tableIngredientDetail"); $ingredientCount = (int) ($ingredientCountResult[0]['total'] ?? 0); $relationCountResult = $zbp->db->Query("SELECT COUNT(*) as total FROM $tableRecipeIngredient"); $relationCount = (int) ($relationCountResult[0]['total'] ?? 0); $categoryCount = count($zbp->categories); $tagCountResult = $zbp->db->Query("SELECT COUNT(*) as total FROM $tableTag"); $tagCount = (int) ($tagCountResult[0]['total'] ?? 0); $recipeViewsResult = $zbp->db->Query("SELECT SUM(log_ViewNums) as total FROM $tablePost WHERE log_Type = 0"); $recipeViews = (int) ($recipeViewsResult[0]['total'] ?? 0); $ingredientViewsResult = $zbp->db->Query("SELECT SUM(view_count) as total FROM $tableIngredientDetail"); $ingredientViews = (int) ($ingredientViewsResult[0]['total'] ?? 0); $totalViews = $recipeViews + $ingredientViews; $hotRecipes = array(); $hotRecipesSql = "SELECT log_ID, log_Title, log_ViewNums FROM $tablePost WHERE log_Type = 0 AND log_Status = 0 ORDER BY log_ViewNums DESC LIMIT 5"; $hotRecipesResults = $zbp->db->Query($hotRecipesSql); foreach ($hotRecipesResults as $row) { $hotRecipes[] = array( 'id' => (int) $row['log_ID'], 'title' => $row['log_Title'], 'view_count' => (int) $row['log_ViewNums'], 'url' => '?id=' . $row['log_ID'] ); } $hotIngredients = array(); $hotIngredientsSql = "SELECT ingredient_id, name, view_count FROM $tableIngredientDetail ORDER BY view_count DESC LIMIT 5"; $ingredients = $zbp->db->Query($hotIngredientsSql); foreach ($ingredients as $row) { $hotIngredients[] = array( 'id' => (int) $row['ingredient_id'], 'name' => $row['name'], 'view_count' => (int) ($row['view_count'] ?? 0) ); } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'recipe_count' => $recipeCount, 'ingredient_count' => $ingredientCount, 'relation_count' => $relationCount, 'category_count' => $categoryCount, 'tag_count' => $tagCount, 'total_views' => $totalViews, 'recipe_views' => $recipeViews, 'ingredient_views' => $ingredientViews, 'hot_recipes' => $hotRecipes, 'hot_ingredients' => $hotIngredients ) ); } // ==================== 辅助函数 ==================== function parse_tags($tagString) { global $zbp; $tags = array(); if (preg_match_all('/\{(\d+)\}/', $tagString, $matches)) { foreach ($matches[1] as $tagId) { $tag = $zbp->GetTagByID($tagId); if ($tag && $tag->ID > 0) { $tags[] = array('id' => $tag->ID, 'name' => $tag->Name); } } } return $tags; } function get_recipe_ingredients($recipeId) { global $zbp; $table = $zbp->db->dbpre . 'recipe_ingredient'; $sql = "SELECT * FROM $table WHERE log_id = $recipeId"; $results = $zbp->db->Query($sql); $ingredients = array(); foreach ($results as $row) { $ingredients[] = array( 'name' => $row['name'], 'amount' => $row['amount'] ?? '', 'unit' => $row['unit'] ?? '', 'type' => $row['type'] ?? '', 'detail_id' => (int) ($row['detail_id'] ?? 0) ); } return $ingredients; } function extract_cover($post) { $content = ''; if (is_object($post)) { $content = $post->Content ?? ''; } if (preg_match('/]+src=["\']([^"\']+)["\']/i', $content, $matches)) { return $matches[1]; } return ''; } function extract_cover_from_content($content) { if (preg_match('/]+src=["\']([^"\']+)["\']/i', $content, $matches)) { return $matches[1]; } return ''; } /** * 高级查询 - 精确查询和模糊查询 * * 参数: * - module: recipe(菜谱) / ingredient(食材) / category(分类) / tag(标签) * - field: 查询字段名 * - value: 查询值 * - operator: eq(等于) / like(模糊) / gt(大于) / lt(小于) / gte(大于等于) / lte(小于等于) / in(包含) * - fields: 返回字段(逗号分隔),如 id,title,views * - page: 页码 * - limit: 每页数量 * - order: 排序字段 * - sort: asc/desc * * 示例: * - ?act=query&module=recipe&field=process&value=炒 * - ?act=query&module=recipe&field=title&value=鸡蛋&operator=like * - ?act=query&module=ingredient&field=name&value=番茄&operator=like&fields=id,name */ function advanced_query() { global $zbp; $module = strtolower(trim($_GET['module'] ?? 'recipe')); $field = trim($_GET['field'] ?? ''); $value = $_GET['value'] ?? ''; $operator = strtolower(trim($_GET['operator'] ?? 'eq')); $fields = trim($_GET['fields'] ?? ''); $page = (int) ($_GET['page'] ?? 1); $limit = (int) ($_GET['limit'] ?? 20); $order = trim($_GET['order'] ?? ''); $sort = strtolower(trim($_GET['sort'] ?? 'desc')); if ($limit > 100) $limit = 100; if ($limit < 1) $limit = 20; if ($page < 1) $page = 1; $allowedModules = ['recipe', 'ingredient', 'category', 'tag']; if (!in_array($module, $allowedModules)) { return array('code' => 400, 'message' => '❌ module参数必须是: ' . implode(', ', $allowedModules)); } if (empty($field)) { return array('code' => 400, 'message' => '❌ 缺少field参数'); } $allowedOperators = ['eq', 'like', 'gt', 'lt', 'gte', 'lte', 'in', 'neq']; if (!in_array($operator, $allowedOperators)) { return array('code' => 400, 'message' => '❌ operator参数必须是: ' . implode(', ', $allowedOperators)); } $tableConfig = get_table_config($module); if (!$tableConfig) { return array('code' => 400, 'message' => '❌ 不支持的模块'); } $table = $tableConfig['table']; $primaryKey = $tableConfig['primary_key']; $allowedFields = $tableConfig['fields']; if (!in_array($field, $allowedFields)) { return array('code' => 400, 'message' => '❌ 不允许查询的字段: ' . $field . ',允许的字段: ' . implode(', ', $allowedFields)); } $selectFields = '*'; if (!empty($fields)) { $fieldArray = array_map('trim', explode(',', $fields)); $validFields = array_intersect($fieldArray, $allowedFields); if (!empty($validFields)) { if (!in_array($primaryKey, $validFields)) { array_unshift($validFields, $primaryKey); } $selectFields = implode(', ', $validFields); } } $whereClause = build_where_clause($field, $value, $operator); $offset = ($page - 1) * $limit; $countSql = "SELECT COUNT(*) as total FROM $table WHERE $whereClause"; $countResult = $zbp->db->Query($countSql); $total = (int) ($countResult[0]['total'] ?? 0); $orderClause = ''; if (!empty($order) && in_array($order, $allowedFields)) { $sortDir = ($sort === 'asc') ? 'ASC' : 'DESC'; $orderClause = "ORDER BY $order $sortDir"; } else { $orderClause = $tableConfig['default_order']; } $sql = "SELECT $selectFields FROM $table WHERE $whereClause $orderClause LIMIT $offset, $limit"; $results = $zbp->db->Query($sql); $list = format_results($results, $module, $fields); return array( 'code' => 200, 'message' => 'success', 'data' => array( 'module' => $module, 'query' => array( 'field' => $field, 'value' => $value, 'operator' => $operator ), 'list' => $list, 'page' => $page, 'limit' => $limit, 'total' => $total, 'total_pages' => ceil($total / $limit) ) ); } /** * 字段筛选 - 获取指定字段的所有值 * * 参数: * - module: recipe(菜谱) / ingredient(食材) * - field: 要筛选的字段 * - distinct: 是否去重(默认true) * * 示例: * - ?act=filter&module=recipe&field=process * - ?act=filter&module=recipe&field=taste */ function field_filter() { global $zbp; $module = strtolower(trim($_GET['module'] ?? 'recipe')); $field = trim($_GET['field'] ?? ''); $distinct = ($_GET['distinct'] ?? 'true') === 'true'; $allowedModules = ['recipe', 'ingredient']; if (!in_array($module, $allowedModules)) { return array('code' => 400, 'message' => '❌ module参数必须是: ' . implode(', ', $allowedModules)); } if (empty($field)) { return array('code' => 400, 'message' => '❌ 缺少field参数'); } $tableConfig = get_table_config($module); if (!$tableConfig) { return array('code' => 400, 'message' => '❌ 不支持的模块'); } $table = $tableConfig['table']; $allowedFields = $tableConfig['fields']; if (!in_array($field, $allowedFields)) { return array('code' => 400, 'message' => '❌ 不允许筛选的字段: ' . $field); } if ($distinct) { $sql = "SELECT DISTINCT $field FROM $table WHERE $field IS NOT NULL AND $field != '' ORDER BY $field ASC"; } else { $sql = "SELECT $field, COUNT(*) as count FROM $table WHERE $field IS NOT NULL AND $field != '' GROUP BY $field ORDER BY count DESC LIMIT 100"; } $results = $zbp->db->Query($sql); $list = array(); foreach ($results as $row) { if ($distinct) { $list[] = $row[$field]; } else { $list[] = array( 'value' => $row[$field], 'count' => (int) $row['count'] ); } } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'module' => $module, 'field' => $field, 'distinct' => $distinct, 'list' => $list, 'total' => count($list) ) ); } /** * 获取表配置 */ function get_table_config($module) { global $zbp; $configs = array( 'recipe' => array( 'table' => $zbp->db->dbpre . 'post', 'primary_key' => 'log_ID', 'fields' => ['log_ID', 'log_Title', 'log_CateID', 'log_AuthorID', 'log_Tag', 'log_Status', 'log_Type', 'log_IsTop', 'log_Intro', 'log_Content', 'log_CreateTime', 'log_PostTime', 'log_UpdateTime', 'log_CommNums', 'log_ViewNums'], 'default_order' => 'ORDER BY log_PostTime DESC' ), 'ingredient' => array( 'table' => $zbp->db->dbpre . 'ingredient_detail', 'primary_key' => 'ingredient_id', 'fields' => ['ingredient_id', 'name', 'alias', 'usage_tip', 'introduction', 'nutrition', 'guidance', 'effect', 'other', 'nutrients', 'create_time', 'update_time', 'author', 'view_count', 'cate_ID', 'allergen', 'allergen_type'], 'default_order' => 'ORDER BY ingredient_id ASC' ), 'category' => array( 'table' => $zbp->db->dbpre . 'category', 'primary_key' => 'cate_ID', 'fields' => ['cate_ID', 'cate_Name', 'cate_Order', 'cate_Type', 'cate_Count', 'cate_Alias', 'cate_ParentID', 'cate_RootID'], 'default_order' => 'ORDER BY cate_Order ASC' ), 'tag' => array( 'table' => $zbp->db->dbpre . 'tag', 'primary_key' => 'tag_ID', 'fields' => ['tag_ID', 'tag_Name', 'tag_Order', 'tag_Type', 'tag_Count', 'tag_Alias'], 'default_order' => 'ORDER BY tag_Count DESC' ) ); return $configs[$module] ?? null; } /** * 构建WHERE子句 */ function build_where_clause($field, $value, $operator) { $value = addslashes($value); switch ($operator) { case 'eq': return "$field = '$value'"; case 'neq': return "$field != '$value'"; case 'like': return "$field LIKE '%$value%'"; case 'gt': return "$field > '$value'"; case 'lt': return "$field < '$value'"; case 'gte': return "$field >= '$value'"; case 'lte': return "$field <= '$value'"; case 'in': $values = array_map(function($v) { return "'" . addslashes(trim($v)) . "'"; }, explode(',', $value)); return "$field IN (" . implode(',', $values) . ")"; default: return "$field = '$value'"; } } /** * 格式化查询结果 */ function format_results($results, $module, $fields) { global $zbp; $list = array(); foreach ($results as $row) { $item = array(); if ($module === 'recipe') { $item = array( 'id' => (int) $row['log_ID'], 'title' => $row['log_Title'] ?? '', 'intro' => $row['log_Intro'] ?? '', 'category_id' => (int) ($row['log_CateID'] ?? 0), 'author_id' => (int) ($row['log_AuthorID'] ?? 0), 'view_count' => (int) ($row['log_ViewNums'] ?? 0), 'comment_count' => (int) ($row['log_CommNums'] ?? 0), 'post_time' => (int) ($row['log_PostTime'] ?? 0), 'status' => (int) ($row['log_Status'] ?? 0), 'url' => '?act=detail&id=' . $row['log_ID'] ); $cate = $zbp->GetCategoryByID($item['category_id']); $item['category_name'] = $cate ? $cate->Name : ''; } elseif ($module === 'ingredient') { $item = array( 'id' => (int) $row['ingredient_id'], 'name' => $row['name'] ?? '', 'view_count' => (int) ($row['view_count'] ?? 0), 'author' => $row['author'] ?? '', 'cate_id' => (int) ($row['cate_ID'] ?? 0), 'create_time' => (int) ($row['create_time'] ?? 0), 'url' => '?act=ingredient_detail&id=' . $row['ingredient_id'] ); } elseif ($module === 'category') { $item = array( 'id' => (int) $row['cate_ID'], 'name' => $row['cate_Name'] ?? '', 'alias' => $row['cate_Alias'] ?? '', 'parent_id' => (int) ($row['cate_ParentID'] ?? 0), 'count' => (int) ($row['cate_Count'] ?? 0), 'order' => (int) ($row['cate_Order'] ?? 0) ); } elseif ($module === 'tag') { $item = array( 'id' => (int) $row['tag_ID'], 'name' => $row['tag_Name'] ?? '', 'alias' => $row['tag_Alias'] ?? '', 'count' => (int) ($row['tag_Count'] ?? 0), 'order' => (int) ($row['tag_Order'] ?? 0) ); } $list[] = $item; } return $list; } // ==================== 统一格式输出函数 ==================== /** * 统一列表输出 * @param string $type recipe/ingredient * @return array */ function get_unified_list($type) { global $zbp; $page = (int) ($_GET['page'] ?? 1); $limit = (int) ($_GET['limit'] ?? 20); $cateId = (int) ($_GET['cate_id'] ?? 0); $search = trim($_GET['search'] ?? ''); if ($limit > 100) $limit = 100; if ($limit < 1) $limit = 20; if ($page < 1) $page = 1; $offset = ($page - 1) * $limit; if ($type === 'recipe') { return get_recipe_list_unified($page, $limit, $offset, $cateId, $search); } else { return get_ingredient_list_unified($page, $limit, $offset, $cateId, $search); } } /** * 食谱列表(统一格式) */ function get_recipe_list_unified($page, $limit, $offset, $cateId, $search) { global $zbp; $tablePost = $zbp->db->dbpre . 'post'; $tableCategory = $zbp->db->dbpre . 'category'; $tablePostStat = $zbp->db->dbpre . 'post_stat'; $whereSql = "WHERE p.log_Type = 0 AND p.log_Status = 0"; if ($cateId > 0) { $whereSql .= " AND p.log_CateID = $cateId"; } if (!empty($search)) { $search = $zbp->db->EscapeString($search); $whereSql .= " AND (p.log_Title LIKE '%$search%' OR p.log_Intro LIKE '%$search%')"; } $countSql = "SELECT COUNT(*) as total FROM $tablePost p $whereSql"; $countResult = $zbp->db->Query($countSql); $total = (int) ($countResult[0]['total'] ?? 0); $sql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime, p.log_ViewNums, p.log_Tag, c.cate_Name, COALESCE(s.like_nums, 0) as like_nums, COALESCE(s.recommend_nums, 0) as recommend_nums 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 $whereSql ORDER BY p.log_PostTime DESC LIMIT $offset, $limit"; $results = $zbp->db->Query($sql); $list = array(); foreach ($results as $row) { $list[] = format_unified_item($row, 'recipe'); } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'type' => 'recipe', 'type_name' => '食谱', 'list' => $list, 'page' => $page, 'limit' => $limit, 'total' => $total, 'has_more' => ($page * $limit) < $total ) ); } /** * 食材列表(统一格式) */ function get_ingredient_list_unified($page, $limit, $offset, $cateId, $search) { global $zbp; $table = $zbp->db->dbpre . 'ingredient_detail'; $whereClauses = array(); if ($cateId > 0) { $whereClauses[] = "cate_ID = $cateId"; } if (!empty($search)) { $search = $zbp->db->EscapeString($search); $whereClauses[] = "name LIKE '%$search%'"; } $whereSql = empty($whereClauses) ? '' : 'WHERE ' . implode(' AND ', $whereClauses); $countSql = "SELECT COUNT(*) as total FROM $table $whereSql"; $countResult = $zbp->db->Query($countSql); $total = (int) ($countResult[0]['total'] ?? 0); $sql = "SELECT ingredient_id, name, view_count, allergen, allergen_type, introduction, cate_ID, create_time FROM $table $whereSql ORDER BY ingredient_id ASC LIMIT $offset, $limit"; $results = $zbp->db->Query($sql); $list = array(); foreach ($results as $row) { $list[] = format_unified_item($row, 'ingredient'); } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'type' => 'ingredient', 'type_name' => '食材', 'list' => $list, 'page' => $page, 'limit' => $limit, 'total' => $total, 'has_more' => ($page * $limit) < $total ) ); } /** * 统一详情输出 */ function get_unified_detail($type) { global $zbp; $id = (int) ($_GET['id'] ?? 0); if ($id <= 0) { return array('code' => 400, 'message' => '缺少ID参数'); } if ($type === 'recipe') { return get_recipe_detail_unified($id); } else { return get_ingredient_detail_unified($id); } } /** * 食谱详情(统一格式) */ function get_recipe_detail_unified($id) { global $zbp; $tablePost = $zbp->db->dbpre . 'post'; $tableCategory = $zbp->db->dbpre . 'category'; $tablePostStat = $zbp->db->dbpre . 'post_stat'; $tableRecipeIngredient = $zbp->db->dbpre . 'recipe_ingredient'; $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, c.cate_Name, COALESCE(s.like_nums, 0) as like_nums, COALESCE(s.recommend_nums, 0) as recommend_nums 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 p.log_ID = $id AND p.log_Type = 0 AND p.log_Status = 0 LIMIT 1"; $results = $zbp->db->Query($sql); if (empty($results)) { return array('code' => 404, 'message' => '食谱不存在'); } $row = $results[0]; $item = format_unified_item($row, 'recipe', true); $ingredientSql = "SELECT name, amount, unit FROM $tableRecipeIngredient WHERE log_id = $id"; $ingredientResults = $zbp->db->Query($ingredientSql); $item['ingredients'] = array(); foreach ($ingredientResults as $ing) { $item['ingredients'][] = array( 'name' => $ing['name'], 'amount' => $ing['amount'], 'unit' => $ing['unit'] ); } return array('code' => 200, 'message' => 'success', 'data' => $item); } /** * 食材详情(统一格式) */ function get_ingredient_detail_unified($id) { global $zbp; $table = $zbp->db->dbpre . 'ingredient_detail'; $tableStat = $zbp->db->dbpre . 'ingredient_stat'; $sql = "SELECT * FROM $table WHERE ingredient_id = $id LIMIT 1"; $results = $zbp->db->Query($sql); if (empty($results)) { return array('code' => 404, 'message' => '食材不存在'); } $row = $results[0]; $item = format_unified_item($row, 'ingredient', true); $statSql = "SELECT * FROM $tableStat WHERE ingredient_id = $id LIMIT 1"; $statResults = $zbp->db->Query($statSql); if (!empty($statResults)) { $stat = $statResults[0]; $item['statistics']['like_count'] = (int) ($stat['like_nums'] ?? 0); $item['statistics']['recommend_count'] = (int) ($stat['recommend_nums'] ?? 0); } return array('code' => 200, 'message' => 'success', 'data' => $item); } /** * 统一搜索 */ function get_unified_search($type) { global $zbp; $keyword = trim($_GET['keyword'] ?? ''); $page = (int) ($_GET['page'] ?? 1); $limit = (int) ($_GET['limit'] ?? 20); if (empty($keyword)) { return array('code' => 400, 'message' => '缺少搜索关键词'); } if ($limit > 100) $limit = 100; $offset = ($page - 1) * $limit; $keyword = $zbp->db->EscapeString($keyword); if ($type === 'recipe') { $tablePost = $zbp->db->dbpre . 'post'; $tableCategory = $zbp->db->dbpre . 'category'; $countSql = "SELECT COUNT(*) as total FROM $tablePost WHERE log_Type = 0 AND log_Status = 0 AND (log_Title LIKE '%$keyword%' OR log_Intro LIKE '%$keyword%')"; $countResult = $zbp->db->Query($countSql); $total = (int) ($countResult[0]['total'] ?? 0); $sql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime, p.log_ViewNums, c.cate_Name FROM $tablePost p LEFT JOIN $tableCategory c ON p.log_CateID = c.cate_ID WHERE p.log_Type = 0 AND p.log_Status = 0 AND (p.log_Title LIKE '%$keyword%' OR p.log_Intro LIKE '%$keyword%') ORDER BY p.log_PostTime DESC LIMIT $offset, $limit"; $results = $zbp->db->Query($sql); $list = array(); foreach ($results as $row) { $list[] = format_unified_item($row, 'recipe'); } } else { $table = $zbp->db->dbpre . 'ingredient_detail'; $countSql = "SELECT COUNT(*) as total FROM $table WHERE name LIKE '%$keyword%'"; $countResult = $zbp->db->Query($countSql); $total = (int) ($countResult[0]['total'] ?? 0); $sql = "SELECT ingredient_id, name, view_count, allergen, allergen_type FROM $table WHERE name LIKE '%$keyword%' ORDER BY ingredient_id ASC LIMIT $offset, $limit"; $results = $zbp->db->Query($sql); $list = array(); foreach ($results as $row) { $list[] = format_unified_item($row, 'ingredient'); } } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'type' => $type, 'type_name' => $type === 'recipe' ? '食谱' : '食材', 'keyword' => $keyword, 'list' => $list, 'page' => $page, 'limit' => $limit, 'total' => $total, 'has_more' => ($page * $limit) < $total ) ); } /** * 统一热门 */ function get_unified_hot($type) { global $zbp; $limit = (int) ($_GET['limit'] ?? 20); if ($limit > 50) $limit = 50; if ($type === 'recipe') { $tablePost = $zbp->db->dbpre . 'post'; $tableCategory = $zbp->db->dbpre . 'category'; $sql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime, p.log_ViewNums, c.cate_Name FROM $tablePost p LEFT JOIN $tableCategory c ON p.log_CateID = c.cate_ID WHERE p.log_Type = 0 AND p.log_Status = 0 ORDER BY p.log_ViewNums DESC LIMIT $limit"; $results = $zbp->db->Query($sql); $list = array(); foreach ($results as $row) { $list[] = format_unified_item($row, 'recipe'); } } else { $table = $zbp->db->dbpre . 'ingredient_detail'; $sql = "SELECT ingredient_id, name, view_count, allergen, allergen_type FROM $table ORDER BY view_count DESC LIMIT $limit"; $results = $zbp->db->Query($sql); $list = array(); foreach ($results as $row) { $list[] = format_unified_item($row, 'ingredient'); } } return array( 'code' => 200, 'message' => 'success', 'data' => array( 'type' => $type, 'type_name' => $type === 'recipe' ? '食谱' : '食材', 'list' => $list, 'limit' => $limit ) ); } /** * 格式化统一输出项 */ function format_unified_item($row, $type, $isDetail = false) { if ($type === 'recipe') { $item = array( 'id' => (int) $row['log_ID'], 'type' => 'recipe', 'type_name' => '食谱', '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'] ?? '' ), 'statistics' => array( 'view_count' => (int) ($row['log_ViewNums'] ?? 0), 'like_count' => (int) ($row['like_nums'] ?? 0), 'recommend_count' => (int) ($row['recommend_nums'] ?? 0) ), 'publish_time' => strtotime($row['log_PostTime']), 'url' => '?act=unified_detail&type=recipe&id=' . $row['log_ID'] ); if ($isDetail) { $item['content'] = $row['log_Content'] ?? ''; $item['tags'] = parse_tags($row['log_Tag'] ?? ''); } } else { $allergenType = json_decode($row['allergen_type'] ?? '[]', true); $allergen = json_decode($row['allergen'] ?? '[]', true); $item = array( 'id' => (int) $row['ingredient_id'], 'type' => 'ingredient', 'type_name' => '食材', 'title' => $row['name'], 'intro' => mb_substr(strip_tags($row['introduction'] ?? ''), 0, 100), 'category' => array( 'id' => (int) ($row['cate_ID'] ?? 0), 'name' => '' ), 'statistics' => array( 'view_count' => (int) ($row['view_count'] ?? 0), 'like_count' => 0, 'recommend_count' => 0 ), 'publish_time' => strtotime($row['create_time'] ?? 'now'), 'url' => '?act=unified_detail&type=ingredient&id=' . $row['ingredient_id'] ); if (!empty($allergenType)) { $item['allergen_type'] = $allergenType; } if (!empty($allergen)) { $item['allergen'] = $allergen; } if ($isDetail) { $item['content'] = $row['introduction'] ?? ''; $item['usage_tip'] = $row['usage_tip'] ?? ''; $item['nutrition'] = json_decode($row['nutrition'] ?? '[]', true); $item['effect'] = $row['effect'] ?? ''; } } return $item; }