import { NextResponse } from "next/server"; import { ZodError } from "zod"; /** * 统一的 API 响应格式与错误码。 * * 所有接口遵循同一契约,便于前端与脚本一致地处理: * * 成功:{ "data": ..., "meta"?: ... } * 失败:{ "error": { "code": "...", "message": "...", "details"?: [...] } } * * skill 要求"显式优于隐式":错误码是机器可读的稳定标识, * message 面向人,details 用于字段级校验错误。 */ /** 机器可读的错误码 */ export const ErrorCode = { VALIDATION_ERROR: "VALIDATION_ERROR", NOT_FOUND: "NOT_FOUND", CONFLICT: "CONFLICT", BAD_REQUEST: "BAD_REQUEST", INTERNAL_ERROR: "INTERNAL_ERROR", METHOD_NOT_ALLOWED: "METHOD_NOT_ALLOWED", SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE", } as const; export type ErrorCodeValue = (typeof ErrorCode)[keyof typeof ErrorCode]; /** 错误码 → HTTP 状态码 */ const STATUS_BY_CODE: Record = { VALIDATION_ERROR: 400, BAD_REQUEST: 400, NOT_FOUND: 404, CONFLICT: 409, METHOD_NOT_ALLOWED: 405, INTERNAL_ERROR: 500, SERVICE_UNAVAILABLE: 503, }; export interface ApiErrorDetail { /** 出错的字段路径,如 "slug" 或 "tags.0" */ field?: string; message: string; } /** 业务异常:路由中抛出,由 withErrorHandling 统一转成响应 */ export class ApiError extends Error { constructor( readonly code: ErrorCodeValue, message: string, readonly details?: ApiErrorDetail[], ) { super(message); this.name = "ApiError"; } static notFound(message = "资源不存在") { return new ApiError(ErrorCode.NOT_FOUND, message); } static conflict(message: string, details?: ApiErrorDetail[]) { return new ApiError(ErrorCode.CONFLICT, message, details); } static badRequest(message: string, details?: ApiErrorDetail[]) { return new ApiError(ErrorCode.BAD_REQUEST, message, details); } static validation(message: string, details?: ApiErrorDetail[]) { return new ApiError(ErrorCode.VALIDATION_ERROR, message, details); } } /** 成功响应 */ export function ok(data: T, meta?: Record, status = 200) { return NextResponse.json(meta ? { data, meta } : { data }, { status }); } /** 创建成功(201) */ export function created(data: T) { return NextResponse.json({ data }, { status: 201 }); } /** 无内容(删除成功) */ export function noContent() { return new NextResponse(null, { status: 204 }); } /** 失败响应 */ export function fail( code: ErrorCodeValue, message: string, details?: ApiErrorDetail[], ) { return NextResponse.json( { error: { code, message, ...(details ? { details } : {}) } }, { status: STATUS_BY_CODE[code] }, ); } /** 把 Zod 的校验错误转成字段级 details */ function zodToDetails(error: ZodError): ApiErrorDetail[] { return error.issues.map((issue) => ({ field: issue.path.join(".") || undefined, message: issue.message, })); } /** * 从错误对象中提取 Postgres 错误码。 * * 关键点:Drizzle 会把驱动抛出的原始错误包在自己的 `DrizzleQueryError` * 里,因此 `error.code` 是 undefined,真正的 PG 错误码在 `error.cause.code`。 * 这里沿 cause 链向下查找,最多 5 层,避免无限循环。 */ function extractPostgresErrorCode(error: unknown): string | undefined { let current = error; for (let depth = 0; depth < 5 && current; depth++) { const code = (current as { code?: unknown }).code; // PG 错误码是 5 位数字字符串(如 23505);排除其他库的同名字段 if (typeof code === "string" && /^\d{5}$/.test(code)) { return code; } current = (current as { cause?: unknown }).cause; } return undefined; } /** * 路由处理器的错误包装器。 * * 统一处理三类异常: * 1. ApiError → 按错误码返回 * 2. ZodError → 400 + 字段级 details * 3. 其他(含数据库错误)→ 500,并把细节写进服务端日志 * * 数据库约束冲突单独识别为 4xx,这对 slug 重复是常见场景。 */ export function withErrorHandling( handler: (...args: Args) => Promise, ) { return async (...args: Args): Promise => { try { return await handler(...args); } catch (error) { if (error instanceof ApiError) { return fail(error.code, error.message, error.details); } if (error instanceof ZodError) { return fail( ErrorCode.VALIDATION_ERROR, "请求参数校验失败", zodToDetails(error), ); } const pgCode = extractPostgresErrorCode(error); // 唯一约束冲突 if (pgCode === "23505") { return fail(ErrorCode.CONFLICT, "该 slug 已存在,请换一个", [ { field: "slug", message: "该 slug 已被占用" }, ]); } // CHECK 约束冲突(如非法 status/slug 格式) if (pgCode === "23514") { return fail( ErrorCode.VALIDATION_ERROR, "数据不满足数据库约束,请检查 status 与 slug 格式", ); } // 非空约束冲突 if (pgCode === "23502") { return fail(ErrorCode.VALIDATION_ERROR, "缺少必填字段"); } // 字符串长度超限 if (pgCode === "22001") { return fail(ErrorCode.VALIDATION_ERROR, "字段长度超出限制"); } console.error("[api] 未处理的异常:", error); return fail(ErrorCode.INTERNAL_ERROR, "服务器内部错误"); } }; } /** 解析并校验 JSON 请求体 */ export async function parseJsonBody(request: Request): Promise { try { return await request.json(); } catch { throw ApiError.badRequest("请求体不是合法的 JSON"); } }