feat(fulfillment): 接入Rock发货平台(0666.pro)代理履约、通用库存池与UID强一致风控
1. 数据库新增 012_rock_fulfillment 迁移,创建 rock_configs 配置表与 rock_tokens 通用库存表,补齐黑色ROCK长袖T恤与蓝色运动服上衣商品目录; 2. 封装 RockClient 与 RockService,实现 0666.pro 平台 Bearer Token 极简连通校验、批量采购50个入库、水位监控巡检与自动补货; 3. 新增 RockProvider 实现标准履约路由,引入 Anti-Ghost 旧绑定基线比对与 UID 强一致门禁,扫错号硬阻断并记录为 mismatch 状态; 4. 针对黑色ROCK与蓝色运动服实现动态发货(绑定后发货前动态选择 skin),并提取腾讯官方发货流水号 fulfillId 对账; 5. 新增全局自动发货总开关(Kill Switch),支持一键软熔断降级转人工接单处理; 6. 前端新增 PlatformRock 管理大屏,提供总开关切换、Token配置与连通测试、库存监控、一键补货及错单解冻重置。
This commit is contained in:
@@ -14,6 +14,7 @@ import AlertSettings from './pages/AlertSettings'
|
||||
import PlatformMerchants from './pages/PlatformMerchants'
|
||||
import PlatformProducts from './pages/PlatformProducts'
|
||||
import PlatformUsers from './pages/PlatformUsers'
|
||||
import PlatformRock from './pages/PlatformRock'
|
||||
import type { ReactNode } from 'react'
|
||||
function PrivateRoute({ children }: { children: ReactNode }) {
|
||||
const { token } = useAuth()
|
||||
@@ -95,6 +96,14 @@ function AppRoutes() {
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="platform-rock"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<PlatformRock />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="platform-users" element={<AdminRoute><PlatformUsers /></AdminRoute>} />
|
||||
<Route path="open-api" element={<MerchantPermissionRoute permission="api:manage"><OpenApiDocs /></MerchantPermissionRoute>} />
|
||||
<Route
|
||||
|
||||
@@ -252,4 +252,71 @@ export const lewanApi = {
|
||||
request.put(`/platform/products/${id}/routing`, data).then((r) => r.data.data as { updated: boolean }),
|
||||
}
|
||||
|
||||
export interface RockConfig {
|
||||
id: number
|
||||
auto_delivery_enabled: boolean
|
||||
base_url: string
|
||||
bearer_token: string
|
||||
username: string
|
||||
points: number
|
||||
min_points_warning: number
|
||||
token_warning_stock: number
|
||||
auto_purchase_enabled: boolean
|
||||
auto_purchase_count: number
|
||||
purchase_cooldown_minutes: number
|
||||
last_purchased_at?: string
|
||||
last_heartbeat_at?: string
|
||||
}
|
||||
|
||||
export interface RockStats {
|
||||
available_stock: number
|
||||
using_stock: number
|
||||
bound_stock: number
|
||||
redeemed_stock: number
|
||||
mismatch_stock: number
|
||||
abnormal_stock: number
|
||||
upstream_points: number
|
||||
auto_delivery_enabled: boolean
|
||||
token_warning_stock: number
|
||||
auto_purchase_enabled: boolean
|
||||
username: string
|
||||
last_heartbeat_at?: string
|
||||
}
|
||||
|
||||
export interface RockTokenItem {
|
||||
id: number
|
||||
upstream_token_id: string
|
||||
redeem_code: string
|
||||
color: string
|
||||
region: string
|
||||
status: string
|
||||
assigned_order_no?: string
|
||||
expected_uid?: string
|
||||
actual_role_name?: string
|
||||
actual_role_id?: string
|
||||
actual_channel?: string
|
||||
upstream_fulfill_id?: number
|
||||
target_skin?: string
|
||||
target_currency?: string
|
||||
target_currency_quantity?: number
|
||||
mismatch_at?: string
|
||||
mismatch_reason?: string
|
||||
claimed_at?: string
|
||||
bound_at?: string
|
||||
redeemed_at?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export const rockApi = {
|
||||
getConfig: () => request.get('/platform/rock/config').then((r) => r.data.data as RockConfig),
|
||||
updateConfig: (data: Partial<RockConfig>) => request.put('/platform/rock/config', data).then((r) => r.data.data as RockConfig),
|
||||
testConnection: (bearer_token?: string) => request.post('/platform/rock/test-conn', { bearer_token }).then((r) => r.data.data),
|
||||
getStats: () => request.get('/platform/rock/stats').then((r) => r.data.data as RockStats),
|
||||
purchaseTokens: (count = 50) => request.post('/platform/rock/purchase', { count }).then((r) => r.data.data),
|
||||
listTokens: (params?: { page?: number; page_size?: number; status?: string; search?: string }) =>
|
||||
request.get('/platform/rock/tokens', { params }).then((r) => r.data.data as { list: RockTokenItem[]; total: number; page: number; page_size: number }),
|
||||
resetToken: (id: number) => request.post(`/platform/rock/tokens/${id}/reset`).then((r) => r.data.data),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ const adminSections: SidebarSection[] = [
|
||||
children: [
|
||||
{ key: 'merchant-products', label: '商品列表', path: '/merchant-products' },
|
||||
{ key: 'platform-products', label: '渠道与路由配置', path: '/platform-products' },
|
||||
{ key: 'platform-rock', label: 'Rock发货与库存风控', path: '/platform-rock' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Radio,
|
||||
Space,
|
||||
Statistic,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
FireOutlined,
|
||||
KeyOutlined,
|
||||
PlusCircleOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
ShoppingCartOutlined,
|
||||
ThunderboltOutlined,
|
||||
UndoOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { rockApi, type RockConfig, type RockStats, type RockTokenItem } from '../api'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export default function PlatformRock() {
|
||||
const [config, setConfig] = useState<RockConfig | null>(null)
|
||||
const [stats, setStats] = useState<RockStats | null>(null)
|
||||
const [loadingStats, setLoadingStats] = useState(false)
|
||||
const [testingConn, setTestingConn] = useState(false)
|
||||
const [savingConfig, setSavingConfig] = useState(false)
|
||||
const [purchasing, setPurchasing] = useState(false)
|
||||
|
||||
// Token 列表与筛选
|
||||
const [tokens, setTokens] = useState<RockTokenItem[]>([])
|
||||
const [totalTokens, setTotalTokens] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [filterStatus, setFilterStatus] = useState<string>('all')
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [loadingTokens, setLoadingTokens] = useState(false)
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoadingStats(true)
|
||||
try {
|
||||
const [cfg, st] = await Promise.all([rockApi.getConfig(), rockApi.getStats()])
|
||||
setConfig(cfg)
|
||||
setStats(st)
|
||||
form.setFieldsValue({
|
||||
auto_delivery_enabled: cfg.auto_delivery_enabled,
|
||||
base_url: cfg.base_url,
|
||||
bearer_token: cfg.bearer_token,
|
||||
min_points_warning: cfg.min_points_warning,
|
||||
token_warning_stock: cfg.token_warning_stock,
|
||||
auto_purchase_enabled: cfg.auto_purchase_enabled,
|
||||
auto_purchase_count: cfg.auto_purchase_count,
|
||||
purchase_cooldown_minutes: cfg.purchase_cooldown_minutes,
|
||||
})
|
||||
} catch (e) {
|
||||
message.error('加载 Rock 平台配置失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
} finally {
|
||||
setLoadingStats(false)
|
||||
}
|
||||
}, [form])
|
||||
|
||||
const loadTokens = useCallback(async () => {
|
||||
setLoadingTokens(true)
|
||||
try {
|
||||
const res = await rockApi.listTokens({
|
||||
page,
|
||||
page_size: pageSize,
|
||||
status: filterStatus === 'all' ? undefined : filterStatus,
|
||||
search: searchKeyword || undefined,
|
||||
})
|
||||
setTokens(res.list || [])
|
||||
setTotalTokens(res.total || 0)
|
||||
} catch (e) {
|
||||
message.error('加载库存列表失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
} finally {
|
||||
setLoadingTokens(false)
|
||||
}
|
||||
}, [page, pageSize, filterStatus, searchKeyword])
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [loadData])
|
||||
|
||||
useEffect(() => {
|
||||
loadTokens()
|
||||
}, [loadTokens])
|
||||
|
||||
// 一键切换自动发货总开关 (Kill Switch)
|
||||
const handleToggleAutoDelivery = async (checked: boolean) => {
|
||||
Modal.confirm({
|
||||
title: checked ? '确认开启 Rock 自动发货通道?' : '确认一键关闭 Rock 自动发货通道(软熔断)?',
|
||||
content: checked
|
||||
? '开启后,黑色 ROCK 与蓝色运动服两款商品将恢复全自动反向代理发货。'
|
||||
: '关闭后,两款商品将彻底脱离自动发货流程,订单将转交人工打手或客服处理,避免错单穿透。',
|
||||
okText: '确认更改',
|
||||
cancelText: '取消',
|
||||
okType: checked ? 'primary' : 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const updated = await rockApi.updateConfig({ auto_delivery_enabled: checked })
|
||||
setConfig(updated)
|
||||
message.success(checked ? '自动发货已开启' : '自动发货已成功熔断关闭')
|
||||
loadData()
|
||||
} catch (e) {
|
||||
message.error('更新总开关失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 测试连通性
|
||||
const handleTestConnection = async () => {
|
||||
const bearerToken = form.getFieldValue('bearer_token')
|
||||
if (!bearerToken) {
|
||||
message.warning('请先输入 Bearer Token')
|
||||
return
|
||||
}
|
||||
setTestingConn(true)
|
||||
try {
|
||||
const res = await rockApi.testConnection(bearerToken)
|
||||
message.success(`连通成功!采购商户:${res.user?.username || '正常'},可用积分:${res.user?.points || 0}`)
|
||||
loadData()
|
||||
} catch (e) {
|
||||
message.error('连通性测试失败:' + (e instanceof Error ? e.message : '未知错误'))
|
||||
} finally {
|
||||
setTestingConn(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存策略配置
|
||||
const handleSaveConfig = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
setSavingConfig(true)
|
||||
const updated = await rockApi.updateConfig(values)
|
||||
setConfig(updated)
|
||||
message.success('配置已保存生效')
|
||||
loadData()
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
message.error('保存失败: ' + e.message)
|
||||
}
|
||||
} finally {
|
||||
setSavingConfig(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量采购 50 个
|
||||
const handlePurchase = async (count = 50) => {
|
||||
setPurchasing(true)
|
||||
try {
|
||||
const res = await rockApi.purchaseTokens(count)
|
||||
message.success(res.message || `成功采购 ${count} 个通用黑色 Token!`)
|
||||
loadData()
|
||||
loadTokens()
|
||||
} catch (e) {
|
||||
message.error('采购失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
} finally {
|
||||
setPurchasing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 人工重置解冻 Mismatch Token
|
||||
const handleResetToken = async (id: number) => {
|
||||
try {
|
||||
await rockApi.resetToken(id)
|
||||
message.success('已解冻重置为可用库存')
|
||||
loadData()
|
||||
loadTokens()
|
||||
} catch (e) {
|
||||
message.error('重置失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<RockTokenItem> = [
|
||||
{
|
||||
title: 'ID / 提取码',
|
||||
dataIndex: 'id',
|
||||
width: 140,
|
||||
render: (id, record) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text strong>#{id}</Text>
|
||||
<Text copyable={{ text: record.redeem_code }} type="secondary" style={{ fontSize: 12 }}>
|
||||
{record.redeem_code?.substring(0, 10)}...
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '颜色与适用范围',
|
||||
dataIndex: 'color',
|
||||
width: 130,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Tag color="purple">通用黑色 Token</Tag>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{record.region || 'Q/V可用'}
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 120,
|
||||
render: (status) => {
|
||||
switch (status) {
|
||||
case 'available':
|
||||
return <Tag color="green">可用库存</Tag>
|
||||
case 'using':
|
||||
return <Tag color="blue">使用中 (扫码)</Tag>
|
||||
case 'bound':
|
||||
return <Tag color="cyan">已绑定授权</Tag>
|
||||
case 'redeemed':
|
||||
return <Tag color="purple">已交付发货</Tag>
|
||||
case 'mismatch':
|
||||
return <Tag color="red">绑定错误 (阻断)</Tag>
|
||||
default:
|
||||
return <Tag>{status}</Tag>
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '关联订单',
|
||||
dataIndex: 'assigned_order_no',
|
||||
width: 170,
|
||||
render: (orderNo) =>
|
||||
orderNo ? (
|
||||
<Text copyable style={{ fontSize: 12 }}>
|
||||
{orderNo}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="secondary">-</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '预期 UID vs 扫码角色',
|
||||
width: 220,
|
||||
render: (_, record) => {
|
||||
const hasMismatch = record.status === 'mismatch'
|
||||
return (
|
||||
<Space direction="vertical" size={2} style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>预期UID: </Text>
|
||||
<Text strong>{record.expected_uid || '-'}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>扫码角色: </Text>
|
||||
<Text type={hasMismatch ? 'danger' : undefined}>
|
||||
{record.actual_role_name ? `${record.actual_role_name} (${record.actual_role_id || '-'})` : '-'}
|
||||
</Text>
|
||||
</div>
|
||||
{hasMismatch && record.mismatch_reason && (
|
||||
<Tooltip title={record.mismatch_reason}>
|
||||
<Tag color="error" style={{ fontSize: 11, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{record.mismatch_reason}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '官方发货凭证 (FulfillId)',
|
||||
dataIndex: 'upstream_fulfill_id',
|
||||
width: 150,
|
||||
render: (fulfillId, record) =>
|
||||
fulfillId ? (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text copyable strong style={{ color: '#52c41a' }}>
|
||||
{fulfillId}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{record.target_skin || '和平精英道具'}
|
||||
</Text>
|
||||
</Space>
|
||||
) : (
|
||||
<Text type="secondary">-</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
render: (_, record) => {
|
||||
if (record.status === 'mismatch') {
|
||||
return (
|
||||
<Popconfirm
|
||||
title="确定解冻重置该 Token?"
|
||||
description="重置后将清除订单关联并重新释放为可用库存。"
|
||||
onConfirm={() => handleResetToken(record.id)}
|
||||
>
|
||||
<Button size="small" type="link" danger icon={<UndoOutlined />}>
|
||||
解冻重置
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)
|
||||
}
|
||||
return <Text type="secondary">-</Text>
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const autoDelivery = config?.auto_delivery_enabled ?? true
|
||||
const availableStock = stats?.available_stock ?? 0
|
||||
const isStockLow = availableStock <= (config?.token_warning_stock ?? 10)
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, maxWidth: 1400, margin: '0 auto' }}>
|
||||
<PageHeader
|
||||
title="Rock 发货平台 (0666.pro) 自动发货与库存风控"
|
||||
subtitle="管理和平精英「黑色ROCK长袖T恤」与「蓝色运动服上衣」通用库存池、采购策略、UID强一致防错门禁与软熔断机制"
|
||||
/>
|
||||
|
||||
{/* 熔断状态警示条 */}
|
||||
{!autoDelivery && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 20 }}
|
||||
message="【注意】Rock 自动发货全局总开关已关闭(已软熔断)"
|
||||
description="当前黑色 ROCK 和蓝色运动服两款商品已停止调用 0666.pro 自动发货,订单保留在待发货状态,将由人工打手在接单平台处理。"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 核心指标统计看板 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
|
||||
<Statistic
|
||||
title="可用通用库存 (Available)"
|
||||
value={availableStock}
|
||||
valueStyle={{ color: isStockLow ? '#cf1322' : '#3f8600', fontWeight: 'bold' }}
|
||||
prefix={<ShoppingCartOutlined />}
|
||||
suffix="个"
|
||||
/>
|
||||
<div style={{ marginTop: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
警戒水位: {config?.token_warning_stock ?? 10} 个
|
||||
</Text>
|
||||
{isStockLow && <Tag color="error">库存紧缺</Tag>}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
|
||||
<Statistic
|
||||
title="绑定错误阻断 (Mismatch)"
|
||||
value={stats?.mismatch_stock ?? 0}
|
||||
valueStyle={{ color: (stats?.mismatch_stock ?? 0) > 0 ? '#f5222d' : '#8c8c8c', fontWeight: 'bold' }}
|
||||
prefix={<ExclamationCircleOutlined />}
|
||||
suffix="个"
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{(stats?.mismatch_stock ?? 0) > 0 ? '存在 UID 校验不匹配的待排查记录' : '当前无绑定异常'}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
|
||||
<Statistic
|
||||
title="履约中 / 已交付"
|
||||
value={stats?.redeemed_stock ?? 0}
|
||||
valueStyle={{ color: '#1677ff', fontWeight: 'bold' }}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
suffix={`(扫码中: ${stats?.using_stock ?? 0})`}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
已完成发货交付总量
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
|
||||
<Statistic
|
||||
title="0666 账号积分余额"
|
||||
value={stats?.upstream_points ?? 0}
|
||||
precision={1}
|
||||
valueStyle={{ color: (stats?.upstream_points ?? 0) < (config?.min_points_warning ?? 1050) ? '#faad14' : '#722ed1', fontWeight: 'bold' }}
|
||||
prefix={<FireOutlined />}
|
||||
suffix="点"
|
||||
/>
|
||||
<div style={{ marginTop: 8, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
账号: {stats?.username || '未同步'}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
约可采购 {Math.floor((stats?.upstream_points ?? 0) / 21)} 个
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 上游凭据与策略设置 */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<KeyOutlined />
|
||||
<span>0666.pro 采购凭据与自动发货控制</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadData} loading={loadingStats}>
|
||||
刷新状态
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusCircleOutlined />}
|
||||
onClick={() => handlePurchase(50)}
|
||||
loading={purchasing}
|
||||
>
|
||||
一键采购 50 个通用 Token
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: 24, borderRadius: 8 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<Text strong>自动发货总开关 (Kill Switch)</Text>
|
||||
<Tooltip title="关闭后黑色 ROCK 与蓝色运动服两款商品停走自动发货通道,平滑转人工打手接单">
|
||||
<ExclamationCircleOutlined />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space size="middle">
|
||||
<Switch
|
||||
checked={autoDelivery}
|
||||
onChange={handleToggleAutoDelivery}
|
||||
checkedChildren="自动发货中"
|
||||
unCheckedChildren="已熔断转人工"
|
||||
/>
|
||||
<Text type={autoDelivery ? 'success' : 'danger'}>
|
||||
{autoDelivery ? '通道正常,全自动反代履约中' : '已熔断!商品已下线自动通道转交人工处理'}
|
||||
</Text>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="base_url" label="上游平台接口地址 (Base URL)">
|
||||
<Input placeholder="https://0666.pro" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24}>
|
||||
<Form.Item
|
||||
name="bearer_token"
|
||||
label={
|
||||
<Space>
|
||||
<Text strong>上游商户 Bearer Token (yd_token)</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
(平台无密码,直接输入从 0666.pro 网页复制的 64 位 Token)
|
||||
</Text>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
💡 极简提取指引:登录 0666.pro 网页后,按 F12 打开控制台,在 Console 执行{' '}
|
||||
<code>localStorage.getItem('yd_token')</code> 即可一键复制。
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="粘贴 64 位十六进制 Bearer Token"
|
||||
addonAfter={
|
||||
<Button type="link" size="small" onClick={handleTestConnection} loading={testingConn}>
|
||||
测试连通性
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Form.Item name="token_warning_stock" label="库存告警安全水位">
|
||||
<InputNumber min={1} max={500} style={{ width: '100%' }} addonAfter="个" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Form.Item name="auto_purchase_enabled" label="自动补货触发开关" valuePropName="checked">
|
||||
<Switch checkedChildren="开启自动采购" unCheckedChildren="关闭" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Form.Item name="auto_purchase_count" label="单次自动补货数量">
|
||||
<InputNumber min={5} max={200} style={{ width: '100%' }} addonAfter="个" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Form.Item name="purchase_cooldown_minutes" label="补货防抖冷却时间">
|
||||
<InputNumber min={1} max={60} style={{ width: '100%' }} addonAfter="分钟" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider style={{ margin: '12px 0 20px 0' }} />
|
||||
|
||||
<Space>
|
||||
<Button type="primary" onClick={handleSaveConfig} loading={savingConfig}>
|
||||
保存策略配置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 本地通用库存池明细 */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<ThunderboltOutlined />
|
||||
<span>通用 Token 库存池与错单审计列表</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Radio.Group value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)} buttonStyle="solid">
|
||||
<Radio.Button value="all">全部</Radio.Button>
|
||||
<Radio.Button value="available">可用</Radio.Button>
|
||||
<Radio.Button value="using">使用中</Radio.Button>
|
||||
<Radio.Button value="bound">已绑定</Radio.Button>
|
||||
<Radio.Button value="redeemed">已发货</Radio.Button>
|
||||
<Radio.Button value="mismatch">
|
||||
<Badge count={stats?.mismatch_stock ?? 0} offset={[8, -2]}>
|
||||
<span>绑定错误</span>
|
||||
</Badge>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
<Input
|
||||
placeholder="搜索订单号 / UID / 角色名"
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 220 }}
|
||||
onPressEnter={(e) => setSearchKeyword(e.currentTarget.value)}
|
||||
onChange={(e) => {
|
||||
if (!e.target.value) setSearchKeyword('')
|
||||
}}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadTokens} loading={loadingTokens}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={tokens}
|
||||
loading={loadingTokens}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total: totalTokens,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, ps) => {
|
||||
setPage(p)
|
||||
setPageSize(ps)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user