PHP 数据删除合规

wen PHP项目 1

本文目录导读:

PHP 数据删除合规

  1. 核心合规原则(先于代码)
  2. 技术实现方案(PHP 代码)
  3. 特殊数据类型处理的合规
  4. 合规检查清单(PHP 开发规范)
  5. 常见陷阱与反模式

在 PHP 开发中,数据删除合规是一个涉及法律(GDPR、中国《个人信息保护法》等)业务逻辑技术实现的多层面问题,简单地执行 DELETE FROM table WHERE id = ? 往往是不合规的。

以下是 PHP 数据删除合规的完整指南,涵盖策略、技术实现和代码示例。


核心合规原则(先于代码)

  1. 区分“物理删除”与“逻辑删除”
    • 物理删除:直接从数据库抹去数据(DELETE 语句),风险高,不可恢复。
    • 逻辑删除:打上标记(如 deleted_at 时间戳),数据仍在库中,但业务层不展示。推荐用于核心业务数据(用户、订单)。
  2. 最小化原则:只删除实现目的所必需的数据,不要“连坐”删除关联数据(除非法律要求),注销账号时,是否必须删除其发帖内容?通常保留帖子,但匿名化作者信息。
  3. 可追溯原则:删除操作必须有日志(操作人、操作时间、原因、审批单号),特别是涉及管理员操作时。
  4. “被遗忘权”:用户有权要求删除其个人数据,你需要提供一个用户自助的删除入口。
  5. 保留期(Retention Period):法律不允许无限期保留数据,对于财务凭证类数据,法律有强制保留年限(如中国税法要求5年),不能提前物理删除,删除策略必须与法务约定的保留期联动。

技术实现方案(PHP 代码)

方案 A:逻辑删除(软删除)—— 首选推荐

适用场景:用户账号、订单、业务主数据。

数据库设计

ALTER TABLE users 
ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL; -- 为 NULL 表示未删除
ADD INDEX idx_deleted_at (deleted_at);

PHP 实现(以 Laravel 为例,原生 PDO 同理)

<?php
class UserService
{
    private PDO $pdo;
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }
    /**
     * 逻辑删除用户(注销账号)
     * @param int $userId
     * @param string $reason 合规要求:记录删除原因
     * @return bool
     */
    public function softDeleteUser(int $userId, string $reason = ''): bool
    {
        try {
            $this->pdo->beginTransaction();
            // 1. 标记用户为已删除
            $stmt = $this->pdo->prepare(
                "UPDATE users 
                 SET deleted_at = NOW(), delete_reason = :reason 
                 WHERE id = :id AND deleted_at IS NULL"
            );
            $stmt->execute([
                ':id' => $userId,
                ':reason' => $reason
            ]);
            // 2. 处理关联数据(关键合规点)
            // 将用户评论匿名化,而不是删除帖子
            $this->anonymizeUserComments($userId);
            // 3. 记录删除日志(审计)
            $this->logDeletion($userId, 'user', 'soft_delete', $reason);
            $this->pdo->commit();
            return true;
        } catch (Exception $e) {
            $this->pdo->rollBack();
            error_log("Soft delete failed: " . $e->getMessage());
            return false;
        }
    }
    private function anonymizeUserComments(int $userId): void
    {
        $stmt = $this->pdo->prepare(
            "UPDATE comments 
             SET user_id = NULL, 
                 display_name = '[已注销用户]' 
             WHERE user_id = :userId"
        );
        $stmt->execute([':userId' => $userId]);
    }
    private function logDeletion(int $entityId, string $entityType, string $type, string $reason): void
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO deletion_logs (entity_id, entity_type, deletion_type, reason, operator_id, deleted_at) 
             VALUES (?, ?, ?, ?, ?, NOW())"
        );
        $stmt->execute([$entityId, $entityType, $type, $reason, $_SESSION['admin_id'] ?? null]);
    }
}

方案 B:物理删除(真实删除)—— 谨慎使用

适用场景:用户主动清空缓存、临时文件、测试数据,且确无法律/审计要求

关键点:先备份(或导出),再删除。

