2026-04-17 12:50:06 +08:00

313 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 统一 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<T>(
data: T,
meta?: Partial<ResponseMeta>
): ApiResponse<T> {
return {
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
requestId: meta?.requestId || nanoid(12),
...meta,
},
}
}
/**
* 创建分页成功响应
*/
export function successWithPagination<T>(
data: T,
pagination: PaginationMeta,
requestId?: string
): ApiResponse<T> {
return {
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
requestId: requestId || nanoid(12),
pagination,
},
}
}
/**
* 创建错误响应
*/
export function error(
code: ErrorCode,
message?: string,
details?: Record<string, unknown>,
requestId?: string
): ApiResponse<never> {
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<never> {
return error(appError.code, appError.message, appError.details, requestId)
}
// ============ NextResponse 包装器 ============
/**
* 返回成功的 NextResponse
*/
export function jsonSuccess<T>(
data: T,
status: number = HTTP_STATUS.OK,
requestId?: string
): NextResponse<ApiResponse<T>> {
return NextResponse.json(success(data, { requestId }), { status })
}
/**
* 返回分页成功的 NextResponse
*/
export function jsonSuccessWithPagination<T>(
data: T,
pagination: PaginationMeta,
requestId?: string
): NextResponse<ApiResponse<T>> {
return NextResponse.json(successWithPagination(data, pagination, requestId), {
status: HTTP_STATUS.OK,
})
}
/**
* 返回错误的 NextResponse
*/
export function jsonError(
code: ErrorCode,
message?: string,
status?: number,
requestId?: string
): NextResponse<ApiResponse<never>> {
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<ApiResponse<never>> {
return NextResponse.json(errorFromAppError(appError, requestId), {
status: appError.statusCode,
})
}
// ============ 请求处理包装器 ============
type RequestHandler<T> = (ctx: RequestContext) => Promise<T>
type OptionalAuthRequestHandler<T> = (ctx: OptionalAuthRequestContext) => Promise<T>
interface HandleRequestOptions {
/** 是否需要认证 */
requireAuth?: boolean
/** 允许的角色 */
allowedRoles?: AllowedRole[]
}
// 函数重载:requireAuth: false 时使用 OptionalAuthRequestContext
export async function handleRequest<T>(
request: NextRequest,
handler: OptionalAuthRequestHandler<NextResponse<T>>,
options: HandleRequestOptions & { requireAuth: false }
): Promise<NextResponse<T | ApiResponse<never>>>
// 函数重载:默认或 requireAuth: true 时使用 RequestContext
export async function handleRequest<T>(
request: NextRequest,
handler: RequestHandler<NextResponse<T>>,
options?: HandleRequestOptions
): Promise<NextResponse<T | ApiResponse<never>>>
// 实现
export async function handleRequest<T>(
request: NextRequest,
handler: RequestHandler<NextResponse<T>> | OptionalAuthRequestHandler<NextResponse<T>>,
options: HandleRequestOptions = {}
): Promise<NextResponse<T | ApiResponse<never>>> {
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<NextResponse<T>>)(ctx)
}
// 可选认证:尝试解析 token,失败不拦截
const optionalUser = await getUserFromRequest(request)
// 构建可选认证上下文(user 可选)
const ctx: OptionalAuthRequestContext = {
request,
user: optionalUser,
requestId,
startTime,
}
return await (handler as OptionalAuthRequestHandler<NextResponse<T>>)(ctx)
} catch (err) {
// 记录错误日志
console.error(`[${requestId}] Request error:`, err)
// 应用错误
if (isAppError(err)) {
return jsonErrorFromAppError(err, requestId) as NextResponse<ApiResponse<never>>
}
// 未知错误
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<AuthUser | undefined> {
// 从现有的认证中间件获取用户信息
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 }
}