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 商品被剔除、渠道失败透传、列表只认最近一套、刷新清理别处死行
This commit is contained in:
yml2213
2026-10-15 21:01:23 +08:00
parent 86ca2ed021
commit 9a0d59825c
5 changed files with 315 additions and 47 deletions
+180 -8
View File
@@ -8,6 +8,9 @@
补充:它失败只记 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
@@ -19,14 +22,12 @@ CONFIG = {"room_pid": str(ROOM_PID)}
# 真机 webActUI.getActTaskDetail(act 25135) 响应字节(status=601 / msg=活动已下线)
ACT_TASK_OFFLINE_HEX = "0a010259160fe6b4bbe58aa8e5b7b2e4b88be7babf0b"
# 积分中心页面上的开通商品(getGoodsInfoV5 真机验证 code=200)
# 开通商品候选:必须 getGoodsInfoV5 与 listPayChannelV5 都 200(2026-10-15 真机核对)
BUILTIN_SPU_LITERALS = [
"hy-8328015",
"hy-8327356",
"hy-5872824",
"hy-5879346",
"hy-5874269",
"hy-5874270",
"hy-8343549",
"hy-8343551",
]
@@ -65,10 +66,18 @@ class _FakeGoodsInfo:
class _StubShopClient:
def __init__(self, task: _FakeActTaskDetail | None, goods: bool = True):
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
@@ -84,6 +93,17 @@ class _StubShopClient:
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(
@@ -140,8 +160,8 @@ def test_refresh_recharge_goods_survives_offline_activity(db, monkeypatch):
status, message, result = marks[-1]
assert status == "success"
assert message == "已刷新开通商品 6 个"
assert result["goods_count"] == 6
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
@@ -157,6 +177,41 @@ def test_refresh_recharge_goods_survives_offline_activity(db, monkeypatch):
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,
@@ -240,6 +295,123 @@ def test_refresh_recharge_goods_drops_stale_rows_of_other_sids(db, monkeypatch):
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
+43 -1
View File
@@ -70,12 +70,18 @@ class _Client:
orders=None,
auth_code: int = 200,
auth_message: str = "",
channel_code: int = 200,
channel_message: str = "",
channels=("Zfb",),
):
self.create_code = create_code
self.create_message = create_message
self.orders = orders or []
self.auth_code = auth_code
self.auth_message = auth_message
self.channel_code = channel_code
self.channel_message = channel_message
self.channels = list(channels)
self.pay_order_ids: list[int] = []
self.order_list_calls = 0
self.create_calls = 0
@@ -97,7 +103,9 @@ class _Client:
return resp
def list_pay_channels(self, **kwargs):
return SimpleNamespace(code=200, message="", channels=["Zfb"])
return SimpleNamespace(
code=self.channel_code, message=self.channel_message, channels=self.channels
)
def check_hy_protocol(self, **kwargs):
return SimpleNamespace(code=200, message="")
@@ -245,6 +253,40 @@ def test_auth_no_response_fails_without_touching_orders():
assert client.order_list_calls == 0
def test_unbuyable_goods_surfaces_channel_message():
"""能看不能买的商品(listPayChannelV5 401):把服务端原话透出来。
真机 2026-10-15:hy-5872824「精英徽章」是积分中心的礼物 spu,getGoodsInfoV5
返回 200 + ¥1,但支付渠道接口返回 401「您的设备环境暂不支持购买该商品」。
以前一律报「当前支付渠道不可用」,看不出是商品不能买。
"""
client = _Client(
create_code=200,
channel_code=401,
channel_message="您的设备环境暂不支持购买该商品",
channels=(),
)
outcome = _run(client)
assert outcome.ok is False
assert outcome.message == "您的设备环境暂不支持购买该商品"
# 渠道校验没过就不该下单、更不该起支付
assert client.create_calls == 0
assert client.pay_order_ids == []
def test_unavailable_pay_channel_lists_what_is_available():
client = _Client(create_code=200, channels=("Weixin",))
outcome = _run(client) # 默认走 Zfb
assert outcome.ok is False
assert "当前支付渠道不可用" in outcome.message
assert "Weixin" in outcome.message
assert client.create_calls == 0
def test_paid_order_is_not_reused():
client = _Client(
create_code=703, create_message=BUY_LIMIT_MESSAGE, orders=[PAID_ELITE_ORDER]