Files
cc892786825-qiandao/ltqycs.py
2025-12-15 23:18:27 +08:00

453 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
专业优化版 — 联通权益超市自动任务脚本
Version: 3.0-Pro
"""
import os
import sys
import time
import json
import logging
import requests
from urllib.parse import urlparse, parse_qs
from datetime import datetime
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# ======================
# 日志格式(带毫秒)
# ======================
class MsFormatter(logging.Formatter):
def formatTime(self, record, datefmt=None):
dt = datetime.fromtimestamp(record.created)
s = dt.strftime("%Y-%m-%d %H:%M:%S.%f")
return s[:-3]
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(message)s')
for h in logging.getLogger().handlers:
h.setFormatter(MsFormatter('[%(asctime)s] %(message)s'))
# ======================
# 共享 Session
# ======================
sess = requests.Session()
adapter = HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.3))
sess.mount("http://", adapter)
sess.mount("https://", adapter)
# ======================
# 统一 UA
# ======================
def ua():
return {
"User-Agent":
"Mozilla/5.0 (Linux; Android 10; Redmi K30 Pro Build/QKQ1.191117.002; wv) "
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.58 "
"Mobile Safari/537.36 unicom{version:android@11.0500}",
"Accept": "*/*",
}
# ======================
# 主类
# ======================
class CUAPI:
def __init__(self, accounts):
self.accounts = accounts
self.GrantPrize = True
# ======================
# ✨ 重构 do_send专业版 3.0
# 支持:
# - raw 模式
# - 自动 JSON 解析
# - 自动错误处理
# ======================
def do_send(self, url, method="GET", headers=None,
params=None, data=None, timeout=10,
raw=False, allow_redirects=True):
try:
resp = sess.request(
method=method,
url=url,
headers=headers,
params=params,
json=None if (data and "token_online" in str(data)) else data,
data=data if (data and "token_online" in str(data)) else None,
timeout=timeout,
allow_redirects=allow_redirects
)
except Exception as e:
logging.error(f"请求失败: {e}")
return None
# raw 直接返回响应对象
if raw:
return resp
if resp.status_code == 302:
return resp
try:
return resp.json()
except:
logging.error("响应非 JSON 格式")
return None
# ======================
# 登录 — token_online
# ======================
def login_with_token_online(self, phone, tok, appid):
url = "https://m.client.10010.com/mobileService/onLine.htm"
data = {
"reqtime": str(int(time.time() * 1000)),
"netWay": "Wifi",
"version": "android@11.0000",
"token_online": tok,
"appId": appid,
"deviceModel": "Mi10",
"step": "welcome",
"androidId": "e1d2c3b4a5f6"
}
resp = self.do_send(url, method="POST", headers=ua(), data=data)
if resp and resp.get("ecs_token"):
logging.info(f"{phone} token 登录成功")
return resp["ecs_token"]
logging.error(f"{phone} token 登录失败")
return None
# ======================
# 获取 ticket核心修复点
# ======================
def get_ticket(self, ecs_token):
"""
使用联通官方 H5 openPlatLine 跳转链路强制获取 ticket
此链路比 openPlatLineNew 更稳定token_online 登录也可使用
"""
url = (
"https://m.client.10010.com/mobileService/openPlatform/"
"openPlatLine.htm"
)
headers = {
"User-Agent":
"Mozilla/5.0 (Linux; Android 10; MI 10) "
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 "
"Chrome/108.0.5359.128 Mobile Safari/537.36 "
"unicom{version:android@11.0500}",
"X-Requested-With": "com.sinovatech.unicom.ui",
"Origin": "https://img.client.10010.com",
"Referer": "https://img.client.10010.com/",
"Cookie": f"ecs_token={ecs_token}",
}
params = {
"to_url": "https://contact.bol.wo.cn/market",
"reqtime": str(int(time.time() * 1000)),
"version": "android@11.0500"
}
# 强制获取响应,不自动解析
resp = self.do_send(
url, method="GET",
headers=headers,
params=params,
raw=True,
allow_redirects=False
)
if not resp:
logging.error("❌ ticket 请求失败")
return None
# 必须要带 Location 才行
loc = resp.headers.get("Location")
if not loc:
logging.error("❌ 联通拒绝跳转(无 Location")
return None
qs = parse_qs(urlparse(loc).query)
ticket = qs.get("ticket", [None])[0]
return ticket
# ======================
# 获取 userToken
# ======================
def get_userToken(self, ticket):
url = f"https://backward.bol.wo.cn/prod-api/auth/marketUnicomLogin?ticket={ticket}"
resp = self.do_send(url, method="POST", headers=ua())
return resp.get("data", {}).get("token") if resp else None
# ======================
# 获取任务列表
# ======================
def get_tasks(self, ecs_token, userToken):
url = (
"https://backward.bol.wo.cn/prod-api/promotion/activityTask/"
"getAllActivityTasks?activityId=12"
)
headers = ua()
headers["Authorization"] = f"Bearer {userToken}"
headers["Cookie"] = f"ecs_token={ecs_token}"
resp = self.do_send(url, headers=headers)
if not resp:
return []
return resp.get("data", {}).get("activityTaskUserDetailVOList", [])
# ======================
# 执行单个任务
# ======================
def run_task(self, task, userToken):
name = task.get("name", "")
param = task.get("param1")
target = int(task.get("triggerTime", 1))
done = int(task.get("triggeredTime", 0))
# 跳过购买 / 秒杀任务
if "购买" in name or "秒杀" in name:
logging.info(f"[跳过复杂任务] {name}")
return
if done >= target:
logging.info(f"任务已完成:{name}")
return
# 任务类型判断
if "浏览" in name or "查看" in name:
api = "checkView"
elif "分享" in name:
api = "checkShare"
else:
logging.info(f"无法识别任务类型:{name}")
return
url = f"https://backward.bol.wo.cn/prod-api/promotion/activityTaskShare/{api}?checkKey={param}"
headers = ua()
headers["Authorization"] = f"Bearer {userToken}"
resp = self.do_send(url, method="POST", headers=headers)
if resp and resp.get("code") == 200:
logging.info(f"任务完成:{name}")
else:
logging.error(f"任务失败:{name}")
# ======================
# 检查抽奖池是否放水
# ======================
def check_raffle(self, userToken):
url = (
"https://backward.bol.wo.cn/prod-api/promotion/home/"
"raffleActivity/prizeList?id=12"
)
headers = ua()
headers["Authorization"] = f"Bearer {userToken}"
resp = self.do_send(url, method="POST", headers=headers)
if not resp:
return False
# 判断是否有“月卡”、“月会员”等奖品
prize_list = resp.get("data", [])
has_live = any(("" in p.get("name", "")) for p in prize_list)
return has_live
# ======================
# 抽奖次数获取 + 循环抽奖
# ======================
def raffle(self, userToken):
url = (
"https://backward.bol.wo.cn/prod-api/promotion/home/"
"raffleActivity/getUserRaffleCount?id=12"
)
headers = ua()
headers["Authorization"] = f"Bearer {userToken}"
resp = self.do_send(url, method="POST", headers=headers)
if not resp:
return
count = resp.get("data", 0)
logging.info(f"当前剩余抽奖次数:{count}")
for _ in range(count):
self.raffle_once(userToken)
time.sleep(1) # 给接口缓冲时间
# ======================
# 执行一次抽奖
# ======================
def raffle_once(self, userToken):
url = (
"https://backward.bol.wo.cn/prod-api/promotion/home/"
"raffleActivity/userRaffle?id=12&channel="
)
headers = ua()
headers["Authorization"] = f"Bearer {userToken}"
resp = self.do_send(url, method="POST", headers=headers)
if not resp:
logging.error("抽奖请求失败")
return
if resp.get("code") != 200:
logging.error("抽奖失败")
return
data = resp.get("data", {})
prize = data.get("prizesName")
msg = data.get("message", "")
logging.info(f"🎁 抽奖结果:{prize or msg}")
# ======================
# 查询待领奖品
# ======================
def get_pending_prizes(self, userToken):
url = "https://backward.bol.wo.cn/prod-api/promotion/home/raffleActivity/getMyPrize"
headers = ua()
headers["Authorization"] = f"Bearer {userToken}"
data = {
"id": 12,
"type": 0,
"page": 1,
"limit": 100
}
resp = self.do_send(url, method="POST", headers=headers, data=data)
if not resp:
return []
return resp.get("data", {}).get("list", [])
# ======================
# 自动领奖
# ======================
def grant_prize(self, userToken, recordId, prizeName):
url = (
"https://backward.bol.wo.cn/prod-api/promotion/home/"
"raffleActivity/grantPrize?activityId=12"
)
headers = ua()
headers["Authorization"] = f"Bearer {userToken}"
headers["Content-Type"] = "application/json"
resp = self.do_send(url, method="POST", headers=headers, data={"recordId": recordId})
if resp and resp.get("code") == 200:
logging.info(f"🎉 奖品领取成功:{prizeName}")
else:
logging.error(f"领奖失败:{prizeName}")
# ======================
# 单账号完整流程
# ======================
def run_account(self, phone, ecs_token=None, token_online=None, appid=None):
logging.info(f"\n===== 开始处理账号:{phone} =====")
# 登录
if ecs_token:
final_token = ecs_token
else:
final_token = self.login_with_token_online(phone, token_online, appid)
if not final_token:
return
# Ticket
ticket = self.get_ticket(final_token)
if not ticket:
logging.error("❌ 获取 ticket 失败")
return
logging.info("✔ ticket 获取成功")
# userToken
userToken = self.get_userToken(ticket)
if not userToken:
logging.error("❌ 获取 userToken 失败")
return
logging.info("✔ userToken 获取成功")
# 任务列表
tasks = self.get_tasks(final_token, userToken)
for t in tasks:
self.run_task(t, userToken)
# 抽奖池检查
logging.info("检查抽奖池放水情况...")
if self.check_raffle(userToken):
logging.info("✔ 抽奖池已放水,开始抽奖")
self.raffle(userToken)
else:
logging.info("❌ 今日未放水,跳过抽奖")
# 查询待领奖品
pending = self.get_pending_prizes(userToken)
if pending:
logging.info(f"发现 {len(pending)} 个待领取奖品,开始领取...")
for item in pending:
recordId = item.get("id")
prizeName = item.get("prizesName")
self.grant_prize(userToken, recordId, prizeName)
else:
logging.info("暂无待领取奖品")
logging.info(f"===== 账号 {phone} 处理完成 =====\n")
# ======================
# 主程序入口
# ======================
def run(self):
for acc in self.accounts:
parts = acc.split("#")
phone = parts[0]
if len(parts) == 2:
self.run_account(phone, ecs_token=parts[1])
elif len(parts) >= 3:
self.run_account(phone, token_online=parts[1], appid=parts[2])
time.sleep(3)
# ======================
# 入口
# ======================
if __name__ == "__main__":
raw = os.getenv("UNICOM_ACCOUNTS", "").strip()
if not raw:
print("❌ 未设置环境变量 UNICOM_ACCOUNTS")
print("示例:")
print(" 手机号#ecs_token")
print(" 手机号#token_online#appid")
sys.exit(1)
accounts = [line for line in raw.splitlines() if line.strip()]
CUAPI(accounts).run()