feat(backend): 增强名片高保真种子数据与 SEO 微官网服务端渲染能力
这个提交包含在:
@@ -1,16 +1,25 @@
|
||||
import { MOCK_CARDS_DATA, MockCardBundle } from '../../../src/modules/card/mock/seed-cards';
|
||||
import { CardPortalService } from '../../../src/modules/card/service/portal';
|
||||
import { CardSeoService } from '../../../src/modules/card/service/seo';
|
||||
import { CardInfoEntity } from '../../../src/modules/card/entity/info';
|
||||
import { CardBentoEntity } from '../../../src/modules/card/entity/bento';
|
||||
|
||||
describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试', () => {
|
||||
it('1. 应包含至少 5 组不同国际行业的高保真名片数据包', () => {
|
||||
it('1. 应包含至少 6 组不同国际顶级行业的高保真名片数据包且 Slug 唯一', () => {
|
||||
expect(Array.isArray(MOCK_CARDS_DATA)).toBe(true);
|
||||
expect(MOCK_CARDS_DATA.length).toBeGreaterThanOrEqual(5);
|
||||
expect(MOCK_CARDS_DATA.length).toBeGreaterThanOrEqual(6);
|
||||
|
||||
const slugs = MOCK_CARDS_DATA.map(item => item.card.slug);
|
||||
const uniqueSlugs = new Set(slugs);
|
||||
expect(uniqueSlugs.size).toBe(MOCK_CARDS_DATA.length); // Slug 必须唯一
|
||||
|
||||
// 必须包含涵盖 6 大赛道的关键人物 slug
|
||||
expect(slugs).toContain('alex-zheng'); // 消费电子硬件
|
||||
expect(slugs).toContain('elena-rostova'); // 云原生与大模型架构
|
||||
expect(slugs).toContain('tariq-al-mansoor'); // 中东智慧基建与绿色能源
|
||||
expect(slugs).toContain('chloe-dubois'); // 巴黎可持续时尚奢品
|
||||
expect(slugs).toContain('kenji-tanaka'); // 跨国汽车制造与智能物流
|
||||
expect(slugs).toContain('marcus-weber'); // 瑞士生物医药与数字健康
|
||||
});
|
||||
|
||||
it('2. 每张名片的基础信息必须完整且配有高分辨率头像与国际化联系方式', () => {
|
||||
@@ -30,14 +39,15 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
expect(card.phone).toMatch(/^\+[1-9]\d{1,14}$/); // E.164 规范
|
||||
expect(card.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
|
||||
expect(card.timezone).toBeTruthy();
|
||||
expect(card.audioPronunciationUrl).toMatch(/^https?:\/\/.+\.(mp3|wav|ogg|m4a)$/i);
|
||||
});
|
||||
});
|
||||
|
||||
it('3. 每张名片必须包含有效的视频 Bento 块,且配有可播放视频流与专属海报封面', () => {
|
||||
it('3. 每张名片必须包含丰富的 Bento Grid 块 (>= 5 个),包含视频与社交/留资互动块', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
const { bentoBlocks } = bundle;
|
||||
expect(Array.isArray(bentoBlocks)).toBe(true);
|
||||
expect(bentoBlocks.length).toBeGreaterThan(0);
|
||||
expect(bentoBlocks.length).toBeGreaterThanOrEqual(5);
|
||||
|
||||
// 必须包含至少一个 video 类型的 Bento 块
|
||||
const videoBlocks = bentoBlocks.filter(b => b.type === 'video');
|
||||
@@ -45,9 +55,7 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
|
||||
videoBlocks.forEach(videoBlock => {
|
||||
expect(videoBlock.title).toBeTruthy();
|
||||
// mediaUrl 作为视频封面海报,必须是合法图片 URL
|
||||
expect(videoBlock.mediaUrl).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
// linkUrl 或 metaJson.videoUrl 必须是可播放的视频源 (mp4/m3u8/webm)
|
||||
let videoUrl = videoBlock.linkUrl;
|
||||
if (typeof videoBlock.metaJson === 'object' && videoBlock.metaJson !== null) {
|
||||
videoUrl = (videoBlock.metaJson as any).videoUrl || videoUrl;
|
||||
@@ -57,10 +65,14 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
}
|
||||
expect(videoUrl).toMatch(/^https?:\/\/.+\.(mp4|webm|m3u8)(\?.*)?$/i);
|
||||
});
|
||||
|
||||
// 必须包含 social 或 form 等交互块
|
||||
const interactiveBlocks = bentoBlocks.filter(b => b.type === 'social' || b.type === 'form');
|
||||
expect(interactiveBlocks.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('4. 每张名片必须包含图文案例与文章,且均配置了真实多媒体封面配图', () => {
|
||||
it('4. 每张名片必须包含图文案例与文章,且配置了真实多媒体封面配图与量化商业指标', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
// 检查案例
|
||||
expect(Array.isArray(bundle.cases)).toBe(true);
|
||||
@@ -68,6 +80,8 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
bundle.cases.forEach(c => {
|
||||
expect(c.title).toBeTruthy();
|
||||
expect(c.client).toBeTruthy();
|
||||
expect(c.metrics).toBeTruthy(); // 必须包含量化商业成果
|
||||
expect(c.description).toBeTruthy();
|
||||
expect(c.coverImage).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
});
|
||||
|
||||
@@ -76,12 +90,14 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
expect(bundle.articles.length).toBeGreaterThanOrEqual(1);
|
||||
bundle.articles.forEach(a => {
|
||||
expect(a.title).toBeTruthy();
|
||||
expect(a.summary).toBeTruthy();
|
||||
expect(a.content).toBeTruthy();
|
||||
expect(a.coverImage).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('5. 微官网服务 (CardPortalService) 能够无缝聚合各名片详情,正确渲染视频与图文块', async () => {
|
||||
it('5. 微官网服务 (CardPortalService) 能够无缝聚合各名片详情,正确渲染多媒体 Bento 块', async () => {
|
||||
const cardInfoRepo: any = {
|
||||
findOne: jest.fn().mockImplementation(async options => {
|
||||
const slug = options?.where?.slug;
|
||||
@@ -118,4 +134,59 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
expect(videoBlock?.mediaUrl).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('6. 服务端渲染微官网预览 (CardSeoService.renderCardHtml) 包含完整的 Bento Grid 结构与多媒体', async () => {
|
||||
const seoService = new CardSeoService();
|
||||
const firstBundle = MOCK_CARDS_DATA[0];
|
||||
|
||||
const portalDetail = {
|
||||
...firstBundle.card,
|
||||
bentoBlocks: firstBundle.bentoBlocks as any[],
|
||||
cases: firstBundle.cases,
|
||||
articles: firstBundle.articles,
|
||||
} as any;
|
||||
|
||||
const html = seoService.renderCardHtml(portalDetail);
|
||||
expect(html).toContain('<!DOCTYPE html>');
|
||||
expect(html).toContain(firstBundle.card.name);
|
||||
// 应包含 Bento Grid 容器类名
|
||||
expect(html).toContain('bento-grid');
|
||||
// 应包含 Hero 与 Video 相关卡片内容
|
||||
expect(html).toContain(firstBundle.bentoBlocks[0].title);
|
||||
expect(html).toContain(firstBundle.cases[0].title);
|
||||
});
|
||||
|
||||
it('7. 控制器公开路由 /mock-cards 能返回 6 套名片高保真预览清单', async () => {
|
||||
// 动态验证 OpenCardPortalController.mockCards
|
||||
const { OpenCardPortalController } = require('../../../src/modules/card/controller/open/portal');
|
||||
const controller = new OpenCardPortalController();
|
||||
controller.ok = jest.fn((data: any) => ({ code: 1000, data }));
|
||||
|
||||
const res = await controller.mockCards();
|
||||
expect(res.code).toBe(1000);
|
||||
expect(Array.isArray(res.data)).toBe(true);
|
||||
expect(res.data.length).toBe(6);
|
||||
expect(res.data[0].slug).toBe('alex-zheng');
|
||||
expect(res.data[5].slug).toBe('marcus-weber');
|
||||
expect(res.data[0].bentoBlocksCount).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
|
||||
it('8. seedCardsToDatabase 函数能够幂等同步数据到仓库', async () => {
|
||||
const { seedCardsToDatabase } = require('../../../src/modules/card/mock/seed-cards');
|
||||
const mockRepo = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
create: jest.fn((data: any) => ({ ...data, id: 1 })),
|
||||
save: jest.fn((data: any) => Promise.resolve({ ...data, id: 1 })),
|
||||
delete: jest.fn().mockResolvedValue({ affected: 1 }),
|
||||
};
|
||||
|
||||
const mockDataSource = {
|
||||
getRepository: jest.fn().mockReturnValue(mockRepo),
|
||||
};
|
||||
|
||||
const insertedCount = await seedCardsToDatabase(mockDataSource);
|
||||
expect(insertedCount).toBe(6);
|
||||
expect(mockRepo.save).toHaveBeenCalled();
|
||||
expect(mockRepo.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户