优化结构

This commit is contained in:
zhinianboke 2025-08-11 22:48:28 +08:00
parent b508c3e858
commit 5e5fee9d5b
22 changed files with 2634 additions and 480 deletions

243
.env
View File

@ -1,243 +0,0 @@
# 闲鱼自动回复系统 Docker 环境变量配置文件
# 复制此文件为 .env 并根据需要修改配置
# 或者直接重命名为 .env 使用默认配置
# ================================
# 基础配置
# ================================
# 时区设置
TZ=Asia/Shanghai
# Python配置
PYTHONUNBUFFERED=1
PYTHONDONTWRITEBYTECODE=1
# 日志级别 (DEBUG, INFO, WARNING, ERROR)
LOG_LEVEL=INFO
# ================================
# 数据库配置
# ================================
# 数据库文件路径
DB_PATH=/app/data/xianyu_data.db
# ================================
# 服务配置
# ================================
# Web服务端口
WEB_PORT=8080
# API服务配置
API_HOST=0.0.0.0
API_PORT=8080
# ================================
# 安全配置
# ================================
# 管理员账号密码 (建议修改)
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin123
# JWT密钥 (建议修改为随机字符串)
JWT_SECRET_KEY=your-secret-key-here
# Session超时时间 (秒)
SESSION_TIMEOUT=3600
# ================================
# 多用户系统配置
# ================================
# 多用户功能开关
MULTIUSER_ENABLED=true
# 用户注册开关
USER_REGISTRATION_ENABLED=true
# 邮箱验证开关
EMAIL_VERIFICATION_ENABLED=true
# 图形验证码开关
CAPTCHA_ENABLED=true
# Token过期时间默认24小时
TOKEN_EXPIRE_TIME=86400
# ================================
# 闲鱼API配置
# ================================
# WebSocket连接URL
WEBSOCKET_URL=wss://wss-goofish.dingtalk.com/
# 心跳间隔 (秒)
HEARTBEAT_INTERVAL=15
# 心跳超时 (秒)
HEARTBEAT_TIMEOUT=5
# Token刷新间隔 (秒)
TOKEN_REFRESH_INTERVAL=3600
# Token重试间隔 (秒)
TOKEN_RETRY_INTERVAL=300
# 消息过期时间 (毫秒)
MESSAGE_EXPIRE_TIME=300000
# ==================== AI回复配置 ====================
# AI回复功能总开关 (true/false)
AI_REPLY_ENABLED=false
# 默认AI模型
DEFAULT_AI_MODEL=qwen-plus
# 默认AI API地址
DEFAULT_AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# AI请求超时时间 (秒)
AI_REQUEST_TIMEOUT=30
# AI最大生成token数
AI_MAX_TOKENS=100
# ================================
# 自动回复配置
# ================================
# 是否启用自动回复
AUTO_REPLY_ENABLED=true
# 默认回复消息
AUTO_REPLY_DEFAULT_MESSAGE=亲爱的"{send_user_name}" 老板你好!所有宝贝都可以拍,秒发货的哈~不满意的话可以直接申请退款哈~
# 最大重试次数
AUTO_REPLY_MAX_RETRY=3
# 重试间隔 (秒)
AUTO_REPLY_RETRY_INTERVAL=5
# ================================
# 自动发货配置
# ================================
# 是否启用自动发货
AUTO_DELIVERY_ENABLED=true
# 自动发货超时时间 (秒)
AUTO_DELIVERY_TIMEOUT=30
# API卡券请求超时时间 (秒)
API_CARD_TIMEOUT=10
# 批量数据并发保护
BATCH_DATA_LOCK_TIMEOUT=5
# ================================
# 代理配置 (可选)
# ================================
# HTTP代理
# HTTP_PROXY=http://proxy.example.com:8080
# HTTPS代理
# HTTPS_PROXY=https://proxy.example.com:8080
# 不使用代理的地址
# NO_PROXY=localhost,127.0.0.1
# ================================
# 监控配置
# ================================
# 健康检查间隔 (秒)
HEALTH_CHECK_INTERVAL=30
# 健康检查超时 (秒)
HEALTH_CHECK_TIMEOUT=10
# ================================
# 资源限制
# ================================
# 内存限制 (MB)
MEMORY_LIMIT=512
# CPU限制 (核心数)
CPU_LIMIT=0.5
# 内存预留 (MB)
MEMORY_RESERVATION=256
# CPU预留 (核心数)
CPU_RESERVATION=0.25
# ================================
# SQL日志配置
# ================================
# 是否启用SQL日志
SQL_LOG_ENABLED=true
# SQL日志级别
SQL_LOG_LEVEL=INFO
# ================================
# 备份配置
# ================================
# 自动备份间隔 (小时)
BACKUP_INTERVAL=24
# 备份保留天数
BACKUP_RETENTION_DAYS=7
# 备份目录
BACKUP_DIR=/app/backups
# ================================
# 开发配置
# ================================
# 开发模式 (true/false)
DEBUG=false
# 热重载 (true/false)
RELOAD=false
# API文档 (true/false)
ENABLE_DOCS=true
# ================================
# 第三方服务配置
# ================================
# 邮件通知配置 (可选)
# SMTP_HOST=smtp.example.com
# SMTP_PORT=587
# SMTP_USERNAME=your-email@example.com
# SMTP_PASSWORD=your-password
# SMTP_FROM=noreply@example.com
# 钉钉机器人通知 (可选)
# DINGTALK_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=your-token
# 企业微信机器人通知 (可选)
# WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key
# ================================
# 自定义配置
# ================================
# 自定义配置文件路径
# CUSTOM_CONFIG_PATH=/app/custom_config.yml
# 插件目录
# PLUGINS_DIR=/app/plugins
# 模板目录
# TEMPLATES_DIR=/app/templates

3
.gitignore vendored
View File

@ -193,9 +193,6 @@ dmypy.json
.pyre/
# ==================== 项目特定新增 ====================
# 环境变量文件
.env.example
.env.*.example
# 数据库文件
*.db-journal

View File

@ -409,7 +409,6 @@ python Start.py
- **`docker-deploy.bat`** - Windows版本部署脚本支持Windows环境一键部署
- **`entrypoint.sh`** - Docker容器启动脚本处理环境初始化和服务启动
- **`nginx/nginx.conf`** - Nginx反向代理配置支持负载均衡和SSL终端
- **`.env`** - 环境变量配置文件,包含所有可配置的系统参数和敏感信息
- **`requirements.txt`** - Python依赖包列表精简版本无冗余依赖按功能分类组织
## ⚙️ 配置说明
@ -517,7 +516,7 @@ python Start.py
## ❓ 常见问题
### 1. 端口被占用
如果8080端口被占用可以修改 `.env` 文件中的 `WEB_PORT` 配置
如果8080端口被占用可以修改 `global_config.yml` 文件中的 `AUTO_REPLY.api.port` 配置,或者在 Docker 启动时通过环境变量 `WEB_PORT` 指定端口
### 2. 数据库连接失败
检查数据库文件权限,确保应用有读写权限。

View File

