refactor(douyu): runner 按功能域拆分 Mixin + 精英手册兑换对齐真机流程 (HAR 审核通过)

拆分: douyu_runner.py (-3186行) → core/bind/manual/gold/donate/goods/xpd 七个
Mixin + 入口聚合类, douyu_batch_registry 供 routers 重导出

精英手册兑换对齐 HAR 抓包 (activity_client +379 行):
- csrf 复用 Cookie 值不再每次 generateCsrf (HAR 实测全程零 csrf 请求),
  仅服务端报 csrf 错误时 force_refresh 重试一次
- pay Referer 补 roomId 对齐浏览器
- 新增手册链路接口: elite_user_info/storedetail/pre_exchange check+confirm/
  subscribe/batch_limit/赠品兑换/自动转换
- resolve_exchange_plan: 浏览器兑换按钮状态机路由 (normal/subscribe/
  pre_exchange/wait/blocked)
- 兑换执行: 人类节奏抖动 → 锁单(火爆重试1次) → 支付分级退避
  (频控 30/60/90s 4次上限, 普错 2/5/10/20s 5次上限, 300s 锁单期 15s 余量,
  可中断睡眠)

测试: 新增 17 项 (csrf 复用语义/状态机路由/Referer), 全量 94 通过;
未夹带代理改动
This commit is contained in:
yml2213
2026-08-30 11:05:18 +08:00
parent 6e17a575dd
commit 39b202bc3f
11 changed files with 3959 additions and 3196 deletions
+3 -3
View File
@@ -54,7 +54,7 @@ class DouyuGoldRechargeChannelTests(unittest.TestCase):
Base.metadata.drop_all(self.engine)
self.engine.dispose()
@patch("web.backend.services.douyu_runner.FishFinRechargeClient")
@patch("web.backend.services.douyu_runner_gold.FishFinRechargeClient")
def test_supplier_channel_creates_order_using_nickname_and_finishes_on_success(self, client_class):
supplier = Mock()
supplier.create_order.return_value = {
@@ -99,7 +99,7 @@ class DouyuGoldRechargeChannelTests(unittest.TestCase):
self.assertNotIn("pay_url", self.task.result)
self.assertNotIn("sign", self.task.result["supplier_order"])
@patch("web.backend.services.douyu_runner.FishFinRechargeClient")
@patch("web.backend.services.douyu_runner_gold.FishFinRechargeClient")
def test_supplier_channel_rejects_account_without_nickname(self, client_class):
self.account.nickname = ""
self.session.commit()
@@ -157,7 +157,7 @@ class DouyuGoldRechargeChannelTests(unittest.TestCase):
self.assertIn("10001", audit.detail)
self.assertNotIn("password", audit.detail)
@patch("web.backend.services.douyu_runner.FishFinRechargeClient")
@patch("web.backend.services.douyu_runner_gold.FishFinRechargeClient")
def test_supplier_channel_marks_non_200_create_response_as_failed(self, client_class):
supplier = Mock()
supplier.create_order.return_value = {"code": 400, "msg": "商品已下架"}