优化订单履约后台界面

This commit is contained in:
yml2213
2026-07-09 09:51:54 +08:00
parent 9a24268b5a
commit 71602683d2
5 changed files with 702 additions and 75 deletions
@@ -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<AdminTaskDetail['kuaishouIndustryVoucher']>
type ManualDispatchForm = {
deliveryReference: string
deliveredCredential: string
@@ -252,6 +255,12 @@ export default function AdminTaskDetailPage() {
}
/>
<FulfillmentOverviewPanel
detail={resolvedDetail}
claimUrl={claimUrl}
claimLinkInvalid={claimLinkInvalid}
/>
<TaskActionPanel
detail={resolvedDetail}
canManageTaskLifecycle={canManageTaskLifecycle}
@@ -297,6 +306,22 @@ export default function AdminTaskDetailPage() {
`确认退还任务 ${resolvedDetail.task.taskNo} 当前使用的虚拟号吗?退号后该流程会正式收口。`,
)
}
onResendIndustryVoucher={() =>
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() {
<Empty description="无快手 Cloud 履约上下文" />
),
},
{
key: 'industry-voucher',
label: '电子凭证',
children: resolvedDetail.kuaishouIndustryVoucher ? (
<KuaishouIndustryVoucherPanel voucher={resolvedDetail.kuaishouIndustryVoucher} />
) : (
<Empty description="无电子凭证绑定" />
),
},
{
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 (
<Card title="任务操作">
<Space wrap>
<div className="task-action-groups">
{canManageTaskLifecycle ? (
<Button
type="primary"
icon={<ReloadOutlined />}
disabled={loading || !operations.canRetry}
loading={actionLoadingKey === 'retry'}
onClick={onRetry}
>
</Button>
<section className="task-action-group">
<Typography.Text type="secondary"></Typography.Text>
<Space wrap>
<Button
type="primary"
icon={<ReloadOutlined />}
disabled={loading || !operations.canRetry}
loading={actionLoadingKey === 'retry'}
onClick={onRetry}
>
</Button>
<Button
icon={<UserSwitchOutlined />}
disabled={loading || !operations.canMarkManualReview}
loading={actionLoadingKey === 'manual-review'}
onClick={onMarkManualReview}
>
</Button>
</Space>
</section>
) : null}
{operations.canRebindKuaishouCloudRole ? (
<Button
icon={<SwapOutlined />}
disabled={loading}
loading={actionLoadingKey === 'rebind-role'}
onClick={onRebindRole}
>
</Button>
{hasCloudActions ? (
<section className="task-action-group">
<Typography.Text type="secondary"> Cloud</Typography.Text>
<Space wrap>
{canManageTaskLifecycle && operations.canPrepareKuaishouCloudFulfillment ? (
<Button
type="primary"
ghost
icon={<ToolOutlined />}
disabled={loading}
loading={actionLoadingKey === 'prepare-kuaishou-cloud'}
onClick={onPrepareKuaishouCloud}
>
</Button>
) : null}
{operations.canRebindKuaishouCloudRole ? (
<Button
icon={<SwapOutlined />}
disabled={loading}
loading={actionLoadingKey === 'rebind-role'}
onClick={onRebindRole}
>
</Button>
) : null}
{canManageTaskLifecycle && operations.canDispatchKuaishouCloudFulfillment ? (
<Button
type="primary"
icon={<SendOutlined />}
disabled={loading}
loading={actionLoadingKey === 'dispatch-kuaishou-cloud'}
onClick={onDispatchKuaishouCloud}
>
</Button>
) : null}
{canManageTaskLifecycle && operations.canReturnKuaishouCloudFulfillment ? (
<Button
icon={<RollbackOutlined />}
disabled={loading}
loading={actionLoadingKey === 'return-kuaishou-cloud'}
onClick={onReturnKuaishouCloud}
>
退
</Button>
) : null}
</Space>
</section>
) : null}
{canManageTaskLifecycle && operations.canPrepareKuaishouCloudFulfillment ? (
<Button
type="primary"
ghost
icon={<ToolOutlined />}
disabled={loading}
loading={actionLoadingKey === 'prepare-kuaishou-cloud'}
onClick={onPrepareKuaishouCloud}
>
</Button>
) : null}
{canManageTaskLifecycle && operations.canDispatchKuaishouCloudFulfillment ? (
<Button
type="primary"
icon={<SendOutlined />}
disabled={loading}
loading={actionLoadingKey === 'dispatch-kuaishou-cloud'}
onClick={onDispatchKuaishouCloud}
>
</Button>
) : null}
{canManageTaskLifecycle && operations.canReturnKuaishouCloudFulfillment ? (
<Button
icon={<RollbackOutlined />}
disabled={loading}
loading={actionLoadingKey === 'return-kuaishou-cloud'}
onClick={onReturnKuaishouCloud}
>
退
</Button>
) : null}
{canManageTaskLifecycle ? (
<Button
icon={<UserSwitchOutlined />}
disabled={loading || !operations.canMarkManualReview}
loading={actionLoadingKey === 'manual-review'}
onClick={onMarkManualReview}
>
</Button>
{canManageTaskLifecycle && hasIndustryVoucherActions ? (
<section className="task-action-group">
<Typography.Text type="secondary"></Typography.Text>
<Space wrap>
<Button
icon={<ReloadOutlined />}
disabled={loading || !operations.canResendKuaishouIndustryVoucherCode}
loading={actionLoadingKey === 'resend-industry-voucher'}
onClick={onResendIndustryVoucher}
>
</Button>
<Button
type="primary"
ghost
icon={<CheckCircleOutlined />}
disabled={loading || !operations.canConsumeKuaishouIndustryVoucher}
loading={actionLoadingKey === 'consume-industry-voucher'}
onClick={onConsumeIndustryVoucher}
>
</Button>
</Space>
</section>
) : null}
{canCloseTasks ? (
<Button
danger
icon={<CloseCircleOutlined />}
disabled={loading || !operations.canClose}
loading={actionLoadingKey === 'close'}
onClick={onCloseTask}
>
</Button>
<section className="task-action-group">
<Typography.Text type="secondary"></Typography.Text>
<Button
danger
icon={<CloseCircleOutlined />}
disabled={loading || !operations.canClose}
loading={actionLoadingKey === 'close'}
onClick={onCloseTask}
>
</Button>
</section>
) : null}
{!canManageTaskLifecycle && !canCloseTasks && !operations.canRebindKuaishouCloudRole ? (
{!canManageTaskLifecycle && !canCloseTasks && !hasCloudActions ? (
<Typography.Text type="secondary"></Typography.Text>
) : null}
</Space>
</div>
{loading ? (
<Typography.Text className="task-action-hint" type="secondary">
@@ -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 (
<Card
title="履约链路总览"
extra={<Tag color={executor.color}>{executor.label}</Tag>}
>
<div className="fulfillment-overview-grid">
{steps.map((step) => (
<div key={step.key} className="fulfillment-step-card">
<Space size={[6, 6]} wrap>
<StatusTag value={step.status} />
<Typography.Text strong>{step.label}</Typography.Text>
</Space>
<Typography.Text type="secondary">{step.detail}</Typography.Text>
</div>
))}
</div>
<Alert
className="platform-section-gap"
type={currentBlocker.level}
showIcon
message={currentBlocker.title}
description={currentBlocker.detail}
/>
</Card>
)
}
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 (
<Card title="电子凭证">
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
<Descriptions.Item label="订单号">{voucher.oid || '-'}</Descriptions.Item>
<Descriptions.Item label="凭证码">{voucher.voucherCode || '-'}</Descriptions.Item>
<Descriptions.Item label="序号">{voucher.unitIndex || '-'}</Descriptions.Item>
<Descriptions.Item label="凭证状态">
<StatusTag value={voucher.status} />
</Descriptions.Item>
<Descriptions.Item label="发码回调">
<StatusTag value={voucher.sendCallbackStatus} />
</Descriptions.Item>
<Descriptions.Item label="发码次数">{voucher.sendCallbackAttemptCount}</Descriptions.Item>
<Descriptions.Item label="发码时间">
{formatAdminDateTime(voucher.sendCallbackSentAt)}
</Descriptions.Item>
<Descriptions.Item label="核销流水">
{voucher.consumeSerialNum || '-'}
</Descriptions.Item>
<Descriptions.Item label="核销时间">
{formatAdminDateTime(voucher.consumedAt)}
</Descriptions.Item>
<Descriptions.Item label="最近异常" span={3}>
{voucher.sendCallbackLastError || '-'}
</Descriptions.Item>
</Descriptions>
{voucher.sendCallbackLastError ? (
<Alert
className="platform-section-gap"
type="warning"
showIcon
message="电子凭证发码回调存在异常"
description={voucher.sendCallbackLastError}
/>
) : null}
{consumeDetails.length > 0 ? (
<Table<Record<string, unknown>>
className="platform-section-gap"
rowKey={(_, index) => String(index ?? 0)}
pagination={false}
dataSource={consumeDetails}
scroll={{ x: 760 }}
columns={[
{
title: '核销明细',
render: (_, row) => <JsonPreview value={row} />,
},
]}
/>
) : null}
</Card>
)
}
function KuaishouCloudPanel({
flow,
canRefresh,