From ee336968a050663adfca8d1051c5cfbfca8506a2 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Fri, 28 Aug 2026 15:33:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(product-family):=20derive=20tags=20per=20l?= =?UTF-8?q?ink=20name=20(=E5=8D=B0=E8=8A=B1=E6=95=B0=E9=87=8F/=E5=B7=A5?= =?UTF-8?q?=E8=89=BA/=E7=89=A9=E6=B5=81=20rules)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/goods/goods.service.ts | 19 +- .../product-families/auto-tag-rules.spec.ts | 58 +++++++ .../src/product-families/auto-tag-rules.ts | 56 ++++++ .../family-recompute.service.ts | 131 ++++++++------ .../product-families/family-tag-sync.spec.ts | 162 +++++++++++++----- 5 files changed, 314 insertions(+), 112 deletions(-) create mode 100644 apps/api/src/product-families/auto-tag-rules.spec.ts create mode 100644 apps/api/src/product-families/auto-tag-rules.ts diff --git a/apps/api/src/goods/goods.service.ts b/apps/api/src/goods/goods.service.ts index 67ba945..9abeed1 100644 --- a/apps/api/src/goods/goods.service.ts +++ b/apps/api/src/goods/goods.service.ts @@ -13,6 +13,7 @@ import { BatchPriorityDto } from './dto/batch-priority.dto'; import { GoodDetailDto, GoodDto, PaginatedGoods } from './dto/good.dto'; import { SyncService } from '../sync/sync.service'; import { FamilyRecomputeService } from '../product-families/family-recompute.service'; +import { isAutoTagGroupName } from '../product-families/auto-tag-rules'; import { randomUUID } from 'crypto'; import { CreateCustomGoodDto, @@ -619,24 +620,18 @@ export class GoodsService { if (!c) throw new BadRequestException(`Category ${id} not found`); } - /** 有族商品:剔除自动组(物流/工艺/位置)标签——它们由族同步管理 */ + /** 有族商品:剔除自动组(物流/工艺/印花数量等)标签——它们由族按链接名称派生 */ private async stripAutoGroupTags( tagIds: number[], familyId: bigint | null, ): Promise { if (!familyId || !tagIds.length) return tagIds; - const autoGroups = await this.prisma.tagGroup.findMany({ - where: { - OR: [ - { groupName: { contains: '物流' } }, - { groupName: { contains: '工艺' } }, - { groupName: { contains: '位置' } }, - ], - }, - select: { id: true }, + const groups = await this.prisma.tagGroup.findMany({ + select: { id: true, groupName: true }, }); - if (!autoGroups.length) return tagIds; - const autoIds = new Set(autoGroups.map((g) => g.id.toString())); + const autoIds = new Set( + groups.filter((g) => isAutoTagGroupName(g.groupName)).map((g) => g.id.toString()), + ); const tags = await this.prisma.tag.findMany({ where: { id: { in: tagIds.map((id) => BigInt(id)) } }, select: { id: true, tagGroupId: true }, diff --git a/apps/api/src/product-families/auto-tag-rules.spec.ts b/apps/api/src/product-families/auto-tag-rules.spec.ts new file mode 100644 index 0000000..01eaeac --- /dev/null +++ b/apps/api/src/product-families/auto-tag-rules.spec.ts @@ -0,0 +1,58 @@ +import { deriveLinkTagNames, isAutoTagGroupName } from './auto-tag-rules'; + +describe('auto-tag-rules / deriveLinkTagNames', () => { + it('单面印花 + 包邮 + 无工艺关键字 → 单面印花/烫画/包邮', () => { + expect( + deriveLinkTagNames('美国(包邮)180g纯棉T恤成人款-DG001-单面印花'), + ).toEqual(['单面印花', '烫画', '包邮']); + }); + + it('双面印花 + 不包邮 → 双面印花/烫画/不包邮', () => { + expect( + deriveLinkTagNames('美国(不包邮)180g纯棉T恤成人款-DG001-双面印花·美东新泽西仓'), + ).toEqual(['双面印花', '烫画', '不包邮']); + }); + + it('不打印 + 光板(物流备注)+ 不包邮 → 两个工艺标签 + 不包邮,无印花数量标签', () => { + expect( + deriveLinkTagNames('美国(不包邮光板)180GT恤成人款-JSA002-不打印·美西洛杉矶二仓'), + ).toEqual(['不打印', '光板', '不包邮']); + }); + + it('直喷命中时不给默认烫画', () => { + expect(deriveLinkTagNames('美国(包邮)卫衣-DG002-直喷')).toEqual(['直喷', '包邮']); + }); + + it('物流备注既无包邮也无不包邮时不下发物流标签', () => { + const names = deriveLinkTagNames('美国(快递)卫衣-DG002-单面印花'); + expect(names).toContain('单面印花'); + expect(names).toContain('烫画'); + expect(names).not.toContain('包邮'); + expect(names).not.toContain('不包邮'); + }); + + it('空名称返回空数组', () => { + expect(deriveLinkTagNames(null)).toEqual([]); + expect(deriveLinkTagNames('')).toEqual([]); + }); + + it('先判「不包邮」再判「包邮」,避免子串误命中', () => { + const names = deriveLinkTagNames('美国(不包邮)T恤-DG001-单面印花'); + expect(names).toContain('不包邮'); + expect(names).not.toContain('包邮'); + }); +}); + +describe('auto-tag-rules / isAutoTagGroupName', () => { + it('命中真实派生组:物流渠道 / 印刷工艺 / 印花数量 / 印刷位置', () => { + expect(isAutoTagGroupName('物流渠道')).toBe(true); + expect(isAutoTagGroupName('印刷工艺')).toBe(true); + expect(isAutoTagGroupName('印花数量')).toBe(true); + expect(isAutoTagGroupName('印刷位置')).toBe(true); + }); + + it('其他分组不视为自动组', () => { + expect(isAutoTagGroupName('潮流')).toBe(false); + expect(isAutoTagGroupName('场景')).toBe(false); + }); +}); diff --git a/apps/api/src/product-families/auto-tag-rules.ts b/apps/api/src/product-families/auto-tag-rules.ts new file mode 100644 index 0000000..b9defba --- /dev/null +++ b/apps/api/src/product-families/auto-tag-rules.ts @@ -0,0 +1,56 @@ +/** + * 链接标签自动派生规则(产品确认版,2026-08-28): + * + * 标签与「产品链接」一一对应 —— 每条链接因 印花数量/工艺/物流 不同而价格不同 + * (价格矩阵维度 = 印花数量·工艺 × 物流 × 尺码 × 颜色),因此标签按链接名称解析: + * + * - 印花数量:名称含「双面印花」→ 双面印花;否则含「单面印花」→ 单面印花; + * - 工艺:名称含「直喷」「不打印」「光板」→ 对应标签(可多个);都不含 → 默认「烫画」; + * - 物流:名称含「不包邮」→ 不包邮;否则含「包邮」→ 包邮(先判「不包邮」,避免子串误命中)。 + * + * 这些分组(印花数量 / 印刷工艺 / 物流渠道)下的标签为派生数据,不接受手动写入; + * 其他分组保持人工管理。 + */ + +/** 自动(派生)标签组名匹配:命中即视为族托管,手输的这类标签会被剔除 */ +export function isAutoTagGroupName(name: string): boolean { + return /物流|工艺|位置|印花数量/.test(name); +} + +export interface DerivedTagGroupSpec { + /** 标签组名(查找用 includes 匹配,缺失时自动建组) */ + group: string; + /** 组内规则标签(缺失时自动建标) */ + tags: string[]; +} + +/** 派生标签所属组及其全部合法取值 */ +export const DERIVED_TAG_GROUP_SPECS: DerivedTagGroupSpec[] = [ + { group: '物流渠道', tags: ['包邮', '不包邮'] }, + { group: '印花数量', tags: ['单面印花', '双面印花'] }, + { group: '印刷工艺', tags: ['烫画', '直喷', '不打印', '光板'] }, +]; + +const CRAFT_KEYWORDS = ['直喷', '不打印', '光板'] as const; +const CRAFT_DEFAULT = '烫画'; + +/** + * 由链接名称解析标签名集合(不含组信息)。 + * 确定性、纯函数 —— 同名链接在任何环境派生结果一致。 + */ +export function deriveLinkTagNames(name: string | null | undefined): string[] { + if (!name) return []; + const names: string[] = []; + + if (name.includes('双面印花')) names.push('双面印花'); + else if (name.includes('单面印花')) names.push('单面印花'); + + const craftHits = CRAFT_KEYWORDS.filter((keyword) => name.includes(keyword)); + if (craftHits.length > 0) names.push(...craftHits); + else names.push(CRAFT_DEFAULT); + + if (name.includes('不包邮')) names.push('不包邮'); + else if (name.includes('包邮')) names.push('包邮'); + + return names; +} diff --git a/apps/api/src/product-families/family-recompute.service.ts b/apps/api/src/product-families/family-recompute.service.ts index b355621..25bb1a2 100644 --- a/apps/api/src/product-families/family-recompute.service.ts +++ b/apps/api/src/product-families/family-recompute.service.ts @@ -1,6 +1,11 @@ import { Injectable, Logger } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; +import { + DERIVED_TAG_GROUP_SPECS, + deriveLinkTagNames, + isAutoTagGroupName, +} from './auto-tag-rules'; /** * 产品族重算:并集尺码表/包装规则 + 五维价格矩阵物化。 @@ -291,73 +296,45 @@ export class FamilyRecomputeService { } // 派生标签同步(无论是否锁定:标签是派生数据而非人工策展) - await this.syncFamilyTags(family.id, members); + await this.syncFamilyTags(family.id); } /** - * 族 → 商品标签同步:物流/工艺/印刷位置类标签组(组名含「物流」「工艺」或「位置」, - * 与官网筛选维度一致)下的标签由族成员的 logisticsLabel/craftLabel 派生; - * 标签名与链接标签存在词尾差异(单面印花 ↔ 单面印),采用前缀匹配并取最长命中。 - * 其他分组保持人工管理。仅更新发生变化的商品,幂等。 + * 族 → 商品标签同步:标签与「产品链接」一一对应,按每条链接自身的名称解析 + * (印花数量 / 工艺 / 物流,规则见 auto-tag-rules.ts)。派生组缺失的组/标签 + * 自动补建;仅更新发生变化的商品,幂等。 */ - async syncFamilyTags( - familyId: bigint, - preloadedMembers?: Member[], - ): Promise<{ goodsUpdated: number }> { - const members = - preloadedMembers ?? - (await this.prisma.originGood.findMany({ - where: { familyId, delisted: false }, - select: { logisticsLabel: true, craftLabel: true }, - })); - const labels = members - .flatMap((m) => [m.logisticsLabel, m.craftLabel]) - .filter((l): l is string => !!l); - - const autoGroups = await this.prisma.tagGroup.findMany({ - where: { - OR: [ - { groupName: { contains: '物流' } }, - { groupName: { contains: '工艺' } }, - { groupName: { contains: '位置' } }, - ], - }, - select: { id: true }, - }); - const autoGroupIds = new Set(autoGroups.map((g) => g.id.toString())); - if (!autoGroupIds.size || !labels.length) { - return { goodsUpdated: 0 }; - } - - const candidates = await this.prisma.tag.findMany({ - where: { tagGroupId: { in: autoGroups.map((g) => g.id) } }, - select: { id: true, tagName: true }, - }); - // 每个链接标签取「最长前缀命中」的标签(避免短名误吃长名场景) - const derivedIds = new Set(); - for (const label of new Set(labels)) { - let best: { id: bigint; name: string } | null = null; - for (const tag of candidates) { - if (label === tag.tagName || label.startsWith(tag.tagName)) { - if (!best || tag.tagName.length > best.name.length) { - best = { id: tag.id, name: tag.tagName }; - } - } - } - if (best) derivedIds.add(best.id); - } - + async syncFamilyTags(familyId: bigint): Promise<{ goodsUpdated: number }> { + const tagMap = await this.ensureDerivedTagMap(); const goods = await this.prisma.good.findMany({ where: { familyId }, - include: { goodTags: { include: { tag: { select: { id: true, tagGroupId: true } } } } }, + include: { + goodTags: { include: { tag: { select: { id: true, tagGroupId: true } } } }, + originGood: { select: { goodName: true, source: true } }, + }, }); + if (!goods.length) return { goodsUpdated: 0 }; + + const groups = await this.prisma.tagGroup.findMany({ + select: { id: true, groupName: true }, + }); + const autoGroupIds = new Set( + groups.filter((g) => isAutoTagGroupName(g.groupName)).map((g) => g.id.toString()), + ); let goodsUpdated = 0; for (const good of goods) { - // 保留:非自动分组的既有标签(人工管理)∪ 派生标签 + // 保留:非自动分组的既有标签(人工管理)∪ 本链接名称的派生标签 const keep = good.goodTags .filter((gt) => !autoGroupIds.has(gt.tag.tagGroupId?.toString() ?? '')) .map((gt) => gt.tagId); + const derivedNames = + good.originGood && good.originGood.source === 'SDS' + ? deriveLinkTagNames(good.originGood.goodName) + : []; + const derivedIds = derivedNames + .map((name) => tagMap.get(name)) + .filter((id): id is bigint => id !== undefined); const target = [...new Set([...keep, ...derivedIds])].sort((a, b) => Number(a - b), ); @@ -379,4 +356,50 @@ export class FamilyRecomputeService { } return { goodsUpdated }; } + + /** 确保派生标签组与标签存在,返回「标签名 → 标签 id」映射(并发下取最小 id,天然去重) */ + private async ensureDerivedTagMap(): Promise> { + const map = new Map(); + for (const spec of DERIVED_TAG_GROUP_SPECS) { + const findGroups = () => + this.prisma.tagGroup.findMany({ + where: { groupName: { contains: spec.group } }, + orderBy: { id: 'asc' }, + }); + let group = (await findGroups())[0] ?? null; + if (!group) { + try { + group = await this.prisma.tagGroup.create({ data: { groupName: spec.group } }); + } catch { + group = (await findGroups())[0] ?? null; + } + } + if (!group) continue; + + const tags = await this.prisma.tag.findMany({ + where: { tagGroupId: group.id, tagName: { in: spec.tags } }, + orderBy: { id: 'asc' }, + }); + for (const tagName of spec.tags) { + const existing = tags.find((t) => t.tagName === tagName); + if (existing) { + map.set(tagName, existing.id); + continue; + } + try { + const created = await this.prisma.tag.create({ + data: { tagGroupId: group.id, tagName }, + }); + map.set(tagName, created.id); + } catch { + const fallback = await this.prisma.tag.findFirst({ + where: { tagGroupId: group.id, tagName }, + orderBy: { id: 'asc' }, + }); + if (fallback) map.set(tagName, fallback.id); + } + } + } + return map; + } } diff --git a/apps/api/src/product-families/family-tag-sync.spec.ts b/apps/api/src/product-families/family-tag-sync.spec.ts index dc67adf..4643c52 100644 --- a/apps/api/src/product-families/family-tag-sync.spec.ts +++ b/apps/api/src/product-families/family-tag-sync.spec.ts @@ -1,10 +1,9 @@ import { Test } from '@nestjs/testing'; -import { Prisma } from '@prisma/client'; import { FamilyRecomputeService } from './family-recompute.service'; import { PrismaService } from '../prisma/prisma.service'; -/** 族 → 商品标签自动同步(物流/工艺组由成员标签派生,其他分组人工保留) */ -describe('FamilyRecomputeService.syncFamilyTags', () => { +/** 族 → 商品标签自动同步:标签按每条链接自身名称派生(印花数量/工艺/物流) */ +describe('FamilyRecomputeService.syncFamilyTags(按链接派生)', () => { let service: FamilyRecomputeService; let prisma: PrismaService; const stamp = Date.now(); @@ -15,7 +14,6 @@ describe('FamilyRecomputeService.syncFamilyTags', () => { country: [] as bigint[], category: [] as bigint[], group: [] as bigint[], - otherGroup: [] as bigint[], tag: [] as bigint[], }; @@ -29,47 +27,54 @@ describe('FamilyRecomputeService.syncFamilyTags', () => { ids.country.push((await prisma.country.create({ data: { countryName: `标签同步国家-${stamp}` } })).id); ids.category.push((await prisma.category.create({ data: { categoryName: `标签同步分类-${stamp}` } })).id); - // 物流组(自动)+ 其他组(人工) - ids.group.push((await prisma.tagGroup.create({ data: { groupName: `物流渠道${stamp}`, sortOrder: 1 } })).id); - ids.otherGroup.push((await prisma.tagGroup.create({ data: { groupName: `风格${stamp}`, sortOrder: 2 } })).id); + // 人工分组(派生规则之外,标签应保留) + ids.group.push((await prisma.tagGroup.create({ data: { groupName: `风格${stamp}`, sortOrder: 9 } })).id); }); afterAll(async () => { - await prisma.goodTag.deleteMany({ where: { tagId: { in: ids.tag } } }); + await prisma.goodTag.deleteMany({ where: { goodId: { in: ids.good } } }); await prisma.good.deleteMany({ where: { id: { in: ids.good } } }); await prisma.originGood.deleteMany({ where: { id: { in: ids.originGood } } }); await prisma.productFamily.deleteMany({ where: { id: { in: ids.family } } }); await prisma.tag.deleteMany({ where: { id: { in: ids.tag } } }); - await prisma.tagGroup.deleteMany({ where: { id: { in: [...ids.group, ...ids.otherGroup] } } }); + await prisma.tagGroup.deleteMany({ where: { id: { in: ids.group } } }); await prisma.country.deleteMany({ where: { id: { in: ids.country } } }); await prisma.category.deleteMany({ where: { id: { in: ids.category } } }); await prisma.$disconnect(); }); - function mkTag(name: string, groupId: bigint) { - return prisma.tag.create({ data: { tagName: name, tagGroupId: groupId } }); + async function tagNames(goodId: bigint): Promise { + const rows = await prisma.goodTag.findMany({ + where: { goodId }, + include: { tag: true }, + }); + return rows.map((r) => r.tag.tagName); } - it('族成员标签 → 自动同步到族下商品;人工分组标签保留;成员变化后增量更新', async () => { - const tagBaoyou = await mkTag(`包邮${stamp}`, ids.group[0]); - const tagDanyin = await mkTag(`单面印花${stamp}`, ids.group[0]); - const tagStyle = await mkTag(`潮流${stamp}`, ids.otherGroup[0]); - ids.tag.push(tagBaoyou.id, tagDanyin.id, tagStyle.id); + it('每个商品的标签只来自自己的链接名称;旧自动组标签被剔除;人工分组保留', async () => { + const styleTag = await prisma.tag.create({ data: { tagName: `潮流${stamp}`, tagGroupId: ids.group[0] } }); + ids.tag.push(styleTag.id); + const legacyPositionTag = await prisma.tag.findFirst({ + where: { tagName: '单面印', tagGroup: { groupName: { contains: '印刷位置' } } }, + }); + const legacyBaoyou = await prisma.tag.findFirst({ + where: { tagName: '包邮', tagGroup: { groupName: { contains: '物流渠道' } } }, + }); const og1 = await prisma.originGood.create({ data: { sdsGoodId: `tagsync-${stamp}-1`, - goodName: `美国(包邮${stamp})T恤-TS${stamp}-单面印花${stamp}`, - logisticsLabel: `包邮${stamp}`, - craftLabel: `单面印花${stamp}`, + goodName: `美国(包邮)180g纯棉T恤成人款-DG${stamp}-单面印花`, + logisticsLabel: '包邮', + craftLabel: '单面印花', }, }); const og2 = await prisma.originGood.create({ data: { sdsGoodId: `tagsync-${stamp}-2`, - goodName: `美国(不包邮)T恤-TS${stamp}-双面印花`, - logisticsLabel: `不包邮`, - craftLabel: `双面印花`, + goodName: `美国(不包邮光板)180GT恤成人款-JS${stamp}-不打印·美西洛杉矶二仓`, + logisticsLabel: '不包邮光板', + craftLabel: '不打印', }, }); ids.originGood.push(og1.id, og2.id); @@ -81,48 +86,113 @@ describe('FamilyRecomputeService.syncFamilyTags', () => { where: { id: { in: [og1.id, og2.id] } }, data: { familyId: family.id }, }); - const good = await prisma.good.create({ + + const good1 = await prisma.good.create({ data: { originGoodId: og1.id, familyId: family.id, countryId: ids.country[0], categoryId: ids.category[0], - goodName: `标签同步商品-${stamp}`, + goodName: `标签同步商品1-${stamp}`, }, }); - ids.good.push(good.id); - // 预置一个人工分组标签 - await prisma.goodTag.create({ data: { goodId: good.id, tagId: tagStyle.id } }); + const good2 = await prisma.good.create({ + data: { + originGoodId: og2.id, + familyId: family.id, + countryId: ids.country[0], + categoryId: ids.category[0], + goodName: `标签同步商品2-${stamp}`, + }, + }); + ids.good.push(good1.id, good2.id); + // 预置:人工分组标签(保留)+ 旧自动组标签(应被剔除) + await prisma.goodTag.create({ data: { goodId: good1.id, tagId: styleTag.id } }); + if (legacyPositionTag) { + await prisma.goodTag.create({ data: { goodId: good1.id, tagId: legacyPositionTag.id } }); + } + if (legacyBaoyou) { + await prisma.goodTag.create({ data: { goodId: good2.id, tagId: legacyBaoyou.id } }); + } - // 初次同步:物流/工艺组标签由成员标签派生(含真实组的前缀命中),人工"潮流"保留 const r1 = await service.syncFamilyTags(family.id); - expect(r1.goodsUpdated).toBe(1); - const names1 = (await prisma.goodTag.findMany({ - where: { goodId: good.id }, - include: { tag: true }, - })).map((t) => t.tag.tagName); - expect(names1).toContain(`包邮${stamp}`); // og1 物流(最长前缀命中戳记标签) - expect(names1).toContain(`单面印花${stamp}`); // og1 工艺 - expect(names1).toContain('不包邮'); // og2 物流(真实组命中) - expect(names1).toContain('双面印'); // og2 工艺 双面印花 → 前缀命中真实标签 - expect(names1).toContain(`潮流${stamp}`); // 人工分组标签保留 + expect(r1.goodsUpdated).toBe(2); + + const names1 = await tagNames(good1.id); + expect(names1).toContain('单面印花'); + expect(names1).toContain('烫画'); + expect(names1).toContain('包邮'); + expect(names1).toContain(`潮流${stamp}`); + expect(names1).not.toContain('单面印'); + expect(names1).not.toContain('双面印花'); + expect(names1).not.toContain('不包邮'); + + // og2 名称含 不打印 + 光板(物流备注)→ 两个工艺标签,无默认烫画、无印花数量标签 + const names2 = await tagNames(good2.id); + expect(names2).toContain('不打印'); + expect(names2).toContain('光板'); + expect(names2).toContain('不包邮'); + expect(names2).not.toContain('烫画'); + expect(names2).not.toContain('包邮'); + expect(names2).not.toContain('单面印花'); // 幂等:无变化不写 const r2 = await service.syncFamilyTags(family.id); expect(r2.goodsUpdated).toBe(0); - // og1 工艺改掉 → 其派生的 单面印花stamped 消失(og2 工艺是 双面印花,不命中它),人工标签保留 + // 链接名称变化 → good1 标签跟随新名称 await prisma.originGood.update({ where: { id: og1.id }, - data: { craftLabel: `双面印花` }, + data: { goodName: `美国(不包邮)180g纯棉T恤成人款-DG${stamp}-双面印花` }, }); await service.syncFamilyTags(family.id); - const names3 = (await prisma.goodTag.findMany({ - where: { goodId: good.id }, - include: { tag: true }, - })).map((t) => t.tag.tagName); - expect(names3).not.toContain(`单面印花${stamp}`); - expect(names3).toContain(`包邮${stamp}`); + const names3 = await tagNames(good1.id); + expect(names3).toContain('双面印花'); + expect(names3).toContain('烫画'); + expect(names3).toContain('不包邮'); expect(names3).toContain(`潮流${stamp}`); + expect(names3).not.toContain('单面印花'); + expect(names3).not.toContain('包邮'); + }); + + it('自定义来源的成员不派生标签,仅剔除自动组标签', async () => { + const realBaoyou = await prisma.tag.findFirst({ + where: { tagName: '包邮', tagGroup: { groupName: { contains: '物流渠道' } } }, + }); + const ogCustom = await prisma.originGood.create({ + data: { + source: 'CUSTOM', + sdsGoodId: `tagsync-custom-${stamp}`, + goodName: `自定义包邮T恤${stamp}`, + }, + }); + ids.originGood.push(ogCustom.id); + const family = await prisma.productFamily.create({ + data: { familyName: `自定义标签族-${stamp}`, primaryOriginGoodId: ogCustom.id }, + }); + ids.family.push(family.id); + await prisma.originGood.update({ + where: { id: ogCustom.id }, + data: { familyId: family.id }, + }); + const good = await prisma.good.create({ + data: { + originGoodId: ogCustom.id, + familyId: family.id, + countryId: ids.country[0], + categoryId: ids.category[0], + goodName: `自定义标签商品-${stamp}`, + }, + }); + ids.good.push(good.id); + if (realBaoyou) { + await prisma.goodTag.create({ data: { goodId: good.id, tagId: realBaoyou.id } }); + } + + const r = await service.syncFamilyTags(family.id); + expect(r.goodsUpdated).toBe(realBaoyou ? 1 : 0); + const names = await tagNames(good.id); + expect(names).not.toContain('包邮'); + expect(names).not.toContain('烫画'); }); });