fix(finance): 重写履约路由迁移与统一渠道配置

重写 120/121 迁移,统一内部 SKU、渠道 id、config_json 和旧规则转换。

后台渠道配置改读统一渠道表,卡券与人工配置不再写旧规则。

增加安全渠道切换,并排除已关闭、失败或过期的历史任务载体。

按生产 119 基线重跑迁移、完整履约财务链路和严格预检。
This commit is contained in:
yml2213
2026-11-02 15:29:13 +08:00
parent 2f87445ed9
commit 6bf28489e0
31 changed files with 958 additions and 464 deletions
@@ -58,11 +58,7 @@ const CHANNEL_COLOR: Record<string, string> = {
manual: 'orange',
}
const MATCH_KEY_OPTIONS = [
{ value: 'sku_code', label: '平台 SKU' },
{ value: 'internal_sku', label: '内部 SKU' },
{ value: 'sku_name', label: 'SKU 名称' },
]
const MATCH_KEY_OPTIONS = [{ value: 'internal_sku', label: '内部 SKU' }]
export default function AdminFulfillmentChannelPage() {
return (
@@ -214,7 +210,7 @@ function PendingPanel() {
type="info"
showIcon
style={{ marginBottom: 12 }}
message="待分类订单的收入已经计入「未分类」桶,不会丢失;指定渠道后会自动进入商品归一与成本核算。"
message="指定渠道会写入该内部 SKU 的全局配置,并影响所有平台和店铺;系统会先确认载体创建成功,再进入商品归一与成本核算。"
/>
<PercentTable
rowKey={(row) => rowKey(row)}
@@ -259,8 +255,7 @@ function RulesPanel() {
})
const columns = [
{ title: '来源', dataIndex: 'provider', width: 120, render: (v: string) => v || '不限' },
{ title: '店铺', dataIndex: 'shop_id', width: 140, render: (v: string) => v || '不限' },
{ title: '作用范围', width: 110, render: () => <Tag color="blue">全局</Tag> },
{
title: '匹配键',
dataIndex: 'match_key_type',
@@ -303,7 +298,7 @@ function RulesPanel() {
return (
<Card
title={`渠道规则(${query.data?.data?.length ?? 0})`}
title={`统一渠道配置(${query.data?.data?.length ?? 0})`}
extra={
<Space>
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
@@ -316,8 +311,8 @@ function RulesPanel() {
}
>
<Typography.Paragraph type="secondary">
接单平台由接单模板匹配决定,不在此表维护;这里负责自动发货、人工、第三方卡券三个渠道与运营强制覆盖。
同优先级命中多个渠道时订单保持待分类并进「配置冲突」。
配置按内部 SKU 全局生效,会同时影响所有平台和店铺。每条内部 SKU 只能有一个主渠道;
其他渠道请设置 90 以上优先级作为兜底。接单平台的具体模板由统一配置中的 workRuleKey 控制。
</Typography.Paragraph>
<PercentTable
rowKey="id"
@@ -329,7 +324,7 @@ function RulesPanel() {
/>
<Modal
title="新增渠道规则"
title="新增统一渠道配置"
open={open}
onCancel={() => setOpen(false)}
onOk={() => form.submit()}
@@ -338,15 +333,13 @@ function RulesPanel() {
<Form
form={form}
layout="vertical"
initialValues={{ matchKeyType: 'sku_code', channel: 'third_party_voucher', priority: 10 }}
initialValues={{
matchKeyType: 'internal_sku',
channel: 'third_party_voucher',
priority: 10,
}}
onFinish={(values) => saveMutation.mutate(values)}
>
<Form.Item name="provider" label="来源(留空=不限)">
<Input placeholder="91kaquan / pdd / douyin_mashida" />
</Form.Item>
<Form.Item name="shopId" label="店铺(留空=不限)">
<Input />
</Form.Item>
<Form.Item name="matchKeyType" label="匹配键" rules={[{ required: true }]}>
<Select options={MATCH_KEY_OPTIONS} />
</Form.Item>
@@ -24,9 +24,7 @@ export type PendingChannelGroupListResult = {
/** 渠道规则(L1)。 */
export type ChannelRule = {
id: number
provider: string
shop_id: string
match_key_type: 'sku_code' | 'internal_sku' | 'sku_name'
match_key_type: 'internal_sku'
match_key: string
channel: 'work_order' | 'affiliate_dash' | 'third_party_voucher' | 'manual'
priority: number
@@ -117,14 +115,24 @@ export function assignChannelToItems(payload: {
)
}
export function switchChannelForItems(payload: {
orderItemIds: number[]
channel: string
note?: string
}) {
return apiPost<{
switched: number
failed: number
errors: Array<{ orderItemId: number; reason: string }>
}>('/api/v1/admin/fulfillment/switch', payload)
}
export function fetchChannelRules(params: { channel?: string } = {}) {
return apiGet<ChannelRule[]>('/api/v1/admin/fulfillment/rules', params)
}
export function saveChannelRule(payload: {
id?: number
provider?: string
shopId?: string
matchKeyType: string
matchKey: string
channel: string