824 lines
27 KiB
PHP
824 lines
27 KiB
PHP
<?php
|
||
/**
|
||
* 📱 信息流接口
|
||
*
|
||
* 支持多种信息流类型:
|
||
* - recommend: 推荐流(热门+最新+随机混合)
|
||
* - latest: 最新发布
|
||
* - hot: 热门排行
|
||
* - follow: 关注动态(需要用户ID)
|
||
*
|
||
* 性能优化:支持缓存、Stale模式、多格式输出
|
||
*/
|
||
|
||
$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', 'personal');
|
||
|
||
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 'personal':
|
||
$result = get_personal_feed();
|
||
break;
|
||
case 'prefetch':
|
||
$result = get_prefetch_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'
|
||
),
|
||
array(
|
||
'type' => 'personal',
|
||
'name' => '个性化流',
|
||
'description' => '基于用户偏好推荐',
|
||
'example' => '?act=personal&user_id=xxx'
|
||
),
|
||
array(
|
||
'type' => 'prefetch',
|
||
'name' => '预加载',
|
||
'description' => '预加载下一页数据',
|
||
'example' => '?act=prefetch&pages=3'
|
||
)
|
||
),
|
||
'params' => array(
|
||
'page' => '页码,默认1',
|
||
'limit' => '每页数量,默认20,最大50',
|
||
'user_id' => '用户ID(个性化流必填)',
|
||
'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';
|
||
$tableRecipeIdMap = $zbp->db->dbpre . 'recipe_id_map';
|
||
|
||
$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.recommend_nums, 0) as recommend_nums
|
||
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.recommend_nums, 0) as recommend_nums
|
||
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.recommend_nums, 0) as recommend_nums
|
||
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';
|
||
$tableRecipeIdMap = $zbp->db->dbpre . 'recipe_id_map';
|
||
|
||
$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.recommend_nums, 0) as recommend_nums
|
||
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';
|
||
$tableRecipeIdMap = $zbp->db->dbpre . 'recipe_id_map';
|
||
|
||
$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.recommend_nums, 0) as recommend_nums
|
||
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.recommend_nums, 0) as recommend_nums
|
||
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 get_personal_feed() {
|
||
global $zbp;
|
||
|
||
$userId = trim($_GET['user_id'] ?? '');
|
||
$page = (int) ($_GET['page'] ?? 1);
|
||
$limit = (int) ($_GET['limit'] ?? 20);
|
||
$debug = isset($_GET['_debug']) && $_GET['_debug'] === '1';
|
||
|
||
if (empty($userId)) {
|
||
return array('code' => 400, 'message' => '缺少用户ID参数');
|
||
}
|
||
|
||
if ($limit > 50) $limit = 50;
|
||
if ($limit < 1) $limit = 20;
|
||
|
||
$preference = load_user_preference_data($userId);
|
||
$preferredTags = $preference['preferred_tags'] ?? array();
|
||
$preferredCategories = $preference['preferred_categories'] ?? array();
|
||
$blockedAllergens = $preference['blocked_allergens'] ?? array();
|
||
|
||
$viewHistory = load_user_view_history($userId);
|
||
$categoryViewCount = $viewHistory['categories'] ?? array();
|
||
|
||
$adminRecommend = get_admin_recommend_config();
|
||
$topCategories = $adminRecommend['top_categories'] ?? array();
|
||
$recommendCategories = $adminRecommend['recommend_categories'] ?? array();
|
||
|
||
$tablePost = $zbp->db->dbpre . 'post';
|
||
$tableCategory = $zbp->db->dbpre . 'category';
|
||
$tablePostStat = $zbp->db->dbpre . 'post_stat';
|
||
$tableIngredient = $zbp->db->dbpre . 'recipe_ingredient';
|
||
$tableRecipeIdMap = $zbp->db->dbpre . 'recipe_id_map';
|
||
|
||
$fetchLimit = $limit * 5;
|
||
$sql = "SELECT p.log_ID, p.log_Title, p.log_Intro, p.log_CateID, p.log_PostTime, p.log_Tag,
|
||
p.log_ViewNums, c.cate_Name,
|
||
COALESCE(s.like_nums, 0) as like_nums,
|
||
COALESCE(s.recommend_nums, 0) as recommend_nums
|
||
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 p.log_Type = 0 AND p.log_Status = 0
|
||
ORDER BY p.log_PostTime DESC
|
||
LIMIT $fetchLimit";
|
||
|
||
$results = $zbp->db->Query($sql);
|
||
|
||
$scoredItems = array();
|
||
foreach ($results as $row) {
|
||
$itemId = (int) $row['log_ID'];
|
||
$cateId = (int) $row['log_CateID'];
|
||
|
||
if (!empty($blockedAllergens)) {
|
||
$hasAllergen = check_item_allergen($itemId, $blockedAllergens);
|
||
if ($hasAllergen) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
$score = calculate_item_score(
|
||
$row,
|
||
$preferredTags,
|
||
$preferredCategories,
|
||
$categoryViewCount,
|
||
$topCategories,
|
||
$recommendCategories
|
||
);
|
||
|
||
$item = format_feed_item($row, 'personal');
|
||
$item['_score'] = $score;
|
||
$item['_score_detail'] = $debug ? get_score_detail(
|
||
$row,
|
||
$preferredTags,
|
||
$preferredCategories,
|
||
$categoryViewCount,
|
||
$topCategories,
|
||
$recommendCategories
|
||
) : null;
|
||
|
||
$scoredItems[] = $item;
|
||
}
|
||
|
||
usort($scoredItems, function($a, $b) {
|
||
return $b['_score'] <=> $a['_score'];
|
||
});
|
||
|
||
$offset = ($page - 1) * $limit;
|
||
$list = array_slice($scoredItems, $offset, $limit);
|
||
|
||
foreach ($list as &$item) {
|
||
unset($item['_score']);
|
||
if (!$debug) {
|
||
unset($item['_score_detail']);
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'code' => 200,
|
||
'message' => 'success',
|
||
'data' => array(
|
||
'type' => 'personal',
|
||
'user_id' => $userId,
|
||
'list' => $list,
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'total' => count($scoredItems),
|
||
'has_more' => ($page * $limit) < count($scoredItems),
|
||
'preference' => array(
|
||
'tags' => count($preferredTags),
|
||
'categories' => count($preferredCategories),
|
||
'blocked_allergens' => count($blockedAllergens),
|
||
'view_history_categories' => count($categoryViewCount)
|
||
),
|
||
'admin_config' => array(
|
||
'top_categories' => count($topCategories),
|
||
'recommend_categories' => count($recommendCategories)
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 加载用户偏好数据
|
||
*/
|
||
function load_user_preference_data($userId) {
|
||
$cacheFile = sys_get_temp_dir() . '/user_preference_' . md5($userId) . '.json';
|
||
|
||
if (file_exists($cacheFile)) {
|
||
$data = json_decode(file_get_contents($cacheFile), true);
|
||
if ($data && isset($data['expire_time']) && $data['expire_time'] > time()) {
|
||
return $data;
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'user_id' => $userId,
|
||
'preferred_tags' => array(),
|
||
'preferred_categories' => array(),
|
||
'blocked_allergens' => array(),
|
||
'expire_time' => time() + 86400
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 计算内容评分
|
||
*/
|
||
function calculate_item_score($row, $preferredTags, $preferredCategories, $categoryViewCount, $topCategories, $recommendCategories) {
|
||
$score = 0;
|
||
$cateId = (int) $row['log_CateID'];
|
||
$tagStr = $row['log_Tag'] ?? '';
|
||
|
||
if (in_array($cateId, $topCategories)) {
|
||
$score += 50;
|
||
} elseif (in_array($cateId, $recommendCategories)) {
|
||
$score += 30;
|
||
}
|
||
|
||
if (in_array($cateId, $preferredCategories)) {
|
||
$score += 25;
|
||
}
|
||
|
||
if (!empty($preferredTags) && !empty($tagStr)) {
|
||
$tagIds = explode(',', $tagStr);
|
||
$matchedTags = array_intersect($tagIds, $preferredTags);
|
||
$score += count($matchedTags) * 30;
|
||
}
|
||
|
||
if (isset($categoryViewCount[$cateId])) {
|
||
$viewWeight = min($categoryViewCount[$cateId] * 5, 20);
|
||
$score += $viewWeight;
|
||
}
|
||
|
||
$viewScore = min(floor(($row['log_ViewNums'] ?? 0) / 100), 20);
|
||
$score += $viewScore;
|
||
|
||
$likeScore = min(($row['like_nums'] ?? 0) * 2, 15);
|
||
$score += $likeScore;
|
||
|
||
$recommendScore = min(($row['recommend_nums'] ?? 0) * 3, 15);
|
||
$score += $recommendScore;
|
||
|
||
$postTime = strtotime($row['log_PostTime']);
|
||
if ($postTime) {
|
||
$daysAgo = (time() - $postTime) / 86400;
|
||
if ($daysAgo < 1) {
|
||
$score += 15;
|
||
} elseif ($daysAgo < 7) {
|
||
$score += 10;
|
||
} elseif ($daysAgo < 30) {
|
||
$score += 5;
|
||
}
|
||
}
|
||
|
||
return $score;
|
||
}
|
||
|
||
/**
|
||
* 获取评分详情(调试用)
|
||
*/
|
||
function get_score_detail($row, $preferredTags, $preferredCategories, $categoryViewCount, $topCategories, $recommendCategories) {
|
||
$cateId = (int) $row['log_CateID'];
|
||
$tagStr = $row['log_Tag'] ?? '';
|
||
|
||
$detail = array(
|
||
'admin_top' => in_array($cateId, $topCategories) ? 50 : 0,
|
||
'admin_recommend' => in_array($cateId, $recommendCategories) ? 30 : 0,
|
||
'preferred_category' => in_array($cateId, $preferredCategories) ? 25 : 0,
|
||
'preferred_tags' => 0,
|
||
'view_history' => isset($categoryViewCount[$cateId]) ? min($categoryViewCount[$cateId] * 5, 20) : 0,
|
||
'hot_view' => min(floor(($row['log_ViewNums'] ?? 0) / 100), 20),
|
||
'hot_like' => min(($row['like_nums'] ?? 0) * 2, 15),
|
||
'hot_recommend' => min(($row['recommend_nums'] ?? 0) * 3, 15),
|
||
'time_bonus' => 0
|
||
);
|
||
|
||
if (!empty($preferredTags) && !empty($tagStr)) {
|
||
$tagIds = explode(',', $tagStr);
|
||
$matchedTags = array_intersect($tagIds, $preferredTags);
|
||
$detail['preferred_tags'] = count($matchedTags) * 30;
|
||
}
|
||
|
||
$postTime = strtotime($row['log_PostTime']);
|
||
if ($postTime) {
|
||
$daysAgo = (time() - $postTime) / 86400;
|
||
if ($daysAgo < 1) {
|
||
$detail['time_bonus'] = 15;
|
||
} elseif ($daysAgo < 7) {
|
||
$detail['time_bonus'] = 10;
|
||
} elseif ($daysAgo < 30) {
|
||
$detail['time_bonus'] = 5;
|
||
}
|
||
}
|
||
|
||
$detail['total'] = array_sum($detail);
|
||
return $detail;
|
||
}
|
||
|
||
/**
|
||
* 检查内容是否包含过敏原
|
||
*/
|
||
function check_item_allergen($itemId, $blockedAllergens) {
|
||
global $zbp;
|
||
|
||
$tableIngredient = $zbp->db->dbpre . 'recipe_ingredient';
|
||
|
||
$allergenMap = array(
|
||
'nuts' => array('核桃', '杏仁', '腰果', '榛子', '松子', '开心果', '栗子', '花生'),
|
||
'seafood' => array('鱼', '虾', '蟹', '贝', '海参', '鲍鱼', '扇贝'),
|
||
'dairy' => array('牛奶', '奶粉', '奶酪', '奶油', '酸奶', '黄油'),
|
||
'eggs' => array('鸡蛋', '鸭蛋', '鹅蛋', '鸽蛋', '鹌鹑蛋'),
|
||
'grains' => array('小麦', '面粉', '面包', '面条'),
|
||
'beans' => array('黄豆', '绿豆', '红豆', '蚕豆', '豌豆'),
|
||
'meat' => array('猪肉', '牛肉', '羊肉', '鸡肉', '鸭肉', '鹅肉'),
|
||
'fruits' => array('桃', '芒果', '菠萝', '草莓', '猕猴桃'),
|
||
'vegetables' => array('芹菜', '茄子', '韭菜', '香菜', '姜', '蒜'),
|
||
'mushrooms' => array('香菇', '金针菇', '木耳', '银耳'),
|
||
'seasonings' => array('胡椒', '花椒', '芥末', '味精', '料酒'),
|
||
'other' => array('蜂蜜', '巧克力', '可可', '芝麻')
|
||
);
|
||
|
||
$checkIngredients = array();
|
||
foreach ($blockedAllergens as $allergenType) {
|
||
if (isset($allergenMap[$allergenType])) {
|
||
$checkIngredients = array_merge($checkIngredients, $allergenMap[$allergenType]);
|
||
}
|
||
}
|
||
|
||
if (empty($checkIngredients)) {
|
||
return false;
|
||
}
|
||
|
||
$ingredientList = array_map(function($item) use ($zbp) {
|
||
return "'" . $zbp->db->EscapeString($item) . "'";
|
||
}, $checkIngredients);
|
||
$ingredientStr = implode(',', $ingredientList);
|
||
|
||
$sql = "SELECT COUNT(*) as cnt FROM $tableIngredient
|
||
WHERE log_id = $itemId AND ingredient_name IN ($ingredientStr)";
|
||
|
||
$result = $zbp->db->Query($sql);
|
||
return (int) ($result[0]['cnt'] ?? 0) > 0;
|
||
}
|
||
|
||
/**
|
||
* 加载用户浏览历史
|
||
*/
|
||
function load_user_view_history($userId) {
|
||
$cacheFile = sys_get_temp_dir() . '/user_view_history_' . md5($userId) . '.json';
|
||
|
||
if (file_exists($cacheFile)) {
|
||
$data = json_decode(file_get_contents($cacheFile), true);
|
||
if ($data && isset($data['expire_time']) && $data['expire_time'] > time()) {
|
||
return $data;
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'categories' => array(),
|
||
'tags' => array(),
|
||
'recent_views' => array(),
|
||
'expire_time' => time() + 86400
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 获取管理员推荐配置
|
||
*/
|
||
function get_admin_recommend_config() {
|
||
$configFile = dirname(__FILE__) . '/config/admin_recommend.json';
|
||
|
||
if (file_exists($configFile)) {
|
||
$data = json_decode(file_get_contents($configFile), true);
|
||
if ($data) {
|
||
return array(
|
||
'top_categories' => $data['top_categories'] ?? array(),
|
||
'recommend_categories' => $data['recommend_categories'] ?? array(),
|
||
'top_tags' => $data['top_tags'] ?? array(),
|
||
'expire_time' => time() + 3600
|
||
);
|
||
}
|
||
}
|
||
|
||
$cacheFile = sys_get_temp_dir() . '/admin_recommend_config.json';
|
||
|
||
if (file_exists($cacheFile)) {
|
||
$data = json_decode(file_get_contents($cacheFile), true);
|
||
if ($data && isset($data['expire_time']) && $data['expire_time'] > time()) {
|
||
return $data;
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'top_categories' => array(),
|
||
'recommend_categories' => array(),
|
||
'top_tags' => array(),
|
||
'expire_time' => time() + 3600
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 预加载多页数据
|
||
*/
|
||
function get_prefetch_feed() {
|
||
global $zbp;
|
||
|
||
$pages = (int) ($_GET['pages'] ?? 3);
|
||
$limit = (int) ($_GET['limit'] ?? 20);
|
||
|
||
if ($pages > 5) $pages = 5;
|
||
if ($pages < 1) $pages = 3;
|
||
if ($limit > 30) $limit = 30;
|
||
|
||
$allData = array();
|
||
|
||
for ($i = 1; $i <= $pages; $i++) {
|
||
$_GET['page'] = $i;
|
||
$_GET['limit'] = $limit;
|
||
$feedResult = get_recommend_feed();
|
||
if ($feedResult['code'] === 200) {
|
||
$allData['page_' . $i] = $feedResult['data'];
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'code' => 200,
|
||
'message' => 'success',
|
||
'data' => array(
|
||
'type' => 'prefetch',
|
||
'pages' => $pages,
|
||
'items_per_page' => $limit,
|
||
'total_items' => $pages * $limit,
|
||
'pages_data' => $allData
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 格式化信息流项目
|
||
*/
|
||
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;
|
||
}
|
||
|
||
$publishTime = strtotime($row['log_PostTime']);
|
||
return array(
|
||
'id' => $recipeId,
|
||
'pic_id' => $picIdCache[$recipeId],
|
||
'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'] ?? ''
|
||
),
|
||
'statistics' => array(
|
||
'view_count' => (int) ($row['log_ViewNums'] ?? 0),
|
||
'like_count' => (int) ($row['like_nums'] ?? 0),
|
||
'recommend_count' => (int) ($row['recommend_nums'] ?? 0)
|
||
),
|
||
'publish_time' => $publishTime !== false ? $publishTime : null,
|
||
'source' => $source,
|
||
'url' => '?id=' . $row['log_ID']
|
||
);
|
||
}
|