/** * 把旧的 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 { 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( ` 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( ` 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), 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())