Files
order_site/apps/backend/scripts/cleanup-legacy-sku-code-rules.ts
T
yml2213 2f87445ed9 fix(finance): 收敛内部 SKU 履约路由与载体状态
统一内部 SKU 与渠道主/兜底解析,禁止并列主渠道。

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

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

迁移人工配置与旧 sku_code 规则,补充严格预检和集成测试。
2026-11-02 15:02:49 +08:00

146 lines
4.2 KiB
TypeScript

/**
* 把旧的 sku_code 渠道规则转换为 internal_sku 全局规则,并停用旧规则。
*
* 用法:
* npx tsx scripts/cleanup-legacy-sku-code-rules.ts
* npx tsx scripts/cleanup-legacy-sku-code-rules.ts --apply
*/
import { closeDb, query } from '../src/db/client.js'
type LegacyRule = {
id: number
provider: string
shop_id: string
match_key: string
channel: string
priority: number
source: string
}
async function resolveInternalSku(rule: LegacyRule): Promise<string> {
if (rule.channel === 'affiliate_dash') {
const product = await query<{ canonical_name: string }>(
`SELECT canonical_name FROM product_catalog
WHERE canonical_sku = $1 AND status = 'active' AND btrim(canonical_name) <> ''
LIMIT 1`,
[`aff:${rule.match_key}`],
)
if (product.rows[0]?.canonical_name) return String(product.rows[0].canonical_name).trim()
}
const params: unknown[] = [rule.match_key]
let scope = ''
if (rule.provider) {
params.push(rule.provider)
scope += ` AND o.provider = $${params.length}`
}
if (rule.shop_id) {
params.push(rule.shop_id)
scope += ` AND o.shop_id = $${params.length}`
}
const items = await query<{ internal_sku: string }>(
`
SELECT DISTINCT btrim(oi.manual_sku_key) AS internal_sku
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
WHERE oi.sku_code = $1
AND btrim(COALESCE(oi.manual_sku_key, '')) ~ '[一-龥]'
${scope}
LIMIT 2
`,
params,
)
const values = items.rows.map((row) => String(row.internal_sku || '').trim()).filter(Boolean)
return values.length === 1 ? values[0]! : ''
}
async function main() {
const apply = process.argv.includes('--apply')
const rules = await query<LegacyRule>(
`
SELECT id, provider, shop_id, match_key, channel, priority, source
FROM sku_channel_rules
WHERE match_key_type = 'sku_code' AND enabled = TRUE
ORDER BY id
`,
)
const converted: Array<{ id: number; matchKey: string; internalSku: string; channel: string }> =
[]
const unresolved: LegacyRule[] = []
for (const rule of rules.rows) {
const internalSku = await resolveInternalSku(rule)
if (!internalSku) {
unresolved.push(rule)
continue
}
converted.push({ id: rule.id, matchKey: rule.match_key, internalSku, channel: rule.channel })
if (!apply) continue
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 ('', '', 'internal_sku', $1, $2, $3, $4, TRUE,
'由旧 sku_code 规则迁移', NOW(), NOW())
ON CONFLICT (provider, shop_id, match_key_type, match_key, channel) DO NOTHING
`,
[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 (
internal_sku, channel, priority, is_fallback, enabled, source
) 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()
`,
[internalSku, rule.channel, rule.priority],
)
await query(
`UPDATE sku_channel_rules
SET enabled = FALSE,
note = CONCAT(COALESCE(note, ''), ' [已迁移 internal_sku:', $2::text, ']'),
updated_at = NOW()
WHERE id = $1`,
[rule.id, internalSku],
)
}
console.log(
JSON.stringify(
{
apply,
legacyRules: rules.rows.length,
convertible: converted.length,
converted,
unresolved,
},
null,
2,
),
)
}
main()
.catch((error) => {
console.error(error)
process.exitCode = 1
})
.finally(() => closeDb())