<?php
class DataPurger
{
    private PDO $pdo;
    // 使用匿名函数回调处理旧数据
    public function purgeOldLogs(int $daysToKeep = 90): void
    {
        $cutoffDate = date('Y-m-d', strtotime("-{$daysToKeep} days"));
        try {
            $this->pdo->beginTransaction();
            // 1. 导出备份到 CSV 或归档表
            $stmt = $this->pdo->prepare(
                "INSERT INTO archive_logs (select * from operation_logs where created_at < ?)"
            );
            $stmt->execute([$cutoffDate]);
            // 2. 物理删除
            $deleteStmt = $this->pdo->prepare(
                "DELETE FROM operation_logs WHERE created_at < ?"
            );
            $deleteStmt->execute([$cutoffDate]);
            $this->pdo->commit();
        } catch (Exception $e) {
            $this->pdo->rollBack();
            // 抛给上层处理
            throw $e;
        }
    }
}

特殊数据类型处理的合规

数据类型 合规要求 技术处理方案
个人资料 需彻底删除,防止恢复 脱敏/粉碎:直接置空(NULL)比用假数据填充更安全,若数据库支持 SECURE DELETE(如InnoDB 加密),或使用物理删除后执行 OPTIMIZE TABLE 释放空间。
关联外键 防止 FOREIGN KEY 约束导致删除失败 使用 ON DELETE SET NULL(不删除关联记录)或 ON DELETE CASCADE(级联删除,需确保合法)。建议手动管理关联逻辑,而非依赖外键级联,因为级联难以控制。
二进制文件 文件系统删除 ≠ 数据库删除 两段式提交:先将数据库记录删除,再延迟(如24小时后)物理删除文件服务器上的文件,保留一个“回收站”文件夹。
缓存数据 缓存不是持久化存储 逻辑删除后,立即调用 Redis->del($key)Memcached->delete($key),避免脏数据。

合规检查清单(PHP 开发规范)

  1. 杜绝裸 DELETE:在代码审查中,禁止出现没有 WHERE 条件且带有 id 参数的 DELETE 语句,除非经过三层审批。
  2. 强制定义接口
    • DELETE /api/user/me/account(用户自助删除)
    • POST /admin/users/{id}/anonymize(管理员匿名化)
  3. 异步与队列:对于大数据量的删除(如清理百万行日志),应引入 消息队列 分片处理,避免阻塞 PHP-FPM 进程导致超时。
  4. 幂等性设计:删除请求应支持幂等,如果用户重复点击“删除”,第二次点击应返回“已删除”状态码(204),而不是报错。
  5. 记录操作者:删除日志中必须包含删除者是用户本人还是管理员,如果是管理员,必须关联后台操作记录(IP、User-Agent)。
  6. 测试用例:你的 PHPUnit 测试套件应该包含:
    • 删除后,再按 ID 查询是否返回 404。
    • 删除后,关联表的查询是否被限制(例如帖子作者显示“游客”)。
    • 删除后,数据库备份是否还能恢复该数据(如果是逻辑删除,则备份恢复会复活数据)。

常见陷阱与反模式

🚫 反模式 1:直接 unlink() 用户上传的文件

// 错误做法
unlink('/path/to/avatar/'.$userId.'.jpg');
DB::table('users')->where('id',$userId)->delete();

✅ 合规做法:文件先移动到“待删除归档区”,数据库打上 file_purge_at = DATE_ADD(NOW(), INTERVAL 1 DAY) 标记,由定时任务一天后清理。

🚫 反模式 2:不加事务的批量删除

// 错误做法:循环删除,中途失败导致数据不一致
foreach ($ids as $id) {
    DB::table('shipping_addresses')->delete($id);
}

✅ 合规做法:在事务包裹下批量更新标记,或使用 WHERE IN 一次性操作。


在 PHP 中执行删除操作,“逻辑删除 + 定时清理 + 审计日志” 是应付法律风险的最优解。

  • 对于用户个人信息:执行匿名化(字段置空)而非物理粉碎。
  • 对于非重要日志:执行物理删除,但需要先导出备份,且必须加上 LIMIT 分步进行。

代码审查红线:如果看到 DELETE FROM table;(无WHERE),必须拒绝合并,如果看到 ->delete() 方法调用后未记录日志,必须要求补充。

抱歉,评论功能暂时关闭!