Files
novalon-website/docs/cms/api-contract.md
T
zhangxiang a0328a623f chore(qa): 验收台账与证据入库 + 构建/部署配置同步
- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。
- 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。
- 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径;
  next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐
  standalone 产物装配与部署形态。
2026-09-28 10:48:09 +08:00

961 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Novalon CMS 后端 API 设计规范
> **版本**:v1.1
> **日期**:2026-07-02
> **技术栈**:Java 21 + Spring Boot WebFlux + PostgreSQL + Flyway
> **前端对接**:Next.js 16(App Router,`output: 'standalone'` 混合渲染)+ 读取层 `src/lib/cms/data-server.ts`
>
> ⚠ **路径时效注(2026-08-31 一致性审计)**:`src/lib/cms/client.ts` / `mock-data.ts` 在当前代码库中不存在(`src/lib/cms/` 现含 `data-server.ts` / `workflow.ts` / `notifications.ts` 等)。本文档 API 契约本身仍有效,前端对接实现以 `src/lib/cms/` 实际代码为准。
---
## 一、总体设计原则
### 1.1 API 风格
- **风格**:RESTful API
- **数据格式**:JSON
- **字符编码**:UTF-8
- **时间格式**:ISO 8601(`2024-01-01T00:00:00Z`)
- **统一响应结构**:
```json
{
"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 |
**响应结构**:
```json
{
"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(内容模型表)
```sql
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(内容项表)
```sql
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(内容版本表)
```sql
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(内容区域表)
```sql
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(主题配置表)
```sql
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(媒体资源表)
```sql
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(审计日志表)
```sql
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 结构**:
```json
{
"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` - 模型编码
**响应 data**:ContentModel 对象
#### 3.1.3 创建内容模型
```
POST /api/cms/models
```
**请求体**:
```json
{
"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 结构**:
```json
{
"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
**响应 data**:ContentItem 对象
#### 3.2.3 按 slug 获取内容项
```
GET /api/cms/items/slug/{modelCode}/{slug}
```
**路径参数**:
- `modelCode` - 模型编码
- `slug` - 内容 slug
**响应 data**:ContentItem 对象
#### 3.2.4 创建内容项
```
POST /api/cms/items
```
**请求体**:
```json
{
"modelCode": "case-study",
"title": "某制造企业 ERP 升级",
"slug": "manufacturing-erp-upgrade",
"data": {
"client": "某上市制造企业",
"industry": "制造业"
}
}
```
**说明**:
- 创建时状态默认为 `draft`
- 版本号默认为 1
- 自动创建第一个版本记录
- **本仓库当前实现(`POST /api/admin/items`)**:`status` 只接受缺省或 `draft`,其余取值一律 400 并指向 workflow 接口;落库固定 `status: 'draft'` 且 `publishedAt: null`(`src/app/api/admin/items/route.ts:138-142`、`:163-176`)。创建接口不是发布通道。
**响应 data**:创建后的 ContentItem 对象
#### 3.2.5 更新内容项
```
PUT /api/cms/items/{id}
```
**请求体**:
```json
{
"title": "新标题",
"data": {
"client": "新客户名称"
},
"changeLog": "更新客户名称"
}
```
**说明**:
- 更新时版本号 +1
- 自动保存到版本历史表
- 状态**不随更新改变**:任何「把 status 改成别的值」的载荷都被拒绝(400,指向 workflow 接口);原样回传当前状态是被接受的(admin 编辑页每次保存都会带上现有 status)。见 `src/app/api/admin/items/route.ts:222-226`
**响应 data**:更新后的 ContentItem 对象
#### 3.2.6 发布内容
```
POST /api/cms/items/{id}/publish
```
**请求体**(可选):
```json
{
"changeLog": "发布正式版本"
}
```
**说明**:
- 不存在「从 `draft` 一步到 `published`」的通道:`draft` 只能先 `submit` 进入 `review`,再由 `review` `approve` 到 `published`(`src/lib/cms/workflow.ts:20-32`)
- 审批通过时设置 `publishedAt` 时间(`src/lib/cms/workflow.ts:77-79`)
- 每次流转 `version` +1(与内容更新同为 `{ increment: 1 }`,见 `src/lib/cms/workflow.ts:68-75`)
- **本仓库当前实现**:状态变更的唯一入口是 workflow 接口 `POST /api/admin/items/[id]/workflow`(`src/app/api/admin/items/[id]/workflow/route.ts`),请求体 `{ action: 'submit' | 'approve' | 'reject' | 'archive', reason? }`;`approve` / `reject` / `archive` 需要 `publish` 权限,`submit` 只需 `update` 权限(`:31-36`)。非法流转与并发状态冲突返回 400,不会静默忽略
**响应 data**:发布后的 ContentItem 对象
#### 3.2.7 下架内容
```
POST /api/cms/items/{id}/unpublish
```
**说明**:将状态从 `published` 改为 `archived`
**响应 data**:下架后的 ContentItem 对象
#### 3.2.8 获取版本历史
```
GET /api/cms/items/{id}/versions
```
**响应 data**:ContentVersion 数组
#### 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` - 区域编码
**响应 data**:ContentZone 对象(含 items 详情)
#### 3.3.2 获取页面的所有内容区域
```
GET /api/cms/zones/page/{pageCode}
```
**路径参数**:`pageCode` - 页面编码
**响应 data**:ContentZone 数组
#### 3.3.3 更新内容区域配置
```
PUT /api/cms/zones/{code}
```
**请求体**:
```json
{
"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
```
**响应 data**:ThemeConfig 对象
**ThemeConfig 结构**:
```json
{
"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-Type**:`multipart/form-data`
**表单字段**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | file | 是 | 文件内容 |
| alt | string | 否 | 替代文本 |
| folder | string | 否 | 存储目录 |
**响应 data**:MediaAsset 对象
#### 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
```
**请求体**:
```json
{
"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_code` 与 `cms_content_model.code` 保持一致(冗余设计,提升查询性能)
2. **版本号递增**:每次更新内容项,`version` 必须 +1
3. **状态流转**(本仓库实现见 `src/lib/cms/workflow.ts:20-32`,动作即唯一通道):
- `draft` --submit--> `review` --approve--> `published` --archive--> `archived`
- `review` --reject--> `draft`(驳回回草稿,**不是** `published` 直接回退)
- `archived` 为终态,无任何后继动作;`published` 只能归档,不能再改回 `draft`
- 每个状态只允许上述动作,其余组合一律抛错并由路由转为 400(`src/app/api/admin/items/[id]/workflow/route.ts:86-88`)
---
## 九、前端内容字段定义(对接参考)
> **目的**:以下字段定义与前端数据层保持一致,后端实现时需按此结构创建 `fields_json`。
> **源码位置**:`src/lib/cms/content-types.ts`(全部模型的 `CONTENT_TYPE_CONFIGS`,`:1981` 起;§九 列出的 7 个模型对应其中 7 个键,case-study 配置在 `:1837`)。同文件 `:1978` 注明「数据模型定义已全部移至 CONTENT_TYPE_CONFIGS」,`mock-data.ts` 已不存在(见文首路径时效注)。
> **字段类型**:以 `src/lib/cms/types.ts:24-40` 的 `FieldType` 为准 —— `text` | `textarea` | `richtext` | `number` | `boolean` | `date` | `datetime` | `image` | `media` | `reference` | `references` | `json` | `array` | `object` | `select` | `dropdown`。其中 admin 编辑页 `renderField` 只为 `text` / `textarea` / `number` / `boolean` / `select` / `dropdown` / `json` / `image` / `array` / `object` 提供专门控件,其余类型(含 `richtext`、`date`、`datetime`、`media`、`reference`、`references`)落入 `default:` 的单行 `<input type="text">`(`src/app/admin/content/[modelCode]/[itemId]/page.tsx:447-628`,见 §10.4)
> **漂移告知(2026-09-23 核对)**:§9.0–§9.7 是早期对接快照,与 `content-types.ts` 已系统性偏离,**不得当作现行字段清单引用**——已知三类差异:① 类型口径:文档写作 `select` 的字段在代码里是带 `options` 的 `text`(`news.category`、`product.categoryId`、`product.status`、`case-study.color`、`stat-item.trendDirection`、`hero-banner.theme` 等,二者语义同义,见 `src/lib/cms/validate-content-data.ts:29-30`);② 文档写作 `array` / `object` 的列表字段多已改为 `json`(如 `service.features`、`solution.painPoints`);③ 字段名与集合有变更(`hero-banner` 的 `primaryCtaText`/`primaryCtaLink` 现为 `ctaLabel`/`ctaHref`,`solution.valueProps` 现为 `challenges`/`solutions`/`valueProposition`,`case-study` 另有 `projectDuration`/`departments`/`dataScale`/`businessProblem`/`verified` 未列出)。逐字段口径请以 `CONTENT_TYPE_CONFIGS` 为准。
### 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-banner` 和 `stat-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` | text(带 `options`,语义同 `select`) | 是 | 分类 | 代码侧选项为 公司新闻 / 研发动态(`src/lib/cms/content-types.ts:58-66`) |
| `image` | image | 是 | 封面图 | - |
| `content` | richtext | 是 | 正文内容 | 富文本(HTML);admin 侧无专用编辑器,落 `default:` 单行输入框由作者直接书写 HTML(见 §10.4),渲染端经 `src/lib/sanitize.ts` 净化 |
| `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-banner(Hero 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
---
## 十、前端数据层架构说明
### 10.1 数据流转(现状)
```
src/lib/constants/*.ts、prisma/seeds/*.ts(原始业务数据)
↓ prisma/seed.ts 写入(contentItem.upsert,`prisma/seed.ts:42`)
Prisma ContentItem(data 列存 JSON 字符串)
↓ getPublishedItems / getPublishedItemBySlug / getZone / getResolvedHomeZones(React cache)
src/lib/cms/data-server.ts(server 端读取层)
↓ 以 props 传入
src/app/(marketing)/**/page.tsx(server 组件)→ client.tsx / home-content-v15.tsx(client 渲染)
```
> **历史口径**:本节曾描述 `lib/cms/mock-data.ts` → `lib/cms/client.ts`(`cmsClient`)的纯 Mock 流转,两个文件均已不存在(文首路径时效注)。「CMS 优先 + 常量兜底」的语义仍成立,但实现分散在读取层与页面里:首页在 zone 未解析到内容时回落到逐模型 `getPublishedItems()`(`src/app/(marketing)/page.tsx:8-33`),结构性文案在 CMS 未配置时由组件回退 `src/lib/constants/*`(见 `CONTEXT.md`「结构性文案 CMS 化」)。
### 10.2 切换到真实 API 的步骤
1. 读取层已落在 `src/lib/cms/data-server.ts`(Prisma + React `cache`),**不存在** `CMS_MODE` 开关与 `src/lib/cms/client.ts`。
2. 接真实后端时改的是 `data-server.ts` 的取数函数(`getPublishedItems` / `getPublishedItemBySlug` / `getZone` / `getResolvedHomeZones`),业务页与 client 组件按 props 契约保持不变。
3. `ContentItem` 的结构定义在 `src/lib/cms/types.ts`,字段声明在 `src/lib/cms/content-types.ts` 的 `CONTENT_TYPE_CONFIGS`。
### 10.3 渲染模式与预渲染兼容性
- 构建产物为 `output: 'standalone'`(`next.config.mjs:44`),`output: 'export'` 早已移除:营销页在构建时预渲染并走 ISR(页面级 `export const revalidate = 3600`,如 `src/app/(marketing)/page.tsx:5`、`src/app/privacy/page.tsx:12`),`/api/*` 与 `/admin/*` 需要 Node 运行时
- 读取层(`data-server.ts`)不标记 `'use client'`;`generateStaticParams` 与 `generateMetadata` 在 server 端调用它
- CMS 通知型路由只导出 POST(`src/app/api/cms/draft/enable/route.ts:13`、`src/app/api/cms/draft/disable/route.ts:6`、`src/app/api/cms/revalidate/route.ts:33`),且密钥为 fail-closed:`CMS_PREVIEW_SECRET`(draft)/ `CMS_REVALIDATE_SECRET`(revalidate)未配置时直接拒绝,不放行
### 10.4 富文本字段(`richtext`)
- admin 编辑页的 `renderField`(`src/app/admin/content/[modelCode]/[itemId]/page.tsx:447` 的 switch)**没有 `richtext` 分支**,该类型落入 `default:`(`:613-627`)⇒ 渲染成单行 `<input type="text">`,作者需直接书写 HTML
- 历史组件 `src/components/cms/RichTextEditor.tsx`(TipTap)与配套 `.rich-text-editor-content` 样式**在生产代码中无任何引用**,已作为死代码删除(`src/components/cms/` 目录整体不存在);恢复富文本编辑需重新接线到 `renderField`。`package.json` 仍留着 `@tiptap/*` 依赖,但不参与任何运行时路径
- `richtext` 字段产出的 HTML 存于 `ContentItem.data` 对应字段(如 `news.content`),公开页经 `src/lib/sanitize.ts` 净化后才进入 `dangerouslySetInnerHTML`(白名单见 `src/lib/sanitize.ts:7-46`,用例见 `src/app/privacy/page.tsx:268`、`src/app/terms/page.tsx:213`)
### 10.5 状态字段:只有 workflow 能改(admin 编辑页只读)
- `ContentStatus` 只有四个取值:`draft` / `review` / `published` / `archived`(`src/lib/cms/types.ts:81`),变更**只能**经 `POST /api/admin/items/[id]/workflow`(动作 `submit` / `approve` / `reject` / `archive`,权限口径见 §3.2.6)
- `POST /api/admin/items`:`status` 传缺省或 `draft` 以外的值即 400;新建条目固定 `status: 'draft'`、`publishedAt: null`(`src/app/api/admin/items/route.ts:138-142`、`:163-176`)
- `PUT /api/admin/items`(`?id=`):载荷**改变** status 即 400;原样回传当前 status 放行 —— 编辑页每次保存(含自动保存)都会带上现有 status,一律拒绝会让所有编辑保存变红(`src/app/api/admin/items/route.ts:222-226`)
- admin 编辑页的「状态」是**只读展示**(`<p>` + 文案「状态通过「发布」提交审核变更,不可在此直接修改」,`src/app/admin/content/[modelCode]/[itemId]/page.tsx:821-828`),不再是早前的 `<select>`;「发布」按钮走 `handlePublishConfirm` → 先落库再依次 `submit`(draft→review)、`approve`(review→published),账号无 publish 权限时 approve 返回 403、条目停在「待审核」,这是审批制的预期结果(同文件 `:407-441`)
- 列表页只做状态展示、不提供改 status 的入口(`src/app/admin/content/[modelCode]/page.tsx:197-204`);客户端唯一的写状态通道是 `adminApi.runWorkflow` → `POST /api/admin/items/[id]/workflow`(`src/lib/admin-api.ts:146-153`),不存在「绕过状态机直接写 status」的路径