Files
novalon-website/docs/cms/api-contract.md
T
张翔 636bc4ecde docs(test): 添加设计文档、测试规范与 E2E 测试套件
- 新增 ADR 架构决策记录 (Design DNA 集成与深化)
- 新增 CMS 系统设计文档
- 新增实施计划文档 (Phase1-3)
- 新增 Bain 品牌升级设计规格
- 新增 E2E 分层测试套件 (P1-P4)
- 新增视觉回归测试配置
- 新增光效分析、视觉验证等辅助脚本
- 更新验收测试报告
2026-07-07 06:54:25 +08:00

25 KiB
Raw Blame History

Novalon CMS 后端 API 设计规范

版本v1.1
日期2026-07-02
技术栈Java 21 + Spring Boot WebFlux + PostgreSQL + Flyway
前端对接Next.js 14 + CMS SDKsrc/lib/cms/client.ts


一、总体设计原则

1.1 API 风格

  • 风格RESTful API
  • 数据格式JSON
  • 字符编码UTF-8
  • 时间格式ISO 86012024-01-01T00:00:00Z
  • 统一响应结构
{
  "code": 0,
  "message": "success",
  "data": {},
  "timestamp": "2024-01-01T00:00:00Z"
}
字段 类型 说明
code int 业务状态码,0 表示成功
message string 提示信息
data object/array/null 响应数据
timestamp string 服务器时间

1.2 分页统一格式

请求参数

参数 类型 默认值 说明
page int 1 页码,从 1 开始
pageSize int 20 每页数量,最大 100
sortBy string created_at 排序字段
sortOrder string desc 排序方向:asc/desc

响应结构

{
  "code": 0,
  "message": "success",
  "data": {
    "items": [],
    "total": 100,
    "page": 1,
    "pageSize": 20,
    "totalPages": 5
  },
  "timestamp": "2024-01-01T00:00:00Z"
}

1.3 错误码规范

错误码 说明 HTTP 状态码
0 成功 200
40000 请求参数错误 400
40100 未授权 401
40300 无权限 403
40400 资源不存在 404
40900 资源冲突(如 code 重复) 409
50000 服务器内部错误 500

二、数据库表结构

2.1 cms_content_model(内容模型表)

CREATE TABLE cms_content_model (
  id BIGSERIAL PRIMARY KEY,
  code VARCHAR(100) NOT NULL UNIQUE,
  name VARCHAR(200) NOT NULL,
  description VARCHAR(500),
  fields_json JSONB NOT NULL DEFAULT '[]'::jsonb,
  is_page_type BOOLEAN NOT NULL DEFAULT FALSE,
  url_pattern VARCHAR(200),
  has_versions BOOLEAN NOT NULL DEFAULT TRUE,
  has_draft BOOLEAN NOT NULL DEFAULT TRUE,
  icon VARCHAR(50),
  created_by VARCHAR(100) NOT NULL,
  updated_by VARCHAR(100) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted BOOLEAN NOT NULL DEFAULT FALSE
);

CREATE INDEX idx_cms_content_model_code ON cms_content_model(code) WHERE deleted = FALSE;

2.2 cms_content_item(内容项表)

CREATE TABLE cms_content_item (
  id BIGSERIAL PRIMARY KEY,
  model_id BIGINT NOT NULL REFERENCES cms_content_model(id),
  model_code VARCHAR(100) NOT NULL,
  title VARCHAR(500) NOT NULL,
  slug VARCHAR(200),
  status VARCHAR(20) NOT NULL DEFAULT 'draft',
  data_json JSONB NOT NULL DEFAULT '{}'::jsonb,
  version INT NOT NULL DEFAULT 1,
  published_at TIMESTAMP,
  created_by VARCHAR(100) NOT NULL,
  updated_by VARCHAR(100) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted BOOLEAN NOT NULL DEFAULT FALSE
);

CREATE UNIQUE INDEX idx_cms_content_item_model_slug 
  ON cms_content_item(model_code, slug) WHERE deleted = FALSE;
