/** * 订单状态展示口径(hjc-web)。 * * **必须与 `hjc-h5/src/utils/orderStatus.js` 保持一致**——两端是不同仓库、不同技术栈 * (Nuxt4 + TS / uni-app + JS),代码无法真正共享,只能靠"两处同一套定义"来对齐。 * 改这里的时候,另一边也要改。 * * 优先级(从高到低): * payStatus === 3 → 已退款 * orderStatus === 2 → 已取消 * payStatus === 1 || orderStatus === 1 → 已完成 * 其余 → 待支付 * * 为什么需要它:hjc-web 此前**完全不读 `orderStatus`**(只看 payStatus), * 于是订单一旦被置为已取消,卡片仍会显示「待支付」并带上「去支付」按钮。 */ /** 订单状态归类键。四端(web / h5 / 小程序 / 后台)未来的展示都应落在这四个之一。 */ export type HjcOrderStatusKey = 'pending' | 'done' | 'cancel' | 'refund' /** * 取消来源「超时取消」——后端 `HjcCancelSource.TIMEOUT` 的 name。 * * 两种取消对外完全同形(都是「已取消」),区别只在这个来源,而它必须被展示出来: * 超时取消是**系统趁用户不在时替他做的**,用户看不到任何自己触发过的动作, * 而「我什么都没干,订单怎么没了」正是客服必然会接到的那类问询。 */ export const HJC_CANCEL_SOURCE_TIMEOUT = 'TIMEOUT' /** 判定所需的订单字段 */ export interface HjcOrderLike { payStatus?: number | null orderStatus?: number | null /** 取消来源:`BUYER`(买家取消)/ `TIMEOUT`(超时取消),仅已取消订单有值 */ cancelSource?: string | null /** * 支付时限的到期时刻(ISO 字符串)。**由后端下发**,前端不自己拿创建时刻加时限—— * 两边各算一次的后果是「倒计时归零」与「后端判定超时」在边界上错开。 */ payDeadline?: string | null } export function hjcOrderStatusKey(o?: HjcOrderLike | null): HjcOrderStatusKey { if (!o) return 'pending' if (o.payStatus === 3) return 'refund' if (o.orderStatus === 2) return 'cancel' if (o.payStatus === 1 || o.orderStatus === 1) return 'done' return 'pending' } const LABELS: Record = { pending: '待支付', done: '已完成', cancel: '已取消', refund: '已退款' } /** 这张单是不是**超时取消**的(而不是买家自己取消的) */ export function hjcIsTimeoutCancelled(o?: HjcOrderLike | null): boolean { return hjcOrderStatusKey(o) === 'cancel' && o?.cancelSource === HJC_CANCEL_SOURCE_TIMEOUT } export function hjcOrderStatusLabel(o?: HjcOrderLike | null): string { // 超时取消要说清是谁取消的:只说「已取消」,用户和客服都无法自证 if (hjcIsTimeoutCancelled(o)) return '已取消(支付超时)' return LABELS[hjcOrderStatusKey(o)] } /** * 解析后端下发的时间串。 * * **后端的时间格式是 `yyyy-MM-dd HH:mm:ss`**(`application.yml` 的 `date-format` 与 * `JacksonConfig` 共同决定:没有 `T`、没有时区)。而 `Date.parse` 对**不带 `T`** 的写法 * 在 **iOS Safari / 微信内置浏览器**上返回 `NaN`——那恰恰是本功能最主要的那一端 * (公众号 iOS)。后果是倒计时不显示、「到点锁页面」静默失效,而且不报任何错。 * * 所以统一在这里把空格换成 `T`:ES 规范的 ISO 无时区写法按**本地时间**解析, * 与服务端的 `GMT+8` 一致。返回 null 表示**不知道**(解析不出来), * 调用方必须按「不显示倒计时、不锁页面」处理——拿一个假值去锁死收银台更糟。 */ export function hjcParseServerTime(raw?: string | null): number | null { if (!raw) return null const text = String(raw).trim() if (!text) return null const normalized = text.includes('T') ? text : text.replace(' ', 'T') const ms = Date.parse(normalized) return Number.isNaN(ms) ? null : ms } /** 支付时限到期时刻的毫秒数;后端没下发或解析不出来时返回 `null` */ export function hjcPayDeadlineMs(o?: HjcOrderLike | null): number | null { return hjcParseServerTime(o?.payDeadline) } /** 收银台倒计时剩余毫秒;没有到期时刻时返回 null,已归零时返回 0(不是负数) */ export function hjcPayRemainMs(o?: HjcOrderLike | null, now: number = Date.now()): number | null { const deadline = hjcPayDeadlineMs(o) return deadline == null ? null : Math.max(0, deadline - now) } /** * 支付时限是否已到。 * * **到期时刻未知时返回 false**(不是 true):拿不到 `payDeadline` 就不该把按钮藏起来, * 那种"因为不知道所以当成过期"的写法会把正常订单的操作入口一并藏掉。 */ export function hjcIsPayExpired(o?: HjcOrderLike | null, now: number = Date.now()): boolean { const deadline = hjcPayDeadlineMs(o) return deadline != null && now >= deadline } /** * 页面用的完整判定:这张单**能操作**(去支付 / 取消)**且支付时限未到**。 * * 为什么必须共享而不是各页面自己拼:`hjcCanActOnOrder` 只管状态,时限要当前时刻, * 于是每个页面都会顺手写一遍 `canAct`——两处一分叉,就会出现"列表里不能点、详情里能点"。 * `hjc-h5` 那边导出的是 `canActOn`,两侧是同一套定义(见本文件头注释)。 */ export function hjcCanActOn(o?: HjcOrderLike | null, now: number = Date.now()): boolean { return hjcCanActOnOrder(o) && !hjcIsPayExpired(o, now) } /** 倒计时文案。超过一小时给 `h:mm:ss`,免得显示成 `90:00` 那样难读 */ export function hjcFormatCountdown(ms: number): string { const total = Math.max(0, Math.floor(ms / 1000)) const h = Math.floor(total / 3600) const m = Math.floor((total % 3600) / 60) const s = total % 60 const pad = (n: number) => String(n).padStart(2, '0') return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}` } /** 状态徽标的 Tailwind 配色 */ export function hjcOrderStatusClass(o?: HjcOrderLike | null): string { switch (hjcOrderStatusKey(o)) { case 'done': return 'bg-green-100 text-green-700' case 'cancel': case 'refund': return 'bg-gray-100 text-gray-500' default: return 'bg-amber-100 text-amber-700' } } /** * 这张订单是否该出现「去支付」「取消订单」。 * * **不要用 `hjcOrderStatusKey(o) === 'pending'` 当条件**:`payStatus=2`(支付失败)也会落进 * `pending` 这个兜底键。虽然它目前是死状态(全仓 `setPayStatus` 只有 0/1/3 三个写入点), * 但把"能不能操作"绑在一个展示口径的兜底分支上,是下次改动最容易踩的坑。 */ export function hjcCanActOnOrder(o?: HjcOrderLike | null): boolean { if (!o) return false return o.payStatus === 0 && o.orderStatus !== 2 }