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:
@@ -0,0 +1,14 @@
|
||||
-- 112_cost_reporting_indexes.sql —— 成本版本页按接单规则汇总用户实付。
|
||||
--
|
||||
-- 背景:成本版本列表需要同时展示「用户实付均价」和「模板发单金额」。
|
||||
-- 原查询按 work_orders.product_rule_id 聚合约 11 万工单 + 31 万订单项,
|
||||
-- 没有覆盖索引时需要 1 秒以上;以下两个覆盖索引把聚合降到约 150 毫秒。
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_work_orders_rule_cover
|
||||
ON work_orders (product_rule_id)
|
||||
INCLUDE (order_item_id)
|
||||
WHERE order_item_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_revenue_cover
|
||||
ON order_items (id)
|
||||
INCLUDE (order_id, revenue_amount_fen);
|
||||
@@ -312,6 +312,25 @@ export async function syncOpenWorkOrderPriceFromRule(input: {
|
||||
})
|
||||
}
|
||||
|
||||
export async function detachWorkProductRuleCostVersions(
|
||||
ruleId: number,
|
||||
label: string,
|
||||
): Promise<{ updated: number }> {
|
||||
const result = await query(
|
||||
`
|
||||
UPDATE cost_versions
|
||||
SET work_product_rule_id = NULL,
|
||||
status = 'retired',
|
||||
effective_to = COALESCE(effective_to, NOW()),
|
||||
change_reason = LEFT('已删除模板 ' || $2 || ';' || change_reason, 500),
|
||||
updated_at = NOW()
|
||||
WHERE work_product_rule_id = $1
|
||||
`,
|
||||
[ruleId, label],
|
||||
)
|
||||
return { updated: result.rowCount || 0 }
|
||||
}
|
||||
|
||||
export async function deleteWorkProductRule(
|
||||
ruleId: number | string,
|
||||
): Promise<{ deleted: boolean }> {
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
resolveAdminCostException,
|
||||
retireAdminCostVersion,
|
||||
syncAdminAffiliateDashProducts,
|
||||
syncAdminWorkOrderTemplateCostVersions,
|
||||
updateAdminChannelMapping,
|
||||
updateAdminCostItem,
|
||||
updateAdminProduct,
|
||||
@@ -150,6 +151,22 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/cost-center/work-order-template-costs/sync',
|
||||
requireAdminPermission('finance.config'),
|
||||
createJsonHandler(() => syncAdminWorkOrderTemplateCostVersions(), {
|
||||
successMessage: '接单模板发单金额已同步',
|
||||
errorMessage: '同步接单模板发单金额失败',
|
||||
scope: '[admin/cost-center/work-order-template-costs/sync]',
|
||||
audit: (_req, data) => ({
|
||||
action: 'cost_work_order_template_costs_synced',
|
||||
targetType: 'cost_versions',
|
||||
targetId: '',
|
||||
data: data as Record<string, unknown>,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/cost-center/products',
|
||||
requireAdminPermission('finance.view'),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express'
|
||||
import { setHttpCache } from '../../../middleware/http-cache.js'
|
||||
import { syncAdminWorkOrderTemplateCostVersions } from '../../../services/admin/admin-cost-center-service.js'
|
||||
|
||||
import {
|
||||
deleteAdminWorkCategory,
|
||||
@@ -147,17 +148,25 @@ router.get(
|
||||
router.post(
|
||||
'/worker-platform/product-rules',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler((req) => saveAdminWorkProductRule(req.body || {}), {
|
||||
successMessage: '接单物品规则已保存',
|
||||
errorMessage: '保存接单物品规则失败',
|
||||
scope: '[admin/worker-platform/product-rules]',
|
||||
audit: (_req, data) => ({
|
||||
action: 'work_product_rule_saved',
|
||||
targetType: 'work_product_rule',
|
||||
targetId: String((data as { rule?: { ruleKey?: string } })?.rule?.ruleKey || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
}),
|
||||
createJsonHandler(
|
||||
async (req) => {
|
||||
const result = await saveAdminWorkProductRule(req.body || {})
|
||||
const ruleId = Number((result as { rule?: { id?: number } })?.rule?.id || 0)
|
||||
const templateCost = ruleId ? await syncAdminWorkOrderTemplateCostVersions(ruleId) : null
|
||||
return { ...result, templateCost }
|
||||
},
|
||||
{
|
||||
successMessage: '接单物品规则已保存',
|
||||
errorMessage: '保存接单物品规则失败',
|
||||
scope: '[admin/worker-platform/product-rules]',
|
||||
audit: (_req, data) => ({
|
||||
action: 'work_product_rule_saved',
|
||||
targetType: 'work_product_rule',
|
||||
targetId: String((data as { rule?: { ruleKey?: string } })?.rule?.ruleKey || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
|
||||
@@ -51,6 +51,8 @@ const COST_VERSION_SOURCES = new Set(['manual', 'upstream_sync', 'import'])
|
||||
const COST_VERSION_STATUSES = new Set(['draft', 'active', 'retired'])
|
||||
const COST_EXCEPTION_STATUSES = new Set(['open', 'resolved', 'ignored'])
|
||||
const PROVIDERS = ['91kaquan', 'pdd', 'douyin_mashida']
|
||||
/** 首次把接单模板发单金额落成成本版本时使用的基线时间。 */
|
||||
const WORK_ORDER_TEMPLATE_COST_BASELINE = '2026-07-10T00:00:00+08:00'
|
||||
const PRODUCT_CATEGORY_JOINS = `
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT wc.id, wc.name, wc.parent_id
|
||||
@@ -180,6 +182,14 @@ type CostVersionRow = {
|
||||
canonical_sku: string | null
|
||||
canonical_name: string | null
|
||||
work_product_rule_id: number | null
|
||||
work_product_rule_name: string | null
|
||||
work_product_rule_key: string | null
|
||||
rule_reward_amount: number | null
|
||||
rule_unit_price_fen: number | null
|
||||
revenue_item_count: number | null
|
||||
revenue_fen: number | null
|
||||
reward_avg_fen: number | null
|
||||
reward_fen: number | null
|
||||
amount_fen: number
|
||||
rate_bps: number
|
||||
effective_from: string
|
||||
@@ -466,7 +476,20 @@ export async function listAdminProducts(payload: JsonObject = {}) {
|
||||
root.id AS root_category_id,
|
||||
root.name AS root_category_name,
|
||||
(SELECT COUNT(*)::int FROM channel_product_mappings m WHERE m.canonical_product_id = p.id) AS mapping_count,
|
||||
(SELECT COUNT(*)::int FROM cost_versions v WHERE v.canonical_product_id = p.id AND v.status = 'active') AS cost_version_count,
|
||||
(
|
||||
SELECT COUNT(*)::int
|
||||
FROM cost_versions v
|
||||
WHERE v.status = 'active'
|
||||
AND v.canonical_product_id = p.id
|
||||
) + (
|
||||
SELECT COUNT(*)::int
|
||||
FROM cost_versions v
|
||||
JOIN work_product_rules wpr ON wpr.id = v.work_product_rule_id
|
||||
WHERE v.status = 'active'
|
||||
AND v.scope_type = 'rule'
|
||||
AND p.canonical_sku LIKE 'tpl:%'
|
||||
AND wpr.rule_key = substring(p.canonical_sku from 5)
|
||||
) AS cost_version_count,
|
||||
COALESCE(aff.task_count, 0) AS affiliate_task_count,
|
||||
aff.last_fulfilled_at AS affiliate_last_fulfilled_at
|
||||
FROM product_catalog p
|
||||
@@ -1457,6 +1480,7 @@ export async function applyAdminTemplateProductBootstrap(
|
||||
await backfillAdminNormalizedQuantities({ limit: 50000, all: true })
|
||||
const mappingBackfill = await backfillAdminProductMappings({ limit: 20000, all: true })
|
||||
mappedOrderItemCount = mappingBackfill.updatedCount
|
||||
const templateCosts = await syncAdminWorkOrderTemplateCostVersions()
|
||||
|
||||
return {
|
||||
templateCount: data.templates.length,
|
||||
@@ -1469,6 +1493,9 @@ export async function applyAdminTemplateProductBootstrap(
|
||||
resetOrderItemCount: resetResult.resetOrderItemCount,
|
||||
mappingConflictCount: plan.conflicts.length,
|
||||
mappingConflictItemCount: plan.unresolvedItemCount,
|
||||
templateCostVersionCount: templateCosts.activeVersionCount,
|
||||
templateCostCreatedCount: templateCosts.createdCount,
|
||||
templateCostRetiredCount: templateCosts.retiredCount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1907,6 +1934,197 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
}
|
||||
}
|
||||
|
||||
type WorkOrderTemplateCostSyncRow = {
|
||||
rule_count: number
|
||||
unit_price_rule_count: number
|
||||
active_version_count: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 把接单模板的发单金额同步成规则级成本版本。
|
||||
*
|
||||
* 固定金额模板取 `reward_amount`;按件模板取 `unit_price_fen`,数量由订单 SKU
|
||||
* 决定。全局 0 元版本只作为“按工单实际结算”的占位,不再代表模板价格。
|
||||
*/
|
||||
export async function syncAdminWorkOrderTemplateCostVersions(ruleId?: number) {
|
||||
const now = nowIso()
|
||||
const scopedRuleId = ruleId && Number.isFinite(ruleId) ? Number(ruleId) : null
|
||||
const retired = await query(
|
||||
`
|
||||
WITH target AS (
|
||||
SELECT
|
||||
wpr.id AS rule_id,
|
||||
CASE
|
||||
WHEN wpr.unit_price_fen > 0 AND wpr.reward_amount = 0 THEN wpr.unit_price_fen
|
||||
ELSE wpr.reward_amount
|
||||
END AS amount_fen
|
||||
FROM work_product_rules wpr
|
||||
WHERE ($2::bigint IS NULL OR wpr.id = $2)
|
||||
),
|
||||
sys AS (
|
||||
SELECT id FROM cost_items
|
||||
WHERE code = 'sys_work_order_reward' AND status = 'active'
|
||||
),
|
||||
changed AS (
|
||||
SELECT v.id
|
||||
FROM cost_versions v
|
||||
JOIN sys si ON si.id = v.cost_item_id
|
||||
JOIN target t ON t.rule_id = v.work_product_rule_id
|
||||
WHERE v.scope_type = 'rule'
|
||||
AND v.status = 'active'
|
||||
AND v.amount_fen <> t.amount_fen
|
||||
)
|
||||
UPDATE cost_versions v
|
||||
SET status = 'retired',
|
||||
effective_to = $1,
|
||||
change_reason = '模板发单金额变更',
|
||||
updated_at = $1
|
||||
FROM changed c
|
||||
WHERE v.id = c.id
|
||||
RETURNING v.id
|
||||
`,
|
||||
[now, scopedRuleId],
|
||||
)
|
||||
|
||||
const inserted = await query<{ id: number }>(
|
||||
`
|
||||
WITH target AS (
|
||||
SELECT
|
||||
wpr.id AS rule_id,
|
||||
CASE
|
||||
WHEN wpr.unit_price_fen > 0 AND wpr.reward_amount = 0 THEN wpr.unit_price_fen
|
||||
ELSE wpr.reward_amount
|
||||
END AS amount_fen
|
||||
FROM work_product_rules wpr
|
||||
WHERE ($3::bigint IS NULL OR wpr.id = $3)
|
||||
),
|
||||
sys AS (
|
||||
SELECT id FROM cost_items
|
||||
WHERE code = 'sys_work_order_reward' AND status = 'active'
|
||||
),
|
||||
missing AS (
|
||||
SELECT
|
||||
t.rule_id,
|
||||
t.amount_fen,
|
||||
si.id AS cost_item_id,
|
||||
EXISTS (
|
||||
SELECT 1 FROM cost_versions h
|
||||
WHERE h.cost_item_id = si.id
|
||||
AND h.scope_type = 'rule'
|
||||
AND h.work_product_rule_id = t.rule_id
|
||||
) AS has_history
|
||||
FROM target t
|
||||
CROSS JOIN sys si
|
||||
WHERE t.amount_fen > 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM cost_versions v
|
||||
WHERE v.cost_item_id = si.id
|
||||
AND v.scope_type = 'rule'
|
||||
AND v.work_product_rule_id = t.rule_id
|
||||
AND v.status = 'active'
|
||||
)
|
||||
)
|
||||
INSERT INTO cost_versions (
|
||||
cost_item_id, scope_type, provider, shop_id, canonical_product_id,
|
||||
work_product_rule_id, amount_fen, rate_bps, effective_from, effective_to,
|
||||
source, status, change_reason, created_by_admin_user_id,
|
||||
approved_by_admin_user_id, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
m.cost_item_id,
|
||||
'rule',
|
||||
'',
|
||||
'',
|
||||
NULL,
|
||||
m.rule_id,
|
||||
m.amount_fen,
|
||||
0,
|
||||
CASE WHEN m.has_history THEN $1::timestamptz ELSE $2::timestamptz END,
|
||||
NULL,
|
||||
'import',
|
||||
'active',
|
||||
CASE
|
||||
WHEN wpr.unit_price_fen > 0 AND wpr.reward_amount = 0
|
||||
THEN '模板按件发单单价(数量取订单 SKU)'
|
||||
ELSE '模板固定发单金额'
|
||||
END,
|
||||
NULL,
|
||||
NULL,
|
||||
$1,
|
||||
$1
|
||||
FROM missing m
|
||||
JOIN work_product_rules wpr ON wpr.id = m.rule_id
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id
|
||||
`,
|
||||
[now, WORK_ORDER_TEMPLATE_COST_BASELINE, scopedRuleId],
|
||||
)
|
||||
|
||||
const linked = await query(
|
||||
`
|
||||
WITH rule_version AS (
|
||||
SELECT DISTINCT ON (v.work_product_rule_id)
|
||||
v.work_product_rule_id,
|
||||
v.id AS cost_version_id,
|
||||
v.cost_item_id
|
||||
FROM cost_versions v
|
||||
JOIN cost_items ci ON ci.id = v.cost_item_id
|
||||
AND ci.code = 'sys_work_order_reward'
|
||||
WHERE v.scope_type = 'rule'
|
||||
AND v.status = 'active'
|
||||
AND ($1::bigint IS NULL OR v.work_product_rule_id = $1)
|
||||
ORDER BY v.work_product_rule_id, v.effective_from DESC, v.id DESC
|
||||
)
|
||||
UPDATE cost_snapshots cs
|
||||
SET cost_version_id = rv.cost_version_id
|
||||
FROM work_orders wo
|
||||
JOIN rule_version rv ON rv.work_product_rule_id = wo.product_rule_id
|
||||
WHERE cs.order_item_id = wo.order_item_id
|
||||
AND cs.cost_item_id = rv.cost_item_id
|
||||
AND cs.superseded_at IS NULL
|
||||
AND cs.cost_version_id <> rv.cost_version_id
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM cost_versions gv
|
||||
WHERE gv.id = cs.cost_version_id AND gv.scope_type = 'global'
|
||||
)
|
||||
AND ($1::bigint IS NULL OR wo.product_rule_id = $1)
|
||||
`,
|
||||
[scopedRuleId],
|
||||
)
|
||||
|
||||
const summary = await query<WorkOrderTemplateCostSyncRow>(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*)::int AS rule_count,
|
||||
COUNT(*) FILTER (
|
||||
WHERE wpr.unit_price_fen > 0 AND wpr.reward_amount = 0
|
||||
)::int AS unit_price_rule_count,
|
||||
COUNT(v.id) FILTER (
|
||||
WHERE v.scope_type = 'rule' AND v.status = 'active'
|
||||
)::int AS active_version_count
|
||||
FROM work_product_rules wpr
|
||||
LEFT JOIN cost_versions v
|
||||
ON v.cost_item_id = (
|
||||
SELECT id FROM cost_items WHERE code = 'sys_work_order_reward' LIMIT 1
|
||||
)
|
||||
AND v.work_product_rule_id = wpr.id
|
||||
WHERE ($1::bigint IS NULL OR wpr.id = $1)
|
||||
`,
|
||||
[scopedRuleId],
|
||||
)
|
||||
|
||||
const row = summary.rows[0]
|
||||
return {
|
||||
ruleId: scopedRuleId,
|
||||
ruleCount: Number(row?.rule_count || 0),
|
||||
unitPriceRuleCount: Number(row?.unit_price_rule_count || 0),
|
||||
activeVersionCount: Number(row?.active_version_count || 0),
|
||||
createdCount: inserted.rows.length,
|
||||
retiredCount: retired.rowCount || 0,
|
||||
linkedSnapshotCount: linked.rowCount || 0,
|
||||
}
|
||||
}
|
||||
|
||||
type TemplateBootstrapTemplateRow = {
|
||||
id: number
|
||||
rule_key: string
|
||||
@@ -2697,7 +2915,20 @@ export async function listAdminCostVersions(payload: JsonObject = {}) {
|
||||
const productId = normalizeOptionalId(payload.canonicalProductId)
|
||||
if (productId) {
|
||||
params.push(productId)
|
||||
clauses.push(`v.canonical_product_id = $${params.length}`)
|
||||
clauses.push(
|
||||
`(
|
||||
v.canonical_product_id = $${params.length}
|
||||
OR (
|
||||
v.scope_type = 'rule'
|
||||
AND v.work_product_rule_id IN (
|
||||
SELECT wpr.id
|
||||
FROM work_product_rules wpr
|
||||
JOIN product_catalog pp ON pp.canonical_sku = 'tpl:' || wpr.rule_key
|
||||
WHERE pp.id = $${params.length}
|
||||
)
|
||||
)
|
||||
)`,
|
||||
)
|
||||
}
|
||||
const provider = text(payload.provider)
|
||||
if (provider) {
|
||||
@@ -2721,7 +2952,8 @@ export async function listAdminCostVersions(payload: JsonObject = {}) {
|
||||
params.push(`%${keyword}%`)
|
||||
clauses.push(
|
||||
`(ci.code ILIKE $${params.length} OR ci.name ILIKE $${params.length}
|
||||
OR p.canonical_sku ILIKE $${params.length} OR p.canonical_name ILIKE $${params.length})`,
|
||||
OR p.canonical_sku ILIKE $${params.length} OR p.canonical_name ILIKE $${params.length}
|
||||
OR wpr.product_name ILIKE $${params.length} OR wpr.rule_key ILIKE $${params.length})`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2732,6 +2964,7 @@ export async function listAdminCostVersions(payload: JsonObject = {}) {
|
||||
FROM cost_versions v
|
||||
JOIN cost_items ci ON ci.id = v.cost_item_id
|
||||
LEFT JOIN product_catalog p ON p.id = v.canonical_product_id
|
||||
LEFT JOIN work_product_rules wpr ON wpr.id = v.work_product_rule_id
|
||||
${where}
|
||||
`,
|
||||
params,
|
||||
@@ -2746,12 +2979,44 @@ export async function listAdminCostVersions(payload: JsonObject = {}) {
|
||||
ci.carrier_type AS cost_item_carrier_type,
|
||||
ci.pricing_mode,
|
||||
p.canonical_sku,
|
||||
p.canonical_name
|
||||
p.canonical_name,
|
||||
wpr.product_name AS work_product_rule_name,
|
||||
wpr.rule_key AS work_product_rule_key,
|
||||
wpr.reward_amount AS rule_reward_amount,
|
||||
wpr.unit_price_fen AS rule_unit_price_fen,
|
||||
rev.revenue_item_count,
|
||||
rev.revenue_fen,
|
||||
rev.reward_avg_fen,
|
||||
rev.reward_fen
|
||||
FROM cost_versions v
|
||||
JOIN cost_items ci ON ci.id = v.cost_item_id
|
||||
LEFT JOIN product_catalog p ON p.id = v.canonical_product_id
|
||||
LEFT JOIN work_product_rules wpr ON wpr.id = v.work_product_rule_id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
wo.product_rule_id,
|
||||
COUNT(*)::int AS revenue_item_count,
|
||||
SUM(COALESCE(oi.revenue_amount_fen, 0))::bigint AS revenue_fen,
|
||||
ROUND(AVG(wo.reward_amount))::bigint AS reward_avg_fen,
|
||||
SUM(wo.reward_amount)::bigint AS reward_fen
|
||||
FROM work_orders wo
|
||||
JOIN order_items oi ON oi.id = wo.order_item_id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE o.pay_status IN ('paid', 'refunded')
|
||||
GROUP BY wo.product_rule_id
|
||||
) rev ON rev.product_rule_id = v.work_product_rule_id
|
||||
${where}
|
||||
ORDER BY v.status ASC, v.effective_from DESC, v.id DESC
|
||||
ORDER BY
|
||||
v.status ASC,
|
||||
CASE
|
||||
WHEN ci.code = 'sys_work_order_reward'
|
||||
AND v.scope_type = 'global'
|
||||
AND v.amount_fen = 0
|
||||
THEN 1 ELSE 0
|
||||
END ASC,
|
||||
COALESCE(rev.revenue_item_count, 0) DESC,
|
||||
v.effective_from DESC,
|
||||
v.id DESC
|
||||
LIMIT $${listParams.length - 1} OFFSET $${listParams.length}
|
||||
`,
|
||||
listParams,
|
||||
@@ -3728,6 +3993,19 @@ function mapCostItem(row: CostItemRow) {
|
||||
}
|
||||
|
||||
function mapCostVersion(row: CostVersionRow) {
|
||||
const revenueItemCount = Number(row.revenue_item_count || 0)
|
||||
const revenueFen = Number(row.revenue_fen || 0)
|
||||
const rewardAvgFen = Number(row.reward_avg_fen || 0)
|
||||
const rewardFen = Number(row.reward_fen || 0)
|
||||
const ruleUnitPriceFen = Number(row.rule_unit_price_fen || 0)
|
||||
const ruleRewardAmountFen = Number(row.rule_reward_amount || 0)
|
||||
const amountKind =
|
||||
row.cost_item_code === 'sys_work_order_reward' &&
|
||||
row.scope_type === 'rule' &&
|
||||
ruleUnitPriceFen > 0 &&
|
||||
ruleRewardAmountFen === 0
|
||||
? 'unit_price'
|
||||
: 'fixed'
|
||||
return {
|
||||
id: Number(row.id),
|
||||
costItemId: Number(row.cost_item_id),
|
||||
@@ -3742,6 +4020,20 @@ function mapCostVersion(row: CostVersionRow) {
|
||||
canonicalSku: row.canonical_sku || '',
|
||||
canonicalName: row.canonical_name || '',
|
||||
workProductRuleId: row.work_product_rule_id ? Number(row.work_product_rule_id) : null,
|
||||
workProductRuleName: row.work_product_rule_name || '',
|
||||
workProductRuleKey: row.work_product_rule_key || '',
|
||||
ruleRewardAmountFen,
|
||||
ruleUnitPriceFen,
|
||||
revenueItemCount,
|
||||
revenueFen,
|
||||
revenueAvgFen: revenueItemCount > 0 ? Math.round(revenueFen / revenueItemCount) : 0,
|
||||
rewardAvgFen,
|
||||
rewardFen,
|
||||
amountKind,
|
||||
isSystemPlaceholder:
|
||||
row.cost_item_code === 'sys_work_order_reward' &&
|
||||
row.scope_type === 'global' &&
|
||||
Number(row.amount_fen) === 0,
|
||||
amountFen: Number(row.amount_fen),
|
||||
rateBps: Number(row.rate_bps),
|
||||
effectiveFrom: row.effective_from,
|
||||
|
||||
@@ -382,6 +382,7 @@ async function syncDirectWorkOrderRewardSnapshots(
|
||||
SELECT DISTINCT ON (wo.order_item_id)
|
||||
wo.id AS work_order_id,
|
||||
wo.order_item_id,
|
||||
wo.product_rule_id,
|
||||
GREATEST(COALESCE(wo.reward_amount, 0), 0)::bigint AS reward_amount,
|
||||
COALESCE(wo.accepted_at, wo.updated_at, o.paid_at, o.created_at, NOW()) AS accepted_at
|
||||
FROM work_orders wo
|
||||
@@ -402,7 +403,8 @@ async function syncDirectWorkOrderRewardSnapshots(
|
||||
)
|
||||
SELECT
|
||||
'order_item', a.order_item_id, oi.order_id, a.order_item_id, a.work_order_id,
|
||||
'work_order', si.cost_item_id, si.cost_version_id, oi.canonical_product_id,
|
||||
'work_order', si.cost_item_id, COALESCE(rv.id, si.cost_version_id),
|
||||
oi.canonical_product_id,
|
||||
1, a.reward_amount, a.reward_amount, 'actual', 'resolved',
|
||||
jsonb_build_object(
|
||||
'source', 'system_work_order_reward',
|
||||
@@ -414,6 +416,18 @@ async function syncDirectWorkOrderRewardSnapshots(
|
||||
FROM accepted a
|
||||
CROSS JOIN system_item si
|
||||
JOIN order_items oi ON oi.id = a.order_item_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT v.id
|
||||
FROM cost_versions v
|
||||
WHERE v.cost_item_id = si.cost_item_id
|
||||
AND v.scope_type = 'rule'
|
||||
AND v.work_product_rule_id = a.product_rule_id
|
||||
AND v.status IN ('active', 'retired')
|
||||
AND v.effective_from <= a.accepted_at
|
||||
AND (v.effective_to IS NULL OR v.effective_to > a.accepted_at)
|
||||
ORDER BY v.effective_from DESC, v.id DESC
|
||||
LIMIT 1
|
||||
) rv ON TRUE
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM cost_snapshots cs
|
||||
WHERE cs.subject_type = 'order_item'
|
||||
@@ -508,6 +522,7 @@ async function syncCommittedWorkOrderRewardSnapshots(
|
||||
SELECT DISTINCT ON (wo.order_item_id)
|
||||
wo.id AS work_order_id,
|
||||
wo.order_item_id,
|
||||
wo.product_rule_id,
|
||||
wo.status,
|
||||
GREATEST(COALESCE(wo.reward_amount, 0), 0)::bigint AS reward_amount,
|
||||
COALESCE(wo.created_at, o.paid_at, o.created_at, NOW()) AS basis_at
|
||||
@@ -531,7 +546,8 @@ async function syncCommittedWorkOrderRewardSnapshots(
|
||||
)
|
||||
SELECT
|
||||
'order_item', l.order_item_id, oi.order_id, l.order_item_id, l.work_order_id,
|
||||
'work_order', si.cost_item_id, si.cost_version_id, oi.canonical_product_id,
|
||||
'work_order', si.cost_item_id, COALESCE(rv.id, si.cost_version_id),
|
||||
oi.canonical_product_id,
|
||||
1, l.reward_amount, l.reward_amount, 'estimated', 'resolved',
|
||||
jsonb_build_object(
|
||||
'source', 'work_order_reward_committed',
|
||||
@@ -543,6 +559,18 @@ async function syncCommittedWorkOrderRewardSnapshots(
|
||||
FROM latest l
|
||||
CROSS JOIN system_item si
|
||||
JOIN order_items oi ON oi.id = l.order_item_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT v.id
|
||||
FROM cost_versions v
|
||||
WHERE v.cost_item_id = si.cost_item_id
|
||||
AND v.scope_type = 'rule'
|
||||
AND v.work_product_rule_id = l.product_rule_id
|
||||
AND v.status IN ('active', 'retired')
|
||||
AND v.effective_from <= l.basis_at
|
||||
AND (v.effective_to IS NULL OR v.effective_to > l.basis_at)
|
||||
ORDER BY v.effective_from DESC, v.id DESC
|
||||
LIMIT 1
|
||||
) rv ON TRUE
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM work_orders accepted
|
||||
WHERE accepted.order_item_id = l.order_item_id
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
countWorkProductRulesByCategory,
|
||||
deleteWorkProductRule,
|
||||
detachWorkProductRuleCostVersions,
|
||||
getWorkProductRuleById,
|
||||
listOpenWorkOrdersByProductRuleId,
|
||||
listWorkProductRulesPage,
|
||||
@@ -615,6 +616,17 @@ export async function deleteAdminWorkProductRule(ruleId: number | string) {
|
||||
errorCode: 'work_product_rule_id_invalid',
|
||||
})
|
||||
}
|
||||
const rule = await getWorkProductRuleById(normalizedRuleId)
|
||||
if (!rule) {
|
||||
throw createHttpError('物品规则不存在或已删除', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_product_rule_not_found',
|
||||
})
|
||||
}
|
||||
const detached = await detachWorkProductRuleCostVersions(
|
||||
normalizedRuleId,
|
||||
`${rule.product_name} / ${rule.rule_key}`,
|
||||
)
|
||||
const { deleted } = await deleteWorkProductRule(normalizedRuleId)
|
||||
if (!deleted) {
|
||||
throw createHttpError('物品规则不存在或已删除', {
|
||||
@@ -622,5 +634,5 @@ export async function deleteAdminWorkProductRule(ruleId: number | string) {
|
||||
errorCode: 'work_product_rule_not_found',
|
||||
})
|
||||
}
|
||||
return { deleted }
|
||||
return { deleted, retiredCostVersionCount: detached.updated }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user