问题:一件货身上挂着三套互不相通的状态(提报单审核态、仓库库存态、工单赠送态),
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);按寄卖单号搜索命中正确。临时数据已清理。
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
/**
|
||
* 寄卖「加好友赠送冷却」倒计时文案。
|
||
*
|
||
* 后端只给一个冷却截止时间(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} 小时`
|
||
}
|