- 新增 internal_sku_mappings / internal_sku_channels,平台字段统一映射到内部中文 SKU - 下单入口按内部 SKU 渠道决策修正默认人工路由,自动发货补全 dash sku - 财务 channel_assign 读取统一渠道表;卡券/人工能力兜底,自动发货/接单要求真实载体 - carrier reconciliation 自动补自动发货任务和接单工单 - pending 落明确原因:无内部 SKU / 无渠道能力 / 无履约载体 - 合并、重判、旧 sku_code 规则清理、部署后 preflight 核对脚本 - 待分类队列按内部 SKU 聚合并展示原因
134 lines
3.9 KiB
TypeScript
134 lines
3.9 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(
|
|
`
|
|
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())
|