import crypto from 'crypto' import { Prisma } from '@prisma/client' import { prisma } from '@/lib/prisma' export const STANDARD_MODEL_WHITE_BACKGROUND_BARCODE_OPERATION = { UPLOAD: 'standard-model-white-background.by-barcode.upload', CLEANUP: 'standard-model-white-background.by-barcode.cleanup', UPLOAD_BY_SPU: 'standard-model-white-background.by-spu.upload', } as const export type BarcodeOperationType = typeof STANDARD_MODEL_WHITE_BACKGROUND_BARCODE_OPERATION[keyof typeof STANDARD_MODEL_WHITE_BACKGROUND_BARCODE_OPERATION] export class ExternalWhiteBackgroundBarcodeError extends Error { status: number code: string constructor(status: number, code: string, message: string) { super(message) this.status = status this.code = code } } export type ResolvedStandardModelSkuByBarcode = { barcode: string materialId: string skuId: string skuName: string resolvedSpu: string resolvedSkuName: string } let supportTablesInitialized = false async function executeRaw(sql: string) { try { await prisma.$executeRawUnsafe(sql) } catch (error) { console.error('[external-white-background-by-barcode] 执行 SQL 失败:', error) } } export async function ensureStandardModelWhiteBackgroundBarcodeSupportTables() { if (supportTablesInitialized) { return } await executeRaw(` CREATE TABLE IF NOT EXISTS "material_sku_barcodes" ( "id" TEXT PRIMARY KEY, "sku_id" TEXT NOT NULL, "barcode" TEXT NOT NULL, "deleted_at" TIMESTAMP(3), "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ) `) await executeRaw(` DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'material_sku_barcodes_sku_id_fkey' ) THEN ALTER TABLE "material_sku_barcodes" ADD CONSTRAINT "material_sku_barcodes_sku_id_fkey" FOREIGN KEY ("sku_id") REFERENCES "material_skus"("id") ON DELETE CASCADE ON UPDATE CASCADE; END IF; END $$; `) await executeRaw(` CREATE UNIQUE INDEX IF NOT EXISTS "material_sku_barcodes_barcode_active_key" ON "material_sku_barcodes" ("barcode") WHERE "deleted_at" IS NULL `) await executeRaw(`CREATE INDEX IF NOT EXISTS "material_sku_barcodes_barcode_idx" ON "material_sku_barcodes" ("barcode")`) await executeRaw(`CREATE INDEX IF NOT EXISTS "material_sku_barcodes_sku_id_idx" ON "material_sku_barcodes" ("sku_id")`) await executeRaw(`CREATE INDEX IF NOT EXISTS "material_sku_barcodes_deleted_at_idx" ON "material_sku_barcodes" ("deleted_at")`) await executeRaw(` CREATE TABLE IF NOT EXISTS "external_api_idempotency_records" ( "id" TEXT PRIMARY KEY, "api_key_id" TEXT NOT NULL, "operation_type" TEXT NOT NULL, "client_request_id" TEXT NOT NULL, "request_hash" TEXT NOT NULL, "status" TEXT NOT NULL, "response_status" INTEGER, "response_body" JSONB, "completed_at" TIMESTAMP(3), "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ) `) await executeRaw(` DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'external_api_idempotency_records_api_key_id_fkey' ) THEN ALTER TABLE "external_api_idempotency_records" ADD CONSTRAINT "external_api_idempotency_records_api_key_id_fkey" FOREIGN KEY ("api_key_id") REFERENCES "api_keys"("id") ON DELETE CASCADE ON UPDATE CASCADE; END IF; END $$; `) await executeRaw(` CREATE UNIQUE INDEX IF NOT EXISTS "external_api_idempotency_records_api_key_operation_client_key" ON "external_api_idempotency_records" ("api_key_id", "operation_type", "client_request_id") `) await executeRaw(`CREATE INDEX IF NOT EXISTS "external_api_idempotency_records_api_key_id_idx" ON "external_api_idempotency_records" ("api_key_id")`) await executeRaw(`CREATE INDEX IF NOT EXISTS "external_api_idempotency_records_operation_type_idx" ON "external_api_idempotency_records" ("operation_type")`) await executeRaw(`CREATE INDEX IF NOT EXISTS "external_api_idempotency_records_client_request_id_idx" ON "external_api_idempotency_records" ("client_request_id")`) await executeRaw(`CREATE INDEX IF NOT EXISTS "external_api_idempotency_records_status_idx" ON "external_api_idempotency_records" ("status")`) await executeRaw(`CREATE INDEX IF NOT EXISTS "external_api_idempotency_records_created_at_idx" ON "external_api_idempotency_records" ("created_at")`) supportTablesInitialized = true } export function normalizeExternalBarcode(value: unknown) { return String(value || '').trim() } export function stableHashPayload(payload: unknown) { return crypto .createHash('sha256') .update(JSON.stringify(sortPayload(payload))) .digest('hex') } function sortPayload(payload: unknown): unknown { if (Array.isArray(payload)) { return payload.map(sortPayload) } if (payload && typeof payload === 'object') { return Object.keys(payload as Record) .sort() .reduce((acc, key) => { acc[key] = sortPayload((payload as Record)[key]) return acc }, {} as Record) } return payload } export async function startExternalApiIdempotencyRecord(input: { apiKeyId: string operationType: BarcodeOperationType clientRequestId: string requestHash: string }) { await ensureStandardModelWhiteBackgroundBarcodeSupportTables() if (!input.clientRequestId.trim()) { throw new ExternalWhiteBackgroundBarcodeError(400, 'INVALID_CLIENT_REQUEST_ID', 'clientRequestId 不能为空') } try { const result = await prisma.$transaction( async (tx: any) => { const existing = await tx.externalApiIdempotencyRecord.findUnique({ where: { apiKeyId_operationType_clientRequestId: { apiKeyId: input.apiKeyId, operationType: input.operationType, clientRequestId: input.clientRequestId, }, }, }) if (existing) { if (existing.requestHash !== input.requestHash) { throw new ExternalWhiteBackgroundBarcodeError( 409, 'IDEMPOTENCY_CONFLICT', '同一个 clientRequestId 已用于不同请求,请更换 clientRequestId', ) } if (existing.status === 'succeeded' && existing.responseBody && existing.responseStatus) { return { mode: 'replay' as const, record: existing, } } if (existing.status === 'failed') { const record = await tx.externalApiIdempotencyRecord.update({ where: { id: existing.id }, data: { status: 'processing', responseStatus: null, responseBody: Prisma.JsonNull, completedAt: null, }, }) return { mode: 'created' as const, record, } } throw new ExternalWhiteBackgroundBarcodeError( 409, 'IDEMPOTENCY_CONFLICT', '同一个 clientRequestId 的请求仍在处理中,请稍后重试', ) } const record = await tx.externalApiIdempotencyRecord.create({ data: { apiKeyId: input.apiKeyId, operationType: input.operationType, clientRequestId: input.clientRequestId, requestHash: input.requestHash, status: 'processing', }, }) return { mode: 'created' as const, record, } }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable, }, ) return result } catch (error: any) { if (error instanceof ExternalWhiteBackgroundBarcodeError) { throw error } if (error?.code === 'P2002') { const existing = await (prisma as any).externalApiIdempotencyRecord.findUnique({ where: { apiKeyId_operationType_clientRequestId: { apiKeyId: input.apiKeyId, operationType: input.operationType, clientRequestId: input.clientRequestId, }, }, }) if (existing?.requestHash !== input.requestHash) { throw new ExternalWhiteBackgroundBarcodeError( 409, 'IDEMPOTENCY_CONFLICT', '同一个 clientRequestId 已用于不同请求,请更换 clientRequestId', ) } if (existing?.status === 'succeeded' && existing.responseBody && existing.responseStatus) { return { mode: 'replay' as const, record: existing, } } if (existing?.status === 'failed') { const record = await (prisma as any).externalApiIdempotencyRecord.update({ where: { id: existing.id }, data: { status: 'processing', responseStatus: null, responseBody: Prisma.JsonNull, completedAt: null, }, }) return { mode: 'created' as const, record, } } throw new ExternalWhiteBackgroundBarcodeError( 409, 'IDEMPOTENCY_CONFLICT', '同一个 clientRequestId 的请求仍在处理中,请稍后重试', ) } throw error } } export async function completeExternalApiIdempotencyRecord(input: { recordId: string responseStatus: number responseBody: unknown }) { await (prisma as any).externalApiIdempotencyRecord.update({ where: { id: input.recordId }, data: { status: 'succeeded', responseStatus: input.responseStatus, responseBody: input.responseBody, completedAt: new Date(), }, }) } export async function failExternalApiIdempotencyRecord(recordId: string) { try { await (prisma as any).externalApiIdempotencyRecord.update({ where: { id: recordId }, data: { status: 'failed', }, }) } catch (error) { console.error('[external-white-background-by-barcode] 更新幂等失败状态失败:', error) } } export async function resolveStandardModelSkuByBarcode( barcode: string, ): Promise { await ensureStandardModelWhiteBackgroundBarcodeSupportTables() const normalizedBarcode = normalizeExternalBarcode(barcode) if (!normalizedBarcode) { throw new ExternalWhiteBackgroundBarcodeError(400, 'INVALID_BARCODE', 'barcode 不能为空') } const matches = await (prisma as any).materialSkuBarcode.findMany({ where: { barcode: normalizedBarcode, deletedAt: null, }, select: { id: true, barcode: true, sku: { select: { id: true, skuName: true, deletedAt: true, materialId: true, material: { select: { id: true, productModel: true, materialType: true, deletedAt: true, }, }, }, }, }, take: 3, }) if (matches.length === 0) { throw new ExternalWhiteBackgroundBarcodeError(404, 'BARCODE_NOT_FOUND', '未找到对应的标准模型 SKU 条码绑定') } const validTargets = matches.filter((match: any) => match.sku) const activeSkuIds = Array.from(new Set(validTargets.map((match: any) => String(match.sku.id)))) if (activeSkuIds.length > 1 || matches.length > 1) { throw new ExternalWhiteBackgroundBarcodeError(409, 'BARCODE_AMBIGUOUS', '该 barcode 绑定了多个 SKU,无法确定唯一标准模型 SKU') } const match = validTargets[0] const sku = match?.sku const material = sku?.material if (!sku) { throw new ExternalWhiteBackgroundBarcodeError(404, 'BARCODE_NOT_FOUND', '未找到对应的标准模型 SKU 条码绑定') } if (sku.deletedAt) { throw new ExternalWhiteBackgroundBarcodeError(410, 'SKU_DELETED', '该 barcode 绑定的 SKU 已被删除') } if (!material) { throw new ExternalWhiteBackgroundBarcodeError( 404, 'BARCODE_NOT_BOUND_TO_STANDARD_MODEL_SKU', '该 barcode 未绑定到有效标准模型素材', ) } if (material.deletedAt) { throw new ExternalWhiteBackgroundBarcodeError(410, 'MATERIAL_DELETED', '该 barcode 绑定的素材已被删除') } if (material.materialType !== '标准模型') { throw new ExternalWhiteBackgroundBarcodeError( 400, 'BARCODE_NOT_BOUND_TO_STANDARD_MODEL_SKU', '该 barcode 绑定的不是标准模型 SKU', ) } return { barcode: normalizedBarcode, materialId: sku.materialId, skuId: sku.id, skuName: sku.skuName, resolvedSpu: material.productModel, resolvedSkuName: sku.skuName, } }