From 031e08570266d1797177677cb4fdb6bc8fd9ccce Mon Sep 17 00:00:00 2001 From: GuDong <1735590698@qq.com> Date: Mon, 25 May 2026 21:27:02 +0800 Subject: [PATCH] =?UTF-8?q?add(=E6=B1=82=E5=B0=8F=E7=BA=A2=E8=8A=B1):=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=87=AA=E5=8A=A8=E6=B1=82=E5=B0=8F=E7=BA=A2?= =?UTF-8?q?=E8=8A=B1=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto_red_flower_task.py | 185 ++++++++++++++++++++++++++++ db_manager.py | 239 +++++++++++++++++++++++++++++++++++- reply_server.py | 172 ++++++++++++++++++++++++++ static/css/accounts.css | 168 +++++++++++++++++++++++++ static/index.html | 20 +-- static/js/app.js | 173 ++++++++++++++++++++------ utils/red_flower_service.py | 216 ++++++++++++++++++++++++++++++++ 7 files changed, 1120 insertions(+), 53 deletions(-) create mode 100644 auto_red_flower_task.py create mode 100644 utils/red_flower_service.py diff --git a/auto_red_flower_task.py b/auto_red_flower_task.py new file mode 100644 index 0000000..7afe09f --- /dev/null +++ b/auto_red_flower_task.py @@ -0,0 +1,185 @@ +"""自动求小红花任务。 + +轻量本地调度版本:不引入上游独立 scheduler 服务,直接复用当前 SQLite、Cookie 和订单数据。 +""" + +from __future__ import annotations + +import asyncio +import os +import time +import uuid +from typing import Any, Dict, Optional + +from loguru import logger + +from db_manager import db_manager +from utils.red_flower_service import RedFlowerService + + +DEFAULT_INTERVAL_SECONDS = int(os.getenv("AUTO_RED_FLOWER_TASK_INTERVAL_SECONDS", "300") or 300) +DEFAULT_BATCH_LIMIT = int(os.getenv("AUTO_RED_FLOWER_TASK_BATCH_LIMIT", "5") or 5) +DEFAULT_LOOKBACK_DAYS = int(os.getenv("AUTO_RED_FLOWER_TASK_LOOKBACK_DAYS", "10") or 10) +DEFAULT_COOLDOWN_MINUTES = int(os.getenv("AUTO_RED_FLOWER_TASK_COOLDOWN_MINUTES", "30") or 30) + + +def _status_from_result(result: Dict[str, Any]) -> str: + if result.get("success"): + return "success" + if result.get("session_expired"): + return "cookie_expired" + return "failed" + + +async def request_red_flower_once( + cookie_id: str, + order_id: str, + *, + batch_id: Optional[str] = None, + source: str = "manual", +) -> Dict[str, Any]: + """对单个订单执行一次求小红花,并写入日志。""" + cookie_id = str(cookie_id or "").strip() + order_id = str(order_id or "").strip() + batch_id = batch_id or f"{source}_{uuid.uuid4()}" + + order_info = db_manager.get_order_by_id(order_id) + if not order_info: + message = "订单不存在" + db_manager.add_scheduled_red_flower_log(batch_id, cookie_id, order_id=order_id, status="skipped", message=message) + return {"success": False, "message": message, "status": "skipped"} + + order_cookie_id = str(order_info.get("cookie_id") or "").strip() + if order_cookie_id and order_cookie_id != cookie_id: + message = "订单不属于当前账号" + db_manager.add_scheduled_red_flower_log( + batch_id, cookie_id, order_id=order_id, item_id=order_info.get("item_id"), + buyer_id=order_info.get("buyer_id"), buyer_nick=order_info.get("buyer_nick"), + status="skipped", message=message, + ) + return {"success": False, "message": message, "status": "skipped"} + + if order_info.get("is_red_flower"): + message = "订单已标记为已求小红花" + db_manager.add_scheduled_red_flower_log( + batch_id, cookie_id, order_id=order_id, item_id=order_info.get("item_id"), + buyer_id=order_info.get("buyer_id"), buyer_nick=order_info.get("buyer_nick"), + status="already_red_flower", message=message, + ) + return {"success": True, "message": message, "status": "already_red_flower", "already_red_flower": True} + + cookie_string = db_manager.get_cookie(cookie_id) + if not cookie_string: + message = "账号 Cookie 为空或不存在" + db_manager.add_scheduled_red_flower_log( + batch_id, cookie_id, order_id=order_id, item_id=order_info.get("item_id"), + buyer_id=order_info.get("buyer_id"), buyer_nick=order_info.get("buyer_nick"), + status="cookie_expired", message=message, + ) + return {"success": False, "message": message, "status": "cookie_expired"} + + service = RedFlowerService(cookie_string, account_id=cookie_id) + result = await service.request_red_flower(order_id) + status = _status_from_result(result) + message = str(result.get("message") or "") + + db_manager.add_scheduled_red_flower_log( + batch_id=batch_id, + cookie_id=cookie_id, + order_id=order_id, + item_id=order_info.get("item_id"), + buyer_id=order_info.get("buyer_id"), + buyer_nick=order_info.get("buyer_nick"), + status=status, + message=message, + raw_response=result.get("raw") or result, + ) + + if result.get("success"): + db_manager.mark_order_red_flower(order_id, True) + else: + db_manager.mark_order_red_flower(order_id, False, message) + + return { + "success": bool(result.get("success")), + "message": message, + "status": status, + "order_id": order_id, + "cookie_id": cookie_id, + "already_red_flower": bool(result.get("already_red_flower")), + } + + +async def run_auto_red_flower_batch( + *, + batch_limit: int = DEFAULT_BATCH_LIMIT, + lookback_days: int = DEFAULT_LOOKBACK_DAYS, + cooldown_minutes: int = DEFAULT_COOLDOWN_MINUTES, +) -> Dict[str, Any]: + """执行一轮自动求小红花。""" + batch_id = str(uuid.uuid4()) + started_at = time.time() + stats = { + "batch_id": batch_id, + "accounts": 0, + "orders": 0, + "success": 0, + "failed": 0, + "skipped": 0, + } + + all_cookies = db_manager.get_all_cookies() + for cookie_id in list(all_cookies.keys()): + try: + if not db_manager.get_auto_red_flower(cookie_id): + continue + stats["accounts"] += 1 + + orders = db_manager.get_pending_red_flower_orders( + cookie_id, + limit=batch_limit, + days=lookback_days, + cooldown_minutes=cooldown_minutes, + ) + if not orders: + continue + + logger.info(f"【{cookie_id}】自动求小红花找到 {len(orders)} 个待处理订单") + for order in orders: + stats["orders"] += 1 + result = await request_red_flower_once( + cookie_id, + order.get("order_id"), + batch_id=batch_id, + source="scheduled_red_flower", + ) + if result.get("success"): + stats["success"] += 1 + elif result.get("status") in {"skipped", "already_red_flower"}: + stats["skipped"] += 1 + else: + stats["failed"] += 1 + await asyncio.sleep(1) + except Exception as exc: + stats["failed"] += 1 + logger.error(f"【{cookie_id}】自动求小红花账号处理异常: {exc}") + + stats["duration_seconds"] = round(time.time() - started_at, 2) + if stats["orders"]: + logger.info(f"自动求小红花批次完成: {stats}") + return stats + + +async def auto_red_flower_task_loop(interval_seconds: int = DEFAULT_INTERVAL_SECONDS): + """后台自动求小红花循环。""" + interval_seconds = max(60, int(interval_seconds or DEFAULT_INTERVAL_SECONDS)) + logger.info(f"自动求小红花任务已启动,检查间隔 {interval_seconds} 秒") + while True: + try: + await run_auto_red_flower_batch() + except asyncio.CancelledError: + logger.info("自动求小红花任务已取消") + raise + except Exception as exc: + logger.error(f"自动求小红花任务异常: {exc}") + await asyncio.sleep(interval_seconds) diff --git a/db_manager.py b/db_manager.py index 4e88529..cf822e4 100644 --- a/db_manager.py +++ b/db_manager.py @@ -323,6 +323,7 @@ class DBManager: value TEXT NOT NULL, user_id INTEGER NOT NULL, auto_confirm INTEGER DEFAULT 1, + auto_red_flower INTEGER DEFAULT 0, remark TEXT DEFAULT '', status_note TEXT DEFAULT '', qr_login_grace_until INTEGER DEFAULT 0, @@ -471,6 +472,9 @@ class DBManager: is_rated INTEGER DEFAULT 0, rated_at TIMESTAMP, rate_error TEXT, + is_red_flower INTEGER DEFAULT 0, + red_flower_at TIMESTAMP, + red_flower_error TEXT, cookie_id TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -941,6 +945,7 @@ Cookie数量: {cookie_count} ('verification_email_api_url', '', '验证码邮件 API 地址(留空则仅使用 SMTP,不再向旧硬编码地址外发)'), ('qq_notification_api_url', '', 'QQ 私信通知 API 地址(留空则禁用 QQ 私信通知)'), ('auto_comment_api_url', '', '自动好评辅助 API 地址(留空则禁用此功能,避免 Cookie 外发)'), + ('auto_red_flower_interval_seconds', '300', '自动求小红花后台任务检查间隔秒数'), ('qq_reply_secret_key', 'xianyu_qq_reply_2024', 'QQ回复消息API秘钥') ''') @@ -1003,10 +1008,16 @@ Cookie数量: {cookie_count} cursor.execute("ALTER TABLE cookies ADD COLUMN auto_comment INTEGER DEFAULT 0") logger.info("数据库迁移完成:添加auto_comment列") + if 'auto_red_flower' not in cookie_columns: + logger.info("添加cookies表的auto_red_flower列...") + cursor.execute("ALTER TABLE cookies ADD COLUMN auto_red_flower INTEGER DEFAULT 0") + logger.info("数据库迁移完成:添加auto_red_flower列") + # 历史版本可能缺少订单平台时间字段,不能再依赖旧版本号分支触发 self._ensure_orders_platform_time_columns(cursor) self._ensure_orders_auto_comment_columns(cursor) self._ensure_scheduled_rate_logs_table(cursor) + self._ensure_scheduled_red_flower_logs_table(cursor) # 迁移notification_templates表以支持新的模板类型 self._migrate_notification_templates(cursor) @@ -1079,6 +1090,9 @@ Cookie数量: {cookie_count} "is_rated": "INTEGER DEFAULT 0", "rated_at": "TIMESTAMP", "rate_error": "TEXT", + "is_red_flower": "INTEGER DEFAULT 0", + "red_flower_at": "TIMESTAMP", + "red_flower_error": "TEXT", } for column_name, column_def in column_defs.items(): try: @@ -1087,6 +1101,7 @@ Cookie数量: {cookie_count} self._execute_sql(cursor, f"ALTER TABLE orders ADD COLUMN {column_name} {column_def}") logger.info(f"为orders表添加自动评价字段({column_name})") self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_orders_auto_comment ON orders(cookie_id, order_status, is_rated, updated_at)") + self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_orders_red_flower ON orders(cookie_id, order_status, is_red_flower, updated_at)") def _ensure_scheduled_rate_logs_table(self, cursor): """创建自动评价执行日志表。""" @@ -1111,6 +1126,28 @@ Cookie数量: {cookie_count} self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_rate_logs_batch ON scheduled_rate_logs(batch_id)") self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_rate_logs_order ON scheduled_rate_logs(order_id)") + def _ensure_scheduled_red_flower_logs_table(self, cursor): + """创建求小红花执行日志表。""" + self._execute_sql(cursor, ''' + CREATE TABLE IF NOT EXISTS scheduled_red_flower_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + batch_id TEXT NOT NULL, + cookie_id TEXT NOT NULL, + order_id TEXT, + item_id TEXT, + buyer_id TEXT, + buyer_nick TEXT, + status TEXT NOT NULL, + message TEXT, + raw_response TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE + ) + ''') + self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_red_flower_logs_cookie_time ON scheduled_red_flower_logs(cookie_id, created_at DESC)") + self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_red_flower_logs_batch ON scheduled_red_flower_logs(batch_id)") + self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_scheduled_red_flower_logs_order ON scheduled_red_flower_logs(order_id)") + def _update_cards_table_constraints(self, cursor): """更新cards表的CHECK约束以支持image和yifan_api类型""" try: @@ -2498,6 +2535,39 @@ Cookie数量: {cookie_count} logger.error(f"获取自动确认发货设置失败: {e}") return True # 出错时默认开启 + # -------------------- 自动求小红花操作 -------------------- + def get_auto_red_flower(self, cookie_id: str) -> bool: + """获取Cookie的自动求小红花设置。""" + with self.lock: + try: + cursor = self.conn.cursor() + self._execute_sql(cursor, "SELECT auto_red_flower FROM cookies WHERE id = ?", (cookie_id,)) + result = cursor.fetchone() + if result and result[0] is not None: + return bool(result[0]) + return False + except Exception as e: + logger.error(f"获取自动求小红花设置失败: {e}") + return False + + def update_auto_red_flower(self, cookie_id: str, auto_red_flower: bool) -> bool: + """更新Cookie的自动求小红花设置。""" + with self.lock: + try: + cursor = self.conn.cursor() + self._execute_sql( + cursor, + "UPDATE cookies SET auto_red_flower = ? WHERE id = ?", + (int(auto_red_flower), cookie_id) + ) + self.conn.commit() + logger.info(f"更新账号 {cookie_id} 自动求小红花设置: {'开启' if auto_red_flower else '关闭'}") + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"更新自动求小红花设置失败: {e}") + self.conn.rollback() + return False + # -------------------- 自动好评操作 -------------------- def get_auto_comment(self, cookie_id: str) -> bool: """获取Cookie的自动好评设置""" @@ -7049,7 +7119,8 @@ Cookie数量: {cookie_count} SELECT order_id, item_id, buyer_id, buyer_nick, sid, spec_name, spec_value, spec_name_2, spec_value_2, quantity, amount, bargain_flow_detected, bargain_success_detected, order_status, pre_refund_status, cookie_id, platform_created_at, platform_paid_at, - platform_completed_at, is_rated, rated_at, rate_error, created_at, updated_at + platform_completed_at, is_rated, rated_at, rate_error, + is_red_flower, red_flower_at, red_flower_error, created_at, updated_at FROM orders WHERE order_id = ? ''', (order_id,)) @@ -7078,8 +7149,11 @@ Cookie数量: {cookie_count} 'is_rated': bool(row[19]), 'rated_at': row[20], 'rate_error': row[21], - 'created_at': row[22], - 'updated_at': row[23] + 'is_red_flower': bool(row[22]), + 'red_flower_at': row[23], + 'red_flower_error': row[24], + 'created_at': row[25], + 'updated_at': row[26] } return None @@ -7141,7 +7215,8 @@ Cookie数量: {cookie_count} SELECT order_id, item_id, buyer_id, buyer_nick, sid, spec_name, spec_value, spec_name_2, spec_value_2, quantity, amount, order_status, platform_created_at, platform_paid_at, platform_completed_at, - is_rated, rated_at, rate_error, created_at, updated_at + is_rated, rated_at, rate_error, + is_red_flower, red_flower_at, red_flower_error, created_at, updated_at FROM orders WHERE cookie_id = ? ORDER BY created_at DESC LIMIT ? ''', (cookie_id, limit)) @@ -7170,8 +7245,11 @@ Cookie数量: {cookie_count} 'is_rated': bool(row[15]), 'rated_at': row[16], 'rate_error': row[17], - 'created_at': row[18], - 'updated_at': row[19] + 'is_red_flower': bool(row[18]), + 'red_flower_at': row[19], + 'red_flower_error': row[20], + 'created_at': row[21], + 'updated_at': row[22] }) return orders @@ -7204,6 +7282,30 @@ Cookie数量: {cookie_count} self.conn.rollback() return False + def mark_order_red_flower(self, order_id: str, is_red_flower: bool = True, error_message: str = None) -> bool: + """更新订单求小红花状态。""" + with self.lock: + try: + cursor = self.conn.cursor() + if is_red_flower: + cursor.execute(''' + UPDATE orders + SET is_red_flower = 1, red_flower_at = CURRENT_TIMESTAMP, red_flower_error = NULL, updated_at = CURRENT_TIMESTAMP + WHERE order_id = ? + ''', (order_id,)) + else: + cursor.execute(''' + UPDATE orders + SET red_flower_error = ?, updated_at = CURRENT_TIMESTAMP + WHERE order_id = ? + ''', (str(error_message or '')[:1000], order_id)) + self.conn.commit() + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"更新订单求小红花状态失败: order_id={order_id}, error={e}") + self.conn.rollback() + return False + def add_scheduled_rate_log(self, batch_id: str, cookie_id: str, order_id: str = None, item_id: str = None, buyer_id: str = None, buyer_nick: str = None, comment: str = None, status: str = 'failed', message: str = None, @@ -7330,6 +7432,131 @@ Cookie数量: {cookie_count} logger.error(f"查询待自动评价订单失败: cookie_id={cookie_id}, error={e}") return [] + def add_scheduled_red_flower_log(self, batch_id: str, cookie_id: str, order_id: str = None, + item_id: str = None, buyer_id: str = None, buyer_nick: str = None, + status: str = 'failed', message: str = None, + raw_response: Any = None) -> Optional[int]: + """写入求小红花执行日志。""" + with self.lock: + try: + cursor = self.conn.cursor() + if raw_response is None: + raw_text = None + elif isinstance(raw_response, str): + raw_text = raw_response + else: + raw_text = json.dumps(raw_response, ensure_ascii=False, default=str) + cursor.execute(''' + INSERT INTO scheduled_red_flower_logs ( + batch_id, cookie_id, order_id, item_id, buyer_id, buyer_nick, + status, message, raw_response + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + batch_id, cookie_id, order_id, item_id, buyer_id, buyer_nick, + status, message, raw_text + )) + log_id = cursor.lastrowid + self.conn.commit() + return log_id + except Exception as e: + logger.error(f"写入求小红花日志失败: {e}") + self.conn.rollback() + return None + + def get_scheduled_red_flower_logs(self, user_id: int = None, cookie_id: str = None, + limit: int = 100, offset: int = 0) -> List[Dict]: + """查询求小红花日志。""" + with self.lock: + try: + cursor = self.conn.cursor() + conditions = [] + params = [] + if user_id is not None: + conditions.append("c.user_id = ?") + params.append(user_id) + if cookie_id: + conditions.append("l.cookie_id = ?") + params.append(cookie_id) + where_sql = f"WHERE {' AND '.join(conditions)}" if conditions else "" + params.extend([max(1, min(int(limit or 100), 500)), max(0, int(offset or 0))]) + cursor.execute(f''' + SELECT l.id, l.batch_id, l.cookie_id, l.order_id, l.item_id, l.buyer_id, + l.buyer_nick, l.status, l.message, l.raw_response, l.created_at + FROM scheduled_red_flower_logs l + LEFT JOIN cookies c ON c.id = l.cookie_id + {where_sql} + ORDER BY l.created_at DESC, l.id DESC + LIMIT ? OFFSET ? + ''', params) + logs = [] + for row in cursor.fetchall(): + logs.append({ + 'id': row[0], + 'batch_id': row[1], + 'cookie_id': row[2], + 'order_id': row[3], + 'item_id': row[4], + 'buyer_id': row[5], + 'buyer_nick': row[6], + 'status': row[7], + 'message': row[8], + 'raw_response': row[9], + 'created_at': row[10], + }) + return logs + except Exception as e: + logger.error(f"查询求小红花日志失败: {e}") + return [] + + def get_pending_red_flower_orders(self, cookie_id: str, limit: int = 5, + days: int = 10, cooldown_minutes: int = 30) -> List[Dict]: + """获取待自动求小红花订单。""" + with self.lock: + try: + cursor = self.conn.cursor() + cursor.execute(''' + SELECT o.order_id, o.item_id, o.buyer_id, o.buyer_nick, o.sid, + o.order_status, o.cookie_id, o.platform_completed_at, o.created_at, o.updated_at, + o.is_red_flower, o.red_flower_at, o.red_flower_error + FROM orders o + WHERE o.cookie_id = ? + AND o.order_status NOT IN ('cancelled', 'processing', 'pending_payment') + AND COALESCE(o.is_red_flower, 0) = 0 + AND datetime(COALESCE(o.platform_created_at, o.platform_paid_at, o.created_at)) >= datetime('now', ?) + AND NOT EXISTS ( + SELECT 1 FROM scheduled_red_flower_logs l + WHERE l.order_id = o.order_id + AND l.status IN ('failed', 'cookie_expired') + AND datetime(l.created_at) >= datetime('now', ?) + ) + ORDER BY datetime(COALESCE(o.platform_created_at, o.platform_paid_at, o.created_at)) ASC + LIMIT ? + ''', (cookie_id, f'-{max(1, int(days or 10))} days', f'-{max(1, int(cooldown_minutes or 30))} minutes', max(1, min(int(limit or 5), 50)))) + orders = [] + for row in cursor.fetchall(): + buyer_nick = self._sanitize_order_buyer_nick(row[3]) + if not buyer_nick: + buyer_nick = self._lookup_buyer_nick_from_chat_messages(cookie_id, row[4], row[2]) + orders.append({ + 'order_id': row[0], + 'item_id': row[1], + 'buyer_id': row[2], + 'buyer_nick': buyer_nick, + 'sid': row[4], + 'order_status': row[5], + 'cookie_id': row[6], + 'platform_completed_at': row[7], + 'created_at': row[8], + 'updated_at': row[9], + 'is_red_flower': bool(row[10]), + 'red_flower_at': row[11], + 'red_flower_error': row[12], + }) + return orders + except Exception as e: + logger.error(f"查询待求小红花订单失败: cookie_id={cookie_id}, error={e}") + return [] + def delete_order(self, order_id: str, cookie_id: str = None) -> bool: """删除订单,可选限定所属账号。""" with self.lock: diff --git a/reply_server.py b/reply_server.py index 284d98a..15fe1c0 100644 --- a/reply_server.py +++ b/reply_server.py @@ -1101,6 +1101,12 @@ async def start_scheduled_task_checker(): logger.info("自动补评价任务已启动") except Exception as exc: logger.error(f"自动补评价任务启动失败: {exc}") + try: + from auto_red_flower_task import auto_red_flower_task_loop + asyncio.create_task(auto_red_flower_task_loop()) + logger.info("自动求小红花任务已启动") + except Exception as exc: + logger.error(f"自动求小红花任务启动失败: {exc}") # 添加请求日志中间件 @@ -3558,6 +3564,7 @@ def get_cookies_details(current_user: Dict[str, Any] = Depends(get_current_user) cookie_enabled = cookie_manager.manager.get_cookie_status(cookie_id) auto_confirm = db_manager.get_auto_confirm(cookie_id) auto_comment = db_manager.get_auto_comment(cookie_id) + auto_red_flower = db_manager.get_auto_red_flower(cookie_id) # 获取备注信息 cookie_details = db_manager.get_cookie_details(cookie_id) remark = cookie_details.get('remark', '') if cookie_details else '' @@ -3572,6 +3579,7 @@ def get_cookies_details(current_user: Dict[str, Any] = Depends(get_current_user) 'enabled': cookie_enabled, 'auto_confirm': auto_confirm, 'auto_comment': auto_comment, + 'auto_red_flower': auto_red_flower, 'remark': remark, 'status_note': status_note, 'username': username, @@ -7393,11 +7401,19 @@ class AutoCommentUpdate(BaseModel): auto_comment: bool +class AutoRedFlowerUpdate(BaseModel): + auto_red_flower: bool + + class AutoCommentOrderRequest(BaseModel): cookie_id: Optional[str] = None comment: Optional[str] = None +class RedFlowerOrderRequest(BaseModel): + cookie_id: Optional[str] = None + + class CommentTemplateCreate(BaseModel): name: str content: str @@ -7478,6 +7494,56 @@ def get_auto_confirm(cid: str, current_user: Dict[str, Any] = Depends(get_curren raise HTTPException(status_code=500, detail=str(e)) +# ==================== 自动求小红花相关API ==================== + +@app.put("/cookies/{cid}/auto-red-flower") +def update_auto_red_flower(cid: str, update_data: AutoRedFlowerUpdate, current_user: Dict[str, Any] = Depends(get_current_user)): + """更新账号的自动求小红花设置""" + if cookie_manager.manager is None: + raise HTTPException(status_code=500, detail="CookieManager 未就绪") + try: + user_id = current_user['user_id'] + user_cookies = db_manager.get_all_cookies(user_id) + if cid not in user_cookies: + raise HTTPException(status_code=403, detail="无权限操作该Cookie") + + success = db_manager.update_auto_red_flower(cid, update_data.auto_red_flower) + if not success: + raise HTTPException(status_code=500, detail="更新自动求小红花设置失败") + + return { + "msg": "success", + "auto_red_flower": update_data.auto_red_flower, + "message": f"自动求小红花已{'开启' if update_data.auto_red_flower else '关闭'}" + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/cookies/{cid}/auto-red-flower") +def get_auto_red_flower(cid: str, current_user: Dict[str, Any] = Depends(get_current_user)): + """获取账号的自动求小红花设置""" + if cookie_manager.manager is None: + raise HTTPException(status_code=500, detail="CookieManager 未就绪") + try: + user_id = current_user['user_id'] + user_cookies = db_manager.get_all_cookies(user_id) + if cid not in user_cookies: + raise HTTPException(status_code=403, detail="无权限操作该Cookie") + + auto_red_flower = db_manager.get_auto_red_flower(cid) + return { + "auto_red_flower": auto_red_flower, + "message": f"自动求小红花当前{'开启' if auto_red_flower else '关闭'}" + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + # ==================== 自动好评相关API ==================== @app.put("/cookies/{cid}/auto-comment") @@ -11562,6 +11628,7 @@ def get_user_orders(current_user: Dict[str, Any] = Depends(get_current_user)): raise HTTPException(status_code=500, detail=f"查询订单失败: {str(e)}") + @app.get('/api/auto-comment/logs') def get_auto_comment_logs( cookie_id: str = None, @@ -11673,6 +11740,111 @@ async def run_auto_comment_once( raise HTTPException(status_code=500, detail=f"手动触发自动补评价失败: {str(e)}") +@app.get('/api/auto-red-flower/logs') +def get_auto_red_flower_logs( + cookie_id: str = None, + limit: int = 100, + offset: int = 0, + current_user: Dict[str, Any] = Depends(get_current_user), +): + """查询求小红花执行日志。""" + try: + if cookie_id: + cookie_id = _ensure_cookie_access(cookie_id, current_user) + logs = db_manager.get_scheduled_red_flower_logs( + user_id=current_user['user_id'], + cookie_id=cookie_id, + limit=limit, + offset=offset, + ) + return {"success": True, "data": logs} + except HTTPException: + raise + except Exception as e: + log_with_user('error', f"查询求小红花日志失败: {str(e)}", current_user) + raise HTTPException(status_code=500, detail=f"查询求小红花日志失败: {str(e)}") + + +@app.post('/api/orders/{order_id}/red-flower') +async def request_order_red_flower_once( + order_id: str, + request: RedFlowerOrderRequest = RedFlowerOrderRequest(), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """手动对指定订单执行一次求小红花。""" + try: + order = db_manager.get_order_by_id(order_id) + if not order: + raise HTTPException(status_code=404, detail='订单不存在') + + cookie_id = str(request.cookie_id or order.get('cookie_id') or '').strip() + cookie_id = _ensure_cookie_access(cookie_id, current_user) + if order.get('cookie_id') and order.get('cookie_id') != cookie_id: + raise HTTPException(status_code=403, detail='订单不属于该账号') + + from auto_red_flower_task import request_red_flower_once + + result = await request_red_flower_once( + cookie_id=cookie_id, + order_id=order_id, + batch_id=f"manual_red_flower_{uuid.uuid4()}", + source='manual', + ) + log_with_user('info', f"手动求小红花: order_id={order_id}, cookie_id={cookie_id}, result={result}", current_user) + return {"success": bool(result.get('success')), "data": result, "message": result.get('message')} + except HTTPException: + raise + except Exception as e: + log_with_user('error', f"手动求小红花失败: order_id={order_id}, error={str(e)}", current_user) + raise HTTPException(status_code=500, detail=f"手动求小红花失败: {str(e)}") + + +@app.post('/api/auto-red-flower/run-once') +async def run_auto_red_flower_once( + request: RedFlowerOrderRequest = RedFlowerOrderRequest(), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """手动触发一轮当前用户范围内的自动求小红花。""" + try: + from auto_red_flower_task import request_red_flower_once + + user_cookies = db_manager.get_all_cookies(current_user['user_id']) + target_cookie_ids = [request.cookie_id] if request.cookie_id else list(user_cookies.keys()) + batch_id = f"manual_red_flower_batch_{uuid.uuid4()}" + results = [] + stats = {"batch_id": batch_id, "accounts": 0, "orders": 0, "success": 0, "failed": 0, "skipped": 0} + + for raw_cookie_id in target_cookie_ids: + cookie_id = _ensure_cookie_access(raw_cookie_id, current_user) + if not db_manager.get_auto_red_flower(cookie_id): + continue + stats['accounts'] += 1 + orders = db_manager.get_pending_red_flower_orders(cookie_id, limit=5, days=10, cooldown_minutes=0) + for order in orders: + stats['orders'] += 1 + result = await request_red_flower_once( + cookie_id=cookie_id, + order_id=order.get('order_id'), + batch_id=batch_id, + source='manual_batch', + ) + results.append(result) + if result.get('success'): + stats['success'] += 1 + elif result.get('status') in {'skipped', 'already_red_flower'}: + stats['skipped'] += 1 + else: + stats['failed'] += 1 + await asyncio.sleep(1) + + return {"success": True, "data": {"stats": stats, "results": results}} + except HTTPException: + raise + except Exception as e: + log_with_user('error', f"手动触发自动求小红花失败: {str(e)}", current_user) + raise HTTPException(status_code=500, detail=f"手动触发自动求小红花失败: {str(e)}") + + @app.get('/api/orders/stream') def stream_user_orders(current_user: Dict[str, Any] = Depends(get_current_user)): """订单实时事件流,仅在订单页激活时使用。""" diff --git a/static/css/accounts.css b/static/css/accounts.css index 3324883..5895df4 100644 --- a/static/css/accounts.css +++ b/static/css/accounts.css @@ -71,6 +71,30 @@ input:checked + .status-slider:before { border: 1px solid #fecaca; } +.account-status-cell { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.35rem; + min-width: 92px; +} + +.account-status-main { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + flex-wrap: nowrap; + white-space: nowrap; +} + +.account-status-cell .account-status-note-badge { + max-width: 100%; + justify-content: center; + text-align: center; +} + .account-status-note-badge { display: inline-flex; align-items: center; @@ -89,6 +113,14 @@ input:checked + .status-slider:before { font-size: 0.8rem; } +#cookieTable thead th { + white-space: nowrap; +} + +#cookieTable thead th:nth-child(5) { + min-width: 4.5rem; +} + .account-row.disabled { opacity: 0.6; background-color: #f9fafb; @@ -98,6 +130,107 @@ input:checked + .status-slider:before { background-color: #f3f4f6; } +.account-actions-cell { + min-width: 340px; +} + +.account-actions-toolbar { + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; + gap: 0.35rem; + width: 100%; + overflow-x: auto; + overflow-y: hidden; + padding: 0.1rem 0; + scrollbar-width: thin; +} + +.account-actions-toolbar::-webkit-scrollbar { + height: 4px; +} + +.account-action-group { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + gap: 0.18rem; + padding: 0.18rem; + border: 1px solid #e5e7eb; + border-radius: 0.65rem; + background: #f8fafc; +} + +.account-action-group-label, +.account-action-btn .action-text { + display: none; +} + +.account-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + min-width: 28px; + height: 28px; + border-radius: 0.5rem !important; + margin-left: 0 !important; + padding: 0; + font-size: 0.78rem; + line-height: 1; + white-space: nowrap; +} + +.account-action-btn .bi { + font-size: 0.95rem; +} + +.account-action-group-danger { + margin-left: auto; +} + +.account-action-delete { + min-width: 28px; +} + +.account-action-group-basic { + background: #f8fafc; + border-color: #e2e8f0; +} + +.account-action-group-reply { + background: #f0fdf4; + border-color: #bbf7d0; +} + +.account-action-group-item { + background: #eff6ff; + border-color: #bfdbfe; +} + +.account-action-group-flower { + background: #fff7ed; + border-color: #fed7aa; +} + +.account-action-group-danger { + background: #fef2f2; + border-color: #fecaca; +} + +.account-actions-group { + display: flex !important; + flex-wrap: wrap; + justify-content: center; + gap: 0.25rem; +} + +.account-actions-group > .btn { + border-radius: 0.375rem !important; + margin-left: 0 !important; +} + /* 关键词管理界面的状态提示 */ .account-badge .badge.bg-warning { animation: pulse 2s infinite; @@ -1237,6 +1370,41 @@ input:checked + .status-slider:before { border-color: rgba(245, 158, 11, 0.32); } +[data-theme="dark"] .account-action-group { + background: rgba(15, 23, 42, 0.72); + border-color: rgba(148, 163, 184, 0.22); +} + +[data-theme="dark"] .account-action-group-label { + background: rgba(51, 65, 85, 0.9); + color: #cbd5e1; +} + +[data-theme="dark"] .account-action-group-basic { + background: rgba(30, 41, 59, 0.55); + border-color: rgba(148, 163, 184, 0.22); +} + +[data-theme="dark"] .account-action-group-reply { + background: rgba(6, 78, 59, 0.24); + border-color: rgba(16, 185, 129, 0.24); +} + +[data-theme="dark"] .account-action-group-item { + background: rgba(30, 64, 175, 0.2); + border-color: rgba(59, 130, 246, 0.24); +} + +[data-theme="dark"] .account-action-group-flower { + background: rgba(146, 64, 14, 0.2); + border-color: rgba(251, 146, 60, 0.24); +} + +[data-theme="dark"] .account-action-group-danger { + background: rgba(127, 29, 29, 0.2); + border-color: rgba(248, 113, 113, 0.24); +} + /* 禁用账号提示 */ [data-theme="dark"] .disabled-account-notice { background: rgba(245, 158, 11, 0.15); diff --git a/static/index.html b/static/index.html index c41eef9..1c287ae 100644 --- a/static/index.html +++ b/static/index.html @@ -642,23 +642,23 @@
| 账号ID | -Cookie值 | -关键词 | -状态 | -默认回复 | -AI回复 | -自动确认发货 | +账号ID | +Cookie值 | +关键词 | +状态 | +默认回复 | +AI回复 | +自动确认发货 | 自动好评 | -备注 | -+ | 备注 | +暂停时间 | -操作 | +操作 |
-
-
-
-
-
+
+
+
+
+
+
+
${statusNoteBadge}
|
@@ -4253,30 +4258,49 @@ async function loadCookies() {
-
-
-
-
-
-
-
-
-
-
+
+
|
`;
@@ -4727,7 +4751,7 @@ function cancelCookieEdit(id) {
cookieValueCell.innerHTML = window.editingCookieData.originalContent;
// 恢复按钮状态
- const actionButtons = row.querySelectorAll('.btn-group button');
+ const actionButtons = row.querySelectorAll('.account-actions-toolbar button, .btn-group button');
actionButtons.forEach(btn => btn.disabled = false);
// 清理全局数据
@@ -4816,7 +4840,7 @@ function updateAccountRowStatus(accountId, enabled, statusNote = '') {
const row = toggle.closest('tr');
const statusBadge = row.querySelector('.status-badge');
const statusCell = row.querySelector('.account-status-cell');
- const actionButtons = row.querySelectorAll('.btn-group .btn:not(.btn-outline-info):not(.btn-outline-danger)');
+ const actionButtons = row.querySelectorAll('.account-actions-toolbar .btn[data-requires-enabled="true"], .btn-group .btn:not(.btn-outline-info):not(.btn-outline-danger)');
// 更新行样式
row.className = `account-row ${enabled ? 'enabled' : 'disabled'}`;
@@ -4837,17 +4861,22 @@ function updateAccountRowStatus(accountId, enabled, statusNote = '') {
statusCell.insertAdjacentHTML('beforeend', renderedStatusNote);
}
- // 更新按钮状态(只禁用编辑Cookie按钮,其他按钮保持可用)
+ // 更新依赖账号启用状态的按钮;自动回复规则入口始终可用
actionButtons.forEach(btn => {
- if (btn.onclick && btn.onclick.toString().includes('editCookieInline')) {
+ if (btn.dataset.requiresEnabled === 'true') {
btn.disabled = !enabled;
}
- // 设置自动回复按钮始终可用,但更新提示文本
if (btn.onclick && btn.onclick.toString().includes('goToAutoReply')) {
btn.title = enabled ? '设置自动回复' : '配置关键词 (账号已禁用)';
}
});
+ const redFlowerRunButton = row.querySelector('[data-red-flower-run]');
+ if (redFlowerRunButton) {
+ const redFlowerEnabled = redFlowerRunButton.dataset.redFlowerActive === 'true';
+ redFlowerRunButton.disabled = !enabled || !redFlowerEnabled;
+ }
+
// 更新切换按钮的提示
const label = toggle.closest('.status-toggle');
label.title = enabled ? '点击禁用' : '点击启用';
@@ -4987,6 +5016,76 @@ function updateAutoCommentRowStatus(accountId, enabled) {
}
}
+// 切换自动求小红花状态
+async function toggleAutoRedFlower(accountId, enabled) {
+ try {
+ toggleLoading(true);
+
+ const response = await fetch(`${apiBase}/cookies/${accountId}/auto-red-flower`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`
+ },
+ body: JSON.stringify({ auto_red_flower: enabled })
+ });
+
+ if (response.ok) {
+ const result = await response.json();
+ showToast(result.message || (enabled ? '已开启自动求小红花' : '已关闭自动求小红花'), 'success');
+ await loadCookies();
+ } else {
+ const error = await response.json().catch(() => ({}));
+ showToast(error.detail || '更新自动求小红花设置失败', 'error');
+ }
+ } catch (error) {
+ console.error('切换自动求小红花状态失败:', error);
+ showToast('网络错误,请稍后重试', 'error');
+ } finally {
+ toggleLoading(false);
+ }
+}
+
+// 立即执行当前账号的求小红花补偿
+async function runAutoRedFlowerForAccount(accountId) {
+ if (!accountId) {
+ showToast('缺少账号ID', 'warning');
+ return;
+ }
+
+ const confirmed = confirm(`确定要立即为账号「${accountId}」执行一轮求小红花吗?`);
+ if (!confirmed) return;
+
+ toggleLoading(true);
+ showToast('正在执行求小红花,请稍候...', 'info');
+ try {
+ const response = await fetch(`${apiBase}/api/auto-red-flower/run-once`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`
+ },
+ body: JSON.stringify({ cookie_id: accountId })
+ });
+ const data = await response.json().catch(() => ({}));
+ if (!response.ok || data.success === false) {
+ showToast(data.detail || data.message || '求小红花执行失败', 'danger');
+ return;
+ }
+ const stats = data?.data?.stats || {};
+ showToast(
+ `求小红花完成:处理 ${stats.orders || 0} 单,成功 ${stats.success || 0},失败 ${stats.failed || 0},跳过 ${stats.skipped || 0}`,
+ (stats.failed || 0) > 0 ? 'warning' : 'success'
+ );
+ await loadCookies();
+ } catch (error) {
+ console.error('执行求小红花失败:', error);
+ showToast(`求小红花请求异常: ${error.message}`, 'danger');
+ } finally {
+ toggleLoading(false);
+ }
+}
+
// 当前编辑的好评模板账号ID
let currentCommentTemplateAccountId = null;
diff --git a/utils/red_flower_service.py b/utils/red_flower_service.py
new file mode 100644
index 0000000..33c2b13
--- /dev/null
+++ b/utils/red_flower_service.py
@@ -0,0 +1,216 @@
+"""闲鱼求小红花服务。
+
+参考上游 red_flower_task 的核心请求逻辑,适配当前单体项目:
+- 调用 mtop.taobao.idlemessage.red.flower;
+- 令牌过期时合并响应 Set-Cookie、保存 Cookie 后重试一次;
+- 返回统一结果供实时接口、手动接口和后台补偿任务复用。
+"""
+
+from __future__ import annotations
+
+import asyncio
+import hashlib
+import json
+import time
+from typing import Any, Dict, Tuple
+
+import aiohttp
+from loguru import logger
+
+
+APP_KEY = "34839810"
+RED_FLOWER_API_URL = "https://h5api.m.goofish.com/h5/mtop.taobao.idlemessage.red.flower/1.0/"
+RED_FLOWER_API_NAME = "mtop.taobao.idlemessage.red.flower"
+
+
+class RedFlowerService:
+ """闲鱼求小红花服务。"""
+
+ def __init__(self, cookie_string: str, account_id: str | None = None):
+ self.cookie_string = str(cookie_string or "").strip()
+ self.account_id = account_id
+ self.cookies_dict = self._parse_cookies(self.cookie_string)
+
+ @staticmethod
+ def _parse_cookies(cookies_str: str) -> Dict[str, str]:
+ cookies: Dict[str, str] = {}
+ for part in str(cookies_str or "").replace("\ufeff", "").split(";"):
+ part = part.strip()
+ if not part or "=" not in part:
+ continue
+ key, value = part.split("=", 1)
+ key = key.strip()
+ if key:
+ cookies[key] = value.strip()
+ return cookies
+
+ @staticmethod
+ def _cookie_dict_to_string(cookies: Dict[str, str]) -> str:
+ return "; ".join(f"{key}={value}" for key, value in cookies.items())
+
+ @staticmethod
+ def _generate_sign(t: str, token: str, data: str) -> str:
+ msg = f"{token}&{t}&{APP_KEY}&{data}"
+ md5_hash = hashlib.md5()
+ md5_hash.update(msg.encode("utf-8"))
+ return md5_hash.hexdigest()
+
+ @staticmethod
+ def _ret_to_text(ret: Any) -> str:
+ if isinstance(ret, list):
+ return "; ".join(str(item) for item in ret)
+ return str(ret or "")
+
+ @classmethod
+ def _is_token_expired(cls, ret: Any) -> bool:
+ ret_text = cls._ret_to_text(ret)
+ return "FAIL_SYS_TOKEN_EXOIRED" in ret_text or "令牌过期" in ret_text
+
+ @classmethod
+ def _is_session_expired(cls, ret: Any) -> bool:
+ ret_text = cls._ret_to_text(ret)
+ return "FAIL_SYS_SESSION_EXPIRED" in ret_text or "Session过期" in ret_text
+
+ @classmethod
+ def _is_already_requested(cls, ret: Any, result: Any) -> bool:
+ text = cls._ret_to_text(ret) + " " + json.dumps(result, ensure_ascii=False, default=str)
+ return any(keyword in text for keyword in (
+ "已送出小红花", "已收下", "已求过", "已赠送", "已经送", "重复", "不能重复",
+ ))
+
+ @staticmethod
+ def _extract_set_cookies(response: aiohttp.ClientResponse) -> Dict[str, str]:
+ new_cookies: Dict[str, str] = {}
+ try:
+ for cookie_header in response.headers.getall("set-cookie", []):
+ first_part = cookie_header.split(";", 1)[0]
+ if "=" not in first_part:
+ continue
+ name, value = first_part.split("=", 1)
+ name = name.strip()
+ if name:
+ new_cookies[name] = value.strip()
+ except Exception as exc:
+ logger.warning(f"提取求小红花接口 Set-Cookie 失败: {exc}")
+ return new_cookies
+
+ def _merge_response_cookies(self, response: aiohttp.ClientResponse) -> Tuple[bool, str]:
+ new_cookies = self._extract_set_cookies(response)
+ if not new_cookies:
+ return False, self.cookie_string
+ merged = dict(self.cookies_dict)
+ merged.update(new_cookies)
+ merged_string = self._cookie_dict_to_string(merged)
+ logger.info(
+ f"【{self.account_id or '未知账号'}】求小红花接口返回新 Cookie,已合并 {len(new_cookies)} 个字段"
+ )
+ return True, merged_string
+
+ async def _persist_cookie_if_needed(self, new_cookie_string: str) -> None:
+ if not self.account_id or not new_cookie_string or new_cookie_string == self.cookie_string:
+ return
+ try:
+ from db_manager import db_manager
+
+ db_manager.save_cookie(self.account_id, new_cookie_string)
+ logger.info(f"【{self.account_id}】求小红花接口刷新后的 Cookie 已保存到数据库")
+ except Exception as exc:
+ logger.warning(f"【{self.account_id}】保存求小红花刷新 Cookie 失败: {exc}")
+
+ async def request_red_flower(self, order_id: str, is_retry: bool = False) -> Dict[str, Any]:
+ """对指定订单发送求小红花请求。"""
+ order_id = str(order_id or "").strip()
+ if not order_id:
+ return {"success": False, "message": "缺少订单号"}
+ if not self.cookie_string:
+ return {"success": False, "message": "账号 Cookie 为空"}
+
+ m_h5_tk = self.cookies_dict.get("_m_h5_tk", "")
+ token = m_h5_tk.split("_", 1)[0] if m_h5_tk else ""
+ if not token:
+ return {"success": False, "message": "Cookie 中缺少 _m_h5_tk,无法生成求小红花签名"}
+
+ timestamp = str(int(time.time() * 1000))
+ data_obj = {
+ "orderId": order_id,
+ "channel": "list",
+ }
+ data_val = json.dumps(data_obj, separators=(",", ":"), ensure_ascii=False)
+ sign = self._generate_sign(timestamp, token, data_val)
+
+ params = {
+ "jsv": "2.7.2",
+ "appKey": APP_KEY,
+ "t": timestamp,
+ "sign": sign,
+ "v": "4.0",
+ "type": "originaljson",
+ "accountSite": "xianyu",
+ "dataType": "json",
+ "timeout": "20000",
+ "api": RED_FLOWER_API_NAME,
+ "sessionOption": "AutoLoginOnly",
+ }
+ headers = {
+ "accept": "application/json",
+ "content-type": "application/x-www-form-urlencoded",
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+ "referer": "https://www.goofish.com/",
+ "origin": "https://www.goofish.com",
+ "cookie": self.cookie_string,
+ }
+
+ try:
+ timeout = aiohttp.ClientTimeout(total=30)
+ async with aiohttp.ClientSession(timeout=timeout) as session:
+ async with session.post(RED_FLOWER_API_URL, params=params, headers=headers, data={"data": data_val}) as response:
+ try:
+ result = await response.json(content_type=None)
+ except Exception:
+ body = await response.text()
+ return {
+ "success": False,
+ "message": f"求小红花接口返回非 JSON: HTTP {response.status}",
+ "raw": body[:1000],
+ }
+
+ # 无论是否成功,先合并响应中的新 Cookie,供后续请求使用。
+ has_cookie_update, merged_cookie_string = self._merge_response_cookies(response)
+ if has_cookie_update:
+ await self._persist_cookie_if_needed(merged_cookie_string)
+ self.cookie_string = merged_cookie_string
+ self.cookies_dict = self._parse_cookies(merged_cookie_string)
+
+ ret = result.get("ret", []) if isinstance(result, dict) else []
+ ret_text = self._ret_to_text(ret) or str(result)
+ retry_tag = "[令牌过期重试] " if is_retry else ""
+
+ if ret_text == "SUCCESS::调用成功" or "SUCCESS" in ret_text:
+ logger.info(
+ f"【{self.account_id or '未知账号'}】{retry_tag}求小红花成功: order_id={order_id}"
+ )
+ return {"success": True, "message": "求小红花成功", "raw": result}
+
+ if self._is_already_requested(ret, result):
+ logger.info(
+ f"【{self.account_id or '未知账号'}】订单已求过小红花,按成功处理: order_id={order_id}, ret={ret}"
+ )
+ return {"success": True, "already_red_flower": True, "message": "订单已求过小红花", "raw": result}
+
+ if not is_retry and self._is_token_expired(ret):
+ if has_cookie_update:
+ return await self.request_red_flower(order_id, is_retry=True)
+ return {"success": False, "message": "令牌过期且响应未返回新 Cookie", "raw": result}
+
+ if self._is_session_expired(ret):
+ return {"success": False, "session_expired": True, "message": ret_text, "raw": result}
+
+ logger.warning(
+ f"【{self.account_id or '未知账号'}】{retry_tag}求小红花失败: order_id={order_id}, ret={ret}"
+ )
+ return {"success": False, "message": ret_text, "raw": result}
+ except asyncio.TimeoutError:
+ return {"success": False, "message": "求小红花接口请求超时"}
+ except Exception as exc:
+ logger.error(f"【{self.account_id or '未知账号'}】求小红花异常: order_id={order_id}, error={exc}")
+ return {"success": False, "message": str(exc)}
|
|---|