Files
live-hub-py/tests/test_huya_recharge_goods_source.py
T
yml2213 7ca0f7ee28 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 个商品,价格/库存与网页一致
2026-10-15 15:57:25 +08:00

287 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""开通商品来源:不再依赖已下线的旧活动任务接口(2026-10-15 真机实测)。
真机现状(账号 hy_297832664,HTTP cdnws 通道):
- ``webActUI.getActTaskDetail(act_id=25135)`` → 601「活动已下线」
- ``shopMiddleUI.getGoodsInfoV5`` 对 hy-8328015 / hy-8327356 / hy-5879340 均 code=200
因此刷新开通商品改为「内置商品候选 + getGoodsInfoV5 详情」,活动任务详情只做可选
补充:它失败只记 warning,不再让整次刷新失败。
"""
from core.huya.activity_structs import GetActTaskDetailResp
from core.huya.frame_decoder import decode_struct_bytes
from core.huya.taf_protocol import TafInputStream, TafType
from web.backend.models import HuyaAccount, HuyaRechargeGoodsSnapshot
from web.backend.services.huya_runner import HuyaBatchRunner
ROOM_PID = 1199650619883
CONFIG = {"room_pid": str(ROOM_PID)}
# 真机 webActUI.getActTaskDetail(act 25135) 响应字节(status=601 / msg=活动已下线)
ACT_TASK_OFFLINE_HEX = "0a010259160fe6b4bbe58aa8e5b7b2e4b88be7babf0b"
# 积分中心页面上的开通商品(getGoodsInfoV5 真机验证 code=200)
BUILTIN_SPU_LITERALS = [
"hy-8328015",
"hy-8327356",
"hy-5872824",
"hy-5879346",
"hy-5874269",
"hy-5874270",
]
class _FakeActTaskDetail:
def __init__(self, status: int, msg: str, tasks: list[dict] | None = None):
self.status = status
self.msg = msg
self._tasks = tasks or []
def to_dict(self) -> dict:
return {
"status": self.status,
"msg": self.msg,
"task_count": len(self._tasks),
"tasks": self._tasks,
}
class _FakeGoodsInfo:
def __init__(self, spu_id: str, sku_id: str, name: str, price: int):
self.code = 200
self.message = ""
self._data = {
"spu_id": spu_id,
"sku_id": sku_id,
"name": name,
"price": price,
"stock": 100,
"buy_limit": 1,
"icon": "",
"description": "",
}
def to_dict(self) -> dict:
return dict(self._data)
class _StubShopClient:
def __init__(self, task: _FakeActTaskDetail | None, goods: bool = True):
self.task = task
self.goods = goods
self.goods_calls: list[str] = []
def get_act_task_detail(self, uid: int, cookie: str, act_id: int):
return self.task
def get_goods_info(self, **kwargs):
self.goods_calls.append(kwargs["spu_id"])
if not self.goods:
return None
return _FakeGoodsInfo(
spu_id=kwargs["spu_id"],
sku_id=f"sku-{kwargs['spu_id']}",
name=f"name-{kwargs['spu_id']}",
price=3000,
)
def _runner(monkeypatch, client) -> tuple[HuyaBatchRunner, list, list]:
runner = HuyaBatchRunner(
db=None, batch_id="test-batch", task_type="refresh_recharge_goods"
)
marks: list[tuple[str, str, dict | None]] = []
logs: list[tuple[str, str]] = []
monkeypatch.setattr(
runner, "_shop_client", lambda uid, cookie: client, raising=False
)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
raising=False,
)
monkeypatch.setattr(
runner,
"_push_log",
lambda level, message: logs.append((level, message)),
raising=False,
)
return runner, marks, logs
def test_real_offline_response_bytes_report_activity_offline():
payload = decode_struct_bytes(bytes.fromhex(ACT_TASK_OFFLINE_HEX))
assert payload["tag0"] == 601
assert payload["tag1"] == "活动已下线"
# 结构体解析路径:tRsp 以 STRUCT_BEGIN 包一层(同 WupResponse.readStruct)
ins = TafInputStream(bytes.fromhex(ACT_TASK_OFFLINE_HEX))
_tag, dtype = ins.peek_head()
assert dtype == TafType.STRUCT_BEGIN
ins.read_head()
parsed = GetActTaskDetailResp()
parsed.read_from(ins)
assert (parsed.status, parsed.msg) == (601, "活动已下线")
def test_refresh_recharge_goods_survives_offline_activity(db, monkeypatch):
client = _StubShopClient(_FakeActTaskDetail(601, "活动已下线"))
runner, marks, logs = _runner(monkeypatch, client)
account = HuyaAccount(uid="1199665170519", cookie="yyuid=1199665170519", tag="")
db.add(account)
db.commit()
runner._execute_refresh_recharge_goods(
db, None, account, {"uid": 1199665170519, "cookie": account.cookie}, CONFIG
)
status, message, result = marks[-1]
assert status == "success"
assert message == "已刷新开通商品 6 个"
assert result["goods_count"] == 6
assert result["task_detail_error"] == "活动已下线"
assert client.goods_calls == BUILTIN_SPU_LITERALS
assert [item["spu_id"] for item in result["goods"]] == BUILTIN_SPU_LITERALS
# 活动接口不可用只记 warning,不把任务标失败
assert logs and logs[-1][0] == "warning" and "活动已下线" in logs[-1][1]
rows = (
db.query(HuyaRechargeGoodsSnapshot)
.filter(HuyaRechargeGoodsSnapshot.account_id == account.id)
.all()
)
assert sorted(row.spu_id for row in rows) == sorted(BUILTIN_SPU_LITERALS)
assert all(row.sku_id for row in rows)
def test_refresh_recharge_goods_still_merges_activity_candidates(db, monkeypatch):
task = _FakeActTaskDetail(
200,
"请求成功",
[
{"task_type": 67, "spu_id": "hy-7770001", "name": "活动商品", "task_id": 5},
{"task_type": 66, "spu_id": "hy-ignore-me", "name": "非充值任务"},
],
)
client = _StubShopClient(task)
runner, marks, logs = _runner(monkeypatch, client)
account = HuyaAccount(uid="1199665170519", cookie="yyuid=1199665170519", tag="")
db.add(account)
db.commit()
runner._execute_refresh_recharge_goods(
db, None, account, {"uid": 1199665170519, "cookie": account.cookie}, CONFIG
)
status, _message, result = marks[-1]
assert status == "success"
assert result["task_detail_error"] == ""
assert "hy-7770001" in client.goods_calls
assert "hy-ignore-me" not in client.goods_calls
assert result["goods_count"] == len(BUILTIN_SPU_LITERALS) + 1
assert logs == []
def test_refresh_recharge_goods_reports_goods_failure_not_activity_status(
db, monkeypatch
):
client = _StubShopClient(_FakeActTaskDetail(601, "活动已下线"), goods=False)
runner, marks, _logs = _runner(monkeypatch, client)
account = HuyaAccount(uid="1199665170519", cookie="yyuid=1199665170519", tag="")
db.add(account)
db.commit()
runner._execute_refresh_recharge_goods(
db, None, account, {"uid": 1199665170519, "cookie": account.cookie}, CONFIG
)
status, message, result = marks[-1]
assert status == "failed"
assert message == "开通商品刷新失败: 商品详情接口无响应"
# 失败原因必须来自商品详情,而不是把旧活动接口的「活动已下线」当结论
assert "活动已下线" not in message
assert result["failed_count"] == len(BUILTIN_SPU_LITERALS)
def test_refresh_recharge_goods_drops_stale_rows_of_other_sids(db, monkeypatch):
"""同账号跨 sid 的旧行必须清掉,否则列表会出现同名重复商品。"""
client = _StubShopClient(_FakeActTaskDetail(601, "活动已下线"))
runner, marks, _logs = _runner(monkeypatch, client)
account = HuyaAccount(uid="1199665170519", cookie="yyuid=1199665170519", tag="")
db.add(account)
db.commit()
# 上次刷新用的是另一个 sid(真实库里就存在 sid=0 的旧行)
db.add(
HuyaRechargeGoodsSnapshot(
account_id=account.id,
sid=0,
spu_id="hy-5879340",
sku_id="old-sku",
name="精英宝典",
)
)
db.commit()
runner._execute_refresh_recharge_goods(
db, None, account, {"uid": 1199665170519, "cookie": account.cookie}, CONFIG
)
assert marks[-1][0] == "success"
rows = (
db.query(HuyaRechargeGoodsSnapshot)
.filter(HuyaRechargeGoodsSnapshot.account_id == account.id)
.all()
)
assert len(rows) == len(BUILTIN_SPU_LITERALS)
assert {row.sid for row in rows} == {2203}
assert len({row.spu_id for row in rows}) == len(rows)
def test_list_recharge_goods_hides_global_seed_rows_and_dedupes(db):
"""列表只返回账号级快照:隐藏全局默认行,同一 spu 只留最新一行。"""
from web.backend.routers.huya import list_recharge_goods
account = HuyaAccount(uid="1199665170519", cookie="yyuid=1199665170519", tag="")
db.add(account)
db.commit()
# 历史全局默认行(2026-07-04 种子,含旧版精英宝典):不属于当前在售商品
db.add(
HuyaRechargeGoodsSnapshot(
account_id=None,
sid=0,
spu_id="hy-5879340",
sku_id="seed-sku",
name="精英宝典",
)
)
db.commit()
# 同一 spu 的两次账号刷新结果:只保留最新一行
db.add(
HuyaRechargeGoodsSnapshot(
account_id=account.id,
sid=2203,
spu_id="hy-8328015",
sku_id="stale-sku",
name="精英宝典",
)
)
db.commit()
db.add(
HuyaRechargeGoodsSnapshot(
account_id=account.id,
sid=2203,
spu_id="hy-8328015",
sku_id="fresh-sku",
name="精英宝典",
)
)
db.commit()
rows = list_recharge_goods(db=db, current=None)
assert [row.spu_id for row in rows] == ["hy-8328015"]
assert rows[0].sku_id == "fresh-sku"