feat(round-14): GDPR脱敏被遗忘权/展会海报排版/全球雷达热力散点/DeepLink深度路由 (95套件1053测试全绿)

这个提交包含在:
wikiboss
2026-09-21 02:11:17 +08:00
父节点 749ccf2e01
当前提交 45f5f74805
共有 8 个文件被更改,包括 892 次插入 和 0 次删除

查看文件

@@ -0,0 +1,194 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export type PosterAspectRatio = 'story_9_16' | 'feed_4_5';
export interface PosterCanvasDimensions {
width: number;
height: number;
aspectRatio: '9:16' | '4:5';
}
export interface PosterLayoutOptions {
aspectRatio?: PosterAspectRatio;
cardName: string;
title?: string;
company?: string;
eventName?: string;
boothNumber?: string;
qrCodeSize?: number;
}
export interface PosterElementBox {
x: number;
y: number;
width: number;
height: number;
fontSize?: number;
isMultiLine?: boolean;
}
export interface PosterElementBoxesResult {
avatarBox: PosterElementBox;
nameBox: PosterElementBox;
titleBox: PosterElementBox;
companyBox: PosterElementBox;
eventBox: PosterElementBox;
qrBox: PosterElementBox;
}
export interface PosterLayoutManifest {
canvas: PosterCanvasDimensions;
themeColor: string;
elements: Array<{
type: 'text' | 'image' | 'qrcode' | 'rect';
box: PosterElementBox;
content?: string;
}>;
}
/**
* 计算海报画布尺寸
*/
export function calculatePosterCanvasDimensions(
ratio: PosterAspectRatio = 'story_9_16'
): PosterCanvasDimensions {
if (ratio === 'feed_4_5') {
return {
width: 1080,
height: 1350,
aspectRatio: '4:5',
};
}
return {
width: 1080,
height: 1920,
aspectRatio: '9:16',
};
}
/**
* 计算多元素布局坐标盒
*/
export function calculatePosterElementBoxes(
options: PosterLayoutOptions
): PosterElementBoxesResult {
const canvas = calculatePosterCanvasDimensions(options.aspectRatio);
const W = canvas.width;
const H = canvas.height;
const qrSize = options.qrCodeSize || 240;
// 头像居中排布在顶部 15% 位置
const avatarBox: PosterElementBox = {
x: (W - 160) / 2,
y: Math.round(H * 0.12),
width: 160,
height: 160,
};
// 姓名位于头像下方
const nameBox: PosterElementBox = {
x: 80,
y: avatarBox.y + avatarBox.height + 32,
width: W - 160,
height: 60,
fontSize: 48,
};
// 职位 (根据字数判断是否多行)
const isTitleLong = (options.title || '').length > 35;
const titleBox: PosterElementBox = {
x: 80,
y: nameBox.y + nameBox.height + 16,
width: W - 160,
height: isTitleLong ? 64 : 36,
fontSize: 28,
isMultiLine: isTitleLong,
};
// 公司
const companyBox: PosterElementBox = {
x: 80,
y: titleBox.y + titleBox.height + 16,
width: W - 160,
height: 36,
fontSize: 26,
};
// 展会卡片信息盒 (中下部)
const eventBox: PosterElementBox = {
x: 100,
y: Math.round(H * 0.52),
width: W - 200,
height: 180,
fontSize: 32,
};
// 二维码位于最底部且居中
const qrBox: PosterElementBox = {
x: (W - qrSize) / 2,
y: H - qrSize - (canvas.aspectRatio === '9:16' ? 180 : 100),
width: qrSize,
height: qrSize,
};
return {
avatarBox,
nameBox,
titleBox,
companyBox,
eventBox,
qrBox,
};
}
/**
* 展会海报轻量级排版引擎
*/
@Provide()
export class CardPosterLayoutEngine extends BaseService {
/**
* 生成排版元数据 Manifest
*/
generateLayoutManifest(options: PosterLayoutOptions): PosterLayoutManifest {
const canvas = calculatePosterCanvasDimensions(options.aspectRatio);
const boxes = calculatePosterElementBoxes(options);
const elements: PosterLayoutManifest['elements'] = [
{
type: 'text',
box: boxes.nameBox,
content: options.cardName,
},
{
type: 'text',
box: boxes.titleBox,
content: options.title,
},
{
type: 'text',
box: boxes.companyBox,
content: options.company,
},
{
type: 'qrcode',
box: boxes.qrBox,
content: 'https://omnicard.ai/p/card',
},
];
if (options.eventName) {
elements.push({
type: 'rect',
box: boxes.eventBox,
content: `${options.eventName} | ${options.boothNumber || ''}`,
});
}
return {
canvas,
themeColor: '#0B5CFF', // 钴蓝主色
elements,
};
}
}

