Files
weidian/server/services/auth_service.py
Jeason 822a4636c0 feat: Web管理系统 + Docker支持
- 多账号管理(异步登录、状态轮询)
- 购物车预售商品同步(倒计时/定时开售)
- 定时抢购(自动刷新、SKU选择、重试机制)
- 账号隔离调度(同账号顺序、跨账号并行)
- Web面板(任务分组、实时倒计时、批量操作)
- Dockerfile + docker-compose
2026-03-18 13:38:17 +08:00

183 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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