feat(product-family): link-level tags (origin_good_tags) with manual override + member detail enrichment

This commit is contained in:
yeuimu
2026-08-28 16:00:46 +08:00
parent 37b52c5ebb
commit 08201f18a4
11 changed files with 477 additions and 87 deletions
@@ -1,8 +1,27 @@
import { Injectable } from '@nestjs/common';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { FamilyRecomputeService } from '../product-families/family-recompute.service';
import { QueryOriginGoodDto } from './dto/query-origin-good.dto';
/** 链接标签(origin_good_tags 行,含人工/派生标记) */
export interface OriginGoodTagItem {
id: string;
tagName: string;
tagColor: string | null;
tagFontColor: string | null;
manual: boolean;
}
export interface OriginGoodTagsResult {
tagsManual: boolean;
tags: OriginGoodTagItem[];
}
export interface PaginatedOriginGoods {
items: Array<{
id: string;
@@ -67,7 +86,94 @@ export interface OriginGoodsTreeResponse {
@Injectable()
export class OriginGoodsService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly familyRecompute: FamilyRecomputeService,
) {}
/** 链接当前标签(含 manual 标记) */
async getTags(id: bigint): Promise<OriginGoodTagsResult> {
const og = await this.prisma.originGood.findUnique({
where: { id },
include: {
originGoodTags: {
include: {
tag: { select: { id: true, tagName: true, tagColor: true, tagFontColor: true } },
},
orderBy: { tagId: 'asc' },
},
},
});
if (!og) throw new NotFoundException(`Origin good ${id} not found`);
return {
tagsManual: og.tagsManual,
tags: og.originGoodTags.map((r) => ({
id: r.tag.id.toString(),
tagName: r.tag.tagName,
tagColor: r.tag.tagColor,
tagFontColor: r.tag.tagFontColor,
manual: r.manual,
})),
};
}
/**
* 人工接管链接标签:全量替换为 manual 行(自动同步永不覆盖),
* 并把有效标签镜像到该链接名下的商品。
*/
async updateTags(id: bigint, tagIds: number[]): Promise<OriginGoodTagsResult> {
const og = await this.prisma.originGood.findUnique({
where: { id },
select: { id: true },
});
if (!og) throw new NotFoundException(`Origin good ${id} not found`);
const uniqueIds = [...new Set(tagIds.map((v) => BigInt(v)))].sort((a, b) =>
Number(a - b),
);
if (uniqueIds.length) {
const count = await this.prisma.tag.count({ where: { id: { in: uniqueIds } } });
if (count !== uniqueIds.length) {
throw new BadRequestException('存在无效标签');
}
}
await this.prisma.$transaction([
this.prisma.originGoodTag.deleteMany({ where: { originGoodId: id } }),
...(!uniqueIds.length
? []
: [
this.prisma.originGoodTag.createMany({
data: uniqueIds.map((tagId) => ({
originGoodId: id,
tagId,
manual: true,
})),
}),
]),
this.prisma.originGood.update({ where: { id }, data: { tagsManual: true } }),
]);
await this.familyRecompute.mirrorLinkTagsToGoods(id);
return this.getTags(id);
}
/** 恢复自动:清掉全部标签行(含人工行),回到按链接名称派生 */
async resetTags(id: bigint): Promise<OriginGoodTagsResult> {
const og = await this.prisma.originGood.findUnique({
where: { id },
select: { id: true, familyId: true },
});
if (!og) throw new NotFoundException(`Origin good ${id} not found`);
await this.prisma.$transaction([
this.prisma.originGoodTag.deleteMany({ where: { originGoodId: id } }),
this.prisma.originGood.update({ where: { id }, data: { tagsManual: false } }),
]);
if (og.familyId) {
// 族内链接:整族重派生 + 商品镜像
await this.familyRecompute.syncFamilyTags(og.familyId);
} else {
await this.familyRecompute.refreshLinkTags(id);
}
return this.getTags(id);
}
async findAll(query: QueryOriginGoodDto): Promise<PaginatedOriginGoods> {
const { page, pageSize, keyword } = query;