"""精英宝典开通流程:先查状态、不出无用支付码、按积分基线判到账。 线上问题:工作台看不到「是否开通」,而开通时二维码会闪一下就变小 —— 原因是开通前不查状态,且到账判定写成「积分 >= 300」:账上本来就有积分的 账号在出码后第一轮轮询(2~4 秒)就被判成功,前端按 paymentArrived 自动 关闭了支付弹窗,只剩列表里 56px 的小二维码。 """ from unittest.mock import Mock import pytest from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from web.backend.database import Base from web.backend.models import Account, DouyuTask, User from web.backend.services import douyu_runner_manual from web.backend.services.douyu_runner import DouyuBatchRunner from web.backend.services.douyu_service import ( DOUYU_HANDBOOK_TASK_TYPES, SUPPORTED_DOUYU_TASK_TYPES, ) class TestEliteHandbookOpenFlow: def setup_method(self): self.engine = create_engine("sqlite://") @event.listens_for(self.engine, "connect") def enable_foreign_keys(connection, _): connection.execute("PRAGMA foreign_keys=ON") Base.metadata.create_all(self.engine) self.session = sessionmaker(bind=self.engine)() self.user = User(username="operator", password_hash="hash", role="super_admin") self.account = Account( username="dyuser", password="pw", email="a@example.com", email_password="em", ) self.session.add_all([self.user, self.account]) self.session.commit() def teardown_method(self): self.session.close() Base.metadata.drop_all(self.engine) self.engine.dispose() def _runner(self, task_type: str) -> DouyuBatchRunner: return DouyuBatchRunner(db=self.session, batch_id="batch1", task_type=task_type) def _task(self, task_type: str) -> DouyuTask: task = DouyuTask( batch_id="batch1", account_id=self.account.id, task_type=task_type, handbook_scope="elite", status="running", result={"payload": {}}, created_by=self.user.id, ) self.session.add(task) self.session.commit() return task def _config(self) -> dict: return { "manual_id": "G4KA4Qnz4LDp7", "rid": "9263298", "room_id": "9252212", "elite_amount": 3000, "bind_act_alias": "20260120QYOOB", "confirm_act_alias": "20260120QYOOB", } def test_task_type_registered_for_elite_scope(self): assert SUPPORTED_DOUYU_TASK_TYPES["query_elite_status"] == "查询精英宝典状态" assert "query_elite_status" in DOUYU_HANDBOOK_TASK_TYPES["elite"] def test_query_elite_status_writes_account_state(self): task = self._task("query_elite_status") runner = self._runner("query_elite_status") client = Mock() client.elite_user_info.return_value = { "manual_type": 1, "manual_score": 300, "expire_time": 1793462399, } runner._client = Mock(return_value=client) runner._execute_query_elite_status( self.session, task, self.account, "acf_uid=100", self._config() ) self.session.refresh(task) self.session.refresh(self.account) assert task.status == "success" assert "已开通" in task.message assert "到期" in task.message assert self.account.elite_opened is True assert self.account.elite_expire_time == 1793462399 assert self.account.points == 300 def test_query_elite_status_marks_unopened(self): task = self._task("query_elite_status") runner = self._runner("query_elite_status") client = Mock() client.elite_user_info.return_value = { "manual_type": 0, "manual_score": 0, "expire_time": 0, } runner._client = Mock(return_value=client) runner._execute_query_elite_status( self.session, task, self.account, "acf_uid=100", self._config() ) self.session.refresh(task) self.session.refresh(self.account) assert task.status == "success" assert "未开通" in task.message assert self.account.elite_opened is False assert self.account.elite_expire_time is None def test_create_elite_qr_skips_payment_when_already_opened(self): """已开通的账号不再生成支付码(避免二维码闪现与白付)。""" task = self._task("create_elite_qr") runner = self._runner("create_elite_qr") client = Mock() client.elite_user_info.return_value = { "manual_type": 1, "manual_score": 300, "expire_time": 1793462399, } runner._client = Mock(return_value=client) runner._execute_create_elite_qr( self.session, task, self.account, "acf_uid=100", self._config() ) self.session.refresh(task) self.session.refresh(self.account) assert task.status == "success" assert "已开通" in task.message assert task.result["elite_already_opened"] is True assert "pay_url" not in task.result client.create_elite_qr.assert_not_called() assert self.account.bind_status == "elite_opened" def test_create_elite_qr_uses_points_baseline(self, monkeypatch): """账上已有 680 积分时不能立刻判成功,必须等积分真的增长。""" monkeypatch.setattr(douyu_runner_manual, "DOUYU_PAYMENT_POLL_SECONDS", 0.05) monkeypatch.setattr(douyu_runner_manual, "DOUYU_PAYMENT_POLL_INTERVAL", 0.01) task = self._task("create_elite_qr") runner = self._runner("create_elite_qr") client = Mock() client.acf_ccn.return_value = "ctn-1" client.elite_user_info.return_value = { "manual_type": 0, "manual_score": 680, "expire_time": 0, } client.create_elite_qr.return_value = {"pay_url": "https://example.test/pay"} runner._client = Mock(return_value=client) # 基线取自 credit/balance,与到账轮询同源:开通前后都是 680 -> 没有增长 runner._refresh_account_points = Mock(return_value={"points": 680}) runner._execute_create_elite_qr( self.session, task, self.account, "acf_uid=100", self._config() ) self.session.refresh(task) # 积分没有增长 -> 不能判成功(旧口径会立刻 success,二维码随即消失) assert task.status == "failed" assert task.result["elite_opened"] is False assert task.result["elite_points_baseline"] == 680 client.create_elite_qr.assert_called_once() def test_create_elite_qr_succeeds_when_points_grow(self, monkeypatch): monkeypatch.setattr(douyu_runner_manual, "DOUYU_PAYMENT_POLL_SECONDS", 0.05) monkeypatch.setattr(douyu_runner_manual, "DOUYU_PAYMENT_POLL_INTERVAL", 0.01) task = self._task("create_elite_qr") runner = self._runner("create_elite_qr") client = Mock() client.acf_ccn.return_value = "ctn-1" client.elite_user_info.return_value = { "manual_type": 0, "manual_score": 0, "expire_time": 0, } client.create_elite_qr.return_value = {"pay_url": "https://example.test/pay"} runner._client = Mock(return_value=client) # 第一次是开通前的基线 0,之后轮询到账 300 runner._refresh_account_points = Mock( side_effect=[{"points": 0}, {"points": 300}] ) runner._execute_create_elite_qr( self.session, task, self.account, "acf_uid=100", self._config() ) self.session.refresh(task) self.session.refresh(self.account) assert task.status == "success" assert task.result["elite_opened"] is True assert self.account.elite_opened is True def test_wait_points_falls_back_without_baseline(self, monkeypatch): """拿不到基线时退回旧口径,避免完全无法判定到账。""" monkeypatch.setattr(douyu_runner_manual, "DOUYU_PAYMENT_POLL_SECONDS", 0.05) monkeypatch.setattr(douyu_runner_manual, "DOUYU_PAYMENT_POLL_INTERVAL", 0.01) task = self._task("create_elite_qr") runner = self._runner("create_elite_qr") client = Mock() runner._refresh_account_points = Mock(return_value={"points": 680}) opened = runner._wait_points_after_payment( self.session, task, self.account, client, "acf_uid=100", "ctn-1", {}, baseline_points=None, ) assert opened is True @pytest.mark.parametrize("already_points", [300, 680]) def test_wait_points_ignores_existing_points(self, monkeypatch, already_points): monkeypatch.setattr(douyu_runner_manual, "DOUYU_PAYMENT_POLL_SECONDS", 0.05) monkeypatch.setattr(douyu_runner_manual, "DOUYU_PAYMENT_POLL_INTERVAL", 0.01) task = self._task("create_elite_qr") runner = self._runner("create_elite_qr") client = Mock() runner._refresh_account_points = Mock(return_value={"points": already_points}) opened = runner._wait_points_after_payment( self.session, task, self.account, client, "acf_uid=100", "ctn-1", {}, baseline_points=already_points, ) assert opened is False