json([ 'name' => Setting::get('site_name', config('blog.name')), 'description' => Setting::get('site_description', config('blog.description')), 'icp' => Setting::get('site_icp', config('blog.icp')), 'url' => url('/'), 'rss' => route('feed.rss'), 'api_version' => '1.0', ]); } /** * 文章列表(分页)。 * * @queryParam page int 页码 Example: 1 * @queryParam per_page int 每页数量(默认 10,最大 50)Example: 10 * @queryParam category string 分类 slug Example: tech * @queryParam tag string 标签名 Example: laravel * @queryParam q string 关键词搜索 Example: laravel * * @response array{data: array, meta: array} */ public function posts(Request $request): JsonResponse { $query = Post::published()->with('category:id,name,slug')->with('tags:id,name'); if ($category = $request->query('category')) { $query->whereHas('category', fn ($q) => $q->where('slug', $category)->orWhere('name', $category)); } if ($tag = $request->query('tag')) { $query->whereHas('tags', fn ($q) => $q->where('name', $tag)->orWhere('slug', $tag)); } if ($q = trim((string) $request->query('q', ''))) { $query->search($q); } $perPage = min((int) $request->query('per_page', 10), 50); $posts = $query->orderByDesc('published_at')->paginate($perPage); return response()->json([ 'data' => $posts->through(fn (Post $post) => $this->postSummary($post))->items(), 'meta' => [ 'current_page' => $posts->currentPage(), 'last_page' => $posts->lastPage(), 'per_page' => $posts->perPage(), 'total' => $posts->total(), ], ]); } /** * 文章详情。 * * @urlParam slug string required 文章 slug 或 ID Example: post-1 * * @response array{id: int, title: string, content_html: string, ...} */ public function post(string $slug): JsonResponse { $post = Post::query() ->where('slug', $slug) ->orWhere('id', (int) $slug) ->first(); abort_unless($post && $post->status === 'published', 404); // 会员/付费内容过滤与前台一致 $contentHtml = app(PostContentRenderer::class)->render($post); return response()->json([ 'id' => $post->id, 'title' => $post->title, 'slug' => $post->slug, 'excerpt' => $post->excerpt_or_fallback, 'content_html' => $contentHtml, 'content_format' => $post->content_format, 'keywords' => $post->keywords, 'category' => $post->category ? ['id' => $post->category->id, 'name' => $post->category->name, 'slug' => $post->category->slug] : null, 'tags' => $post->tags->map(fn ($t) => ['id' => $t->id, 'name' => $t->name, 'slug' => $t->slug])->values(), 'views' => $post->views, 'comment_count' => $post->comment_count, 'published_at' => $post->published_at?->toIso8601String(), 'url' => route('posts.show', $post->slug ?: $post->id), ]); } /** * 分类列表。 * * @response array{data: array} */ public function categories(): JsonResponse { return response()->json([ 'data' => Category::query()->orderBy('display_order')->orderBy('name')->get(['id', 'name', 'slug', 'post_count']), ]); } /** * 标签列表。 * * @response array{data: array} */ public function tags(): JsonResponse { $counts = DB::table('taggables') ->selectRaw('tag_id, COUNT(*) as total') ->where('taggable_type', Post::class) ->groupBy('tag_id') ->pluck('total', 'tag_id'); return response()->json([ 'data' => Tag::query()->where('type', 'post')->get() ->map(fn (Tag $tag) => [ 'id' => $tag->id, 'name' => $tag->name, 'slug' => $tag->slug, 'post_count' => (int) ($counts[$tag->id] ?? 0), ]) ->values(), ]); } /** * 提交评论(公开)。 * * @bodyParam author_name string required 昵称 Example: 访客 * @bodyParam author_email string 邮箱 Example: guest@example.com * @bodyParam author_url string 网站 Example: https://example.com * @bodyParam content string required 评论内容 Example: 写得很好 * @bodyParam website string 蜜罐字段,留空 Example: */ public function storeComment(Request $request): JsonResponse { $data = $request->validate([ 'post_id' => ['required', 'integer', 'exists:posts,id'], 'author_name' => ['required', 'string', 'max:50'], 'author_email' => ['nullable', 'email', 'max:255'], 'author_url' => ['nullable', 'url', 'max:255'], 'content' => ['required', 'string', 'min:'.(int) Setting::get('comment_min_len', 2), 'max:'.(int) Setting::get('comment_max_len', 6000)], ]); // 蜜罐 if ($request->filled('website')) { return response()->json(['message' => '提交失败'], 422); } $post = Post::findOrFail((int) $data['post_id']); if ($post->status !== 'published' || $post->close_comment) { return response()->json(['message' => '该文章不允许评论'], 422); } $status = (int) Setting::get('comment_audit', 0) === 1 ? 'pending' : 'published'; $comment = $post->comments()->create([ 'user_id' => $request->user()?->id, 'author_name' => $data['author_name'], 'author_email' => $data['author_email'] ?? null, 'author_url' => $data['author_url'] ?? null, 'content' => $data['content'], 'ip' => $request->ip(), 'status' => $status, ]); if ($status === 'published') { $post->increment('comment_count'); } app(\App\Blog\Support\PluginManager::class)->doAction('comment.created', $comment); return response()->json([ 'message' => $status === 'published' ? '评论已发布' : '评论已提交,等待审核', 'data' => ['id' => $comment->id, 'status' => $status], ], 201); } /** * 当前登录用户(需 Bearer Token)。 * * @authenticated * * @response array{id: int, name: string, email: string, roles: array} */ public function me(Request $request): JsonResponse { $user = $request->user(); return response()->json([ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'roles' => $user->getRoleNames(), 'url' => $user->url, ]); } private function postSummary(Post $post): array { return [ 'id' => $post->id, 'title' => $post->title, 'slug' => $post->slug, 'excerpt' => $post->excerpt_or_fallback, 'category' => $post->category?->name, 'tags' => $post->tags->pluck('name')->values(), 'views' => $post->views, 'comment_count' => $post->comment_count, 'published_at' => $post->published_at?->toIso8601String(), 'url' => route('posts.show', $post->slug ?: $post->id), ]; } }