# 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:` 的单行 ``(`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`)⇒ 渲染成单行 ``,作者需直接书写 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 编辑页的「状态」是**只读展示**(`

` + 文案「状态通过「发布」提交审核变更,不可在此直接修改」,`src/app/admin/content/[modelCode]/[itemId]/page.tsx:821-828`),不再是早前的 `