|
|
/** 登录态:首价后补充信息确认窗口(对客户不暴露时限/渠道细节) */
|
|
|
|
|
|
/**
|
|
|
* 决策窗:与总窗口对齐,前端不再单独倒计时逼迫用户。
|
|
|
* 驻留页仍需 Redis/浏览器 TTL;过短会导致填写中会话被清。
|
|
|
*/
|
|
|
export const MS_REFINE_DECISION_MS = 3_600_000;
|
|
|
|
|
|
/** 自首次驻留起的会话保留窗口(1 小时;仅内部资源回收,不对客户提示) */
|
|
|
export const MS_REFINE_TOTAL_MS = 3_600_000;
|
|
|
|
|
|
export const MS_REFINE_HOLD_REDIS_PREFIX = "ms_refine_hold:";
|
|
|
|
|
|
export type MsRefineHoldState = {
|
|
|
quote_id: string;
|
|
|
quote_session_id: string;
|
|
|
customer_id: string;
|
|
|
parked_at_ms: number;
|
|
|
decision_deadline_ms: number;
|
|
|
total_deadline_ms: number;
|
|
|
/** awaiting_decision | filling | refining | declined | refined | expired */
|
|
|
status:
|
|
|
| "awaiting_decision"
|
|
|
| "filling"
|
|
|
| "refining"
|
|
|
| "declined"
|
|
|
| "refined"
|
|
|
| "expired";
|
|
|
};
|
|
|
|
|
|
export type MsRefineHoldPublic = {
|
|
|
available: boolean;
|
|
|
quote_session_id: string;
|
|
|
decision_deadline_ms: number;
|
|
|
total_deadline_ms: number;
|
|
|
decision_remaining_ms: number;
|
|
|
total_remaining_ms: number;
|
|
|
};
|
|
|
|
|
|
export function buildMsRefineHoldState(input: {
|
|
|
quoteId: string;
|
|
|
sessionId: string;
|
|
|
customerId: string;
|
|
|
nowMs?: number;
|
|
|
}): MsRefineHoldState {
|
|
|
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 + MS_REFINE_DECISION_MS,
|
|
|
total_deadline_ms: now + MS_REFINE_TOTAL_MS,
|
|
|
status: "awaiting_decision",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
export function toMsRefineHoldPublic(
|
|
|
state: MsRefineHoldState,
|
|
|
nowMs = Date.now(),
|
|
|
): MsRefineHoldPublic {
|
|
|
const decision_remaining_ms = Math.max(0, state.decision_deadline_ms - nowMs);
|
|
|
const total_remaining_ms = Math.max(0, state.total_deadline_ms - nowMs);
|
|
|
/** filling/refining 期间始终可用,避免填写过程中因倒计时丢失提交入口 */
|
|
|
const available =
|
|
|
state.status === "refining" ||
|
|
|
state.status === "filling" ||
|
|
|
(state.status === "awaiting_decision" && total_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,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 不再因决策窗自动放弃:用户可随时选择继续或放弃。
|
|
|
* (保留函数签名,兼容 GET/服务层调用)
|
|
|
*/
|
|
|
export function shouldAutoDeclineRefineHold(
|
|
|
_state: MsRefineHoldState,
|
|
|
_nowMs = Date.now(),
|
|
|
): boolean {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
/** 内部会话窗口是否已过(资源回收 / 降级重查用,不对客户直出) */
|
|
|
export function isMsRefineHoldExpired(
|
|
|
state: MsRefineHoldState,
|
|
|
nowMs = Date.now(),
|
|
|
): boolean {
|
|
|
return nowMs >= state.total_deadline_ms;
|
|
|
}
|
|
|
|
|
|
/** 倒计时文案(内部/兼容;客户 UI 不再展示) */
|
|
|
export function formatMsRefineRemainingLabel(
|
|
|
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}秒`;
|
|
|
}
|
|
|
|
|
|
/** 客户可见提示(禁止:超时 / 二级 / 官网 / Save&update 等敏感词) */
|
|
|
export const MS_REFINE_MSG = {
|
|
|
decisionTimeout: "请补充必要信息后重新获取报价",
|
|
|
totalTimeout: "请补充必要信息后重新获取报价",
|
|
|
totalTimeoutApi: "暂时无法获取报价,请稍后重试",
|
|
|
decisionTimeoutApi: "暂时无法获取报价,请稍后重试",
|
|
|
sessionGone: "报价会话已失效,请重新询价",
|
|
|
} as const;
|
|
|
|
|
|
/** 一级无价、需补详情后刷价(落库 error_code,status=done) */
|
|
|
export const MS_NEEDS_DETAILS_ERROR_CODE = "MS_NEEDS_DETAILS" as const;
|
|
|
|
|
|
export const MS_NEEDS_DETAILS_MESSAGE =
|
|
|
"请补充必要信息后重新获取报价";
|