feat(fulfillment): 支持删除待分类和强制删除渠道规则
This commit is contained in:
@@ -38,8 +38,10 @@ import PercentTable from '@/components/common/PercentTable'
|
||||
import FinanceSubNav from '@/components/admin/FinanceSubNav'
|
||||
import {
|
||||
assignChannelByInternalSku,
|
||||
deletePendingChannelGroup,
|
||||
deleteInternalSkuMapping,
|
||||
deleteChannelRules,
|
||||
forceDeleteChannelRules,
|
||||
fetchChannelExceptions,
|
||||
fetchChannelRules,
|
||||
fetchInternalSkuMappings,
|
||||
@@ -176,6 +178,20 @@ function PendingPanel() {
|
||||
message.error(error instanceof Error ? error.message : '重新匹配接单模板失败'),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (row: PendingChannelGroup) =>
|
||||
deletePendingChannelGroup({ internalSku: row.internal_sku }),
|
||||
onSuccess: (result) => {
|
||||
const data = result.data
|
||||
message.success(
|
||||
`已删除 ${data.archivedItems} 个待分类订单项,原始订单保留;财务事实已重算 ${data.refreshedDays} 天`,
|
||||
)
|
||||
void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['fulfillment-exceptions'] })
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '删除失败'),
|
||||
})
|
||||
|
||||
const pipelineMutation = useMutation({
|
||||
mutationFn: () => runFinancePipeline({ days: 3 }),
|
||||
onSuccess: () => {
|
||||
@@ -194,10 +210,27 @@ function PendingPanel() {
|
||||
},
|
||||
{
|
||||
title: '店铺',
|
||||
dataIndex: 'shop_ids',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render: (value: string[] | undefined) => value?.join(' / ') || '-',
|
||||
dataIndex: 'shops',
|
||||
width: 210,
|
||||
render: (_: unknown, row: PendingChannelGroup) => {
|
||||
const shops =
|
||||
row.shops?.length > 0
|
||||
? row.shops
|
||||
: (row.shop_ids || []).map((shopId) => ({ id: shopId, name: shopId }))
|
||||
if (shops.length === 0) return '-'
|
||||
return (
|
||||
<Space direction="vertical" size={2}>
|
||||
{shops.map((shop) => (
|
||||
<span key={`${shop.id}-${shop.name}`}>
|
||||
<Typography.Text>{shop.name}</Typography.Text>
|
||||
{shop.name !== shop.id ? (
|
||||
<Typography.Text type="secondary"> · {shop.id}</Typography.Text>
|
||||
) : null}
|
||||
</span>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{ title: '内部SKU', dataIndex: 'internal_sku', width: 220 },
|
||||
{
|
||||
@@ -236,9 +269,9 @@ function PendingPanel() {
|
||||
},
|
||||
{
|
||||
title: '指定渠道',
|
||||
width: 320,
|
||||
width: 360,
|
||||
render: (_: unknown, row: PendingChannelGroup) => (
|
||||
<Space>
|
||||
<Space size={4}>
|
||||
<Select
|
||||
size="small"
|
||||
style={{ width: 180 }}
|
||||
@@ -256,6 +289,33 @@ function PendingPanel() {
|
||||
>
|
||||
指定
|
||||
</Button>
|
||||
<Tooltip title="删除待分类记录,不删除原始订单">
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={deleteMutation.isPending}
|
||||
onClick={() =>
|
||||
Modal.confirm({
|
||||
title: '删除待分类记录',
|
||||
content: (
|
||||
<div>
|
||||
<p>
|
||||
将归档 <strong>{row.item_count}</strong> 个订单项,并清除已生成的成本快照、
|
||||
分类异常,重算对应销售事实与总账。
|
||||
</p>
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
原始订单和订单商品不会被删除;已关账期间禁止删除。
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
okText: '确认删除',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: () => deleteMutation.mutateAsync(row),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -296,10 +356,10 @@ function PendingPanel() {
|
||||
}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message="待分类来自真实订单,系统不提供直接删除,避免掩盖未履约收入。配置错误请修正模板/映射后点「重试接单匹配」,或通过「指定渠道」完成分流;系统会先确认载体创建成功,再进入商品归一与成本核算。"
|
||||
message="删除只会归档对应订单项并清理财务成本记录,不会删除原始订单。配置错误优先修正模板/映射后重试匹配;确认无需履约或核算时,可直接删除。"
|
||||
/>
|
||||
<PercentTable
|
||||
rowKey={(row) => rowKey(row)}
|
||||
@@ -614,6 +674,20 @@ function RulesPanel() {
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '删除规则失败'),
|
||||
})
|
||||
|
||||
const forceDeleteMutation = useMutation({
|
||||
mutationFn: (ids: number[]) => forceDeleteChannelRules(ids),
|
||||
onSuccess: (result) => {
|
||||
const missingHint = result.data.missingIds.length
|
||||
? `,${result.data.missingIds.length} 条已不存在`
|
||||
: ''
|
||||
message.success(`已强制删除 ${result.data.deletedCount} 条渠道规则${missingHint}`)
|
||||
setSelectedRuleIds([])
|
||||
void queryClient.invalidateQueries({ queryKey: ['fulfillment-rules'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] })
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '强制删除规则失败'),
|
||||
})
|
||||
|
||||
function resetFilters() {
|
||||
setKeyword('')
|
||||
setChannelFilter('')
|
||||
@@ -635,6 +709,19 @@ function RulesPanel() {
|
||||
})
|
||||
}
|
||||
|
||||
function confirmForceDeleteRules(ids: number[]) {
|
||||
if (ids.length === 0) return
|
||||
Modal.confirm({
|
||||
title: ids.length === 1 ? '强制删除渠道规则' : `批量强制删除 ${ids.length} 条渠道规则`,
|
||||
content:
|
||||
'将跳过系统来源保护,只删除统一渠道规则本身。历史订单和成本数据不会被删除;对应来源后续同步时,规则仍可能重新生成。',
|
||||
okText: '确认强制删除',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: () => forceDeleteMutation.mutateAsync(ids),
|
||||
})
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '作用范围', width: 110, render: () => <Tag color="blue">全局</Tag> },
|
||||
{
|
||||
@@ -676,26 +763,30 @@ function RulesPanel() {
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
width: 120,
|
||||
render: (_: unknown, row: ChannelRule) => {
|
||||
const button = (
|
||||
if (!canDeleteChannelRule(row)) {
|
||||
return (
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
loading={forceDeleteMutation.isPending}
|
||||
onClick={() => confirmForceDeleteRules([row.id])}
|
||||
>
|
||||
强制删除
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
loading={deleteMutation.isPending}
|
||||
disabled={!canDeleteChannelRule(row)}
|
||||
onClick={() => confirmDeleteRules([row.id])}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)
|
||||
return canDeleteChannelRule(row) ? (
|
||||
button
|
||||
) : (
|
||||
<Tooltip title="系统生成或由来源配置维护,不能在统一渠道页直接删除;配置有误时请新增同 SKU 的正确规则覆盖。">
|
||||
<span>{button}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -709,10 +800,10 @@ function RulesPanel() {
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={selectedRuleIds.length === 0}
|
||||
loading={deleteMutation.isPending}
|
||||
onClick={() => confirmDeleteRules(selectedRuleIds)}
|
||||
loading={forceDeleteMutation.isPending}
|
||||
onClick={() => confirmForceDeleteRules(selectedRuleIds)}
|
||||
>
|
||||
批量删除{selectedRuleIds.length ? ` (${selectedRuleIds.length})` : ''}
|
||||
批量强制删除{selectedRuleIds.length ? ` (${selectedRuleIds.length})` : ''}
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
|
||||
刷新
|
||||
@@ -726,8 +817,8 @@ function RulesPanel() {
|
||||
<Typography.Paragraph type="secondary">
|
||||
配置按内部 SKU 全局生效,会同时影响所有平台和店铺。每条内部 SKU 只能有一个主渠道;
|
||||
其他渠道请设置 90 以上优先级作为兜底。接单平台的具体模板由统一配置中的 workRuleKey 控制。
|
||||
来源为商品能力迁移、人工配置、卡券或历史回填的系统规则受保护,配置有误时请新增同 SKU
|
||||
的正确规则覆盖。
|
||||
来源为商品能力迁移、人工配置、卡券或历史回填的系统规则默认受保护;确认配置错误时可使用
|
||||
「强制删除」,但对应来源后续同步时仍可能重新生成。
|
||||
</Typography.Paragraph>
|
||||
<Space wrap style={{ marginBottom: 12 }}>
|
||||
<Input
|
||||
@@ -791,7 +882,6 @@ function RulesPanel() {
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedRuleIds,
|
||||
onChange: (keys) => setSelectedRuleIds(keys.map(Number)),
|
||||
getCheckboxProps: (row: ChannelRule) => ({ disabled: !canDeleteChannelRule(row) }),
|
||||
}}
|
||||
pagination={buildAdminTablePagination({
|
||||
current: query.data?.data?.page || page,
|
||||
|
||||
@@ -42,7 +42,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
deleteAdminManualConfig,
|
||||
deleteAdminManualSku,
|
||||
fetchAdminManualPlatforms,
|
||||
fetchAdminManualSkuOverview,
|
||||
fetchAdminManualSkus,
|
||||
@@ -161,13 +161,17 @@ export function ManualSkuConfigPanel() {
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (skuKey: string) => deleteAdminManualConfig(skuKey),
|
||||
onSuccess: () => {
|
||||
message.success('配置已删除')
|
||||
mutationFn: (skuKey: string) => deleteAdminManualSku(skuKey),
|
||||
onSuccess: (response) => {
|
||||
message.success(
|
||||
`人工分类已移除;${response.data.affectedItems} 个订单项已退回待分类,原始订单保留`,
|
||||
)
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-cost-center-manual-skus'] })
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['admin-cost-center-manual-skus-overview'],
|
||||
})
|
||||
void queryClient.invalidateQueries({ queryKey: ['fulfillment-rules'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] })
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(error instanceof Error ? error.message : '删除失败')
|
||||
@@ -259,24 +263,24 @@ export function ManualSkuConfigPanel() {
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 130,
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Space size={4}>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => openConfig(row)}>
|
||||
{row.configured ? '编辑' : '配置'}
|
||||
</Button>
|
||||
{row.configured ? (
|
||||
<Popconfirm
|
||||
title="删除该 SKU 的配置?"
|
||||
description="删除后该 SKU 回到未配置状态,已回填的订单项不再带配置。"
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
onConfirm={() => deleteMutation.mutate(row.skuKey)}
|
||||
>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
<Popconfirm
|
||||
title="移除该人工分类?"
|
||||
description="会删除人工登记与配置、撤销人工渠道规则、清除已生成的人工成本快照,并把关联订单项退回待分类;原始订单不会删除。"
|
||||
okText="确认移除"
|
||||
cancelText="取消"
|
||||
onConfirm={() => deleteMutation.mutate(row.skuKey)}
|
||||
>
|
||||
<Button size="small" danger icon={<DeleteOutlined />}>
|
||||
移除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -393,6 +393,18 @@ export function deleteAdminManualConfig(skuKey: string) {
|
||||
)
|
||||
}
|
||||
|
||||
export function deleteAdminManualSku(skuKey: string) {
|
||||
return apiDelete<{
|
||||
skuKey: string
|
||||
affectedItems: number
|
||||
deletedConfigs: number
|
||||
deletedRegistry: number
|
||||
deletedRules: number
|
||||
refreshedDays: number
|
||||
skippedClosedDays: number
|
||||
}>(`/api/v1/admin/cost-center/manual-skus/${encodeURIComponent(skuKey)}`)
|
||||
}
|
||||
|
||||
export function rematchAdminManualFulfillments(payload: { skuKey?: string } = {}) {
|
||||
return apiPost<ManualRematchResult>('/api/v1/admin/cost-center/manual-configs/rematch', payload)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export type PendingChannelGroup = {
|
||||
sku_name: string
|
||||
providers: string[]
|
||||
shop_ids: string[]
|
||||
shops: Array<{ id: string; name: string }>
|
||||
sku_codes: string[]
|
||||
pending_reasons: string[]
|
||||
item_count: number
|
||||
@@ -99,6 +100,15 @@ export type PendingWorkOrderRetryResult = {
|
||||
skipReasons: Record<string, number>
|
||||
}
|
||||
|
||||
export type DeletePendingChannelGroupResult = {
|
||||
archivedItems: number
|
||||
preservedOrders: number
|
||||
deletedCostSnapshots: number
|
||||
resolvedExceptions: number
|
||||
refreshedDays: number
|
||||
skippedClosedDays: number
|
||||
}
|
||||
|
||||
export type FinancePipelineStatus = {
|
||||
pipeline: string
|
||||
label: string
|
||||
@@ -148,6 +158,13 @@ export function retryPendingWorkOrderRouting(
|
||||
)
|
||||
}
|
||||
|
||||
export function deletePendingChannelGroup(payload: { internalSku: string }) {
|
||||
return apiPost<DeletePendingChannelGroupResult>(
|
||||
'/api/v1/admin/fulfillment/pending/delete',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function assignChannelByInternalSku(payload: {
|
||||
internalSku: string
|
||||
channel: string
|
||||
@@ -252,6 +269,14 @@ export function deleteChannelRules(ids: number[]) {
|
||||
}>('/api/v1/admin/fulfillment/rules/batch-delete', { ids })
|
||||
}
|
||||
|
||||
export function forceDeleteChannelRules(ids: number[]) {
|
||||
return apiPost<{
|
||||
deletedCount: number
|
||||
deletedIds: number[]
|
||||
missingIds: number[]
|
||||
}>('/api/v1/admin/fulfillment/rules/force-delete', { ids })
|
||||
}
|
||||
|
||||
export function fetchPipelineStatus() {
|
||||
return apiGet<FinancePipelineStatus[]>('/api/v1/admin/fulfillment/pipeline/status')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user