feat(cost-center): 商品初始化/回填增加时间范围,默认最近 30 天

- 接单模板初始化预览/执行支持 dateFrom/dateTo,只使用范围内工单证据;
  范围外模板不再先建商品
- 按工单回填商品支持时间范围;人工商品归一 UI 传时间范围
- 模板初始化重置只清理范围内导入映射/订单项,避免误伤范围外数据
- 初始化/回填默认最近 30 天,自动发货仍固定 30 天
- 文档更新初始化口径与页面操作
This commit is contained in:
yml2213
2026-10-26 16:26:48 +08:00
parent 288a0526ad
commit 2b967ddd4c
9 changed files with 206 additions and 53 deletions
@@ -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<ProductFormValues>()
// 初始化/回填只处理最近 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<number | null>(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: (
<Space direction="vertical" size={8}>
<Alert
type="info"
showIcon
message={`统计范围:${data.dateFrom} ~ ${data.dateTo}(默认最近 30 天)`}
/>
<span>
启用接单模板 <strong>{data.templateCount}</strong> 个:将新建{' '}
<strong>{data.productCreateCount}</strong> 个标准商品,复用{' '}
<strong>{data.productExistingCount}</strong> 个已有商品。
</span>
<span>
根据历史工单中已命中的模板,将建立 <strong>{data.mappingCreateCount}</strong>{' '}
根据所选时间范围内工单命中的模板,将建立 <strong>{data.mappingCreateCount}</strong>{' '}
条渠道映射:编码精确 <strong>{data.mappingCreateByMatchType.sku_code}</strong>{' '}
条、编码 + 名称系列 <strong>{data.mappingCreateByMatchType.sku_code_series}</strong>{' '}
条、 编码 + 精确名称 <strong>{data.mappingCreateByMatchType.sku_code_name}</strong>{' '}
@@ -157,10 +176,11 @@ export function ProductCatalogPanel() {
</span>
<span>
预计可归一 <strong>{data.orderItemCoverCount}</strong> / {data.evidenceItemCount}{' '}
个有工单证据的订单项。
个范围内有工单证据的订单项。
</span>
<span>
<strong>{data.templatesWithoutWorkOrders}</strong> 个模板暂无历史工单,仍会先建商品。
<strong>{data.templatesWithoutWorkOrders}</strong>{' '}
个启用模板在范围内没有工单证据,本次不会创建。
</span>
{data.mappingConflictCount > 0 ? (
<Alert
@@ -186,7 +206,7 @@ export function ProductCatalogPanel() {
<Alert
type="success"
showIcon
message="所有可判定的历史证据都能唯一归属,没有需要人工处理的 SKU 冲突。"
message="范围内可判定的工单证据都能唯一归属,没有需要人工处理的 SKU 冲突。"
/>
)}
<Typography.Text type="secondary">
@@ -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 ? (
<Space size={6} wrap>
{!productSourceFilter ||
productSourceFilter === 'work_order' ||
productSourceFilter === 'manual' ? (
<AdminRangePicker
value={backfillRange}
onChange={(value) => {
if (value?.[0] && value?.[1]) setBackfillRange([value[0], value[1]])
}}
allowClear={false}
/>
) : null}
{productSourceFilter === 'affiliate_dash' ? (
<>
<Button
@@ -41,16 +41,23 @@ export function fetchAdminCostCenterOptions() {
return apiGet<CostCenterOptions>('/api/v1/admin/cost-center/options')
}
export function previewAdminTemplateProductBootstrap() {
export function previewAdminTemplateProductBootstrap(payload?: {
dateFrom?: string
dateTo?: string
}) {
return apiGet<TemplateProductBootstrapPreview>(
'/api/v1/admin/cost-center/template-bootstrap/preview',
payload,
)
}
export function applyAdminTemplateProductBootstrap() {
export function applyAdminTemplateProductBootstrap(payload?: {
dateFrom?: string
dateTo?: string
}) {
return apiPost<TemplateProductBootstrapResult>(
'/api/v1/admin/cost-center/template-bootstrap/apply',
{ repair: true },
{ repair: true, ...payload },
{ timeout: 300_000 },
)
}
@@ -66,7 +73,10 @@ export function syncAdminWorkOrderTemplateCostVersions() {
}>('/api/v1/admin/cost-center/work-order-template-costs/sync')
}
export function backfillAdminWorkOrderProductSkus() {
export function backfillAdminWorkOrderProductSkus(payload?: {
dateFrom?: string
dateTo?: string
}) {
return apiPost<{
updatedCount: number
skippedCount: number
@@ -84,7 +94,7 @@ export function backfillAdminWorkOrderProductSkus() {
insertedCount: number
batchCount: number
} | null
}>('/api/v1/admin/cost-center/work-order-products/backfill')
}>('/api/v1/admin/cost-center/work-order-products/backfill', payload || {})
}
export function fetchAdminProductCatalog(params?: Record<string, unknown>) {
@@ -173,6 +173,8 @@ export interface CostCenterOptions {
}
export interface TemplateProductBootstrapPreview {
dateFrom: string
dateTo: string
templateCount: number
productExistingCount: number
productCreateCount: number