feat(admin): 增加履约渠道批量删除并统一分页配置

This commit is contained in:
yml2213
2026-11-02 17:31:55 +08:00
parent 735a791a2e
commit d871b93fde
14 changed files with 184 additions and 46 deletions
@@ -12,6 +12,7 @@ import {
assignChannelBySku,
assignChannelToOrderItems,
deleteChannelRule,
deleteChannelRules,
listChannelRules,
listPendingChannelGroups,
switchChannelForOrderItems,
@@ -234,6 +235,22 @@ router.delete(
}),
)
router.post(
'/fulfillment/rules/batch-delete',
requireAdminPermission('finance.config'),
createJsonHandler((req) => deleteChannelRules(Array.isArray(req.body?.ids) ? req.body.ids : []), {
successMessage: '渠道规则已批量删除',
errorMessage: '批量删除渠道规则失败',
scope: '[admin/fulfillment/rules/batch-delete]',
audit: (_req, data) => ({
action: 'fulfillment.batch_delete_rules',
targetType: 'internal_sku_channel',
targetId: '',
data: data as Record<string, unknown>,
}),
}),
)
router.get(
'/fulfillment/pipeline/status',
requireAdminPermission('finance.view'),
@@ -25,6 +25,7 @@ import {
} from './channel-types.js'
import {
deleteInternalSkuChannelById,
deleteInternalSkuChannelsByIds,
planInternalSkuChannels,
setInternalSkuPrimaryChannel,
upsertInternalSkuChannel,
@@ -1119,6 +1120,21 @@ export async function deleteChannelRule(ruleId: number): Promise<{ deleted: bool
return { deleted: await deleteInternalSkuChannelById(id) }
}
export async function deleteChannelRules(idsInput: number[]): Promise<{
deletedCount: number
deletedIds: number[]
missingIds: number[]
}> {
if (!Array.isArray(idsInput) || idsInput.length === 0) {
throw createHttpError('请选择要删除的渠道规则', {
statusCode: 400,
errorCode: 'channel_rule_ids_required',
})
}
const result = await deleteInternalSkuChannelsByIds(idsInput)
return { deletedCount: result.deletedIds.length, ...result }
}
async function loadEnabledRules(): Promise<ChannelRule[]> {
const rows = await query<ChannelRule>(
`
@@ -20,7 +20,7 @@ test(
const { closeDb, query } = await import('../../db/client.js')
const {
assignChannelByInternalSku,
deleteChannelRule,
deleteChannelRules,
ensureVoucherRuleForSku,
listChannelRules,
switchChannelForOrderItems,
@@ -171,7 +171,9 @@ test(
assert.ok(
listedRules.some((rule) => rule.id === savedRule.id && rule.match_key === voucherSku),
)
assert.equal((await deleteChannelRule(savedRule.id)).deleted, true)
const deletedRules = await deleteChannelRules([savedRule.id])
assert.equal(deletedRules.deletedCount, 1)
assert.deepEqual(deletedRules.deletedIds, [savedRule.id])
} finally {
if (orderItemIds.length > 0) {
await query(`DELETE FROM cost_exception_items WHERE order_item_id = ANY($1::bigint[])`, [
@@ -203,3 +203,21 @@ export async function deleteInternalSkuChannelById(channelId: number): Promise<b
const result = await query(`DELETE FROM internal_sku_channels WHERE id = $1`, [id])
return (result.rowCount || 0) > 0
}
export async function deleteInternalSkuChannelsByIds(idsInput: number[]): Promise<{
deletedIds: number[]
missingIds: number[]
}> {
const ids = [...new Set(idsInput.map(Number).filter((id) => Number.isInteger(id) && id > 0))]
if (ids.length === 0) return { deletedIds: [], missingIds: [] }
const result = await query<{ id: number }>(
`DELETE FROM internal_sku_channels WHERE id = ANY($1::bigint[]) RETURNING id`,
[ids],
)
const deletedIds = result.rows.map((row) => Number(row.id))
const deleted = new Set(deletedIds)
return {
deletedIds,
missingIds: ids.filter((id) => !deleted.has(id)),
}
}
@@ -27,6 +27,7 @@ import type { Dayjs } from 'dayjs'
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
import FinanceSubNav from '@/components/admin/FinanceSubNav'
import PercentTable from '@/components/common/PercentTable'
import { buildAdminTablePagination } from '@/utils/admin-pagination'
import dayjs from '@/lib/dayjs'
import {
closeFinancePeriod,
@@ -509,16 +510,15 @@ export default function AdminFinanceLedgerPage() {
dataSource={entriesQuery.data?.data.items || []}
columns={entryColumns}
onRow={(row) => ({ onClick: () => setEntryDetail(row) })}
pagination={{
pagination={buildAdminTablePagination({
current: entryPage,
pageSize: entryPageSize,
total: entriesQuery.data?.data.pagination.total || 0,
showSizeChanger: true,
onChange: (page, size) => {
setEntryPage(page)
setEntryPageSize(size)
},
}}
})}
/>
</Card>
</Space>
@@ -566,16 +566,15 @@ export default function AdminFinanceLedgerPage() {
loading={reconciliationsQuery.isLoading}
dataSource={reconciliationsQuery.data?.data.items || []}
columns={reconColumns}
pagination={{
pagination={buildAdminTablePagination({
current: reconPage,
pageSize: reconPageSize,
total: reconciliationsQuery.data?.data.pagination.total || 0,
showSizeChanger: true,
onChange: (page, size) => {
setReconPage(page)
setReconPageSize(size)
},
}}
})}
/>
</Card>
),
@@ -6,7 +6,12 @@
* · 没有命中规则的新单落在「待分类」,收入进未分类桶,不产生成本噪音;
* · 第三方卡券是「只统计」渠道:不建任务、不发领取页。
*/
import { PlusOutlined, ReloadOutlined, ThunderboltOutlined } from '@ant-design/icons'
import {
DeleteOutlined,
PlusOutlined,
ReloadOutlined,
ThunderboltOutlined,
} from '@ant-design/icons'
import {
Alert,
Button,
@@ -30,7 +35,7 @@ import PercentTable from '@/components/common/PercentTable'
import FinanceSubNav from '@/components/admin/FinanceSubNav'
import {
assignChannelBySku,
deleteChannelRule,
deleteChannelRules,
fetchChannelExceptions,
fetchChannelRules,
fetchPendingChannelGroups,
@@ -43,6 +48,11 @@ import {
} from '@/services/admin'
import { formatRedeemAmount } from '@/utils/redeem'
import { formatDateTime } from '@/utils/date-time'
import {
ADMIN_DEFAULT_PAGE_SIZE,
buildAdminLocalTablePagination,
buildAdminTablePagination,
} from '@/utils/admin-pagination'
const CHANNEL_OPTIONS = [
{ value: 'work_order', label: '接单平台' },
@@ -80,11 +90,13 @@ export default function AdminFulfillmentChannelPage() {
function PendingPanel() {
const queryClient = useQueryClient()
const [keyword, setKeyword] = useState('')
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [selections, setSelections] = useState<Record<string, string>>({})
const query = useQuery({
queryKey: ['fulfillment-pending', keyword],
queryFn: () => fetchPendingChannelGroups({ page: 1, pageSize: 100, keyword }),
queryKey: ['fulfillment-pending', keyword, page, pageSize],
queryFn: () => fetchPendingChannelGroups({ page, pageSize, keyword }),
})
const assignMutation = useMutation({
@@ -191,7 +203,10 @@ function PendingPanel() {
allowClear
placeholder="SKU / 商品名"
style={{ width: 220 }}
onSearch={(value) => setKeyword(value)}
onSearch={(value) => {
setKeyword(value)
setPage(1)
}}
/>
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
刷新
@@ -218,7 +233,15 @@ function PendingPanel() {
loading={query.isFetching}
dataSource={query.data?.data?.items || []}
columns={columns}
pagination={{ pageSize: 20, showSizeChanger: false }}
pagination={buildAdminTablePagination({
current: query.data?.data?.page || page,
pageSize: query.data?.data?.pageSize || pageSize,
total: query.data?.data?.total || 0,
onChange: (nextPage, nextPageSize) => {
setPage(nextPageSize !== pageSize ? 1 : nextPage)
setPageSize(nextPageSize)
},
})}
/>
</Card>
)
@@ -227,6 +250,7 @@ function PendingPanel() {
function RulesPanel() {
const queryClient = useQueryClient()
const [open, setOpen] = useState(false)
const [selectedRuleIds, setSelectedRuleIds] = useState<number[]>([])
const [form] = Form.useForm()
const query = useQuery({
@@ -246,14 +270,31 @@ function RulesPanel() {
})
const deleteMutation = useMutation({
mutationFn: (id: number) => deleteChannelRule(id),
onSuccess: () => {
message.success('规则已删除')
mutationFn: (ids: number[]) => deleteChannelRules(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: () => message.error('删除规则失败'),
})
function confirmDeleteRules(ids: number[]) {
if (ids.length === 0) return
Modal.confirm({
title: ids.length === 1 ? '删除渠道规则' : `批量删除 ${ids.length} 条渠道规则`,
content: '删除后会立即影响新订单的渠道判定;历史订单和成本数据不会被删除。',
okText: '确认删除',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: () => deleteMutation.mutateAsync(ids),
})
}
const columns = [
{ title: '作用范围', width: 110, render: () => <Tag color="blue">全局</Tag> },
{
@@ -288,7 +329,7 @@ function RulesPanel() {
danger
size="small"
loading={deleteMutation.isPending}
onClick={() => deleteMutation.mutate(row.id)}
onClick={() => confirmDeleteRules([row.id])}
>
删除
</Button>
@@ -301,6 +342,15 @@ function RulesPanel() {
title={`统一渠道配置(${query.data?.data?.length ?? 0})`}
extra={
<Space>
<Button
danger
icon={<DeleteOutlined />}
disabled={selectedRuleIds.length === 0}
loading={deleteMutation.isPending}
onClick={() => confirmDeleteRules(selectedRuleIds)}
>
批量删除{selectedRuleIds.length ? ` (${selectedRuleIds.length})` : ''}
</Button>
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
刷新
</Button>
@@ -320,7 +370,11 @@ function RulesPanel() {
loading={query.isFetching}
dataSource={query.data?.data || []}
columns={columns}
pagination={{ pageSize: 20, showSizeChanger: false }}
rowSelection={{
selectedRowKeys: selectedRuleIds,
onChange: (keys) => setSelectedRuleIds(keys.map(Number)),
}}
pagination={buildAdminLocalTablePagination()}
/>
<Modal
@@ -366,9 +420,11 @@ function RulesPanel() {
function ExceptionsPanel() {
const [reason, setReason] = useState('')
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const query = useQuery({
queryKey: ['fulfillment-exceptions', reason],
queryFn: () => fetchChannelExceptions({ reason, status: 'open', page: 1, pageSize: 100 }),
queryKey: ['fulfillment-exceptions', reason, page, pageSize],
queryFn: () => fetchChannelExceptions({ reason, status: 'open', page, pageSize }),
})
const REASON_OPTIONS = [
@@ -408,7 +464,10 @@ function ExceptionsPanel() {
style={{ width: 180 }}
value={reason}
options={REASON_OPTIONS}
onChange={setReason}
onChange={(value) => {
setReason(value)
setPage(1)
}}
/>
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
刷新
@@ -422,7 +481,15 @@ function ExceptionsPanel() {
loading={query.isFetching}
dataSource={query.data?.data?.items || []}
columns={columns}
pagination={{ pageSize: 20, showSizeChanger: false }}
pagination={buildAdminTablePagination({
current: query.data?.data?.page || page,
pageSize: query.data?.data?.pageSize || pageSize,
total: query.data?.data?.total || 0,
onChange: (nextPage, nextPageSize) => {
setPage(nextPageSize !== pageSize ? 1 : nextPage)
setPageSize(nextPageSize)
},
})}
/>
</Card>
)
@@ -27,25 +27,25 @@ import {
setAdminProductPrice,
} from '@/services/admin'
import type { ProductCatalogItem } from '@/types/admin'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import { useCostCenter } from './CostCenterContext'
const PAGE_SIZE = 100
export default function VoucherSkuPanel() {
const queryClient = useQueryClient()
const { canConfig, refreshAll } = useCostCenter()
const [page] = useState(1)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [selected, setSelected] = useState<ProductCatalogItem | null>(null)
const [ensuringRules, setEnsuringRules] = useState(false)
const [form] = Form.useForm<{ amountFen: number; changeReason?: string }>()
const query = useQuery({
queryKey: ['cost-center-voucher-products', page],
queryKey: ['cost-center-voucher-products', page, pageSize],
queryFn: () =>
fetchAdminProductCatalog({
carrierType: 'third_party_voucher',
page,
pageSize: PAGE_SIZE,
pageSize,
}),
})
@@ -154,7 +154,15 @@ export default function VoucherSkuPanel() {
loading={query.isFetching}
dataSource={items}
columns={columns}
pagination={{ pageSize: 20, showSizeChanger: false }}
pagination={buildAdminTablePagination({
current: page,
pageSize,
total,
onChange: (nextPage, nextPageSize) => {
setPage(nextPageSize !== pageSize ? 1 : nextPage)
setPageSize(nextPageSize)
},
})}
/>
<Modal
@@ -41,6 +41,7 @@ import {
import type { PlatformCreditAccount, PlatformCreditEntry, PlatformCreditOwner } from '@/types/admin'
import { hasAdminPermission } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import { buildAdminLocalTablePagination } from '@/utils/admin-pagination'
const PLATFORM_TAG_COLORS: Record<string, string> = {
nanhang: 'blue',
@@ -406,7 +407,7 @@ export function PlatformCreditPanel() {
dataSource={visibleAccounts}
loading={accountsQuery.isLoading}
scroll={{ x: 1080 }}
pagination={{ pageSize: 20, showSizeChanger: true }}
pagination={buildAdminLocalTablePagination()}
locale={{ emptyText: '还没有客服积分账户' }}
/>
@@ -29,6 +29,7 @@ import {
Typography,
} from 'antd'
import PercentTable from '@/components/common/PercentTable'
import { buildAdminTablePagination } from '@/utils/admin-pagination'
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
@@ -880,18 +881,16 @@ export default function ConsignmentInventoryPanel() {
dataSource={items}
loading={inventoriesQuery.isLoading}
scroll={{ x: 1590 }}
pagination={{
pagination={buildAdminTablePagination({
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (t) => `共 ${t} 件物品`,
onChange: (p, ps) => {
setPage(p)
setPageSize(ps)
},
}}
})}
/>
</Card>
@@ -26,6 +26,7 @@ import {
App,
} from 'antd'
import PercentTable from '@/components/common/PercentTable'
import { buildAdminTablePagination } from '@/utils/admin-pagination'
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
@@ -474,17 +475,16 @@ export default function ConsignmentSubmissionsPanel() {
dataSource={submissions}
loading={submissionsQuery.isLoading}
scroll={{ x: 1490 }}
pagination={{
pagination={buildAdminTablePagination({
current: submissionPage,
pageSize: submissionPageSize,
total: submissionTotal,
showSizeChanger: true,
showTotal: (t) => `共 ${t} 条提报记录`,
onChange: (p, ps) => {
setSubmissionPage(p)
setSubmissionPageSize(ps)
},
}}
})}
/>
</Card>
@@ -44,7 +44,11 @@ import {
import type { KuaishouMatchSource, WorkProductMatchLog } from '@/types/worker-platform'
import type { AdminKuaishouIndustryShopOption } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import {
ADMIN_DEFAULT_PAGE_SIZE,
buildAdminLocalTablePagination,
buildAdminTablePagination,
} from '@/utils/admin-pagination'
import { formatMoney } from './shared'
export default function ProductMatchPanel() {
@@ -366,7 +370,7 @@ export default function ProductMatchPanel() {
loading={sourcesQuery.isLoading}
columns={sourceColumns}
dataSource={sources}
pagination={{ pageSize: 10, showSizeChanger: false }}
pagination={buildAdminLocalTablePagination()}
size="small"
/>
</div>
@@ -14,6 +14,7 @@ import {
Tag,
} from 'antd'
import PercentTable from '@/components/common/PercentTable'
import { buildAdminTablePagination } from '@/utils/admin-pagination'
import type { ColumnsType } from 'antd/es/table'
import { useEffect, useState } from 'react'
@@ -635,16 +636,15 @@ export default function RiskControlPanel() {
dataSource={bans?.items || []}
loading={bansQuery.isLoading}
locale={{ emptyText: '暂无封禁记录' }}
pagination={{
pagination={buildAdminTablePagination({
current: banPage,
pageSize: banPageSize,
total: bans?.total || 0,
showSizeChanger: true,
onChange: (nextPage, nextPageSize) => {
setBanPage(nextPage)
setBanPageSize(nextPageSize)
},
}}
})}
/>
</Card>
@@ -690,16 +690,15 @@ export default function RiskControlPanel() {
columns={eventColumns}
dataSource={events?.items || []}
loading={eventsQuery.isLoading}
pagination={{
pagination={buildAdminTablePagination({
current: page,
pageSize,
total: events?.total || 0,
showSizeChanger: true,
onChange: (nextPage, nextPageSize) => {
setPage(nextPage)
setPageSize(nextPageSize)
},
}}
})}
/>
</Card>
</Space>
@@ -1697,7 +1697,7 @@ function AffiliateDashPlatformPanel() {
<PercentTable<SkuMappingRow>
rowKey={(row) => row.productNo}
size="small"
pagination={{ pageSize: 10, showSizeChanger: false }}
pagination={buildAdminLocalTablePagination()}
dataSource={builtinSkuRows}
columns={[
{ title: '91 中文编码', dataIndex: 'productNo', key: 'productNo' },
@@ -147,6 +147,14 @@ export function deleteChannelRule(ruleId: number) {
return apiDelete<{ deleted: boolean }>(`/api/v1/admin/fulfillment/rules/${ruleId}`)
}
export function deleteChannelRules(ids: number[]) {
return apiPost<{
deletedCount: number
deletedIds: number[]
missingIds: number[]
}>('/api/v1/admin/fulfillment/rules/batch-delete', { ids })
}
export function fetchPipelineStatus() {
return apiGet<FinancePipelineStatus[]>('/api/v1/admin/fulfillment/pipeline/status')
}