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

612 lines
21 KiB
PHP

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