feat(crm): 实现GDPR Article 17数据库级个人数据级联擦除与雷达IP掩码脱敏

这个提交包含在:
wikiboss
2026-09-21 11:23:26 +08:00
父节点 13752f9906
当前提交 e89c6ba34c
共有 2 个文件被更改,包括 140 次插入 和 0 次删除

查看文件

@@ -72,4 +72,86 @@ describe('Point 53: 海外合规 GDPR / CCPA 访客数据匿名化脱敏与被
expect(receipt.certificateHash).toHaveLength(64);
});
});
describe('4. purgeVisitorPersonalData 数据库级合规擦除执行', () => {
it('根据 email 或 leadId 执行级联清洗,将线索脱敏且掩码历史雷达 IP', async () => {
const mockLead = {
id: 105,
name: 'Pierre Dupont',
email: 'pierre@paris.fr',
phone: '+33 6 12345678',
company: 'Paris Luxury',
notes: 'VIP customer',
};
const mockTracks = [
{ id: 1, visitorId: 'vis_fr_99', ip: '193.252.122.103', stayDurationMs: 5000 },
{ id: 2, visitorId: 'vis_fr_99', ip: '82.120.45.67', stayDurationMs: 8000 },
];
const savedLeads: any[] = [];
const savedTracks: any[] = [];
service.crmLeadEntity = {
findOne: jest.fn().mockResolvedValue(mockLead),
save: jest.fn().mockImplementation((rec) => {
savedLeads.push(rec);
return Promise.resolve(rec);
}),
} as any;
service.crmTrackEntity = {
find: jest.fn().mockResolvedValue(mockTracks),
save: jest.fn().mockImplementation((tracks) => {
savedTracks.push(...(Array.isArray(tracks) ? tracks : [tracks]));
return Promise.resolve(tracks);
}),
} as any;
const receipt = await service.purgeVisitorPersonalData({
leadId: 105,
email: 'pierre@paris.fr',
visitorId: 'vis_fr_99',
});
expect(receipt.subjectId).toBe('pierre@paris.fr');
expect(receipt.certificateHash).toHaveLength(64);
// 验证线索已被彻底清洗
expect(savedLeads[0].name).toBe('[GDPR_DELETED]');
expect(savedLeads[0].email).toBe('deleted-105@privacy.anonymized');
expect(savedLeads[0].phone).toBe('');
// 验证足迹 IP 已被掩码
expect(savedTracks[0].ip).toBe('193.252.122.xxx');
expect(savedTracks[1].ip).toBe('82.120.45.xxx');
});
});
describe('5. AdminCrmComplianceController 控制器接口契约', () => {
let controller: any;
beforeEach(() => {
controller = new (require('../../../src/modules/crm/controller/admin/compliance').AdminCrmComplianceController)();
controller.complianceGdprService = service;
controller.ok = (data: any) => ({ code: 1000, message: 'success', data });
});
it('POST /erasure 缺少主体参数应抛出 CoolCommException', async () => {
await expect(controller.erasure({})).rejects.toThrow();
});
it('POST /erasure 传入合法主体参数应成功返回销毁收据', async () => {
service.purgeVisitorPersonalData = jest.fn().mockResolvedValue({
subjectId: 'vis_test',
action: 'PURGED_AND_ANONYMIZED',
executedAt: new Date().toISOString(),
certificateHash: 'hash_abc_123',
});
const res = await controller.erasure({ visitorId: 'vis_test' });
expect(res.code).toBe(1000);
expect(res.data.certificateHash).toBe('hash_abc_123');
});
});
});