fix(finance): 收敛内部 SKU 履约路由与载体状态

统一内部 SKU 与渠道主/兜底解析,禁止并列主渠道。

运营指定渠道改为先创建真实载体,失败保持明确 pending。

历史缺载体、多载体冲突和自动发货兜底统一由管道修复。

迁移人工配置与旧 sku_code 规则,补充严格预检和集成测试。
This commit is contained in:
yml2213
2026-11-02 15:02:49 +08:00
parent 5fe33b0b46
commit 2f87445ed9
33 changed files with 2199 additions and 446 deletions
@@ -34,6 +34,7 @@ async function main() {
work_orders: number
affiliate_tasks: number
manual_tasks: number
config_missing: number
no_carrier: number
}>(
`
@@ -44,8 +45,19 @@ async function main() {
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'))::int AS affiliate_tasks,
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch'))::int AS manual_tasks,
count(*) FILTER (
WHERE NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
AND NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id)
WHERE oi.fulfillment_channel = 'manual' AND oi.fulfillment_config_id IS NULL
)::int AS config_missing,
count(*) FILTER (
WHERE (
oi.fulfillment_channel = 'affiliate_dash'
AND NOT EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
)
) OR (
oi.fulfillment_channel = 'work_order'
AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
)
)::int AS no_carrier
FROM order_items oi
WHERE oi.fulfillment_status = 'assigned'
@@ -63,9 +75,15 @@ async function main() {
`
SELECT
count(*)::int AS total,
count(*) FILTER (WHERE btrim(COALESCE(manual_sku_key, '')) = '')::int AS null_or_blank,
count(*) FILTER (WHERE btrim(COALESCE(manual_sku_key, '')) ~ '^[0-9]+$')::int AS numeric_only,
count(*) FILTER (WHERE btrim(COALESCE(manual_sku_key, '')) ~ '[一-龥]')::int AS with_cjk
count(*) FILTER (WHERE btrim(COALESCE(resolve_internal_sku(
manual_sku_key, sku_name, sku_code
), '')) = '')::int AS null_or_blank,
count(*) FILTER (WHERE btrim(COALESCE(resolve_internal_sku(
manual_sku_key, sku_name, sku_code
), '')) ~ '^[0-9]+$')::int AS numeric_only,
count(*) FILTER (WHERE btrim(COALESCE(resolve_internal_sku(
manual_sku_key, sku_name, sku_code
), '')) ~ '[一-龥]')::int AS with_cjk
FROM order_items
WHERE fulfillment_status = 'assigned'
`,
@@ -104,6 +122,45 @@ async function main() {
`SELECT channel, count(*)::int AS total FROM product_channel_capabilities WHERE enabled GROUP BY 1 ORDER BY 2 DESC`,
)
const internalChannelConflicts = await query<{ internal_sku: string; total: number }>(
`SELECT internal_sku, count(*)::int AS total
FROM internal_sku_channels
WHERE enabled = TRUE AND is_fallback = FALSE
GROUP BY internal_sku
HAVING count(*) > 1
ORDER BY count(*) DESC, internal_sku`,
)
const assignedCarrierConflicts = await query<{
order_item_id: number
channel: string
carrier_count: number
}>(
`SELECT oi.id AS order_item_id, oi.fulfillment_channel AS channel,
(EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch'
))::int AS carrier_count
FROM order_items oi
WHERE oi.fulfillment_status = 'assigned'
AND (EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch'
))::int > 1
ORDER BY oi.id
LIMIT 200`,
)
const byProvider = await query<{
provider: string
total: number
@@ -135,6 +192,8 @@ async function main() {
rules: rules.rows,
duplicateProducts: duplicateProducts.rows[0],
capabilities: capabilities.rows,
internalChannelConflicts: internalChannelConflicts.rows,
assignedCarrierConflicts: assignedCarrierConflicts.rows,
byProvider: byProvider.rows,
},
null,
@@ -59,7 +59,7 @@ async function main() {
note: `从 ${row.total} 条订单项回填`,
})
}
written += 1
if (apply) written += 1
}
console.log(
@@ -88,6 +88,17 @@ async function main() {
`,
[internalSku, rule.channel, rule.priority, rule.source || 'operator'],
)
await query(
`UPDATE internal_sku_channels
SET is_fallback = TRUE,
priority = GREATEST(priority, 90),
updated_at = NOW()
WHERE btrim(internal_sku) = $1
AND channel <> $2
AND enabled = TRUE
AND is_fallback = FALSE`,
[internalSku, rule.channel],
)
await query(
`
INSERT INTO internal_sku_channels (
@@ -95,6 +106,7 @@ async function main() {
) VALUES ($1, $2, $3, FALSE, TRUE, 'legacy-rule')
ON CONFLICT (internal_sku, channel) DO UPDATE SET
priority = LEAST(internal_sku_channels.priority, EXCLUDED.priority),
is_fallback = FALSE,
enabled = TRUE,
updated_at = NOW()
`,
@@ -147,6 +147,18 @@ async function mergeGroup(
[group.keeper_id],
)
await query(
`UPDATE internal_sku_channels
SET is_fallback = TRUE,
priority = GREATEST(priority, 90),
updated_at = NOW()
WHERE btrim(internal_sku) = $1
AND channel <> 'affiliate_dash'
AND enabled = TRUE
AND is_fallback = FALSE`,
[group.canonical_name],
)
await query(
`
INSERT INTO internal_sku_channels (
@@ -157,6 +169,7 @@ async function mergeGroup(
ON CONFLICT (internal_sku, channel) DO UPDATE SET
priority = LEAST(internal_sku_channels.priority, EXCLUDED.priority),
is_fallback = internal_sku_channels.is_fallback OR EXCLUDED.is_fallback,
enabled = TRUE,
canonical_product_id = COALESCE(EXCLUDED.canonical_product_id, internal_sku_channels.canonical_product_id),
updated_at = NOW()
`,
@@ -11,6 +11,7 @@ type CountRow = { label: string; total: number }
async function main() {
const strict = process.argv.includes('--strict')
const includeDemo = process.argv.includes('--include-demo')
const checks: Record<string, unknown> = {}
checks.migrations = (
@@ -54,14 +55,96 @@ async function main() {
`SELECT 'pending_no_internal_channel' AS label, count(*)::int AS total
FROM order_items oi
WHERE oi.fulfillment_status = 'pending'
AND btrim(COALESCE(oi.manual_sku_key, '')) <> ''
AND btrim(COALESCE(resolve_internal_sku(
oi.manual_sku_key, oi.sku_name, oi.sku_code
), '')) <> ''
AND NOT EXISTS (
SELECT 1 FROM internal_sku_channels c
WHERE c.enabled AND btrim(c.internal_sku) = btrim(oi.manual_sku_key)
WHERE c.enabled AND btrim(c.internal_sku) = resolve_internal_sku(
oi.manual_sku_key, oi.sku_name, oi.sku_code
)
)`,
)
).rows[0]
checks.assignedWithoutCarrier = (
await query<CountRow>(
`SELECT 'assigned_without_carrier' AS label, count(*)::int AS total
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
WHERE oi.fulfillment_status = 'assigned'
AND oi.fulfillment_channel IN ('affiliate_dash', 'work_order')
AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%')
AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
AND NOT EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
)`,
[includeDemo],
)
).rows[0]
checks.assignedWithMultipleCarriers = (
await query<CountRow>(
`SELECT 'assigned_with_multiple_carriers' AS label, count(*)::int AS total
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
WHERE oi.fulfillment_status = 'assigned'
AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%')
AND (EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash'
))::int
+ (EXISTS (
SELECT 1 FROM fulfillment_tasks ft
WHERE ft.order_item_id = oi.id AND ft.executor_key = 'manual_dispatch'
))::int > 1`,
[includeDemo],
)
).rows[0]
checks.internalChannelConflicts = (
await query<CountRow>(
`SELECT internal_sku AS label, count(*)::int AS total
FROM internal_sku_channels
WHERE enabled = TRUE AND is_fallback = FALSE
GROUP BY internal_sku
HAVING count(*) > 1
ORDER BY count(*) DESC, internal_sku`,
)
).rows
checks.enabledLegacyRules = (
await query<CountRow>(
`SELECT match_key_type || '/' || channel AS label, count(*)::int AS total
FROM sku_channel_rules
WHERE enabled = TRUE AND match_key_type = 'sku_code'
GROUP BY 1 ORDER BY 2 DESC`,
)
).rows
checks.demoPending = (
await query<CountRow>(
`SELECT 'demo_pending' AS label, count(*)::int AS total
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
WHERE oi.fulfillment_status = 'pending' AND o.shop_id LIKE 'sim-%'`,
)
).rows[0]
checks.pendingWithoutReason = (
await query<CountRow>(
`SELECT 'pending_without_reason' AS label, count(*)::int AS total
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
WHERE oi.fulfillment_status = 'pending'
AND ($1::boolean OR o.shop_id NOT LIKE 'sim-%')
AND oi.fulfillment_pending_reason IS NULL`,
[includeDemo],
)
).rows[0]
checks.channelCarrier = (
await query<{
channel: string
@@ -84,8 +167,42 @@ async function main() {
const failed: string[] = []
const rename = checks.renameCardChannels as CountRow[]
if (rename.some((row) => row.label === 'pending')) failed.push('11月改名卡仍有 pending')
if ((checks.pendingWithoutChannel as CountRow).total > 0)
if (!includeDemo && (checks.demoPending as CountRow).total > 0) {
checks.excludedDemoPending = (checks.demoPending as CountRow).total
}
if ((checks.pendingWithoutChannel as CountRow).total > 0 && !includeDemo) {
// 本地演示店铺故意保留 pending;严格模式默认只校验非演示数据。
const productionPending = await query<CountRow>(
`SELECT 'pending_no_internal_channel' AS label, count(*)::int AS total
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
WHERE oi.fulfillment_status = 'pending'
AND o.shop_id NOT LIKE 'sim-%'
AND btrim(COALESCE(resolve_internal_sku(
oi.manual_sku_key, oi.sku_name, oi.sku_code
), '')) <> ''
AND NOT EXISTS (
SELECT 1 FROM internal_sku_channels c
WHERE c.enabled AND btrim(c.internal_sku) = resolve_internal_sku(
oi.manual_sku_key, oi.sku_name, oi.sku_code
)
)`,
)
if (productionPending.rows[0]!.total > 0)
failed.push('存在有内部 SKU 但无渠道能力的非演示 pending')
} else if ((checks.pendingWithoutChannel as CountRow).total > 0) {
failed.push('存在有内部 SKU 但无渠道能力的 pending')
}
if ((checks.assignedWithoutCarrier as CountRow).total > 0)
failed.push('存在已分配自动发货/接单渠道但没有真实载体的订单项')
if ((checks.assignedWithMultipleCarriers as CountRow).total > 0)
failed.push('存在同时挂载多个真实履约载体的已分配订单项')
if ((checks.internalChannelConflicts as CountRow[]).length > 0)
failed.push('存在多个非兜底主渠道')
if ((checks.enabledLegacyRules as CountRow[]).length > 0)
failed.push('仍有启用的 sku_code 兼容规则,未完全收敛到内部 SKU')
if ((checks.pendingWithoutReason as CountRow).total > 0)
failed.push('存在未写入 pending 原因的订单项,请先执行履约/财务管道')
console.log(JSON.stringify({ strict, failed, ...checks }, null, 2))
if (strict && failed.length > 0) process.exitCode = 2
@@ -36,7 +36,9 @@ async function loadCandidates(limit: number): Promise<Candidate[]> {
FROM internal_sku_channels c
WHERE c.enabled
AND c.channel IN ('affiliate_dash', 'manual', 'third_party_voucher')
AND btrim(c.internal_sku) = COALESCE(NULLIF(btrim(oi.manual_sku_key), ''), btrim(oi.sku_name))
AND btrim(c.internal_sku) = resolve_internal_sku(
oi.manual_sku_key, oi.sku_name, oi.sku_code
)
ORDER BY
c.priority ASC,
CASE c.channel
@@ -68,7 +70,7 @@ async function loadCandidates(limit: number): Promise<Candidate[]> {
)
AND NOT EXISTS (
SELECT 1 FROM work_orders wo
WHERE wo.order_item_id = oi.id AND wo.status = 'accepted'
WHERE wo.order_item_id = oi.id
)
AND NOT EXISTS (
SELECT 1 FROM cost_snapshots cs
@@ -17,24 +17,41 @@ async function cleanup() {
[SHOPS],
)
await query(`DELETE FROM sales_daily_facts WHERE shop_id = ANY($1)`, [SHOPS])
await query(
`DELETE FROM internal_sku_channels
WHERE internal_sku IN ('模拟自动发货商品', '模拟三方卡券', '模拟人工商品')`,
)
await query(
`DELETE FROM order_items WHERE order_id IN (SELECT id FROM orders WHERE shop_id = ANY($1))`,
[SHOPS],
)
await query(`DELETE FROM orders WHERE shop_id = ANY($1)`, [SHOPS])
await query(
`DELETE FROM sku_channel_rules WHERE match_key IN ('SIM_AFF_1', 'SIMV001', 'SIMM001')`,
`DELETE FROM sku_channel_rules
WHERE match_key IN ('SIM_AFF_1', 'SIMV001', 'SIMM001',
'模拟自动发货商品', '模拟三方卡券', '模拟人工商品')`,
)
await query(
`DELETE FROM product_channel_capabilities WHERE product_id IN (SELECT id FROM product_catalog WHERE canonical_sku = 'aff:SIM_AFF_1')`,
)
await query(
`DELETE FROM cost_versions WHERE canonical_product_id IN (SELECT id FROM product_catalog WHERE canonical_sku = 'aff:SIM_AFF_1')`,
`DELETE FROM channel_product_mappings
WHERE canonical_product_id IN (
SELECT id FROM product_catalog
WHERE canonical_sku IN ('aff:SIM_AFF_1', 'tpv:模拟三方卡券', 'tpv:模拟三方卡券B')
)`,
)
await query(
`DELETE FROM cost_versions
WHERE canonical_product_id IN (
SELECT id FROM product_catalog
WHERE canonical_sku IN ('aff:SIM_AFF_1', 'tpv:模拟三方卡券', 'tpv:模拟三方卡券B')
)`,
)
await query(`DELETE FROM product_catalog WHERE canonical_sku = 'aff:SIM_AFF_1'`)
await query(`DELETE FROM manual_fulfillment_configs WHERE sku_key = '模拟人工商品'`)
await query(
`DELETE FROM product_catalog WHERE canonical_sku IN ('tpv:模拟三方卡券', 'tpv:模拟三方卡券B')`,
`DELETE FROM product_catalog
WHERE canonical_sku IN ('aff:SIM_AFF_1', 'tpv:模拟三方卡券', 'tpv:模拟三方卡券B')`,
)
}
@@ -97,15 +114,24 @@ async function main() {
[affProductId],
)
await query(
`INSERT INTO sku_channel_rules (provider, shop_id, match_key_type, match_key, channel, priority, source, enabled, note, created_at, updated_at)
VALUES ('','','sku_code','SIM_AFF_1','affiliate_dash',60,'operator',TRUE,'本地演示',NOW(),NOW())`,
`INSERT INTO internal_sku_channels (
internal_sku, channel, priority, is_fallback, enabled,
canonical_product_id, source, created_at, updated_at
) VALUES
('模拟自动发货商品', 'affiliate_dash', 10, FALSE, TRUE, $1, 'demo', NOW(), NOW()),
('模拟自动发货商品', 'manual', 90, TRUE, TRUE, $1, 'demo', NOW(), NOW())`,
[affProductId],
)
// 人工演示配置
await query(
`INSERT INTO manual_fulfillment_configs (sku_key, platform_key, credit_quantity, cost_mode, enabled, note, created_at, updated_at)
VALUES ('模拟人工商品','nanhang',100,'fixed',TRUE,'本地演示',NOW(),NOW())`,
)
await query(
`INSERT INTO internal_sku_channels (
internal_sku, channel, priority, is_fallback, enabled, source, created_at, updated_at
) VALUES ('模拟人工商品', 'manual', 10, FALSE, TRUE, 'demo', NOW(), NOW())`,
)
// 1. 三方卡券:待分类,指定后自动建商品
const voucherOrder = await createOrder('sim-voucher', 'SIM-VOUCHER-1', 1950)