223 lines
6.4 KiB
TypeScript
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)
|
|
}
|
|
}
|