diff --git a/README.md b/README.md index d5bc022..8598593 100644 --- a/README.md +++ b/README.md @@ -358,51 +358,57 @@ python Start.py ## 📁 核心文件功能说明 -### 🚀 启动和核心模块 -- **`Start.py`** - 项目启动入口,初始化CookieManager和FastAPI服务,管理多账号任务 -- **`XianyuAutoAsync.py`** - 闲鱼WebSocket连接核心,处理消息收发、自动回复、自动发货 -- **`reply_server.py`** - FastAPI Web服务器,提供完整的管理界面和RESTful API接口 -- **`cookie_manager.py`** - 多账号Cookie管理器,负责账号任务的启动、停止和状态管理 +### 🚀 核心启动模块 +- **`Start.py`** - 项目启动入口,初始化CookieManager和FastAPI服务,从数据库加载账号任务并启动后台API服务 +- **`XianyuAutoAsync.py`** - 闲鱼WebSocket连接核心,处理消息收发、自动回复、自动发货、商品信息收集 +- **`reply_server.py`** - FastAPI Web服务器,提供完整的管理界面和RESTful API接口,支持多用户系统 +- **`cookie_manager.py`** - 多账号Cookie管理器,负责账号任务的启动、停止、状态管理和线程安全操作 ### 🗄️ 数据和配置管理 -- **`db_manager.py`** - SQLite数据库管理器,支持多用户数据隔离、自动迁移、版本管理 -- **`config.py`** - 全局配置文件管理器,加载YAML配置和环境变量 -- **`global_config.yml`** - 全局配置文件,包含WebSocket、API、自动回复等所有配置项 +- **`db_manager.py`** - SQLite数据库管理器,支持多用户数据隔离、自动迁移、版本管理、完整的CRUD操作 +- **`config.py`** - 全局配置文件管理器,加载YAML配置和环境变量,提供配置项访问接口 +- **`global_config.yml`** - 全局配置文件,包含WebSocket、API、自动回复、AI等所有系统配置项 ### 🤖 智能功能模块 -- **`ai_reply_engine.py`** - AI智能回复引擎,支持OpenAI、通义千问等多种AI模型 -- **`secure_confirm_ultra.py`** - 自动确认发货模块,采用多层加密保护核心业务逻辑 -- **`secure_freeshipping_ultra.py`** - 自动免拼发货模块,支持批量处理和异常恢复 -- **`file_log_collector.py`** - 实时日志收集器,提供Web界面日志查看和管理 +- **`ai_reply_engine.py`** - AI智能回复引擎,支持OpenAI、通义千问等多种AI模型,意图识别和上下文管理 +- **`secure_confirm_ultra.py`** - 自动确认发货模块,采用多层加密保护,调用闲鱼API确认发货状态 +- **`secure_freeshipping_ultra.py`** - 自动免拼发货模块,支持批量处理、异常恢复和智能匹配 +- **`file_log_collector.py`** - 实时日志收集器,提供Web界面日志查看、搜索和管理功能 -### 🛠️ 工具模块 -- **`utils/xianyu_utils.py`** - 闲鱼API工具函数,包含加密解密、签名生成、数据解析 -- **`utils/message_utils.py`** - 消息格式化和处理工具,支持变量替换和模板渲染 -- **`utils/ws_utils.py`** - WebSocket客户端封装,提供连接管理和重连机制 -- **`utils/item_search.py`** - 商品搜索功能,基于Playwright获取真实闲鱼数据 -- **`utils/order_detail_fetcher.py`** - 订单详情获取工具,支持多规格商品信息解析 -- **`utils/image_utils.py`** - 图片处理工具,支持压缩、格式转换、尺寸调整 -- **`utils/image_uploader.py`** - 图片上传到CDN工具,支持闲鱼图片服务器上传 -- **`utils/qr_login.py`** - 二维码登录功能,支持扫码获取Cookie +### 🛠️ 工具模块 (`utils/`) +- **`xianyu_utils.py`** - 闲鱼API核心工具,包含加密算法、签名生成、数据解析、Cookie处理 +- **`message_utils.py`** - 消息处理工具,格式化消息内容、变量替换、内容过滤和模板渲染 +- **`ws_utils.py`** - WebSocket客户端封装,处理连接管理、心跳检测、重连机制和消息队列 +- **`qr_login.py`** - 二维码登录功能,生成登录二维码、状态检测、Cookie获取和验证 +- **`item_search.py`** - 商品搜索功能,基于Playwright获取真实闲鱼商品数据,支持分页和过滤 +- **`order_detail_fetcher.py`** - 订单详情获取工具,解析订单信息、买家信息、SKU详情,支持缓存优化 +- **`image_utils.py`** - 图片处理工具,支持压缩、格式转换、尺寸调整、水印添加 +- **`image_uploader.py`** - 图片上传工具,支持多种CDN服务商,自动压缩和格式优化 -### 🌐 前端界面 -- **`static/index.html`** - 主管理界面,集成账号管理、系统监控、功能配置 -- **`static/login.html`** - 用户登录页面,支持图形验证码和记住登录状态 -- **`static/register.html`** - 用户注册页面,支持邮箱验证和实时验证 -- **`static/user_management.html`** - 用户管理页面,管理员专用功能 -- **`static/data_management.html`** - 数据管理页面,支持Excel导入导出和批量操作 -- **`static/log_management.html`** - 日志管理页面,实时日志查看和过滤 -- **`static/item_search.html`** - 商品搜索页面,获取真实闲鱼商品数据 -- **`static/js/app.js`** - 主要JavaScript逻辑,处理前端交互和API调用 -- **`static/css/style.css`** - 自定义样式文件,美化界面和响应式设计 +### 🌐 前端界面 (`static/`) +- **`index.html`** - 主管理界面,包含账号管理、关键词管理、系统监控、实时状态显示 +- **`login.html`** - 用户登录页面,支持图形验证码、记住登录状态、多重安全验证 +- **`register.html`** - 用户注册页面,支持邮箱验证码、实时验证、密码强度检测 +- **`user_management.html`** - 用户管理页面,管理员专用,用户增删改查、权限管理 +- **`data_management.html`** - 数据管理页面,支持Excel导入导出、数据备份、批量操作 +- **`log_management.html`** - 日志管理页面,实时日志查看、日志搜索过滤、日志下载 +- **`item_search.html`** - 商品搜索页面,获取真实闲鱼商品数据,支持多条件搜索 +- **`js/app.js`** - 主要JavaScript逻辑,处理前端交互、API调用、实时更新 +- **`css/`** - 模块化样式文件,包含布局、组件、主题等分类样式,响应式设计 +- **`xianyu_js_version_2.js`** - 闲鱼JavaScript工具库,加密解密、数据处理、API封装 +- **`lib/`** - 前端依赖库,包含Bootstrap、jQuery、Chart.js等第三方库 ### 🐳 部署配置 -- **`Dockerfile`** - Docker镜像构建文件,包含Python环境、Playwright浏览器等 -- **`docker-compose.yml`** - Docker Compose配置,支持一键部署和Nginx反向代理 -- **`docker-deploy.sh`** - Docker部署管理脚本,提供构建、启动、监控等功能 +- **`Dockerfile`** - Docker镜像构建文件,包含Python环境、Playwright浏览器、系统依赖 +- **`Dockerfile-cn`** - 中国镜像源版本,优化国内网络环境下的构建速度 +- **`docker-compose.yml`** - Docker Compose配置,支持一键部署、Nginx反向代理、资源限制 +- **`docker-compose-cn.yml`** - 中国镜像源版本,适配国内网络环境 +- **`docker-deploy.sh`** - Docker部署管理脚本,提供构建、启动、监控、日志查看等功能 +- **`docker-deploy.bat`** - Windows版本部署脚本,支持Windows环境一键部署 +- **`entrypoint.sh`** - Docker容器启动脚本,处理环境初始化和服务启动 - **`nginx/nginx.conf`** - Nginx反向代理配置,支持负载均衡和SSL终端 -- **`.env`** - 环境变量配置文件,包含所有可配置的系统参数 -- **`requirements.txt`** - Python依赖包列表,精简版本无冗余依赖 +- **`.env`** - 环境变量配置文件,包含所有可配置的系统参数和敏感信息 +- **`requirements.txt`** - Python依赖包列表,精简版本无冗余依赖,按功能分类组织 ## ⚙️ 配置说明 diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 74abb60..def2fe9 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -142,7 +142,7 @@ class XianyuLive: self.myid = self.cookies['unb'] logger.info(f"【{cookie_id}】用户ID: {self.myid}") self.device_id = generate_device_id(self.myid) - + # 心跳相关配置 self.heartbeat_interval = HEARTBEAT_INTERVAL self.heartbeat_timeout = HEARTBEAT_TIMEOUT @@ -150,7 +150,7 @@ class XianyuLive: self.last_heartbeat_response = 0 self.heartbeat_task = None self.ws = None - + # Token刷新相关配置 self.token_refresh_interval = TOKEN_REFRESH_INTERVAL self.token_retry_interval = TOKEN_RETRY_INTERVAL @@ -238,16 +238,34 @@ class XianyuLive: # 先查看消息的完整结构 logger.debug(f"【{self.cookie_id}】🔍 完整消息结构: {message}") - # 检查message['1']的结构 + # 检查message['1']的结构,处理可能是列表、字典或字符串的情况 message_1 = message.get('1', {}) - logger.debug(f"【{self.cookie_id}】🔍 message['1'] keys: {list(message_1.keys()) if message_1 else 'None'}") + content_json_str = '' - # 检查message['1']['6']的结构 - message_1_6 = message_1.get('6', {}) if message_1 else {} - logger.debug(f"【{self.cookie_id}】🔍 message['1']['6'] keys: {list(message_1_6.keys()) if message_1_6 else 'None'}") + if isinstance(message_1, dict): + logger.debug(f"【{self.cookie_id}】🔍 message['1'] 是字典,keys: {list(message_1.keys())}") + + # 检查message['1']['6']的结构 + message_1_6 = message_1.get('6', {}) + if isinstance(message_1_6, dict): + logger.debug(f"【{self.cookie_id}】🔍 message['1']['6'] 是字典,keys: {list(message_1_6.keys())}") + # 方法1: 从button的targetUrl中提取orderId + content_json_str = message_1_6.get('3', {}).get('5', '') if isinstance(message_1_6.get('3', {}), dict) else '' + else: + logger.debug(f"【{self.cookie_id}】🔍 message['1']['6'] 不是字典: {type(message_1_6)}") + + elif isinstance(message_1, list): + logger.debug(f"【{self.cookie_id}】🔍 message['1'] 是列表,长度: {len(message_1)}") + # 如果message['1']是列表,跳过这种提取方式 + + elif isinstance(message_1, str): + logger.debug(f"【{self.cookie_id}】🔍 message['1'] 是字符串,长度: {len(message_1)}") + # 如果message['1']是字符串,跳过这种提取方式 + + else: + logger.debug(f"【{self.cookie_id}】🔍 message['1'] 未知类型: {type(message_1)}") + # 其他类型,跳过这种提取方式 - # 方法1: 从button的targetUrl中提取orderId - content_json_str = message.get('1', {}).get('6', {}).get('3', {}).get('5', '') if content_json_str: try: content_data = json.loads(content_json_str) @@ -287,10 +305,40 @@ class XianyuLive: except Exception as parse_e: logger.debug(f"解析dynamicOperation JSON失败: {parse_e}") + # 方法3: 如果前面的方法都失败,尝试在整个消息中搜索订单ID模式 + if not order_id: + try: + # 将整个消息转换为字符串进行搜索 + message_str = str(message) + + # 搜索各种可能的订单ID模式 + patterns = [ + r'orderId[=:](\d{10,})', # orderId=123456789 或 orderId:123456789 + r'order_detail\?id=(\d{10,})', # order_detail?id=123456789 + r'"id"\s*:\s*"?(\d{10,})"?', # "id":"123456789" 或 "id":123456789 + r'bizOrderId[=:](\d{10,})', # bizOrderId=123456789 + ] + + for pattern in patterns: + matches = re.findall(pattern, message_str) + if matches: + # 取第一个匹配的订单ID + order_id = matches[0] + logger.info(f'【{self.cookie_id}】✅ 从消息字符串中提取到订单ID: {order_id} (模式: {pattern})') + break + + except Exception as search_e: + logger.debug(f"在消息字符串中搜索订单ID失败: {search_e}") + + if order_id: + logger.info(f'【{self.cookie_id}】🎯 最终提取到订单ID: {order_id}') + else: + logger.debug(f'【{self.cookie_id}】❌ 未能从消息中提取到订单ID') + return order_id except Exception as e: - logger.error(f"提取订单ID失败: {self._safe_str(e)}") + logger.error(f"【{self.cookie_id}】提取订单ID失败: {self._safe_str(e)}") return None async def _handle_auto_delivery(self, websocket, message: dict, send_user_name: str, send_user_id: str, @@ -321,7 +369,7 @@ class XianyuLive: logger.info(f"【{self.cookie_id}】准备自动发货: item_id={item_id}, item_title={item_title}") # 调用自动发货方法(包含自动确认发货) - delivery_content = await self._auto_delivery(item_id, item_title, order_id) + delivery_content = await self._auto_delivery(item_id, item_title, order_id, send_user_id) if delivery_content: # 标记已发货(防重复)- 基于订单ID @@ -394,18 +442,18 @@ class XianyuLive: data = { 'data': data_val, } - + # 获取token token = None token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else '' - + sign = generate_sign(params['t'], token, data_val) params['sign'] = sign - + # 发送请求 headers = DEFAULT_HEADERS.copy() headers['cookie'] = self.cookies_str - + async with aiohttp.ClientSession() as session: async with session.post( API_ENDPOINTS.get('token'), @@ -414,7 +462,7 @@ class XianyuLive: headers=headers ) as response: res_json = await response.json() - + # 检查并更新Cookie if 'set-cookie' in response.headers: new_cookies = {} @@ -422,7 +470,7 @@ class XianyuLive: if '=' in cookie: name, value = cookie.split(';')[0].split('=', 1) new_cookies[name.strip()] = value.strip() - + # 更新cookies if new_cookies: self.cookies.update(new_cookies) @@ -431,7 +479,7 @@ class XianyuLive: # 更新数据库中的Cookie await self.update_config_cookies() logger.debug("已更新Cookie到数据库") - + if isinstance(res_json, dict): ret_value = res_json.get('ret', []) # 检查ret是否包含成功信息 @@ -442,7 +490,7 @@ class XianyuLive: self.last_token_refresh_time = time.time() logger.info(f"【{self.cookie_id}】Token刷新成功") return new_token - + logger.error(f"【{self.cookie_id}】Token刷新失败: {res_json}") # 发送Token刷新失败通知 await self.send_token_refresh_notification(f"Token刷新失败: {res_json}", "token_refresh_failed") @@ -822,7 +870,7 @@ class XianyuLive: if '=' in cookie: name, value = cookie.split(';')[0].split('=', 1) new_cookies[name.strip()] = value.strip() - + # 更新cookies if new_cookies: self.cookies.update(new_cookies) @@ -839,7 +887,7 @@ class XianyuLive: # 检查ret是否包含成功信息 if not any('SUCCESS::调用成功' in ret for ret in ret_value): logger.warning(f"商品信息API调用失败,错误信息: {ret_value}") - + await asyncio.sleep(0.5) return await self.get_item_info(item_id, retry_count + 1) else: @@ -982,9 +1030,9 @@ class XianyuLive: return None reply_content = default_reply_settings.get('reply_content', '') - if not reply_content: - logger.warning(f"账号 {self.cookie_id} 默认回复内容为空") - return None + if not reply_content or (reply_content and reply_content.strip() == ''): + logger.info(f"账号 {self.cookie_id} 默认回复内容为空,不进行回复") + return "EMPTY_REPLY" # 返回特殊标记表示不回复 # 进行变量替换 try: @@ -1039,7 +1087,12 @@ class XianyuLive: # 图片类型关键词,发送图片 return await self._handle_image_keyword(keyword, image_url, send_user_name, send_user_id, send_message) else: - # 文本类型关键词,进行变量替换 + # 文本类型关键词,检查回复内容是否为空 + if not reply or (reply and reply.strip() == ''): + logger.info(f"商品ID关键词 '{keyword}' 回复内容为空,不进行回复") + return "EMPTY_REPLY" # 返回特殊标记表示匹配到但不回复 + + # 进行变量替换 try: formatted_reply = reply.format( send_user_name=send_user_name, @@ -1069,7 +1122,12 @@ class XianyuLive: # 图片类型关键词,发送图片 return await self._handle_image_keyword(keyword, image_url, send_user_name, send_user_id, send_message) else: - # 文本类型关键词,进行变量替换 + # 文本类型关键词,检查回复内容是否为空 + if not reply or (reply and reply.strip() == ''): + logger.info(f"通用关键词 '{keyword}' 回复内容为空,不进行回复") + return "EMPTY_REPLY" # 返回特殊标记表示匹配到但不回复 + + # 进行变量替换 try: formatted_reply = reply.format( send_user_name=send_user_name, @@ -1254,14 +1312,17 @@ class XianyuLive: from db_manager import db_manager import aiohttp + logger.info(f"📱 开始发送消息通知 - 账号: {self.cookie_id}, 买家: {send_user_name}") # 获取当前账号的通知配置 notifications = db_manager.get_account_notifications(self.cookie_id) if not notifications: - logger.debug(f"账号 {self.cookie_id} 未配置消息通知") + logger.warning(f"📱 账号 {self.cookie_id} 未配置消息通知,跳过通知发送") return + logger.info(f"📱 找到 {len(notifications)} 个通知渠道配置") + # 构建通知消息 notification_msg = f"🚨 接收消息通知\n\n" \ f"账号: {self.cookie_id}\n" \ @@ -1271,38 +1332,54 @@ class XianyuLive: f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n" # 发送通知到各个渠道 - for notification in notifications: + for i, notification in enumerate(notifications, 1): + logger.info(f"📱 处理第 {i} 个通知渠道: {notification.get('channel_name', 'Unknown')}") + if not notification.get('enabled', True): + logger.warning(f"📱 通知渠道 {notification.get('channel_name')} 已禁用,跳过") continue channel_type = notification.get('channel_type') channel_config = notification.get('channel_config') + logger.info(f"📱 渠道类型: {channel_type}, 配置: {channel_config}") + try: # 解析配置数据 config_data = self._parse_notification_config(channel_config) + logger.info(f"📱 解析后的配置数据: {config_data}") match channel_type: case 'qq': + logger.info(f"📱 开始发送QQ通知...") await self._send_qq_notification(config_data, notification_msg) case 'ding_talk' | 'dingtalk': + logger.info(f"📱 开始发送钉钉通知...") await self._send_dingtalk_notification(config_data, notification_msg) case 'email': + logger.info(f"📱 开始发送邮件通知...") await self._send_email_notification(config_data, notification_msg) case 'webhook': + logger.info(f"📱 开始发送Webhook通知...") await self._send_webhook_notification(config_data, notification_msg) case 'wechat': + logger.info(f"📱 开始发送微信通知...") await self._send_wechat_notification(config_data, notification_msg) case 'telegram': + logger.info(f"📱 开始发送Telegram通知...") await self._send_telegram_notification(config_data, notification_msg) case _: - logger.warning(f"不支持的通知渠道类型: {channel_type}") + logger.warning(f"📱 不支持的通知渠道类型: {channel_type}") except Exception as notify_error: - logger.error(f"发送通知失败 ({notification.get('channel_name', 'Unknown')}): {self._safe_str(notify_error)}") + logger.error(f"📱 发送通知失败 ({notification.get('channel_name', 'Unknown')}): {self._safe_str(notify_error)}") + import traceback + logger.error(f"📱 详细错误信息: {traceback.format_exc()}") except Exception as e: - logger.error(f"处理消息通知失败: {self._safe_str(e)}") + logger.error(f"📱 处理消息通知失败: {self._safe_str(e)}") + import traceback + logger.error(f"📱 详细错误信息: {traceback.format_exc()}") def _parse_notification_config(self, config: str) -> dict: """解析通知配置数据""" @@ -1319,12 +1396,16 @@ class XianyuLive: try: import aiohttp + logger.info(f"📱 QQ通知 - 开始处理配置数据: {config_data}") + # 解析配置(QQ号码) qq_number = config_data.get('qq_number') or config_data.get('config', '') qq_number = qq_number.strip() if qq_number else '' + logger.info(f"📱 QQ通知 - 解析到QQ号码: {qq_number}") + if not qq_number: - logger.warning("QQ通知配置为空") + logger.warning("📱 QQ通知 - QQ号码配置为空,无法发送通知") return # 构建请求URL @@ -1334,16 +1415,25 @@ class XianyuLive: 'msg': message } + logger.info(f"📱 QQ通知 - 请求URL: {api_url}") + logger.info(f"📱 QQ通知 - 请求参数: qq={qq_number}, msg长度={len(message)}") + # 发送GET请求 async with aiohttp.ClientSession() as session: async with session.get(api_url, params=params, timeout=10) as response: + response_text = await response.text() + logger.info(f"📱 QQ通知 - 响应状态: {response.status}") + logger.info(f"📱 QQ通知 - 响应内容: {response_text}") + if response.status == 200: - logger.info(f"QQ通知发送成功: {qq_number}") + logger.info(f"📱 QQ通知发送成功: {qq_number}") else: - logger.warning(f"QQ通知发送失败: {response.status}") + logger.warning(f"📱 QQ通知发送失败: HTTP {response.status}, 响应: {response_text}") except Exception as e: - logger.error(f"发送QQ通知异常: {self._safe_str(e)}") + logger.error(f"📱 发送QQ通知异常: {self._safe_str(e)}") + import traceback + logger.error(f"📱 QQ通知异常详情: {traceback.format_exc()}") async def _send_dingtalk_notification(self, config_data: dict, message: str): """发送钉钉通知""" @@ -1788,25 +1878,37 @@ class XianyuLive: except Exception as e: logger.error(f"发送自动发货通知异常: {self._safe_str(e)}") - async def auto_confirm(self, order_id, retry_count=0): + async def auto_confirm(self, order_id, item_id=None, retry_count=0): """自动确认发货 - 使用加密模块,不包含延时处理(延时已在_auto_delivery中处理)""" try: logger.debug(f"【{self.cookie_id}】开始确认发货,订单ID: {order_id}") - # 导入超级混淆加密模块 - from secure_confirm_ultra import SecureConfirm + # 导入解密后的确认发货模块 + from secure_confirm_decrypted import SecureConfirm - # 创建加密确认实例 - secure_confirm = SecureConfirm(self.session, self.cookies_str, self.cookie_id) + # 创建确认实例,传入主界面类实例 + secure_confirm = SecureConfirm(self.session, self.cookies_str, self.cookie_id, self) # 传递必要的属性 secure_confirm.current_token = self.current_token secure_confirm.last_token_refresh_time = self.last_token_refresh_time secure_confirm.token_refresh_interval = self.token_refresh_interval - secure_confirm.refresh_token = self.refresh_token # 传递refresh_token方法 - # 调用加密的确认方法 - return await secure_confirm.auto_confirm(order_id, retry_count) + # 调用确认方法,传入item_id用于token刷新 + result = await secure_confirm.auto_confirm(order_id, item_id, retry_count) + + # 同步更新后的cookies和token + if secure_confirm.cookies_str != self.cookies_str: + self.cookies_str = secure_confirm.cookies_str + self.cookies = secure_confirm.cookies + logger.debug(f"【{self.cookie_id}】已同步确认发货模块更新的cookies") + + if secure_confirm.current_token != self.current_token: + self.current_token = secure_confirm.current_token + self.last_token_refresh_time = secure_confirm.last_token_refresh_time + logger.debug(f"【{self.cookie_id}】已同步确认发货模块更新的token") + + return result except Exception as e: logger.error(f"【{self.cookie_id}】加密确认模块调用失败: {self._safe_str(e)}") @@ -1836,20 +1938,26 @@ class XianyuLive: logger.error(f"【{self.cookie_id}】加密免拼发货模块调用失败: {self._safe_str(e)}") return {"error": f"加密免拼发货模块调用失败: {self._safe_str(e)}", "order_id": order_id} - async def fetch_order_detail_info(self, order_id: str): + async def fetch_order_detail_info(self, order_id: str, item_id: str = None, buyer_id: str = None, debug_headless: bool = None): """获取订单详情信息""" try: logger.info(f"【{self.cookie_id}】开始获取订单详情: {order_id}") # 导入订单详情获取器 from utils.order_detail_fetcher import fetch_order_detail_simple + from db_manager import db_manager # 获取当前账号的cookie字符串 cookie_string = self.cookies_str logger.debug(f"【{self.cookie_id}】使用Cookie长度: {len(cookie_string) if cookie_string else 0}") - # 异步获取订单详情(使用当前账号的cookie和无头模式) - result = await fetch_order_detail_simple(order_id, cookie_string, headless=True) + # 确定是否使用有头模式(调试用) + headless_mode = True if debug_headless is None else debug_headless + if not headless_mode: + logger.info(f"【{self.cookie_id}】🖥️ 启用有头模式进行调试") + + # 异步获取订单详情(使用当前账号的cookie) + result = await fetch_order_detail_simple(order_id, cookie_string, headless=headless_mode) if result: logger.info(f"【{self.cookie_id}】订单详情获取成功: {order_id}") @@ -1858,6 +1966,8 @@ class XianyuLive: # 获取解析后的规格信息 spec_name = result.get('spec_name', '') spec_value = result.get('spec_value', '') + quantity = result.get('quantity', '') + amount = result.get('amount', '') if spec_name and spec_value: logger.info(f"【{self.cookie_id}】📋 规格名称: {spec_name}") @@ -1867,6 +1977,29 @@ class XianyuLive: logger.warning(f"【{self.cookie_id}】未获取到有效的规格信息") print(f"⚠️ 【{self.cookie_id}】订单 {order_id} 规格信息获取失败") + # 插入或更新订单信息到数据库 + try: + success = db_manager.insert_or_update_order( + order_id=order_id, + item_id=item_id, + buyer_id=buyer_id, + spec_name=spec_name, + spec_value=spec_value, + quantity=quantity, + amount=amount, + order_status='processed', # 已处理状态 + cookie_id=self.cookie_id + ) + + if success: + logger.info(f"【{self.cookie_id}】订单信息已保存到数据库: {order_id}") + print(f"💾 【{self.cookie_id}】订单 {order_id} 信息已保存到数据库") + else: + logger.warning(f"【{self.cookie_id}】订单信息保存失败: {order_id}") + + except Exception as db_e: + logger.error(f"【{self.cookie_id}】保存订单信息到数据库失败: {self._safe_str(db_e)}") + return result else: logger.warning(f"【{self.cookie_id}】订单详情获取失败: {order_id}") @@ -1876,7 +2009,7 @@ class XianyuLive: logger.error(f"【{self.cookie_id}】获取订单详情异常: {self._safe_str(e)}") return None - async def _auto_delivery(self, item_id: str, item_title: str = None, order_id: str = None): + async def _auto_delivery(self, item_id: str, item_title: str = None, order_id: str = None, send_user_id: str = None): """自动发货功能 - 获取卡券规则,执行延时,确认发货,发送内容""" try: from db_manager import db_manager @@ -1987,7 +2120,7 @@ class XianyuLive: if is_multi_spec and order_id: logger.info(f"检测到多规格商品,获取订单规格信息: {order_id}") try: - order_detail = await self.fetch_order_detail_info(order_id) + order_detail = await self.fetch_order_detail_info(order_id, item_id, send_user_id) if order_detail: spec_name = order_detail.get('spec_name', '') spec_value = order_detail.get('spec_value', '') @@ -2087,8 +2220,8 @@ class XianyuLive: should_confirm = False if should_confirm: - logger.info(f"开始自动确认发货: 订单ID={order_id}") - confirm_result = await self.auto_confirm(order_id) + logger.info(f"开始自动确认发货: 订单ID={order_id}, 商品ID={item_id}") + confirm_result = await self.auto_confirm(order_id, item_id) if confirm_result.get('success'): self.confirmed_orders[order_id] = current_time logger.info(f"🎉 自动确认发货成功!订单ID: {order_id}") @@ -2098,6 +2231,23 @@ class XianyuLive: # 检查是否存在订单ID,只有存在订单ID才处理发货内容 if order_id: + # 保存订单基本信息到数据库(如果还没有详细信息) + try: + from db_manager import db_manager + existing_order = db_manager.get_order_by_id(order_id) + if not existing_order: + # 插入基本订单信息 + db_manager.insert_or_update_order( + order_id=order_id, + item_id=item_id, + buyer_id=send_user_id, + order_status='processing', # 处理中状态 + cookie_id=self.cookie_id + ) + logger.info(f"保存基本订单信息到数据库: {order_id}") + except Exception as db_e: + logger.error(f"保存基本订单信息失败: {self._safe_str(db_e)}") + # 开始处理发货内容 logger.info(f"开始处理发货内容,规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})") @@ -2384,7 +2534,7 @@ class XianyuLive: else: logger.info("由于刚刚尝试过token刷新,跳过重复的初始化失败通知") raise Exception("Token获取失败") - + msg = { "lwp": "/reg", "headers": { @@ -2572,8 +2722,8 @@ class XianyuLive: """判断是否为用户聊天消息""" try: return ( - isinstance(message, dict) - and "1" in message + isinstance(message, dict) + and "1" in message and isinstance(message["1"], dict) and "10" in message["1"] and isinstance(message["1"]["10"], dict) @@ -2621,7 +2771,7 @@ class XianyuLive: api_config = AUTO_REPLY.get('api', {}) timeout = aiohttp.ClientTimeout(total=api_config.get('timeout', 10)) - + payload = { "cookie_id": self.cookie_id, "msg_time": msg_time, @@ -2632,14 +2782,14 @@ class XianyuLive: "send_message": send_message, "chat_id": chat_id } - + async with self.session.post( api_config.get('url', 'http://localhost:8080/xianyu/reply'), json=payload, timeout=timeout ) as response: result = await response.json() - + # 将code转换为字符串进行比较,或者直接用数字比较 if str(result.get('code')) == '200' or result.get('code') == 200: send_msg = result.get('data', {}).get('send_msg') @@ -2656,7 +2806,7 @@ class XianyuLive: else: logger.warning(f"API返回错误: {result.get('msg', '未知错误')}") return None - + except asyncio.TimeoutError: logger.error("API调用超时") return None @@ -2699,7 +2849,7 @@ class XianyuLive: # 获取并解密数据 sync_data = message_data["body"]["syncPushPackage"]["data"][0] - + # 检查是否有必要的字段 if "data" not in sync_data: logger.debug("同步包中无data字段") @@ -2749,6 +2899,62 @@ class XianyuLive: logger.debug(f"消息内容: {message}") return + # 【优先处理】尝试获取订单ID并获取订单详情 + order_id = None + try: + order_id = self._extract_order_id(message) + if order_id: + msg_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + logger.info(f'[{msg_time}] 【{self.cookie_id}】✅ 检测到订单ID: {order_id},开始获取订单详情') + + # 立即获取订单详情信息 + try: + # 先尝试提取用户ID和商品ID用于订单详情获取 + temp_user_id = None + temp_item_id = None + + # 提取用户ID + try: + message_1 = message.get("1") + if isinstance(message_1, str) and '@' in message_1: + temp_user_id = message_1.split('@')[0] + elif isinstance(message_1, dict): + # 从字典中提取用户ID + if "10" in message_1 and isinstance(message_1["10"], dict): + temp_user_id = message_1["10"].get("senderUserId", "unknown_user") + else: + temp_user_id = "unknown_user" + else: + temp_user_id = "unknown_user" + except: + temp_user_id = "unknown_user" + + # 提取商品ID + try: + if "1" in message and isinstance(message["1"], dict) and "10" in message["1"] and isinstance(message["1"]["10"], dict): + url_info = message["1"]["10"].get("reminderUrl", "") + if isinstance(url_info, str) and "itemId=" in url_info: + temp_item_id = url_info.split("itemId=")[1].split("&")[0] + + if not temp_item_id: + temp_item_id = self.extract_item_id_from_message(message) + except: + pass + + # 调用订单详情获取方法 + order_detail = await self.fetch_order_detail_info(order_id, temp_item_id, temp_user_id) + if order_detail: + logger.info(f'[{msg_time}] 【{self.cookie_id}】✅ 订单详情获取成功: {order_id}') + else: + logger.warning(f'[{msg_time}] 【{self.cookie_id}】⚠️ 订单详情获取失败: {order_id}') + + except Exception as detail_e: + logger.error(f'[{msg_time}] 【{self.cookie_id}】❌ 获取订单详情异常: {self._safe_str(detail_e)}') + else: + logger.debug(f"【{self.cookie_id}】未检测到订单ID") + except Exception as e: + logger.error(f"【{self.cookie_id}】提取订单ID失败: {self._safe_str(e)}") + # 安全地获取用户ID user_id = None try: @@ -2756,8 +2962,11 @@ class XianyuLive: if isinstance(message_1, str) and '@' in message_1: user_id = message_1.split('@')[0] elif isinstance(message_1, dict): - # 如果message['1']是字典,尝试其他方式提取user_id - user_id = "unknown_user" + # 如果message['1']是字典,从message["1"]["10"]["senderUserId"]中提取user_id + if "10" in message_1 and isinstance(message_1["10"], dict): + user_id = message_1["10"].get("senderUserId", "unknown_user") + else: + user_id = "unknown_user" else: user_id = "unknown_user" except Exception as e: @@ -2856,6 +3065,11 @@ class XianyuLive: else: logger.info(f"[{msg_time}] 【收到】用户: {send_user_name} (ID: {send_user_id}), 商品({item_id}): {send_message}") + # 🔔 立即发送消息通知(独立于自动回复功能) + try: + await self.send_notification(send_user_name, send_user_id, send_message, item_id) + except Exception as notify_error: + logger.error(f"📱 发送消息通知失败: {self._safe_str(notify_error)}") @@ -2875,7 +3089,7 @@ class XianyuLive: # 构造用户URL user_url = f'https://www.goofish.com/personal?userId={send_user_id}' - + reply = None # 判断是否启用API回复 if AUTO_REPLY.get('api', {}).get('enabled', False): @@ -2885,7 +3099,7 @@ class XianyuLive: ) if not reply: logger.error(f"[{msg_time}] 【API调用失败】用户: {send_user_name} (ID: {send_user_id}), 商品({item_id}): {send_message}") - + if send_message == '[我已拍下,待付款]': logger.info(f'[{msg_time}] 【{self.cookie_id}】系统消息不处理') return @@ -2978,7 +3192,11 @@ class XianyuLive: if not reply: # 1. 首先尝试关键词匹配(传入商品ID) reply = await self.get_keyword_reply(send_user_name, send_user_id, send_message, item_id) - if reply: + if reply == "EMPTY_REPLY": + # 匹配到关键词但回复内容为空,不进行任何回复 + logger.info(f"[{msg_time}] 【{self.cookie_id}】匹配到空回复关键词,跳过自动回复") + return + elif reply: reply_source = '关键词' # 标记为关键词回复 else: # 2. 关键词匹配失败,如果AI开关打开,尝试AI回复 @@ -2988,12 +3206,15 @@ class XianyuLive: else: # 3. 最后使用默认回复 reply = await self.get_default_reply(send_user_name, send_user_id, send_message, chat_id) + if reply == "EMPTY_REPLY": + # 默认回复内容为空,不进行任何回复 + logger.info(f"[{msg_time}] 【{self.cookie_id}】默认回复内容为空,跳过自动回复") + return reply_source = '默认' # 标记为默认回复 # 注意:这里只有商品ID,没有标题和详情,根据新的规则不保存到数据库 # 商品信息会在其他有完整信息的地方保存(如发货规则匹配时) - # 发送通知 - await self.send_notification(send_user_name, send_user_id, send_message, item_id) + # 消息通知已在收到消息时立即发送,此处不再重复发送 # 如果有回复内容,发送消息 if reply: @@ -3022,7 +3243,7 @@ class XianyuLive: else: msg_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) logger.info(f"[{msg_time}] 【{self.cookie_id}】【系统】未找到匹配的回复规则,不回复") - + except Exception as e: logger.error(f"处理消息时发生错误: {self._safe_str(e)}") logger.debug(f"原始消息: {message_data}") @@ -3194,7 +3415,7 @@ class XianyuLive: if '=' in cookie: name, value = cookie.split(';')[0].split('=', 1) new_cookies[name.strip()] = value.strip() - + # 更新cookies if new_cookies: self.cookies.update(new_cookies) diff --git a/db_manager.py b/db_manager.py index 3e3cb84..c1eac1c 100644 --- a/db_manager.py +++ b/db_manager.py @@ -212,6 +212,24 @@ class DBManager: ) ''') + # 创建订单表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS orders ( + order_id TEXT PRIMARY KEY, + item_id TEXT, + buyer_id TEXT, + spec_name TEXT, + spec_value TEXT, + quantity TEXT, + amount TEXT, + order_status TEXT DEFAULT 'unknown', + cookie_id TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE + ) + ''') + # 检查并添加 user_id 列(用于数据库迁移) try: self._execute_sql(cursor, "SELECT user_id FROM cards LIMIT 1") @@ -4017,6 +4035,138 @@ class DBManager: logger.error(f"获取表数据失败: {table_name} - {e}") return [], [] + def insert_or_update_order(self, order_id: str, item_id: str = None, buyer_id: str = None, + spec_name: str = None, spec_value: str = None, quantity: str = None, + amount: str = None, order_status: str = None, cookie_id: str = None): + """插入或更新订单信息""" + with self.lock: + try: + cursor = self.conn.cursor() + + # 检查订单是否已存在 + cursor.execute("SELECT order_id FROM orders WHERE order_id = ?", (order_id,)) + existing = cursor.fetchone() + + if existing: + # 更新现有订单 + update_fields = [] + update_values = [] + + if item_id is not None: + update_fields.append("item_id = ?") + update_values.append(item_id) + if buyer_id is not None: + update_fields.append("buyer_id = ?") + update_values.append(buyer_id) + if spec_name is not None: + update_fields.append("spec_name = ?") + update_values.append(spec_name) + if spec_value is not None: + update_fields.append("spec_value = ?") + update_values.append(spec_value) + if quantity is not None: + update_fields.append("quantity = ?") + update_values.append(quantity) + if amount is not None: + update_fields.append("amount = ?") + update_values.append(amount) + if order_status is not None: + update_fields.append("order_status = ?") + update_values.append(order_status) + if cookie_id is not None: + update_fields.append("cookie_id = ?") + update_values.append(cookie_id) + + if update_fields: + update_fields.append("updated_at = CURRENT_TIMESTAMP") + update_values.append(order_id) + + sql = f"UPDATE orders SET {', '.join(update_fields)} WHERE order_id = ?" + cursor.execute(sql, update_values) + logger.info(f"更新订单信息: {order_id}") + else: + # 插入新订单 + cursor.execute(''' + INSERT INTO orders (order_id, item_id, buyer_id, spec_name, spec_value, + quantity, amount, order_status, cookie_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (order_id, item_id, buyer_id, spec_name, spec_value, + quantity, amount, order_status or 'unknown', cookie_id)) + logger.info(f"插入新订单: {order_id}") + + self.conn.commit() + return True + + except Exception as e: + logger.error(f"插入或更新订单失败: {order_id} - {e}") + self.conn.rollback() + return False + + def get_order_by_id(self, order_id: str): + """根据订单ID获取订单信息""" + with self.lock: + try: + cursor = self.conn.cursor() + cursor.execute(''' + SELECT order_id, item_id, buyer_id, spec_name, spec_value, + quantity, amount, order_status, cookie_id, created_at, updated_at + FROM orders WHERE order_id = ? + ''', (order_id,)) + + row = cursor.fetchone() + if row: + return { + 'order_id': row[0], + 'item_id': row[1], + 'buyer_id': row[2], + 'spec_name': row[3], + 'spec_value': row[4], + 'quantity': row[5], + 'amount': row[6], + 'order_status': row[7], + 'cookie_id': row[8], + 'created_at': row[9], + 'updated_at': row[10] + } + return None + + except Exception as e: + logger.error(f"获取订单信息失败: {order_id} - {e}") + return None + + def get_orders_by_cookie(self, cookie_id: str, limit: int = 100): + """根据Cookie ID获取订单列表""" + with self.lock: + try: + cursor = self.conn.cursor() + cursor.execute(''' + SELECT order_id, item_id, buyer_id, spec_name, spec_value, + quantity, amount, order_status, created_at, updated_at + FROM orders WHERE cookie_id = ? + ORDER BY created_at DESC LIMIT ? + ''', (cookie_id, limit)) + + orders = [] + for row in cursor.fetchall(): + orders.append({ + 'order_id': row[0], + 'item_id': row[1], + 'buyer_id': row[2], + 'spec_name': row[3], + 'spec_value': row[4], + 'quantity': row[5], + 'amount': row[6], + 'order_status': row[7], + 'created_at': row[8], + 'updated_at': row[9] + }) + + return orders + + except Exception as e: + logger.error(f"获取Cookie订单列表失败: {cookie_id} - {e}") + return [] + def delete_table_record(self, table_name: str, record_id: str): """删除指定表的指定记录""" with self.lock: @@ -4036,7 +4186,8 @@ class DBManager: 'notification_channels': 'id', 'user_settings': 'id', 'email_verifications': 'id', - 'captcha_codes': 'id' + 'captcha_codes': 'id', + 'orders': 'order_id' } primary_key = primary_key_map.get(table_name, 'id') diff --git a/reply_server.py b/reply_server.py index 46232d8..9b7c45e 100644 --- a/reply_server.py +++ b/reply_server.py @@ -477,6 +477,9 @@ async def data_management_page(): return HTMLResponse('

