feat(huya): 统一积分兑换并完善兑换记录
This commit is contained in:
@@ -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("})();")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user