# -*- coding: utf-8 -*- # Copyright (c) 2025 relakkes@gmail.com # # This file is part of MediaCrawler project. # Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/api/creator/models.py # GitHub: https://github.com/NanmiCoder # Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1 # # 声明:本代码仅供学习和研究目的使用。使用者应遵守以下原则: # 1. 不得用于任何商业用途。 # 2. 使用时应遵守目标平台的使用条款和robots.txt规则。 # 3. 不得进行大规模爬取或对平台造成运营干扰。 # 4. 应合理控制请求频率,避免给目标平台带来不必要的负担。 # 5. 不得用于任何非法或不当的用途。 # # 详细许可条款请参阅项目根目录下的LICENSE文件。 # 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。 """运营模块的数据模型。 **刻意复用 `MonitorBase`**:这样 `init_db` 的 `create_all` 会顺手建出新表,而 `_ensure_columns`(已改为按模型元数据推导)也会自动给新表补字段 —— 不必再维护一份 建表语句。表落在同一个库里,与监控互不干扰。 """ from typing import Optional from sqlalchemy import BigInteger, Float, ForeignKey, Index, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from ..monitor.models import MonitorBase # 创作者后台的数据权限是「首次访问时自动申请、次日生效」。这个状态必须如实呈现: # 显示成"没数据"会让人以为采集坏了,实际是在等审批。 PERMISSION_UNKNOWN = "unknown" PERMISSION_PENDING = "pending" # 已申请,未生效(提示语:"次日可查看") PERMISSION_ACTIVE = "active" PERMISSION_MISSING = "missing" # 接口明确说没有权限 # 账号自身的可用性。 ACCOUNT_OK = "ok" ACCOUNT_EXPIRED = "expired" # cookie 失效,需要重新扫码 ACCOUNT_ERROR = "error" class CreatorAccount(MonitorBase): """一个自己的小红书账号。 纯请求路线下,**一个账号的全部身份就是一份 cookie** —— 没有浏览器 profile、 没有独立目录。所以"多账号"在这里只是表里的多行,不是多套运行环境。 `cookie` 是凭证:与监控的 cookie 同样对待,只存库、绝不回显接口。 """ __tablename__ = "creator_account" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) # 展示名。优先用后台返回的昵称,用户可以改。 nickname: Mapped[str] = mapped_column(String(128), nullable=False, default="") # 创作者后台的账号标识,由 /api/galaxy/user/info 返回,用于去重。 user_id: Mapped[str] = mapped_column(String(64), nullable=False, default="", index=True) red_id: Mapped[str] = mapped_column(String(64), nullable=False, default="") avatar: Mapped[str] = mapped_column(Text, nullable=False, default="") cookie: Mapped[str] = mapped_column(Text, nullable=False, default="") status: Mapped[str] = mapped_column(String(16), nullable=False, default=ACCOUNT_OK) permission_status: Mapped[str] = mapped_column( String(16), nullable=False, default=PERMISSION_UNKNOWN ) # 后台原话,例如"已为您申请数据权限,次日可查看"。照抄,不改写。 permission_tip: Mapped[str] = mapped_column(Text, nullable=False, default="") last_checked_at: Mapped[Optional[int]] = mapped_column(BigInteger) last_synced_at: Mapped[Optional[int]] = mapped_column(BigInteger) last_error: Mapped[Optional[str]] = mapped_column(Text) created_at: Mapped[int] = mapped_column(BigInteger, nullable=False) updated_at: Mapped[int] = mapped_column(BigInteger, nullable=False) notes: Mapped[list["CreatorNoteStat"]] = relationship( back_populates="account", cascade="all, delete-orphan" ) class CreatorNoteStat(MonitorBase): """一篇作品在某个采集时点的运营数据。 创作者后台给的是**累计值**(截至查询时点),所以反复采集天然形成时间序列 —— 与监控的"快照 + 差分"是同一个思路,因此这里保留 `captured_at` 而不是覆盖写。 """ __tablename__ = "creator_note_stat" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) account_id: Mapped[int] = mapped_column( ForeignKey("creator_account.id", ondelete="CASCADE"), nullable=False, index=True ) note_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) title: Mapped[str] = mapped_column(Text, nullable=False, default="") # 发布时间(毫秒)。后台按发布时间筛选,这是它的主时间轴。 publish_time: Mapped[Optional[int]] = mapped_column(BigInteger) # --- 运营指标 --------------------------------------------------------- # 计数用 BigInteger:曝光量可以很大,用 INT 迟早溢出。 exposure: Mapped[Optional[int]] = mapped_column(BigInteger) views: Mapped[Optional[int]] = mapped_column(BigInteger) likes: Mapped[Optional[int]] = mapped_column(BigInteger) comments: Mapped[Optional[int]] = mapped_column(BigInteger) favorites: Mapped[Optional[int]] = mapped_column(BigInteger) shares: Mapped[Optional[int]] = mapped_column(BigInteger) new_followers: Mapped[Optional[int]] = mapped_column(BigInteger) danmaku: Mapped[Optional[int]] = mapped_column(BigInteger) # 比率与时长。后台返回的可能是 "12.3%"/"1分30秒" 这类字符串,解析不了的存 NULL # 而不是 0 —— 与监控层的口径一致:0 是真实值,NULL 是"不知道"。 cover_ctr: Mapped[Optional[float]] = mapped_column(Float) avg_watch_seconds: Mapped[Optional[float]] = mapped_column(Float) two_second_exit_rate: Mapped[Optional[float]] = mapped_column(Float) completion_rate: Mapped[Optional[float]] = mapped_column(Float) captured_at: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) account: Mapped["CreatorAccount"] = relationship(back_populates="notes") __table_args__ = ( # 同一个时点同一篇只留一行,重复同步不会堆积。 Index("ix_creator_note_stat_unique", "account_id", "note_id", "captured_at", unique=True), )