- 明细新增图片、产品标签、含大单近30日日销(条码/SKU 数值快照) - 表头漏斗筛选(模糊/多选/数值比较/日期区间)、批量搜索、筛选设置 - 列设置拖动排序、快捷查询保存与套用 - 视图统计:日/周/月趋势、部门/需求人/原因/状态分布、饼图与明细下钻 - 通知中心:停产确认/待走流程/需求驳回通知、待办快照同步、钉钉全屏链接 - 新增迁移与历史数据回填脚本
130 lines
4.3 KiB
TypeScript
130 lines
4.3 KiB
TypeScript
/**
|
||
* 回填停产需求明细的档案快照字段:「含大单近30日日销-条码 / -SKU」「产品标签」「图片」。
|
||
* 口径:条码维度 = 产品全量档案库该条码行的「含大单近30日日销」;
|
||
* SKU 维度 = 同 SKU 全部条码求和;
|
||
* 产品标签 = SABC 标签优先,未打标按销售状态 × 考核周期真实业绩兜底;
|
||
* 图片 = 档案库该条码行的图片地址。
|
||
* 只回填当前为空的值,不覆盖已有快照。
|
||
*
|
||
* 用法:npx tsx scripts/backfill-production-stop-archive-snapshot.ts
|
||
*/
|
||
import 'dotenv/config'
|
||
import { prisma } from '../src/lib/prisma'
|
||
import { resolveProductionStopProductLabel } from '../src/lib/production-stop-demands'
|
||
import {
|
||
productionStopDemandRepository,
|
||
type ProductionStopArchiveProfile,
|
||
} from '../src/server/repositories/production-stop-demands.repository'
|
||
|
||
const ARCHIVE_BATCH_SIZE = 300
|
||
|
||
function numericValue(value: string) {
|
||
const normalized = value.trim()
|
||
return /^-?\d+(\.\d+)?$/.test(normalized) ? normalized : null
|
||
}
|
||
|
||
async function main() {
|
||
const items = await prisma.productionStopDemandItem.findMany({
|
||
where: {
|
||
OR: [
|
||
{ sales30dWithBig: '' },
|
||
{ sales30dWithBigSku: '' },
|
||
{ productLabel: '' },
|
||
{ image: '' },
|
||
],
|
||
},
|
||
select: {
|
||
id: true,
|
||
barcode: true,
|
||
sku: true,
|
||
productStatus: true,
|
||
productLabel: true,
|
||
image: true,
|
||
sales30dWithBig: true,
|
||
sales30dWithBigSku: true,
|
||
},
|
||
orderBy: { createdAt: 'asc' },
|
||
})
|
||
if (!items.length) {
|
||
console.log('[production-stop-archive-snapshot] 没有需要回填的明细')
|
||
return
|
||
}
|
||
|
||
const barcodes = [...new Set(
|
||
items.map((item) => item.barcode.trim().toUpperCase()).filter(Boolean),
|
||
)]
|
||
const skus = [...new Set(items.map((item) => item.sku.trim().toUpperCase()).filter(Boolean))]
|
||
|
||
const profilesByBarcode = new Map<string, ProductionStopArchiveProfile>()
|
||
for (let index = 0; index < barcodes.length; index += ARCHIVE_BATCH_SIZE) {
|
||
const chunk = barcodes.slice(index, index + ARCHIVE_BATCH_SIZE)
|
||
const profiles = await productionStopDemandRepository.findArchivedProfiles(chunk)
|
||
for (const [barcode, profile] of profiles) profilesByBarcode.set(barcode, profile)
|
||
}
|
||
|
||
const skuTotals = new Map<string, string>()
|
||
for (let index = 0; index < skus.length; index += ARCHIVE_BATCH_SIZE) {
|
||
const chunk = skus.slice(index, index + ARCHIVE_BATCH_SIZE)
|
||
const totals = await productionStopDemandRepository.sumSkuSales30dWithBig(chunk)
|
||
for (const [sku, total] of totals) {
|
||
if (total) skuTotals.set(sku, total)
|
||
}
|
||
}
|
||
|
||
let updated = 0
|
||
let unmatched = 0
|
||
for (const item of items) {
|
||
const profile = profilesByBarcode.get(item.barcode.trim().toUpperCase())
|
||
const barcodeValue = item.sales30dWithBig
|
||
|| (profile?.sales30dWithBig ?? '')
|
||
const skuValue = item.sales30dWithBigSku
|
||
|| skuTotals.get(item.sku.trim().toUpperCase())
|
||
|| ''
|
||
const productLabel = item.productLabel
|
||
|| (profile
|
||
? resolveProductionStopProductLabel({
|
||
sabcRating: profile.sabcRating,
|
||
productStatus: profile.productStatus,
|
||
realPerformance: profile.sabcRealPerformance,
|
||
})
|
||
: '')
|
||
const image = item.image || (profile?.image ?? '')
|
||
if (!barcodeValue && !skuValue && !productLabel && !image) {
|
||
unmatched += 1
|
||
continue
|
||
}
|
||
if (
|
||
barcodeValue === item.sales30dWithBig
|
||
&& skuValue === item.sales30dWithBigSku
|
||
&& productLabel === item.productLabel
|
||
&& image === item.image
|
||
) {
|
||
continue
|
||
}
|
||
await prisma.productionStopDemandItem.update({
|
||
where: { id: item.id },
|
||
data: {
|
||
sales30dWithBig: barcodeValue,
|
||
sales30dWithBigSku: skuValue,
|
||
sales30dWithBigValue: numericValue(barcodeValue),
|
||
sales30dWithBigSkuValue: numericValue(skuValue),
|
||
productLabel,
|
||
image,
|
||
},
|
||
})
|
||
updated += 1
|
||
}
|
||
console.log(
|
||
`[production-stop-archive-snapshot] 待回填明细 ${items.length} 条:更新 ${updated} 条,档案未匹配 ${unmatched} 条`,
|
||
)
|
||
}
|
||
|
||
main()
|
||
.catch((error) => {
|
||
console.error('[production-stop-archive-snapshot] FAILED:', error)
|
||
process.exitCode = 1
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect().catch(() => null)
|
||
})
|