Data management page not found

') + + + # 商品搜索页面路由 @app.get('/item_search.html', response_class=HTMLResponse) async def item_search_page(): @@ -1893,8 +1896,8 @@ def update_keywords_with_item_id(cid: str, body: KeywordWithItemIdIn, current_us reply = kw_data.get('reply', '').strip() item_id = kw_data.get('item_id', '').strip() or None - if not keyword or not reply: - raise HTTPException(status_code=400, detail="关键词和回复内容不能为空") + if not keyword: + raise HTTPException(status_code=400, detail="关键词不能为空") # 检查当前提交的关键词中是否有重复 keyword_key = f"{keyword}|{item_id or ''}" @@ -2105,8 +2108,8 @@ async def import_keywords(cid: str, file: UploadFile = File(...), current_user: item_id = str(row['商品ID']).strip() if pd.notna(row['商品ID']) and str(row['商品ID']).strip() else None reply = str(row['关键词内容']).strip() - if not keyword or not reply: - continue # 跳过空行 + if not keyword: + continue # 跳过没有关键词的行 # 检查是否重复 key = f"{keyword}|{item_id or ''}" @@ -3661,7 +3664,7 @@ def get_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(require 'users', 'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records', 'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info', 'message_notifications', 'cards', 'delivery_rules', 'notification_channels', - 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes' + 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders' ] if table_name not in allowed_tables: @@ -3698,7 +3701,7 @@ def delete_table_record(table_name: str, record_id: str, admin_user: Dict[str, A 'users', 'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records', 'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info', 'message_notifications', 'cards', 'delivery_rules', 'notification_channels', - 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes' + 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders' ] if table_name not in allowed_tables: @@ -3738,7 +3741,7 @@ def clear_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(requi 'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records', 'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info', 'message_notifications', 'cards', 'delivery_rules', 'notification_channels', - 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes' + 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders' ] # 不允许清空用户表 diff --git a/secure_confirm_decrypted.py b/secure_confirm_decrypted.py new file mode 100644 index 0000000..09bde9d --- /dev/null +++ b/secure_confirm_decrypted.py @@ -0,0 +1,345 @@ +""" +自动确认发货模块 - 解密版本 +这是secure_confirm_ultra.py的解密版本,用于自动确认发货功能 +""" + +import asyncio +import json +import time +import aiohttp +from loguru import logger +from utils.xianyu_utils import generate_sign, trans_cookies + + +class SecureConfirm: + """自动确认发货类""" + + def __init__(self, session, cookies_str, cookie_id, main_instance=None): + """ + 初始化确认发货实例 + + Args: + session: aiohttp会话对象 + cookies_str: Cookie字符串 + cookie_id: Cookie ID + main_instance: 主实例对象(XianyuLive) + """ + self.session = session + self.cookies_str = cookies_str + self.cookie_id = cookie_id + self.main_instance = main_instance + + # 解析cookies + self.cookies = trans_cookies(cookies_str) if cookies_str else {} + + # Token相关属性 + self.current_token = None + self.last_token_refresh_time = 0 + self.token_refresh_interval = 3600 # 1小时 + + def _safe_str(self, obj): + """安全字符串转换""" + try: + return str(obj) + except: + return "无法转换的对象" + + async def _get_real_item_id(self): + """从数据库中获取一个真实的商品ID""" + try: + from db_manager import db_manager + + # 获取该账号的商品列表 + items = db_manager.get_items_by_cookie(self.cookie_id) + if items: + # 返回第一个商品的ID + item_id = items[0].get('item_id') + if item_id: + logger.debug(f"【{self.cookie_id}】获取到真实商品ID: {item_id}") + return item_id + + # 如果该账号没有商品,尝试获取任意一个商品ID + all_items = db_manager.get_all_items() + if all_items: + item_id = all_items[0].get('item_id') + if item_id: + logger.debug(f"【{self.cookie_id}】使用其他账号的商品ID: {item_id}") + return item_id + + logger.warning(f"【{self.cookie_id}】数据库中没有找到任何商品ID") + return None + + except Exception as e: + logger.error(f"【{self.cookie_id}】获取真实商品ID失败: {self._safe_str(e)}") + return None + + async def refresh_token_by_detail_api(self, retry_count=0): + """通过商品详情API刷新token - 参照get_item_info方法""" + if retry_count >= 3: # 最多重试2次 + logger.error(f"【{self.cookie_id}】通过详情API刷新token失败,重试次数过多") + return False + + try: + # 优先使用传入的item_id,否则从数据库获取 + real_item_id = None + if hasattr(self, '_current_item_id') and self._current_item_id: + real_item_id = self._current_item_id + logger.debug(f"【{self.cookie_id}】使用传入的商品ID: {real_item_id}") + else: + # 从数据库中获取一个真实的商品ID来请求详情API + real_item_id = await self._get_real_item_id() + + if not real_item_id: + logger.warning(f"【{self.cookie_id}】无法获取真实商品ID,使用默认ID") + real_item_id = "123456789" # 备用ID + + params = { + 'jsv': '2.7.2', + 'appKey': '34839810', + 't': str(int(time.time()) * 1000), + 'sign': '', + 'v': '1.0', + 'type': 'originaljson', + 'accountSite': 'xianyu', + 'dataType': 'json', + 'timeout': '20000', + 'api': 'mtop.taobao.idle.pc.detail', + 'sessionOption': 'AutoLoginOnly', + } + + data_val = f'{{"itemId":"{real_item_id}"}}' + data = { + 'data': data_val, + } + + # 从当前cookies中获取token + token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else '' + + if token: + logger.debug(f"【{self.cookie_id}】使用当前token刷新: {token}") + else: + logger.warning(f"【{self.cookie_id}】当前cookies中没有找到token") + + # 生成签名 + sign = generate_sign(params['t'], token, data_val) + params['sign'] = sign + + logger.info(f"【{self.cookie_id}】通过详情API刷新token,使用商品ID: {real_item_id}") + + async with self.session.post( + 'https://h5api.m.goofish.com/h5/mtop.taobao.idle.pc.detail/1.0/', + params=params, + data=data + ) as response: + # 检查并更新Cookie + if 'set-cookie' in response.headers: + new_cookies = {} + for cookie in response.headers.getall('set-cookie', []): + if '=' in cookie: + name, value = cookie.split(';')[0].split('=', 1) + new_cookies[name.strip()] = value.strip() + + # 更新cookies + if new_cookies: + self.cookies.update(new_cookies) + # 生成新的cookie字符串 + self.cookies_str = '; '.join([f"{k}={v}" for k, v in self.cookies.items()]) + # 更新数据库中的Cookie + await self._update_config_cookies() + logger.debug(f"【{self.cookie_id}】已通过详情API更新Cookie到数据库") + + # 获取新的token + new_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else '' + if new_token and new_token != token: + self.current_token = new_token + self.last_token_refresh_time = time.time() + logger.info(f"【{self.cookie_id}】通过详情API成功刷新token: {new_token}") + return True + else: + logger.debug(f"【{self.cookie_id}】详情API返回的token未变化") + + # 检查响应状态 + try: + res_json = await response.json() + if isinstance(res_json, dict): + ret_value = res_json.get('ret', []) + if any('SUCCESS::调用成功' in ret for ret in ret_value): + logger.debug(f"【{self.cookie_id}】详情API调用成功") + return True + else: + logger.warning(f"【{self.cookie_id}】详情API调用失败: {ret_value}") + if retry_count < 2: + await asyncio.sleep(0.5) + return await self.refresh_token_by_detail_api(retry_count + 1) + except: + logger.debug(f"【{self.cookie_id}】详情API响应解析失败,但可能已获取到新cookies") + + return bool(self.current_token) + + except Exception as e: + logger.error(f"【{self.cookie_id}】通过详情API刷新token异常: {self._safe_str(e)}") + if retry_count < 2: + await asyncio.sleep(0.5) + return await self.refresh_token_by_detail_api(retry_count + 1) + return False + + async def _update_config_cookies(self): + """更新数据库中的Cookie配置""" + try: + from db_manager import db_manager + # 更新数据库中的cookies + db_manager.update_cookie_value(self.cookie_id, self.cookies_str) + logger.debug(f"【{self.cookie_id}】已更新数据库中的Cookie") + except Exception as e: + logger.error(f"【{self.cookie_id}】更新数据库Cookie失败: {self._safe_str(e)}") + + async def refresh_token(self): + """刷新token - 优先使用详情API,失败时调用主界面类的方法""" + # 首先尝试通过详情API刷新token + success = await self.refresh_token_by_detail_api() + if success: + return self.current_token + + # 如果详情API失败,尝试调用主界面类的方法 + if self.main_instance and hasattr(self.main_instance, 'refresh_token'): + try: + logger.debug(f"【{self.cookie_id}】详情API刷新失败,调用主界面类的refresh_token方法") + new_token = await self.main_instance.refresh_token() + if new_token: + self.current_token = new_token + self.last_token_refresh_time = time.time() + # 更新本地的cookies_str + self.cookies_str = self.main_instance.cookies_str + # 重新解析cookies + self.cookies = {} + if self.cookies_str: + for cookie in self.cookies_str.split(';'): + if '=' in cookie: + key, value = cookie.strip().split('=', 1) + self.cookies[key] = value + logger.debug(f"【{self.cookie_id}】通过主界面类Token刷新成功,已同步cookies") + return new_token + else: + logger.warning(f"【{self.cookie_id}】主界面类Token刷新失败") + return None + except Exception as e: + logger.error(f"【{self.cookie_id}】调用主界面类refresh_token失败: {self._safe_str(e)}") + return None + else: + logger.warning(f"【{self.cookie_id}】主界面类实例不存在或没有refresh_token方法") + return None + + async def auto_confirm(self, order_id, item_id=None, retry_count=0): + """自动确认发货 - 使用真实商品ID刷新token""" + if retry_count >= 4: # 最多重试3次 + logger.error("自动确认发货失败,重试次数过多") + return {"error": "自动确认发货失败,重试次数过多"} + + # 保存item_id供Token刷新使用 + if item_id: + self._current_item_id = item_id + logger.debug(f"【{self.cookie_id}】设置当前商品ID: {item_id}") + + # 如果是重试(retry_count > 0),强制刷新token + if retry_count > 0: + old_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else '' + logger.info(f"【{self.cookie_id}】重试第{retry_count}次,强制刷新token... 当前_m_h5_tk: {old_token}") + await self.refresh_token() + new_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else '' + logger.info(f"【{self.cookie_id}】重试刷新token完成,新的_m_h5_tk: {new_token}") + else: + # 确保使用最新的token(首次调用时的正常逻辑) + if not self.current_token or (time.time() - self.last_token_refresh_time) >= self.token_refresh_interval: + old_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else '' + logger.info(f"【{self.cookie_id}】Token过期或不存在,刷新token... 当前_m_h5_tk: {old_token}") + await self.refresh_token() + new_token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else '' + logger.info(f"【{self.cookie_id}】Token刷新完成,新的_m_h5_tk: {new_token}") + + # 确保session已创建 + if not self.session: + raise Exception("Session未创建") + + params = { + 'jsv': '2.7.2', + 'appKey': '34839810', + 't': str(int(time.time()) * 1000), + 'sign': '', + 'v': '1.0', + 'type': 'originaljson', + 'accountSite': 'xianyu', + 'dataType': 'json', + 'timeout': '20000', + 'api': 'mtop.taobao.idle.logistic.consign.dummy', + 'sessionOption': 'AutoLoginOnly', + } + + data_val = '{"orderId":"' + order_id + '", "tradeText":"","picList":[],"newUnconsign":true}' + data = { + 'data': data_val, + } + + # 始终从最新的cookies中获取_m_h5_tk token(刷新后cookies会被更新) + token = trans_cookies(self.cookies_str).get('_m_h5_tk', '').split('_')[0] if trans_cookies(self.cookies_str).get('_m_h5_tk') else '' + + if token: + logger.info(f"使用cookies中的_m_h5_tk token: {token}") + else: + logger.warning("cookies中没有找到_m_h5_tk token") + + sign = generate_sign(params['t'], token, data_val) + params['sign'] = sign + + try: + logger.info(f"【{self.cookie_id}】开始自动确认发货,订单ID: {order_id}") + async with self.session.post( + 'https://h5api.m.goofish.com/h5/mtop.taobao.idle.logistic.consign.dummy/1.0/', + params=params, + data=data + ) as response: + res_json = await response.json() + + # 检查并更新Cookie + if 'set-cookie' in response.headers: + new_cookies = {} + for cookie in response.headers.getall('set-cookie', []): + if '=' in cookie: + name, value = cookie.split(';')[0].split('=', 1) + new_cookies[name.strip()] = value.strip() + + # 更新cookies + if new_cookies: + self.cookies.update(new_cookies) + # 生成新的cookie字符串 + self.cookies_str = '; '.join([f"{k}={v}" for k, v in self.cookies.items()]) + # 更新数据库中的Cookie + await self._update_config_cookies() + logger.debug("已更新Cookie到数据库") + + logger.info(f"【{self.cookie_id}】自动确认发货响应: {res_json}") + + # 检查响应结果 + if res_json.get('ret') and res_json['ret'][0] == 'SUCCESS::调用成功': + logger.info(f"【{self.cookie_id}】✅ 自动确认发货成功,订单ID: {order_id}") + return {"success": True, "order_id": order_id} + else: + error_msg = res_json.get('ret', ['未知错误'])[0] if res_json.get('ret') else '未知错误' + logger.warning(f"【{self.cookie_id}】❌ 自动确认发货失败: {error_msg}") + + # 如果是token相关错误,进行重试 + if 'token' in error_msg.lower() or 'sign' in error_msg.lower(): + logger.info(f"【{self.cookie_id}】检测到token错误,准备重试...") + return await self.auto_confirm(order_id, item_id, retry_count + 1) + + return {"error": error_msg, "order_id": order_id} + + except Exception as e: + logger.error(f"【{self.cookie_id}】自动确认发货API请求异常: {self._safe_str(e)}") + await asyncio.sleep(0.5) + + # 网络异常也进行重试 + if retry_count < 2: + logger.info(f"【{self.cookie_id}】网络异常,准备重试...") + return await self.auto_confirm(order_id, item_id, retry_count + 1) + + return {"error": f"网络异常: {self._safe_str(e)}", "order_id": order_id} diff --git a/static/css/dashboard.css b/static/css/dashboard.css index bdd29cd..531c662 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -49,6 +49,11 @@ color: var(--warning-color); } +.stat-icon.info { + background: rgba(59, 130, 246, 0.1); + color: #3b82f6; +} + .stat-number { font-size: 2rem; font-weight: 700; diff --git a/static/data_management.html b/static/data_management.html index cd0f83a..207168a 100644 --- a/static/data_management.html +++ b/static/data_management.html @@ -111,6 +111,7 @@ +
@@ -275,7 +276,8 @@ 'user_settings': '用户设置表', 'system_settings': '系统设置表', 'email_verifications': '邮箱验证表', - 'captcha_codes': '验证码表' + 'captcha_codes': '验证码表', + 'orders': '订单表' }; // 页面加载完成后初始化 diff --git a/static/index.html b/static/index.html index ed1146b..cdc5317 100644 --- a/static/index.html +++ b/static/index.html @@ -42,6 +42,12 @@ 商品管理
+ +
+
+ +
+
0
+
总订单数
+
@@ -443,6 +456,137 @@ + +
+
+

