import { NextResponse } from "next/server"; import { ok, created, withErrorHandling, parseJsonBody } from "@/lib/api/response"; import { createPostSchema, listPostsQuerySchema } from "@/lib/validation/post"; import { createPost, listPosts } from "@/lib/posts/repository"; /** * GET /api/posts —— 文章列表 * * 查询参数: * - limit 1–100,默认 20 * - offset 默认 0 * - tag 按标签过滤 * - status draft | published(不传则只返回已发布) * - includeDrafts true 时包含草稿(内部/管理用途) * * 响应:{ data: PostMeta[], meta: { total, limit, offset, hasMore } } */ export const GET = withErrorHandling(async (request: Request) => { const { searchParams } = new URL(request.url); // 校验查询参数:非法值直接返回 400 + 字段级错误 const query = listPostsQuerySchema.parse( Object.fromEntries(searchParams.entries()), ); const result = await listPosts({ limit: query.limit, offset: query.offset, tag: query.tag, includeDrafts: query.includeDrafts, status: query.status, }); return ok(result.items, { total: result.total, limit: result.limit, offset: result.offset, hasMore: result.hasMore, }); }); /** * POST /api/posts —— 新建文章 * * 请求体(JSON): * - slug 必填,^[a-z0-9]+(-[a-z0-9]+)*$ * - title 必填 * - summary 必填 * - content 必填(Markdown / MDX 源码) * - tags 可选,字符串数组 * - status draft | published,默认 draft * - publishedAt 可选,ISO 日期;status=published 时缺省为当前时间 * - cover 可选 * - description 可选,覆盖 SEO 描述 * * 响应:201 { data: Post } */ export const POST = withErrorHandling(async (request: Request) => { const body = await parseJsonBody(request); const input = createPostSchema.parse(body); // slug 唯一性在应用层先查一次,给出友好的字段级报错; // 数据库唯一索引仍作为最终防线(并发下可能拦截,返回 409) const post = await createPost(input); return created(post); }); /** 其他方法统一返回 405 */ export function OPTIONS() { return new NextResponse(null, { status: 204, headers: { Allow: "GET, POST, OPTIONS", }, }); }