Files
order_site/apps/backend/scripts/sync-affiliate-dash-prices.ts
T
yml2213 78580c6165 feat(cost-center): 成本项编码改系统自动生成,补自动发货价格导入脚本
- 新建成本项目不再手填编码:后端按成本类型生成基础编码(auto_purchase /
  work_order_reward / manual_service / platform_fee / payment_fee /
  after_sales_loss / shipping_other / other_cost),重复时追加 _2、_3;
  接口显式传 code 时仍优先使用,兼容脚本导入
- 前端移除编码输入框,改为说明提示,创建成功提示展示生成的编码
- 新增 scripts/sync-affiliate-dash-prices.ts + scripts/data/affiliate-dash-prices.json:
  按上游 38 条自动发货价格补建 aff:<sku> 商品,并按 cost_amount(分)生成
  scope=product 的按件成本版本;价格变化时停用旧版本再插新版本
- 自动发货商品同步与价格导入的 attributes_json 改为合并,避免互相覆盖
2026-10-22 15:07:29 +08:00

218 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 按上游自动发货商品价格,批量生成「自动发货采购成本」成本版本。
*
* 用法:
* npx tsx scripts/sync-affiliate-dash-prices.ts [价格 JSON 路径] [--cost-item-id=5] [--dry-run]
*
* 规则:
* - 标准商品编码固定为 `aff:<sku>`,载体 affiliate_dash;
* - 成本版本范围 = 标准商品,金额 = 上游 cost_amount(分);
* - 同一商品已有相同金额的生效版本时跳过;
* - 金额变化时先停用旧版本,再插入新版本(历史快照不变)。
*/
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { closeDb, query } from '../src/db/client.js'
type PriceRow = {
sku: string
name: string
category: string
costAmountFen: number
}
type CostItemRow = {
id: number
code: string
name: string
pricing_mode: string
}
function parseArgs(argv: string[]) {
let filePath = ''
let costItemId: number | null = null
let dryRun = false
for (const arg of argv) {
if (arg === '--dry-run') {
dryRun = true
continue
}
if (arg.startsWith('--cost-item-id=')) {
const parsed = Number(arg.slice('--cost-item-id='.length))
if (Number.isFinite(parsed) && parsed > 0) costItemId = parsed
continue
}
if (!arg.startsWith('--') && !filePath) filePath = arg
}
return {
filePath: path.resolve(filePath || 'scripts/data/affiliate-dash-prices.json'),
costItemId,
dryRun,
}
}
async function resolveCostItem(costItemId: number | null): Promise<CostItemRow> {
if (costItemId) {
const result = await query<CostItemRow>('SELECT * FROM cost_items WHERE id = $1', [costItemId])
const row = result.rows[0]
if (!row) throw new Error(`成本项不存在: ${costItemId}`)
if (row.pricing_mode !== 'per_item') {
throw new Error(`成本项 ${row.code} 的计价方式不是按件,不能导入按件采购价`)
}
return row
}
const existing = await query<CostItemRow>(
`SELECT * FROM cost_items
WHERE carrier_type = 'affiliate_dash' AND cost_type = 'auto_purchase' AND status = 'active'
ORDER BY id LIMIT 1`,
)
if (existing.rows[0]) return existing.rows[0]
const now = new Date().toISOString()
const inserted = await query<CostItemRow>(
`INSERT INTO cost_items (
code, name, cost_type, carrier_type, pricing_mode, unit_name, currency, status, note, created_at, updated_at
) VALUES ('auto_purchase', '自动发货采购成本', 'auto_purchase', 'affiliate_dash', 'per_item', '件', 'CNY', 'active', '上游自动发货价格同步', $1, $1)
ON CONFLICT (code) DO NOTHING
RETURNING *`,
[now],
)
if (inserted.rows[0]) return inserted.rows[0]
const fallback = await query<CostItemRow>(
"SELECT * FROM cost_items WHERE code = 'auto_purchase' LIMIT 1",
)
if (!fallback.rows[0]) throw new Error('无法创建自动发货采购成本项')
return fallback.rows[0]
}
async function main() {
const { filePath, costItemId, dryRun } = parseArgs(process.argv.slice(2))
const rows = JSON.parse(fs.readFileSync(filePath, 'utf8')) as PriceRow[]
const costItem = await resolveCostItem(costItemId)
console.log(
`[affiliate-price] 成本项 #${costItem.id} ${costItem.name}(${costItem.code});价格条目 ${rows.length} 条${dryRun ? '(dry-run)' : ''}`,
)
let createdProductCount = 0
let updatedProductCount = 0
let createdVersionCount = 0
let retiredVersionCount = 0
let skippedCount = 0
for (const row of rows) {
const now = new Date().toISOString()
const canonicalSku = `aff:${row.sku}`
const unitName = row.name.includes('个') ? '个' : '件'
if (dryRun) {
const existingProduct = await query<{ id: number }>(
'SELECT id FROM product_catalog WHERE canonical_sku = $1',
[canonicalSku],
)
const existingId = existingProduct.rows[0]?.id
if (!existingId) {
createdProductCount += 1
createdVersionCount += 1
continue
}
updatedProductCount += 1
const current = await query<{ amount_fen: number }>(
`SELECT amount_fen FROM cost_versions
WHERE cost_item_id = $1 AND scope_type = 'product' AND canonical_product_id = $2 AND status = 'active'
ORDER BY effective_from DESC, id DESC LIMIT 1`,
[costItem.id, existingId],
)
if (current.rows[0] && Number(current.rows[0].amount_fen) === row.costAmountFen) {
skippedCount += 1
} else {
createdVersionCount += 1
}
continue
}
const productResult = await query<{ id: number; inserted: boolean }>(
`INSERT INTO product_catalog (
canonical_sku, canonical_name, category, unit_name, carrier_type, status,
attributes_json, created_at, updated_at
) VALUES ($1, $2, $3, $4, 'affiliate_dash', 'active', $5::jsonb, $6, $6)
ON CONFLICT (canonical_sku) DO UPDATE
SET canonical_name = EXCLUDED.canonical_name,
category = EXCLUDED.category,
unit_name = EXCLUDED.unit_name,
carrier_type = 'affiliate_dash',
attributes_json = product_catalog.attributes_json || EXCLUDED.attributes_json,
updated_at = $6
RETURNING id, (xmax = 0) AS inserted`,
[
canonicalSku,
row.name,
row.category,
unitName,
JSON.stringify({
source: 'affiliate_dash_price',
skuCode: row.sku,
priceFen: row.costAmountFen,
}),
now,
],
)
const product = productResult.rows[0]
if (!product) throw new Error(`商品写入失败: ${canonicalSku}`)
if (product.inserted) createdProductCount += 1
else updatedProductCount += 1
const currentResult = await query<{ id: number; amount_fen: number }>(
`SELECT id, amount_fen FROM cost_versions
WHERE cost_item_id = $1 AND scope_type = 'product' AND canonical_product_id = $2 AND status = 'active'
ORDER BY effective_from DESC, id DESC LIMIT 1`,
[costItem.id, product.id],
)
const current = currentResult.rows[0]
if (current && Number(current.amount_fen) === row.costAmountFen) {
skippedCount += 1
continue
}
if (current) {
const retired = await query(
`UPDATE cost_versions
SET status = 'retired', change_reason = '上游价格更新', updated_at = $1
WHERE cost_item_id = $2 AND canonical_product_id = $3 AND status = 'active'`,
[now, costItem.id, product.id],
)
retiredVersionCount += retired.rowCount || 0
}
await query(
`INSERT INTO cost_versions (
cost_item_id, scope_type, provider, shop_id, canonical_product_id, work_product_rule_id,
amount_fen, rate_bps, effective_from, effective_to, source, status, change_reason,
created_by_admin_user_id, approved_by_admin_user_id, created_at, updated_at
) VALUES ($1, 'product', '', '', $2, NULL, $3, 0, $4, NULL, 'upstream_sync', 'active',
'上游自动发货价格同步', NULL, NULL, $4, $4)`,
[costItem.id, product.id, row.costAmountFen, now],
)
createdVersionCount += 1
}
console.log(
`[affiliate-price] 完成:新增商品 ${createdProductCount}、更新商品 ${updatedProductCount}、` +
`新增版本 ${createdVersionCount}、停用旧版本 ${retiredVersionCount}、金额未变跳过 ${skippedCount}`,
)
}
main()
.then(async () => {
await closeDb()
})
.catch(async (error) => {
console.error(error)
await closeDb()
process.exitCode = 1
})