重构:affiliate-dash 平台配置从履约配置页移至平台配置页

- AdminPlatformShopsPage 新增 affiliateDash tab + AffiliateDashPlatformPanel(密钥/钱包/商品/映射/测试)
- AdminPlatformFulfillmentPage 移除 affiliate-dash tab 与组件
- 后端与 API 路径不变(本就挂 /platform-config),纯前端 UI 归位
- 前端 tsc -b 通过
This commit is contained in:
yml2213
2026-08-05 17:45:44 +08:00
parent 0f81380d9c
commit 5f5a1a8bcf
3 changed files with 316 additions and 289 deletions
@@ -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` tablabel「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` 通过;后端未改动。