商品SABC打标全部源码:页面/筛选/经营总览、服务端核算规则、同款商品编码匹配、 每日同步(聚水潭档案/销售日表/替换规则/类目档位/产品类型回填)、API 与导出、测试用例。
147 lines
5.4 KiB
TypeScript
147 lines
5.4 KiB
TypeScript
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
|
||
})
|