mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-08-29 17:17:38 +08:00
新增订单管理等;优化程序支持系统消息回复
This commit is contained in:
parent
383e841acc
commit
46f7066519
78
README.md
78
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依赖包列表,精简版本无冗余依赖,按功能分类组织
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
|
@ -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)
|
||||
|
153
db_manager.py
153
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')
|
||||
|
@ -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
345
secure_confirm_decrypted.py
Normal 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}
|
@ -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;
|
||||
|
@ -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': '订单表'
|
||||
};
|
||||
|
||||
// 页面加载完成后初始化
|
||||
|
@ -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"> </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>
|
||||
|
749
static/js/app.js
749
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 = '<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);
|
||||
});
|
||||
|
@ -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("❌ 订单详情获取失败")
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user