@ -457,6 +457,19 @@ class XianyuLive:
item_id: str, chat_id: str, msg_time: str):
"""统一处理自动发货逻辑"""
try:
# 检查商品是否属于当前cookies
if item_id and item_id != "未知商品":
try:
from db_manager import db_manager
item_info = db_manager.get_item_info(self.cookie_id, item_id)
if not item_info:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】❌ 商品 {item_id} 不属于当前账号,跳过自动发货')
return
logger.debug(f'[{msg_time}] 【{self.cookie_id}】✅ 商品 {item_id} 归属验证通过')
except Exception as e:
logger.error(f'[{msg_time}] 【{self.cookie_id}】检查商品归属失败: {self._safe_str(e)},跳过自动发货')
return
# 提取订单ID
order_id = self._extract_order_id(message)
@ -2183,23 +2196,28 @@ class XianyuLive:
# 插入或更新订单信息到数据库
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} 信息已保存到数据库")
# 检查cookie_id是否在cookies表中存在
cookie_info = db_manager.get_cookie_by_id(self.cookie_id)
if not cookie_info:
logger.warning(f"Cookie ID {self.cookie_id} 不存在于cookies表中丢弃订单 {order_id}")
else:
logger.warning(f"{self.cookie_id}】订单信息保存失败: {order_id}")
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)}")
@ -2438,17 +2456,23 @@ class XianyuLive:
# 保存订单基本信息到数据库(如果还没有详细信息)
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}")
# 检查cookie_id是否在cookies表中存在
cookie_info = db_manager.get_cookie_by_id(self.cookie_id)
if not cookie_info:
logger.warning(f"Cookie ID {self.cookie_id} 不存在于cookies表中丢弃订单 {order_id}")
else:
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)}")
@ -3346,6 +3370,20 @@ class XianyuLive:
# 检查是否为"我已小刀,待刀成"
if card_title == "我已小刀,待刀成":
logger.info(f'[{msg_time}] 【{self.cookie_id}】【系统】检测到"我已小刀,待刀成",即使在暂停期间也继续处理')
# 检查商品是否属于当前cookies
if item_id and item_id != "未知商品":
try:
from db_manager import db_manager
item_info = db_manager.get_item_info(self.cookie_id, item_id)
if not item_info:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】❌ 商品 {item_id} 不属于当前账号,跳过免拼发货')
return
logger.debug(f'[{msg_time}] 【{self.cookie_id}】✅ 商品 {item_id} 归属验证通过')
except Exception as e:
logger.error(f'[{msg_time}] 【{self.cookie_id}】检查商品归属失败: {self._safe_str(e)},跳过免拼发货')
return
# 提取订单ID
order_id = self._extract_order_id(message)
if order_id:

View File

