Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

403 lines
16 KiB
Markdown
Raw Permalink 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.
# CMS 栏目管理与内容管理 技术设计方案
## 一、概述
### 1.1 设计目标
为飞点健康 CMS 后台新增两大顶级模块:
- **栏目管理模块**:负责全站分类架构、前端展示样式配置、导航绑定。栏目本身为纯框架载体,不存储具体业务内容。
- **内容管理模块**:负责产品、专家、服务、图文资讯等各类业务内容的统一录入、编辑与维护。所有内容必须关联到栏目。
### 1.2 核心原则
1. **本期只实现后台管理**:访客端展示完全不开发,但后台设计预留访客端扩展接口。
2. **栏目是框架,内容是填充**:栏目本身无内容,内容由内容管理模块生成后关联到栏目。
3. **现有新闻资讯模块完全保留**:新的栏目管理和内容管理是独立顶级模块,与现有新闻资讯并行存在,本期不改造、不合并。
4. **命名与现有项目严格一致**:参照 `news_category.py`、`home_expert.py`、`nav_menu.py` 等现有文件命名风格,不使用 `cms_` 前缀。
### 1.3 模块关系图
```
后台管理
├── 仪表盘
├── 首页管理
├── 导航菜单
├── 新闻资讯(现有,保留不动)
│ ├── 资讯管理
│ └── 栏目管理(现有,保留不动)
├── 图片管理
├── [NEW] 栏目管理(新顶级模块)
│ └── 栏目列表
├── [NEW] 内容管理(新顶级模块)
│ ├── 内容列表
│ ├── 添加内容
│ └── 采集内容
└── 系统设置
```
---
## 二、命名规范
### 2.1 命名与现有项目保持一致
| 层级 | 现有风格示例 | 新模块命名 |
|------|-------------|-----------|
| 后端模型文件 | `news_category.py` / `home_expert.py` | `column.py`(栏目) / `article.py`(内容) |
| 后端模型类 | `NewsCategory` / `HomeExpert` | `ColumnModel` / `Article` |
| 数据库表 | `news_categories` / `home_experts` | `columns` / `articles` |
| 后端路由文件 | `news.py` / `home_experts.py` | `columns.py` / `articles.py` |
| 前端页面目录 | `pages/admin/news/` | `pages/admin/columns/` / `pages/admin/articles/` |
| 前端组件名 | `CategoryList.jsx` / `PostEdit.jsx` | `ColumnList.jsx` / `ArticleList.jsx` / `ArticleEdit.jsx` / `ArticleCrawl.jsx` |
| API 前缀 | `/api/news/posts` / `/api/home-experts` | `/api/columns` / `/api/articles` |
### 2.2 关于 `ColumnModel` 类名说明
SQLAlchemy 内置了 `Column` 类(`sqlalchemy.Column`),因此栏目模型类名使用 `ColumnModel` 以避免命名冲突。这是项目中已有的做法(如 `Image` 模型类名不会与 Python 内置冲突)。
---
## 三、数据库设计
### 3.1 `columns` 表(栏目表)
栏目是纯框架载体,本身不存储具体业务内容。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | Integer | PK, auto_increment | 主键 |
| name | String(50) | NOT NULL | 栏目名称 |
| slug | String(50) | UNIQUE, NOT NULL | URL标识(拼音转换生成) |
| description | String(200) | NULL | 栏目描述 |
| parent_id | Integer | FK -> columns.id, NULL | 父栏目ID,NULL表示顶级栏目 |
| sort_order | Integer | DEFAULT 0 | 排序权重,数字越小越靠前 |
| is_enabled | Boolean | DEFAULT True | 是否前端可见(True=可见,False=隐藏) |
| display_mode | String(20) | DEFAULT 'list' | 展示模式:`list` 列表式 / `card` 卡片式 |
| layout_style | String(20) | DEFAULT 'left' | 布局样式:`left` 图左文右 / `right` 图右文左 / `top` 上图下文 |
| created_at | DateTime | server_default=now() | 创建时间 |
| updated_at | DateTime | server_default=now(), onupdate=now() | 更新时间 |
**索引**:
- `slug` 唯一索引
- `parent_id` 索引(用于树形查询)
### 3.2 `articles` 表(内容表)
全站业务内容的统一载体,产品、专家、服务、图文资讯等均通过此表存储。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | Integer | PK, auto_increment | 主键 |
| title | String(200) | NOT NULL | 标题 |
| slug | String(200) | UNIQUE, NOT NULL | URL友好的标识(拼音转换生成) |
| content | Text | NOT NULL | 正文 |
| content_type | String(10) | DEFAULT 'markdown' | 内容类型:`markdown` / `html` |
| summary | String(500) | NULL | 摘要,留空时自动从正文提取纯文本 |
| cover_image | String(500) | NULL | 封面图路径 |
| column_id | Integer | FK -> columns.id, NOT NULL | 所属栏目ID |
| author | String(50) | NULL | 作者 |
| status | String(20) | DEFAULT 'draft' | 状态:`published` 已发布 / `draft` 草稿 |
| sort_order | Integer | DEFAULT 0 | 排序权重,数字越小越靠前 |
| view_count | Integer | DEFAULT 0 | 浏览次数 |
| created_at | DateTime | server_default=now() | 创建时间 |
| updated_at | DateTime | server_default=now(), onupdate=now() | 更新时间 |
**索引**:
- `slug` 唯一索引
- `column_id` 索引(用于栏目关联查询)
- `status` 索引(用于状态筛选)
---
## 四、栏目与内容的关联规则
### 4.1 两级栏目架构
- **顶级栏目**:`parent_id = NULL`
- **二级栏目**:`parent_id` 指向某个顶级栏目
- 本期最多支持两级,代码层面不限制层级,但 API 校验按两级处理
### 4.2 内容关联约束(核心规则)
| 栏目类型 | 是否有二级栏目 | 内容关联规则 |
|---------|--------------|-------------|
| 顶级栏目 | 无二级栏目 | 内容可直接关联到该顶级栏目 |
| 顶级栏目 | 有二级栏目 | **禁止**内容直接关联,内容必须关联到其下属二级栏目 |
| 二级栏目 | - | 内容可直接关联 |
### 4.3 栏目删除规则
| 场景 | 处理方式 |
|------|---------|
| 栏目无子栏目、无关联内容 | 直接删除 |
| 栏目无子栏目、有关联内容 + 未勾选强制删除 | 报错:"该栏目下还有N条内容,是否强制删除?" |
| 栏目无子栏目、有关联内容 + 勾选强制删除 | 先删除所有关联内容,再删除栏目 |
| 栏目有子栏目 + 未勾选强制删除 | 报错:"该栏目存在N个子栏目,请先删除子栏目或勾选强制删除" |
| 栏目有子栏目 + 勾选强制删除 | 递归删除所有子栏目及其关联内容,再删除当前栏目 |
---
## 五、后端 API 设计
### 5.1 栏目管理 API
路由文件:`backend/app/routers/columns.py`
前缀:`/api/columns`
| 方法 | 路径 | 功能 | 请求参数 |
|------|------|------|---------|
| GET | `/` | 栏目列表 | `tree`: bool(是否返回树形结构) |
| GET | `/tree` | 树形栏目列表(用于下拉选择) | 无 |
| GET | `/{id}` | 栏目详情 | 无 |
| POST | `/` | 创建栏目 | Body: `ColumnCreate` |
| PUT | `/{id}` | 更新栏目 | Body: `ColumnUpdate` |
| DELETE | `/{id}` | 删除栏目 | Query: `force`: bool |
**Pydantic 模型**:
```python
class ColumnCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
slug: Optional[str] = Field(None, max_length=50)
description: Optional[str] = Field(None, max_length=200)
parent_id: Optional[int] = Field(None)
sort_order: int = Field(default=0)
is_enabled: bool = Field(default=True)
display_mode: str = Field(default="list", pattern="^(list|card)$")
layout_style: str = Field(default="left", pattern="^(left|right|top)$")
class ColumnUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=50)
slug: Optional[str] = Field(None, min_length=1, max_length=50)
description: Optional[str] = Field(None, max_length=200)
parent_id: Optional[int] = Field(None)
sort_order: Optional[int] = None
is_enabled: Optional[bool] = None
display_mode: Optional[str] = Field(None, pattern="^(list|card)$")
layout_style: Optional[str] = Field(None, pattern="^(left|right|top)$")
```
**创建栏目时的自动处理**:
- `slug` 为空时,自动调用 `generate_slug(name)` 生成拼音标识
- `parent_id` 不为空时,校验父栏目是否存在,且父栏目本身不能是二级栏目(最多两级)
### 5.2 内容管理 API
路由文件:`backend/app/routers/articles.py`
前缀:`/api/articles`
| 方法 | 路径 | 功能 | 请求参数 |
|------|------|------|---------|
| GET | `/` | 内容列表(分页 + 筛选) | `page`, `size`, `column_id`, `keyword`, `status`, `start_date`, `end_date` |
| GET | `/{id}` | 内容详情 | 无 |
| POST | `/` | 创建内容 | Body: `ArticleCreate` |
| PUT | `/{id}` | 更新内容 | Body: `ArticleUpdate` |
| DELETE | `/{id}` | 删除内容 | 无 |
| POST | `/batch-delete` | 批量删除 | Body: `{ids: number[]}` |
| POST | `/batch-move` | 批量转移栏目 | Body: `{ids: number[], column_id: number}` |
| POST | `/{id}/publish` | 发布/取消发布 | Body: `{is_published: bool}` |
**Pydantic 模型**:
```python
class ArticleCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
slug: Optional[str] = Field(None, max_length=200)
content: str = Field(..., min_length=1)
content_type: str = Field(default="markdown", pattern="^(markdown|html)$")
summary: Optional[str] = Field(None, max_length=500)
cover_image: Optional[str] = Field(None, max_length=500)
column_id: int
author: Optional[str] = Field(None, max_length=50)
status: str = Field(default="draft", pattern="^(published|draft)$")
sort_order: int = Field(default=0)
class ArticleUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=200)
slug: Optional[str] = Field(None, min_length=1, max_length=200)
content: Optional[str] = Field(None, min_length=1)
content_type: Optional[str] = Field(None, pattern="^(markdown|html)$")
summary: Optional[str] = Field(None, max_length=500)
cover_image: Optional[str] = Field(None, max_length=500)
column_id: Optional[int] = None
author: Optional[str] = Field(None, max_length=50)
status: Optional[str] = Field(None, pattern="^(published|draft)$")
sort_order: Optional[int] = None
```
**创建/更新内容时的校验**:
- `slug` 为空时,自动调用 `generate_slug(title)` 生成拼音标识
- `column_id` 必须指向有效栏目
- 若关联的栏目是顶级栏目,需检查该顶级栏目下是否有二级栏目:
- 有二级栏目 → 报错:"该栏目存在子栏目,内容必须关联到子栏目"
- 无二级栏目 → 允许关联
**摘要自动提取**:
- `summary` 为空时,调用 `extract_summary(content)` 从正文中提取纯文本摘要(去除 HTML 标签、解码 HTML 实体、清理 Markdown 标记)
---
## 六、前端页面设计
### 6.1 后台导航菜单
在 `AdminLayout.jsx` 的 `menuItems` 数组中新增两个顶级模块:
```javascript
{
key: 'columns',
label: '栏目管理',
icon: FolderTree, // lucide-react
children: [
{ key: 'column-list', label: '栏目列表', path: '/admin/columns' }
]
},
{
key: 'articles',
label: '内容管理',
icon: FileStack, // lucide-react
children: [
{ key: 'article-list', label: '内容列表', path: '/admin/articles' },
{ key: 'article-new', label: '添加内容', path: '/admin/articles/new' },
{ key: 'article-crawl', label: '采集内容', path: '/admin/articles/crawl' }
]
}
```
### 6.2 页面文件结构
```
frontend/src/pages/admin/
├── columns/
│ └── ColumnList.jsx # 栏目列表(树形展示)+ 增删改弹窗
└── articles/
├── ArticleList.jsx # 内容列表 + 搜索筛选 + 批量操作
├── ArticleEdit.jsx # 内容编辑(复用现有编辑器)
└── ArticleCrawl.jsx # 内容采集(复用现有采集逻辑)
```
### 6.3 栏目列表页(ColumnList.jsx)
**功能**:
- 树形表格展示栏目(顶级栏目可展开查看二级栏目)
- 每行显示:排序权重、栏目名称、slug、内容数量、显示状态
- 操作:编辑、删除
**新增/编辑弹窗表单字段**:
- 栏目名称(必填)
- 上级栏目(下拉选择,顶级选"无")
- URL标识(自动生成拼音)
- 栏目描述(选填)
- 排序权重(数字输入)
- 是否前端可见(开关)
- 展示模式(单选:列表式/卡片式)
- 布局样式(单选:图左文右/图右文左/上图下文)
**删除弹窗**:
- 无关联内容 → 普通确认删除
- 有关联内容 → 警告提示 + "强制删除关联内容" 复选框
### 6.4 内容列表页(ArticleList.jsx)
**功能**:
- 表格展示内容列表
- 每行显示:封面缩略图、标题、所属栏目、状态、排序、更新时间
- 顶部筛选栏:
- 栏目筛选(树形下拉选择器)
- 状态筛选(全部/已发布/草稿)
- 关键词搜索(标题)
- 发布时间范围
- 批量操作:勾选后批量删除、批量转移栏目
- 操作列:编辑、删除、预览
### 6.5 内容编辑页(ArticleEdit.jsx)
**复用现有 `PostEdit.jsx` 的以下组件和逻辑**:
- MDEditor 编辑器(Markdown / HTML 富文本)
- 图片上传/选择器
- 表单布局和样式
**字段调整**:
- 标题(必填)
- URL标识(留空由后端自动生成拼音)
- 所属栏目(树形选择器,受栏目约束规则校验)
- 摘要(留空自动从正文提取)
- 封面图(复用现有图片选择器)
- 正文(MDEditor)
- 作者(选填)
- 排序权重
- 状态(草稿/发布)
### 6.6 内容采集页(ArticleCrawl.jsx)
**复用现有 `CrawlNews.jsx` 的采集逻辑**:
- URL 输入框
- 采集解析
- 内容预览
- 保存到 `articles` 表(而非 `news_posts` 表)
---
## 七、关键复用点
| 现有文件 | 复用内容 | 新文件 |
|---------|---------|--------|
| `PostEdit.jsx` | MDEditor 编辑器、图片选择器、表单布局 | `ArticleEdit.jsx` |
| `CrawlNews.jsx` | URL 采集逻辑、内容解析 | `ArticleCrawl.jsx` |
| `news.py` | `extract_summary()`、`generate_slug()`、图片迁移逻辑 | `articles.py` |
| `CategoryList.jsx`(news) | 弹窗表单、列表展示、排序逻辑 | `ColumnList.jsx` |
| `api/index.js` | `ApiClient` 封装模式 | 新增 `columnsApi`、`articlesApi` |
---
## 八、访客端预留接口(后续阶段开发)
为后续访客端开发预留以下 API 和页面设计:
### 预留 API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/public/columns` | 获取启用的栏目树 |
| GET | `/api/public/columns/{slug}` | 获取栏目详情 |
| GET | `/api/public/articles` | 获取已发布内容列表(按栏目筛选) |
| GET | `/api/public/articles/{slug}` | 通过 slug 获取内容详情 |
### 预留页面路由
| 路由 | 说明 |
|------|------|
| `/column/:slug` | 栏目内容列表页 |
| `/article/:slug` | 内容详情页 |
---
## 九、实施阶段规划
### 阶段一:栏目管理模块
1. `backend/app/models/column.py` - 栏目模型定义
2. `backend/app/routers/columns.py` - 栏目管理 API
3. `backend/app/main.py` - 注册栏目路由
4. `frontend/src/pages/admin/columns/ColumnList.jsx` - 栏目列表页
5. `frontend/src/components/AdminLayout.jsx` - 新增"栏目管理"导航
6. `frontend/src/App.jsx` - 新增栏目管理路由
7. `frontend/src/api/index.js` - 新增 `columnsApi`
8. 测试验证
### 阶段二:内容管理模块
1. `backend/app/models/article.py` - 内容模型定义
2. `backend/app/routers/articles.py` - 内容管理 API
3. `backend/app/main.py` - 注册内容路由
4. `frontend/src/pages/admin/articles/ArticleList.jsx` - 内容列表页
5. `frontend/src/pages/admin/articles/ArticleEdit.jsx` - 内容编辑页
6. `frontend/src/pages/admin/articles/ArticleCrawl.jsx` - 内容采集页
7. `frontend/src/components/AdminLayout.jsx` - 新增"内容管理"导航
8. `frontend/src/App.jsx` - 新增内容管理路由
9. `frontend/src/api/index.js` - 新增 `articlesApi`
10. 测试验证