添加 Points_Based/D_SFSY_DWHD.py
This commit is contained in:
759
Points_Based/D_SFSY_DWHD.py
Normal file
759
Points_Based/D_SFSY_DWHD.py
Normal file
@@ -0,0 +1,759 @@
|
|||||||
|
# cron: 19 12,20 * * *
|
||||||
|
# const $ = new Env("顺丰2026端午活动");
|
||||||
|
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 = [] # 运行时从账号列表动态提取
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 配置常量 ====================
|
||||||
|
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', 'flase').lower() == 'true'
|
||||||
|
|
||||||
|
print_lock = Lock()
|
||||||
|
|
||||||
|
# 端午龙舟活动配置
|
||||||
|
ACTIVITY_CODE = "DRAGONBOAT_2026"
|
||||||
|
TOKEN = 'wwesldfs29aniversaryvdld29'
|
||||||
|
SYS_CODE = 'MCS-MIMP-CORE'
|
||||||
|
|
||||||
|
# 需要跳过的任务类型(需要实际操作的)
|
||||||
|
SKIP_TASK_TYPES = [
|
||||||
|
'SEND_INTERNATIONAL_PACKAGE', # 寄一单国际件
|
||||||
|
'LOOK_BIG_PACKAGE_GET_CASH', # 寄大件重货
|
||||||
|
'SEND_SUCCESS_RECALL', # 去寄快递
|
||||||
|
'CHARGE_NEW_EXPRESS_CARD', # 充值新速运通全国卡
|
||||||
|
'CHARGE_COLLECT_ALL', # 充值得8888金币
|
||||||
|
'OPEN_FAMILY_HOME_MUTUAL', # 开通家庭8折互寄权益
|
||||||
|
'BUY_PAID_PACKET', # 购买付费券包
|
||||||
|
'INVITEFRIENDS_PARTAKE_ACTIVITY', # 邀好友首次访问活动
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 日志 ====================
|
||||||
|
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]]:
|
||||||
|
# 如果代理开关关闭,直接返回 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()
|
||||||
|
# 假设 JSON 结构为 {"data":{"list":[{"ip":"x.x.x.x","port":"xxxx"}]}}
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
|
||||||
|
# 根据开关决定是否尝试获取代理
|
||||||
|
if ENABLE_PROXY:
|
||||||
|
proxy = self.proxy_manager.get_proxy()
|
||||||
|
if proxy:
|
||||||
|
self.session.proxies = proxy
|
||||||
|
else:
|
||||||
|
if self.proxy_manager.api_url:
|
||||||
|
print("⚠️ 代理获取失败,将不使用代理")
|
||||||
|
# else:
|
||||||
|
# print("🔒 代理已禁用(ENABLE_PROXY=false)")
|
||||||
|
|
||||||
|
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}×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:
|
||||||
|
"""登录(兼容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 DragonBoatExecutor:
|
||||||
|
# MODIFIED: 增加 user_id 参数
|
||||||
|
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 []
|
||||||
|
|
||||||
|
# ---------- API 方法 ----------
|
||||||
|
|
||||||
|
def get_activity_index(self) -> Optional[Dict]:
|
||||||
|
"""获取端午龙舟活动首页信息"""
|
||||||
|
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~dragonBoat2026IndexService~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": "SFAPP"
|
||||||
|
}
|
||||||
|
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 get_accrued_task_award(self) -> Optional[Dict]:
|
||||||
|
"""获取累计任务奖励进度"""
|
||||||
|
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~dragonBoat2026TaskService~getAccruedTaskAward'
|
||||||
|
resp = self.http.request(url, data={})
|
||||||
|
if resp and resp.get('success'):
|
||||||
|
return resp.get('obj', {})
|
||||||
|
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_crush(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~dragonBoat2026TaskService~integralExchange'
|
||||||
|
data = {
|
||||||
|
"exchangeNum": 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 query_zongzi_status(self) -> Optional[Dict]:
|
||||||
|
"""查询粽子状态(金币、粽子数量等)"""
|
||||||
|
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~dragonBoat2026ZongziService~queryStatus'
|
||||||
|
resp = self.http.request(url, data={})
|
||||||
|
if resp and resp.get('success'):
|
||||||
|
return resp.get('obj', {})
|
||||||
|
return None
|
||||||
|
|
||||||
|
def crush_zongzi(self) -> Optional[Dict]:
|
||||||
|
"""砸粽子"""
|
||||||
|
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~dragonBoat2026ZongziService~crush'
|
||||||
|
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_daily_gift_status(self, city_code: str = '551') -> Optional[Dict]:
|
||||||
|
"""查询每日礼包状态"""
|
||||||
|
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~dragonBoat2026DailyService~getDailyGiftStatus'
|
||||||
|
data = {"cityCode": city_code}
|
||||||
|
resp = self.http.request(url, data=data)
|
||||||
|
if resp and resp.get('success'):
|
||||||
|
return resp.get('obj', {})
|
||||||
|
return None
|
||||||
|
|
||||||
|
def query_extra_reward_cards(self) -> Optional[List]:
|
||||||
|
"""查询额外奖励卡片(倒计时等)"""
|
||||||
|
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~dragonBoat2026ZongziService~queryExtraRewardCards'
|
||||||
|
resp = self.http.request(url, data={})
|
||||||
|
if resp and resp.get('success'):
|
||||||
|
return resp.get('obj', [])
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_family_status(self) -> Optional[Dict]:
|
||||||
|
"""查询家庭权益状态"""
|
||||||
|
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~dragonBoat2026FamilyService~familyStatus'
|
||||||
|
resp = self.http.request(url, data={})
|
||||||
|
if resp and resp.get('success'):
|
||||||
|
return resp.get('obj', {})
|
||||||
|
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 = "https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~dragonBoat2026IndexService~index"
|
||||||
|
data = {"inviteType": 1, "inviteUserId": random_invite}
|
||||||
|
resp = self.http.request(url, data=data)
|
||||||
|
self.logger.info(f"已向 {random_invite} 发起邀请")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"邀请初始化异常: {str(e)}")
|
||||||
|
|
||||||
|
def get_invite_list(self) -> Optional[List[Dict]]:
|
||||||
|
"""查询已邀请好友列表"""
|
||||||
|
url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~dragonBoat2026TaskService~taskInviteList'
|
||||||
|
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 do_tasks(self, result: Dict) -> None:
|
||||||
|
"""执行所有可自动完成的任务"""
|
||||||
|
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 == 'INTEGRAL_EXCHANGE':
|
||||||
|
self.logger.task(f'[{task_name}] 尝试用积分兑换砸粽子次数...')
|
||||||
|
if self.exchange_points_for_crush():
|
||||||
|
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)
|
||||||
|
award = self.get_accrued_task_award()
|
||||||
|
if award:
|
||||||
|
progress = award.get('currentProgress', 0)
|
||||||
|
config = award.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})')
|
||||||
|
else:
|
||||||
|
self.logger.info(f'累计完成任务数: {progress}')
|
||||||
|
|
||||||
|
def do_daily_gift(self, result: Dict) -> None:
|
||||||
|
"""查看每日礼包状态"""
|
||||||
|
self.logger.info('查看每日礼包...')
|
||||||
|
time.sleep(1)
|
||||||
|
gift = self.get_daily_gift_status()
|
||||||
|
if gift:
|
||||||
|
if gift.get('received'):
|
||||||
|
products = gift.get('dailyGiftProductList', [])
|
||||||
|
if products:
|
||||||
|
names = ', '.join([p.get('productName', '未知') for p in products])
|
||||||
|
self.logger.success(f'今日礼包已领取: {names}')
|
||||||
|
else:
|
||||||
|
self.logger.success('今日礼包已领取')
|
||||||
|
elif gift.get('canReceive'):
|
||||||
|
self.logger.info('每日礼包待领取(可能需手动领取)')
|
||||||
|
else:
|
||||||
|
self.logger.info('每日礼包暂不可领取')
|
||||||
|
else:
|
||||||
|
self.logger.warning('获取每日礼包状态失败')
|
||||||
|
|
||||||
|
def do_extra_rewards(self, result: Dict) -> None:
|
||||||
|
"""查询额外奖励卡片(倒计时等)"""
|
||||||
|
cards = self.query_extra_reward_cards()
|
||||||
|
if cards:
|
||||||
|
for card in cards:
|
||||||
|
extra_type = card.get('extraType', '')
|
||||||
|
latest_time = card.get('latestTime', '')
|
||||||
|
countdown = card.get('countdownHours', 0)
|
||||||
|
type_map = {'DAILY': '每日奖励', 'BUSINESS_COUPON': '商业券'}
|
||||||
|
type_name = type_map.get(extra_type, extra_type)
|
||||||
|
self.logger.info(f'额外奖励 [{type_name}]: 上次领取 {latest_time}, 倒计时 {countdown}h')
|
||||||
|
|
||||||
|
def do_crush_zongzi(self, result: Dict) -> None:
|
||||||
|
"""砸粽子直到没有粽子"""
|
||||||
|
# 先查看当前状态
|
||||||
|
zongzi_status = self.query_zongzi_status()
|
||||||
|
if zongzi_status:
|
||||||
|
accounts = zongzi_status.get('currentAccountList', [])
|
||||||
|
gold_coin_balance = 0
|
||||||
|
zongzi_balance = 0
|
||||||
|
for acc in accounts:
|
||||||
|
if acc.get('currency') == 'GOLD_COIN':
|
||||||
|
gold_coin_balance = acc.get('balance', 0)
|
||||||
|
elif acc.get('currency') == 'GOLD_ZONGZI':
|
||||||
|
zongzi_balance = acc.get('balance', 0)
|
||||||
|
total_crush = zongzi_status.get('totalCrushTimes', 0)
|
||||||
|
|
||||||
|
self.logger.info(f'当前金币: {gold_coin_balance}, 金粽子: {zongzi_balance}, 已砸次数: {total_crush}')
|
||||||
|
|
||||||
|
if zongzi_balance <= 0:
|
||||||
|
self.logger.info('无金粽子可砸,跳过')
|
||||||
|
return
|
||||||
|
|
||||||
|
# 开始砸粽子
|
||||||
|
self.logger.info('开始砸粽子...')
|
||||||
|
crush_count = 0
|
||||||
|
max_crush = 30
|
||||||
|
|
||||||
|
while crush_count < max_crush:
|
||||||
|
time.sleep(1)
|
||||||
|
crush_result = self.crush_zongzi()
|
||||||
|
if crush_result is None:
|
||||||
|
# 检查是否没粽子了
|
||||||
|
current_status = self.query_zongzi_status()
|
||||||
|
if current_status:
|
||||||
|
for acc in current_status.get('currentAccountList', []):
|
||||||
|
if acc.get('currency') == 'GOLD_ZONGZI':
|
||||||
|
if acc.get('balance', 0) <= 0:
|
||||||
|
self.logger.info('金粽子已用完')
|
||||||
|
break
|
||||||
|
break
|
||||||
|
|
||||||
|
received = crush_result.get('receivedAccountList', [])
|
||||||
|
current_accounts = crush_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['crush_detail'].append({'type': currency, 'amount': amount})
|
||||||
|
result['gold_coin_earned'] += amount
|
||||||
|
crush_count += 1
|
||||||
|
else:
|
||||||
|
self.logger.info('没有获得奖励')
|
||||||
|
break
|
||||||
|
|
||||||
|
# 检查剩余粽子
|
||||||
|
zongzi_balance = 0
|
||||||
|
gold_coin_balance = 0
|
||||||
|
for acc in current_accounts:
|
||||||
|
if acc.get('currency') == 'GOLD_ZONGZI':
|
||||||
|
zongzi_balance = acc.get('balance', 0)
|
||||||
|
elif acc.get('currency') == 'GOLD_COIN':
|
||||||
|
gold_coin_balance = acc.get('balance', 0)
|
||||||
|
|
||||||
|
result['gold_coin_balance'] = gold_coin_balance
|
||||||
|
self.logger.info(f'剩余粽子: {zongzi_balance}, 金币: {gold_coin_balance}')
|
||||||
|
|
||||||
|
if zongzi_balance <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.logger.info(f'砸粽子完成,本次共获得 {result["gold_coin_earned"]} 金币')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def run(self) -> Dict[str, Any]:
|
||||||
|
"""执行端午龙舟活动全流程"""
|
||||||
|
result = {
|
||||||
|
'tasks_completed': 0,
|
||||||
|
'gold_coin_earned': 0,
|
||||||
|
'crush_detail': [],
|
||||||
|
'gold_coin_balance': 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# MODIFIED: 在任务开始前先发送邀请
|
||||||
|
self.do_invite()
|
||||||
|
# ===== 查询并显示已邀请好友列表 =====
|
||||||
|
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', '未知')
|
||||||
|
self.logger.info(f" {i}. {mobile} (邀请时间: {invite_date})")
|
||||||
|
else:
|
||||||
|
self.logger.info("暂无已邀请好友")
|
||||||
|
# 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_daily_gift(result)
|
||||||
|
# 2. 额外奖励
|
||||||
|
self.do_extra_rewards(result)
|
||||||
|
# 3. 执行任务
|
||||||
|
self.do_tasks(result)
|
||||||
|
# 4. 查看累计奖励进度
|
||||||
|
self.do_fetch_rewards(result)
|
||||||
|
# 5. 砸粽子
|
||||||
|
self.do_crush_zongzi(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, 'gold_coin_earned': 0, 'crush_detail': [], 'gold_coin_balance': 0}
|
||||||
|
|
||||||
|
masked_phone = phone[:3] + "****" + phone[7:] if len(phone) >= 7 else phone
|
||||||
|
logger.success(f'账号{index + 1}: 【{phone}】登录成功')
|
||||||
|
|
||||||
|
# 随机延迟
|
||||||
|
time.sleep(random.uniform(1, 3))
|
||||||
|
|
||||||
|
executor = DragonBoatExecutor(http, logger, user_id, invite_pool)
|
||||||
|
activity_result = executor.run()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'phone': phone,
|
||||||
|
'index': index,
|
||||||
|
**activity_result,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 主程序 ====================
|
||||||
|
def main():
|
||||||
|
# 收集所有 sfsyUrl 相关环境变量(兼容青龙面板同名多变量)
|
||||||
|
# 方式1:青龙可能把多条同名变量合并为一个,值用换行分隔
|
||||||
|
# 方式2:青龙可能重命名为 sfsyUrl, sfsyUrl_1, sfsyUrl_2 ...
|
||||||
|
account_urls = []
|
||||||
|
invite_pool = [] # 列表内互邀池
|
||||||
|
|
||||||
|
for key, value in os.environ.items():
|
||||||
|
if key == 'sfsyUrl' or key.startswith('sfsyUrl_'):
|
||||||
|
# 青龙同名变量用 & 拼接,例如: url1&url2&url3
|
||||||
|
# 每个账号格式: sessionId=xxx;_login_mobile_=xxx;_login_user_id_=xxx
|
||||||
|
# 拼接后: ...&sessionId=xxx;...
|
||||||
|
# 按 &sessionId= 拆分,首段已有 sessionId= 前缀,后续段补回
|
||||||
|
parts = value.split('&sessionId=')
|
||||||
|
if parts[0].strip():
|
||||||
|
account_urls.append(parts[0].strip())
|
||||||
|
for p in parts[1:]:
|
||||||
|
u = ('sessionId=' + p).strip()
|
||||||
|
if u and u not in account_urls:
|
||||||
|
account_urls.append(u)
|
||||||
|
|
||||||
|
if not account_urls:
|
||||||
|
print("未找到任何 sfsyUrl / sfsyUrl_* 环境变量,请检查青龙面板变量配置")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 从账号 URL 提取所有 user_id 组成邀请池
|
||||||
|
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))
|
||||||
|
inviteId = invite_pool
|
||||||
|
print(f"邀请池: {len(invite_pool)} 个账号")
|
||||||
|
|
||||||
|
vars_found = [k for k in os.environ if k == 'sfsyUrl' or k.startswith('sfsyUrl_')]
|
||||||
|
print(f"共获取到 {len(account_urls)} 个账号(来自环境变量: {vars_found})")
|
||||||
|
all_results = []
|
||||||
|
|
||||||
|
if CONCURRENT_NUM <= 1:
|
||||||
|
for idx, url in enumerate(account_urls):
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"开始处理第 {idx+1}/{len(account_urls)} 个账号")
|
||||||
|
print("="*60)
|
||||||
|
result = run_account(url, idx)
|
||||||
|
all_results.append(result)
|
||||||
|
if idx < len(account_urls) - 1:
|
||||||
|
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("\n" + "=" * 60)
|
||||||
|
print(f"全部执行完毕,共处理 {len(all_results)} 个账号")
|
||||||
|
print("=" * 60)
|
||||||
|
for r in all_results:
|
||||||
|
status = "成功" if r['success'] else "失败"
|
||||||
|
phone = r.get('phone', '未知')
|
||||||
|
masked = phone[:3] + "****" + phone[7:] if len(phone) >= 7 else phone
|
||||||
|
print(f" [{status}] {masked} | 完成任务: {r['tasks_completed']} | 获得金币: {r['gold_coin_earned']}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user