feat(worker): support batch sharing cancellation

This commit is contained in:
yml2213
2026-08-28 19:40:49 +08:00
parent e189811617
commit d91d7180ad
13 changed files with 633 additions and 96 deletions
@@ -3,6 +3,7 @@ import {
acceptWorkOrderAndSettle,
acceptWorkOrderShareAndSettle,
cancelWorkOrderShare,
cancelWorkOrderSharing,
countWorkOrderPendingSharingSubmissions,
updateWorkOrder,
} from '../../repositories/worker-platform/index.js'
@@ -157,6 +158,81 @@ export async function cancelAdminWorkOrderShare(
}
}
export async function cancelAdminWorkOrderSharing(
workOrderId: number | string,
payload: JsonObject = {},
actorName = '',
actorPermissions: string[] = [],
) {
const action = String(payload.action || '').trim()
if (!['return_to_hall', 'cancel_order'].includes(action)) {
throw createHttpError('请选择拼单撤单处理方式', {
statusCode: 400,
errorCode: 'work_order_sharing_cancel_action_required',
})
}
const requiredPermission =
action === 'return_to_hall' ? 'worker_order.return_to_hall' : 'worker_order.cancel'
if (!actorPermissions.includes(requiredPermission)) {
throw createHttpError('当前账号没有此拼单撤单权限', {
statusCode: 403,
errorCode: 'admin_permission_denied',
})
}
const reason = String(payload.reason || '').trim()
if (action === 'cancel_order' && !reason) {
throw createHttpError('取消拼单订单时请填写退款或撤单原因', {
statusCode: 400,
errorCode: 'work_order_sharing_cancel_reason_required',
})
}
const result = await cancelWorkOrderSharing({
workOrderId: Number(workOrderId),
action: action as 'return_to_hall' | 'cancel_order',
reason,
now: nowIso(),
actorName,
})
if (result.failureReason === 'work_order_not_cancellable') {
throw createHttpError('当前订单不是可撤单的拼单', {
statusCode: 409,
errorCode: 'work_order_sharing_cancel_status_invalid',
})
}
if (result.failureReason === 'no_cancellable_shares') {
throw createHttpError('没有可撤销的未验收拼单份额', {
statusCode: 409,
errorCode: 'work_order_sharing_cancel_no_cancellable_shares',
})
}
if (result.failureReason === 'accepted_shares_present') {
throw createHttpError('存在已验收份额,不能取消整单;请通过售后处理退款', {
statusCode: 409,
errorCode: 'work_order_sharing_cancel_accepted_shares_present',
})
}
if (!result.order) {
throw createHttpError('拼单撤单失败,订单状态可能已变化', {
statusCode: 409,
errorCode: 'work_order_sharing_cancel_conflict',
})
}
const activeWorkerIds = await resolveWorkOrderRealtimeWorkerIds(result.order)
const workerIds = [...new Set([...activeWorkerIds, ...result.cancelledWorkerIds])]
publishWorkOrderRealtimeChange({
workOrderId: Number(workOrderId),
workerIds,
hallChanged: action === 'return_to_hall',
})
for (const workerId of result.cancelledWorkerIds) publishWorkerWalletRealtimeChange(workerId)
return {
order: mapWorkOrderAdmin(result.order),
action,
cancelledShareCount: result.cancelledShareCount,
releasedDepositAmount: result.releasedDepositAmount,
}
}
export async function acceptAdminWorkOrders(payload: JsonObject = {}, actorName = '') {
const workOrderIds = [
...new Set(
@@ -51,6 +51,7 @@ export {
acceptAdminWorkOrderShare,
acceptAdminWorkOrders,
cancelAdminWorkOrderShare,
cancelAdminWorkOrderSharing,
} from './admin-work-order-acceptance-service.js'
export * from './worker-product-match-config-service.js'
export * from './worker-announcement-config-service.js'
@@ -174,6 +174,10 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderSha
canPublish:
workOrder.status === WORK_ORDER_STATUS.UNASSIGNED && Number(workOrder.reward_amount || 0) > 0,
canCancel: status === WORK_ORDER_STATUS.IN_PROGRESS && Boolean(workOrder.assigned_worker_id),
canCancelSharing:
workOrder.sharing_enabled === true &&
hasActiveShares &&
['open', 'pending_acceptance', 'in_progress'].includes(workOrder.status),
canResolveProblem: workOrder.status === WORK_ORDER_STATUS.PROBLEM,
canDeduct: workOrder.status === WORK_ORDER_STATUS.ACCEPTED,
canConfirmFriend:
@@ -13,6 +13,7 @@ import type {
import {
buildWorkOrderWhere,
resolveWorkOrderShareCancellationStatus,
resolveWorkOrderSharingCancellationPlan,
resolveWorkOrderShareJoinQuantity,
resolveOpenSharingSyncReward,
} from '../../repositories/worker-platform/index.js'
@@ -991,6 +992,7 @@ test('后台拼单聚合状态同时收敛筛选语义和操作能力', () => {
assert.equal(partial.capabilities.canConfigureSharing, false)
assert.equal(partial.capabilities.canAssign, false)
assert.equal(partial.capabilities.canUpdateMaterial, true)
assert.equal(partial.capabilities.canCancelSharing, true)
const filled = mapWorkOrderAdmin(
buildWorkOrderRow({
@@ -1005,6 +1007,7 @@ test('后台拼单聚合状态同时收敛筛选语义和操作能力', () => {
assert.equal(filled.hallAvailable, false)
assert.equal(filled.capabilities.canAccept, true)
assert.equal(filled.capabilities.canUnpublish, false)
assert.equal(filled.capabilities.canCancelSharing, true)
})
test('已取消工单可重新启用,恢复未分配后可编辑和发布', () => {
@@ -1051,6 +1054,44 @@ test('拼单份额撤单只允许未验收份额并让母工单回到大厅状
assert.equal(resolveWorkOrderShareCancellationStatus('open', 'cancelled'), null)
})
test('拼单母单撤单区分退回大厅和取消整单的验收边界', () => {
assert.deepEqual(
resolveWorkOrderSharingCancellationPlan(
'pending_acceptance',
['submitted', 'accepted'],
'return_to_hall',
),
{
nextOrderStatus: 'open',
cancellableShareCount: 1,
acceptedShareCount: 1,
failureReason: null,
},
)
assert.equal(
resolveWorkOrderSharingCancellationPlan(
'pending_acceptance',
['submitted', 'accepted'],
'cancel_order',
).failureReason,
'accepted_shares_present',
)
assert.deepEqual(
resolveWorkOrderSharingCancellationPlan('open', ['joined', 'submitted'], 'cancel_order'),
{
nextOrderStatus: 'cancelled',
cancellableShareCount: 2,
acceptedShareCount: 0,
failureReason: null,
},
)
assert.equal(
resolveWorkOrderSharingCancellationPlan('accepted', ['submitted'], 'return_to_hall')
.failureReason,
'work_order_not_cancellable',
)
})
test('问题单退回打手按标记前状态恢复', () => {
assert.equal(resolveProblemReturnToWorkerStatus('pending_acceptance'), 'pending_acceptance')
assert.equal(resolveProblemReturnToWorkerStatus('open'), 'open')