From 1245df497bb1e3c6d9ee3382f5853f80240f0625 Mon Sep 17 00:00:00 2001 From: R2m1liA <15258427350@163.com> Date: Thu, 4 Dec 2025 17:38:43 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E7=9B=B8=E5=85=B3Mapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 空值处理与类型控制:为相关关系型字段的数据处理添加空值处理与类型控制 - 调整目录结构:将文件目录按照实际查询划分为Product和ProductList两个文件 --- server/mappers/documentMapper.test.ts | 63 +-- server/mappers/documentMapper.ts | 28 -- server/mappers/productListMapper.test.ts | 125 +++++ server/mappers/productListMapper.ts | 54 ++ server/mappers/productMapper.test.ts | 470 ++++++++++++++---- server/mappers/productMapper.ts | 255 ++++++---- server/mappers/questionMapper.test.ts | 42 +- server/mappers/questionMapper.ts | 19 - server/services/cms/productService.ts | 6 +- shared/types/views/index.ts | 3 - shared/types/views/product-document-view.ts | 23 - shared/types/views/product-question-view.ts | 14 - shared/types/views/product-spec-group-view.ts | 29 -- shared/types/views/product-view.ts | 84 +++- 14 files changed, 801 insertions(+), 414 deletions(-) create mode 100644 server/mappers/productListMapper.test.ts create mode 100644 server/mappers/productListMapper.ts delete mode 100644 shared/types/views/product-document-view.ts delete mode 100644 shared/types/views/product-question-view.ts delete mode 100644 shared/types/views/product-spec-group-view.ts diff --git a/server/mappers/documentMapper.test.ts b/server/mappers/documentMapper.test.ts index d62e449..e5cd0ff 100644 --- a/server/mappers/documentMapper.test.ts +++ b/server/mappers/documentMapper.test.ts @@ -1,9 +1,5 @@ import { describe, test, expect } from 'vitest'; -import { - toProductDocumentView, - toDocumentListView, - toDocumentTypeView, -} from './documentMapper'; +import { toDocumentListView, toDocumentTypeView } from './documentMapper'; /** * 单元测试: toDocumentTypeView @@ -46,63 +42,6 @@ describe('toDocumentTypeView', () => { }); }); }); -/** - * 单元测试: toProductDocumentView - */ -describe('toProductDocumentView', () => { - const baseData: ProductDocument = { - id: 1, - file: { - id: 'rand-om__-uuid-1234', - filename_download: 'document.pdf', - filesize: 2048, - }, - translations: [{ id: 10, title: 'Document Title' }], - }; - - test('convert raw data with fileMeta to ProductDocumentView correctly', () => { - const rawData: ProductDocument = { ...baseData }; - expect(toProductDocumentView(rawData)).toEqual({ - id: '1', - fileId: 'rand-om__-uuid-1234', - filename: 'document.pdf', - title: 'Document Title', - url: '/api/assets/rand-om__-uuid-1234', - size: 2048, - }); - }); - - test('convert raw data with fileId', () => { - const rawData: ProductDocument = { - ...baseData, - file: 'rand-om__-uuid-1234', - }; - - expect(toProductDocumentView(rawData)).toEqual({ - id: '1', - fileId: '', - filename: '', - title: 'Document Title', - url: '/api/assets/', - size: 0, - }); - }); - - test('convert raw data with missing translations', () => { - const rawData: ProductDocument = { - ...baseData, - translations: [], - }; - expect(toProductDocumentView(rawData)).toEqual({ - id: '1', - fileId: 'rand-om__-uuid-1234', - filename: 'document.pdf', - title: '', - url: '/api/assets/rand-om__-uuid-1234', - size: 2048, - }); - }); -}); /** * 单元测试: toDocumentListView diff --git a/server/mappers/documentMapper.ts b/server/mappers/documentMapper.ts index 42ee54d..6ebaa96 100644 --- a/server/mappers/documentMapper.ts +++ b/server/mappers/documentMapper.ts @@ -26,34 +26,6 @@ export function toDocumentTypeView( }; } -/** - * 将 Directus 返回的 Document 数据转换为 ProductDocumentView 视图模型 - * - * @param raw: 原始的 Document 数据 - * @returns 转换后的 ProductDocumentView 对象 - * - * @example - * const view = toProductDocumentView(rawDocument); - */ -export function toProductDocumentView( - raw: ProductDocument -): ProductDocumentView { - const trans = raw.translations?.[0]; - const file = isObject(raw.file) ? raw.file : undefined; - const fileId = file?.id ?? ''; - - const url = `/api/assets/${fileId}`; - - return { - id: raw.id.toString(), - fileId: fileId, - filename: file?.filename_download ?? '', - title: trans?.title ?? '', - url: url, - size: file?.filesize ?? 0, - }; -} - /** * 将 Directus 返回的 Document 数据转换为 DocumentListView 视图模型 * diff --git a/server/mappers/productListMapper.test.ts b/server/mappers/productListMapper.test.ts new file mode 100644 index 0000000..dd69d3d --- /dev/null +++ b/server/mappers/productListMapper.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, test } from 'vitest'; +import { toProductTypeView, toProductListView } from './productListMapper'; + +/** + * 单元测试: toProductTypeView + */ +describe('toProductTypeView', () => { + const baseData: ProductType = { + id: 1, + translations: [ + { + id: 1, + name: 'Type Name', + }, + ], + sort: 5, + }; + + test('convert raw data to ProductTypeView correctly', () => { + const rawData: ProductType = { ...baseData }; + + expect(toProductTypeView(rawData)).toEqual({ + id: '1', + name: 'Type Name', + sort: 5, + }); + }); + + test('convert raw data with missing translations', () => { + const rawData: ProductType = { + ...baseData, + translations: [], + }; + + expect(toProductTypeView(rawData)).toEqual({ + id: '1', + name: '', + sort: 5, + }); + }); + + test('convert raw data with missing sort', () => { + const rawData: ProductType = { + ...baseData, + sort: undefined, + }; + + expect(toProductTypeView(rawData)).toEqual({ + id: '1', + name: 'Type Name', + sort: 999, + }); + }); + + test('convert null input to default value', () => { + const rawData: ProductType | string | null = null; + + expect(toProductTypeView(rawData)).toEqual({ + id: '-1', + name: '', + sort: 999, + }); + }); +}); + +/** + * 单元测试: toProductListView + */ +describe('toProductListView', () => { + test('convert raw data to ProductListView correctly', () => { + const rawData: Product = { + id: 1, + translations: [ + { id: 10, name: 'Product Name', summary: 'Product Summary' }, + ], + cover: { + id: 'cover-file-uuid-1234', + }, + product_type: { + id: 1, + translations: [{ id: 20, name: 'Type Name' }], + sort: 1, + }, + }; + + expect(toProductListView(rawData)).toEqual({ + id: '1', + name: 'Product Name', + summary: 'Product Summary', + cover: 'cover-file-uuid-1234', + product_type: { + id: '1', + name: 'Type Name', + sort: 1, + }, + }); + }); + + test('convert raw data with missing translations', () => { + const rawData: Product = { + id: 1, + translations: [], + cover: { + id: 'cover-file-uuid-1234', + }, + product_type: { + id: 20, + translations: [], + sort: 1, + }, + }; + + expect(toProductListView(rawData)).toEqual({ + id: '1', + name: '', + summary: '', + cover: 'cover-file-uuid-1234', + product_type: { + id: '20', + name: '', + sort: 1, + }, + }); + }); +}); diff --git a/server/mappers/productListMapper.ts b/server/mappers/productListMapper.ts new file mode 100644 index 0000000..7cf2844 --- /dev/null +++ b/server/mappers/productListMapper.ts @@ -0,0 +1,54 @@ +import { isObject } from '../../server/utils/object'; + +/** + * 将Directus返回的ProductType数据转换为ProductTypeView视图模型 + * + * @param raw: 原始的ProductType数据 + * @returns 转换后的ProductTypeView对象 + * + * @example + * const view = toProductTypeView(rawProductType); + */ +export function toProductTypeView( + raw: ProductType | string | null +): ProductTypeView { + if (typeof raw === 'string' || raw === null) { + return { + id: '-1', + name: '', + sort: 999, + } satisfies ProductTypeView; + } + const trans = raw.translations?.[0] ?? { name: '' }; + + return { + id: raw.id.toString(), + name: trans.name, + sort: raw?.sort ?? 999, + } satisfies ProductTypeView; +} + +/** + * 将 Directus返回的 Product 数据转换为 ProductListView 视图模型 + * + * @param raw: 原始的 Product 数据 + * @returns 转换后的 ProductListView 对象 + * + * @example + * const view = toProductListView(rawProduct); + */ +export function toProductListView(raw: Product): ProductListView { + const trans = raw.translations?.[0]; + + const type = toProductTypeView(raw.product_type ?? null); + + const cover = isObject(raw.cover) ? raw.cover.id : ''; + + return { + id: raw.id.toString(), + product_type: type, + name: trans?.name ?? '', + summary: trans?.summary ?? '', + cover: cover, + } satisfies ProductListView; +} diff --git a/server/mappers/productMapper.test.ts b/server/mappers/productMapper.test.ts index 5163aae..3afad1a 100644 --- a/server/mappers/productMapper.test.ts +++ b/server/mappers/productMapper.test.ts @@ -1,69 +1,111 @@ import { describe, test, expect } from 'vitest'; import { - toProductListView, + toProductImageView, toProductSpecView, toProductSpecGroupView, + toProductQuestionView, + toProductDocumentView, toProductView, } from './productMapper'; /** - * 单元测试: toProductListView + * 单元测试: toProductImageView */ -describe('toProductListView', () => { - test('convert raw data to ProductListView correctly', () => { - const rawData: Product = { +describe('toProductImageView', () => { + const baseData: Array = [ + { id: 1, - translations: [ - { id: 10, name: 'Product Name', summary: 'Product Summary' }, - ], - cover: { - id: 'cover-file-uuid-1234', - }, - product_type: { + product_images_id: { id: 1, - translations: [{ id: 20, name: 'Type Name' }], - sort: 1, + image: { + id: 'rand-om__-uuid-1234', + }, + translations: [ + { + id: 1, + caption: 'Image Caption', + }, + ], }, - }; + }, + { + id: 2, + product_images_id: { + id: 2, + image: { + id: 'rand-om__-uuid-5678', + }, + translations: [ + { + id: 2, + caption: 'Image Caption 2', + }, + ], + }, + }, + ]; - expect(toProductListView(rawData)).toEqual({ - id: '1', - name: 'Product Name', - summary: 'Product Summary', - cover: 'cover-file-uuid-1234', - product_type: { + test('convert raw data to ProductImageView correctly', () => { + const rawData: Array = [...baseData]; + + expect(toProductImageView(rawData)).toEqual([ + { id: '1', - name: 'Type Name', - sort: 1, + image: 'rand-om__-uuid-1234', + caption: 'Image Caption', }, - }); + { + id: '2', + image: 'rand-om__-uuid-5678', + caption: 'Image Caption 2', + }, + ]); }); test('convert raw data with missing translations', () => { - const rawData: Product = { - id: 1, - translations: [], - cover: { - id: 'cover-file-uuid-1234', + const rawData: Array = [ + { + id: 1, + product_images_id: { + id: 1, + image: { + id: 'rand-om__-uuid-1234', + }, + translations: [], + }, }, - product_type: { - id: 20, - translations: [], - sort: 1, - }, - }; + ]; - expect(toProductListView(rawData)).toEqual({ - id: '1', - name: '', - summary: '', - cover: 'cover-file-uuid-1234', - product_type: { - id: '20', - name: '', - sort: 1, + expect(toProductImageView(rawData)).toEqual([ + { + id: '1', + image: 'rand-om__-uuid-1234', + caption: '', }, - }); + ]); + }); + + test('convert empty array to empty ProductImageView array', () => { + const rawData: Array = []; + + expect(toProductImageView(rawData)).toEqual([]); + }); + + test('convert input with wrong type to default value', () => { + const rawData: string[] = ['1', '2']; + + expect(toProductImageView(rawData)).toEqual([ + { + id: '1', + image: '', + caption: '', + }, + { + id: '2', + image: '', + caption: '', + }, + ]); }); }); @@ -71,30 +113,78 @@ describe('toProductListView', () => { * 单元测试: toProductSpecView */ describe('toProductSpecView', () => { - test('convert raw data to ProductSpecView correctly', () => { - const rawData: ProductSpec = { + const baseData: ProductSpec[] = [ + { id: 1, - translations: [{ id: 1, key: 'Key', value: 'Value' }], - }; + translations: [ + { + id: 1, + key: 'key 1', + value: 'value 1', + }, + ], + }, + { + id: 2, + translations: [ + { + id: 2, + key: 'key 2', + value: 'value 2', + }, + ], + }, + ]; + test('convert raw data to ProductSpecView correctly', () => { + const rawData: ProductSpec[] = [...baseData]; - expect(toProductSpecView(rawData)).toEqual({ - id: '1', - key: 'Key', - value: 'Value', - }); + expect(toProductSpecView(rawData)).toEqual([ + { + id: '1', + key: 'key 1', + value: 'value 1', + }, + { + id: '2', + key: 'key 2', + value: 'value 2', + }, + ]); }); test('convert raw data with missing translations', () => { - const rawData: ProductSpec = { - id: 1, - translations: [], - }; + const rawData: ProductSpec[] = [{ id: 1, translations: [] }]; - expect(toProductSpecView(rawData)).toEqual({ - id: '1', - key: '', - value: '', - }); + expect(toProductSpecView(rawData)).toEqual([ + { + id: '1', + key: '', + value: '', + }, + ]); + }); + + test('convert empty input to empty array', () => { + const rawData: ProductSpec[] = []; + + expect(toProductSpecView(rawData)).toEqual([]); + }); + + test('convert input with wrong type to default value', () => { + const rawData: string[] = ['1', '2']; + + expect(toProductSpecView(rawData)).toEqual([ + { + id: '1', + key: '', + value: '', + }, + { + id: '2', + key: '', + value: '', + }, + ]); }); }); @@ -103,37 +193,237 @@ describe('toProductSpecView', () => { */ describe('toProductSpecGroupView', () => { test('convert raw data to ProductSpecGroupView correctly', () => { - const rawData: ProductSpecGroup = { - id: 1, - translations: [{ id: 1, name: 'Group Name' }], - specs: [ - { id: 1, translations: [{ id: 1, key: 'Key1', value: 'Value1' }] }, - { id: 2, translations: [{ id: 2, key: 'Key2', value: 'Value2' }] }, - ], - }; + const rawData: ProductSpecGroup[] = [ + { + id: 1, + translations: [{ id: 1, name: 'Group Name' }], + specs: [ + { id: 1, translations: [{ id: 1, key: 'Key1', value: 'Value1' }] }, + { id: 2, translations: [{ id: 2, key: 'Key2', value: 'Value2' }] }, + ], + }, + ]; - expect(toProductSpecGroupView(rawData)).toEqual({ - id: '1', - name: 'Group Name', - specs: [ - { id: '1', key: 'Key1', value: 'Value1' }, - { id: '2', key: 'Key2', value: 'Value2' }, - ], - }); + expect(toProductSpecGroupView(rawData)).toEqual([ + { + id: '1', + name: 'Group Name', + specs: [ + { id: '1', key: 'Key1', value: 'Value1' }, + { id: '2', key: 'Key2', value: 'Value2' }, + ], + }, + ]); }); test('convert raw data with missing translations', () => { - const rawData: ProductSpecGroup = { - id: 1, - translations: [], - specs: [], - }; + const rawData: ProductSpecGroup[] = [ + { + id: 1, + translations: [], + specs: [], + }, + ]; - expect(toProductSpecGroupView(rawData)).toEqual({ - id: '1', - name: '', - specs: [], - }); + expect(toProductSpecGroupView(rawData)).toEqual([ + { + id: '1', + name: '', + specs: [], + }, + ]); + }); + + test('convert input with wrong type to default value', () => { + const rawData: string[] = ['1', '2']; + + expect(toProductSpecGroupView(rawData)).toEqual([ + { + id: '1', + name: '', + specs: [], + }, + { + id: '2', + name: '', + specs: [], + }, + ]); + }); + + test('convert empty ar ray to empty ProductSpecGroupView array', () => { + const rawData: ProductSpec[] = []; + + expect(toProductSpecGroupView(rawData)).toEqual([]); + }); +}); + +/** + * 单元测试: toProductQuestionView + */ +describe('toProductQuestionView', () => { + test('convert raw data to ProductQuestionView correctly', () => { + const rawData: ProductsQuestion[] = [ + { + id: 1, + questions_id: { + id: 1, + translations: [ + { + id: 1, + title: 'Question Title', + content: 'Question Content', + }, + ], + }, + }, + ]; + + expect(toProductQuestionView(rawData)).toEqual([ + { + id: '1', + title: 'Question Title', + content: 'Question Content', + }, + ]); + }); + + test('convert raw data with missing translations', () => { + const rawData: ProductsQuestion[] = [ + { + id: 1, + questions_id: { + id: 1, + translations: [], + }, + }, + ]; + + expect(toProductQuestionView(rawData)).toEqual([ + { + id: '1', + title: '', + content: '', + }, + ]); + }); + + test('convert empty array to empty ProductQuestionView array', () => { + const rawData: ProductsQuestion[] = []; + + expect(toProductQuestionView(rawData)).toEqual([]); + }); + + test('convert input with wrong type to default value', () => { + const rawData: string[] = ['1', '2']; + + expect(toProductQuestionView(rawData)).toEqual([ + { + id: '1', + title: '', + content: '', + }, + { + id: '2', + title: '', + content: '', + }, + ]); + }); +}); + +/** + * 单元测试: toProductDocumentView + */ +describe('toProductDocumentView', () => { + test('convert raw data to ProductDocumentView correctly', () => { + const rawData: ProductsProductDocument[] = [ + { + id: 1, + product_documents_id: { + id: 1, + file: { + id: 'rand-om__-uuid-1234', + filesize: 1000, + filename_download: 'doc1.pdf', + }, + translations: [ + { + id: 1, + title: 'Document Title 1', + }, + ], + }, + }, + ]; + + expect(toProductDocumentView(rawData)).toEqual([ + { + id: '1', + fileId: 'rand-om__-uuid-1234', + filename: 'doc1.pdf', + title: 'Document Title 1', + size: 1000, + url: '/api/assets/rand-om__-uuid-1234', + }, + ]); + }); + + test('convert raw data with missing translations', () => { + const rawData: ProductsProductDocument[] = [ + { + id: 1, + product_documents_id: { + id: 1, + file: { + id: 'rand-om__-uuid-1234', + filesize: 1000, + filename_download: 'doc1.pdf', + }, + translations: [], + }, + }, + ]; + + expect(toProductDocumentView(rawData)).toEqual([ + { + id: '1', + fileId: 'rand-om__-uuid-1234', + filename: 'doc1.pdf', + title: '', + size: 1000, + url: '/api/assets/rand-om__-uuid-1234', + }, + ]); + }); + + test('convert empty array to empty ProductDocumentView array', () => { + const rawData: ProductsProductDocument[] = []; + + expect(toProductDocumentView(rawData)).toEqual([]); + }); + + test('convert input with wrong type to default value', () => { + const rawData: string[] = ['1', '2']; + + expect(toProductDocumentView(rawData)).toEqual([ + { + id: '1', + fileId: '', + filename: '', + title: '', + size: 0, + url: '', + }, + { + id: '2', + fileId: '', + filename: '', + title: '', + size: 0, + url: '', + }, + ]); }); }); @@ -159,7 +449,6 @@ describe('toProductView', () => { name: 'Product Name', summary: 'Product Summary', description: 'Product Description', - product_type: '', images: [], documents: [], faqs: [], @@ -178,7 +467,6 @@ describe('toProductView', () => { name: '', summary: '', description: '', - product_type: '', images: [], documents: [], faqs: [], diff --git a/server/mappers/productMapper.ts b/server/mappers/productMapper.ts index 185e355..2896d96 100644 --- a/server/mappers/productMapper.ts +++ b/server/mappers/productMapper.ts @@ -1,55 +1,41 @@ -import { toProductQuestionView } from './questionMapper'; -import { toProductDocumentView } from './documentMapper'; import { isObject } from '../../server/utils/object'; /** - * 将Directus返回的ProductType数据转换为ProductTypeView视图模型 + * 将 Directus 返回的 ProductImage 数据转换为 ProductImageView 视图模型 * - * @param raw: 原始的ProductType数据 - * @returns 转换后的ProductTypeView对象 + * @param raw: 原始的 ProductsProductImage 数据 + * @returns 转换后的 ProductImageView 对象 * * @example - * const view = toProductTypeView(rawProductType); + * const view = toProductImageView(rawProductImage); */ -export function toProductTypeView(raw: ProductType): ProductTypeView { - const trans = raw.translations?.[0] ?? { name: '' }; +export function toProductImageView( + raw: (ProductsProductImage | string)[] +): ProductImageView[] { + return (raw ?? []).map((item) => { + if (!isObject(item)) + return { + id: item, + image: '', + caption: '', + } satisfies ProductImageView; - return { - id: raw.id.toString(), - name: trans.name, - sort: raw?.sort ?? 999, - }; -} + const image = item.product_images_id; + if (!isObject(image)) + return { + id: item.id.toString(), + image: '', + caption: '', + } satisfies ProductImageView; -/** - * 将 Directus返回的 Product 数据转换为 ProductListView 视图模型 - * - * @param raw: 原始的 Product 数据 - * @returns 转换后的 ProductListView 对象 - * - * @example - * const view = toProductListView(rawProduct); - */ -export function toProductListView(raw: Product): ProductListView { - const trans = raw.translations?.[0]; + const trans = image.translations?.[0]; - const type = isObject(raw.product_type) - ? toProductTypeView(raw.product_type) - : ({ - id: '', - name: '', - sort: 999, - } satisfies ProductTypeView); - - const cover = isObject(raw.cover) ? raw.cover.id : ''; - - return { - id: raw.id.toString(), - product_type: type, - name: trans?.name ?? '', - summary: trans?.summary ?? '', - cover: cover, - }; + return { + id: item.id.toString(), + image: isObject(image.image) ? image.image.id : '', + caption: trans?.caption || '', + } satisfies ProductImageView; + }); } /** @@ -62,19 +48,26 @@ export function toProductListView(raw: Product): ProductListView { * const view = toProductSpecGroupView(rawSpecGroup); */ export function toProductSpecGroupView( - raw: ProductSpecGroup -): ProductSpecGroupView { - const trans = raw.translations?.[0]; + raw: (ProductSpecGroup | string)[] +): ProductSpecGroupView[] { + return (raw ?? []).map((item) => { + if (!isObject(item)) { + return { + id: item, + name: '', + specs: [], + } satisfies ProductSpecGroupView; + } + const trans = item.translations?.[0]; - const specs = raw.specs ?? []; + const specs = toProductSpecView(item?.specs ?? []); - return { - id: raw.id.toString(), - name: trans?.name ?? '', - specs: specs - .filter(isObject) - .map((item) => toProductSpecView(item)), - }; + return { + id: item.id.toString(), + name: trans?.name ?? '', + specs: specs, + } satisfies ProductSpecGroupView; + }); } /** @@ -86,14 +79,120 @@ export function toProductSpecGroupView( * @example * const view = toProductSpecView(rawSpecGroup); */ -export function toProductSpecView(raw: ProductSpec): ProductSpecView { - const trans = raw.translations?.[0]; +export function toProductSpecView( + raw: (ProductSpec | string)[] +): ProductSpecView[] { + return (raw ?? []).map((item) => { + if (!isObject(item)) { + return { + id: item, + key: '', + value: '', + } satisfies ProductSpecView; + } + const trans = item.translations?.[0]; - return { - id: raw.id.toString(), - key: trans?.key ?? '', - value: trans?.value ?? '', - }; + return { + id: item.id.toString(), + key: trans?.key ?? '', + value: trans?.value ?? '', + } satisfies ProductSpecView; + }); +} + +/** + * 将 Directus 返回的 ProductQuestion 数据转换为 ProductQuestionView 视图模型 + * + * @param raw: 原始的ProductQuestion 数据 + * @returns 转换后的 ProductQuestionView 对象 + * + * @example + * const view = toProductQuestionView(rawQuestion); + */ +export function toProductQuestionView( + raw: (ProductsQuestion | string)[] +): ProductQuestionView[] { + return (raw ?? []).map((item) => { + if (!isObject(item)) { + return { + id: item, + title: '', + content: '', + } satisfies ProductQuestionView; + } + + const question = item.questions_id; + if (!isObject(question)) { + return { + id: item.id.toString(), + title: '', + content: '', + } satisfies ProductQuestionView; + } + + const trans = question.translations?.[0]; + + return { + id: item.id.toString(), + title: trans?.title ?? '', + content: trans?.content ?? '', + } satisfies ProductQuestionView; + }); +} + +/** + * 将 Directus 返回的 ProductDocument 数据转换为 ProductDocumentView 视图模型 + * + * @param raw: 原始的ProductDocument 数据 + * @returns 转换后的 ProductDocumentView 对象 + * + * @example + * const view = toProductDocumentView(rawDocument); + */ +export function toProductDocumentView( + raw: (ProductsProductDocument | string)[] +): ProductDocumentView[] { + return (raw ?? []).map((item) => { + if (!isObject(item)) { + return { + id: item, + fileId: '', + filename: '', + size: 0, + title: '', + url: '', + } satisfies ProductDocumentView; + } + + const document = item.product_documents_id; + if (!isObject(document)) { + return { + id: item.id.toString(), + fileId: '', + filename: '', + size: 0, + title: '', + url: '', + } satisfies ProductDocumentView; + } + + const file = isObject(document.file) + ? document.file + : undefined; + + const url = file ? `/api/assets/${file.id}` : ''; + + const trans = document.translations?.[0]; + + return { + id: item.id.toString(), + fileId: file?.id ?? '', + filename: file?.filename_download ?? '', + size: file?.filesize ?? 0, + title: trans?.title ?? '', + url: url, + } satisfies ProductDocumentView; + }); } /** @@ -108,42 +207,16 @@ export function toProductSpecView(raw: ProductSpec): ProductSpecView { export function toProductView(raw: Product): ProductView { const trans = raw.translations?.[0]; - const images = (raw.images ?? []) - .filter(isObject) - .map((item) => item.product_images_id) - .filter(isObject) - .map((item) => { - const image = isObject(item.image) ? item.image.id : ''; - return { - id: item.id.toString(), - image: image, - caption: item.translations?.[0]?.caption || '', - }; - }); + const images = toProductImageView(raw.images ?? []); - const type = isObject(raw.product_type) - ? (raw.product_type.translations?.[0]?.name ?? '') - : ''; + const specs = toProductSpecGroupView(raw.specs ?? []); - const specs = (raw.specs ?? []) - .filter(isObject) - .map((item) => toProductSpecGroupView(item)); + const faqs = toProductQuestionView(raw.faqs ?? []); - const faqs = (raw.faqs ?? []) - .filter(isObject) - .map((item) => item.questions_id) - .filter(isObject) - .map((item) => toProductQuestionView(item)); - - const documents = (raw.documents ?? []) - .filter(isObject) - .map((item) => item.product_documents_id) - .filter(isObject) - .map((item) => toProductDocumentView(item)); + const documents = toProductDocumentView(raw.documents ?? []); return { id: raw.id.toString(), - product_type: type, name: trans?.name ?? '', summary: trans?.summary ?? '', images: images, @@ -151,5 +224,5 @@ export function toProductView(raw: Product): ProductView { specs: specs, faqs: faqs, documents: documents, - }; + } satisfies ProductView; } diff --git a/server/mappers/questionMapper.test.ts b/server/mappers/questionMapper.test.ts index a043db1..df2cdbb 100644 --- a/server/mappers/questionMapper.test.ts +++ b/server/mappers/questionMapper.test.ts @@ -1,9 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { - toProductQuestionView, - toQuestionListView, - toQuestionTypeView, -} from './questionMapper'; +import { toQuestionListView, toQuestionTypeView } from './questionMapper'; /** * 单元测试: toQuestionTypeView @@ -47,42 +43,6 @@ describe('toQuestionTypeView', () => { }); }); -/** - * 单元测试: toProductQuestionView - */ -describe('toProductQuestionView', () => { - const baseData: Question = { - id: 1, - translations: [ - { id: 1, title: 'Question Title', content: 'Question Answer' }, - ], - }; - test('convert raw data to ProductQuestionView correctly', () => { - const rawData: Question = { - ...baseData, - }; - - expect(toProductQuestionView(rawData)).toEqual({ - id: '1', - title: 'Question Title', - content: 'Question Answer', - }); - }); - - test('convert raw data with missing translations', () => { - const rawData: Question = { - ...baseData, - translations: [], - }; - - expect(toProductQuestionView(rawData)).toEqual({ - id: '1', - title: '', - content: '', - }); - }); -}); - /** * 单元测试: toQuestionListView */ diff --git a/server/mappers/questionMapper.ts b/server/mappers/questionMapper.ts index 041c21e..88781ac 100644 --- a/server/mappers/questionMapper.ts +++ b/server/mappers/questionMapper.ts @@ -26,25 +26,6 @@ export function toQuestionTypeView( }; } -/** - * 将 Directus 返回的 Question 数据转换为 ProductQuestionView 视图模型 - * - * @param raw: 原始的 Question 数据 - * @returns 转换后的 ProductQuestionView 对象 - * - * @example - * const view = toProductQuestionView(rawQuestion); - */ -export function toProductQuestionView(raw: Question): ProductQuestionView { - const trans = raw.translations?.[0]; - - return { - id: raw.id.toString(), - title: trans?.title ?? '', - content: trans?.content ?? '', - }; -} - /** * 将 Directus 返回的 Question 数据转换为 QuestionListView 视图模型 * diff --git a/server/services/cms/productService.ts b/server/services/cms/productService.ts index c45d64d..2d34284 100644 --- a/server/services/cms/productService.ts +++ b/server/services/cms/productService.ts @@ -1,7 +1,5 @@ -import { - toProductView, - toProductListView, -} from '~~/server/mappers/productMapper'; +import { toProductView } from '~~/server/mappers/productMapper'; +import { toProductListView } from '~~/server/mappers/productListMapper'; export const productService = { async getProductList(locale: string) { diff --git a/shared/types/views/index.ts b/shared/types/views/index.ts index c45e40d..b1d2a8c 100644 --- a/shared/types/views/index.ts +++ b/shared/types/views/index.ts @@ -2,9 +2,6 @@ export * from './solution-view'; export * from './solution-list-view'; export * from './product-view'; export * from './product-list-view'; -export * from './product-spec-group-view'; -export * from './product-document-view'; -export * from './product-question-view'; export * from './document-list-view'; export * from './question-list-view'; export * from './company-profile-view'; diff --git a/shared/types/views/product-document-view.ts b/shared/types/views/product-document-view.ts deleted file mode 100644 index fd0747e..0000000 --- a/shared/types/views/product-document-view.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 文档视图模型 - * 用于文档列表渲染的数据结构 - */ -export interface ProductDocumentView { - /** 唯一标识符 **/ - id: string; - - /** 文件UUID **/ - fileId: string; - - /** 文件名 **/ - filename: string; - - /** 文档标题 **/ - title: string; - - /** 文档大小 **/ - size: number; - - /** 文档链接 **/ - url: string; -} diff --git a/shared/types/views/product-question-view.ts b/shared/types/views/product-question-view.ts deleted file mode 100644 index 331b5b8..0000000 --- a/shared/types/views/product-question-view.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 常见问题视图模型 - * 用于产品页常见问题渲染的数据结构 - */ -export interface ProductQuestionView { - /** 唯一标识符 **/ - id: string; - - /** 问题标题 **/ - title: string; - - /** 问题内容 **/ - content: string; -} diff --git a/shared/types/views/product-spec-group-view.ts b/shared/types/views/product-spec-group-view.ts deleted file mode 100644 index 203c8c3..0000000 --- a/shared/types/views/product-spec-group-view.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 产品规格模型 - * 用于产品规格渲染的数据结构 - */ -export interface ProductSpecView { - /** 唯一标识符 **/ - id: string; - - /** 规格名称 **/ - key: string; - - /** 规格值 **/ - value: string; -} - -/** - * 产品规格表模型 - * 用于产品规格表渲染的数据结构 - */ -export interface ProductSpecGroupView { - /** 唯一标识符 **/ - id: string; - - /** 规格组名称 **/ - name: string; - - /** 规格组 **/ - specs: ProductSpecView[]; -} diff --git a/shared/types/views/product-view.ts b/shared/types/views/product-view.ts index 01d076c..7407ddc 100644 --- a/shared/types/views/product-view.ts +++ b/shared/types/views/product-view.ts @@ -1,13 +1,82 @@ -import type { ProductSpecGroupView } from './product-spec-group-view'; -import type { ProductQuestionView } from './product-question-view'; -import type { ProductDocumentView } from './product-document-view'; - -interface ImageView { +/** + * 产品图片视图模型 + * 用于产品详情页(/products/[slug])中的产品图片数据结构 + */ +export interface ProductImageView { id: string; image: string; caption: string; } +/** + * 产品规格模型 + * 用于产品规格渲染的数据结构 + */ +export interface ProductSpecView { + /** 唯一标识符 **/ + id: string; + + /** 规格名称 **/ + key: string; + + /** 规格值 **/ + value: string; +} + +/** + * 产品规格表模型 + * 用于产品规格表渲染的数据结构 + */ +export interface ProductSpecGroupView { + /** 唯一标识符 **/ + id: string; + + /** 规格组名称 **/ + name: string; + + /** 规格组 **/ + specs: ProductSpecView[]; +} + +/** + * 常见问题视图模型 + * 用于产品页常见问题渲染的数据结构 + */ +export interface ProductQuestionView { + /** 唯一标识符 **/ + id: string; + + /** 问题标题 **/ + title: string; + + /** 问题内容 **/ + content: string; +} + +/** + * 文档视图模型 + * 用于文档列表渲染的数据结构 + */ +export interface ProductDocumentView { + /** 唯一标识符 **/ + id: string; + + /** 文件UUID **/ + fileId: string; + + /** 文件名 **/ + filename: string; + + /** 文档标题 **/ + title: string; + + /** 文档大小 **/ + size: number; + + /** 文档链接 **/ + url: string; +} + /** * 产品视图模型 * 用于产品详情页(/products/[slug])渲染的数据结构 @@ -22,11 +91,8 @@ export interface ProductView { /** 产品简介 **/ summary: string; - /** 产品类型 **/ - product_type: string; - /** 产品图片 **/ - images: ImageView[]; + images: ProductImageView[]; /** 产品详情 **/ description: string;