Files
kitchen/docs/api/api_discover.php
Developer 5ce8759a18 release
2026-04-13 10:00:55 +08:00

547 lines
16 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
/**
* 发现页随机数据接口
*
* @file api_discover.php
* @author AI Assistant
* @date 2026-04-12
* @version 1.1.0
* @desc 提供随机数据用于首页/发现页瀑布流,支持响应式布局
* 同一客户端多次请求返回不重复数据30分钟后重置
* @lastUpdate 2026-04-12 添加已返回ID排除机制避免重复数据
*/
$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=5');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$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;
}
} else {
$params = array();
}
$format = ApiResponse::getFormat();
$forceRefresh = isset($params['_refresh']) && $params['_refresh'] === '1';
$clientIP = getClientIP();
header('X-Cache: MISS');
$counts = calculateCounts($params);
$data = array(
'recipes' => getRandomRecipes($counts['recipe'], $clientIP),
'ingredients' => getRandomIngredients($counts['ingredient'], $clientIP),
'categories' => getRandomCategories($counts['category'], $clientIP),
'tags' => getRandomTags($counts['tag'], $clientIP),
'nutrition_types' => getNutritionTypes($counts['nutrition'], $clientIP),
'meal_times' => getMealTimes($counts['meal_time'])
);
$recipeIds = array_column($data['recipes'], 'id');
$ingredientIds = array_column($data['ingredients'], 'id');
$categoryIds = array_column($data['categories'], 'id');
$tagIds = array_column($data['tags'], 'id');
$nutritionNames = array_column($data['nutrition_types'], 'name');
if (!empty($recipeIds)) {
ApiCache::addDiscoverReturnedIds($clientIP, 'recipe', $recipeIds);
}
if (!empty($ingredientIds)) {
ApiCache::addDiscoverReturnedIds($clientIP, 'ingredient', $ingredientIds);
}
if (!empty($categoryIds)) {
ApiCache::addDiscoverReturnedIds($clientIP, 'category', $categoryIds);
}
if (!empty($tagIds)) {
ApiCache::addDiscoverReturnedIds($clientIP, 'tag', $tagIds);
}
if (!empty($nutritionNames)) {
ApiCache::addDiscoverReturnedIds($clientIP, 'nutrition', $nutritionNames);
}
$result = array(
'code' => 200,
'message' => 'success',
'data' => $data,
'meta' => array(
'cache_status' => 'fresh',
'counts' => $counts
)
);
$result['_query_time'] = round((microtime(true) - $startTime) * 1000, 2) . 'ms';
ApiResponse::output($result, $format);
exit;
/**
* 获取客户端IP
*/
function getClientIP() {
$ip = '';
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
}
return trim($ip) ?: 'unknown';
}
/**
* 计算各类型数量
*/
function calculateCounts($params) {
$total = isset($params['total']) ? min((int) $params['total'], 100) : 50;
$ratios = array(
'recipe' => 0.75,
'ingredient' => 0.03,
'category' => 0.10,
'tag' => 0.03,
'nutrition' => 0.02,
'meal_time' => 0.02
);
$counts = array();
$specifiedTotal = 0;
$specifiedTypes = array();
foreach ($ratios as $type => $ratio) {
$paramName = $type === 'nutrition' ? 'nutrition' : $type;
if (isset($params[$paramName]) && (int) $params[$paramName] > 0) {
$counts[$type] = min((int) $params[$paramName], 80);
$specifiedTotal += $counts[$type];
$specifiedTypes[] = $type;
}
}
if (count($specifiedTypes) === count($ratios)) {
return $counts;
}
$remaining = $total - $specifiedTotal;
$remainingTypes = array_diff(array_keys($ratios), $specifiedTypes);
$remainingRatioSum = 0;
foreach ($remainingTypes as $type) {
$remainingRatioSum += $ratios[$type];
}
foreach ($remainingTypes as $type) {
$adjustedRatio = $remainingRatioSum > 0 ? $ratios[$type] / $remainingRatioSum : 1 / count($remainingTypes);
$counts[$type] = max(1, round($remaining * $adjustedRatio));
}
return $counts;
}
/**
* 获取随机菜品
*/
function getRandomRecipes($limit, $clientIP = '') {
global $zbp;
if ($limit <= 0) return array();
$tablePost = $zbp->db->dbpre . 'post';
$tableCategory = $zbp->db->dbpre . 'category';
$tablePostStat = $zbp->db->dbpre . 'post_stat';
$tableIdMap = $zbp->db->dbpre . 'recipe_id_map';
$excludeIds = array();
if (!empty($clientIP)) {
$excludeIds = ApiCache::getDiscoverReturnedIds($clientIP, 'recipe');
}
$whereClause = "p.log_Type = 0 AND p.log_Status = 0";
if (!empty($excludeIds)) {
$excludeIdsStr = implode(',', array_map('intval', $excludeIds));
$whereClause .= " AND p.log_ID NOT IN ($excludeIdsStr)";
}
$sql = "SELECT p.log_ID, p.log_Title, p.log_CateID, p.log_ViewNums,
c.cate_Name,
s.rate_nums, s.rate_score,
m.old_id
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
LEFT JOIN $tableIdMap m ON p.log_ID = m.new_log_id
WHERE $whereClause
ORDER BY RAND()
LIMIT $limit";
$results = $zbp->db->Query($sql);
if (empty($results) && !empty($excludeIds)) {
ApiCache::clearDiscoverReturnedIds($clientIP, 'recipe');
return getRandomRecipes($limit, $clientIP);
}
$list = array();
foreach ($results as $row) {
$rateNums = (int) ($row['rate_nums'] ?? 0);
$rateScore = (float) ($row['rate_score'] ?? 0);
$rating = ApiResponse::getRatingSummary($rateScore, $rateNums);
$cover = '';
if (!empty($row['old_id'])) {
$cover = 'http://eat.wktyl.com/api/assets/pic/' . $row['old_id'] . 'a.jpg';
}
$list[] = array(
'id' => (int) $row['log_ID'],
'title' => $row['log_Title'],
'cover' => $cover,
'category' => array(
'id' => (int) $row['log_CateID'],
'name' => $row['cate_Name'] ?? ''
),
'rating' => $rating,
'views' => (int) $row['log_ViewNums']
);
}
return $list;
}
/**
* 获取随机食材
*/
function getRandomIngredients($limit, $clientIP = '') {
global $zbp;
if ($limit <= 0) return array();
$tableIngredient = $zbp->db->dbpre . 'ingredient_detail';
$tableCategory = $zbp->db->dbpre . 'category';
$excludeIds = array();
if (!empty($clientIP)) {
$excludeIds = ApiCache::getDiscoverReturnedIds($clientIP, 'ingredient');
}
$whereClause = "1=1";
if (!empty($excludeIds)) {
$excludeIdsStr = implode(',', array_map('intval', $excludeIds));
$whereClause .= " AND i.ingredient_id NOT IN ($excludeIdsStr)";
}
$sql = "SELECT i.ingredient_id, i.name, i.cate_ID, i.allergen, i.introduction,
c.cate_Name
FROM $tableIngredient i
LEFT JOIN $tableCategory c ON i.cate_ID = c.cate_ID
WHERE $whereClause
ORDER BY RAND()
LIMIT $limit";
$results = $zbp->db->Query($sql);
if (empty($results) && !empty($excludeIds)) {
ApiCache::clearDiscoverReturnedIds($clientIP, 'ingredient');
return getRandomIngredients($limit, $clientIP);
}
$list = array();
foreach ($results as $row) {
$allergen = array();
if (!empty($row['allergen'])) {
$decoded = json_decode($row['allergen'], true);
$allergen = is_array($decoded) ? $decoded : array($row['allergen']);
}
$intro = $row['introduction'] ?? '';
if (mb_strlen($intro) > 100) {
$intro = mb_substr($intro, 0, 100) . '...';
}
$list[] = array(
'id' => (int) $row['ingredient_id'],
'name' => $row['name'],
'category' => array(
'id' => (int) $row['cate_ID'],
'name' => $row['cate_Name'] ?? ''
),
'allergen' => $allergen,
'intro' => $intro
);
}
return $list;
}
/**
* 获取随机分类
*/
function getRandomCategories($limit, $clientIP = '') {
global $zbp;
if ($limit <= 0) return array();
$tableCategory = $zbp->db->dbpre . 'category';
$tableIngredient = $zbp->db->dbpre . 'ingredient_detail';
$excludeIds = array();
if (!empty($clientIP)) {
$excludeIds = ApiCache::getDiscoverReturnedIds($clientIP, 'category');
}
$excludeClause = '';
if (!empty($excludeIds)) {
$excludeIdsStr = implode(',', array_map('intval', $excludeIds));
$excludeClause = " AND cate_ID NOT IN ($excludeIdsStr)";
}
$recipeLimit = max(1, ceil($limit * 0.6));
$ingredientLimit = $limit - $recipeLimit;
$results = array();
$sqlRecipe = "SELECT cate_ID, cate_Name, cate_Count, cate_ParentID
FROM $tableCategory
WHERE cate_ID > 10 AND cate_ID < 1000 AND cate_ParentID != 11 $excludeClause
ORDER BY RAND()
LIMIT $recipeLimit";
$recipeResults = $zbp->db->Query($sqlRecipe);
$results = array_merge($results, $recipeResults);
$sqlIngredient = "SELECT cate_ID, cate_Name, cate_Count, cate_ParentID
FROM $tableCategory
WHERE cate_ID >= 1000 $excludeClause
ORDER BY RAND()
LIMIT $ingredientLimit";
$ingredientResults = $zbp->db->Query($sqlIngredient);
$results = array_merge($results, $ingredientResults);
if (empty($results) && !empty($excludeIds)) {
ApiCache::clearDiscoverReturnedIds($clientIP, 'category');
return getRandomCategories($limit, $clientIP);
}
$ingredientCounts = array();
$cateIds = array_column($results, 'cate_ID');
if (!empty($cateIds)) {
$cateIdsStr = implode(',', array_map('intval', $cateIds));
$sqlIngredientCount = "SELECT cate_ID, COUNT(*) as cnt FROM $tableIngredient WHERE cate_ID IN ($cateIdsStr) GROUP BY cate_ID";
$ingredientCountResults = $zbp->db->Query($sqlIngredientCount);
foreach ($ingredientCountResults as $row) {
$ingredientCounts[$row['cate_ID']] = (int) $row['cnt'];
}
}
$parentIds = array_unique(array_filter(array_column($results, 'cate_ParentID')));
$parentNames = array();
if (!empty($parentIds)) {
$parentIdsStr = implode(',', array_map('intval', $parentIds));
$sqlParent = "SELECT cate_ID, cate_Name FROM $tableCategory WHERE cate_ID IN ($parentIdsStr)";
$parentResults = $zbp->db->Query($sqlParent);
foreach ($parentResults as $row) {
$parentNames[$row['cate_ID']] = $row['cate_Name'];
}
}
$list = array();
foreach ($results as $row) {
$type = 'recipe';
if ($row['cate_ParentID'] >= 1000 || $row['cate_ID'] >= 1000) {
$type = 'ingredient';
} elseif ($row['cate_ParentID'] == 11) {
$type = 'recipe_main';
}
$recipeCount = (int) ($row['cate_Count'] ?? 0);
$ingredientCount = isset($ingredientCounts[$row['cate_ID']]) ? $ingredientCounts[$row['cate_ID']] : 0;
$parentId = (int) $row['cate_ParentID'];
$parentName = isset($parentNames[$parentId]) ? $parentNames[$parentId] : '';
$list[] = array(
'id' => (int) $row['cate_ID'],
'name' => $row['cate_Name'],
'type' => $type,
'recipe_count' => $recipeCount,
'ingredient_count' => $ingredientCount,
'count' => $type === 'ingredient' ? $ingredientCount : $recipeCount,
'parent_id' => $parentId,
'parent_name' => $parentName
);
}
shuffle($list);
return $list;
}
/**
* 获取随机标签
*/
function getRandomTags($limit, $clientIP = '') {
global $zbp;
if ($limit <= 0) return array();
$tableTag = $zbp->db->dbpre . 'tag';
$excludeIds = array();
if (!empty($clientIP)) {
$excludeIds = ApiCache::getDiscoverReturnedIds($clientIP, 'tag');
}
$whereClause = "tag_Alias IN ('口味', '做法')";
if (!empty($excludeIds)) {
$excludeIdsStr = implode(',', array_map('intval', $excludeIds));
$whereClause .= " AND tag_ID NOT IN ($excludeIdsStr)";
}
$sql = "SELECT tag_ID, tag_Name, tag_Alias, tag_Count
FROM $tableTag
WHERE $whereClause
ORDER BY RAND()
LIMIT $limit";
$results = $zbp->db->Query($sql);
if (empty($results) && !empty($excludeIds)) {
ApiCache::clearDiscoverReturnedIds($clientIP, 'tag');
return getRandomTags($limit, $clientIP);
}
$list = array();
foreach ($results as $row) {
$type = 'taste';
if ($row['tag_Alias'] === '做法') {
$type = 'cooking';
}
$list[] = array(
'id' => (int) $row['tag_ID'],
'name' => $row['tag_Name'],
'type' => $type,
'count' => (int) ($row['tag_Count'] ?? 0)
);
}
return $list;
}
/**
* 获取营养成分类型
*/
function getNutritionTypes($limit, $clientIP = '') {
global $zbp;
if ($limit <= 0) return array();
$tableNutrition = $zbp->db->dbpre . 'recipe_nutrition';
$excludeNames = array();
if (!empty($clientIP)) {
$excludeNames = ApiCache::getDiscoverReturnedIds($clientIP, 'nutrition');
}
$whereClause = "name IS NOT NULL AND name != ''";
if (!empty($excludeNames)) {
$escapedNames = array_map(function($name) use ($zbp) {
return $zbp->db->EscapeString($name);
}, $excludeNames);
$excludeNamesStr = "'" . implode("','", $escapedNames) . "'";
$whereClause .= " AND name NOT IN ($excludeNamesStr)";
}
$sql = "SELECT DISTINCT name, unit
FROM $tableNutrition
WHERE $whereClause
ORDER BY RAND()
LIMIT $limit";
$results = $zbp->db->Query($sql);
if (empty($results) && !empty($excludeNames)) {
ApiCache::clearDiscoverReturnedIds($clientIP, 'nutrition');
return getNutritionTypes($limit, $clientIP);
}
$list = array();
foreach ($results as $row) {
$list[] = array(
'name' => $row['name'],
'unit' => $row['unit'] ?? ''
);
}
return $list;
}
/**
* 获取用餐时段
*/
function getMealTimes($limit) {
global $zbp;
if ($limit <= 0) return array();
$tablePost = $zbp->db->dbpre . 'post';
$sql = "SELECT log_Intro FROM $tablePost
WHERE log_Type = 0 AND log_Status = 0
AND log_Intro IS NOT NULL AND log_Intro != ''
LIMIT 500";
$results = $zbp->db->Query($sql);
$mealTimes = array();
foreach ($results as $row) {
$times = preg_split('/[、,,]/u', $row['log_Intro']);
foreach ($times as $time) {
$time = trim($time);
if (!empty($time) && mb_strlen($time) <= 10) {
if (!isset($mealTimes[$time])) {
$mealTimes[$time] = 0;
}
$mealTimes[$time]++;
}
}
}
arsort($mealTimes);
$list = array();
$id = 1;
foreach ($mealTimes as $name => $count) {
if ($id > $limit) break;
$list[] = array(
'id' => $id++,
'name' => $name,
'count' => $count
);
}
return $list;
}