完善发货链接签名和作废恢复
This commit is contained in:
@@ -6,6 +6,3 @@
|
||||
|
||||
# 后端 API 代理目标(见 vite.config.ts)
|
||||
VITE_API_PROXY_TARGET=http://localhost:8080
|
||||
|
||||
# 用户侧发货页面基础地址;为空时按当前前端域名生成
|
||||
VITE_DELIVERY_BASE_URL=
|
||||
|
||||
+26
-11
@@ -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 = {
|
||||
|
||||
@@ -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 || '已提交')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user