feat(cost-center): 商品初始化/回填增加时间范围,默认最近 30 天
- 接单模板初始化预览/执行支持 dateFrom/dateTo,只使用范围内工单证据; 范围外模板不再先建商品 - 按工单回填商品支持时间范围;人工商品归一 UI 传时间范围 - 模板初始化重置只清理范围内导入映射/订单项,避免误伤范围外数据 - 初始化/回填默认最近 30 天,自动发货仍固定 30 天 - 文档更新初始化口径与页面操作
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user