feat(huya): 统一积分兑换并完善兑换记录

This commit is contained in:
yml
2026-11-13 19:03:55 +08:00
parent b89ee4fd18
commit 711be0e2b6
17 changed files with 2064 additions and 193 deletions
+7 -5
View File
@@ -9,7 +9,10 @@ from fastapi import HTTPException
from pydantic import ValidationError
from tests.support.db import get_test_engine, make_test_session
from web.backend.huya_defaults import HUYA_DEFAULT_ROOM_PID
from web.backend.huya_defaults import (
HUYA_DEFAULT_POINTS_PAGE_CODE,
HUYA_DEFAULT_ROOM_PID,
)
from web.backend.models import AuditLog, HuyaAccount, HuyaConfig, User
from web.backend.routers.huya import create_browser_exchange_kit
from web.backend.schemas import HuyaBrowserExchangeRequest
@@ -55,10 +58,9 @@ class TestHuyaBrowserExchangeKit:
"精英宝典",
"电竞宝典",
]
# 默认目标地址=精英宝典(沿用历史行为:绑定/充值活动的 zt 活动页)
assert kit["target_url"].startswith(
"https://zt.huya.com/b02faae1/pc/index.html?"
)
# 2026-11-13 最新官方入口已切到积分中心页,不再跳旧 zt 宝典页。
assert kit["target_url"].startswith("https://m-shop.huya.com/points.html?")
assert f"pagecode={HUYA_DEFAULT_POINTS_PAGE_CODE}" in kit["target_url"]
assert f"pid={HUYA_DEFAULT_ROOM_PID}" in kit["target_url"]
assert kit["cookie_string"] == "yyuid=100; udb_uid=100"
assert kit["cookie_script"].endswith("})();")
+32
View File
@@ -45,6 +45,38 @@ def test_http_create_order_matches_points_capture():
assert req.gameCategoryId == 507
def test_http_create_order_v6_matches_points_exchange_capture():
client, recorded = _recording_client()
client.create_order_v6(
uid=1199669523011,
guid="",
cookie="yyuid=1199669523011",
pid=1199650619883,
items=[
{
"spu_id": "hy-8315982",
"sku_id": 7747527,
"item_count": 1,
}
],
)
service, method, req = recorded[-1]
assert (service, method) == ("shopMiddleUI", "createOrderV6")
assert req.pid == 1199650619883
assert len(req.items) == 1
assert (req.items[0].spuId, req.items[0].skuId, req.items[0].itemCount) == (
"hy-8315982",
7747527,
1,
)
assert req.src == 2
assert req.orderType == 6
assert req.orderScene == 3
assert req.extraParam.bizParams == '{ "jifencenter": 1 }'
def test_http_get_goods_info_uses_app_scene():
client, recorded = _recording_client()
+479
View File
@@ -0,0 +1,479 @@
"""虎牙积分兑换新链路:直兑、购买积分包后再兑、前置条件。"""
from __future__ import annotations
from types import SimpleNamespace
from web.backend.models import HuyaAccount
from web.backend.services import huya_points_exchange as exchange
from web.backend.services.huya_runner import HuyaBatchRunner
from web.backend.services.huya_shop_flow import ShopOrderOutcome
class _Response:
def __init__(self, code=200, message="", payload=None, detail=None):
self.code = code
self.message = message
self.payload = payload or {}
self._detail = detail or {}
def value(self, *path):
node = self.payload
for key in path:
if not isinstance(node, dict):
return None
node = node.get(key)
return node
def to_dict(self):
return dict(self._detail or self.payload)
class _Client:
def __init__(self, states, *, goods_code=200):
self.states = list(states)
self.goods_code = goods_code
self.create_v6_calls = []
def get_redeem_need_goods(self, **_kwargs):
return self.states.pop(0)
def get_goods_info(self, **kwargs):
return _Response(
code=self.goods_code,
message="" if self.goods_code == 200 else "商品详情失败",
detail={
"sku_id": 7747527,
"name": "幸运币礼包(小)",
"price": 20,
"goods_type": 5,
"virtual_type": 507,
"spu_id": kwargs["spu_id"],
},
)
def create_order_v6(self, **kwargs):
self.create_v6_calls.append(kwargs)
return SimpleNamespace(
code=200,
message="",
orderId=12104642,
to_dict=lambda: {
"code": 200,
"order_id": 12104642,
"points": 20,
},
)
def _state(*, full: bool, precondition: bool = True, plans=None):
return _Response(
payload={
"tag2": 20,
"tag3": 20,
"tag4": 0 if full else 20,
"tag5": 1 if precondition else 0,
"tag6": 1 if full else 0,
"tag7": plans or [],
}
)
def test_points_enough_uses_create_order_v6_without_payment(monkeypatch):
client = _Client([_state(full=True)])
purchase_called = False
def fail_purchase(*_args, **_kwargs):
nonlocal purchase_called
purchase_called = True
raise AssertionError("积分足够时不应购买积分包")
monkeypatch.setattr(exchange, "run_open_flow", fail_purchase)
result = exchange.prepare_exchange(
client,
uid=1199669523011,
cookie="yyuid=1199669523011",
pid=1199650619883,
spu_id="hy-8315982",
)
assert result.status == exchange.EXCHANGE_STATUS_EXCHANGED
assert result.message == "兑换成功: 幸运币礼包(小)"
assert result.order["order_id"] == 12104642
assert purchase_called is False
assert client.create_v6_calls[0]["items"] == [
{
"spu_id": "hy-8315982",
"sku_id": 7747527,
"item_count": 1,
"goods_type": 5,
"virtual_type": 507,
}
]
def test_points_shortage_returns_points_package_payment(monkeypatch):
client = _Client(
[
_state(
full=False,
plans=[
{
"tag0": 8327356,
"tag1": 7759407,
"tag2": "精英币",
"tag5": 1,
"tag6": 10,
"tag7": 20,
"tag9": 2,
"tag21": 20,
}
],
)
]
)
calls = []
def fake_purchase(client_arg, **kwargs):
calls.append((client_arg, kwargs))
return ShopOrderOutcome(
ok=True,
message="已生成支付二维码",
order_id=12095620,
pay_url="https://openapi.alipay.com/gateway.do?probe=1",
amount=200,
product_name="精英币",
count=20,
)
monkeypatch.setattr(exchange, "run_open_flow", fake_purchase)
result = exchange.prepare_exchange(
client,
uid=1199669523011,
cookie="yyuid=1199669523011",
pid=1199650619883,
spu_id="hy-8315982",
pay_channel="Zfb",
)
assert result.status == exchange.EXCHANGE_STATUS_PAYMENT_REQUIRED
assert result.plan is not None
assert result.plan["spu_id"] == "hy-8327356"
assert result.plan["sku_id"] == 7759407
assert result.plan["count"] == 20
assert result.purchase is not None
assert result.purchase.order_id == 12095620
assert calls[0][1]["spu_id"] == "hy-8327356"
assert calls[0][1]["count"] == 20
assert client.create_v6_calls == []
def test_complete_after_payment_waits_for_points_then_uses_v6():
client = _Client([_state(full=False), _state(full=True)])
result = exchange.complete_exchange_after_payment(
client,
uid=1199669523011,
cookie="yyuid=1199669523011",
pid=1199650619883,
spu_id="hy-8315982",
attempts=2,
interval=0,
sleep=lambda _seconds: None,
)
assert result.status == exchange.EXCHANGE_STATUS_EXCHANGED
assert result.state is not None
assert result.state.user_score == 20
assert result.order["order_id"] == 12104642
assert len(client.create_v6_calls) == 1
def test_precondition_failure_does_not_create_order():
client = _Client([_state(full=True, precondition=False)])
result = exchange.prepare_exchange(
client,
uid=1199669523011,
cookie="yyuid=1199669523011",
pid=1199650619883,
spu_id="hy-8315982",
)
assert result.status == exchange.EXCHANGE_STATUS_FAILED
assert "前置条件" in result.message
assert client.create_v6_calls == []
def test_runner_uses_points_exchange_service_for_direct_redeem(monkeypatch):
client = _Client([_state(full=True)])
runner = HuyaBatchRunner(
db=None,
batch_id="exchange-direct",
task_type="exchange_goods",
payload={"product_id": "hy-8315982"},
)
marks = []
monkeypatch.setattr(runner, "_http_client", lambda: client)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
)
account = HuyaAccount(uid="1199669523011", cookie="yyuid=1199669523011")
runner._execute_exchange_goods(
None,
SimpleNamespace(id=1),
account,
{"uid": 1199669523011, "cookie": "yyuid=1199669523011"},
{"room_pid": "1199650619883", "pay_channel": "Zfb"},
)
assert marks[-1][0] == "success"
assert marks[-1][2]["exchange_mode"] == "points_only"
assert marks[-1][2]["exchange_order"]["order_id"] == 12104642
assert account.status == "goods_exchanged"
assert len(client.create_v6_calls) == 1
def test_runner_waits_points_payment_then_redeems(monkeypatch):
client = _Client(
[
_state(
full=False,
plans=[
{
"tag0": 8327356,
"tag1": 7759407,
"tag2": "精英币",
"tag5": 1,
"tag6": 10,
"tag7": 20,
"tag9": 2,
}
],
),
_state(full=True),
]
)
runner = HuyaBatchRunner(
db=None,
batch_id="exchange-pay",
task_type="exchange_goods",
payload={"product_id": "hy-8315982"},
)
marks = []
progress = []
monkeypatch.setattr(runner, "_http_client", lambda: client)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
)
monkeypatch.setattr(
runner,
"_update_task_progress",
lambda _db, _task, status, message, result=None: progress.append(
(status, message, result)
),
)
monkeypatch.setattr(
runner,
"_wait_recharge_payment",
lambda **_kwargs: ("paid", {"order_id": 12095620, "pay_time": 1}),
)
monkeypatch.setattr(
exchange,
"run_open_flow",
lambda *_args, **_kwargs: ShopOrderOutcome(
ok=True,
message="已生成支付二维码",
order_id=12095620,
pay_url="https://openapi.alipay.com/gateway.do?probe=1",
amount=200,
product_name="精英币",
count=20,
),
)
account = HuyaAccount(uid="1199669523011", cookie="yyuid=1199669523011")
runner._execute_exchange_goods(
None,
SimpleNamespace(id=2),
account,
{"uid": 1199669523011, "cookie": "yyuid=1199669523011"},
{
"room_pid": "1199650619883",
"pay_channel": "Zfb",
"sid": "2203",
},
)
assert progress[0][0] == "running"
assert progress[0][2]["exchange_mode"] == "buy_points_then_exchange"
assert progress[0][2]["points_purchase"]["order_id"] == 12095620
assert marks[-1][0] == "success"
assert marks[-1][2]["exchange_order"]["order_id"] == 12104642
assert account.points == 20
def test_runner_refresh_goods_uses_points_center_query(monkeypatch):
from web.backend.services import huya_collab_service
runner = HuyaBatchRunner(
db=None,
batch_id="refresh-points-goods",
task_type="refresh_goods",
)
marks = []
added = []
calls = []
class _Query:
def filter(self, *_args, **_kwargs):
return self
def delete(self, **_kwargs):
return 0
class _DB:
def query(self, *_args, **_kwargs):
return _Query()
def add(self, item):
added.append(item)
monkeypatch.setattr(
huya_collab_service,
"fetch_points_center_gifts",
lambda uid, cookie, **kwargs: (
calls.append((uid, cookie, kwargs))
or (
[
{
"product_id": "hy-8315982",
"name": "幸运币礼包(小)",
"price": 20,
"remain_text": "11197",
"stock": 11197,
}
],
"已拉取 1 个礼包",
)
),
)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
)
account = SimpleNamespace(
id=8,
uid="1199669523011",
cookie="yyuid=1199669523011",
points=0,
status="",
updated_at=None,
)
runner._execute_refresh_goods(
_DB(),
SimpleNamespace(id=9, batch_id="refresh-points-goods", result=None),
account,
{"uid": 1199669523011, "cookie": "yyuid=1199669523011"},
{
"sid": "2203",
"points_page_code": "heping_baodian_credit_center_page",
"points_source_id": "yellowcarlist",
"points_scene": "7",
},
)
assert calls[0][2]["page_code"] == "heping_baodian_credit_center_page"
assert calls[0][2]["source_id"] == "yellowcarlist"
assert len(added) == 1
assert added[0].product_id == "hy-8315982"
assert marks[-1][2]["source"] == "PlayMallUI.queryAllocation"
def test_exchange_record_query_discovers_current_sid(monkeypatch):
record_calls = []
class _PointsClient:
def play_mall_query_allocation(self, **_kwargs):
return SimpleNamespace(
code=200,
payload={
"tag2": [
{
"tag2_2": {
"tag0": {
"tag7": 2277,
}
}
}
]
},
)
def get_user_prize_records(self, **kwargs):
record_calls.append(kwargs)
return SimpleNamespace(
status=200,
msg="请求成功",
to_dict=lambda: {
"status": 200,
"msg": "请求成功",
"records": [
{
"record_id": 9922423,
"prize_name": "幸运币礼包(小)",
"score": 20,
"exchange_time": 1794564026,
}
],
},
)
runner = HuyaBatchRunner(
db=None,
batch_id="exchange-records",
task_type="query_exchange_records",
)
marks = []
monkeypatch.setattr(runner, "_http_client", _PointsClient)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
)
account = SimpleNamespace(status="", updated_at=None)
runner._execute_query_exchange_records(
None,
SimpleNamespace(id=3),
account,
{"uid": 1199669523011, "cookie": "yyuid=1199669523011"},
{
"sid": "2203",
"points_page_code": "heping_baodian_credit_center_page",
"points_source_id": "yellowcarlist",
"points_scene": "7",
},
)
assert record_calls[0]["sid"] == 2277
assert marks[-1][0] == "success"
assert marks[-1][2]["sid"] == 2277
assert marks[-1][2]["sid_source"] == "queryAllocation"
+65 -1
View File
@@ -14,7 +14,7 @@ from core.huya.http_client import (
_extract_ws_hosts,
generate_ws_baseinfo,
)
from core.huya.shop_structs import shop_spu_number
from core.huya.shop_structs import extract_points_center_sid, shop_spu_number
from core.huya.taf_protocol import TafInputStream, TafOutputStream, TafType
from core.huya.wss_client import (
HuyaWssClient,
@@ -218,6 +218,70 @@ def test_create_order_v5_matches_points_capture():
asyncio.run(run())
def test_create_order_v6_matches_points_exchange_capture():
async def run():
client = _points_client()
await client.create_order_v6(
uid=UID,
guid=GUID,
cookie=COOKIE,
pid=PID,
items=[
{
"spu_id": "hy-8315982",
"sku_id": 7747527,
"item_count": 1,
}
],
)
message = WssMessage.decode(client.ws.sent[-1])
assert message.command == WssCommand.RPC_REQUEST
decoded = _decode_wup_body(message.body)
assert (decoded["servant"], decoded["func"]) == (
"shopMiddleUI",
"createOrderV6",
)
t_req = decoded["tReq"]
assert t_req["tag3"] == [
{
"tag0": "hy-8315982",
"tag1": 7747527,
"tag2": 1,
"tag3": [],
"tag4": 0,
"tag5": 0,
}
]
assert t_req["tag4"] == PID
assert t_req["tag6"] == "0"
assert t_req["tag8"] == 2
assert t_req["tag10"] == 6
assert t_req["tag11"]["tag4"] == BIZ_PARAMS_LITERAL
assert t_req["tag14"] == "yellowcarlist"
assert t_req["tag16"] == 3
asyncio.run(run())
def test_current_points_center_payload_exposes_exchange_sid():
payload = {
"tag2": [
{
"tag2_2": {
"tag0": {
"tag1": "精英宝典福利季",
"tag7": 2277,
}
}
}
]
}
assert extract_points_center_sid(payload) == 2277
def test_pay_order_submit_v5_callback_points_page():
async def run():
client = _points_client()