/** * Flock 登录态首价 hold(Redis) */ import { FLOCK_HOLD_REDIS_PREFIX, FLOCK_HOLD_TOTAL_MS, type FlockQuoteHoldState, buildFlockQuoteHoldState, isFlockQuoteHoldExpired, } from "@/lib/constants/flock-quote-hold"; import type { FlockCarrierOption, FlockCheckoutTier, FlockFlexibilityOption, } from "@/lib/flock/flock-checkout-selection"; import { withBoundedRedis } from "@/lib/redis"; import { flockCarriersByTierSchema, flockFlexibilityByTierSchema, flockQuoteHoldStateSchema, } from "@/modules/flock/hold-validation"; export type FlockFlexibilityByTier = Partial< Record >; export type FlockCarriersByTier = Partial< Record >; function key(sessionId: string): string { return `${FLOCK_HOLD_REDIS_PREFIX}${sessionId}`; } function flexKey(quoteId: string): string { return `${FLOCK_HOLD_REDIS_PREFIX}flex:${quoteId}`; } function carriersKey(quoteId: string): string { return `${FLOCK_HOLD_REDIS_PREFIX}carriers:${quoteId}`; } function ttlSeconds(): number { return Math.ceil(FLOCK_HOLD_TOTAL_MS / 1000) + 30; } export async function saveFlockQuoteHold( state: FlockQuoteHoldState, ): Promise { const ok = await withBoundedRedis((redis) => redis.setex( key(state.quote_session_id), ttlSeconds(), JSON.stringify(state), ), ); if (ok === null) { console.warn("[flock-quote-hold] save skipped: Redis 不可达"); } } /** 导出供单测:Redis JSON → hold state */ export function parseFlockQuoteHoldJson( raw: string, ): FlockQuoteHoldState | null { try { const parsed = flockQuoteHoldStateSchema.safeParse(JSON.parse(raw)); if (!parsed.success) { console.warn("[flock-quote-hold] invalid hold JSON", parsed.error.issues); return null; } return parsed.data; } catch { console.warn("[flock-quote-hold] hold JSON parse failed"); return null; } } /** 导出供单测:Redis JSON → 两侧灵活价 */ export function parseFlockHoldFlexibilityJson( raw: string, ): FlockFlexibilityByTier | null { try { const parsed = flockFlexibilityByTierSchema.safeParse(JSON.parse(raw)); if (!parsed.success) { console.warn( "[flock-quote-hold] invalid flex JSON", parsed.error.issues, ); return null; } return parsed.data; } catch { console.warn("[flock-quote-hold] flex JSON parse failed"); return null; } } export async function readFlockQuoteHold( sessionId: string, ): Promise { const raw = await withBoundedRedis((redis) => redis.get(key(sessionId))); if (!raw) return null; return parseFlockQuoteHoldJson(raw); } export async function readFlockQuoteHoldByQuoteId( quoteId: string, ): Promise { const sid = await withBoundedRedis((redis) => redis.get(`${FLOCK_HOLD_REDIS_PREFIX}by_quote:${quoteId}`), ); if (!sid) return null; return readFlockQuoteHold(sid); } export async function createFlockQuoteHold(input: { quoteId: string; sessionId: string; customerId: string; flexibilityByTier?: FlockFlexibilityByTier; carriersByTier?: FlockCarriersByTier; }): Promise { const state = buildFlockQuoteHoldState(input); await saveFlockQuoteHold(state); await withBoundedRedis((redis) => redis.setex( `${FLOCK_HOLD_REDIS_PREFIX}by_quote:${input.quoteId}`, ttlSeconds(), input.sessionId, ), ); if (input.flexibilityByTier) { await saveFlockHoldFlexibility(input.quoteId, input.flexibilityByTier); } if (input.carriersByTier && Object.keys(input.carriersByTier).length > 0) { await saveFlockHoldCarriers(input.quoteId, input.carriersByTier); } return state; } export async function saveFlockHoldFlexibility( quoteId: string, byTier: FlockFlexibilityByTier, ): Promise { await withBoundedRedis((redis) => redis.setex(flexKey(quoteId), ttlSeconds(), JSON.stringify(byTier)), ); } export async function readFlockHoldFlexibility( quoteId: string, ): Promise { const raw = await withBoundedRedis((redis) => redis.get(flexKey(quoteId))); if (!raw) return null; return parseFlockHoldFlexibilityJson(raw); } /** 导出供单测:Redis JSON → 两侧承运商列表 */ export function parseFlockHoldCarriersJson( raw: string, ): FlockCarriersByTier | null { try { const parsed = flockCarriersByTierSchema.safeParse(JSON.parse(raw)); if (!parsed.success) { console.warn( "[flock-quote-hold] invalid carriers JSON", parsed.error.issues, ); return null; } return parsed.data; } catch { console.warn("[flock-quote-hold] carriers JSON parse failed"); return null; } } export async function saveFlockHoldCarriers( quoteId: string, byTier: FlockCarriersByTier, ): Promise { await withBoundedRedis((redis) => redis.setex(carriersKey(quoteId), ttlSeconds(), JSON.stringify(byTier)), ); } export async function readFlockHoldCarriers( quoteId: string, ): Promise { const raw = await withBoundedRedis((redis) => redis.get(carriersKey(quoteId)), ); if (!raw) return null; return parseFlockHoldCarriersJson(raw); } export async function updateFlockQuoteHoldStatus( sessionId: string, status: FlockQuoteHoldState["status"], ): Promise { const cur = await readFlockQuoteHold(sessionId); if (!cur) return null; const next = { ...cur, status }; await saveFlockQuoteHold(next); return next; } export async function clearFlockQuoteHold(sessionId: string): Promise { const cur = await readFlockQuoteHold(sessionId); await withBoundedRedis(async (redis) => { await redis.del(key(sessionId)); if (cur?.quote_id) { await redis.del(`${FLOCK_HOLD_REDIS_PREFIX}by_quote:${cur.quote_id}`); await redis.del(flexKey(cur.quote_id)); await redis.del(carriersKey(cur.quote_id)); } }); } export async function assertFlockQuoteHoldUsable( sessionId: string, ): Promise { const state = await readFlockQuoteHold(sessionId); if (!state) throw new Error("询价会话已失效,请重新询价"); if (isFlockQuoteHoldExpired(state)) { throw new Error("二级填写已超时(超过 5 分钟),请重新询价"); } return state; }