fix(consignment): 补齐指派补偿与规格复检,封闭寄卖资金/货物不一致窗口

- 指派失败改为完整补偿:取消指派(解冻打手押金 + 工单退回大厅 + 解绑现货)→ 兜底释放占用 →
  摘除工单寄售标记。此前只把现货放回在库池,一旦"指派已提交、写 material 失败",工单会停在
  in_progress + 押金冻结却不带寄售记录,现货还回到可匹配池,同一件货会被第二张单占用。
- 电子凭证核销失败写 consignment_fulfill_voucher_consume_failed 事件,前端按 voucherConsume.ok
  弹警告并提示人工核销,不再无条件报成功(核销走快手外部接口,无法与数据库事务合并回滚)。
- 匹配规格改为 match/mismatch/unknown 三态,钻级兜底由 || 1 改 || 0:订单规格信息不全一律不进
  "精确匹配",降级 partial 并在原因里列出缺失字段,消除"任意现货都 100 分精确匹配"的错发根因。
- 履约与指派在占库前做服务端规格复检:冲突 409 consignment_spec_mismatch;非 exact 必须显式
  传 allowPartial(前端在部分匹配候选上先危险确认),高配充低配保留但不可误触。
- 人工解绑守卫由"状态黑名单"收窄为只拦交付中三态(与撤销白名单对齐),修复
  "工单未开始 + 现货仍绑定"没有任何人工出口的问题。
- 新增 detachConsignmentMaterialFromWorkOrderWithClient,解绑回库/人工退回复库/作废同步摘掉
  工单寄售标记,消除"已回库却仍显示寄卖履约"的陈旧痕迹。
