import type { Prisma } from "@prisma/client"; import { prisma } from "@/lib/prisma"; import { formatCustomerAddressForAdmin, formatSelectedAddressForAdmin, formatCargoForAdmin, } from "@/modules/alert/alert-presentation"; import type { NormalizedCargo } from "@/modules/quote/types"; export const QUOTE_QUERY_LOG_MAX_ROWS = 1000; export type QuoteQueryOutcome = "processing" | "success" | "failed" | "stale"; function buildCargoJson(cargo: NormalizedCargo): Prisma.InputJsonValue { return { weight_lb: cargo.weightLb, dim_l_in: cargo.dimLIn, dim_w_in: cargo.dimWIn, dim_h_in: cargo.dimHIn, pallet_count: cargo.palletCount, cargo_type: cargo.cargoType, }; } async function trimQuoteQueryLogs(): Promise { const total = await prisma.quoteQueryLog.count(); if (total <= QUOTE_QUERY_LOG_MAX_ROWS) { return; } const excess = total - QUOTE_QUERY_LOG_MAX_ROWS; const oldest = await prisma.quoteQueryLog.findMany({ orderBy: { createdAt: "asc" }, take: excess, select: { id: true }, }); if (oldest.length === 0) { return; } await prisma.quoteQueryLog.deleteMany({ where: { id: { in: oldest.map((row) => row.id) } }, }); } /** 询价提交时写入查询审计(processing) */ export async function recordQuoteQueryStart( cargo: NormalizedCargo, quoteId: string, outcome: QuoteQueryOutcome = "processing", ): Promise { try { await prisma.quoteQueryLog.upsert({ where: { quoteId }, create: { quoteId, requestId: cargo.requestId, customerId: cargo.customerId, pickupJson: cargo.pickupAddress as Prisma.InputJsonValue, deliveryJson: cargo.deliveryAddress as Prisma.InputJsonValue, cargoJson: buildCargoJson(cargo), outcome, }, update: { outcome, }, }); await trimQuoteQueryLogs(); } catch (error) { console.error("[quote-query-log] 写入失败:", error); } } /** 询价完成或失败时更新审计 */ export async function recordQuoteQueryOutcome( quoteId: string, outcome: QuoteQueryOutcome, options?: { failureReason?: string | null; errorCode?: string | null; sourceType?: string | null; tierCount?: number | null; }, ): Promise { try { await prisma.quoteQueryLog.updateMany({ where: { quoteId }, data: { outcome, failureReason: options?.failureReason ?? null, errorCode: options?.errorCode ?? null, sourceType: options?.sourceType ?? null, tierCount: options?.tierCount ?? null, completedAt: outcome === "processing" ? null : new Date(), }, }); } catch (error) { console.error("[quote-query-log] 更新失败:", error); } } export type QuoteQueryLogDto = { quote_id: string; request_id: string; customer_id: string; outcome: QuoteQueryOutcome; failure_reason: string | null; error_code: string | null; source_type: string | null; tier_count: number | null; pickup_customer: string; pickup_selected: string; delivery_customer: string; delivery_selected: string; cargo_summary: string; created_at: string; completed_at: string | null; }; function cargoJsonToRecord(cargoJson: unknown) { if (!cargoJson || typeof cargoJson !== "object") { return null; } const c = cargoJson as Record; return { weightLb: c.weight_lb as number | undefined, palletCount: c.pallet_count as number | undefined, cargoType: c.cargo_type as string | undefined, dimLIn: c.dim_l_in as number | undefined, dimWIn: c.dim_w_in as number | undefined, dimHIn: c.dim_h_in as number | undefined, }; } function serializeQueryLog(row: { quoteId: string; requestId: string; customerId: string; outcome: string; failureReason: string | null; errorCode: string | null; sourceType: string | null; tierCount: number | null; pickupJson: unknown; deliveryJson: unknown; cargoJson: unknown; createdAt: Date; completedAt: Date | null; }): QuoteQueryLogDto { const cargoRecord = cargoJsonToRecord(row.cargoJson); return { quote_id: row.quoteId, request_id: row.requestId, customer_id: row.customerId, outcome: row.outcome as QuoteQueryOutcome, failure_reason: row.failureReason, error_code: row.errorCode, source_type: row.sourceType, tier_count: row.tierCount, pickup_customer: formatCustomerAddressForAdmin(row.pickupJson), pickup_selected: formatSelectedAddressForAdmin(row.pickupJson), delivery_customer: formatCustomerAddressForAdmin(row.deliveryJson), delivery_selected: formatSelectedAddressForAdmin(row.deliveryJson), cargo_summary: formatCargoForAdmin(row.cargoJson, cargoRecord as never), created_at: row.createdAt.toISOString(), completed_at: row.completedAt?.toISOString() ?? null, }; } export async function listQuoteQueryLogs(options: { page: number; size: number; customerId?: string; outcome?: QuoteQueryOutcome; }): Promise<{ list: QuoteQueryLogDto[]; total: number; page: number; size: number; }> { const where = { ...(options.customerId ? { customerId: options.customerId } : {}), ...(options.outcome ? { outcome: options.outcome } : {}), }; const [total, rows] = await Promise.all([ prisma.quoteQueryLog.count({ where }), prisma.quoteQueryLog.findMany({ where, orderBy: { createdAt: "desc" }, skip: (options.page - 1) * options.size, take: options.size, }), ]); return { list: rows.map((row) => serializeQueryLog(row)), total, page: options.page, size: options.size, }; }