import type { QuoteRecord } from "@prisma/client"; import type { AlertType } from "@/modules/alert/types"; import { formatQuoteErrorMessage, normalizeTechnicalReason, } from "@/modules/quote/quote-error-messages"; const CARGO_TYPE_ZH: Record = { general_freight: "普通货物", machinery: "机械设备", furniture: "家具", electronics: "电子产品", building_materials: "建材", auto_parts: "汽车配件", food_nonperishable: "非生鲜食品", apparel: "服装", other: "其他", }; function asRecord(value: unknown): Record | null { if (value && typeof value === "object" && !Array.isArray(value)) { return value as Record; } return null; } /** 客户填写的地址(街道/城市/州/邮编) */ export function formatCustomerAddressForAdmin(addr: unknown): string { const o = asRecord(addr); if (!o) { return "—"; } const formatted = typeof o.formatted_address === "string" ? o.formatted_address.trim() : ""; const parts = [ typeof o.street === "string" ? o.street.trim() : "", typeof o.city === "string" ? o.city.trim() : "", typeof o.state === "string" ? o.state.trim() : "", typeof o.zip === "string" ? o.zip.trim() : "", ].filter(Boolean); if (parts.length > 0) { return parts.join(", "); } return formatted || "—"; } /** MotherShip 联想确认后的精确地址 */ export function formatSelectedAddressForAdmin(addr: unknown): string { const o = asRecord(addr); if (!o) { return "—"; } const label = (typeof o.mothership_display_label === "string" && o.mothership_display_label.trim()) || ""; return label || formatCustomerAddressForAdmin(addr); } /** @deprecated 使用 formatSelectedAddressForAdmin */ export function formatAddressForAdmin(addr: unknown): string { return formatSelectedAddressForAdmin(addr); } export function formatCargoForAdmin( cargo: unknown, record?: Pick< QuoteRecord, "weightLb" | "palletCount" | "cargoType" | "dimLIn" | "dimWIn" | "dimHIn" > | null, ): string { const o = asRecord(cargo); const pallets = record?.palletCount ?? o?.pallet_count; const cargoType = record?.cargoType ?? o?.cargo_type; const weightLb = record?.weightLb != null ? Number(record.weightLb) : typeof o?.weight_lb === "number" ? o.weight_lb : null; const parts: string[] = []; if (pallets != null) { parts.push(`${pallets} 托`); } if (weightLb != null && Number.isFinite(weightLb)) { parts.push(`${weightLb.toFixed(2)} lb`); } if (record) { parts.push( `${Number(record.dimLIn)}×${Number(record.dimWIn)}×${Number(record.dimHIn)} in`, ); } else if (o?.dimensions && typeof o.dimensions === "object") { const d = o.dimensions as Record; const unit = d.unit === "cm" ? "cm" : "in"; parts.push( `${d.length}×${d.width}×${d.height} ${unit}`, ); } if (typeof cargoType === "string") { parts.push(CARGO_TYPE_ZH[cargoType] ?? cargoType); } return parts.length > 0 ? parts.join(" · ") : "—"; } /** @deprecated 使用 modules/quote/quote-error-messages.normalizeTechnicalReason */ export { normalizeTechnicalReason }; const ALERT_TYPE_DEFAULT_CAUSE: Partial> = { RPA_FAILED: "询价自动化失败,未能获取 MotherShip 实时报价", RPA_CAPTCHA: "MotherShip 触发人机验证,RPA 已暂停", STRUCT_CHANGE: "MotherShip 页面结构变更,自动化脚本需更新", SESSION_EXPIRED: "MotherShip 会话过期,需重新登录或录制 cookies", STALE_FALLBACK: "实时询价失败,已降级使用历史缓存报价", PRICE_DEVIATION: "报价与历史基准偏差超过阈值", MANUAL_FALLBACK: "已触发人工兜底询价流程", SECURITY: "检测到安全相关异常请求", }; /** 管理端展示用:准确中文根因,避免直接展示 JSON */ export function resolveAlertRootCause( alertType: AlertType, detail: unknown, quote?: QuoteRecord | null, ): string { const d = asRecord(detail); const detailText = typeof d?.message === "string" ? normalizeTechnicalReason(d.message) : typeof d?.reason === "string" ? normalizeTechnicalReason(d.reason) : typeof d?.error === "string" ? normalizeTechnicalReason(d.error) : ""; if (quote?.errorMessage?.trim()) { const normalized = normalizeTechnicalReason(quote.errorMessage.trim()); return normalized || formatQuoteErrorMessage(quote.errorCode, quote.errorMessage); } if (quote?.errorCode) { return formatQuoteErrorMessage(quote.errorCode, quote.errorMessage); } if (detailText) { return detailText; } if (d?.entry_error === "QUOTE_ENTRY_UNAVAILABLE") { return "MotherShip 报价入口无法打开(网络/会话/被拦截)"; } return ALERT_TYPE_DEFAULT_CAUSE[alertType] ?? "系统预警,请结合报价单号追查"; } export type AlertPresentation = { root_cause: string; customer_id: string | null; pickup_customer: string; pickup_selected: string; delivery_customer: string; delivery_selected: string; cargo_summary: string; }; export function buildAlertPresentation( alertType: AlertType, detail: unknown, quote?: QuoteRecord | null, ): AlertPresentation { const pickup = quote?.pickupJson ?? null; const delivery = quote?.deliveryJson ?? null; return { root_cause: resolveAlertRootCause(alertType, detail, quote), customer_id: quote?.customerId ?? (typeof asRecord(detail)?.customer_id === "string" ? (asRecord(detail)!.customer_id as string) : null), pickup_customer: formatCustomerAddressForAdmin(pickup), pickup_selected: formatSelectedAddressForAdmin(pickup), delivery_customer: formatCustomerAddressForAdmin(delivery), delivery_selected: formatSelectedAddressForAdmin(delivery), cargo_summary: formatCargoForAdmin(null, quote ?? null), }; }