"""开通商品来源:不再依赖已下线的旧活动任务接口(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"