- 多账号管理(异步登录、状态轮询) - 购物车预售商品同步(倒计时/定时开售) - 定时抢购(自动刷新、SKU选择、重试机制) - 账号隔离调度(同账号顺序、跨账号并行) - Web面板(任务分组、实时倒计时、批量操作) - Dockerfile + docker-compose
277 lines
11 KiB
Python
277 lines
11 KiB
Python
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)
|