- 手机号列:外部 CK 的 `----手机号` 本来就落在 huya_accounts.game_phone,
列表一直没展示;现在单独成列,并纳入搜索
- 精英宝典列:显示「已开通/未开通/未查询」+ 开通时间 + 有效期
· 开通状态 = 积分中心 getRedeemNeedGoods 的 tag5(真机对照网页:1=已开通,
0=未开通,此时 tag7 会列出开通套餐),拿不准时为 NULL(显示「未查询」)
· 开通时间 = revenueWebUI.queryUserOrderList 里已支付的宝典订单 pay_time
(按 SPU 白名单 + 订单名含「精英宝典」识别,未支付订单不计,取最近一次)
· 有效期 = PlayMallUI.queryAllocation 页面配置的本赛季节点 tag10/tag11
(2026-10-15 抓包:精英宝典福利季丨Q4,2026-10-09 ~ 2026-12-31 23:59:59),
不写死索引,递归找带 tag10/tag11 的节点
- 新任务 query_all「一键查询」:勾选账号一次跑完角色绑定 + 宝典开通 + 积分,
按部分失败如实汇总(某个接口挂了不影响其它结论),并把宝典状态写回账号
- 迁移 20261015_0053:huya_accounts 新增 elite_opened / elite_open_time /
elite_expire_time(时间存 Unix 秒,与斗鱼 elite_expire_time 同约定)
- 开通商品按页面「赠送指定礼物得积分」那一排对齐成 6 款(去掉页面已不用的
旧版精英宝典 hy-5879340),列表接口只返回账号级快照并按 spu 去重
- 建 WSS 会话失败(积分中心端点静默不回包)时回退 HTTP cdnws,
不再让整批任务判失败
- 列表列宽改按比例分配,避免宽屏下被拉出奇怪的宽度;最近操作不再重复状态
- 新增 tests/test_huya_elite_query.py(10 条);同步迁移头版本断言
真机验证:#65 已开通(10-13 20:41:45,有效期 12-31)、#70 新开通
(10-15 16:37:37)判定与订单/网页一致
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""虎牙精英宝典开通状态:`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)
|