Node.js + PostgreSQL GDPR 用户数据管理完整指南
概述
欧盟《通用数据保护条例》(GDPR)为处理欧盟公民数据的组织设定了严格标准。本文将介绍如何在 Node.js + PostgreSQL 架构中实现 GDPR 核心要求。
GDPR 核心原则
- 合法性、公平性与透明性:明确告知数据处理目的
- 目的限制:数据仅用于声明的目的
- 数据最小化:仅收集必要的数据
- 准确性:保持数据准确并及时更新
- 存储限制:数据保存不超过必要时间
- 完整性与机密性:确保数据安全
数据主体权利
| 权利 | 实现要求 |
|---|---|
| 知情权 | 隐私政策、 cookie 横幅 |
| 访问权 | 数据导出 API(DSAR) |
| 更正权 | 用户资料编辑功能 |
| 删除权 | 数据删除 API(被遗忘权) |
| 限制处理权 | 数据冻结机制 |
| 可携权 | 机器可读导出格式 |
| 反对权 | 营销退订、数据处理异议 |
| 自动化决策权利 | 人工干预选项 |
数据库设计
1. GDPR 核心表结构
code
-- =============================================================================
-- GDPR 合规数据库架构
-- =============================================================================
-- 用户同意记录表
CREATE TABLE user_consent (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
consent_type VARCHAR(50) NOT NULL, -- 'marketing', 'analytics', 'cookies'
granted BOOLEAN NOT NULL,
granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
revoked_at TIMESTAMP WITH TIME ZONE,
ip_address INET,
user_agent TEXT,
consent_document_version VARCHAR(20),
UNIQUE(user_id, consent_type, granted_at)
);
-- 数据处理记录表(Article 30 合规)
CREATE TABLE data_processing_records (
id BIGSERIAL PRIMARY KEY,
purpose VARCHAR(200) NOT NULL,
data_categories TEXT[] NOT NULL, -- ['personal', 'health', 'location']
recipients TEXT[], -- 第三方接收方
retention_period VARCHAR(100),
security_measures TEXT[],
international_transfer BOOLEAN DEFAULT FALSE,
transfer_countries TEXT[],
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 数据主体访问请求(DSAR)表
CREATE TABLE data_subject_requests (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
request_type VARCHAR(20) NOT NULL, -- 'access', 'deletion', 'portability', 'rectification'
status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'processing', 'completed', 'rejected'
requested_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
rejection_reason TEXT,
processed_by BIGINT REFERENCES users(id),
notes TEXT,
export_url TEXT, -- 安全下载链接
expires_at TIMESTAMP WITH TIME ZONE
);
-- 数据删除日志表
CREATE TABLE data_deletion_log (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
tables_affected TEXT[] NOT NULL,
rows_deleted INTEGER NOT NULL,
deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
verification_hash VARCHAR(64), -- 删除验证哈希
performed_by BIGINT REFERENCES users(id)
);
-- Cookie 同意表
CREATE TABLE cookie_consent (
id BIGSERIAL PRIMARY KEY,
consent_id VARCHAR(100) UNIQUE NOT NULL,
preferences JSONB NOT NULL, -- {'necessary': true, 'marketing': false, ...}
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
);
-- 创建索引
CREATE INDEX idx_user_consent_user_id ON user_consent(user_id);
CREATE INDEX idx_user_consent_type ON user_consent(consent_type);
CREATE INDEX idx_dsar_user_id ON data_subject_requests(user_id);
CREATE INDEX idx_dsar_status ON data_subject_requests(status);
CREATE INDEX idx_deletion_log_user_id ON data_deletion_log(user_id);
-- 创建更新时间触发器
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_cookie_consent_updated_at
BEFORE UPDATE ON cookie_consent
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
Code collapsed
2. 数据分类标签系统
code
-- 数据分类系统
CREATE TYPE data_sensitivity AS ENUM (
'public', -- 公开信息
'contact', -- 联系方式
'personal', -- 个人信息
'sensitive', -- 敏感个人信息
'health' -- 健康数据(特殊类别)
);
-- 为现有表添加数据分类
ALTER TABLE users ADD COLUMN data_classification data_sensitivity DEFAULT 'personal';
-- 敏感数据标记
CREATE TABLE sensitive_data_fields (
table_name VARCHAR(100) NOT NULL,
column_name VARCHAR(100) NOT NULL,
sensitivity data_sensitivity NOT NULL,
legal_basis VARCHAR(100),
retention_period VARCHAR(100),
PRIMARY KEY (table_name, column_name)
);
-- 插入字段分类
INSERT INTO sensitive_data_fields (table_name, column_name, sensitivity, legal_basis, retention_period) VALUES
('users', 'email', 'contact', 'contract', '3_years_after_contract_end'),
('users', 'phone', 'contact', 'contract', '3_years_after_contract_end'),
('users', 'date_of_birth', 'personal', 'consent', '5_years'),
('users', 'national_id', 'sensitive', 'legal_obligation', 'legal_retention'),
('health_records', 'diagnosis', 'health', 'explicit_consent', '10_years'),
('health_records', 'medication', 'health', 'explicit_consent', '10_years');
Code collapsed
Node.js 实现
1. 同意管理系统
code
// lib/consent/consent-manager.ts
import { pool } from '@/lib/db';
export interface ConsentRecord {
id: number;
userId: number;
consentType: 'marketing' | 'analytics' | 'cookies' | 'health_data';
granted: boolean;
grantedAt: Date;
documentVersion: string;
}
export class ConsentManager {
/**
* 记录用户同意
*/
static async recordConsent(params: {
userId: number;
consentType: string;
granted: boolean;
documentVersion: string;
ipAddress?: string;
userAgent?: string;
}): Promise<ConsentRecord> {
const { userId, consentType, granted, documentVersion, ipAddress, userAgent } = params;
const query = `
INSERT INTO user_consent (
user_id, consent_type, granted, consent_document_version,
ip_address, user_agent
)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
`;
const result = await pool.query(query, [
userId,
consentType,
granted,
documentVersion,
ipAddress,
userAgent,
]);
// 如果是撤销同意,记录撤销时间
if (!granted) {
await pool.query(
'UPDATE user_consent SET revoked_at = NOW() WHERE id = $1',
[result.rows[0].id]
);
}
return result.rows[0];
}
/**
* 检查用户是否同意特定数据处理
*/
static async hasConsent(
userId: number,
consentType: string
): Promise<boolean> {
const query = `
SELECT granted
FROM user_consent
WHERE user_id = $1
AND consent_type = $2
AND revoked_at IS NULL
ORDER BY granted_at DESC
LIMIT 1
`;
const result = await pool.query(query, [userId, consentType]);
return result.rows[0]?.granted ?? false;
}
/**
* 获取用户所有有效同意
*/
static async getUserConsents(userId: number): Promise<ConsentRecord[]> {
const query = `
SELECT * FROM user_consent
WHERE user_id = $1 AND revoked_at IS NULL
ORDER BY granted_at DESC
`;
const result = await pool.query(query, [userId]);
return result.rows;
}
/**
* 撤销所有同意
*/
static async revokeAllConsents(userId: number): Promise<void> {
await pool.query(
'UPDATE user_consent SET revoked_at = NOW() WHERE user_id = $1 AND revoked_at IS NULL',
[userId]
);
}
}
Code collapsed
2. 数据主体访问请求(DSAR)
code
// lib/gdpr/data-subject-request.ts
import { pool } from '@/lib/db';
import { crypto } from 'crypto';
export type RequestType = 'access' | 'deletion' | 'portability' | 'rectification';
export type RequestStatus = 'pending' | 'processing' | 'completed' | 'rejected';
export interface DataSubjectRequest {
id: number;
userId: number;
requestType: RequestType;
status: RequestStatus;
requestedAt: Date;
completedAt?: Date;
exportUrl?: string;
expiresAt?: Date;
}
export class DataSubjectRequestManager {
/**
* 创建数据主体请求
*/
static async createRequest(params: {
userId?: number;
requestType: RequestType;
email?: string; // 用于未登录用户
}): Promise<DataSubjectRequest> {
const { userId, requestType, email } = params;
// 查找用户(如果提供的是邮箱)
let targetUserId = userId;
if (email && !userId) {
const userResult = await pool.query(
'SELECT id FROM users WHERE email = $1',
[email]
);
targetUserId = userResult.rows[0]?.id;
}
const query = `
INSERT INTO data_subject_requests (user_id, request_type)
VALUES ($1, $2)
RETURNING *
`;
const result = await pool.query(query, [targetUserId, requestType]);
// 设置过期时间(30 天后)
await pool.query(
'UPDATE data_subject_requests SET expires_at = NOW() + INTERVAL \'30 days\' WHERE id = $1',
[result.rows[0].id]
);
return result.rows[0];
}
/**
* 收集用户所有数据(用于访问权和数据可携权)
*/
static async collectUserData(userId: number): Promise<any> {
const userData = {
profile: await this.getUserProfile(userId),
activity: await this.getUserActivity(userId),
consents: await this.getUserConsents(userId),
healthData: await this.getUserHealthData(userId),
metadata: {
exportDate: new Date().toISOString(),
formatVersion: '1.0',
},
};
return userData;
}
/**
* 获取用户基本资料
*/
private static async getUserProfile(userId: number) {
const result = await pool.query(
'SELECT id, email, name, created_at, updated_at FROM users WHERE id = $1',
[userId]
);
return result.rows[0];
}
/**
* 获取用户活动日志
*/
private static async getUserActivity(userId: number) {
const result = await pool.query(
`SELECT * FROM user_activity_logs
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 1000`,
[userId]
);
return result.rows;
}
/**
* 获取用户同意记录
*/
private static async getUserConsents(userId: number) {
const result = await pool.query(
'SELECT * FROM user_consent WHERE user_id = $1 ORDER BY granted_at DESC',
[userId]
);
return result.rows;
}
/**
* 获取用户健康数据
*/
private static async getUserHealthData(userId: number) {
const result = await pool.query(
'SELECT * FROM health_records WHERE user_id = $1 ORDER BY recorded_at DESC',
[userId]
);
return result.rows;
}
/**
* 生成安全下载链接
*/
static async generateDownloadLink(
requestId: number,
data: any
): Promise<string> {
// 加密数据
const encrypted = await this.encryptUserData(data);
// 生成安全的下载令牌
const downloadToken = crypto.randomBytes(32).toString('hex');
// 存储加密数据(临时,7 天后过期)
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await pool.query(
`INSERT INTO data_exports (request_id, token, encrypted_data, expires_at)
VALUES ($1, $2, $3, $4)`,
[requestId, downloadToken, encrypted, expiresAt]
);
// 更新请求状态
await pool.query(
`UPDATE data_subject_requests
SET status = 'completed', export_url = $1, completed_at = NOW()
WHERE id = $2`,
[`/api/gdpr/download/${downloadToken}`, requestId]
);
return `/api/gdpr/download/${downloadToken}`;
}
/**
* 加密用户数据
*/
private static async encryptUserData(data: any): Promise<string> {
// 实现加密逻辑
return JSON.stringify(data);
}
}
Code collapsed
3. 数据删除实现(被遗忘权)
code
// lib/gdpr/right-to-erasure.ts
import { pool } from '@/lib/db';
export interface DeletionResult {
success: boolean;
tablesAffected: string[];
rowsDeleted: number;
anonymizedRows: number;
}
export class RightToErasure {
/**
* 执行用户数据删除
* 注意:某些数据可能因法律要求需要匿名化而非完全删除
*/
static async deleteUserAccount(userId: number): Promise<DeletionResult> {
const client = await pool.connect();
const tablesAffected: string[] = [] as string[];
let totalDeleted = 0;
let totalAnonymized = 0;
try {
await client.query('BEGIN');
// 1. 删除/匿名化用户活动日志
const activityResult = await client.query(
`UPDATE user_activity_logs
SET user_id = NULL, ip_address = NULL, user_agent = NULL
WHERE user_id = $1`,
[userId]
);
tablesAffected.push('user_activity_logs');
totalAnonymized += activityResult.rowCount || 0;
// 2. 删除健康记录(需用户明确同意)
const healthResult = await client.query(
'DELETE FROM health_records WHERE user_id = $1',
[userId]
);
tablesAffected.push('health_records');
totalDeleted += healthResult.rowCount || 0;
// 3. 匿名化通知记录
const notificationResult = await client.query(
`UPDATE notifications
SET user_id = NULL, metadata = metadata - 'email'
WHERE user_id = $1`,
[userId]
);
tablesAffected.push('notifications');
totalAnonymized += notificationResult.rowCount || 0;
// 4. 删除会话数据
const sessionResult = await client.query(
'DELETE FROM user_sessions WHERE user_id = $1',
[userId]
);
tablesAffected.push('user_sessions');
totalDeleted += sessionResult.rowCount || 0;
// 5. 记录删除操作
const deletionHash = this.generateDeletionHash(userId);
await client.query(
`INSERT INTO data_deletion_log
(user_id, tables_affected, rows_deleted, verification_hash, performed_by)
VALUES ($1, $2, $3, $4, $5)`,
[userId, tablesAffected, totalDeleted, deletionHash, userId]
);
// 6. 最后删除用户账户
await client.query('DELETE FROM users WHERE id = $1', [userId]);
tablesAffected.push('users');
await client.query('COMMIT');
return {
success: true,
tablesAffected,
rowsDeleted: totalDeleted,
anonymizedRows: totalAnonymized,
};
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
/**
* 生成删除验证哈希
*/
private static generateDeletionHash(userId: number): string {
const timestamp = Date.now();
const data = `${userId}-${timestamp}`;
return crypto.createHash('sha256').update(data).digest('hex');
}
/**
* 匿名化数据(保留统计用途)
*/
static async anonymizeUserData(userId: number): Promise<void> {
await pool.query(
`UPDATE users
SET
email = 'deleted-' || id || '@anon.example',
name = NULL,
phone = NULL,
date_of_birth = NULL,
national_id = NULL,
data_classification = 'anonymized'
WHERE id = $1`,
[userId]
);
}
}
Code collapsed
4. API 路由实现
code
// app/api/gdpr/request/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { DataSubjectRequestManager } from '@/lib/gdpr/data-subject-request';
export async function POST(req: NextRequest) {
try {
const { requestType, email } = await req.json();
// 验证请求类型
const validTypes = ['access', 'deletion', 'portability', 'rectification'];
if (!validTypes.includes(requestType)) {
return NextResponse.json(
{ error: '无效的请求类型' },
{ status: 400 }
);
}
// 创建请求
const request = await DataSubjectRequestManager.createRequest({
requestType,
email,
});
// 发送确认邮件
await sendConfirmationEmail(email, request.id);
return NextResponse.json({
requestId: request.id,
message: '请求已创建,确认邮件已发送',
});
} catch (error) {
return NextResponse.json(
{ error: '创建请求失败' },
{ status: 500 }
);
}
}
// app/api/gdpr/export/[userId]/route.ts
export async function GET(
req: NextRequest,
{ params }: { params: { userId: string } }
) {
try {
const userId = parseInt(params.userId);
const token = req.headers.get('Authorization')?.replace('Bearer ', '');
// 验证令牌
if (!isValidExportToken(token, userId)) {
return NextResponse.json(
{ error: '无效的授权令牌' },
{ status: 401 }
);
}
// 收集用户数据
const userData = await DataSubjectRequestManager.collectUserData(userId);
// 生成 JSON 文件
const filename = `user-data-${userId}-${Date.now()}.json`;
return new NextResponse(JSON.stringify(userData, null, 2), {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename: "${filename}"`,
},
});
} catch (error) {
return NextResponse.json(
{ error: '导出数据失败' },
{ status: 500 }
);
}
}
// app/api/gdpr/delete/route.ts
export async function DELETE(req: NextRequest) {
try {
const { userId, confirmationCode } = await req.json();
// 验证确认码
const isValid = await verifyDeletionCode(userId, confirmationCode);
if (!isValid) {
return NextResponse.json(
{ error: '无效的确认码' },
{ status: 400 }
);
}
// 执行删除
const result = await RightToErasure.deleteUserAccount(userId);
// 发送删除确认邮件
await sendDeletionConfirmation(userId);
return NextResponse.json({
message: '账户已删除',
details: result,
});
} catch (error) {
return NextResponse.json(
{ error: '删除账户失败' },
{ status: 500 }
);
}
}
Code collapsed
5. Cookie 同意管理
code
// lib/consent/cookie-consent.ts
import { cookies } from 'next/headers';
import { pool } from '@/lib/db';
export interface CookiePreferences {
necessary: boolean; // 必需 Cookies
functional: boolean; // 功能性 Cookies
analytics: boolean; // 分析 Cookies
marketing: boolean; // 营销 Cookies
}
export class CookieConsentManager {
private static readonly CONSENT_ID_COOKIE = 'cookie_consent_id';
private static readonly CONSENT_DURATION = 365 * 24 * 60 * 60 * 1000; // 1 年
/**
* 保存用户 Cookie 偏好
*/
static async savePreferences(preferences: CookiePreferences): Promise<string> {
const consentId = crypto.randomUUID();
// 保存到数据库
await pool.query(
`INSERT INTO cookie_consent (consent_id, preferences, expires_at)
VALUES ($1, $2, $3)`,
[
consentId,
JSON.stringify(preferences),
new Date(Date.now() + this.CONSENT_DURATION),
]
);
// 设置 Cookie
cookies().set({
name: this.CONSENT_ID_COOKIE,
value: consentId,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: this.CONSENT_DURATION,
path: '/',
});
return consentId;
}
/**
* 获取用户 Cookie 偏好
*/
static async getPreferences(): Promise<CookiePreferences> {
const consentId = cookies().get(this.CONSENT_ID_COOKIE)?.value;
if (!consentId) {
return this.getDefaultPreferences();
}
const result = await pool.query(
`SELECT preferences FROM cookie_consent
WHERE consent_id = $1 AND expires_at > NOW()`,
[consentId]
);
if (result.rows.length === 0) {
return this.getDefaultPreferences();
}
return result.rows[0].preferences as CookiePreferences;
}
/**
* 检查是否同意特定类型的 Cookie
*/
static async hasConsent(type: keyof CookiePreferences): Promise<boolean> {
const prefs = await this.getPreferences();
return prefs[type] ?? false;
}
/**
* 更新偏好
*/
static async updatePreferences(preferences: Partial<CookiePreferences>) {
const consentId = cookies().get(this.CONSENT_ID_COOKIE)?.value;
if (consentId) {
await pool.query(
`UPDATE cookie_consent
SET preferences = $1, updated_at = NOW()
WHERE consent_id = $2`,
[JSON.stringify(preferences), consentId]
);
}
}
private static getDefaultPreferences(): CookiePreferences {
return {
necessary: true,
functional: false,
analytics: false,
marketing: false,
};
}
}
Code collapsed
6. Cookie 横幅组件
code
// components/consent/CookieBanner.tsx
'use client';
import { useState, useEffect } from 'react';
import { X } from 'lucide-react';
import { CookieConsentManager, CookiePreferences } from '@/lib/consent/cookie-consent';
export function CookieBanner() {
const [isVisible, setIsVisible] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [preferences, setPreferences] = useState<CookiePreferences>({
necessary: true,
functional: false,
analytics: false,
marketing: false,
});
useEffect(() => {
// 检查是否已做出选择
const checkConsent = async () => {
const prefs = await CookieConsentManager.getPreferences();
// 如果没有有效同意记录,显示横幅
const hasValidConsent = await fetch('/api/cookies/check').then(r => r.json());
setIsVisible(!hasValidConsent.hasConsent);
};
checkConsent();
}, []);
const handleAcceptAll = async () => {
const allAccepted: CookiePreferences = {
necessary: true,
functional: true,
analytics: true,
marketing: true,
};
await CookieConsentManager.savePreferences(allAccepted);
setIsVisible(false);
};
const handleAcceptSelected = async () => {
await CookieConsentManager.savePreferences(preferences);
setIsVisible(false);
};
const handleRejectAll = async () => {
const minimal: CookiePreferences = {
necessary: true,
functional: false,
analytics: false,
marketing: false,
};
await CookieConsentManager.savePreferences(minimal);
setIsVisible(false);
};
if (!isVisible) return null;
return (
<div className: "fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t shadow-lg z-50">
<div className: "max-w-6xl mx-auto p-6">
{!showSettings ? (
<div className: "flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className: "flex-1">
<h3 className: "font-semibold text-lg mb-2">Cookie 偏好设置</h3>
<p className: "text-sm text-gray-600 dark:text-gray-400">
我们使用 Cookie 来改善您的体验并分析网站使用情况。您可以选择接受所有 Cookie 或管理您的偏好。
</p>
</div>
<div className: "flex gap-3">
<button
onClick={handleRejectAll}
className: "px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
拒绝所有
</button>
<button
onClick={() => setShowSettings(true)}
className: "px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
管理偏好
</button>
<button
onClick={handleAcceptAll}
className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
接受所有
</button>
</div>
</div>
) : (
<div className: "space-y-4">
<h3 className: "font-semibold text-lg">Cookie 偏好设置</h3>
<CookieOption
name: "必需 Cookies"
description: "这些 Cookie 对于网站正常运行是必需的。"
checked={preferences.necessary}
disabled
/>
<CookieOption
name: "功能性 Cookies"
description: "这些 Cookie 启用额外功能,如记住您的偏好。"
checked={preferences.functional}
onChange={(checked) =>
setPreferences({ ...preferences, functional: checked })
}
/>
<CookieOption
name: "分析 Cookies"
description: "这些 Cookie 帮助我们了解网站如何被使用。"
checked={preferences.analytics}
onChange={(checked) =>
setPreferences({ ...preferences, analytics: checked })
}
/>
<CookieOption
name: "营销 Cookies"
description: "这些 Cookie 用于向您展示相关广告。"
checked={preferences.marketing}
onChange={(checked) =>
setPreferences({ ...preferences, marketing: checked })
}
/>
<div className: "flex gap-3 pt-4">
<button
onClick={() => setShowSettings(false)}
className: "px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
取消
</button>
<button
onClick={handleAcceptSelected}
className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
保存偏好
</button>
</div>
</div>
)}
</div>
</div>
);
}
function CookieOption({
name,
description,
checked,
onChange,
disabled = false,
}: {
name: string;
description: string;
checked: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
}) {
return (
<div className: "flex items-start gap-3 p-3 border rounded-lg">
<input
type: "checkbox"
checked={checked}
onChange={(e) => onChange?.(e.target.checked)}
disabled={disabled}
className: "mt-1"
/>
<div>
<p className: "font-medium">{name}</p>
<p className: "text-sm text-gray-600 dark:text-gray-400">{description}</p>
</div>
</div>
);
}
Code collapsed
合规检查清单
技术实现
- 实施同意管理系统(Cookie 横幅、偏好记录)
- 实现数据访问请求 API(DSAR)
- 实现数据删除功能(被遗忘权)
- 实现数据导出功能(数据可携权)
- 记录所有数据处理活动(Article 30)
- 实施数据分类和敏感性标记
安全措施
- 数据加密(静态和传输中)
- 访问控制和身份验证
- 审计日志和活动监控
- 定期安全评估
- 数据泄露响应计划
文档要求
- 隐私政策(详细说明数据处理)
- Cookie 政策
- 数据处理记录(ROPA)
- 数据保护影响评估(DPIA)
- 与处理者的数据处理协议(DPA)
用户权利
- 知情权(透明的隐私政策)
- 访问权(DSAR 响应 ≤ 30 天)
- 更正权(用户可编辑资料)
- 删除权(被遗忘权)
- 限制处理权
- 数据可携权(机器可读格式)
- 反对权
参考资料
免责声明:本文提供的信息仅供参考,不构成法律建议。实施 GDPR 合规措施前,请咨询合格的法律专业人士。