功能:接入乐玩开放平台双渠道路由,并修复渠道锁定与 Webhook 可靠性
在保持 DLC 默认零破坏的前提下,支持 priority/仅乐玩/仅 DLC 履约策略与后台 GUI 配置;同步补齐 Prepare 渠道落库、乐玩订单取消、Webhook 验签与失败重试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import MerchantCenter from './pages/MerchantCenter'
|
||||
import MerchantRecharge from './pages/MerchantRecharge'
|
||||
import AlertSettings from './pages/AlertSettings'
|
||||
import PlatformMerchants from './pages/PlatformMerchants'
|
||||
import PlatformProducts from './pages/PlatformProducts'
|
||||
import PlatformUsers from './pages/PlatformUsers'
|
||||
import type { ReactNode } from 'react'
|
||||
function PrivateRoute({ children }: { children: ReactNode }) {
|
||||
@@ -86,6 +87,14 @@ function AppRoutes() {
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="platform-products"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<PlatformProducts />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="platform-users" element={<AdminRoute><PlatformUsers /></AdminRoute>} />
|
||||
<Route path="open-api" element={<MerchantPermissionRoute permission="api:manage"><OpenApiDocs /></MerchantPermissionRoute>} />
|
||||
<Route
|
||||
|
||||
@@ -28,6 +28,9 @@ import type {
|
||||
User,
|
||||
WalletAccount,
|
||||
WalletLedgerEntry,
|
||||
LewanProductListResult,
|
||||
UpdateProductRoutingInput,
|
||||
SyncLewanProductInput,
|
||||
} from '../types'
|
||||
|
||||
export const authApi = {
|
||||
@@ -231,3 +234,13 @@ export const platformApi = {
|
||||
reviewRechargeApplication: (id: number, data: { approved: boolean; review_note?: string }) =>
|
||||
request.post(`/platform/recharge/applications/${id}/review`, data).then((r) => r.data.data as RechargeApplication),
|
||||
}
|
||||
|
||||
export const lewanApi = {
|
||||
listProducts: (params?: { page?: number; size?: number; keyword?: string }) =>
|
||||
request.get('/platform/lewan/products', { params }).then((r) => r.data.data as LewanProductListResult),
|
||||
syncCatalog: (data: SyncLewanProductInput) =>
|
||||
request.post('/platform/lewan/sync-catalog', data).then((r) => r.data.data as ProductCatalogItem),
|
||||
updateRouting: (id: number, data: UpdateProductRoutingInput) =>
|
||||
request.put(`/platform/products/${id}/routing`, data).then((r) => r.data.data as { updated: boolean }),
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,10 @@ const adminSections: SidebarSection[] = [
|
||||
key: 'products',
|
||||
label: '商品管理',
|
||||
icon: <AppstoreOutlined />,
|
||||
children: [{ key: 'merchant-products', label: '商品列表', path: '/merchant-products' }],
|
||||
children: [
|
||||
{ key: 'merchant-products', label: '商品列表', path: '/merchant-products' },
|
||||
{ key: 'platform-products', label: '渠道与路由配置', path: '/platform-products' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'shops',
|
||||
|
||||
@@ -0,0 +1,919 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import {
|
||||
BranchesOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloudDownloadOutlined,
|
||||
DesktopOutlined,
|
||||
ImportOutlined,
|
||||
LinkOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
SwapOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { lewanApi, platformApi } from '../api'
|
||||
import type {
|
||||
LewanProductItem,
|
||||
ProductCatalogItem,
|
||||
ProductRoutingConfig,
|
||||
SyncLewanProductInput,
|
||||
UpdateProductRoutingInput,
|
||||
} from '../types'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
export default function PlatformProducts() {
|
||||
const [catalog, setCatalog] = useState<ProductCatalogItem[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [filterMode, setFilterMode] = useState<string>('all')
|
||||
const [filterCategory, setFilterCategory] = useState<string>('all')
|
||||
|
||||
// 乐玩上游商品抽屉状态
|
||||
const [lewanDrawerOpen, setLewanDrawerOpen] = useState(false)
|
||||
const [lewanLoading, setLewanLoading] = useState(false)
|
||||
const [lewanProducts, setLewanProducts] = useState<LewanProductItem[]>([])
|
||||
const [lewanPage, setLewanPage] = useState(1)
|
||||
const [lewanTotal, setLewanTotal] = useState(0)
|
||||
const [lewanKeyword, setLewanKeyword] = useState('')
|
||||
|
||||
// 路由配置 Modal 状态
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false)
|
||||
const [configuringItem, setConfiguringItem] = useState<ProductCatalogItem | null>(null)
|
||||
const [configSubmitting, setConfigSubmitting] = useState(false)
|
||||
const [configForm] = Form.useForm()
|
||||
|
||||
// 乐玩商品导入 Modal 状态
|
||||
const [syncModalOpen, setSyncModalOpen] = useState(false)
|
||||
const [syncSubmitting, setSyncSubmitting] = useState(false)
|
||||
const [syncForm] = Form.useForm()
|
||||
|
||||
// 加载自营平台商品目录
|
||||
const loadCatalog = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const list = await platformApi.productCatalog()
|
||||
setCatalog(list || [])
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载商品目录失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 加载乐玩商品列表
|
||||
const loadLewanProducts = useCallback(async (page = 1, keyword = '') => {
|
||||
setLewanLoading(true)
|
||||
try {
|
||||
const res = await lewanApi.listProducts({ page, size: 20, keyword })
|
||||
setLewanProducts(res.items || [])
|
||||
setLewanTotal(res.total || 0)
|
||||
setLewanPage(page)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '拉取乐玩商品列表失败,请确认乐玩配置与网络连接')
|
||||
} finally {
|
||||
setLewanLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadCatalog()
|
||||
}, [loadCatalog])
|
||||
|
||||
// 打开乐玩商品库抽屉
|
||||
const handleOpenLewanDrawer = () => {
|
||||
setLewanDrawerOpen(true)
|
||||
loadLewanProducts(1, lewanKeyword)
|
||||
}
|
||||
|
||||
// 统计指标
|
||||
const metrics = useMemo(() => {
|
||||
let priorityCount = 0
|
||||
let lewanOnlyCount = 0
|
||||
let dlcOnlyCount = 0
|
||||
|
||||
catalog.forEach((item) => {
|
||||
const mode = item.routing?.routing_mode || 'dlc'
|
||||
if (mode === 'priority') priorityCount++
|
||||
else if (mode === 'lewan') lewanOnlyCount++
|
||||
else dlcOnlyCount++
|
||||
})
|
||||
|
||||
return {
|
||||
total: catalog.length,
|
||||
priorityCount,
|
||||
lewanOnlyCount,
|
||||
dlcOnlyCount,
|
||||
}
|
||||
}, [catalog])
|
||||
|
||||
// 目录所有分类
|
||||
const categories = useMemo(() => {
|
||||
const set = new Set<string>()
|
||||
catalog.forEach((item) => {
|
||||
if (item.category) set.add(item.category)
|
||||
})
|
||||
return Array.from(set)
|
||||
}, [catalog])
|
||||
|
||||
// 过滤后的目录列表
|
||||
const filteredCatalog = useMemo(() => {
|
||||
return catalog.filter((item) => {
|
||||
const matchKeyword =
|
||||
!searchKeyword ||
|
||||
item.display_name.toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
||||
item.sku.toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
||||
String(item.id).includes(searchKeyword)
|
||||
|
||||
const mode = item.routing?.routing_mode || 'dlc'
|
||||
const matchMode =
|
||||
filterMode === 'all' ||
|
||||
(filterMode === 'priority' && mode === 'priority') ||
|
||||
(filterMode === 'lewan' && mode === 'lewan') ||
|
||||
(filterMode === 'dlc' && mode === 'dlc')
|
||||
|
||||
const matchCategory = filterCategory === 'all' || item.category === filterCategory
|
||||
|
||||
return matchKeyword && matchMode && matchCategory
|
||||
})
|
||||
}, [catalog, searchKeyword, filterMode, filterCategory])
|
||||
|
||||
// 打开渠道与优先级配置 Modal
|
||||
const openConfigModal = (record: ProductCatalogItem) => {
|
||||
setConfiguringItem(record)
|
||||
const routing = record.routing || { routing_mode: 'dlc' }
|
||||
const mode = routing.routing_mode || 'dlc'
|
||||
const priorityList = routing.priority || ['lewan', 'dlc']
|
||||
const priorityOrder = priorityList[0] === 'dlc' ? 'dlc_first' : 'lewan_first'
|
||||
|
||||
configForm.setFieldsValue({
|
||||
routing_mode: mode,
|
||||
priority_order: priorityOrder,
|
||||
dlc_good_id: routing.dlc_good_id || '',
|
||||
lewan_product_id: routing.lewan_product_id || '',
|
||||
lewan_name: routing.lewan_name || '',
|
||||
delivery_type: routing.delivery_type || 'LINK_BINDING',
|
||||
})
|
||||
setConfigModalOpen(true)
|
||||
}
|
||||
|
||||
// 提交渠道与优先级配置
|
||||
const submitConfig = async () => {
|
||||
if (!configuringItem) return
|
||||
const values = await configForm.validateFields()
|
||||
setConfigSubmitting(true)
|
||||
try {
|
||||
const priority =
|
||||
values.routing_mode === 'priority'
|
||||
? values.priority_order === 'dlc_first'
|
||||
? ['dlc', 'lewan']
|
||||
: ['lewan', 'dlc']
|
||||
: values.routing_mode === 'lewan'
|
||||
? ['lewan']
|
||||
: ['dlc']
|
||||
|
||||
const payload: UpdateProductRoutingInput = {
|
||||
routing_mode: values.routing_mode,
|
||||
priority,
|
||||
dlc_good_id: values.dlc_good_id?.trim() || '',
|
||||
lewan_product_id: values.lewan_product_id?.trim() || '',
|
||||
lewan_name: values.lewan_name?.trim() || '',
|
||||
delivery_type: values.delivery_type || 'LINK_BINDING',
|
||||
}
|
||||
|
||||
await lewanApi.updateRouting(configuringItem.id, payload)
|
||||
message.success(`商品 [${configuringItem.display_name}] 履约策略更新成功`)
|
||||
setConfigModalOpen(false)
|
||||
loadCatalog()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '更新配置失败')
|
||||
} finally {
|
||||
setConfigSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开将乐玩商品导入为自营目录的 Modal
|
||||
const openSyncModal = (lewanProduct: LewanProductItem) => {
|
||||
syncForm.resetFields()
|
||||
const unitPrice = Number.parseFloat(lewanProduct.effectiveUnitPrice || '')
|
||||
syncForm.setFieldsValue({
|
||||
lewan_product_id: lewanProduct.id,
|
||||
name: lewanProduct.name,
|
||||
category: lewanProduct.categoryName || '未分类',
|
||||
sku: `LEWAN_${lewanProduct.id.slice(0, 8).toUpperCase()}`,
|
||||
price_yuan: Number.isFinite(unitPrice) ? unitPrice.toFixed(2) : 0,
|
||||
delivery_type: lewanProduct.deliveryType || 'LINK_BINDING',
|
||||
})
|
||||
setSyncModalOpen(true)
|
||||
}
|
||||
|
||||
// 提交乐玩商品导入
|
||||
const submitSync = async () => {
|
||||
const values = await syncForm.validateFields()
|
||||
setSyncSubmitting(true)
|
||||
try {
|
||||
const priceCents = Math.round(Number(values.price_yuan || 0) * 100)
|
||||
const payload: SyncLewanProductInput = {
|
||||
lewan_product_id: values.lewan_product_id,
|
||||
name: values.name,
|
||||
category: values.category,
|
||||
sku: values.sku,
|
||||
price: priceCents,
|
||||
delivery_type: values.delivery_type,
|
||||
}
|
||||
await lewanApi.syncCatalog(payload)
|
||||
message.success(`乐玩商品 [${values.name}] 已成功同步导入至自营目录!`)
|
||||
setSyncModalOpen(false)
|
||||
loadCatalog()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '同步导入失败')
|
||||
} finally {
|
||||
setSyncSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷从乐玩列表快速关联到当前配置的商品
|
||||
const quickAttachLewanProduct = (p: LewanProductItem) => {
|
||||
configForm.setFieldsValue({
|
||||
lewan_product_id: p.id,
|
||||
lewan_name: p.name,
|
||||
delivery_type: p.deliveryType || 'LINK_BINDING',
|
||||
})
|
||||
message.success(`已选定乐玩商品: ${p.name}`)
|
||||
}
|
||||
|
||||
// 渲染策略 Tag
|
||||
const renderRoutingTag = (routing?: ProductRoutingConfig) => {
|
||||
const mode = routing?.routing_mode || 'dlc'
|
||||
if (mode === 'priority') {
|
||||
const isLewanFirst = !routing?.priority || routing.priority[0] === 'lewan'
|
||||
return isLewanFirst ? (
|
||||
<Tag color="cyan" icon={<SwapOutlined />} style={{ fontWeight: 600, padding: '3px 8px' }}>
|
||||
双平台: 乐玩优先 ➔ DLC兜底
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="blue" icon={<SwapOutlined />} style={{ fontWeight: 600, padding: '3px 8px' }}>
|
||||
双平台: DLC优先 ➔ 乐玩兜底
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
if (mode === 'lewan') {
|
||||
return (
|
||||
<Tag color="success" icon={<CheckCircleOutlined />} style={{ fontWeight: 600, padding: '3px 8px' }}>
|
||||
仅走乐玩平台
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tag color="orange" icon={<DesktopOutlined />} style={{ fontWeight: 600, padding: '3px 8px' }}>
|
||||
仅走第一平台 (DLC)
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
// 目录表格列
|
||||
const columns: ColumnsType<ProductCatalogItem> = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: 70,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'SKU / 编码',
|
||||
dataIndex: 'sku',
|
||||
width: 190,
|
||||
render: (v) => <Text code copyable={{ text: v }}>{v}</Text>,
|
||||
},
|
||||
{
|
||||
title: '商品名称',
|
||||
dataIndex: 'display_name',
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
render: (v, r) => (
|
||||
<div>
|
||||
<span style={{ fontWeight: 600, color: '#0f172a' }}>{v}</span>
|
||||
{r.category && (
|
||||
<Tag color="default" style={{ marginLeft: 6, fontSize: 11 }}>
|
||||
{r.category}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '参考售价 / 成本',
|
||||
key: 'price',
|
||||
width: 170,
|
||||
render: (_, r) => (
|
||||
<Space orientation="vertical" size={2}>
|
||||
<Text style={{ fontWeight: 600, color: '#2563eb' }}>
|
||||
售价: {(r.price_amount / 100).toFixed(2)} 元
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
成本: {(r.cost_amount / 100).toFixed(2)} 元
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '履约与优先级策略',
|
||||
key: 'routing_mode',
|
||||
width: 230,
|
||||
render: (_, r) => (
|
||||
<div>
|
||||
{renderRoutingTag(r.routing)}
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{r.status === 'active' ? (
|
||||
<Badge status="success" text="可售" />
|
||||
) : (
|
||||
<Badge status="default" text="已下架" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '渠道映射详情',
|
||||
key: 'mapping',
|
||||
width: 280,
|
||||
render: (_, r) => {
|
||||
const routing = r.routing
|
||||
const lewanId = routing?.lewan_product_id
|
||||
const dlcId = routing?.dlc_good_id
|
||||
return (
|
||||
<Space direction="vertical" size={2} style={{ width: '100%', fontSize: 12 }}>
|
||||
<div>
|
||||
<Text type="secondary">第一平台 (DLC): </Text>
|
||||
{dlcId ? (
|
||||
<Text code copyable={{ text: dlcId }}>{dlcId}</Text>
|
||||
) : (
|
||||
<Tag color="default" style={{ fontSize: 11 }}>默认按 SKU 匹配</Tag>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">乐玩平台: </Text>
|
||||
{lewanId ? (
|
||||
<span>
|
||||
<Tooltip title={`乐玩UUID: ${lewanId}`}>
|
||||
<Tag color="purple" style={{ fontSize: 11, cursor: 'pointer' }}>
|
||||
{routing.lewan_name || lewanId.slice(0, 8) + '...'}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
{routing.delivery_type && (
|
||||
<Tag color="cyan" style={{ fontSize: 10 }}>
|
||||
{routing.delivery_type === 'LINK_BINDING' ? '授权链接' : routing.delivery_type}
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<Text type="secondary" style={{ color: '#94a3b8' }}>未关联乐玩商品</Text>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
render: (_, r) => (
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => openConfigModal(r)}
|
||||
>
|
||||
配置渠道
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
// 乐玩商品表格列
|
||||
const lewanColumns: ColumnsType<LewanProductItem> = [
|
||||
{
|
||||
title: '商品名称',
|
||||
dataIndex: 'name',
|
||||
render: (v, r) => (
|
||||
<div>
|
||||
<span style={{ fontWeight: 600 }}>{v}</span>
|
||||
{r.categoryName && <Tag style={{ marginLeft: 6 }}>{r.categoryName}</Tag>}
|
||||
<div>
|
||||
<Text type="secondary" copyable={{ text: r.id }} style={{ fontSize: 11 }}>
|
||||
{r.id}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '供货价',
|
||||
dataIndex: 'effectiveUnitPrice',
|
||||
width: 100,
|
||||
render: (v?: string) => {
|
||||
const n = Number.parseFloat(v || '')
|
||||
return Number.isFinite(n) ? `${n.toFixed(2)} 元` : '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'stock',
|
||||
width: 80,
|
||||
render: (v?: number) => (v == null ? '-' : v),
|
||||
},
|
||||
{
|
||||
title: '交付形式',
|
||||
dataIndex: 'deliveryType',
|
||||
width: 120,
|
||||
render: (v) =>
|
||||
v === 'LINK_BINDING' ? (
|
||||
<Tag color="blue" icon={<LinkOutlined />}>链接绑定</Tag>
|
||||
) : v === 'EMAIL_BINDING' ? (
|
||||
<Tag color="orange">邮箱绑定(未接入)</Tag>
|
||||
) : (
|
||||
<Tag>{v || '标准'}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
render: (_, r) => (
|
||||
<Space size={8}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<ImportOutlined />}
|
||||
onClick={() => openSyncModal(r)}
|
||||
>
|
||||
导入为自营
|
||||
</Button>
|
||||
{configModalOpen && (
|
||||
<Tooltip title="将该乐玩商品快速填入当前正在配置的商品中">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => quickAttachLewanProduct(r)}
|
||||
>
|
||||
关联到此项
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0 4px' }}>
|
||||
<PageHeader
|
||||
title="渠道与路由配置"
|
||||
subtitle="多履约平台(乐玩开放平台 / 第一平台 DLC)优先级策略、上游商品拉取与路由配置"
|
||||
extra={
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CloudDownloadOutlined />}
|
||||
onClick={handleOpenLewanDrawer}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
|
||||
boxShadow: '0 2px 4px rgba(37,99,235,0.2)',
|
||||
}}
|
||||
>
|
||||
拉取乐玩商品库
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadCatalog} loading={loading}>
|
||||
刷新目录
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 顶部指标卡片 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 20 }}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="metric-card" styles={{ body: { padding: '16px 20px' } }}>
|
||||
<div className="metric-card-header">
|
||||
<span className="metric-card-title">自营目录商品数</span>
|
||||
<div className="metric-card-icon">
|
||||
<BranchesOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric-card-value">{metrics.total}</div>
|
||||
<div className="metric-card-footer">
|
||||
<span>基础平台预设商品库</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="metric-card" styles={{ body: { padding: '16px 20px' } }}>
|
||||
<div className="metric-card-header">
|
||||
<span className="metric-card-title">双平台优先策略</span>
|
||||
<div className="metric-card-icon metric-card-icon--purple">
|
||||
<SwapOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric-card-value">{metrics.priorityCount}</div>
|
||||
<div className="metric-card-footer">
|
||||
<span>乐玩与DLC高可用智能切换</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="metric-card" styles={{ body: { padding: '16px 20px' } }}>
|
||||
<div className="metric-card-header">
|
||||
<span className="metric-card-title">仅乐玩平台履约</span>
|
||||
<div className="metric-card-icon metric-card-icon--emerald">
|
||||
<CheckCircleOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric-card-value">{metrics.lewanOnlyCount}</div>
|
||||
<div className="metric-card-footer">
|
||||
<span>专属乐玩上游独占商品</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="metric-card" styles={{ body: { padding: '16px 20px' } }}>
|
||||
<div className="metric-card-header">
|
||||
<span className="metric-card-title">仅第一平台 (DLC)</span>
|
||||
<div className="metric-card-icon metric-card-icon--amber">
|
||||
<DesktopOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric-card-value">{metrics.dlcOnlyCount}</div>
|
||||
<div className="metric-card-footer">
|
||||
<span>原有第一平台商品通道</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 筛选与搜索工具条 */}
|
||||
<Card
|
||||
styles={{ body: { padding: '16px 20px' } }}
|
||||
style={{ marginBottom: 16, borderRadius: 10, border: '1px solid #e2e8f0' }}
|
||||
>
|
||||
<Row gutter={[16, 12]} align="middle" justify="space-between">
|
||||
<Col xs={24} md={14}>
|
||||
<Space size="middle" wrap>
|
||||
<Input
|
||||
placeholder="搜索商品名称 / SKU / ID"
|
||||
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
style={{ width: 240 }}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
value={filterMode}
|
||||
onChange={setFilterMode}
|
||||
style={{ width: 180 }}
|
||||
options={[
|
||||
{ value: 'all', label: '所有履约策略' },
|
||||
{ value: 'priority', label: '双平台优先 (Priority)' },
|
||||
{ value: 'lewan', label: '仅乐玩开放平台' },
|
||||
{ value: 'dlc', label: '仅第一平台 (DLC)' },
|
||||
]}
|
||||
/>
|
||||
{categories.length > 0 && (
|
||||
<Select
|
||||
value={filterCategory}
|
||||
onChange={setFilterCategory}
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: 'all', label: '全部分类' },
|
||||
...categories.map((c) => ({ value: c, label: c })),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} md={10} style={{ textAlign: 'right' }}>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
共检索到 <strong style={{ color: '#0f172a' }}>{filteredCatalog.length}</strong> 件商品
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 主商品表格 */}
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={filteredCatalog}
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
showTotal: (total) => `共 ${total} 件自营商品`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 渠道与优先级配置 Modal */}
|
||||
<Modal
|
||||
title={
|
||||
<Space align="center">
|
||||
<SettingOutlined style={{ color: '#2563eb' }} />
|
||||
<span>配置履约渠道与优先级</span>
|
||||
{configuringItem && (
|
||||
<Tag color="blue">{configuringItem.display_name} ({configuringItem.sku})</Tag>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
open={configModalOpen}
|
||||
onOk={submitConfig}
|
||||
onCancel={() => setConfigModalOpen(false)}
|
||||
confirmLoading={configSubmitting}
|
||||
destroyOnClose
|
||||
width={720}
|
||||
okText="保存配置"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form
|
||||
form={configForm}
|
||||
layout="vertical"
|
||||
style={{ marginTop: 16 }}
|
||||
initialValues={{
|
||||
routing_mode: 'priority',
|
||||
priority_order: 'lewan_first',
|
||||
delivery_type: 'LINK_BINDING',
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="routing_mode"
|
||||
label={<strong style={{ fontSize: 14 }}>履约模式 (Routing Mode)</strong>}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group style={{ width: '100%' }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Radio value="priority" style={{ padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 8, width: '100%' }}>
|
||||
<div>
|
||||
<strong style={{ color: '#0f172a' }}>双平台优先策略 (Priority Routing)</strong>
|
||||
<div style={{ color: '#64748b', fontSize: 12, marginTop: 2 }}>
|
||||
两平台都有货时首选指定优先级平台发货;当首选平台无库存或异常时,自动平滑兜底至备用平台。
|
||||
</div>
|
||||
</div>
|
||||
</Radio>
|
||||
<Radio value="lewan" style={{ padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 8, width: '100%' }}>
|
||||
<div>
|
||||
<strong style={{ color: '#0f172a' }}>仅乐玩开放平台 (Lewan Only)</strong>
|
||||
<div style={{ color: '#64748b', fontSize: 12, marginTop: 2 }}>
|
||||
所有订单均通过乐玩开放平台发货(需配置乐玩商品ID)。
|
||||
</div>
|
||||
</div>
|
||||
</Radio>
|
||||
<Radio value="dlc" style={{ padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 8, width: '100%' }}>
|
||||
<div>
|
||||
<strong style={{ color: '#0f172a' }}>仅第一平台 (DLC Only)</strong>
|
||||
<div style={{ color: '#64748b', fontSize: 12, marginTop: 2 }}>
|
||||
所有订单均通过第一平台 (DLC / Unified BFF) 进行履约。
|
||||
</div>
|
||||
</div>
|
||||
</Radio>
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{/* 当选择 priority 时展示优先级顺序选择 */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prev, cur) => prev.routing_mode !== cur.routing_mode}
|
||||
>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('routing_mode') === 'priority' ? (
|
||||
<Form.Item
|
||||
name="priority_order"
|
||||
label={<strong style={{ fontSize: 14 }}>双平台优先级顺序</strong>}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group orientation="vertical" style={{ width: '100%' }}>
|
||||
<Radio value="lewan_first">
|
||||
<span><strong>乐玩优先 ➔ 第一平台 (DLC) 兜底</strong> (推荐: 乐玩直接授权,成本透明)</span>
|
||||
</Radio>
|
||||
<Radio value="dlc_first" style={{ marginTop: 8 }}>
|
||||
<span><strong>第一平台 (DLC) 优先 ➔ 乐玩兜底</strong> (优先走老平台)</span>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
<Divider titlePlacement="start" style={{ margin: '16px 0', fontSize: 13, color: '#64748b' }}>
|
||||
渠道商品映射信息
|
||||
</Divider>
|
||||
|
||||
<Alert
|
||||
message="渠道映射说明"
|
||||
description="乐玩平台需要指定对应的商品ID(可在右侧“拉取乐玩商品库”中一键关联或复制);第一平台若留空则自动按系统内置的标准 SKU 映射表匹配。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="lewan_product_id"
|
||||
label="乐玩商品ID (UUID)"
|
||||
extra={
|
||||
<Space style={{ marginTop: 4 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
若该商品参与乐玩履约,请输入乐玩后台的商品 UUID
|
||||
</Text>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SearchOutlined />}
|
||||
onClick={handleOpenLewanDrawer}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
在乐玩库中查找
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Input placeholder="例如: a1b2c3d4-e5f6-7890-abcd-ef1234567890" allowClear />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={14}>
|
||||
<Form.Item name="lewan_name" label="乐玩商品名称 (选填)">
|
||||
<Input placeholder="乐玩对应的商品名,方便识别" allowClear />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item name="delivery_type" label="发货交付模式">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'LINK_BINDING', label: '链接授权绑定 (LINK_BINDING)' },
|
||||
{ value: 'EMAIL_BINDING', label: '邮箱绑定 (暂未接入)', disabled: true },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="dlc_good_id"
|
||||
label="第一平台 (DLC) GoodID (选填)"
|
||||
extra="留空将按系统内置预设映射自动选择(例如 yg_60_points 对应 1936306173007360000)"
|
||||
>
|
||||
<Input placeholder="留空使用预设映射,或手动指定数字ID" allowClear />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 乐玩商品库抽屉 */}
|
||||
<Drawer
|
||||
title={
|
||||
<Space align="center">
|
||||
<CloudDownloadOutlined style={{ color: '#2563eb' }} />
|
||||
<span>乐玩开放平台商品库 (实时拉取)</span>
|
||||
</Space>
|
||||
}
|
||||
placement="right"
|
||||
width={780}
|
||||
open={lewanDrawerOpen}
|
||||
onClose={() => setLewanDrawerOpen(false)}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
size="small"
|
||||
onClick={() => loadLewanProducts(lewanPage, lewanKeyword)}
|
||||
loading={lewanLoading}
|
||||
>
|
||||
刷新乐玩库
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Paragraph type="secondary" style={{ fontSize: 13, marginBottom: 16 }}>
|
||||
此处实时连通乐玩开放平台 API 拉取最新可供货商品。您可以点击「导入为自营」直接生成自营商品,或在配置弹窗中直接关联该商品。
|
||||
</Paragraph>
|
||||
|
||||
<Space style={{ marginBottom: 16, width: '100%' }}>
|
||||
<Input
|
||||
placeholder="按乐玩商品名称或关键字搜索"
|
||||
prefix={<SearchOutlined />}
|
||||
value={lewanKeyword}
|
||||
onChange={(e) => setLewanKeyword(e.target.value)}
|
||||
onPressEnter={() => loadLewanProducts(1, lewanKeyword)}
|
||||
style={{ width: 320 }}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => loadLewanProducts(1, lewanKeyword)}
|
||||
loading={lewanLoading}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={lewanLoading}
|
||||
columns={lewanColumns}
|
||||
dataSource={lewanProducts}
|
||||
pagination={{
|
||||
current: lewanPage,
|
||||
pageSize: 20,
|
||||
total: lewanTotal,
|
||||
onChange: (page) => loadLewanProducts(page, lewanKeyword),
|
||||
showTotal: (total) => `共 ${total} 件乐玩上游商品`,
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
{/* 导入乐玩商品至自营目录 Modal */}
|
||||
<Modal
|
||||
title="导入乐玩商品至自营商品目录"
|
||||
open={syncModalOpen}
|
||||
onOk={submitSync}
|
||||
onCancel={() => setSyncModalOpen(false)}
|
||||
confirmLoading={syncSubmitting}
|
||||
destroyOnClose
|
||||
width={560}
|
||||
okText="确认导入"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form form={syncForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="lewan_product_id" label="乐玩商品ID" rules={[{ required: true }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="自营显示名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="category" label="分类">
|
||||
<Input placeholder="如:游戏道具 / 月卡" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="sku" label="自定义平台 SKU" rules={[{ required: true }]}>
|
||||
<Input placeholder="大写英文或拼音" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="price_yuan" label="参考售价 (元)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} step={0.01} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="delivery_type" label="发货模式">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'LINK_BINDING', label: '链接授权绑定' },
|
||||
{ value: 'EMAIL_BINDING', label: '邮箱绑定 (暂未接入)', disabled: true },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Alert
|
||||
message="导入提示"
|
||||
description="导入后,此商品将默认以「乐玩平台」作为第一优先级渠道,并进入自营商品目录供各下游商户分配。"
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -84,6 +84,15 @@ export interface MerchantProduct {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ProductRoutingConfig {
|
||||
routing_mode: 'priority' | 'lewan' | 'dlc'
|
||||
priority?: string[]
|
||||
dlc_good_id?: string
|
||||
lewan_product_id?: string
|
||||
lewan_name?: string
|
||||
delivery_type?: string
|
||||
}
|
||||
|
||||
export interface ProductCatalogItem {
|
||||
id: number
|
||||
product_id: number
|
||||
@@ -95,6 +104,8 @@ export interface ProductCatalogItem {
|
||||
currency: string
|
||||
stock: number
|
||||
status: 'active' | 'inactive'
|
||||
fulfillment_config?: string
|
||||
routing?: ProductRoutingConfig
|
||||
}
|
||||
|
||||
export interface WalletAccount {
|
||||
@@ -336,3 +347,42 @@ export interface AlertChannelInput {
|
||||
key?: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface LewanProductItem {
|
||||
id: string
|
||||
name: string
|
||||
categoryName?: string
|
||||
categoryId?: string
|
||||
deliveryType?: string
|
||||
/** 乐玩供货单价(元,字符串,如 "12.50") */
|
||||
effectiveUnitPrice?: string
|
||||
stock?: number
|
||||
accessMode?: string
|
||||
thumbnailUrl?: string
|
||||
summary?: string
|
||||
}
|
||||
|
||||
export interface LewanProductListResult {
|
||||
items: LewanProductItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface UpdateProductRoutingInput {
|
||||
routing_mode: 'priority' | 'lewan' | 'dlc'
|
||||
priority?: string[]
|
||||
dlc_good_id?: string
|
||||
lewan_product_id?: string
|
||||
lewan_name?: string
|
||||
delivery_type?: string
|
||||
}
|
||||
|
||||
export interface SyncLewanProductInput {
|
||||
lewan_product_id: string
|
||||
name: string
|
||||
category?: string
|
||||
sku?: string
|
||||
price?: number
|
||||
delivery_type?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user