From d871b93fde60fb5f5df5d554b57c62199a8b314e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 2 Nov 2026 17:31:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=A2=9E=E5=8A=A0=E5=B1=A5?= =?UTF-8?q?=E7=BA=A6=E6=B8=A0=E9=81=93=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=B9=B6=E7=BB=9F=E4=B8=80=E5=88=86=E9=A1=B5=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/routes/admin/fulfillment-channel.ts | 17 ++++ .../fulfillment/channel-assign-service.ts | 16 +++ .../fulfillment-routing.integration.test.ts | 6 +- .../internal-sku-channel-service.ts | 18 ++++ .../pages/admin/AdminFinanceLedgerPage.tsx | 11 +-- .../admin/AdminFulfillmentChannelPage.tsx | 97 ++++++++++++++++--- .../admin/cost-center/VoucherSkuPanel.tsx | 20 ++-- .../pages/admin/fund/PlatformCreditPanel.tsx | 3 +- .../panels/ConsignmentInventoryPanel.tsx | 7 +- .../panels/ConsignmentSubmissionsPanel.tsx | 6 +- .../pages/admin/panels/ProductMatchPanel.tsx | 8 +- .../pages/admin/panels/RiskControlPanel.tsx | 11 +-- .../admin/platform/AdminPlatformShopsPage.tsx | 2 +- .../src/services/admin/fulfillment-channel.ts | 8 ++ 14 files changed, 184 insertions(+), 46 deletions(-) diff --git a/apps/backend/src/routes/admin/fulfillment-channel.ts b/apps/backend/src/routes/admin/fulfillment-channel.ts index 778803e3..220a100e 100644 --- a/apps/backend/src/routes/admin/fulfillment-channel.ts +++ b/apps/backend/src/routes/admin/fulfillment-channel.ts @@ -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, + }), + }), +) + router.get( '/fulfillment/pipeline/status', requireAdminPermission('finance.view'), diff --git a/apps/backend/src/services/fulfillment/channel-assign-service.ts b/apps/backend/src/services/fulfillment/channel-assign-service.ts index b13ec83f..0df16aa4 100644 --- a/apps/backend/src/services/fulfillment/channel-assign-service.ts +++ b/apps/backend/src/services/fulfillment/channel-assign-service.ts @@ -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 { const rows = await query( ` diff --git a/apps/backend/src/services/fulfillment/fulfillment-routing.integration.test.ts b/apps/backend/src/services/fulfillment/fulfillment-routing.integration.test.ts index 189b3570..8db7760f 100644 --- a/apps/backend/src/services/fulfillment/fulfillment-routing.integration.test.ts +++ b/apps/backend/src/services/fulfillment/fulfillment-routing.integration.test.ts @@ -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[])`, [ diff --git a/apps/backend/src/services/fulfillment/internal-sku-channel-service.ts b/apps/backend/src/services/fulfillment/internal-sku-channel-service.ts index a32f4b11..88cca624 100644 --- a/apps/backend/src/services/fulfillment/internal-sku-channel-service.ts +++ b/apps/backend/src/services/fulfillment/internal-sku-channel-service.ts @@ -203,3 +203,21 @@ export async function deleteInternalSkuChannelById(channelId: number): Promise 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)), + } +} diff --git a/apps/frontend/src/pages/admin/AdminFinanceLedgerPage.tsx b/apps/frontend/src/pages/admin/AdminFinanceLedgerPage.tsx index 21526b0e..0b44b6d0 100644 --- a/apps/frontend/src/pages/admin/AdminFinanceLedgerPage.tsx +++ b/apps/frontend/src/pages/admin/AdminFinanceLedgerPage.tsx @@ -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) }, - }} + })} /> @@ -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) }, - }} + })} /> ), diff --git a/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx b/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx index df7de64a..eff0fda5 100644 --- a/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx +++ b/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx @@ -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>({}) 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) + }} /> @@ -301,6 +342,15 @@ function RulesPanel() { title={`统一渠道配置(${query.data?.data?.length ?? 0})`} extra={ + @@ -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()} /> 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) + }} />