feat: 实现PIPL数据主体删除权支持功能
- 新增 LeadDeletionRequest 模型,支持邮箱验证的删除申请/确认流程 - POST /api/v1/custom/leads/deletion-requests/ 发起申请(通用响应避免探测邮箱是否存在) - POST /api/v1/custom/leads/deletion-requests/confirm/ 确认删除(24小时token有效期) - 按邮箱在 Lead.data 中匹配并删除对应记录,跨SQLite/Postgres使用Python级匹配 - LeadDeletionRequest 通过 Django Admin 只读展示供合规审计 - 新增 FRONTEND_BASE_URL 设置用于拼接邮件确认链接 - apps/forms/tests.py 新增6个测试用例(含节流缓存隔离修复),全仓库30个用例通过 - 更新设计文档 §2.15/§2.16 勾选项
This commit is contained in:
@@ -2,8 +2,14 @@
|
||||
线索表单:FormDefinition 是可复用的表单定义 Snippet,通过 core.blocks.FormBlock
|
||||
嵌入任意 StreamField 页面;提交结果保存为 Lead。
|
||||
详见 documents/设计方案分析与完善版.md §2.6(FormBlock)与 B2B 官网核心转化路径。
|
||||
|
||||
LeadDeletionRequest 支持 PIPL 数据主体删除权(详见 §2.15)。
|
||||
"""
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from modelcluster.fields import ParentalKey
|
||||
from modelcluster.models import ClusterableModel
|
||||
from wagtail.admin.panels import FieldPanel, InlinePanel
|
||||
@@ -111,3 +117,50 @@ class Lead(models.Model):
|
||||
def __str__(self):
|
||||
form_name = self.form.name if self.form_id else "(表单已删除)"
|
||||
return f"{form_name} - {self.created_at:%Y-%m-%d %H:%M}"
|
||||
|
||||
|
||||
class LeadDeletionRequest(models.Model):
|
||||
"""PIPL 数据主体删除权申请记录(合规审计用途)。
|
||||
|
||||
匿名用户凭提交表单时留下的邮箱发起申请,邮件确认后才会删除匹配的 Lead 记录,
|
||||
避免任意人凭他人邮箱发起删除。仅支持邮箱验证(暂未接入短信网关,
|
||||
手机号验证需待国内短信/验证码服务落地后再支持)。
|
||||
"""
|
||||
|
||||
STATUS_PENDING = "pending"
|
||||
STATUS_COMPLETED = "completed"
|
||||
STATUS_EXPIRED = "expired"
|
||||
STATUS_CHOICES = [
|
||||
(STATUS_PENDING, "待确认"),
|
||||
(STATUS_COMPLETED, "已完成"),
|
||||
(STATUS_EXPIRED, "已过期"),
|
||||
]
|
||||
|
||||
TOKEN_VALID_HOURS = 24
|
||||
|
||||
contact = models.EmailField(verbose_name="联系邮箱")
|
||||
token = models.UUIDField(
|
||||
default=uuid.uuid4, unique=True, editable=False, verbose_name="确认令牌"
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default=STATUS_PENDING,
|
||||
verbose_name="状态",
|
||||
)
|
||||
requested_at = models.DateTimeField(auto_now_add=True, verbose_name="申请时间")
|
||||
confirmed_at = models.DateTimeField(null=True, blank=True, verbose_name="确认时间")
|
||||
completed_at = models.DateTimeField(null=True, blank=True, verbose_name="完成时间")
|
||||
deleted_count = models.PositiveIntegerField(default=0, verbose_name="已删除记录数")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "数据删除申请(PIPL)"
|
||||
verbose_name_plural = "数据删除申请(PIPL)"
|
||||
ordering = ["-requested_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.contact} - {self.get_status_display()}"
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
return timezone.now() > self.requested_at + timedelta(hours=self.TOKEN_VALID_HOURS)
|
||||
|
||||
Reference in New Issue
Block a user