|
|
/** Flock 登录态:首价后二级填写确认窗口(对齐 MS refine-hold) */
|
|
|
|
|
|
/** 用户决定是否继续填二级详情的倒计时 */
|
|
|
export const FLOCK_HOLD_DECISION_MS = 60_000;
|
|
|
|
|
|
/** 自首次驻留起的总硬限(含二级填写与选档结账) */
|
|
|
export const FLOCK_HOLD_TOTAL_MS = 300_000;
|
|
|
|
|
|
export const FLOCK_HOLD_REDIS_PREFIX = "flock_quote_hold:";
|
|
|
|
|
|
export type FlockQuoteHoldStatus =
|
|
|
| "awaiting_decision"
|
|
|
| "filling"
|
|
|
| "checking_out"
|
|
|
| "declined"
|
|
|
| "done"
|
|
|
| "expired";
|
|
|
|
|
|
export type FlockQuoteHoldState = {
|
|
|
quote_id: string;
|
|
|
quote_session_id: string;
|
|
|
customer_id: string;
|
|
|
parked_at_ms: number;
|
|
|
decision_deadline_ms: number;
|
|
|
total_deadline_ms: number;
|
|
|
status: FlockQuoteHoldStatus;
|
|
|
};
|
|
|
|
|
|
export type FlockQuoteHoldPublic = {
|
|
|
available: boolean;
|
|
|
quote_session_id: string;
|
|
|
decision_deadline_ms: number;
|
|
|
total_deadline_ms: number;
|
|
|
decision_remaining_ms: number;
|
|
|
total_remaining_ms: number;
|
|
|
status: FlockQuoteHoldStatus;
|
|
|
};
|
|
|
|
|
|
export function buildFlockQuoteHoldState(input: {
|
|
|
quoteId: string;
|
|
|
sessionId: string;
|
|
|
customerId: string;
|
|
|
nowMs?: number;
|
|
|
}): FlockQuoteHoldState {
|
|
|
const now = input.nowMs ?? Date.now();
|
|
|
return {
|
|
|
quote_id: input.quoteId,
|
|
|
quote_session_id: input.sessionId,
|
|
|
customer_id: input.customerId,
|
|
|
parked_at_ms: now,
|
|
|
decision_deadline_ms: now + FLOCK_HOLD_DECISION_MS,
|
|
|
total_deadline_ms: now + FLOCK_HOLD_TOTAL_MS,
|
|
|
status: "awaiting_decision",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
export function toFlockQuoteHoldPublic(
|
|
|
state: FlockQuoteHoldState,
|
|
|
nowMs = Date.now(),
|
|
|
): FlockQuoteHoldPublic {
|
|
|
const decision_remaining_ms = Math.max(0, state.decision_deadline_ms - nowMs);
|
|
|
const total_remaining_ms = Math.max(0, state.total_deadline_ms - nowMs);
|
|
|
const available =
|
|
|
total_remaining_ms > 0 &&
|
|
|
(state.status === "checking_out" ||
|
|
|
state.status === "filling" ||
|
|
|
(state.status === "awaiting_decision" && decision_remaining_ms > 0));
|
|
|
return {
|
|
|
available,
|
|
|
quote_session_id: state.quote_session_id,
|
|
|
decision_deadline_ms: state.decision_deadline_ms,
|
|
|
total_deadline_ms: state.total_deadline_ms,
|
|
|
decision_remaining_ms,
|
|
|
total_remaining_ms,
|
|
|
status: state.status,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
export function shouldAutoDeclineFlockHold(
|
|
|
state: FlockQuoteHoldState,
|
|
|
nowMs = Date.now(),
|
|
|
): boolean {
|
|
|
if (state.status !== "awaiting_decision") return false;
|
|
|
return nowMs >= state.decision_deadline_ms;
|
|
|
}
|
|
|
|
|
|
export function isFlockQuoteHoldExpired(
|
|
|
state: FlockQuoteHoldState,
|
|
|
nowMs = Date.now(),
|
|
|
): boolean {
|
|
|
return nowMs >= state.total_deadline_ms;
|
|
|
}
|
|
|
|
|
|
export function formatFlockHoldRemainingLabel(
|
|
|
deadlineMs: number,
|
|
|
nowMs = Date.now(),
|
|
|
): string {
|
|
|
const sec = Math.max(0, Math.ceil((deadlineMs - nowMs) / 1000));
|
|
|
const m = Math.floor(sec / 60);
|
|
|
const s = sec % 60;
|
|
|
return m > 0 ? `${m}分${String(s).padStart(2, "0")}秒` : `${s}秒`;
|
|
|
}
|
|
|
|
|
|
export const FLOCK_HOLD_MSG = {
|
|
|
decisionTimeout: "确认窗口已超时,已保留初步报价",
|
|
|
totalTimeout: "二级填写已超时,已保留初步报价",
|
|
|
totalTimeoutApi: "二级填写已超时(超过 5 分钟),请重新询价",
|
|
|
decisionTimeoutApi: "确认窗口已超时,请重新询价",
|
|
|
sessionGone: "询价会话已失效,请重新询价",
|
|
|
} as const;
|
|
|
|
|
|
export function flockHoldSessionId(quoteId: string): string {
|
|
|
return `flock_${quoteId}`;
|
|
|
}
|