Files
dy/backend/models/models.py
T
2026-07-23 17:56:25 +08:00

279 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, UniqueConstraint
from sqlalchemy.orm import relationship
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(255), unique=True, index=True, nullable=True)
password_hash = Column(String(255), nullable=False)
display_name = Column(String(100), nullable=True)
role = Column(String(20), default="operator", index=True) # admin, operator, viewer
is_active = Column(Boolean, default=True)
email_verified = Column(Boolean, default=False)
email_verified_at = Column(DateTime, nullable=True)
max_accounts = Column(Integer, default=3)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
accounts = relationship("Account", back_populates="owner")
verification_tokens = relationship(
"EmailVerificationToken",
back_populates="user",
cascade="all, delete-orphan",
)
password_reset_tokens = relationship(
"PasswordResetToken",
back_populates="user",
cascade="all, delete-orphan",
)
class EmailVerificationToken(Base):
__tablename__ = "email_verification_tokens"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
token = Column(String(128), unique=True, index=True, nullable=False)
expires_at = Column(DateTime, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="verification_tokens")
class PasswordResetToken(Base):
__tablename__ = "password_reset_tokens"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
token = Column(String(128), unique=True, index=True, nullable=False)
expires_at = Column(DateTime, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="password_reset_tokens")
class AppConfig(Base):
"""系统功能配置(单例行 id=1JSON 存储)。"""
__tablename__ = "app_config"
id = Column(Integer, primary_key=True)
data = Column(Text, nullable=False, default="{}")
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Account(Base):
__tablename__ = "accounts"
id = Column(Integer, primary_key=True, index=True)
owner_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
username = Column(String(100), unique=True, index=True, nullable=True) # 登录后获取的用户名/ID
avatar_url = Column(String(512), nullable=True) # 登录后获取的头像 URL
douyin_uid = Column(String(64), nullable=True, index=True) # 抖音 UID
phone = Column(String(20), nullable=True) # 绑定的手机号(可选)
status = Column(String(50), default="offline") # offline, logging_in, online, error
cookie_path = Column(String(255), nullable=True) # 存储 cookie/session 的路径
cookie_data = Column(Text, nullable=True) # Playwright storage_state JSON
cookie_updated_at = Column(DateTime, nullable=True) # Cookie 最近更新时间
im_session_data = Column(Text, nullable=True) # IM 直连会话 (WS URL / device_id 等)
reply_delay_seconds = Column(Integer, default=0) # 账号回复排队间隔;0/NULL=继承系统默认
reply_cooldown_seconds = Column(Integer, nullable=True) # 自动回复冷却秒数;NULL=继承全局设置
follow_welcome_enabled = Column(Boolean, default=False) # 新粉丝关注后自动发送欢迎语
follow_welcome_content = Column(Text, nullable=True) # 关注欢迎语内容(空=不发)
user_agent = Column(Text, nullable=True) # 伪装设备头(User-Agent),空=默认
qr_code_base64 = Column(Text, nullable=True) # 当前登录二维码的 base64 字符串
error_message = Column(Text, nullable=True) # 错误信息
quota_disabled = Column(Boolean, default=False, index=True) # 额度不足被停用
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联
owner = relationship("User", back_populates="accounts")
rules = relationship("AutoReplyRule", back_populates="account", cascade="all, delete-orphan")
logs = relationship("MessageLog", back_populates="account", cascade="all, delete-orphan")
profile_detail = relationship(
"AccountProfileDetail",
back_populates="account",
uselist=False,
cascade="all, delete-orphan",
)
videos = relationship(
"AccountVideo",
back_populates="account",
cascade="all, delete-orphan",
order_by="AccountVideo.sort_order",
)
class AccountProfileDetail(Base):
"""托管账号抖音详细资料(本地缓存)。"""
__tablename__ = "account_profile_details"
id = Column(Integer, primary_key=True, index=True)
account_id = Column(Integer, ForeignKey("accounts.id", ondelete="CASCADE"), unique=True, nullable=False, index=True)
uid = Column(String(64), nullable=True)
nickname = Column(String(200), nullable=True)
avatar_url = Column(String(512), nullable=True)
unique_id = Column(String(100), nullable=True)
signature = Column(Text, nullable=True)
sec_user_id = Column(String(255), nullable=True)
video_count = Column(Integer, nullable=True)
follower_count = Column(Integer, nullable=True)
following_count = Column(Integer, nullable=True)
total_favorited = Column(Integer, nullable=True)
favoriting_count = Column(Integer, nullable=True)
sync_message = Column(Text, nullable=True)
synced_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
account = relationship("Account", back_populates="profile_detail")
class AccountVideo(Base):
"""托管账号已发布作品(本地缓存)。"""
__tablename__ = "account_videos"
id = Column(Integer, primary_key=True, index=True)
account_id = Column(Integer, ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
aweme_id = Column(String(64), nullable=False, index=True)
title = Column(Text, nullable=True)
cover_url = Column(String(1024), nullable=True)
video_url = Column(String(2048), nullable=True)
share_url = Column(String(1024), nullable=True)
create_time = Column(DateTime, nullable=True)
digg_count = Column(Integer, nullable=True)
comment_count = Column(Integer, nullable=True)
play_count = Column(Integer, nullable=True)
media_type = Column(String(20), nullable=True) # video, image, other
sort_order = Column(Integer, default=0, index=True)
synced_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
account = relationship("Account", back_populates="videos")
class LinkCardPage(Base):
"""规则回复用的链接卡片落地页(带 SEO meta,打开后跳转至目标 URL)。"""
__tablename__ = "link_card_pages"
id = Column(Integer, primary_key=True, index=True)
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
slug = Column(String(64), unique=True, index=True, nullable=False)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=True)
target_url = Column(String(2000), nullable=False)
image_path = Column(String(512), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class AutoReplyRule(Base):
__tablename__ = "rules"
id = Column(Integer, primary_key=True, index=True)
owner_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
account_id = Column(Integer, ForeignKey("accounts.id", ondelete="CASCADE"), nullable=True) # 为空代表全局规则
keyword = Column(String(255), index=True) # 触发关键词,或者空代表兜底
reply_content = Column(Text) # 回复内容
match_type = Column(String(50), default="contains") # exact (精确), contains (包含), regex (正则), default (兜底)
sort_order = Column(Integer, default=0, index=True) # 规则优先级,越小越优先
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
account = relationship("Account", back_populates="rules")
class MessageLog(Base):
__tablename__ = "message_logs"
id = Column(Integer, primary_key=True, index=True)
account_id = Column(Integer, ForeignKey("accounts.id", ondelete="CASCADE"))
sender_name = Column(String(100)) # 发送者名字
sender_id = Column(String(100), nullable=True) # 发送者唯一ID
sender_avatar = Column(String(512), nullable=True) # 发送者头像 URL
message_content = Column(Text) # 接收到的消息
reply_content = Column(Text, nullable=True) # 回复的消息
status = Column(String(50), default="received") # received, replied, ignored, failed
error_message = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
account = relationship("Account", back_populates="logs")
class ReceivedMessageLog(Base):
"""接收消息原始日志:仅记录收到的消息,内容原样保存。"""
__tablename__ = "received_message_logs"
id = Column(Integer, primary_key=True, index=True)
account_id = Column(Integer, ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
conversation_id = Column(String(128), nullable=True, index=True)
sender_id = Column(String(100), nullable=True, index=True)
sender_name = Column(String(100), nullable=True)
sender_avatar = Column(String(512), nullable=True)
message_type = Column(Integer, nullable=True)
server_message_id = Column(String(64), nullable=True, index=True)
raw_content = Column(Text, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, index=True)
class FollowWelcomeLog(Base):
"""关注欢迎语去重表:每个账号对每个新粉丝只发送一次欢迎语(重启后仍生效)。"""
__tablename__ = "follow_welcome_logs"
id = Column(Integer, primary_key=True, index=True)
account_id = Column(Integer, ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
follower_uid = Column(String(64), nullable=False, index=True) # 新粉丝的抖音 UID
status = Column(String(20), default="sent") # sent, failed
detail = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
__table_args__ = (
UniqueConstraint("account_id", "follower_uid", name="uq_follow_welcome_account_follower"),
)
class SystemLog(Base):
"""系统诊断日志:记录私信收发/连接/鉴权等链路事件,便于排查失败原因。"""
__tablename__ = "system_logs"
id = Column(Integer, primary_key=True, index=True)
account_id = Column(Integer, nullable=True, index=True) # 关联账号(可空,全局事件)
level = Column(String(20), default="info", index=True) # info, success, warning, error
category = Column(String(40), default="system", index=True) # ws, send, recv, auth, poll, system
event = Column(String(255)) # 事件标题
detail = Column(Text, nullable=True) # 详细原因
created_at = Column(DateTime, default=datetime.utcnow, index=True)
class PaymentOrder(Base):
"""账号额度购买订单。"""
__tablename__ = "payment_orders"
id = Column(Integer, primary_key=True, index=True)
order_no = Column(String(64), unique=True, index=True, nullable=False)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
channel = Column(String(20), nullable=False) # wechat, alipay
slots = Column(Integer, nullable=False)
amount_fen = Column(Integer, nullable=False)
status = Column(String(20), default="pending", index=True) # pending, paid, expired, cancelled, refunded
slots_applied = Column(Boolean, default=False)
qr_code = Column(Text, nullable=True)
pay_url = Column(Text, nullable=True)
trade_no = Column(String(128), nullable=True)
notify_payload = Column(Text, nullable=True)
paid_at = Column(DateTime, nullable=True)
expires_at = Column(DateTime, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)