新增 affiliate_dash 履约配置与商品映射(阶段 3)
- bootstrap CORE_PROFILES + routing ROUTABLE/DEFAULT_PRIORITY 接入 affiliate_dash - planner 上下文 affiliateDash 块 + resolveDynamicAffiliateDashProfile - product-resolution: 91 productNo→sku 映射匹配(skuMapping) + snapshot.affiliateDash - prepare 前余额预检(getAffiliateDashWallet) - AFFILIATE_DASH_SKU_MAPPING_JSON env 注入 - admin 后端接口(GET/POST 配置、match、products、wallet)+ 前端 Tabs 面板 - 联调:模拟 91 进单命中 affiliate_dash 路由, 未命中回退;前后端 typecheck + 212 测试通过
This commit is contained in:
@@ -29,18 +29,28 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminAffiliateDashConfig,
|
||||
fetchAdminAffiliateDashProducts,
|
||||
fetchAdminAffiliateDashWallet,
|
||||
fetchAdminCloudtentaclesOverrideRules,
|
||||
fetchAdminCloudtentaclesSkuList,
|
||||
fetchAdminCloudtentaclesSourceConfig,
|
||||
fetchAdminFulfillmentRoutingConfig,
|
||||
fetchAdminKuaishouFeifeiConfig,
|
||||
matchAdminAffiliateDashSku,
|
||||
matchAdminKuaishouFeifeiProduct,
|
||||
previewAdminFulfillmentRouting,
|
||||
saveAdminAffiliateDashConfig,
|
||||
saveAdminCloudtentaclesOverrideRules,
|
||||
saveAdminFulfillmentRoutingConfig,
|
||||
syncAdminKuaishouFeifeiProducts,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminAffiliateDashConfig,
|
||||
AdminAffiliateDashConfigResponse,
|
||||
AdminAffiliateDashMatchResult,
|
||||
AdminAffiliateDashProductItem,
|
||||
AdminAffiliateDashWallet,
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesSessionItem,
|
||||
@@ -72,6 +82,7 @@ type SourceRow = AdminCloudtentaclesSourceItem & {
|
||||
const ROUTING_EXECUTORS = [
|
||||
{ key: 'kuaishou_ct_assisted', label: 'kuaishou-lewan', color: 'blue' },
|
||||
{ key: 'kuaishou_feifei', label: 'kuaishou-feifei', color: 'purple' },
|
||||
{ key: 'affiliate_dash', label: 'affiliate-dash', color: 'gold' },
|
||||
{ key: 'manual_dispatch', label: '人工履约', color: 'orange' },
|
||||
]
|
||||
|
||||
@@ -102,6 +113,11 @@ export default function AdminPlatformFulfillmentPage() {
|
||||
label: 'kuaishou-feifei',
|
||||
children: <KuaishouFeifeiFulfillmentPanel />,
|
||||
},
|
||||
{
|
||||
key: 'affiliate-dash',
|
||||
label: 'affiliate-dash',
|
||||
children: <AffiliateDashFulfillmentPanel />,
|
||||
},
|
||||
{
|
||||
key: 'kuaishou-cloud',
|
||||
label: 'kuaishou-lewan',
|
||||
@@ -1315,3 +1331,278 @@ function formatSkuOption(sku: AdminCloudtentaclesSkuItem) {
|
||||
function createRuleId() {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminAffiliateDashConfig,
|
||||
AdminAffiliateDashConfigResponse,
|
||||
AdminAffiliateDashMatchResult,
|
||||
AdminAffiliateDashProductListResult,
|
||||
AdminAffiliateDashWallet,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminAffiliateDashConfig() {
|
||||
return apiGet<AdminAffiliateDashConfigResponse>(
|
||||
'/api/v1/admin/platform-config/affiliate-dash',
|
||||
)
|
||||
}
|
||||
|
||||
export function saveAdminAffiliateDashConfig(payload: AdminAffiliateDashConfig) {
|
||||
return apiPost<AdminAffiliateDashConfigResponse>(
|
||||
'/api/v1/admin/platform-config/affiliate-dash',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function matchAdminAffiliateDashSku(productNo: string) {
|
||||
return apiPost<AdminAffiliateDashMatchResult>(
|
||||
'/api/v1/admin/platform-config/affiliate-dash/match',
|
||||
{ productNo },
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminAffiliateDashProducts(payload: {
|
||||
page?: number
|
||||
size?: number
|
||||
} = {}) {
|
||||
return apiPost<AdminAffiliateDashProductListResult>(
|
||||
'/api/v1/admin/platform-config/affiliate-dash/products',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminAffiliateDashWallet() {
|
||||
return apiPost<AdminAffiliateDashWallet>(
|
||||
'/api/v1/admin/platform-config/affiliate-dash/wallet',
|
||||
{},
|
||||
)
|
||||
}
|
||||
@@ -2,5 +2,6 @@ export * from './notifications'
|
||||
export * from './scheduled-jobs'
|
||||
export * from './kuaishou-industry'
|
||||
export * from './kuaishou-feifei'
|
||||
export * from './affiliate-dash'
|
||||
export * from './cloudtentacles'
|
||||
export * from './fulfillment-routing'
|
||||
|
||||
@@ -99,4 +99,11 @@ export type {
|
||||
AdminFulfillmentRoutingExecutorConfig,
|
||||
AdminFulfillmentRoutingPreviewResult,
|
||||
AdminFulfillmentRoutingRule,
|
||||
AdminAffiliateDashConfig,
|
||||
AdminAffiliateDashEffectiveConfig,
|
||||
AdminAffiliateDashConfigResponse,
|
||||
AdminAffiliateDashMatchResult,
|
||||
AdminAffiliateDashProductItem,
|
||||
AdminAffiliateDashProductListResult,
|
||||
AdminAffiliateDashWallet,
|
||||
} from './platform-config'
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
export interface AdminAffiliateDashConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
appKey: string
|
||||
appSecret: string
|
||||
callbackSecret: string
|
||||
timeoutMs: number
|
||||
notifyUrl: string
|
||||
timestampToleranceSeconds: number
|
||||
skuMapping: Record<string, string>
|
||||
}
|
||||
|
||||
export interface AdminAffiliateDashEffectiveConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
timeoutMs: number
|
||||
notifyUrl: string
|
||||
timestampToleranceSeconds: number
|
||||
hasAppKey: boolean
|
||||
hasAppSecret: boolean
|
||||
hasCallbackSecret: boolean
|
||||
skuMappingCount: number
|
||||
}
|
||||
|
||||
export interface AdminAffiliateDashConfigResponse {
|
||||
source: AdminAffiliateDashConfig
|
||||
effective: AdminAffiliateDashEffectiveConfig
|
||||
}
|
||||
|
||||
export interface AdminAffiliateDashMatchResult {
|
||||
productNo: string
|
||||
sku: string
|
||||
matched: boolean
|
||||
}
|
||||
|
||||
export interface AdminAffiliateDashProductItem {
|
||||
sku: string
|
||||
displayName: string
|
||||
priceAmount: number
|
||||
costAmount: number
|
||||
currency: string
|
||||
stock: number
|
||||
status: string
|
||||
category: string
|
||||
code: string
|
||||
productId: number
|
||||
}
|
||||
|
||||
export interface AdminAffiliateDashProductListResult {
|
||||
list: AdminAffiliateDashProductItem[]
|
||||
total: number
|
||||
page: number
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface AdminAffiliateDashWallet {
|
||||
availableBalance: number
|
||||
frozenBalance: number
|
||||
currency: string
|
||||
}
|
||||
@@ -39,6 +39,16 @@ export type {
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
} from './kuaishou-feifei'
|
||||
|
||||
export type {
|
||||
AdminAffiliateDashConfig,
|
||||
AdminAffiliateDashEffectiveConfig,
|
||||
AdminAffiliateDashConfigResponse,
|
||||
AdminAffiliateDashMatchResult,
|
||||
AdminAffiliateDashProductItem,
|
||||
AdminAffiliateDashProductListResult,
|
||||
AdminAffiliateDashWallet,
|
||||
} from './affiliate-dash'
|
||||
|
||||
export type {
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
|
||||
Reference in New Issue
Block a user