+ + 订单管理 +

+

查看和管理所有订单信息

+
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+
+ +
订单列表
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
+ + 订单ID商品ID买家ID规格信息数量金额状态账号ID操作
加载中...
+
+
+ + + +
+
+
+
@@ -511,8 +655,8 @@
- - + +
@@ -535,7 +679,9 @@ 支持变量: {send_user_name} 用户昵称, {send_user_id} 用户ID, - {send_message} 用户消息 + {send_message} 用户消息
+ + 提示:回复内容留空时,匹配到关键词但不会自动回复,可用于屏蔽特定消息
diff --git a/static/js/app.js b/static/js/app.js index f014999..bf239df 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -25,6 +25,14 @@ let itemsPerPage = 20; // 每页显示数量 let totalItemsPages = 0; // 总页数 let currentSearchKeyword = ''; // 当前搜索关键词 +// 订单列表搜索和分页相关变量 +let allOrdersData = []; // 存储所有订单数据 +let filteredOrdersData = []; // 存储过滤后的订单数据 +let currentOrdersPage = 1; // 当前页码 +let ordersPerPage = 20; // 每页显示数量 +let totalOrdersPages = 0; // 总页数 +let currentOrderSearchKeyword = ''; // 当前搜索关键词 + // ================================ // 通用功能 - 菜单切换和导航 // ================================ @@ -69,6 +77,9 @@ function showSection(sectionName) { case 'items': // 【商品管理菜单】 loadItems(); break; + case 'orders': // 【订单管理菜单】 + loadOrders(); + break; case 'auto-reply': // 【自动回复菜单】 refreshAccountList(); break; @@ -191,6 +202,9 @@ async function loadDashboard() { dashboardData.totalKeywords = totalKeywords; + // 加载订单数量 + await loadOrdersCount(); + // 更新仪表盘显示 updateDashboardStats(accountsWithKeywords.length, totalKeywords, enabledAccounts); updateDashboardAccountsList(accountsWithKeywords); @@ -203,6 +217,30 @@ async function loadDashboard() { } } +// 加载订单数量 +async function loadOrdersCount() { + try { + const token = localStorage.getItem('auth_token'); + const response = await fetch('/admin/data/orders', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + const data = await response.json(); + if (data.success) { + const ordersCount = data.data ? data.data.length : 0; + document.getElementById('totalOrders').textContent = ordersCount; + } else { + console.error('加载订单数量失败:', data.message); + document.getElementById('totalOrders').textContent = '0'; + } + } catch (error) { + console.error('加载订单数量失败:', error); + document.getElementById('totalOrders').textContent = '0'; + } +} + // 更新仪表盘统计数据 function updateDashboardStats(totalAccounts, totalKeywords, enabledAccounts) { document.getElementById('totalAccounts').textContent = totalAccounts; @@ -610,8 +648,8 @@ async function addKeyword() { const reply = document.getElementById('newReply').value.trim(); const itemId = document.getElementById('newItemIdSelect').value.trim(); - if (!keyword || !reply) { - showToast('请填写关键词和回复内容', 'warning'); + if (!keyword) { + showToast('请填写关键词', 'warning'); return; } @@ -1757,10 +1795,9 @@ document.addEventListener('DOMContentLoaded', async () => { if (value.length > 0) { e.target.style.borderColor = '#10b981'; - if (replyInput.value.trim().length > 0) { + // 只要关键词有内容就可以添加,不需要回复内容 addBtn.style.opacity = '1'; addBtn.style.transform = 'scale(1)'; - } } else { e.target.style.borderColor = '#e5e7eb'; addBtn.style.opacity = '0.7'; @@ -1770,17 +1807,21 @@ document.addEventListener('DOMContentLoaded', async () => { document.getElementById('newReply')?.addEventListener('input', function(e) { const value = e.target.value.trim(); - const addBtn = document.querySelector('.add-btn'); const keywordInput = document.getElementById('newKeyword'); + // 回复内容可以为空,只需要关键词有内容即可 if (value.length > 0) { e.target.style.borderColor = '#10b981'; - if (keywordInput.value.trim().length > 0) { - addBtn.style.opacity = '1'; - addBtn.style.transform = 'scale(1)'; - } } else { e.target.style.borderColor = '#e5e7eb'; + } + + // 按钮状态只依赖关键词是否有内容 + const addBtn = document.querySelector('.add-btn'); + if (keywordInput.value.trim().length > 0) { + addBtn.style.opacity = '1'; + addBtn.style.transform = 'scale(1)'; + } else { addBtn.style.opacity = '0.7'; addBtn.style.transform = 'scale(0.95)'; } @@ -6981,3 +7022,693 @@ async function updateRegistrationSettings() { showToast('更新注册设置失败', 'danger'); } } + +// ================================ +// 订单管理功能 +// ================================ + +// 加载订单列表 +async function loadOrders() { + try { + // 先加载Cookie列表用于筛选 + await loadOrderCookieFilter(); + + // 加载订单列表 + await refreshOrdersData(); + } catch (error) { + console.error('加载订单列表失败:', error); + showToast('加载订单列表失败', 'danger'); + } +} + +// 只刷新订单数据,不重新加载筛选器 +async function refreshOrdersData() { + try { + const selectedCookie = document.getElementById('orderCookieFilter').value; + if (selectedCookie) { + await loadOrdersByCookie(); + } else { + await loadAllOrders(); + } + } catch (error) { + console.error('刷新订单数据失败:', error); + showToast('刷新订单数据失败', 'danger'); + } +} + +// 加载Cookie筛选选项 +async function loadOrderCookieFilter() { + try { + const response = await fetch(`${apiBase}/admin/data/orders`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + const data = await response.json(); + if (data.success && data.data) { + // 提取唯一的cookie_id + const cookieIds = [...new Set(data.data.map(order => order.cookie_id).filter(id => id))]; + + const select = document.getElementById('orderCookieFilter'); + if (select) { + select.innerHTML = ''; + + cookieIds.forEach(cookieId => { + const option = document.createElement('option'); + option.value = cookieId; + option.textContent = cookieId; + select.appendChild(option); + }); + } + } + } catch (error) { + console.error('加载Cookie选项失败:', error); + } +} + +// 加载所有订单 +async function loadAllOrders() { + try { + const response = await fetch(`${apiBase}/admin/data/orders`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + const data = await response.json(); + if (data.success) { + allOrdersData = data.data || []; + // 按创建时间倒序排列 + allOrdersData.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + // 应用当前筛选条件 + filterOrders(); + } else { + console.error('加载订单失败:', data.message); + showToast('加载订单数据失败: ' + data.message, 'danger'); + } + } catch (error) { + console.error('加载订单失败:', error); + showToast('加载订单数据失败,请检查网络连接', 'danger'); + } +} + +// 根据Cookie加载订单 +async function loadOrdersByCookie() { + const selectedCookie = document.getElementById('orderCookieFilter').value; + if (!selectedCookie) { + await loadAllOrders(); + return; + } + + try { + const response = await fetch(`${apiBase}/admin/data/orders`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + const data = await response.json(); + if (data.success) { + // 筛选指定Cookie的订单 + allOrdersData = (data.data || []).filter(order => order.cookie_id === selectedCookie); + // 按创建时间倒序排列 + allOrdersData.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + // 应用当前筛选条件 + filterOrders(); + } else { + console.error('加载订单失败:', data.message); + showToast('加载订单数据失败: ' + data.message, 'danger'); + } + } catch (error) { + console.error('加载订单失败:', error); + showToast('加载订单数据失败,请检查网络连接', 'danger'); + } +} + +// 筛选订单 +function filterOrders() { + const searchKeyword = document.getElementById('orderSearchInput')?.value.toLowerCase() || ''; + const statusFilter = document.getElementById('orderStatusFilter')?.value || ''; + + filteredOrdersData = allOrdersData.filter(order => { + // 搜索关键词筛选(订单ID或商品ID) + const matchesSearch = !searchKeyword || + (order.order_id && order.order_id.toLowerCase().includes(searchKeyword)) || + (order.item_id && order.item_id.toLowerCase().includes(searchKeyword)); + + // 状态筛选 + const matchesStatus = !statusFilter || order.order_status === statusFilter; + + return matchesSearch && matchesStatus; + }); + + currentOrderSearchKeyword = searchKeyword; + currentOrdersPage = 1; // 重置到第一页 + + updateOrdersDisplay(); +} + +// 更新订单显示 +function updateOrdersDisplay() { + displayOrders(); + updateOrdersPagination(); + updateOrdersSearchStats(); +} + +// 显示订单列表 +function displayOrders() { + const tbody = document.getElementById('ordersTableBody'); + if (!tbody) return; + + if (filteredOrdersData.length === 0) { + tbody.innerHTML = ` + + + + ${currentOrderSearchKeyword ? '没有找到匹配的订单' : '暂无订单数据'} + + + `; + return; + } + + // 计算分页 + totalOrdersPages = Math.ceil(filteredOrdersData.length / ordersPerPage); + const startIndex = (currentOrdersPage - 1) * ordersPerPage; + const endIndex = startIndex + ordersPerPage; + const pageOrders = filteredOrdersData.slice(startIndex, endIndex); + + // 生成表格行 + tbody.innerHTML = pageOrders.map(order => createOrderRow(order)).join(''); +} + +// 创建订单行HTML +function createOrderRow(order) { + const statusClass = getOrderStatusClass(order.order_status); + const statusText = getOrderStatusText(order.order_status); + + return ` + + + + + + + ${order.order_id} + + + + + ${order.item_id || '-'} + + + + + ${order.buyer_id || '-'} + + + + ${order.spec_name && order.spec_value ? + `${order.spec_name}:
${order.spec_value}` : + '-' + } + + ${order.quantity || '-'} + + ¥${order.amount || '0.00'} + + + ${statusText} + + + + ${order.cookie_id || '-'} + + + +
+ + +
+ + + `; +} + +// 获取订单状态样式类 +function getOrderStatusClass(status) { + const statusMap = { + 'processing': 'bg-warning text-dark', + 'processed': 'bg-info text-white', + 'completed': 'bg-success text-white', + 'unknown': 'bg-secondary text-white' + }; + return statusMap[status] || 'bg-secondary text-white'; +} + +// 获取订单状态文本 +function getOrderStatusText(status) { + const statusMap = { + 'processing': '处理中', + 'processed': '已处理', + 'completed': '已完成', + 'unknown': '未知' + }; + return statusMap[status] || '未知'; +} + +// 更新订单分页 +function updateOrdersPagination() { + const pageInfo = document.getElementById('ordersPageInfo'); + const pageInput = document.getElementById('ordersPageInput'); + const totalPagesSpan = document.getElementById('ordersTotalPages'); + + if (pageInfo) { + const startIndex = (currentOrdersPage - 1) * ordersPerPage + 1; + const endIndex = Math.min(currentOrdersPage * ordersPerPage, filteredOrdersData.length); + pageInfo.textContent = `显示第 ${startIndex}-${endIndex} 条,共 ${filteredOrdersData.length} 条记录`; + } + + if (pageInput) { + pageInput.value = currentOrdersPage; + } + + if (totalPagesSpan) { + totalPagesSpan.textContent = totalOrdersPages; + } + + // 更新分页按钮状态 + const firstPageBtn = document.getElementById('ordersFirstPage'); + const prevPageBtn = document.getElementById('ordersPrevPage'); + const nextPageBtn = document.getElementById('ordersNextPage'); + const lastPageBtn = document.getElementById('ordersLastPage'); + + if (firstPageBtn) firstPageBtn.disabled = currentOrdersPage === 1; + if (prevPageBtn) prevPageBtn.disabled = currentOrdersPage === 1; + if (nextPageBtn) nextPageBtn.disabled = currentOrdersPage === totalOrdersPages || totalOrdersPages === 0; + if (lastPageBtn) lastPageBtn.disabled = currentOrdersPage === totalOrdersPages || totalOrdersPages === 0; +} + +// 更新搜索统计信息 +function updateOrdersSearchStats() { + const searchStats = document.getElementById('orderSearchStats'); + const searchStatsText = document.getElementById('orderSearchStatsText'); + + if (searchStats && searchStatsText) { + if (currentOrderSearchKeyword) { + searchStatsText.textContent = `搜索 "${currentOrderSearchKeyword}" 找到 ${filteredOrdersData.length} 个结果`; + searchStats.style.display = 'block'; + } else { + searchStats.style.display = 'none'; + } + } +} + +// 跳转到指定页面 +function goToOrdersPage(page) { + if (page < 1 || page > totalOrdersPages) return; + + currentOrdersPage = page; + updateOrdersDisplay(); +} + +// 初始化订单搜索功能 +function initOrdersSearch() { + // 初始化分页大小 + const pageSizeSelect = document.getElementById('ordersPageSize'); + if (pageSizeSelect) { + ordersPerPage = parseInt(pageSizeSelect.value) || 20; + pageSizeSelect.addEventListener('change', changeOrdersPageSize); + } + + // 初始化搜索输入框事件监听器 + const searchInput = document.getElementById('orderSearchInput'); + if (searchInput) { + // 使用防抖来避免频繁搜索 + let searchTimeout; + searchInput.addEventListener('input', function() { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + filterOrders(); + }, 300); // 300ms 防抖延迟 + }); + } + + // 初始化页面输入框事件监听器 + const pageInput = document.getElementById('ordersPageInput'); + if (pageInput) { + pageInput.addEventListener('keydown', handleOrdersPageInput); + } +} + +// 处理分页大小变化 +function changeOrdersPageSize() { + const pageSizeSelect = document.getElementById('ordersPageSize'); + if (pageSizeSelect) { + ordersPerPage = parseInt(pageSizeSelect.value) || 20; + currentOrdersPage = 1; // 重置到第一页 + updateOrdersDisplay(); + } +} + +// 处理页面输入 +function handleOrdersPageInput(event) { + if (event.key === 'Enter') { + const pageInput = document.getElementById('ordersPageInput'); + if (pageInput) { + const page = parseInt(pageInput.value); + if (page >= 1 && page <= totalOrdersPages) { + goToOrdersPage(page); + } else { + pageInput.value = currentOrdersPage; // 恢复当前页码 + showToast('页码超出范围', 'warning'); + } + } + } +} + +// 刷新订单列表 +async function refreshOrders() { + await refreshOrdersData(); + showToast('订单列表已刷新', 'success'); +} + +// 清空订单筛选条件 +function clearOrderFilters() { + const searchInput = document.getElementById('orderSearchInput'); + const statusFilter = document.getElementById('orderStatusFilter'); + const cookieFilter = document.getElementById('orderCookieFilter'); + + if (searchInput) searchInput.value = ''; + if (statusFilter) statusFilter.value = ''; + if (cookieFilter) cookieFilter.value = ''; + + filterOrders(); + showToast('筛选条件已清空', 'info'); +} + +// 显示订单详情 +async function showOrderDetail(orderId) { + try { + const order = allOrdersData.find(o => o.order_id === orderId); + if (!order) { + showToast('订单不存在', 'warning'); + return; + } + + // 创建模态框内容 + const modalContent = ` + + `; + + // 移除已存在的模态框 + const existingModal = document.getElementById('orderDetailModal'); + if (existingModal) { + existingModal.remove(); + } + + // 添加新模态框到页面 + document.body.insertAdjacentHTML('beforeend', modalContent); + + // 显示模态框 + const modal = new bootstrap.Modal(document.getElementById('orderDetailModal')); + modal.show(); + + // 异步加载商品详情 + if (order.item_id) { + loadItemDetailForOrder(order.item_id, order.cookie_id); + } + + } catch (error) { + console.error('显示订单详情失败:', error); + showToast('显示订单详情失败', 'danger'); + } +} + +// 为订单加载商品详情 +async function loadItemDetailForOrder(itemId, cookieId) { + try { + const token = localStorage.getItem('auth_token'); + + // 尝试从数据库获取商品信息 + let response = await fetch(`${apiBase}/items/${cookieId}/${itemId}`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + const content = document.getElementById('itemDetailContent'); + if (!content) return; + + if (response.ok) { + const data = await response.json(); + const item = data.item; + + content.innerHTML = ` +
+
+
${item.item_title || '商品标题未知'}
+

