优化履约路由并重命名乐玩通道

This commit is contained in:
yml2213
2026-07-09 10:49:33 +08:00
parent 71602683d2
commit 4d5b0d6fc6
28 changed files with 1448 additions and 53 deletions
@@ -411,7 +411,7 @@ export default function AdminTaskDetailPage() {
items={[
{
key: 'cloud',
label: '快手 Cloud',
label: 'kuaishou-lewan',
children: flow ? (
<KuaishouCloudPanel
flow={flow}
@@ -427,7 +427,7 @@ export default function AdminTaskDetailPage() {
}
/>
) : (
<Empty description="无快手 Cloud 履约上下文" />
<Empty description="无 kuaishou-lewan 履约上下文" />
),
},
{
@@ -552,7 +552,7 @@ function TaskActionPanel({
{hasCloudActions ? (
<section className="task-action-group">
<Typography.Text type="secondary"> Cloud</Typography.Text>
<Typography.Text type="secondary">kuaishou-lewan</Typography.Text>
<Space wrap>
{canManageTaskLifecycle && operations.canPrepareKuaishouCloudFulfillment ? (
<Button
@@ -871,7 +871,7 @@ function resolveDeliveryStepSummary(detail: AdminTaskDetail) {
function resolveTaskExecutorDisplay(value: unknown) {
const executorKey = String(value || '').trim()
if (executorKey === 'kuaishou_ct_assisted') {
return { label: '快手 Cloud', color: 'blue' }
return { label: 'kuaishou-lewan', color: 'blue' }
}
if (executorKey === 'kuaishou-industry') {
return { label: '行业电子凭证', color: 'cyan' }
@@ -1129,7 +1129,7 @@ function KuaishouCloudPanel({
return (
<section className="kuaishou-cloud-stack">
<Card
title="快手 Cloud 履约"
title="kuaishou-lewan 履约"
extra={
<Button
icon={<ReloadOutlined />}
@@ -424,7 +424,7 @@ function canRebindListTask(item: AdminTaskListItem, canManageTaskLifecycle: bool
function resolveTaskExecutorDisplay(value: unknown) {
const executorKey = String(value || '').trim()
if (executorKey === 'kuaishou_ct_assisted') {
return { label: '快手 Cloud', color: 'blue' }
return { label: 'kuaishou-lewan', color: 'blue' }
}
if (executorKey === 'kuaishou-industry') {
return { label: '行业电子凭证', color: 'cyan' }
@@ -1,4 +1,6 @@
import {
ArrowDownOutlined,
ArrowUpOutlined,
DeleteOutlined,
PlusOutlined,
ReloadOutlined,
@@ -30,9 +32,12 @@ import {
fetchAdminCloudtentaclesOverrideRules,
fetchAdminCloudtentaclesSkuList,
fetchAdminCloudtentaclesSourceConfig,
fetchAdminFulfillmentRoutingConfig,
fetchAdminKuaishouFeifeiConfig,
matchAdminKuaishouFeifeiProduct,
previewAdminFulfillmentRouting,
saveAdminCloudtentaclesOverrideRules,
saveAdminFulfillmentRoutingConfig,
syncAdminKuaishouFeifeiProducts,
} from '@/services/admin'
import type {
@@ -42,6 +47,9 @@ import type {
AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSkuItem,
AdminCloudtentaclesSourceItem,
AdminFulfillmentRoutingConfig,
AdminFulfillmentRoutingPreviewResult,
AdminFulfillmentRoutingRule,
AdminKuaishouFeifeiConfigResponse,
AdminKuaishouFeifeiMatchResult,
AdminKuaishouFeifeiProductRule,
@@ -51,18 +59,27 @@ import { hasAdminRole } from '@/utils/admin-auth'
type EditableOverrideRule = Omit<AdminCloudtentaclesOverrideRule, 'normalizedProductName'> & {
normalizedProductName?: string
}
type EditableRoutingRule = Omit<AdminFulfillmentRoutingRule, 'normalizedProductName'> & {
normalizedProductName?: string
}
type SourceRow = AdminCloudtentaclesSourceItem & {
session: AdminCloudtentaclesSessionItem | null
ready: boolean
}
const ROUTING_EXECUTORS = [
{ key: 'kuaishou_ct_assisted', label: 'kuaishou-lewan', color: 'blue' },
{ key: 'kuaishou_feifei', label: 'kuaishou-feifei', color: 'purple' },
{ key: 'manual_dispatch', label: '人工履约', color: 'orange' },
]
export default function AdminPlatformFulfillmentPage() {
return (
<section className="page-stack">
<PageHeader
title="履约配置"
description="维护 91 卡券商品到 kuaishou-feifei 与 cloudtentacles 的履约映射规则。"
description="维护 91 卡券商品到 kuaishou-feifei 与 kuaishou-lewan 的履约映射规则。"
/>
{!hasAdminRole('admin') ? (
@@ -74,6 +91,11 @@ export default function AdminPlatformFulfillmentPage() {
className="platform-tabs"
destroyOnHidden={false}
items={[
{
key: 'fulfillment-routing',
label: '履约路由',
children: <FulfillmentRoutingPanel />,
},
{
key: 'kuaishou-feifei',
label: 'kuaishou-feifei',
@@ -81,7 +103,7 @@ export default function AdminPlatformFulfillmentPage() {
},
{
key: 'kuaishou-cloud',
label: 'kuaishou-cloud',
label: 'kuaishou-lewan',
children: <KuaishouCloudFulfillmentPanel />,
},
]}
@@ -91,6 +113,384 @@ export default function AdminPlatformFulfillmentPage() {
)
}
function FulfillmentRoutingPanel() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [previewing, setPreviewing] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [filePath, setFilePath] = useState('')
const [enabled, setEnabled] = useState(true)
const [priority, setPriority] = useState<string[]>(['kuaishou_ct_assisted', 'kuaishou_feifei'])
const [unmatchedExecutorKey, setUnmatchedExecutorKey] = useState('')
const [executors, setExecutors] = useState<AdminFulfillmentRoutingConfig['executors']>({})
const [rules, setRules] = useState<EditableRoutingRule[]>([])
const [previewInput, setPreviewInput] = useState('')
const [previewResult, setPreviewResult] = useState<AdminFulfillmentRoutingPreviewResult | null>(null)
const enabledRuleCount = rules.filter((rule) => rule.enabled !== false).length
const selectedExecutorKey = previewResult?.fulfillmentRoute?.selectedExecutorKey || ''
useEffect(() => {
void loadConfig()
}, [])
async function loadConfig() {
setLoading(true)
setErrorMessage('')
try {
const response = await fetchAdminFulfillmentRoutingConfig()
hydrateConfig(response.data)
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : '读取履约路由配置失败')
} finally {
setLoading(false)
}
}
async function saveConfig() {
setSaving(true)
setErrorMessage('')
try {
const response = await saveAdminFulfillmentRoutingConfig({
enabled,
defaultExecutorPriority: priority,
unmatchedExecutorKey,
executors,
rules: rules.map((rule) => ({
id: rule.id,
enabled: rule.enabled !== false,
productName: rule.productName,
normalizedProductName: rule.normalizedProductName || '',
matchType: rule.matchType,
executorKey: rule.executorKey,
priority: rule.priority,
notes: rule.notes,
})),
})
hydrateConfig(response.data)
showSuccess('履约路由配置已保存')
} catch (error) {
const message = error instanceof Error ? error.message : '保存履约路由配置失败'
setErrorMessage(message)
showError(message)
} finally {
setSaving(false)
}
}
async function previewRoute() {
const productName = previewInput.trim()
if (!productName) {
showError('请输入 91 商品名')
return
}
setPreviewing(true)
setPreviewResult(null)
try {
const response = await previewAdminFulfillmentRouting(productName)
setPreviewResult(response.data)
} catch (error) {
showError(error instanceof Error ? error.message : '预览履约路由失败')
} finally {
setPreviewing(false)
}
}
function hydrateConfig(data: AdminFulfillmentRoutingConfig) {
setFilePath(data.filePath || '')
setEnabled(data.enabled !== false)
setPriority(normalizeRoutingPriority(data.defaultExecutorPriority))
setUnmatchedExecutorKey(data.unmatchedExecutorKey || '')
setExecutors(normalizeRoutingExecutors(data.executors))
setRules(Array.isArray(data.rules) ? data.rules.map((rule) => normalizeRoutingRule(rule)) : [])
}
function movePriority(executorKey: string, offset: -1 | 1) {
const currentIndex = priority.indexOf(executorKey)
const nextIndex = currentIndex + offset
if (currentIndex < 0 || nextIndex < 0 || nextIndex >= priority.length) {
return
}
const next = [...priority]
const [item] = next.splice(currentIndex, 1)
next.splice(nextIndex, 0, item)
setPriority(next)
}
function updateExecutorEnabled(executorKey: string, nextEnabled: boolean) {
setExecutors({
...executors,
[executorKey]: {
enabled: nextEnabled,
},
})
}
function updateRule(index: number, patch: Partial<EditableRoutingRule>) {
setRules((current) =>
current.map((rule, ruleIndex) =>
ruleIndex === index
? {
...rule,
...patch,
}
: rule,
),
)
}
function addRule() {
const productName = previewInput.trim()
const nextId = `${Date.now()}-${rules.length + 1}`
setRules([
{
id: nextId,
enabled: true,
productName,
normalizedProductName: '',
matchType: 'exact',
executorKey: 'kuaishou_feifei',
priority: 100,
notes: '',
},
...rules,
])
}
function removeRule(index: number) {
setRules((current) => current.filter((_, ruleIndex) => ruleIndex !== index))
}
if (loading) {
return <Spin />
}
return (
<section className="platform-panel-stack">
{errorMessage ? <Alert type="error" showIcon message={errorMessage} /> : null}
<div className="metric-grid four">
<MetricCard label="路由状态" value={enabled ? '启用' : '停用'} detail={filePath || '默认配置'} />
<MetricCard label="商品规则" value={String(rules.length)} detail={`启用 ${enabledRuleCount}`} />
<MetricCard
label="全局优先级"
value={`${priority.length} 个通道`}
detail={priority.map(formatRoutingExecutorLabel).join(' > ') || '未配置'}
/>
<Card>
<Space wrap>
<Button icon={<ReloadOutlined />} loading={loading} onClick={loadConfig}>
</Button>
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={saveConfig}>
</Button>
</Space>
</Card>
</div>
<Card title="通道开关">
<div className="platform-routing-grid">
<div className="field-switch">
<span></span>
<Switch checked={enabled} checkedChildren="启用" unCheckedChildren="停用" onChange={setEnabled} />
</div>
{ROUTING_EXECUTORS.map((executor) => (
<div key={executor.key} className="field-switch">
<span>{executor.label}</span>
<Switch
checked={executors[executor.key]?.enabled !== false}
checkedChildren="启用"
unCheckedChildren="停用"
onChange={(checked) => updateExecutorEnabled(executor.key, checked)}
/>
</div>
))}
</div>
</Card>
<Card title="全局优先级">
<Space direction="vertical" className="full-width" size={10}>
{priority.map((executorKey, index) => (
<div key={executorKey} className="platform-routing-priority-row">
<Space>
<Tag color={resolveRoutingExecutorColor(executorKey)}>
{index + 1}. {formatRoutingExecutorLabel(executorKey)}
</Tag>
</Space>
<Space>
<Button
size="small"
icon={<ArrowUpOutlined />}
disabled={index === 0}
onClick={() => movePriority(executorKey, -1)}
/>
<Button
size="small"
icon={<ArrowDownOutlined />}
disabled={index === priority.length - 1}
onClick={() => movePriority(executorKey, 1)}
/>
</Space>
</div>
))}
<Select
value={unmatchedExecutorKey}
options={[
{ label: '未命中时保持未配置', value: '' },
{ label: '未命中时转人工履约', value: 'manual_dispatch' },
]}
onChange={(value) => setUnmatchedExecutorKey(String(value || ''))}
/>
</Space>
</Card>
<Card
title="路由预览"
extra={
<Button icon={<PlusOutlined />} onClick={addRule}>
</Button>
}
>
<Space.Compact className="full-width">
<Input
allowClear
value={previewInput}
placeholder="91 商品名 / productNo"
onChange={(event) => setPreviewInput(event.target.value)}
onPressEnter={previewRoute}
/>
<Button icon={<SearchOutlined />} loading={previewing} onClick={previewRoute}>
</Button>
</Space.Compact>
{previewResult ? (
<Alert
className="platform-section-gap"
type={previewResult.isConfigured ? 'success' : 'warning'}
showIcon
message={
previewResult.isConfigured
? `最终路由:${formatRoutingExecutorLabel(selectedExecutorKey)}`
: '未命中可用履约通道'
}
description={
<Space direction="vertical" size={4}>
<Typography.Text>
{previewResult.fulfillmentRoute?.reason || previewResult.normalizedProductName}
</Typography.Text>
<Space wrap>
{previewResult.fulfillmentRoute?.candidates.map((candidate) => (
<Tag
key={candidate.executorKey}
color={candidate.available ? 'green' : 'default'}
>
{formatRoutingExecutorLabel(candidate.executorKey)}
{candidate.available ? ' 可用' : ' 未命中'}
</Tag>
))}
</Space>
</Space>
}
/>
) : null}
</Card>
<Card
title="商品路由规则"
extra={
<Space wrap>
<Button icon={<PlusOutlined />} onClick={addRule}>
</Button>
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={saveConfig}>
</Button>
</Space>
}
>
{rules.length === 0 ? (
<Empty description="暂无商品路由规则" />
) : (
<Space direction="vertical" className="full-width" size={12}>
{rules.map((rule, index) => (
<div key={rule.id} className="platform-route-rule">
<div className="platform-route-rule-head">
<Space wrap>
<Switch
size="small"
checked={rule.enabled !== false}
onChange={(checked) => updateRule(index, { enabled: checked })}
/>
<Typography.Text strong>{rule.productName || '未命名规则'}</Typography.Text>
</Space>
<Button
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => removeRule(index)}
>
</Button>
</div>
<div className="platform-route-rule-grid">
<Input
value={rule.productName}
placeholder="91 商品名 / productNo"
onChange={(event) => updateRule(index, { productName: event.target.value })}
/>
<Select
value={rule.matchType}
options={[
{ label: '精确', value: 'exact' },
{ label: '包含', value: 'contains' },
]}
onChange={(matchType) =>
updateRule(index, {
matchType: matchType === 'contains' ? 'contains' : 'exact',
})
}
/>
<Select
value={rule.executorKey}
options={ROUTING_EXECUTORS.map((executor) => ({
label: executor.label,
value: executor.key,
}))}
onChange={(executorKey) =>
updateRule(index, { executorKey: String(executorKey || '') })
}
/>
<InputNumber
value={rule.priority}
min={0}
max={9999}
onChange={(priorityValue) =>
updateRule(index, { priority: Number(priorityValue || 0) || 0 })
}
/>
</div>
<Input
value={rule.notes}
placeholder="备注"
onChange={(event) => updateRule(index, { notes: event.target.value })}
/>
</div>
))}
</Space>
)}
</Card>
</section>
)
}
function KuaishouFeifeiFulfillmentPanel() {
const [loading, setLoading] = useState(true)
const [syncing, setSyncing] = useState(false)
@@ -791,6 +1191,58 @@ function MetricCard({ label, value, detail }: { label: string; value: string; de
)
}
function normalizeRoutingPriority(value: unknown) {
const rawItems = Array.isArray(value) ? value : []
const allowed = ROUTING_EXECUTORS
.filter((executor) => executor.key !== 'manual_dispatch')
.map((executor) => executor.key)
const seen = new Set<string>()
const items = rawItems
.map((item) => String(item || '').trim())
.filter((item) => allowed.includes(item) && !seen.has(item) && (seen.add(item), true))
return items.length > 0 ? items : ['kuaishou_ct_assisted', 'kuaishou_feifei']
}
function normalizeRoutingExecutors(value: unknown): AdminFulfillmentRoutingConfig['executors'] {
const source = value && typeof value === 'object' && !Array.isArray(value)
? value as AdminFulfillmentRoutingConfig['executors']
: {}
return ROUTING_EXECUTORS.reduce<AdminFulfillmentRoutingConfig['executors']>((result, executor) => {
result[executor.key] = {
enabled: source[executor.key]?.enabled !== false,
}
return result
}, {})
}
function normalizeRoutingRule(rule: AdminFulfillmentRoutingRule): EditableRoutingRule {
const productName = String(rule.productName || '').trim()
const executorKey = String(rule.executorKey || '').trim()
return {
id: String(rule.id || `${productName}-${executorKey}` || createRuleId()).trim(),
enabled: rule.enabled !== false,
productName,
normalizedProductName: String(rule.normalizedProductName || '').trim(),
matchType: rule.matchType === 'contains' ? 'contains' : 'exact',
executorKey: executorKey || 'kuaishou_feifei',
priority: Number(rule.priority || 0) || 0,
notes: String(rule.notes || '').trim(),
}
}
function formatRoutingExecutorLabel(executorKey: unknown) {
const key = String(executorKey || '').trim()
return ROUTING_EXECUTORS.find((executor) => executor.key === key)?.label || key || '-'
}
function resolveRoutingExecutorColor(executorKey: unknown) {
const key = String(executorKey || '').trim()
return ROUTING_EXECUTORS.find((executor) => executor.key === key)?.color || 'default'
}
function normalizeFeifeiRule(
rule: Partial<AdminKuaishouFeifeiProductRule>,
): AdminKuaishouFeifeiProductRule {
+1 -1
View File
@@ -669,7 +669,7 @@ function ClaimHeaderCard({
return (
<Card className="claim-header-card">
<div className="claim-header-copy">
<span className="eyebrow"> Cloud </span>
<span className="eyebrow">kuaishou-lewan </span>
<h1>{product?.title || '商品领取'}</h1>
<p>{order?.platformOrderId || '-'}</p>
<ClaimProductItems product={product} compact />
@@ -0,0 +1,25 @@
import { apiGet, apiPost } from '@/lib/http'
import type {
AdminFulfillmentRoutingConfig,
AdminFulfillmentRoutingPreviewResult,
} from '@/types/admin'
export function fetchAdminFulfillmentRoutingConfig() {
return apiGet<AdminFulfillmentRoutingConfig>(
'/api/v1/admin/platform-config/fulfillment-routing',
)
}
export function saveAdminFulfillmentRoutingConfig(payload: Partial<AdminFulfillmentRoutingConfig>) {
return apiPost<AdminFulfillmentRoutingConfig>(
'/api/v1/admin/platform-config/fulfillment-routing',
payload,
)
}
export function previewAdminFulfillmentRouting(productName: string) {
return apiPost<AdminFulfillmentRoutingPreviewResult>(
'/api/v1/admin/platform-config/fulfillment-routing/preview',
{ productName },
)
}
@@ -5,3 +5,4 @@ export * from './kuaishou-eticket'
export * from './kuaishou-industry'
export * from './kuaishou-feifei'
export * from './cloudtentacles'
export * from './fulfillment-routing'
+38
View File
@@ -885,6 +885,37 @@ select {
align-items: end;
}
.platform-routing-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.platform-routing-priority-row,
.platform-route-rule-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.platform-route-rule {
min-width: 0;
padding: 12px;
border: 1px solid #edf0f5;
border-radius: 6px;
background: #f8fafc;
display: grid;
gap: 10px;
}
.platform-route-rule-grid {
display: grid;
grid-template-columns: minmax(220px, 1fr) 110px minmax(160px, 0.7fr) 110px;
gap: 10px;
align-items: center;
}
.field-switch {
min-height: 54px;
padding: 10px 12px;
@@ -1065,10 +1096,17 @@ select {
.metric-grid.four,
.metric-grid.five,
.platform-form-grid,
.platform-routing-grid,
.platform-route-rule-grid,
.platform-split-layout {
grid-template-columns: 1fr;
}
.platform-routing-priority-row,
.platform-route-rule-head {
display: grid;
}
.platform-side-card {
position: static;
}
+4
View File
@@ -85,4 +85,8 @@ export type {
AdminCloudtentaclesSkuListResult,
AdminCloudtentaclesDeliveryRecordItem,
AdminCloudtentaclesDeliveryRecordListResult,
AdminFulfillmentRoutingConfig,
AdminFulfillmentRoutingExecutorConfig,
AdminFulfillmentRoutingPreviewResult,
AdminFulfillmentRoutingRule,
} from './platform-config'
@@ -0,0 +1,53 @@
export interface AdminFulfillmentRoutingExecutorConfig {
enabled: boolean
}
export interface AdminFulfillmentRoutingRule {
id: string
enabled: boolean
productName: string
normalizedProductName: string
matchType: 'exact' | 'contains'
executorKey: string
priority: number
notes: string
}
export interface AdminFulfillmentRoutingConfig {
filePath: string
enabled: boolean
defaultExecutorPriority: string[]
unmatchedExecutorKey: string
executors: Record<string, AdminFulfillmentRoutingExecutorConfig>
rules: AdminFulfillmentRoutingRule[]
}
export interface AdminFulfillmentRoutingPreviewResult {
productName: string
normalizedProductName: string
resolvedSkuCode: string
resolvedSkuName: string
isConfigured: boolean
fulfillmentRoute: null | {
selectedExecutorKey: string
selectedRuleId: string
reason: string
matchedRule: null | {
id: string
productName: string
executorKey: string
priority: number
}
candidates: Array<{
executorKey: string
available: boolean
reason?: string
}>
skipped: Array<{
executorKey: string
reason: string
}>
}
cloudtentacles: Record<string, unknown> | null
kuaishouFeifei: Record<string, unknown> | null
}
@@ -73,3 +73,10 @@ export type {
AdminCloudtentaclesDeliveryRecordItem,
AdminCloudtentaclesDeliveryRecordListResult,
} from './cloudtentacles'
export type {
AdminFulfillmentRoutingConfig,
AdminFulfillmentRoutingExecutorConfig,
AdminFulfillmentRoutingPreviewResult,
AdminFulfillmentRoutingRule,
} from './fulfillment-routing'