对接皮肤源头开放接口:查询、发货推送与 HMAC 签名鉴权
- 新增订单查询与发货结果推送开放接口,支持 can_ship 与幂等 - 鉴权采用 X-Api-Key + Timestamp + Nonce + HMAC-SHA256 签名 - 订单扩展发货字段与 ship_logs,管理端增加发货记录与开放文档页
This commit is contained in:
@@ -9,6 +9,8 @@ import Dashboard from './pages/Dashboard'
|
||||
import Skins from './pages/Skins'
|
||||
import Orders from './pages/Orders'
|
||||
import Distributors from './pages/Distributors'
|
||||
import ShipLogs from './pages/ShipLogs'
|
||||
import OpenApiDocs from './pages/OpenApiDocs'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
function PrivateRoute({ children }: { children: ReactNode }) {
|
||||
@@ -47,6 +49,22 @@ function AppRoutes() {
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="ship-logs"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<ShipLogs />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="open-api"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<OpenApiDocs />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
LoginResult,
|
||||
Order,
|
||||
PageResult,
|
||||
ShipLog,
|
||||
Skin,
|
||||
User,
|
||||
} from '../types'
|
||||
@@ -58,3 +59,8 @@ export const userApi = {
|
||||
updateStatus: (id: number, status: number) =>
|
||||
request.patch(`/users/${id}/status`, { status }).then((r) => r.data.data),
|
||||
}
|
||||
|
||||
export const shipLogApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get('/ship-logs', { params }).then((r) => r.data.data as PageResult<ShipLog>),
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
SendOutlined,
|
||||
ApiOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useAuth } from '../store/auth'
|
||||
import type { MenuProps } from 'antd'
|
||||
@@ -40,7 +42,11 @@ export default function MainLayout() {
|
||||
{ key: '/orders', icon: <ShoppingOutlined />, label: '订单管理' },
|
||||
]
|
||||
if (isAdmin) {
|
||||
items.push({ key: '/distributors', icon: <TeamOutlined />, label: '分销商' })
|
||||
items.push(
|
||||
{ key: '/distributors', icon: <TeamOutlined />, label: '分销商' },
|
||||
{ key: '/ship-logs', icon: <SendOutlined />, label: '发货记录' },
|
||||
{ key: '/open-api', icon: <ApiOutlined />, label: '开放接口' },
|
||||
)
|
||||
}
|
||||
return items
|
||||
}, [isAdmin])
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import { Alert, Card, Descriptions, Space, Table, Tabs, Tag, Typography } from 'antd'
|
||||
|
||||
const { Title, Paragraph, Text, Link } = Typography
|
||||
|
||||
const baseUrl =
|
||||
typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':8080') : 'http://localhost:8080'
|
||||
|
||||
const signPython = `import hmac, hashlib, time, uuid, requests
|
||||
|
||||
API_KEY = "sk_source_dev_key_change_me"
|
||||
API_SECRET = "sk_source_dev_secret_change_me"
|
||||
BASE = "${baseUrl}"
|
||||
|
||||
def sign_headers(method: str, path: str, body: str = "") -> dict:
|
||||
ts = str(int(time.time()))
|
||||
nonce = uuid.uuid4().hex
|
||||
raw = "\\n".join([API_KEY, ts, nonce, method.upper(), path, body])
|
||||
sign = hmac.new(API_SECRET.encode(), raw.encode(), hashlib.sha256).hexdigest()
|
||||
return {
|
||||
"X-Api-Key": API_KEY,
|
||||
"X-Timestamp": ts,
|
||||
"X-Nonce": nonce,
|
||||
"X-Sign": sign,
|
||||
}
|
||||
|
||||
# 查询
|
||||
path = "/api/open/v1/orders/O你的订单号"
|
||||
print(requests.get(BASE + path, headers=sign_headers("GET", path)).json())
|
||||
|
||||
# 推送(body 必须与签名一致)
|
||||
path = "/api/open/v1/orders/ship-notify"
|
||||
body = '{"order_no":"O你的订单号","ship_status":"success","provider_order_no":"SRC001"}'
|
||||
headers = {"Content-Type": "application/json", **sign_headers("POST", path, body)}
|
||||
print(requests.post(BASE + path, headers=headers, data=body.encode()).json())`
|
||||
|
||||
export default function OpenApiDocs() {
|
||||
return (
|
||||
<div>
|
||||
<Title level={4} style={{ marginTop: 0 }}>
|
||||
开放接口文档(皮肤源头)
|
||||
</Title>
|
||||
<Paragraph type="secondary">
|
||||
给上游发货系统对接使用。详细 Markdown 文档见仓库{' '}
|
||||
<Text code>docs/开放接口-皮肤源头对接.md</Text>。
|
||||
</Paragraph>
|
||||
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="鉴权:X-Api-Key + HMAC 签名(必填)"
|
||||
description={
|
||||
<div>
|
||||
请求头必须同时携带:
|
||||
<Text code>X-Api-Key</Text>、<Text code>X-Timestamp</Text>、
|
||||
<Text code>X-Nonce</Text>、<Text code>X-Sign</Text>
|
||||
<br />
|
||||
开发默认 Key:
|
||||
<Text code copyable>
|
||||
sk_source_dev_key_change_me
|
||||
</Text>
|
||||
,Secret:
|
||||
<Text code copyable>
|
||||
sk_source_dev_secret_change_me
|
||||
</Text>
|
||||
<br />
|
||||
生产环境变量:
|
||||
<Text code>OPEN_API_KEY</Text> / <Text code>OPEN_API_SECRET</Text> /{' '}
|
||||
<Text code>OPEN_SIGN_SKEW</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'auth',
|
||||
label: '签名规则',
|
||||
children: (
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Card size="small" title="待签名字符串(6 行,\\n 分隔)">
|
||||
<pre style={preStyle}>{`{api_key}
|
||||
{timestamp}
|
||||
{nonce}
|
||||
{METHOD}
|
||||
{path}
|
||||
{body}`}</pre>
|
||||
<Descriptions size="small" column={1} bordered style={{ marginTop: 12 }}>
|
||||
<Descriptions.Item label="METHOD">大写 GET / POST</Descriptions.Item>
|
||||
<Descriptions.Item label="path">
|
||||
URL.Path,不含 query,如 /api/open/v1/orders/O123
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="body">
|
||||
原始请求体;GET 用空字符串。POST 必须与实际发送 body 字节一致
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="X-Sign">
|
||||
hex(HMAC-SHA256(api_secret, string_to_sign)) 小写
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="时间窗">默认 ±300 秒</Descriptions.Item>
|
||||
<Descriptions.Item label="Nonce">8~64 字符,有效期内同一 Key 不可重复</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
<Card size="small" title="Python 完整示例">
|
||||
<pre style={preStyle}>{signPython}</pre>
|
||||
</Card>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'flow',
|
||||
label: '对接流程',
|
||||
children: (
|
||||
<Card size="small">
|
||||
<Paragraph>
|
||||
<ol>
|
||||
<li>买家在店铺下单并完成支付(订单 status = paid)</li>
|
||||
<li>上游拿到店铺订单号 <Text code>order_no</Text></li>
|
||||
<li>
|
||||
带签名调用「订单查询」确认 <Text code>product.sku</Text> 与{' '}
|
||||
<Text code>can_ship</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text code>can_ship=true</Text> 时执行发货(按 sku)
|
||||
</li>
|
||||
<li>带签名调用「发货推送」回传 success / failed / processing</li>
|
||||
<li>我方同步订单状态;重复 success 幂等成功</li>
|
||||
</ol>
|
||||
</Paragraph>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
Base URL 示例:<Text code>{baseUrl}</Text>
|
||||
</Paragraph>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'query',
|
||||
label: '订单查询',
|
||||
children: (
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Card size="small" title="请求">
|
||||
<Paragraph>
|
||||
<Tag color="blue">GET</Tag>
|
||||
<Text code>/api/open/v1/orders/{order_no}</Text>
|
||||
</Paragraph>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
Header:X-Api-Key / X-Timestamp / X-Nonce / X-Sign(见「签名规则」)
|
||||
</Paragraph>
|
||||
</Card>
|
||||
<Card size="small" title="响应字段">
|
||||
<Table
|
||||
size="small"
|
||||
pagination={false}
|
||||
rowKey="field"
|
||||
dataSource={[
|
||||
{ field: 'order_no', desc: '店铺订单号' },
|
||||
{ field: 'status', desc: '订单状态' },
|
||||
{ field: 'can_ship', desc: '是否可发货(发货前置请以此为准)' },
|
||||
{ field: 'cannot_ship_reason', desc: '不可发货原因' },
|
||||
{ field: 'product.sku', desc: '商品英文固定标识(发货用)' },
|
||||
{ field: 'product.name', desc: '商品中文名' },
|
||||
{ field: 'product.game', desc: '游戏,如和平精英' },
|
||||
{ field: 'buyer_name', desc: '买家名' },
|
||||
{ field: 'amount', desc: '金额' },
|
||||
{ field: 'shipped_at', desc: '发货成功时间' },
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
title: '字段',
|
||||
dataIndex: 'field',
|
||||
width: 200,
|
||||
render: (v) => <Text code>{v}</Text>,
|
||||
},
|
||||
{ title: '说明', dataIndex: 'desc' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card size="small" title="can_ship 规则">
|
||||
<Table
|
||||
size="small"
|
||||
pagination={false}
|
||||
rowKey="status"
|
||||
dataSource={[
|
||||
{ status: 'pending', ship: 'false', note: '未支付' },
|
||||
{ status: 'paid', ship: 'true', note: '可发' },
|
||||
{ status: 'delivering', ship: 'false', note: '发货中' },
|
||||
{ status: 'delivered', ship: 'false', note: '已完成' },
|
||||
{ status: 'ship_failed', ship: 'true', note: '可重试' },
|
||||
{ status: 'cancelled', ship: 'false', note: '已取消' },
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
title: 'status',
|
||||
dataIndex: 'status',
|
||||
render: (v) => <Text code>{v}</Text>,
|
||||
},
|
||||
{
|
||||
title: 'can_ship',
|
||||
dataIndex: 'ship',
|
||||
render: (v) =>
|
||||
v === 'true' ? <Tag color="green">true</Tag> : <Tag>false</Tag>,
|
||||
},
|
||||
{ title: '说明', dataIndex: 'note' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'notify',
|
||||
label: '发货推送',
|
||||
children: (
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Card size="small" title="请求">
|
||||
<Paragraph>
|
||||
<Tag color="green">POST</Tag>
|
||||
<Text code>/api/open/v1/orders/ship-notify</Text>
|
||||
</Paragraph>
|
||||
<Paragraph type="secondary">
|
||||
除签名头外,Body 参与签名。Body 示例:
|
||||
</Paragraph>
|
||||
<pre style={preStyle}>{`{
|
||||
"order_no": "O202607201550038000",
|
||||
"ship_status": "success",
|
||||
"provider_order_no": "SRC20260720001",
|
||||
"shipped_at": "2026-07-20T16:00:00+08:00",
|
||||
"fail_reason": ""
|
||||
}`}</pre>
|
||||
</Card>
|
||||
<Card size="small" title="请求参数">
|
||||
<Descriptions size="small" column={1} bordered>
|
||||
<Descriptions.Item label="order_no">必填,店铺订单号</Descriptions.Item>
|
||||
<Descriptions.Item label="ship_status">
|
||||
必填:success / failed / processing
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="provider_order_no">可选,上游单号</Descriptions.Item>
|
||||
<Descriptions.Item label="shipped_at">
|
||||
可选,RFC3339;success 缺省用服务端时间
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="fail_reason">可选,失败原因</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
<Card size="small" title="状态映射与幂等">
|
||||
<Table
|
||||
size="small"
|
||||
pagination={false}
|
||||
rowKey="ship"
|
||||
style={{ marginBottom: 12 }}
|
||||
dataSource={[
|
||||
{ ship: 'processing', order: 'delivering', note: '已接单/发货中' },
|
||||
{ ship: 'success', order: 'delivered', note: '发货成功' },
|
||||
{ ship: 'failed', order: 'ship_failed', note: '失败可重试' },
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
title: 'ship_status',
|
||||
dataIndex: 'ship',
|
||||
render: (v) => <Text code>{v}</Text>,
|
||||
},
|
||||
{
|
||||
title: '订单状态',
|
||||
dataIndex: 'order',
|
||||
render: (v) => <Text code>{v}</Text>,
|
||||
},
|
||||
{ title: '说明', dataIndex: 'note' },
|
||||
]}
|
||||
/>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="幂等:订单已 delivered 时再次推送 success 仍返回成功,不会重复处理。"
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'errors',
|
||||
label: '错误码',
|
||||
children: (
|
||||
<Card size="small">
|
||||
<Table
|
||||
size="small"
|
||||
pagination={false}
|
||||
rowKey="code"
|
||||
dataSource={[
|
||||
{ code: 0, http: 200, msg: '成功' },
|
||||
{ code: 401, http: 401, msg: '鉴权失败:Key/签名/时间/Nonce' },
|
||||
{ code: 404, http: 404, msg: '订单不存在' },
|
||||
{ code: 400, http: 400, msg: '参数错误 / 状态不允许' },
|
||||
]}
|
||||
columns={[
|
||||
{ title: 'code', dataIndex: 'code', width: 80 },
|
||||
{ title: 'HTTP', dataIndex: 'http', width: 80 },
|
||||
{ title: '说明', dataIndex: 'msg' },
|
||||
]}
|
||||
/>
|
||||
<Paragraph type="secondary" style={{ marginTop: 12, marginBottom: 0 }}>
|
||||
商品 sku 对照:
|
||||
<Link href="/skins"> 皮肤商品列表</Link>
|
||||
(字段「英文名」)或仓库 docs/商品英文名对照.md
|
||||
</Paragraph>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const preStyle: CSSProperties = {
|
||||
margin: 0,
|
||||
padding: 12,
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
}
|
||||
@@ -18,7 +18,9 @@ import type { Order } from '../types'
|
||||
const statusMap: 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: '已取消' },
|
||||
}
|
||||
|
||||
@@ -59,29 +61,33 @@ export default function Orders() {
|
||||
}
|
||||
|
||||
const columns: ColumnsType<Order> = [
|
||||
{ title: '订单号', dataIndex: 'order_no', width: 200 },
|
||||
{ title: '订单号', dataIndex: 'order_no', width: 190, ellipsis: true },
|
||||
{
|
||||
title: '皮肤',
|
||||
dataIndex: ['skin', 'name'],
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (_, r) => r.skin?.name || `#${r.skin_id}`,
|
||||
},
|
||||
{
|
||||
title: 'SKU',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (_, r) => r.skin?.sku || '-',
|
||||
},
|
||||
{
|
||||
title: '分销商',
|
||||
dataIndex: ['distributor', 'nickname'],
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
render: (_, r) => r.distributor?.nickname || r.distributor?.username || `#${r.distributor_id}`,
|
||||
},
|
||||
{ title: '买家', dataIndex: 'buyer_name', width: 100 },
|
||||
{ title: '买家', dataIndex: 'buyer_name', width: 90, ellipsis: true },
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
width: 100,
|
||||
render: (v: number) => `¥${v.toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '佣金',
|
||||
dataIndex: 'commission_amt',
|
||||
width: 100,
|
||||
render: (v: number) => `¥${v.toFixed(2)}`,
|
||||
width: 90,
|
||||
render: (v: number) => `¥${Number(v ?? 0).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
@@ -92,10 +98,17 @@ export default function Orders() {
|
||||
return <Tag color={s.color}>{s.text}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '上游单号',
|
||||
dataIndex: 'provider_order_no',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
render: (v?: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
width: 160,
|
||||
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
]
|
||||
@@ -117,7 +130,7 @@ export default function Orders() {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{record.status === 'paid' && (
|
||||
{(record.status === 'paid' || record.status === 'ship_failed' || record.status === 'delivering') && (
|
||||
<Button type="link" size="small" onClick={() => changeStatus(record.id, 'delivered')}>
|
||||
标记交付
|
||||
</Button>
|
||||
@@ -146,7 +159,9 @@ export default function Orders() {
|
||||
options={[
|
||||
{ value: 'pending', label: '待支付' },
|
||||
{ value: 'paid', label: '已支付' },
|
||||
{ value: 'delivering', label: '发货中' },
|
||||
{ value: 'delivered', label: '已交付' },
|
||||
{ value: 'ship_failed', label: '发货失败' },
|
||||
{ value: 'cancelled', label: '已取消' },
|
||||
]}
|
||||
/>
|
||||
@@ -161,6 +176,8 @@ export default function Orders() {
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: 1100 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,22 @@ export interface Order {
|
||||
commission_amt: number
|
||||
status: string
|
||||
remark: string
|
||||
provider_order_no?: string
|
||||
shipped_at?: string | null
|
||||
ship_fail_reason?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ShipLog {
|
||||
id: number
|
||||
order_no: string
|
||||
order_id: number
|
||||
ship_status: string
|
||||
provider_order_no: string
|
||||
fail_reason: string
|
||||
payload: string
|
||||
result_status: string
|
||||
message: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user