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;
}