feat(接单): 支持客服手动结束赠送冷却
This commit is contained in:
@@ -479,6 +479,7 @@ export type WorkOrderCapabilities = {
|
||||
canResolveProblem: boolean
|
||||
canDeduct: boolean
|
||||
canConfirmFriend: boolean
|
||||
canCompleteGiftCooldown: boolean
|
||||
canResetGiftFlow: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,82 @@ export async function confirmWorkOrderFriendAdded(input: {
|
||||
})
|
||||
}
|
||||
|
||||
/** 后台人工确认赠送冷却已完成:立即开放打手提交赠送凭证,并从当前重新计算任务时限。 */
|
||||
export async function completeWorkOrderGiftCooldown(input: {
|
||||
workOrderId: number
|
||||
note?: string
|
||||
actorName?: string
|
||||
now: string
|
||||
}): Promise<{
|
||||
order: WorkOrderRow | null
|
||||
failureReason:
|
||||
| 'work_order_not_found'
|
||||
| 'work_order_mode_invalid'
|
||||
| 'work_order_status_invalid'
|
||||
| 'work_order_gift_phase_invalid'
|
||||
| 'work_order_gift_cooldown_completed'
|
||||
| null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const currentResult = await client.query<WorkOrderRow>(
|
||||
'SELECT * FROM work_orders WHERE id = $1 FOR UPDATE',
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = currentResult.rows[0] || null
|
||||
if (!workOrder) return { order: null, failureReason: 'work_order_not_found' }
|
||||
if (
|
||||
normalizeWorkOrderAcceptanceMode(workOrder.acceptance_mode) !==
|
||||
WORK_ORDER_ACCEPTANCE_MODE.FRIEND_GIFT
|
||||
) {
|
||||
return { order: null, failureReason: 'work_order_mode_invalid' }
|
||||
}
|
||||
if (!['in_progress', 'problem'].includes(workOrder.status)) {
|
||||
return { order: null, failureReason: 'work_order_status_invalid' }
|
||||
}
|
||||
if (workOrder.gift_phase !== WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN) {
|
||||
return { order: null, failureReason: 'work_order_gift_phase_invalid' }
|
||||
}
|
||||
const availableAt = Date.parse(String(workOrder.gift_available_at || ''))
|
||||
if (Number.isFinite(availableAt) && availableAt <= Date.parse(input.now)) {
|
||||
return { order: null, failureReason: 'work_order_gift_cooldown_completed' }
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET
|
||||
gift_available_at = $1::timestamptz,
|
||||
deadline_at = CASE
|
||||
WHEN timeout_minutes > 0
|
||||
THEN $1::timestamptz + (timeout_minutes * INTERVAL '1 minute')
|
||||
ELSE NULL
|
||||
END,
|
||||
updated_at = $1::timestamptz
|
||||
WHERE id = $2
|
||||
`,
|
||||
[input.now, input.workOrderId],
|
||||
)
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'admin',
|
||||
actorId: input.actorName || '',
|
||||
eventType: 'gift_cooldown_completed_by_admin',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: workOrder.status,
|
||||
payloadJson: JSON.stringify({
|
||||
previousGiftAvailableAt: workOrder.gift_available_at,
|
||||
completedAt: input.now,
|
||||
note: String(input.note || '').trim(),
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
return {
|
||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 后台重置打手资料(资料有误或误确认好友):清空好友状态并让打手重新提交。 */
|
||||
export async function resetWorkOrderGiftMaterial(input: {
|
||||
workOrderId: number
|
||||
|
||||
@@ -841,7 +841,13 @@ export function buildWorkOrderWhere({
|
||||
)`)
|
||||
} else if (Object.values(WORK_ORDER_GIFT_PHASE).includes(normalizedGiftPhase as never)) {
|
||||
params.push(normalizedGiftPhase)
|
||||
filters.push(`wo.acceptance_mode = 'friend_gift' AND wo.gift_phase = $${params.length}`)
|
||||
const giftPhaseFilter = `wo.acceptance_mode = 'friend_gift' AND wo.gift_phase = $${params.length}`
|
||||
// 待打手提交资料仅代表已接单后的赠送流程;待抢单/未分配模板单不应混入该筛选。
|
||||
filters.push(
|
||||
normalizedGiftPhase === WORK_ORDER_GIFT_PHASE.MATERIAL_REQUIRED
|
||||
? `(${giftPhaseFilter} AND wo.assigned_worker_id IS NOT NULL AND wo.status IN ('in_progress', 'problem'))`
|
||||
: giftPhaseFilter,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (hallCandidateLimit > 0) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
addAdminMockCustomShop,
|
||||
assignAdminWorkOrderToWorker,
|
||||
cancelAdminWorkOrder,
|
||||
completeAdminWorkOrderGiftCooldown,
|
||||
confirmAdminWorkOrderFriendAdded,
|
||||
createAdminMockWorkOrder,
|
||||
deleteAdminMockCustomShop,
|
||||
@@ -441,6 +442,30 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/complete-gift-cooldown',
|
||||
requireAdminPermission('worker_order.manage_problem'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
completeAdminWorkOrderGiftCooldown(
|
||||
String(req.params.workOrderId || ''),
|
||||
req.body || {},
|
||||
req.adminSession?.username || '',
|
||||
),
|
||||
{
|
||||
successMessage: '已手动结束赠送冷却,打手现在可以提交验收',
|
||||
errorMessage: '结束赠送冷却失败',
|
||||
scope: '[admin/worker-platform/orders/:workOrderId/complete-gift-cooldown]',
|
||||
audit: (req, data) => ({
|
||||
action: 'work_order_gift_cooldown_completed',
|
||||
targetType: 'work_order',
|
||||
targetId: String(req.params.workOrderId || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/worker-platform/orders/:workOrderId/sharing',
|
||||
requireAdminPermission('worker_order.view'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
completeWorkOrderGiftCooldown,
|
||||
confirmWorkOrderFriendAdded,
|
||||
resetWorkOrderGiftMaterial,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
@@ -39,6 +40,51 @@ export async function confirmAdminWorkOrderFriendAdded(
|
||||
return { order: mapWorkOrderAdmin(order) }
|
||||
}
|
||||
|
||||
/** 客服手动结束好友赠送冷却,让打手立即可以提交赠送凭证。 */
|
||||
export async function completeAdminWorkOrderGiftCooldown(
|
||||
workOrderId: number | string,
|
||||
payload: { note?: unknown } = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
const { order, failureReason } = await completeWorkOrderGiftCooldown({
|
||||
workOrderId: Number(workOrder.id),
|
||||
note: String(payload.note || '').trim(),
|
||||
actorName,
|
||||
now: nowIso(),
|
||||
})
|
||||
if (failureReason === 'work_order_gift_phase_invalid') {
|
||||
throw createHttpError('当前订单不在好友赠送冷却中,请刷新后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_gift_cooldown_phase_invalid',
|
||||
})
|
||||
}
|
||||
if (failureReason === 'work_order_gift_cooldown_completed') {
|
||||
throw createHttpError('好友赠送冷却已结束,无需手动操作', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_gift_cooldown_already_completed',
|
||||
})
|
||||
}
|
||||
if (failureReason === 'work_order_status_invalid') {
|
||||
throw createHttpError('当前订单状态不能结束赠送冷却', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_gift_cooldown_status_invalid',
|
||||
})
|
||||
}
|
||||
if (!order) {
|
||||
throw createHttpError('结束赠送冷却失败,订单可能已变化', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_gift_cooldown_complete_failed',
|
||||
})
|
||||
}
|
||||
const workerId = Number(order.assigned_worker_id || 0)
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: Number(order.id),
|
||||
workerIds: workerId > 0 ? [workerId] : [],
|
||||
})
|
||||
return { order: mapWorkOrderAdmin(order) }
|
||||
}
|
||||
|
||||
/** 打手资料有误或误确认好友时,重置资料让打手重新提交(冷却与好友时间清零)。 */
|
||||
export async function resetAdminWorkOrderGiftMaterial(
|
||||
workOrderId: number | string,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import {
|
||||
isFriendGiftAcceptanceMode,
|
||||
isWorkOrderGiftAvailable,
|
||||
normalizeWorkOrderAcceptanceMode,
|
||||
normalizeWorkOrderGiftCooldownHours,
|
||||
normalizeWorkOrderGiftCooldownMinutes,
|
||||
@@ -185,6 +186,13 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderSha
|
||||
isFriendGift &&
|
||||
workOrder.status === WORK_ORDER_STATUS.IN_PROGRESS &&
|
||||
giftPhase === WORK_ORDER_GIFT_PHASE.MATERIAL_SUBMITTED,
|
||||
canCompleteGiftCooldown:
|
||||
isFriendGift &&
|
||||
[WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PROBLEM].includes(
|
||||
workOrder.status as never,
|
||||
) &&
|
||||
giftPhase === WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN &&
|
||||
!isWorkOrderGiftAvailable(workOrder),
|
||||
canResetGiftFlow:
|
||||
isFriendGift &&
|
||||
[WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PROBLEM].includes(
|
||||
|
||||
@@ -971,6 +971,42 @@ test('打手订单状态筛选使用个人拼单份额状态', () => {
|
||||
assert.match(whereClause, /= ANY\(\$2::text\[\]\)/)
|
||||
})
|
||||
|
||||
test('好友赠送待打手提交资料筛选排除待抢单订单', () => {
|
||||
const { whereClause, params } = buildWorkOrderWhere({ giftPhase: 'material_required' })
|
||||
|
||||
assert.deepEqual(params, ['material_required'])
|
||||
assert.match(whereClause, /wo\.acceptance_mode = 'friend_gift'/)
|
||||
assert.match(whereClause, /wo\.gift_phase = \$1/)
|
||||
assert.match(whereClause, /wo\.assigned_worker_id IS NOT NULL/)
|
||||
assert.match(whereClause, /wo\.status IN \('in_progress', 'problem'\)/)
|
||||
})
|
||||
|
||||
test('好友赠送冷却中才允许客服手动结束冷却', () => {
|
||||
const cooling = mapWorkOrderAdmin(
|
||||
buildWorkOrderRow({
|
||||
status: 'in_progress',
|
||||
assigned_worker_id: 1,
|
||||
acceptance_mode: 'friend_gift',
|
||||
gift_phase: 'friend_countdown',
|
||||
gift_available_at: new Date(Date.now() + 60_000).toISOString(),
|
||||
}),
|
||||
)
|
||||
assert.equal(cooling.gift?.phase, 'friend_countdown')
|
||||
assert.equal(cooling.capabilities.canCompleteGiftCooldown, true)
|
||||
|
||||
const completed = mapWorkOrderAdmin(
|
||||
buildWorkOrderRow({
|
||||
status: 'in_progress',
|
||||
assigned_worker_id: 1,
|
||||
acceptance_mode: 'friend_gift',
|
||||
gift_phase: 'friend_countdown',
|
||||
gift_available_at: new Date(Date.now() - 60_000).toISOString(),
|
||||
}),
|
||||
)
|
||||
assert.equal(completed.gift?.phase, 'gift_ready')
|
||||
assert.equal(completed.capabilities.canCompleteGiftCooldown, false)
|
||||
})
|
||||
|
||||
test('后台拼单聚合状态同时收敛筛选语义和操作能力', () => {
|
||||
const waitingForClaim = mapWorkOrderAdmin(buildWorkOrderRow({ status: 'open' }))
|
||||
assert.equal(waitingForClaim.status, 'open')
|
||||
|
||||
@@ -157,6 +157,20 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
|
||||
case 'material_updated':
|
||||
return { title: '更新了订单资料', color: 'cyan' }
|
||||
|
||||
// —— 好友赠送节点 ——
|
||||
case 'gift_material_submitted':
|
||||
return { title: '打手提交好友赠送资料,等待客服确认加好友', color: 'cyan' }
|
||||
case 'friend_confirmed':
|
||||
return { title: '客服确认已加好友,赠送冷却开始', color: 'gold' }
|
||||
case 'gift_cooldown_completed_by_admin':
|
||||
return {
|
||||
title: note ? `客服手动结束赠送冷却:${note}` : '客服手动结束赠送冷却',
|
||||
description: '打手现在可以提交赠送凭证与验收资料',
|
||||
color: 'green',
|
||||
}
|
||||
case 'gift_material_reset':
|
||||
return { title: note ? `客服重置打手好友赠送资料:${note}` : '客服重置打手好友赠送资料', color: 'orange' }
|
||||
|
||||
// —— 验收节点 ——
|
||||
case 'acceptance_submitted': {
|
||||
const imageCount = readPayload(event, 'imageCount')
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
cancelAdminWorkOrder,
|
||||
cancelAdminWorkOrderShare,
|
||||
cancelAdminWorkOrderSharing,
|
||||
completeAdminWorkOrderGiftCooldown,
|
||||
confirmAdminWorkOrderFriend,
|
||||
deductAdminWorkOrderPendingDeposit,
|
||||
deleteAdminWorkOrder,
|
||||
@@ -886,6 +887,22 @@ export default function WorkOrdersPanel() {
|
||||
})
|
||||
}
|
||||
|
||||
function completeGiftCooldown(row: WorkOrder) {
|
||||
modal.confirm({
|
||||
title: '手动结束赠送冷却?',
|
||||
content:
|
||||
'确认后将立即允许打手提交赠送凭证与验收资料;订单任务时限将从当前时刻重新计算。此操作会记录在订单流转记录中。',
|
||||
okText: '确认结束冷却',
|
||||
okButtonProps: { type: 'primary' },
|
||||
cancelText: '取消',
|
||||
onOk: () =>
|
||||
runAction(
|
||||
() => completeAdminWorkOrderGiftCooldown(row.workOrderId),
|
||||
'已手动结束赠送冷却,打手现在可以提交验收',
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
function requestResetGiftMaterial(row: WorkOrder) {
|
||||
modal.confirm({
|
||||
title: '重置打手好友赠送资料?',
|
||||
@@ -1018,7 +1035,9 @@ export default function WorkOrdersPanel() {
|
||||
</Tag>
|
||||
) : null}
|
||||
{row.acceptanceMode === 'friend_gift' ? <Tag color="purple">好友赠送</Tag> : null}
|
||||
{row.acceptanceMode === 'friend_gift' && row.gift ? (
|
||||
{row.acceptanceMode === 'friend_gift' &&
|
||||
row.gift &&
|
||||
!['pending_material', 'unassigned', 'open'].includes(row.rawStatus || row.status) ? (
|
||||
<Tag color={resolveGiftPhaseColor(row.gift.phase)}>
|
||||
{formatGiftPhase(row.gift.phase)}
|
||||
</Tag>
|
||||
@@ -1234,6 +1253,11 @@ export default function WorkOrdersPanel() {
|
||||
确认加好友
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canCompleteGiftCooldown && canManageProblem ? (
|
||||
<Button type="primary" onClick={() => completeGiftCooldown(row)}>
|
||||
结束冷却
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canResetGiftFlow && canManageProblem ? (
|
||||
<Button danger onClick={() => requestResetGiftMaterial(row)}>
|
||||
重置打手资料
|
||||
@@ -1639,6 +1663,15 @@ export default function WorkOrdersPanel() {
|
||||
确认加好友
|
||||
</Button>
|
||||
) : null}
|
||||
{detailOrder.capabilities?.canCompleteGiftCooldown && canManageProblem ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => completeGiftCooldown(detailOrder)}
|
||||
>
|
||||
结束冷却
|
||||
</Button>
|
||||
) : null}
|
||||
{detailOrder.capabilities?.canResetGiftFlow && canManageProblem ? (
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
@@ -686,6 +686,13 @@ export function confirmAdminWorkOrderFriend(workOrderId: number) {
|
||||
)
|
||||
}
|
||||
|
||||
/** 好友赠送模式:客服人工确认冷却完成,立即允许打手提交验收。 */
|
||||
export function completeAdminWorkOrderGiftCooldown(workOrderId: number) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/complete-gift-cooldown`,
|
||||
)
|
||||
}
|
||||
|
||||
/** 好友赠送模式:重置打手资料,冷却与好友时间清零 */
|
||||
export function resetAdminWorkOrderGiftMaterial(workOrderId: number, note?: string) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
|
||||
@@ -557,6 +557,7 @@ export type WorkOrder = {
|
||||
canResolveProblem: boolean
|
||||
canDeduct: boolean
|
||||
canConfirmFriend?: boolean
|
||||
canCompleteGiftCooldown?: boolean
|
||||
canResetGiftFlow?: boolean
|
||||
}
|
||||
rewardAmount: number
|
||||
|
||||
Reference in New Issue
Block a user