feat(finance): 接单模板发单金额落规则级成本版本

此前接单平台成本版本只有 sys_work_order_reward 的全局 0 元占位,662 个模板的发单金额从未进入 cost_versions。\n\n现在为 662 个模板各生成一条规则级版本:634 条固定发单金额 + 28 条按件单价;全局 0 元版本仅作“按工单实际结算”占位。成本版本页新增“用户实付均价”和“平均实发/项”,支持按模板名称/规则键搜索,按商品筛选也能命中规则版本。\n\n历史工单快照关联到对应模板版本;模板保存时自动同步单条成本版本;删除模板会先把历史版本脱钩停用,保留快照可追溯。\n\n新增迁移 112 覆盖索引,用户实付聚合从约 1.1s 降到约 150ms。
This commit is contained in:
yml2213
2026-10-22 19:29:53 +08:00
parent 72e1141c8a
commit 7c8832078f
12 changed files with 547 additions and 43 deletions
@@ -65,6 +65,7 @@ import {
resolveAdminCostException,
retireAdminCostVersion,
syncAdminAffiliateDashProducts,
syncAdminWorkOrderTemplateCostVersions,
updateAdminChannelProductMapping,
updateAdminCostItem,
updateAdminProductCatalog,
@@ -370,6 +371,7 @@ export default function AdminCostCenterPage() {
const [syncingAffiliate, setSyncingAffiliate] = useState(false)
const [backfillingAffiliateSkus, setBackfillingAffiliateSkus] = useState(false)
const [backfillingWorkOrderSkus, setBackfillingWorkOrderSkus] = useState(false)
const [syncingTemplateCosts, setSyncingTemplateCosts] = useState(false)
const versionScopeType = Form.useWatch('scopeType', versionForm)
const versionCostItemId = Form.useWatch('costItemId', versionForm)
@@ -640,7 +642,7 @@ export default function AdminCostCenterPage() {
? `;保留 ${applied.mappingConflictCount} 组冲突待人工处理`
: ''
message.success(
`已新建 ${applied.createdProductCount} 个商品、${applied.createdMappingCount} 条映射,回填 ${applied.mappedOrderItemCount} 个订单项${rebuildText}${conflictText}`,
`已新建 ${applied.createdProductCount} 个商品、${applied.createdMappingCount} 条映射,同步 ${applied.templateCostVersionCount} 条模板发单金额,回填 ${applied.mappedOrderItemCount} 个订单项${rebuildText}${conflictText}`,
)
refreshAll()
} catch (error) {
@@ -723,6 +725,22 @@ export default function AdminCostCenterPage() {
}
}
async function syncTemplateCosts() {
setSyncingTemplateCosts(true)
try {
const response = await syncAdminWorkOrderTemplateCostVersions()
const result = response.data
message.success(
`已同步 ${result.activeVersionCount} 条模板发单金额(新建 ${result.createdCount},停用 ${result.retiredCount}),关联 ${result.linkedSnapshotCount} 条成本快照`,
)
refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '同步接单模板发单金额失败')
} finally {
setSyncingTemplateCosts(false)
}
}
function openEditProduct(item: ProductCatalogItem) {
setEditingProduct(item)
productForm.setFieldsValue({
@@ -1201,9 +1219,11 @@ export default function AdminCostCenterPage() {
title: '成本版本',
dataIndex: 'costVersionCount',
width: 110,
render: (value: number) =>
render: (value: number, row) =>
value > 0 ? (
<Typography.Text type="success">{value} 个版本</Typography.Text>
) : row.carrierType === 'work_order' ? (
<Tag color="blue">按工单实际结算</Tag>
) : (
<Typography.Text type="secondary">未配置</Typography.Text>
),
@@ -1515,22 +1535,73 @@ export default function AdminCostCenterPage() {
title: '范围',
dataIndex: 'scopeType',
width: 200,
ellipsis: true,
render: (value: string, row) => {
if (value === 'global') return '全局'
if (value === 'global') {
return row.isSystemPlaceholder ? '全局(系统占位)' : '全局'
}
if (value === 'provider') return providerLabel(row.provider)
if (value === 'shop') return `${providerLabel(row.provider)} / ${row.shopId}`
if (value === 'product') return row.canonicalName || `商品 #${row.canonicalProductId}`
return `规则 #${row.workProductRuleId}`
const ruleLabel =
row.workProductRuleName ||
(row.workProductRuleId ? `规则 #${row.workProductRuleId}` : '历史模板版本')
const ruleSub =
row.workProductRuleKey ||
(row.workProductRuleId ? '' : row.changeReason || '模板已删除,历史版本保留')
return (
<Space direction="vertical" size={0} style={{ width: '100%' }}>
<Typography.Text ellipsis={{ tooltip: ruleLabel }}>{ruleLabel}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }} ellipsis>
{ruleSub}
</Typography.Text>
</Space>
)
},
},
{
title: '金额/比例',
width: 130,
title: '用户实付均价',
width: 160,
render: (_, row) =>
row.pricingMode === 'percent_of_revenue'
? formatRate(row.rateBps)
: formatMoney(row.amountFen),
row.revenueItemCount > 0 ? (
<Space direction="vertical" size={0}>
<span>{formatMoney(row.revenueAvgFen)}</span>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.revenueItemCount} 项 / 合计 {formatMoney(row.revenueFen)}
</Typography.Text>
</Space>
) : (
'-'
),
},
{
title: activeCarrier === 'work_order' ? '发单金额/比例' : '金额/比例',
width: 170,
render: (_, row) => {
if (row.isSystemPlaceholder) return <Tag>按工单实际结算</Tag>
if (row.pricingMode === 'percent_of_revenue') return formatRate(row.rateBps)
if (row.amountKind === 'unit_price') {
return (
<Space direction="vertical" size={0}>
<span>{formatMoney(row.amountFen)} / 件</span>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.revenueItemCount > 0
? `平均实发 ${formatMoney(row.rewardAvgFen)} / 项`
: '暂无成交'}
</Typography.Text>
</Space>
)
}
return (
<Space direction="vertical" size={0}>
<span>{formatMoney(row.amountFen)}</span>
{row.revenueItemCount > 0 ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
平均实发 {formatMoney(row.rewardAvgFen)} / 项
</Typography.Text>
) : null}
</Space>
)
},
},
{
title: '生效时间',
@@ -1538,9 +1609,13 @@ export default function AdminCostCenterPage() {
width: 190,
render: (value: string, row) => (
<Space direction="vertical" size={0}>
<span>{formatAdminDateTime(value)}</span>
<span>{row.isSystemPlaceholder ? '系统占位' : formatAdminDateTime(value)}</span>
<Typography.Text type="secondary">
{row.effectiveTo ? `至 ${formatAdminDateTime(row.effectiveTo)}` : '长期'}
{row.isSystemPlaceholder
? '按工单实际结算'
: row.effectiveTo
? `至 ${formatAdminDateTime(row.effectiveTo)}`
: '长期'}
</Typography.Text>
</Space>
),
@@ -2270,13 +2345,24 @@ export default function AdminCostCenterPage() {
}
extra={
canConfig ? (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => openCreateVersion()}
>
新建版本
</Button>
<Space>
{activeCarrier === 'all' || activeCarrier === 'work_order' ? (
<Button
icon={<SyncOutlined />}
loading={syncingTemplateCosts}
onClick={() => void syncTemplateCosts()}
>
同步模板发单金额
</Button>
) : null}
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => openCreateVersion()}
>
新建版本
</Button>
</Space>
) : null
}
>
@@ -2285,7 +2371,7 @@ export default function AdminCostCenterPage() {
showIcon
style={{ marginBottom: 12 }}
message="成本版本 = 给成本项定价:一条版本只表达「多少钱 + 何时生效 + 对谁适用」"
description="按件/按单/按比例计价;范围优先级 接单规则 > 标准商品 > 店铺 > 来源 > 全局。改价请新建版本,不要改已生效版本;每个成本项建议只保留一个生效版本。"
description="接单平台按模板逐条建规则级版本;同一适用范围只保留一个生效版本,改价请新增版本。全局 0 元版本只是「按工单实际结算」的系统占位。"
/>
<Space style={{ marginBottom: 16 }} wrap>
<Input
@@ -2322,7 +2408,7 @@ export default function AdminCostCenterPage() {
columns={versionColumns}
dataSource={versions}
loading={versionsQuery.isLoading}
scroll={{ x: 1100 }}
scroll={{ x: 1340 }}
pagination={buildAdminTablePagination({
current: versionPage,
pageSize: versionPageSize,
@@ -38,6 +38,17 @@ export function applyAdminTemplateProductBootstrap() {
)
}
export function syncAdminWorkOrderTemplateCostVersions() {
return apiPost<{
ruleCount: number
unitPriceRuleCount: number
activeVersionCount: number
createdCount: number
retiredCount: number
linkedSnapshotCount: number
}>('/api/v1/admin/cost-center/work-order-template-costs/sync')
}
export function backfillAdminWorkOrderProductSkus() {
return apiPost<{
updatedCount: number
@@ -92,6 +92,17 @@ export interface CostVersion {
canonicalSku: string
canonicalName: string
workProductRuleId: number | null
workProductRuleName: string
workProductRuleKey: string
ruleRewardAmountFen: number
ruleUnitPriceFen: number
revenueItemCount: number
revenueFen: number
revenueAvgFen: number
rewardAvgFen: number
rewardFen: number
amountKind: 'fixed' | 'unit_price'
isSystemPlaceholder: boolean
amountFen: number
rateBps: number
effectiveFrom: string
@@ -195,6 +206,9 @@ export interface TemplateProductBootstrapResult {
resetOrderItemCount: number
mappingConflictCount: number
mappingConflictItemCount: number
templateCostVersionCount: number
templateCostCreatedCount: number
templateCostRetiredCount: number
}
export interface CostLine {