功能:接入乐玩开放平台双渠道路由,并修复渠道锁定与 Webhook 可靠性

在保持 DLC 默认零破坏的前提下,支持 priority/仅乐玩/仅 DLC 履约策略与后台 GUI 配置;同步补齐 Prepare 渠道落库、乐玩订单取消、Webhook 验签与失败重试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-11 15:48:03 +08:00
co-authored by Cursor
parent a314f3f51b
commit f7f48d0feb
22 changed files with 6489 additions and 189 deletions
+9
View File
@@ -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
+13
View File
@@ -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 }),
}
+4 -1
View File
@@ -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',
+919
View File
@@ -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>
)
}
+50
View File
@@ -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
}