From 437d9ca93b94c780136b16fc0e34c17e5f516589 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Fri, 21 Aug 2026 10:24:35 +0800 Subject: [PATCH] refactor(api): pair public tag filters with groups --- .../src/public/dto/public-query-good.dto.ts | 14 ++++++- apps/api/src/public/public.service.spec.ts | 21 ++++++++-- apps/api/src/public/public.service.ts | 38 +++++++++++++++---- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/apps/api/src/public/dto/public-query-good.dto.ts b/apps/api/src/public/dto/public-query-good.dto.ts index 7e3c8e4..747e6bf 100644 --- a/apps/api/src/public/dto/public-query-good.dto.ts +++ b/apps/api/src/public/dto/public-query-good.dto.ts @@ -7,6 +7,7 @@ import { IsNumberString, IsOptional, IsString, + Matches, Max, Min, } from 'class-validator'; @@ -46,11 +47,20 @@ export class PublicQueryGoodDto { @IsNumberString() categoryId?: string; - @ApiProperty({ required: false, type: [String], description: '标签 ID;支持重复参数或逗号分隔' }) + @ApiProperty({ + required: false, + type: [String], + description: + '标签筛选项,格式为 tagGroupId:tagId;支持重复参数或逗号分隔。同组 OR,跨组 AND', + example: ['1:11', '1:12', '2:25'], + }) @IsOptional() @Transform(stringList) @IsArray() - @IsNumberString({}, { each: true }) + @Matches(/^\d+:\d+$/, { + each: true, + message: 'tagIds 每个元素必须为 tagGroupId:tagId', + }) tagIds?: string[]; @ApiProperty({ required: false }) diff --git a/apps/api/src/public/public.service.spec.ts b/apps/api/src/public/public.service.spec.ts index 68d4bfe..125ea37 100644 --- a/apps/api/src/public/public.service.spec.ts +++ b/apps/api/src/public/public.service.spec.ts @@ -1,5 +1,5 @@ import { Test } from '@nestjs/testing'; -import { NotFoundException } from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { PublicService } from './public.service'; import { PrismaService } from '../prisma/prisma.service'; @@ -247,7 +247,9 @@ describe('PublicService', () => { pageSize: 50, countryId: countryId.toString(), keyword: `Pub `, - tagIds: filterTagIds.slice(0, 2).map(String), + tagIds: filterTagIds + .slice(0, 2) + .map((tagId) => `${filterGroupIds[0]}:${tagId}`), }); expect(sameGroup.items.map((item) => item.goodName)).toEqual( expect.arrayContaining([`Pub High ${stamp}`, `Pub Mid ${stamp}`]), @@ -258,12 +260,25 @@ describe('PublicService', () => { pageSize: 50, countryId: countryId.toString(), keyword: `Pub `, - tagIds: filterTagIds.map(String), + tagIds: filterTagIds.map( + (tagId, index) => + `${index < 2 ? filterGroupIds[0] : filterGroupIds[1]}:${tagId}`, + ), }); expect(acrossGroups.items.map((item) => item.goodName)).toContain(`Pub High ${stamp}`); expect(acrossGroups.items.map((item) => item.goodName)).not.toContain(`Pub Mid ${stamp}`); }); + it('rejects a tag paired with the wrong tag group', async () => { + await expect( + service.getGoods({ + page: 1, + pageSize: 20, + tagIds: [`${filterGroupIds[1]}:${filterTagIds[0]}`], + }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + it('returns the SDS product id as the public product id', async () => { const result = await service.getGoods({ page: 1, diff --git a/apps/api/src/public/public.service.ts b/apps/api/src/public/public.service.ts index 0e1f31a..e69c2ba 100644 --- a/apps/api/src/public/public.service.ts +++ b/apps/api/src/public/public.service.ts @@ -319,22 +319,46 @@ export class PublicService { } private async buildTagGroupFilters( - selectedTagIds: string[], + selectedTagValues: string[], ): Promise { - const selected = selectedTagIds.length + const selections = selectedTagValues.map((value) => { + const [tagGroupId, tagId] = value.split(':'); + if (!tagGroupId || !tagId || !/^\d+$/.test(tagGroupId) || !/^\d+$/.test(tagId)) { + throw new BadRequestException( + 'tagIds 每个元素必须为 tagGroupId:tagId', + ); + } + return { tagGroupId, tagId }; + }); + const uniqueTagIds = [...new Set(selections.map((item) => item.tagId))]; + const selected = uniqueTagIds.length ? await this.prisma.tag.findMany({ - where: { id: { in: selectedTagIds.map((id) => BigInt(id)) } }, + where: { id: { in: uniqueTagIds.map((id) => BigInt(id)) } }, select: { id: true, tagGroupId: true }, }) : []; - if (selected.length !== selectedTagIds.length) { + if (selected.length !== uniqueTagIds.length) { throw new BadRequestException('包含不存在的标签 ID'); } + const actualGroups = new Map( + selected.map((tag) => [ + tag.id.toString(), + tag.tagGroupId?.toString() ?? null, + ]), + ); + for (const selection of selections) { + if (actualGroups.get(selection.tagId) !== selection.tagGroupId) { + throw new BadRequestException( + `标签 ${selection.tagId} 不属于标签组 ${selection.tagGroupId}`, + ); + } + } const byGroup = new Map(); - for (const tag of selected) { - const key = tag.tagGroupId?.toString() ?? `tag:${tag.id.toString()}`; + for (const selection of selections) { + const key = selection.tagGroupId; const ids = byGroup.get(key) ?? []; - if (!ids.some((id) => id === tag.id)) ids.push(tag.id); + const id = BigInt(selection.tagId); + if (!ids.includes(id)) ids.push(id); byGroup.set(key, ids); } return [...byGroup.values()].map((ids) => ({