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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user