更新 Points_Based/D_SFSY_SJBHD.py

This commit is contained in:
2026-07-05 16:53:02 +00:00
parent 6b5e264b07
commit 107540a9fe
2 changed files with 816 additions and 856 deletions
-856
View File
@@ -1,856 +0,0 @@
# cron: 19 12,20 * * *
# const $ = new Env("顺丰33周年庆活动");
"""
顺丰33周年活动 - 完成任务抽勋章
Author: 爱学习的呆子
Version: 1.1.0
Date: 2026-03-19
顺丰暗号环境变量:sfsyah
"""
import hashlib
import json
import os
import random
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
# 禁用SSL警告
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
inviteId = ['C6E5C3BDD7624520AB869D2AF9E75D95']
# ==================== 配置常量 ====================
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
print_lock = Lock()
# 周年活动配置
ACTIVITY_CODE = "ANNIVERSARY_2026"
TOKEN = 'wwesldfs29aniversaryvdld29'
SYS_CODE = 'MCS-MIMP-CORE'
# 暗号环境变量(可选,不设置则自动从API获取答案)
GUESS_ANSWER_ENV = 'sfsyah'
# 需要跳过的任务类型(需要实际操作的)
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', # 充值1000元一键集齐
'OPEN_FAMILY_HOME_MUTUAL', # 开通家庭8折互寄权益
'BUY_ANNIVERSARY_LIMITED_PACKET', # 33周年宠粉券包
# 'INTEGRAL_EXCHANGE', # 积分兑换抽勋章次数(已特殊处理)
]
# ==================== 日志 ====================
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 medal(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]]:
try:
if not self.api_url:
return None
response = requests.get(self.api_url, timeout=10)
if response.status_code == 200:
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"❌ 获取代理失败: HTTP {response.status_code}")
return None
except Exception as e:
with print_lock:
print(f"❌ 获取代理异常: {str(e)[:100]}")
return None
# ==================== HTTP客户端 ====================
class SFHttpClient:
def __init__(self, proxy_manager: ProxyManager):
self.proxy_manager = proxy_manager
self.session = requests.Session()
self.session.verify = False
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 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf254173b) XWEB/19027',
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
'channel': 'xcxpart',
'platform': 'MINI_PROGRAM',
'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}&timestamp={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
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 '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:
"""登录(兼容URL和CK格式)"""
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 AnniversaryExecutor:
# MODIFIED: 增加 user_id 参数
def __init__(self, http: SFHttpClient, logger: Logger, user_id: str):
self.http = http
self.logger = logger
self.user_id = user_id
# ---------- API 方法 ----------
def get_activity_index(self) -> Optional[Dict]:
"""获取周年活动首页信息"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026IndexService~index'
resp = self.http.request(url, data={})
if resp and resp.get('success'):
return resp.get('obj', {})
return None
def get_task_list(self) -> Optional[List[Dict]]:
"""获取周年活动任务列表"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~activityTaskService~taskList'
data = {
"activityCode": ACTIVITY_CODE,
"channelType": "MINI_PROGRAM"
}
resp = self.http.request(url, data=data)
if resp and resp.get('success'):
return resp.get('obj', [])
else:
error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败'
self.logger.error(f'获取任务列表失败: {error_msg}')
return None
def finish_task(self, task_code: str) -> bool:
"""完成任务(浏览类)"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonRoutePost/memberEs/taskRecord/finishTask'
data = {"taskCode": task_code}
resp = self.http.request(url, data=data)
if resp and resp.get('success'):
return True
return False
# ========== 新增:专用接口-领取寄件会员权益 ==========
def receive_vip_benefit(self) -> bool:
"""领取寄件会员权益(特殊任务)"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberManage~memberEquity~commonEquityReceive'
data = {"key": "surprise_benefit"}
resp = self.http.request(url, data=data)
if resp and resp.get('success'):
self.logger.success(f'[领取寄件会员权益] 完成成功')
# 可选:解析返回的奖励信息
if resp.get('obj'):
self.logger.info(f'领取详情: {resp["obj"]}')
return True
else:
error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败'
self.logger.warning(f'[领取寄件会员权益] 完成失败: {error_msg}')
return False
# =================================================
def fetch_tasks_reward(self) -> Optional[Dict]:
"""领取已完成任务的奖励(获得抽勋章次数)"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026TaskService~fetchTasksReward'
data = {
"channelType": "MINI_PROGRAM",
"activityCode": ACTIVITY_CODE
}
resp = self.http.request(url, data=data)
if resp and resp.get('success'):
return resp.get('obj', {})
else:
error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败'
self.logger.warning(f'领取任务奖励失败: {error_msg}')
return None
def get_card_status(self) -> Optional[Dict]:
"""查看勋章收集状态"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026CardService~cardStatus'
resp = self.http.request(url, data={})
if resp and resp.get('success'):
return resp.get('obj', {})
return None
def claim_medal(self, batch: bool = False) -> Optional[Dict]:
"""抽勋章"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026CardService~claim'
data = {"batchClaim": batch}
resp = self.http.request(url, data=data)
if resp and resp.get('success'):
return resp.get('obj', {})
else:
error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败'
self.logger.error(f'抽勋章失败: {error_msg}')
return None
def give_countdown_chance(self) -> Optional[Dict]:
"""领取倒计时奖励(每日一次免费抽勋章机会)"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026CardService~giveClaimChance'
resp = self.http.request(url, data={})
if resp and resp.get('success'):
return resp.get('obj', {})
else:
error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败'
self.logger.warning(f'领取倒计时奖励失败: {error_msg}')
return None
def get_user_rest_integral(self) -> int:
"""查询用户剩余积分"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~activityTaskService~getUserRestIntegral'
resp = self.http.request(url, data={})
if resp and resp.get('success'):
points = resp.get('obj', 0)
self.logger.info(f'当前可用积分: {points}')
return points
return 0
def exchange_points_for_chance(self) -> bool:
"""积分兑换抽勋章次数(10积分兑1次)"""
points = self.get_user_rest_integral()
if points < 10:
self.logger.warning(f'积分不足10,无法兑换(当前: {points}')
return False
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026TaskService~integralExchange'
data = {
"exchangeNum": 1, # 一次兑换1次机会
"activityCode": ACTIVITY_CODE # 当前活动码
}
resp = self.http.request(url, data=data)
if resp and resp.get('success'):
self.logger.success('积分兑换成功,获得1次抽勋章次数')
return True
else:
error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败'
self.logger.warning(f'积分兑换失败: {error_msg}')
return False
def get_guess_title_list(self) -> Optional[Dict]:
"""获取对暗号题目列表(含答案)"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026GuessService~titleList'
resp = self.http.request(url, data={})
if resp and resp.get('success'):
return resp.get('obj', {})
return None
def submit_guess_answer(self, answer: str, period: str = '') -> Optional[Dict]:
"""提交暗号答案,返回完整响应"""
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026GuessService~answer'
data = {"answerInfo": answer}
if period:
data["period"] = period
resp = self.http.request(url, data=data)
if resp:
self.logger.info(f'[对暗号] 提交响应: {json.dumps(resp, ensure_ascii=False)[:300]}')
if resp.get('success'):
obj = resp.get('obj', {})
# 检查obj中是否有失败标志
if isinstance(obj, dict) and obj.get('answerStatus') == 0:
self.logger.warning(f'[对暗号] 答案错误')
return None
return obj
else:
error_msg = resp.get('errorMessage', '未知错误')
self.logger.warning(f'提交暗号失败: {error_msg}')
return None
else:
self.logger.warning(f'提交暗号失败: 请求失败')
return None
# ---------- 新增:邀请初始化 ----------
def do_invite(self):
"""发送邀请初始化请求(借鉴自第二段代码)"""
try:
# 从全局 inviteId 列表中排除自己的 user_id
available_invites = [inv for inv in inviteId if inv != self.user_id]
if not available_invites:
self.logger.warning("没有可用的邀请码(排除自身后为空),跳过邀请")
return
random_invite = random.choice(available_invites)
# self.logger.info(f"随机选择邀请码: {random_invite} (排除自身 {self.user_id})")
# 构造邀请请求 URL,使用 commonNoLoginPost
url = "https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026IndexService~index"
data = {"inviteType": 1, "inviteUserId": random_invite}
resp = self.http.request(url, data=data)
# if resp and resp.get('success'):
# self.logger.success(f"邀请初始化成功,邀请码: {random_invite}")
# else:
# error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败'
# self.logger.warning(f"邀请初始化失败: {error_msg}")
except Exception as e:
self.logger.error(f"邀请初始化异常: {str(e)}")
# ---------- 业务逻辑 ----------
def do_guess_game(self) -> bool:
"""对暗号任务:读取所有日期的题目,按顺序匹配环境变量中的暗号逐日提交"""
self.logger.task('[对暗号赢免单] 开始...')
# 1. 获取题目列表
guess_info = self.get_guess_title_list()
if not guess_info:
self.logger.warning('[对暗号] 获取题目列表失败')
return False
current_period = guess_info.get('currentPeriod', '')
title_list = guess_info.get('guessTitleInfoList', [])
if not title_list:
self.logger.warning('[对暗号] 题目列表为空')
return False
# 按日期排序
title_list.sort(key=lambda x: x.get('period', ''))
self.logger.info(f'[对暗号] 共 {len(title_list)} 天题目,当前日期: {current_period}')
# 2. 读取环境变量中的暗号列表(每行一个,按日期顺序对应)
env_answers_raw = os.getenv(GUESS_ANSWER_ENV, '').strip()
env_answers = [a.strip() for a in env_answers_raw.split('\n') if a.strip()] if env_answers_raw else []
if env_answers:
self.logger.info(f'[对暗号] 环境变量已设置 {len(env_answers)} 个暗号')
# 3. 逐日处理
any_success = False
for i, title in enumerate(title_list):
period = title.get('period', '')
answer_status = title.get('answerStatus')
tip = title.get('tip', '')
# 已答过的跳过
if answer_status == 1:
self.logger.success(f'[对暗号] {period} 已作答: {title.get("answerInfo", "")}')
any_success = True
continue
# 超过当前日期的不处理(未来题目)
if period > current_period:
self.logger.info(f'[对暗号] {period} 尚未开放,跳过')
continue
# 获取答案:优先环境变量按顺序匹配,其次API返回的answerInfo
answer = ''
if i < len(env_answers):
answer = env_answers[i]
if not answer:
answer = title.get('answerInfo', '')
if not answer:
self.logger.warning(f'[对暗号] {period} 无法获取答案(提示: {tip}')
self.logger.info(f'[对暗号] 请在环境变量 {GUESS_ANSWER_ENV}{i + 1} 行填写该日暗号')
continue
self.logger.info(f'[对暗号] {period} 提交答案: {answer}')
# 提交答案
result = self.submit_guess_answer(answer, period)
if result is not None:
# 提交后重新获取题目列表验证是否真的成功
time.sleep(1)
verify_info = self.get_guess_title_list()
if verify_info:
for t in verify_info.get('guessTitleInfoList', []):
if t.get('period') == period:
if t.get('answerStatus') == 1:
self.logger.success(f'[对暗号] {period} 验证通过,答案已录入: {t.get("answerInfo", "")}')
any_success = True
else:
self.logger.warning(f'[对暗号] {period} 验证失败,答案未被采纳 (answerStatus={t.get("answerStatus")})')
break
else:
self.logger.success(f'[对暗号] {period} 提交成功(未能验证)')
any_success = True
else:
self.logger.warning(f'[对暗号] {period} 提交失败')
time.sleep(1)
return any_success
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')
process = task.get('process', '')
rest_finish = task.get('restFinishTime', 0)
virtual_token = task.get('virtualTokenNum', 0)
# status=3 表示已完成且已领奖,status=1且restFinishTime=0表示已完成待领奖
if status == 3 or (status == 1 and rest_finish <= 0):
can_receive = task.get('canReceiveTokenNum', 0)
if can_receive > 0:
self.logger.info(f'[{task_name}] 已完成,待领取 {can_receive} 次抽勋章机会')
else:
self.logger.success(f'[{task_name}] 已完成 ({process})')
continue
if task_type in SKIP_TASK_TYPES:
self.logger.info(f'[{task_name}] 需要实际操作,跳过')
continue
# 对暗号单独处理
if task_type == 'GUESS_GAME_TIP':
if self.do_guess_game():
result['tasks_completed'] += 1
continue
# 积分兑换特殊处理
if task_type == 'INTEGRAL_EXCHANGE':
self.logger.task(f'[{task_name}] 尝试用积分兑换抽勋章次数...')
if self.exchange_points_for_chance():
self.logger.success(f'[{task_name}] 积分兑换成功')
result['tasks_completed'] += 1
continue
# ========== 新增:领取寄件会员权益特殊处理 ==========
if task_type == 'RECEIVE_VIP_BENEFIT':
self.logger.task(f'[{task_name}] 尝试专用接口领取...')
if self.receive_vip_benefit():
result['tasks_completed'] += 1
continue
# =================================================
# 有taskCode的任务尝试自动完成(通用接口)
if task_code:
self.logger.task(f'[{task_name}] 尝试完成任务 (taskCode: {task_code})')
if self.finish_task(task_code):
self.logger.success(f'[{task_name}] 完成成功,可获得 {virtual_token} 次抽勋章机会')
result['tasks_completed'] += 1
else:
self.logger.warning(f'[{task_name}] 完成失败')
time.sleep(1)
else:
self.logger.info(f'[{task_name}] 无taskCode,跳过 ({task_type})')
def do_fetch_rewards(self, result: Dict) -> None:
"""领取所有已完成任务的奖励"""
self.logger.info('领取任务奖励...')
time.sleep(1)
reward_resp = self.fetch_tasks_reward()
if reward_resp:
received = reward_resp.get('receivedAccountList', [])
if received:
for item in received:
currency = item.get('currency', '')
amount = item.get('amount', 0)
task_type = item.get('taskType', '')
self.logger.success(f'领取奖励: {currency} x{amount} (来自: {task_type})')
else:
self.logger.info('无新的任务奖励可领取')
# 显示累计进度
accrued = reward_resp.get('accruedTaskAward', {})
progress = accrued.get('currentProgress', 0)
config = accrued.get('progressConfig', {})
if config:
milestones = ', '.join([f'{k}个任务得{v}' for k, v in sorted(config.items(), key=lambda x: int(x[0]))])
self.logger.info(f'累计完成任务数: {progress} (里程碑: {milestones})')
def do_countdown_chance(self, result: Dict) -> None:
"""领取倒计时奖励(每日免费抽勋章机会)"""
self.logger.info('领取倒计时奖励...')
time.sleep(1)
resp = self.give_countdown_chance()
if resp:
if resp.get('todayCountdownChanceGiven'):
received = 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.success('倒计时奖励已领取')
else:
self.logger.info('今日倒计时奖励未到领取时间')
else:
self.logger.warning('倒计时奖励领取失败')
def do_claim_medals(self, result: Dict) -> None:
"""抽勋章直到没有次数"""
# 先查看当前状态
card_status = self.get_card_status()
if card_status:
claim_balance = 0
for acc in card_status.get('currentAccountList', []):
if acc.get('currency') == 'CLAIM_CHANCE':
claim_balance = acc.get('balance', 0)
break
self.logger.info(f'当前可抽勋章次数: {claim_balance}')
# 显示已有勋章
medals_owned = []
for acc in card_status.get('currentAccountList', []):
currency = acc.get('currency', '')
balance = acc.get('balance', 0)
if currency != 'CLAIM_CHANCE' and balance > 0:
medals_owned.append(f'{currency}x{balance}')
if medals_owned:
self.logger.info(f'已有勋章: {", ".join(medals_owned)}')
if claim_balance <= 0:
self.logger.info('无抽勋章次数,跳过')
return
# 开始抽勋章
self.logger.info('开始抽勋章...')
claim_count = 0
max_claims = 30
while claim_count < max_claims:
time.sleep(1)
claim_result = self.claim_medal(batch=False)
if claim_result is None:
break
received = claim_result.get('receivedAccountList', [])
current_accounts = claim_result.get('currentAccountList', [])
if received:
for item in received:
currency = item.get('currency', '未知')
amount = item.get('amount', 0)
self.logger.medal(f'抽到勋章: {currency} x{amount}')
result['medals_detail'].append({'type': currency, 'amount': amount})
result['medals_claimed'] += amount
claim_count += 1
else:
self.logger.info('没有抽到勋章或无抽取次数')
break
# 检查剩余次数
claim_balance = 0
for acc in current_accounts:
if acc.get('currency') == 'CLAIM_CHANCE':
claim_balance = acc.get('balance', 0)
break
result['claim_balance'] = claim_balance
self.logger.info(f'剩余抽取次数: {claim_balance}')
if claim_balance <= 0:
break
self.logger.info(f'抽勋章完成,本次共抽取 {result["medals_claimed"]} 个勋章')
def run(self) -> Dict[str, Any]:
"""执行周年活动全流程"""
result = {
'tasks_completed': 0,
'medals_claimed': 0,
'medals_detail': [],
'claim_balance': 0,
}
# MODIFIED: 在任务开始前先发送邀请
self.do_invite()
# 0. 获取活动首页信息
index_info = self.get_activity_index()
if index_info:
self.logger.info(f'活动时间: {index_info.get("acStartTime", "")} ~ {index_info.get("acEndTime", "")}')
self.logger.info(f'历史寄件数: {index_info.get("sendNum", 0)},累计支付: {index_info.get("payAmount", 0)}')
# 1. 执行任务(含对暗号)
self.do_tasks(result)
# 2. 领取任务奖励(获得抽勋章次数)
self.do_fetch_rewards(result)
# 3. 抽勋章
self.do_claim_medals(result)
return result
# ==================== 账号执行 ====================
def run_account(account_url: str, index: int) -> 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 as e:
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, 'medals_claimed': 0, 'medals_detail': [], 'claim_balance': 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))
# MODIFIED: 将 user_id 传递给 AnniversaryExecutor
executor = AnniversaryExecutor(http, logger, user_id)
activity_result = executor.run()
return {
'success': True,
'phone': phone,
'index': index,
**activity_result,
}
# ==================== 主程序 ====================
def main():
env_name = 'sfsyUrl'
env_value = os.getenv(env_name)
if not env_value:
print(f"❌ 未找到环境变量 {env_name},请检查配置")
return
account_urls = [url.strip() for url in env_value.split('&') if url.strip()]
if not account_urls:
print(f"❌ 环境变量 {env_name} 为空或格式错误")
return
# 暗号提示
guess_answer = os.getenv(GUESS_ANSWER_ENV, '')
if guess_answer:
answer_lines = [a.strip() for a in guess_answer.strip().split('\n') if a.strip()]
print(f"🔑 已设置暗号环境变量,共 {len(answer_lines)} 个暗号(按日期顺序对应)")
for i, a in enumerate(answer_lines):
print(f"{i+1}天: {a}")
else:
print(f"🔑 未设置暗号环境变量({GUESS_ANSWER_ENV}),将自动从API获取答案")
random.shuffle(account_urls)
print("=" * 60)
print(f"🎉 顺丰33周年活动 - 完成任务抽勋章")
print(f"👨‍💻 作者: 爱学习的呆子")
print(f"📱 共获取到 {len(account_urls)} 个账号")
print(f"⚙️ 并发数量: {CONCURRENT_NUM}")
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)
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): 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" + "=" * 70)
print(f"📊 周年活动汇总")
print("=" * 70)
print(f"{'序号':<6} {'手机号':<15} {'完成任务数':<12} {'抽到勋章数':<12} {'勋章详情':<20}")
print("-" * 70)
total_tasks = 0
total_medals = 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', '未登录')
tasks = r.get('tasks_completed', 0)
medals = r.get('medals_claimed', 0)
detail = ', '.join([f"{d['type']}x{d['amount']}" for d in r.get('medals_detail', [])]) or ''
status = "" if r['success'] else ""
total_tasks += tasks
total_medals += medals
print(f"{idx:<6} {phone:<15} {tasks:<12} {medals:<12} {detail:<20} {status}")
print("-" * 70)
print(f"{'汇总':<6} {'账号: ' + str(len(all_results)):<15} {total_tasks:<12} {total_medals:<12}")
print("=" * 70)
print("\n🎊 所有账号执行完成!")
if __name__ == '__main__':
main()
+816
View File
@@ -0,0 +1,816 @@
# cron: 19 12,20 * * *
# const $ = new Env("顺丰2026世界杯活动");
"""
顺丰2026世界杯活动 - 射门游戏 + 比赛竞猜 + 每日礼物
Author: Auto-generated (参考端午活动脚本)
Version: 1.2.0
Date: 2026-07-06
更新日志:
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}&timestamp={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():
# 收集所有 sfsyUrl 相关环境变量(兼容青龙面板同名多变量)
account_urls = []
invite_pool = [] #