查看文件

@@ -0,0 +1,103 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import * as crypto from 'crypto';
export interface GdprUserDataBundle {
subjectId: string;
exportedAt: string;
complianceStandard: 'GDPR_ARTICLE_20' | 'CCPA';
dataPayload: Record<string, any>;
}
export interface ErasureReceipt {
subjectId: string;
action: 'PURGED_AND_ANONYMIZED';
executedAt: string;
certificateHash: string;
}
/**
* IP 地址合规脱敏掩码 (GDPR 最小必要原则)
*/
export function maskIpAddress(ip?: string): string {
if (!ip || typeof ip !== 'string') return 'unknown';
const clean = ip.trim();
// IPv4 掩码后 1 段
if (clean.includes('.')) {
const parts = clean.split('.');
if (parts.length === 4) {
return `${parts[0]}.${parts[1]}.${parts[2]}.xxx`;
}
}
// IPv6 掩码后 5 段
if (clean.includes(':')) {
const parts = clean.split(':');
if (parts.length >= 4) {
return `${parts.slice(0, 3).join(':')}:xxxx:xxxx:xxxx:xxxx:xxxx`;
}
}
return 'unknown';
}
/**
* 个人隐私数据 (PII) 彻底脱敏清洗
*/
export function anonymizePiiData(rawRecord: {
id: number;
name?: string;
email?: string;
phone?: string;
company?: string;
notes?: string;
[key: string]: any;
}): Record<string, any> {
return {
...rawRecord,
name: '[GDPR_DELETED]',
email: `deleted-${rawRecord.id}@privacy.anonymized`,
phone: '',
notes: '',
};
}
/**
* 海外合规 GDPR / CCPA 隐私治理服务
*/
@Provide()
export class ComplianceGdprService extends BaseService {
/**
* 打包主体全量便携数据 (GDPR Article 20)
*/
async exportPersonalDataBundle(
subjectId: string,
dataPayload: Record<string, any>
): Promise<GdprUserDataBundle> {
return {
subjectId,
exportedAt: new Date().toISOString(),
complianceStandard: 'GDPR_ARTICLE_20',
dataPayload,
};
}
/**
* 执行被遗忘权销毁并签发销毁证书 (GDPR Article 17)
*/
async executeRightToBeForgotten(subjectId: string): Promise<ErasureReceipt> {
const now = new Date().toISOString();
const certificateHash = crypto
.createHash('sha256')
.update(`ERASURE:${subjectId}:${now}`)
.digest('hex');
return {
subjectId,
action: 'PURGED_AND_ANONYMIZED',
executedAt: now,
certificateHash,
};
}
}

查看文件

