fix(admin): 恢复任务重试/转人工/人工完结等后台写接口
删除咸鱼链路时误删了 retry、mark-manual-review、complete-manual-dispatch、regenerate-claim-link;现按 affiliate_dash + manual_dispatch 语义恢复,历史下线通道可人工完结但不可自动重试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,13 @@ import type { Request } from 'express'
|
||||
import { Router } from 'express'
|
||||
|
||||
import { getAdminTaskDetail, getAdminTasks } from '../../services/admin/admin-read-service.js'
|
||||
import { closeAdminTask } from '../../services/admin/write/task-actions.js'
|
||||
import {
|
||||
closeAdminTask,
|
||||
completeAdminTaskManualDispatch,
|
||||
markAdminTaskManualReview,
|
||||
regenerateAdminTaskClaimLink,
|
||||
retryAdminTask,
|
||||
} from '../../services/admin/write/task-actions.js'
|
||||
import {
|
||||
consumeAdminTaskKuaishouIndustryVoucher,
|
||||
resendAdminTaskKuaishouIndustryVoucherCode,
|
||||
@@ -10,10 +16,14 @@ import {
|
||||
import { createJsonHandler, requireAdminRoles } from './session.js'
|
||||
import type {
|
||||
AdminTaskKuaishouIndustryConsumeRouteBody,
|
||||
AdminTaskManualDispatchRouteBody,
|
||||
AdminTaskRouteParams,
|
||||
AdminTaskRouteQuery,
|
||||
} from '../../types/admin/route-inputs.js'
|
||||
import type { AdminTaskActionResponse } from '../../types/admin/write-models.js'
|
||||
import type {
|
||||
AdminTaskActionResponse,
|
||||
AdminTaskManualDispatchResponse,
|
||||
} from '../../types/admin/write-models.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -44,6 +54,31 @@ router.get(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/tasks/:taskId/retry',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler((req) => retryAdminTask(getTaskId(req), req.adminSession || null), {
|
||||
successMessage: '任务已重试',
|
||||
errorMessage: '重试任务失败',
|
||||
scope: '[admin/tasks/:taskId/retry]',
|
||||
audit: (req, data) => buildTaskAudit('task_retry', req, data),
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/tasks/:taskId/regenerate-claim-link',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) => regenerateAdminTaskClaimLink(getTaskId(req), req.adminSession || null),
|
||||
{
|
||||
successMessage: '领取链接已重新生成',
|
||||
errorMessage: '重新生成领取链接失败',
|
||||
scope: '[admin/tasks/:taskId/regenerate-claim-link]',
|
||||
audit: (req, data) => buildTaskAudit('task_regenerate_claim_link', req, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/tasks/:taskId/close',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
@@ -55,6 +90,50 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/tasks/:taskId/mark-manual-review',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler((req) => markAdminTaskManualReview(getTaskId(req), req.adminSession || null), {
|
||||
successMessage: '任务已转人工处理',
|
||||
errorMessage: '标记人工处理失败',
|
||||
scope: '[admin/tasks/:taskId/mark-manual-review]',
|
||||
audit: (req, data) => buildTaskAudit('task_mark_manual_review', req, data),
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/tasks/:taskId/complete-manual-dispatch',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
completeAdminTaskManualDispatch(
|
||||
getTaskId(req),
|
||||
req.body as AdminTaskManualDispatchRouteBody,
|
||||
req.adminSession || null,
|
||||
),
|
||||
{
|
||||
successMessage: '人工履约结果已回写',
|
||||
errorMessage: '回写人工履约结果失败',
|
||||
scope: '[admin/tasks/:taskId/complete-manual-dispatch]',
|
||||
audit: (req, data) => {
|
||||
const result = data as AdminTaskManualDispatchResponse
|
||||
return {
|
||||
action: 'task_complete_manual_dispatch',
|
||||
targetType: 'task',
|
||||
targetId: String(getTaskId(req)),
|
||||
data: {
|
||||
taskId: result.task.taskId,
|
||||
taskNo: result.task.taskNo,
|
||||
outcome: result.outcome,
|
||||
deliveryStatus: result.task.deliveryStatus,
|
||||
resultCode: result.task.resultCode,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/tasks/:taskId/kuaishou-industry/resend-code',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
resolveOrderItemSkuNick,
|
||||
resolveOrderItemTitle,
|
||||
} from './admin-read-shared-helpers.js'
|
||||
import { isRetiredFulfillmentExecutor } from '../fulfillment/executors/types.js'
|
||||
import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js'
|
||||
import {
|
||||
buildOrderFulfillmentProgress,
|
||||
@@ -385,6 +386,7 @@ export async function getAdminTaskDetail(
|
||||
canRetry:
|
||||
viewerContext.canManageTaskLifecycle &&
|
||||
!isManualDispatchTask(task) &&
|
||||
!isRetiredFulfillmentExecutor(task.executor_key) &&
|
||||
canRetryTaskStatus(task.task_status),
|
||||
canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext),
|
||||
canClose: canViewerCloseTask(task, viewerContext),
|
||||
@@ -392,7 +394,7 @@ export async function getAdminTaskDetail(
|
||||
viewerContext.canManageTaskLifecycle && canMarkManualReviewTaskStatus(task.task_status),
|
||||
canCompleteManualDispatch:
|
||||
viewerContext.canManageTaskLifecycle &&
|
||||
isManualDispatchTask(task) &&
|
||||
(isManualDispatchTask(task) || isRetiredFulfillmentExecutor(task.executor_key)) &&
|
||||
canCompleteManualDispatchTaskStatus(task.task_status),
|
||||
canPrepareKuaishouCloudFulfillment: false,
|
||||
canRefreshKuaishouCloudRoleInfo: false,
|
||||
|
||||
@@ -1,17 +1,36 @@
|
||||
import { updateClaimToken } from '../../../repositories/claim-token-repo.js'
|
||||
import { updateTask } from '../../../repositories/task-repo.js'
|
||||
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
||||
import { createTaskClaimToken, buildClaimUrl } from '../../claim/claim-service.js'
|
||||
import { notifyTaskAutoManualReview } from '../../notification/domain-notifications.js'
|
||||
import { preparePaidFulfillmentTask } from '../../fulfillment/executors/registry.js'
|
||||
import {
|
||||
canRetryTaskStatus,
|
||||
canRegenerateClaimLinkStatus,
|
||||
TASK_STATUS,
|
||||
} from '../../../domain/task-status.js'
|
||||
import { isRetiredFulfillmentExecutor } from '../../fulfillment/executors/types.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import { canViewerCloseTask, createAdminViewerContext } from '../admin-read-shared-helpers.js'
|
||||
import {
|
||||
canViewerCloseTask,
|
||||
createAdminViewerContext,
|
||||
getTaskPrimaryClaimTokenId,
|
||||
isManualDispatchTask,
|
||||
parseTaskContext,
|
||||
} from '../admin-read-shared-helpers.js'
|
||||
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
|
||||
import { maskCode, normalizeManualDispatchOutcome } from '../write-helpers.js'
|
||||
|
||||
import type {
|
||||
AdminEntityIdInput,
|
||||
AdminViewerSessionInput,
|
||||
} from '../../../types/admin/read-inputs.js'
|
||||
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
|
||||
import type { AdminTaskManualDispatchInput } from '../../../types/admin/write-inputs.js'
|
||||
import type {
|
||||
AdminTaskActionResponse,
|
||||
AdminTaskManualDispatchResponse,
|
||||
} from '../../../types/admin/write-models.js'
|
||||
|
||||
export async function closeAdminTask(
|
||||
taskId: AdminEntityIdInput,
|
||||
@@ -76,3 +95,309 @@ export async function closeAdminTask(
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
export async function markAdminTaskManualReview(
|
||||
taskId: AdminEntityIdInput,
|
||||
session: AdminViewerSessionInput | null = null,
|
||||
): Promise<AdminTaskActionResponse> {
|
||||
const task = await getRequiredTask(taskId)
|
||||
const now = nowIso()
|
||||
const reason = task.last_error || '已转人工处理'
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: TASK_STATUS.MANUAL_REVIEW,
|
||||
delivery_status: task.delivery_status || 'pending',
|
||||
last_error: reason,
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
if (!updatedTask) {
|
||||
throw createHttpError('标记人工处理失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'admin_task_mark_manual_review_failed',
|
||||
})
|
||||
}
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'task_mark_manual_review',
|
||||
{
|
||||
reason,
|
||||
markedBy: session
|
||||
? {
|
||||
userId: session.userId,
|
||||
username: session.username,
|
||||
role: session.role,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
export async function retryAdminTask(
|
||||
taskId: AdminEntityIdInput,
|
||||
session: AdminViewerSessionInput | null = null,
|
||||
): Promise<AdminTaskActionResponse> {
|
||||
const task = await getRequiredTask(taskId)
|
||||
|
||||
if (isManualDispatchTask(task)) {
|
||||
throw createHttpError('人工履约任务不能走自动重试,请在详情页直接回写人工履约结果', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_manual_dispatch_retry_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
if (isRetiredFulfillmentExecutor(task.executor_key)) {
|
||||
throw createHttpError('该履约通道已下线,请转人工处理', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_executor_retired',
|
||||
})
|
||||
}
|
||||
|
||||
if (!canRetryTaskStatus(task.task_status)) {
|
||||
throw createHttpError('当前任务状态不允许重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_retry_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const resetTask = await updateTask(task.id, {
|
||||
task_status: TASK_STATUS.PAID,
|
||||
last_error: '',
|
||||
result_code: '',
|
||||
result_message: '',
|
||||
attempt_count: Number(task.attempt_count || 0) + 1,
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
if (!resetTask) {
|
||||
throw createHttpError('任务重试失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'admin_task_retry_failed',
|
||||
})
|
||||
}
|
||||
|
||||
const prepared = await preparePaidFulfillmentTask(resetTask, {
|
||||
updateTask,
|
||||
createTaskClaimToken,
|
||||
notifyTaskAutoManualReview,
|
||||
nowIso,
|
||||
})
|
||||
|
||||
const effectiveTask = prepared || resetTask
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'task_retry',
|
||||
{
|
||||
previousStatus: task.task_status,
|
||||
nextStatus: effectiveTask.task_status,
|
||||
executorKey: task.executor_key,
|
||||
retriedBy: session
|
||||
? {
|
||||
userId: session.userId,
|
||||
username: session.username,
|
||||
role: session.role,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
const claimToken = String(
|
||||
effectiveTask.primary_claim_token || effectiveTask.claim_token || '',
|
||||
).trim()
|
||||
const response: AdminTaskActionResponse = {
|
||||
task: mapTaskActionPayload(effectiveTask),
|
||||
}
|
||||
|
||||
if (claimToken) {
|
||||
response.claimUrl = buildClaimUrl(claimToken)
|
||||
response.token = claimToken
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export async function regenerateAdminTaskClaimLink(
|
||||
taskId: AdminEntityIdInput,
|
||||
session: AdminViewerSessionInput | null = null,
|
||||
): Promise<AdminTaskActionResponse> {
|
||||
const task = await getRequiredTask(taskId)
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
|
||||
|
||||
if (isManualDispatchTask(task)) {
|
||||
throw createHttpError('人工履约任务不需要领取链接,请直接回写人工履约结果', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_manual_dispatch_claim_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
if (!canRegenerateClaimLinkStatus(task.task_status)) {
|
||||
throw createHttpError('当前任务状态不允许重新生成领取链接', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_regenerate_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
if (!viewerContext.canManageTaskLifecycle) {
|
||||
throw createHttpError('当前账号无权重发领取链接', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_task_regenerate_permission_denied',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
if (primaryClaimTokenId) {
|
||||
await updateClaimToken(primaryClaimTokenId, {
|
||||
status: 'revoked',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = await createTaskClaimToken(task.id)
|
||||
const nextStatus =
|
||||
task.task_status === TASK_STATUS.MANUAL_REVIEW || task.task_status === TASK_STATUS.RETRY_PENDING
|
||||
? TASK_STATUS.LINK_GENERATED
|
||||
: task.task_status === TASK_STATUS.PAID
|
||||
? TASK_STATUS.LINK_GENERATED
|
||||
: task.task_status
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
claim_token: claimToken.token,
|
||||
claim_expires_at: claimToken.expired_at,
|
||||
task_status: nextStatus,
|
||||
user_action_status: 'pending_claim',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
if (!updatedTask) {
|
||||
throw createHttpError('重新生成领取链接失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'admin_task_regenerate_failed',
|
||||
})
|
||||
}
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'task_regenerate_claim_link',
|
||||
{
|
||||
regeneratedBy: session
|
||||
? {
|
||||
userId: session.userId,
|
||||
username: session.username,
|
||||
role: session.role,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
claimUrl: claimToken.claimUrl,
|
||||
token: claimToken.token,
|
||||
}
|
||||
}
|
||||
|
||||
export async function completeAdminTaskManualDispatch(
|
||||
taskId: AdminEntityIdInput,
|
||||
payload: AdminTaskManualDispatchInput = {},
|
||||
session: AdminViewerSessionInput | null = null,
|
||||
): Promise<AdminTaskManualDispatchResponse> {
|
||||
const task = await getRequiredTask(taskId)
|
||||
|
||||
if (
|
||||
!isManualDispatchTask(task) &&
|
||||
!isRetiredFulfillmentExecutor(task.executor_key)
|
||||
) {
|
||||
throw createHttpError('当前任务不是人工履约任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_not_manual_dispatch',
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
task.task_status === TASK_STATUS.REDEEMED ||
|
||||
task.task_status === TASK_STATUS.CLOSED ||
|
||||
task.task_status === TASK_STATUS.COMPLETED
|
||||
) {
|
||||
throw createHttpError('当前任务已经完结,不能重复回写人工履约结果', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_manual_dispatch_already_completed',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const outcome = normalizeManualDispatchOutcome(payload.outcome)
|
||||
const resultMessage = String(payload.resultMessage || '').trim()
|
||||
const deliveryReference = String(payload.deliveryReference || '').trim()
|
||||
const deliveredCredential = String(payload.deliveredCredential || '').trim()
|
||||
const context = parseTaskContext(task)
|
||||
const resultCode = outcome === 'failed' ? 'manual_dispatch_failed' : 'manual_dispatch_delivered'
|
||||
const fallbackMessage = outcome === 'failed' ? '人工履约失败' : '人工履约已完成'
|
||||
const nextTaskStatus = outcome === 'failed' ? TASK_STATUS.CLOSED : TASK_STATUS.REDEEMED
|
||||
const nextDeliveryStatus = outcome === 'failed' ? 'failed' : 'delivered'
|
||||
const manualDispatch = {
|
||||
outcome,
|
||||
deliveryReference,
|
||||
deliveredCredential,
|
||||
resultMessage: resultMessage || fallbackMessage,
|
||||
completedAt: now,
|
||||
completedBy: session
|
||||
? {
|
||||
userId: Number(session.userId || 0),
|
||||
username: String(session.username || ''),
|
||||
role: String(session.role || ''),
|
||||
}
|
||||
: null,
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: nextTaskStatus,
|
||||
delivery_status: nextDeliveryStatus,
|
||||
result_code: resultCode,
|
||||
result_message: resultMessage || fallbackMessage,
|
||||
user_action_status: 'not_required',
|
||||
last_error: outcome === 'failed' ? resultMessage || fallbackMessage : '',
|
||||
redeemed_at: outcome === 'delivered' ? now : task.redeemed_at || null,
|
||||
context_json: JSON.stringify({
|
||||
...context,
|
||||
manualDispatch,
|
||||
}),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
if (!updatedTask) {
|
||||
throw createHttpError('回写人工履约结果失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'admin_task_manual_dispatch_failed',
|
||||
})
|
||||
}
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'manual_dispatch_completed',
|
||||
{
|
||||
outcome,
|
||||
resultCode,
|
||||
resultMessage: resultMessage || fallbackMessage,
|
||||
deliveryReference,
|
||||
deliveredCredentialMasked: maskCode(deliveredCredential),
|
||||
completedBy: manualDispatch.completedBy,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
outcome,
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,8 @@ export default function AdminTasksPage() {
|
||||
const canRetry =
|
||||
canManageTaskLifecycle &&
|
||||
row.executorKey !== 'manual_dispatch' &&
|
||||
row.executorKey !== 'kuaishou_ct_assisted' &&
|
||||
row.executorKey !== 'kuaishou_feifei' &&
|
||||
['retry_pending', 'manual_review'].includes(row.status)
|
||||
const canManualReview =
|
||||
canManageTaskLifecycle &&
|
||||
|
||||
Reference in New Issue
Block a user