Files
live-hub-py/tests/test_huya_recharge_goods_source.py
T
yml2213 9a0d59825c fix(huya): 开通商品只保留真正可购买的 spu,并清理历史死商品行
真机 2026-10-15(HAR 10.15-精英徽章两个-1 + 逐个 spu 探测):面板里的「精英徽章 hy-5872824」是积分中心「赠送指定礼物得积分」那排的礼物 spu —— getGoodsInfoV5 仍返回 200+价格,但 listPayChannelV5 返回 401「您的设备环境暂不支持购买该商品」,所以下单必报「当前支付渠道不可用」(与数量无关)。真能买的是 hy-8343549 精英徽章 ¥1 / hy-8343551 精英令 ¥10。

- 内置候选换成可购买的 4 个:hy-8328015 宝典 / hy-8327356 币 / hy-8343549 徽章 / hy-8343551 令
- 刷新商品增加「能买到」闸门:详情 200 后再校验 listPayChannelV5,非 200 或渠道为空即剔除并记录服务端原话
- 下单流程渠道失败时透出服务端消息,只有渠道真不在可用列表才说「当前支付渠道不可用(可用:…)」
- 商品列表只认「最近一次刷新」那一套:原先按账号并集返回,别的账号旧刷新留下的礼物 spu 会和最新一套叠成 7 个选项
- 刷新时清理历史死行:本账号旧行 + 全局默认行(account_id 为空)+ 其它账号里已不在候选中的 spu 行
- 测试:401 商品被剔除、渠道失败透传、列表只认最近一套、刷新清理别处死行
2026-10-15 21:01:23 +08:00

459 lines
16 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 datetime import UTC, datetime
from types import SimpleNamespace
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 与 listPayChannelV5 都 200(2026-10-15 真机核对)
BUILTIN_SPU_LITERALS = [
"hy-8328015",
"hy-8327356",
"hy-8343549",
"hy-8343551",
]
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,
unbuyable: set[str] | None = None,
):
self.task = task
self.goods = goods
#: 这些 spu 能看不能买(真机:积分中心那批礼物 spu,listPayChannelV5 → 401)
self.unbuyable = unbuyable or set()
self.goods_calls: list[str] = []
self.channel_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 list_pay_channels(self, **kwargs):
spu_id = kwargs["spu_id"]
self.channel_calls.append(spu_id)
if spu_id in self.unbuyable:
return SimpleNamespace(
code=401,
message="您的设备环境暂不支持购买该商品",
channels=[],
)
return SimpleNamespace(code=200, message="", channels=["Zfb", "Weixin"])
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 == f"已刷新开通商品 {len(BUILTIN_SPU_LITERALS)} 个"
assert result["goods_count"] == len(BUILTIN_SPU_LITERALS)
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_drops_products_that_cannot_be_bought(db, monkeypatch):
"""能看不能买的商品(listPayChannelV5 401)不进商品列表,避免下单时才报渠道错误。
真机 2026-10-15:hy-5872824「精英徽章」getGoodsInfoV5=200 + ¥1,但
listPayChannelV5=401「您的设备环境暂不支持购买该商品」;真正能买的是 hy-8343549。
"""
client = _StubShopClient(
_FakeActTaskDetail(601, "活动已下线"), unbuyable={"hy-8343549"}
)
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 [item["spu_id"] for item in result["goods"]] == [
spu for spu in BUILTIN_SPU_LITERALS if spu != "hy-8343549"
]
assert result["failed_count"] == 1
assert result["failed"][0]["spu_id"] == "hy-8343549"
assert result["failed"][0]["message"] == "您的设备环境暂不支持购买该商品"
assert "失败 1 个" in message
rows = (
db.query(HuyaRechargeGoodsSnapshot)
.filter(HuyaRechargeGoodsSnapshot.account_id == account.id)
.all()
)
assert "hy-8343549" not in {row.spu_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_refresh_recharge_goods_purges_dead_products_everywhere(db, monkeypatch):
"""刷新顺手清掉别处的死商品行:别的账号遗留的旧礼物 spu + 7-04 的全局默认行。
真机 2026-10-15:账号 65/66 还留着修复前的礼物 spu 行,和最新一套并集后面板里
出现 7 个选项,选到礼物 spu 下单就报「当前支付渠道不可用」。
"""
client = _StubShopClient(_FakeActTaskDetail(601, "活动已下线"))
runner, marks, _logs = _runner(monkeypatch, client)
other = HuyaAccount(uid="1199665170009", cookie="c9")
account = HuyaAccount(uid="1199665170519", cookie="yyuid=1199665170519", tag="")
db.add_all([other, account])
db.commit()
old_time = datetime(2026, 10, 15, 7, 55, 17, tzinfo=UTC)
db.add_all(
[
# 别的账号遗留的礼物 spu(能看不能买)→ 必须清掉
HuyaRechargeGoodsSnapshot(
account_id=other.id,
sid=2203,
spu_id="hy-5872824",
sku_id="dead",
name="精英徽章",
updated_at=old_time,
),
# 别的账号里仍在候选内的商品 → 保留
HuyaRechargeGoodsSnapshot(
account_id=other.id,
sid=2203,
spu_id="hy-8343549",
sku_id="keep",
name="精英徽章",
updated_at=old_time,
),
# 2026-07-04 的全局默认行 → 清掉
HuyaRechargeGoodsSnapshot(
account_id=None,
sid=0,
spu_id="hy-5879340",
sku_id="seed",
name="精英宝典",
updated_at=old_time,
),
]
)
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).all()
spus = {row.spu_id for row in rows}
assert "hy-5872824" not in spus # 别的账号的死商品被清
assert "hy-5879340" not in spus # 全局默认行被清
assert "hy-8343549" in spus # 候选内的商品行保留
assert {row.spu_id for row in rows if row.account_id == account.id} == set(
BUILTIN_SPU_LITERALS
)
def test_list_recharge_goods_returns_only_latest_refresh_set(db):
"""列表只认「最近一次刷新」那一套:别的账号旧刷新留下的死商品不能并集进来。"""
from web.backend.routers.huya import list_recharge_goods
old_acc = HuyaAccount(uid="1199665170001", cookie="c1")
new_acc = HuyaAccount(uid="1199665170002", cookie="c2")
db.add_all([old_acc, new_acc])
db.commit()
old_time = datetime(2026, 10, 15, 7, 55, 17, tzinfo=UTC)
new_time = datetime(2026, 10, 15, 12, 52, 0, tzinfo=UTC)
db.add_all(
[
HuyaRechargeGoodsSnapshot(
account_id=old_acc.id,
sid=2203,
spu_id="hy-5872824",
sku_id="old",
name="精英徽章",
updated_at=old_time,
),
HuyaRechargeGoodsSnapshot(
account_id=old_acc.id,
sid=2203,
spu_id="hy-5879346",
sku_id="old",
name="精英令",
updated_at=old_time,
),
HuyaRechargeGoodsSnapshot(
account_id=new_acc.id,
sid=2203,
spu_id="hy-8343549",
sku_id="new",
name="精英徽章",
updated_at=new_time,
),
HuyaRechargeGoodsSnapshot(
account_id=new_acc.id,
sid=2203,
spu_id="hy-8343551",
sku_id="new",
name="精英令",
updated_at=new_time,
),
]
)
db.commit()
rows = list_recharge_goods(db=db, current=None)
assert sorted(row.spu_id for row in rows) == [
"hy-8343549",
"hy-8343551",
]
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"