2026-05-07 16:37:22 +08:00

146 lines
3.6 KiB
TypeScript

import { randomUUID } from 'crypto'
import { NextRequest, NextResponse } from 'next/server'
import {
getApiKeyFromRequest,
getClientIp,
logApiCall,
successResponse,
validateApiKey,
} from '@/lib/external-api'
import { prisma } from '@/lib/prisma'
import { MATERIAL_TYPE_OPTIONS } from '@/lib/material-type-options'
type TraceContext = {
traceId: string
startTime: number
ipAddress?: string
apiKeyName?: string
}
function getElapsedMs(startTime: number) {
return Math.max(0, Date.now() - startTime)
}
function buildErrorResponse(status: number, code: string, message: string) {
return NextResponse.json(
{
success: false,
error: {
code,
message,
},
},
{ status },
)
}
function finalizeResponse(
response: NextResponse,
trace: TraceContext,
statusCode: number,
outcome: string,
) {
const duration = getElapsedMs(trace.startTime)
response.headers.set('X-Trace-Id', trace.traceId)
response.headers.set('X-Server-Duration-Ms', String(duration))
response.headers.set('Server-Timing', `app;dur=${duration}`)
console.info(
'[external/dingtalk-material-options]',
JSON.stringify({
traceId: trace.traceId,
outcome,
statusCode,
duration,
ipAddress: trace.ipAddress || null,
apiKeyName: trace.apiKeyName || null,
}),
)
return response
}
export async function GET(request: NextRequest) {
const trace: TraceContext = {
traceId: randomUUID(),
startTime: Date.now(),
ipAddress: getClientIp(request),
}
const apiKey = getApiKeyFromRequest(request, { allowQuery: true })
const keyRecord = await validateApiKey(apiKey)
if (!keyRecord) {
return finalizeResponse(
buildErrorResponse(401, 'INVALID_API_KEY', 'API Key 无效或缺失'),
trace,
401,
'INVALID_API_KEY',
)
}
trace.apiKeyName = keyRecord.name
try {
const [brands, sceneCategories, productCategories] = await Promise.all([
prisma.category.findMany({
orderBy: { name: 'asc' },
select: { name: true },
}),
prisma.scene.findMany({
orderBy: { name: 'asc' },
select: { name: true },
}),
prisma.productCategory.findMany({
orderBy: { name: 'asc' },
select: { name: true },
}),
])
const duration = getElapsedMs(trace.startTime)
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/dingtalk-material-options',
method: 'GET',
queryType: 'options',
queryValue: 'all',
statusCode: 200,
duration,
ipAddress: trace.ipAddress,
userAgent: request.headers.get('user-agent') || undefined,
})
return finalizeResponse(
successResponse({
requirementTypes: [...MATERIAL_TYPE_OPTIONS],
brands: brands.map((item) => item.name),
sceneCategories: sceneCategories.map((item) => item.name),
productCategories: productCategories.map((item) => item.name),
}),
trace,
200,
'OPTIONS_OK',
)
} catch (error) {
console.error('[external/dingtalk-material-options] error:', error)
const duration = getElapsedMs(trace.startTime)
await logApiCall({
apiKeyId: keyRecord.id,
endpoint: '/api/external/dingtalk-material-options',
method: 'GET',
queryType: 'options',
queryValue: 'all',
statusCode: 500,
duration,
ipAddress: trace.ipAddress,
userAgent: request.headers.get('user-agent') || undefined,
})
return finalizeResponse(
buildErrorResponse(500, 'SERVER_ERROR', '服务器内部错误'),
trace,
500,
'SERVER_ERROR',
)
}
}