fix(huya): 刷新开通商品不再依赖已下线的活动任务接口

真机(2026-10-15)实测 webActUI.getActTaskDetail(act 25135) 返回
601「活动已下线」,刷新开通商品第一步就失败,下拉框一直为空。

- 商品来源改为内置候选(积分中心页面「赠送指定礼物得积分」那一排,
  全部经 getGoodsInfoV5 真机验证 code=200):精英宝典 hy-8328015 ¥30、
  精英币 hy-8327356 ¥0.1、精英徽章 hy-5872824 ¥1、精英令 hy-5879346 ¥10、
  精英令*10 hy-5874269 ¥100、精英令*100 hy-5874270 ¥1000;
  旧版精英宝典 hy-5879340 页面已不再使用,从候选移除
- 活动任务详情降级为可选补充:返回非 200 只记 warning 并写入
  result.task_detail_error,不再让整次刷新失败;失败原因以商品详情为准
- 刷新时清掉该账号全部旧商品行(不限 sid),避免同名商品重复
- 列表接口只返回账号级快照并过滤 2026-07-04 的全局种子行,
  同一 spu_id 保留最新一行(原先 精英宝典 会出现两次)
- 前端:下拉标签带 spu 便于区分,按内置顺序排序,默认自动选中第一项
- 新增 tests/test_huya_recharge_goods_source.py(6 条):活动下线响应字节
  回归、下线不阻断刷新、活动商品仍会并入、失败原因归属、跨 sid 清理、
  列表去重与隐藏种子行(均已做反向验证)

