feat(routing): 渠道配置页把 Rock 作为独立履约策略展示

最新的 Rock 商品之前被算进第一平台,配置里也无法单独选择 Rock 库存池。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-10-05 21:15:20 +08:00
co-authored by Cursor
parent 8c43fb4569
commit 4c68103217
4 changed files with 81 additions and 9 deletions
+5 -2
View File
@@ -271,9 +271,12 @@ func ensureProduct(tx *gorm.DB, in CreateMerchantProductInput) (*model.Product,
// UpdateProductRouting 更新平台目录商品的履约渠道与优先级配置
func (s *MerchantService) UpdateProductRouting(catalogID uint, routing ProductRoutingConfig, actorUserID uint) error {
switch routing.RoutingMode {
case RoutingModePriority, RoutingModeLewan, RoutingModeDLC:
case RoutingModePriority, RoutingModeLewan, RoutingModeDLC, RoutingModeRock:
default:
return errors.New("routing_mode 仅支持 priority / lewan / dlc")
return errors.New("routing_mode 仅支持 priority / lewan / dlc / rock")
}
if routing.RoutingMode == RoutingModeRock {
routing.Priority = []string{ProviderRock}
}
if routing.RoutingMode == RoutingModePriority {
if len(routing.Priority) == 0 {
@@ -55,6 +55,11 @@ func TestParseProductRoutingConfig(t *testing.T) {
if cfg5.DLCGoodID != "682ef39ca8f40c4234c59f38" {
t.Fatalf("expected builtin DLC good id, got %s", cfg5.DLCGoodID)
}
cfgRock := ParseProductRoutingConfig(`{"routing_mode":"rock","priority":["rock"],"delivery_type":"LINK_BINDING"}`, "rock_black_tshirt")
if cfgRock.RoutingMode != RoutingModeRock || len(cfgRock.Priority) != 1 || cfgRock.Priority[0] != ProviderRock {
t.Fatalf("expected rock-only routing, got %+v", cfgRock)
}
}
func TestFulfillmentRouterResolution(t *testing.T) {
+69 -5
View File
@@ -32,6 +32,7 @@ import {
SearchOutlined,
SettingOutlined,
SwapOutlined,
ThunderboltOutlined,
} from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import { lewanApi, platformApi } from '../api'
@@ -79,7 +80,7 @@ export default function PlatformProducts() {
setLoading(true)
try {
const list = await platformApi.productCatalog()
setCatalog(list || [])
setCatalog([...(list || [])].sort((a, b) => b.id - a.id))
} catch (e) {
message.error(e instanceof Error ? e.message : '加载商品目录失败')
} finally {
@@ -117,11 +118,13 @@ export default function PlatformProducts() {
let priorityCount = 0
let lewanOnlyCount = 0
let dlcOnlyCount = 0
let rockOnlyCount = 0
catalog.forEach((item) => {
const mode = item.routing?.routing_mode || 'dlc'
if (mode === 'priority') priorityCount++
else if (mode === 'lewan') lewanOnlyCount++
else if (mode === 'rock') rockOnlyCount++
else dlcOnlyCount++
})
@@ -130,6 +133,7 @@ export default function PlatformProducts() {
priorityCount,
lewanOnlyCount,
dlcOnlyCount,
rockOnlyCount,
}
}, [catalog])
@@ -156,7 +160,8 @@ export default function PlatformProducts() {
filterMode === 'all' ||
(filterMode === 'priority' && mode === 'priority') ||
(filterMode === 'lewan' && mode === 'lewan') ||
(filterMode === 'dlc' && mode === 'dlc')
(filterMode === 'dlc' && mode === 'dlc') ||
(filterMode === 'rock' && mode === 'rock')
const matchCategory = filterCategory === 'all' || item.category === filterCategory
@@ -196,14 +201,16 @@ export default function PlatformProducts() {
: ['lewan', 'dlc']
: values.routing_mode === 'lewan'
? ['lewan']
: values.routing_mode === 'rock'
? ['rock']
: ['dlc']
const payload: UpdateProductRoutingInput = {
routing_mode: values.routing_mode,
priority,
dlc_good_id: values.dlc_good_id?.trim() || '',
lewan_product_id: values.lewan_product_id?.trim() || '',
lewan_name: values.lewan_name?.trim() || '',
dlc_good_id: values.routing_mode === 'rock' ? '' : values.dlc_good_id?.trim() || '',
lewan_product_id: values.routing_mode === 'rock' ? '' : values.lewan_product_id?.trim() || '',
lewan_name: values.routing_mode === 'rock' ? '' : values.lewan_name?.trim() || '',
delivery_type: values.delivery_type || 'LINK_BINDING',
}
@@ -379,6 +386,13 @@ export default function PlatformProducts() {
</Tag>
)
}
if (mode === 'rock') {
return (
<Tag color="magenta" icon={<ThunderboltOutlined />} style={{ fontWeight: 600, padding: '3px 8px' }}>
仅走 Rock 库存池
</Tag>
)
}
return (
<Tag color="orange" icon={<DesktopOutlined />} style={{ fontWeight: 600, padding: '3px 8px' }}>
仅走第一平台 (DLC)
@@ -456,6 +470,17 @@ export default function PlatformProducts() {
const routing = r.routing
const lewanId = routing?.lewan_product_id
const dlcId = routing?.dlc_good_id
if (routing?.routing_mode === 'rock') {
return (
<Space direction="vertical" size={2} style={{ width: '100%', fontSize: 12 }}>
<div>
<Text type="secondary">Rock: </Text>
<Tag color="magenta" style={{ fontSize: 11 }}>通用黑色库存池</Tag>
</div>
<Text type="secondary">不走第一平台 / 乐玩,库存不足时按自动补货策略采购</Text>
</Space>
)
}
return (
<Space direction="vertical" size={2} style={{ width: '100%', fontSize: 12 }}>
<div>
@@ -665,6 +690,20 @@ export default function PlatformProducts() {
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card className="metric-card" styles={{ body: { padding: '16px 20px' } }}>
<div className="metric-card-header">
<span className="metric-card-title">仅 Rock 库存池</span>
<div className="metric-card-icon metric-card-icon--purple">
<ThunderboltOutlined />
</div>
</div>
<div className="metric-card-value">{metrics.rockOnlyCount}</div>
<div className="metric-card-footer">
<span>最新 Rock 发货,库存不足自动补货</span>
</div>
</Card>
</Col>
</Row>
{/* 筛选与搜索工具条 */}
@@ -692,6 +731,7 @@ export default function PlatformProducts() {
{ value: 'priority', label: '双平台优先 (Priority)' },
{ value: 'lewan', label: '仅乐玩开放平台' },
{ value: 'dlc', label: '仅第一平台 (DLC)' },
{ value: 'rock', label: '仅 Rock 库存池' },
]}
/>
{categories.length > 0 && (
@@ -791,6 +831,14 @@ export default function PlatformProducts() {
</div>
</div>
</Radio>
<Radio value="rock" style={{ padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 8, width: '100%' }}>
<div>
<strong style={{ color: '#0f172a' }}>仅 Rock 库存池 (Rock Only)</strong>
<div style={{ color: '#64748b', fontSize: 12, marginTop: 2 }}>
走 0666.pro 通用库存。可用 Token 低于安全水位时,按自动补货数量再采购一批。
</div>
</div>
</Radio>
</Space>
</Radio.Group>
</Form.Item>
@@ -820,6 +868,18 @@ export default function PlatformProducts() {
}
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.routing_mode !== cur.routing_mode}>
{({ getFieldValue }) =>
getFieldValue('routing_mode') === 'rock' ? (
<Alert
message="Rock 不需要上游商品 ID"
description="这类商品走 Rock 通用黑色库存池。可用数量降到安全水位及以下时,会按「单次自动补货数量」再采购一批,不是把库存补到固定水位。"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
) : (
<>
<Divider titlePlacement="start" style={{ margin: '16px 0', fontSize: 13, color: '#64748b' }}>
渠道商品映射信息
</Divider>
@@ -885,6 +945,10 @@ export default function PlatformProducts() {
</Form.Item>
</Col>
</Row>
</>
)
}
</Form.Item>
</Form>
</Modal>
+2 -2
View File
@@ -85,7 +85,7 @@ export interface MerchantProduct {
}
export interface ProductRoutingConfig {
routing_mode: 'priority' | 'lewan' | 'dlc'
routing_mode: 'priority' | 'lewan' | 'dlc' | 'rock'
priority?: string[]
dlc_good_id?: string
lewan_product_id?: string
@@ -379,7 +379,7 @@ export interface LewanProductListResult {
}
export interface UpdateProductRoutingInput {
routing_mode: 'priority' | 'lewan' | 'dlc'
routing_mode: 'priority' | 'lewan' | 'dlc' | 'rock'
priority?: string[]
dlc_good_id?: string
lewan_product_id?: string