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/', methods=['POST']) def start(task_id): ok, msg = start_task(task_id) return jsonify(success=ok, msg=msg) @bp.route('/stop/', methods=['POST']) def stop(task_id): ok, msg = stop_task(task_id) return jsonify(success=ok, msg=msg) @bp.route('/delete/', 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/', 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', '') title = item.get('title', '') # 去重:优先用 item_id,其次 cart_item_id,最后 title dedup_key = item_id or cart_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', '') # 只有真正的 itemID 才拼 URL(cart_item_id 不是 itemID) if not url and item_id and item_id.isdigit(): url = f'https://weidian.com/item.html?itemID={item_id}' if not item_id: item_id = cart_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', '') title = item.get('title', '') dedup_key = item_id or cart_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}' if not item_id: item_id = cart_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/', methods=['POST']) def start_account(account_id): """启动指定账号的所有 pending 任务""" ok, msg = start_account_tasks(account_id) return jsonify(success=ok, msg=msg)