feat(consignment): 内部现货履约改为停在「待加好友」,由客服确认后进赠送冷却
需求:发布匹配成功后订单要停在待加好友,客服线下通知加好友、手动确认后才进赠送冷却。 - 新增 parkWorkOrderWaitingFriendAddWithClient:占库落地时把工单停在 acceptance_mode=friend_gift + gift_phase=material_required 的「待加好友」阶段, friend_added_at / gift_available_at / deadline_at 都留空(没有打手在跑,不该背时限), 并把商品规则里的冷却时长落到工单上(两者都为 0 时补默认 72 小时, 否则客服确认后会得到 0 秒冷却而立刻可赠送)。 - 履约服务不再传 forceFriendGift(该开关会直接把 friend_added_at 写成当前时间、 跳过客服确认);同步删除这个已无调用方的开关,冷却起始事件里的 skippedWorkerMaterial 改为按是否内部现货履约判断。 - 前端:内部现货单在该阶段显示「待加好友」(原为「待打手提交资料」), 确认按钮统一为「确认加好友」,确认弹窗补上"先用库存账号加好友、通知进展记备注"的引导。 - 后台备注增加快捷项「已通知加好友」,一键追加「[已通知加好友] 操作人 时间」。 - 阶段文案抽到 utils/gift-phase.ts 以便单测覆盖这条口径。 验证:真实库跑通全链路(停阶段 → 确认加好友进冷却 → 重复确认被拒 → 事件流正确,临时单已清理); 后端 415 例(412 通过,新增 3 例)、前端 25 例(新增 4 例)全绿。
This commit is contained in:
@@ -83,8 +83,15 @@ export function hasInternalConsignmentFulfillment(workOrder: {
|
||||
return true
|
||||
}
|
||||
|
||||
/** 开始好友赠送冷却:内部寄售跳过打手资料,直接进入等 N 天。 */
|
||||
export async function startWorkOrderGiftCountdownWithClient(
|
||||
/**
|
||||
* 内部寄售现货履约落地:占库成功,但**不**算已加好友。
|
||||
*
|
||||
* 把工单停在「待加好友」阶段,由客服线下通知买家/加好友后**手动**点确认,
|
||||
* 才进入等 N 天赠送冷却(见 confirmWorkOrderFriendAdded)。落库复用
|
||||
* acceptance_mode=friend_gift 的初始阶段 material_required:内部现货单本来就没有打手资料,
|
||||
* 该阶段在这里只表示"待加好友",前端对内部现货单展示为「待加好友」。
|
||||
*/
|
||||
export async function parkWorkOrderWaitingFriendAddWithClient(
|
||||
client: PoolClient,
|
||||
workOrder: WorkOrderRow,
|
||||
input: {
|
||||
@@ -93,14 +100,15 @@ export async function startWorkOrderGiftCountdownWithClient(
|
||||
eventType?: string | undefined
|
||||
materialJson?: unknown
|
||||
extraPayload?: Record<string, unknown>
|
||||
forceFriendGift?: boolean
|
||||
},
|
||||
): Promise<WorkOrderRow | null> {
|
||||
if (GIFT_COUNTDOWN_BLOCKED_STATUSES.has(workOrder.status)) return null
|
||||
|
||||
// 冷却时长按商品规则落到工单上:两者都为 0 时补默认 72 小时,
|
||||
// 否则客服确认后会得到 0 秒冷却(gift_available_at 立刻到点)。
|
||||
let cooldownHours = normalizeWorkOrderGiftCooldownHours(workOrder.gift_cooldown_hours)
|
||||
const cooldownMinutes = normalizeWorkOrderGiftCooldownMinutes(workOrder.gift_cooldown_minutes)
|
||||
if (input.forceFriendGift && cooldownHours === 0 && cooldownMinutes === 0) {
|
||||
if (cooldownHours === 0 && cooldownMinutes === 0) {
|
||||
cooldownHours = DEFAULT_GIFT_COOLDOWN_HOURS
|
||||
}
|
||||
const nextStatus = ['problem'].includes(workOrder.status) ? workOrder.status : 'in_progress'
|
||||
@@ -118,8 +126,72 @@ export async function startWorkOrderGiftCountdownWithClient(
|
||||
UPDATE work_orders
|
||||
SET
|
||||
status = $1,
|
||||
acceptance_mode = CASE WHEN $7 THEN 'friend_gift' ELSE acceptance_mode END,
|
||||
gift_cooldown_hours = CASE WHEN $7 THEN $3 ELSE gift_cooldown_hours END,
|
||||
acceptance_mode = 'friend_gift',
|
||||
gift_cooldown_hours = $3,
|
||||
gift_phase = 'material_required',
|
||||
friend_added_at = NULL,
|
||||
gift_available_at = NULL,
|
||||
deadline_at = NULL,
|
||||
accepted_at = CASE WHEN $4 THEN NULL ELSE accepted_at END,
|
||||
material_json = COALESCE($5::jsonb, material_json),
|
||||
updated_at = $2::timestamptz
|
||||
WHERE id = $6
|
||||
`,
|
||||
[nextStatus, input.now, cooldownHours, clearAcceptedAt, materialJson, workOrder.id],
|
||||
)
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: Number(workOrder.id),
|
||||
actorType: 'admin',
|
||||
actorId: input.actorName || '',
|
||||
eventType: input.eventType || 'consignment_waiting_friend_add',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: nextStatus,
|
||||
payloadJson: JSON.stringify({
|
||||
cooldownHours,
|
||||
cooldownMinutes,
|
||||
giftPhase: WORK_ORDER_GIFT_PHASE.MATERIAL_REQUIRED,
|
||||
internalConsignment: hasInternalConsignmentFulfillment({
|
||||
...workOrder,
|
||||
material_json: materialForInternalCheck,
|
||||
}),
|
||||
...(input.extraPayload || {}),
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
return getWorkOrderByIdWithClient(client, workOrder.id)
|
||||
}
|
||||
|
||||
/** 开始好友赠送冷却:客服确认加好友后进入等 N 天。 */
|
||||
export async function startWorkOrderGiftCountdownWithClient(
|
||||
client: PoolClient,
|
||||
workOrder: WorkOrderRow,
|
||||
input: {
|
||||
now: string
|
||||
actorName?: string | undefined
|
||||
eventType?: string | undefined
|
||||
materialJson?: unknown
|
||||
extraPayload?: Record<string, unknown>
|
||||
},
|
||||
): Promise<WorkOrderRow | null> {
|
||||
if (GIFT_COUNTDOWN_BLOCKED_STATUSES.has(workOrder.status)) return null
|
||||
|
||||
const cooldownHours = normalizeWorkOrderGiftCooldownHours(workOrder.gift_cooldown_hours)
|
||||
const cooldownMinutes = normalizeWorkOrderGiftCooldownMinutes(workOrder.gift_cooldown_minutes)
|
||||
const nextStatus = ['problem'].includes(workOrder.status) ? workOrder.status : 'in_progress'
|
||||
const clearAcceptedAt = ['accepted', 'completed'].includes(workOrder.status)
|
||||
const materialJson =
|
||||
input.materialJson === undefined
|
||||
? null
|
||||
: typeof input.materialJson === 'string'
|
||||
? input.materialJson
|
||||
: JSON.stringify(input.materialJson)
|
||||
const materialForInternalCheck =
|
||||
input.materialJson === undefined ? workOrder.material_json : input.materialJson
|
||||
await client.query(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET
|
||||
status = $1,
|
||||
gift_phase = 'friend_countdown',
|
||||
friend_added_at = $2::timestamptz,
|
||||
gift_available_at = $2::timestamptz + ($3::int * INTERVAL '1 hour') + ($4::int * INTERVAL '1 minute'),
|
||||
@@ -129,7 +201,7 @@ export async function startWorkOrderGiftCountdownWithClient(
|
||||
ELSE NULL
|
||||
END,
|
||||
accepted_at = CASE WHEN $5 THEN NULL ELSE accepted_at END,
|
||||
material_json = COALESCE($8::jsonb, material_json),
|
||||
material_json = COALESCE($7::jsonb, material_json),
|
||||
updated_at = $2::timestamptz
|
||||
WHERE id = $6
|
||||
`,
|
||||
@@ -140,7 +212,6 @@ export async function startWorkOrderGiftCountdownWithClient(
|
||||
cooldownMinutes,
|
||||
clearAcceptedAt,
|
||||
workOrder.id,
|
||||
Boolean(input.forceFriendGift),
|
||||
materialJson,
|
||||
],
|
||||
)
|
||||
@@ -159,7 +230,10 @@ export async function startWorkOrderGiftCountdownWithClient(
|
||||
...workOrder,
|
||||
material_json: materialForInternalCheck,
|
||||
}),
|
||||
skippedWorkerMaterial: Boolean(input.forceFriendGift),
|
||||
skippedWorkerMaterial: hasInternalConsignmentFulfillment({
|
||||
...workOrder,
|
||||
material_json: materialForInternalCheck,
|
||||
}),
|
||||
giftPhase: WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN,
|
||||
...(input.extraPayload || {}),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user