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:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user