diff --git a/apps/backend/src/routes/admin/cost-center-catalog.ts b/apps/backend/src/routes/admin/cost-center-catalog.ts index 207f0385..dbfad7fb 100644 --- a/apps/backend/src/routes/admin/cost-center-catalog.ts +++ b/apps/backend/src/routes/admin/cost-center-catalog.ts @@ -47,7 +47,7 @@ const router = Router() router.get( '/cost-center/template-bootstrap/preview', requireAdminPermission('finance.view'), - createJsonHandler(() => previewAdminTemplateProductBootstrap(), { + createJsonHandler((req) => previewAdminTemplateProductBootstrap(req.query as JsonObject), { successMessage: 'ok', errorMessage: '读取接单模板初始化预览失败', scope: '[admin/cost-center/template-bootstrap/preview]', @@ -156,7 +156,7 @@ router.post( router.post( '/cost-center/work-order-products/backfill', requireAdminPermission('finance.config'), - createJsonHandler(() => backfillAdminWorkOrderProductSkus(), { + createJsonHandler((req) => backfillAdminWorkOrderProductSkus(bodyOf(req)), { successMessage: '接单平台商品已回填', errorMessage: '回填接单平台商品失败', scope: '[admin/cost-center/work-order-products/backfill]', diff --git a/apps/backend/src/services/cost-center/product-catalog-service.ts b/apps/backend/src/services/cost-center/product-catalog-service.ts index cf9f8f90..f87d5e39 100644 --- a/apps/backend/src/services/cost-center/product-catalog-service.ts +++ b/apps/backend/src/services/cost-center/product-catalog-service.ts @@ -10,6 +10,7 @@ import { query } from '../../db/client.js' import type { AdminSession } from '../admin/admin-auth-service.js' import type { JsonObject } from '../../types/json.js' import { createHttpError } from '../../utils/http.js' +import { resolveFinanceDateRange } from '../../utils/finance-date-range.js' import { formatShanghaiDate, shiftDate } from '../../utils/finance-date-range.js' import { nowIso } from '../../utils/time.js' import { normalizeMatchText, normalizeSkuSeriesEquivalence } from '../product-match/normalize.js' @@ -978,6 +979,11 @@ export async function backfillAdminProductMappings(payload: JsonObject = {}) { const limit = Math.min(20000, Math.max(1, Math.trunc(Number(payload.limit) || 5000))) const all = payload.all === true || payload.all === 'true' const maxBatches = all ? 40 : 1 + const range = resolveOptionalDateRange(payload) + const dateSql = range + ? `AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date BETWEEN $4::date AND $5::date` + : '' + const rangeParams = range ? [range.fromDate, range.toDate] : [] let updatedCount = 0 let batches = 0 let lastId = 0 @@ -1015,10 +1021,11 @@ export async function backfillAdminProductMappings(payload: JsonObject = {}) { WHERE oi.canonical_product_id IS NULL AND oi.id > $1 AND ($2 = '' OR o.provider = $2) + ${dateSql} ORDER BY oi.id LIMIT $3 `, - [lastId, provider, limit], + [lastId, provider, limit, ...rangeParams], ) if (itemsResult.rows.length === 0) break @@ -1078,8 +1085,9 @@ export async function backfillAdminProductMappings(payload: JsonObject = {}) { JOIN orders o ON o.id = oi.order_id WHERE oi.canonical_product_id IS NULL AND ($1 = '' OR o.provider = $1) + ${dateSql.replace('$4', '$2').replace('$5', '$3')} `, - [provider], + [provider, ...rangeParams], ) return { updatedCount, @@ -1155,6 +1163,11 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {} const limit = Math.min(50000, Math.max(1, Math.trunc(Number(payload.limit) || 20000))) const all = payload.all === true || payload.all === 'true' const maxBatches = all ? 40 : 1 + const range = resolveOptionalDateRange(payload) + const dateSql = range + ? `AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date BETWEEN $2::date AND $3::date` + : '' + const rangeParams = range ? [range.fromDate, range.toDate] : [] let updatedCount = 0 let batches = 0 @@ -1168,16 +1181,18 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {} normalized_quantity: number | null }>( ` - SELECT id, sku_name, sku_code, normalized_sku_name, normalized_sku_series, - normalized_quantity - FROM order_items - WHERE normalized_quantity IS NULL - OR normalized_sku_name IS NULL - OR normalized_sku_series IS NULL - ORDER BY id + SELECT oi.id, oi.sku_name, oi.sku_code, oi.normalized_sku_name, + oi.normalized_sku_series, oi.normalized_quantity + FROM order_items oi + ${range ? 'JOIN orders o ON o.id = oi.order_id' : ''} + WHERE (oi.normalized_quantity IS NULL + OR oi.normalized_sku_name IS NULL + OR oi.normalized_sku_series IS NULL) + ${dateSql.replaceAll('oi.', 'oi.')} + ORDER BY oi.id LIMIT $1 `, - [limit], + [limit, ...rangeParams], ) if (result.rows.length === 0) break @@ -1218,10 +1233,13 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {} const remainingResult = await query<{ total: number }>( `SELECT COUNT(*)::int AS total - FROM order_items - WHERE normalized_quantity IS NULL - OR normalized_sku_name IS NULL - OR normalized_sku_series IS NULL`, + FROM order_items oi + ${range ? 'JOIN orders o ON o.id = oi.order_id' : ''} + WHERE (oi.normalized_quantity IS NULL + OR oi.normalized_sku_name IS NULL + OR oi.normalized_sku_series IS NULL) + ${dateSql.replace('$2', '$1').replace('$3', '$2')}`, + rangeParams, ) return { updatedCount, @@ -1231,6 +1249,16 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {} } } +function resolveOptionalDateRange(payload: JsonObject) { + const rawFrom = payload.dateFrom ?? payload.date_from + const rawTo = payload.dateTo ?? payload.date_to + if (!rawFrom && !rawTo) return null + return resolveFinanceDateRange( + { dateFrom: rawFrom, dateTo: rawTo }, + { defaultDays: 30, maxDays: 400 }, + ) +} + export async function getProductRow(id: number) { const result = await query('SELECT * FROM product_catalog WHERE id = $1 LIMIT 1', [ id, diff --git a/apps/backend/src/services/cost-center/template-bootstrap-service.ts b/apps/backend/src/services/cost-center/template-bootstrap-service.ts index d1c22e5c..4f57be21 100644 --- a/apps/backend/src/services/cost-center/template-bootstrap-service.ts +++ b/apps/backend/src/services/cost-center/template-bootstrap-service.ts @@ -8,6 +8,7 @@ import { query, withTransaction } from '../../db/client.js' import type { JsonObject } from '../../types/json.js' import { createHttpError } from '../../utils/http.js' +import { resolveFinanceDateRange } from '../../utils/finance-date-range.js' import { nowIso } from '../../utils/time.js' import type { AdminSession } from '../admin/admin-auth-service.js' import { normalizeMatchText, normalizeSkuSeriesEquivalence } from '../product-match/normalize.js' @@ -19,8 +20,9 @@ import { } from './product-catalog-service.js' import { syncAdminWorkOrderTemplateCostVersions } from './work-order-product-service.js' -export async function previewAdminTemplateProductBootstrap() { - const data = await loadTemplateProductBootstrapData() +export async function previewAdminTemplateProductBootstrap(payload: JsonObject = {}) { + const range = resolveBootstrapDateRange(payload) + const data = await loadTemplateProductBootstrapData(range) const plan = buildTemplateMappingPlan(data.evidenceRows) const templateRuleKeys = new Set(data.templates.map((row) => row.rule_key)) const candidates = plan.candidates.filter( @@ -33,6 +35,8 @@ export async function previewAdminTemplateProductBootstrap() { const usedRuleIds = new Set(data.evidenceRows.map((row) => Number(row.product_rule_id))) const candidatesByMatchType = countMappingCandidates(candidates) return { + dateFrom: range.fromDate, + dateTo: range.toDate, templateCount: data.templates.length, productExistingCount: data.templates.length - data.productsToCreate.length, productCreateCount: data.productsToCreate.length, @@ -75,10 +79,11 @@ export async function applyAdminTemplateProductBootstrap( _session?: AdminSession, ) { const repair = payload.repair !== false + const range = resolveBootstrapDateRange(payload) const resetResult = repair - ? await resetImportedTemplateProductBootstrap() + ? await resetImportedTemplateProductBootstrap(range) : { deletedMappingCount: 0, resetOrderItemCount: 0 } - const data = await loadTemplateProductBootstrapData() + const data = await loadTemplateProductBootstrapData(range) if (data.templates.length === 0) { throw createHttpError('没有可导入的接单模板', { statusCode: 400, @@ -185,12 +190,24 @@ export async function applyAdminTemplateProductBootstrap( }) // 名称/系列键必须先于映射回填;所有批次都放在事务外,避免语句超时。 - await backfillAdminNormalizedQuantities({ limit: 50000, all: true }) - const mappingBackfill = await backfillAdminProductMappings({ limit: 20000, all: true }) + await backfillAdminNormalizedQuantities({ + limit: 50000, + all: true, + dateFrom: range.fromDate, + dateTo: range.toDate, + }) + const mappingBackfill = await backfillAdminProductMappings({ + limit: 20000, + all: true, + dateFrom: range.fromDate, + dateTo: range.toDate, + }) mappedOrderItemCount = mappingBackfill.updatedCount const templateCosts = await syncAdminWorkOrderTemplateCostVersions() return { + dateFrom: range.fromDate, + dateTo: range.toDate, templateCount: data.templates.length, createdProductCount, reusedProductCount, @@ -556,7 +573,7 @@ function compareMappingCandidates(left: TemplateMappingCandidate, right: Templat ) } -async function resetImportedTemplateProductBootstrap() { +async function resetImportedTemplateProductBootstrap(range: { fromDate: string; toDate: string }) { let resetOrderItemCount = 0 for (let batch = 0; batch < 20; batch += 1) { const result = await query<{ id: number }>( @@ -589,6 +606,8 @@ async function resetImportedTemplateProductBootstrap() { ) ) WHERE oi.canonical_product_id IS NOT NULL + AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date + BETWEEN $1::date AND $2::date LIMIT 20000 ) UPDATE order_items oi @@ -598,21 +617,31 @@ async function resetImportedTemplateProductBootstrap() { WHERE oi.id = c.id RETURNING oi.id `, + [range.fromDate, range.toDate], ) resetOrderItemCount += result.rowCount || 0 if ((result.rowCount || 0) < 20000) break } const deleted = await query( ` - DELETE FROM channel_product_mappings - WHERE source = 'import' - AND note LIKE '接单模板 %自动初始化%' + DELETE FROM channel_product_mappings m + WHERE m.source = 'import' + AND m.note LIKE '接单模板 %自动初始化%' + AND EXISTS ( + SELECT 1 + FROM order_items oi + JOIN orders o ON o.id = oi.order_id + WHERE oi.canonical_product_id = m.canonical_product_id + AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date + BETWEEN $1::date AND $2::date + ) `, + [range.fromDate, range.toDate], ) return { deletedMappingCount: deleted.rowCount || 0, resetOrderItemCount } } -async function loadTemplateProductBootstrapData() { +async function loadTemplateProductBootstrapData(range: { fromDate: string; toDate: string }) { const [templatesResult, evidenceResult, productsResult, mappingsResult] = await Promise.all([ query( ` @@ -656,6 +685,8 @@ async function loadTemplateProductBootstrapData() { JOIN work_orders wo ON wo.order_item_id = oi.id JOIN work_product_rules r ON r.id = wo.product_rule_id AND r.enabled = TRUE WHERE wo.product_rule_id IS NOT NULL + AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date + BETWEEN $1::date AND $2::date GROUP BY o.provider, o.shop_id, @@ -665,6 +696,7 @@ async function loadTemplateProductBootstrapData() { r.rule_key ORDER BY o.provider, o.shop_id, oi.sku_code, oi.sku_name, wo.product_rule_id `, + [range.fromDate, range.toDate], ), query<{ id: number; canonical_sku: string; canonical_name: string }>( 'SELECT id, canonical_sku, canonical_name FROM product_catalog', @@ -681,7 +713,9 @@ async function loadTemplateProductBootstrapData() { ), ]) - const templates = templatesResult.rows + const usedRuleIds = new Set(evidenceResult.rows.map((row) => Number(row.product_rule_id))) + // 只初始化时间范围内真正有工单证据的模板,避免把很早以前、已不再使用的模板商品重新带出来。 + const templates = templatesResult.rows.filter((row) => usedRuleIds.has(Number(row.id))) const productByCanonicalSku = new Map( productsResult.rows.map((row) => [row.canonical_sku, Number(row.id)]), ) @@ -737,6 +771,14 @@ export function inferTemplateUnitName(productName: string) { return '件' } +function resolveBootstrapDateRange(payload: JsonObject) { + const range = resolveFinanceDateRange( + { dateFrom: payload.dateFrom, dateTo: payload.dateTo }, + { defaultDays: 30, maxDays: 400 }, + ) + return { fromDate: range.fromDate, toDate: range.toDate } +} + function normalizeProductNameKey(value: unknown) { return String(value || '') .normalize('NFKC') diff --git a/apps/backend/src/services/cost-center/work-order-product-service.ts b/apps/backend/src/services/cost-center/work-order-product-service.ts index e8b42a30..7a70aa1d 100644 --- a/apps/backend/src/services/cost-center/work-order-product-service.ts +++ b/apps/backend/src/services/cost-center/work-order-product-service.ts @@ -2,6 +2,7 @@ * 接单平台(work_order)商品回填与模板发单金额同步。 */ import { query } from '../../db/client.js' +import { resolveFinanceDateRange } from '../../utils/finance-date-range.js' import { nowIso } from '../../utils/time.js' import { rebuildFactsForRange } from './affiliate-product-service.js' import { WORK_ORDER_TEMPLATE_COST_BASELINE } from './cost-center-shared.js' @@ -14,6 +15,14 @@ type WorkOrderSkuBackfillRow = { const WORK_ORDER_BACKFILL_BATCH_SIZE = 20000 +function resolveWorkOrderRange(payload: Record) { + const range = resolveFinanceDateRange( + { dateFrom: payload.dateFrom, dateTo: payload.dateTo }, + { defaultDays: 30, maxDays: 400 }, + ) + return { fromDate: range.fromDate, toDate: range.toDate } +} + /** * 回填接单平台订单项的标准商品。 * @@ -21,8 +30,9 @@ const WORK_ORDER_BACKFILL_BATCH_SIZE = 20000 * 仍无法归一的长尾项,再按 `provider + shop_id + sku_code + normalized_sku_name` * 补建 `wo:` 商品和 `sku_code_name` 渠道映射,保证后续同类订单能自动命中。 */ -export async function backfillAdminWorkOrderProductSkus() { +export async function backfillAdminWorkOrderProductSkus(payload: Record = {}) { const now = nowIso() + const range = resolveWorkOrderRange(payload) // order_item_id 有唯一索引,这里可以直接 count,避免 DISTINCT ON 被 // 三个子查询反复物化后撞 15 秒语句超时。 const result = await query( @@ -41,7 +51,10 @@ export async function backfillAdminWorkOrderProductSkus() { JOIN order_items oi ON oi.id = wo.order_item_id JOIN orders o ON o.id = oi.order_id WHERE wo.order_item_id IS NOT NULL + AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date + BETWEEN $1::date AND $2::date `, + [range.fromDate, range.toDate], ) // 单条 UPDATE 覆盖十几万行会撞 15 秒语句超时;按订单项 id 分批, @@ -57,10 +70,14 @@ export async function backfillAdminWorkOrderProductSkus() { wo.order_item_id AS id, wo.product_rule_id FROM work_orders wo + JOIN order_items oi ON oi.id = wo.order_item_id + JOIN orders o ON o.id = oi.order_id WHERE wo.order_item_id IS NOT NULL - AND wo.order_item_id > $1 + AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date + BETWEEN $1::date AND $2::date + AND wo.order_item_id > $3 ORDER BY wo.order_item_id, wo.id DESC - LIMIT $2 + LIMIT $4 ) SELECT woi.id, p.id AS canonical_product_id FROM work_order_item woi @@ -68,7 +85,7 @@ export async function backfillAdminWorkOrderProductSkus() { JOIN product_catalog p ON p.canonical_sku = 'tpl:' || wpr.rule_key ORDER BY woi.id `, - [lastRuleItemId, WORK_ORDER_BACKFILL_BATCH_SIZE], + [range.fromDate, range.toDate, lastRuleItemId, WORK_ORDER_BACKFILL_BATCH_SIZE], ) if (targetRows.rows.length === 0) break const lastSelectedId = Number(targetRows.rows[targetRows.rows.length - 1]?.id || 0) @@ -103,8 +120,11 @@ export async function backfillAdminWorkOrderProductSkus() { oi.normalized_sku_name AS name, MIN(oi.sku_name) AS sample_name FROM order_items oi + JOIN orders o ON o.id = oi.order_id WHERE oi.canonical_product_id IS NULL AND btrim(COALESCE(oi.normalized_sku_name, '')) <> '' + AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date + BETWEEN $2::date AND $3::date AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) GROUP BY 1 ), @@ -138,7 +158,7 @@ export async function backfillAdminWorkOrderProductSkus() { SET updated_at = $1 RETURNING (xmax = 0) AS inserted `, - [now], + [now, range.fromDate, range.toDate], ) const tailMappings = await query<{ id: number }>( @@ -154,6 +174,8 @@ export async function backfillAdminWorkOrderProductSkus() { JOIN orders o ON o.id = oi.order_id WHERE oi.canonical_product_id IS NULL AND btrim(COALESCE(oi.normalized_sku_name, '')) <> '' + AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date + BETWEEN $2::date AND $3::date AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) GROUP BY 1, 2, 3, 5 ), @@ -183,7 +205,7 @@ export async function backfillAdminWorkOrderProductSkus() { ) DO NOTHING RETURNING id `, - [now], + [now, range.fromDate, range.toDate], ) let lastTailItemId = 0 @@ -194,12 +216,15 @@ export async function backfillAdminWorkOrderProductSkus() { WITH tail AS ( SELECT oi.id, oi.normalized_sku_name FROM order_items oi + JOIN orders o ON o.id = oi.order_id WHERE oi.canonical_product_id IS NULL AND btrim(COALESCE(oi.normalized_sku_name, '')) <> '' + AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date + BETWEEN $2::date AND $3::date AND oi.id > $1 AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) ORDER BY oi.id - LIMIT $2 + LIMIT $4 ) SELECT t.id, p.id AS canonical_product_id FROM tail t @@ -212,7 +237,7 @@ export async function backfillAdminWorkOrderProductSkus() { ) p ON TRUE ORDER BY t.id `, - [lastTailItemId, WORK_ORDER_BACKFILL_BATCH_SIZE], + [lastTailItemId, range.fromDate, range.toDate, WORK_ORDER_BACKFILL_BATCH_SIZE], ) if (tailRows.rows.length === 0) break const lastSelectedId = Number(tailRows.rows[tailRows.rows.length - 1]?.id || 0) @@ -248,9 +273,13 @@ export async function backfillAdminWorkOrderProductSkus() { ` SELECT COUNT(*)::int AS total FROM order_items oi + JOIN orders o ON o.id = oi.order_id WHERE oi.canonical_product_id IS NULL + AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date + BETWEEN $1::date AND $2::date AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) `, + [range.fromDate, range.toDate], ) const remainingCount = Number(remainingResult.rows[0]?.total || 0) const matchedCount = Math.max(0, workOrderItemCount - remainingCount) diff --git a/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx b/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx index 072387b6..869ca293 100644 --- a/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx +++ b/apps/frontend/src/pages/admin/cost-center/ProductCatalogPanel.tsx @@ -35,7 +35,9 @@ import type { TableColumnsType } from 'antd' import PercentTable from '@/components/common/PercentTable' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect, useState } from 'react' +import type { Dayjs } from 'dayjs' +import { AdminRangePicker } from '@/components/admin/AdminDatePicker' import dayjs from '@/lib/dayjs' import { applyAdminTemplateProductBootstrap, @@ -68,6 +70,11 @@ export function ProductCatalogPanel() { const queryClient = useQueryClient() const { canConfig, productSourceFilter, refreshAll } = useCostCenter() const [productForm] = Form.useForm() + // 初始化/回填只处理最近 30 天,避免很早以前的乱 SKU 再次进入商品与映射。 + const [backfillRange, setBackfillRange] = useState<[Dayjs, Dayjs]>([ + dayjs().subtract(29, 'day').startOf('day'), + dayjs().endOf('day'), + ]) const [productKeyword, setProductKeyword] = useState('') const [productRootCategoryId, setProductRootCategoryId] = useState(null) @@ -133,23 +140,35 @@ export function ProductCatalogPanel() { : null const productSubCategories = selectedProductRoot?.children || [] + function backfillPayload() { + return { + dateFrom: backfillRange[0].format('YYYY-MM-DD'), + dateTo: backfillRange[1].format('YYYY-MM-DD'), + } + } + async function runTemplateBootstrap() { setBootstrapPreviewing(true) try { - const response = await previewAdminTemplateProductBootstrap() + const response = await previewAdminTemplateProductBootstrap(backfillPayload()) const data = response.data modal.confirm({ title: '从接单模板初始化商品与映射', width: 720, content: ( + 启用接单模板 {data.templateCount} 个:将新建{' '} {data.productCreateCount} 个标准商品,复用{' '} {data.productExistingCount} 个已有商品。 - 根据历史工单中已命中的模板,将建立 {data.mappingCreateCount}{' '} + 根据所选时间范围内工单命中的模板,将建立 {data.mappingCreateCount}{' '} 条渠道映射:编码精确 {data.mappingCreateByMatchType.sku_code}{' '} 条、编码 + 名称系列 {data.mappingCreateByMatchType.sku_code_series}{' '} 条、 编码 + 精确名称 {data.mappingCreateByMatchType.sku_code_name}{' '} @@ -157,10 +176,11 @@ export function ProductCatalogPanel() { 预计可归一 {data.orderItemCoverCount} / {data.evidenceItemCount}{' '} - 个有工单证据的订单项。 + 个范围内有工单证据的订单项。 - {data.templatesWithoutWorkOrders} 个模板暂无历史工单,仍会先建商品。 + {data.templatesWithoutWorkOrders}{' '} + 个启用模板在范围内没有工单证据,本次不会创建。 {data.mappingConflictCount > 0 ? ( )} @@ -200,7 +220,7 @@ export function ProductCatalogPanel() { onOk: async () => { setBootstrapApplying(true) try { - const result = await applyAdminTemplateProductBootstrap() + const result = await applyAdminTemplateProductBootstrap(backfillPayload()) const applied = result.data const rebuildText = applied.deletedImportedMappingCount > 0 @@ -232,7 +252,7 @@ export function ProductCatalogPanel() { async function backfillWorkOrderProducts() { setBackfillingWorkOrderSkus(true) try { - const response = await backfillAdminWorkOrderProductSkus() + const response = await backfillAdminWorkOrderProductSkus(backfillPayload()) const result = response.data const factsText = result.factsRebuild ? `;已重算 ${result.factsRebuild.fromDate} ~ ${result.factsRebuild.toDate} 的销售事实` @@ -259,7 +279,7 @@ export function ProductCatalogPanel() { async function normalizeManualProductCatalog() { setNormalizingManualProducts(true) try { - const preview = await normalizeAdminManualProducts({ dryRun: true }) + const preview = await normalizeAdminManualProducts({ dryRun: true, ...backfillPayload() }) const data = preview.data if (data.distinctSkuCount === 0) { message.success('没有待归一的人工订单项') @@ -283,7 +303,7 @@ export function ProductCatalogPanel() { okText: '开始归一', cancelText: '取消', onOk: async () => { - const response = await normalizeAdminManualProducts({}) + const response = await normalizeAdminManualProducts(backfillPayload()) const result = response.data message.success( `已新建 ${result.createdProductCount} 个商品、归一 ${result.updatedItemCount} 个订单项${ @@ -714,6 +734,17 @@ export function ProductCatalogPanel() { extra={ canConfig ? ( + {!productSourceFilter || + productSourceFilter === 'work_order' || + productSourceFilter === 'manual' ? ( + { + if (value?.[0] && value?.[1]) setBackfillRange([value[0], value[1]]) + }} + allowClear={false} + /> + ) : null} {productSourceFilter === 'affiliate_dash' ? ( <>