|
|
/**
|
|
|
* 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<FlockCheckoutTier, FlockFlexibilityOption[]>
|
|
|
>;
|
|
|
|
|
|
export type FlockCarriersByTier = Partial<
|
|
|
Record<FlockCheckoutTier, FlockCarrierOption[]>
|
|
|
>;
|
|
|
|
|
|
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<void> {
|
|
|
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<FlockQuoteHoldState | null> {
|
|
|
const raw = await withBoundedRedis((redis) => redis.get(key(sessionId)));
|
|
|
if (!raw) return null;
|
|
|
return parseFlockQuoteHoldJson(raw);
|
|
|
}
|
|
|
|
|
|
export async function readFlockQuoteHoldByQuoteId(
|
|
|
quoteId: string,
|
|
|
): Promise<FlockQuoteHoldState | null> {
|
|
|
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<FlockQuoteHoldState> {
|
|
|
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<void> {
|
|
|
await withBoundedRedis((redis) =>
|
|
|
redis.setex(flexKey(quoteId), ttlSeconds(), JSON.stringify(byTier)),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export async function readFlockHoldFlexibility(
|
|
|
quoteId: string,
|
|
|
): Promise<FlockFlexibilityByTier | null> {
|
|
|
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<void> {
|
|
|
await withBoundedRedis((redis) =>
|
|
|
redis.setex(carriersKey(quoteId), ttlSeconds(), JSON.stringify(byTier)),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export async function readFlockHoldCarriers(
|
|
|
quoteId: string,
|
|
|
): Promise<FlockCarriersByTier | null> {
|
|
|
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<FlockQuoteHoldState | null> {
|
|
|
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<void> {
|
|
|
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<FlockQuoteHoldState> {
|
|
|
const state = await readFlockQuoteHold(sessionId);
|
|
|
if (!state) throw new Error("询价会话已失效,请重新询价");
|
|
|
if (isFlockQuoteHoldExpired(state)) {
|
|
|
throw new Error("二级填写已超时(超过 5 分钟),请重新询价");
|
|
|
}
|
|
|
return state;
|
|
|
}
|