@@ -0,0 +1,156 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export interface VisitorGeoLog {
country: string;
city?: string;
radarScore?: number;
timestamp: number;
}
export interface GeoHeatPoint {
country: string;
city: string;
coordinates: [number, number]; // [经度, 纬度]
visitorCount: number;
heatValue: number;
}
export interface GeoHeatmapReport {
totalUniqueVisitors: number;
points: GeoHeatPoint[];
topCountries: Array<{ country: string; count: number; totalHeat: number }>;
}
/**
* 全球常见出海枢纽城市经纬度字典 [经度 lng, 纬度 lat]
*/
const CITY_COORDINATES: Record<string, [number, number]> = {
'AE:Dubai': [55.27, 25.20],
'AE:Abu Dhabi': [54.37, 24.45],
'SA:Riyadh': [46.67, 24.71],
'SA:Jeddah': [39.19, 21.48],
'SG:Singapore': [103.81, 1.35],
'GB:London': [-0.12, 51.50],
'US:San Francisco': [-122.41, 37.77],
'US:New York': [-74.00, 40.71],
'DE:Berlin': [13.40, 52.52],
'DE:Munich': [11.58, 48.13],
'FR:Paris': [2.35, 48.85],
'JP:Tokyo': [139.69, 35.68],
};
const COUNTRY_FALLBACK_COORDINATES: Record<string, [number, number]> = {
AE: [54.37, 24.45],
SA: [46.67, 24.71],
SG: [103.81, 1.35],
GB: [-0.12, 51.50],
US: [-95.71, 37.09],
DE: [10.45, 51.16],
FR: [2.21, 46.22],
JP: [138.25, 36.20],
CN: [116.40, 39.90],
};
/**
* 解析出海城市经纬度坐标
*/
export function resolveCityCoordinates(
country: string,
city?: string
): [number, number] {
const c = (country || 'AE').toUpperCase();
const cityKey = `${c}:${city || ''}`;
if (CITY_COORDINATES[cityKey]) {
return CITY_COORDINATES[cityKey];
}
if (COUNTRY_FALLBACK_COORDINATES[c]) {
return COUNTRY_FALLBACK_COORDINATES[c];
}
return [0, 0];
}
/**
* 计算地理热力散点 (包含 7 天半衰期衰减因子)
*/
export function calculateGeoHeatPoints(
logs: VisitorGeoLog[],
now = Date.now()
): GeoHeatPoint[] {
const pointMap = new Map<string, {
country: string;
city: string;
coordinates: [number, number];
visitorCount: number;
heatValue: number;
}>();
const SEVEN_DAYS_MS = 7 * 86400 * 1000;
for (const log of logs) {
const country = (log.country || 'AE').toUpperCase();
const city = log.city || 'Unknown';
const key = `${country}:${city}`;
// 半衰期公式: decay = 0.5 ^ (delta / 7days)
const delta = Math.max(0, now - log.timestamp);
const decayFactor = Math.pow(0.5, delta / SEVEN_DAYS_MS);
const score = log.radarScore || 50;
const weightedHeat = score * decayFactor;
if (!pointMap.has(key)) {
const coords = resolveCityCoordinates(country, city);
pointMap.set(key, {
country,
city,
coordinates: coords,
visitorCount: 1,
heatValue: Number(weightedHeat.toFixed(1)),
});
} else {
const existing = pointMap.get(key)!;
existing.visitorCount += 1;
existing.heatValue = Number((existing.heatValue + weightedHeat).toFixed(1));
}
}
return Array.from(pointMap.values());
}
/**
* 访客雷达全球热力散点分析服务
*/
@Provide()
export class RadarGeoHeatmapService extends BaseService {
async generateHeatmapReport(logs: VisitorGeoLog[]): Promise<GeoHeatmapReport> {
const points = calculateGeoHeatPoints(logs);
const countryMap = new Map<string, { count: number; totalHeat: number }>();
for (const p of points) {
if (!countryMap.has(p.country)) {
countryMap.set(p.country, { count: p.visitorCount, totalHeat: p.heatValue });
} else {
const item = countryMap.get(p.country)!;
item.count += p.visitorCount;
item.totalHeat = Number((item.totalHeat + p.heatValue).toFixed(1));
}
}
const topCountries = Array.from(countryMap.entries())
.map(([country, val]) => ({
country,
count: val.count,
totalHeat: val.totalHeat,
}))
.sort((a, b) => b.totalHeat - a.totalHeat);
return {
totalUniqueVisitors: logs.length,
points,
topCountries,
};
}
}