|
|
@@ -36,7 +36,8 @@ SALT = "biu_card_nbclass" # 签名固定 salt(逆向自 mf/c0.java
|
|
|
USE_PROXY = False # 代理开关:默认直连,遇 IP 风控再置 True
|
|
|
TOKEN_FILE = "token.json" # token 持久化文件(进程重启复用),refreshToken 续期为主、密码登录兜底
|
|
|
|
|
|
-# 密码登录兜底凭证(敏感信息,勿外传/勿提交公开仓库;换账号改此三项)
|
|
|
+# 密码登录兜底凭证(默认主号;实际优先取各机 token.json 里的 phone/password/countryCode,
|
|
|
+# 见 load_token/login。敏感信息,勿外传/勿提交公开仓库)
|
|
|
LOGIN_COUNTRY_CODE = "86"
|
|
|
LOGIN_PHONE = "13014617614"
|
|
|
LOGIN_PASSWORD = "pass2022"
|
|
|
@@ -140,7 +141,8 @@ def make_signature(params: dict, current_time: str) -> str:
|
|
|
|
|
|
|
|
|
@retry(stop=stop_after_attempt(5), wait=wait_fixed(2), after=after_log)
|
|
|
-def do_request(log, path: str, body: dict, need_auth: bool = False) -> dict | None:
|
|
|
+def do_request(log, path: str, body: dict, need_auth: bool = False,
|
|
|
+ use_proxy: bool = None) -> dict | None:
|
|
|
"""通用带签名 POST 请求(带重试)。
|
|
|
|
|
|
Args:
|
|
|
@@ -148,6 +150,9 @@ def do_request(log, path: str, body: dict, need_auth: bool = False) -> dict | No
|
|
|
path (str): 接口相对路径(以 / 开头)。
|
|
|
body (dict): JSON body,同时用于签名。
|
|
|
need_auth (bool, optional): 是否需要携带 Bearer token。Defaults to False。
|
|
|
+ use_proxy (bool, optional): 是否走代理。None=沿用全局 USE_PROXY;显式 True/False 覆盖。
|
|
|
+ 用于「带 token 的请求走直连(False)、免 token 高频请求走隧道(True)」按调用区分,
|
|
|
+ 避免一个账号在轮换 IP 间跳动而被风控关联。Defaults to None。
|
|
|
|
|
|
Returns:
|
|
|
dict | None: 响应 JSON。
|
|
|
@@ -163,7 +168,8 @@ def do_request(log, path: str, body: dict, need_auth: bool = False) -> dict | No
|
|
|
headers["Authorization"] = f"Bearer {ensure_token(log)}"
|
|
|
|
|
|
data = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
|
- proxies = get_proxys(log) if USE_PROXY else None
|
|
|
+ _use_proxy = USE_PROXY if use_proxy is None else use_proxy # 按调用覆盖全局代理开关
|
|
|
+ proxies = get_proxys(log) if _use_proxy else None
|
|
|
resp = requests.post(BASE + path, headers=headers, data=data, timeout=(5, 30), proxies=proxies)
|
|
|
if resp.status_code != 200:
|
|
|
log.error(f"请求失败 {resp.status_code}: {path}")
|
|
|
@@ -172,7 +178,8 @@ def do_request(log, path: str, body: dict, need_auth: bool = False) -> dict | No
|
|
|
|
|
|
|
|
|
@retry(stop=stop_after_attempt(5), wait=wait_fixed(2), after=after_log)
|
|
|
-def do_get(log, path: str, params: dict = None, need_auth: bool = False) -> dict | None:
|
|
|
+def do_get(log, path: str, params: dict = None, need_auth: bool = False,
|
|
|
+ use_proxy: bool = None) -> dict | None:
|
|
|
"""通用带签名 GET 请求(带重试)。签名参数取 query。
|
|
|
|
|
|
Args:
|
|
|
@@ -180,6 +187,7 @@ def do_get(log, path: str, params: dict = None, need_auth: bool = False) -> dict
|
|
|
path (str): 接口相对路径(以 / 开头)。
|
|
|
params (dict, optional): URL query 参数(同时用于签名)。Defaults to None。
|
|
|
need_auth (bool, optional): 是否需要携带 Bearer token。Defaults to False。
|
|
|
+ use_proxy (bool, optional): 是否走代理。None=沿用全局 USE_PROXY;显式 True/False 覆盖。Defaults to None。
|
|
|
|
|
|
Returns:
|
|
|
dict | None: 响应 JSON。
|
|
|
@@ -194,7 +202,7 @@ def do_get(log, path: str, params: dict = None, need_auth: bool = False) -> dict
|
|
|
if need_auth:
|
|
|
headers["Authorization"] = f"Bearer {ensure_token(log)}"
|
|
|
|
|
|
- proxies = get_proxys(log) if USE_PROXY else None
|
|
|
+ proxies = get_proxys(log) if (USE_PROXY if use_proxy is None else use_proxy) else None
|
|
|
resp = requests.get(BASE + path, headers=headers, params=params, timeout=(5, 30), proxies=proxies)
|
|
|
if resp.status_code != 200:
|
|
|
log.error(f"请求失败 {resp.status_code}: {path}")
|
|
|
@@ -276,17 +284,30 @@ def _token_lock(log=None, timeout=30):
|
|
|
|
|
|
|
|
|
def save_token():
|
|
|
- """把当前 _TOKEN(access/refresh/exp) 持久化到 TOKEN_FILE。"""
|
|
|
+ """把当前 _TOKEN 持久化到 TOKEN_FILE(access/refresh/exp + 账号凭据 phone/password/countryCode)。
|
|
|
+
|
|
|
+ 账号凭据随 token.json 一并落盘,方便人工打开文件直接分辨该程序用的是哪个账号;仅当 _TOKEN
|
|
|
+ 里存在这些字段时才写入(避免把默认主号凭据注入到未配置的机器的 token.json)。
|
|
|
+ """
|
|
|
try:
|
|
|
+ out = {"phone": _TOKEN.get("phone"), "password": _TOKEN.get("password"),
|
|
|
+ "countryCode": _TOKEN.get("countryCode"),
|
|
|
+ "access": _TOKEN.get("access"), "refresh": _TOKEN.get("refresh"),
|
|
|
+ "exp": _TOKEN.get("exp", 0)}
|
|
|
+ # 未配置账号凭据的机器(如沿用默认主号)不写空的 phone/password,保持 token.json 干净
|
|
|
+ out = {k: v for k, v in out.items() if not (k in ("phone", "password", "countryCode") and v is None)}
|
|
|
with open(TOKEN_FILE, "w", encoding="utf-8") as f:
|
|
|
- json.dump({"access": _TOKEN.get("access"), "refresh": _TOKEN.get("refresh"),
|
|
|
- "exp": _TOKEN.get("exp", 0)}, f)
|
|
|
+ json.dump(out, f, ensure_ascii=False, indent=2)
|
|
|
except Exception as e:
|
|
|
logger.warning(f"token 持久化失败: {e}")
|
|
|
|
|
|
|
|
|
def load_token():
|
|
|
- """从 TOKEN_FILE 恢复 _TOKEN;文件不存在或损坏时静默跳过。"""
|
|
|
+ """从 TOKEN_FILE 恢复 _TOKEN(access/refresh/exp + 账号凭据);文件不存在或损坏时静默跳过。
|
|
|
+
|
|
|
+ 若 token.json 内含 phone/password/countryCode,则一并载入 _TOKEN,供密码登录兜底按「本机所配
|
|
|
+ 账号」登录——从而三台机器各用各的账号、互不顶号(凭据缺失时 login 回落到模块默认主号)。
|
|
|
+ """
|
|
|
if not os.path.exists(TOKEN_FILE):
|
|
|
return
|
|
|
try:
|
|
|
@@ -295,6 +316,9 @@ def load_token():
|
|
|
_TOKEN["access"] = d.get("access")
|
|
|
_TOKEN["refresh"] = d.get("refresh")
|
|
|
_TOKEN["exp"] = d.get("exp", 0)
|
|
|
+ for k in ("phone", "password", "countryCode"): # 账号凭据:存在才载入,供 login 兜底用本机账号
|
|
|
+ if d.get(k):
|
|
|
+ _TOKEN[k] = d[k]
|
|
|
except Exception as e:
|
|
|
logger.warning(f"token 读取失败: {e}")
|
|
|
|
|
|
@@ -351,7 +375,12 @@ def login(log) -> bool:
|
|
|
log.error(f"密码登录已连续失败 {_login_fail_count} 次达上限({MAX_LOGIN_ATTEMPTS}),"
|
|
|
"熔断不再登录,请人工检查账号(密码/风控/验证码)")
|
|
|
return False
|
|
|
- body = {"countryCode": LOGIN_COUNTRY_CODE, "password": LOGIN_PASSWORD, "phone": LOGIN_PHONE}
|
|
|
+ # 账号凭据优先取 token.json 里所配(load_token 载入到 _TOKEN),缺失才回落模块默认主号——
|
|
|
+ # 从而三台机器各用各的账号登录兜底、不互相顶号
|
|
|
+ phone = _TOKEN.get("phone") or LOGIN_PHONE
|
|
|
+ password = _TOKEN.get("password") or LOGIN_PASSWORD
|
|
|
+ country_code = _TOKEN.get("countryCode") or LOGIN_COUNTRY_CODE
|
|
|
+ body = {"countryCode": country_code, "password": password, "phone": phone}
|
|
|
try:
|
|
|
resp = do_request(log, "/api/v1/app/auth/password/login", body, need_auth=False)
|
|
|
data = (resp or {}).get("data") or {}
|
|
|
@@ -368,7 +397,7 @@ def login(log) -> bool:
|
|
|
_TOKEN["exp"] = time.time() + expires_in
|
|
|
save_token()
|
|
|
_login_fail_count = 0
|
|
|
- log.info(f"密码登录成功,access 有效 {expires_in}s(refreshToken 已更新)")
|
|
|
+ log.info(f"密码登录成功(账号 {phone}),access 有效 {expires_in}s(refreshToken 已更新)")
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
_login_fail_count += 1
|