feat(finance): 商品渠道能力、自动发货转人工与成本异常收口

- 商品可同时支持自动发货/人工,新增渠道能力与成本冲销字段
- 卡券缺商品时按内部中文 SKU 自动建 tpv: 商品并补渠道规则
- 自动发货失败转人工并冲销原渠道成本
- 配置成本后按最早缺失订单生效并立即补算、关闭异常
- tpv: 商品来源计入第三方卡券
This commit is contained in:
yml2213
2026-11-02 13:17:15 +08:00
parent 86b5347e40
commit 3c4c81003f
16 changed files with 532 additions and 34 deletions
@@ -622,7 +622,7 @@ async function assertPeriodCloseable(
SELECT 1 FROM cost_snapshots cs
WHERE cs.subject_type = 'manual_case'
AND cs.order_item_id = oi.id
AND cs.superseded_at IS NULL
AND cs.superseded_at IS NULL AND reversed_at IS NULL
)
`,
[dateFrom, dateTo],
@@ -639,7 +639,7 @@ async function assertPeriodCloseable(
JOIN order_items oi ON oi.id = cs.order_item_id
JOIN orders o ON o.id = oi.order_id
WHERE cs.subject_type = 'manual_case'
AND cs.superseded_at IS NULL
AND cs.superseded_at IS NULL AND reversed_at IS NULL
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
BETWEEN $1::date AND $2::date
AND cs.created_at >= (SELECT MIN(created_at) FROM manual_fulfillment_configs)
@@ -661,7 +661,7 @@ async function assertPeriodCloseable(
JOIN order_items oi ON oi.id = cs.order_item_id
JOIN orders o ON o.id = oi.order_id
WHERE cs.subject_type = 'manual_case'
AND cs.superseded_at IS NULL
AND cs.superseded_at IS NULL AND reversed_at IS NULL
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
BETWEEN $1::date AND $2::date
AND cs.created_at >= (SELECT MIN(created_at) FROM manual_fulfillment_configs)
@@ -134,7 +134,7 @@ export async function listManualFulfillmentPending(
cs.order_item_id,
MAX(cs.created_at) AS fulfilled_at
FROM cost_snapshots cs
WHERE cs.subject_type = 'manual_case' AND cs.superseded_at IS NULL
WHERE cs.subject_type = 'manual_case' AND cs.superseded_at IS NULL AND reversed_at IS NULL
GROUP BY cs.order_item_id
)
`
@@ -294,7 +294,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
// 锁内复查:并发/双击的第二个请求会等锁,醒来后看到快照直接拒绝,不会重复扣款。
const existing = await client.query<{ id: number }>(
`SELECT id FROM cost_snapshots
WHERE subject_type = 'manual_case' AND order_item_id = $1 AND superseded_at IS NULL
WHERE subject_type = 'manual_case' AND order_item_id = $1 AND superseded_at IS NULL AND reversed_at IS NULL
LIMIT 1`,
[orderItemId],
)
@@ -874,7 +874,7 @@ export async function getManualFulfillmentCountsByOrderIds(
WITH fulfilled AS (
SELECT order_item_id, MAX(created_at) AS fulfilled_at
FROM cost_snapshots
WHERE subject_type = 'manual_case' AND superseded_at IS NULL
WHERE subject_type = 'manual_case' AND superseded_at IS NULL AND reversed_at IS NULL
GROUP BY order_item_id
)
SELECT
@@ -105,18 +105,18 @@ export async function rebuildChannelSalesDailyFacts(input: {
FROM cost_snapshots cs
JOIN cost_items ci ON ci.id = cs.cost_item_id
WHERE cs.order_item_id = s.order_item_id
AND cs.superseded_at IS NULL
AND cs.superseded_at IS NULL AND reversed_at IS NULL AND cs.reversed_at IS NULL
AND ci.code = s.channel_cost_code
)
WHEN s.channel = 'manual' THEN NOT EXISTS (
SELECT 1 FROM cost_snapshots cs
WHERE cs.order_item_id = s.order_item_id
AND cs.superseded_at IS NULL
AND cs.superseded_at IS NULL AND reversed_at IS NULL AND cs.reversed_at IS NULL
AND cs.subject_type = 'manual_case'
)
ELSE NOT EXISTS (
SELECT 1 FROM cost_snapshots cs
WHERE cs.order_item_id = s.order_item_id AND cs.superseded_at IS NULL
WHERE cs.order_item_id = s.order_item_id AND cs.superseded_at IS NULL AND reversed_at IS NULL AND cs.reversed_at IS NULL
)
END AS cost_missing,
COALESCE(c.has_actual, FALSE) AS has_actual
@@ -128,7 +128,7 @@ export async function rebuildChannelSalesDailyFacts(input: {
SUM(cs.total_amount_fen)::bigint AS cost_fen,
BOOL_AND(cs.snapshot_stage = 'actual') AS has_actual
FROM cost_snapshots cs
WHERE cs.superseded_at IS NULL
WHERE cs.superseded_at IS NULL AND reversed_at IS NULL AND cs.reversed_at IS NULL
GROUP BY cs.order_item_id
) c ON c.order_item_id = s.order_item_id
)