Files
live-hub-py/core/weibo/client.py
T

900 lines
30 KiB
Python

"""微博微豆 CK、资料、商城、充值账单与绑定 Scheme 客户端。
覆盖手动触发的只读查询和充值下单:
- 微博 CK -> 微豆首页资料
- bind_url -> 微信/QQ 小程序 URL Scheme
- 商品列表、兑换记录、微豆获取明细
- 创建充值订单并解析支付二维码,不自动完成支付
不提交角色绑定、不自动兑换。
"""
from __future__ import annotations
import json
import time
from dataclasses import dataclass
from html.parser import HTMLParser
from typing import Any
from urllib.parse import parse_qs, urljoin, urlsplit
import requests
WEIBO_HOME_URL = "https://vipclub.e.weibo.com/aj/mall/home"
WEIBO_API_BASE = "https://vipclub.e.weibo.com/aj"
WEIBO_VPLUS_API_BASE = "https://vipclub.weibo.com/aj"
WEIBO_PAY_BASE = "https://pay.sc.weibo.com"
WEIBO_SCHEME_URL = "https://livelinkbind.game.qq.com/livelink"
WEIBO_MOBILE_UA = (
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) "
"AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Weibo"
)
WEIBO_DESKTOP_UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/156.0.0.0 Safari/537.36"
)
WEIBO_AUTH_ERROR_CODES = {100002, 100004, 400000, 400002}
class WeiboError(RuntimeError):
"""微博基础流程的基类异常。"""
class WeiboTransportError(WeiboError):
"""网络或 HTTP 层错误。"""
class WeiboAuthError(WeiboError):
"""CK 无效或登录态失效。"""
class WeiboApiError(WeiboError):
"""微博业务接口返回非成功状态。"""
class WeiboBindError(WeiboError):
"""绑定链接或二维码 Scheme 无法生成。"""
class WeiboRechargeError(WeiboError):
"""充值下单、收银台或支付状态解析失败。"""
class WeiboExchangeError(WeiboError):
"""兑换前校验失败或服务端明确拒绝。"""
class WeiboExchangeUncertainError(WeiboExchangeError):
"""兑换请求已发出但结果无法确认。"""
@dataclass(frozen=True, slots=True)
class WeiboHomeSnapshot:
"""微豆首页中基础账号流程需要的字段。"""
uid: str
nickname: str
is_bound: bool
role_name: str
can_rebind: bool
rebind_remain_days: int
weidou_balance: int
bind_url: str
recharge_url: str = ""
activity: str = ""
@dataclass(frozen=True, slots=True)
class WeiboRechargeOrder:
order_id: str
pay_id: str
amount_yuan: int
single_reward: int
alipay_qr_code: str
desktop_pay_url: str
@dataclass(frozen=True, slots=True)
class WeiboPaymentStatus:
pay_id: str
out_pay_id: str
status: str
sub_status: str
@dataclass(frozen=True, slots=True)
class WeiboExchangeResult:
goods_id: int
goods_name: str
price: int
balance_before: int
balance_after: int | None
remain_before: int | None
remain_after: int | None
verified: bool
verify_error: str = ""
class _CashierParser(HTMLParser):
"""从微博 PC 收银台 HTML 中提取支付要素。"""
def __init__(self) -> None:
super().__init__()
self.pay_id = ""
self.desktop_pay_url = ""
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
values = {name: value or "" for name, value in attrs}
if tag == "input" and (
values.get("name") == "pay_id" or values.get("node-type") == "pay_id"
):
self.pay_id = values.get("value", "").strip()
elif tag == "a" and values.get("href", "").startswith(
f"{WEIBO_PAY_BASE}/pay/pc/comb?"
):
self.desktop_pay_url = values["href"].strip()
class _AlipayCashierParser(HTMLParser):
"""从支付宝电脑收银台 HTML 提取实际支付二维码内容。"""
def __init__(self) -> None:
super().__init__()
self.qr_code = ""
self.qr_image_url = ""
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag != "input":
return
values = {name: value or "" for name, value in attrs}
input_id = values.get("id", "")
input_name = values.get("name", "")
value = values.get("value", "").strip()
if input_id == "J_qrCode" or input_name == "qrCode":
self.qr_code = value
elif input_id == "J_qrImgUrl" or input_name == "qrImgUrl":
self.qr_image_url = value
class _HtmlFormParser(HTMLParser):
"""提取简单 HTML 表单,用于跟随支付宝网关的自动提交页。"""
def __init__(self) -> None:
super().__init__()
self.action = ""
self.method = "GET"
self.fields: dict[str, str] = {}
self._inside_form = False
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
values = {name: value or "" for name, value in attrs}
if tag == "form" and not self.action:
self.action = values.get("action", "").strip()
self.method = values.get("method", "GET").strip().upper() or "GET"
self._inside_form = True
elif tag == "input" and self._inside_form:
name = values.get("name", "").strip()
if name:
self.fields[name] = values.get("value", "")
def handle_endtag(self, tag: str) -> None:
if tag == "form":
self._inside_form = False
def normalize_weibo_cookie(raw: str) -> str:
"""把粘贴内容整理成一行 Cookie。
支持带 ``Cookie:`` 前缀、多行 Cookie 和普通分号格式。不解析或改写值,
避免破坏微博 Cookie 中可能包含的百分号编码。
"""
text = (raw or "").strip()
if not text:
raise ValueError("CK 为空")
if text.lower().startswith("cookie:"):
text = text.split(":", 1)[1].strip()
normalized = text.replace("\r", ";").replace("\n", ";")
pairs: list[str] = []
for part in normalized.split(";"):
item = part.strip()
if not item:
continue
if "=" not in item:
raise ValueError(f"Cookie 片段格式错误: {item[:40]}")
pairs.append(item)
if not pairs:
raise ValueError("CK 为空")
return "; ".join(pairs)
def _parse_cookie_pairs(cookie: str) -> dict[str, str]:
pairs: dict[str, str] = {}
for item in cookie.split(";"):
name, separator, value = item.strip().partition("=")
if separator and name:
pairs[name] = value
return pairs
def _extract_subscribe_url(action_url: str) -> str:
"""把 ``sinaweibo://transparentbrowser`` 还原成实际 V+ 页面 URL。"""
value = (action_url or "").strip()
if not value:
return ""
if value.startswith("sinaweibo://transparentbrowser?"):
query = parse_qs(urlsplit(value).query, keep_blank_values=True)
return str((query.get("url") or [""])[0]).strip()
return value
def _subscribe_params(action_url: str) -> dict[str, str]:
url = _extract_subscribe_url(action_url)
if not url:
return {}
query = parse_qs(urlsplit(url).query, keep_blank_values=True)
return {key: str((values or [""])[0]) for key, values in query.items()}
def extract_bind_raw_url(bind_url: str) -> str:
"""从微博 ``sinaweibo://wbdiversion`` 链接提取来联内层 Query。
outer query 只解码一次,得到 ``path``;内层 ``code/faceUrl`` 仍保留
抓包中的多重编码,不能再 unquote。
"""
value = (bind_url or "").strip()
if not value:
raise WeiboBindError("微博未返回 bind_url")
try:
outer = parse_qs(urlsplit(value).query, keep_blank_values=True)
except ValueError as exc:
raise WeiboBindError("微博 bind_url 格式错误") from exc
path = (outer.get("path") or [""])[0]
marker = "/pages/gameAccountBind/index?"
if marker not in path:
raise WeiboBindError("bind_url 中缺少绑定小程序路径")
raw_url = path.split(marker, 1)[1]
if not raw_url:
raise WeiboBindError("bind_url 内层参数为空")
return raw_url
class WeiboClient:
"""微博微豆基础客户端。"""
def __init__(
self,
cookie: str = "",
*,
timeout: float = 20.0,
session: requests.Session | None = None,
):
self.cookie = normalize_weibo_cookie(cookie) if cookie else ""
self.timeout = timeout
self.session = session or requests.Session()
self.session.headers.update(
{
"User-Agent": WEIBO_MOBILE_UA,
"Accept": "application/json, text/plain, */*",
}
)
# 微博 Cookie 只挂到 .weibo.com,避免把 CK 发给来联/QQ 或支付宝域名。
for name, value in _parse_cookie_pairs(self.cookie).items():
self.session.cookies.set(name, value, domain=".weibo.com", path="/")
def _request_json(
self,
method: str,
url: str,
*,
headers: dict[str, str] | None = None,
params: dict[str, Any] | None = None,
data: dict[str, Any] | None = None,
json: dict[str, Any] | None = None,
) -> dict[str, Any]:
try:
response = self.session.request(
method,
url,
headers=headers,
params=params,
data=data,
json=json,
timeout=self.timeout,
)
response.raise_for_status()
payload = response.json()
except requests.RequestException as exc:
raise WeiboTransportError(f"请求微博失败: {exc}") from exc
except ValueError as exc:
raise WeiboTransportError("微博返回内容不是合法 JSON") from exc
if not isinstance(payload, dict):
raise WeiboTransportError("微博返回 JSON 结构异常")
return payload
def _request_text(
self,
method: str,
url: str,
*,
headers: dict[str, str] | None = None,
params: dict[str, Any] | None = None,
data: dict[str, Any] | None = None,
) -> str:
try:
response = self.session.request(
method,
url,
headers=headers,
params=params,
data=data,
timeout=self.timeout,
)
response.raise_for_status()
return response.text
except requests.RequestException as exc:
raise WeiboTransportError(f"请求微博收银台失败: {exc}") from exc
def _alipay_qr_from_page(self, html: str, page_url: str) -> str:
"""穿过支付宝网关自动提交表单,提取扫码支付内容。"""
current_html = html
current_url = page_url
for _ in range(4):
cashier = _AlipayCashierParser()
cashier.feed(current_html)
if cashier.qr_code:
return cashier.qr_code
form = _HtmlFormParser()
form.feed(current_html)
if not form.action:
break
action_url = urljoin(current_url, form.action)
current_html = self._request_text(
form.method,
action_url,
headers={
"User-Agent": WEIBO_DESKTOP_UA,
"Referer": current_url,
},
params=form.fields if form.method == "GET" else None,
data=form.fields if form.method != "GET" else None,
)
current_url = action_url
raise WeiboRechargeError("支付宝收银台没有返回扫码支付内容")
def _weibo_api(
self,
method: str,
path: str,
*,
params: dict[str, Any],
data: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
base: str = WEIBO_API_BASE,
) -> dict[str, Any]:
payload = self._request_json(
method,
f"{base}{path}",
params=params,
data=data,
headers=headers,
)
code = payload.get("code")
if code in WEIBO_AUTH_ERROR_CODES:
raise WeiboAuthError(str(payload.get("msg") or "微博 CK 已失效"))
if code != 100000:
raise WeiboApiError(str(payload.get("msg") or f"微博接口返回 code={code}"))
return payload
def fetch_home(self, activity: str = "") -> WeiboHomeSnapshot:
"""读取微豆首页并校验 CK。"""
if not self.cookie:
raise WeiboAuthError("CK 为空")
url = f"{WEIBO_HOME_URL}?F={activity}&_t={int(time.time() * 1000)}"
payload = self._request_json(
"GET",
url,
headers={
"Referer": f"https://vipclub.e.weibo.com/game/homepage?F={activity}",
},
)
code = payload.get("code")
if code in WEIBO_AUTH_ERROR_CODES:
raise WeiboAuthError(str(payload.get("msg") or "微博 CK 已失效"))
if code != 100000:
trace_id = str(payload.get("trace_id") or "")
message = str(payload.get("msg") or f"微博接口返回 code={code}")
if trace_id:
message = f"{message} (trace_id={trace_id})"
raise WeiboApiError(message)
data = payload.get("data") or {}
user_info = data.get("user_info") or {}
game_role = data.get("game_role") or {}
balance = data.get("weidou_balance") or {}
recharge_url = ""
for task in data.get("task_list") or []:
if not isinstance(task, dict) or task.get("button_type") != "cashier":
continue
candidate = _extract_subscribe_url(str(task.get("action_url") or ""))
if candidate:
recharge_url = candidate
break
subscribe = _subscribe_params(recharge_url)
resolved_activity = subscribe.get("F") or activity
uid = str(user_info.get("uid") or "").strip()
bind_url = str(game_role.get("bind_url") or "").strip()
if not uid:
raise WeiboApiError("微博首页没有返回 uid")
return WeiboHomeSnapshot(
uid=uid,
nickname=str(user_info.get("nickname") or ""),
is_bound=bool(game_role.get("is_bound")),
role_name=str(game_role.get("role_name") or ""),
can_rebind=bool(game_role.get("can_rebind")),
rebind_remain_days=int(game_role.get("rebind_remain_days") or 0),
weidou_balance=int(balance.get("balance") or 0),
bind_url=bind_url,
recharge_url=recharge_url,
activity=resolved_activity,
)
def list_mall(self, activity: str = "") -> dict[str, Any]:
"""查询微豆商城商品列表。"""
payload = self._weibo_api(
"GET",
"/mall/list",
params={"F": activity, "_t": int(time.time() * 1000)},
)
return payload.get("data") or {}
def exchange_goods(
self,
goods_id: int,
*,
expected_price: int,
activity: str = "",
) -> WeiboExchangeResult:
"""手动兑换商品:预检后只提交一次,成功后尽力复核。"""
before = self.list_mall(activity)
goods_list = before.get("list") or []
goods = next(
(
item
for item in goods_list
if isinstance(item, dict) and int(item.get("id") or 0) == goods_id
),
None,
)
if goods is None:
raise WeiboExchangeError("商品不存在或已下架")
price = int(goods.get("price") or 0)
balance_before = int(before.get("weidou_balance") or 0)
remain_before = (
int(goods["remain_count"])
if goods.get("remain_count") is not None
else None
)
if expected_price > 0 and price != expected_price:
raise WeiboExchangeError(
f"商品价格已变化:页面 {expected_price},当前 {price}"
)
if goods.get("is_sold_out") or remain_before == 0:
raise WeiboExchangeError("商品库存不足")
if balance_before < price:
raise WeiboExchangeError(
f"微豆余额不足:当前 {balance_before},需要 {price}"
)
try:
payload = self._weibo_api(
"GET",
"/game/exchangegoods",
params={
"F": activity,
"goods_id": goods_id,
"_t": int(time.time() * 1000),
},
headers={
"User-Agent": WEIBO_DESKTOP_UA,
"Referer": f"https://vipclub.e.weibo.com/game/mall?F={activity}",
},
)
except WeiboTransportError as exc:
raise WeiboExchangeUncertainError(
"兑换请求已发出,但网络中断导致结果未知;请先查兑换记录,不要立即重试"
) from exc
if payload.get("code") != 100000:
raise WeiboExchangeError(
str(payload.get("msg") or f"兑换失败 code={payload.get('code')}")
)
balance_after: int | None = None
remain_after: int | None = None
verified = False
verify_error = ""
try:
after = self.list_mall(activity)
balance_after = int(after.get("weidou_balance") or 0)
refreshed = next(
(
item
for item in (after.get("list") or [])
if isinstance(item, dict) and int(item.get("id") or 0) == goods_id
),
None,
)
if refreshed and refreshed.get("remain_count") is not None:
remain_after = int(refreshed["remain_count"])
verified = balance_after != balance_before or (
remain_after is not None
and remain_before is not None
and remain_after != remain_before
)
if not verified:
verify_error = "兑换成功,但余额和库存暂未变化,请查兑换记录复核"
except (WeiboError, ValueError, TypeError) as exc:
verify_error = f"兑换已提交,但复核失败: {exc}"
return WeiboExchangeResult(
goods_id=goods_id,
goods_name=str(goods.get("goods_name") or ""),
price=price,
balance_before=balance_before,
balance_after=balance_after,
remain_before=remain_before,
remain_after=remain_after,
verified=verified,
verify_error=verify_error,
)
def list_exchange_records(
self,
activity: str = "",
*,
page: int = 1,
page_size: int = 20,
) -> dict[str, Any]:
"""查询微豆兑换记录。"""
payload = self._weibo_api(
"GET",
"/mall/exchange",
params={
"F": activity,
"page": page,
"page_size": page_size,
"_t": int(time.time() * 1000),
},
)
return payload.get("data") or {}
def list_point_history(
self,
activity: str = "",
*,
page: int = 1,
page_size: int = 20,
) -> dict[str, Any]:
"""查询微豆获取/充值明细。"""
payload = self._weibo_api(
"GET",
"/point/get_point_recharge_history",
params={
"F": activity,
"page_no": page,
"page_size": page_size,
"_t": int(time.time() * 1000),
},
)
return payload.get("data") or {}
def _load_recharge_context(
self,
activity: str,
) -> tuple[dict[str, str], dict[str, Any], dict[str, Any]]:
home = self.fetch_home(activity)
if not home.recharge_url:
raise WeiboRechargeError("当前活动没有可用的助威充值入口")
subscribe = _subscribe_params(home.recharge_url)
vuid = subscribe.get("vuid", "")
source_itemid = subscribe.get("source_itemid", "")
if not vuid or not source_itemid:
raise WeiboRechargeError("充值入口缺少 vuid 或 source_itemid")
resolved_activity = subscribe.get("F") or home.activity or activity
subscribe["F"] = resolved_activity
page_headers = {
"User-Agent": WEIBO_DESKTOP_UA,
"Referer": home.recharge_url,
}
self._weibo_api(
"GET",
"/vmember/gfsupport",
params={
"vuid": vuid,
"source_itemid": source_itemid,
"F": resolved_activity,
"_t": int(time.time() * 1000),
},
base=WEIBO_VPLUS_API_BASE,
headers=page_headers,
)
user_payload = self._weibo_api(
"GET",
"/paycomponent/userinfo",
params={
"vuid": vuid,
"source_itemid": source_itemid,
"_t": int(time.time() * 1000),
},
base=WEIBO_VPLUS_API_BASE,
headers=page_headers,
)
user_data = user_payload.get("data") or {}
tabs = user_data.get("tab_info") or []
current_tab = next(
(tab for tab in tabs if isinstance(tab, dict) and tab.get("is_current")),
tabs[0] if tabs and isinstance(tabs[0], dict) else {},
)
tab_id = str(current_tab.get("tab_id") or "")
cashier_id = str(
current_tab.get("cashier_id") or user_data.get("cashier_id") or ""
)
if not tab_id or not cashier_id:
raise WeiboRechargeError("收银台没有返回 tab_id 或 cashier_id")
cashier_payload = self._weibo_api(
"GET",
"/paycomponent/cashierinfo",
params={
"F": resolved_activity,
"appName": "",
"from": "",
"wm": "",
"profileuid": "",
"tab_id": tab_id,
"cashier_id": cashier_id,
"device_id": "",
"tab_type": "",
"cashier_version": "0.2.3",
"vuid": vuid,
"bid": "",
"source_itemid": source_itemid,
},
base=WEIBO_VPLUS_API_BASE,
headers=page_headers,
)
return subscribe, current_tab, cashier_payload.get("data") or {}
def _recharge_bid(
self,
vuid: str,
source_itemid: str,
tab_type: str,
activity: str,
) -> str:
payload = self._weibo_api(
"GET",
"/paycomponent/privilege",
params={
"tab_type": tab_type,
"vuid": vuid,
"F": activity,
"_t": int(time.time() * 1000),
},
base=WEIBO_VPLUS_API_BASE,
headers={
"User-Agent": WEIBO_DESKTOP_UA,
"Referer": f"https://vipclub.weibo.com/vmember/subscribe?vuid={vuid}&source_itemid={source_itemid}&F={activity}",
},
)
data = payload.get("data") or {}
privilege = data.get("privilege") or {}
url = str(privilege.get("url") or "")
return str((parse_qs(urlsplit(url).query).get("bid") or [""])[0])
def create_recharge_order(
self,
*,
amount_yuan: int,
activity: str = "",
) -> WeiboRechargeOrder:
"""创建人工充值订单并解析支付二维码,不执行支付。"""
if amount_yuan < 1:
raise WeiboRechargeError("充值金额至少为 1 元")
subscribe, current_tab, cashier_data = self._load_recharge_context(activity)
tabs = cashier_data.get("tab_info") or []
tab = next(
(
item
for item in tabs
if isinstance(item, dict) and item.get("is_current")
),
tabs[0] if tabs and isinstance(tabs[0], dict) else current_tab,
)
menus = tab.get("menu") or []
menu = next(
(
item
for item in menus
if isinstance(item, dict)
and item.get("max_money") is not None
and str(item.get("subitemid") or "")
),
None,
)
if menu is None:
raise WeiboRechargeError("收银台没有返回自定义微豆充值档位")
max_money = int(menu.get("max_money") or 0)
if max_money and amount_yuan > max_money:
raise WeiboRechargeError(f"充值金额不能超过 {max_money} 元")
vuid = subscribe["vuid"]
source_itemid = subscribe["source_itemid"]
resolved_activity = subscribe["F"]
tab_type = str(tab.get("tab_type") or current_tab.get("tab_type") or "")
cashier_id = str(tab.get("cashier_id") or current_tab.get("cashier_id") or "")
bid = self._recharge_bid(
vuid,
source_itemid,
tab_type,
resolved_activity,
)
single_reward = amount_yuan * 100
ext = json.dumps(
{"bid": bid, "send_blog": 1},
ensure_ascii=False,
separators=(",", ":"),
)
params = {
"subitemid": str(menu.get("subitemid") or ""),
"itemid": str(menu.get("itemid") or ""),
"appkey": str(menu.get("appkey") or ""),
"commodity_id": str(menu.get("commodity_id") or ""),
"num": "1",
"salestrategy_id": "",
"discount_id": "",
"from": "",
"device_id": "",
"level_up_tag": "0",
"cashier_id": cashier_id,
"F": resolved_activity,
"to_uid": "",
"is_from_cashier": "1",
"return_url": "",
"ext": ext,
"ori_t_id": "",
"will_auto_renew": "0",
"cashierpw": "1",
"subpw": "",
"account": "",
"follow": "false",
"coupon_id": "",
"vuid": vuid,
"tab_type": tab_type,
"cashier_version": "0.2.3",
"gc": "",
"bid": "",
"mid": "",
"single_reward": str(single_reward),
}
payload = self._request_json(
"POST",
f"{WEIBO_VPLUS_API_BASE}/paycomponent/createorder",
params=params,
data=params,
headers={
"User-Agent": WEIBO_DESKTOP_UA,
"Origin": "https://vipclub.weibo.com",
"Referer": f"https://vipclub.weibo.com/vmember/subscribe?vuid={vuid}&source_itemid={source_itemid}&F={resolved_activity}",
},
)
if payload.get("code") in WEIBO_AUTH_ERROR_CODES:
raise WeiboAuthError(str(payload.get("msg") or "微博 CK 已失效"))
if payload.get("code") != 100000:
raise WeiboRechargeError(str(payload.get("msg") or "创建充值订单失败"))
order_data = payload.get("data") or {}
cashier_url = str(order_data.get("url") or "")
order_id = str(order_data.get("orderid") or "")
if not cashier_url or not order_id:
raise WeiboRechargeError("充值订单缺少支付地址或订单号")
html = self._request_text(
"GET",
cashier_url,
headers={
"User-Agent": WEIBO_DESKTOP_UA,
"Referer": "https://vipclub.weibo.com/",
},
)
parser = _CashierParser()
parser.feed(html)
if not parser.pay_id:
raise WeiboRechargeError("收银台页面没有返回 pay_id")
if not parser.desktop_pay_url:
raise WeiboRechargeError("收银台页面没有返回电脑端支付链接")
alipay_html = self._request_text(
"GET",
parser.desktop_pay_url,
headers={
"User-Agent": WEIBO_DESKTOP_UA,
"Referer": "https://pay.sc.weibo.com/pay/pc/cashier",
},
)
alipay_qr_code = self._alipay_qr_from_page(
alipay_html,
parser.desktop_pay_url,
)
return WeiboRechargeOrder(
order_id=order_id,
pay_id=parser.pay_id,
amount_yuan=amount_yuan,
single_reward=single_reward,
alipay_qr_code=alipay_qr_code,
desktop_pay_url=parser.desktop_pay_url,
)
def get_payment_status(self, pay_id: str) -> WeiboPaymentStatus:
"""手动查询充值支付状态。"""
if not pay_id.strip():
raise ValueError("pay_id 为空")
payload = self._request_json(
"POST",
f"{WEIBO_PAY_BASE}/aj/pay/status",
data={"pay_id": pay_id, "ispage": "1", "_t": "0"},
headers={
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": WEIBO_DESKTOP_UA,
"X-Requested-With": "XMLHttpRequest",
"Origin": WEIBO_PAY_BASE,
"Referer": f"{WEIBO_PAY_BASE}/pay/pc/cashier",
},
)
if str(payload.get("code")) != "100000":
raise WeiboRechargeError(str(payload.get("msg") or "查询支付状态失败"))
data = payload.get("data") or {}
return WeiboPaymentStatus(
pay_id=str(data.get("pay_id") or pay_id),
out_pay_id=str(data.get("out_pay_id") or ""),
status=str(data.get("status") or ""),
sub_status=str(data.get("sub_status") or ""),
)
def generate_bind_scheme(self, bind_url: str, account_type: str) -> str:
"""生成微信或 QQ 小程序 URL Scheme。"""
if account_type not in {"wx", "qq"}:
raise ValueError("account_type 只能是 wx 或 qq")
raw_url = extract_bind_raw_url(bind_url)
url = f"{WEIBO_SCHEME_URL}?c=Sdk&a=GenMiniappScheme&{raw_url}"
payload = self._request_json(
"POST",
url,
headers={"Content-Type": "application/json"},
json={"accountType": account_type},
)
if payload.get("iRet") != 0:
raise WeiboBindError(
f"生成绑定 Scheme 失败: {payload.get('sMsg') or payload}"
)
scheme = str((payload.get("jData") or {}).get("urlScheme") or "").strip()
if not scheme:
raise WeiboBindError("来联接口没有返回 urlScheme")
return scheme