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:
@@ -20,7 +20,7 @@ import {
|
||||
createWorkOrderEvent,
|
||||
createWorkOrderEventWithClient,
|
||||
getWorkOrderById,
|
||||
startWorkOrderGiftCountdownWithClient,
|
||||
parkWorkOrderWaitingFriendAddWithClient,
|
||||
updateWorkOrder,
|
||||
type WorkOrderRow,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
@@ -198,7 +198,8 @@ export async function fulfillWorkOrderWithConsignment(input: {
|
||||
|
||||
const now = nowIso()
|
||||
|
||||
// 开启原子事务:占库 + 工单进入赠送冷静期。押金留到验收再释放。
|
||||
// 开启原子事务:占库 + 工单停在「待加好友」。押金留到验收再释放,
|
||||
// 客服线下通知加好友并手动确认后(confirm-friend)才进入赠送冷却。
|
||||
const { updatedOrder, updatedInventory } = await withTransaction(async (client: PoolClient) => {
|
||||
const lockedOrderRes = await client.query<WorkOrderRow>(
|
||||
'SELECT * FROM work_orders WHERE id = $1 AND status = $2 FOR UPDATE',
|
||||
@@ -265,12 +266,11 @@ export async function fulfillWorkOrderWithConsignment(input: {
|
||||
},
|
||||
}
|
||||
|
||||
const wo = await startWorkOrderGiftCountdownWithClient(client, lockedOrder, {
|
||||
const wo = await parkWorkOrderWaitingFriendAddWithClient(client, lockedOrder, {
|
||||
now,
|
||||
actorName: input.actorName,
|
||||
eventType: 'consignment_fulfilled',
|
||||
materialJson: updatedMaterial,
|
||||
forceFriendGift: true,
|
||||
extraPayload: {
|
||||
inventoryId: inv.id,
|
||||
inventoryNo: inv.inventory_no,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
resolveOpenSharingSyncReward,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import { resolveProblemReturnToWorkerStatus } from '../../repositories/worker-platform/work-order-issue-repo.js'
|
||||
import type { WorkOrderRow } from '../../repositories/worker-platform/types.js'
|
||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||
import {
|
||||
assertWithdrawChannelAllowedForAmount,
|
||||
@@ -69,6 +70,7 @@ import {
|
||||
} from '../../repositories/worker-platform/consignment-repo.js'
|
||||
import { ADMIN_PERMISSION_KEYS } from '../admin/admin-rbac-service.js'
|
||||
import { updateAdminConsignmentInventory } from './consignment-admin-service.js'
|
||||
import { parkWorkOrderWaitingFriendAddWithClient } from '../../repositories/worker-platform/work-order-gift-repo.js'
|
||||
import {
|
||||
assertConsignmentRevocableWorkOrderStatus,
|
||||
resolveRevocableConsignmentInventoryId,
|
||||
@@ -2048,3 +2050,86 @@ test('编辑接口仍然拒绝直接改物品状态(回归)', async () => {
|
||||
(error as { errorCode?: string }).errorCode === 'consignment_status_update_forbidden',
|
||||
)
|
||||
})
|
||||
|
||||
test('内部现货履约停在「待加好友」:不写 friend_added_at,也不进冷却', async () => {
|
||||
const calls: Array<{ sql: string; values?: unknown[] }> = []
|
||||
const client = {
|
||||
query: async (sql: string, values?: unknown[]) => {
|
||||
calls.push({ sql, values })
|
||||
return { rows: [] }
|
||||
},
|
||||
} as unknown as PoolClient
|
||||
|
||||
await parkWorkOrderWaitingFriendAddWithClient(
|
||||
client,
|
||||
{
|
||||
id: 100154,
|
||||
status: 'pending_material',
|
||||
acceptance_mode: 'standard',
|
||||
gift_cooldown_hours: 0,
|
||||
gift_cooldown_minutes: 0,
|
||||
assigned_worker_id: null,
|
||||
material_json: { consignment: { inventoryId: 1, fulfillmentType: 'internal_stock' } },
|
||||
} as unknown as WorkOrderRow,
|
||||
{ now: '2026-09-16T12:00:00.000Z', actorName: '客服A' },
|
||||
)
|
||||
|
||||
const update = calls.find((call) => call.sql.includes('UPDATE work_orders'))
|
||||
assert.ok(update, '停阶段必须更新工单')
|
||||
const sql = update.sql
|
||||
assert.ok(sql.includes("gift_phase = 'material_required'"), '停留阶段应为待加好友阶段')
|
||||
assert.ok(sql.includes('friend_added_at = NULL'), '不能自动确认加好友')
|
||||
assert.ok(sql.includes('gift_available_at = NULL'))
|
||||
assert.ok(sql.includes('deadline_at = NULL'), '没有打手在跑,不应背任务时限')
|
||||
assert.ok(sql.includes("acceptance_mode = 'friend_gift'"))
|
||||
assert.ok(!sql.includes('friend_added_at = $2'), '不能把当前时间写成已加好友')
|
||||
|
||||
// 冷却时长两者都为 0 时补默认 72 小时,否则客服确认后会得到 0 秒冷却
|
||||
assert.equal(update.values?.[2], 72)
|
||||
|
||||
const event = calls.find((call) => call.sql.includes('INSERT INTO work_order_events'))
|
||||
assert.ok(
|
||||
String(event?.values?.join(' ')).includes('"giftPhase":"material_required"'),
|
||||
'事件要记录停留在待加好友阶段',
|
||||
)
|
||||
})
|
||||
|
||||
test('停在待加好友时保留商品规则已配的冷却时长', async () => {
|
||||
const calls: Array<{ sql: string; values?: unknown[] }> = []
|
||||
const client = {
|
||||
query: async (sql: string, values?: unknown[]) => {
|
||||
calls.push({ sql, values })
|
||||
return { rows: [] }
|
||||
},
|
||||
} as unknown as PoolClient
|
||||
|
||||
await parkWorkOrderWaitingFriendAddWithClient(
|
||||
client,
|
||||
{
|
||||
id: 100155,
|
||||
status: 'pending_material',
|
||||
acceptance_mode: 'friend_gift',
|
||||
gift_cooldown_hours: 48,
|
||||
gift_cooldown_minutes: 0,
|
||||
material_json: {},
|
||||
} as unknown as WorkOrderRow,
|
||||
{ now: '2026-09-16T12:00:00.000Z' },
|
||||
)
|
||||
|
||||
const update = calls.find((call) => call.sql.includes('UPDATE work_orders'))
|
||||
assert.equal(update?.values?.[2], 48, '规则里配了 48 小时就不能被默认值覆盖')
|
||||
})
|
||||
|
||||
test('内部现货履约不再自动跳过加好友直接进冷却', async () => {
|
||||
const readFile = await import('node:fs/promises')
|
||||
const src = await readFile.readFile(
|
||||
new URL('./consignment-fulfillment-service.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
src.includes('parkWorkOrderWaitingFriendAddWithClient('),
|
||||
'履约成功后必须停在待加好友,等客服确认',
|
||||
)
|
||||
assert.ok(!src.includes('forceFriendGift'), '不能再有"自动确认加好友"的强制进冷却开关')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user