mirror of
https://github.com/GuDong2003/xianyu-auto-reply-fix.git
synced 2026-08-28 17:40:45 +08:00
add(求小红花): 增加自动求小红花流程
This commit is contained in:
@@ -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)
|
||||
+233
-6
@@ -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:
|
||||
|
||||
+172
@@ -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)):
|
||||
"""订单实时事件流,仅在订单页激活时使用。"""
|
||||
|
||||
@@ -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);
|
||||
|
||||
+10
-10
@@ -642,23 +642,23 @@
|
||||
<table class="table table-hover text-center" id="cookieTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 8%">账号ID</th>
|
||||
<th style="width: 10%">Cookie值</th>
|
||||
<th style="width: 6%">关键词</th>
|
||||
<th style="width: 6%">状态</th>
|
||||
<th style="width: 8%">默认回复</th>
|
||||
<th style="width: 6%">AI回复</th>
|
||||
<th style="width: 9%">自动确认发货</th>
|
||||
<th style="width: 7%">账号ID</th>
|
||||
<th style="width: 8%">Cookie值</th>
|
||||
<th style="width: 5%">关键词</th>
|
||||
<th style="width: 5%">状态</th>
|
||||
<th style="width: 7%">默认回复</th>
|
||||
<th style="width: 5%">AI回复</th>
|
||||
<th style="width: 8%">自动确认发货</th>
|
||||
<th style="width: 7%">自动好评</th>
|
||||
<th style="width: 11%">备注</th>
|
||||
<th style="width: 11%; white-space: nowrap;">
|
||||
<th style="width: 9%">备注</th>
|
||||
<th style="width: 9%; white-space: nowrap;">
|
||||
暂停时间
|
||||
<i class="bi bi-question-circle ms-1"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="检测到手动发出消息后,自动回复暂停的时间长度(分钟)。设置为0表示不暂停。如果在暂停期间再次手动发出消息,会重新开始计时。"></i>
|
||||
</th>
|
||||
<th style="width: 18%">操作</th>
|
||||
<th style="width: 30%; min-width: 340px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
|
||||
+136
-37
@@ -4180,6 +4180,9 @@ async function loadCookies() {
|
||||
// 自动好评状态(默认关闭)
|
||||
const autoComment = cookie.auto_comment === undefined ? false : cookie.auto_comment;
|
||||
|
||||
// 自动求小红花状态(默认关闭)
|
||||
const autoRedFlower = cookie.auto_red_flower === undefined ? false : cookie.auto_red_flower;
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="align-middle">
|
||||
<div class="cookie-id">
|
||||
@@ -4197,14 +4200,16 @@ async function loadCookies() {
|
||||
</span>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap account-status-cell">
|
||||
<label class="status-toggle" title="${isEnabled ? '点击禁用' : '点击启用'}">
|
||||
<input type="checkbox" ${isEnabled ? 'checked' : ''} onchange="toggleAccountStatus('${cookie.id}', this.checked)">
|
||||
<span class="status-slider"></span>
|
||||
</label>
|
||||
<span class="status-badge ${isEnabled ? 'enabled' : 'disabled'}" title="${isEnabled ? '账号已启用' : '账号已禁用'}">
|
||||
<i class="bi bi-${isEnabled ? 'check-circle-fill' : 'x-circle-fill'}"></i>
|
||||
</span>
|
||||
<div class="account-status-cell">
|
||||
<div class="account-status-main">
|
||||
<label class="status-toggle" title="${isEnabled ? '点击禁用' : '点击启用'}">
|
||||
<input type="checkbox" ${isEnabled ? 'checked' : ''} onchange="toggleAccountStatus('${cookie.id}', this.checked)">
|
||||
<span class="status-slider"></span>
|
||||
</label>
|
||||
<span class="status-badge ${isEnabled ? 'enabled' : 'disabled'}" title="${isEnabled ? '账号已启用' : '账号已禁用'}">
|
||||
<i class="bi bi-${isEnabled ? 'check-circle-fill' : 'x-circle-fill'}"></i>
|
||||
</span>
|
||||
</div>
|
||||
${statusNoteBadge}
|
||||
</div>
|
||||
</td>
|
||||
@@ -4253,30 +4258,49 @@ async function loadCookies() {
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="showFaceVerification('${cookie.id}')" title="验证截图">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="editCookieInline('${cookie.id}', '${cookie.value}')" title="修改Cookie" ${!isEnabled ? 'disabled' : ''}>
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-success" onclick="goToAutoReply('${cookie.id}')" title="${isEnabled ? '设置自动回复' : '配置关键词 (账号已禁用)'}">
|
||||
<i class="bi bi-arrow-right-circle"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="configAIReply('${cookie.id}')" title="配置AI回复" ${!isEnabled ? 'disabled' : ''}>
|
||||
<i class="bi bi-robot"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="polishAccountItems('${cookie.id}')" title="一键擦亮" ${!isEnabled ? 'disabled' : ''}>
|
||||
<i class="bi bi-stars"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info" onclick="openPolishScheduleModal('${cookie.id}')" title="定时擦亮" ${!isEnabled ? 'disabled' : ''}>
|
||||
<i class="bi bi-clock"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="delCookie('${cookie.id}')" title="删除账号">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
<td class="align-middle account-actions-cell">
|
||||
<div class="account-actions-toolbar" role="group" aria-label="账号操作">
|
||||
<div class="account-action-group account-action-group-basic" aria-label="基础操作">
|
||||
<span class="account-action-group-label">基础</span>
|
||||
<button class="btn btn-sm btn-outline-secondary account-action-btn" onclick="showFaceVerification('${cookie.id}')" title="查看验证截图" data-action="face-verification">
|
||||
<i class="bi bi-shield-check"></i><span class="action-text">验证</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary account-action-btn" onclick="editCookieInline('${cookie.id}', '${cookie.value}')" title="修改账号信息与Cookie" data-action="edit-cookie" data-requires-enabled="true" ${!isEnabled ? 'disabled' : ''}>
|
||||
<i class="bi bi-pencil"></i><span class="action-text">编辑</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="account-action-group account-action-group-reply" aria-label="回复配置">
|
||||
<span class="account-action-group-label">回复</span>
|
||||
<button class="btn btn-sm btn-outline-success account-action-btn" onclick="goToAutoReply('${cookie.id}')" title="${isEnabled ? '设置自动回复' : '配置关键词 (账号已禁用)'}" data-action="auto-reply">
|
||||
<i class="bi bi-chat-dots"></i><span class="action-text">规则</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning account-action-btn" onclick="configAIReply('${cookie.id}')" title="配置AI回复" data-action="ai-reply" data-requires-enabled="true" ${!isEnabled ? 'disabled' : ''}>
|
||||
<i class="bi bi-robot"></i><span class="action-text">AI</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="account-action-group account-action-group-item" aria-label="商品操作">
|
||||
<span class="account-action-group-label">商品</span>
|
||||
<button class="btn btn-sm btn-outline-secondary account-action-btn" onclick="polishAccountItems('${cookie.id}')" title="立即擦亮全部商品" data-action="polish-items" data-requires-enabled="true" ${!isEnabled ? 'disabled' : ''}>
|
||||
<i class="bi bi-stars"></i><span class="action-text">擦亮</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info account-action-btn" onclick="openPolishScheduleModal('${cookie.id}')" title="设置定时擦亮" data-action="polish-schedule" data-requires-enabled="true" ${!isEnabled ? 'disabled' : ''}>
|
||||
<i class="bi bi-clock"></i><span class="action-text">定时</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="account-action-group account-action-group-flower" aria-label="小红花操作">
|
||||
<span class="account-action-group-label">小红花</span>
|
||||
<button class="btn btn-sm ${autoRedFlower ? 'btn-outline-danger' : 'btn-outline-secondary'} account-action-btn" onclick="toggleAutoRedFlower('${cookie.id}', ${!autoRedFlower})" title="${autoRedFlower ? '关闭自动求小红花' : '开启自动求小红花'}" data-auto-red-flower-toggle="${cookie.id}" data-auto-red-flower-active="${autoRedFlower ? 'true' : 'false'}">
|
||||
<i class="bi bi-flower${autoRedFlower ? '1' : '2'}"></i><span class="action-text">${autoRedFlower ? '已开' : '开启'}</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger account-action-btn" onclick="runAutoRedFlowerForAccount('${cookie.id}')" title="立即执行求小红花" data-red-flower-run="${cookie.id}" data-red-flower-active="${autoRedFlower ? 'true' : 'false'}" ${(!isEnabled || !autoRedFlower) ? 'disabled' : ''}>
|
||||
<i class="bi bi-send-fill"></i><span class="action-text">执行</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="account-action-group account-action-group-danger" aria-label="危险操作">
|
||||
<button class="btn btn-sm btn-outline-danger account-action-btn account-action-delete" onclick="delCookie('${cookie.id}')" title="删除账号" data-action="delete-account">
|
||||
<i class="bi bi-trash"></i><span class="action-text">删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)}
|
||||
Reference in New Issue
Block a user