mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-06 00:57:51 +08:00
login+ai cluster
This commit is contained in:
+194
-111
@@ -29,35 +29,40 @@ BigIntType = BigInteger().with_variant(Integer, "sqlite")
|
||||
|
||||
class SourceType(str, enum.Enum):
|
||||
"""信息源的抓取方式"""
|
||||
HOT_TREND = "HOT_TREND" # 热搜榜单类
|
||||
RSS_FEED = "RSS_FEED" # 传统RSS订阅
|
||||
API = "API" # 接口抓取类
|
||||
HOT_TREND = "HOT_TREND" # 热搜榜单类 (如微博热搜)
|
||||
RSS_FEED = "RSS_FEED" # 传统RSS订阅 (如36氪、纽约时报)
|
||||
API = "API" # 接口直接接入类
|
||||
|
||||
|
||||
class TargetType(str, enum.Enum):
|
||||
"""
|
||||
多态目标类型 (Polymorphic Target)
|
||||
用于标记一条评论或一个标签到底是挂载在哪个实体下的。
|
||||
用于标记一条评论、标签或推送记录,到底是挂载在哪个实体下的。
|
||||
"""
|
||||
EVENT = "EVENT" # 挂载在单个热搜事件下
|
||||
TREND = "TREND" # 挂载在宏观趋势下
|
||||
ARTICLE = "ARTICLE" # 挂载在具体新闻文章下
|
||||
EVENT = "EVENT" # 挂载在AI聚合后的大事件下
|
||||
TREND = "TREND" # 挂载在单个平台的热搜条目下
|
||||
ARTICLE = "ARTICLE" # 挂载在具体的长篇新闻文章下
|
||||
|
||||
|
||||
class TaskStatus(str, enum.Enum):
|
||||
"""后台任务状态"""
|
||||
SUCCESS = "SUCCESS"
|
||||
ERROR = "ERROR"
|
||||
"""后台爬虫/推送任务的执行状态"""
|
||||
SUCCESS = "SUCCESS" # 执行成功
|
||||
ERROR = "ERROR" # 发生报错
|
||||
|
||||
|
||||
class GenderType(str, enum.Enum):
|
||||
"""用户性别枚举"""
|
||||
"""用户性别枚举,常用于给AI提供Prompt背景信息以生成个性化摘要"""
|
||||
MALE = "MALE"
|
||||
FEMALE = "FEMALE"
|
||||
OTHER = "OTHER"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
class VerificationPurpose(str, enum.Enum):
|
||||
REGISTER = "REGISTER"
|
||||
LOGIN = "LOGIN"
|
||||
|
||||
|
||||
def utcnow():
|
||||
"""
|
||||
获取带UTC时区的当前时间 (最佳实践)
|
||||
@@ -77,10 +82,14 @@ class InfoSource(Base):
|
||||
__tablename__ = "info_sources"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
source_name: Mapped[str] = mapped_column(String(100), comment="信息源名称")
|
||||
source_type: Mapped[SourceType] = mapped_column(Enum(SourceType))
|
||||
home_url: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
is_enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
# 信息源的展示名称,例如 "微博热搜", "今日头条"
|
||||
source_name: Mapped[str] = mapped_column(String(100), comment="信息源中文名称")
|
||||
# 抓取类型,决定爬虫调用哪个解析逻辑
|
||||
source_type: Mapped[SourceType] = mapped_column(Enum(SourceType), comment="抓取方式枚举")
|
||||
# 极其重要:原意存官网链接,但实际开发中常借用来存放 API的专属标识(如 'weibo', 'toutiao')
|
||||
home_url: Mapped[Optional[str]] = mapped_column(String(255), comment="官网链接或API的平台标识ID")
|
||||
# 爬虫开关:如果某平台封禁了我们,可以直接置为False,爬虫将自动跳过该平台
|
||||
is_enabled: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否开启定时抓取")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
|
||||
@@ -91,19 +100,22 @@ class InfoSource(Base):
|
||||
# ==========================================
|
||||
class UnifiedEvent(Base):
|
||||
"""
|
||||
AI 统一事件表
|
||||
核心业务逻辑:比如微博热搜叫“苹果发布会”,知乎热搜叫“iPhone 16 测评”,
|
||||
它们在子表(TrendingEvent)是两条记录,但通过 AI 语义向量对比后,
|
||||
会将它们统一挂载到这个表的一个 UnifiedEvent ID 下,实现跨平台事件聚合。
|
||||
AI 统一事件表 (核心大脑)
|
||||
逻辑:微博的“苹果发布会”和知乎的“iPhone 16 测评”,通过语义相似度碰撞后,统一归入此表的一行记录中。
|
||||
"""
|
||||
__tablename__ = "unified_events"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigIntType, primary_key=True, autoincrement=True)
|
||||
unified_title: Mapped[str] = mapped_column(String(255), comment="AI统一标题")
|
||||
ai_comprehensive_summary: Mapped[Optional[str]] = mapped_column(Text, comment="AI全局深度总结")
|
||||
# 经过AI润色去重后的中立、客观的标准大标题
|
||||
unified_title: Mapped[str] = mapped_column(String(255), comment="AI生成的客观统一大标题")
|
||||
# AI阅读子新闻后生成的千字长文摘要,直接用于早报推送
|
||||
ai_comprehensive_summary: Mapped[Optional[str]] = mapped_column(Text, comment="AI综合全网子新闻生成的深度总结")
|
||||
|
||||
center_embedding: Mapped[Optional[str]] = mapped_column(Text, comment="中心向量") # 用于高维空间相似度计算
|
||||
hot_score: Mapped[int] = mapped_column(Integer, default=0, comment="聚合热度得分")
|
||||
# [高阶字段] 将文本转化成高维浮点数向量,爬虫抓到新新闻时,跟这个向量算余弦相似度来判断是不是同一个事件
|
||||
center_embedding: Mapped[Optional[str]] = mapped_column(Text, comment="该事件簇的中心语义向量")
|
||||
# 事件热度值:挂载的平台越多、相关评论越多,分数越高,用于首页的热榜排序
|
||||
hot_score: Mapped[int] = mapped_column(Integer, default=0, comment="聚合热度得分(分数越高排名越靠前)")
|
||||
last_summarized_trends_count: Mapped[int] = mapped_column(Integer, default=0, comment="用于判断是否需要重新调用LLM")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
|
||||
@@ -115,51 +127,71 @@ class UnifiedEvent(Base):
|
||||
class TrendingEvent(Base):
|
||||
"""
|
||||
各平台热搜数据明细表
|
||||
存放从爬虫直接拉取下来的最原始的热搜数据。
|
||||
"""
|
||||
__tablename__ = "trending_events"
|
||||
__table_args__ = (
|
||||
# 联合唯一索引:同一个来源(比如微博)的同一条外部ID(MD5)只能存在一条记录,防重插核心保障
|
||||
# 联合唯一索引:同一个来源的同一个哈希只能存一条,完美实现 UPSERT (去重更新)
|
||||
UniqueConstraint("source_id", "external_id", name="idx_unique_external_trend"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigIntType, primary_key=True, autoincrement=True)
|
||||
source_id: Mapped[int] = mapped_column(ForeignKey("info_sources.id"))
|
||||
unified_event_id: Mapped[Optional[int]] = mapped_column(ForeignKey("unified_events.id"))
|
||||
# 关联:这条热搜是从哪个平台(InfoSource)抓来的
|
||||
source_id: Mapped[int] = mapped_column(ForeignKey("info_sources.id"), comment="所属信息源ID")
|
||||
# 关联:它被AI归类到了哪个大事件(UnifiedEvent)之下 (可为空,表示未归类的新鲜事)
|
||||
unified_event_id: Mapped[Optional[int]] = mapped_column(ForeignKey("unified_events.id"),
|
||||
comment="所属的聚合大事件ID")
|
||||
|
||||
external_id: Mapped[str] = mapped_column(String(32), comment="32位MD5哈希指纹防重")
|
||||
title_embedding: Mapped[Optional[str]] = mapped_column(Text)
|
||||
# 极其核心:将第三方易变的标题/URL,强制压平为32位不变的 MD5 字符串,用作唯一防重指纹
|
||||
external_id: Mapped[str] = mapped_column(String(32), comment="通过平台ID+原始ID生成的32位MD5防重指纹")
|
||||
# 这条特定热搜标题的独立语义向量,用于和 unified_events 做碰撞
|
||||
title_embedding: Mapped[Optional[str]] = mapped_column(Text, comment="标题的语义向量")
|
||||
|
||||
icon_url: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
current_headline: Mapped[str] = mapped_column(String(255))
|
||||
event_url: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
app_link: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
current_ranking: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
brief_snippet: Mapped[Optional[str]] = mapped_column(Text)
|
||||
# 爬虫抓下来的热搜配图、带'爆'或'热'字的角标链接
|
||||
icon_url: Mapped[Optional[str]] = mapped_column(String(500), comment="热榜附带的小图标或配图链接")
|
||||
# 最新标题 (注意:小编随时可能改标题,所以绝不能放入唯一索引)
|
||||
current_headline: Mapped[str] = mapped_column(String(255), comment="当前最新的热搜标题")
|
||||
# 该热点在PC/H5端的访问链接
|
||||
event_url: Mapped[Optional[str]] = mapped_column(String(500), comment="浏览器访问链接")
|
||||
# 该热点专门用于手机App唤醒的 DeepLink (如 sinaweibo://...)
|
||||
app_link: Mapped[Optional[str]] = mapped_column(String(500), comment="移动端App唤醒专属链接")
|
||||
# 本次抓取时,它在平台上的名次 (如 1, 2, 3)
|
||||
current_ranking: Mapped[Optional[int]] = mapped_column(Integer, comment="当前最新排名(可能随时上下浮动)")
|
||||
# 有些平台在热搜底下会配一句话简介
|
||||
brief_snippet: Mapped[Optional[str]] = mapped_column(Text, comment="平台自带的一句话简介或导语")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
|
||||
|
||||
|
||||
class NewsArticle(Base):
|
||||
"""新闻文章明细表 (与 TrendingEvent 类似,但侧重长文本阅读)"""
|
||||
"""
|
||||
新闻文章明细表 (长篇资讯)
|
||||
与 TrendingEvent 类似,但它主要用来存放 36氪、纽约时报等长篇正文,用于提供深度的阅读素材。
|
||||
"""
|
||||
__tablename__ = "news_articles"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("source_id", "external_id", name="idx_unique_external_article"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigIntType, primary_key=True, autoincrement=True)
|
||||
source_id: Mapped[int] = mapped_column(ForeignKey("info_sources.id"))
|
||||
unified_event_id: Mapped[Optional[int]] = mapped_column(ForeignKey("unified_events.id"))
|
||||
source_id: Mapped[int] = mapped_column(ForeignKey("info_sources.id"), comment="所属信息源ID")
|
||||
unified_event_id: Mapped[Optional[int]] = mapped_column(ForeignKey("unified_events.id"),
|
||||
comment="深度文章也可归入大事件分析")
|
||||
|
||||
external_id: Mapped[str] = mapped_column(String(32))
|
||||
title_embedding: Mapped[Optional[str]] = mapped_column(Text)
|
||||
external_id: Mapped[str] = mapped_column(String(32), comment="RSS原文<guid>生成的MD5防重指纹")
|
||||
title_embedding: Mapped[Optional[str]] = mapped_column(Text, comment="新闻标题/摘要的语义向量")
|
||||
|
||||
cover_image_url: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
article_title: Mapped[str] = mapped_column(String(255))
|
||||
article_url: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
author_name: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
original_summary: Mapped[Optional[str]] = mapped_column(Text)
|
||||
publish_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
# 新闻文章的封面大图,很适合前端做瀑布流展示
|
||||
cover_image_url: Mapped[Optional[str]] = mapped_column(String(500), comment="新闻封面大图链接")
|
||||
article_title: Mapped[str] = mapped_column(String(255), comment="新闻原文标题")
|
||||
article_url: Mapped[Optional[str]] = mapped_column(String(500), comment="新闻原文链接")
|
||||
# 作者或发布机构 (如 "澎湃新闻", "虎嗅作者X")
|
||||
author_name: Mapped[Optional[str]] = mapped_column(String(100), comment="作者或发布机构名称")
|
||||
# RSS原文中附带的长摘要,甚至是完整的 HTML 格式正文
|
||||
original_summary: Mapped[Optional[str]] = mapped_column(Text, comment="原文自带的长摘要或正文片段")
|
||||
# 新闻在平台上的真实发布时间 (可能比我们爬取的时间要早几天)
|
||||
publish_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), comment="新闻实际发布的时间")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
|
||||
@@ -171,33 +203,40 @@ class NewsArticle(Base):
|
||||
class HeadlineRevision(Base):
|
||||
"""
|
||||
标题修订历史表
|
||||
用于记录平台方暗戳戳修改热搜词条的行为(例如公关介入改标题)。
|
||||
当系统通过哈希发现某条新闻是老熟人,但标题发生了改变时,会自动往这里插一条记录。
|
||||
常用于公关监测(看看谁半夜偷偷改了标题)。
|
||||
"""
|
||||
__tablename__ = "headline_revisions"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigIntType, primary_key=True, autoincrement=True)
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("trending_events.id"))
|
||||
previous_headline: Mapped[str] = mapped_column(String(255))
|
||||
revised_headline: Mapped[str] = mapped_column(String(255))
|
||||
# 属于哪一条被修改的热搜
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("trending_events.id"), comment="关联的热搜ID")
|
||||
previous_headline: Mapped[str] = mapped_column(String(255), comment="修改前的旧标题")
|
||||
revised_headline: Mapped[str] = mapped_column(String(255), comment="修改后的新标题")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow,
|
||||
comment="系统发现被修改的时间")
|
||||
|
||||
|
||||
class RankingLog(Base):
|
||||
"""
|
||||
热搜排名时间序列化日志
|
||||
每一次抓取都会生成一条记录,可以用于前端绘制热搜“排名起伏折线图”。
|
||||
每次爬虫运行(例如每10分钟),都会往这里塞一堆数据,记录某热搜当时的具体名次。
|
||||
前端可以通过这张表画出非常漂亮的“名次起伏折线图(K线图)”。
|
||||
"""
|
||||
__tablename__ = "ranking_logs"
|
||||
__table_args__ = (
|
||||
# 针对时间序列查询优化的复合索引,加速类似 "查询某事件在过去24小时内的排名变化" 的操作
|
||||
# 复合索引,加速 "查询某事件在某段时间内的走势"
|
||||
Index("idx_event_time", "event_id", "observed_at"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigIntType, primary_key=True, autoincrement=True)
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("trending_events.id"))
|
||||
ranking_position: Mapped[int] = mapped_column(Integer)
|
||||
observed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("trending_events.id"), comment="关联的热搜ID")
|
||||
# 当时它在第几名
|
||||
ranking_position: Mapped[int] = mapped_column(Integer, comment="当时抓取时的排名名次")
|
||||
# 爬虫看到它的那一瞬间的时间
|
||||
observed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow,
|
||||
comment="观察到该名次的准确时间")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
|
||||
@@ -205,44 +244,49 @@ class RankingLog(Base):
|
||||
# ==========================================
|
||||
# 模块五:多态话题与多态评论
|
||||
# ==========================================
|
||||
# 【设计模式】:多态设计
|
||||
# 通过 target_type (存表名/类型) + target_id (存主键ID) 的组合,
|
||||
# 让这两个表既能挂载在"单一热搜"下,也能挂载在"新闻文章"下,甚至挂在"统一大事件"下,避免了建立无数个外键的冗余。
|
||||
|
||||
class ExtractedTopic(Base):
|
||||
"""AI 提取的核心话题标签表"""
|
||||
"""
|
||||
AI 提取的核心话题标签表
|
||||
设计模式(多态):一条标签("AI")既能打在大事件上,也能打在单篇文章上。
|
||||
"""
|
||||
__tablename__ = "extracted_topics"
|
||||
__table_args__ = (
|
||||
Index("idx_topic_keyword", "topic_keyword"),
|
||||
# 多态查询索引,加速 target_type + target_id 的组合查询
|
||||
Index("idx_polymorphic_topics", "target_type", "target_id"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigIntType, primary_key=True, autoincrement=True)
|
||||
target_type: Mapped[TargetType] = mapped_column(Enum(TargetType))
|
||||
target_id: Mapped[int] = mapped_column(BigIntType)
|
||||
topic_keyword: Mapped[str] = mapped_column(String(100))
|
||||
relevance_score: Mapped[Optional[float]] = mapped_column(Float)
|
||||
target_type: Mapped[TargetType] = mapped_column(Enum(TargetType), comment="挂载目标的类型(大事件/热点/文章)")
|
||||
target_id: Mapped[int] = mapped_column(BigIntType, comment="对应的具体主键ID")
|
||||
# 提取出的标签词,例如 "自动驾驶", "马斯克"
|
||||
topic_keyword: Mapped[str] = mapped_column(String(100), comment="提取出的核心关键词汇")
|
||||
# AI 认为这个词和这篇文章的相关程度(0~100),方便以后做精准度过滤
|
||||
relevance_score: Mapped[Optional[float]] = mapped_column(Float, comment="AI计算的相关度得分")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
|
||||
|
||||
class DiscussionComment(Base):
|
||||
"""全平台统一评论表"""
|
||||
"""
|
||||
全平台统一评论库
|
||||
不论是微博网友的短评,还是新闻网站的长评,全部扔进这张多态表。
|
||||
"""
|
||||
__tablename__ = "discussion_comments"
|
||||
__table_args__ = (
|
||||
Index("idx_polymorphic_comments", "target_type", "target_id"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigIntType, primary_key=True, autoincrement=True)
|
||||
target_type: Mapped[TargetType] = mapped_column(Enum(TargetType))
|
||||
target_id: Mapped[int] = mapped_column(BigIntType)
|
||||
target_type: Mapped[TargetType] = mapped_column(Enum(TargetType), comment="被评论内容的类型")
|
||||
target_id: Mapped[int] = mapped_column(BigIntType, comment="被评论内容的主键ID")
|
||||
|
||||
commenter_name: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
comment_content: Mapped[str] = mapped_column(Text)
|
||||
likes_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
external_comment_id: Mapped[Optional[str]] = mapped_column(String(32))
|
||||
comment_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
commenter_name: Mapped[Optional[str]] = mapped_column(String(100), comment="发评人昵称")
|
||||
comment_content: Mapped[str] = mapped_column(Text, comment="评论正文内容")
|
||||
# 这条评论本身获得的点赞数,可用于筛选出“神评论”一并推送给用户
|
||||
likes_count: Mapped[int] = mapped_column(Integer, default=0, comment="评论被点赞的数量")
|
||||
# 防复抓:用第三方平台原生评论ID做的MD5哈希
|
||||
external_comment_id: Mapped[Optional[str]] = mapped_column(String(32), comment="第三方评论ID的MD5指纹")
|
||||
comment_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), comment="评论实际发布时间")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
|
||||
@@ -251,29 +295,53 @@ class DiscussionComment(Base):
|
||||
# 模块六:用户画像与多渠道高可用推送系统
|
||||
# ==========================================
|
||||
class AppUser(Base):
|
||||
"""系统核心用户表"""
|
||||
"""
|
||||
系统核心用户表
|
||||
不仅存放密码,还包含用户的基础画像,供大模型做个性化阅读推荐。
|
||||
"""
|
||||
__tablename__ = "app_users"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
email: Mapped[str] = mapped_column(String(150), unique=True, index=True)
|
||||
password_hash: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
email: Mapped[str] = mapped_column(String(150), unique=True, index=True, comment="主账号邮箱")
|
||||
password_hash: Mapped[Optional[str]] = mapped_column(String(255), comment="密码哈希(第三方登录可为空)")
|
||||
|
||||
nickname: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
avatar_url: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
gender: Mapped[GenderType] = mapped_column(Enum(GenderType), default=GenderType.UNKNOWN)
|
||||
nickname: Mapped[Optional[str]] = mapped_column(String(100), comment="用户展示昵称")
|
||||
avatar_url: Mapped[Optional[str]] = mapped_column(String(500), comment="用户头像地址")
|
||||
gender: Mapped[GenderType] = mapped_column(Enum(GenderType), default=GenderType.UNKNOWN,
|
||||
comment="用户性别(用于AI调整行文语气)")
|
||||
|
||||
# 预留的 JSON 字段,可以存放未来灵活变化的用户配置,避免频繁修改表结构
|
||||
metadata_: Mapped[Optional[Any]] = mapped_column("metadata", JSON, comment="自定义扩展偏好")
|
||||
# 极其强大:一个万能收纳箱!前端未来想加任何诸如“夜间模式”、“字体变大”的开关,
|
||||
# 全部丢进这个 JSON 字段即可,从此免去手动修改后端表结构的麻烦。
|
||||
metadata_: Mapped[Optional[Any]] = mapped_column("metadata", JSON,
|
||||
comment="JSON扩展字段: 存放灵活多变的前端用户偏好设置")
|
||||
|
||||
timezone: Mapped[str] = mapped_column(String(50), default="Asia/Shanghai")
|
||||
# 时区对于定时推送系统极其重要!保证纽约的用户和北京的用户都能在早晨8点收到新闻。
|
||||
timezone: Mapped[str] = mapped_column(String(50), default="Asia/Shanghai", comment="用户所在地时区")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
|
||||
|
||||
|
||||
class EmailVerificationCode(Base):
|
||||
__tablename__ = "email_verification_codes"
|
||||
__table_args__ = (
|
||||
Index("idx_email_code_lookup", "email", "purpose", "is_used", "expires_at"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
email: Mapped[str] = mapped_column(String(150), index=True, nullable=False)
|
||||
purpose: Mapped[VerificationPurpose] = mapped_column(Enum(VerificationPurpose), nullable=False)
|
||||
code_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
is_used: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
|
||||
|
||||
|
||||
class UserPushEndpoint(Base):
|
||||
"""
|
||||
多渠道推送端点配置表
|
||||
一个用户可能绑定了邮箱(EMAIL)和微信(WECHAT),支持配置降级重试(priority_level)。
|
||||
多渠道推送端点配置表 (高可用解耦设计)
|
||||
一个用户可以配置好几个推送渠道(邮箱、微信、钉钉),
|
||||
万一主渠道今天报错了,系统会自动按优先级(priority)降级寻找备用渠道重发。
|
||||
"""
|
||||
__tablename__ = "user_push_endpoints"
|
||||
__table_args__ = (
|
||||
@@ -281,63 +349,76 @@ class UserPushEndpoint(Base):
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"))
|
||||
channel_type: Mapped[str] = mapped_column(String(50), comment="如 EMAIL, WECHAT")
|
||||
channel_account: Mapped[str] = mapped_column(String(255))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
priority_level: Mapped[int] = mapped_column(Integer, default=1, comment="1最高,降级重试")
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"), comment="所属用户ID")
|
||||
# 填入大写的纯字符串,如 EMAIL, WECHAT_BOT, TELEGRAM
|
||||
channel_type: Mapped[str] = mapped_column(String(50), comment="推送渠道类型标识")
|
||||
# 具体的发送目标地址
|
||||
channel_account: Mapped[str] = mapped_column(String(255), comment="具体的接收账号(邮箱号/微信号/Webhook)")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="用户是否临时关闭了该渠道")
|
||||
# 高可用容灾:比如 1 代表必须先发微信,如果报错了,再去找 priority=2 的邮箱补发
|
||||
priority_level: Mapped[int] = mapped_column(Integer, default=1, comment="推送优先级(1最高,用于错误降级重试)")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
|
||||
|
||||
|
||||
class UserTopicPreference(Base):
|
||||
"""用户订阅的兴趣标签库"""
|
||||
"""
|
||||
用户订阅的兴趣标签库
|
||||
当这里的标签和 ExtractedTopic 表里的标签匹配上时,就会触发相关新闻的推送。
|
||||
"""
|
||||
__tablename__ = "user_topic_preferences"
|
||||
__table_args__ = (
|
||||
# 联合防抖限制:防止用户在界面卡顿时连点两次,订阅了两个同样的词
|
||||
UniqueConstraint("user_id", "interested_keyword", name="idx_unique_preference"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"))
|
||||
interested_keyword: Mapped[str] = mapped_column(String(100))
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"), comment="所属用户ID")
|
||||
interested_keyword: Mapped[str] = mapped_column(String(100), comment="用户填写的感兴趣标签(如'马斯克')")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
|
||||
|
||||
class UserDeliverySchedule(Base):
|
||||
"""用户勿扰/定时推送时间表"""
|
||||
"""
|
||||
用户专属的定时推送时间表
|
||||
如果用户设定了早上 08:30,后台的定时任务就会在每天 08:30 精准地把匹配到的聚合新闻发出去。
|
||||
"""
|
||||
__tablename__ = "user_delivery_schedules"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "delivery_time", name="idx_unique_schedule"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"))
|
||||
delivery_time: Mapped[time] = mapped_column(Time, comment="如 08:30:00")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"), comment="所属用户ID")
|
||||
delivery_time: Mapped[time] = mapped_column(Time, comment="每天期望收到推送的具体时间")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用此时段")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
|
||||
|
||||
class DeliveryHistory(Base):
|
||||
"""
|
||||
推送历史防刷表
|
||||
核心用途:一旦给某个用户推送过某条新闻/事件,就记录在这里。
|
||||
下次再触发推荐时,检查这个表,防止给同一个用户反复发送相同的内容。
|
||||
推送历史防刷表 (推送系统的绝对底线)
|
||||
核心业务逻辑:一旦大事件或者文章通过某个渠道成功发给用户,就记一笔帐。
|
||||
明天如果这条旧新闻还在热搜上,系统查一下这个表,发现发过了,直接抛弃,绝不轰炸用户。
|
||||
"""
|
||||
__tablename__ = "delivery_history"
|
||||
__table_args__ = (
|
||||
# 终极去重约束:一个用户,针对同一篇新闻,永远只允许存在一条记录
|
||||
UniqueConstraint("user_id", "target_type", "target_id", name="idx_prevent_duplicate_push"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigIntType, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"))
|
||||
target_type: Mapped[TargetType] = mapped_column(Enum(TargetType))
|
||||
target_id: Mapped[int] = mapped_column(BigIntType)
|
||||
status: Mapped[TaskStatus] = mapped_column(Enum(TaskStatus))
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("app_users.id"), comment="接收推送的用户")
|
||||
target_type: Mapped[TargetType] = mapped_column(Enum(TargetType), comment="推送出去的具体内容类型")
|
||||
target_id: Mapped[int] = mapped_column(BigIntType, comment="推送内容的主键ID")
|
||||
# 记录这次推送是彻底成功了,还是由于渠道网络问题失败了
|
||||
status: Mapped[TaskStatus] = mapped_column(Enum(TaskStatus), comment="最终推送结果状态")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow,
|
||||
comment="记录或实际推送的准确时间")
|
||||
|
||||
|
||||
# ==========================================
|
||||
@@ -345,15 +426,17 @@ class DeliveryHistory(Base):
|
||||
# ==========================================
|
||||
class DataSyncTask(Base):
|
||||
"""
|
||||
数据同步健康度监控表
|
||||
这就是爬虫脚本每次运行都要写入记录的地方,用于后台 Dashboard 监控爬虫健康状态和错误堆栈。
|
||||
数据同步健康度监控表 (运维巡检专用)
|
||||
爬虫每跑完一个平台的轮询,就在这里打卡上报。
|
||||
方便后台画出爬虫成功率的饼图,一旦 error_trace 堆积,能迅速报警排查。
|
||||
"""
|
||||
__tablename__ = "data_sync_tasks"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigIntType, primary_key=True, autoincrement=True)
|
||||
source_id: Mapped[int] = mapped_column(ForeignKey("info_sources.id"))
|
||||
items_fetched: Mapped[int] = mapped_column(Integer, default=0)
|
||||
task_status: Mapped[TaskStatus] = mapped_column(Enum(TaskStatus))
|
||||
error_trace: Mapped[Optional[str]] = mapped_column(Text)
|
||||
source_id: Mapped[int] = mapped_column(ForeignKey("info_sources.id"), comment="本次运行爬取的哪个源")
|
||||
items_fetched: Mapped[int] = mapped_column(Integer, default=0, comment="本次爬虫成功插入或更新的新闻条数")
|
||||
task_status: Mapped[TaskStatus] = mapped_column(Enum(TaskStatus), comment="该平台的宏观抓取状态")
|
||||
# 如果代码意外崩溃、或是遭遇403/502,把 Python的 traceback 堆栈原封不动存进这里
|
||||
error_trace: Mapped[Optional[str]] = mapped_column(Text, comment="若失败则保存完整报错堆栈")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, comment="任务执行的发生时间")
|
||||
|
||||
Reference in New Issue
Block a user