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

102 lines
3.6 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.
"""
购物车预售商品抓取服务
通过 Playwright 打开购物车页面,从 DOM 的 item_warp 提取商品信息
"""
import asyncio
from playwright.async_api import async_playwright
from utils.stealth import stealth_async
from server.services.auth_service import get_browser_context, has_auth
CART_URL = "https://weidian.com/new-cart/index.php"
# 提取购物车商品的 JS与 test_cart.py 保持一致
EXTRACT_JS = """() => {
const R = [];
const sws = document.querySelectorAll(
'div.shop_info.cart_content div.shop_warp'
);
for (const sw of sws) {
const sn = (sw.querySelector('.shop_name') || {}).textContent || '';
const iws = sw.querySelectorAll('.item_warp');
for (const iw of iws) {
const o = {
shop_name: sn.trim(),
cart_item_id: iw.id,
title: '', sku_name: '', price: '',
is_presale: false, countdown_text: '',
sale_time: '', presale_type: ''
};
const te = iw.querySelector('.item_title');
if (te) o.title = te.textContent.trim();
const sk = iw.querySelector('.item_sku');
if (sk) o.sku_name = sk.textContent.trim();
const pr = iw.querySelector('.item_prices');
if (pr) o.price = pr.textContent.replace(/[^\\d.]/g, '');
const de = iw.querySelector('.item_desc');
if (de) {
const dt = de.querySelector('.title');
const dd = de.querySelector('.desc');
const wm = de.querySelector('.warn_msg');
if (dt && /\\u5b9a\\u65f6\\s*\\u5f00\\u552e/.test(dt.textContent)) {
o.is_presale = true;
const d = dd ? dd.textContent.trim() : '';
const w = wm ? wm.textContent.trim() : '';
if (d.includes('\\u8ddd\\u79bb\\u5f00\\u552e\\u8fd8\\u5269')) {
o.presale_type = 'countdown';
o.countdown_text = w;
} else if (d.includes('\\u5f00\\u552e\\u65f6\\u95f4')) {
o.presale_type = 'scheduled';
o.sale_time = w;
} else {
o.presale_type = 'unknown';
o.countdown_text = w;
}
}
}
R.push(o);
}
}
return R;
}"""
async def fetch_cart_presale_items(account_id):
"""
获取指定账号购物车中的预售商品列表
返回: (success, items_or_msg)
"""
if not has_auth(account_id):
return False, "账号未登录"
async with async_playwright() as p:
browser, context = await get_browser_context(
p, account_id, headless=True
)
page = await context.new_page()
await stealth_async(page)
try:
await page.goto(
CART_URL, wait_until="networkidle", timeout=20000
)
await asyncio.sleep(3)
if "login" in page.url.lower():
await browser.close()
return False, "登录态已过期,请重新登录"
if "error" in page.url.lower():
await browser.close()
return False, "购物车页面加载失败"
except Exception as e:
await browser.close()
return False, f"打开购物车失败: {e}"
raw_items = await page.evaluate(EXTRACT_JS)
await browser.close()
# 只返回预售商品
presale = [it for it in raw_items if it.get("is_presale")]
return True, presale