Files
weidian/server/routers/tasks.py

277 lines
11 KiB
Python
Raw Normal View History

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)