403 lines
16 KiB
Markdown
403 lines
16 KiB
Markdown
# 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. 测试验证
|