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

437 lines
14 KiB
PHP
Raw Permalink 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_feed.php
* @author AI Assistant
* @date 2026-04-12
* @version 2.1.0
* @desc 提供推荐流、最新流、热门流
* @lastUpdate 2026-04-12 添加评分显示功能,更新字段名 rate_nums/rate_score
*/
$startTime = microtime(true);
define('ZBP_HOOKERROR', false);
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');
$act = strtolower(trim($_GET['act'] ?? 'feed'));
$format = ApiResponse::getFormat();
$forceRefresh = isset($_GET['_refresh']) && $_GET['_refresh'] === '1';
$staleMode = isset($_GET['_stale']) && $_GET['_stale'] === '1';
$cacheableActs = array('feed', 'recommend', 'latest', 'hot');
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('feed', $cacheParams);
if ($cachedResult !== null) {
header('X-Cache: HIT');
$cachedResult['_cached'] = true;
$cachedResult['_cache_age'] = ApiCache::getCacheAge('feed', $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('feed', $cacheParams);
if ($staleResult !== null) {
header('X-Cache: STALE');
$staleResult['_cached'] = true;
$staleResult['_stale'] = true;
$staleResult['_cache_age'] = ApiCache::getCacheAge('feed', $cacheParams);
$staleResult['_query_time'] = round((microtime(true) - $startTime) * 1000, 2) . 'ms';
ApiResponse::output($staleResult, $format);
exit;
}
}
}
header('X-Cache: MISS');
$result = array();
switch ($act) {
case 'feed':
case 'recommend':
$result = get_recommend_feed();
break;
case 'latest':
$result = get_latest_feed();
break;
case 'hot':
$result = get_hot_feed();
break;
case 'index':
default:
$result = get_feed_index();
break;
}
if (in_array($act, $cacheableActs) && $result['code'] === 200) {
$cacheParams = $_GET;
unset($cacheParams['act']);
unset($cacheParams['_format']);
unset($cacheParams['_pretty']);
ApiCache::set('feed', $cacheParams, $result);
}
$result['_query_time'] = round((microtime(true) - $startTime) * 1000, 2) . 'ms';
ApiResponse::output($result, $format);
exit;
/**
* 信息流接口索引
*/
function get_feed_index() {
return array(
'code' => 200,
'message' => 'success',
'data' => array(
'description' => '信息流接口',
'types' => array(
array(
'type' => 'recommend',
'name' => '推荐流',
'description' => '热门+最新+随机混合推荐',
'example' => '?act=recommend&page=1'
),
array(
'type' => 'latest',
'name' => '最新流',
'description' => '按发布时间排序',
'example' => '?act=latest&page=1'
),
array(
'type' => 'hot',
'name' => '热门流',
'description' => '按浏览量排序',
'example' => '?act=hot&page=1'
)
),
'params' => array(
'page' => '页码默认1',
'limit' => '每页数量默认20最大50',
'cate_id' => '分类筛选',
'exclude' => '排除已读ID逗号分隔'
)
)
);
}
/**
* 推荐信息流(混合推荐)
*/
function get_recommend_feed() {
global $zbp;
$page = (int) ($_GET['page'] ?? 1);
$limit = (int) ($_GET['limit'] ?? 20);
$cateId = (int) ($_GET['cate_id'] ?? 0);
$excludeIds = isset($_GET['exclude']) ? array_map('intval', explode(',', $_GET['exclude'])) : array();
if ($limit > 50) $limit = 50;
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 (!empty($excludeIds)) {
$excludeList = implode(',', $excludeIds);
$whereSql .= " AND p.log_ID NOT IN ($excludeList)";
}
$hotLimit = (int) ($limit * 0.4);
$latestLimit = (int) ($limit * 0.4);
$randomLimit = $limit - $hotLimit - $latestLimit;
$hotItems = array();
$hotSql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime,
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
$whereSql
ORDER BY p.log_ViewNums DESC
LIMIT $hotLimit";
$hotResults = $zbp->db->Query($hotSql);
foreach ($hotResults as $row) {
$hotItems[] = format_feed_item($row, 'hot');
}
$latestItems = array();
$latestSql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime,
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
$whereSql
ORDER BY p.log_PostTime DESC
LIMIT $latestLimit";
$latestResults = $zbp->db->Query($latestSql);
foreach ($latestResults as $row) {
$latestItems[] = format_feed_item($row, 'latest');
}
$randomItems = array();
$randomSql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime,
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
$whereSql
ORDER BY RAND()
LIMIT $randomLimit";
$randomResults = $zbp->db->Query($randomSql);
foreach ($randomResults as $row) {
$randomItems[] = format_feed_item($row, 'random');
}
$allItems = array_merge($hotItems, $latestItems, $randomItems);
shuffle($allItems);
$countSql = "SELECT COUNT(*) as total FROM $tablePost p $whereSql";
$countResult = $zbp->db->Query($countSql);
$total = (int) ($countResult[0]['total'] ?? 0);
return array(
'code' => 200,
'message' => 'success',
'data' => array(
'type' => 'recommend',
'list' => $allItems,
'page' => $page,
'limit' => $limit,
'total' => $total,
'has_more' => ($page * $limit) < $total,
'mix_ratio' => array(
'hot' => $hotLimit,
'latest' => $latestLimit,
'random' => $randomLimit
)
)
);
}
/**
* 最新信息流
*/
function get_latest_feed() {
global $zbp;
$page = (int) ($_GET['page'] ?? 1);
$limit = (int) ($_GET['limit'] ?? 20);
$cateId = (int) ($_GET['cate_id'] ?? 0);
$excludeIds = isset($_GET['exclude']) ? array_map('intval', explode(',', $_GET['exclude'])) : array();
if ($limit > 50) $limit = 50;
if ($limit < 1) $limit = 20;
$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 (!empty($excludeIds)) {
$excludeList = implode(',', $excludeIds);
$whereSql .= " AND p.log_ID NOT IN ($excludeList)";
}
$sql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime,
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
$whereSql
ORDER BY p.log_PostTime DESC
LIMIT $offset, $limit";
$results = $zbp->db->Query($sql);
$list = array();
foreach ($results as $row) {
$list[] = format_feed_item($row, 'latest');
}
$countSql = "SELECT COUNT(*) as total FROM $tablePost p $whereSql";
$countResult = $zbp->db->Query($countSql);
$total = (int) ($countResult[0]['total'] ?? 0);
return array(
'code' => 200,
'message' => 'success',
'data' => array(
'type' => 'latest',
'list' => $list,
'page' => $page,
'limit' => $limit,
'total' => $total,
'has_more' => ($page * $limit) < $total
)
);
}
/**
* 热门信息流
*/
function get_hot_feed() {
global $zbp;
$page = (int) ($_GET['page'] ?? 1);
$limit = (int) ($_GET['limit'] ?? 20);
$cateId = (int) ($_GET['cate_id'] ?? 0);
$period = trim($_GET['period'] ?? 'total');
if ($limit > 50) $limit = 50;
if ($limit < 1) $limit = 20;
$tablePost = $zbp->db->dbpre . 'post';
$tableCategory = $zbp->db->dbpre . 'category';
$tablePostStat = $zbp->db->dbpre . 'post_stat';
$tableStatLog = $zbp->db->dbpre . 'recipe_stat_log';
$offset = ($page - 1) * $limit;
$whereSql = "WHERE p.log_Type = 0 AND p.log_Status = 0";
if ($cateId > 0) {
$whereSql .= " AND p.log_CateID = $cateId";
}
$orderBy = 'p.log_ViewNums DESC';
if ($period === 'today') {
$today = date('Y-m-d');
$sql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime,
p.log_ViewNums, c.cate_Name, l.view_count,
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 $tableStatLog l
INNER JOIN $tablePost p ON l.log_id = p.log_ID
LEFT JOIN $tableCategory c ON p.log_CateID = c.cate_ID
LEFT JOIN $tablePostStat s ON p.log_ID = s.log_id
WHERE l.stat_date = '$today' AND p.log_Type = 0 AND p.log_Status = 0
ORDER BY l.view_count DESC
LIMIT $offset, $limit";
} else {
$sql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime,
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
$whereSql
ORDER BY $orderBy
LIMIT $offset, $limit";
}
$results = $zbp->db->Query($sql);
$list = array();
foreach ($results as $row) {
$list[] = format_feed_item($row, 'hot');
}
return array(
'code' => 200,
'message' => 'success',
'data' => array(
'type' => 'hot',
'period' => $period,
'list' => $list,
'page' => $page,
'limit' => $limit,
'has_more' => count($list) >= $limit
)
);
}
/**
* 格式化信息流项目
*/
function format_feed_item($row, $source = 'unknown') {
global $zbp;
static $picIdCache = array();
$recipeId = (int) $row['log_ID'];
if (!isset($picIdCache[$recipeId])) {
$tableRecipeIdMap = $zbp->db->dbpre . 'recipe_id_map';
$idMapSql = "SELECT old_id FROM $tableRecipeIdMap WHERE new_log_id = $recipeId LIMIT 1";
$idMapResult = $zbp->db->Query($idMapSql);
$picIdCache[$recipeId] = !empty($idMapResult) ? (int) $idMapResult[0]['old_id'] : null;
}
$cover = '';
if (preg_match('/<img[^>]+src=["\']([^"\']+)["\']/i', $row['log_Intro'] ?? '', $matches)) {
$cover = $matches[1];
}
return array(
'id' => $recipeId,
'pic_id' => $picIdCache[$recipeId],
'title' => $row['log_Title'],
'intro' => mb_substr(strip_tags($row['log_Intro'] ?? ''), 0, 100),
'cover' => $cover,
'category' => array(
'id' => (int) ($row['log_CateID'] ?? 0),
'name' => $row['cate_Name'] ?? ''
),
'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)
),
'source' => $source,
'publish_time' => $row['log_PostTime'] ?? ''
);
}