diff --git a/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx b/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx index 1e89fa9e..4c8ad1c6 100644 --- a/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx +++ b/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx @@ -37,6 +37,7 @@ import StatusTag from '@/components/admin/StatusTag' import { closeAdminTask, completeAdminTaskManualDispatch, + consumeAdminTaskKuaishouIndustryVoucher, dispatchAdminTaskKuaishouCloudFulfillment, fetchAdminTaskDetail, fetchAdminTaskScreenshot, @@ -44,6 +45,7 @@ import { prepareAdminTaskKuaishouCloudFulfillment, rebindAdminTaskKuaishouCloudRole, refreshAdminTaskKuaishouCloudRoleInfo, + resendAdminTaskKuaishouIndustryVoucherCode, retryAdminTask, returnNumberAdminTaskKuaishouCloudFulfillment, } from '@/services/admin' @@ -59,6 +61,7 @@ import { import { formatAdminDateTime } from '@/utils/admin-time' type TaskEvent = AdminTaskDetail['events'][number] +type KuaishouIndustryVoucher = NonNullable type ManualDispatchForm = { deliveryReference: string deliveredCredential: string @@ -252,6 +255,12 @@ export default function AdminTaskDetailPage() { } /> + + + runTaskAction( + 'resend-industry-voucher', + () => resendAdminTaskKuaishouIndustryVoucherCode(resolvedDetail.task.taskId), + '电子凭证发码回调已重发', + `确认重新发送任务 ${resolvedDetail.task.taskNo} 的电子凭证发码回调吗?`, + ) + } + onConsumeIndustryVoucher={() => + runTaskAction( + 'consume-industry-voucher', + () => consumeAdminTaskKuaishouIndustryVoucher(resolvedDetail.task.taskId), + '电子凭证已执行核销', + `确认立即核销任务 ${resolvedDetail.task.taskNo} 绑定的电子凭证吗?`, + ) + } onMarkManualReview={() => runTaskAction( 'manual-review', @@ -405,6 +430,15 @@ export default function AdminTaskDetailPage() { ), }, + { + key: 'industry-voucher', + label: '电子凭证', + children: resolvedDetail.kuaishouIndustryVoucher ? ( + + ) : ( + + ), + }, { key: 'resolution', label: '兑换重试', @@ -457,6 +491,8 @@ function TaskActionPanel({ onPrepareKuaishouCloud, onDispatchKuaishouCloud, onReturnKuaishouCloud, + onResendIndustryVoucher, + onConsumeIndustryVoucher, onMarkManualReview, onCloseTask, }: { @@ -469,94 +505,147 @@ function TaskActionPanel({ onPrepareKuaishouCloud: () => void onDispatchKuaishouCloud: () => void onReturnKuaishouCloud: () => void + onResendIndustryVoucher: () => void + onConsumeIndustryVoucher: () => void onMarkManualReview: () => void onCloseTask: () => void }) { const operations = detail.operations const loading = Boolean(actionLoadingKey) + const hasCloudActions = + operations.canPrepareKuaishouCloudFulfillment || + operations.canRebindKuaishouCloudRole || + operations.canDispatchKuaishouCloudFulfillment || + operations.canReturnKuaishouCloudFulfillment + const hasIndustryVoucherActions = + operations.canResendKuaishouIndustryVoucherCode || + operations.canConsumeKuaishouIndustryVoucher || + Boolean(detail.kuaishouIndustryVoucher) return ( - +
{canManageTaskLifecycle ? ( - +
+ 通用 + + + + +
) : null} - {operations.canRebindKuaishouCloudRole ? ( - + + {hasCloudActions ? ( +
+ 快手 Cloud + + {canManageTaskLifecycle && operations.canPrepareKuaishouCloudFulfillment ? ( + + ) : null} + {operations.canRebindKuaishouCloudRole ? ( + + ) : null} + {canManageTaskLifecycle && operations.canDispatchKuaishouCloudFulfillment ? ( + + ) : null} + {canManageTaskLifecycle && operations.canReturnKuaishouCloudFulfillment ? ( + + ) : null} + +
) : null} - {canManageTaskLifecycle && operations.canPrepareKuaishouCloudFulfillment ? ( - - ) : null} - {canManageTaskLifecycle && operations.canDispatchKuaishouCloudFulfillment ? ( - - ) : null} - {canManageTaskLifecycle && operations.canReturnKuaishouCloudFulfillment ? ( - - ) : null} - {canManageTaskLifecycle ? ( - + + {canManageTaskLifecycle && hasIndustryVoucherActions ? ( +
+ 电子凭证 + + + + +
) : null} + {canCloseTasks ? ( - +
+ 收口 + +
) : null} - {!canManageTaskLifecycle && !canCloseTasks && !operations.canRebindKuaishouCloudRole ? ( + + {!canManageTaskLifecycle && !canCloseTasks && !hasCloudActions ? ( 当前账号没有可执行的任务操作 ) : null} - +
{loading ? ( 操作执行中,请稍候。 @@ -566,6 +655,236 @@ function TaskActionPanel({ ) } +function FulfillmentOverviewPanel({ + detail, + claimUrl, + claimLinkInvalid, +}: { + detail: AdminTaskDetail + claimUrl: string + claimLinkInvalid: boolean +}) { + const executor = resolveTaskExecutorDisplay(detail.task.executorKey) + const steps = buildFulfillmentOverviewSteps(detail, claimUrl, claimLinkInvalid) + const currentBlocker = resolveTaskBlocker(detail.task, claimUrl, claimLinkInvalid) + + return ( + {executor.label}} + > +
+ {steps.map((step) => ( +
+ + + {step.label} + + {step.detail} +
+ ))} +
+ + +
+ ) +} + +function buildFulfillmentOverviewSteps( + detail: AdminTaskDetail, + claimUrl: string, + claimLinkInvalid: boolean, +) { + const task = detail.task + const executor = resolveTaskExecutorDisplay(task.executorKey) + const voucher = detail.kuaishouIndustryVoucher + const manualDispatch = detail.manualDispatch + const claimStatus = claimUrl + ? 'success' + : claimLinkInvalid + ? 'failed' + : detail.claimToken?.status || 'pending' + + return [ + { + key: 'source', + label: '订单来源', + status: detail.order?.payStatus || task.status, + detail: `${detail.order?.provider || '-'} / ${detail.order?.platform || '-'} / ${task.platformOrderId || '-'}`, + }, + { + key: 'product', + label: '商品解析', + status: detail.orderItem || task.skuName ? 'success' : 'pending_config', + detail: detail.orderItem?.skuName || task.skuName || task.skuCode || '未命中履约商品', + }, + { + key: 'planner', + label: '履约计划', + status: task.executorKey ? 'success' : 'pending_config', + detail: executor.label, + }, + { + key: 'prepare', + label: '任务准备', + status: task.resourceStatus || task.status, + detail: resolveTaskPrepareSummary(detail), + }, + { + key: 'delivery', + label: '平台执行', + status: resolveDeliveryStepStatus(detail), + detail: resolveDeliveryStepSummary(detail), + }, + { + key: 'voucher', + label: '电子凭证', + status: voucher ? voucher.sendCallbackStatus || voucher.status : 'skipped', + detail: voucher + ? `发码 ${voucher.sendCallbackStatus || '-'} / 凭证 ${voucher.status || '-'}` + : '当前任务未绑定电子凭证', + }, + { + key: 'claim', + label: '交付链接', + status: task.executorKey === 'manual_dispatch' ? 'skipped' : claimStatus, + detail: + task.executorKey === 'manual_dispatch' + ? '人工履约不需要客户领取链接' + : claimUrl + ? '已有可用链接' + : claimLinkInvalid + ? '链接已失效' + : '等待生成或同步链接', + }, + { + key: 'finish', + label: '履约收口', + status: manualDispatch?.outcome || task.deliveryStatus || task.status, + detail: task.resultMessage || task.lastError || '等待履约完成', + }, + ] +} + +function resolveTaskBlocker( + task: AdminTaskDetail['task'], + claimUrl: string, + claimLinkInvalid: boolean, +): { level: 'success' | 'info' | 'warning' | 'error'; title: string; detail: string } { + if (['completed', 'redeemed', 'delivered'].includes(task.status) || task.deliveryStatus === 'delivered') { + return { + level: 'success', + title: '履约已完成', + detail: task.resultMessage || '任务已经收口。', + } + } + + if (['manual_review', 'retry_pending', 'failed'].includes(task.status)) { + return { + level: 'error', + title: '当前卡在异常处理', + detail: task.lastError || task.resultMessage || '需要人工检查后重试或收口。', + } + } + + if (claimLinkInvalid) { + return { + level: 'warning', + title: '领取链接不可用', + detail: '链接状态不是 active,可重新生成或检查平台交付链接。', + } + } + + if (!claimUrl && task.executorKey !== 'manual_dispatch') { + return { + level: 'warning', + title: '等待交付链接', + detail: '任务尚未拿到可交付链接,先检查任务准备和平台执行状态。', + } + } + + if (task.customerStatus === 'waiting_customer') { + return { + level: 'info', + title: '等待客户操作', + detail: '资源已准备,客户还没有完成绑定或领取动作。', + } + } + + return { + level: 'info', + title: '履约推进中', + detail: task.lastError || '当前任务没有明确异常,可按链路状态继续观察。', + } +} + +function resolveTaskPrepareSummary(detail: AdminTaskDetail) { + const flow = detail.kuaishouCloudFulfillment + if (flow) { + return flow.binding.bindPreparedAt + ? `绑定资源 ${flow.binding.vnPhone || flow.binding.vnId || '-'}` + : '等待准备 Cloud 资源' + } + + if (detail.task.executorKey === 'manual_dispatch') { + return '人工履约任务无需自动准备资源' + } + + return detail.task.resultMessage || detail.task.lastError || '等待执行器准备' +} + +function resolveDeliveryStepStatus(detail: AdminTaskDetail) { + const flow = detail.kuaishouCloudFulfillment + if (flow) { + return flow.dispatch.status || detail.task.deliveryStatus || detail.task.status + } + + if (detail.manualDispatch) { + return detail.manualDispatch.outcome || detail.task.deliveryStatus + } + + return detail.task.deliveryStatus || detail.task.status +} + +function resolveDeliveryStepSummary(detail: AdminTaskDetail) { + const flow = detail.kuaishouCloudFulfillment + if (flow) { + if (flow.dispatch.dispatchAt) { + return `已于 ${formatAdminDateTime(flow.dispatch.dispatchAt)} 发货` + } + return flow.dispatch.note || '等待平台发货执行' + } + + if (detail.manualDispatch) { + return detail.manualDispatch.resultMessage || detail.manualDispatch.deliveryReference || '人工履约已回写' + } + + return detail.task.resultMessage || detail.task.lastError || '等待执行器推进' +} + +function resolveTaskExecutorDisplay(value: unknown) { + const executorKey = String(value || '').trim() + if (executorKey === 'kuaishou_ct_assisted') { + return { label: '快手 Cloud', color: 'blue' } + } + if (executorKey === 'kuaishou-industry') { + return { label: '行业电子凭证', color: 'cyan' } + } + if (executorKey === 'kuaishou_feifei') { + return { label: 'kuaishou-feifei', color: 'purple' } + } + if (executorKey === 'manual_dispatch') { + return { label: '人工履约', color: 'orange' } + } + return { label: executorKey || '未分配执行器', color: 'default' } +} + function ManualDispatchPanel({ detail, form, @@ -680,6 +999,65 @@ function ManualDispatchPanel({ ) } +function KuaishouIndustryVoucherPanel({ voucher }: { voucher: KuaishouIndustryVoucher }) { + const consumeDetails = Array.isArray(voucher.consumeDetails) ? voucher.consumeDetails : [] + + return ( + + + {voucher.oid || '-'} + {voucher.voucherCode || '-'} + {voucher.unitIndex || '-'} + + + + + + + {voucher.sendCallbackAttemptCount} + + {formatAdminDateTime(voucher.sendCallbackSentAt)} + + + {voucher.consumeSerialNum || '-'} + + + {formatAdminDateTime(voucher.consumedAt)} + + + {voucher.sendCallbackLastError || '-'} + + + + {voucher.sendCallbackLastError ? ( + + ) : null} + + {consumeDetails.length > 0 ? ( + > + className="platform-section-gap" + rowKey={(_, index) => String(index ?? 0)} + pagination={false} + dataSource={consumeDetails} + scroll={{ x: 760 }} + columns={[ + { + title: '核销明细', + render: (_, row) => , + }, + ]} + /> + ) : null} + + ) +} + function KuaishouCloudPanel({ flow, canRefresh, diff --git a/apps/frontend/src/pages/admin/AdminTasksPage.tsx b/apps/frontend/src/pages/admin/AdminTasksPage.tsx index 417e7c01..21e3ac1d 100644 --- a/apps/frontend/src/pages/admin/AdminTasksPage.tsx +++ b/apps/frontend/src/pages/admin/AdminTasksPage.tsx @@ -5,7 +5,7 @@ import { SwapOutlined, UserSwitchOutlined, } from '@ant-design/icons' -import { Alert, App, Button, Card, DatePicker, Form, Input, Select, Space, Table, Typography } from 'antd' +import { Alert, App, Button, Card, DatePicker, Form, Input, Select, Space, Table, Tag, Typography } from 'antd' import type { TableColumnsType } from 'antd' import { useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' @@ -36,6 +36,12 @@ type TaskFilterForm = { } type TaskListAction = 'retry' | 'rebind_role' | 'manual_review' | 'close' +type TaskListBlocker = { + label: string + color: string + level: 'normal' | 'error' + detail: string +} export default function AdminTasksPage() { const navigate = useNavigate() @@ -100,12 +106,38 @@ export default function AdminTasksPage() { ), }, + { + title: '履约 / 卡点', + minWidth: 260, + render: (_, row) => { + const executor = resolveTaskExecutorDisplay(row.executorKey) + const blocker = resolveTaskListBlocker(row) + + return ( +
+ + {executor.label} + {blocker.label} + + + {blocker.detail} + +
+ ) + }, + }, { title: '状态', - minWidth: 250, + minWidth: 280, render: (_, row) => ( + @@ -364,7 +396,7 @@ export default function AdminTasksPage() { loading={query.isLoading || query.isFetching} columns={columns} dataSource={data?.items || []} - scroll={{ x: 1240 }} + scroll={{ x: 1500 }} pagination={{ current: data?.pagination.page || page, pageSize: data?.pagination.pageSize || pageSize, @@ -389,6 +421,133 @@ function canRebindListTask(item: AdminTaskListItem, canManageTaskLifecycle: bool ) } +function resolveTaskExecutorDisplay(value: unknown) { + const executorKey = String(value || '').trim() + if (executorKey === 'kuaishou_ct_assisted') { + return { label: '快手 Cloud', color: 'blue' } + } + if (executorKey === 'kuaishou-industry') { + return { label: '行业电子凭证', color: 'cyan' } + } + if (executorKey === 'kuaishou_feifei') { + return { label: 'kuaishou-feifei', color: 'purple' } + } + if (executorKey === 'manual_dispatch') { + return { label: '人工履约', color: 'orange' } + } + return { label: executorKey || '未分配执行器', color: 'default' } +} + +function resolveTaskListBlocker(item: AdminTaskListItem): TaskListBlocker { + const taskStatus = normalizeTaskListStatus(item.status) + const deliveryStatus = normalizeTaskListStatus(item.deliveryStatus) + const resourceStatus = normalizeTaskListStatus(item.resourceStatus) + const customerStatus = normalizeTaskListStatus(item.customerStatus) + const errorText = item.lastError || item.resultMessage + + if ( + ['completed', 'redeemed'].includes(taskStatus) || + ['delivered', 'completed', 'success', 'consumed'].includes(deliveryStatus) + ) { + return { + label: '已收口', + color: 'green', + level: 'normal', + detail: item.resultMessage || '履约链路已完成。', + } + } + + if (taskStatus === 'closed') { + return { + label: '已关闭', + color: 'default', + level: 'normal', + detail: item.resultMessage || item.lastError || '任务已关闭,不会继续自动推进。', + } + } + + if ( + item.lastError || + ['manual_review', 'retry_pending', 'failed'].includes(taskStatus) || + ['resource_exception', 'binding_exception', 'failed'].includes(resourceStatus) || + ['customer_exception', 'failed'].includes(customerStatus) || + ['failed'].includes(deliveryStatus) + ) { + return { + label: '异常处理', + color: 'red', + level: 'error', + detail: errorText || '需要进入详情页按履约层级检查并重试。', + } + } + + if (item.executorKey === 'manual_dispatch') { + return { + label: '人工履约', + color: 'orange', + level: 'normal', + detail: item.resultMessage || '等待人工回写履约结果。', + } + } + + if (['pending_config'].includes(resourceStatus) || !item.executorKey) { + return { + label: '待配置', + color: 'gold', + level: 'normal', + detail: item.resultMessage || '商品还没有命中明确的履约配置。', + } + } + + if ( + ['not_started', 'pending', 'pending_prepare', 'pending_binding', 'pending_binding_prepare'].includes( + resourceStatus, + ) + ) { + return { + label: '资源层', + color: 'gold', + level: 'normal', + detail: item.resultMessage || '等待准备履约资源,可在详情页触发对应执行器动作。', + } + } + + if ( + ['waiting_customer', 'waiting_binding', 'waiting_user_claim', 'user_binding'].includes( + customerStatus, + ) + ) { + return { + label: '客户层', + color: 'blue', + level: 'normal', + detail: item.roleConfirmedAt + ? '客户已提交角色信息,等待平台履约收口。' + : '等待客户打开链接、绑定或提交信息。', + } + } + + if (deliveryStatus && !['pending', 'not_started'].includes(deliveryStatus)) { + return { + label: '平台层', + color: 'cyan', + level: 'normal', + detail: item.resultMessage || '平台履约执行中,后续可在详情页按层查看。', + } + } + + return { + label: '推进中', + color: 'blue', + level: 'normal', + detail: item.resultMessage || '当前没有明确异常,等待任务自动推进。', + } +} + +function normalizeTaskListStatus(value: unknown) { + return String(value || '').trim().toLowerCase() +} + function cleanParams(params: Record) { const next = new URLSearchParams() Object.entries(params).forEach(([key, value]) => { diff --git a/apps/frontend/src/services/admin/tasks.ts b/apps/frontend/src/services/admin/tasks.ts index 4e715205..eb643e0d 100644 --- a/apps/frontend/src/services/admin/tasks.ts +++ b/apps/frontend/src/services/admin/tasks.ts @@ -91,3 +91,26 @@ export function returnNumberAdminTaskKuaishouCloudFulfillment(taskId: number | s {}, ) } + +export function resendAdminTaskKuaishouIndustryVoucherCode(taskId: number | string) { + return apiPost( + `/api/v1/admin/tasks/${taskId}/kuaishou-industry/resend-code`, + {}, + ) +} + +export function consumeAdminTaskKuaishouIndustryVoucher( + taskId: number | string, + payload: { + consumeType?: string + storeName?: string + storeAddress?: string + expressCode?: string + expressNo?: string + } = {}, +) { + return apiPost( + `/api/v1/admin/tasks/${taskId}/kuaishou-industry/consume`, + payload, + ) +} diff --git a/apps/frontend/src/styles/main.css b/apps/frontend/src/styles/main.css index 1caa8482..789026eb 100644 --- a/apps/frontend/src/styles/main.css +++ b/apps/frontend/src/styles/main.css @@ -286,6 +286,7 @@ select { .cell-stack { display: grid; gap: 4px; + min-width: 0; } .muted { @@ -310,6 +311,55 @@ select { margin-top: 10px; } +.task-action-groups { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; + align-items: start; +} + +.task-action-group { + min-width: 0; + min-height: 86px; + padding: 12px; + border: 1px solid #edf0f5; + border-radius: 6px; + background: #f8fafc; + display: grid; + gap: 8px; + align-content: start; +} + +.task-action-group > .ant-typography { + font-size: 13px; + font-weight: 700; +} + +.task-list-blocker-detail { + max-width: 240px; +} + +.fulfillment-overview-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.fulfillment-step-card { + min-width: 0; + padding: 12px; + border: 1px solid #edf0f5; + border-radius: 6px; + background: #ffffff; + display: grid; + gap: 8px; + align-content: start; +} + +.fulfillment-step-card .ant-typography { + min-width: 0; +} + .manual-dispatch-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -1003,6 +1053,7 @@ select { } .manual-dispatch-grid, + .fulfillment-overview-grid, .task-flow-grid { grid-template-columns: 1fr; } diff --git a/apps/frontend/src/types/admin/tasks.ts b/apps/frontend/src/types/admin/tasks.ts index 20193cb5..23b58c7a 100644 --- a/apps/frontend/src/types/admin/tasks.ts +++ b/apps/frontend/src/types/admin/tasks.ts @@ -37,6 +37,8 @@ export interface AdminTaskOperations { canRebindKuaishouCloudRole: boolean canDispatchKuaishouCloudFulfillment: boolean canReturnKuaishouCloudFulfillment: boolean + canResendKuaishouIndustryVoucherCode?: boolean + canConsumeKuaishouIndustryVoucher?: boolean canViewSensitiveTaskData: boolean } @@ -201,6 +203,20 @@ export interface AdminTaskDetail { } notes: string } + kuaishouIndustryVoucher?: null | { + oid: string + voucherCode: string + unitIndex: number + status: string + sendCallbackStatus: string + sendCallbackLastError: string + sendCallbackAttemptCount: number + sendCallbackSentAt: string | null + consumeSerialNum: string + consumeDetails: Array> + consumedAt: string | null + destroyedAt: string | null + } manualDispatch: null | { outcome: string deliveryReference: string