import { Prisma } from "@prisma/client"; import { isFlockRpaEnabled } from "@/lib/flock/env"; import { getFlockBatchExclusiveBusyMessage } from "@/lib/flock/exclusive-lock"; import { FLOCK_UI } from "@/lib/flock/ui-labels"; import { prisma } from "@/lib/prisma"; import { hashFlockInput } from "@/modules/flock/hash"; import { enqueueFlockQuoteJob } from "@/modules/flock/rpa-queue"; import { validateFlockQuoteInput } from "@/modules/flock/validation"; import { generateQuoteId, handleQuoteIdConflict, isQuoteIdUniqueConflict, } from "@/modules/quote/quote-id"; import { markQuoteStarted, safeRecord } from "@/modules/metrics/collector"; import { buildFlockProgressPayload } from "@/lib/flock/rpa-progress"; import type { FlockQuoteInput } from "@/workers/rpa/flock/types"; export class FlockEnqueueError extends Error { readonly code = "QUOTE_UNAVAILABLE" as const; constructor(message: string) { super(message); this.name = "FlockEnqueueError"; } } export type FlockSubmitResult = { quote_id: string; status: "processing"; }; function zipJson( zip: string, input: FlockQuoteInput, side: "pickup" | "delivery", ): Prisma.InputJsonValue { return { street: "", city: "", state: "", zip, place_id: "", formatted_address: zip, selected_from_suggestions: false, provider: "flock", ...(side === "pickup" ? { flock_pickup_date: input.pickupDate, flock_pallet_count: input.palletCount, flock_total_weight_lb: input.totalWeightLb, } : {}), }; } /** Flock Freight 询价入队 */ export async function submitFlockQuote(body: unknown): Promise { if (!isFlockRpaEnabled()) { throw new FlockEnqueueError(FLOCK_UI.disabledHint); } // 仅 batch 探针占锁时拒绝入队;Worker 查价中仍可排队(本机串行锁保证不互踩) const busy = getFlockBatchExclusiveBusyMessage(); if (busy) { throw new FlockEnqueueError(busy); } const parsed = validateFlockQuoteInput(body); const { request_id, customer_id, ...input } = parsed; const cargoHash = hashFlockInput(input); let quoteId = await generateQuoteId(); const baseRecord = { requestId: request_id, customerId: customer_id, cargoHash, status: "processing" as const, pickupJson: zipJson(input.pickupZip, input, "pickup"), deliveryJson: zipJson(input.deliveryZip, input, "delivery"), weightLb: input.totalWeightLb, dimLIn: input.lengthIn, dimWIn: input.widthIn, dimHIn: input.heightIn, palletCount: input.palletCount, cargoType: "general_freight", quotesJson: buildFlockProgressPayload( "queued", ) as unknown as Prisma.InputJsonValue, }; try { await prisma.quoteRecord.create({ data: { quoteId, ...baseRecord }, }); } catch (error) { if (!isQuoteIdUniqueConflict(error)) { throw error; } quoteId = await generateQuoteId(); try { await prisma.quoteRecord.create({ data: { quoteId, ...baseRecord }, }); } catch (retryError) { if (isQuoteIdUniqueConflict(retryError)) { await handleQuoteIdConflict(quoteId, customer_id); } throw retryError; } } try { await enqueueFlockQuoteJob({ quoteId, requestId: request_id, customerId: customer_id, cargoHash, input, }); } catch (error) { console.error("[flock] 入队失败:", error); await prisma.quoteRecord.update({ where: { quoteId }, data: { status: "failed", errorCode: "QUOTE_UNAVAILABLE", errorMessage: "Flock 报价队列暂不可用", }, }); throw new FlockEnqueueError("Flock 报价服务暂不可用,请稍后重试"); } safeRecord(() => markQuoteStarted(quoteId)); return { quote_id: quoteId, status: "processing" }; }