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 产物装配与部署形态。
This commit is contained in:
+49
-41
@@ -3,7 +3,7 @@
|
||||
> **版本**:v1.1
|
||||
> **日期**:2026-07-02
|
||||
> **技术栈**:Java 21 + Spring Boot WebFlux + PostgreSQL + Flyway
|
||||
> **前端对接**:Next.js 14 + CMS SDK(`src/lib/cms/client.ts`)
|
||||
> **前端对接**: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/` 实际代码为准。
|
||||
|
||||
@@ -412,6 +412,7 @@ POST /api/cms/items
|
||||
- 创建时状态默认为 `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 对象
|
||||
|
||||
@@ -436,7 +437,7 @@ PUT /api/cms/items/{id}
|
||||
**说明**:
|
||||
- 更新时版本号 +1
|
||||
- 自动保存到版本历史表
|
||||
- 如果当前状态是 published,更新后变为 draft(可选配置)
|
||||
- 状态**不随更新改变**:任何「把 status 改成别的值」的载荷都被拒绝(400,指向 workflow 接口);原样回传当前状态是被接受的(admin 编辑页每次保存都会带上现有 status)。见 `src/app/api/admin/items/route.ts:222-226`
|
||||
|
||||
**响应 data**:更新后的 ContentItem 对象
|
||||
|
||||
@@ -455,9 +456,10 @@ POST /api/cms/items/{id}/publish
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 将状态从 `draft`/`review` 改为 `published`
|
||||
- 设置 `publishedAt` 时间
|
||||
- 版本号不变(发布当前版本)
|
||||
- 不存在「从 `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 对象
|
||||
|
||||
@@ -777,18 +779,20 @@ POST https://website.example.com/api/revalidate
|
||||
|
||||
1. **model_code 冗余**:`cms_content_item.model_code` 与 `cms_content_model.code` 保持一致(冗余设计,提升查询性能)
|
||||
2. **版本号递增**:每次更新内容项,`version` 必须 +1
|
||||
3. **状态流转**:
|
||||
- `draft` → `review` → `published` → `archived`
|
||||
- `published` → `draft`(修改时)
|
||||
- 任意状态 → `archived`
|
||||
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`)
|
||||
|
||||
---
|
||||
|
||||
## 九、前端 Mock 数据字段定义(对接参考)
|
||||
## 九、前端内容字段定义(对接参考)
|
||||
|
||||
> **目的**:以下字段定义与前端 Mock 数据保持一致,后端实现时需按此结构创建 `fields_json`。
|
||||
> **源码位置**:`src/lib/cms/mock-data.ts`(case-study)、`src/lib/cms/content-types.ts`(其余 6 个模型)。
|
||||
> **字段类型**:`text` | `textarea` | `richtext` | `number` | `boolean` | `date` | `select` | `image` | `array` | `object`
|
||||
> **目的**:以下字段定义与前端数据层保持一致,后端实现时需按此结构创建 `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 模型总览
|
||||
|
||||
@@ -828,9 +832,9 @@ POST https://website.example.com/api/revalidate
|
||||
|--------|------|------|------|------|
|
||||
| `excerpt` | textarea | 是 | 摘要 | 新闻摘要,用于列表页展示 |
|
||||
| `date` | date | 是 | 发布日期 | - |
|
||||
| `category` | select | 是 | 分类 | 选项:company(公司动态) / industry(行业资讯) / tech(技术分享) |
|
||||
| `category` | text(带 `options`,语义同 `select`) | 是 | 分类 | 代码侧选项为 公司新闻 / 研发动态(`src/lib/cms/content-types.ts:58-66`) |
|
||||
| `image` | image | 是 | 封面图 | - |
|
||||
| `content` | richtext | 是 | 正文内容 | 富文本(HTML),通过 TipTap 编辑器产出 |
|
||||
| `content` | richtext | 是 | 正文内容 | 富文本(HTML);admin 侧无专用编辑器,落 `default:` 单行输入框由作者直接书写 HTML(见 §10.4),渲染端经 `src/lib/sanitize.ts` 净化 |
|
||||
| `featured` | boolean | 否 | 精选推荐 | 是否在首页精选区展示,默认 false |
|
||||
|
||||
### 9.3 service(服务)
|
||||
@@ -913,40 +917,44 @@ POST https://website.example.com/api/revalidate
|
||||
|
||||
---
|
||||
|
||||
## 十、前端 Mock 模式架构说明
|
||||
## 十、前端数据层架构说明
|
||||
|
||||
### 10.1 Mock 数据流转
|
||||
### 10.1 数据流转(现状)
|
||||
|
||||
```
|
||||
lib/constants/*.ts(原始业务数据)
|
||||
↓ toContentItem<T>()
|
||||
lib/cms/mock-data.ts(allMockItems,server-safe 同步数据源)
|
||||
↓ getMockItems(modelCode) / getMockItemBySlug / getMockItemById
|
||||
lib/cms/client.ts(cmsClient,运行时数据访问层)
|
||||
↓ getContentItems / getItem / getZone
|
||||
src/app/(marketing)/*/page.tsx(server 端同步预取 ContentItem)
|
||||
↓ 传 ContentItem prop 给 client 组件
|
||||
src/app/(marketing)/*/client.tsx(client 端接收 prop 渲染)
|
||||
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/client.ts` 中将 `CMS_MODE` 从 `'mock'` 改为 `'api'`
|
||||
2. 配置 `CMS_API_BASE_URL` 环境变量指向 Java 后端
|
||||
3. `cmsClient` 的 `getItems/getItem/getItemBySlug/getZone` 会自动切换为 HTTP 请求
|
||||
4. 业务层代码(page.tsx、client.tsx)无需任何改动,ContentItem 数据结构保持一致
|
||||
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 静态导出兼容性
|
||||
### 10.3 渲染模式与预渲染兼容性
|
||||
|
||||
- Mock 数据源(`mock-data.ts`)不标记 `'use client'`,提供同步函数
|
||||
- `generateStaticParams` 和 `generateMetadata` 在 server 端同步调用 `getMockItems` / `getMockItemBySlug`
|
||||
- 静态导出(`output: 'export'`)模式下,构建时预渲染所有页面路径
|
||||
- CMS API 路由(`/api/cms/draft/*`、`/api/cms/revalidate`)已改为 POST 方法,避免静态导出预渲染时报错(GET + searchParams 在 `output: 'export'` 下不支持)
|
||||
- 构建产物为 `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 富文本编辑器(TipTap)
|
||||
### 10.4 富文本字段(`richtext`)
|
||||
|
||||
- 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` 选择器下
|
||||
- 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」的路径
|
||||
|
||||
Reference in New Issue
Block a user