146 lines
3.6 KiB
TypeScript
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',
|
|
)
|
|
}
|
|
}
|