313 lines
7.8 KiB
TypeScript
313 lines
7.8 KiB
TypeScript
/**
|
||
* 统一 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 }
|
||
}
|