Files
ephron-ren-prd/PRD-blog-sort-and-created-at.md

230 lines
7.1 KiB
Markdown
Raw 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.
# PRD: 博客集合排序 & 文章生成时间记录
## 背景
### 问题一:集合内新文章排序异常
当前 AI-daily 集合中,新加入的文章排在第二位而非第一位。
**根因分析:**
`add_item_to_collection()` 默认 `sort_order=0`,而集合中第一篇文章的 sort_order 也是 0。当两条记录 sort_order 相同时SQLite 按 ROWID插入顺序做 tie-break导致先插入的老文章排在前面新文章排到第二位。
**相关代码:**
- `blog/src/services/blog_collections.py` 第 170-192 行:`add_item_to_collection` 默认 `sort_order=0`
- `blog/src/routes/service_api.py` 第 247 行:创建文章时调用 `add_item_to_collection(col_key, slug)` 未传 sort_order
- `blog/src/services/blog_collections.py` 第 49 行:查询排序 `ORDER BY bci.sort_order`(升序)
### 问题二:同日期文章排序依赖文件系统时间
当前排序逻辑(最新代码 ff539d4
```python
posts.sort(key=lambda p: (not p.pinned, -p.date.toordinal(), -p.file_path.stat().st_mtime))
```
`st_mtime`(文件修改时间)做同日期 tie-break。问题
- 文章被编辑后 mtime 会变,排序不再是「生成时间」而是「最后编辑时间」
- frontmatter 中只有 `date: YYYY-MM-DD`,不记录精确的生成时间
**相关代码:**
- `blog/src/services/posts.py` 第 325 行:`get_all_posts` 排序
- `blog/src/services/posts.py` 第 643 行:`search_posts` 排序
- `blog/src/services/posts.py` 第 837-839 行:`create_post` 只写入 `date.today().isoformat()`
---
## 需求
### 需求一:集合内新文章默认排在最前面
新加入集合的文章应自动排在集合内所有文章的最前面sort_order 最小)。
### 需求二:记录文章精确生成时间
在 frontmatter 中新增 `created_at` 字段,记录文章创建的精确时间(东八区,精确到秒),用于同日期文章排序的 tie-break。
---
## 详细设计
### 一、集合排序修复
**修改文件:** `blog/src/services/blog_collections.py`
**修改函数:** `add_item_to_collection()`
**方案:** 插入前查询当前集合的最小 sort_order新文章设为 `min_sort_order - 1`
```python
def add_item_to_collection(
collection_key: str,
post_slug: str,
sort_order: int | None = None, # None 表示自动计算
note: str = "",
) -> bool:
"""向集合添加文章(新文章默认排在最前面)"""
try:
with get_connection() as conn:
cursor = conn.cursor()
# 自动计算 sort_order取当前最小值 - 1
if sort_order is None:
cursor.execute(
"SELECT MIN(sort_order) FROM blog_collection_items WHERE collection_key = ?",
(collection_key,)
)
row = cursor.fetchone()
min_order = row[0] if row and row[0] is not None else 0
sort_order = min_order - 1
cursor.execute(
"""
INSERT OR REPLACE INTO blog_collection_items
(collection_key, post_slug, sort_order, note)
VALUES (?, ?, ?, ?)
""",
(collection_key, post_slug, sort_order, note)
)
conn.commit()
return True
except Exception as e:
print(f"Error adding item to blog collection: {e}")
return False
```
**对调用方的影响:**
| 调用方 | 是否需要修改 | 说明 |
|--------|-------------|------|
| `service_api.py` 第 247 行 | 不需要 | `add_item_to_collection(col_key, slug)` 不传 sort_order自动走新逻辑 |
| `create_collection_with_items()` | 不需要 | 批量创建集合时 sort_order 由调用方显式传入,不受影响 |
| `update_collection_items()` | 不需要 | 手动排序时显式传入 sort_order不受影响 |
| 前端拖拽排序 | 不需要 | 前端传 0,1,2,3... 显式值,不受影响 |
**与手动排序的交互:**
- 手动拖拽排序会将所有 sort_order 重置为 0, 1, 2, 3...(前端实现,不改)
- 手动排序后再有新文章加入 → 自动取 min(0) - 1 = -1 → 排在手动排序文章前面 ✅
- 不会与手动排序产生冲突
---
### 二、文章生成时间记录
**修改文件:** `blog/src/services/posts.py`
#### 2.1 新增 `created_at` 字段
**修改函数:** `create_post()`
在 frontmatter 中新增 `created_at` 字段使用东八区时间ISO 8601 格式,精确到秒:
```python
from datetime import date, datetime, timezone, timedelta
CST = timezone(timedelta(hours=8))
# create_post() 中:
frontmatter = {
"title": title,
"date": date.today().isoformat(),
"created_at": datetime.now(CST).isoformat(), # 新增
"views": 0,
"likes": 0,
"draft": draft,
...
}
```
**示例输出:**
```yaml
---
title: AI日报 · 2026-05-15
date: 2026-05-15
created_at: '2026-05-15T08:30:45+08:00'
---
```
#### 2.2 解析 `created_at` 字段
**修改位置:** `_parse_post_meta()` 函数
`PostMeta` dataclass 和解析逻辑中新增 `created_at` 字段:
```python
@dataclass
class PostMeta:
slug: str
title: str
date: date
created_at: datetime | None # 新增
tags: list[str]
# ... 其余字段不变
```
解析逻辑:
```python
# 在 _parse_post_meta() 中
created_at = None
created_at_raw = frontmatter.get("created_at")
if isinstance(created_at_raw, datetime):
created_at = created_at_raw
elif isinstance(created_at_raw, str):
try:
created_at = datetime.fromisoformat(created_at_raw)
except ValueError:
logger.warning(f"Invalid created_at format in {file_path}")
```
#### 2.3 修改排序逻辑
**修改位置:** `get_all_posts()``search_posts()` 的排序
`st_mtime` 替换为 `created_at`
```python
# get_all_posts() 和 search_posts() 中
posts.sort(key=lambda p: (
not p.pinned,
-p.date.toordinal(),
-(p.created_at.timestamp() if p.created_at else p.file_path.stat().st_mtime)
))
```
**兼容性说明:**
- 历史文章没有 `created_at` 字段 → 回退到 `st_mtime`,行为与当前一致
- 新文章有 `created_at` → 用精确时间排序,不受编辑影响
#### 2.4 更新文章时保留 `created_at`
**修改函数:** `update_post()`
`update_post` 不应覆盖 `created_at`(它是首次创建时间,不是更新时间)。当前 `update_post` 只修改传入的字段,不传 `created_at` 就不会被覆盖,**无需额外修改**。
---
## 影响范围
| 模块 | 影响 |
|------|------|
| `blog_collections.py` | 修改 `add_item_to_collection` 一个函数 |
| `posts.py` | 修改 `PostMeta``_parse_post_meta``create_post``get_all_posts``search_posts` |
| 前端模板 | 无需修改 |
| `service_api.py` | 无需修改 |
| `admin.py` | 无需修改 |
| 数据库 | 无需迁移(集合表结构不变) |
| 历史文章 | 兼容,无 `created_at` 时回退到 `st_mtime` |
## 不做的事
- 不改集合表结构(不加 `added_at` 列)
- 不改前端拖拽排序逻辑
- 不改 `date` 字段格式(保持 `YYYY-MM-DD`
- 不对历史文章批量补写 `created_at`