# 企业官网 Headless CMS 设计方案 —— 分析与完善版 > 本文档基于 `documents/` 目录下现有的 5 份 PDF 设计文档整理分析,并结合当前代码仓库的真实状态(`manage.py` + 裸 Django 6.0 项目骨架,尚未引入 Wagtail),产出一份可直接指导开发的**完善版设计方案**。 --- ## 一、现有文档分析 ### 1.1 文档清单 | 文档 | 定位 | |---|---| | 基于 Wagtail 的现代前后端分离 CMS 设计方案.pdf | 通用架构设计(30 章节,最全面) | | Next.js + Wagtail 企业级 Headless CMS 完整项目代码架构.pdf | Monorepo + 代码架构细化 | | Wagtail CMS Backend 完整源码(企业级 Headless CMS).pdf | 后端代码骨架示例 | | Next.js 企业官网完整源码模板(基于 Wagtail Headless CMS).pdf | 前端代码骨架示例 | | 软件公司企业官网设计方案(基于 Wagtail Headless CMS).pdf | 官网 IA / 页面 / 视觉设计 | | 整体设计图.png | 首页高保真视觉稿(TechFlow 品牌示例) | ### 1.2 共性结论 5 份文档高度一致地收敛到同一技术选型:**Django 5 + Wagtail 7 + DRF + Strawberry GraphQL + Next.js 15 (React 19) + PostgreSQL + Redis + Celery + OpenSearch/Elasticsearch + MinIO/S3 + Docker/K8s**。这说明该技术栈选型是合理、成熟的,可以直接作为最终方案基线,**不需要重新论证**。 ### 1.3 已发现的不足 / 待完善点 逐份文档都以"罗列技术名词 + 目录结构"为主,**普遍缺少可落地的细节**,具体归纳如下: **A. 架构完整性缺口** 1. 只给出多租户"共享库 / Schema 隔离"两个方案名词,未给出本项目该如何取舍(本项目是单一企业官网,暂不需要多租户,过度设计)。 2. 没有数据库 ER 图 / 字段级设计,`Page` 模型都只写了 1-2 个示例字段。 3. 没有 Headless 预览(Preview)机制设计 —— 编辑在 Wagtail 后台点"预览",Next.js 如何展示草稿内容(draft mode / preview token)完全没有提及。 4. 没有"发布后前端缓存失效"的具体实现(只画了 `Content Published → Event Bus → Webhook → Frontend Revalidate` 的框图,没有落地到 Wagtail `page_published` signal + Next.js `revalidateTag` 的代码级方案)。 5. API 只给了 URL 列表,没有分页、排序、过滤、错误码、版本弃用策略等规范。 **B. 安全设计不落地** 6. 安全章节只列了名词(HTTPS/CSP/WAF/JWT Rotation…),没有给出 Django 层面应设置的具体 settings 项(`SECURE_HSTS_SECONDS`、`SESSION_COOKIE_SECURE`、`CSRF_TRUSTED_ORIGINS`、DRF 节流类等)。 7. `CORS_ALLOW_ALL_ORIGINS = True` 是示例代码里的真实隐患,生产环境必须收敛为白名单。 8. 没有考虑富文本 XSS(Wagtail RichTextField 需要配置白名单 `WAGTAILADMIN_RICH_TEXT_EDITORS` / `bleach`)。 **C. 本地化 / 合规缺失(重要)** 9. 所有文档默认使用 **AWS S3 + Cloudflare CDN + Google 字体生态**,但"整体设计图.png"高保真稿明确显示目标是**中国大陆企业官网**(简体中文导航、"沪ICP备xxxxxxxx号-1"、中国移动/招商银行/中国平安/上汽集团/美团 等客户 Logo)。这是一个关键但被忽视的落地风险: - AWS S3/Cloudflare 在中国大陆访问速度和合规性都存在问题,需替换为 **阿里云 OSS/腾讯云 COS + 阿里云 CDN/腾讯云 CDN** 等国内方案。 - 网站备案:需要 **ICP 备案**(个人/企业备案流程、幕布拍照要求等),这会影响上线时间表,必须提前规划。 - 需要 **隐私政策 / Cookie 同意条**,遵循《个人信息保护法》(PIPL),而不是文档里默认的 GDPR 话术。 - Google Fonts / Google Maps 在中国大陆不可用,需要自托管字体(思源黑体/HarmonyOS Sans 子集化)、替换为高德/腾讯地图。 - 人机验证码建议用极验/腾讯云验证码而非 reCAPTCHA。 - 表单营销自动化提到 HubSpot/Salesforce,国内客户更常用企业微信 + 探迹/纷享销客等 CRM,需要补充可选国内方案。 **D. 工程化缺口** 10. 没有测试策略(单元测试、集成测试、E2E),文档提到 `pytest` 仅在 lint 章节一笔带过。 11. 没有环境变量与密钥管理方案(`.env.example`、多环境 settings 分层的实际代码)。 12. 没有健康检查 / 就绪探针(`/healthz`、`/readyz`),K8s 部署缺了这个会导致滚动更新时无法探活。 13. 日志没有约定格式(结构化 JSON 日志 + request-id 贯穿链路),排障困难。 **E. 内容建模细节不足** 14. StreamField Block 只给了 `HeroBlock`、`FeatureBlock` 两个例子,官网设计方案里列出的 14 个 Block(Stats/CTA/Pricing/FAQ/CaseStudy/Timeline/Team/LogoCloud/TechStack/BlogList/Video/Form 等)都没有代码,容易导致前后端字段不对齐。 15. 没有说明图片如何做响应式(Wagtail `Renditions` + `srcset`)与前端 `next/image` 的对接方式。 ### 1.4 与当前代码仓库现状的差距 当前仓库仅是 `django-admin startproject` 生成的裸项目(Django 6.0.5,未安装 Wagtail/DRF,`INSTALLED_APPS` 只有 Django 内置应用,`urls.py` 只有 `admin/`)。也就是说,**目前处于"设计文档阶段",代码尚未开始落地**,因此完善设计的同时,应当同步启动 Phase 1 的工程骨架搭建(见本文档第二部分 §18 路线图,以及仓库中已完成的初始化改动)。 --- ## 二、完善后的整体设计方案 ### 2.1 项目目标 在原有目标基础上明确补充**中国大陆本地化与合规目标**: - 面向中国大陆企业客户的现代化 B2B 官网 + Headless CMS 内容中台 - 前后端彻底分离:Wagtail(内容与 API)+ Next.js(展示层) - 满足 ICP 备案、PIPL 合规、国内 CDN/存储加速 - 可平滑演进到多语言(中/英/日)、多站点,**暂不做多租户**(YAGNI,待 SaaS 化需求明确后再引入 `django-tenants`) - 内容编辑体验优先(StreamField 组件化、预览、审核流) ### 2.2 总体架构 ```mermaid flowchart TB subgraph Client Browser["浏览器 / 移动端"] end subgraph Edge["Edge / CDN"] CDN["国内 CDN(阿里云 CDN / 腾讯云 CDN)"] end subgraph FE["Next.js 前端(App Router)"] NextApp["React 19 + SSR/ISR"] end subgraph Gateway Nginx["Nginx / Traefik 网关"] end subgraph BE["Wagtail 后端"] Wagtail["Django 5 + Wagtail 7"] DRF["DRF REST v2"] GQL["Strawberry GraphQL"] end subgraph Infra["基础设施"] PG[(PostgreSQL)] Redis[(Redis)] Search[(OpenSearch)] OSS[(阿里云 OSS / 腾讯云 COS)] Celery["Celery Worker"] end Browser --> CDN --> NextApp --> Nginx --> Wagtail Wagtail --> DRF Wagtail --> GQL Wagtail --> PG Wagtail --> Redis Wagtail --> Search Wagtail --> OSS Wagtail -. 异步任务 .-> Celery Wagtail -- "publish webhook" --> NextApp ``` ### 2.3 技术栈(含国内替代方案对照) | 分层 | 通用方案(原文档) | 中国大陆推荐替代 / 补充 | |---|---|---| | 对象存储 | AWS S3 / MinIO | 阿里云 OSS / 腾讯云 COS(`django-storages` 均支持 S3 兼容协议) | | CDN | Cloudflare | 阿里云 CDN、腾讯云 CDN、又拍云 | | 字体 | Google Fonts | 思源黑体 / HarmonyOS Sans,构建期子集化后自托管,避免运行时依赖 Google | | 地图 | Google Maps | 高德地图 / 腾讯地图 JS SDK | | 验证码 | reCAPTCHA | 极验 GeeTest / 腾讯云验证码 | | 短信/通知 | 无 | 阿里云短信服务 + 企业微信机器人 Webhook(替代 Slack) | | 登录方式(可选) | OAuth2/Auth0/Keycloak | 保留通用 OAuth2;如需 C 端可加"企业微信扫码登录" | | 搜索 | Elasticsearch/OpenSearch | 同左,国内可用阿里云 OpenSearch 托管版或自建 | | 备案 | 无 | 需在页脚展示 ICP 备案号,域名/服务器需完成 ICP 备案后方可上线 | 其余后端(Django/Wagtail/DRF/GraphQL/PostgreSQL/Redis/Celery)与前端(Next.js/React/Tailwind/Zustand/React Query)技术栈维持原文档选型不变,属于合理选型。 ### 2.4 Monorepo 目录结构(沿用并细化) ``` wagtailcms/ # 当前仓库根目录(后端仓库,独立 git 仓库) ├── manage.py ├── requirements/ │ ├── base.txt │ ├── dev.txt │ └── production.txt ├── wagtailcms/ # 设置包(实际未按早期草案重命名为 config/,沿用项目同名包) │ ├── settings/ │ │ ├── base.py │ │ ├── dev.py │ │ └── production.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py ├── apps/ │ ├── core/ # 基础抽象:SEOablePage、健康检查、公共 Block ✅ │ ├── home/ # 首页 ✅ │ ├── blog/ # 技术博客 ✅ │ ├── products/ # 产品 ✅ │ ├── solutions/ # 解决方案(行业)✅ │ ├── cases/ # 客户案例 ✅ │ ├── forms/ # 线索表单 ✅ │ └── api/ # DRF + Wagtail API v2 路由 ✅ ├── documents/ # 设计文档(当前目录) └── (frontend/ 已拆分为独立仓库,不在本仓库内,见下方说明) ``` > 说明:前后端已**拆分为两个独立 git 仓库**(后端本仓库 + `frontend/` 独立仓库),通过 REST API(Wagtail API v2 + `/api/v1/custom/`)契约解耦。后端根 `.gitignore` 已排除 `frontend/`,避免嵌套仓库冲突。两个仓库目前均为本地仓库,尚未配置远程/推送。 ### 2.5 内容模型与数据库设计 在原有 `Page + StreamField` 思路基础上,补齐字段级设计: ```python # apps/core/models.py class SEOablePage(Page): """所有业务页面的抽象基类,统一 SEO 字段""" seo_title = models.CharField(max_length=70, blank=True) seo_description = models.CharField(max_length=160, blank=True) og_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) canonical_url = models.URLField(blank=True) schema_json = models.JSONField(blank=True, default=dict) promote_panels = Page.promote_panels + [ FieldPanel("seo_title"), FieldPanel("seo_description"), FieldPanel("og_image"), FieldPanel("canonical_url"), FieldPanel("schema_json"), ] class Meta: abstract = True ``` 页面树结构(细化到本项目实际官网 IA): ``` HomePage ├── ProductIndexPage │ └── ProductPage[] ├── SolutionIndexPage │ └── SolutionPage[] ├── CaseStudyIndexPage │ └── CaseStudyPage[] ├── BlogIndexPage │ └── BlogPage[] ├── AboutPage ├── CareerIndexPage │ └── CareerPage[] └── ContactPage ``` Snippet(非页面树内容,用于跨页面复用):`TeamMember`、`Testimonial`、`Partner/LogoCloud`、`NavigationMenu`、`SiteSettings`(`BaseSiteSetting`,存放公司电话/ICP备案号/社交账号等全局配置)。 ### 2.6 StreamField 组件体系(补全清单) | Block | 用途 | 关键字段 | 状态 | |---|---|---|---| | HeroBlock | 首屏 | title, subtitle, cta_buttons(list), background_image/video | ✅ 已实现 | | StatsBlock | 数据展示 | items: [{value, label}] | ✅ 已实现 | | FeatureGridBlock | 能力/优势 | icon, title, description | ✅ 已实现(对应原 FeatureBlock) | | ProductCardBlock | 产品矩阵 | title, description, image, link | ✅ 已实现 | | LogoCloudBlock | 客户 Logo 墙 | logos: [{url, title}](暂为图片列表,未接入 Partner Snippet) | ✅ 已实现(简化版) | | CaseStudyBlock | 案例卡片 | case_page: PageChooser(cases.CaseStudyPage), summary | ✅ 已实现 | | PricingBlock | 定价 | plans: [{name, price, features, highlighted, button}] | ✅ 已实现 | | FAQBlock | 常见问题 | items: [{question, answer(richtext)}] | ✅ 已实现 | | TimelineBlock | 发展历程 | items: [{year, event}] | ✅ 已实现 | | TeamBlock | 团队展示 | members: [SnippetChooser(TeamMember)],`get_api_representation` 展开完整字段(name/role/bio/photo)供前端渲染 | ✅ 已实现(依赖 TeamMember Snippet) | | TechStackBlock | 技术架构图 | items: [{icon, label}] | ✅ 已实现 | | VideoBlock | 视频 | video_url(建议自建 OSS/COS 直链或腾讯视频/哔哩哔哩等国内平台嵌入地址),poster | ✅ 已实现 | | FormBlock | 线索表单 | form: SnippetChooser(forms.FormDefinition),`get_api_representation` 展开完整字段定义供前端渲染 | ✅ 已实现 | | CTABlock | 行动号召 | heading, button_text, button_link | ✅ 已实现 | 所有 Block 统一放在 `apps/core/blocks.py` 的 `COMMON_BLOCKS`,前端 `frontend/blocks/BlockRenderer.tsx` 维护对应的"type → 组件"映射表,目前两者手动保持一致;CI 自动化一致性校验仍待建立(见 §2.17 风险表)。 ### 2.7 Headless API 设计规范(补全缺失细节) **REST(Wagtail API v2 + DRF 自定义)** - 统一前缀:`/api/v2/`(Wagtail 官方 API v2)+ `/api/v1/custom/`(自定义业务接口,如表单提交) - 分页:统一 `limit` / `offset`,响应体包含 `meta: {total, limit, offset}` - 排序/过滤:`?order=-first_published_at`、`?type=blog.BlogPage` - 错误响应统一结构: ```json { "error": { "code": "NOT_FOUND", "message": "..." } } ``` - 限流:DRF `ScopedRateThrottle`,公开只读接口 `100/min`,表单提交接口 `5/min`(防刷) - 版本弃用策略:新版本上线后旧版本保留至少 2 个发布周期,响应头带 `Deprecation` / `Sunset` **预览模式(已实现,详见 §2.16)** - 后端引入 `wagtail-headless-preview`(0.9.0),`SEOablePage` 混入 `HeadlessPreviewMixin`,编辑器点击"预览"时自动将草稿序列化存入 `PagePreview` 表,生成带签名 token 的预览链接并重定向到 `{FRONTEND_BASE_URL}/preview` - 自定义 `PagePreviewAPIViewSet`(`apps/api/urls.py`)挂载于 `/api/v2/page_preview/`,根据 `content_type`+`token` 从 `PagePreview` 取回草稿快照并序列化返回 - Next.js `/preview` Route Handler 校验 token 有效后开启内置 Draft Mode(cookie 方式,非 Redis),并重定向到对应内容类型的前端路由;页面组件通过 `getPreviewOrFallback` 优先拉取草稿,否则回退到正常已发布内容 **发布后缓存失效(原文档只有框图,这里补齐实现)** ```python # apps/core/signals.py from wagtail.signals import page_published @receiver(page_published) def notify_frontend_revalidate(sender, instance, **kwargs): requests.post( settings.FRONTEND_REVALIDATE_URL, json={"tags": [f"page:{instance.id}", instance.url_path]}, headers={"Authorization": f"Bearer {settings.REVALIDATE_SECRET}"}, timeout=3, ) ``` Next.js 侧对应 `revalidateTag` API Route,收到 webhook 后失效对应 ISR 缓存标签。 ### 2.8 权限体系(RBAC 矩阵,细化落地) | 角色 | 页面编辑 | 提交审核 | 审批发布 | 用户管理 | Snippet 管理 | Django Admin | |---|---|---|---|---|---|---| | Super Admin | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Tenant/Site Admin | ✅ | ✅ | ✅ | ✅(本站点) | ✅ | ❌ | | Editor | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | | Reviewer | 查看 | ✅ | ✅ | ❌ | 查看 | ❌ | | Author | ✅(仅自己) | ✅ | ❌ | ❌ | ❌ | ❌ | | Viewer | 查看 | ❌ | ❌ | ❌ | ❌ | ❌ | 落地方式:使用 Wagtail 原生 `Group` + `GroupPagePermission` + `collections` 权限,不需要额外引入 RBAC 框架。 **已落地(`python manage.py setup_rbac_groups`,幂等命令,`apps/core/management/commands/setup_rbac_groups.py`):** - Super Admin 直接对应 Django `is_superuser=True`,不建单独 Group;其余 5 个角色对应 5 个 Wagtail `Group`:站点管理员/编辑/审核员/作者/查看者。 - 页面权限(`GroupPagePermission`,挂载于页面树根节点 `depth=1`,适用于当前单站点部署):站点管理员拥有全部类型(add/change/publish/delete/bulk_delete/lock/unlock/view);编辑拥有 add/change/view(无 publish,创建的页面停留在草稿态等待发布);审核员拥有 change/publish/view(用于打开页面并执行发布,属于对“页面编辑=查看”矩阵条目的必要放宽);**作者仅拥有 add/view(不含 change)**——利用 Wagtail 内置 `OwnershipPermissionPolicy`:只有 `add` 权限时,用户可创建新页面,且只能编辑/删除自己拥有(`owner`)的页面,天然满足“仅自己”要求,无需自定义 `wagtail_hooks`;查看者仅有 view。 - Snippet 权限(`TeamMember`/`Testimonial`/`Partner`/`NavigationMenu`/`FormDefinition`):站点管理员与编辑均为 add/change/delete/view,审核员仅 view,作者/查看者无权限。`Lead`(线索,含 PIPL 个人信息)出于数据保护考虑做了比矩阵更严格的收紧:仅站点管理员可管理,审核员仅 view,编辑/作者均不授予。`SiteSettings` 作为站点级单例配置仅站点管理员可 change/view。 - Collection(图片/文档)权限(`GroupCollectionPermission`,挂载于根 Collection):站点管理员 add/change/delete/choose/view;编辑与作者 add/change/choose/view(便于上传 StreamField 图片);审核员 choose/view;查看者无。 - 用户管理权限(`auth.add_user`/`change_user`/`delete_user`/`view_user`)仅授予站点管理员,对应矩阵“用户管理”列;“Django Admin”列不通过 Group 授予(Django Admin 访问由账号的 `is_staff` 属性决定,仅超级用户具备)。 - 简化说明:设计方案 §2.9 的 Workflow 审核模块尚未实现,因此“提交审核/审批发布”当前通过“编辑者无 publish、审核员/站点管理员有 publish”的权限差异形成审核闸门,而非正式的 Wagtail `Workflow`/`TaskState`;待 Workflow 模块落地后可在现有 Group 基础上叠加 `GroupApprovalTask`,无需重新设计权限组。 - 测试:`apps/core/tests.py` 新增 7 个用例覆盖 Group 创建、`access_admin` 授予、页面权限矩阵、Author 仅编辑自己页面、Lead 权限收紧、用户管理权限范围、命令幂等性。 ### 2.9 工作流与内容审核 采用 Wagtail 5+ 内置 `Workflow` 模块,定义 `草稿 → 编辑自检 → 审核 → 通过 → 定时发布`,并在 `TaskState` 变更时通过企业微信机器人 Webhook 通知责任人(替代原文档 Slack 方案)。 ### 2.10 搜索、SEO、多语言 - 搜索:中文分词是关键缺失点,OpenSearch 需配置 **IK 分词插件**(原文档完全没提中文分词,这是国内落地必须解决的问题)。调研结论见下方小节。 - SEO:站点级 `sitemap.xml`(`wagtail.contrib.sitemaps`)+ 页面级 `schema_json`(JSON-LD)+ `robots.txt` 动态生成。 - 多语言:`WAGTAIL_I18N_ENABLED` + `wagtail-localize`,URL 采用 `/zh/`、`/en/` 前缀,默认 `/zh/` 对国内用户免前缀(根路径直接是中文)。 #### OpenSearch + 中文分词调研(Phase 2 提前调研,新增) - **Wagtail 原生已支持 OpenSearch**:本仓库安装的 Wagtail 7.4.2 底层依赖 `django-modelsearch`,已内置 `wagtail.search.backends.opensearch2`(OpenSearch 2.x)与 `opensearch3`(OpenSearch 3.x)两个后端模块,配置方式与 Elasticsearch 后端一致,**不需要任何第三方 Wagtail 插件**: ```python WAGTAILSEARCH_BACKENDS = { "default": { "BACKEND": "wagtail.search.backends.opensearch2", "URLS": [env("OPENSEARCH_URL", default="http://localhost:9200")], "INDEX_PREFIX": "wagtailcms_", } } ``` 需额外安装客户端包 `pip install "opensearch-py>=2,<3"`(版本需与 OpenSearch 服务端大版本一致)。**此配置尚未写入 settings**,留待真正接入、且本地/测试环境有可用 OpenSearch 服务时再启用,避免开发环境无 OpenSearch 服务导致搜索功能报错。 - **中文分词方案**:OpenSearch 官方未内置中文分词器,采用社区插件 [infinilabs/analysis-ik](https://github.com/infinilabs/analysis-ik)(同一套插件同时支持 Elasticsearch 和 OpenSearch,Apache-2.0 协议,17k+ star,长期维护),提供 `ik_smart`(粗粒度,适合短语查询)与 `ik_max_word`(细粒度,适合词项查询)两个分析器,典型字段 mapping 为 `"analyzer": "ik_max_word", "search_analyzer": "ik_smart"`。Wagtail 侧可通过 `WAGTAILSEARCH_BACKENDS['default']['INDEX_SETTINGS']` 覆盖默认 `analysis.analyzer.default` 让 Wagtail 自动生成的索引改用 IK 分词,无需改 Wagtail 源码。 - ⚠️ **插件版本必须与 OpenSearch 核心版本完全一致**才能加载成功,接入前需在 https://release.infinilabs.com/ 或用 `bin/opensearch-plugin install https://get.infini.cloud/opensearch/analysis-ik/` 确认存在对应构建。 - **本地 PoC 脚手架(已创建,未在本环境验证)**:新增 [docker-compose.opensearch.yml](../docker-compose.opensearch.yml) + [docker/opensearch/Dockerfile](../docker/opensearch/Dockerfile),基于官方 `opensearchproject/opensearch:2.19.6` 镜像叠加 IK 插件安装,`plugins.security.disabled=true` 仅用于本地免证书调试。**当前开发环境未安装 Docker CLI,本次调研未能实际拉起容器验证**,需在具备 Docker 的机器上执行 `docker-compose -f docker-compose.opensearch.yml up -d`,并完成以下验证步骤后再正式接入: 1. `curl http://localhost:9200/_cat/plugins` 确认 `analysis-ik` 已加载; 2. 安装 `opensearch-py`,在 settings 中启用上面的 `opensearch2` 后端配置; 3. `python manage.py update_index` 重建索引,用中文短语(如“产品案例”)人工验证检索召回是否符合预期的分词粒度。 - **生产替代方案**:若不想自建/自运维 OpenSearch 集群(插件升级、扩缩容等),可评估阿里云“开放搜索 OpenSearch 版”或腾讯云 ES Serverless 的托管中文分词能力,二者原生支持中文分词、无需自装 IK 插件,代价是绑定云厂商,需按实际预算/团队运维能力二选一(已同步写入 §2.17 风险表)。 ### 2.11 安全设计(落地到具体配置) ```python # config/settings/production.py SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_CONTENT_TYPE_NOSNIFF = True X_FRAME_OPTIONS = "DENY" # 阿里云 SLB / 腾讯云 CLB 等负载均衡在边缘终止 TLS 时,需要此项避免 SECURE_SSL_REDIRECT 死循环重定向 SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS", default=[]) CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[]) REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": ["rest_framework.throttling.ScopedRateThrottle"], "DEFAULT_THROTTLE_RATES": {"public": "100/min", "forms": "5/min"}, } ``` 富文本安全:`WAGTAILADMIN_RICH_TEXT_EDITORS` 限定可用 feature(禁用危险 HTML embed),后端渲染前统一走 `bleach` 白名单清洗。 **白名单配置说明**(三者含义不同,勿混淆): - `ALLOWED_HOSTS`:填**后端自身**域名(如 `cms.example.com`),校验 HTTP `Host` 头,防 Host 头注入。 - `CORS_ALLOWED_ORIGINS`:填 **Next.js 前端**域名(如 `https://www.example.com`),允许浏览器跨域调用 Wagtail REST API;`/api/v1/custom/leads/` 为匿名公开接口,仅受此白名单 + 限流保护,不受 CSRF 校验。 - `CSRF_TRUSTED_ORIGINS`:仅在"浏览器带 session Cookie 发起跨站不安全请求"时生效(如已登录编辑者调用需认证的预览/管理接口),填涉及认证态跨域请求的域名。 - 三者均通过 `.env` 中的 `ALLOWED_HOSTS`/`CORS_ALLOWED_ORIGINS`/`CSRF_TRUSTED_ORIGINS` 环境变量注入,部署时按实际域名填写即可,无需改代码(参见 `.env.example`)。 ### 2.12 可观测性与运维 - 健康检查:`/healthz`(存活)、`/readyz`(含数据库/Redis 连通性检查),供 K8s liveness/readiness 探针使用。 - 日志:`structlog` 输出 JSON,贯穿 `request_id`(中间件生成,透传前端 `X-Request-Id`)。 - 监控:Prometheus + Grafana(`django-prometheus`),错误监控 Sentry,链路追踪 OpenTelemetry。 ### 2.13 测试策略(原文档缺失,新增) | 层级 | 工具 | 覆盖内容 | |---|---|---| | 单元测试 | pytest + pytest-django + factory_boy | Model、Block、Serializer | | 集成测试 | pytest + DRF APIClient | API 端到端响应结构 | | 前端组件测试 | Vitest + React Testing Library | Block 组件渲染 | | E2E | Playwright | 关键转化路径(首页 → 联系我们 → 表单提交) | | 契约测试 | schemathesis(基于 OpenAPI schema) | 防止后端 API 破坏性变更 | CI 中要求单元测试覆盖率不低于 70%,核心 `apps/forms`(涉及线索转化)不低于 90%。 ### 2.14 CI/CD 与环境管理 - `.env.example` 统一列出所有必需环境变量,密钥类使用 GitHub Actions Secrets / 云厂商 KMS,不进仓库。 - 分支策略:`main`(生产)/ `staging`(预发)/ 功能分支 PR,合并前必须通过 Lint + Test + Build。 - 部署流水线:`Lint → Test → Build Image → 推送镜像仓库(阿里云 ACR/腾讯云 TCR)→ 部署 K8s(或轻量场景用 Docker Compose + 云服务器)`。 ### 2.15 合规与本地化清单(新增,落地检查表) - [x] 完成 ICP 备案(域名 + 服务器均已在国内云厂商完成备案) - [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) - [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 开发路线图(对齐当前仓库状态) | 阶段 | 目标 | 状态 | |---|---|---| | **Phase 0** | 需求与设计文档完善(本文档) | ✅ 已完成 | | **Phase 1** | Wagtail 基础 CMS + REST API + Next.js 首页渲染 + 基础 SEO | 🚧 进行中(进度见下方清单) | | **Phase 2** | GraphQL、OpenSearch 中文搜索、工作流审核、多语言 | 待规划 | | **Phase 3** | AI 能力(摘要/翻译/SEO 重写)、SaaS 化评估(视业务需要再决定是否引入多租户) | 待规划 | **Phase 1 详细进度:** 已完成: - [x] 后端 Django + Wagtail 项目骨架,分层 settings(base/dev/production),独立 git 仓库并已提交 - [x] `apps/core`:`SEOablePage` 抽象基类、`COMMON_BLOCKS`(14/14 个 Block 已全部实现,见 §2.6)、健康检查 `/healthz` `/readyz`、发布后 `page_published` signal → 前端 revalidate webhook - [x] `apps/home`、`apps/blog`(含标签 `ClusterTaggableManager`) - [x] `apps/products`(ProductIndexPage/ProductPage)、`apps/cases`(CaseStudyIndexPage/CaseStudyPage)、`apps/solutions`(SolutionIndexPage/SolutionPage) - [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 数据删除权申请/确认流程)编写了基础单元测试,共 31 个用例均通过 - [x] `apps/core.SimpleContentPage`:通用富文本法务/说明类页面模型,用于隐私政策等内容,配套 `seed_privacy_policy` management command 用于幂等创建/更新隐私政策页面(需在 HomePage 实例存在后手动运行) - [x] 部署引导 management command `bootstrap_deployment`:读取 `DJANGO_SUPERUSER_USERNAME`/`_EMAIL`/`_PASSWORD` 三个环境变量,若均已设置且用户名不存在则创建首个超级管理员账号(已存在则跳过,不覆盖密码),随后自动调用 `setup_rbac_groups` 初始化 RBAC 权限组;幂等,可放入部署脚本/容器启动流程的 `migrate` 之后执行,解决“首次部署后如何登录 Wagtail 后台”的鸡生蛋问题;`.env.example` 已补充对应三项环境变量说明;新增 4 个单元测试覆盖建号/跳过/不覆盖密码/联动 RBAC 四种场景 - [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(见下)落地时接入 - [x] 预览模式:后端引入 `wagtail-headless-preview`(0.9.0),`SEOablePage` 混入 `HeadlessPreviewMixin`(自动覆盖全部 6 个页面模型),新增自定义 `PagePreviewAPIViewSet` 挂载于 `/api/v2/page_preview/`;前端新增 `services/preview.service.ts`(`getPreviewOrFallback`/`fetchPreviewPage`/`resolvePreviewPath`)、`/preview`与 `/preview/disable` 两个 Route Handler(基于 Next.js Draft Mode + httpOnly cookie),并在首页/博客/产品/案例/解决方案详情页接入草稿优先逻辑,`layout.tsx` 新增预览模式提示条(含退出链接);`lint`/`build` 均验证通过,后端 31 个现有用例均通过(本次未新增自动化测试,仅依靠人工验证) - [x] 剩余 6 个 StreamField Block(`ProductCardBlock`/`PricingBlock`/`TimelineBlock`/`TeamBlock`/`TechStackBlock`/`VideoBlock`)已全部开发完成:`apps/core/blocks.py` 新增对应 Block 类并扩充 `COMMON_BLOCKS`(现共 14 个内容 Block + 1 个 `richtext`),因 Wagtail StreamField 会将 Block 结构写入迁移(`use_json_field=True` 不代表可跳过迁移),已为 blog/cases/home/products/solutions 五个 Page 模型生成并应用 `alter field body` 迁移;`TeamBlock` 复用 `FormBlock` 的 `get_api_representation` 覆写模式,新增 `TeamMemberChooserBlock(SnippetChooserBlock)` 展开 `TeamMember` 完整字段(含 `serialize_image` 复用),并补充对应的空值/展开单元测试(新增 2 个用例,后端共 33 个测试全部通过);前端 `types/wagtail.ts` 新增 6 个 Block 值类型与 `StreamFieldBlock` 联合类型分支,新建 6 个渲染组件(`ProductCardGrid`/`Pricing`/`Timeline`/`Team`/`TechStack`/`Video`)并接入 `BlockRenderer.tsx` 映射表,`lint`/`build` 均验证通过 - [x] RBAC 落地:`python manage.py setup_rbac_groups` 幂等命令创建 5 个 Wagtail Group(站点管理员/编辑/审核员/作者/查看者)并按 §2.8 矩阵配置 `GroupPagePermission`/`GroupCollectionPermission`/Snippet 与用户管理 `Permission`;Author“仅自己”利用 Wagtail 内置 `OwnershipPermissionPolicy`(仅 add 权限)原生实现,无需自定义 hook;新增 7 个后端单元测试,全仓库 pytest 从 33 增至 40,全部通过 未完成(待规划排期): - [ ] 测试覆盖率持续提升中:后端 pytest 用例从 40 个增至 65 个,新增覆盖 `apps/blog`(标签、列表排序/草稿过滤)、`apps/solutions`(页面树、草稿过滤)、`apps/home`(`HomePage.subpage_types` 回归测试,覆盖此前"后台无法添加子页面"的 bug 场景)、`apps/core` 健康检查端点 `/healthz`/`/readyz`、发布后 revalidate webhook 信号(含正常/未配置/网络异常三种路径)、`apps/api` 预览接口 `page_preview`(含无效 token/content_type 场景,过程中顺带修复了一个真实 bug:无效 token 之前会 500 而非文档承诺的 404)、`bootstrap_deployment` 部署引导命令(建号/跳过/不覆盖密码/联动 RBAC);但集成测试、前端组件测试、E2E 均未编写,当前无 CI 流水线 - [x] 生产安全 settings(§2.11 中 `SECURE_*`/CORS/CSRF 白名单等)已在 `production.py` 中逐项落实,并补充 `SECURE_PROXY_SSL_HEADER`(适配国内云厂商 SLB/CLB 边缘终止 TLS 场景);实际域名需在部署时通过 `.env` 填入 - [x] ICP 备案、页脚备案号展示、隐私政策内容、Cookie 同意条控件、表单 PIPL 数据删除权支持、字体/地图/验证码/CDN 国内可访问排查均已完成,§2.15 合规与本地化清单已全部完成 - [ ] 中文分词、OpenSearch 集成(Phase 2 提前项):**调研已完成**(结论见 §2.10 “OpenSearch + 中文分词调研”小节:确认 Wagtail 7.4.2 原生内置 `opensearch2`/`opensearch3` 后端 + infinilabs `analysis-ik` 插件的可行方案,并新增本地 Docker Compose PoC 脚手架 `docker-compose.opensearch.yml`),**实际接入(拉起容器验证插件加载、切换 `WAGTAILSEARCH_BACKENDS`、建站内容重建索引)仍未开始**,待具备 Docker 环境后再验证落地 ### 2.17 风险与备选方案 | 风险 | 应对 | |---|---| | 过早引入多租户/微服务导致过度设计 | Phase 1/2 保持 Monolith,指标驱动再拆分 | | 国内备案周期拖慢上线(通常 7-20 个工作日) | 项目启动即刻同步提交备案,不等开发完成 | | Elasticsearch/OpenSearch 中文分词效果不佳 | 提前验证 IK 分词插件,或评估阿里云开放搜索托管方案 | | StreamField Block 前后端字段不一致 | 建立 Block Schema 自动化一致性校验(CI) |