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.

168 lines
4.6 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 { Prisma } from "@prisma/client";
import { writeAlert } from "@/modules/alert/service";
import { getL3 } from "@/modules/cache/redis-cache";
import { QUOTE_VALIDITY_MS } from "@/lib/constants/quote";
import { prisma } from "@/lib/prisma";
import { getConfidenceScore } from "@/modules/quote/confidence";
import {
applyMarkupToQuotes,
getMarkupRule,
type RawQuoteTier,
} from "@/modules/pricing/engine";
import { prepareMotherShipStorageQuotes } from "@/modules/quote/quote-completeness";
import { recordPostDone, safeRecord } from "@/modules/metrics/collector";
import { RpaError } from "@/modules/rpa/errors";
import { isEntryErrorCode } from "@/modules/rpa/error-mapper";
import {
resolveQuoteFailureCode,
resolveQuoteFailureMessage,
truncateQuoteErrorMessage,
type QuoteFailureCode,
} from "@/modules/quote/quote-error-messages";
import type { L2CachePayload } from "@/modules/quote/types";
import { recordQuoteQueryOutcome } from "@/modules/quote/query-log";
export type FallbackResult =
| {
status: "done";
sourceType: "stale";
isRealtime: false;
quotes: ReturnType<typeof applyMarkupToQuotes>;
}
| {
status: "failed";
errorCode: QuoteFailureCode;
errorMessage: string;
};
/** 入口类错误不重试直接降级task-129 */
export function shouldConsumeRpaRetry(error: unknown): boolean {
if (error instanceof RpaError && isEntryErrorCode(error.code)) {
return false;
}
return true;
}
/**
* RPA 失败 → L3 降级完整编排task-092
* 不抛异常阻断主流程
*/
export async function handleRpaFailure(
quoteId: string,
cargoHash: string,
customerId: string,
error: unknown,
options?: { skipStaleFallback?: boolean },
): Promise<FallbackResult> {
const l3 = options?.skipStaleFallback
? null
: await getL3<L2CachePayload>(cargoHash);
const errorMessage =
error instanceof Error ? error.message : String(error ?? "unknown");
const failureCode = resolveQuoteFailureCode(error);
const userMessage = truncateQuoteErrorMessage(
resolveQuoteFailureMessage(error, failureCode),
);
if (l3?.quotes?.length) {
let uiQuotes: RawQuoteTier[];
try {
uiQuotes = prepareMotherShipStorageQuotes(l3.quotes as RawQuoteTier[]);
} catch {
await prisma.quoteRecord.update({
where: { quoteId },
data: {
status: "failed",
errorCode: failureCode,
errorMessage: userMessage,
sourceType: null,
isRealtime: false,
},
});
await writeAlert("RPA_FAILED", {
quoteId,
cargoHash,
detail: { reason: "L3 数据不完整", error: errorMessage },
});
await recordQuoteQueryOutcome(quoteId, "failed", {
failureReason: userMessage,
errorCode: "QUOTE_UNAVAILABLE",
});
return {
status: "failed",
errorCode: "QUOTE_UNAVAILABLE",
errorMessage: userMessage,
};
}
const markupRule = await getMarkupRule(customerId);
const markedQuotes = applyMarkupToQuotes(uiQuotes, markupRule);
const validUntil = new Date(Date.now() + QUOTE_VALIDITY_MS);
await prisma.quoteRecord.update({
where: { quoteId },
data: {
status: "done",
sourceType: "stale",
isRealtime: false,
confidenceScore: getConfidenceScore("stale"),
markupPercent:
markupRule.type === "percent" ? markupRule.percent : 0,
quotesJson: markedQuotes as unknown as Prisma.InputJsonValue,
validUntil,
errorCode: null,
},
});
await writeAlert("STALE_FALLBACK", {
quoteId,
cargoHash,
detail: { reason: errorMessage, customer_id: customerId },
});
await recordQuoteQueryOutcome(quoteId, "stale", {
failureReason: userMessage,
sourceType: "stale",
tierCount: markedQuotes.length,
});
safeRecord(() =>
recordPostDone({
quoteId,
sourceType: "stale",
isRealtime: false,
}),
);
return {
status: "done",
sourceType: "stale",
isRealtime: false,
quotes: markedQuotes,
};
}
await prisma.quoteRecord.update({
where: { quoteId },
data: {
status: "failed",
errorCode: failureCode,
errorMessage: userMessage,
isRealtime: false,
},
});
await writeAlert("RPA_FAILED", {
quoteId,
cargoHash,
detail: { reason: errorMessage, customer_id: customerId },
});
await recordQuoteQueryOutcome(quoteId, "failed", {
failureReason: userMessage,
errorCode: failureCode,
});
return { status: "failed", errorCode: failureCode, errorMessage: userMessage };
}