""" 线索提交接口。挂载在 /api/v1/custom/leads/(详见 apps/forms/urls.py 与 documents/设计方案分析与完善版.md §2.7 自定义业务接口设计)。 公开表单提交接口,限流走 DRF ScopedRateThrottle 的 "forms" scope(5/min,防刷)。 LeadDeletionRequestView/LeadDeletionConfirmView 支持 PIPL 数据主体删除权(§2.15)。 """ import json import logging from django.conf import settings from django.core.mail import send_mail from django.utils import timezone from rest_framework.response import Response from rest_framework.throttling import ScopedRateThrottle from rest_framework.views import APIView from .models import FormDefinition, Lead, LeadDeletionRequest from .serializers import ( LeadDeletionConfirmSerializer, LeadDeletionRequestSerializer, LeadSubmitSerializer, ) logger = logging.getLogger(__name__) class LeadSubmitView(APIView): throttle_classes = [ScopedRateThrottle] throttle_scope = "forms" def post(self, request, *args, **kwargs): serializer = LeadSubmitSerializer(data=request.data) serializer.is_valid(raise_exception=True) form_id = serializer.validated_data["form_id"] try: form_def = FormDefinition.objects.get(pk=form_id) except FormDefinition.DoesNotExist: return Response( {"error": {"code": "NOT_FOUND", "message": "表单不存在"}}, status=404 ) lead = Lead.objects.create( form=form_def, data=serializer.validated_data["data"], source_url=serializer.validated_data.get("source_url", ""), ) if form_def.notification_email: send_mail( subject=f"【新线索】{form_def.name}", message=json.dumps(lead.data, ensure_ascii=False, indent=2), from_email=getattr(settings, "DEFAULT_FROM_EMAIL", None), recipient_list=[form_def.notification_email], fail_silently=True, ) return Response( {"success": True, "message": form_def.success_message}, status=201 ) # PIPL 数据删除权:出于隐私考虑,无论邮箱是否存在关联数据,均返回相同的通用提示, # 避免被用来探测某个邮箱是否曾提交过表单。 GENERIC_DELETION_REQUEST_MESSAGE = ( "如果我们持有与该邮箱关联的信息,确认邮件将发送至该邮箱,请查收并点击链接完成删除确认。" ) class LeadDeletionRequestView(APIView): """发起 PIPL 数据删除申请:POST {"contact": "user@example.com"}。""" throttle_classes = [ScopedRateThrottle] throttle_scope = "forms" def post(self, request, *args, **kwargs): serializer = LeadDeletionRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) contact = serializer.validated_data["contact"] deletion_request = LeadDeletionRequest.objects.create(contact=contact) confirm_url = ( f"{settings.FRONTEND_BASE_URL.rstrip('/')}/privacy-policy/delete-confirm" f"?token={deletion_request.token}" ) try: send_mail( subject="确认删除您的个人信息", message=( "我们收到了删除您个人信息的申请。如果这不是您本人操作,请忽略此邮件。\n\n" f"请在 {LeadDeletionRequest.TOKEN_VALID_HOURS} 小时内点击以下链接确认删除:\n" f"{confirm_url}" ), from_email=getattr(settings, "DEFAULT_FROM_EMAIL", None), recipient_list=[contact], fail_silently=False, ) except Exception: logger.exception("发送删除确认邮件失败:%s", contact) return Response({"message": GENERIC_DELETION_REQUEST_MESSAGE}, status=202) class LeadDeletionConfirmView(APIView): """确认 PIPL 数据删除申请:POST {"token": "..."},校验通过后删除匹配的 Lead 记录。""" throttle_classes = [ScopedRateThrottle] throttle_scope = "forms" def post(self, request, *args, **kwargs): serializer = LeadDeletionConfirmSerializer(data=request.data) serializer.is_valid(raise_exception=True) token = serializer.validated_data["token"] try: deletion_request = LeadDeletionRequest.objects.get(token=token) except LeadDeletionRequest.DoesNotExist: return Response( {"error": {"code": "INVALID_TOKEN", "message": "删除链接无效。"}}, status=404, ) if deletion_request.status != LeadDeletionRequest.STATUS_PENDING: return Response( { "error": { "code": "ALREADY_PROCESSED", "message": "该删除申请已处理或已过期。", } }, status=400, ) if deletion_request.is_expired: deletion_request.status = LeadDeletionRequest.STATUS_EXPIRED deletion_request.save(update_fields=["status"]) return Response( {"error": {"code": "TOKEN_EXPIRED", "message": "删除链接已过期,请重新申请。"}}, status=400, ) contact = deletion_request.contact.strip().lower() matched_ids = [ lead.pk for lead in Lead.objects.all() if any( isinstance(value, str) and value.strip().lower() == contact for value in lead.data.values() ) ] deleted_count = len(matched_ids) if matched_ids: Lead.objects.filter(pk__in=matched_ids).delete() now = timezone.now() deletion_request.status = LeadDeletionRequest.STATUS_COMPLETED deletion_request.confirmed_at = now deletion_request.completed_at = now deletion_request.deleted_count = deleted_count deletion_request.save( update_fields=["status", "confirmed_at", "completed_at", "deleted_count"] ) return Response( { "success": True, "deleted_count": deleted_count, "message": "已成功删除与该邮箱关联的个人信息。", } )