CREATE INDEX idx_cms_content_item_model_status 
  ON cms_content_item(model_code, status, created_at DESC) WHERE deleted = FALSE;
CREATE INDEX idx_cms_content_item_data ON cms_content_item USING GIN (data_json) WHERE deleted = FALSE;

2.3 cms_content_version(内容版本表)

CREATE TABLE cms_content_version (
  id BIGSERIAL PRIMARY KEY,
  item_id BIGINT NOT NULL REFERENCES cms_content_item(id),
  version INT NOT NULL,
  data_json JSONB NOT NULL,
  status VARCHAR(20) NOT NULL,
  change_log VARCHAR(500),
  created_by VARCHAR(100) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_cms_content_version_item 
  ON cms_content_version(item_id, version DESC);

2.4 cms_content_zone(内容区域表)

CREATE TABLE cms_content_zone (
  id BIGSERIAL PRIMARY KEY,
  code VARCHAR(100) NOT NULL UNIQUE,
  name VARCHAR(200) NOT NULL,
  description VARCHAR(500),
  page_code VARCHAR(100),
  allowed_models VARCHAR(500),
  items_json JSONB NOT NULL DEFAULT '[]'::jsonb,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_cms_content_zone_page ON cms_content_zone(page_code);

2.5 cms_theme_config(主题配置表)

CREATE TABLE cms_theme_config (
  id BIGSERIAL PRIMARY KEY,
  config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
  updated_by VARCHAR(100) NOT NULL,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- 初始插入一条默认记录(id=1)
INSERT INTO cms_theme_config (id, config_json, updated_by) 
VALUES (1, '{}', 'system');

2.6 cms_media_asset(媒体资源表)

CREATE TABLE cms_media_asset (
  id BIGSERIAL PRIMARY KEY,
  name VARCHAR(200) NOT NULL,
  path VARCHAR(500) NOT NULL,
  url VARCHAR(500) NOT NULL,
  mime_type VARCHAR(100) NOT NULL,
  size BIGINT NOT NULL,
  width INT,
  height INT,
  alt VARCHAR(200),
  storage_type VARCHAR(20) NOT NULL DEFAULT 'local',
  created_by VARCHAR(100) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted BOOLEAN NOT NULL DEFAULT FALSE
);

CREATE INDEX idx_cms_media_mime ON cms_media_asset(mime_type) WHERE deleted = FALSE;

2.7 cms_audit_log(审计日志表)

CREATE TABLE cms_audit_log (
  id BIGSERIAL PRIMARY KEY,
  module VARCHAR(50) NOT NULL,
  target_id BIGINT NOT NULL,
  action VARCHAR(50) NOT NULL,
  operator VARCHAR(100) NOT NULL,
  before_json JSONB,
  after_json JSONB,
  ip VARCHAR(50),
  user_agent VARCHAR(500),
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_cms_audit_module ON cms_audit_log(module, target_id, created_at DESC);
CREATE INDEX idx_cms_audit_operator ON cms_audit_log(operator, created_at DESC);

三、API 接口详细设计

3.1 内容模型管理

3.1.1 获取内容模型列表

GET /api/cms/models

查询参数

参数 类型 必填 说明
page int 页码,默认 1
pageSize int 每页数量,默认 20
keyword string 搜索关键词(按 name/code 搜索)
isPageType boolean 是否页面类型

响应 data:分页 + ContentModel 数组

ContentModel 结构

{
  "id": "1",
  "code": "case-study",
  "name": "案例研究",
  "description": "客户成功案例",
  "fields": [...],
  "isPageType": true,
  "urlPattern": "/cases/{slug}",
  "hasVersions": true,
  "hasDraft": true,
  "icon": "briefcase",
  "createdAt": "2024-01-01T00:00:00Z",
  "updatedAt": "2024-01-01T00:00:00Z"
}

3.1.2 获取单个内容模型

GET /api/cms/models/{code}

路径参数code - 模型编码

响应 dataContentModel 对象

3.1.3 创建内容模型

POST /api/cms/models

请求体

{
  "code": "case-study",
  "name": "案例研究",
  "description": "客户成功案例",
  "fields": [...],
  "isPageType": true,
  "urlPattern": "/cases/{slug}",
  "hasVersions": true,
  "hasDraft": true,
  "icon": "briefcase"
}

响应 data:创建后的 ContentModel 对象

3.1.4 更新内容模型

PUT /api/cms/models/{code}

请求体:同创建接口

响应 data:更新后的 ContentModel 对象

3.1.5 删除内容模型

DELETE /api/cms/models/{code}

说明:逻辑删除,同时删除该模型下的所有内容项(逻辑删除)


3.2 内容项管理

3.2.1 分页查询内容项

GET /api/cms/items

查询参数

参数 类型 必填 说明
modelCode string 内容模型编码
status string 状态过滤:draft/review/published/archived
page int 页码
pageSize int 每页数量
sortBy string 排序字段,默认 created_at
sortOrder string 排序方向,默认 desc
search string 搜索关键词(按 title
filters object 动态字段过滤,JSON 格式

响应 data:分页 + ContentItem 数组

ContentItem 结构

{
  "id": "1",
  "modelId": "1",
  "modelCode": "case-study",
  "title": "某制造企业 ERP 升级",
  "slug": "manufacturing-erp-upgrade",
  "status": "published",
  "data": {
    "client": "某上市制造企业",
    "industry": "制造业",
    "metrics": [...]
  },
  "version": 3,
  "publishedAt": "2024-01-15T10:00:00Z",
  "createdBy": "admin",
  "updatedBy": "admin",
  "createdAt": "2024-01-01T00:00:00Z",
  "updatedAt": "2024-01-15T10:00:00Z"
}

3.2.2 获取内容项详情

GET /api/cms/items/{id}

路径参数id - 内容项 ID

响应 dataContentItem 对象

3.2.3 按 slug 获取内容项

GET /api/cms/items/slug/{modelCode}/{slug}

路径参数

  • modelCode - 模型编码
  • slug - 内容 slug

响应 dataContentItem 对象

3.2.4 创建内容项

POST /api/cms/items

请求体

{
  "modelCode": "case-study",
  "title": "某制造企业 ERP 升级",
  "slug": "manufacturing-erp-upgrade",
  "data": {
    "client": "某上市制造企业",
    "industry": "制造业"
  }
}

说明

  • 创建时状态默认为 draft
  • 版本号默认为 1
  • 自动创建第一个版本记录

响应 data:创建后的 ContentItem 对象

3.2.5 更新内容项

PUT /api/cms/items/{id}

请求体

{
  "title": "新标题",
  "data": {
    "client": "新客户名称"
  },
  "changeLog": "更新客户名称"
}

说明

  • 更新时版本号 +1
  • 自动保存到版本历史表
  • 如果当前状态是 published,更新后变为 draft(可选配置)

响应 data:更新后的 ContentItem 对象

3.2.6 发布内容

POST /api/cms/items/{id}/publish

请求体(可选):

{
  "changeLog": "发布正式版本"
}

说明

  • 将状态从 draft/review 改为 published
  • 设置 publishedAt 时间
  • 版本号不变(发布当前版本)

响应 data:发布后的 ContentItem 对象

3.2.7 下架内容

POST /api/cms/items/{id}/unpublish

说明:将状态从 published 改为 archived

响应 data:下架后的 ContentItem 对象

3.2.8 获取版本历史

GET /api/cms/items/{id}/versions

响应 dataContentVersion 数组

3.2.9 回滚到指定版本

POST /api/cms/items/{id}/versions/{version}/rollback

说明:创建新版本,内容为指定版本的快照,版本号 +1

响应 data:回滚后的 ContentItem 对象

3.2.10 删除内容项

DELETE /api/cms/items/{id}

说明:逻辑删除


3.3 内容区域管理

3.3.1 获取单个内容区域

GET /api/cms/zones/{code}

路径参数code - 区域编码

响应 dataContentZone 对象(含 items 详情)

3.3.2 获取页面的所有内容区域

GET /api/cms/zones/page/{pageCode}

路径参数pageCode - 页面编码

响应 dataContentZone 数组

3.3.3 更新内容区域配置

PUT /api/cms/zones/{code}

请求体

{
  "name": "首页 Hero 区",
  "description": "首页顶部大 Banner 区域",
  "allowedModels": ["hero-banner", "video-banner"],
  "items": [
    {
      "itemId": "101",
      "sortOrder": 1,
      "config": {
        "fullWidth": true
      }
    },
    {
      "itemId": "102",
      "sortOrder": 2,
      "config": {}
    }
  ]
}

响应 data:更新后的 ContentZone 对象


3.4 主题配置管理

3.4.1 获取主题配置

GET /api/cms/theme

响应 dataThemeConfig 对象

ThemeConfig 结构

{
  "id": "1",
  "siteName": "Novalon",
  "siteDescription": "企业数字化转型服务商",
  "logo": "/images/logo.svg",
  "favicon": "/favicon.ico",
  "colors": {
    "primary": "#00D084",
    "secondary": "#6C8CFF",
    "accent": "#FFB800",
    "background": "#FFFFFF",
    "text": "#1A1A1A"
  },
  "seo": {
    "defaultTitle": "Novalon - 企业数字化转型",
    "defaultDescription": "Novalon 致力于帮助企业实现数字化转型",
    "defaultKeywords": "数字化转型,ERP,CRM",
    "ogImage": "/images/og-default.png"
  },
  "social": {
    "wechat": "novalon",
    "weibo": "@novalon",
    "linkedin": "novalon",
    "github": "novalon"
  },
  "settings": {
    "enableDarkMode": true,
    "defaultTheme": "light"
  }
}

3.4.2 更新主题配置

PUT /api/cms/theme

请求体:完整的 ThemeConfig 对象(或部分字段)

响应 data:更新后的 ThemeConfig 对象


3.5 媒体资源管理

3.5.1 获取媒体列表

GET /api/cms/media

查询参数

参数 类型 必填 说明
page int 页码
pageSize int 每页数量
mimeType string MIME 类型过滤
search string 按名称搜索

响应 data:分页 + MediaAsset 数组

3.5.2 上传媒体文件

POST /api/cms/media/upload

Content-Typemultipart/form-data

表单字段

字段 类型 必填 说明
file file 文件内容
alt string 替代文本
folder string 存储目录

响应 dataMediaAsset 对象

3.5.3 获取媒体详情

GET /api/cms/media/{id}

3.5.4 删除媒体资源

DELETE /api/cms/media/{id}

说明:逻辑删除,实际文件不删除


3.6 审计日志

3.6.1 获取审计日志列表

GET /api/cms/audit-logs

查询参数

参数 类型 必填 说明
page int 页码
pageSize int 每页数量
module string 模块过滤
targetId string 目标 ID
operator string 操作人
action string 操作类型
startTime string 开始时间
endTime string 结束时间

响应 data:分页 + AuditLog 数组


四、前端公开 API(无需鉴权)

以下 API 用于前端网站展示,仅返回 published 状态的内容,无需登录。 建议使用 API Key 进行简单的访问频率限制。

4.1 获取内容项列表

GET /api/cms/public/items

查询参数:同 3.2.1,但 status 固定为 published

4.2 按 slug 获取内容项

GET /api/cms/public/items/slug/{modelCode}/{slug}

4.3 获取内容区域

GET /api/cms/public/zones/{code}

4.4 获取主题配置

GET /api/cms/public/theme

五、Webhook 通知(可选)

内容发布/更新时,可配置 Webhook 通知前端触发 ISR 重新生成:

POST https://website.example.com/api/revalidate

请求体

{
  "event": "content.published",
  "modelCode": "case-study",
  "itemId": "100",
  "slug": "manufacturing-erp-upgrade",
  "timestamp": "2024-01-15T10:00:00Z"
}

六、性能优化建议

  1. 热点内容缓存:使用 Redis 缓存 published 状态的内容,TTL 5-10 分钟
  2. 数据库索引
    • data_json 字段使用 GIN 索引支持 JSON 查询
    • 常用查询组合建立联合索引
  3. 读写分离:读操作走从库,写操作走主库
  4. 分页优化:深分页使用游标分页(seek pagination
  5. CDN 加速:媒体资源上传到 OSS/S3,走 CDN

七、安全要求

  1. 管理端 API:必须通过 SSO 鉴权,并有角色权限控制
  2. 公开 API
    • 只能访问 published 状态的内容
    • 使用 API Key 进行限流
    • 禁止查询敏感字段(如内部备注)
  3. 审计日志:所有写操作必须记录审计日志
  4. 输入校验
    • code 字段必须匹配正则:^[a-z][a-z0-9-]*$
    • JSON 字段大小限制(如 1MB
    • XSS 防护:富文本内容存储前进行净化
  5. SQL 注入防护:使用参数化查询,禁止拼接 SQL

八、数据一致性约束

  1. model_code 冗余cms_content_item.model_codecms_content_model.code 保持一致(冗余设计,提升查询性能)
  2. 版本号递增:每次更新内容项,version 必须 +1
  3. 状态流转
    • draftreviewpublishedarchived
    • publisheddraft(修改时)
    • 任意状态 → archived

九、前端 Mock 数据字段定义(对接参考)

目的:以下字段定义与前端 Mock 数据保持一致,后端实现时需按此结构创建 fields_json源码位置src/lib/cms/mock-data.tscase-study)、src/lib/cms/content-types.ts(其余 6 个模型)。 字段类型text | textarea | richtext | number | boolean | date | select | image | array | object

9.0 模型总览

model code 名称 页面类型 URL 模式 Mock 数据条数 数据源
case-study 案例研究 /cases/{slug} 6 lib/constants/cases.ts
news 新闻资讯 /news/{slug} 2 lib/constants/news.ts
service 服务 /services/{slug} 4 lib/constants/services.ts
product 产品 /products/{slug} 7 lib/constants/products.ts
solution 解决方案 /solutions/{slug} 4 lib/constants/solutions.ts
hero-banner Hero Banner - 0(首页 CMS 注入) lib/cms/mock-home.ts
stat-item 数据指标 - 0(首页 CMS 注入) lib/cms/mock-home.ts

说明hero-bannerstat-item 不是页面类型,作为首页内容区域(zone)的组件,通过 ContentZoneRenderer 渲染。

9.1 case-study(案例研究)

字段名 类型 必填 标签 说明
client text 客户名称 客户公司或机构名称
industry text 所属行业 客户所属行业领域
companySize text 公司规模 客户公司人员规模
subtitle text 副标题 案例副标题,简要概括项目价值
challenge textarea 挑战 客户面临的业务挑战与痛点
solution textarea 解决方案 我们提供的解决方案与实施路径
result textarea 实施成果 项目交付后的业务成果与价值
metrics array 关键指标 项目关键量化指标,子字段:value(text)、label(text)、highlight(boolean)
timeline array 项目时间线 子字段:phase(text)、duration(text)、description(textarea)
services array 相关服务 子字段:id(text)、title(text)、description(textarea)
testimonial object 客户评价 子字段:quote(textarea)、author(text)、role(text)、company(text)
color select 主题色 选项:brand / blue / teal / amber / purple,默认 brand
featured boolean 精选案例 是否在首页/推荐位展示,默认 false

9.2 news(新闻资讯)

字段名 类型 必填 标签 说明
excerpt textarea 摘要 新闻摘要,用于列表页展示
date date 发布日期 -
category select 分类 选项:company(公司动态) / industry(行业资讯) / tech(技术分享)
image image 封面图 -
content richtext 正文内容 富文本(HTML),通过 TipTap 编辑器产出
featured boolean 精选推荐 是否在首页精选区展示,默认 false

9.3 service(服务)

字段名 类型 必填 标签 说明
description textarea 简短描述 -
icon text 图标标识 图标名称,对应图标库
overview textarea 服务概述 -
features array 核心能力 子字段:name(text)
benefits array 服务价值 子字段:name(text)
process array 服务流程 子字段:step(text)
heroThemeId text Hero 主题 ID 关联的 Hero 区域主题配置

9.4 product(产品)

字段名 类型 必填 标签 说明
description textarea 产品简介 -
image image 产品图片 -
category text 分类名称 -
categoryId select 分类 ID 选项:enterprise(企业套装) / specialized(专业产品)
status select 产品状态 选项:研发中 / 内测中 / 已发布
overview textarea 产品概述 -
features array 核心功能 子字段:name(text)
benefits array 产品价值 子字段:name(text)
tags array 标签 子字段:name(text)
featured boolean 精选产品 默认 false

9.5 solution(解决方案)

字段名 类型 必填 标签 说明
description textarea 方案简介 -
icon text 图标标识 -
industry text 适用行业 -
overview textarea 方案概述 -
painPoints array 行业痛点 子字段:text(text)
valueProps array 方案价值 子字段:text(text)
featured boolean 精选方案 默认 false

9.6 hero-bannerHero Banner

非页面类型,作为首页内容区域组件使用。

字段名 类型 必填 标签 默认值
heading text 主标题 -
subheading textarea 副标题 -
description textarea 描述文本 -
primaryCtaText text 主按钮文案 了解更多
primaryCtaLink text 主按钮链接 /solutions
secondaryCtaText text 副按钮文案 联系我们
secondaryCtaLink text 副按钮链接 /contact
theme select 主题风格 brand
sortOrder number 排序 0

theme 选项brand / blue / teal / amber / purple

9.7 stat-item(数据指标)

非页面类型,作为首页数据展示区组件使用。

字段名 类型 必填 标签 默认值
icon text 图标 -
label text 指标名称 -
value number 数值 -
suffix text 后缀 -
prefix text 前缀 -
decimals number 小数位数 0
description textarea 描述说明 -
trendValue text 趋势值 -
trendDirection select 趋势方向 up
accentColor select 强调色 brand
sortOrder number 排序 0

trendDirection 选项up(上升) / down(下降) / neutral(持平) accentColor 选项brand / blue / teal / amber / purple


十、前端 Mock 模式架构说明

10.1 Mock 数据流转

lib/constants/*.ts(原始业务数据)
        ↓ toContentItem<T>()
lib/cms/mock-data.tsallMockItemsserver-safe 同步数据源)
        ↓ getMockItems(modelCode) / getMockItemBySlug / getMockItemById
lib/cms/client.tscmsClient,运行时数据访问层)
        ↓ getContentItems / getItem / getZone
src/app/(marketing)/*/page.tsxserver 端同步预取 ContentItem
        ↓ 传 ContentItem prop 给 client 组件
src/app/(marketing)/*/client.tsxclient 端接收 prop 渲染)

10.2 切换到真实 API 的步骤

  1. src/lib/cms/client.ts 中将 CMS_MODE'mock' 改为 'api'
  2. 配置 CMS_API_BASE_URL 环境变量指向 Java 后端
  3. cmsClientgetItems/getItem/getItemBySlug/getZone 会自动切换为 HTTP 请求
  4. 业务层代码(page.tsx、client.tsx)无需任何改动,ContentItem 数据结构保持一致

10.3 静态导出兼容性

  • Mock 数据源(mock-data.ts)不标记 'use client',提供同步函数
  • generateStaticParamsgenerateMetadata 在 server 端同步调用 getMockItems / getMockItemBySlug
  • 静态导出(output: 'export')模式下,构建时预渲染所有页面路径
  • CMS API 路由(/api/cms/draft/*/api/cms/revalidate)已改为 POST 方法,避免静态导出预渲染时报错(GET + searchParams 在 output: 'export' 下不支持)

10.4 富文本编辑器(TipTap

  • CMS Studio 的 richtext 字段使用 TipTap 富文本编辑器(src/components/cms/RichTextEditor.tsx
  • 支持:加粗、斜体、H2/H3、有序/无序列表、引用、链接、撤销/重做
  • immediatelyRender: false 避免 SSR hydration mismatch
  • 编辑器产出的 HTML 存储在 ContentItem.data 的对应字段中(如 news.content
  • 样式定义在 src/app/globals.css.rich-text-editor-content 选择器下