Files
inkreach-official-website/apps/api/src/sync/sync-family-hooks.spec.ts
T
yeuimu 93ac525c55 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
2026-08-30 01:27:01 +08:00

193 lines
7.6 KiB
TypeScript

import { Test } from '@nestjs/testing';
import { Prisma } from '@prisma/client';
import { SyncService } from './sync.service';
import { SdsClientService } from './sds-client.service';
import { FamilyRecomputeService } from '../product-families/family-recompute.service';
import { ProductFamiliesService } from '../product-families/product-families.service';
import { OrganizeService } from '../product-families/organize.service';
import { PrismaService } from '../prisma/prisma.service';
/**
* 同步钩子集成测试(解析去运行时化后):
* upsertOriginGood 纯镜像(不写解析列)、新链接不自动挂族(归族走整理)、
* persistProductDetail 后的族重算入队。
*/
describe('SyncService family hooks', () => {
let service: SyncService;
let prisma: PrismaService;
let recompute: FamilyRecomputeService;
let organize: OrganizeService;
const stamp = Date.now();
const createdOriginGoodIds: bigint[] = [];
const createdFamilyIds: bigint[] = [];
const sdsProduct = (id: string, name: string) =>
({ id, name, pic: 'https://example.com/pic.jpg' }) as any;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
SyncService,
{
provide: SdsClientService,
useValue: {},
},
FamilyRecomputeService,
ProductFamiliesService,
OrganizeService,
PrismaService,
],
}).compile();
service = moduleRef.get(SyncService);
prisma = moduleRef.get(PrismaService);
recompute = moduleRef.get(FamilyRecomputeService);
organize = moduleRef.get(OrganizeService);
await prisma.onModuleInit();
});
afterAll(async () => {
await prisma.originGood.deleteMany({ where: { id: { in: createdOriginGoodIds } } });
await prisma.productFamily.deleteMany({ where: { id: { in: createdFamilyIds } } });
await prisma.$disconnect();
});
it('upsertOriginGood:纯镜像——只存原文,不写解析列', async () => {
const sdsId = `hook-${stamp}-parse`;
const result1 = await (service as any).upsertOriginGood(
sdsProduct(sdsId, '美国(包邮)240g涤纶休闲短裤-DG206-单面印花-美西洛杉矶一仓'),
`cat-${stamp}-parse`,
);
expect(result1).toBe('inserted');
const og = await prisma.originGood.findUniqueOrThrow({ where: { sdsGoodId: sdsId } });
createdOriginGoodIds.push(og.id);
expect(og.goodName).toBe('美国(包邮)240g涤纶休闲短裤-DG206-单面印花-美西洛杉矶一仓');
expect(og.skuCode).toBeNull();
expect(og.logisticsLabel).toBeNull();
expect(og.craftLabel).toBeNull();
expect(og.warehouseLabel).toBeNull();
// 更新名称 → 镜像原文变化;解析列保持为空(由整理回填)
await (service as any).upsertOriginGood(
sdsProduct(sdsId, '美国(不包邮)240g涤纶休闲短裤-DG206-单面印花'),
`cat-${stamp}-parse`,
);
const og2 = await prisma.originGood.findUniqueOrThrow({ where: { sdsGoodId: sdsId } });
expect(og2.goodName).toBe('美国(不包邮)240g涤纶休闲短裤-DG206-单面印花');
expect(og2.skuCode).toBeNull();
expect(og2.logisticsLabel).toBeNull();
});
it('新链接不自动挂族(归族由整理显式完成);族矩阵保持不变', async () => {
const sdsCat = `cat-hook-${stamp}`;
const cat = await prisma.category.create({
data: { categoryName: `挂族测试分类-${stamp}`, sdsCategoryId: sdsCat },
});
const seed = await prisma.originGood.create({
data: {
sdsGoodId: `hook-${stamp}-seed`,
goodName: `自动挂${stamp}(包邮)卫衣-ZZA${stamp}-单面印花`,
sdsCategoryId: sdsCat,
},
});
createdOriginGoodIds.push(seed.id);
await prisma.originGoodVariant.create({
data: {
originGoodId: seed.id,
sdsVariantId: 'seed-v1',
sku: 'SEED-S',
sizeId: 'size_S',
sizeName: 'S',
colorId: 'color_blk',
colorName: '黑色',
price: new Prisma.Decimal(25),
},
});
const family = await prisma.productFamily.create({
data: {
familyName: `自动挂族-${stamp}`,
primaryOriginGoodId: seed.id,
},
});
createdFamilyIds.push(family.id);
await prisma.originGood.update({
where: { id: seed.id },
data: { familyId: family.id },
});
await organize.deriveTagsForOg(seed.id);
await recompute.recomputeFamily(family.id);
const before = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
expect((before.priceMatrix as any).rows).toHaveLength(1);
// 同分类新链接(不同工艺)→ 同步后保持无族,等整理/管理员归族
const newSdsId = `hook-${stamp}-new`;
await (service as any).upsertOriginGood(
sdsProduct(newSdsId, `自动挂${stamp}(不包邮)卫衣-ZZA${stamp}-双面印花-某仓`),
sdsCat,
);
const newOg = await prisma.originGood.findUniqueOrThrow({ where: { sdsGoodId: newSdsId } });
createdOriginGoodIds.push(newOg.id);
expect(newOg.familyId).toBeNull();
// 族矩阵不因新链接同步而变化
await new Promise((r) => setTimeout(r, 200));
const after = await prisma.productFamily.findUniqueOrThrow({ where: { id: family.id } });
expect((after.priceMatrix as any).rows).toHaveLength(1);
await prisma.category.delete({ where: { id: cat.id } }).catch(() => undefined);
});
it('多族命中时不确定归属 → 不挂载', async () => {
// 两个族各含一个同分类成员 → 新链接分类命中两个族,归属不明,留给管理员
const sdsCat = `cat-multi-${stamp}`;
const mk = async (suffix: string) => {
const og = await prisma.originGood.create({
data: {
sdsGoodId: `hook-${stamp}-multi-${suffix}`,
goodName: `${suffix}(包邮)卫衣-ZZB${stamp}-单面印花`,
sdsCategoryId: sdsCat,
craftLabel: '单面印花',
logisticsLabel: '包邮',
},
});
createdOriginGoodIds.push(og.id);
const family = await prisma.productFamily.create({
data: { familyName: `多族${suffix}-${stamp}`, primaryOriginGoodId: og.id },
});
createdFamilyIds.push(family.id);
await prisma.originGood.update({ where: { id: og.id }, data: { familyId: family.id } });
return og;
};
await mk('x');
await mk('y');
const sdsId = `hook-${stamp}-multi-new`;
await (service as any).upsertOriginGood(
sdsProduct(sdsId, `新(包邮)卫衣-ZZB${stamp}-单面印花-新仓`),
sdsCat,
);
const og = await prisma.originGood.findUniqueOrThrow({ where: { sdsGoodId: sdsId } });
createdOriginGoodIds.push(og.id);
expect(og.familyId).toBeNull(); // 两个候选族 → 留给管理员
});
it('maybeEnqueueFamilyRecompute:有族入队、无族跳过', async () => {
const enqueueSpy = jest.spyOn(recompute, 'enqueue').mockImplementation(() => undefined);
const og = await prisma.originGood.create({
data: { sdsGoodId: `hook-${stamp}-noattach`, goodName: `不成组名称-${stamp}` },
});
createdOriginGoodIds.push(og.id);
await (service as any).maybeEnqueueFamilyRecompute(og.id);
expect(enqueueSpy).not.toHaveBeenCalled();
const family = await prisma.productFamily.create({
data: { familyName: `入队族-${stamp}` },
});
createdFamilyIds.push(family.id);
await prisma.originGood.update({ where: { id: og.id }, data: { familyId: family.id } });
await (service as any).maybeEnqueueFamilyRecompute(og.id);
expect(enqueueSpy).toHaveBeenCalledWith(family.id);
enqueueSpy.mockRestore();
});
});