康心伴Logo
康心伴WellAlly
Data & Privacy

Node.js + PostgreSQL GDPR 用户数据管理完整指南

5 分钟阅读

Node.js + PostgreSQL GDPR 用户数据管理完整指南

概述

欧盟《通用数据保护条例》(GDPR)为处理欧盟公民数据的组织设定了严格标准。本文将介绍如何在 Node.js + PostgreSQL 架构中实现 GDPR 核心要求。

GDPR 核心原则

  1. 合法性、公平性与透明性:明确告知数据处理目的
  2. 目的限制:数据仅用于声明的目的
  3. 数据最小化:仅收集必要的数据
  4. 准确性:保持数据准确并及时更新
  5. 存储限制:数据保存不超过必要时间
  6. 完整性与机密性:确保数据安全

数据主体权利

权利实现要求
知情权隐私政策、 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 合规措施前,请咨询合格的法律专业人士。

#

文章标签

nodejs
postgres
gdpr
privacy
compliance
data-protection

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