furniture-3d-system-Wu/furniture-3d-system/scripts/backfill-production-stop-archive-snapshot.ts
WuYongJie 6e12f0318d feat(production-stop-demands): 完善档案字段、筛选、视图与通知中心联动
- 明细新增图片、产品标签、含大单近30日日销(条码/SKU 数值快照)

- 表头漏斗筛选(模糊/多选/数值比较/日期区间)、批量搜索、筛选设置

- 列设置拖动排序、快捷查询保存与套用

- 视图统计:日/周/月趋势、部门/需求人/原因/状态分布、饼图与明细下钻

- 通知中心:停产确认/待走流程/需求驳回通知、待办快照同步、钉钉全屏链接

- 新增迁移与历史数据回填脚本
2026-10-20 14:26:38 +08:00

130 lines
4.3 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.

/**
* 回填停产需求明细的档案快照字段:「含大单近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)
})