@ -1120,6 +1120,8 @@ class DBManager:
logger.error(f"获取所有Cookie失败: {e}")
return {}
def get_cookie_by_id(self, cookie_id: str) -> Optional[Dict[str, str]]:
"""根据ID获取Cookie信息
@ -4055,6 +4057,14 @@ class DBManager:
try:
cursor = self.conn.cursor()
# 检查cookie_id是否在cookies表中存在如果提供了cookie_id
if cookie_id:
cursor.execute("SELECT id FROM cookies WHERE id = ?", (cookie_id,))
cookie_exists = cursor.fetchone()
if not cookie_exists:
logger.warning(f"Cookie ID {cookie_id} 不存在于cookies表中拒绝插入订单 {order_id}")
return False
# 检查订单是否已存在
cursor.execute("SELECT order_id FROM orders WHERE order_id = ?", (order_id,))
existing = cursor.fetchone()
@ -4189,14 +4199,21 @@ class DBManager:
primary_key_map = {
'users': 'id',
'cookies': 'id',
'cookie_status': 'id',
'keywords': 'id',
'default_replies': 'id',
'default_reply_records': 'id',
'item_replay': 'item_id',
'ai_reply_settings': 'id',
'ai_conversations': 'id',
'ai_item_cache': 'id',
'item_info': 'id',
'message_notifications': 'id',
'cards': 'id',
'delivery_rules': 'id',
'notification_channels': 'id',
'user_settings': 'id',
'system_settings': 'id',
'email_verifications': 'id',
'captcha_codes': 'id',
'orders': 'order_id'

View File

@ -57,9 +57,7 @@ services:
- TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600}
- TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300}
- MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000}
env_file:
- path: .env
required: false
networks:
- xianyu-network
healthcheck:

View File

@ -57,9 +57,7 @@ services:
- TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600}
- TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300}
- MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000}
env_file:
- path: .env
required: false
networks:
- xianyu-network
healthcheck:

View File

@ -10,7 +10,6 @@ title 闲鱼自动回复系统 Docker 部署
REM 项目配置
set PROJECT_NAME=xianyu-auto-reply
set COMPOSE_FILE=docker-compose.yml
set ENV_FILE=.env
REM 颜色定义Windows CMD不支持ANSI颜色使用echo代替
set "INFO_PREFIX=[INFO]"
@ -41,13 +40,6 @@ echo %SUCCESS_PREFIX% 系统依赖检查通过
REM 初始化配置
echo %INFO_PREFIX% 初始化配置文件...
if not exist "%ENV_FILE%" (
echo %WARNING_PREFIX% %ENV_FILE% 文件不存在,将使用默认配置
echo %INFO_PREFIX% 如需自定义配置,请创建 %ENV_FILE% 文件
) else (
echo %SUCCESS_PREFIX% %ENV_FILE% 配置文件已存在
)
REM 检查关键文件
if not exist "entrypoint.sh" (
echo %ERROR_PREFIX% entrypoint.sh 文件不存在Docker容器将无法启动
@ -58,6 +50,15 @@ if not exist "entrypoint.sh" (
echo %SUCCESS_PREFIX% entrypoint.sh 文件已存在
)
if not exist "global_config.yml" (
echo %ERROR_PREFIX% global_config.yml 配置文件不存在
echo %INFO_PREFIX% 请确保配置文件存在
pause
exit /b 1
) else (
echo %SUCCESS_PREFIX% global_config.yml 配置文件已存在
)
REM 创建必要的目录
if not exist "data" mkdir data
if not exist "logs" mkdir logs

View File

@ -15,7 +15,6 @@ NC='\033[0m' # No Color
# 项目配置
PROJECT_NAME="xianyu-auto-reply"
COMPOSE_FILE="docker-compose.yml"
ENV_FILE=".env"
# 打印带颜色的消息
print_info() {
@ -55,13 +54,6 @@ check_dependencies() {
init_config() {
print_info "初始化配置文件..."
if [ ! -f "$ENV_FILE" ]; then
print_warning "$ENV_FILE 文件不存在,将使用默认配置"
print_info "如需自定义配置,请创建 $ENV_FILE 文件"
else
print_success "$ENV_FILE 配置文件已存在"
fi
# 检查关键文件
if [ ! -f "entrypoint.sh" ]; then
print_error "entrypoint.sh 文件不存在Docker容器将无法启动"
@ -71,6 +63,14 @@ init_config() {
print_success "entrypoint.sh 文件已存在"
fi
if [ ! -f "global_config.yml" ]; then
print_error "global_config.yml 配置文件不存在"
print_info "请确保配置文件存在"
exit 1
else
print_success "global_config.yml 配置文件已存在"
fi
# 创建必要的目录
mkdir -p data logs backups static/uploads/images
print_success "已创建必要的目录"

View File

@ -2742,7 +2742,11 @@ async def search_items(
current_user: Optional[Dict[str, Any]] = Depends(get_current_user_optional)
):
"""搜索闲鱼商品"""
user_info = f"{current_user.get('username', 'unknown')}#{current_user.get('user_id', 'unknown')}" if current_user else "【未登录】"
try:
logger.info(f"{user_info} 开始单页搜索: 关键词='{search_request.keyword}', 页码={search_request.page}, 每页={search_request.page_size}")
from utils.item_search import search_xianyu_items
# 执行搜索
@ -2752,18 +2756,84 @@ async def search_items(
page_size=search_request.page_size
)
return {
# 检查是否有错误
has_error = result.get("error")
items_count = len(result.get("items", []))
logger.info(f"{user_info} 单页搜索完成: 获取到 {items_count} 条数据" +
(f", 错误: {has_error}" if has_error else ""))
response_data = {
"success": True,
"data": result.get("items", []),
"total": result.get("total", 0),
"page": search_request.page,
"page_size": search_request.page_size,
"keyword": search_request.keyword
"keyword": search_request.keyword,
"is_real_data": result.get("is_real_data", False),
"source": result.get("source", "unknown")
}
except Exception as e:
logger.error(f"商品搜索失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"商品搜索失败: {str(e)}")
# 如果有错误信息,也包含在响应中
if has_error:
response_data["error"] = has_error
return response_data
except Exception as e:
error_msg = str(e)
logger.error(f"{user_info} 商品搜索失败: {error_msg}")
raise HTTPException(status_code=500, detail=f"商品搜索失败: {error_msg}")
@app.get("/cookies/check")
async def check_valid_cookies(
current_user: Optional[Dict[str, Any]] = Depends(get_current_user_optional)
):
"""检查是否有有效的cookies账户必须是启用状态"""
try:
if cookie_manager.manager is None:
return {
"success": True,
"hasValidCookies": False,
"validCount": 0,
"enabledCount": 0,
"totalCount": 0
}
from db_manager import db_manager
# 获取所有cookies
all_cookies = db_manager.get_all_cookies()
# 检查启用状态和有效性
valid_cookies = []
enabled_cookies = []
for cookie_id, cookie_value in all_cookies.items():
# 检查是否启用
is_enabled = cookie_manager.manager.get_cookie_status(cookie_id)
if is_enabled:
enabled_cookies.append(cookie_id)
# 检查是否有效长度大于50
if len(cookie_value) > 50:
valid_cookies.append(cookie_id)
return {
"success": True,
"hasValidCookies": len(valid_cookies) > 0,
"validCount": len(valid_cookies),
"enabledCount": len(enabled_cookies),
"totalCount": len(all_cookies)
}
except Exception as e:
logger.error(f"检查cookies失败: {str(e)}")
return {
"success": False,
"hasValidCookies": False,
"error": str(e)
}
@app.post("/items/search_multiple")
async def search_multiple_pages(
@ -2771,7 +2841,11 @@ async def search_multiple_pages(
current_user: Optional[Dict[str, Any]] = Depends(get_current_user_optional)
):
"""搜索多页闲鱼商品"""
user_info = f"{current_user.get('username', 'unknown')}#{current_user.get('user_id', 'unknown')}" if current_user else "【未登录】"
try:
logger.info(f"{user_info} 开始多页搜索: 关键词='{search_request.keyword}', 页数={search_request.total_pages}")
from utils.item_search import search_multiple_pages_xianyu
# 执行多页搜索
@ -2780,7 +2854,14 @@ async def search_multiple_pages(
total_pages=search_request.total_pages
)
return {
# 检查是否有错误
has_error = result.get("error")
items_count = len(result.get("items", []))
logger.info(f"{user_info} 多页搜索完成: 获取到 {items_count} 条数据" +
(f", 错误: {has_error}" if has_error else ""))
response_data = {
"success": True,
"data": result.get("items", []),
"total": result.get("total", 0),
@ -2790,9 +2871,17 @@ async def search_multiple_pages(
"is_fallback": result.get("is_fallback", False),
"source": result.get("source", "unknown")
}
# 如果有错误信息,也包含在响应中
if has_error:
response_data["error"] = has_error
return response_data
except Exception as e:
logger.error(f"多页商品搜索失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"多页商品搜索失败: {str(e)}")
error_msg = str(e)
logger.error(f"{user_info} 多页商品搜索失败: {error_msg}")
raise HTTPException(status_code=500, detail=f"多页商品搜索失败: {error_msg}")
@app.get("/items/detail/{item_id}")
@ -3373,43 +3462,55 @@ def get_system_logs(admin_user: Dict[str, Any] = Depends(require_admin),
# 查找日志文件
log_files = glob.glob("logs/xianyu_*.log")
logger.info(f"找到日志文件: {log_files}")
if not log_files:
return {"logs": [], "message": "未找到日志文件"}
logger.warning("未找到日志文件")
return {"logs": [], "message": "未找到日志文件", "success": False}
# 获取最新的日志文件
latest_log_file = max(log_files, key=os.path.getctime)
logger.info(f"使用最新日志文件: {latest_log_file}")
logs = []
try:
with open(latest_log_file, 'r', encoding='utf-8') as f:
all_lines = f.readlines()
logger.info(f"读取到 {len(all_lines)} 行日志")
# 如果指定了日志级别,进行过滤
if level:
filtered_lines = [line for line in all_lines if f"| {level.upper()} |" in line]
logger.info(f"按级别 {level} 过滤后剩余 {len(filtered_lines)}")
else:
filtered_lines = all_lines
# 获取最后N行
recent_lines = filtered_lines[-lines:] if len(filtered_lines) > lines else filtered_lines
logger.info(f"取最后 {len(recent_lines)} 行日志")
for line in recent_lines:
logs.append(line.strip())
except Exception as e:
logger.error(f"读取日志文件失败: {str(e)}")
log_with_user('error', f"读取日志文件失败: {str(e)}", admin_user)
return {"logs": [], "message": f"读取日志文件失败: {str(e)}"}
return {"logs": [], "message": f"读取日志文件失败: {str(e)}", "success": False}
log_with_user('info', f"返回日志记录 {len(logs)}", admin_user)
logger.info(f"成功返回 {len(logs)} 条日志记录")
return {
"logs": logs,
"log_file": latest_log_file,
"total_lines": len(logs)
"total_lines": len(logs),
"success": True
}
except Exception as e:
logger.error(f"获取系统日志失败: {str(e)}")
log_with_user('error', f"获取系统日志失败: {str(e)}", admin_user)
raise HTTPException(status_code=500, detail=str(e))
return {"logs": [], "message": f"获取系统日志失败: {str(e)}", "success": False}
@app.get('/admin/stats')
def get_system_stats(admin_user: Dict[str, Any] = Depends(require_admin)):

View File

@ -18,7 +18,6 @@ httpx>=0.25.0
# ==================== 配置文件处理 ====================
PyYAML>=6.0.0
python-dotenv>=1.0.1
# ==================== JavaScript执行引擎 ====================
PyExecJS>=1.5.1

View File

@ -165,12 +165,8 @@ class SecureConfirm:
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 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)}")

View File

@ -115,12 +115,8 @@ class SecureFreeshipping:
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_freeshipping(order_id, item_id, buyer_id, retry_count + 1)
return await self.auto_freeshipping(order_id, item_id, buyer_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)}")

241
static/css/admin.css Normal file
View File

@ -0,0 +1,241 @@
/* ================================
管理员功能样式 - 用户管理和数据管理
================================ */
/* 用户管理样式 */
.user-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 1px solid var(--border-color);
border-radius: 12px;
}
.user-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.user-card .card-body {
padding: 1.25rem;
}
.user-card .badge {
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
}
/* 数据管理样式 */
.table-container {
max-height: 600px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
}
.table th {
position: sticky;
top: 0;
background: var(--dark-color);
color: white;
z-index: 10;
border: none;
font-weight: 600;
font-size: 0.875rem;
padding: 0.75rem;
}
.table td {
padding: 0.75rem;
font-size: 0.875rem;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
}
.table tbody tr:hover {
background-color: rgba(79, 70, 229, 0.05);
}
/* 数据统计卡片 */
.data-stats {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
color: white;
border: none;
}
.data-stats .card-body {
padding: 0.75rem;
}
/* 表格状态样式 */
#loadingTable,
#noTableSelected,
#noTableData {
padding: 3rem 1rem;
}
#loadingTable .spinner-border {
width: 3rem;
height: 3rem;
border-width: 0.3em;
}
#noTableSelected i,
#noTableData i {
font-size: 4rem;
color: var(--secondary-color);
opacity: 0.5;
}
/* 按钮组样式 */
.btn-group .btn {
border-radius: 0;
}
.btn-group .btn:first-child {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.btn-group .btn:last-child {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
/* 表格选择器样式 */
#tableSelect {
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 0.75rem;
font-size: 0.875rem;
transition: border-color 0.2s ease;
}
#tableSelect:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(79, 70, 229, 0.25);
}
/* 记录计数样式 */
#recordCount {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary-color);
}
/* 响应式调整 */
@media (max-width: 768px) {
.user-card {
margin-bottom: 1rem;
}
.table-container {
max-height: 400px;
}
.table th,
.table td {
padding: 0.5rem;
font-size: 0.8rem;
}
.btn-group {
flex-direction: column;
}
.btn-group .btn {
border-radius: 6px !important;
margin-bottom: 0.25rem;
}
}
/* 加载状态动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.user-card,
.table-container {
animation: fadeIn 0.3s ease-out;
}
/* 表格滚动条样式 */
.table-container::-webkit-scrollbar {
width: 8px;
}
.table-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.table-container::-webkit-scrollbar-thumb {
background: var(--secondary-color);
border-radius: 4px;
}
.table-container::-webkit-scrollbar-thumb:hover {
background: var(--primary-color);
}
/* 文本截断样式 */
.text-truncate-custom {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
/* 状态徽章样式 */
.status-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 12px;
}
/* 工具提示样式增强 */
[title] {
cursor: help;
}
/* 空状态图标样式 */
.empty-state-icon {
font-size: 4rem;
color: var(--secondary-color);
opacity: 0.3;
margin-bottom: 1rem;
}
/* 操作按钮样式 */
.action-buttons .btn {
margin: 0 0.125rem;
padding: 0.375rem 0.75rem;
}
.action-buttons .btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
/* 统计卡片渐变背景 */
.stats-card-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
}
.stats-card-info {
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
}
.stats-card-success {
background: linear-gradient(135deg, var(--success-color) 0%, #0d9488 100%);
}
.stats-card-warning {
background: linear-gradient(135deg, var(--warning-color) 0%, #d97706 100%);
}

View File

@ -7,6 +7,7 @@
@import url('items.css');
@import url('notifications.css');
@import url('components.css');
@import url('admin.css');

View File

@ -75,3 +75,73 @@
.log-container::-webkit-scrollbar-thumb:hover {
background: #5a5a5c;
}
/* 日志过滤标签样式 */
.filter-badge {
cursor: pointer;
transition: all 0.2s;
margin-right: 0.5rem;
margin-bottom: 0.25rem;
}
.filter-badge:hover {
transform: scale(1.05);
}
.filter-badge.active {
box-shadow: 0 0 0 2px rgba(255,255,255,0.5);
}
/* 自动刷新指示器 */
.auto-refresh-indicator {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
/* 日志行悬停效果 */
.log-entry:hover {
background: rgba(255,255,255,0.05);
}
/* 商品搜索相关样式 */
.item-card {
transition: transform 0.2s;
border: none;
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.item-card:hover {
transform: translateY(-2px);
}
.item-image {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 10px;
}
.price {
color: #e74c3c;
font-weight: bold;
font-size: 1.2em;
}
.seller-name {
color: #6c757d;
font-size: 0.9em;
}
.want-count {
margin-top: 10px;
}
.want-count .badge {
font-size: 0.8em;
}

View File

@ -85,7 +85,7 @@
</a>
</div>
<div class="nav-item">
<a href="/item_search.html" class="nav-link" target="_blank">
<a href="#" class="nav-link" onclick="showSection('item-search')">
<i class="bi bi-search"></i>
商品搜索
</a>
@ -103,19 +103,19 @@
<small class="text-white-50">管理员功能</small>
</div>
<div class="nav-item">
<a href="/user_management.html" class="nav-link" target="_blank">
<a href="#" class="nav-link" onclick="showSection('user-management')">
<i class="bi bi-people"></i>
用户管理
</a>
</div>
<div class="nav-item">
<a href="/log_management.html" class="nav-link" target="_blank">
<a href="#" class="nav-link" onclick="showSection('logs')">
<i class="bi bi-file-text-fill"></i>
系统日志
</a>
</div>
<div class="nav-item">
<a href="/data_management.html" class="nav-link" target="_blank">
<a href="#" class="nav-link" onclick="showSection('data-management')">
<i class="bi bi-database"></i>
数据管理
</a>
@ -411,7 +411,7 @@
</thead>
<tbody id="itemsTableBody">
<tr>
<td colspan="8" class="text-center text-muted">加载中...</td>
<td colspan="9" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
@ -1214,58 +1214,257 @@
</div>
<div class="content-body">
<!-- 日志控制 -->
<div class="row mb-3">
<div class="col-md-3">
<label for="logLines" class="form-label">显示行数</label>
<select class="form-select" id="logLines" onchange="refreshLogs()">
<option value="100">100行</option>
<option value="200" selected>200行</option>
<option value="500">500行</option>
<option value="1000">1000行</option>
<option value="2000">2000行</option>
</select>
<!-- 日志控制面板 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-sliders"></i> 日志控制
</h5>
</div>
<div class="col-md-9">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary" onclick="refreshLogs()">
<i class="bi bi-arrow-clockwise me-1"></i>刷新日志
</button>
<button class="btn btn-outline-secondary" onclick="clearLogsDisplay()">
<i class="bi bi-trash me-1"></i>清空显示
</button>
<button class="btn btn-outline-danger" onclick="clearLogsServer()">
<i class="bi bi-trash3 me-1"></i>清空服务器日志
</button>
<button class="btn btn-outline-info" onclick="showLogStats()">
<i class="bi bi-bar-chart me-1"></i>统计信息
</button>
<button class="btn btn-outline-info" onclick="toggleAutoRefresh()">
<i class="bi bi-play-circle me-1"></i><span id="autoRefreshText">开启自动刷新</span>
</button>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-3">
<label class="form-label">显示行数</label>
<select class="form-select" id="logLines" onchange="loadSystemLogs()">
<option value="50">50行</option>
<option value="100" selected>100行</option>
<option value="200">200行</option>
<option value="500">500行</option>
<option value="1000">1000行</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">日志级别过滤</label>
<div class="d-flex gap-2 flex-wrap">
<span class="badge bg-secondary filter-badge active" data-level="" onclick="filterLogsByLevel('')">
全部
</span>
<span class="badge bg-info filter-badge" data-level="info" onclick="filterLogsByLevel('info')">
INFO
</span>
<span class="badge bg-warning filter-badge" data-level="warning" onclick="filterLogsByLevel('warning')">
WARNING
</span>
<span class="badge bg-danger filter-badge" data-level="error" onclick="filterLogsByLevel('error')">
ERROR
</span>
<span class="badge bg-secondary filter-badge" data-level="debug" onclick="filterLogsByLevel('debug')">
DEBUG
</span>
</div>
</div>
<div class="col-md-3">
<label class="form-label">自动刷新</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="autoRefreshLogs" onchange="toggleLogAutoRefresh()">
<label class="form-check-label" for="autoRefreshLogs">
<span id="autoRefreshLogLabel">关闭</span>
<i id="autoRefreshLogIcon" class="bi bi-circle" style="display: none;"></i>
</label>
</div>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-grid">
<button class="btn btn-primary" onclick="loadSystemLogs()">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 日志显示区域 -->
<!-- 日志信息 -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-info-circle"></i> 日志信息
</h5>
<small class="text-muted" id="logLastUpdate">最后更新: -</small>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<strong>日志文件:</strong>
<span id="logFileName" class="text-muted">-</span>
</div>
<div class="col-md-4">
<strong>显示行数:</strong>
<span id="logDisplayLines" class="text-muted">-</span>
</div>
<div class="col-md-4">
<strong>当前过滤:</strong>
<span id="logCurrentFilter" class="text-muted">全部</span>
</div>
</div>
</div>
</div>
<!-- 日志内容 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>系统日志</span>
<h5 class="mb-0">
<i class="bi bi-terminal"></i> 日志内容
</h5>
<div class="d-flex gap-2">
<span class="badge bg-secondary" id="logCount">0 条日志</span>
<span class="badge bg-info" id="lastUpdate">未更新</span>
<button class="btn btn-sm btn-outline-secondary" onclick="scrollLogToTop()">
<i class="bi bi-arrow-up"></i> 顶部
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="scrollLogToBottom()">
<i class="bi bi-arrow-down"></i> 底部
</button>
</div>
</div>
<div class="card-body p-0">
<div id="logContainer" class="log-container">
<div class="text-center p-4 text-muted">
<i class="bi bi-file-text fs-1"></i>
<p class="mt-2">点击刷新按钮加载日志</p>
<div id="loadingSystemLogs" class="text-center py-4">
<div class="spinner-border" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">正在加载日志...</p>
</div>
<div id="systemLogContainer" class="log-container" style="display: none;"></div>
<div id="noSystemLogs" class="text-center py-4" style="display: none;">
<i class="bi bi-file-text" style="font-size: 3rem; color: #ccc;"></i>
<p class="mt-2 text-muted">暂无日志数据</p>
</div>
</div>
</div>
</div>
</div>
<!-- 商品搜索内容 -->
<div id="item-search-section" class="content-section">
<div class="content-header">
<h2 class="mb-0">
<i class="bi bi-search me-2"></i>
商品搜索
</h2>
<p class="text-muted mb-0">搜索闲鱼商品,获取真实商品数据</p>
</div>
<div class="content-body">
<!-- 搜索表单 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-search me-2"></i>
闲鱼商品搜索
</h5>
</div>
<div class="card-body">
<div class="alert alert-info mb-4">
<i class="bi bi-info-circle me-2"></i>
<strong>功能说明:</strong>
<ul class="mb-0 mt-2">
<li><strong>查询总页数:</strong>输入要获取的页数1-20页系统会一次性获取所有页面的数据</li>
<li><strong>每页显示:</strong>前端分页显示的每页商品数量</li>
<li><strong>数据来源:</strong>使用真实的闲鱼数据,通过浏览器自动化技术获取</li>
</ul>
</div>
<form id="itemSearchForm">
<div class="row">
<div class="col-md-5">
<label for="searchKeyword" class="form-label">搜索关键词</label>
<input type="text" class="form-control" id="searchKeyword" placeholder="请输入商品关键词" required>
</div>
<div class="col-md-3">
<label for="searchTotalPages" class="form-label">查询总页数</label>
<input type="number" class="form-control" id="searchTotalPages" value="1" min="1" max="20" placeholder="输入页数">
</div>
<div class="col-md-2">
<label for="searchPageSize" class="form-label">每页显示</label>
<select class="form-select" id="searchPageSize">
<option value="10">10条</option>
<option value="20" selected>20条</option>
<option value="30">30条</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search me-2"></i>
搜索
</button>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- 搜索状态 -->
<div id="searchStatus" class="card mb-4" style="display: none;">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-3" role="status">
<span class="visually-hidden">搜索中...</span>
</div>
<div>
<strong>正在搜索商品...</strong>
<div class="text-muted small" id="searchProgress">准备开始搜索</div>
</div>
</div>
</div>
</div>
<!-- 搜索结果统计 -->
<div id="searchResultStats" class="card mb-4" style="display: none;">
<div class="card-body">
<div class="row text-center">
<div class="col-md-3">
<div class="stat-number text-primary" id="totalItemsFound">0</div>
<div class="stat-label">找到商品</div>
</div>
<div class="col-md-3">
<div class="stat-number text-success" id="totalPagesSearched">0</div>
<div class="stat-label">搜索页数</div>
</div>
<div class="col-md-3">
<div class="stat-number text-info" id="currentDisplayPage">0</div>
<div class="stat-label">当前页</div>
</div>
<div class="col-md-3">
<div class="stat-number text-warning" id="totalDisplayPages">0</div>
<div class="stat-label">总页数</div>
</div>
</div>
</div>
</div>
<!-- 搜索结果 -->
<div id="searchResults" class="card" style="display: none;">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-grid me-2"></i>
搜索结果
</h5>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary" onclick="exportSearchResults()">
<i class="bi bi-download"></i> 导出结果
</button>
</div>
</div>
<div class="card-body">
<div id="searchResultsContainer" class="row">
<!-- 搜索结果将在这里显示 -->
</div>
<!-- 分页控件 -->
<div id="searchPagination" class="d-flex justify-content-center mt-4">
<!-- 分页按钮将在这里显示 -->
</div>
</div>
</div>
<!-- 无结果提示 -->
<div id="noSearchResults" class="card" style="display: none;">
<div class="card-body text-center py-5">
<i class="bi bi-search" style="font-size: 3rem; color: #ccc;"></i>
<h5 class="mt-3 text-muted">未找到相关商品</h5>
<p class="text-muted">请尝试使用其他关键词或调整搜索条件</p>
</div>
</div>
</div>
</div>
@ -1444,6 +1643,187 @@
</div>
</div>
<!-- 用户管理内容 -->
<div id="user-management-section" class="content-section">
<div class="content-header">
<h2 class="mb-0">
<i class="bi bi-people me-2"></i>
用户管理
</h2>
<p class="text-muted mb-0">管理系统中的所有用户账号</p>
</div>
<div class="content-body">
<!-- 统计信息 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<h3 id="totalUsers">-</h3>
<p class="mb-0">总用户数</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body text-center">
<h3 id="totalUserCookies">-</h3>
<p class="mb-0">总Cookie数</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h3 id="totalUserCards">-</h3>
<p class="mb-0">总卡券数</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body text-center">
<h3 id="systemUptime">运行中</h3>
<p class="mb-0">系统状态</p>
</div>
</div>
</div>
</div>
<!-- 用户列表 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-people-fill"></i> 用户列表
</h5>
<button class="btn btn-primary" onclick="refreshUsers()">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
<div class="card-body">
<div id="loadingUsers" class="text-center py-4">
<div class="spinner-border" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">正在加载用户信息...</p>
</div>
<div id="usersList" class="row" style="display: none;"></div>
<div id="noUsers" class="text-center py-4" style="display: none;">
<i class="bi bi-people" style="font-size: 3rem; color: #ccc;"></i>
<p class="mt-2 text-muted">暂无用户</p>
</div>
</div>
</div>
</div>
</div>
<!-- 数据管理内容 -->
<div id="data-management-section" class="content-section">
<div class="content-header">
<h2 class="mb-0">
<i class="bi bi-database me-2"></i>
数据管理
</h2>
<p class="text-muted mb-0">查看和管理数据库中的所有表数据</p>
</div>
<div class="content-body">
<!-- 表选择器 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-table"></i> 数据表选择
</h5>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-6">
<label class="form-label">选择数据表</label>
<select class="form-select" id="tableSelect" onchange="loadTableData()">
<option value="">请选择数据表...</option>
<option value="users">users - 用户表</option>
<option value="cookies">cookies - Cookie账号表</option>
<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>
<option value="ai_item_cache">ai_item_cache - AI商品信息缓存表</option>
<option value="item_info">item_info - 商品信息表</option>
<option value="message_notifications">message_notifications - 消息通知表</option>
<option value="cards">cards - 卡券表</option>
<option value="delivery_rules">delivery_rules - 发货规则表</option>
<option value="notification_channels">notification_channels - 通知渠道表</option>
<option value="user_settings">user_settings - 用户设置表</option>
<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">
<label class="form-label">数据统计</label>
<div class="card">
<div class="card-body text-center py-2">
<h5 id="recordCount" class="mb-0">-</h5>
<small>条记录</small>
</div>
</div>
</div>
<div class="col-md-3">
<label class="form-label">&nbsp;</label>
<div class="d-grid">
<button class="btn btn-primary" onclick="refreshTableData()">
<i class="bi bi-arrow-clockwise"></i> 刷新数据
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 数据表格 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" id="tableTitle">
<i class="bi bi-table"></i> 数据表
</h5>
<div class="btn-group">
<button class="btn btn-outline-danger btn-sm" onclick="clearTableData()" id="clearBtn" disabled>
<i class="bi bi-trash"></i> 清空
</button>
</div>
</div>
<div class="card-body">
<div id="loadingTable" class="text-center py-4" style="display: none;">
<div class="spinner-border" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">正在加载数据...</p>
</div>
<div id="noTableSelected" class="text-center py-4">
<i class="bi bi-table" style="font-size: 3rem; color: #ccc;"></i>
<p class="mt-2 text-muted">请选择要查看的数据表</p>
</div>
<div id="noTableData" class="text-center py-4" style="display: none;">
<i class="bi bi-inbox" style="font-size: 3rem; color: #ccc;"></i>
<p class="mt-2 text-muted">该表暂无数据</p>
</div>
<div id="tableContainer" style="display: none;">
<div class="table-responsive" style="max-height: 600px; overflow-y: auto;">
<table class="table table-striped table-hover" id="dataTable">
<thead class="table-dark sticky-top">
<tr id="tableHeaders"></tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 关于页面内容 -->
<div id="about-section" class="content-section">
<div class="content-header">
@ -2650,5 +3030,68 @@
</div>
</div>
<!-- 删除用户确认模态框 -->
<div class="modal fade" id="deleteUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-exclamation-triangle text-warning me-2"></i>
确认删除用户
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>您确定要删除这个用户吗?</p>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>警告:</strong>此操作将同时删除该用户的所有数据包括Cookie、关键词、卡券等且不可撤销
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" onclick="confirmDeleteUser()">
<i class="bi bi-trash"></i> 确认删除
</button>
</div>
</div>
</div>
</div>
<!-- 删除数据记录确认模态框 -->
<div class="modal fade" id="deleteRecordModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-exclamation-triangle text-warning me-2"></i>
确认删除记录
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>您确定要删除这条记录吗?</p>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
<strong>记录信息:</strong>
<div id="deleteRecordInfo" class="mt-2">
<!-- 记录信息将在这里显示 -->
</div>
</div>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>警告:</strong>此操作不可撤销!
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" onclick="confirmDeleteRecord()">
<i class="bi bi-trash"></i> 确认删除
</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -415,9 +415,40 @@
});
}
// 检查是否有有效的cookies账户
async function checkValidCookies() {
try {
const headers = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${apiBase}/cookies/check`, {
headers: headers
});
if (response.ok) {
const data = await response.json();
return data.hasValidCookies;
} else {
return false;
}
} catch (error) {
console.error('检查cookies失败:', error);
return false;
}
}
// 搜索多页商品数据
async function searchAllPages(keyword, totalPages) {
try {
// 检查是否有有效的cookies账户
const hasValidCookies = await checkValidCookies();
if (!hasValidCookies) {
showErrorMessage('搜索失败系统中不存在有效的账户信息。请先在Cookie管理中添加有效的闲鱼账户。');
return;
}
// 显示加载状态
showLoading(true);
hideResults();
@ -454,20 +485,36 @@
if (response.ok) {
const data = await response.json();
console.log('API返回的完整数据:', data);
if (data.success) {
allItems = data.data || [];
totalResults = allItems.length;
console.log('设置allItems:', allItems);
console.log('allItems长度:', allItems.length);
console.log('totalResults:', totalResults);
// 检查是否有错误信息
if (data.error) {
showErrorMessage(`搜索完成,但遇到问题: ${data.error}`);
}
console.log('调用displayPaginatedResults...');
displayPaginatedResults(data);
console.log('调用updateFrontendPagination...');
updateFrontendPagination();
} else {
throw new Error(data.message || '搜索失败');
console.error('API返回success=false:', data);
throw new Error(data.message || data.error || '搜索失败');
}
} else {
throw new Error(`HTTP ${response.status}`);
const errorText = await response.text();
throw new Error(`服务器错误 (${response.status}): ${errorText}`);
}
} catch (error) {
console.error('搜索失败:', error);
alert('搜索失败: ' + error.message);
showErrorMessage('搜索失败: ' + error.message);
} finally {
showLoading(false);
}
@ -527,7 +574,7 @@
}
} catch (error) {
console.error('搜索失败:', error);
alert('搜索失败: ' + error.message);
showErrorMessage('搜索失败: ' + error.message);
} finally {
showLoading(false);
}
@ -538,6 +585,36 @@
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
// 显示错误消息
function showErrorMessage(message) {
// 创建或更新错误提示
let errorAlert = document.getElementById('errorAlert');
if (!errorAlert) {
errorAlert = document.createElement('div');
errorAlert.id = 'errorAlert';
errorAlert.className = 'alert alert-danger alert-dismissible fade show';
errorAlert.innerHTML = `
<i class="bi bi-exclamation-triangle me-2"></i>
<span id="errorMessage"></span>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// 插入到搜索表单后面
const searchForm = document.querySelector('.search-form');
searchForm.parentNode.insertBefore(errorAlert, searchForm.nextSibling);
}
document.getElementById('errorMessage').textContent = message;
errorAlert.style.display = 'block';
// 5秒后自动隐藏
setTimeout(() => {
if (errorAlert) {
errorAlert.style.display = 'none';
}
}, 5000);
}
// 隐藏结果
function hideResults() {
document.getElementById('searchResults').innerHTML = '';
@ -548,10 +625,15 @@
// 显示分页搜索结果
function displayPaginatedResults(data) {
console.log('displayPaginatedResults被调用data:', data);
// 显示统计信息
const statsElement = document.getElementById('searchStats');
const statsText = document.getElementById('statsText');
console.log('statsElement:', statsElement);
console.log('statsText:', statsText);
// 检查数据来源
let dataSource = '';
if (data.is_real_data) {
@ -563,11 +645,14 @@
}
statsElement.style.display = 'block';
console.log('统计信息已显示');
// 使用统一的统计信息更新函数
console.log('调用updateStatsDisplay...');
updateStatsDisplay();
// 显示当前页的数据
console.log('调用displayCurrentPage...');
displayCurrentPage();
}
@ -577,14 +662,27 @@
// 显示当前页数据(全局函数)
function displayCurrentPage() {
console.log('displayCurrentPage被调用');
console.log('allItems:', allItems);
console.log('allItems.length:', allItems.length);
console.log('currentPage:', currentPage);
console.log('currentPageSize:', currentPageSize);
const resultsContainer = document.getElementById('searchResults');
console.log('resultsContainer:', resultsContainer);
// 计算当前页的数据范围
const startIndex = (currentPage - 1) * currentPageSize;
const endIndex = startIndex + currentPageSize;
const currentItems = allItems.slice(startIndex, endIndex);
console.log('startIndex:', startIndex);
console.log('endIndex:', endIndex);
console.log('currentItems:', currentItems);
console.log('currentItems.length:', currentItems.length);
if (currentItems.length === 0) {
console.log('没有数据,显示无结果提示');
document.getElementById('noResults').style.display = 'block';
resultsContainer.innerHTML = '';
return;
@ -593,7 +691,11 @@
document.getElementById('noResults').style.display = 'none';
// 生成商品卡片
resultsContainer.innerHTML = currentItems.map(item => createItemCard(item)).join('');
console.log('生成商品卡片...');
const cardsHtml = currentItems.map(item => createItemCard(item)).join('');
console.log('生成的HTML长度:', cardsHtml.length);
resultsContainer.innerHTML = cardsHtml;
console.log('商品卡片已设置到容器中');
}
// 显示搜索结果(保留原有功能)
@ -751,10 +853,21 @@
// 创建商品卡片HTML全局函数
function createItemCard(item) {
console.log('createItemCard被调用item数据:', item);
console.log('item的所有字段:', Object.keys(item));
const tags = item.tags ? item.tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('') : '';
const imageUrl = item.main_image || 'https://via.placeholder.com/200x200?text=暂无图片';
const wantCount = item.want_count || 0;
console.log('处理后的数据:', {
title: item.title,
price: item.price,
seller_name: item.seller_name,
imageUrl: imageUrl,
wantCount: wantCount
});
return `
<div class="col-md-6 col-lg-4 col-xl-3 mb-4">
<div class="card item-card h-100">

File diff suppressed because it is too large Load Diff

View File

@ -286,15 +286,43 @@
'Authorization': `Bearer ${token}`
}
})
.then(response => response.json())
.then(response => {
console.log('API响应状态:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('API响应数据:', data);
loadingDiv.style.display = 'none';
if (data.success === false) {
console.error('API返回错误:', data.message);
noLogsDiv.innerHTML = `
<div class="text-center p-4 text-danger">
<i class="bi bi-exclamation-triangle fs-1"></i>
<p class="mt-2">加载日志失败</p>
<p class="small">${data.message || '未知错误'}</p>
</div>
`;
noLogsDiv.style.display = 'block';
return;
}
if (data.logs && data.logs.length > 0) {
console.log(`显示 ${data.logs.length} 条日志`);
displayLogs(data.logs);
updateLogInfo(data);
logContainer.style.display = 'block';
} else {
console.log('没有日志数据');
noLogsDiv.innerHTML = `
<div class="text-center p-4 text-muted">
<i class="bi bi-file-text fs-1"></i>
<p class="mt-2">暂无日志数据</p>
</div>
`;
noLogsDiv.style.display = 'block';
}
@ -305,8 +333,14 @@
.catch(error => {
console.error('加载日志失败:', error);
loadingDiv.style.display = 'none';
noLogsDiv.innerHTML = `
<div class="text-center p-4 text-danger">
<i class="bi bi-exclamation-triangle fs-1"></i>
<p class="mt-2">加载日志失败</p>
<p class="small">${error.message}</p>
</div>
`;
noLogsDiv.style.display = 'block';
alert('加载日志失败');
});
}

View File

@ -57,6 +57,74 @@ class XianyuSearcher:
return default
return data
async def test_browser_launch(self):
"""测试浏览器是否能正常启动"""
try:
if not PLAYWRIGHT_AVAILABLE:
return False, "Playwright 未安装"
playwright = await async_playwright().start()
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
await page.goto("https://www.baidu.com")
await asyncio.sleep(2)
await browser.close()
return True, "浏览器测试成功"
except Exception as e:
return False, f"浏览器测试失败: {str(e)}"
async def get_first_valid_cookie(self):
"""获取第一个有效的cookie"""
try:
from db_manager import db_manager
# 获取所有cookies返回格式是 {id: value}
cookies = db_manager.get_all_cookies()
# 找到第一个有效的cookie长度大于50的认为是有效的
for cookie_id, cookie_value in cookies.items():
if len(cookie_value) > 50:
logger.info(f"找到有效cookie: {cookie_id}")
return {
'id': cookie_id,
'value': cookie_value
}
return None
except Exception as e:
logger.error(f"获取cookie失败: {str(e)}")
return None
async def set_browser_cookies(self, cookie_value: str):
"""设置浏览器cookies"""
try:
if not cookie_value:
return False
# 解析cookie字符串
cookies = []
for cookie_pair in cookie_value.split(';'):
cookie_pair = cookie_pair.strip()
if '=' in cookie_pair:
name, value = cookie_pair.split('=', 1)
cookies.append({
'name': name.strip(),
'value': value.strip(),
'domain': '.goofish.com',
'path': '/'
})
# 设置cookies到浏览器
await self.context.add_cookies(cookies)
logger.info(f"成功设置 {len(cookies)} 个cookies到浏览器")
return True
except Exception as e:
logger.error(f"设置浏览器cookies失败: {str(e)}")
return False
async def init_browser(self):
"""初始化浏览器"""
if not PLAYWRIGHT_AVAILABLE:
@ -65,60 +133,42 @@ class XianyuSearcher:
if not self.browser:
playwright = await async_playwright().start()
logger.info("正在启动浏览器...")
# Docker环境优化的浏览器启动参数
# 简化的浏览器启动参数,避免冲突
browser_args = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--disable-extensions',
'--disable-default-apps',
'--disable-sync',
'--disable-translate',
'--hide-scrollbars',
'--mute-audio',
'--no-default-browser-check',
'--no-pings',
'--single-process' # 在Docker中使用单进程模式
'--no-default-browser-check'
]
# 在Docker环境中添加额外参数
if os.getenv('DOCKER_ENV'):
# 只在确实是Docker环境时添加额外参数
if os.getenv('DOCKER_ENV') == 'true':
browser_args.extend([
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-client-side-phishing-detection',
'--disable-default-apps',
'--disable-hang-monitor',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-sync',
'--disable-web-resources',
'--metrics-recording-only',
'--no-first-run',
'--safebrowsing-disable-auto-update',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain'
'--disable-gpu',
'--single-process'
])
logger.info("正在启动浏览器...")
self.browser = await playwright.chromium.launch(
headless=True, # 无头模式
headless=True, # 无头模式,后台运行
args=browser_args
)
logger.info("浏览器启动成功,创建上下文...")
# 简化上下文创建,减少可能的问题
self.context = await self.browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
viewport={'width': 1280, 'height': 720}
)
logger.info("创建页面...")
self.page = await self.context.new_page()
logger.info("浏览器初始化完成")
async def close_browser(self):
"""关闭浏览器"""
if self.browser:
@ -192,8 +242,28 @@ class XianyuSearcher:
logger.warning(f"响应处理异常: {str(e)}")
try:
# 获取并设置cookies进行登录
logger.info("正在获取有效的cookies账户...")
cookie_data = await self.get_first_valid_cookie()
if not cookie_data:
raise Exception("未找到有效的cookies账户请先在Cookie管理中添加有效的闲鱼账户")
logger.info(f"使用账户: {cookie_data.get('id', 'unknown')}")
logger.info("正在访问闲鱼首页...")
await self.page.goto("https://www.goofish.com", timeout=30000)
# 设置cookies进行登录
logger.info("正在设置cookies进行登录...")
cookie_success = await self.set_browser_cookies(cookie_data.get('value', ''))
if not cookie_success:
logger.warning("设置cookies失败将以未登录状态继续")
else:
logger.info("✅ cookies设置成功已登录")
# 刷新页面以应用cookies
await self.page.reload()
await asyncio.sleep(2)
await self.page.wait_for_load_state("networkidle", timeout=10000)
logger.info(f"正在搜索关键词: {keyword}")
@ -499,6 +569,7 @@ class XianyuSearcher:
Returns:
搜索结果字典包含所有页面的items列表和总数
"""
browser_initialized = False
try:
if not PLAYWRIGHT_AVAILABLE:
logger.error("Playwright 不可用,无法获取真实数据")
@ -510,7 +581,15 @@ class XianyuSearcher:
logger.info(f"使用 Playwright 搜索多页闲鱼商品: 关键词='{keyword}', 总页数={total_pages}")
# 确保浏览器初始化
await self.init_browser()
browser_initialized = True
# 验证浏览器状态
if not self.browser or not self.page:
raise Exception("浏览器初始化失败")
logger.info("浏览器初始化成功,开始搜索...")
# 清空之前的API响应
self.api_responses = []
@ -552,22 +631,97 @@ class XianyuSearcher:
logger.warning(f"响应处理异常: {str(e)}")
try:
# 检查浏览器状态
if not self.page or self.page.is_closed():
raise Exception("页面已关闭或不可用")
# 获取并设置cookies进行登录
logger.info("正在获取有效的cookies账户...")
cookie_data = await self.get_first_valid_cookie()
if not cookie_data:
raise Exception("未找到有效的cookies账户请先在Cookie管理中添加有效的闲鱼账户")
logger.info(f"使用账户: {cookie_data.get('id', 'unknown')}")
logger.info("正在访问闲鱼首页...")
await self.page.goto("https://www.goofish.com", timeout=30000)
await self.page.wait_for_load_state("networkidle", timeout=10000)
# 设置cookies进行登录
logger.info("正在设置cookies进行登录...")
cookie_success = await self.set_browser_cookies(cookie_data.get('value', ''))
if not cookie_success:
logger.warning("设置cookies失败将以未登录状态继续")
else:
logger.info("✅ cookies设置成功已登录")
# 刷新页面以应用cookies
await self.page.reload()
await asyncio.sleep(2)
# 再次检查页面状态
if self.page.is_closed():
raise Exception("页面在导航后被关闭")
logger.info("等待页面加载完成...")
await self.page.wait_for_load_state("networkidle", timeout=15000)
# 等待页面稳定
logger.info("等待页面稳定...")
await asyncio.sleep(3) # 增加等待时间
# 再次检查页面状态
if self.page.is_closed():
raise Exception("页面在等待加载后被关闭")
# 获取页面标题和URL用于调试
page_title = await self.page.title()
page_url = self.page.url
logger.info(f"当前页面标题: {page_title}")
logger.info(f"当前页面URL: {page_url}")
logger.info(f"正在搜索关键词: {keyword}")
await self.page.fill('input[class*="search-input"]', keyword)
# 尝试多种搜索框选择器
search_selectors = [
'input[class*="search-input"]',
'input[placeholder*="搜索"]',
'input[type="text"]',
'.search-input',
'#search-input'
]
search_input = None
for selector in search_selectors:
try:
logger.info(f"尝试查找搜索框,选择器: {selector}")
search_input = await self.page.wait_for_selector(selector, timeout=5000)
if search_input:
logger.info(f"✅ 找到搜索框,使用选择器: {selector}")
break
except Exception as e:
logger.info(f"❌ 选择器 {selector} 未找到搜索框: {str(e)}")
continue
if not search_input:
raise Exception("未找到搜索框元素")
# 检查页面状态
if self.page.is_closed():
raise Exception("页面在查找搜索框后被关闭")
await search_input.fill(keyword)
logger.info(f"✅ 搜索关键词 '{keyword}' 已填入搜索框")
# 注册响应监听
self.page.on("response", on_response)
logger.info("🖱️ 准备点击搜索按钮...")
await self.page.click('button[type="submit"]')
logger.info("✅ 搜索按钮已点击")
await self.page.wait_for_load_state("networkidle", timeout=15000)
# 等待第一页API响应
logger.info("等待第一页API响应...")
await asyncio.sleep(5)
await asyncio.sleep(10) # 增加等待时间
# 尝试处理弹窗
try:
@ -661,17 +815,29 @@ class XianyuSearcher:
}
finally:
await self.close_browser()
# 确保浏览器被正确关闭
if browser_initialized:
try:
await self.close_browser()
logger.info("浏览器已安全关闭")
except Exception as close_error:
logger.warning(f"关闭浏览器时出错: {str(close_error)}")
except Exception as e:
error_msg = str(e)
logger.error(f"Playwright 多页搜索失败: {error_msg}")
# 检查是否是浏览器安装问题
# 检查是否是浏览器相关问题
if "Executable doesn't exist" in error_msg or "playwright install" in error_msg:
error_msg = "浏览器未安装。请在Docker容器中运行: playwright install chromium"
elif "BrowserType.launch" in error_msg:
error_msg = "浏览器启动失败。请确保Docker容器有足够的权限和资源"
elif "Target page, context or browser has been closed" in error_msg:
error_msg = "浏览器页面被意外关闭。这可能是由于网站反爬虫检测或系统资源限制导致的"
elif "Page.goto" in error_msg and "closed" in error_msg:
error_msg = "页面导航失败,浏览器连接已断开"
elif "Timeout" in error_msg and "exceeded" in error_msg:
error_msg = "页面加载超时。网络连接可能不稳定或网站响应缓慢"
# 如果 Playwright 失败,返回错误信息
return {
@ -840,12 +1006,11 @@ class XianyuSearcher:
return None
# 全局搜索器实例
_searcher = None
# 搜索器工具函数
async def search_xianyu_items(keyword: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""
搜索闲鱼商品的便捷函数
搜索闲鱼商品的便捷函数带重试机制
Args:
keyword: 搜索关键词
@ -855,25 +1020,58 @@ async def search_xianyu_items(keyword: str, page: int = 1, page_size: int = 20)
Returns:
搜索结果
"""
global _searcher
max_retries = 2
retry_delay = 5 # 秒,增加重试间隔
if not _searcher:
_searcher = XianyuSearcher()
for attempt in range(max_retries + 1):
searcher = None
try:
# 每次搜索都创建新的搜索器实例,避免浏览器状态混乱
searcher = XianyuSearcher()
try:
return await _searcher.search_items(keyword, page, page_size)
except Exception as e:
logger.error(f"搜索商品失败: {str(e)}")
return {
'items': [],
'total': 0,
'error': str(e)
}
logger.info(f"开始单页搜索,尝试次数: {attempt + 1}/{max_retries + 1}")
result = await searcher.search_items(keyword, page, page_size)
# 如果成功获取到数据,直接返回
if result.get('items') or not result.get('error'):
logger.info(f"单页搜索成功,获取到 {len(result.get('items', []))} 条数据")
return result
except Exception as e:
error_msg = str(e)
logger.error(f"搜索商品失败 (尝试 {attempt + 1}/{max_retries + 1}): {error_msg}")
# 如果是最后一次尝试,返回错误
if attempt == max_retries:
return {
'items': [],
'total': 0,
'error': f"搜索失败,已重试 {max_retries} 次: {error_msg}"
}
# 等待后重试
logger.info(f"等待 {retry_delay} 秒后重试...")
await asyncio.sleep(retry_delay)
finally:
# 确保搜索器被正确关闭
if searcher:
try:
await searcher.close_browser()
except Exception as close_error:
logger.warning(f"关闭搜索器时出错: {str(close_error)}")
# 理论上不会到达这里
return {
'items': [],
'total': 0,
'error': "未知错误"
}
async def search_multiple_pages_xianyu(keyword: str, total_pages: int = 1) -> Dict[str, Any]:
"""
搜索多页闲鱼商品的便捷函数
搜索多页闲鱼商品的便捷函数带重试机制
Args:
keyword: 搜索关键词
@ -882,27 +1080,55 @@ async def search_multiple_pages_xianyu(keyword: str, total_pages: int = 1) -> Di
Returns:
搜索结果
"""
global _searcher
max_retries = 2
retry_delay = 5 # 秒,增加重试间隔
if not _searcher:
_searcher = XianyuSearcher()
for attempt in range(max_retries + 1):
searcher = None
try:
# 每次搜索都创建新的搜索器实例,避免浏览器状态混乱
searcher = XianyuSearcher()
logger.info(f"开始多页搜索,尝试次数: {attempt + 1}/{max_retries + 1}")
result = await searcher.search_multiple_pages(keyword, total_pages)
# 如果成功获取到数据,直接返回
if result.get('items') or not result.get('error'):
logger.info(f"多页搜索成功,获取到 {len(result.get('items', []))} 条数据")
return result
except Exception as e:
error_msg = str(e)
logger.error(f"多页搜索商品失败 (尝试 {attempt + 1}/{max_retries + 1}): {error_msg}")
# 如果是最后一次尝试,返回错误
if attempt == max_retries:
return {
'items': [],
'total': 0,
'error': f"搜索失败,已重试 {max_retries} 次: {error_msg}"
}
# 等待后重试
logger.info(f"等待 {retry_delay} 秒后重试...")
await asyncio.sleep(retry_delay)
finally:
# 确保搜索器被正确关闭
if searcher:
try:
await searcher.close_browser()
except Exception as close_error:
logger.warning(f"关闭搜索器时出错: {str(close_error)}")
# 理论上不会到达这里
return {
'items': [],
'total': 0,
'error': "未知错误"
}
try:
return await _searcher.search_multiple_pages(keyword, total_pages)
except Exception as e:
logger.error(f"多页搜索商品失败: {str(e)}")
return {
'items': [],
'total': 0,
'error': str(e)
}
async def close_searcher():
"""关闭搜索器"""
global _searcher
if _searcher:
await _searcher.close_session()
_searcher = None
async def get_item_detail_from_api(item_id: str) -> Optional[str]:

View File

@ -492,11 +492,11 @@ class OrderDetailFetcher:
logger.warning(f"未找到或找到异常数量的 sku--u_ddZval 元素: {len(sku_elements)}")
print(f"⚠️ 未找到或找到异常数量的元素: {len(sku_elements)}")
# 如果没有找到sku--u_ddZval元素设置默认数量为0
# 如果没有找到sku--u_ddZval元素设置默认数量为1
if len(sku_elements) == 0:
result['quantity'] = '0'
logger.info("未找到sku--u_ddZval元素数量默认设置为0")
print("📦 数量默认设置为: 0")
result['quantity'] = '1'
logger.info("未找到sku--u_ddZval元素数量默认设置为1")
print("📦 数量默认设置为: 1")
# 尝试获取页面的所有class包含sku的元素进行调试
all_sku_elements = await self.page.query_selector_all('[class*="sku"]')
@ -507,11 +507,11 @@ class OrderDetailFetcher:
text_content = await element.text_content()
logger.info(f"SKU元素 {i+1}: class='{class_name}', text='{text_content}'")
# 确保数量字段存在,如果不存在则设置为0
# 确保数量字段存在,如果不存在则设置为1
if 'quantity' not in result:
result['quantity'] = '0'
logger.info("未获取到数量信息,默认设置为0")
print("📦 数量默认设置为: 0")
result['quantity'] = '1'
logger.info("未获取到数量信息,默认设置为1")
print("📦 数量默认设置为: 1")
# 打印最终结果
if result: