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.
chajia/lib/flock/flock-quote-hold-store.ts

231 lines
6.3 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.

/**
* 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;
}