From 40882aeebb61e9fd7797027ca85a2eaed08356c8 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 17 Sep 2026 11:57:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(consignment):=20=E7=A1=AE=E8=AE=A4=E5=8A=A0?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E4=B8=8E=E4=B8=8A=E4=BC=A0=E8=B5=A0=E9=80=81?= =?UTF-8?q?=E6=88=AA=E5=9B=BE=E6=81=A2=E5=A4=8D=E4=B8=BA=E5=8D=96=E5=AE=B6?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=EF=BC=8C=E5=AE=A2=E6=9C=8D=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:卖家「我的寄卖」里"已匹配-待加好友"的货点不了确认加好友,提示还写着"等待客服确认加好友"。 原因:批次 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)。 --- .../src/domain/consignment-item-stage.test.ts | 44 ++++++++++++++++--- .../src/domain/consignment-item-stage.ts | 29 +++++------- .../consignment-worker-service.test.ts | 12 +++-- .../consignment-worker-service.ts | 12 ++--- .../worker-acceptance-service.ts | 20 +++++++-- docs/寄卖系统优化方案.md | 38 ++++++++++++++++ 6 files changed, 116 insertions(+), 39 deletions(-) diff --git a/apps/backend/src/domain/consignment-item-stage.test.ts b/apps/backend/src/domain/consignment-item-stage.test.ts index 0e9af166..24cc850b 100644 --- a/apps/backend/src/domain/consignment-item-stage.test.ts +++ b/apps/backend/src/domain/consignment-item-stage.test.ts @@ -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('冷却中:给倒计时阶段,不给提交按钮;冷却到点后变成待赠送', () => { diff --git a/apps/backend/src/domain/consignment-item-stage.ts b/apps/backend/src/domain/consignment-item-stage.ts index 926bca9a..13f859ff 100644 --- a/apps/backend/src/domain/consignment-item-stage.ts +++ b/apps/backend/src/domain/consignment-item-stage.ts @@ -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, '等待确认加好友') diff --git a/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts b/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts index 9e89cb63..e1bcc2d5 100644 --- a/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts +++ b/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts @@ -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'")) }) diff --git a/apps/backend/src/services/worker-platform/consignment-worker-service.ts b/apps/backend/src/services/worker-platform/consignment-worker-service.ts index acd28b23..a325c04e 100644 --- a/apps/backend/src/services/worker-platform/consignment-worker-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-worker-service.ts @@ -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 }) } diff --git a/apps/backend/src/services/worker-platform/worker-acceptance-service.ts b/apps/backend/src/services/worker-platform/worker-acceptance-service.ts index d3ba3636..644bdb1e 100644 --- a/apps/backend/src/services/worker-platform/worker-acceptance-service.ts +++ b/apps/backend/src/services/worker-platform/worker-acceptance-service.ts @@ -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', diff --git a/docs/寄卖系统优化方案.md b/docs/寄卖系统优化方案.md index 29dcb209..97b1cf32 100644 --- a/docs/寄卖系统优化方案.md +++ b/docs/寄卖系统优化方案.md @@ -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 · 去掉文件链接的签名/有效期(本轮) **问题(用户截图:客服看到的凭证是裂图)**:寄卖凭证的图片地址是**带签名的临时链接**: