
# 企业官网 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/<version>` 确认存在对应构建。
- **本地 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） |
