#!/usr/bin/env node /** * 修复寄卖「改价上限」被寄卖数量放大的历史数据。 * * 背景:模板驱动规格曾是 unit 计价模板「模板单价 × 寄卖数量」写进 price_cap_fen, * 但售价/库存/匹配引擎都是单价口径,导致改价弹窗提示的上限是单价的 N 倍。 * 取价逻辑已改为按单价返回;本脚本把历史脏数据修正为单价。 * * 只处理命中 bug 特征的行:price_cap_fen === 单价 × quantity 且 quantity > 1。 * 默认 dry-run(只打印),加 --apply 才写库。 * * 用法(apps/backend 目录下): * npx tsx scripts/fix-consignment-price-cap.ts * npx tsx scripts/fix-consignment-price-cap.ts --apply */ import process from 'node:process' import { closeDb, query } from '../src/db/client.js' import { parseConsignmentSpecValues } from '../src/services/worker-platform/consignment-spec-service.js' import { getWorkerConsignmentPriceSuggestion } from '../src/services/worker-platform/consignment-worker-service.js' type CandidateRow = { id: number profile_id: number quantity: number price_fen: number price_cap_fen: number spec_values_json: unknown } type Fix = { table: 'submissions' | 'inventories' id: number quantity: number beforeFen: number afterFen: number } const apply = process.argv.includes('--apply') async function loadCandidates() { const submissions = await query( `SELECT id, profile_id, quantity, asking_price_fen AS price_fen, price_cap_fen, spec_values_json FROM consignment_submissions WHERE profile_id IS NOT NULL AND quantity > 1 AND price_cap_fen > 0 AND status IN ('pending_review', 'approved') ORDER BY id`, ) const inventories = await query( `SELECT id, profile_id, quantity, sale_price_fen AS price_fen, price_cap_fen, spec_values_json FROM consignment_inventories WHERE profile_id IS NOT NULL AND quantity > 1 AND price_cap_fen > 0 ORDER BY id`, ) return { submissions: submissions.rows, inventories: inventories.rows } } async function resolveFixes(table: Fix['table'], rows: CandidateRow[]): Promise { const fixes: Fix[] = [] for (const row of rows) { const suggestion = await getWorkerConsignmentPriceSuggestion({ profileId: Number(row.profile_id), values: parseConsignmentSpecValues(row.spec_values_json), }) const unitCapFen = Math.max(0, Math.round(Number(suggestion.capFen || 0))) // pg 的 BIGINT 默认返回字符串,这里统一转数字再比较 const quantity = Math.max(1, Math.round(Number(row.quantity || 1))) const beforeFen = Math.max(0, Math.round(Number(row.price_cap_fen || 0))) const priceFen = Math.max(0, Math.round(Number(row.price_fen || 0))) // bug 特征:上限 = 单价 × 寄卖数量。用「售价」和「当前建议价」两种口径各判一次, // 避免运营改过折扣后当前建议价与历史值不一致导致漏修。 const inflatedByPrice = priceFen > 0 && beforeFen === Math.round(priceFen * quantity) const inflatedBySuggestion = unitCapFen > 0 && beforeFen === Math.round(unitCapFen * quantity) if (!inflatedByPrice && !inflatedBySuggestion) continue const afterFen = Math.max(0, Math.round(beforeFen / quantity)) if (afterFen <= 0 || afterFen === beforeFen) continue fixes.push({ table, id: Number(row.id), quantity, beforeFen, afterFen, }) } return fixes } const yuan = (fen: number) => `¥${(fen / 100).toFixed(2)}` try { const { submissions, inventories } = await loadCandidates() const fixes = [ ...(await resolveFixes('submissions', submissions)), ...(await resolveFixes('inventories', inventories)), ] console.log(`扫描:提报 ${submissions.length} 条 / 库存 ${inventories.length} 条(数量 > 1)`) if (fixes.length === 0) { console.log('没有命中需要修复的数据。') } else { console.log('需要修复:') for (const fix of fixes) { console.log( ` [${fix.table}] id=${fix.id} 数量=${fix.quantity} 上限 ${yuan(fix.beforeFen)} → ${yuan(fix.afterFen)}`, ) } } if (!apply) { console.log('\n当前为预览模式。确认后追加 `--apply` 执行写库。') } else if (fixes.length > 0) { for (const fix of fixes) { const table = fix.table === 'submissions' ? 'consignment_submissions' : 'consignment_inventories' await query(`UPDATE ${table} SET price_cap_fen = $1 WHERE id = $2`, [fix.afterFen, fix.id]) } console.log(`\n已修正 ${fixes.length} 条记录。`) } } finally { await closeDb() }