import { IDEMPOTENCY_TTL_MS } from "@/lib/constants/quote"; import { prisma } from "@/lib/prisma"; import { getL1, setL1 } from "@/modules/cache/redis-cache"; export type IdempotencyHit = { hit: true; quoteId: string; response: unknown; }; export type IdempotencyMiss = { hit: false; }; export type IdempotencyResult = IdempotencyHit | IdempotencyMiss; /** 查 L1 Redis → DB 双保险,24h 内同 request_id 返回同一 quote_id */ export async function checkIdempotency( requestId: string, ): Promise { const cached = await getL1<{ quoteId: string; response: unknown }>( requestId, ); if (cached?.quoteId) { return { hit: true, quoteId: cached.quoteId, response: cached.response, }; } const record = await prisma.idempotencyRecord.findUnique({ where: { requestId }, select: { quoteId: true }, }); if (!record) { return { hit: false }; } const quote = await prisma.quoteRecord.findUnique({ where: { quoteId: record.quoteId }, select: { quoteId: true, status: true, sourceType: true, isRealtime: true, }, }); const response = { quote_id: record.quoteId, status: quote?.status ?? "done", }; await setL1(requestId, { quoteId: record.quoteId, response }); return { hit: true, quoteId: record.quoteId, response, }; } /** 写入 DB + Redis L1,处理唯一键冲突 */ export async function saveIdempotency( requestId: string, customerId: string, quoteId: string, response: unknown, ): Promise { const expireAt = new Date(Date.now() + IDEMPOTENCY_TTL_MS); try { await prisma.idempotencyRecord.create({ data: { requestId, customerId, quoteId, expireAt, }, }); } catch (error: unknown) { const prismaError = error as { code?: string }; if (prismaError.code === "P2002") { const existing = await prisma.idempotencyRecord.findUnique({ where: { requestId }, select: { quoteId: true }, }); if (existing) { return; } } throw error; } await setL1(requestId, { quoteId, response }); }