refactor(api): pair public tag filters with groups
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
IsNumberString,
|
IsNumberString,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
|
Matches,
|
||||||
Max,
|
Max,
|
||||||
Min,
|
Min,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@@ -46,11 +47,20 @@ export class PublicQueryGoodDto {
|
|||||||
@IsNumberString()
|
@IsNumberString()
|
||||||
categoryId?: string;
|
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()
|
@IsOptional()
|
||||||
@Transform(stringList)
|
@Transform(stringList)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsNumberString({}, { each: true })
|
@Matches(/^\d+:\d+$/, {
|
||||||
|
each: true,
|
||||||
|
message: 'tagIds 每个元素必须为 tagGroupId:tagId',
|
||||||
|
})
|
||||||
tagIds?: string[];
|
tagIds?: string[];
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { NotFoundException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
import { PublicService } from './public.service';
|
import { PublicService } from './public.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
@@ -247,7 +247,9 @@ describe('PublicService', () => {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
countryId: countryId.toString(),
|
countryId: countryId.toString(),
|
||||||
keyword: `Pub `,
|
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(sameGroup.items.map((item) => item.goodName)).toEqual(
|
||||||
expect.arrayContaining([`Pub High ${stamp}`, `Pub Mid ${stamp}`]),
|
expect.arrayContaining([`Pub High ${stamp}`, `Pub Mid ${stamp}`]),
|
||||||
@@ -258,12 +260,25 @@ describe('PublicService', () => {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
countryId: countryId.toString(),
|
countryId: countryId.toString(),
|
||||||
keyword: `Pub `,
|
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)).toContain(`Pub High ${stamp}`);
|
||||||
expect(acrossGroups.items.map((item) => item.goodName)).not.toContain(`Pub Mid ${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 () => {
|
it('returns the SDS product id as the public product id', async () => {
|
||||||
const result = await service.getGoods({
|
const result = await service.getGoods({
|
||||||
page: 1,
|
page: 1,
|
||||||
|
|||||||
@@ -319,22 +319,46 @@ export class PublicService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async buildTagGroupFilters(
|
private async buildTagGroupFilters(
|
||||||
selectedTagIds: string[],
|
selectedTagValues: string[],
|
||||||
): Promise<Prisma.GoodWhereInput[]> {
|
): Promise<Prisma.GoodWhereInput[]> {
|
||||||
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({
|
? 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 },
|
select: { id: true, tagGroupId: true },
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
if (selected.length !== selectedTagIds.length) {
|
if (selected.length !== uniqueTagIds.length) {
|
||||||
throw new BadRequestException('包含不存在的标签 ID');
|
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<string, bigint[]>();
|
const byGroup = new Map<string, bigint[]>();
|
||||||
for (const tag of selected) {
|
for (const selection of selections) {
|
||||||
const key = tag.tagGroupId?.toString() ?? `tag:${tag.id.toString()}`;
|
const key = selection.tagGroupId;
|
||||||
const ids = byGroup.get(key) ?? [];
|
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);
|
byGroup.set(key, ids);
|
||||||
}
|
}
|
||||||
return [...byGroup.values()].map((ids) => ({
|
return [...byGroup.values()].map((ids) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user