真机验证:刷新成功 6 个商品,价格/库存与网页一致
This commit is contained in:
yml2213
2026-10-15 15:57:25 +08:00
parent 905a7ef22c
commit 7ca0f7ee28
5 changed files with 405 additions and 50 deletions
+99 -44
View File
@@ -19,14 +19,58 @@ HUYA_RECHARGE_ACT_ID = 25135
HUYA_RECHARGE_APP_SCENE = SHOP_APP_SCENE
HUYA_PAYMENT_POLL_SECONDS = 180
HUYA_PAYMENT_POLL_INTERVAL = 3
HUYA_RECHARGE_EXTRA_PRODUCTS = [
# 内置开通商品候选 = 积分中心页面(m-shop.huya.com/points.html)「赠送指定礼物得积分」
# 那一排商品,全部经 shopMiddleUI.getGoodsInfoV5 真机验证 code=200:
# hy-8328015 精英宝典 ¥30(送300积分,2026-10-13 抓包下单商品)
# hy-8327356 精英币 ¥0.1(已开通后首格「自定义数量」,送1积分)
# hy-5872824 精英徽章 ¥1(送10积分)、hy-5879346 精英令 ¥10(送100积分)
# hy-5874269 精英令*10 ¥100(送1000积分)、hy-5874270 精英令*100 ¥1000(送10000积分)
# 旧版精英宝典 hy-5879340 页面上已不再使用,从候选里去掉。
# 旧活动接口 webActUI.getActTaskDetail(act 25135) 已下线(601 活动已下线),
# 商品来源不能再依赖它,只能作为可选补充。
HUYA_RECHARGE_BUILTIN_PRODUCTS = [
{
"spu_id": "hy-5879340",
"spu_id": "hy-8328015",
"name": "精英宝典",
"task_name": "开通精英宝典",
"description": "得300积分丨解锁道具兑换权益",
"sort": 0,
},
{
"spu_id": "hy-8327356",
"name": "精英币",
"task_name": "购买精英币",
"description": "得1积分丨可兑指定返场皮肤道具",
"sort": 1,
},
{
"spu_id": "hy-5872824",
"name": "精英徽章",
"task_name": "购买精英徽章",
"description": "送10积分",
"sort": 2,
},
{
"spu_id": "hy-5879346",
"name": "精英令",
"task_name": "购买精英令",
"description": "送100积分",
"sort": 3,
},
{
"spu_id": "hy-5874269",
"name": "精英令*10",
"task_name": "购买精英令*10",
"description": "送1000积分",
"sort": 4,
},
{
"spu_id": "hy-5874270",
"name": "精英令*100",
"task_name": "购买精英令*100",
"description": "送10000积分",
"sort": 5,
},
]
from .huya_runner_core import HUYA_RECHARGE_SOURCE_ID
@@ -226,23 +270,6 @@ class RechargeMixin:
return
client: Any = self._shop_client(uid, cookie)
task_resp = client.get_act_task_detail(
uid=uid, cookie=cookie, act_id=HUYA_RECHARGE_ACT_ID
)
if task_resp is None:
self._mark_task(worker_db, task, "error", "虎牙充值任务详情接口无响应")
return
task_result = task_resp.to_dict()
if task_resp.status != 200:
self._mark_task(
worker_db,
task,
"failed",
task_resp.msg or f"虎牙充值任务详情获取失败: {task_resp.status}",
task_result,
)
return
candidates: list[dict] = []
seen: set[str] = set()
@@ -254,29 +281,51 @@ class RechargeMixin:
seen.add(spu_id)
candidates.append(item)
for item in HUYA_RECHARGE_EXTRA_PRODUCTS:
for item in HUYA_RECHARGE_BUILTIN_PRODUCTS:
add_candidate(dict(item))
for index, item in enumerate(task_result.get("tasks", []), start=1):
if int(item.get("task_type") or 0) != 67:
continue
add_candidate(
{
"spu_id": item.get("spu_id") or "",
"name": item.get("name") or "",
"task_id": str(item.get("task_id") or ""),
"task_name": item.get("name") or "",
"description": item.get("description") or "",
"icon": item.get("icon") or "",
"task_url": item.get("task_url") or "",
"prizes": item.get("prizes") or [],
"sort": index,
}
# 旧活动任务接口(webActUI.getActTaskDetail,act 25135)已下线
# (真机 2026-10-15:601 活动已下线),只作为可选补充:
# 能返回就并入活动里的商品,失败不再阻断整次刷新。
task_result: dict | None = None
task_error = ""
task_resp = client.get_act_task_detail(
uid=uid, cookie=cookie, act_id=HUYA_RECHARGE_ACT_ID
)
if task_resp is None:
task_error = "活动任务详情接口无响应"
else:
task_result = task_resp.to_dict()
if task_resp.status == 200:
for index, item in enumerate(task_result.get("tasks", []), start=1):
if int(item.get("task_type") or 0) != 67:
continue
add_candidate(
{
"spu_id": item.get("spu_id") or "",
"name": item.get("name") or "",
"task_id": str(item.get("task_id") or ""),
"task_name": item.get("name") or "",
"description": item.get("description") or "",
"icon": item.get("icon") or "",
"task_url": item.get("task_url") or "",
"prizes": item.get("prizes") or [],
"sort": 1000 + index,
}
)
else:
task_error = (
task_resp.msg or f"活动任务详情获取失败: {task_resp.status}"
)
if task_error:
self._push_log(
"warning",
f"[{uid}] 活动任务接口不可用({task_error}),按内置开通商品刷新",
)
if not candidates:
self._mark_task(
worker_db, task, "failed", "未从活动任务中发现充值商品", task_result
worker_db, task, "failed", "未发现可用的开通商品", task_result or {}
)
return
@@ -328,10 +377,10 @@ class RechargeMixin:
}
goods.append(item)
# 清掉该账号的全部旧商品行(不限 sid):否则跨 sid 的旧行会和本次结果
# 在列表接口里叠成同一商品的重复项(如 精英宝典 x2)。
worker_db.query(HuyaRechargeGoodsSnapshot).filter(
HuyaRechargeGoodsSnapshot.account_id == account.id,
HuyaRechargeGoodsSnapshot.sid
== (self._to_int(config_info.get("sid")) or 2203),
).delete(synchronize_session=False)
for item in goods:
worker_db.add(
@@ -355,9 +404,6 @@ class RechargeMixin:
account.status = "recharge_goods_refreshed"
account.updated_at = now
message = f"已刷新充值商品 {len(goods)} 个"
if failed:
message += f",失败 {len(failed)} 个"
result = {
"act_id": HUYA_RECHARGE_ACT_ID,
"goods_count": len(goods),
@@ -365,10 +411,19 @@ class RechargeMixin:
"goods": goods,
"failed": failed,
"task_detail": task_result,
"task_detail_error": task_error,
}
self._mark_task(
worker_db, task, "success" if goods else "failed", message, result
)
if not goods:
reason = failed[0].get("message") if failed else "无候选商品"
self._mark_task(
worker_db, task, "failed", f"开通商品刷新失败: {reason}", result
)
return
message = f"已刷新开通商品 {len(goods)} 个"
if failed:
message += f",失败 {len(failed)} 个"
self._mark_task(worker_db, task, "success", message, result)
def _execute_create_recharge_order(
self,