重构:affiliate-dash 平台配置从履约配置页移至平台配置页
- AdminPlatformShopsPage 新增 affiliateDash tab + AffiliateDashPlatformPanel(密钥/钱包/商品/映射/测试) - AdminPlatformFulfillmentPage 移除 affiliate-dash tab 与组件 - 后端与 API 路径不变(本就挂 /platform-config),纯前端 UI 归位 - 前端 tsc -b 通过
This commit is contained in:
@@ -29,28 +29,18 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import PageHeader from '@/components/admin/PageHeader'
|
import PageHeader from '@/components/admin/PageHeader'
|
||||||
import { showError, showSuccess } from '@/lib/feedback'
|
import { showError, showSuccess } from '@/lib/feedback'
|
||||||
import {
|
import {
|
||||||
fetchAdminAffiliateDashConfig,
|
|
||||||
fetchAdminAffiliateDashProducts,
|
|
||||||
fetchAdminAffiliateDashWallet,
|
|
||||||
fetchAdminCloudtentaclesOverrideRules,
|
fetchAdminCloudtentaclesOverrideRules,
|
||||||
fetchAdminCloudtentaclesSkuList,
|
fetchAdminCloudtentaclesSkuList,
|
||||||
fetchAdminCloudtentaclesSourceConfig,
|
fetchAdminCloudtentaclesSourceConfig,
|
||||||
fetchAdminFulfillmentRoutingConfig,
|
fetchAdminFulfillmentRoutingConfig,
|
||||||
fetchAdminKuaishouFeifeiConfig,
|
fetchAdminKuaishouFeifeiConfig,
|
||||||
matchAdminAffiliateDashSku,
|
|
||||||
matchAdminKuaishouFeifeiProduct,
|
matchAdminKuaishouFeifeiProduct,
|
||||||
previewAdminFulfillmentRouting,
|
previewAdminFulfillmentRouting,
|
||||||
saveAdminAffiliateDashConfig,
|
|
||||||
saveAdminCloudtentaclesOverrideRules,
|
saveAdminCloudtentaclesOverrideRules,
|
||||||
saveAdminFulfillmentRoutingConfig,
|
saveAdminFulfillmentRoutingConfig,
|
||||||
syncAdminKuaishouFeifeiProducts,
|
syncAdminKuaishouFeifeiProducts,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
import type {
|
import type {
|
||||||
AdminAffiliateDashConfig,
|
|
||||||
AdminAffiliateDashConfigResponse,
|
|
||||||
AdminAffiliateDashMatchResult,
|
|
||||||
AdminAffiliateDashProductItem,
|
|
||||||
AdminAffiliateDashWallet,
|
|
||||||
AdminCloudtentaclesOverrideDeliveryItem,
|
AdminCloudtentaclesOverrideDeliveryItem,
|
||||||
AdminCloudtentaclesOverrideRule,
|
AdminCloudtentaclesOverrideRule,
|
||||||
AdminCloudtentaclesSessionItem,
|
AdminCloudtentaclesSessionItem,
|
||||||
@@ -113,11 +103,6 @@ export default function AdminPlatformFulfillmentPage() {
|
|||||||
label: 'kuaishou-feifei',
|
label: 'kuaishou-feifei',
|
||||||
children: <KuaishouFeifeiFulfillmentPanel />,
|
children: <KuaishouFeifeiFulfillmentPanel />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'affiliate-dash',
|
|
||||||
label: 'affiliate-dash',
|
|
||||||
children: <AffiliateDashFulfillmentPanel />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'kuaishou-cloud',
|
key: 'kuaishou-cloud',
|
||||||
label: 'kuaishou-lewan',
|
label: 'kuaishou-lewan',
|
||||||
@@ -1332,277 +1317,3 @@ function createRuleId() {
|
|||||||
return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SkuMappingRow = { productNo: string; sku: string }
|
|
||||||
|
|
||||||
function AffiliateDashFulfillmentPanel() {
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
|
||||||
const [enabled, setEnabled] = useState(true)
|
|
||||||
const [baseUrl, setBaseUrl] = useState('')
|
|
||||||
const [appKey, setAppKey] = useState('')
|
|
||||||
const [appSecret, setAppSecret] = useState('')
|
|
||||||
const [callbackSecret, setCallbackSecret] = useState('')
|
|
||||||
const [timeoutMs, setTimeoutMs] = useState(10000)
|
|
||||||
const [notifyUrl, setNotifyUrl] = useState('')
|
|
||||||
const [timestampToleranceSeconds, setTimestampToleranceSeconds] = useState(300)
|
|
||||||
const [skuRows, setSkuRows] = useState<SkuMappingRow[]>([])
|
|
||||||
const [matchInput, setMatchInput] = useState('')
|
|
||||||
const [matchResult, setMatchResult] = useState<AdminAffiliateDashMatchResult | null>(null)
|
|
||||||
const [wallet, setWallet] = useState<AdminAffiliateDashWallet | null>(null)
|
|
||||||
const [products, setProducts] = useState<AdminAffiliateDashProductItem[]>([])
|
|
||||||
const [productTotal, setProductTotal] = useState(0)
|
|
||||||
const [productPage, setProductPage] = useState(1)
|
|
||||||
const [productsLoading, setProductsLoading] = useState(false)
|
|
||||||
|
|
||||||
const skuMapping = skuRows.reduce<Record<string, string>>((acc, row) => {
|
|
||||||
if (row.productNo.trim() && row.sku.trim()) {
|
|
||||||
acc[row.productNo.trim()] = row.sku.trim()
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void loadConfig()
|
|
||||||
void loadWallet()
|
|
||||||
void loadProducts(1)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
async function loadConfig() {
|
|
||||||
setLoading(true)
|
|
||||||
setErrorMessage('')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchAdminAffiliateDashConfig()
|
|
||||||
hydrateConfig(response.data)
|
|
||||||
} catch (error) {
|
|
||||||
setErrorMessage(error instanceof Error ? error.message : '读取 affiliate-dash 配置失败')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hydrateConfig(response: AdminAffiliateDashConfigResponse) {
|
|
||||||
const source = response.source
|
|
||||||
setEnabled(source.enabled !== false)
|
|
||||||
setBaseUrl(source.baseUrl || '')
|
|
||||||
setAppKey(source.appKey || '')
|
|
||||||
setAppSecret(source.appSecret || '')
|
|
||||||
setCallbackSecret(source.callbackSecret || '')
|
|
||||||
setTimeoutMs(Number(source.timeoutMs || 10000))
|
|
||||||
setNotifyUrl(source.notifyUrl || '')
|
|
||||||
setTimestampToleranceSeconds(Number(source.timestampToleranceSeconds || 300))
|
|
||||||
setSkuRows(
|
|
||||||
Object.entries(source.skuMapping || {}).map(([productNo, sku]) => ({
|
|
||||||
productNo,
|
|
||||||
sku: String(sku || ''),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveConfig() {
|
|
||||||
setSaving(true)
|
|
||||||
setErrorMessage('')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload: AdminAffiliateDashConfig = {
|
|
||||||
enabled,
|
|
||||||
baseUrl,
|
|
||||||
appKey,
|
|
||||||
appSecret,
|
|
||||||
callbackSecret,
|
|
||||||
timeoutMs,
|
|
||||||
notifyUrl,
|
|
||||||
timestampToleranceSeconds,
|
|
||||||
skuMapping,
|
|
||||||
}
|
|
||||||
const response = await saveAdminAffiliateDashConfig(payload)
|
|
||||||
hydrateConfig(response.data)
|
|
||||||
showSuccess('affiliate-dash 配置已保存')
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : '保存 affiliate-dash 配置失败'
|
|
||||||
setErrorMessage(message)
|
|
||||||
showError(message)
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function previewMatch() {
|
|
||||||
const productNo = matchInput.trim()
|
|
||||||
if (!productNo) {
|
|
||||||
showError('请输入 91 商品编码(productNo)')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setMatchResult(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await matchAdminAffiliateDashSku(productNo)
|
|
||||||
setMatchResult(response.data)
|
|
||||||
} catch (error) {
|
|
||||||
showError(error instanceof Error ? error.message : '预览 affiliate-dash sku 映射失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadWallet() {
|
|
||||||
try {
|
|
||||||
const response = await fetchAdminAffiliateDashWallet()
|
|
||||||
setWallet(response.data)
|
|
||||||
} catch (error) {
|
|
||||||
showError(error instanceof Error ? error.message : '查询 affiliate-dash 钱包失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadProducts(page: number) {
|
|
||||||
setProductsLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchAdminAffiliateDashProducts({ page, size: 20 })
|
|
||||||
setProducts(response.data.list)
|
|
||||||
setProductTotal(response.data.total)
|
|
||||||
setProductPage(response.data.page)
|
|
||||||
} catch (error) {
|
|
||||||
showError(error instanceof Error ? error.message : '拉取 affiliate-dash 商品失败')
|
|
||||||
} finally {
|
|
||||||
setProductsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSkuRow(index: number, patch: Partial<SkuMappingRow>) {
|
|
||||||
setSkuRows((rows) => rows.map((row, i) => (i === index ? { ...row, ...patch } : row)))
|
|
||||||
}
|
|
||||||
|
|
||||||
const productColumns: TableColumnsType<AdminAffiliateDashProductItem> = [
|
|
||||||
{ title: 'sku', dataIndex: 'sku', key: 'sku', width: 180 },
|
|
||||||
{ title: '名称', dataIndex: 'displayName', key: 'displayName' },
|
|
||||||
{ title: '单价', dataIndex: 'priceAmount', key: 'priceAmount', width: 90 },
|
|
||||||
{ title: '库存', dataIndex: 'stock', key: 'stock', width: 70, render: (v: number) => (v === -1 ? '不限' : v) },
|
|
||||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80, render: (v: string) => <Tag color={v === 'active' ? 'green' : 'red'}>{v}</Tag> },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title="affiliate-dash 发货平台"
|
|
||||||
extra={
|
|
||||||
<Space>
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={loadWallet}>刷新钱包</Button>
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => loadProducts(productPage)}>刷新商品</Button>
|
|
||||||
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={saveConfig}>
|
|
||||||
保存配置
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{errorMessage ? <Alert type="error" message={errorMessage} showIcon style={{ marginBottom: 16 }} /> : null}
|
|
||||||
|
|
||||||
<Spin spinning={loading}>
|
|
||||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
||||||
<Card size="small" title="对接配置">
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
|
||||||
<Space>
|
|
||||||
<Typography.Text>启用</Typography.Text>
|
|
||||||
<Switch checked={enabled} onChange={setEnabled} />
|
|
||||||
</Space>
|
|
||||||
<Space wrap>
|
|
||||||
<span>BASE_URL</span>
|
|
||||||
<Input style={{ width: 320 }} value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} placeholder="https://skin.khhao.com" />
|
|
||||||
</Space>
|
|
||||||
<Space wrap>
|
|
||||||
<span>App Key</span>
|
|
||||||
<Input style={{ width: 320 }} value={appKey} onChange={(e) => setAppKey(e.target.value)} placeholder="ak_..." />
|
|
||||||
</Space>
|
|
||||||
<Space wrap>
|
|
||||||
<span>App Secret</span>
|
|
||||||
<Input.Password style={{ width: 320 }} value={appSecret} onChange={(e) => setAppSecret(e.target.value)} placeholder="sk_..." />
|
|
||||||
</Space>
|
|
||||||
<Space wrap>
|
|
||||||
<span>回调 Secret</span>
|
|
||||||
<Input.Password style={{ width: 320 }} value={callbackSecret} onChange={(e) => setCallbackSecret(e.target.value)} placeholder="cb_..." />
|
|
||||||
</Space>
|
|
||||||
<Space wrap>
|
|
||||||
<span>超时(ms)</span>
|
|
||||||
<InputNumber min={1000} value={timeoutMs} onChange={(v) => setTimeoutMs(Number(v || 10000))} />
|
|
||||||
<span>时间容差(s)</span>
|
|
||||||
<InputNumber min={1} value={timestampToleranceSeconds} onChange={(v) => setTimestampToleranceSeconds(Number(v || 300))} />
|
|
||||||
</Space>
|
|
||||||
<Space wrap>
|
|
||||||
<span>回调 URL(展示)</span>
|
|
||||||
<Typography.Text type="secondary">{notifyUrl || '未配置'}</Typography.Text>
|
|
||||||
</Space>
|
|
||||||
{wallet ? (
|
|
||||||
<Space wrap>
|
|
||||||
<span>钱包余额</span>
|
|
||||||
<Typography.Text strong>{wallet.availableBalance} {wallet.currency}</Typography.Text>
|
|
||||||
<span>(冻结 {wallet.frozenBalance})</span>
|
|
||||||
</Space>
|
|
||||||
) : null}
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card size="small" title="91 商品编码 → affiliate_dash sku 映射">
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
|
||||||
{skuRows.map((row, index) => (
|
|
||||||
<Space key={`${index}_${row.productNo}`} wrap>
|
|
||||||
<Input
|
|
||||||
style={{ width: 240 }}
|
|
||||||
placeholder="91 商品编码(productNo)"
|
|
||||||
value={row.productNo}
|
|
||||||
onChange={(e) => updateSkuRow(index, { productNo: e.target.value })}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
style={{ width: 240 }}
|
|
||||||
placeholder="affiliate_dash sku"
|
|
||||||
value={row.sku}
|
|
||||||
onChange={(e) => updateSkuRow(index, { sku: e.target.value })}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() => setSkuRows((rows) => rows.filter((_, i) => i !== index))}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
))}
|
|
||||||
<Button icon={<PlusOutlined />} onClick={() => setSkuRows((rows) => [...rows, { productNo: '', sku: '' }])}>
|
|
||||||
新增映射
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card size="small" title="映射测试与商品目录">
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
|
||||||
<Space wrap>
|
|
||||||
<Input
|
|
||||||
style={{ width: 240 }}
|
|
||||||
placeholder="输入 91 商品编码测试"
|
|
||||||
value={matchInput}
|
|
||||||
onChange={(e) => setMatchInput(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button icon={<SearchOutlined />} onClick={previewMatch}>测试映射</Button>
|
|
||||||
{matchResult ? (
|
|
||||||
<Typography.Text type={matchResult.matched ? 'success' : 'warning'}>
|
|
||||||
{matchResult.matched
|
|
||||||
? `命中 sku: ${matchResult.sku}`
|
|
||||||
: `未命中(productNo=${matchResult.productNo || '-'})`}
|
|
||||||
</Typography.Text>
|
|
||||||
) : null}
|
|
||||||
</Space>
|
|
||||||
<Table<AdminAffiliateDashProductItem>
|
|
||||||
rowKey="sku"
|
|
||||||
size="small"
|
|
||||||
loading={productsLoading}
|
|
||||||
columns={productColumns}
|
|
||||||
dataSource={products}
|
|
||||||
pagination={buildAdminLocalTablePagination({
|
|
||||||
current: productPage,
|
|
||||||
pageSize: 20,
|
|
||||||
total: productTotal,
|
|
||||||
onChange: (page) => loadProducts(page),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
</Space>
|
|
||||||
</Spin>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,14 +37,19 @@ import {
|
|||||||
fetchAdminCloudtentaclesAsset,
|
fetchAdminCloudtentaclesAsset,
|
||||||
fetchAdminCloudtentaclesSkuList,
|
fetchAdminCloudtentaclesSkuList,
|
||||||
fetchAdminCloudtentaclesSourceConfig,
|
fetchAdminCloudtentaclesSourceConfig,
|
||||||
|
fetchAdminAffiliateDashConfig,
|
||||||
|
fetchAdminAffiliateDashProducts,
|
||||||
|
fetchAdminAffiliateDashWallet,
|
||||||
fetchAdminKuaishouFeifeiConfig,
|
fetchAdminKuaishouFeifeiConfig,
|
||||||
fetchAdminKuaishouIndustrySourceConfig,
|
fetchAdminKuaishouIndustrySourceConfig,
|
||||||
fetchAdminNotificationConfig,
|
fetchAdminNotificationConfig,
|
||||||
fetchAdminScheduledJobsConfig,
|
fetchAdminScheduledJobsConfig,
|
||||||
|
matchAdminAffiliateDashSku,
|
||||||
matchAdminKuaishouFeifeiProduct,
|
matchAdminKuaishouFeifeiProduct,
|
||||||
refreshAdminKuaishouIndustryAccessToken,
|
refreshAdminKuaishouIndustryAccessToken,
|
||||||
runAdminScheduledJob,
|
runAdminScheduledJob,
|
||||||
saveAdminCloudtentaclesSourceConfig,
|
saveAdminCloudtentaclesSourceConfig,
|
||||||
|
saveAdminAffiliateDashConfig,
|
||||||
saveAdminKuaishouFeifeiConfig,
|
saveAdminKuaishouFeifeiConfig,
|
||||||
saveAdminKuaishouIndustrySourceConfig,
|
saveAdminKuaishouIndustrySourceConfig,
|
||||||
saveAdminNotificationConfig,
|
saveAdminNotificationConfig,
|
||||||
@@ -59,6 +64,11 @@ import type {
|
|||||||
AdminCloudtentaclesMonitorAccount,
|
AdminCloudtentaclesMonitorAccount,
|
||||||
AdminCloudtentaclesSourceConfigResponse,
|
AdminCloudtentaclesSourceConfigResponse,
|
||||||
AdminCloudtentaclesSourceItem,
|
AdminCloudtentaclesSourceItem,
|
||||||
|
AdminAffiliateDashConfig,
|
||||||
|
AdminAffiliateDashConfigResponse,
|
||||||
|
AdminAffiliateDashMatchResult,
|
||||||
|
AdminAffiliateDashProductItem,
|
||||||
|
AdminAffiliateDashWallet,
|
||||||
AdminKuaishouIndustryConfigResponse,
|
AdminKuaishouIndustryConfigResponse,
|
||||||
AdminKuaishouIndustryShopConfig,
|
AdminKuaishouIndustryShopConfig,
|
||||||
AdminKuaishouIndustrySourceConfig,
|
AdminKuaishouIndustrySourceConfig,
|
||||||
@@ -83,6 +93,7 @@ type PlatformTab =
|
|||||||
| 'kuaishouIndustry'
|
| 'kuaishouIndustry'
|
||||||
| 'kuaishouFeifei'
|
| 'kuaishouFeifei'
|
||||||
| 'cloudtentacles'
|
| 'cloudtentacles'
|
||||||
|
| 'affiliateDash'
|
||||||
|
|
||||||
type NotificationConfigResponse = {
|
type NotificationConfigResponse = {
|
||||||
filePath: string
|
filePath: string
|
||||||
@@ -101,6 +112,7 @@ const platformTabs: PlatformTab[] = [
|
|||||||
'kuaishouIndustry',
|
'kuaishouIndustry',
|
||||||
'kuaishouFeifei',
|
'kuaishouFeifei',
|
||||||
'cloudtentacles',
|
'cloudtentacles',
|
||||||
|
'affiliateDash',
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function AdminPlatformShopsPage() {
|
export default function AdminPlatformShopsPage() {
|
||||||
@@ -114,6 +126,8 @@ export default function AdminPlatformShopsPage() {
|
|||||||
const [feifeiConfig, setFeifeiConfig] = useState<AdminKuaishouFeifeiConfigResponse | null>(null)
|
const [feifeiConfig, setFeifeiConfig] = useState<AdminKuaishouFeifeiConfigResponse | null>(null)
|
||||||
const [cloudtentaclesConfig, setCloudtentaclesConfig] =
|
const [cloudtentaclesConfig, setCloudtentaclesConfig] =
|
||||||
useState<AdminCloudtentaclesSourceConfigResponse | null>(null)
|
useState<AdminCloudtentaclesSourceConfigResponse | null>(null)
|
||||||
|
const [affiliateDashConfig, setAffiliateDashConfig] =
|
||||||
|
useState<AdminAffiliateDashConfigResponse | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadConfigs()
|
void loadConfigs()
|
||||||
@@ -135,12 +149,14 @@ export default function AdminPlatformShopsPage() {
|
|||||||
industryResponse,
|
industryResponse,
|
||||||
feifeiResponse,
|
feifeiResponse,
|
||||||
cloudtentaclesResponse,
|
cloudtentaclesResponse,
|
||||||
|
affiliateDashResponse,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchAdminNotificationConfig(),
|
fetchAdminNotificationConfig(),
|
||||||
fetchAdminScheduledJobsConfig(),
|
fetchAdminScheduledJobsConfig(),
|
||||||
fetchAdminKuaishouIndustrySourceConfig(),
|
fetchAdminKuaishouIndustrySourceConfig(),
|
||||||
fetchAdminKuaishouFeifeiConfig(),
|
fetchAdminKuaishouFeifeiConfig(),
|
||||||
fetchAdminCloudtentaclesSourceConfig(),
|
fetchAdminCloudtentaclesSourceConfig(),
|
||||||
|
fetchAdminAffiliateDashConfig(),
|
||||||
])
|
])
|
||||||
|
|
||||||
setNotificationConfig(notificationResponse.data)
|
setNotificationConfig(notificationResponse.data)
|
||||||
@@ -148,6 +164,7 @@ export default function AdminPlatformShopsPage() {
|
|||||||
setIndustryConfig(industryResponse.data)
|
setIndustryConfig(industryResponse.data)
|
||||||
setFeifeiConfig(feifeiResponse.data)
|
setFeifeiConfig(feifeiResponse.data)
|
||||||
setCloudtentaclesConfig(cloudtentaclesResponse.data)
|
setCloudtentaclesConfig(cloudtentaclesResponse.data)
|
||||||
|
setAffiliateDashConfig(affiliateDashResponse.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(error instanceof Error ? error.message : '读取平台配置失败')
|
setErrorMessage(error instanceof Error ? error.message : '读取平台配置失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -227,6 +244,11 @@ export default function AdminPlatformShopsPage() {
|
|||||||
<Empty description="kuaishou-feifei 配置未加载" />
|
<Empty description="kuaishou-feifei 配置未加载" />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'affiliateDash',
|
||||||
|
label: 'affiliate-dash',
|
||||||
|
children: <AffiliateDashPlatformPanel />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'cloudtentacles',
|
key: 'cloudtentacles',
|
||||||
label: 'kuaishou-lewan',
|
label: 'kuaishou-lewan',
|
||||||
@@ -2270,3 +2292,278 @@ function normalizePlatformTab(value: unknown): PlatformTab {
|
|||||||
? (value as PlatformTab)
|
? (value as PlatformTab)
|
||||||
: 'notifications'
|
: 'notifications'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SkuMappingRow = { productNo: string; sku: string }
|
||||||
|
|
||||||
|
function AffiliateDashPlatformPanel() {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
|
const [enabled, setEnabled] = useState(true)
|
||||||
|
const [baseUrl, setBaseUrl] = useState('')
|
||||||
|
const [appKey, setAppKey] = useState('')
|
||||||
|
const [appSecret, setAppSecret] = useState('')
|
||||||
|
const [callbackSecret, setCallbackSecret] = useState('')
|
||||||
|
const [timeoutMs, setTimeoutMs] = useState(10000)
|
||||||
|
const [notifyUrl, setNotifyUrl] = useState('')
|
||||||
|
const [timestampToleranceSeconds, setTimestampToleranceSeconds] = useState(300)
|
||||||
|
const [skuRows, setSkuRows] = useState<SkuMappingRow[]>([])
|
||||||
|
const [matchInput, setMatchInput] = useState('')
|
||||||
|
const [matchResult, setMatchResult] = useState<AdminAffiliateDashMatchResult | null>(null)
|
||||||
|
const [wallet, setWallet] = useState<AdminAffiliateDashWallet | null>(null)
|
||||||
|
const [products, setProducts] = useState<AdminAffiliateDashProductItem[]>([])
|
||||||
|
const [productTotal, setProductTotal] = useState(0)
|
||||||
|
const [productPage, setProductPage] = useState(1)
|
||||||
|
const [productsLoading, setProductsLoading] = useState(false)
|
||||||
|
|
||||||
|
const skuMapping = skuRows.reduce<Record<string, string>>((acc, row) => {
|
||||||
|
if (row.productNo.trim() && row.sku.trim()) {
|
||||||
|
acc[row.productNo.trim()] = row.sku.trim()
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadConfig()
|
||||||
|
void loadWallet()
|
||||||
|
void loadProducts(1)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
setLoading(true)
|
||||||
|
setErrorMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchAdminAffiliateDashConfig()
|
||||||
|
hydrateConfig(response.data)
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage(error instanceof Error ? error.message : '读取 affiliate-dash 配置失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateConfig(response: AdminAffiliateDashConfigResponse) {
|
||||||
|
const source = response.source
|
||||||
|
setEnabled(source.enabled !== false)
|
||||||
|
setBaseUrl(source.baseUrl || '')
|
||||||
|
setAppKey(source.appKey || '')
|
||||||
|
setAppSecret(source.appSecret || '')
|
||||||
|
setCallbackSecret(source.callbackSecret || '')
|
||||||
|
setTimeoutMs(Number(source.timeoutMs || 10000))
|
||||||
|
setNotifyUrl(source.notifyUrl || '')
|
||||||
|
setTimestampToleranceSeconds(Number(source.timestampToleranceSeconds || 300))
|
||||||
|
setSkuRows(
|
||||||
|
Object.entries(source.skuMapping || {}).map(([productNo, sku]) => ({
|
||||||
|
productNo,
|
||||||
|
sku: String(sku || ''),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
setSaving(true)
|
||||||
|
setErrorMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: AdminAffiliateDashConfig = {
|
||||||
|
enabled,
|
||||||
|
baseUrl,
|
||||||
|
appKey,
|
||||||
|
appSecret,
|
||||||
|
callbackSecret,
|
||||||
|
timeoutMs,
|
||||||
|
notifyUrl,
|
||||||
|
timestampToleranceSeconds,
|
||||||
|
skuMapping,
|
||||||
|
}
|
||||||
|
const response = await saveAdminAffiliateDashConfig(payload)
|
||||||
|
hydrateConfig(response.data)
|
||||||
|
showSuccess('affiliate-dash 配置已保存')
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : '保存 affiliate-dash 配置失败'
|
||||||
|
setErrorMessage(message)
|
||||||
|
showError(message)
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function previewMatch() {
|
||||||
|
const productNo = matchInput.trim()
|
||||||
|
if (!productNo) {
|
||||||
|
showError('请输入 91 商品编码(productNo)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setMatchResult(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await matchAdminAffiliateDashSku(productNo)
|
||||||
|
setMatchResult(response.data)
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : '预览 affiliate-dash sku 映射失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWallet() {
|
||||||
|
try {
|
||||||
|
const response = await fetchAdminAffiliateDashWallet()
|
||||||
|
setWallet(response.data)
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : '查询 affiliate-dash 钱包失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProducts(page: number) {
|
||||||
|
setProductsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchAdminAffiliateDashProducts({ page, size: 20 })
|
||||||
|
setProducts(response.data.list)
|
||||||
|
setProductTotal(response.data.total)
|
||||||
|
setProductPage(response.data.page)
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : '拉取 affiliate-dash 商品失败')
|
||||||
|
} finally {
|
||||||
|
setProductsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSkuRow(index: number, patch: Partial<SkuMappingRow>) {
|
||||||
|
setSkuRows((rows) => rows.map((row, i) => (i === index ? { ...row, ...patch } : row)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const productColumns: TableColumnsType<AdminAffiliateDashProductItem> = [
|
||||||
|
{ title: 'sku', dataIndex: 'sku', key: 'sku', width: 180 },
|
||||||
|
{ title: '名称', dataIndex: 'displayName', key: 'displayName' },
|
||||||
|
{ title: '单价', dataIndex: 'priceAmount', key: 'priceAmount', width: 90 },
|
||||||
|
{ title: '库存', dataIndex: 'stock', key: 'stock', width: 70, render: (v: number) => (v === -1 ? '不限' : v) },
|
||||||
|
{ title: '状态', dataIndex: 'status', key: 'status', width: 80, render: (v: string) => <Tag color={v === 'active' ? 'green' : 'red'}>{v}</Tag> },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title="affiliate-dash 发货平台"
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={loadWallet}>刷新钱包</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => loadProducts(productPage)}>刷新商品</Button>
|
||||||
|
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={saveConfig}>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{errorMessage ? <Alert type="error" message={errorMessage} showIcon style={{ marginBottom: 16 }} /> : null}
|
||||||
|
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
|
<Card size="small" title="对接配置">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
|
<Space>
|
||||||
|
<Typography.Text>启用</Typography.Text>
|
||||||
|
<Switch checked={enabled} onChange={setEnabled} />
|
||||||
|
</Space>
|
||||||
|
<Space wrap>
|
||||||
|
<span>BASE_URL</span>
|
||||||
|
<Input style={{ width: 320 }} value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} placeholder="https://skin.khhao.com" />
|
||||||
|
</Space>
|
||||||
|
<Space wrap>
|
||||||
|
<span>App Key</span>
|
||||||
|
<Input style={{ width: 320 }} value={appKey} onChange={(e) => setAppKey(e.target.value)} placeholder="ak_..." />
|
||||||
|
</Space>
|
||||||
|
<Space wrap>
|
||||||
|
<span>App Secret</span>
|
||||||
|
<Input.Password style={{ width: 320 }} value={appSecret} onChange={(e) => setAppSecret(e.target.value)} placeholder="sk_..." />
|
||||||
|
</Space>
|
||||||
|
<Space wrap>
|
||||||
|
<span>回调 Secret</span>
|
||||||
|
<Input.Password style={{ width: 320 }} value={callbackSecret} onChange={(e) => setCallbackSecret(e.target.value)} placeholder="cb_..." />
|
||||||
|
</Space>
|
||||||
|
<Space wrap>
|
||||||
|
<span>超时(ms)</span>
|
||||||
|
<InputNumber min={1000} value={timeoutMs} onChange={(v) => setTimeoutMs(Number(v || 10000))} />
|
||||||
|
<span>时间容差(s)</span>
|
||||||
|
<InputNumber min={1} value={timestampToleranceSeconds} onChange={(v) => setTimestampToleranceSeconds(Number(v || 300))} />
|
||||||
|
</Space>
|
||||||
|
<Space wrap>
|
||||||
|
<span>回调 URL(展示)</span>
|
||||||
|
<Typography.Text type="secondary">{notifyUrl || '未配置'}</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
{wallet ? (
|
||||||
|
<Space wrap>
|
||||||
|
<span>钱包余额</span>
|
||||||
|
<Typography.Text strong>{wallet.availableBalance} {wallet.currency}</Typography.Text>
|
||||||
|
<span>(冻结 {wallet.frozenBalance})</span>
|
||||||
|
</Space>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="small" title="91 商品编码 → affiliate_dash sku 映射">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
|
{skuRows.map((row, index) => (
|
||||||
|
<Space key={`${index}_${row.productNo}`} wrap>
|
||||||
|
<Input
|
||||||
|
style={{ width: 240 }}
|
||||||
|
placeholder="91 商品编码(productNo)"
|
||||||
|
value={row.productNo}
|
||||||
|
onChange={(e) => updateSkuRow(index, { productNo: e.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
style={{ width: 240 }}
|
||||||
|
placeholder="affiliate_dash sku"
|
||||||
|
value={row.sku}
|
||||||
|
onChange={(e) => updateSkuRow(index, { sku: e.target.value })}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => setSkuRows((rows) => rows.filter((_, i) => i !== index))}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
<Button icon={<PlusOutlined />} onClick={() => setSkuRows((rows) => [...rows, { productNo: '', sku: '' }])}>
|
||||||
|
新增映射
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="small" title="映射测试与商品目录">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
|
<Space wrap>
|
||||||
|
<Input
|
||||||
|
style={{ width: 240 }}
|
||||||
|
placeholder="输入 91 商品编码测试"
|
||||||
|
value={matchInput}
|
||||||
|
onChange={(e) => setMatchInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button icon={<SearchOutlined />} onClick={previewMatch}>测试映射</Button>
|
||||||
|
{matchResult ? (
|
||||||
|
<Typography.Text type={matchResult.matched ? 'success' : 'warning'}>
|
||||||
|
{matchResult.matched
|
||||||
|
? `命中 sku: ${matchResult.sku}`
|
||||||
|
: `未命中(productNo=${matchResult.productNo || '-'})`}
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
<Table<AdminAffiliateDashProductItem>
|
||||||
|
rowKey="sku"
|
||||||
|
size="small"
|
||||||
|
loading={productsLoading}
|
||||||
|
columns={productColumns}
|
||||||
|
dataSource={products}
|
||||||
|
pagination={buildAdminLocalTablePagination({
|
||||||
|
current: productPage,
|
||||||
|
pageSize: 20,
|
||||||
|
total: productTotal,
|
||||||
|
onChange: (page) => loadProducts(page),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
</Spin>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -712,3 +712,22 @@ order_site 处理要求:
|
|||||||
| `not_a_real_sku` | (无)回退 | — | — |
|
| `not_a_real_sku` | (无)回退 | — | — |
|
||||||
|
|
||||||
**验证**:typecheck 通过;后端全量 224 测试通过(透传纯函数测试 +3)。
|
**验证**:typecheck 通过;后端全量 224 测试通过(透传纯函数测试 +3)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 24. affiliate-dash 配置位置重构(v2.9 · 已完成)
|
||||||
|
|
||||||
|
**需求**:affiliate_dash 平台配置原先杂糅在「履约配置」页(`AdminPlatformFulfillmentPage` 的 affiliate-dash tab),与履约路由/通道混在一起;项目已有独立的「平台配置」页(`AdminPlatformShopsPage`,按平台管理来源接入、凭据与店铺能力),应归位。
|
||||||
|
|
||||||
|
**改动**(纯前端 UI 位置移动,后端与数据通路零变化——后端路由与前端 API 本就挂在 `/api/v1/admin/platform-config/affiliate-dash/*`):
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
| --- | --- |
|
||||||
|
| `AdminPlatformShopsPage.tsx` | 新增 `affiliateDash` tab(label「affiliate-dash」);`AffiliateDashPlatformPanel` 组件整体迁入(自管理状态:对接配置/密钥/钱包/商品目录/映射表/映射测试),加载函数并入 `loadConfigs` 的 `Promise.all` |
|
||||||
|
| `AdminPlatformFulfillmentPage.tsx` | 移除 affiliate-dash tab、`AffiliateDashFulfillmentPanel` 组件及全部 affiliate 相关 import/类型 |
|
||||||
|
|
||||||
|
**页面归属**:
|
||||||
|
- **平台配置**(`AdminPlatformShopsPage`):affiliate-dash、kuaishou-feifei、kuaishou-lewan、行业电子凭证、内部通知 —— 平台接入凭据
|
||||||
|
- **履约配置**(`AdminPlatformFulfillmentPage`):履约路由、kuaishou-feifei 履约、kuaishou-lewan 履约 —— 通道路由与履约参数
|
||||||
|
|
||||||
|
**验证**:前端 `tsc -b --noEmit` 通过;后端未改动。
|
||||||
|
|||||||
Reference in New Issue
Block a user