You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

183 lines
5.8 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<string, string> = {
general_freight: "普通货物",
machinery: "机械设备",
furniture: "家具",
electronics: "电子产品",
building_materials: "建材",
auto_parts: "汽车配件",
food_nonperishable: "非生鲜食品",
apparel: "服装",
other: "其他",
};
function asRecord(value: unknown): Record<string, unknown> | null {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
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<string, unknown>;
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<Record<AlertType, string>> = {
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),
};
}