import { Prisma } from "@prisma/client"; import { prisma } from "@/lib/prisma"; import { markQuoteStarted, safeRecord } from "@/modules/metrics/collector"; import { generateQuoteId, handleQuoteIdConflict, isQuoteIdUniqueConflict } from "@/modules/quote/quote-id"; import type { Priority1DemoInput } from "@/workers/rpa/priority1/demo-input"; import { hashPriority1Input } from "@/modules/priority1/hash"; import { enqueuePriority1QuoteJob } from "@/modules/priority1/rpa-queue"; import { validatePriority1QuoteInput } from "@/modules/priority1/validation"; export class Priority1EnqueueError extends Error { readonly code = "QUOTE_UNAVAILABLE" as const; constructor(message: string) { super(message); this.name = "Priority1EnqueueError"; } } export type Priority1SubmitResult = { quote_id: string; status: "processing"; }; function zipAddressJson( zip: string, input: Priority1DemoInput, side: "origin" | "destination", ): Prisma.InputJsonValue { return { street: "", city: "", state: "", zip, place_id: "", formatted_address: zip, selected_from_suggestions: false, ...(side === "origin" ? { priority1_shipment_type: input.shipmentType, priority1_pickup_date: input.pickupDate, priority1_commodity: input.commodity, } : {}), }; } function recordFields(input: Priority1DemoInput) { const weight = Number(input.weightLb) || 0; const length = Number(input.lengthIn) || 48; const width = Number(input.widthIn) || 40; const height = Number(input.heightIn) || 48; const pallets = Number(input.palletCount) || 1; const cargoType = input.shipmentType === "ltl" ? input.packaging.toLowerCase() : input.commodity.toLowerCase().replace(/\s+/g, "_").slice(0, 32); return { weightLb: weight, dimLIn: length, dimWIn: width, dimHIn: height, palletCount: pallets, cargoType: cargoType || "general_freight", }; } /** Priority1 询价入队 */ export async function submitPriority1Quote(body: unknown): Promise { const parsed = validatePriority1QuoteInput(body); const { request_id, customer_id, ...input } = parsed; const cargoHash = hashPriority1Input(input); let quoteId = await generateQuoteId(); const dims = recordFields(input); const baseRecord = { requestId: request_id, customerId: customer_id, cargoHash, status: "processing" as const, pickupJson: zipAddressJson(input.originZip, input, "origin"), deliveryJson: zipAddressJson(input.destinationZip, input, "destination"), weightLb: dims.weightLb, dimLIn: dims.dimLIn, dimWIn: dims.dimWIn, dimHIn: dims.dimHIn, palletCount: dims.palletCount, cargoType: dims.cargoType, quotesJson: Prisma.JsonNull, }; 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 enqueuePriority1QuoteJob({ quoteId, requestId: request_id, customerId: customer_id, cargoHash, input, }); } catch (error) { const brief = error instanceof Error ? error.message : String(error); console.error(`[priority1] 入队失败 quote_id=${quoteId}:`, error); const redisLike = /redis|REDIS_URL/i.test(brief); const userMessage = redisLike ? "Redis 不可用,无法提交 Priority1 询价(请确认 REDIS_URL 与 Worker 已启动)" : "Priority1 任务入队失败,请稍后重试或联系管理员"; await prisma.quoteRecord .update({ where: { quoteId }, data: { status: "failed", errorCode: "QUOTE_UNAVAILABLE", errorMessage: userMessage, }, }) .catch(() => undefined); throw new Priority1EnqueueError(userMessage); } safeRecord(() => markQuoteStarted(quoteId)); return { quote_id: quoteId, status: "processing" }; }