add(求小红花): 增加自动求小红花流程

This commit is contained in:
GuDong
2026-05-25 21:27:02 +08:00
parent 2771d28703
commit 031e085702
7 changed files with 1120 additions and 53 deletions
+185
View File
@@ -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
View File
@@ -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
View File
@@ -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)):
"""订单实时事件流,仅在订单页激活时使用。"""
+168
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+216
View File
@@ -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)}