Files
order_site/apps/frontend/src/utils/consignment-cooldown.ts
T
yml2213 44de11a3a7 feat(consignment): 一个寄卖单号锚定全流程 + 统一状态机 + 卖家侧确认加好友/提交赠送截图
问题:一件货身上挂着三套互不相通的状态(提报单审核态、仓库库存态、工单赠送态),
my-items 同时返回两个数组、卖家端分两个列表渲染,同一件货显示两行两个号;
且卖家端只映射了前两套状态,"已出库"之后流程就断了,卖家不知道该干什么。

- 提报单号 SUB… 作为唯一「寄卖单号」从发布锚定到结算(零迁移);
  listWorkerConsignments 改成一件货一条记录,库存/工单信息挂上去,
  客服手工入库的现货(无提报单)才回退用库存号当锚点
- 新增 domain/consignment-item-stage.ts:把三套底层状态推导成卖家看得懂的一个阶段
  (待审核/已通过待匹配/已匹配-待加好友/冷却中/待赠送/已赠送待审核/已完成/驳回/作废/取消)
  + 一句"下一步做什么" + 能力位(谁能确认加好友、谁该传截图),列表筛选与 KPI 也改由它驱动
- 卖家侧新增动作(以前只有 publish + my-items,卖家只能干看):
  POST /worker/consignment/items/:submissionId/{confirm-friend,gift-evidence}
  POST /worker/consignment/inventories/:inventoryId/{confirm-friend,gift-evidence}(手工入库现货)
  复用既有工单链路 confirmWorkOrderFriendAdded / submitWorkerOrderAcceptance,
  只加归属校验(不是本人履约的订单 403),顺带白拿"冷却没结束不许提交凭证""必须带图"
- 客服端用同一个号:库存列表/详情补 sourceSubmissionNo(LEFT JOIN 提报单),
  履约/指派写进工单的寄售信息也带上;关键字搜索支持按寄卖单号搜(计数 SQL 同步补 join)

真实库端到端验证:确认加好友 → gift_phase=friend_countdown / friend_added_at 落库 /
gift_available_at 正好 +72h;冷却期内提交截图被拒(work_order_gift_cooldown_active);
越权操作被拒(consignment_item_not_found);按寄卖单号搜索命中正确。临时数据已清理。
2026-09-17 10:57:57 +08:00

73 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 寄卖「加好友赠送冷却」倒计时文案。
*
* 后端只给一个冷却截止时间(giftAvailableAt),卖家端负责把它变成看得懂的剩余时间。
* 这里刻意写成纯函数:不依赖 React / antd,也不使用 `@/` 别名,方便被
* `node --experimental-strip-types --test` 直接跑单测。
*/
export type GiftCooldownRemaining = {
/** 展示文案;无有效截止时间时为空字符串(调用方不渲染倒计时) */
text: string
/** 冷却是否已经结束 */
finished: boolean
}
const MINUTE_MS = 60_000
const HOUR_MS = 3_600_000
/** 冷却已结束的统一文案 */
const FINISHED_TEXT = '冷却已结束,可赠送'
/**
* 计算冷却剩余时间文案。
*
* - 截止时间早于(或等于)now:`冷却已结束,可赠送`,`finished: true`
* - 剩余不足 1 小时:`冷却中,剩余 X 分`
* - 剩余不足 1 天:`冷却中,剩余 X 小时 Y 分`(整小时时省略「0 分」)
* - 剩余 1 天以上:`冷却中,剩余 X 天 Y 小时`(整小时时省略「0 小时」)
*
* 空值 / 非法时间返回 `{ text: '', finished: false }`:既不展示倒计时,也不误判为已结束。
*/
export function formatGiftCooldownRemaining(
giftAvailableAt: string | null | undefined,
now: number = Date.now(),
): GiftCooldownRemaining {
const raw = String(giftAvailableAt ?? '').trim()
if (!raw) {
return { text: '', finished: false }
}
const targetMs = Date.parse(raw)
if (!Number.isFinite(targetMs)) {
return { text: '', finished: false }
}
const nowMs = Number.isFinite(now) ? Number(now) : Date.now()
const remainingMs = targetMs - nowMs
if (remainingMs <= 0) {
return { text: FINISHED_TEXT, finished: true }
}
return { text: `冷却中,剩余 ${formatRemaining(remainingMs)}`, finished: false }
}
function formatRemaining(remainingMs: number) {
// 不足 1 小时只按分钟展示,向上取整避免出现「剩余 0 分」
if (remainingMs < HOUR_MS) {
return `${Math.max(1, Math.ceil(remainingMs / MINUTE_MS))} 分`
}
const totalHours = Math.ceil(remainingMs / HOUR_MS)
if (totalHours < 24) {
const totalMinutes = Math.ceil(remainingMs / MINUTE_MS)
const hours = Math.floor(totalMinutes / 60)
const minutes = totalMinutes % 60
return minutes === 0 ? `${hours} 小时` : `${hours} 小时 ${minutes} 分`
}
const days = Math.floor(totalHours / 24)
const hours = totalHours % 24
return hours === 0 ? `${days} 天` : `${days} 天 ${hours} 小时`
}