16 KiB
16 KiB
CMS 栏目管理与内容管理 技术设计方案
一、概述
1.1 设计目标
为飞点健康 CMS 后台新增两大顶级模块:
- 栏目管理模块:负责全站分类架构、前端展示样式配置、导航绑定。栏目本身为纯框架载体,不存储具体业务内容。
- 内容管理模块:负责产品、专家、服务、图文资讯等各类业务内容的统一录入、编辑与维护。所有内容必须关联到栏目。
1.2 核心原则
- 本期只实现后台管理:访客端展示完全不开发,但后台设计预留访客端扩展接口。
- 栏目是框架,内容是填充:栏目本身无内容,内容由内容管理模块生成后关联到栏目。
- 现有新闻资讯模块完全保留:新的栏目管理和内容管理是独立顶级模块,与现有新闻资讯并行存在,本期不改造、不合并。
- 命名与现有项目严格一致:参照
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 模型:
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 模型:
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 数组中新增两个顶级模块:
{
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 |
内容详情页 |
九、实施阶段规划
阶段一:栏目管理模块
backend/app/models/column.py- 栏目模型定义backend/app/routers/columns.py- 栏目管理 APIbackend/app/main.py- 注册栏目路由frontend/src/pages/admin/columns/ColumnList.jsx- 栏目列表页frontend/src/components/AdminLayout.jsx- 新增"栏目管理"导航frontend/src/App.jsx- 新增栏目管理路由frontend/src/api/index.js- 新增columnsApi- 测试验证
阶段二:内容管理模块
backend/app/models/article.py- 内容模型定义backend/app/routers/articles.py- 内容管理 APIbackend/app/main.py- 注册内容路由frontend/src/pages/admin/articles/ArticleList.jsx- 内容列表页frontend/src/pages/admin/articles/ArticleEdit.jsx- 内容编辑页frontend/src/pages/admin/articles/ArticleCrawl.jsx- 内容采集页frontend/src/components/AdminLayout.jsx- 新增"内容管理"导航frontend/src/App.jsx- 新增内容管理路由frontend/src/api/index.js- 新增articlesApi- 测试验证