2026-06-07 21:51:01 +08:00
|
|
|
"""Article schemas."""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SourceBrief(BaseModel):
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
name: str
|
|
|
|
|
slug: str
|
|
|
|
|
region: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ArticleListItem(BaseModel):
|
2026-06-10 12:07:04 +08:00
|
|
|
"""列表项:首页展示标题/译标/正文摘要/分类/插图,详细阅读进详情页。"""
|
2026-06-07 21:51:01 +08:00
|
|
|
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
source: SourceBrief
|
|
|
|
|
title: str
|
|
|
|
|
title_zh: str | None = None
|
2026-06-10 12:07:04 +08:00
|
|
|
# 翻译后的正文(纯文本);列表里截断显示,详情页展示完整
|
|
|
|
|
body_zh_text: str | None = None
|
2026-06-07 21:51:01 +08:00
|
|
|
summary_zh: str | None = None
|
|
|
|
|
lang_src: str | None = None
|
|
|
|
|
translation_status: str
|
|
|
|
|
category: str | None = None
|
|
|
|
|
published_at: datetime | None = None
|
|
|
|
|
fetched_at: datetime
|
|
|
|
|
image_url: str | None = None
|
2026-06-09 15:59:48 +08:00
|
|
|
# === 列表预览钩子:点击进详情前的"诱导点" ===
|
2026-06-12 19:00:00 +08:00
|
|
|
# 双 provider 评论:Angel(原字段) + 美团(meituan 字段),前端两条都展示
|
|
|
|
|
commentary: str | None = None # Angel 评论(列表里截断显示)
|
2026-06-09 15:59:48 +08:00
|
|
|
commentary_status: str | None = None # ok/failed/pending/n/a
|
2026-06-12 19:00:00 +08:00
|
|
|
commentary_meituan: str | None = None # 美团评论
|
|
|
|
|
commentary_meituan_status: str | None = None
|
|
|
|
|
commentary_engine: str | None = None # angel / meituan / "angel,meituan"
|
2026-06-09 15:59:48 +08:00
|
|
|
image_ai_url: str | None = None # AI 插图(列表里缩略图)
|
2026-06-07 21:51:01 +08:00
|
|
|
is_starred: bool = False
|
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 详情页操作栏加 '标为已读' 按钮(同样乐观)
2026-06-13 21:04:47 +08:00
|
|
|
is_read: bool = False
|
2026-06-07 21:51:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ArticleDetail(BaseModel):
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
source: SourceBrief
|
|
|
|
|
url: str
|
|
|
|
|
title: str
|
|
|
|
|
body_html: str | None = None
|
|
|
|
|
body_text: str
|
|
|
|
|
title_zh: str | None = None
|
|
|
|
|
body_zh_html: str | None = None
|
|
|
|
|
body_zh_text: str | None = None
|
2026-06-08 14:24:23 +08:00
|
|
|
body_zh_formatted: str | None = None # LLM 排版后
|
2026-06-07 21:51:01 +08:00
|
|
|
summary_zh: str | None = None
|
|
|
|
|
lang_src: str | None = None
|
|
|
|
|
author: str | None = None
|
|
|
|
|
image_url: str | None = None
|
2026-06-08 14:24:23 +08:00
|
|
|
image_ai_url: str | None = None # LLM 生成的插图
|
2026-06-07 21:51:01 +08:00
|
|
|
translation_status: str
|
|
|
|
|
translation_engine: str | None = None
|
|
|
|
|
translated_at: datetime | None = None
|
2026-06-08 14:24:23 +08:00
|
|
|
# === LLM 增强状态 + 内容 ===
|
2026-06-07 21:51:01 +08:00
|
|
|
category: str | None = None
|
2026-06-08 14:24:23 +08:00
|
|
|
format_status: str | None = None # pending/ok/failed/n/a
|
|
|
|
|
classify_status: str | None = None
|
|
|
|
|
image_ai_status: str | None = None
|
2026-06-12 19:00:00 +08:00
|
|
|
# 双 provider 评论
|
2026-06-08 14:24:23 +08:00
|
|
|
commentary_status: str | None = None
|
2026-06-12 19:00:00 +08:00
|
|
|
commentary: str | None = None # Angel
|
|
|
|
|
commentary_engine: str | None = None
|
|
|
|
|
commentary_meituan_status: str | None = None
|
|
|
|
|
commentary_meituan: str | None = None
|
|
|
|
|
commentary_meituan_model: str | None = None
|
|
|
|
|
commentary_meituan_error: str | None = None
|
2026-06-07 21:51:01 +08:00
|
|
|
entities: dict | None = None
|
|
|
|
|
sentiment: float | None = None
|
|
|
|
|
duplicate_of: int | None = None
|
|
|
|
|
published_at: datetime | None = None
|
|
|
|
|
fetched_at: datetime
|
|
|
|
|
is_starred: bool = False
|
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 详情页操作栏加 '标为已读' 按钮(同样乐观)
2026-06-13 21:04:47 +08:00
|
|
|
is_read: bool = False
|
2026-06-07 21:51:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ArticleListResponse(BaseModel):
|
|
|
|
|
items: list[ArticleListItem]
|
2026-06-10 12:07:04 +08:00
|
|
|
# 页码分页
|
|
|
|
|
page: int = 1
|
|
|
|
|
page_size: int = 50
|
|
|
|
|
total: int
|
|
|
|
|
total_pages: int
|
2026-06-07 21:51:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ArticleQuery(BaseModel):
|
|
|
|
|
"""用作 ?query= 解析参考(实际 FastAPI 直接用 Query)。"""
|
|
|
|
|
|
|
|
|
|
since: datetime | None = None
|
|
|
|
|
until: datetime | None = None
|
|
|
|
|
source: str | None = None # 逗号分隔 slug
|
|
|
|
|
category: str | None = None
|
|
|
|
|
q: str | None = None
|
|
|
|
|
lang: str = Field(default="both", pattern=r"^(src|zh|both)$")
|
2026-06-10 12:07:04 +08:00
|
|
|
page: int = Field(default=1, ge=1)
|
|
|
|
|
page_size: int = Field(default=50, ge=1, le=200)
|
2026-06-07 21:51:01 +08:00
|
|
|
starred_only: bool = False
|