feat(product-family): derive tags per link name (印花数量/工艺/物流 rules)
This commit is contained in:
@@ -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<bigint>();
|
||||
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<Map<string, bigint>> {
|
||||
const map = new Map<string, bigint>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user