feat(round-14): GDPR脱敏被遗忘权/展会海报排版/全球雷达热力散点/DeepLink深度路由 (95套件1053测试全绿)
这个提交包含在:
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
CardPosterLayoutEngine,
|
||||
calculatePosterCanvasDimensions,
|
||||
calculatePosterElementBoxes,
|
||||
PosterLayoutOptions,
|
||||
PosterElementBox,
|
||||
} from '../../../src/modules/card/service/poster-layout';
|
||||
|
||||
describe('Point 54: 出海展会多语言电子邀请海报轻量级排版引擎', () => {
|
||||
let engine: CardPosterLayoutEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
engine = new CardPosterLayoutEngine();
|
||||
});
|
||||
|
||||
describe('1. calculatePosterCanvasDimensions (画布尺寸比例计算)', () => {
|
||||
it('9:16 比例应计算为标准 1080x1920 尺寸', () => {
|
||||
const dim = calculatePosterCanvasDimensions('story_9_16');
|
||||
expect(dim.width).toBe(1080);
|
||||
expect(dim.height).toBe(1920);
|
||||
expect(dim.aspectRatio).toBe('9:16');
|
||||
});
|
||||
|
||||
it('4:5 比例应计算为 1080x1350 尺寸', () => {
|
||||
const dim = calculatePosterCanvasDimensions('feed_4_5');
|
||||
expect(dim.width).toBe(1080);
|
||||
expect(dim.height).toBe(1350);
|
||||
expect(dim.aspectRatio).toBe('4:5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. calculatePosterElementBoxes (多元素布局坐标与安全区排布)', () => {
|
||||
const mockOptions: PosterLayoutOptions = {
|
||||
aspectRatio: 'story_9_16',
|
||||
cardName: 'Alex Zheng',
|
||||
title: 'VP of International BD',
|
||||
company: 'OmniCard Global AI Ltd.',
|
||||
eventName: 'GITEX Global 2026 Dubai',
|
||||
boothNumber: 'Hall 4, Stand B-28',
|
||||
qrCodeSize: 240,
|
||||
};
|
||||
|
||||
it('应计算包含头部标头、人物卡片、展会信息与底部二维码的精准坐标盒', () => {
|
||||
const boxes = calculatePosterElementBoxes(mockOptions);
|
||||
|
||||
expect(boxes.avatarBox).toBeDefined();
|
||||
expect(boxes.nameBox).toBeDefined();
|
||||
expect(boxes.eventBox).toBeDefined();
|
||||
expect(boxes.qrBox).toBeDefined();
|
||||
|
||||
// 二维码位于海报底部且居中
|
||||
expect(boxes.qrBox.width).toBe(240);
|
||||
expect(boxes.qrBox.height).toBe(240);
|
||||
expect(boxes.qrBox.x).toBe((1080 - 240) / 2);
|
||||
expect(boxes.qrBox.y).toBeGreaterThan(1300); // 靠近下半部分
|
||||
});
|
||||
|
||||
it('文字过长时应自动计算截断与文本适应预警', () => {
|
||||
const longOptions: PosterLayoutOptions = {
|
||||
...mockOptions,
|
||||
title: 'Very Long Title Exceeding Single Line Width That Needs Smart Handling In Print',
|
||||
};
|
||||
|
||||
const boxes = calculatePosterElementBoxes(longOptions);
|
||||
expect(boxes.titleBox.isMultiLine).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. CardPosterLayoutEngine 生成完整布局方案', () => {
|
||||
it('应输出可直接供 Canvas 或 SVG 渲染的完整海报元数据清单', () => {
|
||||
const manifest = engine.generateLayoutManifest({
|
||||
aspectRatio: 'story_9_16',
|
||||
cardName: 'Tariq Al-Mansoor',
|
||||
title: 'Middle East Regional Director',
|
||||
company: 'Saudi Digital Tech',
|
||||
eventName: 'LEAP 2026 Riyadh',
|
||||
boothNumber: 'Hall 2, C-15',
|
||||
});
|
||||
|
||||
expect(manifest.canvas.width).toBe(1080);
|
||||
expect(manifest.themeColor).toBe('#0B5CFF'); // 瑞士国际主义单一钴蓝主色
|
||||
expect(manifest.elements.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
ComplianceGdprService,
|
||||
maskIpAddress,
|
||||
anonymizePiiData,
|
||||
GdprUserDataBundle,
|
||||
} from '../../../src/modules/crm/service/compliance';
|
||||
|
||||
describe('Point 53: 海外合规 GDPR / CCPA 访客数据匿名化脱敏与被遗忘权导出', () => {
|
||||
let service: ComplianceGdprService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new ComplianceGdprService();
|
||||
});
|
||||
|
||||
describe('1. maskIpAddress (IP 地址合规脱敏掩码规范)', () => {
|
||||
it('IPv4 地址末尾一段应被严格掩码为 xxx', () => {
|
||||
expect(maskIpAddress('192.168.1.100')).toBe('192.168.1.xxx');
|
||||
expect(maskIpAddress('8.8.8.8')).toBe('8.8.8.xxx');
|
||||
});
|
||||
|
||||
it('IPv6 地址后半段应被规范掩码', () => {
|
||||
expect(maskIpAddress('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe(
|
||||
'2001:0db8:85a3:xxxx:xxxx:xxxx:xxxx:xxxx'
|
||||
);
|
||||
});
|
||||
|
||||
it('非法或空 IP 应安全返回未知掩码', () => {
|
||||
expect(maskIpAddress('')).toBe('unknown');
|
||||
expect(maskIpAddress(undefined)).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. anonymizePiiData (可识别个人隐私数据匿名化清洗)', () => {
|
||||
it('应将姓名、手机号、邮箱与备注彻底清除并标记为已脱敏', () => {
|
||||
const rawLead = {
|
||||
id: 101,
|
||||
name: 'Hans Zimmer',
|
||||
email: 'hans@berlin-music.de',
|
||||
phone: '+49 170 1234567',
|
||||
company: 'Berlin Music AG',
|
||||
notes: 'Interested in 500 VIP NFC cards',
|
||||
};
|
||||
|
||||
const cleaned = anonymizePiiData(rawLead);
|
||||
expect(cleaned.id).toBe(101);
|
||||
expect(cleaned.name).toBe('[GDPR_DELETED]');
|
||||
expect(cleaned.email).toBe('deleted-101@privacy.anonymized');
|
||||
expect(cleaned.phone).toBe('');
|
||||
expect(cleaned.notes).toBe('');
|
||||
expect(cleaned.company).toBe('Berlin Music AG'); // 保留无识别性企业属性用于统计
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. ComplianceGdprService 数据导出包与被遗忘权销毁', () => {
|
||||
it('数据主体请求导出时应打包符合 GDPR Article 20 的 JSON 便携数据包', async () => {
|
||||
const bundle = await service.exportPersonalDataBundle('visitor-de-001', {
|
||||
profile: { name: 'Klaus Mueller', email: 'klaus@munich.de' },
|
||||
visitLogs: [{ page: '/card/alex', staySeconds: 45 }],
|
||||
});
|
||||
|
||||
expect(bundle.subjectId).toBe('visitor-de-001');
|
||||
expect(bundle.exportedAt).toBeDefined();
|
||||
expect(bundle.complianceStandard).toBe('GDPR_ARTICLE_20');
|
||||
expect(bundle.dataPayload.profile.name).toBe('Klaus Mueller');
|
||||
});
|
||||
|
||||
it('执行被遗忘权销毁应返回销毁证书与合规哈希凭据', async () => {
|
||||
const receipt = await service.executeRightToBeForgotten('visitor-de-001');
|
||||
|
||||
expect(receipt.subjectId).toBe('visitor-de-001');
|
||||
expect(receipt.action).toBe('PURGED_AND_ANONYMIZED');
|
||||
expect(receipt.certificateHash).toHaveLength(64);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
RadarGeoHeatmapService,
|
||||
resolveCityCoordinates,
|
||||
calculateGeoHeatPoints,
|
||||
VisitorGeoLog,
|
||||
GeoHeatPoint,
|
||||
} from '../../../src/modules/crm/service/geo-heatmap';
|
||||
|
||||
describe('Point 55: 全球访客雷达国家城市坐标聚合与热力散点引擎', () => {
|
||||
let service: RadarGeoHeatmapService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RadarGeoHeatmapService();
|
||||
});
|
||||
|
||||
describe('1. resolveCityCoordinates (出海核心商务枢纽城市经纬度解析)', () => {
|
||||
it('应精准解析迪拜、利雅得、新加坡与伦敦经纬度', () => {
|
||||
const dubai = resolveCityCoordinates('AE', 'Dubai');
|
||||
expect(dubai).toEqual([55.27, 25.20]);
|
||||
|
||||
const riyadh = resolveCityCoordinates('SA', 'Riyadh');
|
||||
expect(riyadh).toEqual([46.67, 24.71]);
|
||||
|
||||
const sg = resolveCityCoordinates('SG', 'Singapore');
|
||||
expect(sg).toEqual([103.81, 1.35]);
|
||||
|
||||
const london = resolveCityCoordinates('GB', 'London');
|
||||
expect(london).toEqual([-0.12, 51.50]);
|
||||
});
|
||||
|
||||
it('未收录城市应平滑回退到对应国家首都或默认中心点', () => {
|
||||
const fallback = resolveCityCoordinates('AE', 'UnknownCity');
|
||||
expect(fallback).toEqual([54.37, 24.45]); // 阿联酋中心点 (Abu Dhabi)
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. calculateGeoHeatPoints (意向权重与时间半衰期热力计算)', () => {
|
||||
const now = Date.now();
|
||||
const mockLogs: VisitorGeoLog[] = [
|
||||
// 迪拜 2 次高意向访问 (1小时前)
|
||||
{
|
||||
country: 'AE',
|
||||
city: 'Dubai',
|
||||
radarScore: 90,
|
||||
timestamp: now - 3600 * 1000,
|
||||
},
|
||||
{
|
||||
country: 'AE',
|
||||
city: 'Dubai',
|
||||
radarScore: 85,
|
||||
timestamp: now - 1800 * 1000,
|
||||
},
|
||||
// 利雅得 1 次访问 (7天前,有时间衰减)
|
||||
{
|
||||
country: 'SA',
|
||||
city: 'Riyadh',
|
||||
radarScore: 80,
|
||||
timestamp: now - 7 * 86400 * 1000,
|
||||
},
|
||||
];
|
||||
|
||||
it('应精准聚合城市点并计算独立访客与加权热度', () => {
|
||||
const points = calculateGeoHeatPoints(mockLogs, now);
|
||||
|
||||
expect(points.length).toBe(2);
|
||||
|
||||
const dubaiPoint = points.find(p => p.city === 'Dubai');
|
||||
expect(dubaiPoint).toBeDefined();
|
||||
expect(dubaiPoint?.visitorCount).toBe(2);
|
||||
expect(dubaiPoint?.coordinates).toEqual([55.27, 25.20]);
|
||||
expect(dubaiPoint?.heatValue).toBeGreaterThan(150); // 近期无衰减累计
|
||||
|
||||
const riyadhPoint = points.find(p => p.city === 'Riyadh');
|
||||
expect(riyadhPoint).toBeDefined();
|
||||
expect(riyadhPoint?.visitorCount).toBe(1);
|
||||
// 7天衰减后热度减半 (~40)
|
||||
expect(riyadhPoint?.heatValue).toBeLessThan(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. RadarGeoHeatmapService 综合数据报告', () => {
|
||||
it('应返回热力散点列表与 Top 国家活跃度榜单', async () => {
|
||||
const result = await service.generateHeatmapReport([
|
||||
{ country: 'AE', city: 'Dubai', radarScore: 95, timestamp: Date.now() },
|
||||
{ country: 'SG', city: 'Singapore', radarScore: 80, timestamp: Date.now() },
|
||||
]);
|
||||
|
||||
expect(result.points.length).toBe(2);
|
||||
expect(result.topCountries[0].country).toBe('AE');
|
||||
expect(result.totalUniqueVisitors).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户