fix(consignment): 确认加好友与上传赠送截图恢复为卖家操作,客服兜底

问题:卖家「我的寄卖」里"已匹配-待加好友"的货点不了确认加好友,提示还写着"等待客服确认加好友"。

原因:批次 I 把能力位绑在"订单接单人 = 当前卖家"(selfFulfilling)上,而内部现货履约的订单
根本没有接单打手(assigned_worker_id 为空),于是状态机关掉按钮、后端归属校验还会 403。

业务定调:卖家可以确认加好友,冷却到期后也能自己上传赠送成功截图,客服只是兜底。

- 能力位改成按阶段给:material_submitted 可确认;内部现货履约在 material_required 也可确认
  (与客服侧一致);冷却结束可提交赠送截图;提示改成卖家视角
- 后端归属只认"这张提报单就是当前卖家的"(别人的单仍 404),删除"订单接单人必须是卖家"的 403
- 卖家提交凭证走 asConsignmentSupplier 选项:跳过接单人校验(归属由寄卖单保证),
  冷却是否结束、必须带图等校验全部保留
- 客服侧按钮不变,谁先点谁生效,后点者拿到明确 409

真实库端到端验证:卖家列表按钮=true → 确认加好友 gift_phase=friend_countdown(+72h) →
冷却中被拒 work_order_gift_cooldown_active → 冷却到期后卖家上传赠送截图成功(工单转 pending_acceptance)。
This commit is contained in:
yml2213
2026-09-17 11:57:33 +08:00
parent 0fd09976a5
commit 40882aeebb
6 changed files with 116 additions and 39 deletions
@@ -43,7 +43,7 @@ test('审核通过到在库:已通过(在库待匹配),没人需要操
assert.equal(result.canSubmitEvidence, false)
})
test('内部现货履约:卖家只等客服确认加好友,不给卖家按钮', () => {
test('内部现货履约(订单没有接单打手):卖家照样能点确认加好友,和客服能力一致', () => {
const result = deriveConsignmentItemStage({
submissionStatus: 'approved',
inventoryStatus: 'consumed',
@@ -51,11 +51,13 @@ test('内部现货履约:卖家只等客服确认加好友,不给卖家按
viewer: SELLER,
})
assert.equal(result.stage, CONSIGNMENT_ITEM_STAGE.MATCHED_WAITING_FRIEND)
assert.equal(result.canConfirmFriend, false)
assert.ok(result.hint.includes('客服'))
// 货是卖家的,好友由卖家加、赠送也由卖家做;客服只是兜底,不能把卖家自己的按钮关掉
assert.equal(result.canConfirmFriend, true)
assert.ok(result.hint.includes('押金不退'), '确认加好友必须提示押金不退')
assert.ok(result.hint.includes('客服'), '要说明客服也能代为确认')
})
test('指派打手线:本人履约时卖家可以点确认加好友,别人不行', () => {
test('赠送资料已提交:卖家可以点确认加好友(接单人是谁都不影响)', () => {
const mine = deriveConsignmentItemStage({
submissionStatus: 'approved',
inventoryStatus: 'consumed',
@@ -66,13 +68,43 @@ test('指派打手线:本人履约时卖家可以点确认加好友,别人
assert.equal(mine.canConfirmFriend, true)
assert.ok(mine.hint.includes('押金不退'), '确认加好友必须提示押金不退')
const others = deriveConsignmentItemStage({
const assignedToOther = deriveConsignmentItemStage({
submissionStatus: 'approved',
inventoryStatus: 'consumed',
workOrder: friendGiftOrder({ gift_phase: 'material_submitted', assignedWorkerId: 99 }),
viewer: SELLER,
})
assert.equal(others.canConfirmFriend, false)
assert.equal(assignedToOther.canConfirmFriend, true)
})
test('赠送资料还没提交:打手线要先交资料,卖家暂时看不到确认按钮', () => {
const result = deriveConsignmentItemStage({
submissionStatus: 'approved',
inventoryStatus: 'consumed',
workOrder: friendGiftOrder({ gift_phase: 'material_required' }),
viewer: SELLER,
})
assert.equal(result.canConfirmFriend, false)
assert.ok(result.hint.includes('赠送资料'))
})
test('冷却结束:卖家可以上传赠送成功截图给客服审核', () => {
const result = deriveConsignmentItemStage({
submissionStatus: 'approved',
inventoryStatus: 'consumed',
workOrder: friendGiftOrder({
gift_phase: 'friend_countdown',
friend_added_at: '2026-09-17T00:00:00.000Z',
gift_available_at: '2026-09-19T00:00:00.000Z',
internalStock: true,
assignedWorkerId: null,
}),
viewer: SELLER,
now: Date.parse('2026-09-20T00:00:00.000Z'),
})
assert.equal(result.stage, CONSIGNMENT_ITEM_STAGE.GIFT_READY)
assert.equal(result.canSubmitEvidence, true)
assert.equal(result.evidenceButtonLabel, '提交赠送截图')
})
test('冷却中:给倒计时阶段,不给提交按钮;冷却到点后变成待赠送', () => {
@@ -202,28 +202,25 @@ export function deriveConsignmentItemStage(
phase === WORK_ORDER_GIFT_PHASE.MATERIAL_SUBMITTED ||
phase === WORK_ORDER_GIFT_PHASE.MATERIAL_REQUIRED
) {
// 「确认加好友」是卖家的动作:货是卖家的,好友由卖家加、赠送也由卖家做,客服只兜底。
// 内部现货履约时(内部仓账号由客服代操作)没有"先提交赠送资料"这一步,卖家可以直接确认,
// 和客服侧的按钮能力保持一致。
const internalStock = Boolean(workOrder.internalStock)
if (internalStock) {
// 内部仓账号由客服操作,卖家只等
return build(
CONSIGNMENT_ITEM_STAGE.MATCHED_WAITING_FRIEND,
'等待客服确认加好友,确认后进入赠送冷却期',
)
}
if (phase === WORK_ORDER_GIFT_PHASE.MATERIAL_REQUIRED) {
const canConfirmNow = phase === WORK_ORDER_GIFT_PHASE.MATERIAL_SUBMITTED || internalStock
if (!canConfirmNow) {
return build(
CONSIGNMENT_ITEM_STAGE.MATCHED_WAITING_FRIEND,
selfFulfilling
? '请先在订单页提交赠送资料,再确认加好友'
: '等待打手提交赠送资料后确认加好友',
: '等待履约打手提交赠送资料后确认加好友',
)
}
return build(
CONSIGNMENT_ITEM_STAGE.MATCHED_WAITING_FRIEND,
selfFulfilling
? '好友加好后点「确认加好友」:确认即进入赠送冷却期,未完成赠送押金不退'
: '等待接单打手确认加好友,确认后进入赠送冷却期',
{ canConfirmFriend: selfFulfilling },
phase === WORK_ORDER_GIFT_PHASE.MATERIAL_SUBMITTED && !internalStock
? '好友加好后点「确认加好友」:确认即进入赠送冷却期,未完成赠送押金不退(客服也可代为确认)'
: '请先加上买家好友,加好后点「确认加好友」:确认即进入赠送冷却期,未完成赠送押金不退(客服也可代为确认)',
{ canConfirmFriend: true },
)
}
if (phase === WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN) {
@@ -232,10 +229,8 @@ export function deriveConsignmentItemStage(
if (phase === WORK_ORDER_GIFT_PHASE.GIFT_READY) {
return build(
CONSIGNMENT_ITEM_STAGE.GIFT_READY,
selfFulfilling
? '冷却已结束,请用账号赠送并上传赠送截图给客服审核(未完成赠送押金不退)'
: '冷却已结束,等待履约方赠送并上传截图',
{ canSubmitEvidence: selfFulfilling, evidenceButtonLabel: '提交赠送截图' },
'冷却已结束,请用账号赠送并上传赠送截图给客服审核(未完成赠送押金不退)',
{ canSubmitEvidence: true, evidenceButtonLabel: '提交赠送截图' },
)
}
return build(CONSIGNMENT_ITEM_STAGE.MATCHED_WAITING_FRIEND, '等待确认加好友')
@@ -23,17 +23,21 @@ test('列表阶段由统一状态机推导,工单信息批量取而不是逐
assert.ok(serviceSource.includes('hasInternalConsignmentFulfillment(workOrder)'))
})
test('卖家确认加好友复用既有链路,并且只能动自己的订单', async () => {
test('卖家确认加好友复用既有链路,归属只认"这张提报单是这个卖家的"', async () => {
const serviceSource = await readServiceSource()
assert.ok(serviceSource.includes('confirmAdminWorkOrderFriendAdded('))
// 归属校验:不是本人履约的直接 403,避免卖家操作别人的单
assert.ok(serviceSource.includes("errorCode: 'consignment_fulfillment_not_owned'"))
assert.ok(serviceSource.includes('workOrder.assigned_worker_id'))
// 归属:别人的提报单必须 404,不能拿别人单号来操作
assert.ok(serviceSource.includes("errorCode: 'consignment_item_not_found'"))
// 但不能再按"订单接单人"卡:内部现货履约的单子没有接单打手,按接单人卡会把卖家自己的货挡在外面
// (assigned_worker_id 仍会读,但只用于推导阶段文案,不再作为归属校验)
assert.ok(!serviceSource.includes("errorCode: 'consignment_fulfillment_not_owned'"))
assert.ok(!serviceSource.includes('该订单不是由你履约'))
})
test('赠送截图复用打手验收提交链路(冷却校验、必须带图都在那边)', async () => {
const serviceSource = await readServiceSource()
assert.ok(serviceSource.includes('submitWorkerOrderAcceptance('))
assert.ok(serviceSource.includes('asConsignmentSupplier: true'))
assert.ok(serviceSource.includes("errorCode: 'consignment_item_not_found'"))
})
@@ -368,13 +368,9 @@ async function resolveSellerConsignmentWorkOrder(
errorCode: 'work_order_not_found',
})
}
if (Number(workOrder.assigned_worker_id || 0) !== Number(workerId)) {
throw createHttpError('该订单不是由你履约,无法在此操作', {
statusCode: 403,
errorCode: 'consignment_fulfillment_not_owned',
})
}
// 归属只认"这张提报单就是这个卖家的"(上面已校验),不再要求订单接单人等于卖家:
// 寄卖的货是卖家自己的,赠送由卖家完成;内部现货履约的单子根本没有接单打手,
// 按接单人卡会把卖家自己的货挡在外面。客服侧保留同样的按钮,谁先点谁生效。
return { workOrderId, inventoryId: Number(inventory.id) }
}
@@ -406,5 +402,5 @@ export async function submitWorkerConsignmentGiftEvidence(
session: WorkerSession,
) {
const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, submissionId)
return submitWorkerOrderAcceptance(workOrderId, payload, session)
return submitWorkerOrderAcceptance(workOrderId, payload, session, { asConsignmentSupplier: true })
}
@@ -57,14 +57,26 @@ export async function submitWorkerOrderAcceptance(
workOrderId: number | string,
payload: JsonObject = {},
session: WorkerSession,
options: {
/**
* 寄卖的**供货卖家**提交赠送凭证时用。
*
* 寄卖的货是卖家自己的,赠送由卖家完成,所以不要求 `assigned_worker_id` 等于卖家
* (内部现货履约时订单根本没有接单打手)。归属由寄卖单本身保证:
* 调用方必须先校验"这张提报单就是这个卖家的",这里就不再按接单人来卡。
*/
asConsignmentSupplier?: boolean
} = {},
) {
requireActiveWorkerSession(session)
const workOrder = await getRequiredWorkOrder(workOrderId)
const workerId = Number(session.workerId)
const worker = await getRequiredWorker(session.workerId)
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
if (sharingShare) {
return submitWorkerSharingAcceptance(workOrder, sharingShare, payload, session)
if (!options.asConsignmentSupplier) {
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
if (sharingShare) {
return submitWorkerSharingAcceptance(workOrder, sharingShare, payload, session)
}
}
const canAutoAcceptWithoutEvidence = isFriendGiftAcceptanceMode(workOrder.acceptance_mode)
? false
@@ -93,7 +105,7 @@ export async function submitWorkerOrderAcceptance(
errorCode: 'work_order_timeout_expired',
})
}
if (Number(workOrder.assigned_worker_id || 0) !== workerId) {
if (!options.asConsignmentSupplier && Number(workOrder.assigned_worker_id || 0) !== workerId) {
throw createHttpError('只能提交自己的订单', {
statusCode: 403,
errorCode: 'work_order_owner_required',