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

7.1 KiB
Raw Permalink Blame History

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

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

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 格式,精确到秒:

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,
    ...
}

示例输出:

---
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 字段:

@dataclass
class PostMeta:
    slug: str
    title: str
    date: date
    created_at: datetime | None  # 新增
    tags: list[str]
    # ... 其余字段不变

解析逻辑:

# 在 _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

# 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_metacreate_postget_all_postssearch_posts
前端模板 无需修改
service_api.py 无需修改
admin.py 无需修改
数据库 无需迁移(集合表结构不变)
历史文章 兼容,无 created_at 时回退到 st_mtime

不做的事

  • 不改集合表结构(不加 added_at 列)
  • 不改前端拖拽排序逻辑
  • 不改 date 字段格式(保持 YYYY-MM-DD
  • 不对历史文章批量补写 created_at