From 16e4f97e74a0c945f55d5ef804611ee930b45b00 Mon Sep 17 00:00:00 2001 From: Zhengen TANG Date: Fri, 7 Aug 2026 14:49:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BD=E5=86=85=E5=8F=AF=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E6=9B=BF=E6=8D=A2=E6=8E=92=E6=9F=A5=EF=BC=9A?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=9C=9C=E7=BD=90=E5=8F=8D=E5=9E=83=E5=9C=BE?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=20+=20CDN=20=E5=8A=A0=E9=80=9F=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 ++ apps/forms/serializers.py | 3 +++ apps/forms/tests.py | 20 ++++++++++++++++++++ apps/forms/views.py | 7 +++++++ documents/设计方案分析与完善版.md | 6 +++--- wagtailcms/settings/production.py | 3 +++ 6 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index ce424ac..90e0e9b 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,8 @@ OSS_ACCESS_KEY_ID= OSS_SECRET_ACCESS_KEY= OSS_BUCKET_NAME= OSS_ENDPOINT_URL= +# 绑定到 OSS/COS 源站的 CDN 加速域名(阿里云 CDN / 腾讯云 CDN / 又拍云),留空则直接使用 OSS 源站地址 +OSS_CDN_DOMAIN= # 发布后前端 ISR 缓存失效 Webhook FRONTEND_REVALIDATE_URL=https://www.example.com/api/revalidate diff --git a/apps/forms/serializers.py b/apps/forms/serializers.py index 4f683b2..014adde 100644 --- a/apps/forms/serializers.py +++ b/apps/forms/serializers.py @@ -5,6 +5,9 @@ class LeadSubmitSerializer(serializers.Serializer): form_id = serializers.IntegerField() data = serializers.DictField() source_url = serializers.URLField(required=False, allow_blank=True, default="") + # 蜂蜜陷阱反垃圾注册字段:正常用户不会看到/填写该字段,机器人填写则视为垃圾提交。 + # 无需接入极验/腾讯云等第三方验证码服务即可降低自动化垃圾提交,详见设计文档 §2.15。 + website = serializers.CharField(required=False, allow_blank=True, default="") class LeadDeletionRequestSerializer(serializers.Serializer): diff --git a/apps/forms/tests.py b/apps/forms/tests.py index f1a1d11..c89b048 100644 --- a/apps/forms/tests.py +++ b/apps/forms/tests.py @@ -80,6 +80,26 @@ def test_lead_submit_requires_form_id(): assert response.status_code == 400 +def test_lead_submit_honeypot_filled_silently_rejects_without_creating_lead( + contact_form, +): + """蜜罐字段非空视为机器人:返回“成功”假象但不落库、不发通知邮件。""" + client = APIClient() + response = client.post( + "/api/v1/custom/leads/", + { + "form_id": contact_form.pk, + "data": {"name": "张三"}, + "website": "http://spam.example.com", + }, + format="json", + ) + + assert response.status_code == 201 + assert response.data["success"] is True + assert not Lead.objects.exists() + + # --- PIPL 数据删除权(详见设计文档 §2.15) --- diff --git a/apps/forms/views.py b/apps/forms/views.py index b8ca0ed..8d01c38 100644 --- a/apps/forms/views.py +++ b/apps/forms/views.py @@ -41,6 +41,13 @@ class LeadSubmitView(APIView): {"error": {"code": "NOT_FOUND", "message": "表单不存在"}}, status=404 ) + # 蜜罐反垃圾:honeypot 字段非空说明是自动化脚本提交,直接返回“成功”假象, + # 不落库、不发通知邮件,避免打草惊蛇(详见设计文档 §2.15 验证码/反垃圾方案)。 + if serializer.validated_data.get("website"): + return Response( + {"success": True, "message": form_def.success_message}, status=201 + ) + lead = Lead.objects.create( form=form_def, data=serializer.validated_data["data"], diff --git a/documents/设计方案分析与完善版.md b/documents/设计方案分析与完善版.md index 6e74ac7..768491d 100644 --- a/documents/设计方案分析与完善版.md +++ b/documents/设计方案分析与完善版.md @@ -348,7 +348,7 @@ CI 中要求单元测试覆盖率不低于 70%,核心 `apps/forms`(涉及线 - [x] 页脚展示备案号并链接工信部备案查询(`frontend/components/layout/Footer.tsx`,实际备案号 冀ICP备2025130506号-1) - [x] 隐私政策内容已上线(`apps.core.SimpleContentPage` 通用富文本内容页模型 + `seed_privacy_policy` 管理命令 + 前端 `/privacy-policy` 页面 + 页脚链接,内容覆盖 PIPL 要求的收集目的/用途/用户权利/Cookie 说明等) - [x] Cookie 同意条控件(`frontend/components/layout/CookieConsent.tsx`,`"use client"` 组件,首次访问展示"接受/拒绝"横幅,选择结果存入 `localStorage`,已挂载到根 layout) -- [ ] 字体/地图/验证码/CDN 均替换为可在国内正常访问的服务 +- [x] 字体/地图/验证码/CDN 排查:字体已用系统字体栈(`frontend/app/layout.tsx`,未引入 `next/font/google` 等国外字体服务);地图/视频等嵌入类 Block 尚未开发,落地时需选用腾讯地图/高德地图、腾讯视频/B站等国内可访问服务替代 Google Maps/YouTube(见 §2.6 待办);验证码此前仅靠限流(`ScopedRateThrottle` forms scope 5/min)防护,未接入任何第三方验证码,现补充蜜罐反垃圾字段 `website`(`apps/forms/serializers.py`/`views.py` + `frontend/blocks/form/LeadForm.tsx`,正常用户不可见,机器人误填后静默返回"成功"但不落库不发邮件),无需接入极验/腾讯云验证码 API 即可降低自动化垃圾提交,未来如需更强防护可再接入国内验证码厂商;CDN 方面为 `production.py` 新增 `AWS_S3_CUSTOM_DOMAIN`(`OSS_CDN_DOMAIN` 环境变量)用于绑定阿里云/腾讯云 CDN 加速域名到 OSS/COS 源站,避免直接暴露源站地址;另确认 `sentry-sdk` 虽在 `requirements/production.txt` 中但代码里从未调用 `sentry_sdk.init()`,为休眠依赖,未来若启用需评估国内可访问性(自建 GlitchTip 或国内 APM 替代 sentry.io) - [x] 表单收集的个人信息支持删除请求(PIPL 数据主体权利):新增 `LeadDeletionRequest` 模型(`apps/forms/models.py`),匿名用户凭提交表单时留下的邮箱在 `/privacy-policy/delete-request` 页面发起申请(`POST /api/v1/custom/leads/deletion-requests/`,限流 forms scope 5/min,无论邮箱是否存在关联数据均返回相同提示以避免探测),系统发送含 24 小时有效 token 的确认邮件,用户在 `/privacy-policy/delete-confirm?token=` 页面点击确认后(`POST /api/v1/custom/leads/deletion-requests/confirm/`)按邮箱在 `Lead.data` 中做匹配并删除对应记录;`LeadDeletionRequest` 处理记录通过 Django Admin(只读)供合规审计;隐私政策页面已加入申请入口链接 ### 2.16 开发路线图(对齐当前仓库状态) @@ -370,7 +370,7 @@ CI 中要求单元测试覆盖率不低于 70%,核心 `apps/forms`(涉及线 - [x] `apps/forms`:`FormDefinition`/`FormDefinitionField`/`Lead`(Snippet 方式)+ 提交 API(`/api/v1/custom/leads/`,限流 + 邮件通知) - [x] Wagtail API v2 挂载、分页/过滤/排序(Wagtail 内置) - [x] Next.js 前端脚手架(独立仓库):Header/Footer/layout、首页、博客/产品/案例/解决方案列表与详情页、`BlockRenderer`、ISR + revalidate route,`lint`/`build` 已验证通过 -- [x] pytest 基础测试框架(pytest-django + factory_boy):`pytest.ini` + `conftest.py`(root_page/home_page fixtures),为 `apps/core`(SEOablePage 字段、FormBlock API 表示、SimpleContentPage 页面树、Snippet 模型与只读 API)、`apps/products`、`apps/cases`、`apps/forms`(线索提交 API、PIPL 数据删除权申请/确认流程)编写了基础单元测试,共 30 个用例均通过 +- [x] pytest 基础测试框架(pytest-django + factory_boy):`pytest.ini` + `conftest.py`(root_page/home_page fixtures),为 `apps/core`(SEOablePage 字段、FormBlock API 表示、SimpleContentPage 页面树、Snippet 模型与只读 API)、`apps/products`、`apps/cases`、`apps/forms`(线索提交 API、蜜罐反垃圾字段、PIPL 数据删除权申请/确认流程)编写了基础单元测试,共 31 个用例均通过 - [x] `apps/core.SimpleContentPage`:通用富文本法务/说明类页面模型,用于隐私政策等内容,配套 `seed_privacy_policy` management command 用于幂等创建/更新隐私政策页面(需在 HomePage 实例存在后手动运行) - [x] Snippet:`TeamMember`/`Testimonial`/`Partner`(简单 `@register_snippet`,均含 `order` 排序字段)、`NavigationMenu`+`NavigationMenuItem`(`ClusterableModel`+`Orderable`+`InlinePanel`,内部页面/外部链接二选一)、`SiteSettings`(`wagtail.contrib.settings` + `BaseSiteSetting`,公司信息/ICP备案/社交账号全局配置)均已创建;只读 API 挂载于 `/api/v1/custom/core/`(`team/`、`testimonials/`、`partners/`、`navigation/?name=`、`site-settings/`,限流 public 100/min);前端 `Header`/`Footer` 已接入 `NavigationMenu`/`SiteSettings`(接口不可用时回退静态内容),`TeamMember`/`Testimonial`/`Partner` 已提供 service 层,页面级展示留待对应 StreamField Block(见下)落地时接入 @@ -380,7 +380,7 @@ CI 中要求单元测试覆盖率不低于 70%,核心 `apps/forms`(涉及线 - [ ] RBAC 落地(Wagtail `Group` + `GroupPagePermission` 实际配置)未开始 - [ ] 测试覆盖率仍不完整(已有基础单元测试,但集成测试、前端组件测试、E2E 均未编写,当前无 CI 流水线) - [x] 生产安全 settings(§2.11 中 `SECURE_*`/CORS/CSRF 白名单等)已在 `production.py` 中逐项落实,并补充 `SECURE_PROXY_SSL_HEADER`(适配国内云厂商 SLB/CLB 边缘终止 TLS 场景);实际域名需在部署时通过 `.env` 填入 -- [x] ICP 备案、页脚备案号展示、隐私政策内容、Cookie 同意条控件、表单 PIPL 数据删除权支持均已完成(详见 §2.15),合规清单剩余“字体/地图/验证码/CDN 国内可访问”一项 +- [x] ICP 备案、页脚备案号展示、隐私政策内容、Cookie 同意条控件、表单 PIPL 数据删除权支持、字体/地图/验证码/CDN 国内可访问排查均已完成,§2.15 合规与本地化清单已全部完成 - [ ] 中文分词、OpenSearch 集成(Phase 2 提前项)未开始 ### 2.17 风险与备选方案 diff --git a/wagtailcms/settings/production.py b/wagtailcms/settings/production.py index f4bf187..3eb4722 100644 --- a/wagtailcms/settings/production.py +++ b/wagtailcms/settings/production.py @@ -40,6 +40,9 @@ AWS_ACCESS_KEY_ID = env("OSS_ACCESS_KEY_ID", default="") AWS_SECRET_ACCESS_KEY = env("OSS_SECRET_ACCESS_KEY", default="") AWS_STORAGE_BUCKET_NAME = env("OSS_BUCKET_NAME", default="") AWS_S3_ENDPOINT_URL = env("OSS_ENDPOINT_URL", default="") +# 国内 CDN 加速域名(阿里云 CDN / 腾讯云 CDN / 又拍云,绑定到 OSS/COS 源站后填入), +# 配置后媒体文件 URL 会替换成该加速域名,避免直接暴露 OSS 源站地址(详见 §2.15)。 +AWS_S3_CUSTOM_DOMAIN = env("OSS_CDN_DOMAIN", default="") LOGGING = { "version": 1,