init: consolidate all ephron.ren PRDs and docs
This commit is contained in:
229
PRD-blog-sort-and-created-at.md
Normal file
229
PRD-blog-sort-and-created-at.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user