223 lines
6.4 KiB
TypeScript

import { randomUUID } from 'crypto'
import { NextRequest, NextResponse } from 'next/server'
import {
getApiKeyFromRequest,
getClientIp,
logApiCall,
validateApiKey,
} from '@/lib/external-api'
import {
completeExternalApiIdempotencyRecord,
ExternalWhiteBackgroundBarcodeError,
failExternalApiIdempotencyRecord,
normalizeExternalBarcode,
resolveStandardModelSkuByBarcode,
stableHashPayload,
STANDARD_MODEL_WHITE_BACKGROUND_BARCODE_OPERATION,
startExternalApiIdempotencyRecord,
} from '@/lib/external-standard-model-white-background-by-barcode'
import { cleanupStandardModelWhiteBackgroundForSku } from '@/lib/standard-model-white-background-service'
export const runtime = 'nodejs'
const ENDPOINT = '/api/external/standard-model-white-background/by-barcode/cleanup'
type TraceContext = {
traceId: string
startTime: number
barcode: string
ipAddress?: string
apiKeyName?: string
userAgent?: string
}
function buildErrorBody(code: string, message: string) {
return {
success: false,
error: {
code,
message,
},
}
}
function buildErrorResponse(status: number, code: string, message: string) {
return NextResponse.json(buildErrorBody(code, message), { status })
}
function buildSuccessBody<T>(data: T) {
return {
success: true,
data,
}
}
function getElapsedMs(startTime: number) {
return Math.max(0, Date.now() - startTime)
}
function finalizeResponse(response: NextResponse, trace: TraceContext) {
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}`)
if (trace.barcode) {
response.headers.set('X-Request-Barcode', trace.barcode)
}
return response
}
async function writeApiLog(params: {
apiKeyId: string
trace: TraceContext
statusCode: number
}) {
await logApiCall({
apiKeyId: params.apiKeyId,
endpoint: ENDPOINT,
method: 'POST',
queryType: 'barcode',
queryValue: params.trace.barcode || undefined,
statusCode: params.statusCode,
duration: getElapsedMs(params.trace.startTime),
ipAddress: params.trace.ipAddress,
userAgent: params.trace.userAgent,
})
}
function normalizeBarcodeList(value: unknown) {
if (!Array.isArray(value)) {
return []
}
return Array.from(
new Set(value.map((item) => normalizeExternalBarcode(item)).filter(Boolean)),
)
}
export async function POST(request: NextRequest) {
const trace: TraceContext = {
traceId: randomUUID(),
startTime: Date.now(),
barcode: '',
ipAddress: getClientIp(request),
userAgent: request.headers.get('user-agent') || undefined,
}
const apiKey = getApiKeyFromRequest(request)
const keyRecord = await validateApiKey(apiKey)
if (!keyRecord) {
return finalizeResponse(
buildErrorResponse(401, 'INVALID_API_KEY', 'API Key 无效或缺失'),
trace,
)
}
trace.apiKeyName = keyRecord.name
let idempotencyRecordId: string | null = null
try {
const body = await request.json()
const clientRequestId = String(body?.clientRequestId || '').trim()
const reason = String(body?.reason || '').trim()
const mode = String(body?.mode || 'soft').trim() || 'soft'
const skuBatchNo = body?.skuBatchNo ?? null
const barcodes = normalizeBarcodeList(body?.barcodes)
trace.barcode = barcodes.join(',')
if (!clientRequestId) {
await writeApiLog({ apiKeyId: keyRecord.id, trace, statusCode: 400 })
return finalizeResponse(
buildErrorResponse(400, 'INVALID_CLIENT_REQUEST_ID', 'clientRequestId 不能为空'),
trace,
)
}
if (barcodes.length === 0) {
await writeApiLog({ apiKeyId: keyRecord.id, trace, statusCode: 400 })
return finalizeResponse(buildErrorResponse(400, 'INVALID_BARCODE', 'barcodes 不能为空'), trace)
}
const requestHash = stableHashPayload({
clientRequestId,
reason,
mode,
skuBatchNo,
barcodes,
})
const idempotency = await startExternalApiIdempotencyRecord({
apiKeyId: keyRecord.id,
operationType: STANDARD_MODEL_WHITE_BACKGROUND_BARCODE_OPERATION.CLEANUP,
clientRequestId,
requestHash,
})
if (idempotency.mode === 'replay') {
await writeApiLog({ apiKeyId: keyRecord.id, trace, statusCode: idempotency.record.responseStatus || 200 })
return finalizeResponse(
NextResponse.json(idempotency.record.responseBody, {
status: idempotency.record.responseStatus || 200,
}),
trace,
)
}
const activeIdempotencyRecordId = String(idempotency.record.id)
idempotencyRecordId = activeIdempotencyRecordId
const results = []
for (const barcode of barcodes) {
const resolved = await resolveStandardModelSkuByBarcode(barcode)
const cleanup = await cleanupStandardModelWhiteBackgroundForSku({
materialId: resolved.materialId,
skuId: resolved.skuId,
operatorUserId: keyRecord.createdBy,
reason,
})
results.push({
barcode,
materialId: resolved.materialId,
skuId: resolved.skuId,
resolvedSpu: resolved.resolvedSpu,
resolvedSkuName: resolved.resolvedSkuName,
...cleanup,
})
}
const responseBody = buildSuccessBody({
traceId: trace.traceId,
cleanedCount: results.length,
mode,
reason,
results,
})
await completeExternalApiIdempotencyRecord({
recordId: activeIdempotencyRecordId,
responseStatus: 200,
responseBody,
})
await writeApiLog({ apiKeyId: keyRecord.id, trace, statusCode: 200 })
return finalizeResponse(NextResponse.json(responseBody), trace)
} catch (error) {
if (idempotencyRecordId) {
await failExternalApiIdempotencyRecord(idempotencyRecordId)
}
if (error instanceof ExternalWhiteBackgroundBarcodeError) {
await writeApiLog({ apiKeyId: keyRecord.id, trace, statusCode: error.status })
return finalizeResponse(buildErrorResponse(error.status, error.code, error.message), trace)
}
const message = error instanceof Error ? error.message : '清理白底图失败'
console.error('[external/standard-model-white-background/by-barcode/cleanup] POST error:', error)
await writeApiLog({ apiKeyId: keyRecord.id, trace, statusCode: 500 })
return finalizeResponse(buildErrorResponse(500, 'CLEANUP_FAILED', message || '清理白底图失败'), trace)
}
}