Files
kitchen/docs/api/api.php
Developer 13fdbdc431 瀑布流
2026-04-13 03:39:29 +08:00

1923 lines
65 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 🍳 菜谱API接口 - 静态接口(只读)
*
* 独立API文件直接访问: /api/api.php?act=xxx
* 动态接口: /api/api_action.php?act=xxx
* 管理页面: /api/
*
* 性能优化:支持文件缓存,减少数据库查询
* 格式支持JSON、Gzip、MessagePack、CBOR
*/
$startTime = microtime(true);
require '../zb_system/function/c_system_base.php';
$zbp->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 'mini':
$result = recipe_mini();
break;
case 'index':
default:
$result = array(
'code' => 200,
'message' => '🍳 菜谱API服务正常运行',
'data' => array(
'version' => '1.29.0',
'endpoints' => array(
'list' => '?act=list',
'detail' => '?act=detail&id=1',
'full' => '?act=full&id=1',
'mini' => '?act=mini&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'] ?? '');
if ($limit > 100) $limit = 100;
if ($limit < 1) $limit = 20;
if ($page < 1) $page = 1;
$tablePost = $zbp->db->dbpre . 'post';
$tableCategory = $zbp->db->dbpre . 'category';
$tablePostStat = $zbp->db->dbpre . 'post_stat';
$offset = ($page - 1) * $limit;
$whereSql = "WHERE p.log_Type = 0 AND p.log_Status = 0";
if ($cateId > 0) {
$whereSql .= " AND p.log_CateID = $cateId";
}
if ($tagId > 0) {
$whereSql .= " AND p.log_Tag LIKE '%{$tagId}%'";
}
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, COALESCE(s.rate_nums, 0) as rate_nums, COALESCE(s.rate_score, 0) as rate_score";
$sql = "SELECT $selectFields 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) {
$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'] ?? ''),
'rating' => ApiResponse::getRatingSummary(
(float) ($row['rate_score'] ?? 0),
(int) ($row['rate_nums'] ?? 0)
)
);
}
return array(
'code' => 200,
'message' => 'success',
'data' => array(
'list' => $list,
'page' => $page,
'limit' => $limit,
'total' => $total
)
);
}
/**
* 获取菜谱详情
*/
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);
}
$tableRecipeIdMap = $zbp->db->dbpre . 'recipe_id_map';
$tablePostStat = $zbp->db->dbpre . 'post_stat';
$idMapSql = "SELECT old_id FROM $tableRecipeIdMap WHERE new_log_id = $id LIMIT 1";
$idMapResult = $zbp->db->Query($idMapSql);
$picId = !empty($idMapResult) ? (int) $idMapResult[0]['old_id'] : null;
$statSql = "SELECT rate_nums, rate_score FROM $tablePostStat WHERE log_id = $id LIMIT 1";
$statResult = $zbp->db->Query($statSql);
$rateNums = 0;
$rateScore = 0.00;
if (!empty($statResult)) {
$rateNums = (int) ($statResult[0]['rate_nums'] ?? 0);
$rateScore = (float) ($statResult[0]['rate_score'] ?? 0);
}
$meta = json_decode($post->Meta, true) ?: array();
$ingredients = get_recipe_ingredients($id);
return array(
'code' => 200,
'message' => 'success',
'data' => array(
'id' => $post->ID,
'pic_id' => $picId,
'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,
'rating' => ApiResponse::formatRating($rateScore, $rateNums)
)
);
}
/**
* 获取菜谱迷你信息
* 返回简化的核心信息code, pic_id, title, intro, category, allergens, nutrition
* 适用于列表页、卡片展示等需要快速加载的场景
* @return array 迷你版菜谱信息
*/
function recipe_mini() {
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';
$tableRecipeIdMap = $zbp->db->dbpre . 'recipe_id_map';
$tableRecipeIngredient = $zbp->db->dbpre . 'recipe_ingredient';
$tableIngredientDetail = $zbp->db->dbpre . 'ingredient_detail';
$tableRecipeNutrition = $zbp->db->dbpre . 'recipe_nutrition';
$tablePostStat = $zbp->db->dbpre . 'post_stat';
$sql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID,
c.cate_Name as cate_name, c.cate_ParentID as cate_parent_id,
COALESCE(s.rate_nums, 0) as rate_nums, COALESCE(s.rate_score, 0) as rate_score
FROM $tablePost p
LEFT JOIN $tableCategory c ON p.log_CateID = c.cate_ID
LEFT JOIN $tablePostStat s ON p.log_ID = s.log_id
WHERE p.log_ID = $id AND p.log_Type = 0 AND p.log_Status = 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'];
}
$allergenSql = "SELECT DISTINCT id.allergen
FROM $tableRecipeIngredient ri
INNER JOIN $tableIngredientDetail id ON ri.detail_id = id.ingredient_id
WHERE ri.log_id = $id AND id.allergen IS NOT NULL AND id.allergen != '' AND id.allergen != '[]'";
$allergenResults = $zbp->db->Query($allergenSql);
$allergens = array();
foreach ($allergenResults as $aRow) {
$aList = json_decode($aRow['allergen'] ?? '[]', true);
if (is_array($aList)) {
$allergens = array_merge($allergens, $aList);
}
}
$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 $nRow) {
$nutrition[] = array(
'name' => $nRow['name'],
'value' => (float) $nRow['value'],
'unit' => $nRow['unit']
);
}
$categoryHierarchy = array();
$currentCateId = (int) $row['cate_parent_id'];
while ($currentCateId > 0) {
$cateSql = "SELECT cate_ID, cate_Name, cate_ParentID FROM $tableCategory WHERE cate_ID = $currentCateId LIMIT 1";
$cateResult = $zbp->db->Query($cateSql);
if (!empty($cateResult)) {
$cateRow = $cateResult[0];
array_unshift($categoryHierarchy, array(
'id' => (int) $cateRow['cate_ID'],
'name' => $cateRow['cate_Name']
));
$currentCateId = (int) $cateRow['cate_ParentID'];
} else {
break;
}
}
$categoryHierarchy[] = array(
'id' => (int) $row['log_CateID'],
'name' => $row['cate_name']
);
return array(
'code' => 200,
'message' => 'success',
'data' => array(
'id' => (int) $row['log_ID'],
'code' => 'CP' . str_pad($row['log_ID'], 5, '0', STR_PAD_LEFT),
'pic_id' => $picId,
'title' => $row['log_Title'],
'intro' => $row['log_Intro'],
'category' => array(
'id' => (int) $row['log_CateID'],
'name' => $row['cate_name'],
'hierarchy' => $categoryHierarchy
),
'allergens' => $allergens,
'nutrition' => $nutrition,
'rating' => ApiResponse::getRatingSummary(
(float) ($row['rate_score'] ?? 0),
(int) ($row['rate_nums'] ?? 0)
)
)
);
}
/**
* 获取菜谱完整信息
* 包含:基本信息、食材详情、营养、统计、标签、分类层级等所有信息
* @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.rate_nums, 0) as rate_nums,
COALESCE(s.rate_score, 0) as rate_score
FROM $tablePost p
LEFT JOIN $tableCategory c ON p.log_CateID = c.cate_ID
LEFT JOIN $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('/<img[^>]+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),
'rate_count' => (int) ($row['rate_nums'] ?? 0),
'rate_score' => (float) ($row['rate_score'] ?? 0)
),
'rating' => ApiResponse::formatRating(
(float) ($row['rate_score'] ?? 0),
(int) ($row['rate_nums'] ?? 0)
),
'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'] ?? '');
if ($limit > 100) $limit = 100;
if ($limit < 1) $limit = 20;
if ($page < 1) $page = 1;
$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();
foreach ($results as $row) {
$allergenType = json_decode($row['allergen_type'] ?? '[]', true);
$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
)
);
}
/**
* 获取食材详情
*/
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('/<img[^>]+src=["\']([^"\']+)["\']/i', $content, $matches)) {
return $matches[1];
}
return '';
}
function extract_cover_from_content($content) {
if (preg_match('/<img[^>]+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;
}