# 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. 测试验证