942 lines
36 KiB
Python
942 lines
36 KiB
Python
# cron: 19 12,20 * * *
|
||
# const $ = new Env("顺丰2026世界杯活动");
|
||
"""
|
||
|
||
顺丰2026世界杯活动 - 射门游戏 + 比赛竞猜 + 每日礼物
|
||
Author: Auto-generated (参考端午活动脚本)
|
||
Version: 1.2.1
|
||
Date: 2026-07-07
|
||
|
||
更新日志:
|
||
1.2.1 (2026-07-07) 修复多变量解析:支持青龙同名变量换行分隔,兼容 & 分隔
|
||
1.2.0 (2026-07-06) 优化邀请逻辑:支持列表内账号互相邀请,显示已邀请好友列表
|
||
1.1.0 (2026-07-04) 新增下注结算(settleBet)阶段、赔率显示、潜在收益/今日盈亏列
|
||
1.0.0 (2026-07-03) 初版:每日礼物 + 任务(白名单) + 射门游戏 + 比赛下注
|
||
|
||
活动周期: 2026-07-02 ~ 2026-07-20
|
||
货币: GOLD_COIN (金币)
|
||
活动码: WORLD_CUP
|
||
|
||
"""
|
||
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import random
|
||
import re
|
||
import time
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional, Any
|
||
from urllib.parse import unquote
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
from threading import Lock
|
||
import requests
|
||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||
|
||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||
|
||
inviteId = [] # 运行时从账号列表动态提取
|
||
|
||
PROXY_TIMEOUT = 15
|
||
MAX_PROXY_RETRIES = 5
|
||
REQUEST_RETRY_COUNT = 3
|
||
CONCURRENT_NUM = int(os.getenv('SFBF', '1'))
|
||
if CONCURRENT_NUM > 20:
|
||
CONCURRENT_NUM = 20
|
||
elif CONCURRENT_NUM < 1:
|
||
CONCURRENT_NUM = 1
|
||
|
||
# 代理开关(默认启用)
|
||
ENABLE_PROXY = os.getenv('ENABLE_PROXY', 'true').lower() == 'true'
|
||
|
||
print_lock = Lock()
|
||
|
||
ACTIVITY_CODE = "WORLD_CUP"
|
||
TOKEN = 'wwesldfs29aniversaryvdld29'
|
||
SYS_CODE = 'MCS-MIMP-CORE'
|
||
|
||
CURRENCY_NAMES = {'GOLD_COIN': '金币'}
|
||
CHANNEL = '26sjbapp'
|
||
PLATFORM = 'SFAPP'
|
||
CITY_CODE = '021'
|
||
|
||
# ===== 活动阶段开关(True=开启 / False=关闭)=====
|
||
ENABLE_DAILY_GIFT = True # 每日礼物
|
||
ENABLE_TASKS = True # 任务(仅执行 AUTO_FINISH_TASK_CODES 白名单里的)
|
||
ENABLE_GAME = True # 射门游戏
|
||
ENABLE_BET = True # 比赛竞猜(下注)
|
||
ENABLE_SETTLE = True # 下注结算(早上跑完后,结算前一天已开赛的下注盈亏)
|
||
|
||
# ===== 下注参数 =====
|
||
# 服务端最低下注金币为 10,< 10 会报 notSuccess。修改前请先抓包确认下限。
|
||
BET_COIN = 10 # 每场比赛下注金币数
|
||
|
||
SKIP_TASK_TYPES = [
|
||
'BUY_ADD_VALUE_SERVICE_PACKET',
|
||
'SEND_INTERNATIONAL_PACKAGE',
|
||
'LOOK_BIG_PACKAGE_GET_CASH',
|
||
'SEND_SUCCESS_RECALL',
|
||
'CHARGE_NEW_EXPRESS_CARD',
|
||
'CHARGE_COLLECT_ALL',
|
||
'OPEN_FAMILY_HOME_MUTUAL',
|
||
'INVITEFRIENDS_PARTAKE_ACTIVITY',
|
||
'BROWSE_INTEGRAL_PLANET',
|
||
'BROWSE_FAMILY_HOME_MUTUAL',
|
||
]
|
||
# 浏览类任务一律跳过(含 BROWSE_/VIEWPAGE_ 前缀),不发起 finishTask
|
||
SKIP_TASK_PREFIXES = ('BROWSE_', 'VIEWPAGE_')
|
||
|
||
# 仅对这些 taskCode 直接调用 finishTask
|
||
AUTO_FINISH_TASK_CODES = {
|
||
'0CD7AFA68009402DBE5BF9D3C10D0115', # 浏览积分商城(验证通过)
|
||
'895011E183CE4E61BBACE8A63B98596A', # 去看看互寄8折权益(未验证,可移除)
|
||
}
|
||
|
||
|
||
class Logger:
|
||
def __init__(self):
|
||
self.messages: List[str] = []
|
||
self.lock = Lock()
|
||
|
||
def _log(self, icon: str, msg: str):
|
||
line = f"{icon} {msg}"
|
||
with print_lock:
|
||
print(line)
|
||
with self.lock:
|
||
self.messages.append(line)
|
||
|
||
def info(self, msg): self._log('📝', msg)
|
||
def success(self, msg): self._log('✅', msg)
|
||
def warning(self, msg): self._log('⚠️', msg)
|
||
def error(self, msg): self._log('❌', msg)
|
||
def task(self, msg): self._log('🎯', msg)
|
||
def goal(self, msg): self._log('⚽', msg)
|
||
|
||
|
||
class ProxyManager:
|
||
def __init__(self, api_url: str):
|
||
self.api_url = api_url
|
||
|
||
def get_proxy(self) -> Optional[Dict[str, str]]:
|
||
# 如果代理开关关闭,直接返回 None
|
||
if not ENABLE_PROXY:
|
||
return None
|
||
try:
|
||
if not self.api_url:
|
||
return None
|
||
response = requests.get(self.api_url, timeout=10)
|
||
if response.status_code == 200:
|
||
# 尝试解析 JSON
|
||
try:
|
||
data = response.json()
|
||
if 'data' in data and 'list' in data['data'] and data['data']['list']:
|
||
proxy_info = data['data']['list'][0]
|
||
ip = proxy_info.get('ip')
|
||
port = proxy_info.get('port')
|
||
if ip and port:
|
||
proxy = f'http://{ip}:{port}'
|
||
with print_lock:
|
||
print(f"✅ 获取代理: {proxy}")
|
||
return {'http': proxy, 'https': proxy}
|
||
except (json.JSONDecodeError, KeyError, IndexError):
|
||
# 不是 JSON,按纯文本处理(兼容旧格式)
|
||
proxy_text = response.text.strip()
|
||
if ':' in proxy_text:
|
||
proxy = proxy_text if proxy_text.startswith('http') else f'http://{proxy_text}'
|
||
display = proxy
|
||
if '@' in proxy:
|
||
parts = proxy.split('@')
|
||
display = f"http://***:***@{parts[-1]}"
|
||
with print_lock:
|
||
print(f"✅ 获取代理: {display}")
|
||
return {'http': proxy, 'https': proxy}
|
||
with print_lock:
|
||
print(f"❌ 获取代理失败: 无法解析代理数据")
|
||
return None
|
||
with print_lock:
|
||
print(f"❌ 获取代理失败: HTTP {response.status_code}")
|
||
return None
|
||
except Exception as e:
|
||
with print_lock:
|
||
print(f"❌ 获取代理异常: {str(e)[:100]}")
|
||
return None
|
||
|
||
|
||
class SFHttpClient:
|
||
def __init__(self, proxy_manager: ProxyManager):
|
||
self.proxy_manager = proxy_manager
|
||
self.session = requests.Session()
|
||
self.session.verify = False
|
||
|
||
# 根据开关决定是否尝试获取代理
|
||
if ENABLE_PROXY:
|
||
proxy = self.proxy_manager.get_proxy()
|
||
if proxy:
|
||
self.session.proxies = proxy
|
||
else:
|
||
if self.proxy_manager.api_url:
|
||
with print_lock:
|
||
print("⚠️ 代理获取失败,将不使用代理")
|
||
|
||
self.headers = {
|
||
'Host': 'mcs-mimp-web.sf-express.com',
|
||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 mediaCode=SFEXPRESSAPP-iOS-ML',
|
||
'Accept': 'application/json, text/plain, */*',
|
||
'Content-Type': 'application/json',
|
||
'channel': CHANNEL,
|
||
'platform': PLATFORM,
|
||
'accept-language': 'zh-CN,zh;q=0.9',
|
||
}
|
||
|
||
def _generate_sign(self) -> Dict[str, str]:
|
||
timestamp = str(int(round(time.time() * 1000)))
|
||
data = f'token={TOKEN}×tamp={timestamp}&sysCode={SYS_CODE}'
|
||
signature = hashlib.md5(data.encode()).hexdigest()
|
||
return {
|
||
'sysCode': SYS_CODE,
|
||
'timestamp': timestamp,
|
||
'signature': signature,
|
||
}
|
||
|
||
def request(self, url: str, data: Optional[Dict] = None, method: str = 'POST') -> Optional[Dict]:
|
||
retry_count = 0
|
||
max_proxy_retries = MAX_PROXY_RETRIES if ENABLE_PROXY else 1
|
||
proxy_retry_count = 0
|
||
|
||
while proxy_retry_count < max_proxy_retries:
|
||
sign_data = self._generate_sign()
|
||
headers = {**self.headers, **sign_data}
|
||
|
||
try:
|
||
if method == 'POST':
|
||
resp = self.session.post(url, headers=headers, json=data or {}, timeout=PROXY_TIMEOUT)
|
||
else:
|
||
resp = self.session.get(url, headers=headers, timeout=PROXY_TIMEOUT)
|
||
resp.raise_for_status()
|
||
|
||
try:
|
||
result = resp.json()
|
||
if result is None:
|
||
retry_count += 1
|
||
if retry_count < REQUEST_RETRY_COUNT:
|
||
time.sleep(2)
|
||
continue
|
||
return None
|
||
return result
|
||
except (json.JSONDecodeError, ValueError):
|
||
retry_count += 1
|
||
if retry_count < REQUEST_RETRY_COUNT:
|
||
time.sleep(2)
|
||
continue
|
||
return None
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
retry_count += 1
|
||
error_str = str(e)
|
||
|
||
if ENABLE_PROXY and ('ProxyError' in error_str or 'SSLError' in error_str or 'ConnectionError' in error_str):
|
||
proxy_retry_count += 1
|
||
if proxy_retry_count < MAX_PROXY_RETRIES:
|
||
new_proxy = self.proxy_manager.get_proxy()
|
||
if new_proxy:
|
||
self.session.proxies = new_proxy
|
||
retry_count = 0
|
||
time.sleep(2)
|
||
continue
|
||
|
||
if retry_count < REQUEST_RETRY_COUNT:
|
||
time.sleep(2)
|
||
continue
|
||
return None
|
||
|
||
except Exception:
|
||
return None
|
||
|
||
return None
|
||
|
||
def login(self, url: str) -> tuple:
|
||
try:
|
||
decoded_input = unquote(url)
|
||
if decoded_input.startswith('sessionId=') or '_login_mobile_=' in decoded_input:
|
||
cookie_dict = {}
|
||
for item in decoded_input.split(';'):
|
||
item = item.strip()
|
||
if '=' in item:
|
||
k, v = item.split('=', 1)
|
||
cookie_dict[k] = v
|
||
for k, v in cookie_dict.items():
|
||
self.session.cookies.set(k, v, domain='mcs-mimp-web.sf-express.com')
|
||
user_id = cookie_dict.get('_login_user_id_', '')
|
||
phone = cookie_dict.get('_login_mobile_', '')
|
||
return (True, user_id, phone) if phone else (False, '', '')
|
||
else:
|
||
self.session.get(unquote(url), headers=self.headers, timeout=PROXY_TIMEOUT)
|
||
cookies = self.session.cookies.get_dict()
|
||
user_id = cookies.get('_login_user_id_', '')
|
||
phone = cookies.get('_login_mobile_', '')
|
||
return (True, user_id, phone) if phone else (False, '', '')
|
||
except Exception as e:
|
||
print(f'登录异常: {str(e)}')
|
||
return False, '', ''
|
||
|
||
|
||
class WorldCupExecutor:
|
||
BASE_URL = 'https://mcs-mimp-web.sf-express.com/mcs-mimp'
|
||
|
||
def __init__(self, http: SFHttpClient, logger: Logger, user_id: str, invite_pool: list = None):
|
||
self.http = http
|
||
self.logger = logger
|
||
self.user_id = user_id
|
||
self.invite_pool = invite_pool or []
|
||
self.level_list: List[Dict] = []
|
||
|
||
def _post(self, path: str, data: Optional[Dict] = None) -> Optional[Dict]:
|
||
url = f'{self.BASE_URL}{path}'
|
||
return self.http.request(url, data=data or {})
|
||
|
||
def get_activity_index(self) -> Optional[Dict]:
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupIndexService~index')
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', {})
|
||
return None
|
||
|
||
def get_game_index(self) -> Optional[Dict]:
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupGameService~index')
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', {})
|
||
return None
|
||
|
||
def get_bet_status(self) -> Optional[Dict]:
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupMatchService~betStatus')
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', {})
|
||
return None
|
||
|
||
def get_daily_gift_status(self) -> Optional[Dict]:
|
||
data = {"cityCode": CITY_CODE}
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupDailyService~getDailyGiftStatus', data)
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', {})
|
||
return None
|
||
|
||
def receive_daily_gift(self) -> Optional[Dict]:
|
||
data = {"cityCode": CITY_CODE}
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupDailyService~receiveDailyGift', data)
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', {})
|
||
return None
|
||
|
||
def get_task_list(self) -> Optional[List[Dict]]:
|
||
data = {"activityCode": ACTIVITY_CODE, "channelType": PLATFORM}
|
||
resp = self._post('/commonPost/~memberNonactivity~activityTaskService~taskList', data)
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', [])
|
||
return None
|
||
|
||
def finish_task(self, task_code: str) -> bool:
|
||
url = f'{self.BASE_URL}/commonPost/~memberEs~taskRecord~finishTask'
|
||
data = {"taskCode": task_code}
|
||
resp = self.http.request(url, data=data)
|
||
return bool(resp and resp.get('success'))
|
||
|
||
def fetch_task_reward(self) -> Optional[Dict]:
|
||
data = {"channelType": PLATFORM, "activityCode": ACTIVITY_CODE}
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupTaskService~fetchTaskReward', data)
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', {})
|
||
return None
|
||
|
||
def report_pass(self, level: int, shot_num: int) -> Optional[Dict]:
|
||
data = {"level": level, "shotNum": shot_num}
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupGameService~passReport', data)
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', {})
|
||
err = resp.get('errorMessage', '未知错误') if resp else '请求失败'
|
||
self.logger.warning(f'第{level}关上报失败: {err}')
|
||
return None
|
||
|
||
def place_bet(self, match_id: str, bet_result: int, bet_coin: int) -> bool:
|
||
"""下注
|
||
bet_result: 0=主胜 1=平 2=客胜 (与 odds 数组顺序一致)
|
||
bet_coin: 下注金币数
|
||
"""
|
||
data = {"matchId": match_id, "betResult": bet_result, "betCoin": bet_coin}
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupMatchService~placeBet', data)
|
||
if resp and resp.get('success'):
|
||
return True
|
||
err = resp.get('errorMessage', '未知错误') if resp else '请求失败'
|
||
self.logger.warning(f'下注失败(match={match_id}, result={bet_result}): {err}')
|
||
return False
|
||
|
||
def settle_bet(self) -> Optional[list]:
|
||
"""结算下注
|
||
返回已开赛并结算的下注列表;空数组表示今天没有可结算的
|
||
"""
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupMatchService~settleBet', {})
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', []) or []
|
||
err = resp.get('errorMessage', '未知错误') if resp else '请求失败'
|
||
self.logger.warning(f'结算请求失败: {err}')
|
||
return None
|
||
|
||
# ========== 邀请相关方法 ==========
|
||
def do_invite(self):
|
||
"""列表内账号互相邀请"""
|
||
try:
|
||
pool = self.invite_pool if self.invite_pool else inviteId
|
||
available_invites = [inv for inv in pool if inv != self.user_id]
|
||
if not available_invites:
|
||
self.logger.warning("没有可用的邀请对象(排除自身后为空),跳过邀请")
|
||
return
|
||
random_invite = random.choice(available_invites)
|
||
url = f'{self.BASE_URL}/commonPost/~memberNonactivity~worldCupIndexService~index'
|
||
data = {"inviteType": 1, "inviteUserId": random_invite}
|
||
resp = self.http.request(url, data=data)
|
||
if resp and resp.get('success'):
|
||
self.logger.success(f"已向邀请对象发起邀请 (userId: {random_invite})")
|
||
else:
|
||
err = resp.get('errorMessage', '未知错误') if resp else '请求失败'
|
||
self.logger.warning(f"邀请失败: {err}")
|
||
except Exception as e:
|
||
self.logger.error(f"邀请初始化异常: {str(e)}")
|
||
|
||
def get_invite_list(self) -> Optional[List[Dict]]:
|
||
"""查询已邀请好友列表"""
|
||
resp = self._post('/commonPost/~memberNonactivity~worldCupTaskService~taskInviteList', {})
|
||
if resp and resp.get('success'):
|
||
return resp.get('obj', [])
|
||
err = resp.get('errorMessage', '未知错误') if resp else '请求失败'
|
||
self.logger.warning(f"查询邀请列表失败: {err}")
|
||
return None
|
||
|
||
def do_show_invite_list(self) -> None:
|
||
"""查询并显示已邀请好友列表"""
|
||
invite_list = self.get_invite_list()
|
||
if invite_list:
|
||
self.logger.success(f"已邀请 {len(invite_list)} 位好友:")
|
||
for i, friend in enumerate(invite_list, 1):
|
||
mobile = friend.get('mobile', '未知')
|
||
invite_date = friend.get('inviteDate', '未知')
|
||
if len(mobile) >= 7:
|
||
mobile_masked = mobile[:3] + "****" + mobile[7:]
|
||
else:
|
||
mobile_masked = mobile
|
||
self.logger.info(f" {i}. {mobile_masked} (邀请时间: {invite_date})")
|
||
else:
|
||
self.logger.info("暂无已邀请好友")
|
||
# =================================
|
||
|
||
def do_daily_gift(self, result: Dict) -> None:
|
||
self.logger.task('[每日礼物] 检查状态...')
|
||
status = self.get_daily_gift_status()
|
||
if status is None:
|
||
self.logger.warning('[每日礼物] 获取状态失败')
|
||
return
|
||
if status.get('received'):
|
||
self.logger.success('[每日礼物] 今日已领取')
|
||
return
|
||
if not status.get('canReceive'):
|
||
self.logger.info('[每日礼物] 今日不可领取')
|
||
return
|
||
self.logger.task('[每日礼物] 尝试领取...')
|
||
if self.receive_daily_gift():
|
||
self.logger.success('[每日礼物] 领取成功')
|
||
result['daily_gift'] = True
|
||
else:
|
||
self.logger.warning('[每日礼物] 领取失败')
|
||
|
||
def do_tasks(self, result: Dict) -> None:
|
||
self.logger.info('正在获取世界杯活动任务列表...')
|
||
tasks = self.get_task_list()
|
||
if tasks is None:
|
||
return
|
||
self.logger.info(f'共发现 {len(tasks)} 个任务')
|
||
|
||
for task in tasks:
|
||
task_name = task.get('taskName', '未知')
|
||
task_type = task.get('taskType', '')
|
||
task_code = task.get('taskCode', '')
|
||
status = task.get('status')
|
||
rest_finish = task.get('restFinishTime', 0)
|
||
can_receive = task.get('canReceiveTokenNum', 0)
|
||
|
||
if task_code not in AUTO_FINISH_TASK_CODES:
|
||
continue
|
||
|
||
self.logger.info(f'[{task_name}]')
|
||
|
||
if status == 3 or (status == 1 and rest_finish <= 0):
|
||
self.logger.success(f'[{task_name}] 已完成')
|
||
continue
|
||
|
||
if self.finish_task(task_code):
|
||
self.logger.success(f'[{task_name}] 完成成功')
|
||
result['tasks_completed'] += 1
|
||
else:
|
||
self.logger.warning(f'[{task_name}] 完成失败')
|
||
time.sleep(1)
|
||
|
||
def do_fetch_rewards(self, result: Dict) -> None:
|
||
self.logger.info('领取任务奖励...')
|
||
reward_resp = self.fetch_task_reward()
|
||
if reward_resp:
|
||
received = reward_resp.get('receivedAccountList', [])
|
||
if received:
|
||
for item in received:
|
||
currency = item.get('currency', '')
|
||
amount = item.get('amount', 0)
|
||
self.logger.success(f'领取: {currency} x{amount}')
|
||
else:
|
||
self.logger.info('无新奖励可领取')
|
||
|
||
def play_game(self, result: Dict) -> None:
|
||
self.logger.info('⚽ 开始挑战射门游戏...')
|
||
game_info = self.get_game_index()
|
||
if game_info is None:
|
||
self.logger.warning('获取游戏配置失败')
|
||
return
|
||
|
||
self.level_list = game_info.get('levelList', [])
|
||
cur_stage = game_info.get('curStage', 1)
|
||
cur_level = game_info.get('curLevel', 1)
|
||
|
||
if not self.level_list:
|
||
self.logger.warning('游戏关卡列表为空')
|
||
return
|
||
|
||
self.logger.info(f'当前进度: 第{cur_stage}阶段 第{cur_level}关')
|
||
|
||
for level_cfg in self.level_list:
|
||
level_no = level_cfg.get('level')
|
||
target = level_cfg.get('target', 0)
|
||
reward = level_cfg.get('rewardCoins', 0)
|
||
|
||
if level_no < cur_level:
|
||
continue
|
||
|
||
self.logger.task(f'挑战第{level_no}关(目标 {target} 次进球,奖励 {reward} 金币)')
|
||
|
||
pass_result = self.report_pass(level_no, target)
|
||
|
||
attempts = 0
|
||
while attempts < 3 and (pass_result is None or not pass_result.get('coinNum', 0) >= reward):
|
||
time.sleep(1)
|
||
pass_result = self.report_pass(level_no, target)
|
||
attempts += 1
|
||
|
||
if pass_result:
|
||
result['stages_passed'].append({
|
||
'level': level_no,
|
||
'coins': pass_result.get('coinNum', 0),
|
||
'reward': reward,
|
||
})
|
||
result['game_coins'] += pass_result.get('coinNum', 0)
|
||
self.logger.goal(f'第{level_no}关完成,获得 {pass_result.get("coinNum", 0)} 金币')
|
||
else:
|
||
self.logger.warning(f'第{level_no}关未通过')
|
||
|
||
def do_bet(self, result: Dict) -> None:
|
||
self.logger.info('⚽ 查询比赛竞猜状态...')
|
||
bet_status = self.get_bet_status()
|
||
if bet_status is None:
|
||
return
|
||
|
||
account = bet_status.get('currentAccount', {})
|
||
balance = account.get('balance', 0)
|
||
self.logger.info(f'当前金币余额: {balance}')
|
||
|
||
matches = bet_status.get('matchList', [])
|
||
pending = [
|
||
m for m in matches
|
||
if m.get('matchStatus') == 'Fixture' and m.get('recordStatus', 0) == 0
|
||
]
|
||
self.logger.info(f'共 {len(matches)} 场比赛,{len(pending)} 场可下注')
|
||
self.logger.info(f'单场下注: {BET_COIN} 金币')
|
||
|
||
if balance < BET_COIN:
|
||
self.logger.warning(f'余额不足 {BET_COIN} 金币,跳过下注')
|
||
result['final_balance'] = balance
|
||
result['final_total'] = account.get('totalAmount', 0)
|
||
return
|
||
|
||
choice_map = {0: '主胜', 1: '平', 2: '客胜'}
|
||
|
||
for m in pending:
|
||
match_id = m.get('matchId')
|
||
team_a = m.get('teamAName', '?')
|
||
team_b = m.get('teamBName', '?')
|
||
odds_str = m.get('odds', '?,?,?')
|
||
bet_result = random.randint(0, 2)
|
||
|
||
try:
|
||
odds_list = [float(x) for x in odds_str.split(',')]
|
||
picked_odds = odds_list[bet_result] if bet_result < len(odds_list) else 0.0
|
||
except (ValueError, IndexError):
|
||
picked_odds = 0.0
|
||
|
||
potential_payout = round(BET_COIN * picked_odds, 2)
|
||
potential_profit = round(potential_payout - BET_COIN, 2)
|
||
|
||
self.logger.task(
|
||
f'下注: {team_a} vs {team_b}(赔率 {odds_str})→ '
|
||
f'{choice_map[bet_result]} {BET_COIN} 金币 '
|
||
f'| 命中得 {potential_payout}(净赚 {potential_profit})'
|
||
)
|
||
|
||
time.sleep(1)
|
||
if self.place_bet(match_id, bet_result=bet_result, bet_coin=BET_COIN):
|
||
self.logger.success(
|
||
f'下注成功: {team_a} vs {team_b}(赔率 {picked_odds},命中得 {potential_payout})'
|
||
)
|
||
result['bets_placed'] += 1
|
||
result['bet_details'].append({
|
||
'match': f'{team_a} vs {team_b}',
|
||
'choice': choice_map[bet_result],
|
||
'odds': picked_odds,
|
||
'bet_coin': BET_COIN,
|
||
'potential_payout': potential_payout,
|
||
'potential_profit': potential_profit,
|
||
})
|
||
else:
|
||
self.logger.warning(f'下注失败: {team_a} vs {team_b}')
|
||
|
||
status_after = self.get_bet_status()
|
||
if status_after:
|
||
acc_after = status_after.get('currentAccount', {})
|
||
result['final_balance'] = acc_after.get('balance', 0)
|
||
result['final_total'] = acc_after.get('totalAmount', 0)
|
||
else:
|
||
result['final_balance'] = balance
|
||
result['final_total'] = account.get('totalAmount', 0)
|
||
|
||
def do_settle(self, result: Dict) -> None:
|
||
"""结算已开赛下注的实际盈亏"""
|
||
self.logger.info('💰 结算昨日下注结果...')
|
||
settled = self.settle_bet()
|
||
if settled is None:
|
||
self.logger.warning('结算接口失败,跳过')
|
||
return
|
||
|
||
if not settled:
|
||
self.logger.info('今日无新结算记录')
|
||
return
|
||
|
||
choice_map = {0: '主胜', 1: '平', 2: '客胜'}
|
||
|
||
total_pnl = 0
|
||
wins = 0
|
||
losses = 0
|
||
|
||
for m in settled:
|
||
match_id = m.get('matchId', '?')
|
||
team_a = m.get('teamAName', '?')
|
||
team_b = m.get('teamBName', '?')
|
||
score = f"{m.get('teamAScore', '?')}:{m.get('teamBScore', '?')}"
|
||
bet_coin = m.get('betCoin', 0)
|
||
bet_result = m.get('betResult', -1)
|
||
bet_odds_str = m.get('betOdds', '?,?,?')
|
||
win_coin = m.get('betWinCoin', 0)
|
||
|
||
pnl = win_coin - bet_coin
|
||
total_pnl += pnl
|
||
|
||
try:
|
||
odds_list = [float(x) for x in bet_odds_str.split(',')]
|
||
picked_odds = odds_list[bet_result] if 0 <= bet_result < len(odds_list) else 0.0
|
||
except (ValueError, IndexError):
|
||
picked_odds = 0.0
|
||
|
||
if pnl > 0:
|
||
wins += 1
|
||
icon = '🎉'
|
||
pnl_str = f'+{pnl}'
|
||
elif pnl < 0:
|
||
losses += 1
|
||
icon = '💔'
|
||
pnl_str = f'{pnl}'
|
||
else:
|
||
icon = '➖'
|
||
pnl_str = '0'
|
||
|
||
self.logger.info(
|
||
f' {icon} {team_a} vs {team_b}({score})→ '
|
||
f'押{choice_map.get(bet_result, "?")} 赔率 {picked_odds} | '
|
||
f'投 {bet_coin} 得 {win_coin} | {pnl_str}'
|
||
)
|
||
|
||
result['settle_details'].append({
|
||
'match': f'{team_a} vs {team_b}',
|
||
'score': score,
|
||
'choice': choice_map.get(bet_result, '?'),
|
||
'bet_coin': bet_coin,
|
||
'win_coin': win_coin,
|
||
'pnl': pnl,
|
||
})
|
||
|
||
result['settle_pnl'] = total_pnl
|
||
result['settle_wins'] = wins
|
||
result['settle_losses'] = losses
|
||
|
||
if total_pnl > 0:
|
||
self.logger.success(f'结算完成: {wins}胜{losses}负,净赚 +{total_pnl} 金币')
|
||
elif total_pnl < 0:
|
||
self.logger.warning(f'结算完成: {wins}胜{losses}负,净亏 {total_pnl} 金币')
|
||
else:
|
||
self.logger.info(f'结算完成: {wins}胜{losses}负,持平')
|
||
|
||
status_after = self.get_bet_status()
|
||
if status_after:
|
||
acc_after = status_after.get('currentAccount', {})
|
||
result['final_balance'] = acc_after.get('balance', 0)
|
||
result['final_total'] = acc_after.get('totalAmount', 0)
|
||
|
||
def show_status(self) -> None:
|
||
self.logger.info('=' * 20 + ' 世界杯活动状态 ' + '=' * 20)
|
||
bet_status = self.get_bet_status()
|
||
if bet_status:
|
||
account = bet_status.get('currentAccount', {})
|
||
self.logger.info(f'金币余额: {account.get("balance", 0)} / 累计: {account.get("totalAmount", 0)}')
|
||
|
||
daily = self.get_daily_gift_status()
|
||
if daily:
|
||
status_str = '已领取' if daily.get('received') else ('可领取' if daily.get('canReceive') else '不可领取')
|
||
self.logger.info(f'每日礼物: {status_str}')
|
||
|
||
game = self.get_game_index()
|
||
if game:
|
||
self.logger.info(f'游戏阶段: 第{game.get("curStage", 1)}阶段 第{game.get("curLevel", 1)}关')
|
||
|
||
index = self.get_activity_index()
|
||
if index:
|
||
self.logger.info(f'活动周期: {index.get("acStartTime", "")} ~ {index.get("acEndTime", "")}')
|
||
self.logger.info(f'可领金币: {index.get("availableRewardCoins", 0)}')
|
||
|
||
self.logger.info('=' * 56)
|
||
|
||
def run(self) -> Dict[str, Any]:
|
||
result = {
|
||
'tasks_completed': 0,
|
||
'game_coins': 0,
|
||
'stages_passed': [],
|
||
'daily_gift': False,
|
||
'bets_placed': 0,
|
||
'bet_details': [],
|
||
'settle_pnl': 0,
|
||
'settle_wins': 0,
|
||
'settle_losses': 0,
|
||
'settle_details': [],
|
||
'final_balance': 0,
|
||
'final_total': 0,
|
||
}
|
||
|
||
# 1. 邀请好友(先发起邀请)
|
||
self.do_invite()
|
||
|
||
# 2. 查询并显示已邀请好友列表
|
||
self.do_show_invite_list()
|
||
|
||
# 3. 获取活动首页信息
|
||
index_info = self.get_activity_index()
|
||
if index_info:
|
||
self.logger.success(f'已加入世界杯活动,可领 {index_info.get("availableRewardCoins", 0)} 金币')
|
||
|
||
if ENABLE_DAILY_GIFT:
|
||
self.do_daily_gift(result)
|
||
if ENABLE_TASKS:
|
||
self.do_tasks(result)
|
||
self.do_fetch_rewards(result)
|
||
if ENABLE_GAME:
|
||
self.play_game(result)
|
||
if ENABLE_BET:
|
||
self.do_bet(result)
|
||
else:
|
||
self.logger.info('⚽ [开关] 比赛下注已关闭,跳过')
|
||
if ENABLE_SETTLE:
|
||
self.do_settle(result)
|
||
else:
|
||
self.logger.info('💰 [开关] 下注结算已关闭,跳过')
|
||
|
||
return result
|
||
|
||
|
||
def run_account(account_url: str, index: int, invite_pool: List[str]) -> Dict[str, Any]:
|
||
logger = Logger()
|
||
proxy_url = os.getenv('SF_PROXY_API_URL', '')
|
||
proxy_manager = ProxyManager(proxy_url)
|
||
|
||
http = SFHttpClient(proxy_manager)
|
||
retry_count = 0
|
||
login_success = False
|
||
phone = ''
|
||
user_id = ''
|
||
|
||
while retry_count < MAX_PROXY_RETRIES and not login_success:
|
||
try:
|
||
if retry_count > 0:
|
||
http = SFHttpClient(proxy_manager)
|
||
success, user_id, phone = http.login(account_url)
|
||
if success:
|
||
login_success = True
|
||
break
|
||
except Exception:
|
||
pass
|
||
retry_count += 1
|
||
if retry_count < MAX_PROXY_RETRIES:
|
||
time.sleep(2)
|
||
|
||
if not login_success:
|
||
logger.error(f'账号{index + 1} 登录失败')
|
||
return {
|
||
'success': False, 'phone': '', 'index': index,
|
||
'tasks_completed': 0, 'game_coins': 0, 'stages_passed': [],
|
||
'daily_gift': False, 'bets_placed': 0, 'bet_details': [],
|
||
'settle_pnl': 0, 'settle_wins': 0, 'settle_losses': 0, 'settle_details': [],
|
||
'final_balance': 0, 'final_total': 0,
|
||
}
|
||
|
||
masked_phone = phone[:3] + "****" + phone[7:] if len(phone) >= 7 else phone
|
||
logger.success(f'账号{index + 1}: 【{masked_phone}】登录成功')
|
||
|
||
time.sleep(random.uniform(1, 3))
|
||
|
||
executor = WorldCupExecutor(http, logger, user_id, invite_pool)
|
||
activity_result = executor.run()
|
||
|
||
return {
|
||
'success': True,
|
||
'phone': phone,
|
||
'index': index,
|
||
**activity_result,
|
||
}
|
||
|
||
|
||
# ==================== 主程序(重写解析逻辑,支持同名多变量) ====================
|
||
def main():
|
||
# 第一步:收集所有可能包含账号信息的环境变量
|
||
raw_values = []
|
||
for key, value in os.environ.items():
|
||
if key == 'sfsyUrl' or key.startswith('sfsyUrl_'):
|
||
raw_values.append(value)
|
||
|
||
if not raw_values:
|
||
print("❌ 未找到任何 sfsyUrl / sfsyUrl_* 环境变量,请检查青龙面板变量配置")
|
||
return
|
||
|
||
# 第二步:合并所有值,按 & 和 \n 分割
|
||
combined = '&'.join(raw_values)
|
||
parts = []
|
||
for p in combined.split('&'):
|
||
if '\n' in p:
|
||
parts.extend(p.split('\n'))
|
||
else:
|
||
if p.strip():
|
||
parts.append(p.strip())
|
||
|
||
# 第三步:提取以 sessionId= 开头的有效账号字符串
|
||
account_urls = []
|
||
for p in parts:
|
||
p = p.strip()
|
||
if not p:
|
||
continue
|
||
if p.startswith('sessionId='):
|
||
account_urls.append(p)
|
||
elif 'sessionId=' in p:
|
||
# 从第一个 sessionId= 开始截取
|
||
idx = p.find('sessionId=')
|
||
account_urls.append(p[idx:])
|
||
|
||
# 第四步:去重(保留首次出现顺序)
|
||
seen = set()
|
||
unique_urls = []
|
||
for u in account_urls:
|
||
if u not in seen:
|
||
seen.add(u)
|
||
unique_urls.append(u)
|
||
account_urls = unique_urls
|
||
|
||
if not account_urls:
|
||
print("❌ 未能解析出有效的账号 Cookie,请检查格式(应以 sessionId= 开头)")
|
||
print(f" 原始输入片段: {combined[:200]}...")
|
||
return
|
||
|
||
# 第五步:提取邀请池(所有 user_id)
|
||
invite_pool = []
|
||
for url in account_urls:
|
||
m = re.search(r'_login_user_id_=([A-Fa-f0-9]+)', url)
|
||
if m:
|
||
invite_pool.append(m.group(1))
|
||
|
||
# 设置全局邀请池
|
||
global inviteId
|
||
inviteId = invite_pool
|
||
|
||
# 打乱顺序,防止固定顺序触发风控
|
||
random.shuffle(account_urls)
|
||
|
||
print("=" * 60)
|
||
print(f"⚽ 顺丰2026世界杯活动 - 射门挑战 + 比赛竞猜")
|
||
print(f"👨💻 Author: Auto-generated")
|
||
print(f"📱 共获取到 {len(account_urls)} 个账号")
|
||
print(f"🤝 邀请池: {len(invite_pool)} 个账号")
|
||
print(f"⚙️ 并发数量: {CONCURRENT_NUM}")
|
||
print(f"🎯 阶段开关: 礼物={ENABLE_DAILY_GIFT} 任务={ENABLE_TASKS} 游戏={ENABLE_GAME} 下注={ENABLE_BET} 结算={ENABLE_SETTLE}")
|
||
print(f"💰 单场下注: {BET_COIN} 金币(最低 10)")
|
||
print(f"⏰ 执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
print("=" * 60)
|
||
|
||
all_results = []
|
||
|
||
if CONCURRENT_NUM <= 1:
|
||
for idx, url in enumerate(account_urls):
|
||
result = run_account(url, idx, invite_pool)
|
||
all_results.append(result)
|
||
if idx < len(account_urls) - 1:
|
||
print("-" * 60)
|
||
time.sleep(2)
|
||
else:
|
||
with ThreadPoolExecutor(max_workers=CONCURRENT_NUM) as pool:
|
||
futures = {pool.submit(run_account, url, idx, invite_pool): idx for idx, url in enumerate(account_urls)}
|
||
for future in as_completed(futures):
|
||
all_results.append(future.result())
|
||
|
||
all_results.sort(key=lambda x: x['index'])
|
||
|
||
print(f"\n" + "=" * 90)
|
||
print(f"📊 世界杯活动汇总")
|
||
print("=" * 90)
|
||
print(f"{'序号':<6} {'手机号':<15} {'金币余额':<10} {'通关数':<6} {'下注数':<6} {'潜在收益':<10} {'今日盈亏':<10} {'每日礼物':<8}")
|
||
print("-" * 100)
|
||
|
||
total_balance = 0
|
||
total_payout = 0.0
|
||
total_pnl = 0
|
||
|
||
for r in all_results:
|
||
idx = r['index'] + 1
|
||
phone = r['phone'][:3] + "****" + r['phone'][7:] if r.get('phone') and len(r['phone']) >= 7 else r.get('phone', '未登录')
|
||
daily = '✅' if r.get('daily_gift') else '❌'
|
||
balance = r.get('final_balance', 0)
|
||
stages = len(r.get('stages_passed', []))
|
||
bets = r.get('bets_placed', 0)
|
||
payout = sum(d.get('potential_payout', 0) for d in r.get('bet_details', []))
|
||
pnl = r.get('settle_pnl', 0)
|
||
wins = r.get('settle_wins', 0)
|
||
losses = r.get('settle_losses', 0)
|
||
if wins + losses > 0:
|
||
pnl_str = f'{pnl:+d} ({wins}胜{losses}负)'
|
||
else:
|
||
pnl_str = '-'
|
||
total_payout += payout
|
||
total_pnl += pnl
|
||
total_balance += balance
|
||
|
||
print(f"{idx:<6} {phone:<15} {balance:<10} {stages:<6} {bets:<6} {payout:<10} {pnl_str:<10} {daily:<8}")
|
||
|
||
print("-" * 100)
|
||
print(f"{'汇总':<6} {'账号: ' + str(len(all_results)):<15} 总金币余额: {total_balance} | 累计盈亏: {total_pnl:+d}")
|
||
print("=" * 90)
|
||
print("\n🎊 所有账号执行完成!")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main() |