- 回归测试新增 8 例(后端 410 例 / 407 通过),附迁移 079 上库前检查脚本。
This commit is contained in:
yml2213
2026-09-16 20:12:14 +08:00
parent 632dddc4c3
commit 5d11bf2012
10 changed files with 727 additions and 70 deletions
@@ -78,6 +78,7 @@ import {
checkWorkOrderConsignmentMatches,
fulfillWorkOrderWithConsignment,
revokeWorkOrderConsignmentFulfillment,
type WorkOrderVoucherConsumeResult,
} from '@/services/admin/consignment'
import type { WorkOrderInventoryMatchCheck } from '@/types/consignment'
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
@@ -728,15 +729,53 @@ export default function WorkOrdersPanel() {
confirmPublishWorkOrder(row)
}
async function handleFulfillWithConsignment(inventoryId: number) {
/**
* 电子凭证核销结果必须如实反馈:核销失败时后端已经改完状态,但凭证没消耗,
* 之前这里无条件弹"成功",运营会以为凭证已核销而漏掉人工处理。
*/
function notifyVoucherConsume(voucherConsume?: WorkOrderVoucherConsumeResult) {
if (!voucherConsume || voucherConsume.ok) {
if (voucherConsume?.alreadyConsumedCount) {
message.info(`电子凭证已提前核销 ${voucherConsume.alreadyConsumedCount} 张,本次未重复核销`)
}
return
}
message.warning({
content: `状态已变更,但电子凭证核销失败:${
voucherConsume.errorMessage || '未知错误'
}。请在快手后台人工核销后再发货`,
duration: 8,
})
}
/** 非精确匹配(订单规格信息不全 / 高配充低配)必须人工确认,后端也会二次拦截 */
function confirmPartialMatch(matchType: string | undefined, onOk: () => void) {
if (matchType !== 'exact') {
modal.confirm({
title: '该现货不是精确匹配,确认继续?',
content:
'订单规格信息不全或这件现货高于订单要求(高配充低配)。继续履约后无法自动回退,请确认商品与买家订单一致。',
okText: '确认履约',
okButtonProps: { danger: true },
cancelText: '再想想',
onOk,
})
return
}
onOk()
}
async function handleFulfillWithConsignment(inventoryId: number, allowPartial = false) {
if (!consignmentInterceptModal.order) return
setConsignmentInterceptModal((prev) => ({ ...prev, submitting: true }))
try {
await fulfillWorkOrderWithConsignment({
const res = await fulfillWorkOrderWithConsignment({
workOrderId: consignmentInterceptModal.order.workOrderId,
inventoryId,
allowPartial,
})
message.success('已占用内部库存,跳过打手资料,进入赠送冷静期')
notifyVoucherConsume(res?.data?.voucherConsume)
setConsignmentInterceptModal({ open: false, order: null, matchData: null, submitting: false })
await refreshAll()
} catch (err: any) {
@@ -745,16 +784,22 @@ export default function WorkOrdersPanel() {
}
}
async function handleAssignWithConsignmentWorker(inventoryId: number, workerId?: number | null) {
async function handleAssignWithConsignmentWorker(
inventoryId: number,
workerId?: number | null,
allowPartial = false,
) {
if (!consignmentInterceptModal.order) return
setConsignmentInterceptModal((prev) => ({ ...prev, submitting: true }))
try {
await assignWorkOrderWithConsignmentWorker({
const res = await assignWorkOrderWithConsignmentWorker({
workOrderId: consignmentInterceptModal.order.workOrderId,
inventoryId,
workerId,
allowPartial,
})
message.success('已通过接单平台指派打手!系统已自动冻结履约押金,等待打手交货核销')
notifyVoucherConsume(res?.data?.voucherConsume)
setConsignmentInterceptModal({ open: false, order: null, matchData: null, submitting: false })
await refreshAll()
} catch (err: any) {
@@ -2973,7 +3018,11 @@ export default function WorkOrdersPanel() {
type="primary"
style={{ backgroundColor: '#722ed1', borderColor: '#722ed1' }}
loading={consignmentInterceptModal.submitting}
onClick={() => handleAssignWithConsignmentWorker(inv.id, inv.workerId)}
onClick={() =>
confirmPartialMatch(m.matchType, () => {
void handleAssignWithConsignmentWorker(inv.id, inv.workerId, !isExact)
})
}
>
走接单平台指派打手(冻结押金交付)
</Button>
@@ -2981,7 +3030,11 @@ export default function WorkOrdersPanel() {
<Button
type={inv.workerId ? 'default' : 'primary'}
loading={consignmentInterceptModal.submitting}
onClick={() => handleFulfillWithConsignment(inv.id)}
onClick={() =>
confirmPartialMatch(m.matchType, () => {
void handleFulfillWithConsignment(inv.id, !isExact)
})
}
>
内部现货履约
</Button>
@@ -104,25 +104,42 @@ export function checkWorkOrderConsignmentMatches(workOrderId: number | string) {
)
}
/** 履约/指派返回的电子凭证核销结果:ok=false 时必须提示运营人工核销,不能报成功 */
export type WorkOrderVoucherConsumeResult = {
ok: boolean
voucherCount: number
consumedCount: number
alreadyConsumedCount: number
failedCount: number
voucherCodes: string[]
errorMessage: string
}
export function fulfillWorkOrderWithConsignment(payload: {
workOrderId: number | string
inventoryId: number | string
/** 订单规格信息不全或高配充低配时,必须显式确认才放行(后端会二次校验) */
allowPartial?: boolean
}) {
return apiPost<{ workOrder: unknown; inventory: ConsignmentInventory }>(
'/api/v1/admin/worker-platform/consignment/fulfill',
payload,
)
return apiPost<{
workOrder: unknown
inventory: ConsignmentInventory
voucherConsume?: WorkOrderVoucherConsumeResult
}>('/api/v1/admin/worker-platform/consignment/fulfill', payload)
}
export function assignWorkOrderWithConsignmentWorker(payload: {
workOrderId: number | string
inventoryId: number | string
workerId?: number | null
/** 订单规格信息不全或高配充低配时,必须显式确认才放行(后端会二次校验) */
allowPartial?: boolean
}) {
return apiPost<{ workOrder: unknown; inventory: ConsignmentInventory }>(
'/api/v1/admin/worker-platform/consignment/assign-worker',
payload,
)
return apiPost<{
workOrder: unknown
inventory: ConsignmentInventory
voucherConsume?: WorkOrderVoucherConsumeResult
}>('/api/v1/admin/worker-platform/consignment/assign-worker', payload)
}
export function revokeWorkOrderConsignmentFulfillment(payload: {