refactor(api): parsing out of runtime — pure-mirror sync, explicit organize, auto aggregate recompute
解析去运行时化(三层架构,plans/refactor/organize-script-refactor.md): - 同步 = 纯镜像:upsertOriginGood 不再写解析列、不再自动挂族;详情同步仍触发重算 - 整理 = 显式人工动作(OrganizeService):解析列回填 → 派生标签(人工接管永不 覆盖)→ auto-group 建族 → 全量重算;入口 CLI(pnpm --filter @inkreach/api organize)+ POST /product-families/organize + 后台「整理」按钮 - 重算 = 纯结构化聚合:不再按名称重派生标签(防上游改名倒灌,回归测试覆盖); 矩阵维度只认标签/CUSTOM 显式标签,未整理成员不进矩阵;工艺=不打印时 印花数量以单面占位(纯结构化规则);「恢复自动」走整理的单链接派生 - 派生默认补齐(脚本层假设):名称无单/双面且工艺非不打印 → 印花数量单面印花 - goods 服务建品/更新后仅镜像标签+重算(不派生);含商品名入库规范化 (normalizeGoodName,管理员输入边界质检) - organize.service.spec 由 tag-sync spec 迁移 + 防倒灌回归;sync/recompute/ public/families spec 全部适配;API 173/173,admin typecheck+22/22
This commit is contained in:
@@ -1,81 +1,35 @@
|
||||
/**
|
||||
* 产品族回填脚本(一次性 / 幂等):
|
||||
* 1. 全量 OriginGood 回填四个链接名解析列;
|
||||
* 2. auto-group 全量建族并挂成员(每族建立即重算);
|
||||
* 3. 输出统计与不可解析清单。
|
||||
* 整理脚本(幂等 / 显式人工触发)——解析去运行时化后的唯一解析入口:
|
||||
* 1. 回填结构化解析列(只补 NULL,不覆盖存量);
|
||||
* 2. 派生链接标签(未人工接管的 SDS 链接按名称刷新,人工接管不动)+ 商品镜像;
|
||||
* 3. 自动建族(auto-group);
|
||||
* 4. 全量族重算(并集尺码表/包装 + 五维价格矩阵)。
|
||||
*
|
||||
* 运行:pnpm --filter @inkreach/api backfill:product-families
|
||||
* 幂等性:重复执行时步骤 1 数据不变、步骤 2 候选为空(familyId=null 过滤)。
|
||||
* 运行:pnpm --filter @inkreach/api organize
|
||||
* 后台等价入口:POST /product-families/organize(「整理」按钮)。
|
||||
*/
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../src/prisma/prisma.service';
|
||||
import { FamilyRecomputeService } from '../src/product-families/family-recompute.service';
|
||||
import { ProductFamiliesService } from '../src/product-families/product-families.service';
|
||||
import { parseOriginName } from '../src/product-families/origin-name.parser';
|
||||
import { OrganizeService } from '../src/product-families/organize.service';
|
||||
|
||||
async function main() {
|
||||
const prisma = new PrismaService();
|
||||
await prisma.onModuleInit();
|
||||
const recompute = new FamilyRecomputeService(prisma);
|
||||
const families = new ProductFamiliesService(prisma, recompute);
|
||||
const organize = new OrganizeService(prisma, recompute, families);
|
||||
|
||||
// ---- 1. 解析列回填(分批) ----
|
||||
const BATCH = 100;
|
||||
let parsed = 0;
|
||||
const unparsable: string[] = [];
|
||||
for (;;) {
|
||||
const batch = await prisma.originGood.findMany({
|
||||
orderBy: { id: 'asc' },
|
||||
take: BATCH,
|
||||
skip: parsed,
|
||||
select: { id: true, goodName: true },
|
||||
});
|
||||
if (batch.length === 0) break;
|
||||
for (const og of batch) {
|
||||
const p = parseOriginName(og.goodName);
|
||||
if (!p.skuCode && !p.craftLabel) unparsable.push(`#${og.id} ${og.goodName ?? ''}`);
|
||||
await prisma.originGood.update({
|
||||
where: { id: og.id },
|
||||
data: {
|
||||
skuCode: p.skuCode,
|
||||
logisticsLabel: p.logisticsLabel,
|
||||
craftLabel: p.craftLabel,
|
||||
warehouseLabel: p.warehouseLabel,
|
||||
},
|
||||
});
|
||||
}
|
||||
parsed += batch.length;
|
||||
}
|
||||
console.log(`[1/2] parsed ${parsed} origin goods (${unparsable.length} without sku/craft)`);
|
||||
|
||||
// ---- 2. 自动建族(含逐族重算) ----
|
||||
const result = await families.autoGroup(true);
|
||||
console.log(`[2/2] created ${result.applied} families`);
|
||||
|
||||
// ---- 3. 全量族重算兜底(修复建族早于解析列回填等时序造成的空矩阵) ----
|
||||
const allFamilies = await prisma.productFamily.findMany({ select: { id: true } });
|
||||
for (const f of allFamilies) {
|
||||
await recompute.recomputeFamily(f.id);
|
||||
}
|
||||
console.log(`[3/3] recomputed ${allFamilies.length} families`);
|
||||
|
||||
// ---- 统计 ----
|
||||
const total = await prisma.productFamily.count();
|
||||
const withMatrix = await prisma.productFamily.count({
|
||||
where: { priceMatrix: { not: Prisma.DbNull } },
|
||||
});
|
||||
const members = await prisma.originGood.count({ where: { familyId: { not: null } } });
|
||||
const stale = await prisma.productFamily.count({ where: { stale: true } });
|
||||
console.log(`stats: families=${total}, materialized=${withMatrix}, members=${members}, stale=${stale}`);
|
||||
if (unparsable.length) {
|
||||
console.log('unparsable names:');
|
||||
for (const line of unparsable) console.log(` - ${line}`);
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
const result = await organize.organize();
|
||||
console.log(
|
||||
`[organize] labels parsed=${result.labelsParsed} (unparsable=${result.labelsUnparsable}) | ` +
|
||||
`tags links=${result.linksUpdated} goods=${result.goodsUpdated} | ` +
|
||||
`families created=${result.familiesCreated} recomputed=${result.familiesRecomputed}`,
|
||||
);
|
||||
await prisma.onModuleDestroy();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user