furniture-3d-system-Wu/furniture-3d-system/scripts/sync-product-category-config.ts
居居 ad08dccf1c Merge branch 'codex/xuxiake-sabc-sync-20260929' into codex/xuxiake-mainline
商品SABC打标全部源码:页面/筛选/经营总览、服务端核算规则、同款商品编码匹配、
每日同步(聚水潭档案/销售日表/替换规则/类目档位/产品类型回填)、API 与导出、测试用例。
2026-09-29 18:04:53 +08:00

147 lines
5.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.

import { loadEnvConfig } from '@next/env'
loadEnvConfig(process.cwd())
// 以正式库的类目档位表为准,同步本地类目配置(类目 + 档位阈值)。
// 默认 dry-run;写库需要 --apply --confirm=sync-product-category-config。
import pg from 'pg'
const CONFIRM_TOKEN = 'sync-product-category-config'
function required(name: string) {
const value = process.env[name]?.trim()
if (!value) throw new Error(`${name} is required.`)
return value
}
function flagValue(name: string) {
const prefix = `--${name}=`
return process.argv.slice(2).find((argument) => argument.startsWith(prefix))?.slice(prefix.length) ?? ''
}
async function main() {
const apply = process.argv.includes('--apply')
if (apply && flagValue('confirm') !== CONFIRM_TOKEN) {
throw new Error(`写库需要 --apply --confirm=${CONFIRM_TOKEN}`)
}
const prod = new pg.Client({
host: required('PROD_ARCHIVE_DB_HOST'),
port: Number(process.env.PROD_ARCHIVE_DB_PORT?.trim() || '25432'),
database: required('PROD_ARCHIVE_DB_NAME'),
user: required('PROD_ARCHIVE_DB_USER'),
password: required('PROD_ARCHIVE_DB_PASSWORD'),
ssl: { rejectUnauthorized: false },
statement_timeout: 60_000,
})
const local = new pg.Client({
connectionString: required('DATABASE_URL'),
statement_timeout: 60_000,
})
await prod.connect()
await local.connect()
try {
const sourceRows = (await prod.query(
`select "categoryL1","categoryL2","categoryGroup","scaleLabel",
"performanceStart","performanceEnd","dailySalesStart","dailySalesEnd","sortOrder"
from public."SupplyChainProductArchiveCategoryScaleRow"
order by "categoryL2","sortOrder"`,
)).rows
const localRows = (await local.query(
`select "categoryL2","scaleLabel" from "SupplyChainProductArchiveCategoryScaleRow"`,
)).rows
const localKeys = new Set(localRows.map((row) => `${row.categoryL2}||${row.scaleLabel}`))
const localConfig = (await local.query(
`select "categoryL2" from "SupplyChainProductArchiveCategoryConfig"`,
)).rows
const configKeys = new Set(localConfig.map((row) => row.categoryL2))
const categories = new Map<string, { categoryL1: string; categoryGroup: string }>()
let newScaleRows = 0
let existingScaleRows = 0
for (const row of sourceRows) {
const key = `${row.categoryL2}||${row.scaleLabel}`
if (localKeys.has(key)) existingScaleRows += 1
else newScaleRows += 1
if (!categories.has(row.categoryL2)) {
categories.set(row.categoryL2, { categoryL1: row.categoryL1, categoryGroup: row.categoryGroup })
}
}
const newCategories = [...categories.keys()].filter((categoryL2) => !configKeys.has(categoryL2))
const summary = {
apply,
sourceScaleRows: sourceRows.length,
sourceCategories: categories.size,
localScaleRows: localRows.length,
newScaleRows,
existingScaleRows,
newCategories: newCategories.length,
newCategoryNames: newCategories.slice(0, 20),
}
if (apply) {
await local.query('BEGIN')
try {
for (const row of sourceRows) {
await local.query(
`insert into "SupplyChainProductArchiveCategoryScaleRow"
(id,"categoryL1","categoryL2","categoryGroup","scaleLabel","performanceStart","performanceEnd",
"dailySalesStart","dailySalesEnd","sortOrder","createdAt","updatedAt")
values (gen_random_uuid()::text,$1,$2,$3,$4,$5,$6,$7,$8,$9,now(),now())
on conflict ("categoryL2","scaleLabel") do update set
"categoryL1"=excluded."categoryL1",
"categoryGroup"=excluded."categoryGroup",
"performanceStart"=excluded."performanceStart",
"performanceEnd"=excluded."performanceEnd",
"dailySalesStart"=excluded."dailySalesStart",
"dailySalesEnd"=excluded."dailySalesEnd",
"sortOrder"=excluded."sortOrder",
"updatedAt"=now()`,
[
row.categoryL1,
row.categoryL2,
row.categoryGroup,
row.scaleLabel,
row.performanceStart,
row.performanceEnd,
row.dailySalesStart,
row.dailySalesEnd,
row.sortOrder,
],
)
}
for (const [categoryL2, meta] of categories) {
await local.query(
`insert into "SupplyChainProductArchiveCategoryConfig"
(id,"categoryL2","categoryL1",status,"categoryGroup","scaleLabel","createdAt","updatedAt")
values (gen_random_uuid()::text,$1,$2,'ACTIVE',$3,'',now(),now())
on conflict ("categoryL2") do update set
"categoryL1"=excluded."categoryL1",
"categoryGroup"=excluded."categoryGroup",
"updatedAt"=now()`,
[categoryL2, meta.categoryL1, meta.categoryGroup],
)
}
await local.query('COMMIT')
} catch (error) {
await local.query('ROLLBACK')
throw error
}
}
console.log(JSON.stringify(summary, null, 2))
if (!apply) console.log(`dry-run:未写库。加 --apply --confirm=${CONFIRM_TOKEN} 才会同步。`)
} finally {
await prod.end()
await local.end()
}
}
void main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
})