diff --git a/README.md b/README.md
index d5bc022..8598593 100644
--- a/README.md
+++ b/README.md
@@ -358,51 +358,57 @@ python Start.py
## 📁 核心文件功能说明
-### 🚀 启动和核心模块
-- **`Start.py`** - 项目启动入口,初始化CookieManager和FastAPI服务,管理多账号任务
-- **`XianyuAutoAsync.py`** - 闲鱼WebSocket连接核心,处理消息收发、自动回复、自动发货
-- **`reply_server.py`** - FastAPI Web服务器,提供完整的管理界面和RESTful API接口
-- **`cookie_manager.py`** - 多账号Cookie管理器,负责账号任务的启动、停止和状态管理
+### 🚀 核心启动模块
+- **`Start.py`** - 项目启动入口,初始化CookieManager和FastAPI服务,从数据库加载账号任务并启动后台API服务
+- **`XianyuAutoAsync.py`** - 闲鱼WebSocket连接核心,处理消息收发、自动回复、自动发货、商品信息收集
+- **`reply_server.py`** - FastAPI Web服务器,提供完整的管理界面和RESTful API接口,支持多用户系统
+- **`cookie_manager.py`** - 多账号Cookie管理器,负责账号任务的启动、停止、状态管理和线程安全操作
### 🗄️ 数据和配置管理
-- **`db_manager.py`** - SQLite数据库管理器,支持多用户数据隔离、自动迁移、版本管理
-- **`config.py`** - 全局配置文件管理器,加载YAML配置和环境变量
-- **`global_config.yml`** - 全局配置文件,包含WebSocket、API、自动回复等所有配置项
+- **`db_manager.py`** - SQLite数据库管理器,支持多用户数据隔离、自动迁移、版本管理、完整的CRUD操作
+- **`config.py`** - 全局配置文件管理器,加载YAML配置和环境变量,提供配置项访问接口
+- **`global_config.yml`** - 全局配置文件,包含WebSocket、API、自动回复、AI等所有系统配置项
### 🤖 智能功能模块
-- **`ai_reply_engine.py`** - AI智能回复引擎,支持OpenAI、通义千问等多种AI模型
-- **`secure_confirm_ultra.py`** - 自动确认发货模块,采用多层加密保护核心业务逻辑
-- **`secure_freeshipping_ultra.py`** - 自动免拼发货模块,支持批量处理和异常恢复
-- **`file_log_collector.py`** - 实时日志收集器,提供Web界面日志查看和管理
+- **`ai_reply_engine.py`** - AI智能回复引擎,支持OpenAI、通义千问等多种AI模型,意图识别和上下文管理
+- **`secure_confirm_ultra.py`** - 自动确认发货模块,采用多层加密保护,调用闲鱼API确认发货状态
+- **`secure_freeshipping_ultra.py`** - 自动免拼发货模块,支持批量处理、异常恢复和智能匹配
+- **`file_log_collector.py`** - 实时日志收集器,提供Web界面日志查看、搜索和管理功能
-### 🛠️ 工具模块
-- **`utils/xianyu_utils.py`** - 闲鱼API工具函数,包含加密解密、签名生成、数据解析
-- **`utils/message_utils.py`** - 消息格式化和处理工具,支持变量替换和模板渲染
-- **`utils/ws_utils.py`** - WebSocket客户端封装,提供连接管理和重连机制
-- **`utils/item_search.py`** - 商品搜索功能,基于Playwright获取真实闲鱼数据
-- **`utils/order_detail_fetcher.py`** - 订单详情获取工具,支持多规格商品信息解析
-- **`utils/image_utils.py`** - 图片处理工具,支持压缩、格式转换、尺寸调整
-- **`utils/image_uploader.py`** - 图片上传到CDN工具,支持闲鱼图片服务器上传
-- **`utils/qr_login.py`** - 二维码登录功能,支持扫码获取Cookie
+### 🛠️ 工具模块 (`utils/`)
+- **`xianyu_utils.py`** - 闲鱼API核心工具,包含加密算法、签名生成、数据解析、Cookie处理
+- **`message_utils.py`** - 消息处理工具,格式化消息内容、变量替换、内容过滤和模板渲染
+- **`ws_utils.py`** - WebSocket客户端封装,处理连接管理、心跳检测、重连机制和消息队列
+- **`qr_login.py`** - 二维码登录功能,生成登录二维码、状态检测、Cookie获取和验证
+- **`item_search.py`** - 商品搜索功能,基于Playwright获取真实闲鱼商品数据,支持分页和过滤
+- **`order_detail_fetcher.py`** - 订单详情获取工具,解析订单信息、买家信息、SKU详情,支持缓存优化
+- **`image_utils.py`** - 图片处理工具,支持压缩、格式转换、尺寸调整、水印添加
+- **`image_uploader.py`** - 图片上传工具,支持多种CDN服务商,自动压缩和格式优化
-### 🌐 前端界面
-- **`static/index.html`** - 主管理界面,集成账号管理、系统监控、功能配置
-- **`static/login.html`** - 用户登录页面,支持图形验证码和记住登录状态
-- **`static/register.html`** - 用户注册页面,支持邮箱验证和实时验证
-- **`static/user_management.html`** - 用户管理页面,管理员专用功能
-- **`static/data_management.html`** - 数据管理页面,支持Excel导入导出和批量操作
-- **`static/log_management.html`** - 日志管理页面,实时日志查看和过滤
-- **`static/item_search.html`** - 商品搜索页面,获取真实闲鱼商品数据
-- **`static/js/app.js`** - 主要JavaScript逻辑,处理前端交互和API调用
-- **`static/css/style.css`** - 自定义样式文件,美化界面和响应式设计
+### 🌐 前端界面 (`static/`)
+- **`index.html`** - 主管理界面,包含账号管理、关键词管理、系统监控、实时状态显示
+- **`login.html`** - 用户登录页面,支持图形验证码、记住登录状态、多重安全验证
+- **`register.html`** - 用户注册页面,支持邮箱验证码、实时验证、密码强度检测
+- **`user_management.html`** - 用户管理页面,管理员专用,用户增删改查、权限管理
+- **`data_management.html`** - 数据管理页面,支持Excel导入导出、数据备份、批量操作
+- **`log_management.html`** - 日志管理页面,实时日志查看、日志搜索过滤、日志下载
+- **`item_search.html`** - 商品搜索页面,获取真实闲鱼商品数据,支持多条件搜索
+- **`js/app.js`** - 主要JavaScript逻辑,处理前端交互、API调用、实时更新
+- **`css/`** - 模块化样式文件,包含布局、组件、主题等分类样式,响应式设计
+- **`xianyu_js_version_2.js`** - 闲鱼JavaScript工具库,加密解密、数据处理、API封装
+- **`lib/`** - 前端依赖库,包含Bootstrap、jQuery、Chart.js等第三方库
### 🐳 部署配置
-- **`Dockerfile`** - Docker镜像构建文件,包含Python环境、Playwright浏览器等
-- **`docker-compose.yml`** - Docker Compose配置,支持一键部署和Nginx反向代理
-- **`docker-deploy.sh`** - Docker部署管理脚本,提供构建、启动、监控等功能
+- **`Dockerfile`** - Docker镜像构建文件,包含Python环境、Playwright浏览器、系统依赖
+- **`Dockerfile-cn`** - 中国镜像源版本,优化国内网络环境下的构建速度
+- **`docker-compose.yml`** - Docker Compose配置,支持一键部署、Nginx反向代理、资源限制
+- **`docker-compose-cn.yml`** - 中国镜像源版本,适配国内网络环境
+- **`docker-deploy.sh`** - Docker部署管理脚本,提供构建、启动、监控、日志查看等功能
+- **`docker-deploy.bat`** - Windows版本部署脚本,支持Windows环境一键部署
+- **`entrypoint.sh`** - Docker容器启动脚本,处理环境初始化和服务启动
- **`nginx/nginx.conf`** - Nginx反向代理配置,支持负载均衡和SSL终端
-- **`.env`** - 环境变量配置文件,包含所有可配置的系统参数
-- **`requirements.txt`** - Python依赖包列表,精简版本无冗余依赖
+- **`.env`** - 环境变量配置文件,包含所有可配置的系统参数和敏感信息
+- **`requirements.txt`** - Python依赖包列表,精简版本无冗余依赖,按功能分类组织
## ⚙️ 配置说明
diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py
index 74abb60..def2fe9 100644
--- a/XianyuAutoAsync.py
+++ b/XianyuAutoAsync.py
@@ -142,7 +142,7 @@ class XianyuLive:
self.myid = self.cookies['unb']
logger.info(f"【{cookie_id}】用户ID: {self.myid}")
self.device_id = generate_device_id(self.myid)
-
+
# 心跳相关配置
self.heartbeat_interval = HEARTBEAT_INTERVAL
self.heartbeat_timeout = HEARTBEAT_TIMEOUT
@@ -150,7 +150,7 @@ class XianyuLive:
self.last_heartbeat_response = 0
self.heartbeat_task = None
self.ws = None
-
+
# Token刷新相关配置
self.token_refresh_interval = TOKEN_REFRESH_INTERVAL
self.token_retry_interval = TOKEN_RETRY_INTERVAL
@@ -238,16 +238,34 @@ class XianyuLive:
# 先查看消息的完整结构
logger.debug(f"【{self.cookie_id}】🔍 完整消息结构: {message}")
- # 检查message['1']的结构
+ # 检查message['1']的结构,处理可能是列表、字典或字符串的情况
message_1 = message.get('1', {})
- logger.debug(f"【{self.cookie_id}】🔍 message['1'] keys: {list(message_1.keys()) if message_1 else 'None'}")
+ content_json_str = ''
- # 检查message['1']['6']的结构
- message_1_6 = message_1.get('6', {}) if message_1 else {}
- logger.debug(f"【{self.cookie_id}】🔍 message['1']['6'] keys: {list(message_1_6.keys()) if message_1_6 else 'None'}")
+ if isinstance(message_1, dict):
+ logger.debug(f"【{self.cookie_id}】🔍 message['1'] 是字典,keys: {list(message_1.keys())}")
+
+ # 检查message['1']['6']的结构
+ message_1_6 = message_1.get('6', {})
+ if isinstance(message_1_6, dict):
+ logger.debug(f"【{self.cookie_id}】🔍 message['1']['6'] 是字典,keys: {list(message_1_6.keys())}")
+ # 方法1: 从button的targetUrl中提取orderId
+ content_json_str = message_1_6.get('3', {}).get('5', '') if isinstance(message_1_6.get('3', {}), dict) else ''
+ else:
+ logger.debug(f"【{self.cookie_id}】🔍 message['1']['6'] 不是字典: {type(message_1_6)}")
+
+ elif isinstance(message_1, list):
+ logger.debug(f"【{self.cookie_id}】🔍 message['1'] 是列表,长度: {len(message_1)}")
+ # 如果message['1']是列表,跳过这种提取方式
+
+ elif isinstance(message_1, str):
+ logger.debug(f"【{self.cookie_id}】🔍 message['1'] 是字符串,长度: {len(message_1)}")
+ # 如果message['1']是字符串,跳过这种提取方式
+
+ else:
+ logger.debug(f"【{self.cookie_id}】🔍 message['1'] 未知类型: {type(message_1)}")
+ # 其他类型,跳过这种提取方式
- # 方法1: 从button的targetUrl中提取orderId
- content_json_str = message.get('1', {}).get('6', {}).get('3', {}).get('5', '')
if content_json_str:
try:
content_data = json.loads(content_json_str)
@@ -287,10 +305,40 @@ class XianyuLive:
except Exception as parse_e:
logger.debug(f"解析dynamicOperation JSON失败: {parse_e}")
+ # 方法3: 如果前面的方法都失败,尝试在整个消息中搜索订单ID模式
+ if not order_id:
+ try:
+ # 将整个消息转换为字符串进行搜索
+ message_str = str(message)
+
+ # 搜索各种可能的订单ID模式
+ patterns = [
+ r'orderId[=:](\d{10,})', # orderId=123456789 或 orderId:123456789
+ r'order_detail\?id=(\d{10,})', # order_detail?id=123456789
+ r'"id"\s*:\s*"?(\d{10,})"?', # "id":"123456789" 或 "id":123456789
+ r'bizOrderId[=:](\d{10,})', # bizOrderId=123456789
+ ]
+
+ for pattern in patterns:
+ matches = re.findall(pattern, message_str)
+ if matches:
+ # 取第一个匹配的订单ID
+ order_id = matches[0]
+ logger.info(f'【{self.cookie_id}】✅ 从消息字符串中提取到订单ID: {order_id} (模式: {pattern})')
+ break
+
+ except Exception as search_e:
+ logger.debug(f"在消息字符串中搜索订单ID失败: {search_e}")
+
+ if order_id:
+ logger.info(f'【{self.cookie_id}】🎯 最终提取到订单ID: {order_id}')
+ else:
+ logger.debug(f'【{self.cookie_id}】❌ 未能从消息中提取到订单ID')
+
return order_id
except Exception as e:
- logger.error(f"提取订单ID失败: {self._safe_str(e)}")
+ logger.error(f"【{self.cookie_id}】提取订单ID失败: {self._safe_str(e)}")
return None
async def _handle_auto_delivery(self, websocket, message: dict, send_user_name: str, send_user_id: str,
@@ -321,7 +369,7 @@ class XianyuLive:
logger.info(f"【{self.cookie_id}】准备自动发货: item_id={item_id}, item_title={item_title}")
# 调用自动发货方法(包含自动确认发货)
- delivery_content = await self._auto_delivery(item_id, item_title, order_id)
+ delivery_content = await self._auto_delivery(item_id, item_title, order_id, send_user_id)
if delivery_content:
# 标记已发货(防重复)- 基于订单ID
@@ -394,18 +442,18 @@ class XianyuLive:
data = {
'data': data_val,
}
-
+
# 获取token
token = None
token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else ''
-
+
sign = generate_sign(params['t'], token, data_val)
params['sign'] = sign
-
+
# 发送请求
headers = DEFAULT_HEADERS.copy()
headers['cookie'] = self.cookies_str
-
+
async with aiohttp.ClientSession() as session:
async with session.post(
API_ENDPOINTS.get('token'),
@@ -414,7 +462,7 @@ class XianyuLive:
headers=headers
) as response:
res_json = await response.json()
-
+
# 检查并更新Cookie
if 'set-cookie' in response.headers:
new_cookies = {}
@@ -422,7 +470,7 @@ class XianyuLive:
if '=' in cookie:
name, value = cookie.split(';')[0].split('=', 1)
new_cookies[name.strip()] = value.strip()
-
+
# 更新cookies
if new_cookies:
self.cookies.update(new_cookies)
@@ -431,7 +479,7 @@ class XianyuLive:
# 更新数据库中的Cookie
await self.update_config_cookies()
logger.debug("已更新Cookie到数据库")
-
+
if isinstance(res_json, dict):
ret_value = res_json.get('ret', [])
# 检查ret是否包含成功信息
@@ -442,7 +490,7 @@ class XianyuLive:
self.last_token_refresh_time = time.time()
logger.info(f"【{self.cookie_id}】Token刷新成功")
return new_token
-
+
logger.error(f"【{self.cookie_id}】Token刷新失败: {res_json}")
# 发送Token刷新失败通知
await self.send_token_refresh_notification(f"Token刷新失败: {res_json}", "token_refresh_failed")
@@ -822,7 +870,7 @@ class XianyuLive:
if '=' in cookie:
name, value = cookie.split(';')[0].split('=', 1)
new_cookies[name.strip()] = value.strip()
-
+
# 更新cookies
if new_cookies:
self.cookies.update(new_cookies)
@@ -839,7 +887,7 @@ class XianyuLive:
# 检查ret是否包含成功信息
if not any('SUCCESS::调用成功' in ret for ret in ret_value):
logger.warning(f"商品信息API调用失败,错误信息: {ret_value}")
-
+
await asyncio.sleep(0.5)
return await self.get_item_info(item_id, retry_count + 1)
else:
@@ -982,9 +1030,9 @@ class XianyuLive:
return None
reply_content = default_reply_settings.get('reply_content', '')
- if not reply_content:
- logger.warning(f"账号 {self.cookie_id} 默认回复内容为空")
- return None
+ if not reply_content or (reply_content and reply_content.strip() == ''):
+ logger.info(f"账号 {self.cookie_id} 默认回复内容为空,不进行回复")
+ return "EMPTY_REPLY" # 返回特殊标记表示不回复
# 进行变量替换
try:
@@ -1039,7 +1087,12 @@ class XianyuLive:
# 图片类型关键词,发送图片
return await self._handle_image_keyword(keyword, image_url, send_user_name, send_user_id, send_message)
else:
- # 文本类型关键词,进行变量替换
+ # 文本类型关键词,检查回复内容是否为空
+ if not reply or (reply and reply.strip() == ''):
+ logger.info(f"商品ID关键词 '{keyword}' 回复内容为空,不进行回复")
+ return "EMPTY_REPLY" # 返回特殊标记表示匹配到但不回复
+
+ # 进行变量替换
try:
formatted_reply = reply.format(
send_user_name=send_user_name,
@@ -1069,7 +1122,12 @@ class XianyuLive:
# 图片类型关键词,发送图片
return await self._handle_image_keyword(keyword, image_url, send_user_name, send_user_id, send_message)
else:
- # 文本类型关键词,进行变量替换
+ # 文本类型关键词,检查回复内容是否为空
+ if not reply or (reply and reply.strip() == ''):
+ logger.info(f"通用关键词 '{keyword}' 回复内容为空,不进行回复")
+ return "EMPTY_REPLY" # 返回特殊标记表示匹配到但不回复
+
+ # 进行变量替换
try:
formatted_reply = reply.format(
send_user_name=send_user_name,
@@ -1254,14 +1312,17 @@ class XianyuLive:
from db_manager import db_manager
import aiohttp
+ logger.info(f"📱 开始发送消息通知 - 账号: {self.cookie_id}, 买家: {send_user_name}")
# 获取当前账号的通知配置
notifications = db_manager.get_account_notifications(self.cookie_id)
if not notifications:
- logger.debug(f"账号 {self.cookie_id} 未配置消息通知")
+ logger.warning(f"📱 账号 {self.cookie_id} 未配置消息通知,跳过通知发送")
return
+ logger.info(f"📱 找到 {len(notifications)} 个通知渠道配置")
+
# 构建通知消息
notification_msg = f"🚨 接收消息通知\n\n" \
f"账号: {self.cookie_id}\n" \
@@ -1271,38 +1332,54 @@ class XianyuLive:
f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
# 发送通知到各个渠道
- for notification in notifications:
+ for i, notification in enumerate(notifications, 1):
+ logger.info(f"📱 处理第 {i} 个通知渠道: {notification.get('channel_name', 'Unknown')}")
+
if not notification.get('enabled', True):
+ logger.warning(f"📱 通知渠道 {notification.get('channel_name')} 已禁用,跳过")
continue
channel_type = notification.get('channel_type')
channel_config = notification.get('channel_config')
+ logger.info(f"📱 渠道类型: {channel_type}, 配置: {channel_config}")
+
try:
# 解析配置数据
config_data = self._parse_notification_config(channel_config)
+ logger.info(f"📱 解析后的配置数据: {config_data}")
match channel_type:
case 'qq':
+ logger.info(f"📱 开始发送QQ通知...")
await self._send_qq_notification(config_data, notification_msg)
case 'ding_talk' | 'dingtalk':
+ logger.info(f"📱 开始发送钉钉通知...")
await self._send_dingtalk_notification(config_data, notification_msg)
case 'email':
+ logger.info(f"📱 开始发送邮件通知...")
await self._send_email_notification(config_data, notification_msg)
case 'webhook':
+ logger.info(f"📱 开始发送Webhook通知...")
await self._send_webhook_notification(config_data, notification_msg)
case 'wechat':
+ logger.info(f"📱 开始发送微信通知...")
await self._send_wechat_notification(config_data, notification_msg)
case 'telegram':
+ logger.info(f"📱 开始发送Telegram通知...")
await self._send_telegram_notification(config_data, notification_msg)
case _:
- logger.warning(f"不支持的通知渠道类型: {channel_type}")
+ logger.warning(f"📱 不支持的通知渠道类型: {channel_type}")
except Exception as notify_error:
- logger.error(f"发送通知失败 ({notification.get('channel_name', 'Unknown')}): {self._safe_str(notify_error)}")
+ logger.error(f"📱 发送通知失败 ({notification.get('channel_name', 'Unknown')}): {self._safe_str(notify_error)}")
+ import traceback
+ logger.error(f"📱 详细错误信息: {traceback.format_exc()}")
except Exception as e:
- logger.error(f"处理消息通知失败: {self._safe_str(e)}")
+ logger.error(f"📱 处理消息通知失败: {self._safe_str(e)}")
+ import traceback
+ logger.error(f"📱 详细错误信息: {traceback.format_exc()}")
def _parse_notification_config(self, config: str) -> dict:
"""解析通知配置数据"""
@@ -1319,12 +1396,16 @@ class XianyuLive:
try:
import aiohttp
+ logger.info(f"📱 QQ通知 - 开始处理配置数据: {config_data}")
+
# 解析配置(QQ号码)
qq_number = config_data.get('qq_number') or config_data.get('config', '')
qq_number = qq_number.strip() if qq_number else ''
+ logger.info(f"📱 QQ通知 - 解析到QQ号码: {qq_number}")
+
if not qq_number:
- logger.warning("QQ通知配置为空")
+ logger.warning("📱 QQ通知 - QQ号码配置为空,无法发送通知")
return
# 构建请求URL
@@ -1334,16 +1415,25 @@ class XianyuLive:
'msg': message
}
+ logger.info(f"📱 QQ通知 - 请求URL: {api_url}")
+ logger.info(f"📱 QQ通知 - 请求参数: qq={qq_number}, msg长度={len(message)}")
+
# 发送GET请求
async with aiohttp.ClientSession() as session:
async with session.get(api_url, params=params, timeout=10) as response:
+ response_text = await response.text()
+ logger.info(f"📱 QQ通知 - 响应状态: {response.status}")
+ logger.info(f"📱 QQ通知 - 响应内容: {response_text}")
+
if response.status == 200:
- logger.info(f"QQ通知发送成功: {qq_number}")
+ logger.info(f"📱 QQ通知发送成功: {qq_number}")
else:
- logger.warning(f"QQ通知发送失败: {response.status}")
+ logger.warning(f"📱 QQ通知发送失败: HTTP {response.status}, 响应: {response_text}")
except Exception as e:
- logger.error(f"发送QQ通知异常: {self._safe_str(e)}")
+ logger.error(f"📱 发送QQ通知异常: {self._safe_str(e)}")
+ import traceback
+ logger.error(f"📱 QQ通知异常详情: {traceback.format_exc()}")
async def _send_dingtalk_notification(self, config_data: dict, message: str):
"""发送钉钉通知"""
@@ -1788,25 +1878,37 @@ class XianyuLive:
except Exception as e:
logger.error(f"发送自动发货通知异常: {self._safe_str(e)}")
- async def auto_confirm(self, order_id, retry_count=0):
+ async def auto_confirm(self, order_id, item_id=None, retry_count=0):
"""自动确认发货 - 使用加密模块,不包含延时处理(延时已在_auto_delivery中处理)"""
try:
logger.debug(f"【{self.cookie_id}】开始确认发货,订单ID: {order_id}")
- # 导入超级混淆加密模块
- from secure_confirm_ultra import SecureConfirm
+ # 导入解密后的确认发货模块
+ from secure_confirm_decrypted import SecureConfirm
- # 创建加密确认实例
- secure_confirm = SecureConfirm(self.session, self.cookies_str, self.cookie_id)
+ # 创建确认实例,传入主界面类实例
+ secure_confirm = SecureConfirm(self.session, self.cookies_str, self.cookie_id, self)
# 传递必要的属性
secure_confirm.current_token = self.current_token
secure_confirm.last_token_refresh_time = self.last_token_refresh_time
secure_confirm.token_refresh_interval = self.token_refresh_interval
- secure_confirm.refresh_token = self.refresh_token # 传递refresh_token方法
- # 调用加密的确认方法
- return await secure_confirm.auto_confirm(order_id, retry_count)
+ # 调用确认方法,传入item_id用于token刷新
+ result = await secure_confirm.auto_confirm(order_id, item_id, retry_count)
+
+ # 同步更新后的cookies和token
+ if secure_confirm.cookies_str != self.cookies_str:
+ self.cookies_str = secure_confirm.cookies_str
+ self.cookies = secure_confirm.cookies
+ logger.debug(f"【{self.cookie_id}】已同步确认发货模块更新的cookies")
+
+ if secure_confirm.current_token != self.current_token:
+ self.current_token = secure_confirm.current_token
+ self.last_token_refresh_time = secure_confirm.last_token_refresh_time
+ logger.debug(f"【{self.cookie_id}】已同步确认发货模块更新的token")
+
+ return result
except Exception as e:
logger.error(f"【{self.cookie_id}】加密确认模块调用失败: {self._safe_str(e)}")
@@ -1836,20 +1938,26 @@ class XianyuLive:
logger.error(f"【{self.cookie_id}】加密免拼发货模块调用失败: {self._safe_str(e)}")
return {"error": f"加密免拼发货模块调用失败: {self._safe_str(e)}", "order_id": order_id}
- async def fetch_order_detail_info(self, order_id: str):
+ async def fetch_order_detail_info(self, order_id: str, item_id: str = None, buyer_id: str = None, debug_headless: bool = None):
"""获取订单详情信息"""
try:
logger.info(f"【{self.cookie_id}】开始获取订单详情: {order_id}")
# 导入订单详情获取器
from utils.order_detail_fetcher import fetch_order_detail_simple
+ from db_manager import db_manager
# 获取当前账号的cookie字符串
cookie_string = self.cookies_str
logger.debug(f"【{self.cookie_id}】使用Cookie长度: {len(cookie_string) if cookie_string else 0}")
- # 异步获取订单详情(使用当前账号的cookie和无头模式)
- result = await fetch_order_detail_simple(order_id, cookie_string, headless=True)
+ # 确定是否使用有头模式(调试用)
+ headless_mode = True if debug_headless is None else debug_headless
+ if not headless_mode:
+ logger.info(f"【{self.cookie_id}】🖥️ 启用有头模式进行调试")
+
+ # 异步获取订单详情(使用当前账号的cookie)
+ result = await fetch_order_detail_simple(order_id, cookie_string, headless=headless_mode)
if result:
logger.info(f"【{self.cookie_id}】订单详情获取成功: {order_id}")
@@ -1858,6 +1966,8 @@ class XianyuLive:
# 获取解析后的规格信息
spec_name = result.get('spec_name', '')
spec_value = result.get('spec_value', '')
+ quantity = result.get('quantity', '')
+ amount = result.get('amount', '')
if spec_name and spec_value:
logger.info(f"【{self.cookie_id}】📋 规格名称: {spec_name}")
@@ -1867,6 +1977,29 @@ class XianyuLive:
logger.warning(f"【{self.cookie_id}】未获取到有效的规格信息")
print(f"⚠️ 【{self.cookie_id}】订单 {order_id} 规格信息获取失败")
+ # 插入或更新订单信息到数据库
+ try:
+ success = db_manager.insert_or_update_order(
+ order_id=order_id,
+ item_id=item_id,
+ buyer_id=buyer_id,
+ spec_name=spec_name,
+ spec_value=spec_value,
+ quantity=quantity,
+ amount=amount,
+ order_status='processed', # 已处理状态
+ cookie_id=self.cookie_id
+ )
+
+ if success:
+ logger.info(f"【{self.cookie_id}】订单信息已保存到数据库: {order_id}")
+ print(f"💾 【{self.cookie_id}】订单 {order_id} 信息已保存到数据库")
+ else:
+ logger.warning(f"【{self.cookie_id}】订单信息保存失败: {order_id}")
+
+ except Exception as db_e:
+ logger.error(f"【{self.cookie_id}】保存订单信息到数据库失败: {self._safe_str(db_e)}")
+
return result
else:
logger.warning(f"【{self.cookie_id}】订单详情获取失败: {order_id}")
@@ -1876,7 +2009,7 @@ class XianyuLive:
logger.error(f"【{self.cookie_id}】获取订单详情异常: {self._safe_str(e)}")
return None
- async def _auto_delivery(self, item_id: str, item_title: str = None, order_id: str = None):
+ async def _auto_delivery(self, item_id: str, item_title: str = None, order_id: str = None, send_user_id: str = None):
"""自动发货功能 - 获取卡券规则,执行延时,确认发货,发送内容"""
try:
from db_manager import db_manager
@@ -1987,7 +2120,7 @@ class XianyuLive:
if is_multi_spec and order_id:
logger.info(f"检测到多规格商品,获取订单规格信息: {order_id}")
try:
- order_detail = await self.fetch_order_detail_info(order_id)
+ order_detail = await self.fetch_order_detail_info(order_id, item_id, send_user_id)
if order_detail:
spec_name = order_detail.get('spec_name', '')
spec_value = order_detail.get('spec_value', '')
@@ -2087,8 +2220,8 @@ class XianyuLive:
should_confirm = False
if should_confirm:
- logger.info(f"开始自动确认发货: 订单ID={order_id}")
- confirm_result = await self.auto_confirm(order_id)
+ logger.info(f"开始自动确认发货: 订单ID={order_id}, 商品ID={item_id}")
+ confirm_result = await self.auto_confirm(order_id, item_id)
if confirm_result.get('success'):
self.confirmed_orders[order_id] = current_time
logger.info(f"🎉 自动确认发货成功!订单ID: {order_id}")
@@ -2098,6 +2231,23 @@ class XianyuLive:
# 检查是否存在订单ID,只有存在订单ID才处理发货内容
if order_id:
+ # 保存订单基本信息到数据库(如果还没有详细信息)
+ try:
+ from db_manager import db_manager
+ existing_order = db_manager.get_order_by_id(order_id)
+ if not existing_order:
+ # 插入基本订单信息
+ db_manager.insert_or_update_order(
+ order_id=order_id,
+ item_id=item_id,
+ buyer_id=send_user_id,
+ order_status='processing', # 处理中状态
+ cookie_id=self.cookie_id
+ )
+ logger.info(f"保存基本订单信息到数据库: {order_id}")
+ except Exception as db_e:
+ logger.error(f"保存基本订单信息失败: {self._safe_str(db_e)}")
+
# 开始处理发货内容
logger.info(f"开始处理发货内容,规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})")
@@ -2384,7 +2534,7 @@ class XianyuLive:
else:
logger.info("由于刚刚尝试过token刷新,跳过重复的初始化失败通知")
raise Exception("Token获取失败")
-
+
msg = {
"lwp": "/reg",
"headers": {
@@ -2572,8 +2722,8 @@ class XianyuLive:
"""判断是否为用户聊天消息"""
try:
return (
- isinstance(message, dict)
- and "1" in message
+ isinstance(message, dict)
+ and "1" in message
and isinstance(message["1"], dict)
and "10" in message["1"]
and isinstance(message["1"]["10"], dict)
@@ -2621,7 +2771,7 @@ class XianyuLive:
api_config = AUTO_REPLY.get('api', {})
timeout = aiohttp.ClientTimeout(total=api_config.get('timeout', 10))
-
+
payload = {
"cookie_id": self.cookie_id,
"msg_time": msg_time,
@@ -2632,14 +2782,14 @@ class XianyuLive:
"send_message": send_message,
"chat_id": chat_id
}
-
+
async with self.session.post(
api_config.get('url', 'http://localhost:8080/xianyu/reply'),
json=payload,
timeout=timeout
) as response:
result = await response.json()
-
+
# 将code转换为字符串进行比较,或者直接用数字比较
if str(result.get('code')) == '200' or result.get('code') == 200:
send_msg = result.get('data', {}).get('send_msg')
@@ -2656,7 +2806,7 @@ class XianyuLive:
else:
logger.warning(f"API返回错误: {result.get('msg', '未知错误')}")
return None
-
+
except asyncio.TimeoutError:
logger.error("API调用超时")
return None
@@ -2699,7 +2849,7 @@ class XianyuLive:
# 获取并解密数据
sync_data = message_data["body"]["syncPushPackage"]["data"][0]
-
+
# 检查是否有必要的字段
if "data" not in sync_data:
logger.debug("同步包中无data字段")
@@ -2749,6 +2899,62 @@ class XianyuLive:
logger.debug(f"消息内容: {message}")
return
+ # 【优先处理】尝试获取订单ID并获取订单详情
+ order_id = None
+ try:
+ order_id = self._extract_order_id(message)
+ if order_id:
+ msg_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+ logger.info(f'[{msg_time}] 【{self.cookie_id}】✅ 检测到订单ID: {order_id},开始获取订单详情')
+
+ # 立即获取订单详情信息
+ try:
+ # 先尝试提取用户ID和商品ID用于订单详情获取
+ temp_user_id = None
+ temp_item_id = None
+
+ # 提取用户ID
+ try:
+ message_1 = message.get("1")
+ if isinstance(message_1, str) and '@' in message_1:
+ temp_user_id = message_1.split('@')[0]
+ elif isinstance(message_1, dict):
+ # 从字典中提取用户ID
+ if "10" in message_1 and isinstance(message_1["10"], dict):
+ temp_user_id = message_1["10"].get("senderUserId", "unknown_user")
+ else:
+ temp_user_id = "unknown_user"
+ else:
+ temp_user_id = "unknown_user"
+ except:
+ temp_user_id = "unknown_user"
+
+ # 提取商品ID
+ try:
+ if "1" in message and isinstance(message["1"], dict) and "10" in message["1"] and isinstance(message["1"]["10"], dict):
+ url_info = message["1"]["10"].get("reminderUrl", "")
+ if isinstance(url_info, str) and "itemId=" in url_info:
+ temp_item_id = url_info.split("itemId=")[1].split("&")[0]
+
+ if not temp_item_id:
+ temp_item_id = self.extract_item_id_from_message(message)
+ except:
+ pass
+
+ # 调用订单详情获取方法
+ order_detail = await self.fetch_order_detail_info(order_id, temp_item_id, temp_user_id)
+ if order_detail:
+ logger.info(f'[{msg_time}] 【{self.cookie_id}】✅ 订单详情获取成功: {order_id}')
+ else:
+ logger.warning(f'[{msg_time}] 【{self.cookie_id}】⚠️ 订单详情获取失败: {order_id}')
+
+ except Exception as detail_e:
+ logger.error(f'[{msg_time}] 【{self.cookie_id}】❌ 获取订单详情异常: {self._safe_str(detail_e)}')
+ else:
+ logger.debug(f"【{self.cookie_id}】未检测到订单ID")
+ except Exception as e:
+ logger.error(f"【{self.cookie_id}】提取订单ID失败: {self._safe_str(e)}")
+
# 安全地获取用户ID
user_id = None
try:
@@ -2756,8 +2962,11 @@ class XianyuLive:
if isinstance(message_1, str) and '@' in message_1:
user_id = message_1.split('@')[0]
elif isinstance(message_1, dict):
- # 如果message['1']是字典,尝试其他方式提取user_id
- user_id = "unknown_user"
+ # 如果message['1']是字典,从message["1"]["10"]["senderUserId"]中提取user_id
+ if "10" in message_1 and isinstance(message_1["10"], dict):
+ user_id = message_1["10"].get("senderUserId", "unknown_user")
+ else:
+ user_id = "unknown_user"
else:
user_id = "unknown_user"
except Exception as e:
@@ -2856,6 +3065,11 @@ class XianyuLive:
else:
logger.info(f"[{msg_time}] 【收到】用户: {send_user_name} (ID: {send_user_id}), 商品({item_id}): {send_message}")
+ # 🔔 立即发送消息通知(独立于自动回复功能)
+ try:
+ await self.send_notification(send_user_name, send_user_id, send_message, item_id)
+ except Exception as notify_error:
+ logger.error(f"📱 发送消息通知失败: {self._safe_str(notify_error)}")
@@ -2875,7 +3089,7 @@ class XianyuLive:
# 构造用户URL
user_url = f'https://www.goofish.com/personal?userId={send_user_id}'
-
+
reply = None
# 判断是否启用API回复
if AUTO_REPLY.get('api', {}).get('enabled', False):
@@ -2885,7 +3099,7 @@ class XianyuLive:
)
if not reply:
logger.error(f"[{msg_time}] 【API调用失败】用户: {send_user_name} (ID: {send_user_id}), 商品({item_id}): {send_message}")
-
+
if send_message == '[我已拍下,待付款]':
logger.info(f'[{msg_time}] 【{self.cookie_id}】系统消息不处理')
return
@@ -2978,7 +3192,11 @@ class XianyuLive:
if not reply:
# 1. 首先尝试关键词匹配(传入商品ID)
reply = await self.get_keyword_reply(send_user_name, send_user_id, send_message, item_id)
- if reply:
+ if reply == "EMPTY_REPLY":
+ # 匹配到关键词但回复内容为空,不进行任何回复
+ logger.info(f"[{msg_time}] 【{self.cookie_id}】匹配到空回复关键词,跳过自动回复")
+ return
+ elif reply:
reply_source = '关键词' # 标记为关键词回复
else:
# 2. 关键词匹配失败,如果AI开关打开,尝试AI回复
@@ -2988,12 +3206,15 @@ class XianyuLive:
else:
# 3. 最后使用默认回复
reply = await self.get_default_reply(send_user_name, send_user_id, send_message, chat_id)
+ if reply == "EMPTY_REPLY":
+ # 默认回复内容为空,不进行任何回复
+ logger.info(f"[{msg_time}] 【{self.cookie_id}】默认回复内容为空,跳过自动回复")
+ return
reply_source = '默认' # 标记为默认回复
# 注意:这里只有商品ID,没有标题和详情,根据新的规则不保存到数据库
# 商品信息会在其他有完整信息的地方保存(如发货规则匹配时)
- # 发送通知
- await self.send_notification(send_user_name, send_user_id, send_message, item_id)
+ # 消息通知已在收到消息时立即发送,此处不再重复发送
# 如果有回复内容,发送消息
if reply:
@@ -3022,7 +3243,7 @@ class XianyuLive:
else:
msg_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
logger.info(f"[{msg_time}] 【{self.cookie_id}】【系统】未找到匹配的回复规则,不回复")
-
+
except Exception as e:
logger.error(f"处理消息时发生错误: {self._safe_str(e)}")
logger.debug(f"原始消息: {message_data}")
@@ -3194,7 +3415,7 @@ class XianyuLive:
if '=' in cookie:
name, value = cookie.split(';')[0].split('=', 1)
new_cookies[name.strip()] = value.strip()
-
+
# 更新cookies
if new_cookies:
self.cookies.update(new_cookies)
diff --git a/db_manager.py b/db_manager.py
index 3e3cb84..c1eac1c 100644
--- a/db_manager.py
+++ b/db_manager.py
@@ -212,6 +212,24 @@ class DBManager:
)
''')
+ # 创建订单表
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS orders (
+ order_id TEXT PRIMARY KEY,
+ item_id TEXT,
+ buyer_id TEXT,
+ spec_name TEXT,
+ spec_value TEXT,
+ quantity TEXT,
+ amount TEXT,
+ order_status TEXT DEFAULT 'unknown',
+ cookie_id TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE
+ )
+ ''')
+
# 检查并添加 user_id 列(用于数据库迁移)
try:
self._execute_sql(cursor, "SELECT user_id FROM cards LIMIT 1")
@@ -4017,6 +4035,138 @@ class DBManager:
logger.error(f"获取表数据失败: {table_name} - {e}")
return [], []
+ def insert_or_update_order(self, order_id: str, item_id: str = None, buyer_id: str = None,
+ spec_name: str = None, spec_value: str = None, quantity: str = None,
+ amount: str = None, order_status: str = None, cookie_id: str = None):
+ """插入或更新订单信息"""
+ with self.lock:
+ try:
+ cursor = self.conn.cursor()
+
+ # 检查订单是否已存在
+ cursor.execute("SELECT order_id FROM orders WHERE order_id = ?", (order_id,))
+ existing = cursor.fetchone()
+
+ if existing:
+ # 更新现有订单
+ update_fields = []
+ update_values = []
+
+ if item_id is not None:
+ update_fields.append("item_id = ?")
+ update_values.append(item_id)
+ if buyer_id is not None:
+ update_fields.append("buyer_id = ?")
+ update_values.append(buyer_id)
+ if spec_name is not None:
+ update_fields.append("spec_name = ?")
+ update_values.append(spec_name)
+ if spec_value is not None:
+ update_fields.append("spec_value = ?")
+ update_values.append(spec_value)
+ if quantity is not None:
+ update_fields.append("quantity = ?")
+ update_values.append(quantity)
+ if amount is not None:
+ update_fields.append("amount = ?")
+ update_values.append(amount)
+ if order_status is not None:
+ update_fields.append("order_status = ?")
+ update_values.append(order_status)
+ if cookie_id is not None:
+ update_fields.append("cookie_id = ?")
+ update_values.append(cookie_id)
+
+ if update_fields:
+ update_fields.append("updated_at = CURRENT_TIMESTAMP")
+ update_values.append(order_id)
+
+ sql = f"UPDATE orders SET {', '.join(update_fields)} WHERE order_id = ?"
+ cursor.execute(sql, update_values)
+ logger.info(f"更新订单信息: {order_id}")
+ else:
+ # 插入新订单
+ cursor.execute('''
+ INSERT INTO orders (order_id, item_id, buyer_id, spec_name, spec_value,
+ quantity, amount, order_status, cookie_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ''', (order_id, item_id, buyer_id, spec_name, spec_value,
+ quantity, amount, order_status or 'unknown', cookie_id))
+ logger.info(f"插入新订单: {order_id}")
+
+ self.conn.commit()
+ return True
+
+ except Exception as e:
+ logger.error(f"插入或更新订单失败: {order_id} - {e}")
+ self.conn.rollback()
+ return False
+
+ def get_order_by_id(self, order_id: str):
+ """根据订单ID获取订单信息"""
+ with self.lock:
+ try:
+ cursor = self.conn.cursor()
+ cursor.execute('''
+ SELECT order_id, item_id, buyer_id, spec_name, spec_value,
+ quantity, amount, order_status, cookie_id, created_at, updated_at
+ FROM orders WHERE order_id = ?
+ ''', (order_id,))
+
+ row = cursor.fetchone()
+ if row:
+ return {
+ 'order_id': row[0],
+ 'item_id': row[1],
+ 'buyer_id': row[2],
+ 'spec_name': row[3],
+ 'spec_value': row[4],
+ 'quantity': row[5],
+ 'amount': row[6],
+ 'order_status': row[7],
+ 'cookie_id': row[8],
+ 'created_at': row[9],
+ 'updated_at': row[10]
+ }
+ return None
+
+ except Exception as e:
+ logger.error(f"获取订单信息失败: {order_id} - {e}")
+ return None
+
+ def get_orders_by_cookie(self, cookie_id: str, limit: int = 100):
+ """根据Cookie ID获取订单列表"""
+ with self.lock:
+ try:
+ cursor = self.conn.cursor()
+ cursor.execute('''
+ SELECT order_id, item_id, buyer_id, spec_name, spec_value,
+ quantity, amount, order_status, created_at, updated_at
+ FROM orders WHERE cookie_id = ?
+ ORDER BY created_at DESC LIMIT ?
+ ''', (cookie_id, limit))
+
+ orders = []
+ for row in cursor.fetchall():
+ orders.append({
+ 'order_id': row[0],
+ 'item_id': row[1],
+ 'buyer_id': row[2],
+ 'spec_name': row[3],
+ 'spec_value': row[4],
+ 'quantity': row[5],
+ 'amount': row[6],
+ 'order_status': row[7],
+ 'created_at': row[8],
+ 'updated_at': row[9]
+ })
+
+ return orders
+
+ except Exception as e:
+ logger.error(f"获取Cookie订单列表失败: {cookie_id} - {e}")
+ return []
+
def delete_table_record(self, table_name: str, record_id: str):
"""删除指定表的指定记录"""
with self.lock:
@@ -4036,7 +4186,8 @@ class DBManager:
'notification_channels': 'id',
'user_settings': 'id',
'email_verifications': 'id',
- 'captcha_codes': 'id'
+ 'captcha_codes': 'id',
+ 'orders': 'order_id'
}
primary_key = primary_key_map.get(table_name, 'id')
diff --git a/reply_server.py b/reply_server.py
index 46232d8..9b7c45e 100644
--- a/reply_server.py
+++ b/reply_server.py
@@ -477,6 +477,9 @@ async def data_management_page():
return HTMLResponse('
Data management page not found
')
+
+
+
# 商品搜索页面路由
@app.get('/item_search.html', response_class=HTMLResponse)
async def item_search_page():
@@ -1893,8 +1896,8 @@ def update_keywords_with_item_id(cid: str, body: KeywordWithItemIdIn, current_us
reply = kw_data.get('reply', '').strip()
item_id = kw_data.get('item_id', '').strip() or None
- if not keyword or not reply:
- raise HTTPException(status_code=400, detail="关键词和回复内容不能为空")
+ if not keyword:
+ raise HTTPException(status_code=400, detail="关键词不能为空")
# 检查当前提交的关键词中是否有重复
keyword_key = f"{keyword}|{item_id or ''}"
@@ -2105,8 +2108,8 @@ async def import_keywords(cid: str, file: UploadFile = File(...), current_user:
item_id = str(row['商品ID']).strip() if pd.notna(row['商品ID']) and str(row['商品ID']).strip() else None
reply = str(row['关键词内容']).strip()
- if not keyword or not reply:
- continue # 跳过空行
+ if not keyword:
+ continue # 跳过没有关键词的行
# 检查是否重复
key = f"{keyword}|{item_id or ''}"
@@ -3661,7 +3664,7 @@ def get_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(require
'users', 'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records',
'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info',
'message_notifications', 'cards', 'delivery_rules', 'notification_channels',
- 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes'
+ 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders'
]
if table_name not in allowed_tables:
@@ -3698,7 +3701,7 @@ def delete_table_record(table_name: str, record_id: str, admin_user: Dict[str, A
'users', 'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records',
'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info',
'message_notifications', 'cards', 'delivery_rules', 'notification_channels',
- 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes'
+ 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders'
]
if table_name not in allowed_tables:
@@ -3738,7 +3741,7 @@ def clear_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(requi
'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records',
'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info',
'message_notifications', 'cards', 'delivery_rules', 'notification_channels',
- 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes'
+ 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders'
]
# 不允许清空用户表
diff --git a/secure_confirm_decrypted.py b/secure_confirm_decrypted.py
new file mode 100644
index 0000000..09bde9d
--- /dev/null
+++ b/secure_confirm_decrypted.py
@@ -0,0 +1,345 @@
+"""
+自动确认发货模块 - 解密版本
+这是secure_confirm_ultra.py的解密版本,用于自动确认发货功能
+"""
+
+import asyncio
+import json
+import time
+import aiohttp
+from loguru import logger
+from utils.xianyu_utils import generate_sign, trans_cookies
+
+
+class SecureConfirm:
+ """自动确认发货类"""
+
+ def __init__(self, session, cookies_str, cookie_id, main_instance=None):
+ """
+ 初始化确认发货实例
+
+ Args:
+ session: aiohttp会话对象
+ cookies_str: Cookie字符串
+ cookie_id: Cookie ID
+ main_instance: 主实例对象(XianyuLive)
+ """
+ self.session = session
+ self.cookies_str = cookies_str
+ self.cookie_id = cookie_id
+ self.main_instance = main_instance
+
+ # 解析cookies
+ self.cookies = trans_cookies(cookies_str) if cookies_str else {}
+
+ # Token相关属性
+ self.current_token = None
+ self.last_token_refresh_time = 0
+ self.token_refresh_interval = 3600 # 1小时
+
+ def _safe_str(self, obj):
+ """安全字符串转换"""
+ try:
+ return str(obj)
+ except:
+ return "无法转换的对象"
+
+ async def _get_real_item_id(self):
+ """从数据库中获取一个真实的商品ID"""
+ try:
+ from db_manager import db_manager
+
+ # 获取该账号的商品列表
+ items = db_manager.get_items_by_cookie(self.cookie_id)
+ if items:
+ # 返回第一个商品的ID
+ item_id = items[0].get('item_id')
+ if item_id:
+ logger.debug(f"【{self.cookie_id}】获取到真实商品ID: {item_id}")
+ return item_id
+
+ # 如果该账号没有商品,尝试获取任意一个商品ID
+ all_items = db_manager.get_all_items()
+ if all_items:
+ item_id = all_items[0].get('item_id')
+ if item_id:
+ logger.debug(f"【{self.cookie_id}】使用其他账号的商品ID: {item_id}")
+ return item_id
+
+ logger.warning(f"【{self.cookie_id}】数据库中没有找到任何商品ID")
+ return None
+
+ except Exception as e:
+ logger.error(f"【{self.cookie_id}】获取真实商品ID失败: {self._safe_str(e)}")
+ return None
+
+ async def refresh_token_by_detail_api(self, retry_count=0):
+ """通过商品详情API刷新token - 参照get_item_info方法"""
+ if retry_count >= 3: # 最多重试2次
+ logger.error(f"【{self.cookie_id}】通过详情API刷新token失败,重试次数过多")
+ return False
+
+ try:
+ # 优先使用传入的item_id,否则从数据库获取
+ real_item_id = None
+ if hasattr(self, '_current_item_id') and self._current_item_id:
+ real_item_id = self._current_item_id
+ logger.debug(f"【{self.cookie_id}】使用传入的商品ID: {real_item_id}")
+ else:
+ # 从数据库中获取一个真实的商品ID来请求详情API
+ real_item_id = await self._get_real_item_id()
+
+ if not real_item_id:
+ logger.warning(f"【{self.cookie_id}】无法获取真实商品ID,使用默认ID")
+ real_item_id = "123456789" # 备用ID
+
+ params = {
+ 'jsv': '2.7.2',
+ 'appKey': '34839810',
+ 't': str(int(time.time()) * 1000),
+ 'sign': '',
+ 'v': '1.0',
+ 'type': 'originaljson',
+ 'accountSite': 'xianyu',
+ 'dataType': 'json',
+ 'timeout': '20000',
+ 'api': 'mtop.taobao.idle.pc.detail',
+ 'sessionOption': 'AutoLoginOnly',
+ }
+
+ data_val = f'{{"itemId":"{real_item_id}"}}'
+ data = {
+ 'data': data_val,
+ }
+
+ # 从当前cookies中获取token
+ token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else ''
+
+ if token:
+ logger.debug(f"【{self.cookie_id}】使用当前token刷新: {token}")
+ else:
+ logger.warning(f"【{self.cookie_id}】当前cookies中没有找到token")
+
+ # 生成签名
+ sign = generate_sign(params['t'], token, data_val)
+ params['sign'] = sign
+
+ logger.info(f"【{self.cookie_id}】通过详情API刷新token,使用商品ID: {real_item_id}")
+
+ async with self.session.post(
+ 'https://h5api.m.goofish.com/h5/mtop.taobao.idle.pc.detail/1.0/',
+ params=params,
+ data=data
+ ) as response:
+ # 检查并更新Cookie
+ if 'set-cookie' in response.headers:
+ new_cookies = {}
+ for cookie in response.headers.getall('set-cookie', []):
+ if '=' in cookie:
+ name, value = cookie.split(';')[0].split('=', 1)
+ new_cookies[name.strip()] = value.strip()
+
+ # 更新cookies
+ if new_cookies:
+ self.cookies.update(new_cookies)
+ # 生成新的cookie字符串
+ self.cookies_str = '; '.join([f"{k}={v}" for k, v in self.cookies.items()])
+ # 更新数据库中的Cookie
+ await self._update_config_cookies()
+ logger.debug(f"【{self.cookie_id}】已通过详情API更新Cookie到数据库")
+
+ # 获取新的token
+ new_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else ''
+ if new_token and new_token != token:
+ self.current_token = new_token
+ self.last_token_refresh_time = time.time()
+ logger.info(f"【{self.cookie_id}】通过详情API成功刷新token: {new_token}")
+ return True
+ else:
+ logger.debug(f"【{self.cookie_id}】详情API返回的token未变化")
+
+ # 检查响应状态
+ try:
+ res_json = await response.json()
+ if isinstance(res_json, dict):
+ ret_value = res_json.get('ret', [])
+ if any('SUCCESS::调用成功' in ret for ret in ret_value):
+ logger.debug(f"【{self.cookie_id}】详情API调用成功")
+ return True
+ else:
+ logger.warning(f"【{self.cookie_id}】详情API调用失败: {ret_value}")
+ if retry_count < 2:
+ await asyncio.sleep(0.5)
+ return await self.refresh_token_by_detail_api(retry_count + 1)
+ except:
+ logger.debug(f"【{self.cookie_id}】详情API响应解析失败,但可能已获取到新cookies")
+
+ return bool(self.current_token)
+
+ except Exception as e:
+ logger.error(f"【{self.cookie_id}】通过详情API刷新token异常: {self._safe_str(e)}")
+ if retry_count < 2:
+ await asyncio.sleep(0.5)
+ return await self.refresh_token_by_detail_api(retry_count + 1)
+ return False
+
+ async def _update_config_cookies(self):
+ """更新数据库中的Cookie配置"""
+ try:
+ from db_manager import db_manager
+ # 更新数据库中的cookies
+ db_manager.update_cookie_value(self.cookie_id, self.cookies_str)
+ logger.debug(f"【{self.cookie_id}】已更新数据库中的Cookie")
+ except Exception as e:
+ logger.error(f"【{self.cookie_id}】更新数据库Cookie失败: {self._safe_str(e)}")
+
+ async def refresh_token(self):
+ """刷新token - 优先使用详情API,失败时调用主界面类的方法"""
+ # 首先尝试通过详情API刷新token
+ success = await self.refresh_token_by_detail_api()
+ if success:
+ return self.current_token
+
+ # 如果详情API失败,尝试调用主界面类的方法
+ if self.main_instance and hasattr(self.main_instance, 'refresh_token'):
+ try:
+ logger.debug(f"【{self.cookie_id}】详情API刷新失败,调用主界面类的refresh_token方法")
+ new_token = await self.main_instance.refresh_token()
+ if new_token:
+ self.current_token = new_token
+ self.last_token_refresh_time = time.time()
+ # 更新本地的cookies_str
+ self.cookies_str = self.main_instance.cookies_str
+ # 重新解析cookies
+ self.cookies = {}
+ if self.cookies_str:
+ for cookie in self.cookies_str.split(';'):
+ if '=' in cookie:
+ key, value = cookie.strip().split('=', 1)
+ self.cookies[key] = value
+ logger.debug(f"【{self.cookie_id}】通过主界面类Token刷新成功,已同步cookies")
+ return new_token
+ else:
+ logger.warning(f"【{self.cookie_id}】主界面类Token刷新失败")
+ return None
+ except Exception as e:
+ logger.error(f"【{self.cookie_id}】调用主界面类refresh_token失败: {self._safe_str(e)}")
+ return None
+ else:
+ logger.warning(f"【{self.cookie_id}】主界面类实例不存在或没有refresh_token方法")
+ return None
+
+ async def auto_confirm(self, order_id, item_id=None, retry_count=0):
+ """自动确认发货 - 使用真实商品ID刷新token"""
+ if retry_count >= 4: # 最多重试3次
+ logger.error("自动确认发货失败,重试次数过多")
+ return {"error": "自动确认发货失败,重试次数过多"}
+
+ # 保存item_id供Token刷新使用
+ if item_id:
+ self._current_item_id = item_id
+ logger.debug(f"【{self.cookie_id}】设置当前商品ID: {item_id}")
+
+ # 如果是重试(retry_count > 0),强制刷新token
+ if retry_count > 0:
+ old_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else ''
+ logger.info(f"【{self.cookie_id}】重试第{retry_count}次,强制刷新token... 当前_m_h5_tk: {old_token}")
+ await self.refresh_token()
+ new_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else ''
+ logger.info(f"【{self.cookie_id}】重试刷新token完成,新的_m_h5_tk: {new_token}")
+ else:
+ # 确保使用最新的token(首次调用时的正常逻辑)
+ if not self.current_token or (time.time() - self.last_token_refresh_time) >= self.token_refresh_interval:
+ old_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else ''
+ logger.info(f"【{self.cookie_id}】Token过期或不存在,刷新token... 当前_m_h5_tk: {old_token}")
+ await self.refresh_token()
+ new_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else ''
+ logger.info(f"【{self.cookie_id}】Token刷新完成,新的_m_h5_tk: {new_token}")
+
+ # 确保session已创建
+ if not self.session:
+ raise Exception("Session未创建")
+
+ params = {
+ 'jsv': '2.7.2',
+ 'appKey': '34839810',
+ 't': str(int(time.time()) * 1000),
+ 'sign': '',
+ 'v': '1.0',
+ 'type': 'originaljson',
+ 'accountSite': 'xianyu',
+ 'dataType': 'json',
+ 'timeout': '20000',
+ 'api': 'mtop.taobao.idle.logistic.consign.dummy',
+ 'sessionOption': 'AutoLoginOnly',
+ }
+
+ data_val = '{"orderId":"' + order_id + '", "tradeText":"","picList":[],"newUnconsign":true}'
+ data = {
+ 'data': data_val,
+ }
+
+ # 始终从最新的cookies中获取_m_h5_tk token(刷新后cookies会被更新)
+ token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else ''
+
+ if token:
+ logger.info(f"使用cookies中的_m_h5_tk token: {token}")
+ else:
+ logger.warning("cookies中没有找到_m_h5_tk token")
+
+ sign = generate_sign(params['t'], token, data_val)
+ params['sign'] = sign
+
+ try:
+ logger.info(f"【{self.cookie_id}】开始自动确认发货,订单ID: {order_id}")
+ async with self.session.post(
+ 'https://h5api.m.goofish.com/h5/mtop.taobao.idle.logistic.consign.dummy/1.0/',
+ params=params,
+ data=data
+ ) as response:
+ res_json = await response.json()
+
+ # 检查并更新Cookie
+ if 'set-cookie' in response.headers:
+ new_cookies = {}
+ for cookie in response.headers.getall('set-cookie', []):
+ if '=' in cookie:
+ name, value = cookie.split(';')[0].split('=', 1)
+ new_cookies[name.strip()] = value.strip()
+
+ # 更新cookies
+ if new_cookies:
+ self.cookies.update(new_cookies)
+ # 生成新的cookie字符串
+ self.cookies_str = '; '.join([f"{k}={v}" for k, v in self.cookies.items()])
+ # 更新数据库中的Cookie
+ await self._update_config_cookies()
+ logger.debug("已更新Cookie到数据库")
+
+ logger.info(f"【{self.cookie_id}】自动确认发货响应: {res_json}")
+
+ # 检查响应结果
+ if res_json.get('ret') and res_json['ret'][0] == 'SUCCESS::调用成功':
+ logger.info(f"【{self.cookie_id}】✅ 自动确认发货成功,订单ID: {order_id}")
+ return {"success": True, "order_id": order_id}
+ else:
+ error_msg = res_json.get('ret', ['未知错误'])[0] if res_json.get('ret') else '未知错误'
+ logger.warning(f"【{self.cookie_id}】❌ 自动确认发货失败: {error_msg}")
+
+ # 如果是token相关错误,进行重试
+ if 'token' in error_msg.lower() or 'sign' in error_msg.lower():
+ logger.info(f"【{self.cookie_id}】检测到token错误,准备重试...")
+ return await self.auto_confirm(order_id, item_id, retry_count + 1)
+
+ return {"error": error_msg, "order_id": order_id}
+
+ except Exception as e:
+ logger.error(f"【{self.cookie_id}】自动确认发货API请求异常: {self._safe_str(e)}")
+ await asyncio.sleep(0.5)
+
+ # 网络异常也进行重试
+ if retry_count < 2:
+ logger.info(f"【{self.cookie_id}】网络异常,准备重试...")
+ return await self.auto_confirm(order_id, item_id, retry_count + 1)
+
+ return {"error": f"网络异常: {self._safe_str(e)}", "order_id": order_id}
diff --git a/static/css/dashboard.css b/static/css/dashboard.css
index bdd29cd..531c662 100644
--- a/static/css/dashboard.css
+++ b/static/css/dashboard.css
@@ -49,6 +49,11 @@
color: var(--warning-color);
}
+.stat-icon.info {
+ background: rgba(59, 130, 246, 0.1);
+ color: #3b82f6;
+}
+
.stat-number {
font-size: 2rem;
font-weight: 700;
diff --git a/static/data_management.html b/static/data_management.html
index cd0f83a..207168a 100644
--- a/static/data_management.html
+++ b/static/data_management.html
@@ -111,6 +111,7 @@
+
@@ -275,7 +276,8 @@
'user_settings': '用户设置表',
'system_settings': '系统设置表',
'email_verifications': '邮箱验证表',
- 'captcha_codes': '验证码表'
+ 'captcha_codes': '验证码表',
+ 'orders': '订单表'
};
// 页面加载完成后初始化
diff --git a/static/index.html b/static/index.html
index ed1146b..cdc5317 100644
--- a/static/index.html
+++ b/static/index.html
@@ -42,6 +42,12 @@
商品管理
+
+
@@ -443,6 +456,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/js/app.js b/static/js/app.js
index f014999..bf239df 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -25,6 +25,14 @@ let itemsPerPage = 20; // 每页显示数量
let totalItemsPages = 0; // 总页数
let currentSearchKeyword = ''; // 当前搜索关键词
+// 订单列表搜索和分页相关变量
+let allOrdersData = []; // 存储所有订单数据
+let filteredOrdersData = []; // 存储过滤后的订单数据
+let currentOrdersPage = 1; // 当前页码
+let ordersPerPage = 20; // 每页显示数量
+let totalOrdersPages = 0; // 总页数
+let currentOrderSearchKeyword = ''; // 当前搜索关键词
+
// ================================
// 通用功能 - 菜单切换和导航
// ================================
@@ -69,6 +77,9 @@ function showSection(sectionName) {
case 'items': // 【商品管理菜单】
loadItems();
break;
+ case 'orders': // 【订单管理菜单】
+ loadOrders();
+ break;
case 'auto-reply': // 【自动回复菜单】
refreshAccountList();
break;
@@ -191,6 +202,9 @@ async function loadDashboard() {
dashboardData.totalKeywords = totalKeywords;
+ // 加载订单数量
+ await loadOrdersCount();
+
// 更新仪表盘显示
updateDashboardStats(accountsWithKeywords.length, totalKeywords, enabledAccounts);
updateDashboardAccountsList(accountsWithKeywords);
@@ -203,6 +217,30 @@ async function loadDashboard() {
}
}
+// 加载订单数量
+async function loadOrdersCount() {
+ try {
+ const token = localStorage.getItem('auth_token');
+ const response = await fetch('/admin/data/orders', {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ const ordersCount = data.data ? data.data.length : 0;
+ document.getElementById('totalOrders').textContent = ordersCount;
+ } else {
+ console.error('加载订单数量失败:', data.message);
+ document.getElementById('totalOrders').textContent = '0';
+ }
+ } catch (error) {
+ console.error('加载订单数量失败:', error);
+ document.getElementById('totalOrders').textContent = '0';
+ }
+}
+
// 更新仪表盘统计数据
function updateDashboardStats(totalAccounts, totalKeywords, enabledAccounts) {
document.getElementById('totalAccounts').textContent = totalAccounts;
@@ -610,8 +648,8 @@ async function addKeyword() {
const reply = document.getElementById('newReply').value.trim();
const itemId = document.getElementById('newItemIdSelect').value.trim();
- if (!keyword || !reply) {
- showToast('请填写关键词和回复内容', 'warning');
+ if (!keyword) {
+ showToast('请填写关键词', 'warning');
return;
}
@@ -1757,10 +1795,9 @@ document.addEventListener('DOMContentLoaded', async () => {
if (value.length > 0) {
e.target.style.borderColor = '#10b981';
- if (replyInput.value.trim().length > 0) {
+ // 只要关键词有内容就可以添加,不需要回复内容
addBtn.style.opacity = '1';
addBtn.style.transform = 'scale(1)';
- }
} else {
e.target.style.borderColor = '#e5e7eb';
addBtn.style.opacity = '0.7';
@@ -1770,17 +1807,21 @@ document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('newReply')?.addEventListener('input', function(e) {
const value = e.target.value.trim();
- const addBtn = document.querySelector('.add-btn');
const keywordInput = document.getElementById('newKeyword');
+ // 回复内容可以为空,只需要关键词有内容即可
if (value.length > 0) {
e.target.style.borderColor = '#10b981';
- if (keywordInput.value.trim().length > 0) {
- addBtn.style.opacity = '1';
- addBtn.style.transform = 'scale(1)';
- }
} else {
e.target.style.borderColor = '#e5e7eb';
+ }
+
+ // 按钮状态只依赖关键词是否有内容
+ const addBtn = document.querySelector('.add-btn');
+ if (keywordInput.value.trim().length > 0) {
+ addBtn.style.opacity = '1';
+ addBtn.style.transform = 'scale(1)';
+ } else {
addBtn.style.opacity = '0.7';
addBtn.style.transform = 'scale(0.95)';
}
@@ -6981,3 +7022,693 @@ async function updateRegistrationSettings() {
showToast('更新注册设置失败', 'danger');
}
}
+
+// ================================
+// 订单管理功能
+// ================================
+
+// 加载订单列表
+async function loadOrders() {
+ try {
+ // 先加载Cookie列表用于筛选
+ await loadOrderCookieFilter();
+
+ // 加载订单列表
+ await refreshOrdersData();
+ } catch (error) {
+ console.error('加载订单列表失败:', error);
+ showToast('加载订单列表失败', 'danger');
+ }
+}
+
+// 只刷新订单数据,不重新加载筛选器
+async function refreshOrdersData() {
+ try {
+ const selectedCookie = document.getElementById('orderCookieFilter').value;
+ if (selectedCookie) {
+ await loadOrdersByCookie();
+ } else {
+ await loadAllOrders();
+ }
+ } catch (error) {
+ console.error('刷新订单数据失败:', error);
+ showToast('刷新订单数据失败', 'danger');
+ }
+}
+
+// 加载Cookie筛选选项
+async function loadOrderCookieFilter() {
+ try {
+ const response = await fetch(`${apiBase}/admin/data/orders`, {
+ headers: {
+ 'Authorization': `Bearer ${authToken}`
+ }
+ });
+
+ const data = await response.json();
+ if (data.success && data.data) {
+ // 提取唯一的cookie_id
+ const cookieIds = [...new Set(data.data.map(order => order.cookie_id).filter(id => id))];
+
+ const select = document.getElementById('orderCookieFilter');
+ if (select) {
+ select.innerHTML = '';
+
+ cookieIds.forEach(cookieId => {
+ const option = document.createElement('option');
+ option.value = cookieId;
+ option.textContent = cookieId;
+ select.appendChild(option);
+ });
+ }
+ }
+ } catch (error) {
+ console.error('加载Cookie选项失败:', error);
+ }
+}
+
+// 加载所有订单
+async function loadAllOrders() {
+ try {
+ const response = await fetch(`${apiBase}/admin/data/orders`, {
+ headers: {
+ 'Authorization': `Bearer ${authToken}`
+ }
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ allOrdersData = data.data || [];
+ // 按创建时间倒序排列
+ allOrdersData.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
+
+ // 应用当前筛选条件
+ filterOrders();
+ } else {
+ console.error('加载订单失败:', data.message);
+ showToast('加载订单数据失败: ' + data.message, 'danger');
+ }
+ } catch (error) {
+ console.error('加载订单失败:', error);
+ showToast('加载订单数据失败,请检查网络连接', 'danger');
+ }
+}
+
+// 根据Cookie加载订单
+async function loadOrdersByCookie() {
+ const selectedCookie = document.getElementById('orderCookieFilter').value;
+ if (!selectedCookie) {
+ await loadAllOrders();
+ return;
+ }
+
+ try {
+ const response = await fetch(`${apiBase}/admin/data/orders`, {
+ headers: {
+ 'Authorization': `Bearer ${authToken}`
+ }
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ // 筛选指定Cookie的订单
+ allOrdersData = (data.data || []).filter(order => order.cookie_id === selectedCookie);
+ // 按创建时间倒序排列
+ allOrdersData.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
+
+ // 应用当前筛选条件
+ filterOrders();
+ } else {
+ console.error('加载订单失败:', data.message);
+ showToast('加载订单数据失败: ' + data.message, 'danger');
+ }
+ } catch (error) {
+ console.error('加载订单失败:', error);
+ showToast('加载订单数据失败,请检查网络连接', 'danger');
+ }
+}
+
+// 筛选订单
+function filterOrders() {
+ const searchKeyword = document.getElementById('orderSearchInput')?.value.toLowerCase() || '';
+ const statusFilter = document.getElementById('orderStatusFilter')?.value || '';
+
+ filteredOrdersData = allOrdersData.filter(order => {
+ // 搜索关键词筛选(订单ID或商品ID)
+ const matchesSearch = !searchKeyword ||
+ (order.order_id && order.order_id.toLowerCase().includes(searchKeyword)) ||
+ (order.item_id && order.item_id.toLowerCase().includes(searchKeyword));
+
+ // 状态筛选
+ const matchesStatus = !statusFilter || order.order_status === statusFilter;
+
+ return matchesSearch && matchesStatus;
+ });
+
+ currentOrderSearchKeyword = searchKeyword;
+ currentOrdersPage = 1; // 重置到第一页
+
+ updateOrdersDisplay();
+}
+
+// 更新订单显示
+function updateOrdersDisplay() {
+ displayOrders();
+ updateOrdersPagination();
+ updateOrdersSearchStats();
+}
+
+// 显示订单列表
+function displayOrders() {
+ const tbody = document.getElementById('ordersTableBody');
+ if (!tbody) return;
+
+ if (filteredOrdersData.length === 0) {
+ tbody.innerHTML = `
+
+
+
+ ${currentOrderSearchKeyword ? '没有找到匹配的订单' : '暂无订单数据'}
+ |
+
+ `;
+ return;
+ }
+
+ // 计算分页
+ totalOrdersPages = Math.ceil(filteredOrdersData.length / ordersPerPage);
+ const startIndex = (currentOrdersPage - 1) * ordersPerPage;
+ const endIndex = startIndex + ordersPerPage;
+ const pageOrders = filteredOrdersData.slice(startIndex, endIndex);
+
+ // 生成表格行
+ tbody.innerHTML = pageOrders.map(order => createOrderRow(order)).join('');
+}
+
+// 创建订单行HTML
+function createOrderRow(order) {
+ const statusClass = getOrderStatusClass(order.order_status);
+ const statusText = getOrderStatusText(order.order_status);
+
+ return `
+
+
+
+ |
+
+
+ ${order.order_id}
+
+ |
+
+
+ ${order.item_id || '-'}
+
+ |
+
+
+ ${order.buyer_id || '-'}
+
+ |
+
+ ${order.spec_name && order.spec_value ?
+ `${order.spec_name}: ${order.spec_value}` :
+ '-'
+ }
+ |
+ ${order.quantity || '-'} |
+
+ ¥${order.amount || '0.00'}
+ |
+
+ ${statusText}
+ |
+
+
+ ${order.cookie_id || '-'}
+
+ |
+
+
+
+
+
+ |
+
+ `;
+}
+
+// 获取订单状态样式类
+function getOrderStatusClass(status) {
+ const statusMap = {
+ 'processing': 'bg-warning text-dark',
+ 'processed': 'bg-info text-white',
+ 'completed': 'bg-success text-white',
+ 'unknown': 'bg-secondary text-white'
+ };
+ return statusMap[status] || 'bg-secondary text-white';
+}
+
+// 获取订单状态文本
+function getOrderStatusText(status) {
+ const statusMap = {
+ 'processing': '处理中',
+ 'processed': '已处理',
+ 'completed': '已完成',
+ 'unknown': '未知'
+ };
+ return statusMap[status] || '未知';
+}
+
+// 更新订单分页
+function updateOrdersPagination() {
+ const pageInfo = document.getElementById('ordersPageInfo');
+ const pageInput = document.getElementById('ordersPageInput');
+ const totalPagesSpan = document.getElementById('ordersTotalPages');
+
+ if (pageInfo) {
+ const startIndex = (currentOrdersPage - 1) * ordersPerPage + 1;
+ const endIndex = Math.min(currentOrdersPage * ordersPerPage, filteredOrdersData.length);
+ pageInfo.textContent = `显示第 ${startIndex}-${endIndex} 条,共 ${filteredOrdersData.length} 条记录`;
+ }
+
+ if (pageInput) {
+ pageInput.value = currentOrdersPage;
+ }
+
+ if (totalPagesSpan) {
+ totalPagesSpan.textContent = totalOrdersPages;
+ }
+
+ // 更新分页按钮状态
+ const firstPageBtn = document.getElementById('ordersFirstPage');
+ const prevPageBtn = document.getElementById('ordersPrevPage');
+ const nextPageBtn = document.getElementById('ordersNextPage');
+ const lastPageBtn = document.getElementById('ordersLastPage');
+
+ if (firstPageBtn) firstPageBtn.disabled = currentOrdersPage === 1;
+ if (prevPageBtn) prevPageBtn.disabled = currentOrdersPage === 1;
+ if (nextPageBtn) nextPageBtn.disabled = currentOrdersPage === totalOrdersPages || totalOrdersPages === 0;
+ if (lastPageBtn) lastPageBtn.disabled = currentOrdersPage === totalOrdersPages || totalOrdersPages === 0;
+}
+
+// 更新搜索统计信息
+function updateOrdersSearchStats() {
+ const searchStats = document.getElementById('orderSearchStats');
+ const searchStatsText = document.getElementById('orderSearchStatsText');
+
+ if (searchStats && searchStatsText) {
+ if (currentOrderSearchKeyword) {
+ searchStatsText.textContent = `搜索 "${currentOrderSearchKeyword}" 找到 ${filteredOrdersData.length} 个结果`;
+ searchStats.style.display = 'block';
+ } else {
+ searchStats.style.display = 'none';
+ }
+ }
+}
+
+// 跳转到指定页面
+function goToOrdersPage(page) {
+ if (page < 1 || page > totalOrdersPages) return;
+
+ currentOrdersPage = page;
+ updateOrdersDisplay();
+}
+
+// 初始化订单搜索功能
+function initOrdersSearch() {
+ // 初始化分页大小
+ const pageSizeSelect = document.getElementById('ordersPageSize');
+ if (pageSizeSelect) {
+ ordersPerPage = parseInt(pageSizeSelect.value) || 20;
+ pageSizeSelect.addEventListener('change', changeOrdersPageSize);
+ }
+
+ // 初始化搜索输入框事件监听器
+ const searchInput = document.getElementById('orderSearchInput');
+ if (searchInput) {
+ // 使用防抖来避免频繁搜索
+ let searchTimeout;
+ searchInput.addEventListener('input', function() {
+ clearTimeout(searchTimeout);
+ searchTimeout = setTimeout(() => {
+ filterOrders();
+ }, 300); // 300ms 防抖延迟
+ });
+ }
+
+ // 初始化页面输入框事件监听器
+ const pageInput = document.getElementById('ordersPageInput');
+ if (pageInput) {
+ pageInput.addEventListener('keydown', handleOrdersPageInput);
+ }
+}
+
+// 处理分页大小变化
+function changeOrdersPageSize() {
+ const pageSizeSelect = document.getElementById('ordersPageSize');
+ if (pageSizeSelect) {
+ ordersPerPage = parseInt(pageSizeSelect.value) || 20;
+ currentOrdersPage = 1; // 重置到第一页
+ updateOrdersDisplay();
+ }
+}
+
+// 处理页面输入
+function handleOrdersPageInput(event) {
+ if (event.key === 'Enter') {
+ const pageInput = document.getElementById('ordersPageInput');
+ if (pageInput) {
+ const page = parseInt(pageInput.value);
+ if (page >= 1 && page <= totalOrdersPages) {
+ goToOrdersPage(page);
+ } else {
+ pageInput.value = currentOrdersPage; // 恢复当前页码
+ showToast('页码超出范围', 'warning');
+ }
+ }
+ }
+}
+
+// 刷新订单列表
+async function refreshOrders() {
+ await refreshOrdersData();
+ showToast('订单列表已刷新', 'success');
+}
+
+// 清空订单筛选条件
+function clearOrderFilters() {
+ const searchInput = document.getElementById('orderSearchInput');
+ const statusFilter = document.getElementById('orderStatusFilter');
+ const cookieFilter = document.getElementById('orderCookieFilter');
+
+ if (searchInput) searchInput.value = '';
+ if (statusFilter) statusFilter.value = '';
+ if (cookieFilter) cookieFilter.value = '';
+
+ filterOrders();
+ showToast('筛选条件已清空', 'info');
+}
+
+// 显示订单详情
+async function showOrderDetail(orderId) {
+ try {
+ const order = allOrdersData.find(o => o.order_id === orderId);
+ if (!order) {
+ showToast('订单不存在', 'warning');
+ return;
+ }
+
+ // 创建模态框内容
+ const modalContent = `
+
+
+
+
+
+
+
+
基本信息
+
+ 订单ID | ${order.order_id} |
+ 商品ID | ${order.item_id || '未知'} |
+ 买家ID | ${order.buyer_id || '未知'} |
+ Cookie账号 | ${order.cookie_id || '未知'} |
+ 订单状态 | ${getOrderStatusText(order.order_status)} |
+
+
+
+
商品信息
+
+ 规格名称 | ${order.spec_name || '无'} |
+ 规格值 | ${order.spec_value || '无'} |
+ 数量 | ${order.quantity || '1'} |
+ 金额 | ¥${order.amount || '0.00'} |
+
+
+
+
+
+
时间信息
+
+ 创建时间 | ${formatDateTime(order.created_at)} |
+ 更新时间 | ${formatDateTime(order.updated_at)} |
+
+
+
+
+
+
商品详情
+
+
+
+ 加载中...
+
+
正在加载商品详情...
+
+
+
+
+
+
+
+
+
+ `;
+
+ // 移除已存在的模态框
+ const existingModal = document.getElementById('orderDetailModal');
+ if (existingModal) {
+ existingModal.remove();
+ }
+
+ // 添加新模态框到页面
+ document.body.insertAdjacentHTML('beforeend', modalContent);
+
+ // 显示模态框
+ const modal = new bootstrap.Modal(document.getElementById('orderDetailModal'));
+ modal.show();
+
+ // 异步加载商品详情
+ if (order.item_id) {
+ loadItemDetailForOrder(order.item_id, order.cookie_id);
+ }
+
+ } catch (error) {
+ console.error('显示订单详情失败:', error);
+ showToast('显示订单详情失败', 'danger');
+ }
+}
+
+// 为订单加载商品详情
+async function loadItemDetailForOrder(itemId, cookieId) {
+ try {
+ const token = localStorage.getItem('auth_token');
+
+ // 尝试从数据库获取商品信息
+ let response = await fetch(`${apiBase}/items/${cookieId}/${itemId}`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ const content = document.getElementById('itemDetailContent');
+ if (!content) return;
+
+ if (response.ok) {
+ const data = await response.json();
+ const item = data.item;
+
+ content.innerHTML = `
+
+
+
${item.item_title || '商品标题未知'}
+
${item.item_description || '暂无描述'}
+
+
+ 分类:${item.item_category || '未知'}
+
+
+ 价格:${item.item_price || '未知'}
+
+
+ ${item.item_detail ? `
+
+
详情:
+
+ ${item.item_detail}
+
+
+ ` : ''}
+
+
+ `;
+ } else {
+ content.innerHTML = `
+
+
+ 无法获取商品详情信息
+
+ `;
+ }
+ } catch (error) {
+ console.error('加载商品详情失败:', error);
+ const content = document.getElementById('itemDetailContent');
+ if (content) {
+ content.innerHTML = `
+
+
+ 加载商品详情失败:${error.message}
+
+ `;
+ }
+ }
+}
+
+// 删除订单
+async function deleteOrder(orderId) {
+ try {
+ const confirmed = confirm(`确定要删除订单吗?\n\n订单ID: ${orderId}\n\n此操作不可撤销!`);
+ if (!confirmed) {
+ return;
+ }
+
+ const response = await fetch(`${apiBase}/admin/data/orders/delete`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`
+ },
+ body: JSON.stringify({ record_id: orderId })
+ });
+
+ if (response.ok) {
+ showToast('订单删除成功', 'success');
+ // 刷新列表
+ await refreshOrdersData();
+ } else {
+ const error = await response.text();
+ showToast(`删除失败: ${error}`, 'danger');
+ }
+ } catch (error) {
+ console.error('删除订单失败:', error);
+ showToast('删除订单失败', 'danger');
+ }
+}
+
+// 批量删除订单
+async function batchDeleteOrders() {
+ const checkboxes = document.querySelectorAll('.order-checkbox:checked');
+ if (checkboxes.length === 0) {
+ showToast('请先选择要删除的订单', 'warning');
+ return;
+ }
+
+ const orderIds = Array.from(checkboxes).map(cb => cb.value);
+ const confirmed = confirm(`确定要删除选中的 ${orderIds.length} 个订单吗?\n\n此操作不可撤销!`);
+
+ if (!confirmed) return;
+
+ try {
+ let successCount = 0;
+ let failCount = 0;
+
+ for (const orderId of orderIds) {
+ try {
+ const response = await fetch(`${apiBase}/admin/data/orders/delete`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`
+ },
+ body: JSON.stringify({ record_id: orderId })
+ });
+
+ if (response.ok) {
+ successCount++;
+ } else {
+ failCount++;
+ }
+ } catch (error) {
+ failCount++;
+ }
+ }
+
+ if (successCount > 0) {
+ showToast(`成功删除 ${successCount} 个订单${failCount > 0 ? `,${failCount} 个失败` : ''}`,
+ failCount > 0 ? 'warning' : 'success');
+ await refreshOrdersData();
+ } else {
+ showToast('批量删除失败', 'danger');
+ }
+
+ } catch (error) {
+ console.error('批量删除订单失败:', error);
+ showToast('批量删除订单失败', 'danger');
+ }
+}
+
+// 切换全选订单
+function toggleSelectAllOrders(checkbox) {
+ const orderCheckboxes = document.querySelectorAll('.order-checkbox');
+ orderCheckboxes.forEach(cb => {
+ cb.checked = checkbox.checked;
+ });
+
+ updateBatchDeleteOrdersButton();
+}
+
+// 更新批量删除按钮状态
+function updateBatchDeleteOrdersButton() {
+ const checkboxes = document.querySelectorAll('.order-checkbox:checked');
+ const batchDeleteBtn = document.getElementById('batchDeleteOrdersBtn');
+
+ if (batchDeleteBtn) {
+ batchDeleteBtn.disabled = checkboxes.length === 0;
+ }
+}
+
+// 格式化日期时间
+function formatDateTime(dateString) {
+ if (!dateString) return '未知时间';
+
+ try {
+ const date = new Date(dateString);
+ return date.toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ } catch (error) {
+ return dateString;
+ }
+}
+
+// 页面加载完成后初始化订单搜索功能
+document.addEventListener('DOMContentLoaded', function() {
+ // 延迟初始化,确保DOM完全加载
+ setTimeout(() => {
+ initOrdersSearch();
+
+ // 绑定复选框变化事件
+ document.addEventListener('change', function(e) {
+ if (e.target.classList.contains('order-checkbox')) {
+ updateBatchDeleteOrdersButton();
+ }
+ });
+ }, 100);
+});
diff --git a/utils/order_detail_fetcher.py b/utils/order_detail_fetcher.py
index 52311fa..b7f0164 100644
--- a/utils/order_detail_fetcher.py
+++ b/utils/order_detail_fetcher.py
@@ -10,6 +10,10 @@ import os
from typing import Optional, Dict, Any
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
from loguru import logger
+import re
+import json
+from threading import Lock
+from collections import defaultdict
# 修复Docker环境中的asyncio事件循环策略问题
if sys.platform.startswith('linux') or os.getenv('DOCKER_ENV'):
@@ -33,10 +37,14 @@ if os.getenv('DOCKER_ENV'):
class OrderDetailFetcher:
"""闲鱼订单详情获取器"""
- def __init__(self, cookie_string: str = None):
+ # 类级别的锁字典,为每个order_id维护一个锁
+ _order_locks = defaultdict(lambda: asyncio.Lock())
+
+ def __init__(self, cookie_string: str = None, headless: bool = True):
self.browser: Optional[Browser] = None
self.context: Optional[BrowserContext] = None
self.page: Optional[Page] = None
+ self.headless = headless # 保存headless设置
# 请求头配置
self.headers = {
@@ -58,11 +66,17 @@ class OrderDetailFetcher:
# Cookie配置 - 支持动态传入
self.cookie = cookie_string
- async def init_browser(self, headless: bool = True):
+ async def init_browser(self, headless: bool = None):
"""初始化浏览器"""
try:
+ # 如果没有传入headless参数,使用实例的设置
+ if headless is None:
+ headless = self.headless
+
+ logger.info(f"开始初始化浏览器,headless模式: {headless}")
+
playwright = await async_playwright().start()
-
+
# 启动浏览器(Docker环境优化)
browser_args = [
'--no-sandbox',
@@ -84,10 +98,13 @@ class OrderDetailFetcher:
'--hide-scrollbars',
'--mute-audio',
'--no-default-browser-check',
- '--no-pings',
- '--single-process' # 在Docker中使用单进程模式
+ '--no-pings'
]
+ # 只在Docker环境中使用单进程模式
+ if os.getenv('DOCKER_ENV'):
+ browser_args.append('--single-process')
+
# 在Docker环境中添加额外参数
if os.getenv('DOCKER_ENV'):
browser_args.extend([
@@ -108,26 +125,38 @@ class OrderDetailFetcher:
'--use-mock-keychain'
])
+ logger.info(f"启动浏览器,参数: {browser_args}")
self.browser = await playwright.chromium.launch(
headless=headless,
args=browser_args
)
-
+
+ logger.info("浏览器启动成功,创建上下文...")
+
# 创建浏览器上下文
self.context = await self.browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'
)
-
+
+ logger.info("浏览器上下文创建成功,设置HTTP头...")
+
# 设置额外的HTTP头
await self.context.set_extra_http_headers(self.headers)
-
+
+ logger.info("创建页面...")
+
# 创建页面
self.page = await self.context.new_page()
-
+
+ logger.info("页面创建成功,设置Cookie...")
+
# 设置Cookie
await self._set_cookies()
-
+
+ # 等待一段时间确保浏览器完全初始化
+ await asyncio.sleep(1)
+
logger.info("浏览器初始化成功")
return True
@@ -159,63 +188,160 @@ class OrderDetailFetcher:
async def fetch_order_detail(self, order_id: str, timeout: int = 30) -> Optional[Dict[str, Any]]:
"""
- 获取订单详情
-
+ 获取订单详情(带锁机制和数据库缓存)
+
Args:
order_id: 订单ID
timeout: 超时时间(秒)
-
+
Returns:
包含订单详情的字典,失败时返回None
"""
- try:
- if not self.page:
- logger.error("浏览器未初始化")
+ # 获取该订单ID的锁
+ order_lock = self._order_locks[order_id]
+
+ async with order_lock:
+ logger.info(f"🔒 获取订单 {order_id} 的锁,开始处理...")
+
+ try:
+ # 首先查询数据库中是否已存在该订单(在初始化浏览器之前)
+ from db_manager import db_manager
+ existing_order = db_manager.get_order_by_id(order_id)
+
+ if existing_order:
+ # 检查金额字段是否有效(不为空且不为0)
+ amount = existing_order.get('amount', '')
+ amount_valid = False
+
+ if amount:
+ # 移除可能的货币符号和空格,检查是否为有效数字
+ amount_clean = str(amount).replace('¥', '').replace('¥', '').replace('$', '').strip()
+ try:
+ amount_value = float(amount_clean)
+ amount_valid = amount_value > 0
+ except (ValueError, TypeError):
+ amount_valid = False
+
+ if amount_valid:
+ logger.info(f"📋 订单 {order_id} 已存在于数据库中且金额有效({amount}),直接返回缓存数据")
+ print(f"✅ 订单 {order_id} 使用缓存数据,跳过浏览器获取")
+
+ # 构建返回格式,与浏览器获取的格式保持一致
+ result = {
+ 'order_id': existing_order['order_id'],
+ 'url': f"https://www.goofish.com/order-detail?orderId={order_id}&role=seller",
+ 'title': f"订单详情 - {order_id}",
+ 'sku_info': {
+ 'spec_name': existing_order.get('spec_name', ''),
+ 'spec_value': existing_order.get('spec_value', ''),
+ 'quantity': existing_order.get('quantity', ''),
+ 'amount': existing_order.get('amount', '')
+ },
+ 'spec_name': existing_order.get('spec_name', ''),
+ 'spec_value': existing_order.get('spec_value', ''),
+ 'quantity': existing_order.get('quantity', ''),
+ 'amount': existing_order.get('amount', ''),
+ 'timestamp': time.time(),
+ 'from_cache': True # 标记数据来源
+ }
+ return result
+ else:
+ logger.info(f"📋 订单 {order_id} 存在于数据库中但金额无效({amount}),需要重新获取")
+ print(f"⚠️ 订单 {order_id} 金额无效,重新获取详情...")
+
+ # 只有在数据库中没有有效数据时才初始化浏览器
+ logger.info(f"🌐 订单 {order_id} 需要浏览器获取,开始初始化浏览器...")
+ print(f"🔍 订单 {order_id} 开始浏览器获取详情...")
+
+ # 确保浏览器准备就绪
+ if not await self._ensure_browser_ready():
+ logger.error("浏览器初始化失败,无法获取订单详情")
+ return None
+
+ # 构建订单详情URL
+ url = f"https://www.goofish.com/order-detail?orderId={order_id}&role=seller"
+ logger.info(f"开始访问订单详情页面: {url}")
+
+ # 访问页面(带重试机制)
+ max_retries = 2
+ response = None
+
+ for retry in range(max_retries + 1):
+ try:
+ response = await self.page.goto(url, wait_until='networkidle', timeout=timeout * 1000)
+
+ if response and response.status == 200:
+ break
+ else:
+ logger.warning(f"页面访问失败,状态码: {response.status if response else 'None'},重试 {retry + 1}/{max_retries + 1}")
+
+ except Exception as e:
+ logger.warning(f"页面访问异常: {e},重试 {retry + 1}/{max_retries + 1}")
+
+ # 如果是浏览器连接问题,尝试重新初始化
+ if "Target page, context or browser has been closed" in str(e):
+ logger.info("检测到浏览器连接断开,尝试重新初始化...")
+ if await self._ensure_browser_ready():
+ logger.info("浏览器重新初始化成功,继续重试...")
+ continue
+ else:
+ logger.error("浏览器重新初始化失败")
+ return None
+
+ if retry == max_retries:
+ logger.error(f"页面访问最终失败: {e}")
+ return None
+
+ await asyncio.sleep(1) # 重试前等待1秒
+
+ if not response or response.status != 200:
+ logger.error(f"页面访问最终失败,状态码: {response.status if response else 'None'}")
+ return None
+
+ logger.info("页面加载成功,等待内容渲染...")
+
+ # 等待页面完全加载
+ try:
+ await self.page.wait_for_load_state('networkidle')
+ except Exception as e:
+ logger.warning(f"等待页面加载状态失败: {e}")
+ # 继续执行,不中断流程
+
+ # 额外等待确保动态内容加载完成
+ await asyncio.sleep(3)
+
+ # 获取并解析SKU信息
+ sku_info = await self._get_sku_content()
+
+ # 获取页面标题
+ try:
+ title = await self.page.title()
+ except Exception as e:
+ logger.warning(f"获取页面标题失败: {e}")
+ title = f"订单详情 - {order_id}"
+
+ result = {
+ 'order_id': order_id,
+ 'url': url,
+ 'title': title,
+ 'sku_info': sku_info, # 包含解析后的规格信息
+ 'spec_name': sku_info.get('spec_name', '') if sku_info else '',
+ 'spec_value': sku_info.get('spec_value', '') if sku_info else '',
+ 'quantity': sku_info.get('quantity', '') if sku_info else '', # 数量
+ 'amount': sku_info.get('amount', '') if sku_info else '', # 金额
+ 'timestamp': time.time(),
+ 'from_cache': False # 标记数据来源
+ }
+
+ logger.info(f"订单详情获取成功: {order_id}")
+ if sku_info:
+ logger.info(f"规格信息 - 名称: {result['spec_name']}, 值: {result['spec_value']}")
+ logger.info(f"数量: {result['quantity']}, 金额: {result['amount']}")
+ return result
+
+ except Exception as e:
+ logger.error(f"获取订单详情失败: {e}")
return None
-
- # 构建订单详情URL
- url = f"https://www.goofish.com/order-detail?orderId={order_id}&role=seller"
- logger.info(f"开始访问订单详情页面: {url}")
-
- # 访问页面
- response = await self.page.goto(url, wait_until='networkidle', timeout=timeout * 1000)
-
- if not response or response.status != 200:
- logger.error(f"页面访问失败,状态码: {response.status if response else 'None'}")
- return None
-
- logger.info("页面加载成功,等待内容渲染...")
-
- # 等待页面完全加载
- await self.page.wait_for_load_state('networkidle')
-
- # 额外等待确保动态内容加载完成
- await asyncio.sleep(3)
-
- # 获取并解析SKU信息
- sku_info = await self._get_sku_content()
-
- # 获取页面标题
- title = await self.page.title()
-
- result = {
- 'order_id': order_id,
- 'url': url,
- 'title': title,
- 'sku_info': sku_info, # 包含解析后的规格信息
- 'spec_name': sku_info.get('spec_name', '') if sku_info else '',
- 'spec_value': sku_info.get('spec_value', '') if sku_info else '',
- 'timestamp': time.time()
- }
-
- logger.info(f"订单详情获取成功: {order_id}")
- if sku_info:
- logger.info(f"规格信息 - 名称: {result['spec_name']}, 值: {result['spec_value']}")
- return result
-
- except Exception as e:
- logger.error(f"获取订单详情失败: {e}")
- return None
def _parse_sku_content(self, sku_content: str) -> Dict[str, str]:
"""
@@ -258,38 +384,121 @@ class OrderDetailFetcher:
return {}
async def _get_sku_content(self) -> Optional[Dict[str, str]]:
- """获取并解析SKU内容"""
+ """获取并解析SKU内容,包括规格、数量和金额"""
try:
- # 等待SKU元素出现
+ # 检查浏览器状态
+ if not await self._check_browser_status():
+ logger.error("浏览器状态异常,无法获取SKU内容")
+ return {}
+
+ result = {}
+
+ # 获取所有 sku--u_ddZval 元素
sku_selector = '.sku--u_ddZval'
+ sku_elements = await self.page.query_selector_all(sku_selector)
- # 检查元素是否存在
- sku_element = await self.page.query_selector(sku_selector)
+ logger.info(f"找到 {len(sku_elements)} 个 sku--u_ddZval 元素")
+ print(f"🔍 找到 {len(sku_elements)} 个 sku--u_ddZval 元素")
- if sku_element:
- # 获取元素文本内容
- sku_content = await sku_element.text_content()
- if sku_content:
- sku_content = sku_content.strip()
- logger.info(f"找到SKU原始内容: {sku_content}")
- print(f"🛍️ SKU原始内容: {sku_content}")
-
- # 解析SKU内容
- parsed_sku = self._parse_sku_content(sku_content)
- if parsed_sku:
- print(f"📋 规格名称: {parsed_sku['spec_name']}")
- print(f"📝 规格值: {parsed_sku['spec_value']}")
- return parsed_sku
- else:
- logger.warning("SKU内容解析失败")
- return {}
- else:
- logger.warning("SKU元素内容为空")
- return {}
+ # 获取金额信息
+ amount_selector = '.boldNum--JgEOXfA3'
+ amount_element = await self.page.query_selector(amount_selector)
+ amount = ''
+ if amount_element:
+ amount_text = await amount_element.text_content()
+ if amount_text:
+ amount = amount_text.strip()
+ logger.info(f"找到金额: {amount}")
+ print(f"💰 金额: {amount}")
+ result['amount'] = amount
else:
- logger.warning("未找到SKU元素")
+ logger.warning("未找到金额元素")
+ print("⚠️ 未找到金额信息")
- # 尝试获取页面的所有class包含sku的元素
+ # 处理 sku--u_ddZval 元素
+ if len(sku_elements) == 2:
+ # 有两个元素:第一个是规格,第二个是数量
+ logger.info("检测到两个 sku--u_ddZval 元素,第一个为规格,第二个为数量")
+ print("📋 检测到两个元素:第一个为规格,第二个为数量")
+
+ # 处理规格(第一个元素)
+ spec_content = await sku_elements[0].text_content()
+ if spec_content:
+ spec_content = spec_content.strip()
+ logger.info(f"规格原始内容: {spec_content}")
+ print(f"🛍️ 规格原始内容: {spec_content}")
+
+ # 解析规格内容
+ parsed_spec = self._parse_sku_content(spec_content)
+ if parsed_spec:
+ result.update(parsed_spec)
+ print(f"📋 规格名称: {parsed_spec['spec_name']}")
+ print(f"📝 规格值: {parsed_spec['spec_value']}")
+
+ # 处理数量(第二个元素)
+ quantity_content = await sku_elements[1].text_content()
+ if quantity_content:
+ quantity_content = quantity_content.strip()
+ logger.info(f"数量原始内容: {quantity_content}")
+ print(f"📦 数量原始内容: {quantity_content}")
+
+ # 从数量内容中提取数量值(使用冒号分割,取后面的值)
+ if ':' in quantity_content:
+ quantity_value = quantity_content.split(':', 1)[1].strip()
+ result['quantity'] = quantity_value
+ logger.info(f"提取到数量: {quantity_value}")
+ print(f"🔢 数量: {quantity_value}")
+ else:
+ result['quantity'] = quantity_content
+ logger.info(f"数量内容无冒号,直接使用: {quantity_content}")
+ print(f"🔢 数量: {quantity_content}")
+
+ elif len(sku_elements) == 1:
+ # 只有一个元素:判断是否包含"数量"
+ logger.info("检测到一个 sku--u_ddZval 元素,判断是规格还是数量")
+ print("📋 检测到一个元素,判断是规格还是数量")
+
+ content = await sku_elements[0].text_content()
+ if content:
+ content = content.strip()
+ logger.info(f"元素原始内容: {content}")
+ print(f"🛍️ 元素原始内容: {content}")
+
+ if '数量' in content:
+ # 这是数量信息
+ logger.info("判断为数量信息")
+ print("📦 判断为数量信息")
+
+ if ':' in content:
+ quantity_value = content.split(':', 1)[1].strip()
+ result['quantity'] = quantity_value
+ logger.info(f"提取到数量: {quantity_value}")
+ print(f"🔢 数量: {quantity_value}")
+ else:
+ result['quantity'] = content
+ logger.info(f"数量内容无冒号,直接使用: {content}")
+ print(f"🔢 数量: {content}")
+ else:
+ # 这是规格信息
+ logger.info("判断为规格信息")
+ print("📋 判断为规格信息")
+
+ parsed_spec = self._parse_sku_content(content)
+ if parsed_spec:
+ result.update(parsed_spec)
+ print(f"📋 规格名称: {parsed_spec['spec_name']}")
+ print(f"📝 规格值: {parsed_spec['spec_value']}")
+ else:
+ logger.warning(f"未找到或找到异常数量的 sku--u_ddZval 元素: {len(sku_elements)}")
+ print(f"⚠️ 未找到或找到异常数量的元素: {len(sku_elements)}")
+
+ # 如果没有找到sku--u_ddZval元素,设置默认数量为0
+ if len(sku_elements) == 0:
+ result['quantity'] = '0'
+ logger.info("未找到sku--u_ddZval元素,数量默认设置为0")
+ print("📦 数量默认设置为: 0")
+
+ # 尝试获取页面的所有class包含sku的元素进行调试
all_sku_elements = await self.page.query_selector_all('[class*="sku"]')
if all_sku_elements:
logger.info(f"找到 {len(all_sku_elements)} 个包含'sku'的元素")
@@ -298,12 +507,104 @@ class OrderDetailFetcher:
text_content = await element.text_content()
logger.info(f"SKU元素 {i+1}: class='{class_name}', text='{text_content}'")
- return {}
+ # 确保数量字段存在,如果不存在则设置为0
+ if 'quantity' not in result:
+ result['quantity'] = '0'
+ logger.info("未获取到数量信息,默认设置为0")
+ print("📦 数量默认设置为: 0")
+
+ # 打印最终结果
+ if result:
+ logger.info(f"最终解析结果: {result}")
+ print("✅ 解析结果:")
+ for key, value in result.items():
+ print(f" {key}: {value}")
+ return result
+ else:
+ logger.warning("未能解析到任何有效信息")
+ print("❌ 未能解析到任何有效信息")
+ # 即使没有其他信息,也要返回默认数量
+ return {'quantity': '0'}
except Exception as e:
logger.error(f"获取SKU内容失败: {e}")
return {}
+ async def _check_browser_status(self) -> bool:
+ """检查浏览器状态是否正常"""
+ try:
+ if not self.browser or not self.context or not self.page:
+ logger.warning("浏览器组件不完整")
+ return False
+
+ # 检查浏览器是否已连接
+ if self.browser.is_connected():
+ # 尝试获取页面标题来验证页面是否可用
+ await self.page.title()
+ return True
+ else:
+ logger.warning("浏览器连接已断开")
+ return False
+ except Exception as e:
+ logger.warning(f"浏览器状态检查失败: {e}")
+ return False
+
+ async def _ensure_browser_ready(self) -> bool:
+ """确保浏览器准备就绪,如果不可用则重新初始化"""
+ try:
+ if await self._check_browser_status():
+ return True
+
+ logger.info("浏览器状态异常,尝试重新初始化...")
+
+ # 先尝试关闭现有的浏览器实例
+ await self._force_close_browser()
+
+ # 重新初始化浏览器
+ await self.init_browser()
+
+ # 等待更长时间确保浏览器完全就绪
+ await asyncio.sleep(2)
+
+ # 再次检查状态
+ if await self._check_browser_status():
+ logger.info("浏览器重新初始化成功")
+ return True
+ else:
+ logger.error("浏览器重新初始化失败")
+ return False
+
+ except Exception as e:
+ logger.error(f"确保浏览器就绪失败: {e}")
+ return False
+
+ async def _force_close_browser(self):
+ """强制关闭浏览器,忽略所有错误"""
+ try:
+ if self.page:
+ try:
+ await self.page.close()
+ except:
+ pass
+ self.page = None
+
+ if self.context:
+ try:
+ await self.context.close()
+ except:
+ pass
+ self.context = None
+
+ if self.browser:
+ try:
+ await self.browser.close()
+ except:
+ pass
+ self.browser = None
+
+ except Exception as e:
+ logger.debug(f"强制关闭浏览器过程中的异常(可忽略): {e}")
+
async def close(self):
"""关闭浏览器"""
try:
@@ -316,6 +617,8 @@ class OrderDetailFetcher:
logger.info("浏览器已关闭")
except Exception as e:
logger.error(f"关闭浏览器失败: {e}")
+ # 如果正常关闭失败,尝试强制关闭
+ await self._force_close_browser()
async def __aenter__(self):
"""异步上下文管理器入口"""
@@ -330,7 +633,7 @@ class OrderDetailFetcher:
# 便捷函数
async def fetch_order_detail_simple(order_id: str, cookie_string: str = None, headless: bool = True) -> Optional[Dict[str, Any]]:
"""
- 简单的订单详情获取函数
+ 简单的订单详情获取函数(优化版:先检查数据库,再初始化浏览器)
Args:
order_id: 订单ID
@@ -338,9 +641,71 @@ async def fetch_order_detail_simple(order_id: str, cookie_string: str = None, he
headless: 是否无头模式
Returns:
- 订单详情字典或None
+ 订单详情字典,包含以下字段:
+ - order_id: 订单ID
+ - url: 订单详情页面URL
+ - title: 页面标题
+ - sku_info: 完整的SKU信息字典
+ - spec_name: 规格名称
+ - spec_value: 规格值
+ - quantity: 数量
+ - amount: 金额
+ - timestamp: 获取时间戳
+ 失败时返回None
"""
- fetcher = OrderDetailFetcher(cookie_string)
+ # 先检查数据库中是否有有效数据
+ try:
+ from db_manager import db_manager
+ existing_order = db_manager.get_order_by_id(order_id)
+
+ if existing_order:
+ # 检查金额字段是否有效
+ amount = existing_order.get('amount', '')
+ amount_valid = False
+
+ if amount:
+ amount_clean = str(amount).replace('¥', '').replace('¥', '').replace('$', '').strip()
+ try:
+ amount_value = float(amount_clean)
+ amount_valid = amount_value > 0
+ except (ValueError, TypeError):
+ amount_valid = False
+
+ if amount_valid:
+ logger.info(f"📋 订单 {order_id} 已存在于数据库中且金额有效({amount}),直接返回缓存数据")
+ print(f"✅ 订单 {order_id} 使用缓存数据,跳过浏览器获取")
+
+ # 构建返回格式
+ result = {
+ 'order_id': existing_order['order_id'],
+ 'url': f"https://www.goofish.com/order-detail?orderId={order_id}&role=seller",
+ 'title': f"订单详情 - {order_id}",
+ 'sku_info': {
+ 'spec_name': existing_order.get('spec_name', ''),
+ 'spec_value': existing_order.get('spec_value', ''),
+ 'quantity': existing_order.get('quantity', ''),
+ 'amount': existing_order.get('amount', '')
+ },
+ 'spec_name': existing_order.get('spec_name', ''),
+ 'spec_value': existing_order.get('spec_value', ''),
+ 'quantity': existing_order.get('quantity', ''),
+ 'amount': existing_order.get('amount', ''),
+ 'order_status': existing_order.get('order_status', 'unknown'), # 添加订单状态
+ 'timestamp': time.time(),
+ 'from_cache': True
+ }
+ return result
+ else:
+ logger.info(f"📋 订单 {order_id} 存在于数据库中但金额无效({amount}),需要重新获取")
+ print(f"⚠️ 订单 {order_id} 金额无效,重新获取详情...")
+ except Exception as e:
+ logger.warning(f"检查数据库缓存失败: {e}")
+
+ # 数据库中没有有效数据,使用浏览器获取
+ logger.info(f"🌐 订单 {order_id} 需要浏览器获取,开始初始化浏览器...")
+ print(f"🔍 订单 {order_id} 开始浏览器获取详情...")
+
+ fetcher = OrderDetailFetcher(cookie_string, headless)
try:
if await fetcher.init_browser(headless=headless):
return await fetcher.fetch_order_detail(order_id)
@@ -364,7 +729,10 @@ if __name__ == "__main__":
print(f"📋 订单ID: {result['order_id']}")
print(f"🌐 URL: {result['url']}")
print(f"📄 页面标题: {result['title']}")
- print(f"🛍️ SKU内容: {result['sku_content']}")
+ print(f"🛍️ 规格名称: {result.get('spec_name', '未获取到')}")
+ print(f"📝 规格值: {result.get('spec_value', '未获取到')}")
+ print(f"🔢 数量: {result.get('quantity', '未获取到')}")
+ print(f"💰 金额: {result.get('amount', '未获取到')}")
else:
print("❌ 订单详情获取失败")