新增订单管理等;优化程序支持系统消息回复

This commit is contained in:
zhinianboke 2025-08-07 17:40:35 +08:00
parent 383e841acc
commit 46f7066519
10 changed files with 2193 additions and 215 deletions

View File

@ -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依赖包列表精简版本无冗余依赖,按功能分类组织
## ⚙️ 配置说明

View File

@ -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)

View File

@ -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')

View File

@ -477,6 +477,9 @@ async def data_management_page():
return HTMLResponse('<h3>Data management page not found</h3>')
# 商品搜索页面路由
@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'
]
# 不允许清空用户表

345
secure_confirm_decrypted.py Normal file
View File

@ -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}

View File

@ -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;

View File

@ -111,6 +111,7 @@
<option value="system_settings">system_settings - 系统设置表</option>
<option value="email_verifications">email_verifications - 邮箱验证表</option>
<option value="captcha_codes">captcha_codes - 验证码表</option>
<option value="orders">orders - 订单表</option>
</select>
</div>
<div class="col-md-3">
@ -275,7 +276,8 @@
'user_settings': '用户设置表',
'system_settings': '系统设置表',
'email_verifications': '邮箱验证表',
'captcha_codes': '验证码表'
'captcha_codes': '验证码表',
'orders': '订单表'
};
// 页面加载完成后初始化

View File

@ -42,6 +42,12 @@
商品管理
</a>
</div>
<div class="nav-item">
<a href="#" class="nav-link" onclick="showSection('orders')">
<i class="bi bi-receipt-cutoff"></i>
订单管理
</a>
</div>
<div class="nav-item">
<a href="#" class="nav-link" onclick="showSection('auto-reply')">
<i class="bi bi-chat-left-text"></i>
@ -164,6 +170,13 @@
<div class="stat-number" id="activeAccounts">0</div>
<div class="stat-label">启用账号数</div>
</div>
<div class="stat-card">
<div class="stat-icon info">
<i class="bi bi-receipt-cutoff"></i>
</div>
<div class="stat-number" id="totalOrders">0</div>
<div class="stat-label">总订单数</div>
</div>
</div>
<!-- 账号详情列表 -->
@ -443,6 +456,137 @@
</div>
</div>
<!-- 订单管理内容 -->
<div id="orders-section" class="content-section">
<div class="content-header">
<h2 class="mb-0">
<i class="bi bi-receipt-cutoff me-2"></i>
订单管理
</h2>
<p class="text-muted mb-0">查看和管理所有订单信息</p>
</div>
<div class="content-body">
<!-- Cookie筛选器 -->
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label">筛选账号</label>
<select class="form-select" id="orderCookieFilter" onchange="loadOrdersByCookie()">
<option value="">所有账号</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">订单状态</label>
<select class="form-select" id="orderStatusFilter" onchange="filterOrders()">
<option value="">所有状态</option>
<option value="processing">处理中</option>
<option value="processed">已处理</option>
<option value="completed">已完成</option>
<option value="unknown">未知</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary" onclick="refreshOrders()">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
<button class="btn btn-outline-secondary" onclick="clearOrderFilters()">
<i class="bi bi-x-circle"></i> 清空筛选
</button>
</div>
</div>
</div>
<!-- 订单列表 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-3">
<input type="text" class="form-control" id="orderSearchInput"
placeholder="搜索订单ID或商品ID..." style="width: 300px;">
<h5 class="mb-0">订单列表</h5>
</div>
<button class="btn btn-sm btn-outline-danger" onclick="batchDeleteOrders()" id="batchDeleteOrdersBtn" disabled>
<i class="bi bi-trash"></i> 批量删除
</button>
</div>
<div class="card-body">
<!-- 搜索统计信息 -->
<div id="orderSearchStats" class="text-muted small mb-2" style="display: none;">
<i class="bi bi-search me-1"></i>
<span id="orderSearchStatsText"></span>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 5%">
<input type="checkbox" id="selectAllOrders" onchange="toggleSelectAllOrders(this)">
</th>
<th style="width: 15%">订单ID</th>
<th style="width: 12%">商品ID</th>
<th style="width: 10%">买家ID</th>
<th style="width: 12%">规格信息</th>
<th style="width: 8%">数量</th>
<th style="width: 10%">金额</th>
<th style="width: 8%">状态</th>
<th style="width: 10%">账号ID</th>
<th style="width: 10%">操作</th>
</tr>
</thead>
<tbody id="ordersTableBody">
<tr>
<td colspan="10" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分页控件 -->
<div class="card-footer" id="ordersPagination">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted small">
<span id="ordersPageInfo">显示第 1-10 条,共 0 条记录</span>
</div>
<div class="d-flex align-items-center gap-2">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" id="ordersFirstPage" onclick="goToOrdersPage(1)" disabled>
<i class="bi bi-chevron-double-left"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="ordersPrevPage" onclick="goToOrdersPage(currentOrdersPage - 1)" disabled>
<i class="bi bi-chevron-left"></i>
</button>
</div>
<div class="d-flex align-items-center gap-2 mx-2">
<span class="text-muted small"></span>
<input type="number" class="form-control form-control-sm" id="ordersPageInput"
style="width: 60px;" min="1" value="1">
<span class="text-muted small">页,共 <span id="ordersTotalPages">0</span></span>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" id="ordersNextPage" onclick="goToOrdersPage(currentOrdersPage + 1)" disabled>
<i class="bi bi-chevron-right"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="ordersLastPage" onclick="goToOrdersPage(totalOrdersPages)" disabled>
<i class="bi bi-chevron-double-right"></i>
</button>
</div>
<select class="form-select form-select-sm ms-2" id="ordersPageSize" style="width: auto;">
<option value="10">10条/页</option>
<option value="20" selected>20条/页</option>
<option value="50">50条/页</option>
<option value="100">100条/页</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 自动回复内容 -->
<div id="auto-reply-section" class="content-section">
<div class="content-header">
@ -511,8 +655,8 @@
<input type="text" id="newKeyword" placeholder="例如:你好">
</div>
<div class="input-field">
<label>自动回复内容</label>
<input type="text" id="newReply" placeholder="例如:您好,欢迎咨询!有什么可以帮助您的吗?">
<label>自动回复内容(可选)</label>
<input type="text" id="newReply" placeholder="例如:您好,欢迎咨询!留空表示不回复">
</div>
<div class="input-field">
<label>商品ID可选</label>
@ -535,7 +679,9 @@
<strong>支持变量:</strong>
<code>{send_user_name}</code> 用户昵称,
<code>{send_user_id}</code> 用户ID
<code>{send_message}</code> 用户消息
<code>{send_message}</code> 用户消息<br>
<i class="bi bi-info-circle me-1"></i>
<strong>提示:</strong>回复内容留空时,匹配到关键词但不会自动回复,可用于屏蔽特定消息
</small>
</div>
</div>

