# PRD:集合功能增强
## 一、问题分析
### 问题 1:集合详情页不显示已收录文章
**页面**:`https://blog.ephron.ren/collections/data-structure`
**现象**:集合详情页显示「该集合暂无文章」,但实际上已有文章被收录到该集合。
**根因分析**:
路由代码 `blog/src/routes/pages.py` 第 488-519 行:
```python
for item in collection.get("items", []):
post = get_post_by_slug(item["post_slug"], include_drafts=False)
if post:
items.append({...})
```
可能原因:
1. `blog_collection_items` 表中没有对应数据
2. 文章 slug 与集合中记录的 `post_slug` 不匹配
3. 文章是草稿状态但使用了 `include_drafts=False`
**需要验证**:查询数据库确认 `blog_collection_items` 表是否有数据。
---
### 问题 2:全部文章列表不显示集合归属
**页面**:`https://blog.ephron.ren/posts`
**现象**:文章列表只显示置顶、日期、标签、草稿标记,不显示文章所属的集合。
**用户期望**:已收录到集合的文章应在全部文章列表中显示集合标记(类似标签),方便识别哪些文章已被组织到集合中。
---
### 问题 3:新建文章/提示词时无法选择集合
**页面**:
- Blog:`https://blog.ephron.ren/admin/new`
- Prompt:`https://prompt.ephron.ren/admin/new`
**现象**:新建文章/提示词时,只能先创建内容,再手动编辑集合添加。
**用户期望**:创建时即可选择加入已有集合,提升内容组织效率。
---
## 二、需求详情
### 需求 1:修复集合详情页文章显示
**优先级**:P0(Bug 修复)
**验收标准**:
- [ ] 访问 `/collections/{key}` 能正确显示已收录的文章
- [ ] 文章按 `sort_order` 排序显示
- [ ] 显示文章标题、摘要、备注
- [ ] 空集合显示「该集合暂无文章」
**技术方案**:
1. 检查并修复 `blog_collection_items` 表数据
2. 确保 `get_post_by_slug` 在包含草稿时能正确返回
3. 添加日志排查 slug 匹配问题
---
### 需求 2:全部文章列表显示集合标记
**优先级**:P1(功能增强)
**验收标准**:
- [ ] 文章列表中,已收录到集合的文章显示集合标记
- [ ] 标记可点击,跳转到集合详情页
- [ ] 一篇文章可属于多个集合,显示所有集合标记
- [ ] 未收录到任何集合的文章不显示标记
**UI 设计**:
```
文章标题 📌 置顶
摘要内容...
2025-01-01 [标签1] [标签2] [集合A] [集合B] 草稿
```
集合标记样式:
- 背景色:`var(--accent-glow)`(蓝色半透明)
- 文字色:`var(--accent)`
- 圆角:`4px`
- 前缀图标:`📁` 或 `📚`
**技术方案**:
1. 在 `pages.py` 的 `posts_list` 函数中,批量查询文章的集合归属
2. 使用 `get_all_collection_post_slugs()` 或更高效的批量查询
3. 将集合信息传递给模板
4. 模板中渲染集合标记
**API 变更**:
```python
# 新增批量查询函数
def get_posts_collections(post_slugs: list[str]) -> dict[str, list[dict]]:
"""
批量查询多篇文章的集合归属
返回: {post_slug: [{key, title}, ...]}
"""
```
---
### 需求 3:新建文章/提示词时选择集合
**优先级**:P1(功能增强)
#### 3.1 Blog 新建文章
**页面**:`/admin/new`
**验收标准**:
- [ ] 新建文章表单增加「选择集合」下拉框
- [ ] 支持多选(可加入多个集合)
- [ ] 显示集合名称和已有文章数量
- [ ] 提交时自动创建 `blog_collection_items` 记录
- [ ] 权限控制:需要 `blog.post.create_draft` 权限
**UI 设计**:
```html
按住 Ctrl/Cmd 可多选
```
**技术方案**:
1. `admin.py` 的 `new_collection_page` 函数传递集合列表
2. 表单增加 `collection_keys` 字段(数组)
3. `create_new_post` 函数处理集合关联
4. 调用 `add_item_to_collection` 创建关联记录
**API 变更**:
```python
# POST /admin/new 新增字段
collection_keys: list[str] = Form(default=[]) # 集合 key 列表
```
#### 3.2 Prompt 新建提示词
**页面**:`/admin/new`
**验收标准**:
- [ ] 新建提示词表单增加「选择集合」下拉框
- [ ] 支持多选
- [ ] 提交时自动创建 `collection_items` 记录
- [ ] 权限控制:需要 `prompt.create` 权限
**技术方案**:同 Blog,修改 Prompt 服务的 `admin.py` 和相关模板。
#### 3.3 API 创建文章/提示词
**Blog Service API**:
```python
# POST /api/service/posts
{
"title": "文章标题",
"content": "...",
"tags": ["tag1"],
"collection_keys": ["col1", "col2"] # 新增
}
```
**Prompt Service API**:
```python
# POST /api/service/prompts
{
"key": "prompt-key",
"title": "提示词标题",
"content": "...",
"collection_keys": ["col1", "col2"] # 新增
}
```
**验收标准**:
- [ ] API 支持 `collection_keys` 参数
- [ ] 参数可选,默认为空数组
- [ ] 自动创建集合关联记录
- [ ] 集合不存在时忽略(不报错)
- [ ] 权限检查:需要对应集合的编辑权限
---
## 三、数据模型
### Blog 集合表结构
```sql
-- 集合主表
CREATE TABLE blog_collections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
description TEXT DEFAULT '',
cover_image TEXT DEFAULT '',
created_by TEXT,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT (datetime('now')),
updated_at DATETIME DEFAULT (datetime('now'))
);
-- 集合关联表
CREATE TABLE blog_collection_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
collection_key TEXT NOT NULL,
post_slug TEXT NOT NULL,
sort_order INTEGER DEFAULT 0,
note TEXT DEFAULT '',
created_at DATETIME DEFAULT (datetime('now')),
FOREIGN KEY (collection_key) REFERENCES blog_collections(key) ON DELETE CASCADE,
UNIQUE(collection_key, post_slug)
);
```
### Prompt 集合表结构
```sql
-- 集合主表
CREATE TABLE collections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
description TEXT DEFAULT '',
created_by TEXT,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT (datetime('now')),
updated_at DATETIME DEFAULT (datetime('now'))
);
-- 集合关联表
CREATE TABLE collection_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
collection_key TEXT NOT NULL,
prompt_key TEXT NOT NULL,
sort_order INTEGER DEFAULT 0,
note TEXT DEFAULT '',
created_at DATETIME DEFAULT (datetime('now')),
FOREIGN KEY (collection_key) REFERENCES collections(key) ON DELETE CASCADE,
UNIQUE(collection_key, prompt_key)
);
```
---
## 四、与 Prompt 集合逻辑的差异
| 特性 | Blog 集合 | Prompt 集合 |
|------|----------|------------|
| 内容类型 | 文章(Markdown) | 提示词(模板) |
| 主键 | `post_slug`(文件名) | `prompt_key`(数据库 key) |
| 创建方式 | 文件系统扫描 | 数据库插入 |
| 集合显示 | 需求2:全部文章列表显示集合标记 | 已有:全部提示词列表显示集合标记 |
| 创建时选择集合 | 需求3:新建时可选 | 需求3:新建时可选 |
**关键差异**:
- Blog 文章是文件系统驱动,集合关联是后加的
- Prompt 提示词是数据库驱动,集合关联是原生支持
---
## 五、实施计划
### Phase 1:Bug 修复(需求 1)
**时间**:0.5 天
**任务**:
1. [ ] 检查 `blog_collection_items` 表数据
2. [ ] 修复数据不一致问题
3. [ ] 验证集合详情页正常显示
### Phase 2:全部文章显示集合标记(需求 2)
**时间**:1 天
**任务**:
1. [ ] 实现 `get_posts_collections()` 批量查询函数
2. [ ] 修改 `pages.py` 的 `posts_list` 函数
3. [ ] 修改 `index.html` 模板
4. [ ] 测试多集合、空集合场景
### Phase 3:新建时选择集合(需求 3)
**时间**:1.5 天
**任务**:
1. [ ] Blog:修改 `admin.py` 和 `new.html`
2. [ ] Prompt:修改 `admin.py` 和 `new.html`
3. [ ] Blog API:修改 Service API 支持 `collection_keys`
4. [ ] Prompt API:修改 Service API 支持 `collection_keys`
5. [ ] 测试权限控制和边界场景
---
## 六、测试用例
### 需求 1 测试
| 编号 | 测试内容 | 预期结果 |
|------|---------|---------|
| T-001 | 访收录有文章的集合详情页 | 显示文章列表 |
| T-002 | 访问空集合详情页 | 显示「该集合暂无文章」 |
| T-003 | 文章排序 | 按 sort_order 升序 |
| T-004 | 草稿文章 | 不显示(公开页) |
### 需求 2 测试
| 编号 | 测试内容 | 预期结果 |
|------|---------|---------|
| T-010 | 已收录文章显示集合标记 | 显示蓝色标记 |
| T-011 | 未收录文章 | 不显示集合标记 |
| T-012 | 点击集合标记 | 跳转到集合详情页 |
| T-013 | 文章属于多个集合 | 显示多个标记 |
| T-014 | 搜索结果中显示 | 同样显示集合标记 |
### 需求 3 测试
| 编号 | 测试内容 | 预期结果 |
|------|---------|---------|
| T-020 | Blog 新建选择集合 | 自动关联到集合 |
| T-021 | Blog 新建不选集合 | 正常创建,不关联 |
| T-022 | Blog API 带 collection_keys | 自动关联 |
| T-023 | Prompt 新建选择集合 | 自动关联到集合 |
| T-024 | Prompt API 带 collection_keys | 自动关联 |
| T-025 | 集合 key 不存在 | 忽略,不报错 |
| T-026 | 权限不足 | 返回 403 |
---
## 七、风险与依赖
### 风险
1. **数据一致性**:Blog 文章删除后,`blog_collection_items` 中的关联记录可能成为孤立数据
2. **性能影响**:全部文章列表需要额外查询集合信息,可能增加数据库压力
3. **权限复杂性**:创建文章时选择集合需要同时检查文章和集合的权限
### 依赖
1. 需求 1 是需求 2 的前提(集合详情页必须能正常显示)
2. 需求 3 依赖现有集合 CRUD 功能
3. Blog 和 Prompt 服务的集合逻辑独立,可并行开发
---
## 八、相关代码位置
### Blog 服务
- 集合详情路由:`blog/src/routes/pages.py` 第 488-519 行
- 文章列表路由:`blog/src/routes/pages.py` 第 120-160 行
- Admin 新建路由:`blog/src/routes/admin.py` 第 431-497 行
- Service API 路由:`blog/src/routes/service_api.py`
- 集合服务层:`blog/src/services/blog_collections.py`
- 集合详情模板:`blog/templates/collection_detail.html`
- 文章列表模板:`blog/templates/index.html`
- Admin 新建模板:`blog/templates/admin/new.html`
### Prompt 服务
- Admin 新建路由:`prompt/src/routes/admin.py`
- Service API 路由:`prompt/src/routes/service_api.py`
- 集合服务层:`prompt/src/services/collections.py`
- Admin 新建模板:`prompt/templates/admin/new.html`
---
## 九、验收标准汇总
- [ ] 集合详情页正确显示已收录文章
- [ ] 全部文章列表显示集合标记
- [ ] 新建文章时可选择加入集合
- [ ] 新建提示词时可选择加入集合
- [ ] API 创建支持 `collection_keys` 参数
- [ ] 权限控制正确
- [ ] 边界场景处理(空集合、不存在的 key、重复关联)