mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-09-02 02:38:48 +08:00
Compare commits
3 Commits
383e841acc
...
a525675e73
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a525675e73 | ||
![]() |
5b2a991f41 | ||
![]() |
46f7066519 |
200
.gitignore
vendored
200
.gitignore
vendored
@ -105,3 +105,203 @@ keywords_sample.xlsx
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.7z
|
||||
|
||||
# ==================== 新增忽略项 ====================
|
||||
# Python 字节码和缓存
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# 分发/打包
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# 单元测试/覆盖率报告
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
Pipfile.lock
|
||||
|
||||
# PEP 582
|
||||
__pypackages__/
|
||||
|
||||
# Celery
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# ==================== 项目特定新增 ====================
|
||||
# 环境变量文件
|
||||
.env.example
|
||||
.env.*.example
|
||||
|
||||
# 数据库文件
|
||||
*.db-journal
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
|
||||
# 临时文件和缓存
|
||||
*.cache
|
||||
.cache/
|
||||
cache/
|
||||
|
||||
# 编辑器临时文件
|
||||
*.swp
|
||||
*.swo
|
||||
*.tmp
|
||||
*~
|
||||
.#*
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# 文档生成
|
||||
docs/_build/
|
||||
docs/build/
|
||||
|
||||
# 密钥和证书
|
||||
*.key
|
||||
*.pem
|
||||
*.crt
|
||||
*.cert
|
||||
*.p12
|
||||
*.pfx
|
||||
|
||||
# 配置文件备份
|
||||
*.conf.bak
|
||||
*.config.bak
|
||||
|
||||
# 运行时文件
|
||||
*.pid
|
||||
*.sock
|
||||
|
||||
# 调试文件
|
||||
debug.log
|
||||
*.debug
|
||||
|
||||
# 性能分析文件
|
||||
*.prof
|
||||
|
||||
# 本地开发文件
|
||||
local/
|
||||
.local/
|
||||
|
||||
# Docker相关
|
||||
.dockerignore.bak
|
||||
docker-compose.override.yml
|
||||
|
||||
# 版本控制
|
||||
.svn/
|
||||
.hg/
|
||||
.bzr/
|
||||
|
||||
# 包管理器
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# 前端构建
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
.nuxt/
|
||||
.vuepress/dist
|
||||
|
||||
# 移动端
|
||||
*.apk
|
||||
*.ipa
|
||||
*.app
|
||||
|
||||
# 数据文件
|
||||
*.csv.bak
|
||||
*.json.bak
|
||||
*.xml.bak
|
||||
|
||||
# 媒体文件缓存
|
||||
*.mp4.cache
|
||||
*.mp3.cache
|
||||
*.wav.cache
|
||||
*.avi.cache
|
||||
|
||||
# AI模型文件
|
||||
*.model
|
||||
*.weights
|
||||
*.h5
|
||||
*.pb
|
||||
|
||||
# 大文件
|
||||
*.iso
|
||||
*.dmg
|
||||
*.img
|
88
README.md
88
README.md
@ -25,12 +25,13 @@
|
||||
|
||||
### 🤖 智能回复系统
|
||||
- **关键词匹配** - 支持精确关键词匹配回复
|
||||
- **商品专用回复** - 支持为特定商品设置专用关键词回复
|
||||
- **指定商品回复** - 支持为特定商品设置专门的回复内容,优先级最高
|
||||
- **商品专用关键词** - 支持为特定商品设置专用关键词回复
|
||||
- **通用关键词** - 支持全局通用关键词,适用于所有商品
|
||||
- **批量导入导出** - 支持Excel格式的关键词批量导入导出
|
||||
- **AI智能回复** - 集成OpenAI API,支持上下文理解
|
||||
- **变量替换** - 回复内容支持动态变量(用户名、商品信息等)
|
||||
- **优先级策略** - 商品专用关键词 > 通用关键词 > AI回复
|
||||
- **变量替换** - 回复内容支持动态变量(用户名、商品信息、商品ID等)
|
||||
- **优先级策略** - 指定商品回复 > 商品专用关键词 > 通用关键词 > 默认回复 > AI回复
|
||||
|
||||
### 🚚 自动发货功能
|
||||
- **智能匹配** - 基于商品信息自动匹配发货规则
|
||||
@ -41,8 +42,9 @@
|
||||
- **防重复发货** - 智能防重复机制,避免重复发货
|
||||
- **多种发货方式** - 支持固定文字、批量数据、API调用、图片发货等方式
|
||||
- **图片发货** - 支持上传图片并自动发送给买家,图片自动上传到CDN
|
||||
- **自动确认发货** - 检测到付款后自动调用闲鱼API确认发货
|
||||
- **自动确认发货** - 检测到付款后自动调用闲鱼API确认发货,支持锁机制防并发
|
||||
- **防重复确认** - 智能防重复确认机制,避免重复API调用
|
||||
- **订单详情缓存** - 订单详情获取支持数据库缓存,大幅提升性能
|
||||
- **发货统计** - 完整的发货记录和统计功能
|
||||
|
||||
### 🛍️ 商品管理
|
||||
@ -358,51 +360,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连接核心,处理消息收发、自动回复、指定商品回复、自动发货、商品信息收集、AI回复
|
||||
- **`reply_server.py`** - FastAPI Web服务器,提供完整的管理界面和RESTful API接口,支持多用户系统、JWT认证、权限管理
|
||||
- **`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依赖包列表,精简版本无冗余依赖,按功能分类组织
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
|
@ -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 = ''
|
||||
|
||||
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 message_1 else {}
|
||||
logger.debug(f"【{self.cookie_id}】🔍 message['1']['6'] keys: {list(message_1_6.keys()) if message_1_6 else 'None'}")
|
||||
|
||||
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.get('1', {}).get('6', {}).get('3', {}).get('5', '')
|
||||
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)}")
|
||||
# 其他类型,跳过这种提取方式
|
||||
|
||||
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
|
||||
@ -962,12 +1010,36 @@ class XianyuLive:
|
||||
except Exception as e:
|
||||
logger.error(f"调试消息结构时发生错误: {self._safe_str(e)}")
|
||||
|
||||
async def get_default_reply(self, send_user_name: str, send_user_id: str, send_message: str, chat_id: str = None) -> str:
|
||||
"""获取默认回复内容,支持变量替换和只回复一次功能"""
|
||||
async def get_default_reply(self, send_user_name: str, send_user_id: str, send_message: str, chat_id: str, item_id: str = None) -> str:
|
||||
"""获取默认回复内容,支持指定商品回复、变量替换和只回复一次功能"""
|
||||
try:
|
||||
from db_manager import db_manager
|
||||
|
||||
# 获取当前账号的默认回复设置
|
||||
# 1. 优先检查指定商品回复
|
||||
if item_id:
|
||||
item_reply = db_manager.get_item_reply(self.cookie_id, item_id)
|
||||
if item_reply and item_reply.get('reply_content'):
|
||||
reply_content = item_reply['reply_content']
|
||||
logger.info(f"【{self.cookie_id}】使用指定商品回复: 商品ID={item_id}")
|
||||
|
||||
# 进行变量替换
|
||||
try:
|
||||
formatted_reply = reply_content.format(
|
||||
send_user_name=send_user_name,
|
||||
send_user_id=send_user_id,
|
||||
send_message=send_message,
|
||||
item_id=item_id
|
||||
)
|
||||
logger.info(f"【{self.cookie_id}】指定商品回复内容: {formatted_reply}")
|
||||
return formatted_reply
|
||||
except Exception as format_error:
|
||||
logger.error(f"指定商品回复变量替换失败: {self._safe_str(format_error)}")
|
||||
# 如果变量替换失败,返回原始内容
|
||||
return reply_content
|
||||
else:
|
||||
logger.debug(f"【{self.cookie_id}】商品ID {item_id} 没有配置指定回复,使用默认回复")
|
||||
|
||||
# 2. 获取当前账号的默认回复设置
|
||||
default_reply_settings = db_manager.get_default_reply(self.cookie_id)
|
||||
|
||||
if not default_reply_settings or not default_reply_settings.get('enabled', False):
|
||||
@ -982,9 +1054,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 +1111,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 +1146,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 +1336,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 +1356,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 +1420,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 +1439,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 +1902,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 +1962,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 +1990,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 +2001,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 +2033,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 +2144,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 +2244,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 +2255,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']})")
|
||||
|
||||
@ -2749,6 +2923,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,7 +2986,10 @@ 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
|
||||
# 如果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"
|
||||
@ -2856,6 +3089,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)}")
|
||||
|
||||
|
||||
|
||||
@ -2978,7 +3216,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回复
|
||||
@ -2987,13 +3229,16 @@ class XianyuLive:
|
||||
reply_source = 'AI' # 标记为AI回复
|
||||
else:
|
||||
# 3. 最后使用默认回复
|
||||
reply = await self.get_default_reply(send_user_name, send_user_id, send_message, chat_id)
|
||||
reply = await self.get_default_reply(send_user_name, send_user_id, send_message, chat_id, item_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:
|
||||
|
357
db_manager.py
357
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")
|
||||
@ -297,6 +315,18 @@ class DBManager:
|
||||
if "duplicate column name" not in str(e).lower():
|
||||
logger.warning(f"添加 reply_once 字段失败: {e}")
|
||||
|
||||
# 创建指定商品回复表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS item_replay (
|
||||
item_id TEXT NOT NULL PRIMARY KEY,
|
||||
cookie_id TEXT NOT NULL,
|
||||
reply_content TEXT NOT NULL ,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (item_id) REFERENCES cookies(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建默认回复记录表(记录已回复的chat_id)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS default_reply_records (
|
||||
@ -4017,6 +4047,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 +4198,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')
|
||||
@ -4104,6 +4267,198 @@ class DBManager:
|
||||
except Exception as e:
|
||||
logger.error(f"升级keywords表失败: {e}")
|
||||
raise
|
||||
def get_item_replay(self, item_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据商品ID获取商品回复信息,并返回统一格式
|
||||
|
||||
Args:
|
||||
item_id (str): 商品ID
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 商品回复信息字典(统一格式),找不到返回 None
|
||||
"""
|
||||
try:
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT reply_content FROM item_replay
|
||||
WHERE item_id = ?
|
||||
''', (item_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
(reply_content,) = row
|
||||
return {
|
||||
'reply_content': reply_content or ''
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取商品回复失败: {e}")
|
||||
return None
|
||||
|
||||
def get_item_reply(self, cookie_id: str, item_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定账号和商品的回复内容
|
||||
|
||||
Args:
|
||||
cookie_id (str): 账号ID
|
||||
item_id (str): 商品ID
|
||||
|
||||
Returns:
|
||||
Dict: 包含回复内容的字典,如果不存在返回None
|
||||
"""
|
||||
try:
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT reply_content, created_at, updated_at
|
||||
FROM item_replay
|
||||
WHERE cookie_id = ? AND item_id = ?
|
||||
''', (cookie_id, item_id))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {
|
||||
'reply_content': row[0] or '',
|
||||
'created_at': row[1],
|
||||
'updated_at': row[2]
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取指定商品回复失败: {e}")
|
||||
return None
|
||||
|
||||
def update_item_reply(self, cookie_id: str, item_id: str, reply_content: str) -> bool:
|
||||
"""
|
||||
更新指定cookie和item的回复内容及更新时间
|
||||
|
||||
Args:
|
||||
cookie_id (str): 账号ID
|
||||
item_id (str): 商品ID
|
||||
reply_content (str): 回复内容
|
||||
|
||||
Returns:
|
||||
bool: 更新成功返回True,失败返回False
|
||||
"""
|
||||
try:
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('''
|
||||
UPDATE item_replay
|
||||
SET reply_content = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE cookie_id = ? AND item_id = ?
|
||||
''', (reply_content, cookie_id, item_id))
|
||||
|
||||
if cursor.rowcount == 0:
|
||||
# 如果没更新到,说明该条记录不存在,可以考虑插入
|
||||
cursor.execute('''
|
||||
INSERT INTO item_replay (item_id, cookie_id, reply_content, created_at, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
''', (item_id, cookie_id, reply_content))
|
||||
|
||||
self.conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"更新商品回复失败: {e}")
|
||||
return False
|
||||
|
||||
def get_itemReplays_by_cookie(self, cookie_id: str) -> List[Dict]:
|
||||
"""获取指定Cookie的所有商品信息
|
||||
|
||||
Args:
|
||||
cookie_id: Cookie ID
|
||||
|
||||
Returns:
|
||||
List[Dict]: 商品信息列表
|
||||
"""
|
||||
try:
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT r.item_id, r.cookie_id, r.reply_content, r.created_at, r.updated_at, i.item_title, i.item_detail
|
||||
FROM item_replay r
|
||||
LEFT JOIN item_info i ON i.item_id = r.item_id
|
||||
WHERE r.cookie_id = ?
|
||||
ORDER BY r.updated_at DESC
|
||||
''', (cookie_id,))
|
||||
|
||||
columns = [description[0] for description in cursor.description]
|
||||
items = []
|
||||
|
||||
for row in cursor.fetchall():
|
||||
item_info = dict(zip(columns, row))
|
||||
|
||||
items.append(item_info)
|
||||
|
||||
return items
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取Cookie商品信息失败: {e}")
|
||||
return []
|
||||
|
||||
def delete_item_reply(self, cookie_id: str, item_id: str) -> bool:
|
||||
"""
|
||||
删除指定 cookie_id 和 item_id 的商品回复
|
||||
|
||||
Args:
|
||||
cookie_id: Cookie ID
|
||||
item_id: 商品ID
|
||||
|
||||
Returns:
|
||||
bool: 删除成功返回 True,失败返回 False
|
||||
"""
|
||||
try:
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('''
|
||||
DELETE FROM item_replay
|
||||
WHERE cookie_id = ? AND item_id = ?
|
||||
''', (cookie_id, item_id))
|
||||
self.conn.commit()
|
||||
# 判断是否有删除行
|
||||
return cursor.rowcount > 0
|
||||
except Exception as e:
|
||||
logger.error(f"删除商品回复失败: {e}")
|
||||
return False
|
||||
|
||||
def batch_delete_item_replies(self, items: List[Dict[str, str]]) -> Dict[str, int]:
|
||||
"""
|
||||
批量删除商品回复
|
||||
|
||||
Args:
|
||||
items: List[Dict] 每个字典包含 cookie_id 和 item_id
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: 返回成功和失败的数量,例如 {"success_count": 3, "failed_count": 1}
|
||||
"""
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
|
||||
try:
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
for item in items:
|
||||
cookie_id = item.get('cookie_id')
|
||||
item_id = item.get('item_id')
|
||||
if not cookie_id or not item_id:
|
||||
failed_count += 1
|
||||
continue
|
||||
cursor.execute('''
|
||||
DELETE FROM item_replay
|
||||
WHERE cookie_id = ? AND item_id = ?
|
||||
''', (cookie_id, item_id))
|
||||
if cursor.rowcount > 0:
|
||||
success_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
self.conn.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"批量删除商品回复失败: {e}")
|
||||
# 整体失败则视为全部失败
|
||||
return {"success_count": 0, "failed_count": len(items)}
|
||||
|
||||
return {"success_count": success_count, "failed_count": failed_count}
|
||||
|
||||
|
||||
|
||||
# 全局单例
|
||||
|
167
reply_server.py
167
reply_server.py
@ -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 ''}"
|
||||
@ -3455,6 +3458,156 @@ def get_system_stats(admin_user: Dict[str, Any] = Depends(require_admin)):
|
||||
log_with_user('error', f"获取系统统计信息失败: {str(e)}", admin_user)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# ------------------------- 指定商品回复接口 -------------------------
|
||||
|
||||
@app.get("/itemReplays")
|
||||
def get_all_items(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""获取当前用户的所有商品回复信息"""
|
||||
try:
|
||||
# 只返回当前用户的商品信息
|
||||
user_id = current_user['user_id']
|
||||
from db_manager import db_manager
|
||||
user_cookies = db_manager.get_all_cookies(user_id)
|
||||
|
||||
all_items = []
|
||||
for cookie_id in user_cookies.keys():
|
||||
items = db_manager.get_itemReplays_by_cookie(cookie_id)
|
||||
all_items.extend(items)
|
||||
|
||||
return {"items": all_items}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取商品回复信息失败: {str(e)}")
|
||||
|
||||
@app.get("/itemReplays/cookie/{cookie_id}")
|
||||
def get_items_by_cookie(cookie_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""获取指定Cookie的商品信息"""
|
||||
try:
|
||||
# 检查cookie是否属于当前用户
|
||||
user_id = current_user['user_id']
|
||||
from db_manager import db_manager
|
||||
user_cookies = db_manager.get_all_cookies(user_id)
|
||||
|
||||
if cookie_id not in user_cookies:
|
||||
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
|
||||
|
||||
items = db_manager.get_itemReplays_by_cookie(cookie_id)
|
||||
return {"items": items}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取商品信息失败: {str(e)}")
|
||||
|
||||
@app.put("/item-reply/{cookie_id}/{item_id}")
|
||||
def update_item_reply(
|
||||
cookie_id: str,
|
||||
item_id: str,
|
||||
data: dict,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
更新指定账号和商品的回复内容
|
||||
"""
|
||||
try:
|
||||
user_id = current_user['user_id']
|
||||
from db_manager import db_manager
|
||||
|
||||
# 验证cookie是否属于用户
|
||||
user_cookies = db_manager.get_all_cookies(user_id)
|
||||
if cookie_id not in user_cookies:
|
||||
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
|
||||
|
||||
reply_content = data.get("reply_content", "").strip()
|
||||
if not reply_content:
|
||||
raise HTTPException(status_code=400, detail="回复内容不能为空")
|
||||
|
||||
db_manager.update_item_reply(cookie_id=cookie_id, item_id=item_id, reply_content=reply_content)
|
||||
|
||||
return {"message": "商品回复更新成功"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"更新商品回复失败: {str(e)}")
|
||||
|
||||
@app.delete("/item-reply/{cookie_id}/{item_id}")
|
||||
def delete_item_reply(cookie_id: str, item_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""
|
||||
删除指定账号cookie_id和商品item_id的商品回复
|
||||
"""
|
||||
try:
|
||||
user_id = current_user['user_id']
|
||||
user_cookies = db_manager.get_all_cookies(user_id)
|
||||
if cookie_id not in user_cookies:
|
||||
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
|
||||
|
||||
success = db_manager.delete_item_reply(cookie_id, item_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="商品回复不存在")
|
||||
|
||||
return {"message": "商品回复删除成功"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"删除商品回复失败: {str(e)}")
|
||||
|
||||
class ItemToDelete(BaseModel):
|
||||
cookie_id: str
|
||||
item_id: str
|
||||
|
||||
class BatchDeleteRequest(BaseModel):
|
||||
items: List[ItemToDelete]
|
||||
|
||||
@app.delete("/item-reply/batch")
|
||||
async def batch_delete_item_reply(
|
||||
req: BatchDeleteRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
批量删除商品回复
|
||||
"""
|
||||
user_id = current_user['user_id']
|
||||
from db_manager import db_manager
|
||||
|
||||
# 先校验当前用户是否有权限删除每个cookie对应的回复
|
||||
user_cookies = db_manager.get_all_cookies(user_id)
|
||||
for item in req.items:
|
||||
if item.cookie_id not in user_cookies:
|
||||
raise HTTPException(status_code=403, detail=f"无权限访问Cookie {item.cookie_id}")
|
||||
|
||||
result = db_manager.batch_delete_item_replies([item.dict() for item in req.items])
|
||||
return {
|
||||
"success_count": result["success_count"],
|
||||
"failed_count": result["failed_count"]
|
||||
}
|
||||
|
||||
@app.get("/item-reply/{cookie_id}/{item_id}")
|
||||
def get_item_reply(cookie_id: str, item_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""
|
||||
获取指定账号cookie_id和商品item_id的商品回复内容
|
||||
"""
|
||||
try:
|
||||
user_id = current_user['user_id']
|
||||
# 校验cookie_id是否属于当前用户
|
||||
user_cookies = db_manager.get_all_cookies(user_id)
|
||||
if cookie_id not in user_cookies:
|
||||
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
|
||||
|
||||
# 获取指定商品回复
|
||||
item_replies = db_manager.get_itemReplays_by_cookie(cookie_id)
|
||||
# 找对应item_id的回复
|
||||
item_reply = next((r for r in item_replies if r['item_id'] == item_id), None)
|
||||
|
||||
if item_reply is None:
|
||||
raise HTTPException(status_code=404, detail="商品回复不存在")
|
||||
|
||||
return item_reply
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取商品回复失败: {str(e)}")
|
||||
|
||||
|
||||
# ------------------------- 数据库备份和恢复接口 -------------------------
|
||||
|
||||
@ -3661,7 +3814,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', "item_replay"
|
||||
]
|
||||
|
||||
if table_name not in allowed_tables:
|
||||
@ -3698,7 +3851,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 +3891,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', "item_replay"
|
||||
]
|
||||
|
||||
# 不允许清空用户表
|
||||
|
@ -57,6 +57,9 @@ openpyxl>=3.1.0
|
||||
# ==================== 邮件发送 ====================
|
||||
email-validator>=2.0.0
|
||||
|
||||
# ==================== 数据处理和验证 ====================
|
||||
xlsxwriter>=3.1.0
|
||||
|
||||
# ==================== 说明 ====================
|
||||
# 以下模块是Python内置模块,无需安装:
|
||||
# sqlite3, json, base64, hashlib, hmac, time, datetime, os, sys, re, urllib
|
||||
@ -64,3 +67,4 @@ email-validator>=2.0.0
|
||||
# collections, itertools, functools, copy, pickle, gzip, zipfile, shutil
|
||||
# tempfile, io, csv, xml, html, http, socket, ssl, subprocess, signal
|
||||
# inspect, ast, enum, math, decimal, array, queue, contextlib, warnings
|
||||
# typing, dataclasses, weakref, gc, platform, stat, glob, fnmatch
|
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;
|
||||
|
@ -98,6 +98,7 @@
|
||||
<option value="cookie_status">cookie_status - Cookie状态表</option>
|
||||
<option value="keywords">keywords - 关键字表</option>
|
||||
<option value="default_replies">default_replies - 默认回复表</option>
|
||||
<option value="item_replay">item_replay - 指定商品回复表</option>
|
||||
<option value="default_reply_records">default_reply_records - 默认回复记录表</option>
|
||||
<option value="ai_reply_settings">ai_reply_settings - AI回复设置表</option>
|
||||
<option value="ai_conversations">ai_conversations - AI对话历史表</option>
|
||||
@ -111,6 +112,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">
|
||||
@ -262,6 +264,7 @@
|
||||
'cookies': 'Cookie账号表',
|
||||
'cookie_status': 'Cookie状态表',
|
||||
'keywords': '关键字表',
|
||||
'item_replay': '指定商品回复表',
|
||||
'default_replies': '默认回复表',
|
||||
'default_reply_records': '默认回复记录表',
|
||||
'ai_reply_settings': 'AI回复设置表',
|
||||
@ -275,7 +278,8 @@
|
||||
'user_settings': '用户设置表',
|
||||
'system_settings': '系统设置表',
|
||||
'email_verifications': '邮箱验证表',
|
||||
'captcha_codes': '验证码表'
|
||||
'captcha_codes': '验证码表',
|
||||
'orders': '订单表'
|
||||
};
|
||||
|
||||
// 页面加载完成后初始化
|
||||
|
@ -42,12 +42,24 @@
|
||||
商品管理
|
||||
</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>
|
||||
自动回复
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a href="#" class="nav-link" onclick="showSection('items-reply')">
|
||||
<i class="bi bi-chat-left-text"></i>
|
||||
指定商品回复
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a href="#" class="nav-link" onclick="showSection('cards')">
|
||||
<i class="bi bi-credit-card"></i>
|
||||
@ -164,6 +176,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>
|
||||
|
||||
<!-- 账号详情列表 -->
|
||||
@ -384,6 +403,7 @@
|
||||
<th style="width: 12%">商品ID</th>
|
||||
<th style="width: 18%">商品标题</th>
|
||||
<th style="width: 20%">商品详情</th>
|
||||
<th style="width: 20%">商品价格</th>
|
||||
<th style="width: 8%">多规格</th>
|
||||
<th style="width: 10%">更新时间</th>
|
||||
<th style="width: 15%">操作</th>
|
||||
@ -443,6 +463,271 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 指定商品自动管理内容 -->
|
||||
<div id="items-reply-section" class="content-section">
|
||||
<div class="content-header">
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-box-seam me-2"></i>
|
||||
商品回复管理
|
||||
</h2>
|
||||
<p class="text-muted mb-0">管理各账号的商品信息</p>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- Cookie筛选 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-6">
|
||||
<label for="itemCookieFilter" class="form-label">筛选账号</label>
|
||||
<select class="form-select" id="itemReplayCookieFilter" onchange="loadItemsReplayByCookie()">
|
||||
<option value="">所有账号</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-end align-items-end gap-2">
|
||||
<!-- 页码输入 -->
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label for="pageNumber" class="form-label mb-0 text-nowrap">页码:</label>
|
||||
<input type="number" class="form-control" id="pageNumber" placeholder="页码" min="1" value="1" style="width: 80px;">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-success" onclick="showItemReplayEdit()">
|
||||
添加商品回复
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="refreshItemReplayS()">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">商品列表(自动发货根据商品标题和商品详情匹配关键字)</h5>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="batchDeleteItemReplies()">
|
||||
<i class="bi bi-trash"></i> 批量删除
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%">
|
||||
<input type="checkbox" id="selectAllItemReplay" onchange="toggleSelectAll(this)">
|
||||
</th>
|
||||
<th style="width: 12%">账号ID</th>
|
||||
<th style="width: 12%">商品ID</th>
|
||||
<th style="width: 18%">商品标题</th>
|
||||
<th style="width: 20%">商品内容</th>
|
||||
<th style="width: 18%">回复内容</th>
|
||||
<th style="width: 10%">更新时间</th>
|
||||
<th style="width: 15%">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="itemReplaysTableBody">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品回复弹框 -->
|
||||
<!-- 添加/编辑商品回复模态框 -->
|
||||
<div class="modal fade" id="editItemReplyModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-chat-text me-2"></i><span id="itemReplayTitle">编辑商品回复</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editItemReplyForm">
|
||||
<input type="hidden" id="editReplyCookieId">
|
||||
<input type="hidden" id="editReplyItemId">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">账号ID<span class="text-danger">*</span></label>
|
||||
<select id="editReplyCookieIdSelect" class="form-select" onchange="onCookieChangeForReply()">
|
||||
<option value="">请选择账号</option>
|
||||
<!-- JS 动态填充账号选项 -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">商品ID<span class="text-danger">*</span></label>
|
||||
<select id="editReplyItemIdSelect" class="form-select">
|
||||
<option value="">选择商品</option>
|
||||
<!-- JS 动态填充商品选项 -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editReplyContent" class="form-label">商品回复内容 <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="editItemReplyContent" rows="10"
|
||||
placeholder="请输入商品回复内容..."></textarea>
|
||||
<div class="form-text">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
请输入商品的自动回复内容,用户购买后将收到该回复。
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveItemReply()">
|
||||
<i class="bi bi-check-circle me-1"></i>保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 +796,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 +820,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>
|
||||
|
1203
static/js/app.js
1203
static/js/app.js
File diff suppressed because it is too large
Load Diff
@ -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,9 +66,15 @@ 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环境优化)
|
||||
@ -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,7 +188,7 @@ class OrderDetailFetcher:
|
||||
|
||||
async def fetch_order_detail(self, order_id: str, timeout: int = 30) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取订单详情
|
||||
获取订单详情(带锁机制和数据库缓存)
|
||||
|
||||
Args:
|
||||
order_id: 订单ID
|
||||
@ -168,26 +197,115 @@ class OrderDetailFetcher:
|
||||
Returns:
|
||||
包含订单详情的字典,失败时返回None
|
||||
"""
|
||||
# 获取该订单ID的锁
|
||||
order_lock = self._order_locks[order_id]
|
||||
|
||||
async with order_lock:
|
||||
logger.info(f"🔒 获取订单 {order_id} 的锁,开始处理...")
|
||||
|
||||
try:
|
||||
if not self.page:
|
||||
logger.error("浏览器未初始化")
|
||||
# 首先查询数据库中是否已存在该订单(在初始化浏览器之前)
|
||||
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'}")
|
||||
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)
|
||||
@ -196,7 +314,11 @@ class OrderDetailFetcher:
|
||||
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,
|
||||
@ -205,12 +327,16 @@ class OrderDetailFetcher:
|
||||
'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()
|
||||
'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:
|
||||
@ -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
|
||||
# 获取金额信息
|
||||
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内容解析失败")
|
||||
return {}
|
||||
else:
|
||||
logger.warning("SKU元素内容为空")
|
||||
return {}
|
||||
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