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'] ); }