对接皮肤源头开放接口:查询、发货推送与 HMAC 签名鉴权
- 新增订单查询与发货结果推送开放接口,支持 can_ship 与幂等 - 鉴权采用 X-Api-Key + Timestamp + Nonce + HMAC-SHA256 签名 - 订单扩展发货字段与 ship_logs,管理端增加发货记录与开放文档页
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { shipLogApi } from '../api'
|
||||
import type { ShipLog } from '../types'
|
||||
|
||||
const shipStatusMap: Record<string, { color: string; text: string }> = {
|
||||
success: { color: 'green', text: '成功' },
|
||||
failed: { color: 'red', text: '失败' },
|
||||
processing: { color: 'cyan', text: '发货中' },
|
||||
}
|
||||
|
||||
const orderStatusMap: Record<string, { color: string; text: string }> = {
|
||||
pending: { color: 'orange', text: '待支付' },
|
||||
paid: { color: 'blue', text: '已支付' },
|
||||
delivering: { color: 'cyan', text: '发货中' },
|
||||
delivered: { color: 'green', text: '已交付' },
|
||||
ship_failed: { color: 'red', text: '发货失败' },
|
||||
cancelled: { color: 'default', text: '已取消' },
|
||||
}
|
||||
|
||||
export default function ShipLogs() {
|
||||
const [list, setList] = useState<ShipLog[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [size, setSize] = useState(10)
|
||||
const [orderNo, setOrderNo] = useState('')
|
||||
const [shipStatus, setShipStatus] = useState<string | undefined>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [detail, setDetail] = useState<ShipLog | null>(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await shipLogApi.list({
|
||||
page,
|
||||
size,
|
||||
order_no: orderNo || undefined,
|
||||
ship_status: shipStatus,
|
||||
})
|
||||
setList(data.list || [])
|
||||
setTotal(data.total)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, size, orderNo, shipStatus])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const columns: ColumnsType<ShipLog> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||
{ title: '店铺订单号', dataIndex: 'order_no', width: 200, ellipsis: true },
|
||||
{
|
||||
title: '推送状态',
|
||||
dataIndex: 'ship_status',
|
||||
width: 100,
|
||||
render: (v: string) => {
|
||||
const s = shipStatusMap[v] || { color: 'default', text: v }
|
||||
return <Tag color={s.color}>{s.text}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '处理后订单状态',
|
||||
dataIndex: 'result_status',
|
||||
width: 130,
|
||||
render: (v: string) => {
|
||||
if (!v) return '-'
|
||||
const s = orderStatusMap[v] || { color: 'default', text: v }
|
||||
return <Tag color={s.color}>{s.text}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '上游单号',
|
||||
dataIndex: 'provider_order_no',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'message',
|
||||
ellipsis: true,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '失败原因',
|
||||
dataIndex: 'fail_reason',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 90,
|
||||
render: (_, record) => (
|
||||
<Button type="link" size="small" onClick={() => setDetail(record)}>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
发货推送记录
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Input.Search
|
||||
placeholder="店铺订单号"
|
||||
allowClear
|
||||
onSearch={(v) => {
|
||||
setPage(1)
|
||||
setOrderNo(v)
|
||||
}}
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="推送状态"
|
||||
style={{ width: 140 }}
|
||||
value={shipStatus}
|
||||
onChange={(v) => {
|
||||
setPage(1)
|
||||
setShipStatus(v)
|
||||
}}
|
||||
options={[
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
{ value: 'processing', label: '发货中' },
|
||||
]}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: -4 }}>
|
||||
记录皮肤源头回调的发货结果,便于对账与排错。
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
tableLayout="fixed"
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p, s) => {
|
||||
setPage(p)
|
||||
setSize(s)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="推送详情"
|
||||
open={!!detail}
|
||||
onCancel={() => setDetail(null)}
|
||||
footer={null}
|
||||
width={640}
|
||||
>
|
||||
{detail && (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<div>
|
||||
<Typography.Text type="secondary">店铺订单号:</Typography.Text>
|
||||
<Typography.Text copyable>{detail.order_no}</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">上游单号:</Typography.Text>
|
||||
{detail.provider_order_no || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">推送状态 / 结果状态:</Typography.Text>
|
||||
{detail.ship_status} → {detail.result_status || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">说明:</Typography.Text>
|
||||
{detail.message || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">失败原因:</Typography.Text>
|
||||
{detail.fail_reason || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">原始请求:</Typography.Text>
|
||||
<pre
|
||||
style={{
|
||||
marginTop: 8,
|
||||
padding: 12,
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
maxHeight: 280,
|
||||
overflow: 'auto',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{formatPayload(detail.payload)}
|
||||
</pre>
|
||||
</div>
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatPayload(raw: string) {
|
||||
if (!raw) return '-'
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(raw), null, 2)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user