/** * 统一 API 响应处理 * * @created 2026-01-30 */ import { NextRequest, NextResponse } from 'next/server' import { nanoid } from 'nanoid' import type { ApiResponse, PaginationMeta, ResponseMeta, AuthUser, RequestContext, OptionalAuthRequestContext } from './types' import { AppError, ErrorCode, ERROR_MESSAGES, isAppError } from './errors' import { HTTP_STATUS, ENV } from './constants' import type { AllowedRole } from '@/lib/role-scope' import { hasAllowedRole } from '@/lib/role-scope' // ============ 响应构建器 ============ /** * 创建成功响应 */ export function success( data: T, meta?: Partial ): ApiResponse { return { success: true, data, meta: { timestamp: new Date().toISOString(), requestId: meta?.requestId || nanoid(12), ...meta, }, } } /** * 创建分页成功响应 */ export function successWithPagination( data: T, pagination: PaginationMeta, requestId?: string ): ApiResponse { return { success: true, data, meta: { timestamp: new Date().toISOString(), requestId: requestId || nanoid(12), pagination, }, } } /** * 创建错误响应 */ export function error( code: ErrorCode, message?: string, details?: Record, requestId?: string ): ApiResponse { return { success: false, error: { code, message: message || ERROR_MESSAGES[code], ...(ENV.isDev && details ? { details } : {}), }, meta: { timestamp: new Date().toISOString(), requestId: requestId || nanoid(12), }, } } /** * 从 AppError 创建错误响应 */ export function errorFromAppError( appError: AppError, requestId?: string ): ApiResponse { return error(appError.code, appError.message, appError.details, requestId) } // ============ NextResponse 包装器 ============ /** * 返回成功的 NextResponse */ export function jsonSuccess( data: T, status: number = HTTP_STATUS.OK, requestId?: string ): NextResponse> { return NextResponse.json(success(data, { requestId }), { status }) } /** * 返回分页成功的 NextResponse */ export function jsonSuccessWithPagination( data: T, pagination: PaginationMeta, requestId?: string ): NextResponse> { return NextResponse.json(successWithPagination(data, pagination, requestId), { status: HTTP_STATUS.OK, }) } /** * 返回错误的 NextResponse */ export function jsonError( code: ErrorCode, message?: string, status?: number, requestId?: string ): NextResponse> { const statusCode = status || HTTP_STATUS.BAD_REQUEST return NextResponse.json(error(code, message, undefined, requestId), { status: statusCode }) } /** * 从 AppError 返回错误的 NextResponse */ export function jsonErrorFromAppError( appError: AppError, requestId?: string ): NextResponse> { return NextResponse.json(errorFromAppError(appError, requestId), { status: appError.statusCode, }) } // ============ 请求处理包装器 ============ type RequestHandler = (ctx: RequestContext) => Promise type OptionalAuthRequestHandler = (ctx: OptionalAuthRequestContext) => Promise interface HandleRequestOptions { /** 是否需要认证 */ requireAuth?: boolean /** 允许的角色 */ allowedRoles?: AllowedRole[] } // 函数重载:requireAuth: false 时使用 OptionalAuthRequestContext export async function handleRequest( request: NextRequest, handler: OptionalAuthRequestHandler>, options: HandleRequestOptions & { requireAuth: false } ): Promise>> // 函数重载:默认或 requireAuth: true 时使用 RequestContext export async function handleRequest( request: NextRequest, handler: RequestHandler>, options?: HandleRequestOptions ): Promise>> // 实现 export async function handleRequest( request: NextRequest, handler: RequestHandler> | OptionalAuthRequestHandler>, options: HandleRequestOptions = {} ): Promise>> { const requestId = nanoid(12) const startTime = Date.now() try { // 获取认证用户(如果有) let user: AuthUser | undefined if (options.requireAuth !== false) { // 这里暂时使用简化逻辑,后续会替换为真正的认证中间件 user = await getUserFromRequest(request) if (!user) { return jsonError( ErrorCode.AUTH_INVALID_TOKEN, '请先登录', HTTP_STATUS.UNAUTHORIZED, requestId ) } // 角色检查 if (options.allowedRoles && !hasAllowedRole(user, options.allowedRoles)) { return jsonError( ErrorCode.AUTH_PERMISSION_DENIED, '没有权限执行此操作', HTTP_STATUS.FORBIDDEN, requestId ) } // 构建认证上下文(user 必填) const ctx: RequestContext = { request, user, requestId, startTime, } return await (handler as RequestHandler>)(ctx) } // 可选认证:尝试解析 token,失败不拦截 const optionalUser = await getUserFromRequest(request) // 构建可选认证上下文(user 可选) const ctx: OptionalAuthRequestContext = { request, user: optionalUser, requestId, startTime, } return await (handler as OptionalAuthRequestHandler>)(ctx) } catch (err) { // 记录错误日志 console.error(`[${requestId}] Request error:`, err) // 应用错误 if (isAppError(err)) { return jsonErrorFromAppError(err, requestId) as NextResponse> } // 未知错误 const message = ENV.isDev && err instanceof Error ? err.message : '系统内部错误' return jsonError( ErrorCode.SYSTEM_INTERNAL, message, HTTP_STATUS.INTERNAL_SERVER_ERROR, requestId ) } } /** * 从请求中获取用户信息 * 通过 auth-middleware 验证 JWT Token 并返回用户信息 */ async function getUserFromRequest(request: NextRequest): Promise { // 从现有的认证中间件获取用户信息 try { const { authenticateRequest } = await import('@/lib/auth-middleware') const result = await authenticateRequest(request) // authenticateRequest 返回 { success, userInfo } 而不是 { authenticated, user } if (!result.success || !result.userInfo) { return undefined } return { id: result.userInfo.id, email: result.userInfo.email, name: result.userInfo.name, role: result.userInfo.role as AuthUser['role'], status: result.userInfo.status as AuthUser['status'], departmentId: result.userInfo.departmentId, teamId: result.userInfo.teamId, managedDepartmentId: result.userInfo.managedDepartmentId, managedTeamId: result.userInfo.managedTeamId, } } catch { return undefined } } // ============ 工具函数 ============ /** * 生成唯一的请求 ID */ export function generateRequestId(): string { return nanoid(12) } /** * 计算分页元数据 */ export function calculatePagination( page: number, limit: number, total: number ): PaginationMeta { const totalPages = Math.ceil(total / limit) return { page, limit, total, totalPages, hasMore: page < totalPages, } } /** * 从查询参数获取分页参数 */ export function getPaginationFromParams( searchParams: URLSearchParams, defaultLimit: number = 20 ): { page: number; limit: number; skip: number } { const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10)) const limit = Math.min(100, Math.max(1, parseInt(searchParams.get('limit') || String(defaultLimit), 10))) const skip = (page - 1) * limit return { page, limit, skip } }