547 lines
16 KiB
PHP
547 lines
16 KiB
PHP
<?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;
|
||
}
|