feat(product-family): link-level tags (origin_good_tags) with manual override + member detail enrichment
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
ArrayNotEmpty,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
} from 'class-validator';
|
||||
|
||||
export class UpdateOriginGoodTagsDto {
|
||||
@ApiProperty({ description: '链接标签 id 全量集合(人工接管)', type: [Number] })
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ArrayMaxSize(50)
|
||||
@IsNumber({}, { each: true })
|
||||
@Type(() => Number)
|
||||
tagIds: number[];
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { OriginGoodsService } from './origin-goods.service';
|
||||
import { QueryOriginGoodDto } from './dto/query-origin-good.dto';
|
||||
import { UpdateOriginGoodTagsDto } from './dto/update-origin-good-tags.dto';
|
||||
|
||||
@ApiTags('origin-goods')
|
||||
@ApiBearerAuth()
|
||||
@@ -24,4 +25,22 @@ export class OriginGoodsController {
|
||||
findAll(@Query() query: QueryOriginGoodDto) {
|
||||
return this.service.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':id/tags')
|
||||
@ApiOperation({ summary: '链接当前标签(含 manual 标记)' })
|
||||
getTags(@Param('id') id: string) {
|
||||
return this.service.getTags(BigInt(id));
|
||||
}
|
||||
|
||||
@Put(':id/tags')
|
||||
@ApiOperation({ summary: '人工接管链接标签(全量替换,自动同步不再覆盖)' })
|
||||
updateTags(@Param('id') id: string, @Body() dto: UpdateOriginGoodTagsDto) {
|
||||
return this.service.updateTags(BigInt(id), dto.tagIds);
|
||||
}
|
||||
|
||||
@Delete(':id/tags')
|
||||
@ApiOperation({ summary: '恢复自动派生(清掉人工标签)' })
|
||||
resetTags(@Param('id') id: string) {
|
||||
return this.service.resetTags(BigInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OriginGoodsController } from './origin-goods.controller';
|
||||
import { OriginGoodsService } from './origin-goods.service';
|
||||
import { ProductFamiliesModule } from '../product-families/product-families.module';
|
||||
|
||||
@Module({
|
||||
imports: [ProductFamiliesModule],
|
||||
controllers: [OriginGoodsController],
|
||||
providers: [OriginGoodsService],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { OriginGoodsService } from './origin-goods.service';
|
||||
import { FamilyRecomputeService } from '../product-families/family-recompute.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
describe('OriginGoodsService', () => {
|
||||
@@ -10,7 +11,7 @@ describe('OriginGoodsService', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [OriginGoodsService, PrismaService],
|
||||
providers: [OriginGoodsService, FamilyRecomputeService, PrismaService],
|
||||
}).compile();
|
||||
service = moduleRef.get(OriginGoodsService);
|
||||
prisma = moduleRef.get(PrismaService);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user