1147 lines
56 KiB
Python
1147 lines
56 KiB
Python
# cron: 30 8 * * *
|
||
# new Env("王老吉瓶盖")
|
||
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
青龙面板微信协议自动化脚本 - 王老吉扫码抽奖
|
||
账号从环境变量 WLJ_ID 获取,格式:备注#wxid#手机号,多行分割
|
||
码字从同目录的 WLJ_MZ.txt 中获取
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import time
|
||
import io
|
||
import requests
|
||
import logging
|
||
import random
|
||
import re
|
||
import hashlib
|
||
from datetime import datetime
|
||
from typing import List, Dict, Any, Optional
|
||
from urllib.parse import urlencode
|
||
|
||
# ==================== 养鸡场配置 ====================
|
||
WX_CLOUD = os.getenv('wx_cloud', '') # 养鸡场服务地址
|
||
AUTH_TOKEN = os.getenv('wx_token', '') # 养鸡场认证Token (需要配置)
|
||
|
||
# ==================== 码字文件配置 ====================
|
||
CODE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "WLJ_MZ.txt") # 码字文件路径(与脚本同目录)
|
||
|
||
# ==================== 代理配置 ====================
|
||
PROXY_DEFAULT = "" # 代理API地址(返回格式:ip:端口),留空则不使用代理
|
||
PROXY_API_URL = "" # 青龙变量 pgdl(如果有值且脚本默认也为空时使用)
|
||
USE_PROXY = False # 是否使用多账号代理(True=每个账号使用不同代理,False=不使用代理)
|
||
|
||
# ==================== 账号配置 ====================
|
||
USE_API_ACCOUNTS = False # 是否从养鸡场API获取账号 → 固定关闭
|
||
WX_LIST_MANUAL = os.getenv("WLJ_ID", "") # 手动配置的账号列表 → 从环境变量 WLJ_ID 读取
|
||
|
||
# 需要剔除的wxid列表
|
||
REMOVE_WXIDS = [
|
||
"wxid_x4nz2s4th45k22",
|
||
"wxid_fog306otfw9q22",
|
||
"wxid_tso9447iuq0t22",
|
||
"wxid_5jtncnh3v8ud2",
|
||
]
|
||
|
||
DELAY_BETWEEN_ACCOUNTS = 5 # 账号间延迟(秒)
|
||
WX_APPID = "wxd25dc8ba975776e3" # 小程序AppID(王老吉)
|
||
|
||
# ==================== 抽奖配置 ====================
|
||
DAILY_LOTTERY_LIMIT = 1 # 每账号最大成功抽奖次数
|
||
MAX_CODE_ATTEMPTS = 3 # 每账号最大使用码次数(包括成功和失败的码)
|
||
LOTTERY_INTERVAL_MIN = 5 # 最小抽奖间隔时间(秒)
|
||
LOTTERY_INTERVAL_MAX = 15 # 最大抽奖间隔时间(秒)
|
||
MAX_RETRY_COUNT = 3 # 最大重试次数
|
||
LOTTERY_CODE_PRICE = 0.34 # 有奖码统一价格(元)
|
||
|
||
# ==================== API配置 ====================
|
||
WLJ_BASE_URL = "https://wechatec.brand.wljhealth.com"
|
||
S3_BASE_URL = "https://s3.lsa0.cn"
|
||
|
||
# 固定参数(从HAR抓包获取)
|
||
POSSESSOR = "50c7d6a87202429a9871bf61ec85ad99"
|
||
MALL_CODE = "wxd25dc8ba975776e3"
|
||
SALE_CHANNEL = "mall"
|
||
|
||
# ==================== 位置配置 ====================
|
||
# 扫码位置列表(经纬度),按顺序轮换
|
||
SCAN_LOCATIONS = [
|
||
("28.228521", "112.939423"), # 湖南长沙
|
||
("28.364827", "112.815102"), # 望城
|
||
("28.157439", "113.632571"), # 浏阳
|
||
("28.261334", "112.548920"), # 宁乡
|
||
("27.832155", "113.158607"), # 株洲
|
||
("27.672908", "113.497244"), # 醴陵
|
||
("27.869672", "112.912388"), # 湘潭
|
||
("27.538421", "112.287695"), # 韶山
|
||
("27.751183", "112.503816"), # 湘乡
|
||
("28.358012", "112.196543"), # 益阳
|
||
("28.498775", "112.221098"), # 沅江
|
||
("28.487346", "113.032817"), # 汨罗
|
||
("28.683512", "112.869411"), # 湘阴
|
||
("27.438907", "111.594236"), # 娄底
|
||
]
|
||
|
||
# ==================== 调试配置 ====================
|
||
DEBUG_MODE = False # 是否显示调试信息 True 开启 False 关闭
|
||
|
||
# ==================== User-Agent轮换池 ====================
|
||
USER_AGENTS = [
|
||
"Mozilla/5.0 (Linux; Android 14; HUAWEI Mate 60 RS Build/Mate60RS; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/935 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Honor Magic V2 Build/MagicV2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/870 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Xiaomi 13 Build/13; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/882 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Xiaomi Mix Fold 3 Build/MixFold3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/927 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; HUAWEI Mate 70 Pro Build/Mate70Pro; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/916 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; HUAWEI Mate 70 Pro Build/Mate70Pro; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/914 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; OnePlus Ace 3 Pro Build/Ace3Pro; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/905 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; OnePlus 11 Build/11; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/859 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; OPPO Find N3 Flip Build/FindN3Flip; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/928 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Xiaomi 14 Pro Build/14Pro; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/953 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; HUAWEI Pura 70 Build/Pura70; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/958 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Honor Magic5 Build/Magic5; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/846 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; OPPO Find X7 Ultra Build/FindX7Ultra; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/844 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; HUAWEI Mate 70 Ultra Build/Mate70Ultra; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/823 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Xiaomi Mix Flip Build/MixFlip; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/808 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; HUAWEI P60 Pro Build/P60Pro; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/835 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Honor Magic6 Build/Magic6; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/845 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Honor Magic5 Build/Magic5; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/812 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Honor Magic6 RSR Build/Magic6RSR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/926 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; OnePlus Ace 3 Build/Ace3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/806 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; vivo X90 Pro+ Build/X90ProPlus; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/912 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; OPPO Find X7 Ultra Build/FindX7Ultra; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/886 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; HUAWEI Pura 70 Pro Build/Pura70Pro; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/831 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; vivo Fold 3 Build/Fold3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/949 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; HUAWEI Pura 80 Pro Build/Pura80Pro; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/848 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; vivo Fold 3 Build/Fold3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/901 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Xiaomi Redmi K70 Build/RedmiK70; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/955 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; OnePlus Ace 3 Build/Ace3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/818 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Xiaomi Mix Fold 3 Build/MixFold3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/805 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; OPPO Find X7 Ultra Build/FindX7Ultra; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/875 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; Xiaomi Mix Flip Build/MixFlip; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/950 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
"Mozilla/5.0 (Linux; Android 14; OPPO Find X7 Ultra Build/FindX7Ultra; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.173 Mobile Safari/537.36 XWEB/1420273 MMWEBSDK/20250201 MMWEBID/883 MicroMessenger/8.0.60.2860(0x28003C55) WeChat/arm64 Weixin NetType/5G Language/zh_CN ABI/arm64 MiniProgramEnv/android",
|
||
]
|
||
|
||
# ==================== 设备型号 ====================
|
||
DEVICE_MODELS = [
|
||
"Xiaomi-MixFold3",
|
||
"Huawei-Mate60RS",
|
||
"Huawei-P60",
|
||
"Honor-Magic6RSR",
|
||
"Honor-Magic5",
|
||
"Huawei-Nova12Pro",
|
||
"Xiaomi-RedmiK70Pro",
|
||
"Huawei-Pura70",
|
||
"Huawei-Pura70Pro",
|
||
"OPPO-Reno11",
|
||
"OnePlus-Ace3Pro",
|
||
"Huawei-Nova12",
|
||
"OPPO-FindX7Ultra",
|
||
"vivo-X90ProPlus",
|
||
"Huawei-Mate70Pro+",
|
||
"Huawei-MateX3",
|
||
"Xiaomi-14Ultra",
|
||
"Huawei-Mate70RS",
|
||
"OPPO-Reno11Pro",
|
||
"vivo-X100Ultra",
|
||
"OPPO-FindN3Flip",
|
||
"OnePlus-Ace3",
|
||
"Huawei-Pura80",
|
||
"Huawei-Pura80Pro",
|
||
"Huawei-Mate60Pro+",
|
||
"Huawei-P60Pro+",
|
||
"Huawei-MateX5",
|
||
"Xiaomi-13",
|
||
"Xiaomi-MixFlip",
|
||
"Xiaomi-RedmiK60Pro",
|
||
"vivo-X100",
|
||
"Xiaomi-RedmiK60",
|
||
]
|
||
|
||
|
||
# 自定义日志格式
|
||
class SimpleFormatter(logging.Formatter):
|
||
def format(self, record):
|
||
return record.getMessage()
|
||
|
||
|
||
handler = logging.StreamHandler(io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
|
||
handler.setFormatter(SimpleFormatter())
|
||
|
||
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class WangLaoJiAutomation:
|
||
def __init__(self):
|
||
self.session = requests.Session()
|
||
self.wx_accounts = []
|
||
self.daily_lottery_limit = DAILY_LOTTERY_LIMIT
|
||
self.delay_between_accounts = DELAY_BETWEEN_ACCOUNTS
|
||
self.lottery_interval_min = LOTTERY_INTERVAL_MIN
|
||
self.lottery_interval_max = LOTTERY_INTERVAL_MAX
|
||
# 待使用的码字(失败后顺延,下次优先使用)
|
||
self.pending_code = None
|
||
# 运行时读取代理配置
|
||
env_proxy = os.getenv('pgdl', '')
|
||
if PROXY_DEFAULT:
|
||
self.proxy_url = PROXY_DEFAULT
|
||
logger.info(f"🌐 代理来源: 脚本配置")
|
||
if env_proxy:
|
||
logger.info(f"🌐 青龙变量 pgdl 已设置但未使用")
|
||
elif env_proxy:
|
||
self.proxy_url = env_proxy
|
||
logger.info(f"🌐 代理来源: 青龙变量 pgdl")
|
||
else:
|
||
self.proxy_url = ""
|
||
logger.info(f"⚠️ 未配置代理(脚本和青龙变量都为空)")
|
||
self.use_proxy = USE_PROXY
|
||
self.current_proxy = None
|
||
self.codes_exhausted = False
|
||
self.scan_loc_index = 0 # 位置轮换索引
|
||
self.ua_index = 0 # UA轮换索引
|
||
self.ssl_error_count = 0 # SSL错误计数,连续2次则重新获取IP
|
||
self.load_config()
|
||
self.setup_clients()
|
||
|
||
logger.info("🏮 王老吉扫码抽奖 - 自动化脚本启动")
|
||
logger.info(f"👥 账号数量: {len(self.wx_accounts)}")
|
||
logger.info(f"🎯 每账号执行: {self.daily_lottery_limit} 次抽奖")
|
||
logger.info(f"📄 码字文件: {CODE_FILE}")
|
||
if self.use_proxy and self.proxy_url:
|
||
logger.info(f"🌐 代理模式: 开启")
|
||
|
||
# ==================== 码字文件操作 ====================
|
||
def read_codes_from_file(self) -> List[str]:
|
||
"""从文件读取所有码字"""
|
||
try:
|
||
if os.path.exists(CODE_FILE):
|
||
with open(CODE_FILE, 'r', encoding='utf-8') as f:
|
||
lines = [line.strip() for line in f if line.strip()]
|
||
return lines
|
||
else:
|
||
logger.error(f"❌ 码字文件不存在: {CODE_FILE}")
|
||
return []
|
||
except Exception as e:
|
||
logger.error(f"❌ 读取码字文件失败: {e}")
|
||
return []
|
||
|
||
def remove_used_code(self, code: str) -> bool:
|
||
"""从文件中删除已使用的码字"""
|
||
try:
|
||
codes = self.read_codes_from_file()
|
||
if code in codes:
|
||
codes.remove(code)
|
||
with open(CODE_FILE, 'w', encoding='utf-8') as f:
|
||
f.write('\n'.join(codes))
|
||
logger.info(f"✅ 已从文件删除码字: {code} (剩余: {len(codes)} 个)")
|
||
return True
|
||
else:
|
||
logger.warning(f"⚠️ 码字不在文件中: {code}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"❌ 删除码字失败: {e}")
|
||
return False
|
||
|
||
def get_pending_code(self) -> Optional[str]:
|
||
"""获取待重试的码字"""
|
||
code = self.pending_code
|
||
if code:
|
||
logger.info(f"🔄 复用上次失败的码字: {code}")
|
||
self.pending_code = None
|
||
return code
|
||
|
||
def set_pending_code(self, code: str):
|
||
"""设置待重试的码字(失败时保留)"""
|
||
if code:
|
||
logger.warning(f"⚠️ 码字失败,顺延下次使用: {code}")
|
||
self.pending_code = code
|
||
|
||
def commit_scan_code(self, code: str):
|
||
"""确认码字使用成功,从文件删除"""
|
||
self.remove_used_code(code)
|
||
|
||
# ==================== 代理相关 ====================
|
||
def get_proxy(self) -> Optional[str]:
|
||
"""获取代理"""
|
||
if not self.proxy_url:
|
||
return None
|
||
try:
|
||
response = requests.get(self.proxy_url, timeout=10)
|
||
if response.status_code == 200:
|
||
try:
|
||
data = response.json()
|
||
if isinstance(data, dict) and "data" in data:
|
||
proxy_list = data["data"]
|
||
if isinstance(proxy_list, list) and len(proxy_list) > 0:
|
||
first_proxy = proxy_list[0]
|
||
ip = first_proxy.get("ip")
|
||
port = first_proxy.get("port")
|
||
if ip and port:
|
||
proxy = f"{ip}:{port}"
|
||
logger.info(f"🌐 获取代理成功: {proxy}")
|
||
return proxy
|
||
except:
|
||
pass
|
||
proxy = response.text.strip()
|
||
if proxy:
|
||
logger.info(f"🌐 获取代理成功: {proxy}")
|
||
return proxy
|
||
except Exception as e:
|
||
logger.error(f"🌐 获取代理失败: {e}")
|
||
return None
|
||
|
||
def get_scan_location(self) -> tuple:
|
||
"""获取轮换的扫码位置"""
|
||
lat, lon = SCAN_LOCATIONS[self.scan_loc_index % len(SCAN_LOCATIONS)]
|
||
self.scan_loc_index += 1
|
||
return lat, lon
|
||
|
||
def get_ua(self) -> str:
|
||
"""获取轮换的User-Agent"""
|
||
ua = USER_AGENTS[self.ua_index % len(USER_AGENTS)]
|
||
self.ua_index += 1
|
||
return ua
|
||
|
||
def get_proxies(self, proxy: Optional[str] = None) -> Optional[dict]:
|
||
"""获取代理字典格式"""
|
||
if proxy:
|
||
return {"http": f"http://{proxy}", "https": f"http://{proxy}"}
|
||
return None
|
||
|
||
@staticmethod
|
||
def is_ssl_error(error_str: str) -> bool:
|
||
"""检测是否为SSL错误"""
|
||
ssl_keywords = ['ssl', 'eof', 'SSLEOF', 'EOF occurred', 'ssl.c:', 'SSL']
|
||
return any(keyword in error_str.lower() for keyword in ssl_keywords)
|
||
|
||
def refresh_proxy_ip(self) -> Optional[str]:
|
||
"""重新获取代理IP"""
|
||
self.ssl_error_count = 0 # 重置计数
|
||
new_proxy = self.get_proxy()
|
||
if new_proxy:
|
||
logger.info(f"🔄 SSL错误达到2次,重新获取IP: {new_proxy}")
|
||
return new_proxy
|
||
logger.error(f"❌ 重新获取IP失败")
|
||
return None
|
||
|
||
def load_config(self):
|
||
"""加载配置"""
|
||
self.wx_accounts = self.parse_manual_accounts()
|
||
|
||
def setup_clients(self):
|
||
"""设置HTTP客户端"""
|
||
self.session.headers.update({
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
"charset": "utf-8",
|
||
"Accept-Encoding": "gzip, deflate, br",
|
||
"Referer": "https://servicewechat.com/wxd25dc8ba975776e3/409/page-frame.html",
|
||
})
|
||
|
||
def parse_manual_accounts(self) -> List[Dict[str, str]]:
|
||
"""解析从环境变量 WLJ_ID 读取的账号"""
|
||
accounts = []
|
||
account_text = os.getenv("WLJ_ID", "").strip()
|
||
if not account_text:
|
||
logger.error("❌ 未配置环境变量 WLJ_ID")
|
||
return accounts
|
||
|
||
for line in account_text.splitlines():
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
parts = line.split("#")
|
||
if len(parts) >= 3:
|
||
accounts.append({
|
||
"nickname": parts[0],
|
||
"wxId": parts[1],
|
||
"phone": parts[2],
|
||
"wxName": parts[0],
|
||
})
|
||
accounts = [a for a in accounts if a.get("wxId") not in REMOVE_WXIDS]
|
||
logger.info(f"✅ 从 WLJ_ID 加载账号 {len(accounts)} 个")
|
||
return accounts
|
||
|
||
def get_code_from_chicken_farm(self, wxid: str = None) -> Optional[str]:
|
||
"""从养鸡场获取微信code"""
|
||
try:
|
||
url = f"{WX_CLOUD}/prod-api/wechat/api/getMiniProgramCode"
|
||
headers = {
|
||
"Authorization": AUTH_TOKEN,
|
||
"Content-Type": "application/json",
|
||
}
|
||
|
||
payload = {
|
||
"wxid": wxid if wxid else "",
|
||
"appid": WX_APPID
|
||
}
|
||
|
||
if DEBUG_MODE:
|
||
logger.info(f"📤 养鸡场API: {url}")
|
||
logger.info(f"📤 请求体: {payload}")
|
||
|
||
response = requests.post(url, json=payload, headers=headers, timeout=10)
|
||
response.raise_for_status()
|
||
res_data = response.json()
|
||
|
||
if DEBUG_MODE:
|
||
logger.info(f"📥 养鸡场返回: {res_data}")
|
||
|
||
if res_data.get("code") == 200 and res_data.get("data", {}).get("code"):
|
||
code = res_data["data"]["code"]
|
||
logger.info(f"✅ 获取微信code成功: {code}")
|
||
return code
|
||
else:
|
||
logger.warning(f"⚠️ 养鸡场无可用code")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 获取code失败: {e}")
|
||
return None
|
||
|
||
def get_scan_code(self) -> Optional[str]:
|
||
"""获取扫码码(优先待重试的码,其次从文件读取)"""
|
||
# 1. 优先使用待重试的码字
|
||
pending = self.get_pending_code()
|
||
if pending:
|
||
return pending
|
||
|
||
# 2. 检查文件是否还有码字
|
||
if self.codes_exhausted:
|
||
logger.warning(f"⚠️ 扫码码已用完,不再获取")
|
||
return None
|
||
|
||
codes = self.read_codes_from_file()
|
||
if codes:
|
||
code = codes[0]
|
||
remaining = len(codes) - 1
|
||
logger.info(f"✅ 从文件获取扫码码: {code} (剩余: {remaining} 个)")
|
||
return code
|
||
else:
|
||
logger.warning(f"⚠️ 文件中无码字")
|
||
self.codes_exhausted = True
|
||
return None
|
||
|
||
@staticmethod
|
||
def compute_sign(user_code: str) -> str:
|
||
"""计算sign: MD5(userCode + 'cU9(yZ3{zD6!pE4.xX7#')"""
|
||
secret = "cU9(yZ3{zD6!pE4.xX7#"
|
||
raw = (user_code or "") + secret
|
||
return hashlib.md5(raw.encode('utf-8')).hexdigest().upper()
|
||
|
||
def step1_wlj_login(self, code: str, proxy: Optional[str] = None) -> Optional[Dict]:
|
||
"""步骤1: 微信code换取wljhealth登录token"""
|
||
url = f"{WLJ_BASE_URL}/userInfoMini/userMemberLogin"
|
||
ua = self.get_ua()
|
||
|
||
# sign: MD5(userCode + secret), userCode初始为空
|
||
user_code = ""
|
||
sign = self.compute_sign(user_code)
|
||
|
||
data = {
|
||
"code": code,
|
||
"possessor": POSSESSOR,
|
||
"userCode": user_code,
|
||
"mallCode": MALL_CODE,
|
||
"saleChannel": SALE_CHANNEL,
|
||
"sign": sign,
|
||
}
|
||
proxies = self.get_proxies(proxy)
|
||
|
||
headers = {
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
"charset": "utf-8",
|
||
"Accept-Encoding": "gzip, deflate, br",
|
||
"Referer": "https://servicewechat.com/wxd25dc8ba975776e3/409/page-frame.html",
|
||
"User-Agent": ua,
|
||
}
|
||
|
||
try:
|
||
if DEBUG_MODE:
|
||
logger.info(f"📤 请求URL: {url}")
|
||
logger.info(f"📤 请求头: {headers}")
|
||
logger.info(f"📤 请求体: {data}")
|
||
|
||
body = urlencode(data).encode('utf-8')
|
||
response = self.session.post(url, data=body, headers=headers, proxies=proxies, timeout=10)
|
||
response.raise_for_status()
|
||
|
||
if DEBUG_MODE:
|
||
logger.info(f"📥 响应状态码: {response.status_code}")
|
||
logger.info(f"📥 响应体: {response.text}")
|
||
logger.info(f"📥 响应Set-Cookie: {response.headers.get('Set-Cookie', '无')}")
|
||
|
||
result = response.json()
|
||
|
||
if result.get("success") and result.get("status") == 1:
|
||
content = result["content"]
|
||
# content可能是字符串(错误信息)也可能是字典
|
||
if isinstance(content, str):
|
||
logger.error(f"❌ 步骤1登录失败: {content}")
|
||
return None
|
||
if not isinstance(content, dict):
|
||
logger.error(f"❌ 步骤1登录失败: content类型异常 {type(content)}")
|
||
return None
|
||
token = content.get("token", "")
|
||
# user_summary 嵌套在里面
|
||
user_summary = content.get("user_summary", {})
|
||
openid = user_summary.get("openid", "")
|
||
user_code = user_summary.get("userCode", "")
|
||
user_nickname = user_summary.get("userNickname", "")
|
||
phone = user_summary.get("phone", "") or user_summary.get("userPhone", "")
|
||
logger.info(f"✅ 步骤1成功 - OpenID: {openid}, 昵称: {user_nickname}, 手机: {phone}")
|
||
return {
|
||
"token": token,
|
||
"openid": openid,
|
||
"userCode": user_code,
|
||
"userNickname": user_nickname,
|
||
"phone": phone,
|
||
}
|
||
else:
|
||
logger.error(f"❌ 步骤1登录失败: {result.get('msg')}")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 步骤1请求失败: {e}")
|
||
return None
|
||
|
||
def step2_get_s3_token(self, wlj_token: str, serial_code: str, user_code: str, scan_lat: str, scan_lon: str, proxy: Optional[str] = None) -> Optional[str]:
|
||
"""步骤2: 用wljtoken换取s3.lsa0.cn的token"""
|
||
url = f"{WLJ_BASE_URL}/openapi/getToken"
|
||
ua = self.get_ua()
|
||
|
||
# HAR中getToken没有sign参数,userCode从step1获取
|
||
form_data = {
|
||
"serialCode": serial_code,
|
||
"latitude": scan_lat,
|
||
"longitude": scan_lon,
|
||
"possessor": POSSESSOR,
|
||
"userCode": user_code,
|
||
"mallCode": MALL_CODE,
|
||
"saleChannel": SALE_CHANNEL,
|
||
}
|
||
proxies = self.get_proxies(proxy)
|
||
|
||
headers = {
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
"charset": "utf-8",
|
||
"Accept-Encoding": "gzip, deflate, br",
|
||
"Referer": "https://servicewechat.com/wxd25dc8ba975776e3/409/page-frame.html",
|
||
"User-Agent": ua,
|
||
"Authorization": wlj_token,
|
||
}
|
||
|
||
try:
|
||
if DEBUG_MODE:
|
||
logger.info(f"📤 请求URL: {url}")
|
||
logger.info(f"📤 请求头: {headers}")
|
||
logger.info(f"📤 请求体: {form_data}")
|
||
|
||
body = urlencode(form_data).encode('utf-8')
|
||
response = self.session.post(url, data=body, headers=headers, proxies=proxies, timeout=10)
|
||
response.raise_for_status()
|
||
|
||
if DEBUG_MODE:
|
||
logger.info(f"📥 实际请求Cookie: {self.session.cookies.get_dict()}")
|
||
logger.info(f"📥 响应体: {response.text}")
|
||
|
||
result = response.json()
|
||
|
||
if result.get("success") and result.get("status") == 1:
|
||
s3_token = result["content"]
|
||
logger.info(f"✅ 步骤2成功 - 获取s3 token")
|
||
return s3_token
|
||
else:
|
||
logger.error(f"❌ 步骤2获取s3token失败: {result.get('msg')}")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 步骤2请求失败: {e}")
|
||
return None
|
||
|
||
def step3_s3_third_login(self, s3_token: str, proxy: Optional[str] = None) -> Optional[str]:
|
||
"""步骤3: s3第三方登录,获取最终s3 Bearer token"""
|
||
url = f"{S3_BASE_URL}/openapi/promotion/consumer/auth/thirdLogin"
|
||
|
||
payload = {
|
||
"appid": WX_APPID,
|
||
"sign": s3_token,
|
||
}
|
||
proxies = self.get_proxies(proxy)
|
||
|
||
ua = self.get_ua()
|
||
headers = {
|
||
"Authorization": f"Bearer {s3_token}",
|
||
"Content-Type": "application/json",
|
||
"Origin": "https://s3.lsa0.cn",
|
||
"X-Requested-With": "com.tencent.mm",
|
||
"Referer": "https://s3.lsa0.cn/",
|
||
"Accept-Encoding": "gzip, deflate, br",
|
||
"User-Agent": ua,
|
||
}
|
||
|
||
try:
|
||
if DEBUG_MODE:
|
||
logger.info(f"📤 请求URL: {url}")
|
||
logger.info(f"📤 请求头: {headers}")
|
||
logger.info(f"📤 请求体: {payload}")
|
||
|
||
response = requests.post(url, json=payload, headers=headers, proxies=proxies, timeout=10)
|
||
response.raise_for_status()
|
||
|
||
if DEBUG_MODE:
|
||
logger.info(f"📥 响应体: {response.text}")
|
||
|
||
result = response.json()
|
||
|
||
if result.get("success") and result.get("code") == "200":
|
||
final_token = result["data"]["token"]
|
||
open_id = result["data"].get("openId", "")
|
||
logger.info(f"✅ 步骤3成功 - s3登录成功")
|
||
return final_token, False
|
||
else:
|
||
logger.error(f"❌ 步骤3 s3登录失败: {result.get('message')}")
|
||
return None, False
|
||
|
||
except Exception as e:
|
||
error_str = str(e)
|
||
logger.error(f"❌ 步骤3请求失败: {e}")
|
||
# 判断是否为SSL错误
|
||
is_ssl = self.is_ssl_error(error_str)
|
||
if is_ssl:
|
||
logger.warning(f"⚠️ SSL错误,码字将顺延下次使用")
|
||
return None, is_ssl
|
||
|
||
def step4_scan_mask_code(self, s3_token: str, mask_code: str, scan_lat: str, scan_lon: str, proxy: Optional[str] = None) -> tuple:
|
||
"""步骤4: 扫码核销 返回 (结果, 是否代理错误, 是否每日上限)"""
|
||
url = f"{S3_BASE_URL}/openapi/promotion/campaignExecute/scanMaskCode"
|
||
|
||
payload = {
|
||
"maskCode": mask_code,
|
||
"lng": scan_lon,
|
||
"lat": scan_lat,
|
||
"lastScanResult": False,
|
||
}
|
||
proxies = self.get_proxies(proxy)
|
||
|
||
ua = self.get_ua()
|
||
headers = {
|
||
"Authorization": f"Bearer {s3_token}",
|
||
"Content-Type": "application/json",
|
||
"Origin": "https://s3.lsa0.cn",
|
||
"X-Requested-With": "com.tencent.mm",
|
||
"Referer": "https://s3.lsa0.cn/",
|
||
"Accept-Encoding": "gzip, deflate, br",
|
||
"User-Agent": ua,
|
||
}
|
||
|
||
try:
|
||
if DEBUG_MODE:
|
||
logger.info(f"📤 请求URL: {url}")
|
||
logger.info(f"📤 请求头: {headers}")
|
||
logger.info(f"📤 请求体: {payload}")
|
||
|
||
response = requests.post(url, json=payload, headers=headers, proxies=proxies, timeout=10)
|
||
response.raise_for_status()
|
||
|
||
if DEBUG_MODE:
|
||
logger.info(f"📥 响应体: {response.text}")
|
||
|
||
result = response.json()
|
||
|
||
if result.get("success") and result.get("code") == "200":
|
||
data = result["data"]
|
||
biz_code = data.get("bizCode", "")
|
||
biz_message = data.get("bizMessage", "")
|
||
auto_lottery = data.get("autoLottery", False)
|
||
campaign_name = data.get("campaignName", "")
|
||
# 每日上限检测优先判断
|
||
if "已超上限" in biz_message or "超限" in biz_message:
|
||
logger.warning(f"⚠️ 步骤4扫码 - 每日次数已上限: {biz_message}")
|
||
return data, False, True
|
||
if biz_code == "0000000":
|
||
logger.info(f"✅ 步骤4扫码成功 - 活动: {campaign_name}, 状态: {biz_message}, 自动抽奖: {auto_lottery}")
|
||
return data, False, False
|
||
else:
|
||
logger.error(f"❌ 步骤4扫码失败 - bizCode: {biz_code}, msg: {biz_message}")
|
||
return None, False, False
|
||
else:
|
||
logger.error(f"❌ 步骤4扫码失败: {result.get('message')}")
|
||
return None, False, False
|
||
|
||
except Exception as e:
|
||
error_str = str(e)
|
||
logger.error(f"❌ 步骤4请求失败: {e}")
|
||
is_proxy_error = any(pe in error_str for pe in ['Proxy', 'timeout', 'timed out', 'Cannot connect'])
|
||
return None, is_proxy_error, False
|
||
|
||
def step5_mask_code_lottery(self, s3_token: str, mask_code: str, scan_lat: str, scan_lon: str, proxy: Optional[str] = None) -> tuple:
|
||
"""步骤5: 抽奖 返回 (结果, 是否代理错误)"""
|
||
url = f"{S3_BASE_URL}/openapi/promotion/campaignExecute/maskCodeLottery"
|
||
|
||
payload = {
|
||
"maskCode": mask_code,
|
||
"lng": scan_lon,
|
||
"lat": scan_lat,
|
||
}
|
||
proxies = self.get_proxies(proxy)
|
||
|
||
ua = self.get_ua()
|
||
headers = {
|
||
"Authorization": f"Bearer {s3_token}",
|
||
"Content-Type": "application/json",
|
||
"Origin": "https://s3.lsa0.cn",
|
||
"X-Requested-With": "com.tencent.mm",
|
||
"Referer": "https://s3.lsa0.cn/",
|
||
"Accept-Encoding": "gzip, deflate, br",
|
||
"User-Agent": ua,
|
||
}
|
||
|
||
try:
|
||
if DEBUG_MODE:
|
||
logger.info(f"📤 请求URL: {url}")
|
||
logger.info(f"📤 请求头: {headers}")
|
||
logger.info(f"📤 请求体: {payload}")
|
||
|
||
response = requests.post(url, json=payload, headers=headers, proxies=proxies, timeout=10)
|
||
response.raise_for_status()
|
||
|
||
if DEBUG_MODE:
|
||
logger.info(f"📥 响应体: {response.text}")
|
||
|
||
result = response.json()
|
||
|
||
if result.get("success") and result.get("code") == "200":
|
||
data = result["data"]
|
||
biz_code = data.get("bizCode", "")
|
||
biz_message = data.get("bizMessage", "")
|
||
lucky_list = data.get("luckyRewardList", [])
|
||
campaign_name = data.get("campaignName", "")
|
||
if biz_code == "0000000":
|
||
logger.info(f"✅ 步骤5抽奖成功 - 活动: {campaign_name}, 结果: {biz_message}")
|
||
return data, False
|
||
else:
|
||
logger.error(f"❌ 步骤5抽奖失败 - bizCode: {biz_code}, msg: {biz_message}")
|
||
return None, False
|
||
else:
|
||
logger.error(f"❌ 步骤5抽奖失败: {result.get('message')}")
|
||
return None, False
|
||
|
||
except Exception as e:
|
||
error_str = str(e)
|
||
logger.error(f"❌ 步骤5请求失败: {e}")
|
||
is_proxy_error = any(pe in error_str for pe in ['Proxy', 'timeout', 'timed out', 'Cannot connect'])
|
||
return None, is_proxy_error
|
||
|
||
def step6_receive_reward(self, s3_token: str, user_reward_id: str, proxy: Optional[str] = None) -> tuple:
|
||
"""步骤6: 领取红包(先调用receiveReward)"""
|
||
url = f"{S3_BASE_URL}/openapi/promotion/consumer/receiveReward"
|
||
payload = {"userRewardId": user_reward_id}
|
||
proxies = self.get_proxies(proxy)
|
||
ua = self.get_ua()
|
||
headers = {
|
||
"Authorization": f"Bearer {s3_token}",
|
||
"Content-Type": "application/json",
|
||
"Origin": "https://s3.lsa0.cn",
|
||
"X-Requested-With": "com.tencent.mm",
|
||
"Referer": "https://s3.lsa0.cn/",
|
||
"Accept-Encoding": "gzip, deflate, br",
|
||
"User-Agent": ua,
|
||
}
|
||
|
||
try:
|
||
if DEBUG_MODE:
|
||
logger.info(f"📤 请求URL: {url}")
|
||
logger.info(f"📤 请求体: {payload}")
|
||
|
||
response = requests.post(url, json=payload, headers=headers, proxies=proxies, timeout=10)
|
||
response.raise_for_status()
|
||
|
||
if DEBUG_MODE:
|
||
logger.info(f"📥 响应体: {response.text}")
|
||
|
||
result = response.json()
|
||
|
||
if result.get("success") and result.get("code") == "200":
|
||
biz_code = result.get("data", {}).get("bizCode", "")
|
||
biz_msg = result.get("data", {}).get("bizMessage", "")
|
||
inner_data = result.get("data", {}).get("data", {})
|
||
claim_end = inner_data.get("claimEndTime", "")
|
||
auto_withdraw = inner_data.get("autoWithdraw", False)
|
||
if biz_code == "0000000":
|
||
logger.info(f"✅ 步骤6领取红包成功 - 领取截止: {claim_end}, 自动提现: {auto_withdraw}")
|
||
return inner_data, False
|
||
else:
|
||
logger.error(f"❌ 步骤6领取红包失败 - bizCode: {biz_code}, msg: {biz_msg}")
|
||
return None, False
|
||
else:
|
||
logger.error(f"❌ 步骤6领取红包失败: {result.get('message')}")
|
||
return None, False
|
||
|
||
except Exception as e:
|
||
error_str = str(e)
|
||
logger.error(f"❌ 步骤6请求失败: {e}")
|
||
is_proxy_error = any(pe in error_str for pe in ['Proxy', 'timeout', 'timed out', 'Cannot connect'])
|
||
return None, is_proxy_error
|
||
|
||
def step7_claim_reward(self, s3_token: str, user_reward_id: str, proxy: Optional[str] = None) -> tuple:
|
||
"""步骤7: 确认领取奖励(后调用claimReward)"""
|
||
url = f"{S3_BASE_URL}/openapi/promotion/consumer/claimReward"
|
||
payload = {"id": user_reward_id}
|
||
proxies = self.get_proxies(proxy)
|
||
ua = self.get_ua()
|
||
headers = {
|
||
"Authorization": f"Bearer {s3_token}",
|
||
"Content-Type": "application/json",
|
||
"Origin": "https://s3.lsa0.cn",
|
||
"X-Requested-With": "com.tencent.mm",
|
||
"Referer": "https://s3.lsa0.cn/",
|
||
"Accept-Encoding": "gzip, deflate, br",
|
||
"User-Agent": ua,
|
||
}
|
||
|
||
try:
|
||
if DEBUG_MODE:
|
||
logger.info(f"📤 请求URL: {url}")
|
||
logger.info(f"📤 请求体: {payload}")
|
||
|
||
response = requests.post(url, json=payload, headers=headers, proxies=proxies, timeout=10)
|
||
response.raise_for_status()
|
||
|
||
if DEBUG_MODE:
|
||
logger.info(f"📥 响应体: {response.text}")
|
||
|
||
result = response.json()
|
||
|
||
if result.get("success") and result.get("code") == "200":
|
||
biz_code = result.get("data", {}).get("bizCode", "")
|
||
biz_msg = result.get("data", {}).get("bizMessage", "")
|
||
if biz_code == "0000000":
|
||
logger.info(f"✅ 步骤7确认领取成功 - {biz_msg}")
|
||
return result.get("data", {}), False
|
||
else:
|
||
logger.error(f"❌ 步骤7确认领取失败 - bizCode: {biz_code}, msg: {biz_msg}")
|
||
return None, False
|
||
else:
|
||
logger.error(f"❌ 步骤7确认领取失败: {result.get('message')}")
|
||
return None, False
|
||
|
||
except Exception as e:
|
||
error_str = str(e)
|
||
logger.error(f"❌ 步骤7请求失败: {e}")
|
||
is_proxy_error = any(pe in error_str for pe in ['Proxy', 'timeout', 'timed out', 'Cannot connect'])
|
||
return None, is_proxy_error
|
||
|
||
def process_account(self, account: Dict) -> Dict:
|
||
"""处理单个账号"""
|
||
result = {
|
||
"nickname": account.get("wxName", ""),
|
||
"wxid": account.get("wxId", ""),
|
||
"phone": account.get("phone", ""),
|
||
"lottery_count": 0,
|
||
"code_used_count": 0,
|
||
"prizes": [],
|
||
"total_amount": 0.0,
|
||
"success": False,
|
||
}
|
||
|
||
# 重置SSL错误计数(每个账号独立计算)
|
||
self.ssl_error_count = 0
|
||
|
||
wxid = account.get("wxId", "")
|
||
|
||
# 获取代理
|
||
account_proxy = None
|
||
if self.use_proxy:
|
||
account_proxy = self.get_proxy()
|
||
if account_proxy:
|
||
logger.info(f"🌐 账号使用代理: {account_proxy}")
|
||
|
||
# ====== 完整登录流程(获取s3 token)======
|
||
# 步骤1: 获取微信code并登录wljhealth
|
||
login_code = self.get_code_from_chicken_farm(wxid)
|
||
if not login_code:
|
||
logger.error(f"❌ 无法获取微信code,跳过账号")
|
||
return result
|
||
|
||
# 步骤1: wljhealth登录
|
||
login_result = None
|
||
retry_count = 0
|
||
|
||
while retry_count < MAX_RETRY_COUNT and not login_result:
|
||
login_result = self.step1_wlj_login(login_code, account_proxy)
|
||
|
||
if not login_result:
|
||
if self.use_proxy and account_proxy:
|
||
logger.warning(f"⚠️ wlj登录失败,尝试更换代理...")
|
||
new_proxy = self.get_proxy()
|
||
if new_proxy:
|
||
account_proxy = new_proxy
|
||
logger.info(f"🌐 更换新代理: {account_proxy}")
|
||
login_result = self.step1_wlj_login(login_code, account_proxy)
|
||
if login_result:
|
||
break
|
||
|
||
if not login_result and retry_count < MAX_RETRY_COUNT - 1:
|
||
logger.warning(f"⚠️ wlj登录失败,获取新code重试 ({retry_count + 1}/{MAX_RETRY_COUNT})")
|
||
login_code = self.get_code_from_chicken_farm(wxid)
|
||
if login_code:
|
||
retry_count += 1
|
||
time.sleep(1)
|
||
continue
|
||
else:
|
||
logger.error(f"❌ 无法获取新code")
|
||
break
|
||
else:
|
||
break
|
||
|
||
if not login_result:
|
||
logger.error(f"❌ 登录失败,已重试{retry_count}次,跳过账号")
|
||
return result
|
||
|
||
wlj_token = login_result["token"]
|
||
logger.info(f"✅ 登录完成 - 用户: {login_result.get('userNickname', '')}, 手机: {login_result.get('phone', '')}")
|
||
|
||
# ====== 抽奖主循环 ======
|
||
success_lottery_count = 0
|
||
code_used_count = 0
|
||
current_proxy = account_proxy
|
||
s3_token = None # 缓存s3 token
|
||
s3_token_code = None # 关联的serialCode
|
||
# 每个账号固定一个经纬度
|
||
scan_lat, scan_lon = self.get_scan_location()
|
||
logger.info(f"📍 账号扫码位置 - 纬度: {scan_lat}, 经度: {scan_lon}")
|
||
|
||
while success_lottery_count < self.daily_lottery_limit:
|
||
if code_used_count >= MAX_CODE_ATTEMPTS:
|
||
logger.warning(f"⚠️ 已使用{code_used_count}个码,退出循环")
|
||
break
|
||
|
||
logger.info(f"📝 第 {success_lottery_count + 1}/{self.daily_lottery_limit} 次成功抽奖 (已使用 {code_used_count} 个码)")
|
||
|
||
# 获取扫码码(优先待重试的码)
|
||
scan_code = self.get_scan_code()
|
||
if not scan_code:
|
||
logger.error(f"❌ 无法获取扫码码")
|
||
break
|
||
|
||
# 步骤2: 获取s3 token(需要serialCode)
|
||
s3_token = self.step2_get_s3_token(wlj_token, scan_code, login_result.get("userCode", ""), scan_lat, scan_lon, current_proxy)
|
||
if not s3_token:
|
||
logger.warning(f"⚠️ 获取s3token失败,码字顺延")
|
||
self.set_pending_code(scan_code) # 保留码字
|
||
interval = random.uniform(self.lottery_interval_min, self.lottery_interval_max)
|
||
time.sleep(interval)
|
||
continue
|
||
|
||
# 步骤3: s3第三方登录
|
||
final_s3_token, is_ssl = self.step3_s3_third_login(s3_token, current_proxy)
|
||
|
||
# 处理SSL错误
|
||
if is_ssl:
|
||
self.ssl_error_count += 1
|
||
logger.warning(f"⚠️ SSL错误 ({self.ssl_error_count}/2),码字顺延")
|
||
self.set_pending_code(scan_code) # 保留码字
|
||
|
||
if self.ssl_error_count >= 2:
|
||
# 连续2次SSL错误,重新获取IP
|
||
new_proxy = self.refresh_proxy_ip()
|
||
if new_proxy:
|
||
current_proxy = new_proxy
|
||
else:
|
||
logger.error(f"❌ 无法重新获取IP,退出")
|
||
break
|
||
interval = random.uniform(self.lottery_interval_min, self.lottery_interval_max)
|
||
time.sleep(interval)
|
||
continue
|
||
|
||
if not final_s3_token:
|
||
self.ssl_error_count = 0 # 非SSL错误,重置计数
|
||
logger.warning(f"⚠️ s3登录失败,码字顺延")
|
||
self.set_pending_code(scan_code) # 保留码字,不计入已使用
|
||
interval = random.uniform(self.lottery_interval_min, self.lottery_interval_max)
|
||
time.sleep(interval)
|
||
continue
|
||
|
||
# 步骤4: 扫码核销
|
||
scan_result, is_proxy_error, is_daily_limit = self.step4_scan_mask_code(final_s3_token, scan_code, scan_lat, scan_lon, current_proxy)
|
||
|
||
if is_daily_limit:
|
||
# 每日上限,不删除码字(可以下次再用)
|
||
logger.warning(f"⚠️ 每日上限,停止此账号")
|
||
break
|
||
|
||
if not scan_result:
|
||
if self.use_proxy and is_proxy_error:
|
||
logger.warning(f"⚠️ 扫码代理错误,尝试更换代理...")
|
||
new_proxy = self.get_proxy()
|
||
if new_proxy:
|
||
current_proxy = new_proxy
|
||
logger.info(f"🌐 更换新代理: {current_proxy}")
|
||
# 重新获取s3 token并扫码
|
||
s3_token = self.step2_get_s3_token(wlj_token, scan_code, login_result.get("userCode", ""), scan_lat, scan_lon, current_proxy)
|
||
if s3_token:
|
||
final_s3_token = self.step3_s3_third_login(s3_token, current_proxy)
|
||
if final_s3_token:
|
||
scan_result, _, _ = self.step4_scan_mask_code(final_s3_token, scan_code, scan_lat, scan_lon, current_proxy)
|
||
if scan_result:
|
||
# 扫码成功后再检查一遍每日上限
|
||
scan_biz_msg = scan_result.get("bizMessage", "")
|
||
if "已超上限" in scan_biz_msg or "超限" in scan_biz_msg:
|
||
logger.warning(f"⚠️ {scan_biz_msg},停止此账号")
|
||
break
|
||
|
||
if not scan_result:
|
||
logger.warning(f"⚠️ 扫码失败,码字顺延")
|
||
self.set_pending_code(scan_code) # 保留码字
|
||
interval = random.uniform(self.lottery_interval_min, self.lottery_interval_max)
|
||
time.sleep(interval)
|
||
continue
|
||
|
||
# ====== 扫码成功,使用码字并抽奖 ======
|
||
code_used_count += 1
|
||
result["code_used_count"] = code_used_count
|
||
|
||
# 步骤5: 抽奖
|
||
lottery_result, is_lottery_proxy_error = self.step5_mask_code_lottery(final_s3_token, scan_code, scan_lat, scan_lon, current_proxy)
|
||
|
||
if not lottery_result and self.use_proxy and is_lottery_proxy_error:
|
||
logger.warning(f"⚠️ 抽奖代理错误,尝试更换代理...")
|
||
new_proxy = self.get_proxy()
|
||
if new_proxy:
|
||
current_proxy = new_proxy
|
||
logger.info(f"🌐 更换新代理: {current_proxy}")
|
||
lottery_result, _ = self.step5_mask_code_lottery(final_s3_token, scan_code, scan_lat, scan_lon, current_proxy)
|
||
|
||
if lottery_result is not None:
|
||
lucky_list = lottery_result.get("luckyRewardList", [])
|
||
# 只有抽中奖品(有luckyRewardList)才计入成功次数
|
||
if lucky_list:
|
||
success_lottery_count += 1
|
||
result["lottery_count"] = success_lottery_count
|
||
if lucky_list:
|
||
for prize in lucky_list:
|
||
prize_name = prize.get("rewardName", "未知")
|
||
reward_value = prize.get("rewardValue", 0)
|
||
user_reward_id = prize.get("userRewardId", "")
|
||
reward_type = prize.get("rewardType", "")
|
||
result["prizes"].append(prize_name)
|
||
logger.info(f"🎁 奖品: {prize_name}, 价值: {reward_value/100:.2f}元")
|
||
|
||
# 领取奖励(步骤6+7)- 只有红包类型且incentive=true(激活)才领取
|
||
custom_data = prize.get("customData", {})
|
||
incentive = custom_data.get("incentive", False)
|
||
if user_reward_id and reward_type == "redpacket":
|
||
if incentive:
|
||
logger.info(f"💰 红包已激活,执行领取流程")
|
||
claim_data, is_claim_error = self.step6_receive_reward(final_s3_token, user_reward_id, current_proxy)
|
||
if not claim_data and self.use_proxy and is_claim_error:
|
||
new_proxy = self.get_proxy()
|
||
if new_proxy:
|
||
current_proxy = new_proxy
|
||
claim_data, _ = self.step6_receive_reward(final_s3_token, user_reward_id, current_proxy)
|
||
if claim_data:
|
||
receive_data, is_receive_error = self.step7_claim_reward(final_s3_token, user_reward_id, current_proxy)
|
||
if not receive_data and self.use_proxy and is_receive_error:
|
||
new_proxy = self.get_proxy()
|
||
if new_proxy:
|
||
current_proxy = new_proxy
|
||
self.step7_claim_reward(final_s3_token, user_reward_id, current_proxy)
|
||
if receive_data:
|
||
result["total_amount"] += float(reward_value) / 100.0
|
||
logger.info(f"💰 红包已到账: {reward_value/100:.2f}元")
|
||
else:
|
||
logger.info(f"⏳ 红包未激活,等待下次扫码激活")
|
||
else:
|
||
logger.info(f"✅ 复购类型,不计入金额")
|
||
else:
|
||
biz_message = lottery_result.get("bizMessage", "")
|
||
logger.info(f"🎁 抽奖结果: {biz_message}")
|
||
else:
|
||
interval = random.uniform(self.lottery_interval_min, self.lottery_interval_max)
|
||
time.sleep(interval)
|
||
|
||
# ====== 抽奖完成,从文件删除码字 ======
|
||
self.commit_scan_code(scan_code)
|
||
|
||
# 间隔时间
|
||
if success_lottery_count < self.daily_lottery_limit:
|
||
interval = random.uniform(self.lottery_interval_min, self.lottery_interval_max)
|
||
time.sleep(interval)
|
||
|
||
result["success"] = result["lottery_count"] > 0
|
||
return result
|
||
|
||
def run(self):
|
||
"""运行主流程"""
|
||
if not self.wx_accounts:
|
||
logger.error("❌ 没有可用账号")
|
||
return
|
||
|
||
all_results = []
|
||
total_lottery = 0
|
||
total_amount = 0.0
|
||
success_count = 0
|
||
|
||
for idx, account in enumerate(self.wx_accounts):
|
||
nickname = account.get('wxName', '未知')
|
||
logger.info(f"\n========== 账号 {idx+1}/{len(self.wx_accounts)} 🏮 {nickname} ==========")
|
||
|
||
result = self.process_account(account)
|
||
all_results.append(result)
|
||
|
||
total_lottery += result["lottery_count"]
|
||
total_amount += result["total_amount"]
|
||
if result["success"]:
|
||
success_count += 1
|
||
|
||
account_cost = result.get("code_used_count", result["lottery_count"]) * LOTTERY_CODE_PRICE
|
||
account_profit = result["total_amount"] - account_cost
|
||
logger.info(f"💰 账号 {idx+1} 成功: {result['lottery_count']}次 | 用码: {result.get('code_used_count', 0)}个 | 金额: {result['total_amount']:.2f}元 | 成本: {account_cost:.2f}元 | 利润: {account_profit:.2f}元")
|
||
|
||
if self.codes_exhausted:
|
||
logger.warning(f"⚠️ 扫码码已用完,停止后续账号")
|
||
break
|
||
|
||
if idx < len(self.wx_accounts) - 1:
|
||
logger.info(f"⏳ 等待 {self.delay_between_accounts} 秒后处理下一个账号...")
|
||
time.sleep(self.delay_between_accounts)
|
||
|
||
self.print_summary(all_results, total_lottery, total_amount, success_count)
|
||
|
||
def print_summary(self, results: List[Dict], total_lottery: int, total_amount: float, success_count: int):
|
||
"""打印汇总报告"""
|
||
total_code_used = sum(r.get("code_used_count", r.get("lottery_count", 0)) for r in results)
|
||
|
||
logger.info("\n" + "="*50)
|
||
logger.info("📊 执行汇总")
|
||
logger.info("="*50)
|
||
logger.info(f"👥 总账号数: {len(results)}")
|
||
logger.info(f"✅ 成功账号: {success_count}")
|
||
logger.info(f"🎰 总成功抽奖: {total_lottery} 次")
|
||
logger.info(f"📱 总用码: {total_code_used} 个")
|
||
logger.info(f"💰 总金额: {total_amount:.2f} 元")
|
||
|
||
cost = total_code_used * LOTTERY_CODE_PRICE
|
||
profit = total_amount - cost
|
||
logger.info(f"💵 成本: {cost:.2f} 元")
|
||
logger.info(f"📈 利润: {profit:.2f} 元")
|
||
|
||
logger.info("\n📋 账号详情:")
|
||
for i, r in enumerate(results):
|
||
status = "✅" if r["success"] else "❌"
|
||
prize_info = ", ".join(r["prizes"]) if r["prizes"] else "无"
|
||
logger.info(f" {status} 账号{i+1}: {r['nickname']} | 抽奖: {r['lottery_count']}次 | 金额: {r['total_amount']:.2f}元 | 奖品: {prize_info}")
|
||
|
||
|
||
def main():
|
||
"""主入口"""
|
||
automation = WangLaoJiAutomation()
|
||
automation.run()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|