"""虎牙精英宝典开通状态:`huya_accounts` 新增 elite_opened / elite_open_time / elite_expire_time 工作台列表要能直接看到「这个号开没开宝典、什么时候开的、有效期到什么时候」, 但现在只有一个 `points` 列:开通后把积分花完和从没开通,在界面上长得一模一样。 真机(2026-10-15 两份抓包 + 真机对照网页)核对的来源: * 开通状态 —— 积分中心 ``shopMiddleUI.getRedeemNeedGoods`` 的 ``tag5``: ``1`` = 已开通(网页显示「已开通精英宝典」),``0`` = 未开通(网页显示 「获取积分需先开通精英宝典」,``tag7`` 还会列出开通套餐)。 * 开通时间 —— ``revenueWebUI.queryUserOrderList`` 里已支付的宝典订单,取最近一次。 * 有效期 —— ``PlayMallUI.queryAllocation`` 响应里的赛季节点(``tag10``/``tag11`` 是本赛季起止毫秒时间戳):2026-10-15 抓包为「精英宝典福利季丨Q4」, 即 2026-10-09 ~ 2026-12-31 23:59:59;认不出赛季结构时留空。 两列时间都存 **Unix 秒**(Integer 放不下毫秒时间戳,且与斗鱼 `elite_expire_time` 同一约定)。 历史值不回填:以前没查过宝典状态的账号是 NULL(界面显示「未查询」), 下次「一键查询」会补上 —— 不回填才不会把「没查过」伪装成「未开通」。 """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision: str = "20261015_0053" down_revision: str | None = "20261008_0052" branch_labels: str | None = None depends_on: str | None = None TABLE = "huya_accounts" COLUMNS = ( ( "elite_opened", sa.Boolean(), "精英宝典是否已开通(积分中心 getRedeemNeedGoods 的 tag5;NULL=未查询)", ), ( "elite_open_time", sa.Integer(), "最近一次宝典订单的支付时间(Unix 秒)", ), ( "elite_expire_time", sa.Integer(), "宝典有效期(Unix 秒):积分中心本赛季结束时间;认不出赛季时留空", ), ) def _column_exists(name: str) -> bool: """可安全重跑(应用启动时会自动跑迁移)。""" conn = op.get_bind() count = conn.execute( sa.text( "SELECT COUNT(*) FROM information_schema.columns " "WHERE table_schema = DATABASE() " "AND table_name = :table AND column_name = :name" ), {"table": TABLE, "name": name}, ).scalar() return bool(count) def upgrade() -> None: for name, column_type, comment in COLUMNS: if not _column_exists(name): op.add_column( TABLE, sa.Column(name, column_type, nullable=True, comment=comment), ) def downgrade() -> None: for name, _column_type, _comment in COLUMNS: if _column_exists(name): op.drop_column(TABLE, name)