- 多账号管理(异步登录、状态轮询) - 购物车预售商品同步(倒计时/定时开售) - 定时抢购(自动刷新、SKU选择、重试机制) - 账号隔离调度(同账号顺序、跨账号并行) - Web面板(任务分组、实时倒计时、批量操作) - Dockerfile + docker-compose
183 lines
6.9 KiB
Python
183 lines
6.9 KiB
Python
import asyncio
|
||
import json
|
||
import os
|
||
from playwright.async_api import async_playwright
|
||
from utils.stealth import stealth_async
|
||
|
||
AUTH_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'auth')
|
||
LOGIN_URL = "https://sso.weidian.com/login/index.php"
|
||
|
||
|
||
def get_auth_path(account_id):
|
||
os.makedirs(AUTH_DIR, exist_ok=True)
|
||
return os.path.join(AUTH_DIR, f'auth_state_{account_id}.json')
|
||
|
||
|
||
def has_auth(account_id):
|
||
path = get_auth_path(account_id)
|
||
return os.path.exists(path) and os.path.getsize(path) > 10
|
||
|
||
|
||
async def login_with_password(account_id, phone, password):
|
||
"""
|
||
用 Playwright 模拟浏览器登录微店,通过监听 API 响应提取 cookie。
|
||
流程:
|
||
1. 打开登录页
|
||
2. 点击 #login_init_by_login 进入登录表单
|
||
3. 点击"账号密码登录" tab
|
||
4. 填写手机号、密码
|
||
5. 点击 #login_pwd_submit
|
||
6. 监听 /user/login 响应,从中提取 cookie 并保存
|
||
"""
|
||
login_result = {'success': False, 'msg': '登录超时', 'cookies': []}
|
||
|
||
p = await async_playwright().start()
|
||
browser = await p.chromium.launch(
|
||
headless=True, args=['--disable-gpu', '--no-sandbox']
|
||
)
|
||
device = p.devices['iPhone 13']
|
||
context = await browser.new_context(**device)
|
||
page = await context.new_page()
|
||
await stealth_async(page)
|
||
|
||
# 监听登录接口响应
|
||
async def on_response(response):
|
||
if 'user/login' in response.url and response.status == 200:
|
||
try:
|
||
data = await response.json()
|
||
status = data.get('status', {})
|
||
if status.get('status_code') == 0:
|
||
login_result['success'] = True
|
||
login_result['msg'] = '登录成功'
|
||
login_result['cookies'] = data.get('result', {}).get('cookie', [])
|
||
else:
|
||
login_result['msg'] = f"登录失败: {status.get('status_reason', '未知错误')}"
|
||
except Exception as e:
|
||
login_result['msg'] = f"解析登录响应失败: {e}"
|
||
|
||
page.on("response", on_response)
|
||
|
||
try:
|
||
await page.goto(LOGIN_URL, wait_until='networkidle', timeout=15000)
|
||
await asyncio.sleep(1)
|
||
|
||
# 点击"登录"进入表单
|
||
await page.locator('#login_init_by_login').click(timeout=5000)
|
||
await asyncio.sleep(1.5)
|
||
|
||
# 点击"账号密码登录" tab
|
||
try:
|
||
await page.locator('h4.login_content_h4 span', has_text="账号密码登录").click(timeout=3000)
|
||
await asyncio.sleep(0.5)
|
||
except Exception:
|
||
pass
|
||
|
||
# 填写手机号(逐字输入,触发 JS 事件)
|
||
phone_input = page.locator('input[placeholder*="手机号"]').first
|
||
await phone_input.click()
|
||
await phone_input.fill("")
|
||
await page.keyboard.type(phone, delay=50)
|
||
|
||
# 填写密码
|
||
pwd_input = page.locator('input[placeholder*="登录密码"], input[type="password"]').first
|
||
await pwd_input.click()
|
||
await pwd_input.fill("")
|
||
await page.keyboard.type(password, delay=50)
|
||
await asyncio.sleep(0.5)
|
||
|
||
# 点击登录
|
||
await page.locator('#login_pwd_submit').click(timeout=5000)
|
||
|
||
# 等待 API 响应
|
||
await asyncio.sleep(5)
|
||
|
||
if login_result['success'] and login_result['cookies']:
|
||
# 从 API 响应中提取 cookie,写入 context
|
||
for c in login_result['cookies']:
|
||
await context.add_cookies([{
|
||
"name": c.get("name", ""),
|
||
"value": c.get("value", ""),
|
||
"domain": c.get("domain", ".weidian.com"),
|
||
"path": c.get("path", "/"),
|
||
"httpOnly": c.get("httpOnly", False),
|
||
"secure": c.get("secure", False),
|
||
"sameSite": "Lax",
|
||
}])
|
||
|
||
# 保存完整的 storage_state
|
||
auth_path = get_auth_path(account_id)
|
||
await context.storage_state(path=auth_path)
|
||
return True, "登录成功"
|
||
|
||
return False, login_result['msg']
|
||
|
||
except Exception as e:
|
||
return False, f"登录过程出错: {e}"
|
||
finally:
|
||
await browser.close()
|
||
await p.stop()
|
||
|
||
|
||
async def login_with_api(account_id, phone, password):
|
||
"""
|
||
通过微店 SSO API 直接登录(备选方案,速度快但更容易触发风控)。
|
||
"""
|
||
import aiohttp
|
||
|
||
login_api = "https://sso.weidian.com/user/login"
|
||
headers = {
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) "
|
||
"AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
|
||
"Referer": "https://sso.weidian.com/login/index.php",
|
||
"Origin": "https://sso.weidian.com",
|
||
}
|
||
payload = {"phone": phone, "password": password, "loginMode": "password"}
|
||
|
||
try:
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.post(login_api, data=payload, headers=headers) as resp:
|
||
data = await resp.json()
|
||
status_code = data.get("status", {}).get("status_code", -1)
|
||
status_reason = data.get("status", {}).get("status_reason", "未知错误")
|
||
|
||
if status_code == 0:
|
||
api_cookies = data.get("result", {}).get("cookie", [])
|
||
pw_cookies = []
|
||
for c in api_cookies:
|
||
pw_cookies.append({
|
||
"name": c.get("name", ""),
|
||
"value": c.get("value", ""),
|
||
"domain": c.get("domain", ".weidian.com"),
|
||
"path": c.get("path", "/"),
|
||
"expires": -1,
|
||
"httpOnly": c.get("httpOnly", False),
|
||
"secure": c.get("secure", False),
|
||
"sameSite": "Lax",
|
||
})
|
||
state = {"cookies": pw_cookies, "origins": []}
|
||
auth_path = get_auth_path(account_id)
|
||
with open(auth_path, 'w', encoding='utf-8') as f:
|
||
json.dump(state, f, ensure_ascii=False, indent=2)
|
||
return True, "API登录成功"
|
||
else:
|
||
return False, f"API登录失败: {status_reason}"
|
||
except Exception as e:
|
||
return False, f"API登录出错: {e}"
|
||
|
||
|
||
async def get_browser_context(playwright_instance, account_id, headless=True):
|
||
"""创建带有已保存登录状态的浏览器上下文"""
|
||
browser = await playwright_instance.chromium.launch(
|
||
headless=headless, args=['--disable-gpu', '--no-sandbox']
|
||
)
|
||
device = playwright_instance.devices['iPhone 13']
|
||
auth_path = get_auth_path(account_id)
|
||
|
||
if has_auth(account_id):
|
||
context = await browser.new_context(**device, storage_state=auth_path)
|
||
else:
|
||
context = await browser.new_context(**device)
|
||
|
||
return browser, context
|