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

277 lines
11 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 threading
from datetime import datetime, timedelta
from flask import Blueprint, request, jsonify, render_template
from server.database import get_db
from server.services.scheduler import start_task, stop_task, get_running_task_ids, start_account_tasks
bp = Blueprint('tasks', __name__, url_prefix='/tasks')
@bp.route('/')
def list_tasks():
db = get_db()
tasks = db.execute('''
SELECT t.*, a.name as account_name
FROM tasks t LEFT JOIN accounts a ON t.account_id = a.id
ORDER BY t.account_id, t.snatch_time ASC
''').fetchall()
accounts = db.execute('SELECT id, name, is_logged_in FROM accounts').fetchall()
running_ids = get_running_task_ids()
db.close()
# 按账号分组
grouped = {}
for t in tasks:
aid = t['account_id']
if aid not in grouped:
grouped[aid] = {'name': t['account_name'] or f'账号#{aid}', 'tasks': []}
grouped[aid]['tasks'].append(t)
return render_template('tasks.html', grouped=grouped, accounts=accounts, running_ids=running_ids)
@bp.route('/add', methods=['POST'])
def add_task():
data = request.form
account_id = data.get('account_id')
target_url = data.get('target_url', '').strip()
snatch_time = data.get('snatch_time', '').strip()
item_name = data.get('item_name', '').strip()
item_id = data.get('item_id', '').strip()
sku_id = data.get('sku_id', '').strip()
price = data.get('price', '').strip()
if not account_id or not target_url or not snatch_time:
return jsonify(success=False, msg='请填写必要字段'), 400
db = get_db()
db.execute(
'INSERT INTO tasks (account_id, target_url, item_name, item_id, sku_id, price, snatch_time) VALUES (?, ?, ?, ?, ?, ?, ?)',
(account_id, target_url, item_name, item_id, sku_id, price, snatch_time)
)
db.commit()
db.close()
return jsonify(success=True)
@bp.route('/start/<int:task_id>', methods=['POST'])
def start(task_id):
ok, msg = start_task(task_id)
return jsonify(success=ok, msg=msg)
@bp.route('/stop/<int:task_id>', methods=['POST'])
def stop(task_id):
ok, msg = stop_task(task_id)
return jsonify(success=ok, msg=msg)
@bp.route('/delete/<int:task_id>', methods=['POST'])
def delete_task(task_id):
db = get_db()
db.execute('DELETE FROM tasks WHERE id = ?', (task_id,))
db.commit()
db.close()
return jsonify(success=True)
@bp.route('/sync_cart/<int:account_id>', methods=['POST'])
def sync_cart(account_id):
"""同步购物车预售商品,自动创建抢购任务"""
from server.services.cart_service import fetch_cart_presale_items
db = get_db()
account = db.execute('SELECT * FROM accounts WHERE id = ?', (account_id,)).fetchone()
db.close()
if not account:
return jsonify(success=False, msg='账号不存在'), 404
if not account['is_logged_in']:
return jsonify(success=False, msg='请先登录该账号'), 400
result = {'success': False, 'msg': '同步超时', 'count': 0}
def _run():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
ok, data = loop.run_until_complete(fetch_cart_presale_items(account_id))
if ok:
items = data
db2 = get_db()
added = 0
for item in items:
sale_time = item.get('sale_time') or ''
countdown = item.get('countdown_text') or ''
# 统一转为绝对时间 YYYY-MM-DD HH:MM:SS
snatch_time = ''
if sale_time:
# "2026.03.19 20:00" -> "2026-03-19 20:00:00"
try:
st = sale_time.replace('.', '-').replace('/', '-')
dt = datetime.strptime(st, '%Y-%m-%d %H:%M')
snatch_time = dt.strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
snatch_time = sale_time
elif countdown:
# "22:47:46" -> now + countdown
try:
parts = countdown.split(':')
h, m, s = int(parts[0]), int(parts[1]), int(parts[2])
dt = datetime.now() + timedelta(hours=h, minutes=m, seconds=s)
snatch_time = dt.strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, IndexError):
pass
if not snatch_time:
continue
cart_item_id = item.get('cart_item_id', '')
item_id = item.get('item_id', '') or cart_item_id
title = item.get('title', '')
# 用 cart_item_id 去重(因为可能没有 item_id
dedup_key = item_id or title
if dedup_key:
existing = db2.execute(
'SELECT id FROM tasks WHERE account_id = ? AND (item_id = ? OR item_name = ?) AND status = "pending"',
(account_id, dedup_key, title)
).fetchone()
if existing:
continue
url = item.get('url', '')
if not url and item_id and item_id.isdigit():
url = f'https://weidian.com/item.html?itemID={item_id}'
db2.execute(
'INSERT INTO tasks (account_id, target_url, item_name, item_id, sku_id, price, snatch_time) VALUES (?, ?, ?, ?, ?, ?, ?)',
(account_id, url, title, item_id,
item.get('sku_name', ''), item.get('price', ''), snatch_time)
)
added += 1
db2.commit()
db2.close()
result['success'] = True
result['msg'] = f'同步完成,新增 {added} 个任务' if added else '购物车中没有新的预售商品'
result['count'] = added
else:
result['msg'] = data
except Exception as e:
result['msg'] = str(e)
finally:
loop.close()
t = threading.Thread(target=_run, daemon=True)
t.start()
t.join(timeout=30)
return jsonify(**result)
@bp.route('/sync_all', methods=['POST'])
def sync_all_carts():
"""同步所有已登录账号的购物车"""
db = get_db()
accounts = db.execute('SELECT id, name, phone, password FROM accounts WHERE is_logged_in = 1').fetchall()
db.close()
if not accounts:
return jsonify(success=False, msg='没有已登录的账号')
from server.services.cart_service import fetch_cart_presale_items
total_added = 0
errors = []
def _run():
nonlocal total_added
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
for acc in accounts:
aid = acc['id']
try:
ok, data = loop.run_until_complete(fetch_cart_presale_items(aid))
if not ok:
errors.append(f"{acc['name']}: {data}")
continue
db2 = get_db()
for item in data:
sale_time = item.get('sale_time') or ''
countdown = item.get('countdown_text') or ''
snatch_time = ''
if sale_time:
try:
st = sale_time.replace('.', '-').replace('/', '-')
dt = datetime.strptime(st, '%Y-%m-%d %H:%M')
snatch_time = dt.strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
snatch_time = sale_time
elif countdown:
try:
parts = countdown.split(':')
h, m, s = int(parts[0]), int(parts[1]), int(parts[2])
dt = datetime.now() + timedelta(hours=h, minutes=m, seconds=s)
snatch_time = dt.strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, IndexError):
pass
if not snatch_time:
continue
cart_item_id = item.get('cart_item_id', '')
item_id = item.get('item_id', '') or cart_item_id
title = item.get('title', '')
dedup_key = item_id or title
if dedup_key:
existing = db2.execute(
'SELECT id FROM tasks WHERE account_id = ? AND (item_id = ? OR item_name = ?) AND status = "pending"',
(aid, dedup_key, title)
).fetchone()
if existing:
continue
url = item.get('url', '')
if not url and item_id and item_id.isdigit():
url = f'https://weidian.com/item.html?itemID={item_id}'
db2.execute(
'INSERT INTO tasks (account_id, target_url, item_name, item_id, sku_id, price, snatch_time) VALUES (?, ?, ?, ?, ?, ?, ?)',
(aid, url, title, item_id,
item.get('sku_name', ''), item.get('price', ''), snatch_time)
)
total_added += 1
db2.commit()
db2.close()
except Exception as e:
errors.append(f"{acc['name']}: {e}")
finally:
loop.close()
t = threading.Thread(target=_run, daemon=True)
t.start()
t.join(timeout=60)
msg = f'同步完成,新增 {total_added} 个任务'
if errors:
msg += f'{len(errors)} 个账号出错)'
return jsonify(success=True, msg=msg, count=total_added)
@bp.route('/start_all', methods=['POST'])
def start_all_pending():
"""按账号隔离启动所有 pending 任务"""
db = get_db()
account_ids = db.execute(
"SELECT DISTINCT account_id FROM tasks WHERE status = 'pending'"
).fetchall()
db.close()
started = 0
for row in account_ids:
ok, _ = start_account_tasks(row['account_id'])
if ok:
started += 1
return jsonify(success=True, msg=f'已启动 {started} 个账号的任务')
@bp.route('/start_account/<int:account_id>', methods=['POST'])
def start_account(account_id):
"""启动指定账号的所有 pending 任务"""
ok, msg = start_account_tasks(account_id)
return jsonify(success=ok, msg=msg)