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:
@@ -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',
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
| `632dddc4` | 上一轮:H5 渠道整体下线 + 凭据字段修复 + 状态机封闭(撤销守卫 / 库存绑定唯一性 / 生命周期解绑)+ 押金归属取台账 | 迁移 `079`、`consignment-*` 五个文件、`work-order-*` 三个仓库、两个前端面板 |
|
||||
| `5d11bf20` | 批次 C+D:指派部分成功补偿、凭证核销如实反馈、空规格不放行 + 履约/指派服务端规格复检 | `consignment-fulfillment-service.ts`、`consignment-matching-service.ts`、`consignment-repo.ts`、履约路由、`WorkOrdersPanel.tsx` |
|
||||
| `d6f6219f` | 批次 F1:内部现货履约停在「待加好友」,客服确认后才进赠送冷却 + 备注快捷项 | `work-order-gift-repo.ts`、履约服务、`WorkOrdersPanel.tsx`、`utils/gift-phase.ts` |
|
||||
| 本批提交(批次 P,同一提交内含本文档,故不写自身 hash) | 批次 P:确认加好友/上传赠送截图恢复为卖家操作,客服兜底 | `consignment-item-stage.ts`、`consignment-worker-service.ts`、`worker-acceptance-service.ts` |
|
||||
| 本批提交(批次 O,同一提交内含本文档,故不写自身 hash) | 批次 O:文件链接去掉签名与有效期(修客服端凭证裂图),寄卖出参统一归一化 | `file-storage-service.ts`、`routes/files.ts`、`consignment-admin-service.ts` |
|
||||
| 本批提交(批次 N,同一提交内含本文档,故不写自身 hash) | 批次 N:删掉按库存号的卖家入口,卖家动作只按寄卖单号定位 | `consignment-worker-service.ts`、`routes/worker/consignment.ts`、`WorkerConsignmentPage.tsx` |
|
||||
| 本批提交(批次 M,同一提交内含本文档,故不写自身 hash) | 批次 M:停用客服手动入库(前端入口删除 + 后端 409 守卫),寄卖货源只走平台提报 | `consignment-admin-service.ts`、`consignment-repo.ts`、`ConsignmentInventoryPanel.tsx` |
|
||||
@@ -285,6 +286,43 @@
|
||||
前端在"部分匹配"候选上先 `modal.confirm` 危险确认,再带 `allowPartial: true` 重试,高配充低配的合法玩法保留但不可误触。
|
||||
3. ⏳ 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)未做:当前仍是"取全量在库 → 内存比对"。
|
||||
|
||||
### 批次 P · 「确认加好友 / 上传赠送截图」恢复为卖家操作(本轮)
|
||||
|
||||
**问题(用户截图:卖家端的货点不了确认加好友)**:卖家「我的寄卖」里那件"已匹配-待加好友"的货,
|
||||
提示写着「等待客服确认加好友」,**没有按钮**。
|
||||
|
||||
**原因**:批次 I 把这两个动作的能力位绑在"订单接单人 = 当前卖家"上(`selfFulfilling`),
|
||||
而内部现货履约的订单**根本没有接单打手**(`assigned_worker_id` 为空),于是:
|
||||
状态机把按钮关掉 + 提示写成"等客服";后端归属校验也会 403(`consignment_fulfillment_not_owned`)。
|
||||
|
||||
**业务定调**(用户):**卖家可以确认加好友,冷却到期后也能自己上传赠送成功截图**,客服只是兜底。
|
||||
|
||||
**改法**:
|
||||
|
||||
1. ✅ 状态机能力位改成按阶段给(不再看接单人):
|
||||
- `material_submitted`(赠送资料已交)→ 卖家可点「确认加好友」;
|
||||
- 内部现货履约没有"先交赠送资料"这一步 → `material_required` 阶段卖家也可直接确认(与客服侧能力一致);
|
||||
- 冷却结束(`gift_ready`)→ 卖家可「提交赠送截图」。
|
||||
- 提示统一改成卖家视角:「请先加上买家好友,加好后点『确认加好友』:确认即进入赠送冷却期,
|
||||
未完成赠送押金不退(客服也可代为确认)」。
|
||||
2. ✅ 后端归属只认「这张提报单就是当前卖家的」(别人的单仍 404 `consignment_item_not_found`),
|
||||
删掉"订单接单人必须是卖家"的 403 —— 内部现货单没有接单人,按接单人卡会把卖家自己的货挡在外面。
|
||||
3. ✅ 卖家提交赠送凭证走 `submitWorkerOrderAcceptance(..., { asConsignmentSupplier: true })`:
|
||||
跳过"必须是接单打手"的校验(归属已由寄卖单保证),**冷却是否结束、必须带图**等校验全部保留。
|
||||
4. ✅ 客服侧按钮不变,谁先点谁生效,后点的会拿到明确的 409 提示。
|
||||
|
||||
**真实库端到端验证**(造一件内部现货履约的货,订单无接单打手):
|
||||
|
||||
| 步骤 | 结果 |
|
||||
| --- | --- |
|
||||
| 卖家列表 | 阶段「已匹配-待加好友」,`确认加好友按钮=true`,提示为卖家操作口径 |
|
||||
| 卖家点确认加好友 | `gift_phase=friend_countdown`,冷却到 +72h |
|
||||
| 冷却中传截图 | 被拒 `work_order_gift_cooldown_active` |
|
||||
| 冷却到期 | 阶段「待赠送」,`上传截图按钮=true` |
|
||||
| 卖家上传赠送截图 | 成功 → 工单 `pending_acceptance`(交客服验收) |
|
||||
|
||||
临时数据已清理(残留 0)。
|
||||
|
||||
### 批次 O · 去掉文件链接的签名/有效期(本轮)
|
||||
|
||||
**问题(用户截图:客服看到的凭证是裂图)**:寄卖凭证的图片地址是**带签名的临时链接**:
|
||||
|
||||
Reference in New Issue
Block a user