"""ORM 模型""" from __future__ import annotations from datetime import datetime, timezone from sqlalchemy import ( Column, Integer, String, Boolean, Text, DateTime, ForeignKey, JSON, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from .database import Base from .crypto_storage import EncryptedText from .huya_defaults import ( HUYA_DEFAULT_BIND_ACT_ID, HUYA_DEFAULT_OUTER_ACT_ID, HUYA_DEFAULT_PAY_CHANNEL, HUYA_DEFAULT_ROOM_PID, HUYA_DEFAULT_SID, ) def _utcnow(): """返回时区感知的 UTC 当前时间,替代 _utcnow()。""" return datetime.now(timezone.utc) class User(Base): """系统用户""" __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column( String(64), unique=True, nullable=False, index=True ) password_hash: Mapped[str] = mapped_column(String(256), nullable=False) role: Mapped[str] = mapped_column( String(32), nullable=False, default="support" ) # super_admin / operation / support is_active: Mapped[bool] = mapped_column(Boolean, default=True) remark: Mapped[str] = mapped_column(String(256), default="") custom_permissions: Mapped[list[str] | None] = mapped_column( JSON, nullable=True, comment="自定义权限列表,null表示使用角色默认权限" ) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) deleted_at: Mapped[datetime | None] = mapped_column( DateTime, nullable=True, index=True ) deleted_username: Mapped[str] = mapped_column(String(64), default="") # 客服被分配的账号 assigned_accounts: Mapped[list[Account]] = relationship( "Account", back_populates="assigned_user", foreign_keys="Account.assigned_to" ) huya_accounts: Mapped[list[HuyaAccount]] = relationship( "HuyaAccount", back_populates="assigned_user", foreign_keys="HuyaAccount.assigned_to", ) class Account(Base): """斗鱼账号""" __tablename__ = "accounts" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column(String(128), nullable=False) password: Mapped[str] = mapped_column(EncryptedText(), nullable=False) email: Mapped[str] = mapped_column(EncryptedText(), nullable=False) email_password: Mapped[str] = mapped_column(EncryptedText(), nullable=False) email_imap_server: Mapped[str] = mapped_column(String(128), default="") email_imap_port: Mapped[int] = mapped_column(Integer, default=993) email_imap_ssl: Mapped[bool] = mapped_column(Boolean, default=True) assigned_to: Mapped[int | None] = mapped_column( ForeignKey("users.id"), nullable=True, index=True ) tag: Mapped[str] = mapped_column(String(64), default="") remark: Mapped[str] = mapped_column(String(256), default="") uid: Mapped[str] = mapped_column(String(32), default="", index=True) nickname: Mapped[str] = mapped_column(String(128), default="") points: Mapped[int | None] = mapped_column(Integer, nullable=True) game_name: Mapped[str] = mapped_column(String(128), default="") game_channel: Mapped[str] = mapped_column(String(128), default="") gold_balance: Mapped[int | None] = mapped_column(Integer, nullable=True) exchange_balance: Mapped[int | None] = mapped_column(Integer, nullable=True) bind_status: Mapped[str] = mapped_column(String(32), default="") change_role_wait_time: Mapped[int | None] = mapped_column(Integer, nullable=True) esports_points: Mapped[int | None] = mapped_column(Integer, nullable=True) esports_game_name: Mapped[str] = mapped_column(String(128), default="") esports_game_channel: Mapped[str] = mapped_column(String(128), default="") esports_bind_status: Mapped[str] = mapped_column(String(32), default="") esports_change_role_wait_time: Mapped[int | None] = mapped_column( Integer, nullable=True ) esports_can_change_time: Mapped[int | None] = mapped_column(Integer, nullable=True) xpd_game_name: Mapped[str] = mapped_column(String(128), default="") xpd_openid: Mapped[str] = mapped_column(String(128), default="") xpd_role_id: Mapped[str] = mapped_column(String(64), default="") xpd_plat_id: Mapped[int | None] = mapped_column(Integer, nullable=True) xpd_area_id: Mapped[int | None] = mapped_column(Integer, nullable=True) xpd_balance: Mapped[int | None] = mapped_column(Integer, nullable=True) xpd_fragments: Mapped[int | None] = mapped_column(Integer, nullable=True) xpd_bind_status: Mapped[str] = mapped_column(String(32), default="") created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) assigned_user: Mapped[User | None] = relationship( "User", back_populates="assigned_accounts", foreign_keys=[assigned_to] ) login_tasks: Mapped[list[LoginTask]] = relationship( "LoginTask", back_populates="account" ) douyu_tasks: Mapped[list[DouyuTask]] = relationship( "DouyuTask", back_populates="account" ) class LoginTask(Base): """单次登录任务""" __tablename__ = "login_tasks" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) batch_id: Mapped[str] = mapped_column( String(64), nullable=False, index=True ) # 批次ID account_id: Mapped[int] = mapped_column(ForeignKey("accounts.id"), nullable=False) status: Mapped[str] = mapped_column( String(32), default="pending" ) # pending / running / success / failed / error cookie: Mapped[str] = mapped_column(EncryptedText(), default="") message: Mapped[str] = mapped_column(String(512), default="") created_by: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) # Cookie 有效性检测结果(持久化,刷新不丢失) ck_check_status: Mapped[str] = mapped_column( String(16), default="" ) # valid / invalid / 空=未检测 ck_check_result: Mapped[dict | None] = mapped_column( JSON, nullable=True ) # {fish_ball, nickname, level, message} ck_checked_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) account: Mapped[Account] = relationship("Account", back_populates="login_tasks") class DouyuTask(Base): """斗鱼业务任务""" __tablename__ = "douyu_tasks" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) batch_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) account_id: Mapped[int] = mapped_column(ForeignKey("accounts.id"), nullable=False) task_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True) # 任务归属工作台;避免同一账号的精英/电竞/小店任务在前端串行展示或串弹二维码。 handbook_scope: Mapped[str] = mapped_column( String(16), nullable=False, default="legacy", index=True ) # 供应商直充的来源订单号,用于轮询与异步回调的幂等关联。 supplier_out_order_id: Mapped[str | None] = mapped_column( String(64), nullable=True, unique=True, index=True ) status: Mapped[str] = mapped_column(String(32), default="pending") message: Mapped[str] = mapped_column(String(512), default="") result: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_by: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) account: Mapped[Account] = relationship("Account", back_populates="douyu_tasks") class DouyuWorkbenchAccount(Base): """用户在指定斗鱼工作台中启用的账号,跨浏览器同步。""" __tablename__ = "douyu_workbench_accounts" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column( ForeignKey("users.id"), nullable=False, index=True ) handbook_scope: Mapped[str] = mapped_column(String(16), nullable=False, index=True) account_id: Mapped[int] = mapped_column( ForeignKey("accounts.id"), nullable=False, index=True ) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) __table_args__ = ( UniqueConstraint( "user_id", "handbook_scope", "account_id", name="uq_douyu_workbench_account" ), ) class DouyuWorkbench(Base): """工作台配置哨兵,令空账号集合也能跨浏览器同步。""" __tablename__ = "douyu_workbenches" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column( ForeignKey("users.id"), nullable=False, index=True ) handbook_scope: Mapped[str] = mapped_column(String(16), nullable=False, index=True) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) __table_args__ = ( UniqueConstraint("user_id", "handbook_scope", name="uq_douyu_workbench"), ) class YybRechargeTask(Base): """应用宝和平精英点券充值任务。 YYB 登录身份与斗鱼账号无关,因此任务只关联创建者,不复用 Account。 """ __tablename__ = "yyb_recharge_tasks" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) # 唯一索引(uq_yyb_recharge_tasks_task_id)已覆盖 task_id 查询,无需重复普通索引; # 迁移 20260812_0020 也只建唯一索引,二者保持一致。 task_id: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) created_by: Mapped[int] = mapped_column( ForeignKey("users.id"), nullable=False, index=True ) worker_job_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) provider: Mapped[str] = mapped_column(String(16), default="", nullable=False) platform: Mapped[str] = mapped_column(String(16), default="android", nullable=False) points: Mapped[int | None] = mapped_column(Integer, nullable=True) product_id: Mapped[str] = mapped_column(String(128), default="") zone_id: Mapped[str] = mapped_column(String(64), default="") zone_name: Mapped[str] = mapped_column(String(128), default="") role_id: Mapped[str] = mapped_column(String(64), default="") role_name: Mapped[str] = mapped_column(String(128), default="") price_fen: Mapped[int | None] = mapped_column(Integer, nullable=True) status: Mapped[str] = mapped_column( String(32), default="created", nullable=False, index=True ) phase: Mapped[str] = mapped_column(String(32), default="login", nullable=False) message: Mapped[str] = mapped_column(String(512), default="") result: Mapped[dict | None] = mapped_column(JSON, nullable=True) login_qr_data: Mapped[str] = mapped_column(EncryptedText(), default="") payment_qr_data: Mapped[str] = mapped_column(EncryptedText(), default="") payment_started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) payment_qr_created_at: Mapped[datetime | None] = mapped_column( DateTime, nullable=True ) payment_last_checked_at: Mapped[datetime | None] = mapped_column( DateTime, nullable=True ) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) class DouyuConfig(Base): """斗鱼业务配置""" __tablename__ = "douyu_config" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) manual_id: Mapped[str] = mapped_column(String(64), default="G4KA4Qnz4LDp7") rid: Mapped[str] = mapped_column(String(64), default="9263298") bind_act_alias: Mapped[str] = mapped_column(String(64), default="20260120QYOOB") confirm_act_alias: Mapped[str] = mapped_column(String(64), default="20260120QYOOB") legacy_act_alias: Mapped[str] = mapped_column(String(64), default="cjm") room_id: Mapped[str] = mapped_column(String(64), default="9263298") elite_amount: Mapped[int] = mapped_column(Integer, default=3000) esports_manual_id: Mapped[str] = mapped_column(String(64), default="1W0yGkOdDbAXb") esports_act_alias: Mapped[str] = mapped_column(String(64), default="20260226ZMGDG") esports_amount: Mapped[int] = mapped_column(Integer, default=3000) esports_chicken_gift_id: Mapped[str] = mapped_column(String(64), default="24766") esports_chicken_skin_id: Mapped[str] = mapped_column(String(64), default="0") esports_firework_gift_id: Mapped[str] = mapped_column(String(64), default="24767") esports_firework_skin_id: Mapped[str] = mapped_column(String(64), default="3850") xpd_act_alias: Mapped[str] = mapped_column(String(64), default="20260623KDQFH") xpd_act_id: Mapped[str] = mapped_column(String(64), default="46195") xpd_rid: Mapped[str] = mapped_column(String(64), default="9263298") gold_pay_type: Mapped[int] = mapped_column(Integer, default=1) gold_recharge_channel: Mapped[str] = mapped_column(String(16), default="wechat_qr") gold_api_product_id: Mapped[str] = mapped_column(String(128), default="111570") gold_api_account_template_name: Mapped[str] = mapped_column( String(64), default="斗鱼昵称" ) gift_id: Mapped[str] = mapped_column(String(64), default="23643") skin_id: Mapped[str] = mapped_column(String(64), default="2942") updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) class DouyuGoodsSnapshot(Base): """斗鱼兑换商品快照""" __tablename__ = "douyu_goods_snapshot" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) commodity_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) name: Mapped[str] = mapped_column(String(256), default="") score: Mapped[int | None] = mapped_column(Integer, nullable=True) status: Mapped[str] = mapped_column(String(32), default="") raw: Mapped[dict | None] = mapped_column(JSON, nullable=True) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) class DouyuXpdGoodsSnapshot(Base): """斗鱼和平小店商品快照""" __tablename__ = "douyu_xpd_goods_snapshot" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) commodity_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) name: Mapped[str] = mapped_column(String(256), default="") price: Mapped[int | None] = mapped_column(Integer, nullable=True) org_price: Mapped[int | None] = mapped_column(Integer, nullable=True) category: Mapped[str] = mapped_column(String(32), default="") goods_left: Mapped[int | None] = mapped_column(Integer, nullable=True) raw: Mapped[dict | None] = mapped_column(JSON, nullable=True) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) class DouyuEsportsGoodsSnapshot(Base): """斗鱼电竞手册皮肤商城商品快照""" __tablename__ = "douyu_esports_goods_snapshot" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) commodity_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) name: Mapped[str] = mapped_column(String(256), default="") score: Mapped[int | None] = mapped_column(Integer, nullable=True) status: Mapped[str] = mapped_column(String(32), default="") raw: Mapped[dict | None] = mapped_column(JSON, nullable=True) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) class HuyaAccount(Base): """虎牙账号""" __tablename__ = "huya_accounts" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) uid: Mapped[str] = mapped_column(String(32), default="", index=True) yyuid: Mapped[str] = mapped_column(String(32), default="", index=True) username: Mapped[str] = mapped_column(String(128), default="") account_password: Mapped[str] = mapped_column(EncryptedText(), default="") nickname: Mapped[str] = mapped_column(String(128), default="") cookie: Mapped[str] = mapped_column(EncryptedText(), nullable=False) tag: Mapped[str] = mapped_column(String(64), default="") remark: Mapped[str] = mapped_column(String(256), default="") status: Mapped[str] = mapped_column(String(32), default="imported") # 上次成功登录渠道: web=旧版Web协议 / app=App协议(推荐) / sms=短信; 空=仅导入Cookie login_channel: Mapped[str] = mapped_column(String(16), default="", index=True) points: Mapped[int | None] = mapped_column(Integer, nullable=True) game_name: Mapped[str] = mapped_column(String(128), default="") game_channel: Mapped[str] = mapped_column(String(64), default="") game_phone: Mapped[str] = mapped_column(String(64), default="") sms_url: Mapped[str] = mapped_column(Text, default="") assigned_to: Mapped[int | None] = mapped_column( ForeignKey("users.id"), nullable=True, index=True ) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) assigned_user: Mapped[User | None] = relationship( "User", back_populates="huya_accounts", foreign_keys=[assigned_to] ) tasks: Mapped[list[HuyaTask]] = relationship("HuyaTask", back_populates="account") class HuyaRegisterBatch(Base): """虎牙自动注册批次(持久化元数据)""" __tablename__ = "huya_register_batches" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) batch_id: Mapped[str] = mapped_column( String(64), unique=True, nullable=False, index=True ) tag: Mapped[str] = mapped_column(String(64), default="") created_by: Mapped[int] = mapped_column( ForeignKey("users.id"), nullable=False, index=True ) concurrency: Mapped[int] = mapped_column(Integer, default=1) wait_seconds: Mapped[int] = mapped_column(Integer, default=180) poll_interval: Mapped[int] = mapped_column(Integer, default=5) password_prefix: Mapped[str] = mapped_column(String(16), default="hy") fixed_password: Mapped[str] = mapped_column(EncryptedText(), default="") use_proxy: Mapped[bool] = mapped_column(Boolean, default=False) status: Mapped[str] = mapped_column(String(32), default="pending", index=True) message: Mapped[str] = mapped_column(String(512), default="") total: Mapped[int] = mapped_column(Integer, default=0) success_count: Mapped[int] = mapped_column(Integer, default=0) failed_count: Mapped[int] = mapped_column(Integer, default=0) stopped_count: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) items: Mapped[list[HuyaRegisterItem]] = relationship( "HuyaRegisterItem", back_populates="batch", cascade="all, delete-orphan" ) class HuyaRegisterItem(Base): """虎牙自动注册批次内单条手机号状态""" __tablename__ = "huya_register_items" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) batch_db_id: Mapped[int] = mapped_column( Integer, ForeignKey("huya_register_batches.id"), nullable=False, index=True ) batch_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) line: Mapped[int] = mapped_column(Integer, nullable=False) phone: Mapped[str] = mapped_column(String(64), default="", index=True) provider: Mapped[str] = mapped_column(String(32), default="") sms_url: Mapped[str] = mapped_column(Text, default="") status: Mapped[str] = mapped_column(String(32), default="pending", index=True) message: Mapped[str] = mapped_column(String(512), default="") code: Mapped[str] = mapped_column(String(32), default="") change_code: Mapped[str] = mapped_column(String(32), default="") attempts: Mapped[int] = mapped_column(Integer, default=0) change_attempts: Mapped[int] = mapped_column(Integer, default=0) account_id: Mapped[int | None] = mapped_column( ForeignKey("huya_accounts.id"), nullable=True ) username: Mapped[str] = mapped_column(String(128), default="") uid: Mapped[str] = mapped_column(String(32), default="") password: Mapped[str] = mapped_column(EncryptedText(), default="") password_changed: Mapped[bool] = mapped_column(Boolean, default=False) cookie: Mapped[str] = mapped_column(EncryptedText(), default="") started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) batch: Mapped[HuyaRegisterBatch] = relationship( "HuyaRegisterBatch", back_populates="items" ) class HuyaRegisterSuccessLog(Base): """虎牙自动注册成功流水(只追加,供随时导出)""" __tablename__ = "huya_register_success_logs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) batch_id: Mapped[str] = mapped_column(String(64), default="", index=True) item_id: Mapped[int | None] = mapped_column(Integer, nullable=True) account_id: Mapped[int | None] = mapped_column( ForeignKey("huya_accounts.id"), nullable=True ) phone: Mapped[str] = mapped_column(String(64), default="", index=True) username: Mapped[str] = mapped_column(String(128), default="") uid: Mapped[str] = mapped_column(String(32), default="") password: Mapped[str] = mapped_column(EncryptedText(), default="") sms_url: Mapped[str] = mapped_column(Text, default="") tag: Mapped[str] = mapped_column(String(64), default="", index=True) provider: Mapped[str] = mapped_column(String(32), default="") created_by: Mapped[int | None] = mapped_column( ForeignKey("users.id"), nullable=True ) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, index=True) class HuyaTask(Base): """虎牙业务任务""" __tablename__ = "huya_tasks" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) batch_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) account_id: Mapped[int] = mapped_column( ForeignKey("huya_accounts.id"), nullable=False ) task_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True) status: Mapped[str] = mapped_column(String(32), default="pending") message: Mapped[str] = mapped_column(String(512), default="") result: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_by: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) account: Mapped[HuyaAccount] = relationship("HuyaAccount", back_populates="tasks") class HuyaConfig(Base): """虎牙业务配置""" __tablename__ = "huya_config" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) room_pid: Mapped[str] = mapped_column(String(64), default=HUYA_DEFAULT_ROOM_PID) sid: Mapped[str] = mapped_column(String(32), default=HUYA_DEFAULT_SID) outer_act_id: Mapped[str] = mapped_column( String(32), default=HUYA_DEFAULT_OUTER_ACT_ID ) bind_act_id: Mapped[str] = mapped_column( String(32), default=HUYA_DEFAULT_BIND_ACT_ID ) pay_channel: Mapped[str] = mapped_column( String(16), default=HUYA_DEFAULT_PAY_CHANNEL ) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) class HuyaGoodsSnapshot(Base): """虎牙兑换商品快照""" __tablename__ = "huya_goods_snapshot" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) product_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) name: Mapped[str] = mapped_column(String(256), default="") price: Mapped[int | None] = mapped_column(Integer, nullable=True) remain_text: Mapped[str] = mapped_column(String(64), default="") raw: Mapped[dict | None] = mapped_column(JSON, nullable=True) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) class HuyaRechargeGoodsSnapshot(Base): """虎牙充值商品快照""" __tablename__ = "huya_recharge_goods_snapshot" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) spu_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) sku_id: Mapped[str] = mapped_column(String(64), default="", index=True) name: Mapped[str] = mapped_column(String(256), default="") price: Mapped[int | None] = mapped_column(Integer, nullable=True) stock: Mapped[int | None] = mapped_column(Integer, nullable=True) buy_limit: Mapped[int | None] = mapped_column(Integer, nullable=True) icon: Mapped[str] = mapped_column(String(512), default="") description: Mapped[str] = mapped_column(String(512), default="") task_id: Mapped[str] = mapped_column(String(64), default="") task_name: Mapped[str] = mapped_column(String(256), default="") raw: Mapped[dict | None] = mapped_column(JSON, nullable=True) updated_at: Mapped[datetime] = mapped_column( DateTime, default=_utcnow, onupdate=_utcnow ) class ProxyConfig(Base): """代理配置(全局单条记录)""" __tablename__ = "proxy_config" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) enabled: Mapped[bool] = mapped_column(Boolean, default=False) api_url: Mapped[str] = mapped_column(EncryptedText(), default="") http: Mapped[str] = mapped_column(EncryptedText(), default="") https: Mapped[str] = mapped_column(EncryptedText(), default="") # 白名单 whitelist_enabled: Mapped[bool] = mapped_column(Boolean, default=False) whitelist_platform: Mapped[str] = mapped_column(String(32), default="xiequ") whitelist_credentials: Mapped[dict | None] = mapped_column(JSON, nullable=True) # 旧字段保留(向后兼容双写) whitelist_uid: Mapped[str] = mapped_column(EncryptedText(), default="") whitelist_ukey: Mapped[str] = mapped_column(EncryptedText(), default="") class AuditLog(Base): """操作审计日志""" __tablename__ = "audit_logs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int | None] = mapped_column(Integer, nullable=True) username: Mapped[str] = mapped_column(String(64), default="") action: Mapped[str] = mapped_column(String(128), nullable=False) target: Mapped[str] = mapped_column(String(256), default="") detail: Mapped[str] = mapped_column(Text, default="") # 旧记录未标记结果时为 NULL,避免把历史操作误判成失败。 success: Mapped[bool | None] = mapped_column(Boolean, nullable=True, index=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)