437 lines
14 KiB
PHP
437 lines
14 KiB
PHP
<?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'] ?? ''
|
||
);
|
||
}
|