feat(finance): 完成成本快照运行层、销售事实与人工支出登记

迁移 108-111:快照业务时间与预估转实际、销售事实表、系统成本项、公司人工支出登记。接单平台按工单实际 reward 入账;人工支出支持登记/审批/驳回/报销并写 actual 快照。新增销售毛利页、人工支出页与订单入口。
This commit is contained in:
yml2213
2026-10-22 16:41:03 +08:00
parent 15989af87c
commit 0889235870
39 changed files with 4824 additions and 143 deletions
@@ -200,6 +200,7 @@ type OrderItemCostContext = {
normalizedQuantity: number
canonicalProductId: number | null
revenueFen: number
basisAt: string
workOrderId: number | null
workProductRuleId: number | null
hasAffiliateDashTask: boolean
@@ -2632,6 +2633,8 @@ export async function previewAdminCost(payload: JsonObject = {}) {
const mapping = await findMappingForContext(context)
const productId = context.canonicalProductId || mapping?.canonical_product_id || null
const product = productId ? mapProduct(await getProductRow(productId)) : null
const asOf =
normalizeIsoDate(payload.asOf ?? payload.costBasisAt, '成本基准时间', true) || context.basisAt
const resolution: CostResolution = productId
? await resolveCostLines({
provider: context.provider,
@@ -2641,6 +2644,7 @@ export async function previewAdminCost(payload: JsonObject = {}) {
workProductRuleId: context.workProductRuleId,
quantity: context.normalizedQuantity,
revenueFen: context.revenueFen,
asOf,
})
: { lines: [], missingCostItems: [], applicableCostItemCount: 0 }
@@ -2677,6 +2681,7 @@ export async function previewAdminCost(payload: JsonObject = {}) {
return {
context: {
...context,
basisAt: asOf,
carrierType,
},
mapping: mapping ? mapMapping(mapping) : null,
@@ -2698,23 +2703,61 @@ export async function createAdminCostSnapshot(payload: JsonObject = {}, _session
})
}
const now = nowIso()
const stage = payload.snapshotStage === 'actual' ? 'actual' : 'estimated'
const snapshots: Array<Record<string, unknown>> = []
let exceptionCreatedCount = 0
let supersededCount = 0
await withTransaction(async (client) => {
for (const line of preview.lines) {
const currentResult = await client.query<{
id: number
snapshot_stage: string
total_amount_fen: number
}>(
`
SELECT id, snapshot_stage, total_amount_fen
FROM cost_snapshots
WHERE subject_type = 'order_item'
AND subject_id = $1
AND cost_item_id = $2
AND superseded_at IS NULL
FOR UPDATE
`,
[orderItemId, line.costItemId],
)
const current = currentResult.rows[0]
if (current) {
// 预估快照一旦生成就代表支付时点的成本,不因后续改价重写;
// 只有实际成本允许替代当前有效行。
if (stage === 'estimated') continue
if (
current.snapshot_stage === 'actual' &&
Number(current.total_amount_fen) === line.totalAmountFen
) {
continue
}
await client.query(
`UPDATE cost_snapshots SET superseded_at = $1 WHERE id = $2 AND superseded_at IS NULL`,
[now, current.id],
)
supersededCount += 1
}
const result = await client.query<{ [column: string]: unknown }>(
`
INSERT INTO cost_snapshots (
subject_type, subject_id, order_id, order_item_id, work_order_id,
carrier_type, cost_item_id, cost_version_id, canonical_product_id,
quantity, unit_amount_fen, total_amount_fen, snapshot_stage, source,
metadata_json, resolved_at, created_at
metadata_json, cost_basis_at, resolved_at, created_at
) VALUES (
'order_item', $1, $2, $1, $3, $4, $5, $6, $7,
$8, $9, $10, $11, 'resolved', $12::jsonb, $13, $13
$8, $9, $10, $11, 'resolved', $12::jsonb, $13, $14, $14
)
ON CONFLICT (subject_type, subject_id, cost_item_id) DO NOTHING
ON CONFLICT (subject_type, subject_id, cost_item_id)
WHERE superseded_at IS NULL
DO NOTHING
RETURNING *
`,
[
@@ -2728,14 +2771,16 @@ export async function createAdminCostSnapshot(payload: JsonObject = {}, _session
line.quantity,
line.unitAmountFen,
line.totalAmountFen,
payload.snapshotStage === 'actual' ? 'actual' : 'estimated',
stage,
JSON.stringify({
skuCode: preview.context.skuCode,
skuName: preview.context.skuName,
provider: preview.context.provider,
shopId: preview.context.shopId,
normalizedQuantity: preview.context.normalizedQuantity,
basisAt: preview.context.basisAt,
}),
preview.context.basisAt,
now,
],
)
@@ -2768,7 +2813,13 @@ export async function createAdminCostSnapshot(payload: JsonObject = {}, _session
}
})
return { preview, snapshots, createdCount: snapshots.length, exceptionCreatedCount }
return {
preview,
snapshots,
createdCount: snapshots.length,
supersededCount,
exceptionCreatedCount,
}
}
async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostContext> {
@@ -2787,6 +2838,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
normalized_quantity: number | null
canonical_product_id: number | null
revenue_fen: number
basis_at: string
work_order_id: number | null
work_product_rule_id: number | null
has_affiliate_dash_task: boolean
@@ -2804,7 +2856,8 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
oi.quantity,
oi.normalized_quantity,
oi.canonical_product_id,
o.total_amount AS revenue_fen,
oi.revenue_amount_fen AS revenue_fen,
COALESCE(o.paid_at, o.created_at) AS basis_at,
wo.id AS work_order_id,
wo.product_rule_id AS work_product_rule_id,
EXISTS (
@@ -2813,7 +2866,13 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
) AS has_affiliate_dash_task
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
LEFT JOIN work_orders wo ON wo.order_item_id = oi.id
LEFT JOIN LATERAL (
SELECT id, product_rule_id
FROM work_orders
WHERE order_item_id = oi.id
ORDER BY id DESC
LIMIT 1
) wo ON TRUE
WHERE oi.id = $1
LIMIT 1
`,
@@ -2844,6 +2903,7 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
: Math.max(1, resolveSkuNameQuantity(row.sku_name || row.sku_code)),
canonicalProductId: row.canonical_product_id ? Number(row.canonical_product_id) : null,
revenueFen: Math.max(0, Number(row.revenue_fen) || 0),
basisAt: new Date(row.basis_at).toISOString(),
workOrderId: row.work_order_id ? Number(row.work_order_id) : null,
workProductRuleId: row.work_product_rule_id ? Number(row.work_product_rule_id) : null,
hasAffiliateDashTask: Boolean(row.has_affiliate_dash_task),
@@ -2869,6 +2929,8 @@ async function resolveCostContext(payload: JsonObject): Promise<OrderItemCostCon
payload.canonicalProductId ?? payload.canonical_product_id,
),
revenueFen: Math.max(0, Math.trunc(Number(payload.revenueFen ?? payload.revenue_fen) || 0)),
basisAt:
normalizeIsoDate(payload.asOf ?? payload.costBasisAt, '成本基准时间', true) || nowIso(),
workOrderId: null,
workProductRuleId: normalizeOptionalId(
payload.workProductRuleId ?? payload.work_product_rule_id,
@@ -2933,6 +2995,7 @@ async function resolveCostLines(input: {
workProductRuleId: number | null
quantity: number
revenueFen: number
asOf?: string
}): Promise<CostResolution> {
const result = await query<
CostItemRow & {
@@ -2981,7 +3044,7 @@ async function resolveCostLines(input: {
ORDER BY ci.id
`,
[
nowIso(),
input.asOf || nowIso(),
input.provider,
input.shopId,
input.canonicalProductId,