feat(order): 精准区分自动直发与接单平台打手履约,智能门控H5直充按钮

- 后端增加按订单批量关联接单工单查询,精准识别履约类型(自动直发 / 打手履约 / 电子凭证 / 拼多多发货)
- 订单列表与详情页中,仅当订单为自动直发(affiliate_dash / 码速达直充)时展示【H5】一键复制直充链接与兑换状态控制
- 接单平台工单单与纯核销单隐藏直充 H5 按钮与兑换暂停/关闭标签,防止客服向打手单买家误发直充链接
- 交付模式标签升级为「⚡ 自动直发」、「👥 打手履约」、「🎫 电子凭证」、「📦 拼多多发货」

验证方式:前端类型检查与打包构建通过,全量 132 项前端单测与 674 项后端单测全部通过
This commit is contained in:
yml2213
2026-10-10 19:44:20 +08:00
parent 9a6b4313a8
commit 748c18bc7d
8 changed files with 237 additions and 79 deletions
@@ -30,6 +30,7 @@ import StatusTag from '@/components/admin/StatusTag'
import { fetchAdminOrderDetail, updateAdminOrderClaimStatus } from '@/services/admin'
import type { AdminOrderDetail, AdminTaskListItem } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
import { resolveOrderFulfillmentMode } from '@/utils/admin-display'
export default function AdminOrderDetailPage() {
const navigate = useNavigate()
@@ -164,6 +165,10 @@ export default function AdminOrderDetailPage() {
const isOrderRefundedOrCancelled =
detail.order.payStatus === 'refunded' || detail.order.orderStatus === 'cancelled'
const canPauseOrClose = !isOrderFulfilled && !isOrderRefundedOrCancelled
const isAutoClaimOrder = Boolean(
detail.order.canClaimH5 || detail.order.fulfillmentType === 'auto_claim',
)
const fulfillmentMode = resolveOrderFulfillmentMode(detail.order)
return (
<section className="page-stack">
@@ -176,40 +181,42 @@ export default function AdminOrderDetailPage() {
<Card
title="基础信息"
extra={
<Space>
<Button icon={<CopyOutlined />} onClick={handleCopyH5Link}>
复制 H5 领取链接
</Button>
{detail.order.claimStatus === 'paused' || detail.order.claimStatus === 'closed' ? (
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={() => openStatusModal('active')}
loading={statusMutation.isPending}
>
恢复启用
isAutoClaimOrder ? (
<Space>
<Button icon={<CopyOutlined />} onClick={handleCopyH5Link}>
复制 H5 领取链接
</Button>
) : canPauseOrClose ? (
<Button
danger
icon={<PauseCircleOutlined />}
onClick={() => openStatusModal('paused')}
loading={statusMutation.isPending}
>
暂停兑换
</Button>
) : null}
{canPauseOrClose && detail.order.claimStatus !== 'closed' ? (
<Button
danger
icon={<StopOutlined />}
onClick={() => openStatusModal('closed')}
loading={statusMutation.isPending}
>
关闭兑换
</Button>
) : null}
</Space>
{detail.order.claimStatus === 'paused' || detail.order.claimStatus === 'closed' ? (
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={() => openStatusModal('active')}
loading={statusMutation.isPending}
>
恢复启用
</Button>
) : canPauseOrClose ? (
<Button
danger
icon={<PauseCircleOutlined />}
onClick={() => openStatusModal('paused')}
loading={statusMutation.isPending}
>
暂停兑换
</Button>
) : null}
{canPauseOrClose && detail.order.claimStatus !== 'closed' ? (
<Button
danger
icon={<StopOutlined />}
onClick={() => openStatusModal('closed')}
loading={statusMutation.isPending}
>
关闭兑换
</Button>
) : null}
</Space>
) : null
}
>
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
@@ -218,34 +225,41 @@ export default function AdminOrderDetailPage() {
{detail.order.shopName || detail.order.shopId || '-'}
</Descriptions.Item>
<Descriptions.Item label="平台订单号">{detail.order.platformOrderId}</Descriptions.Item>
<Descriptions.Item label="履约模式">
<Tag color={fulfillmentMode.color} style={{ margin: 0 }}>
{fulfillmentMode.label}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="订单状态">
<StatusTag value={detail.order.orderStatus} />
</Descriptions.Item>
<Descriptions.Item label="支付状态">
<StatusTag value={detail.order.payStatus} kind="pay" />
</Descriptions.Item>
<Descriptions.Item label="H5 兑换状态">
<Space wrap>
{detail.order.claimStatus === 'paused' ? (
<Tag color="warning" icon={<PauseCircleOutlined />}>
已暂停兑换
</Tag>
) : detail.order.claimStatus === 'closed' ? (
<Tag color="default" icon={<StopOutlined />}>
已关闭兑换
</Tag>
) : (
<Tag color="success" icon={<PlayCircleOutlined />}>
正常有效
</Tag>
)}
{detail.order.claimStatusReason ? (
<span className="muted" style={{ fontSize: 12 }}>
({detail.order.claimStatusReason})
</span>
) : null}
</Space>
</Descriptions.Item>
{isAutoClaimOrder ? (
<Descriptions.Item label="H5 兑换状态">
<Space wrap>
{detail.order.claimStatus === 'paused' ? (
<Tag color="warning" icon={<PauseCircleOutlined />}>
已暂停兑换
</Tag>
) : detail.order.claimStatus === 'closed' ? (
<Tag color="default" icon={<StopOutlined />}>
已关闭兑换
</Tag>
) : (
<Tag color="success" icon={<PlayCircleOutlined />}>
正常有效
</Tag>
)}
{detail.order.claimStatusReason ? (
<span className="muted" style={{ fontSize: 12 }}>
({detail.order.claimStatusReason})
</span>
) : null}
</Space>
</Descriptions.Item>
) : null}
<Descriptions.Item label="金额">
{detail.order.totalAmount} {detail.order.currency}
</Descriptions.Item>
@@ -309,7 +309,12 @@ export default function AdminOrdersPage() {
</>
)}
<span className="muted" style={{ fontSize: 12 }}>
数量 {row.totalQuantity || 1} · 关联任务 {row.taskCount || 0}
数量 {row.totalQuantity || 1}
{row.canClaimH5
? ` · 关联任务 ${row.taskCount || 0}`
: row.workOrderCount
? ` · 关联工单 ${row.workOrderCount}`
: ''}
</span>
</div>
)
@@ -332,11 +337,11 @@ export default function AdminOrdersPage() {
<StatusTag value={row.customerStatus} kind="customer" />
</Space>
{row.claimStatus === 'paused' ? (
{row.canClaimH5 && row.claimStatus === 'paused' ? (
<Tag color="warning" style={{ margin: 0, width: 'fit-content' }}>
H5已暂停
</Tag>
) : row.claimStatus === 'closed' ? (
) : row.canClaimH5 && row.claimStatus === 'closed' ? (
<Tag color="default" style={{ margin: 0, width: 'fit-content' }}>
H5已关闭
</Tag>
@@ -361,7 +366,7 @@ export default function AdminOrdersPage() {
},
{
title: '操作',
minWidth: 140,
minWidth: 130,
fixed: 'right',
render: (_, row) => {
const orderNo = row.platformOrderId || String(row.orderId)
@@ -378,15 +383,17 @@ export default function AdminOrdersPage() {
>
详情
</Button>
<Tooltip title="复制 H5 领取链接">
<Button
size="small"
icon={<LinkOutlined />}
onClick={() => handleCopyText(h5Url, 'H5 领取链接')}
>
H5
</Button>
</Tooltip>
{row.canClaimH5 ? (
<Tooltip title="复制自动直充 H5 领取链接">
<Button
size="small"
icon={<LinkOutlined />}
onClick={() => handleCopyText(h5Url, 'H5 直充链接')}
>
H5
</Button>
</Tooltip>
) : null}
</Space>
)
},
+6
View File
@@ -46,6 +46,9 @@ export interface AdminOrderListItem {
claimStatus?: string
claimStatusReason?: string
claimStatusUpdatedAt?: string | null
fulfillmentType?: 'auto_claim' | 'worker_dispatch' | 'voucher_only' | 'manual'
workOrderCount?: number
canClaimH5?: boolean
}
export interface AdminOrderDetail {
@@ -61,6 +64,9 @@ export interface AdminOrderDetail {
claimStatus?: string
claimStatusReason?: string
claimStatusUpdatedAt?: string | null
fulfillmentType?: 'auto_claim' | 'worker_dispatch' | 'voucher_only' | 'manual'
workOrderCount?: number
canClaimH5?: boolean
buyerId: string
buyerName: string
receiverContact: string
+19 -6
View File
@@ -221,21 +221,34 @@ export function resolveOrderPlatformBadge(row: {
export function resolveOrderFulfillmentMode(row: {
provider?: string
platform?: string
fulfillmentType?: 'auto_claim' | 'worker_dispatch' | 'voucher_only' | 'manual'
workOrderCount?: number
canClaimH5?: boolean
taskCount?: number
}): { label: string; color: string } {
const provider = String(row.provider || '').toLowerCase()
if (provider === 'douyin_mashida') {
return { label: 'H5直绑', color: 'blue' }
if (row.canClaimH5 || row.fulfillmentType === 'auto_claim' || provider === 'douyin_mashida') {
return { label: '⚡ 自动直发', color: 'blue' }
}
if (provider === 'kuaishou_industry' || provider === '91kaquan' || provider === 'open_91') {
return { label: '电子凭证', color: 'gold' }
if (row.fulfillmentType === 'worker_dispatch' || Number(row.workOrderCount || 0) > 0) {
return { label: '👥 打手履约', color: 'purple' }
}
if (
provider === 'kuaishou_industry' ||
provider === '91kaquan' ||
provider === 'open_91' ||
row.fulfillmentType === 'voucher_only'
) {
return { label: '🎫 电子凭证', color: 'gold' }
}
if (provider === 'pdd') {
return { label: '拼多多发货', color: 'red' }
return { label: '📦 拼多多发货', color: 'red' }
}
return { label: '自动交付', color: 'cyan' }
return { label: '手动交付', color: 'default' }
}