View File

@ -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 = '<option value="">所有账号</option>';
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 = `
<tr>
<td colspan="10" class="text-center text-muted py-4">
<i class="bi bi-inbox display-6 d-block mb-2"></i>
${currentOrderSearchKeyword ? '没有找到匹配的订单' : '暂无订单数据'}
</td>
</tr>
`;
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 `
<tr>
<td>
<input type="checkbox" class="order-checkbox" value="${order.order_id}">
</td>
<td>
<span class="text-truncate d-inline-block" style="max-width: 120px;" title="${order.order_id}">
${order.order_id}
</span>
</td>
<td>
<span class="text-truncate d-inline-block" style="max-width: 100px;" title="${order.item_id || ''}">
${order.item_id || '-'}
</span>
</td>
<td>
<span class="text-truncate d-inline-block" style="max-width: 80px;" title="${order.buyer_id || ''}">
${order.buyer_id || '-'}
</span>
</td>
<td>
${order.spec_name && order.spec_value ?
`<small class="text-muted">${order.spec_name}:</small><br>${order.spec_value}` :
'-'
}
</td>
<td>${order.quantity || '-'}</td>
<td>
<span class="text-success fw-bold">¥${order.amount || '0.00'}</span>
</td>
<td>
<span class="badge ${statusClass}">${statusText}</span>
</td>
<td>
<span class="text-truncate d-inline-block" style="max-width: 80px;" title="${order.cookie_id || ''}">
${order.cookie_id || '-'}
</span>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary btn-sm" onclick="showOrderDetail('${order.order_id}')" title="查看详情">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteOrder('${order.order_id}')" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
`;
}
// 获取订单状态样式类
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 = `
<div class="modal fade" id="orderDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-receipt-cutoff me-2"></i>
订单详情
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6>基本信息</h6>
<table class="table table-sm">
<tr><td>订单ID</td><td>${order.order_id}</td></tr>
<tr><td>商品ID</td><td>${order.item_id || ''}</td></tr>
<tr><td>买家ID</td><td>${order.buyer_id || ''}</td></tr>
<tr><td>Cookie账号</td><td>${order.cookie_id || ''}</td></tr>
<tr><td>订单状态</td><td><span class="badge ${getOrderStatusClass(order.order_status)}">${getOrderStatusText(order.order_status)}</span></td></tr>
</table>
</div>
<div class="col-md-6">
<h6>商品信息</h6>
<table class="table table-sm">
<tr><td>规格名称</td><td>${order.spec_name || ''}</td></tr>
<tr><td>规格值</td><td>${order.spec_value || ''}</td></tr>
<tr><td>数量</td><td>${order.quantity || '1'}</td></tr>
<tr><td>金额</td><td>¥${order.amount || '0.00'}</td></tr>
</table>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h6>时间信息</h6>
<table class="table table-sm">
<tr><td>创建时间</td><td>${formatDateTime(order.created_at)}</td></tr>
<tr><td>更新时间</td><td>${formatDateTime(order.updated_at)}</td></tr>
</table>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h6>商品详情</h6>
<div id="itemDetailContent">
<div class="text-center">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<span class="ms-2">正在加载商品详情...</span>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
`;
// 移除已存在的模态框
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 = `
<div class="card">
<div class="card-body">
<h6 class="card-title">${item.item_title || '商品标题未知'}</h6>
<p class="card-text">${item.item_description || '暂无描述'}</p>
<div class="row">
<div class="col-md-6">
<small class="text-muted">分类${item.item_category || '未知'}</small>
</div>
<div class="col-md-6">
<small class="text-muted">价格${item.item_price || '未知'}</small>
</div>
</div>
${item.item_detail ? `
<div class="mt-2">
<small class="text-muted">详情</small>
<div class="border p-2 mt-1" style="max-height: 200px; overflow-y: auto;">
<small>${item.item_detail}</small>
</div>
</div>
` : ''}
</div>
</div>
`;
} else {
content.innerHTML = `
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
无法获取商品详情信息
</div>
`;
}
} catch (error) {
console.error('加载商品详情失败:', error);
const content = document.getElementById('itemDetailContent');
if (content) {
content.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
加载商品详情失败${error.message}
</div>
`;
}
}
}
// 删除订单
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);
});

View File

@ -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("❌ 订单详情获取失败")