更新
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
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=1,JSON 存储)。"""
|
||||
|
||||
__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)
|
||||
Reference in New Issue
Block a user