9587a27ede
- 到期时刻由后端下发(payDeadline),倒计时与"到点锁页面"都用它,前端不自己算时限 - 解析后端时间时把空格换成 T:后端格式是 `yyyy-MM-dd HH:mm:ss`, 而 iOS Safari / 微信内置浏览器解析不了不带 T 的写法(返回 NaN, 会让倒计时与锁页在最主要的那一端静默失效) - 修正收银台写死的「2 小时」文案:有效期由后端配置,页面不该复述一个具体数字 - 订单列表/详情:超时后不再给「去支付」,并显示「已取消(支付超时)」 - `hjcCanActOn` 收成共享导出,两个页面不再各写一份判定 - 与 hjc-h5 的 utils/orderStatus 保持同一套定义(两仓逐字对齐)
157 lines
6.8 KiB
TypeScript
157 lines
6.8 KiB
TypeScript
/**
|
||
* 订单状态展示口径(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<HjcOrderStatusKey, string> = {
|
||
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
|
||
}
|