完善发货链接签名和作废恢复

This commit is contained in:
yml2213
2026-07-31 11:43:13 +08:00
parent 6ca226ce4b
commit e4c1306216
17 changed files with 527 additions and 109 deletions
+26 -11
View File
@@ -8,6 +8,7 @@ import type {
CreateTestOrderResult,
DeliveryBindResult,
DeliveryOrderInfo,
DeliveryLinkResult,
DeliverySubmitResult,
FulfillmentOrder,
LoginResult,
@@ -68,6 +69,12 @@ export const merchantApi = {
note?: string
fulfillment_status?: 'pending' | 'failed'
}) => request.post('/merchant/orders/test', data).then((r) => r.data.data as CreateTestOrderResult),
getDeliveryLink: (orderNo: string) =>
request.get(`/merchant/orders/${encodeURIComponent(orderNo)}/delivery-link`).then((r) => r.data.data as DeliveryLinkResult),
revokeDeliveryLink: (orderNo: string) =>
request.post(`/merchant/orders/${encodeURIComponent(orderNo)}/delivery-link/revoke`).then((r) => r.data.data),
restoreDeliveryLink: (orderNo: string) =>
request.post(`/merchant/orders/${encodeURIComponent(orderNo)}/delivery-link/restore`).then((r) => r.data.data as DeliveryLinkResult),
wallet: () =>
request.get('/merchant/wallet').then((r) => r.data.data as WalletAccount),
ledger: (params?: Record<string, unknown>) =>
@@ -97,17 +104,25 @@ export const merchantApi = {
}
export const deliveryApi = {
getOrder: (orderNo: string) =>
request.get(`/delivery/v1/orders/${encodeURIComponent(orderNo)}`).then((r) => r.data.data as DeliveryOrderInfo),
bind: (orderNo: string, gameAccount: string) =>
request.post(`/delivery/v1/orders/${encodeURIComponent(orderNo)}/bind`, {
game_account: gameAccount,
}).then((r) => r.data.data as DeliveryBindResult),
submit: (orderNo: string, gameAccount: string, bindUUID: string) =>
request.post(`/delivery/v1/orders/${encodeURIComponent(orderNo)}/submit`, {
game_account: gameAccount,
bind_uuid: bindUUID,
}).then((r) => r.data.data as DeliverySubmitResult),
getOrder: (orderNo: string, auth?: { exp?: string; sign?: string }) =>
request.get(`/delivery/v1/orders/${encodeURIComponent(orderNo)}`, { params: auth }).then((r) => r.data.data as DeliveryOrderInfo),
bind: (orderNo: string, gameAccount: string, auth?: { exp?: string; sign?: string }) =>
request.post(
`/delivery/v1/orders/${encodeURIComponent(orderNo)}/bind`,
{
game_account: gameAccount,
},
{ params: auth },
).then((r) => r.data.data as DeliveryBindResult),
submit: (orderNo: string, gameAccount: string, bindUUID: string, auth?: { exp?: string; sign?: string }) =>
request.post(
`/delivery/v1/orders/${encodeURIComponent(orderNo)}/submit`,
{
game_account: gameAccount,
bind_uuid: bindUUID,
},
{ params: auth },
).then((r) => r.data.data as DeliverySubmitResult),
}
export const platformApi = {
+11 -5
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react'
import { useParams } from 'react-router-dom'
import { useParams, useSearchParams } from 'react-router-dom'
import {
Alert,
Button,
@@ -35,6 +35,7 @@ const statusMap: Record<string, { color: string; text: string }> = {
export default function Delivery() {
const { channel = 'dlc', orderNo = '' } = useParams()
const [searchParams] = useSearchParams()
const [form] = Form.useForm<{ game_account: string }>()
const [order, setOrder] = useState<DeliveryOrderInfo | null>(null)
const [bindResult, setBindResult] = useState<DeliveryBindResult | null>(null)
@@ -45,6 +46,11 @@ export default function Delivery() {
const [error, setError] = useState('')
const decodedOrderNo = useMemo(() => decodeURIComponent(orderNo), [orderNo])
const deliveryAuth = useMemo(() => {
const exp = searchParams.get('exp') || ''
const sign = searchParams.get('sign') || ''
return exp && sign ? { exp, sign } : undefined
}, [searchParams])
const gameAccount = Form.useWatch('game_account', form)
useEffect(() => {
@@ -54,21 +60,21 @@ export default function Delivery() {
return
}
setLoading(true)
deliveryApi.getOrder(decodedOrderNo)
deliveryApi.getOrder(decodedOrderNo, deliveryAuth)
.then((data) => {
setOrder(data)
setError('')
})
.catch((e) => setError(e instanceof Error ? e.message : '订单加载失败'))
.finally(() => setLoading(false))
}, [decodedOrderNo])
}, [decodedOrderNo, deliveryAuth])
const bindAccount = async () => {
const values = await form.validateFields()
setBinding(true)
setSubmitResult(null)
try {
const result = await deliveryApi.bind(decodedOrderNo, values.game_account)
const result = await deliveryApi.bind(decodedOrderNo, values.game_account, deliveryAuth)
setBindResult(result)
message.success('绑定二维码已生成')
} catch (e) {
@@ -86,7 +92,7 @@ export default function Delivery() {
}
setSubmitting(true)
try {
const result = await deliveryApi.submit(decodedOrderNo, values.game_account, bindResult.bind_uuid)
const result = await deliveryApi.submit(decodedOrderNo, values.game_account, bindResult.bind_uuid, deliveryAuth)
setSubmitResult(result)
setOrder((prev) => prev ? { ...prev, status: result.status, can_ship: false } : prev)
message.success(result.message || '已提交')
+74 -27
View File
@@ -76,9 +76,6 @@ const eventOptions = [
{ value: 'order.cancelled', label: '订单取消' },
]
const deliveryBaseUrl = (import.meta.env.VITE_DELIVERY_BASE_URL || window.location.origin).replace(/\/+$/, '')
const deliveryChannel = 'dlc'
const testOrderStatusOptions = [
{ value: 'pending', label: '已支付,可发货' },
{ value: 'failed', label: '发货失败,可重试' },
@@ -125,11 +122,26 @@ export default function MerchantCenter() {
})), [testOrderProducts])
const copyDeliveryLink = useCallback((orderNo: string) => {
navigator.clipboard.writeText(buildDeliveryLink(orderNo)).then(() => message.success('发货链接已复制'))
merchantApi.getDeliveryLink(orderNo)
.then((data) => navigator.clipboard.writeText(data.delivery_url).then(() => message.success('发货链接已复制')))
.catch((e) => message.error(e instanceof Error ? e.message : '复制失败'))
}, [])
const openDeliveryLink = useCallback((orderNo: string) => {
window.open(buildDeliveryLink(orderNo), '_blank', 'noopener,noreferrer')
merchantApi.getDeliveryLink(orderNo)
.then((data) => window.open(data.delivery_url, '_blank', 'noopener,noreferrer'))
.catch((e) => message.error(e instanceof Error ? e.message : '打开失败'))
}, [])
const patchDeliveryLinkState = useCallback((orderNo: string, patch: Partial<FulfillmentOrder>) => {
setOrders((prev) => ({
...prev,
list: prev.list.map((item) => item.order_no === orderNo ? { ...item, ...patch } : item),
}))
setTestOrderResult((prev) => prev?.order.order_no === orderNo ? {
...prev,
order: { ...prev.order, ...patch },
} : prev)
}, [])
const loadCurrent = useCallback(async () => {
@@ -149,6 +161,28 @@ export default function MerchantCenter() {
setOrders(data)
}, [orders.page, orders.size])
const revokeDeliveryLink = useCallback((orderNo: string) => {
merchantApi.revokeDeliveryLink(orderNo)
.then(() => {
const revokedAt = new Date().toISOString()
message.success('发货链接已作废')
patchDeliveryLinkState(orderNo, { delivery_link_revoked_at: revokedAt })
})
.catch((e) => message.error(e instanceof Error ? e.message : '作废失败'))
}, [patchDeliveryLinkState])
const restoreDeliveryLink = useCallback((orderNo: string) => {
merchantApi.restoreDeliveryLink(orderNo)
.then((data) => {
message.success('发货链接已恢复')
patchDeliveryLinkState(orderNo, {
delivery_link_expires_at: data.expires_at,
delivery_link_revoked_at: null,
})
})
.catch((e) => message.error(e instanceof Error ? e.message : '恢复失败'))
}, [patchDeliveryLinkState])
const loadWallet = useCallback(async (
page = ledger.page,
size = ledger.size,
@@ -435,15 +469,24 @@ export default function MerchantCenter() {
{ title: '支付', dataIndex: 'payment_status', width: 90, render: paymentStatusTag },
{ title: '履约', dataIndex: 'fulfillment_status', width: 100, render: fulfillmentStatusTag },
{ title: '上游单号', dataIndex: 'provider_order_no', width: 140, ellipsis: true, render: (v) => v || '-' },
{
title: '链接有效期',
dataIndex: 'delivery_link_expires_at',
width: 160,
render: (_, record) => record.delivery_link_revoked_at ? <Tag color="red"></Tag> : formatTime(record.delivery_link_expires_at),
},
{ title: '时间', dataIndex: 'created_at', width: 160, render: formatTime },
{
title: '发货链接',
key: 'delivery_link',
width: 150,
render: (_, record) => (
<Space size={4}>
width: 220,
render: (_, record) => record.delivery_link_revoked_at ? (
<Button type="link" size="small" onClick={() => restoreDeliveryLink(record.order_no)}></Button>
) : (
<Space size={4} wrap>
<Button type="link" size="small" onClick={() => copyDeliveryLink(record.order_no)}></Button>
<Button type="link" size="small" onClick={() => openDeliveryLink(record.order_no)}></Button>
<Button type="link" danger size="small" onClick={() => revokeDeliveryLink(record.order_no)}></Button>
</Space>
),
},
@@ -552,7 +595,7 @@ export default function MerchantCenter() {
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> UID</Typography.Text>
<Typography.Text type="secondary"> UID</Typography.Text>
{canManage && (
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={openTestOrderCreate}>
@@ -567,7 +610,7 @@ export default function MerchantCenter() {
columns={orderColumns}
dataSource={orders.list}
tableLayout="fixed"
scroll={{ x: 1450 }}
scroll={{ x: 1500 }}
pagination={pageConfig(orders, loadOrders)}
/>
</Space>
@@ -748,15 +791,22 @@ export default function MerchantCenter() {
footer={(
<Space>
<Button onClick={() => setTestOrderResult(null)}></Button>
<Button
icon={<CopyOutlined />}
onClick={() => copyDeliveryLink(testOrderResult?.order.order_no || '')}
>
</Button>
<Button type="primary" icon={<LinkOutlined />} onClick={() => openDeliveryLink(testOrderResult?.order.order_no || '')}>
</Button>
{testOrderResult?.order.delivery_link_revoked_at ? (
<Button onClick={() => restoreDeliveryLink(testOrderResult?.order.order_no || '')}></Button>
) : (
<>
<Button
icon={<CopyOutlined />}
onClick={() => copyDeliveryLink(testOrderResult?.order.order_no || '')}
>
</Button>
<Button type="primary" icon={<LinkOutlined />} onClick={() => openDeliveryLink(testOrderResult?.order.order_no || '')}>
</Button>
<Button danger onClick={() => revokeDeliveryLink(testOrderResult?.order.order_no || '')}></Button>
</>
)}
</Space>
)}
>
@@ -773,10 +823,11 @@ export default function MerchantCenter() {
{testOrderResult.can_ship ? 'can_ship=true' : 'can_ship=false'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="发货链接">
<Typography.Link href={buildDeliveryLink(testOrderResult.order.order_no)} target="_blank" rel="noreferrer">
{buildDeliveryLink(testOrderResult.order.order_no)}
</Typography.Link>
<Descriptions.Item label="链接有效期">
{testOrderResult.order.delivery_link_revoked_at ? <Tag color="red"></Tag> : formatTime(testOrderResult.order.delivery_link_expires_at)}
</Descriptions.Item>
<Descriptions.Item label="链接状态">
<Typography.Text type="secondary"></Typography.Text>
</Descriptions.Item>
</Descriptions>
)}
@@ -935,10 +986,6 @@ function productOptionLabel(item: MerchantProduct) {
return name === item.sku ? item.sku : `${name} / ${item.sku}`
}
function buildDeliveryLink(orderNo: string) {
return `${deliveryBaseUrl}/delivery/${deliveryChannel}/${encodeURIComponent(orderNo)}`
}
function paymentStatusTag(value: string) {
const item = paymentStatusMap[value] || { color: 'default', text: value }
return <Tag color={item.color}>{item.text}</Tag>
+10
View File
@@ -116,6 +116,8 @@ export interface FulfillmentOrder {
created_at: string
delivered_at?: string | null
cancelled_at?: string | null
delivery_link_expires_at?: string | null
delivery_link_revoked_at?: string | null
}
export interface CreateTestOrderResult {
@@ -164,6 +166,14 @@ export interface DeliverySubmitResult {
upstream_order?: unknown
}
export interface DeliveryLinkResult {
order_no: string
delivery_url: string
expires_at: string
exp: number
sign: string
}
export interface ApiClient {
id: number
merchant_id: number