""" 数据库 ORM 模型模块。 定义全局架构中的五个核心模型,以及邮箱验证码模型: - User:用户(visitor / friend / blogger) - Article:文章(public / friend 两种可见性) - Comment:评论(支持回复,parent_id 指向父评论) - Like:点赞 - Friend:好友申请 - EmailCode:邮箱验证码(一次性、带过期时间) """ from datetime import datetime, timezone from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import relationship from .database import Base # ---------- 常量定义 ---------- # 用户角色(全局架构:visitor / friend / blogger) ROLE_VISITOR = "visitor" ROLE_FRIEND = "friend" ROLE_BLOGGER = "blogger" # 文章可见性(全局架构:public / friend) VISIBILITY_PUBLIC = "public" VISIBILITY_FRIEND = "friend" # 文章分区(life 生活 / study 学习) ARTICLE_CATEGORY_LIFE = "life" ARTICLE_CATEGORY_STUDY = "study" # 项目类型(L0 静态托管 / GitHub 外链) PROJECT_TYPE_STATIC = "static" PROJECT_TYPE_LINK = "link" # 好友申请状态 FRIEND_STATUS_PENDING = "pending" FRIEND_STATUS_ACCEPTED = "accepted" FRIEND_STATUS_REJECTED = "rejected" # 邮箱验证码用途 PURPOSE_REGISTER = "register" PURPOSE_RESET = "reset" def utcnow() -> datetime: """返回当前 UTC 时间(无时区),作为各模型时间字段的默认值。""" return datetime.now(timezone.utc).replace(tzinfo=None) class User(Base): """用户模型:对应权限系统的 visitor / friend / blogger 三种角色。""" __tablename__ = "users" id = Column(Integer, primary_key=True, index=True, comment="用户 ID") email = Column(String(255), unique=True, index=True, nullable=False, comment="邮箱,唯一") username = Column(String(50), unique=True, index=True, nullable=False, comment="用户名,唯一") password_hash = Column(String(255), nullable=False, comment="bcrypt 密码哈希,禁止保存明文") role = Column(String(20), nullable=False, default=ROLE_VISITOR, comment="角色") avatar = Column(String(500), nullable=True, comment="头像图片路径(uploads/avatar/)") avatar_updated_time = Column(DateTime, nullable=True, comment="头像最近一次更换时间(配合更换频率限制)") bio = Column(Text, nullable=True, comment="个人简介(我的简介页面展示)") token_version = Column(Integer, nullable=False, default=0, comment="令牌版本号(重置密码时自增,使旧 JWT 立即失效)") created_time = Column(DateTime, nullable=False, default=utcnow, comment="创建时间") # 关联关系:删除用户时级联删除其文章、评论、点赞与好友记录 articles = relationship("Article", back_populates="author", cascade="all, delete-orphan") comments = relationship("Comment", back_populates="user", cascade="all, delete-orphan") likes = relationship("Like", back_populates="user", cascade="all, delete-orphan") friends = relationship("Friend", back_populates="user", cascade="all, delete-orphan") class Article(Base): """文章模型:Markdown 正文,支持 public / friend 两种可见性。""" __tablename__ = "articles" id = Column(Integer, primary_key=True, index=True, comment="文章 ID") title = Column(String(200), nullable=False, comment="文章标题") content = Column(Text, nullable=False, comment="Markdown 格式正文") cover = Column(String(500), nullable=True, comment="文章封面图片路径(uploads/article/)") visibility = Column(String(20), nullable=False, default=VISIBILITY_PUBLIC, comment="可见性") category = Column(String(30), nullable=False, default=ARTICLE_CATEGORY_LIFE, index=True, comment="分区:life 生活 / study 学习") author_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True, comment="作者 ID") created_time = Column(DateTime, nullable=False, default=utcnow, comment="创建时间") author = relationship("User", back_populates="articles") comments = relationship("Comment", back_populates="article", cascade="all, delete-orphan") likes = relationship("Like", back_populates="article", cascade="all, delete-orphan") class Project(Base): """项目模型:L0 静态托管(static,自动在线演示)或 GitHub 外链(link)。""" __tablename__ = "projects" id = Column(Integer, primary_key=True, index=True, comment="项目 ID") name = Column(String(100), nullable=False, comment="项目名称") description = Column(Text, nullable=True, comment="项目简介") tech = Column(String(500), nullable=True, comment="技术标签(逗号分隔)") project_type = Column(String(20), nullable=False, default=PROJECT_TYPE_STATIC, comment="项目类型:static 静态托管 / link 外链") visibility = Column(String(20), nullable=False, default=VISIBILITY_PUBLIC, comment="可见性:public 公开 / friend 好友") demo_url = Column(String(500), nullable=True, comment="在线运行地址(static 为站内演示地址,link 为 GitHub 链接)") download_url = Column(String(500), nullable=True, comment="下载文件地址(uploads/project/)") github_url = Column(String(500), nullable=True, comment="GitHub 仓库链接(link 类型必填)") created_time = Column(DateTime, nullable=False, default=utcnow, comment="创建时间") updated_time = Column(DateTime, nullable=False, default=utcnow, onupdate=utcnow, comment="更新时间") class Comment(Base): """评论模型:好友及博主可对文章发表评论,支持回复(parent_id)。""" __tablename__ = "comments" id = Column(Integer, primary_key=True, index=True, comment="评论 ID") article_id = Column(Integer, ForeignKey("articles.id"), nullable=False, index=True, comment="文章 ID") user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True, comment="评论者 ID") parent_id = Column(Integer, ForeignKey("comments.id"), nullable=True, index=True, comment="父评论 ID(回复时填写,顶层评论为空)") content = Column(Text, nullable=False, comment="评论内容") created_time = Column(DateTime, nullable=False, default=utcnow, comment="创建时间") article = relationship("Article", back_populates="comments") user = relationship("User", back_populates="comments") parent = relationship("Comment", remote_side=[id], back_populates="replies") replies = relationship("Comment", back_populates="parent") class Like(Base): """点赞模型:同一用户对同一文章只能点赞一次。""" __tablename__ = "likes" id = Column(Integer, primary_key=True, index=True, comment="点赞记录 ID") article_id = Column(Integer, ForeignKey("articles.id"), nullable=False, index=True, comment="文章 ID") user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True, comment="点赞者 ID") # 唯一约束:防止同一用户重复点赞同一文章 __table_args__ = ( UniqueConstraint("article_id", "user_id", name="uq_like_article_user"), ) article = relationship("Article", back_populates="likes") user = relationship("User", back_populates="likes") class Friend(Base): """好友申请模型:按已确认架构字段(id / user_id / status)实现。""" __tablename__ = "friends" id = Column(Integer, primary_key=True, index=True, comment="好友记录 ID") user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True, comment="申请人 ID") status = Column(String(20), nullable=False, default=FRIEND_STATUS_PENDING, comment="申请状态") created_time = Column(DateTime, nullable=False, default=utcnow, comment="申请创建时间") user = relationship("User", back_populates="friends") class EmailCode(Base): """邮箱验证码模型:一次性验证码,带过期时间。""" __tablename__ = "email_codes" id = Column(Integer, primary_key=True, index=True, comment="验证码记录 ID") email = Column(String(255), index=True, nullable=False, comment="目标邮箱") code = Column(String(10), nullable=False, comment="验证码") purpose = Column(String(30), nullable=False, comment="用途:register / reset") expires_at = Column(DateTime, nullable=False, comment="过期时间") used = Column(Boolean, nullable=False, default=False, comment="是否已使用") created_time = Column(DateTime, nullable=False, default=utcnow, comment="创建时间")