#!/usr/bin/env python3 """用已登录的 YYB CK 选择和平精英点券档位、区服和角色。""" from __future__ import annotations import argparse import json import random import re import sys import time import uuid from pathlib import Path from urllib.parse import unquote, urlencode from curl_cffi import requests ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) from pyvm.login_profile import midas_login_params # noqa: E402 YYB_APP_ID = 52575843 SOURCE_ID = "24292013" PRODUCTS_URL = "https://ydd.yyb.qq.com/new_direct_buy_shop/GetTokenMod" CMALL_URL = "https://storeapi.pay.qq.com/api/unipay/{offer_id}/cmall_query" PLATFORMS = { "android": { "label": "Android", "query_platform": "pc_android", "cmall_pf": "mds_storeopen_qb-__mds_myappjp_PC_aW9zd2hpdGVsaX0-android", "order_pf": "mds_myappjp-__mds_myappjp_PC_aW9zd2hpdGVsaX0-android", }, "ios": { "label": "iOS", "query_platform": "pc_ios", "cmall_pf": "mds_storeopen_qb-__mds_myappjp_PC_aW9zd2hpdGVsaX0-iap", "order_pf": "mds_myappjp-__mds_myappjp_PC_aW9zd2hpdGVsaX0-iap", }, } UA = ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36" ) def load_cookies(path: Path) -> dict[str, str]: session = json.loads(path.read_text(encoding="utf-8")) cookies = {str(key): str(value) for key, value in session.get("cookies", {}).items() if value} if not cookies.get("openid") or not cookies.get("accesstoken"): raise ValueError("会话缺少 openid/accesstoken,请先执行 login-wechat.py") return cookies def product_options(cookies: dict[str, str], platform: str) -> list[dict[str, str | int]]: response = requests.post( PRODUCTS_URL, json={"platform": PLATFORMS[platform]["query_platform"], "source_id": SOURCE_ID, "yyb_app_id": YYB_APP_ID}, headers={"Accept": "application/json, text/plain, */*", "Origin": "https://m.yyb.qq.com", "Referer": "https://m.yyb.qq.com/boc-mall/goods-mall/", "User-Agent": UA}, cookies=cookies, impersonate="chrome", timeout=30, ) if response.status_code != 200: raise RuntimeError(f"点券商品查询失败: HTTP {response.status_code}") document = response.json() if document.get("code") not in (None, 0): raise RuntimeError(f"点券商品查询失败: {document.get('code')} {document.get('message', '')}") products = document.get("token_mod", {}).get("products", []) result: list[dict[str, str | int]] = [] for item in products: product = item.get("product", {}) if isinstance(item, dict) else {} match = re.fullmatch(r"(\d+)点券", str(product.get("product_name", ""))) if not match or str(product.get("status")) != "20": continue result.append({ "points": int(match.group(1)), "product_id": str(product.get("product_id", "")), "price_fen": int(product.get("price", 0)), "offer_id": str(product.get("res_offer_id", "")), "name": str(product.get("product_name", "")), }) result.sort(key=lambda item: int(item["points"])) if not result or any(not item["product_id"] or not item["offer_id"] for item in result): raise RuntimeError("点券商品响应缺少 product_id 或 offer_id") return result class Cmall: def __init__(self, cookies: dict[str, str], offer_id: str, platform: str) -> None: self.cookies = cookies self.offer_id = offer_id self.platform = platform self.session_token = f"{str(uuid.uuid4()).upper()}{int(time.time() * 1000)}" def query(self, cmd: str, **extra: str) -> dict: login = midas_login_params(self.cookies) params = { "from_h5": "1", "pf": PLATFORMS[self.platform]["cmall_pf"], "r": str(random.random()), "cmd": cmd, "session_token": self.session_token, "pfkey": "pfkey", "webversion": "", **extra, **login, } # 当前商城页面将查询参数放在 URL 上,但请求方法为 POST 且没有 body。 response = requests.post( CMALL_URL.format(offer_id=self.offer_id) + "?" + urlencode(params), headers={"Origin": "https://z.iwan.yyb.qq.com", "Referer": "https://z.iwan.yyb.qq.com/", "User-Agent": UA}, cookies=self.cookies, impersonate="chrome", timeout=30, ) if response.status_code != 200: raise RuntimeError(f"游戏数据查询失败: HTTP {response.status_code}") document = response.json() if document.get("ret") not in (0, "0"): raise RuntimeError(f"游戏数据查询失败: {document.get('ret')} {document.get('msg', '')}") return document def zones(self) -> list[dict[str, str]]: response = self.query("14", use_currency_offerid="1") zones = response.get("zone_list", []) return [{"zone_id": str(item.get("zone_id", "")), "name": str(item.get("zone_name", ""))} for item in zones if isinstance(item, dict) and item.get("zone_id")] def roles(self, zone_id: str) -> list[dict[str, str]]: response = self.query("15", use_currency_offerid="1", zoneid=zone_id) roles = response.get("role_info") or response.get("role_list") or [] return [{"role_id": str(item.get("role_id", "")), "name": unquote(str(item.get("role_name", ""))), "ban_status": str(item.get("ban_status", ""))} for item in roles if isinstance(item, dict) and item.get("role_id")] def choose(label: str, options: list[dict], display) -> dict: if not options: raise RuntimeError(f"没有可选择的{label}") print(f"\n可选{label}:") for index, item in enumerate(options, start=1): print(f" {index}. {display(item)}") while True: raw = input(f"请选择{label}序号 [1-{len(options)}]: ").strip() if raw.isdigit() and 1 <= int(raw) <= len(options): return options[int(raw) - 1] print("请输入列表中的有效序号。") def main() -> int: parser = argparse.ArgumentParser(description="和平精英点券和角色选择(纯 CK 查询)") parser.add_argument("--session", type=Path, default=ROOT / "config/mall-session.json") parser.add_argument("--output", type=Path, default=ROOT / "config/peace-elite-selection.json") parser.add_argument("--platform", choices=tuple(PLATFORMS), default=None, help="预选 Android/iOS;不传则显示平台菜单") parser.add_argument("--points", type=int, default=None, help="预选点券数;不传则显示菜单") parser.add_argument("--list-products", action="store_true", help="仅列出当前所有点券档位") args = parser.parse_args() cookies = load_cookies(args.session) if args.platform is None: selected_platform = choose( "平台", [{"id": key, **value} for key, value in PLATFORMS.items()], lambda item: item["label"], ) platform = str(selected_platform["id"]) else: platform = args.platform print(f"已选择平台: {PLATFORMS[platform]['label']}") products = product_options(cookies, platform) if args.list_products: for product in products: print(f"{product['points']}点券\t{product['price_fen'] / 100:g}元\t{product['product_id']}") return 0 if args.points is None: product = choose("点券档位", products, lambda item: f"{item['points']}点券({item['price_fen'] / 100:g}元)") else: product = next((item for item in products if item["points"] == args.points), None) if product is None: available = ", ".join(str(item["points"]) for item in products) raise ValueError(f"不支持 {args.points} 点券;当前可选: {available}") print(f"已选择: {product['points']}点券({product['price_fen'] / 100:g}元)") cmall = Cmall(cookies, str(product["offer_id"]), platform) zone = choose("区服", cmall.zones(), lambda item: f"{item['name']}(ID {item['zone_id']})") roles = cmall.roles(zone["zone_id"]) role = choose("角色", roles, lambda item: f"{item['name']}({'禁用' if item['ban_status'] == '1' else '可用'})") if role["ban_status"] == "1": raise RuntimeError("所选角色已被封禁,不能充值") selection = {"platform": platform, "order_pf": PLATFORMS[platform]["order_pf"], "points": product["points"], "product_id": product["product_id"], "offer_id": product["offer_id"], "price_fen": product["price_fen"], "zone_id": zone["zone_id"], "zone_name": zone["name"], "role_id": role["role_id"], "role_name": role["name"]} args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(selection, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(f"\n已选择 {selection['points']}点券 / {selection['zone_name']} / {selection['role_name']}") print(f"选择已保存: {args.output}") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except (OSError, ValueError, RuntimeError, requests.RequestsError) as error: print(f"选择失败: {error}") raise SystemExit(1) from error