diff --git a/.gitignore b/.gitignore index 1a207f3..253c04d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ web/frontend/dist/ .tmp_reverse/ apks/ work/ + +analysis/ diff --git a/core/huya/elite_session.py b/core/huya/elite_session.py index 7aa9558..5c6057d 100644 --- a/core/huya/elite_session.py +++ b/core/huya/elite_session.py @@ -31,6 +31,7 @@ class HuyaEliteWssSession: self.guid = HuyaHttpClient._resolve_cookie_guid(self.cookie) self.kind = kind self.logger = logger or (lambda _message: None) + self.http = HuyaHttpClient(logger=self.logger) self.loop = asyncio.new_event_loop() self.client: HuyaWssClient | None = None self._closed = False @@ -111,6 +112,27 @@ class HuyaEliteWssSession: def get_user_prize_records(self, uid: int, cookie: str, sid: int): return self._run(self.client.get_user_prize_records(uid, cookie, sid)) + def check_user_bind_game_account(self, **kwargs): + return self._run(self.client.check_user_bind_game_account(**kwargs)) + + def confirm_bind_act_account(self, **kwargs): + return self._run(self.client.confirm_bind_act_account(**kwargs)) + + def get_live_link_param(self, **kwargs): + return self._run(self.client.get_live_link_param(**kwargs)) + + def get_user_profile_batch(self, **kwargs): + return self._run(self.client.get_user_profile_batch(**kwargs)) + + def build_bind_urls(self, *args, **kwargs): + return self.http.build_bind_urls(*args, **kwargs) + + def get_livelink_mini_qrcode(self, *args, **kwargs): + return self.http.get_livelink_mini_qrcode(*args, **kwargs) + + def get_livelink_qrcode_status(self, *args, **kwargs): + return self.http.get_livelink_qrcode_status(*args, **kwargs) + def get_goods_info(self, **kwargs): return self._run(self.client.get_goods_info(**kwargs)) diff --git a/core/huya/wss_client.py b/core/huya/wss_client.py index e88f3d5..d10a882 100644 --- a/core/huya/wss_client.py +++ b/core/huya/wss_client.py @@ -800,6 +800,69 @@ class HuyaWssClient: req.sid = int(sid or 0) return await self.call_rpc("webActUI", "getUserPrizeRecords", req, GetUserPrizeRecordsResp) + async def check_user_bind_game_account( + self, uid: int, cookie: str, b_act_id: int, is_use_outer_act_id: int = 1, + gid: int = 0, outer_act_id: str = "", scene: str = "", + ): + from .activity_structs import ( + CheckUserBindGameAccountReq, + CheckUserBindGameAccountResp, + ) + + req = CheckUserBindGameAccountReq() + req.userId = self._build_activity_user(uid, cookie) + req.gid = int(gid or 0) + req.outerActId = str(outer_act_id or "") + req.scene = scene or "" + req.bActId = int(b_act_id or 0) + req.isUseOuterActId = int(is_use_outer_act_id or 0) + return await self.call_rpc( + "webActUI", "checkUserBindGameAccount", req, CheckUserBindGameAccountResp + ) + + async def confirm_bind_act_account( + self, uid: int, cookie: str, b_act_id: int, gid: int = 0, outer_act_id: str = "" + ): + from .activity_structs import ( + ConfirmBindActAccountReq, + ConfirmBindActAccountResp, + ) + + req = ConfirmBindActAccountReq() + req.userId = self._build_activity_user(uid, cookie) + req.gid = int(gid or 0) + req.outerActId = str(outer_act_id or "") + req.bActId = int(b_act_id or 0) + return await self.call_rpc( + "webActUI", "confirmBindActAccount", req, ConfirmBindActAccountResp + ) + + async def get_live_link_param( + self, uid: int, cookie: str, b_act_id: int, gid: int = 0, + outer_act_id: int = 0, game_auth_scene: str = "", + ): + from .activity_structs import GetLiveLinkParamReq, GetLiveLinkParamResp + + req = GetLiveLinkParamReq() + req.userId = self._build_activity_user(uid, cookie) + req.gid = int(gid or 0) + req.outerActId = int(outer_act_id or 0) + req.gameAuthScene = game_auth_scene or "" + req.bActId = int(b_act_id or 0) + return await self.call_rpc("webActUI", "getLiveLinkParam", req, GetLiveLinkParamResp) + + async def get_user_profile_batch(self, uid: int, cookie: str, target_uids: list[int]): + from .activity_structs import GetUserProfileBatchReq, GetUserProfileBatchResp + + req = GetUserProfileBatchReq() + req.userId = self._build_activity_user(0, "") + req.uidList = [int(item) for item in target_uids if int(item or 0)] + if not req.uidList: + return None + return await self.call_rpc( + "huyauserui", "getUserProfileBatch", req, GetUserProfileBatchResp + ) + async def list_pay_channels( self, uid: int, guid: str, cookie: str, spu_id: str, sku_id: int = 0, supplier_id: int = 87401, diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 55df97b..61592c9 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -22,7 +22,7 @@ TESTS_DIR = Path(__file__).resolve().parent PROJECT_ROOT = TESTS_DIR.parent VERSIONS_DIR = PROJECT_ROOT / "web" / "backend" / "migrations" / "versions" -HEAD_REVISION = "20260831_0032" +HEAD_REVISION = "20260901_0033" # 迁移新增、但不声明在模型里的复合查询索引。 EXTRA_INDEXES = ( diff --git a/web/backend/migrations/versions/20260901_0033_huya_snapshot_scope.py b/web/backend/migrations/versions/20260901_0033_huya_snapshot_scope.py new file mode 100644 index 0000000..1f793ba --- /dev/null +++ b/web/backend/migrations/versions/20260901_0033_huya_snapshot_scope.py @@ -0,0 +1,42 @@ +"""按账号和活动隔离虎牙宝典商品快照。""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "20260901_0033" +down_revision: str | None = "20260831_0032" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def _columns(bind, table: str) -> set[str]: + return {item["name"] for item in sa.inspect(bind).get_columns(table)} + + +def upgrade() -> None: + bind = op.get_bind() + for table in ("huya_goods_snapshot", "huya_recharge_goods_snapshot"): + columns = _columns(bind, table) + if "account_id" not in columns: + op.add_column(table, sa.Column("account_id", sa.Integer(), nullable=True)) + op.create_index(f"ix_{table}_account_id", table, ["account_id"]) + if "sid" not in columns: + op.add_column( + table, + sa.Column("sid", sa.Integer(), nullable=False, server_default="0"), + ) + op.create_index(f"ix_{table}_sid", table, ["sid"]) + + +def downgrade() -> None: + bind = op.get_bind() + for table in ("huya_goods_snapshot", "huya_recharge_goods_snapshot"): + columns = _columns(bind, table) + if "sid" in columns: + op.drop_index(f"ix_{table}_sid", table_name=table) + op.drop_column(table, "sid") + if "account_id" in columns: + op.drop_index(f"ix_{table}_account_id", table_name=table) + op.drop_column(table, "account_id") diff --git a/web/backend/models.py b/web/backend/models.py index 314f67c..96bd7dd 100644 --- a/web/backend/models.py +++ b/web/backend/models.py @@ -576,6 +576,8 @@ class HuyaGoodsSnapshot(Base): __tablename__ = "huya_goods_snapshot" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + account_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True) + sid: Mapped[int] = mapped_column(Integer, default=0, index=True) product_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) name: Mapped[str] = mapped_column(String(256), default="") price: Mapped[int | None] = mapped_column(Integer, nullable=True) @@ -592,6 +594,8 @@ class HuyaRechargeGoodsSnapshot(Base): __tablename__ = "huya_recharge_goods_snapshot" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + account_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True) + sid: Mapped[int] = mapped_column(Integer, default=0, index=True) spu_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) sku_id: Mapped[str] = mapped_column(String(64), default="", index=True) name: Mapped[str] = mapped_column(String(256), default="") diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index 3136766..6f38675 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -1823,8 +1823,11 @@ def list_goods( current: User = Depends(require_permission("huya:task")), ): """查看已缓存的虎牙商品快照。""" - rows = db.query(HuyaGoodsSnapshot).order_by(HuyaGoodsSnapshot.id.asc()).all() - return rows + rows = db.query(HuyaGoodsSnapshot).order_by(HuyaGoodsSnapshot.id.desc()).all() + latest = {} + for row in rows: + latest.setdefault((row.sid, row.product_id), row) + return list(latest.values()) @router.get("/recharge-goods", response_model=list[HuyaRechargeGoodsOut]) @@ -1833,12 +1836,11 @@ def list_recharge_goods( current: User = Depends(require_permission("huya:task")), ): """查看已缓存的虎牙充值商品快照。""" - rows = ( - db.query(HuyaRechargeGoodsSnapshot) - .order_by(HuyaRechargeGoodsSnapshot.id.asc()) - .all() - ) - return rows + rows = db.query(HuyaRechargeGoodsSnapshot).order_by(HuyaRechargeGoodsSnapshot.id.desc()).all() + latest = {} + for row in rows: + latest.setdefault((row.sid, row.spu_id), row) + return list(latest.values()) @router.post("/tasks/batch") diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 0cd21ca..25def1f 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -551,6 +551,8 @@ class HuyaTaskOut(BaseModel): class HuyaGoodsOut(BaseModel): id: int + account_id: int | None = None + sid: int = 0 product_id: str name: str = "" price: int | None = None @@ -564,6 +566,8 @@ class HuyaGoodsOut(BaseModel): def _serialize(self) -> dict[str, Any]: return { "id": self.id, + "account_id": self.account_id, + "sid": self.sid, "product_id": self.product_id, "name": self.name, "price": self.price, @@ -575,6 +579,8 @@ class HuyaGoodsOut(BaseModel): class HuyaRechargeGoodsOut(BaseModel): id: int + account_id: int | None = None + sid: int = 0 spu_id: str sku_id: str = "" name: str = "" @@ -594,6 +600,8 @@ class HuyaRechargeGoodsOut(BaseModel): def _serialize(self) -> dict[str, Any]: return { "id": self.id, + "account_id": self.account_id, + "sid": self.sid, "spu_id": self.spu_id, "sku_id": self.sku_id, "name": self.name, diff --git a/web/backend/services/huya_runner_bind.py b/web/backend/services/huya_runner_bind.py index a9d90d9..d4e6c28 100644 --- a/web/backend/services/huya_runner_bind.py +++ b/web/backend/services/huya_runner_bind.py @@ -8,8 +8,6 @@ from typing import TYPE_CHECKING, Any from sqlalchemy.orm import Session -from core.huya import HuyaHttpClient - from ..models import HuyaAccount, HuyaTask HUYA_BIND_ROLE_POLL_SECONDS = 180 @@ -18,6 +16,9 @@ HUYA_BIND_ZT_UUID = "b02faae1" HUYA_BIND_ROOM_ID = "30596253" from .huya_runner_core import HUYA_RECHARGE_SOURCE_ID, HuyaBatchRunnerCore +if TYPE_CHECKING: + from core.huya import HuyaHttpClient + class BindMixin: """游戏角色绑定域:绑定状态机、扫码/确认绑定。""" @@ -36,6 +37,7 @@ class BindMixin: def _push_log(self, level: str, message: str) -> None: ... def _mark_task(self, *args: Any, **kwargs: Any) -> None: ... def _update_task_progress(self, *args: Any, **kwargs: Any) -> None: ... + def _activity_client(self, uid: int, cookie: str): ... @staticmethod def _role_name(bind_status) -> str: @@ -315,9 +317,7 @@ class BindMixin: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return - client: Any = HuyaHttpClient( - logger=lambda msg: self._push_log("info", f"[{uid}] {msg}") - ) + client: Any = self._activity_client(uid, cookie) bind_status, bind_query_result = self._resolve_bind_status( client=client, uid=uid, @@ -503,9 +503,7 @@ class BindMixin: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return - client: Any = HuyaHttpClient( - logger=lambda msg: self._push_log("info", f"[{uid}] {msg}") - ) + client: Any = self._activity_client(uid, cookie) bind_status, bind_query_result = self._resolve_bind_status( client=client, uid=uid, @@ -577,9 +575,7 @@ class BindMixin: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return - client: Any = HuyaHttpClient( - logger=lambda msg: self._push_log("info", f"[{uid}] {msg}") - ) + client: Any = self._activity_client(uid, cookie) role_status, role_query_result = self._resolve_bind_status( client=client, uid=uid, diff --git a/web/backend/services/huya_runner_goods.py b/web/backend/services/huya_runner_goods.py index 35cdaa7..1f514ef 100644 --- a/web/backend/services/huya_runner_goods.py +++ b/web/backend/services/huya_runner_goods.py @@ -245,10 +245,15 @@ class GoodsMixin: if item.get("product_id") and item.get("name") ] now = datetime.now(UTC) - worker_db.query(HuyaGoodsSnapshot).delete(synchronize_session=False) + worker_db.query(HuyaGoodsSnapshot).filter( + HuyaGoodsSnapshot.account_id == account.id, + HuyaGoodsSnapshot.sid == sid_int, + ).delete(synchronize_session=False) for item in goods: worker_db.add( HuyaGoodsSnapshot( + account_id=account.id, + sid=sid_int, product_id=item["product_id"], name=item["name"], price=item["price"], @@ -298,7 +303,11 @@ class GoodsMixin: snapshot = ( worker_db.query(HuyaGoodsSnapshot) - .filter(HuyaGoodsSnapshot.product_id == str(product_id)) + .filter( + HuyaGoodsSnapshot.account_id == account.id, + HuyaGoodsSnapshot.sid == sid_int, + HuyaGoodsSnapshot.product_id == str(product_id), + ) .first() ) product_name = str( diff --git a/web/backend/services/huya_runner_recharge.py b/web/backend/services/huya_runner_recharge.py index 0405e23..d794797 100644 --- a/web/backend/services/huya_runner_recharge.py +++ b/web/backend/services/huya_runner_recharge.py @@ -310,10 +310,15 @@ class RechargeMixin: } goods.append(item) - worker_db.query(HuyaRechargeGoodsSnapshot).delete(synchronize_session=False) + worker_db.query(HuyaRechargeGoodsSnapshot).filter( + HuyaRechargeGoodsSnapshot.account_id == account.id, + HuyaRechargeGoodsSnapshot.sid == (self._to_int(config_info.get("sid")) or 2203), + ).delete(synchronize_session=False) for item in goods: worker_db.add( HuyaRechargeGoodsSnapshot( + account_id=account.id, + sid=self._to_int(config_info.get("sid")) or 2203, spu_id=item["spu_id"], sku_id=item["sku_id"], name=item["name"], @@ -382,7 +387,11 @@ class RechargeMixin: snapshot = ( worker_db.query(HuyaRechargeGoodsSnapshot) - .filter(HuyaRechargeGoodsSnapshot.spu_id == spu_id) + .filter( + HuyaRechargeGoodsSnapshot.account_id == account.id, + HuyaRechargeGoodsSnapshot.sid == (self._to_int(config_info.get("sid")) or 2203), + HuyaRechargeGoodsSnapshot.spu_id == spu_id, + ) .first() ) payload_sku_id = self._to_int(self.payload.get("sku_id"))