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)
|