11、数据主体请求处理:删除账户功能实现、数据导出(JSON/CSV)功能实现、请求响应时限与流程设计。
数据主体请求处理,说白了就是用户找上门来,要你给个说法。要么是“把我的数据还给我”,要么是“把我的账号删了”。这可不是简单的功能开发,背后是 GDPR 第 17 条(被遗忘权)和第 20 条(数据可携带权)的硬性要求。我这些年处理过不少这类需求,踩过的坑比写过的代码还多。今天咱们就聊聊,怎么把这些合规要求落地成实实在在的代码。
11.1 删除账户功能实现
删除账户,听着简单,做起来全是细节。用户点一下“删除”,你后端可不能真的只删一条用户表记录。我见过最离谱的案例,是某 App 删了用户主表,但日志表里还躺着三年的操作记录——这要是被监管查到,罚单跑不了。
11.1.1 逻辑删除 vs 物理删除
我个人习惯,先做逻辑删除,再做物理删除。为什么?因为用户可能反悔。GDPR 给了用户 30 天的撤回期,你直接物理删了,人家反悔了怎么办?
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 逻辑删除 | 标记 is_deleted=1,数据保留在库中 | 用户反悔、审计追溯、法律冻结期 |
| 物理删除 | 从数据库彻底抹除数据 | 冻结期结束、用户明确要求不可逆 |
11.1.2 级联删除策略
用户数据不是孤岛。一个用户可能关联了订单、评论、聊天记录、设备信息。你删主表的时候,这些关联数据怎么处理?我建议分三类:
- 必须删除: 个人身份信息(姓名、手机号、身份证)、登录凭证(token、密码哈希)
- 匿名化保留: 订单记录(去掉用户标识,保留商品和金额用于统计)
- 彻底删除: 用户行为日志、埋点数据、设备指纹
嗯,这里要注意。匿名化不是简单的把用户名置空。你得确保无法通过其他字段反推回具体用户。我曾经遇到一个项目,把用户名删了,但邮箱前缀还留在日志里——这跟没删一样。
11.1.3 代码示例:删除账户核心流程
// Kotlin 伪代码 - 删除账户服务
class AccountDeletionService(
private val userRepo: UserRepository,
private val orderRepo: OrderRepository,
private val logRepo: LogRepository
) {
fun deleteAccount(userId: String, isPhysical: Boolean = false) {
if (isPhysical) {
// 物理删除:先匿名化关联数据,再删主表
orderRepo.anonymizeByUserId(userId) // 去掉用户标识
logRepo.deleteByUserId(userId) // 日志直接删
userRepo.physicalDelete(userId) // 最后删主表
} else {
// 逻辑删除:标记 + 设置过期时间
userRepo.softDelete(userId, expireAt = now() + 30.days)
}
// 记录审计日志
auditLog.write("Account deleted", userId, type = isPhysical)
}
}
11.2 数据导出功能实现(JSON/CSV)
数据可携带权,说白了就是用户要把数据带走。你得给人家一个机器可读的格式。JSON 和 CSV 是标配。我个人更推荐 JSON,结构清晰,嵌套数据也好处理。但有些用户就喜欢 CSV,说能直接拖进 Excel 看。
11.2.1 导出数据范围
不是所有数据都能导出。GDPR 规定,只能导出用户主动提供的数据和系统自动收集的关联数据。举个例子:
- 可导出: 个人资料、订单记录、聊天记录、设备信息
- 不可导出: 系统内部计算出的评分、推荐算法权重、其他用户的隐私数据
你想想看,如果用户导出了你的推荐算法模型,那你的商业机密就全暴露了。所以导出前一定要做数据脱敏和范围限制。
11.2.2 JSON 导出实现
// Kotlin 伪代码 - 数据导出为 JSON
fun exportAsJson(userId: String): String {
val profile = userRepo.findProfile(userId)
val orders = orderRepo.findByUserId(userId)
val devices = deviceRepo.findByUserId(userId)
val exportData = mapOf(
"profile" to mapOf(
"name" to profile.name,
"email" to profile.email,
"createdAt" to profile.createdAt
),
"orders" to orders.map { order ->
mapOf(
"orderId" to order.id,
"amount" to order.amount,
"items" to order.items.map { it.name }
)
},
"devices" to devices.map { it.model }
)
return ObjectMapper().writerWithDefaultPrettyPrinter()
.writeValueAsString(exportData)
}
11.2.3 CSV 导出实现
CSV 的问题在于,它天生不适合嵌套数据。我建议把复杂结构拍平。比如订单里的商品列表,用分号分隔塞到一个单元格里。
// Kotlin 伪代码 - 导出为 CSV
fun exportAsCsv(userId: String): String {
val orders = orderRepo.findByUserId(userId)
val header = "orderId,amount,items,date"
val rows = orders.map { order ->
"${order.id},${order.amount},\"${order.items.joinToString(";")}\",${order.date}"
}
return listOf(header).plus(rows).joinToString("\n")
}
11.3 请求响应时限与流程设计
GDPR 规定,数据主体请求必须在 一个月内 响应。特殊情况下可以延长两个月,但必须通知用户原因。这个时限是从你收到请求那天开始算的,不是你开始处理那天。
11.3.1 流程设计
我建议把流程拆成四个阶段:
- 接收与验证: 用户提交请求,系统验证身份(防止冒用)
- 分类与分派: 判断是删除、导出还是修改,分派给对应服务
- 处理与审核: 执行操作,必要时人工审核(比如删除涉及法律冻结的数据)
- 反馈与归档: 通知用户结果,记录审计日志
11.3.2 时限控制
一个月听起来很长,但实际处理起来经常超时。为什么?因为人工审核环节卡住了。我建议设置自动提醒:
- 第 20 天:系统自动发提醒给审核人
- 第 25 天:升级提醒给上级主管
- 第 28 天:如果还没处理完,自动通知用户“需要延期”
嗯,这里要注意。延期通知必须写明原因和预计完成时间。不能只说“我们正在处理”,用户会投诉的。
11.3.3 状态机设计
我习惯用状态机来管理请求生命周期。每个请求都有明确的状态,不会出现“处理中”卡死的情况。
// Kotlin 伪代码 - 请求状态机
enum class RequestState {
PENDING, // 待验证
VERIFIED, // 已验证
PROCESSING, // 处理中
COMPLETED, // 已完成
REJECTED, // 已拒绝
EXPIRED // 超时
}
class RequestStateMachine {
fun transition(current: RequestState, action: Action): RequestState {
return when (current) {
PENDING -> when (action) {
VERIFY -> VERIFIED
REJECT -> REJECTED
else -> throw IllegalStateException()
}
VERIFIED -> when (action) {
START_PROCESS -> PROCESSING
else -> throw IllegalStateException()
}
// ... 其他状态转换
}
}
}
11.4 知识体系总览
下面这张图,是我梳理的数据主体请求处理的核心逻辑。从用户发起请求,到最终反馈结果,每个环节都不能少。
这张图把整个流程串起来了。从接收验证到反馈归档,每个环节都有明确的时限和输出。你照着这个框架去设计,基本不会漏掉关键节点。
好了,数据主体请求处理这块,核心就是三个点:删除要分级、导出要脱敏、流程要可追溯。你把这些落地了,GDPR 的合规要求基本就稳了。剩下的就是根据业务场景微调,比如有些行业有特殊的数据保留期限,这个需要法务同事一起确认。