$value) { if (!isset($_GET[$key])) { $_GET[$key] = $value; } } } $act = strtolower(trim($_GET['act'] ?? 'share')); // ─── 数据目录配置(与kitchen.php共享) ─── $dataDir = dirname(__FILE__) . '/cache/kitchen/'; if (!is_dir($dataDir)) { if (!@mkdir($dataDir, 0755, true)) { $tmpDir = sys_get_temp_dir() . '/kitchen/'; if (!is_dir($tmpDir)) { @mkdir($tmpDir, 0755, true); } $dataDir = $tmpDir; } } if (!is_writable($dataDir)) { $tmpDir = sys_get_temp_dir() . '/kitchen/'; if (!is_dir($tmpDir)) { @mkdir($tmpDir, 0755, true); } $dataDir = $tmpDir; } $statsFile = $dataDir . 'share_stats.json'; $logFile = $dataDir . 'access_log.json'; // ─── 路由分发 ─── switch ($act) { case 'create': header('Content-Type: application/json; charset=utf-8'); echo json_encode(create_recipe_share(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); break; case 'update': header('Content-Type: application/json; charset=utf-8'); echo json_encode(update_recipe_share(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); break; case 'delete': header('Content-Type: application/json; charset=utf-8'); echo json_encode(delete_recipe_share(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); break; case 'list': header('Content-Type: application/json; charset=utf-8'); echo json_encode(list_recipe_data(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); break; case 'cleanup': header('Content-Type: application/json; charset=utf-8'); echo json_encode(cleanup_expired_recipes(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); break; case 'stats': header('Content-Type: application/json; charset=utf-8'); echo json_encode(get_share_stats(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); break; case 'log': header('Content-Type: application/json; charset=utf-8'); echo json_encode(get_access_log(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); break; case 'api': header('Content-Type: application/json; charset=utf-8'); echo json_encode(fetch_recipe_data(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); break; case 'index': header('Content-Type: application/json; charset=utf-8'); echo json_encode(array( 'code' => 200, 'message' => '🍳 菜谱分享页面', 'data' => array( 'version' => '1.3.0', 'description' => '菜谱分享页面,本地JSON存储,支持CRUD和统计,数据3天后自动过期删除,完整数据传输', 'endpoints' => array( 'share' => '?code=CP00001 或 ?id=123', 'api' => '?act=api&code=CP00001 (JSON数据)', 'create' => 'POST ?act=create {recipe JSON}', 'update' => 'POST ?act=update {recipe JSON}', 'delete' => 'GET ?act=delete&id=xxx', 'list' => 'GET ?act=list', 'cleanup' => 'GET ?act=cleanup (清理过期数据)', 'stats' => '?act=stats (分享统计)', 'log' => '?act=log (访问日志)', 'index' => '?act=index (接口说明)', ), 'storage' => 'JSON文件 (' . $dataDir . 'recipe_*.json)', 'expire_policy' => '数据3天后自动过期删除,读取时自动检查并清理', 'display' => '完整显示:封面/标题/简介/标签/食材/营养/过敏原/步骤', ), ), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); break; case 'share': default: render_share_page(); break; } exit; // ══════════════════════════════════════════════════════════════ // 数据获取函数 // ══════════════════════════════════════════════════════════════ /** * 读取菜谱分享数据(本地JSON文件) * 自动检查过期,过期则删除并返回null */ function read_recipe_data($id) { global $dataDir; $file = $dataDir . 'recipe_' . (int)$id . '.json'; if (!file_exists($file)) { return null; } // 检查是否过期 if (is_recipe_expired($file)) { @unlink($file); return null; } $content = file_get_contents($file); $data = json_decode($content, true); return is_array($data) ? $data : null; } /** * 检查菜谱数据是否过期 */ function is_recipe_expired($file) { if (!file_exists($file)) return true; $content = file_get_contents($file); $data = json_decode($content, true); if (!is_array($data)) return true; $expiresAt = $data['expiresAt'] ?? null; if (empty($expiresAt)) { // 没有过期时间,默认3天后过期(从创建时间算起) $createdAt = $data['createdAt'] ?? $data['updatedAt'] ?? ''; if (empty($createdAt)) return false; $expiresAt = date('c', strtotime($createdAt . ' +3 days')); } return strtotime($expiresAt) < time(); } /** * 清理所有过期的菜谱分享数据 */ function cleanup_expired_recipes() { global $dataDir; $files = glob($dataDir . 'recipe_*.json'); if ($files === false) return array('deleted' => 0, 'errors' => array()); $deleted = 0; $errors = array(); foreach ($files as $file) { if (is_recipe_expired($file)) { if (@unlink($file)) { $deleted++; } else { $errors[] = basename($file); } } } return array( 'deleted' => $deleted, 'errors' => $errors, ); } /** * 写入菜谱分享数据(本地JSON文件) * 注意:数据3天后自动过期删除 */ function write_recipe_data($id, $data) { global $dataDir; $file = $dataDir . 'recipe_' . (int)$id . '.json'; // 添加过期时间(3天后) $data['expiresAt'] = date('c', strtotime('+3 days')); $data['updatedAt'] = date('c'); $fp = fopen($file, 'c'); if (flock($fp, LOCK_EX)) { ftruncate($fp, 0); rewind($fp); fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); flock($fp, LOCK_UN); } fclose($fp); } /** * 删除菜谱分享数据 */ function delete_recipe_data($id) { global $dataDir; $file = $dataDir . 'recipe_' . (int)$id . '.json'; if (file_exists($file)) { return @unlink($file); } return false; } /** * 获取菜谱分享列表 */ function list_recipe_data() { global $dataDir; $recipes = array(); $files = glob($dataDir . 'recipe_*.json'); if ($files === false) return $recipes; foreach ($files as $file) { $content = file_get_contents($file); $data = json_decode($content, true); if (is_array($data)) { $recipes[] = $data; } } // 按更新时间倒序 usort($recipes, function($a, $b) { $timeA = $a['updatedAt'] ?? $a['createdAt'] ?? ''; $timeB = $b['updatedAt'] ?? $b['createdAt'] ?? ''; return strcmp($timeB, $timeA); }); return $recipes; } /** * 将code解析为ID(支持多种格式) * 支持格式: CP00001, CP123, cp123, cpxxxx, 123 */ function resolve_code_to_id($code) { // 支持 CP/cp/cpxxxx 格式,提取数字部分 if (preg_match('/^CPX?(\d+)$/i', $code, $matches)) { return (int)$matches[1]; } return (int)$code; } /** * 获取API基础URL(用于页面链接) */ function get_api_base_url() { $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'eat.wktyl.com'; return $protocol . '://' . $host; } /** * 通过本地JSON文件获取菜谱数据 */ function fetch_recipe_data() { $code = trim($_GET['code'] ?? ''); $id = (int)($_GET['id'] ?? 0); if (empty($code) && $id <= 0) { return array('code' => 400, 'message' => '缺少 code 或 id 参数'); } $resolvedId = $id > 0 ? $id : resolve_code_to_id($code); $recipe = read_recipe_data($resolvedId); if ($recipe === null) { return array('code' => 404, 'message' => '菜谱不存在'); } record_share_access($recipe, $code, $id); return array( 'code' => 200, 'message' => 'success', 'data' => $recipe, ); } /** * 获取菜谱完整数据(用于页面渲染) */ function fetch_recipe_full($id) { $recipe = read_recipe_data((int)$id); if ($recipe === null) return array(); return $recipe; } /** * 创建菜谱分享数据 * POST ?act=create * Body: {recipe JSON} */ function create_recipe_share() { $recipeJson = $_GET['recipe'] ?? $_GET['data'] ?? null; if ($recipeJson === null) { $rawBody = file_get_contents('php://input'); $recipeData = json_decode($rawBody, true); } else { $recipeData = json_decode($recipeJson, true); } if (!is_array($recipeData)) { return array('code' => 400, 'message' => '无效的菜谱数据,需要JSON格式'); } // 补全字段 $id = (int)($recipeData['id'] ?? 0); if ($id <= 0) { return array('code' => 400, 'message' => '缺少菜谱ID'); } if (empty($recipeData['createdAt'])) { $recipeData['createdAt'] = date('c'); } $recipeData['updatedAt'] = date('c'); $recipeData['createdBy'] = get_client_ip(); write_recipe_data($id, $recipeData); return array( 'code' => 200, 'message' => '创建成功', 'data' => $recipeData, ); } /** * 更新菜谱分享数据 * POST ?act=update * Body: {recipe JSON with id} */ function update_recipe_share() { $recipeJson = $_GET['recipe'] ?? $_GET['data'] ?? null; if ($recipeJson === null) { $rawBody = file_get_contents('php://input'); $recipeData = json_decode($rawBody, true); } else { $recipeData = json_decode($recipeJson, true); } if (!is_array($recipeData) || empty($recipeData['id'])) { return array('code' => 400, 'message' => '无效的菜谱数据,缺少id字段'); } $id = (int)$recipeData['id']; $existing = read_recipe_data($id); if ($existing === null) { return array('code' => 404, 'message' => '菜谱不存在'); } // 合并数据 $recipeData = array_merge($existing, $recipeData); $recipeData['updatedAt'] = date('c'); write_recipe_data($id, $recipeData); return array( 'code' => 200, 'message' => '更新成功', 'data' => $recipeData, ); } /** * 删除菜谱分享数据 * GET ?act=delete&id=xxx */ function delete_recipe_share() { $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { return array('code' => 400, 'message' => '缺少菜谱ID参数'); } if (!delete_recipe_data($id)) { return array('code' => 404, 'message' => '菜谱不存在'); } return array( 'code' => 200, 'message' => '删除成功', 'data' => array('deleted_id' => $id), ); } // ══════════════════════════════════════════════════════════════ // 统计和日志函数 // ══════════════════════════════════════════════════════════════ /** * 记录分享访问 */ function record_share_access($recipe, $code, $id) { global $statsFile, $logFile; $recipeId = $recipe['id'] ?? $id; $recipeTitle = $recipe['title'] ?? '未知菜谱'; // 更新统计 $stats = read_json_file($statsFile, array( 'total_views' => 0, 'today_views' => 0, 'today_date' => date('Y-m-d'), 'recipes' => array(), )); $today = date('Y-m-d'); if ($stats['today_date'] !== $today) { $stats['today_views'] = 0; $stats['today_date'] = $today; } $stats['total_views']++; $stats['today_views']++; $key = 'r_' . $recipeId; if (!isset($stats['recipes'][$key])) { $stats['recipes'][$key] = array( 'id' => $recipeId, 'title' => $recipeTitle, 'code' => $code, 'views' => 0, 'first_access' => date('c'), 'last_access' => date('c'), ); } $stats['recipes'][$key]['views']++; $stats['recipes'][$key]['last_access'] = date('c'); if (!empty($recipeTitle) && $recipeTitle !== '未知菜谱') { $stats['recipes'][$key]['title'] = $recipeTitle; } write_json_file($statsFile, $stats); // 记录访问日志(保留最近200条) $log = read_json_file($logFile, array()); array_unshift($log, array( 'time' => date('c'), 'recipe_id' => $recipeId, 'title' => $recipeTitle, 'code' => $code, 'ip' => get_client_ip(), 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'referer' => $_SERVER['HTTP_REFERER'] ?? '', )); $log = array_slice($log, 0, 200); write_json_file($logFile, $log); } /** * 获取分享统计 */ function get_share_stats() { global $statsFile; $stats = read_json_file($statsFile, array( 'total_views' => 0, 'today_views' => 0, 'today_date' => date('Y-m-d'), 'recipes' => array(), )); return array( 'code' => 200, 'data' => $stats, ); } /** * 获取访问日志 */ function get_access_log() { global $logFile; $log = read_json_file($logFile, array()); $page = max(1, (int)($_GET['page'] ?? 1)); $limit = min(50, max(1, (int)($_GET['limit'] ?? 20))); $offset = ($page - 1) * $limit; return array( 'code' => 200, 'data' => array( 'list' => array_slice($log, $offset, $limit), 'total' => count($log), 'page' => $page, 'limit' => $limit, ), ); } // ══════════════════════════════════════════════════════════════ // 工具函数 // ══════════════════════════════════════════════════════════════ function read_json_file($file, $default = array()) { if (!file_exists($file)) return $default; $content = file_get_contents($file); $data = json_decode($content, true); return is_array($data) ? $data : $default; } function write_json_file($file, $data) { $fp = fopen($file, 'c'); if (flock($fp, LOCK_EX)) { ftruncate($fp, 0); rewind($fp); fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); flock($fp, LOCK_UN); } fclose($fp); } function get_client_ip() { if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); return trim($ips[0]); } if (!empty($_SERVER['HTTP_X_REAL_IP'])) { return $_SERVER['HTTP_X_REAL_IP']; } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } function safe_html($str) { return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8'); } function format_time_ago($timestamp) { if (empty($timestamp)) return ''; $now = time(); $ts = is_numeric($timestamp) ? (int)$timestamp : strtotime($timestamp); $diff = $now - $ts; if ($diff < 60) return '刚刚'; if ($diff < 3600) return floor($diff / 60) . '分钟前'; if ($diff < 86400) return floor($diff / 3600) . '小时前'; if ($diff < 604800) return floor($diff / 86400) . '天前'; return date('Y-m-d', $ts); } // ══════════════════════════════════════════════════════════════ // 页面渲染 // ══════════════════════════════════════════════════════════════ /** * 渲染分享页面 */ function render_share_page() { $code = trim($_GET['code'] ?? ''); $id = (int)($_GET['id'] ?? 0); $recipe = array(); $error = ''; if (!empty($code) || $id > 0) { $resolvedId = $id > 0 ? $id : resolve_code_to_id($code); $recipe = fetch_recipe_full($resolvedId); if (!empty($recipe)) { record_share_access($recipe, $code, $id); } else { $error = '菜谱不存在或暂时无法加载'; } } else { $error = '缺少菜谱参数'; } // 提取数据用于显示 $title = !empty($recipe) ? safe_html($recipe['title'] ?? '菜谱分享') : '🍳 菜谱分享'; $cover = !empty($recipe) ? ($recipe['cover'] ?? '') : ''; $intro = !empty($recipe) ? ($recipe['intro'] ?? '') : ''; $content = !empty($recipe) ? ($recipe['content'] ?? '') : ''; $categoryName = !empty($recipe) ? ($recipe['category_name'] ?? ($recipe['categoryName'] ?? '')) : ''; $recipeCode = !empty($recipe) ? ($recipe['code'] ?? $code) : $code; $viewCount = !empty($recipe) ? (($recipe['statistics']['views'] ?? 0) ?: ($recipe['view_count'] ?? 0)) : 0; $likeCount = !empty($recipe) ? (($recipe['statistics']['likes'] ?? 0) ?: ($recipe['like_count'] ?? 0)) : 0; $ratingScore = !empty($recipe) ? (($recipe['rating']['score'] ?? 0) ?: 0) : 0; $ratingNums = !empty($recipe) ? (($recipe['rating']['nums'] ?? 0) ?: 0) : 0; $author = !empty($recipe) ? ($recipe['author'] ?? null) : null; $tags = !empty($recipe) ? ($recipe['tags'] ?? array()) : array(); $ingredients = !empty($recipe) ? ($recipe['ingredients'] ?? array()) : array(); $nutrition = !empty($recipe) ? ($recipe['nutrition'] ?? null) : null; $meta = !empty($recipe) ? ($recipe['meta'] ?? null) : null; $allergens = !empty($recipe) ? ($recipe['allergens'] ?? array()) : array(); $queryTime = round((microtime(true) - $GLOBALS['startTime']) * 1000, 2); echo '
' . nl2br(safe_html($intro)) . '