fix(worker): 整单指派拼单订单无法验收
- 前端:拼单订单整单指派给本人时放行验收 - 后端:整单指派清掉 sharing_enabled;已有有效份额则拒绝
This commit is contained in:
@@ -143,6 +143,7 @@ export async function assignWorkOrderToWorker(input: {
|
|||||||
order: WorkOrderRow | null
|
order: WorkOrderRow | null
|
||||||
failureReason:
|
failureReason:
|
||||||
| 'work_order_not_assignable'
|
| 'work_order_not_assignable'
|
||||||
|
| 'work_order_has_shares'
|
||||||
| 'worker_active_order_limit'
|
| 'worker_active_order_limit'
|
||||||
| 'worker_deposit_insufficient'
|
| 'worker_deposit_insufficient'
|
||||||
| null
|
| null
|
||||||
@@ -167,6 +168,19 @@ export async function assignWorkOrderToWorker(input: {
|
|||||||
}
|
}
|
||||||
const fromStatus = workOrder.status
|
const fromStatus = workOrder.status
|
||||||
|
|
||||||
|
// 已有拼单份额的订单不能按整单指派:否则会出现“整单接单人 + 其他人的份额”混在一起。
|
||||||
|
const shareCountResult = await client.query<{ total: number }>(
|
||||||
|
`
|
||||||
|
SELECT COUNT(*)::int AS total
|
||||||
|
FROM work_order_shares
|
||||||
|
WHERE work_order_id = $1 AND status <> 'cancelled'
|
||||||
|
`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
if (Number(shareCountResult.rows[0]?.total || 0) > 0) {
|
||||||
|
return { order: null, failureReason: 'work_order_has_shares' }
|
||||||
|
}
|
||||||
|
|
||||||
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
||||||
const wallet = await getWorkerWalletWithClient(client, input.workerId)
|
const wallet = await getWorkerWalletWithClient(client, input.workerId)
|
||||||
const activeOrderCount = await countWorkerActiveOrdersWithClient(client, input.workerId)
|
const activeOrderCount = await countWorkerActiveOrdersWithClient(client, input.workerId)
|
||||||
@@ -193,6 +207,9 @@ export async function assignWorkOrderToWorker(input: {
|
|||||||
friend_added_at = NULL,
|
friend_added_at = NULL,
|
||||||
gift_available_at = NULL,
|
gift_available_at = NULL,
|
||||||
${CLEAR_DELIVERY_RESIDUE_SQL},
|
${CLEAR_DELIVERY_RESIDUE_SQL},
|
||||||
|
-- 管理端/寄卖履约都是整单指派,不会创建 work_order_shares;
|
||||||
|
-- 必须同时清掉拼单标记,避免出现“已开拼单但没有份额”的脏状态。
|
||||||
|
sharing_enabled = false,
|
||||||
updated_at = $2
|
updated_at = $2
|
||||||
WHERE id = $4
|
WHERE id = $4
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
countWorkerActiveOrders,
|
countWorkerActiveOrders,
|
||||||
createWorkOrderEvent,
|
createWorkOrderEvent,
|
||||||
getWorkerUserById,
|
getWorkerUserById,
|
||||||
|
listWorkOrderShares,
|
||||||
} from '../../repositories/worker-platform/index.js'
|
} from '../../repositories/worker-platform/index.js'
|
||||||
import type { JsonObject } from '../../types/json.js'
|
import type { JsonObject } from '../../types/json.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
@@ -64,6 +65,18 @@ export async function assignAdminWorkOrderToWorker(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 已有拼单份额的订单不能再按整单指派;放在核销电子凭证之前,
|
||||||
|
// 避免“凭证已核销但指派失败”的副作用。
|
||||||
|
const activeShares = (await listWorkOrderShares(workOrder.id)).filter(
|
||||||
|
(share) => share.status !== 'cancelled',
|
||||||
|
)
|
||||||
|
if (activeShares.length > 0) {
|
||||||
|
throw createHttpError('该订单已有拼单份额,不能按整单指派;请使用拼单管理', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_has_shares',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const voucherConsume = await consumeIndustryVouchersBeforeWorkOrderAssign(workOrder)
|
const voucherConsume = await consumeIndustryVouchersBeforeWorkOrderAssign(workOrder)
|
||||||
if (!voucherConsume.ok) {
|
if (!voucherConsume.ok) {
|
||||||
const failedAt = nowIso()
|
const failedAt = nowIso()
|
||||||
@@ -112,6 +125,15 @@ export async function assignAdminWorkOrderToWorker(
|
|||||||
{ statusCode: 409, errorCode: 'worker_deposit_insufficient' },
|
{ statusCode: 409, errorCode: 'worker_deposit_insufficient' },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (failureReason === 'work_order_has_shares') {
|
||||||
|
throw createHttpError(
|
||||||
|
resolveAssignFailureMessage(
|
||||||
|
voucherConsume.voucherCount,
|
||||||
|
'该订单已有拼单份额,不能按整单指派;请使用拼单管理',
|
||||||
|
),
|
||||||
|
{ statusCode: 409, errorCode: 'work_order_has_shares' },
|
||||||
|
)
|
||||||
|
}
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
resolveAssignFailureMessage(voucherConsume.voucherCount, '指派失败,订单状态可能已变化'),
|
resolveAssignFailureMessage(voucherConsume.voucherCount, '指派失败,订单状态可能已变化'),
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ import type { UploadedFile, WorkOrder, WorkerAcceptanceVideo } from '@/types/wor
|
|||||||
import { isExplicitOrderNoSearch, normalizeSearchText } from '@/utils/admin-realtime-work-orders'
|
import { isExplicitOrderNoSearch, normalizeSearchText } from '@/utils/admin-realtime-work-orders'
|
||||||
import { formatDateTime } from '@/utils/date-time'
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||||
|
import { shouldBlockAcceptanceForMissingShares } from '@/utils/worker-order-acceptance'
|
||||||
import { buildWorkerOrderQuery } from '@/utils/worker-order-query'
|
import { buildWorkerOrderQuery } from '@/utils/worker-order-query'
|
||||||
|
import { getWorkerId } from '@/utils/worker-auth'
|
||||||
import { WorkerOrderDesktopList } from './WorkerOrderDesktopList'
|
import { WorkerOrderDesktopList } from './WorkerOrderDesktopList'
|
||||||
import { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer'
|
import { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer'
|
||||||
import { WorkerOrderMobileList } from './WorkerOrderMobileList'
|
import { WorkerOrderMobileList } from './WorkerOrderMobileList'
|
||||||
@@ -324,8 +326,16 @@ export default function WorkerOrdersPage() {
|
|||||||
|
|
||||||
function openAcceptanceModal(order: WorkOrder) {
|
function openAcceptanceModal(order: WorkOrder) {
|
||||||
const shares = getOrderShares(order)
|
const shares = getOrderShares(order)
|
||||||
// 整单(非拼单)本来就没有份额,不能拦;只有拼单订单缺份额时才提示刷新。
|
// 整单(非拼单)本来就没有份额,不能拦;拼单订单被管理端整单指派给本人时,
|
||||||
if (shares.length === 0 && order.sharing?.enabled === true) {
|
// 后端也会按整单验收,同样不能拦。只有“拼单缺份额且不是指派给本人”才提示刷新。
|
||||||
|
if (
|
||||||
|
shouldBlockAcceptanceForMissingShares({
|
||||||
|
shareCount: shares.length,
|
||||||
|
sharingEnabled: order.sharing?.enabled === true,
|
||||||
|
assignedWorkerId: order.worker?.workerId,
|
||||||
|
currentWorkerId: getWorkerId(),
|
||||||
|
})
|
||||||
|
) {
|
||||||
message.warning('未找到可验收的拼单份额,请刷新后重试')
|
message.warning('未找到可验收的拼单份额,请刷新后重试')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 打手验收前置判断:拼单订单缺份额时是否应该拦截。
|
||||||
|
*
|
||||||
|
* 管理端「指派打手」是整单指派,不会创建 work_order_shares;
|
||||||
|
* 这类订单虽然 sharing.enabled=true,但接单人就是当前打手,
|
||||||
|
* 必须按整单验收放行,否则会被前端防呆误拦。
|
||||||
|
*/
|
||||||
|
export type AcceptanceShareGuardInput = {
|
||||||
|
shareCount: number
|
||||||
|
sharingEnabled: boolean
|
||||||
|
assignedWorkerId: number | null | undefined
|
||||||
|
currentWorkerId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldBlockAcceptanceForMissingShares(input: AcceptanceShareGuardInput): boolean {
|
||||||
|
if (input.shareCount > 0) return false
|
||||||
|
if (input.sharingEnabled !== true) return false
|
||||||
|
|
||||||
|
const currentWorkerId = Number(input.currentWorkerId || 0)
|
||||||
|
// 本地会话里拿不到打手 ID 时保持旧行为,避免误放行。
|
||||||
|
if (!Number.isFinite(currentWorkerId) || currentWorkerId <= 0) return true
|
||||||
|
|
||||||
|
return Number(input.assignedWorkerId || 0) !== currentWorkerId
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import { shouldBlockAcceptanceForMissingShares } from '../src/utils/worker-order-acceptance.ts'
|
||||||
|
|
||||||
|
test('拼单缺份额:整单指派给当前打手时放行', () => {
|
||||||
|
assert.equal(
|
||||||
|
shouldBlockAcceptanceForMissingShares({
|
||||||
|
shareCount: 0,
|
||||||
|
sharingEnabled: true,
|
||||||
|
assignedWorkerId: 1013,
|
||||||
|
currentWorkerId: 1013,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('拼单缺份额:指派给其他打手时拦截', () => {
|
||||||
|
assert.equal(
|
||||||
|
shouldBlockAcceptanceForMissingShares({
|
||||||
|
shareCount: 0,
|
||||||
|
sharingEnabled: true,
|
||||||
|
assignedWorkerId: 999,
|
||||||
|
currentWorkerId: 1013,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('拼单缺份额:没有接单人时拦截', () => {
|
||||||
|
assert.equal(
|
||||||
|
shouldBlockAcceptanceForMissingShares({
|
||||||
|
shareCount: 0,
|
||||||
|
sharingEnabled: true,
|
||||||
|
assignedWorkerId: null,
|
||||||
|
currentWorkerId: 1013,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('有份额时按正常份额流程处理,不拦截', () => {
|
||||||
|
assert.equal(
|
||||||
|
shouldBlockAcceptanceForMissingShares({
|
||||||
|
shareCount: 1,
|
||||||
|
sharingEnabled: true,
|
||||||
|
assignedWorkerId: 1013,
|
||||||
|
currentWorkerId: 1013,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('非拼单整单没有份额,不拦截', () => {
|
||||||
|
assert.equal(
|
||||||
|
shouldBlockAcceptanceForMissingShares({
|
||||||
|
shareCount: 0,
|
||||||
|
sharingEnabled: false,
|
||||||
|
assignedWorkerId: 1013,
|
||||||
|
currentWorkerId: 1013,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('本地取不到当前打手 ID 时保持旧行为(拦截)', () => {
|
||||||
|
assert.equal(
|
||||||
|
shouldBlockAcceptanceForMissingShares({
|
||||||
|
shareCount: 0,
|
||||||
|
sharingEnabled: true,
|
||||||
|
assignedWorkerId: 1013,
|
||||||
|
currentWorkerId: 0,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user