${item.item_description || '暂无描述'}

+
+
+ 分类:${item.item_category || '未知'} +
+
+ 价格:${item.item_price || '未知'} +
+
+ ${item.item_detail ? ` +
+ 详情: +
+ ${item.item_detail} +
+
+ ` : ''} +
+
+ `; + } else { + content.innerHTML = ` +
+ + 无法获取商品详情信息 +
+ `; + } + } catch (error) { + console.error('加载商品详情失败:', error); + const content = document.getElementById('itemDetailContent'); + if (content) { + content.innerHTML = ` +
+ + 加载商品详情失败:${error.message} +
+ `; + } + } +} + +// 删除订单 +async function deleteOrder(orderId) { + try { + const confirmed = confirm(`确定要删除订单吗?\n\n订单ID: ${orderId}\n\n此操作不可撤销!`); + if (!confirmed) { + return; + } + + const response = await fetch(`${apiBase}/admin/data/orders/delete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ record_id: orderId }) + }); + + if (response.ok) { + showToast('订单删除成功', 'success'); + // 刷新列表 + await refreshOrdersData(); + } else { + const error = await response.text(); + showToast(`删除失败: ${error}`, 'danger'); + } + } catch (error) { + console.error('删除订单失败:', error); + showToast('删除订单失败', 'danger'); + } +} + +// 批量删除订单 +async function batchDeleteOrders() { + const checkboxes = document.querySelectorAll('.order-checkbox:checked'); + if (checkboxes.length === 0) { + showToast('请先选择要删除的订单', 'warning'); + return; + } + + const orderIds = Array.from(checkboxes).map(cb => cb.value); + const confirmed = confirm(`确定要删除选中的 ${orderIds.length} 个订单吗?\n\n此操作不可撤销!`); + + if (!confirmed) return; + + try { + let successCount = 0; + let failCount = 0; + + for (const orderId of orderIds) { + try { + const response = await fetch(`${apiBase}/admin/data/orders/delete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ record_id: orderId }) + }); + + if (response.ok) { + successCount++; + } else { + failCount++; + } + } catch (error) { + failCount++; + } + } + + if (successCount > 0) { + showToast(`成功删除 ${successCount} 个订单${failCount > 0 ? `,${failCount} 个失败` : ''}`, + failCount > 0 ? 'warning' : 'success'); + await refreshOrdersData(); + } else { + showToast('批量删除失败', 'danger'); + } + + } catch (error) { + console.error('批量删除订单失败:', error); + showToast('批量删除订单失败', 'danger'); + } +} + +// 切换全选订单 +function toggleSelectAllOrders(checkbox) { + const orderCheckboxes = document.querySelectorAll('.order-checkbox'); + orderCheckboxes.forEach(cb => { + cb.checked = checkbox.checked; + }); + + updateBatchDeleteOrdersButton(); +} + +// 更新批量删除按钮状态 +function updateBatchDeleteOrdersButton() { + const checkboxes = document.querySelectorAll('.order-checkbox:checked'); + const batchDeleteBtn = document.getElementById('batchDeleteOrdersBtn'); + + if (batchDeleteBtn) { + batchDeleteBtn.disabled = checkboxes.length === 0; + } +} + +// 格式化日期时间 +function formatDateTime(dateString) { + if (!dateString) return '未知时间'; + + try { + const date = new Date(dateString); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } catch (error) { + return dateString; + } +} + +// 页面加载完成后初始化订单搜索功能 +document.addEventListener('DOMContentLoaded', function() { + // 延迟初始化,确保DOM完全加载 + setTimeout(() => { + initOrdersSearch(); + + // 绑定复选框变化事件 + document.addEventListener('change', function(e) { + if (e.target.classList.contains('order-checkbox')) { + updateBatchDeleteOrdersButton(); + } + }); + }, 100); +}); diff --git a/utils/order_detail_fetcher.py b/utils/order_detail_fetcher.py index 52311fa..b7f0164 100644 --- a/utils/order_detail_fetcher.py +++ b/utils/order_detail_fetcher.py @@ -10,6 +10,10 @@ import os from typing import Optional, Dict, Any from playwright.async_api import async_playwright, Browser, BrowserContext, Page from loguru import logger +import re +import json +from threading import Lock +from collections import defaultdict # 修复Docker环境中的asyncio事件循环策略问题 if sys.platform.startswith('linux') or os.getenv('DOCKER_ENV'): @@ -33,10 +37,14 @@ if os.getenv('DOCKER_ENV'): class OrderDetailFetcher: """闲鱼订单详情获取器""" - def __init__(self, cookie_string: str = None): + # 类级别的锁字典,为每个order_id维护一个锁 + _order_locks = defaultdict(lambda: asyncio.Lock()) + + def __init__(self, cookie_string: str = None, headless: bool = True): self.browser: Optional[Browser] = None self.context: Optional[BrowserContext] = None self.page: Optional[Page] = None + self.headless = headless # 保存headless设置 # 请求头配置 self.headers = { @@ -58,11 +66,17 @@ class OrderDetailFetcher: # Cookie配置 - 支持动态传入 self.cookie = cookie_string - async def init_browser(self, headless: bool = True): + async def init_browser(self, headless: bool = None): """初始化浏览器""" try: + # 如果没有传入headless参数,使用实例的设置 + if headless is None: + headless = self.headless + + logger.info(f"开始初始化浏览器,headless模式: {headless}") + playwright = await async_playwright().start() - + # 启动浏览器(Docker环境优化) browser_args = [ '--no-sandbox', @@ -84,10 +98,13 @@ class OrderDetailFetcher: '--hide-scrollbars', '--mute-audio', '--no-default-browser-check', - '--no-pings', - '--single-process' # 在Docker中使用单进程模式 + '--no-pings' ] + # 只在Docker环境中使用单进程模式 + if os.getenv('DOCKER_ENV'): + browser_args.append('--single-process') + # 在Docker环境中添加额外参数 if os.getenv('DOCKER_ENV'): browser_args.extend([ @@ -108,26 +125,38 @@ class OrderDetailFetcher: '--use-mock-keychain' ]) + logger.info(f"启动浏览器,参数: {browser_args}") self.browser = await playwright.chromium.launch( headless=headless, args=browser_args ) - + + logger.info("浏览器启动成功,创建上下文...") + # 创建浏览器上下文 self.context = await self.browser.new_context( viewport={'width': 1920, 'height': 1080}, user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' ) - + + logger.info("浏览器上下文创建成功,设置HTTP头...") + # 设置额外的HTTP头 await self.context.set_extra_http_headers(self.headers) - + + logger.info("创建页面...") + # 创建页面 self.page = await self.context.new_page() - + + logger.info("页面创建成功,设置Cookie...") + # 设置Cookie await self._set_cookies() - + + # 等待一段时间确保浏览器完全初始化 + await asyncio.sleep(1) + logger.info("浏览器初始化成功") return True @@ -159,63 +188,160 @@ class OrderDetailFetcher: async def fetch_order_detail(self, order_id: str, timeout: int = 30) -> Optional[Dict[str, Any]]: """ - 获取订单详情 - + 获取订单详情(带锁机制和数据库缓存) + Args: order_id: 订单ID timeout: 超时时间(秒) - + Returns: 包含订单详情的字典,失败时返回None """ - try: - if not self.page: - logger.error("浏览器未初始化") + # 获取该订单ID的锁 + order_lock = self._order_locks[order_id] + + async with order_lock: + logger.info(f"🔒 获取订单 {order_id} 的锁,开始处理...") + + try: + # 首先查询数据库中是否已存在该订单(在初始化浏览器之前) + from db_manager import db_manager + existing_order = db_manager.get_order_by_id(order_id) + + if existing_order: + # 检查金额字段是否有效(不为空且不为0) + amount = existing_order.get('amount', '') + amount_valid = False + + if amount: + # 移除可能的货币符号和空格,检查是否为有效数字 + amount_clean = str(amount).replace('¥', '').replace('¥', '').replace('$', '').strip() + try: + amount_value = float(amount_clean) + amount_valid = amount_value > 0 + except (ValueError, TypeError): + amount_valid = False + + if amount_valid: + logger.info(f"📋 订单 {order_id} 已存在于数据库中且金额有效({amount}),直接返回缓存数据") + print(f"✅ 订单 {order_id} 使用缓存数据,跳过浏览器获取") + + # 构建返回格式,与浏览器获取的格式保持一致 + result = { + 'order_id': existing_order['order_id'], + 'url': f"https://www.goofish.com/order-detail?orderId={order_id}&role=seller", + 'title': f"订单详情 - {order_id}", + 'sku_info': { + 'spec_name': existing_order.get('spec_name', ''), + 'spec_value': existing_order.get('spec_value', ''), + 'quantity': existing_order.get('quantity', ''), + 'amount': existing_order.get('amount', '') + }, + 'spec_name': existing_order.get('spec_name', ''), + 'spec_value': existing_order.get('spec_value', ''), + 'quantity': existing_order.get('quantity', ''), + 'amount': existing_order.get('amount', ''), + 'timestamp': time.time(), + 'from_cache': True # 标记数据来源 + } + return result + else: + logger.info(f"📋 订单 {order_id} 存在于数据库中但金额无效({amount}),需要重新获取") + print(f"⚠️ 订单 {order_id} 金额无效,重新获取详情...") + + # 只有在数据库中没有有效数据时才初始化浏览器 + logger.info(f"🌐 订单 {order_id} 需要浏览器获取,开始初始化浏览器...") + print(f"🔍 订单 {order_id} 开始浏览器获取详情...") + + # 确保浏览器准备就绪 + if not await self._ensure_browser_ready(): + logger.error("浏览器初始化失败,无法获取订单详情") + return None + + # 构建订单详情URL + url = f"https://www.goofish.com/order-detail?orderId={order_id}&role=seller" + logger.info(f"开始访问订单详情页面: {url}") + + # 访问页面(带重试机制) + max_retries = 2 + response = None + + for retry in range(max_retries + 1): + try: + response = await self.page.goto(url, wait_until='networkidle', timeout=timeout * 1000) + + if response and response.status == 200: + break + else: + logger.warning(f"页面访问失败,状态码: {response.status if response else 'None'},重试 {retry + 1}/{max_retries + 1}") + + except Exception as e: + logger.warning(f"页面访问异常: {e},重试 {retry + 1}/{max_retries + 1}") + + # 如果是浏览器连接问题,尝试重新初始化 + if "Target page, context or browser has been closed" in str(e): + logger.info("检测到浏览器连接断开,尝试重新初始化...") + if await self._ensure_browser_ready(): + logger.info("浏览器重新初始化成功,继续重试...") + continue + else: + logger.error("浏览器重新初始化失败") + return None + + if retry == max_retries: + logger.error(f"页面访问最终失败: {e}") + return None + + await asyncio.sleep(1) # 重试前等待1秒 + + if not response or response.status != 200: + logger.error(f"页面访问最终失败,状态码: {response.status if response else 'None'}") + return None + + logger.info("页面加载成功,等待内容渲染...") + + # 等待页面完全加载 + try: + await self.page.wait_for_load_state('networkidle') + except Exception as e: + logger.warning(f"等待页面加载状态失败: {e}") + # 继续执行,不中断流程 + + # 额外等待确保动态内容加载完成 + await asyncio.sleep(3) + + # 获取并解析SKU信息 + sku_info = await self._get_sku_content() + + # 获取页面标题 + try: + title = await self.page.title() + except Exception as e: + logger.warning(f"获取页面标题失败: {e}") + title = f"订单详情 - {order_id}" + + result = { + 'order_id': order_id, + 'url': url, + 'title': title, + 'sku_info': sku_info, # 包含解析后的规格信息 + 'spec_name': sku_info.get('spec_name', '') if sku_info else '', + 'spec_value': sku_info.get('spec_value', '') if sku_info else '', + 'quantity': sku_info.get('quantity', '') if sku_info else '', # 数量 + 'amount': sku_info.get('amount', '') if sku_info else '', # 金额 + 'timestamp': time.time(), + 'from_cache': False # 标记数据来源 + } + + logger.info(f"订单详情获取成功: {order_id}") + if sku_info: + logger.info(f"规格信息 - 名称: {result['spec_name']}, 值: {result['spec_value']}") + logger.info(f"数量: {result['quantity']}, 金额: {result['amount']}") + return result + + except Exception as e: + logger.error(f"获取订单详情失败: {e}") return None - - # 构建订单详情URL - url = f"https://www.goofish.com/order-detail?orderId={order_id}&role=seller" - logger.info(f"开始访问订单详情页面: {url}") - - # 访问页面 - response = await self.page.goto(url, wait_until='networkidle', timeout=timeout * 1000) - - if not response or response.status != 200: - logger.error(f"页面访问失败,状态码: {response.status if response else 'None'}") - return None - - logger.info("页面加载成功,等待内容渲染...") - - # 等待页面完全加载 - await self.page.wait_for_load_state('networkidle') - - # 额外等待确保动态内容加载完成 - await asyncio.sleep(3) - - # 获取并解析SKU信息 - sku_info = await self._get_sku_content() - - # 获取页面标题 - title = await self.page.title() - - result = { - 'order_id': order_id, - 'url': url, - 'title': title, - 'sku_info': sku_info, # 包含解析后的规格信息 - 'spec_name': sku_info.get('spec_name', '') if sku_info else '', - 'spec_value': sku_info.get('spec_value', '') if sku_info else '', - 'timestamp': time.time() - } - - logger.info(f"订单详情获取成功: {order_id}") - if sku_info: - logger.info(f"规格信息 - 名称: {result['spec_name']}, 值: {result['spec_value']}") - return result - - except Exception as e: - logger.error(f"获取订单详情失败: {e}") - return None def _parse_sku_content(self, sku_content: str) -> Dict[str, str]: """ @@ -258,38 +384,121 @@ class OrderDetailFetcher: return {} async def _get_sku_content(self) -> Optional[Dict[str, str]]: - """获取并解析SKU内容""" + """获取并解析SKU内容,包括规格、数量和金额""" try: - # 等待SKU元素出现 + # 检查浏览器状态 + if not await self._check_browser_status(): + logger.error("浏览器状态异常,无法获取SKU内容") + return {} + + result = {} + + # 获取所有 sku--u_ddZval 元素 sku_selector = '.sku--u_ddZval' + sku_elements = await self.page.query_selector_all(sku_selector) - # 检查元素是否存在 - sku_element = await self.page.query_selector(sku_selector) + logger.info(f"找到 {len(sku_elements)} 个 sku--u_ddZval 元素") + print(f"🔍 找到 {len(sku_elements)} 个 sku--u_ddZval 元素") - if sku_element: - # 获取元素文本内容 - sku_content = await sku_element.text_content() - if sku_content: - sku_content = sku_content.strip() - logger.info(f"找到SKU原始内容: {sku_content}") - print(f"🛍️ SKU原始内容: {sku_content}") - - # 解析SKU内容 - parsed_sku = self._parse_sku_content(sku_content) - if parsed_sku: - print(f"📋 规格名称: {parsed_sku['spec_name']}") - print(f"📝 规格值: {parsed_sku['spec_value']}") - return parsed_sku - else: - logger.warning("SKU内容解析失败") - return {} - else: - logger.warning("SKU元素内容为空") - return {} + # 获取金额信息 + amount_selector = '.boldNum--JgEOXfA3' + amount_element = await self.page.query_selector(amount_selector) + amount = '' + if amount_element: + amount_text = await amount_element.text_content() + if amount_text: + amount = amount_text.strip() + logger.info(f"找到金额: {amount}") + print(f"💰 金额: {amount}") + result['amount'] = amount else: - logger.warning("未找到SKU元素") + logger.warning("未找到金额元素") + print("⚠️ 未找到金额信息") - # 尝试获取页面的所有class包含sku的元素 + # 处理 sku--u_ddZval 元素 + if len(sku_elements) == 2: + # 有两个元素:第一个是规格,第二个是数量 + logger.info("检测到两个 sku--u_ddZval 元素,第一个为规格,第二个为数量") + print("📋 检测到两个元素:第一个为规格,第二个为数量") + + # 处理规格(第一个元素) + spec_content = await sku_elements[0].text_content() + if spec_content: + spec_content = spec_content.strip() + logger.info(f"规格原始内容: {spec_content}") + print(f"🛍️ 规格原始内容: {spec_content}") + + # 解析规格内容 + parsed_spec = self._parse_sku_content(spec_content) + if parsed_spec: + result.update(parsed_spec) + print(f"📋 规格名称: {parsed_spec['spec_name']}") + print(f"📝 规格值: {parsed_spec['spec_value']}") + + # 处理数量(第二个元素) + quantity_content = await sku_elements[1].text_content() + if quantity_content: + quantity_content = quantity_content.strip() + logger.info(f"数量原始内容: {quantity_content}") + print(f"📦 数量原始内容: {quantity_content}") + + # 从数量内容中提取数量值(使用冒号分割,取后面的值) + if ':' in quantity_content: + quantity_value = quantity_content.split(':', 1)[1].strip() + result['quantity'] = quantity_value + logger.info(f"提取到数量: {quantity_value}") + print(f"🔢 数量: {quantity_value}") + else: + result['quantity'] = quantity_content + logger.info(f"数量内容无冒号,直接使用: {quantity_content}") + print(f"🔢 数量: {quantity_content}") + + elif len(sku_elements) == 1: + # 只有一个元素:判断是否包含"数量" + logger.info("检测到一个 sku--u_ddZval 元素,判断是规格还是数量") + print("📋 检测到一个元素,判断是规格还是数量") + + content = await sku_elements[0].text_content() + if content: + content = content.strip() + logger.info(f"元素原始内容: {content}") + print(f"🛍️ 元素原始内容: {content}") + + if '数量' in content: + # 这是数量信息 + logger.info("判断为数量信息") + print("📦 判断为数量信息") + + if ':' in content: + quantity_value = content.split(':', 1)[1].strip() + result['quantity'] = quantity_value + logger.info(f"提取到数量: {quantity_value}") + print(f"🔢 数量: {quantity_value}") + else: + result['quantity'] = content + logger.info(f"数量内容无冒号,直接使用: {content}") + print(f"🔢 数量: {content}") + else: + # 这是规格信息 + logger.info("判断为规格信息") + print("📋 判断为规格信息") + + parsed_spec = self._parse_sku_content(content) + if parsed_spec: + result.update(parsed_spec) + print(f"📋 规格名称: {parsed_spec['spec_name']}") + print(f"📝 规格值: {parsed_spec['spec_value']}") + else: + logger.warning(f"未找到或找到异常数量的 sku--u_ddZval 元素: {len(sku_elements)}") + print(f"⚠️ 未找到或找到异常数量的元素: {len(sku_elements)}") + + # 如果没有找到sku--u_ddZval元素,设置默认数量为0 + if len(sku_elements) == 0: + result['quantity'] = '0' + logger.info("未找到sku--u_ddZval元素,数量默认设置为0") + print("📦 数量默认设置为: 0") + + # 尝试获取页面的所有class包含sku的元素进行调试 all_sku_elements = await self.page.query_selector_all('[class*="sku"]') if all_sku_elements: logger.info(f"找到 {len(all_sku_elements)} 个包含'sku'的元素") @@ -298,12 +507,104 @@ class OrderDetailFetcher: text_content = await element.text_content() logger.info(f"SKU元素 {i+1}: class='{class_name}', text='{text_content}'") - return {} + # 确保数量字段存在,如果不存在则设置为0 + if 'quantity' not in result: + result['quantity'] = '0' + logger.info("未获取到数量信息,默认设置为0") + print("📦 数量默认设置为: 0") + + # 打印最终结果 + if result: + logger.info(f"最终解析结果: {result}") + print("✅ 解析结果:") + for key, value in result.items(): + print(f" {key}: {value}") + return result + else: + logger.warning("未能解析到任何有效信息") + print("❌ 未能解析到任何有效信息") + # 即使没有其他信息,也要返回默认数量 + return {'quantity': '0'} except Exception as e: logger.error(f"获取SKU内容失败: {e}") return {} + async def _check_browser_status(self) -> bool: + """检查浏览器状态是否正常""" + try: + if not self.browser or not self.context or not self.page: + logger.warning("浏览器组件不完整") + return False + + # 检查浏览器是否已连接 + if self.browser.is_connected(): + # 尝试获取页面标题来验证页面是否可用 + await self.page.title() + return True + else: + logger.warning("浏览器连接已断开") + return False + except Exception as e: + logger.warning(f"浏览器状态检查失败: {e}") + return False + + async def _ensure_browser_ready(self) -> bool: + """确保浏览器准备就绪,如果不可用则重新初始化""" + try: + if await self._check_browser_status(): + return True + + logger.info("浏览器状态异常,尝试重新初始化...") + + # 先尝试关闭现有的浏览器实例 + await self._force_close_browser() + + # 重新初始化浏览器 + await self.init_browser() + + # 等待更长时间确保浏览器完全就绪 + await asyncio.sleep(2) + + # 再次检查状态 + if await self._check_browser_status(): + logger.info("浏览器重新初始化成功") + return True + else: + logger.error("浏览器重新初始化失败") + return False + + except Exception as e: + logger.error(f"确保浏览器就绪失败: {e}") + return False + + async def _force_close_browser(self): + """强制关闭浏览器,忽略所有错误""" + try: + if self.page: + try: + await self.page.close() + except: + pass + self.page = None + + if self.context: + try: + await self.context.close() + except: + pass + self.context = None + + if self.browser: + try: + await self.browser.close() + except: + pass + self.browser = None + + except Exception as e: + logger.debug(f"强制关闭浏览器过程中的异常(可忽略): {e}") + async def close(self): """关闭浏览器""" try: @@ -316,6 +617,8 @@ class OrderDetailFetcher: logger.info("浏览器已关闭") except Exception as e: logger.error(f"关闭浏览器失败: {e}") + # 如果正常关闭失败,尝试强制关闭 + await self._force_close_browser() async def __aenter__(self): """异步上下文管理器入口""" @@ -330,7 +633,7 @@ class OrderDetailFetcher: # 便捷函数 async def fetch_order_detail_simple(order_id: str, cookie_string: str = None, headless: bool = True) -> Optional[Dict[str, Any]]: """ - 简单的订单详情获取函数 + 简单的订单详情获取函数(优化版:先检查数据库,再初始化浏览器) Args: order_id: 订单ID @@ -338,9 +641,71 @@ async def fetch_order_detail_simple(order_id: str, cookie_string: str = None, he headless: 是否无头模式 Returns: - 订单详情字典或None + 订单详情字典,包含以下字段: + - order_id: 订单ID + - url: 订单详情页面URL + - title: 页面标题 + - sku_info: 完整的SKU信息字典 + - spec_name: 规格名称 + - spec_value: 规格值 + - quantity: 数量 + - amount: 金额 + - timestamp: 获取时间戳 + 失败时返回None """ - fetcher = OrderDetailFetcher(cookie_string) + # 先检查数据库中是否有有效数据 + try: + from db_manager import db_manager + existing_order = db_manager.get_order_by_id(order_id) + + if existing_order: + # 检查金额字段是否有效 + amount = existing_order.get('amount', '') + amount_valid = False + + if amount: + amount_clean = str(amount).replace('¥', '').replace('¥', '').replace('$', '').strip() + try: + amount_value = float(amount_clean) + amount_valid = amount_value > 0 + except (ValueError, TypeError): + amount_valid = False + + if amount_valid: + logger.info(f"📋 订单 {order_id} 已存在于数据库中且金额有效({amount}),直接返回缓存数据") + print(f"✅ 订单 {order_id} 使用缓存数据,跳过浏览器获取") + + # 构建返回格式 + result = { + 'order_id': existing_order['order_id'], + 'url': f"https://www.goofish.com/order-detail?orderId={order_id}&role=seller", + 'title': f"订单详情 - {order_id}", + 'sku_info': { + 'spec_name': existing_order.get('spec_name', ''), + 'spec_value': existing_order.get('spec_value', ''), + 'quantity': existing_order.get('quantity', ''), + 'amount': existing_order.get('amount', '') + }, + 'spec_name': existing_order.get('spec_name', ''), + 'spec_value': existing_order.get('spec_value', ''), + 'quantity': existing_order.get('quantity', ''), + 'amount': existing_order.get('amount', ''), + 'order_status': existing_order.get('order_status', 'unknown'), # 添加订单状态 + 'timestamp': time.time(), + 'from_cache': True + } + return result + else: + logger.info(f"📋 订单 {order_id} 存在于数据库中但金额无效({amount}),需要重新获取") + print(f"⚠️ 订单 {order_id} 金额无效,重新获取详情...") + except Exception as e: + logger.warning(f"检查数据库缓存失败: {e}") + + # 数据库中没有有效数据,使用浏览器获取 + logger.info(f"🌐 订单 {order_id} 需要浏览器获取,开始初始化浏览器...") + print(f"🔍 订单 {order_id} 开始浏览器获取详情...") + + fetcher = OrderDetailFetcher(cookie_string, headless) try: if await fetcher.init_browser(headless=headless): return await fetcher.fetch_order_detail(order_id) @@ -364,7 +729,10 @@ if __name__ == "__main__": print(f"📋 订单ID: {result['order_id']}") print(f"🌐 URL: {result['url']}") print(f"📄 页面标题: {result['title']}") - print(f"🛍️ SKU内容: {result['sku_content']}") + print(f"🛍️ 规格名称: {result.get('spec_name', '未获取到')}") + print(f"📝 规格值: {result.get('spec_value', '未获取到')}") + print(f"🔢 数量: {result.get('quantity', '未获取到')}") + print(f"💰 金额: {result.get('amount', '未获取到')}") else: print("❌ 订单详情获取失败")