feat(read): 已读功能 — 每账号标已读,列表默认隐藏
需求: 每个账号可标已读,已读过的文章刷新/重载后不在 24h feed 中显示。
设计:
- 新表 article_reads (user_id, article_id, read_at) 复合主键,on-delete CASCADE
- 迁移 0007_article_reads
- /me/reads/{id} POST/DELETE 标记 / 取消(幂等,PG upsert on_conflict_do_nothing)
- /me/reads GET 列出已读 IDs(默认 7 天,limit 500)
- articles.py 列表查询加 hide_read=true 参数(默认 true),用 NOT EXISTS 排除已读
- ArticleListItem / ArticleDetail schema 加 is_read 字段
前端:
- types 加 is_read + readsApi(mark/unmark/list)
- Feed 列表:
顶部加 '隐藏已读' 开关,默认 ON
每张卡片加 '标为已读 / 标为未读' 按钮(乐观更新,失败回滚)
已读卡片 opacity 0.7 + 灰背景,标识弱化
- ArticleDetail 详情页操作栏加 '标为已读' 按钮(同样乐观)
This commit is contained in:
54
backend/alembic/versions/0007_article_reads.py
Normal file
54
backend/alembic/versions/0007_article_reads.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""文章已读记录(per-user)
|
||||
|
||||
- article_reads.user_id BIGINT FK users.id ON DELETE CASCADE
|
||||
- article_reads.article_id BIGINT FK articles.id ON DELETE CASCADE
|
||||
- article_reads.read_at TIMESTAMPTZ DEFAULT now()
|
||||
- 复合主键 (user_id, article_id) — 天然幂等
|
||||
|
||||
Revision ID: 0007
|
||||
Revises: 0006
|
||||
Create Date: 2026-06-13
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0007"
|
||||
down_revision: Union[str, None] = "0006"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"article_reads",
|
||||
sa.Column("user_id", sa.BigInteger, nullable=False),
|
||||
sa.Column("article_id", sa.BigInteger, nullable=False),
|
||||
sa.Column("read_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.PrimaryKeyConstraint("user_id", "article_id", name="pk_article_reads"),
|
||||
sa.ForeignKeyConstraint(
|
||||
"user_id", ["users.id"], ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
"article_id", ["articles.id"], ondelete="CASCADE",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_article_reads_user_read_at",
|
||||
"article_reads",
|
||||
["user_id", "read_at"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_article_reads_article",
|
||||
"article_reads",
|
||||
["article_id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_article_reads_article", table_name="article_reads")
|
||||
op.drop_index("ix_article_reads_user_read_at", table_name="article_reads")
|
||||
op.drop_table("article_reads")
|
||||
Reference in New Issue
Block a user