From d826c5bdd4c45fe4731b3531a74b73d5e9643a17 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Fri, 28 Aug 2026 11:19:27 +0800 Subject: [PATCH] feat(public): merge secondary media gallery images with URL dedup --- apps/api/src/public/public.service.spec.ts | 7 +++++ apps/api/src/public/public.service.ts | 31 +++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/api/src/public/public.service.spec.ts b/apps/api/src/public/public.service.spec.ts index 1272ff6..9a68d22 100644 --- a/apps/api/src/public/public.service.spec.ts +++ b/apps/api/src/public/public.service.spec.ts @@ -435,6 +435,7 @@ describe('PublicService', () => { sizeChart: { rows: [{ sizeName: 'S', measurements: [{ key: 'chest', cm: '94' }] }, { sizeName: 'M', measurements: [{ key: 'chest', cm: '100' }] }] }, packageSpecs: { rows: [{ sizeName: 'S' }] }, options: { sizes: [{ name: 'S' }, { name: 'M' }] }, + media: { images: ['http://img/pri-a', 'http://img/pri-b'], primaryImageUrl: 'http://img/pri-a' }, }, }); // Secondary: duplicate Black|S with a DIFFERENT price (must be dropped, @@ -451,6 +452,7 @@ describe('PublicService', () => { sizeChart: { rows: [{ sizeName: 'XXXL', measurements: [{ key: 'chest', cm: '120' }] }] }, packageSpecs: { rows: [{ sizeName: 'M' }, { sizeName: 'XXXL' }] }, options: { sizes: [{ name: 'XXXL' }] }, + media: { images: ['http://img/pri-a', 'http://img/sec-x'], primaryImageUrl: 'http://img/pri-a' }, }, }); const mergedGood = await prisma.good.create({ @@ -483,6 +485,11 @@ describe('PublicService', () => { // Options: sizes unioned S/M + XXXL. const optSizes = (detail.options as any).sizes.map((s: any) => s.name).sort(); expect(optSizes).toEqual(['M', 'S', 'XXXL']); + // Media gallery: primary images first, secondary-only URL appended, + // duplicate URL (pri-a) kept once. + const media = detail.media as any; + expect(media.images).toEqual(['http://img/pri-a', 'http://img/pri-b', 'http://img/sec-x']); + expect(media.primaryImageUrl).toBe('http://img/pri-a'); } finally { await prisma.goodOriginGood.deleteMany({ where: { goodId: mergedGood.id } }); await prisma.good.delete({ where: { id: mergedGood.id } }); diff --git a/apps/api/src/public/public.service.ts b/apps/api/src/public/public.service.ts index 68599d9..24d226a 100644 --- a/apps/api/src/public/public.service.ts +++ b/apps/api/src/public/public.service.ts @@ -385,7 +385,7 @@ export class PublicService { designArea: detail?.designArea ?? null, pictureRequest: detail?.pictureRequest ?? null, }, - media: (detail?.media as Record | null) ?? null, + media: this.mergeMedia(detail, secondaryDetails), mediaByColor: this.groupImagesByColor(allVariants), options: this.mergeOptions(detail, secondaryDetails), sizeChart: this.mergeRowsByKey(detail, secondaryDetails, 'sizeChart'), @@ -496,6 +496,35 @@ export class PublicService { return merged; } + /** + * Merge the gallery `media` across details: primary images first, + * secondary-only image URLs appended (URL-deduped). `primaryImageUrl` + * stays the primary's. + */ + private mergeMedia( + primary: PublicGoodRow['originGood']['detail'], + secondaries: NonNullable[], + ): Record | null { + const toImages = (media: unknown): string[] => { + const images = (media as { images?: unknown } | null)?.images; + return Array.isArray(images) ? images.filter((u): u is string => typeof u === 'string') : []; + }; + const primaryMedia = (primary?.media as Record | null) ?? null; + if (!primaryMedia && secondaries.length === 0) return null; + const images = toImages(primaryMedia); + const seen = new Set(images); + for (const sec of secondaries) { + for (const url of toImages(sec.media)) { + if (!seen.has(url)) { + seen.add(url); + images.push(url); + } + } + } + if (images.length === 0) return null; + return { ...(primaryMedia ?? {}), images }; + } + private async buildTagGroupFilters( selectedGroups: PublicTagFilterDto[], ): Promise {