feat(finance): 自动发货订单项按上游 SKU 直接归一

自动发货只有 affiliate-dash 单一上游渠道,订单项直接按 fulfillment_tasks.context_json.skuCode 归一为 aff:<skuCode>,不写 channel_product_mappings。\n\n新增 /cost-center/affiliate-products/backfill:按 order_item_id 取最新任务 SKU 回填 canonical_product_id,只更新有变化的订单项,并自动重算受影响日期的销售事实;返回 updatedCount/skippedCount/remainingCount。\n\n自动发货 SKU 列表改为实时统计履约任务数与最近履约时间,不读会被价格导入覆盖的 attributes_json。\n\n本地实测 41,676 个订单项全部归一,自动发货未归一收入 5,089,712.26 元降至 0;整体未归一占比 79.67% 降至 60.47%。
This commit is contained in:
yml2213
2026-10-22 18:41:31 +08:00
parent 0889235870
commit 54d2eb2f59
7 changed files with 227 additions and 10 deletions
@@ -40,6 +40,7 @@ import { usePersistedState } from '@/hooks/usePersistedState'
import dayjs from '@/lib/dayjs'
import {
applyAdminTemplateProductBootstrap,
backfillAdminAffiliateDashSkus,
backfillAdminNormalizedQuantities,
backfillAdminProductMappings,
createAdminChannelProductMapping,
@@ -366,6 +367,7 @@ export default function AdminCostCenterPage() {
const [bootstrapPreviewing, setBootstrapPreviewing] = useState(false)
const [bootstrapApplying, setBootstrapApplying] = useState(false)
const [syncingAffiliate, setSyncingAffiliate] = useState(false)
const [backfillingAffiliateSkus, setBackfillingAffiliateSkus] = useState(false)
const versionScopeType = Form.useWatch('scopeType', versionForm)
const versionCostItemId = Form.useWatch('costItemId', versionForm)
@@ -669,6 +671,29 @@ export default function AdminCostCenterPage() {
}
}
async function backfillAffiliateSkus() {
setBackfillingAffiliateSkus(true)
try {
const response = await backfillAdminAffiliateDashSkus()
const result = response.data
const factsText = result.factsRebuild
? `;已重算 ${result.factsRebuild.fromDate} ~ ${result.factsRebuild.toDate} 的销售事实`
: ''
const remainingText =
result.remainingCount > 0
? `;仍有 ${result.remainingCount} 个订单项缺少可匹配的自动发货 SKU`
: ''
message.success(
`已回填 ${result.updatedCount} 个订单项,${result.skippedCount} 个已是归一状态${factsText}${remainingText}`,
)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '回填自动发货 SKU 失败')
} finally {
setBackfillingAffiliateSkus(false)
}
}
function openEditProduct(item: ProductCatalogItem) {
setEditingProduct(item)
productForm.setFieldsValue({
@@ -1970,6 +1995,13 @@ export default function AdminCostCenterPage() {
>
同步自动发货商品
</Button>
<Button
icon={<SyncOutlined />}
loading={backfillingAffiliateSkus}
onClick={() => void backfillAffiliateSkus()}
>
回填自动发货 SKU
</Button>
<Button
icon={<ReloadOutlined />}
loading={productsQuery.isFetching}
@@ -1986,7 +2018,7 @@ export default function AdminCostCenterPage() {
type="info"
showIcon
style={{ marginBottom: 12 }}
message="自动发货只按上游货源 SKU 配采购成本:这里不维护游戏分类、渠道映射和商品上下架,那些只属于接单平台。"
message="自动发货只按上游货源 SKU 配采购成本:订单项直接按 aff:<skuCode> 归一,不走渠道映射;这里不维护游戏分类和商品上下架,那些只属于接单平台。"
/>
<Table
tableLayout="fixed"
@@ -2031,14 +2063,14 @@ export default function AdminCostCenterPage() {
{
title: '履约任务',
width: 110,
render: (_, row) => `${Number(row.attributes?.taskCount || 0)} 单`,
render: (_, row) => `${row.affiliateTaskCount} 单`,
},
{
title: '最近履约',
width: 180,
render: (_, row) =>
row.attributes?.lastFulfilledAt
? formatAdminDateTime(String(row.attributes.lastFulfilledAt))
row.affiliateLastFulfilledAt
? formatAdminDateTime(row.affiliateLastFulfilledAt)
: '-',
},
{
@@ -52,6 +52,23 @@ export function syncAdminAffiliateDashProducts() {
)
}
export function backfillAdminAffiliateDashSkus() {
return apiPost<{
updatedCount: number
skippedCount: number
remainingCount: number
affiliateItemCount: number
matchedCount: number
factsRebuild: {
fromDate: string
toDate: string
deletedCount: number
insertedCount: number
batchCount: number
} | null
}>('/api/v1/admin/cost-center/affiliate-products/backfill')
}
export function updateAdminProductCatalog(productId: number, payload: Record<string, unknown>) {
return apiPatch<{ product: ProductCatalogItem }>(
`/api/v1/admin/cost-center/products/${productId}`,
@@ -27,6 +27,9 @@ export interface ProductCatalogItem {
attributes: Record<string, unknown>
mappingCount: number
costVersionCount: number
/** 自动发货履约任务数,实时取自 fulfillment_tasks。 */
affiliateTaskCount: number
affiliateLastFulfilledAt: string | null
createdAt: string
updatedAt: string
}