mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-08-30 01:27:35 +08:00
优化结构
This commit is contained in:
parent
b508c3e858
commit
5e5fee9d5b
243
.env
243
.env
@ -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
3
.gitignore
vendored
@ -193,9 +193,6 @@ dmypy.json
|
|||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
# ==================== 项目特定新增 ====================
|
# ==================== 项目特定新增 ====================
|
||||||
# 环境变量文件
|
|
||||||
.env.example
|
|
||||||
.env.*.example
|
|
||||||
|
|
||||||
# 数据库文件
|
# 数据库文件
|
||||||
*.db-journal
|
*.db-journal
|
||||||
|
@ -409,7 +409,6 @@ python Start.py
|
|||||||
- **`docker-deploy.bat`** - Windows版本部署脚本,支持Windows环境一键部署
|
- **`docker-deploy.bat`** - Windows版本部署脚本,支持Windows环境一键部署
|
||||||
- **`entrypoint.sh`** - Docker容器启动脚本,处理环境初始化和服务启动
|
- **`entrypoint.sh`** - Docker容器启动脚本,处理环境初始化和服务启动
|
||||||
- **`nginx/nginx.conf`** - Nginx反向代理配置,支持负载均衡和SSL终端
|
- **`nginx/nginx.conf`** - Nginx反向代理配置,支持负载均衡和SSL终端
|
||||||
- **`.env`** - 环境变量配置文件,包含所有可配置的系统参数和敏感信息
|
|
||||||
- **`requirements.txt`** - Python依赖包列表,精简版本无冗余依赖,按功能分类组织
|
- **`requirements.txt`** - Python依赖包列表,精简版本无冗余依赖,按功能分类组织
|
||||||
|
|
||||||
## ⚙️ 配置说明
|
## ⚙️ 配置说明
|
||||||
@ -517,7 +516,7 @@ python Start.py
|
|||||||
## ❓ 常见问题
|
## ❓ 常见问题
|
||||||
|
|
||||||
### 1. 端口被占用
|
### 1. 端口被占用
|
||||||
如果8080端口被占用,可以修改 `.env` 文件中的 `WEB_PORT` 配置。
|
如果8080端口被占用,可以修改 `global_config.yml` 文件中的 `AUTO_REPLY.api.port` 配置,或者在 Docker 启动时通过环境变量 `WEB_PORT` 指定端口。
|
||||||
|
|
||||||
### 2. 数据库连接失败
|
### 2. 数据库连接失败
|
||||||
检查数据库文件权限,确保应用有读写权限。
|
检查数据库文件权限,确保应用有读写权限。
|
||||||
|
@ -457,6 +457,19 @@ class XianyuLive:
|
|||||||
item_id: str, chat_id: str, msg_time: str):
|
item_id: str, chat_id: str, msg_time: str):
|
||||||
"""统一处理自动发货逻辑"""
|
"""统一处理自动发货逻辑"""
|
||||||
try:
|
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
|
# 提取订单ID
|
||||||
order_id = self._extract_order_id(message)
|
order_id = self._extract_order_id(message)
|
||||||
|
|
||||||
@ -2183,23 +2196,28 @@ class XianyuLive:
|
|||||||
|
|
||||||
# 插入或更新订单信息到数据库
|
# 插入或更新订单信息到数据库
|
||||||
try:
|
try:
|
||||||
success = db_manager.insert_or_update_order(
|
# 检查cookie_id是否在cookies表中存在
|
||||||
order_id=order_id,
|
cookie_info = db_manager.get_cookie_by_id(self.cookie_id)
|
||||||
item_id=item_id,
|
if not cookie_info:
|
||||||
buyer_id=buyer_id,
|
logger.warning(f"Cookie ID {self.cookie_id} 不存在于cookies表中,丢弃订单 {order_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:
|
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:
|
except Exception as db_e:
|
||||||
logger.error(f"【{self.cookie_id}】保存订单信息到数据库失败: {self._safe_str(db_e)}")
|
logger.error(f"【{self.cookie_id}】保存订单信息到数据库失败: {self._safe_str(db_e)}")
|
||||||
@ -2438,17 +2456,23 @@ class XianyuLive:
|
|||||||
# 保存订单基本信息到数据库(如果还没有详细信息)
|
# 保存订单基本信息到数据库(如果还没有详细信息)
|
||||||
try:
|
try:
|
||||||
from db_manager import db_manager
|
from db_manager import db_manager
|
||||||
existing_order = db_manager.get_order_by_id(order_id)
|
|
||||||
if not existing_order:
|
# 检查cookie_id是否在cookies表中存在
|
||||||
# 插入基本订单信息
|
cookie_info = db_manager.get_cookie_by_id(self.cookie_id)
|
||||||
db_manager.insert_or_update_order(
|
if not cookie_info:
|
||||||
order_id=order_id,
|
logger.warning(f"Cookie ID {self.cookie_id} 不存在于cookies表中,丢弃订单 {order_id}")
|
||||||
item_id=item_id,
|
else:
|
||||||
buyer_id=send_user_id,
|
existing_order = db_manager.get_order_by_id(order_id)
|
||||||
order_status='processing', # 处理中状态
|
if not existing_order:
|
||||||
cookie_id=self.cookie_id
|
# 插入基本订单信息
|
||||||
)
|
db_manager.insert_or_update_order(
|
||||||
logger.info(f"保存基本订单信息到数据库: {order_id}")
|
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:
|
except Exception as db_e:
|
||||||
logger.error(f"保存基本订单信息失败: {self._safe_str(db_e)}")
|
logger.error(f"保存基本订单信息失败: {self._safe_str(db_e)}")
|
||||||
|
|
||||||
@ -3346,6 +3370,20 @@ class XianyuLive:
|
|||||||
# 检查是否为"我已小刀,待刀成"
|
# 检查是否为"我已小刀,待刀成"
|
||||||
if card_title == "我已小刀,待刀成":
|
if card_title == "我已小刀,待刀成":
|
||||||
logger.info(f'[{msg_time}] 【{self.cookie_id}】【系统】检测到"我已小刀,待刀成",即使在暂停期间也继续处理')
|
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
|
# 提取订单ID
|
||||||
order_id = self._extract_order_id(message)
|
order_id = self._extract_order_id(message)
|
||||||
if order_id:
|
if order_id:
|
||||||
|
@ -1120,6 +1120,8 @@ class DBManager:
|
|||||||
logger.error(f"获取所有Cookie失败: {e}")
|
logger.error(f"获取所有Cookie失败: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_cookie_by_id(self, cookie_id: str) -> Optional[Dict[str, str]]:
|
def get_cookie_by_id(self, cookie_id: str) -> Optional[Dict[str, str]]:
|
||||||
"""根据ID获取Cookie信息
|
"""根据ID获取Cookie信息
|
||||||
|
|
||||||
@ -4055,6 +4057,14 @@ class DBManager:
|
|||||||
try:
|
try:
|
||||||
cursor = self.conn.cursor()
|
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,))
|
cursor.execute("SELECT order_id FROM orders WHERE order_id = ?", (order_id,))
|
||||||
existing = cursor.fetchone()
|
existing = cursor.fetchone()
|
||||||
@ -4189,14 +4199,21 @@ class DBManager:
|
|||||||
primary_key_map = {
|
primary_key_map = {
|
||||||
'users': 'id',
|
'users': 'id',
|
||||||
'cookies': 'id',
|
'cookies': 'id',
|
||||||
|
'cookie_status': 'id',
|
||||||
'keywords': 'id',
|
'keywords': 'id',
|
||||||
'default_replies': 'id',
|
'default_replies': 'id',
|
||||||
|
'default_reply_records': 'id',
|
||||||
|
'item_replay': 'item_id',
|
||||||
'ai_reply_settings': 'id',
|
'ai_reply_settings': 'id',
|
||||||
|
'ai_conversations': 'id',
|
||||||
|
'ai_item_cache': 'id',
|
||||||
|
'item_info': 'id',
|
||||||
'message_notifications': 'id',
|
'message_notifications': 'id',
|
||||||
'cards': 'id',
|
'cards': 'id',
|
||||||
'delivery_rules': 'id',
|
'delivery_rules': 'id',
|
||||||
'notification_channels': 'id',
|
'notification_channels': 'id',
|
||||||
'user_settings': 'id',
|
'user_settings': 'id',
|
||||||
|
'system_settings': 'id',
|
||||||
'email_verifications': 'id',
|
'email_verifications': 'id',
|
||||||
'captcha_codes': 'id',
|
'captcha_codes': 'id',
|
||||||
'orders': 'order_id'
|
'orders': 'order_id'
|
||||||
|
@ -57,9 +57,7 @@ services:
|
|||||||
- TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600}
|
- TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600}
|
||||||
- TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300}
|
- TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300}
|
||||||
- MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000}
|
- MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000}
|
||||||
env_file:
|
|
||||||
- path: .env
|
|
||||||
required: false
|
|
||||||
networks:
|
networks:
|
||||||
- xianyu-network
|
- xianyu-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
@ -57,9 +57,7 @@ services:
|
|||||||
- TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600}
|
- TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600}
|
||||||
- TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300}
|
- TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300}
|
||||||
- MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000}
|
- MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000}
|
||||||
env_file:
|
|
||||||
- path: .env
|
|
||||||
required: false
|
|
||||||
networks:
|
networks:
|
||||||
- xianyu-network
|
- xianyu-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
@ -10,7 +10,6 @@ title 闲鱼自动回复系统 Docker 部署
|
|||||||
REM 项目配置
|
REM 项目配置
|
||||||
set PROJECT_NAME=xianyu-auto-reply
|
set PROJECT_NAME=xianyu-auto-reply
|
||||||
set COMPOSE_FILE=docker-compose.yml
|
set COMPOSE_FILE=docker-compose.yml
|
||||||
set ENV_FILE=.env
|
|
||||||
|
|
||||||
REM 颜色定义(Windows CMD不支持ANSI颜色,使用echo代替)
|
REM 颜色定义(Windows CMD不支持ANSI颜色,使用echo代替)
|
||||||
set "INFO_PREFIX=[INFO]"
|
set "INFO_PREFIX=[INFO]"
|
||||||
@ -41,13 +40,6 @@ echo %SUCCESS_PREFIX% 系统依赖检查通过
|
|||||||
REM 初始化配置
|
REM 初始化配置
|
||||||
echo %INFO_PREFIX% 初始化配置文件...
|
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 检查关键文件
|
REM 检查关键文件
|
||||||
if not exist "entrypoint.sh" (
|
if not exist "entrypoint.sh" (
|
||||||
echo %ERROR_PREFIX% entrypoint.sh 文件不存在,Docker容器将无法启动
|
echo %ERROR_PREFIX% entrypoint.sh 文件不存在,Docker容器将无法启动
|
||||||
@ -58,6 +50,15 @@ if not exist "entrypoint.sh" (
|
|||||||
echo %SUCCESS_PREFIX% 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 创建必要的目录
|
REM 创建必要的目录
|
||||||
if not exist "data" mkdir data
|
if not exist "data" mkdir data
|
||||||
if not exist "logs" mkdir logs
|
if not exist "logs" mkdir logs
|
||||||
|
@ -15,7 +15,6 @@ NC='\033[0m' # No Color
|
|||||||
# 项目配置
|
# 项目配置
|
||||||
PROJECT_NAME="xianyu-auto-reply"
|
PROJECT_NAME="xianyu-auto-reply"
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
COMPOSE_FILE="docker-compose.yml"
|
||||||
ENV_FILE=".env"
|
|
||||||
|
|
||||||
# 打印带颜色的消息
|
# 打印带颜色的消息
|
||||||
print_info() {
|
print_info() {
|
||||||
@ -55,13 +54,6 @@ check_dependencies() {
|
|||||||
init_config() {
|
init_config() {
|
||||||
print_info "初始化配置文件..."
|
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
|
if [ ! -f "entrypoint.sh" ]; then
|
||||||
print_error "entrypoint.sh 文件不存在,Docker容器将无法启动"
|
print_error "entrypoint.sh 文件不存在,Docker容器将无法启动"
|
||||||
@ -71,6 +63,14 @@ init_config() {
|
|||||||
print_success "entrypoint.sh 文件已存在"
|
print_success "entrypoint.sh 文件已存在"
|
||||||
fi
|
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
|
mkdir -p data logs backups static/uploads/images
|
||||||
print_success "已创建必要的目录"
|
print_success "已创建必要的目录"
|
||||||
|
125
reply_server.py
125
reply_server.py
@ -2742,7 +2742,11 @@ async def search_items(
|
|||||||
current_user: Optional[Dict[str, Any]] = Depends(get_current_user_optional)
|
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:
|
try:
|
||||||
|
logger.info(f"{user_info} 开始单页搜索: 关键词='{search_request.keyword}', 页码={search_request.page}, 每页={search_request.page_size}")
|
||||||
|
|
||||||
from utils.item_search import search_xianyu_items
|
from utils.item_search import search_xianyu_items
|
||||||
|
|
||||||
# 执行搜索
|
# 执行搜索
|
||||||
@ -2752,18 +2756,84 @@ async def search_items(
|
|||||||
page_size=search_request.page_size
|
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,
|
"success": True,
|
||||||
"data": result.get("items", []),
|
"data": result.get("items", []),
|
||||||
"total": result.get("total", 0),
|
"total": result.get("total", 0),
|
||||||
"page": search_request.page,
|
"page": search_request.page,
|
||||||
"page_size": search_request.page_size,
|
"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")
|
@app.post("/items/search_multiple")
|
||||||
async def search_multiple_pages(
|
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)
|
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:
|
try:
|
||||||
|
logger.info(f"{user_info} 开始多页搜索: 关键词='{search_request.keyword}', 页数={search_request.total_pages}")
|
||||||
|
|
||||||
from utils.item_search import search_multiple_pages_xianyu
|
from utils.item_search import search_multiple_pages_xianyu
|
||||||
|
|
||||||
# 执行多页搜索
|
# 执行多页搜索
|
||||||
@ -2780,7 +2854,14 @@ async def search_multiple_pages(
|
|||||||
total_pages=search_request.total_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,
|
"success": True,
|
||||||
"data": result.get("items", []),
|
"data": result.get("items", []),
|
||||||
"total": result.get("total", 0),
|
"total": result.get("total", 0),
|
||||||
@ -2790,9 +2871,17 @@ async def search_multiple_pages(
|
|||||||
"is_fallback": result.get("is_fallback", False),
|
"is_fallback": result.get("is_fallback", False),
|
||||||
"source": result.get("source", "unknown")
|
"source": result.get("source", "unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 如果有错误信息,也包含在响应中
|
||||||
|
if has_error:
|
||||||
|
response_data["error"] = has_error
|
||||||
|
|
||||||
|
return response_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"多页商品搜索失败: {str(e)}")
|
error_msg = str(e)
|
||||||
raise HTTPException(status_code=500, detail=f"多页商品搜索失败: {str(e)}")
|
logger.error(f"{user_info} 多页商品搜索失败: {error_msg}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"多页商品搜索失败: {error_msg}")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/items/detail/{item_id}")
|
@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")
|
log_files = glob.glob("logs/xianyu_*.log")
|
||||||
|
logger.info(f"找到日志文件: {log_files}")
|
||||||
|
|
||||||
if not 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)
|
latest_log_file = max(log_files, key=os.path.getctime)
|
||||||
|
logger.info(f"使用最新日志文件: {latest_log_file}")
|
||||||
|
|
||||||
logs = []
|
logs = []
|
||||||
try:
|
try:
|
||||||
with open(latest_log_file, 'r', encoding='utf-8') as f:
|
with open(latest_log_file, 'r', encoding='utf-8') as f:
|
||||||
all_lines = f.readlines()
|
all_lines = f.readlines()
|
||||||
|
logger.info(f"读取到 {len(all_lines)} 行日志")
|
||||||
|
|
||||||
# 如果指定了日志级别,进行过滤
|
# 如果指定了日志级别,进行过滤
|
||||||
if level:
|
if level:
|
||||||
filtered_lines = [line for line in all_lines if f"| {level.upper()} |" in line]
|
filtered_lines = [line for line in all_lines if f"| {level.upper()} |" in line]
|
||||||
|
logger.info(f"按级别 {level} 过滤后剩余 {len(filtered_lines)} 行")
|
||||||
else:
|
else:
|
||||||
filtered_lines = all_lines
|
filtered_lines = all_lines
|
||||||
|
|
||||||
# 获取最后N行
|
# 获取最后N行
|
||||||
recent_lines = filtered_lines[-lines:] if len(filtered_lines) > lines else filtered_lines
|
recent_lines = filtered_lines[-lines:] if len(filtered_lines) > lines else filtered_lines
|
||||||
|
logger.info(f"取最后 {len(recent_lines)} 行日志")
|
||||||
|
|
||||||
for line in recent_lines:
|
for line in recent_lines:
|
||||||
logs.append(line.strip())
|
logs.append(line.strip())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"读取日志文件失败: {str(e)}")
|
||||||
log_with_user('error', f"读取日志文件失败: {str(e)}", admin_user)
|
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)
|
log_with_user('info', f"返回日志记录 {len(logs)} 条", admin_user)
|
||||||
|
logger.info(f"成功返回 {len(logs)} 条日志记录")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"logs": logs,
|
"logs": logs,
|
||||||
"log_file": latest_log_file,
|
"log_file": latest_log_file,
|
||||||
"total_lines": len(logs)
|
"total_lines": len(logs),
|
||||||
|
"success": True
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"获取系统日志失败: {str(e)}")
|
||||||
log_with_user('error', f"获取系统日志失败: {str(e)}", admin_user)
|
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')
|
@app.get('/admin/stats')
|
||||||
def get_system_stats(admin_user: Dict[str, Any] = Depends(require_admin)):
|
def get_system_stats(admin_user: Dict[str, Any] = Depends(require_admin)):
|
||||||
|
@ -18,7 +18,6 @@ httpx>=0.25.0
|
|||||||
|
|
||||||
# ==================== 配置文件处理 ====================
|
# ==================== 配置文件处理 ====================
|
||||||
PyYAML>=6.0.0
|
PyYAML>=6.0.0
|
||||||
python-dotenv>=1.0.1
|
|
||||||
|
|
||||||
# ==================== JavaScript执行引擎 ====================
|
# ==================== JavaScript执行引擎 ====================
|
||||||
PyExecJS>=1.5.1
|
PyExecJS>=1.5.1
|
||||||
|
@ -165,12 +165,8 @@ class SecureConfirm:
|
|||||||
error_msg = res_json.get('ret', ['未知错误'])[0] if res_json.get('ret') else '未知错误'
|
error_msg = res_json.get('ret', ['未知错误'])[0] if res_json.get('ret') else '未知错误'
|
||||||
logger.warning(f"【{self.cookie_id}】❌ 自动确认发货失败: {error_msg}")
|
logger.warning(f"【{self.cookie_id}】❌ 自动确认发货失败: {error_msg}")
|
||||||
|
|
||||||
# 如果是token相关错误,进行重试
|
return await self.auto_confirm(order_id, item_id, retry_count + 1)
|
||||||
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:
|
except Exception as e:
|
||||||
logger.error(f"【{self.cookie_id}】自动确认发货API请求异常: {self._safe_str(e)}")
|
logger.error(f"【{self.cookie_id}】自动确认发货API请求异常: {self._safe_str(e)}")
|
||||||
|
@ -115,12 +115,8 @@ class SecureFreeshipping:
|
|||||||
error_msg = res_json.get('ret', ['未知错误'])[0] if res_json.get('ret') else '未知错误'
|
error_msg = res_json.get('ret', ['未知错误'])[0] if res_json.get('ret') else '未知错误'
|
||||||
logger.warning(f"【{self.cookie_id}】❌ 自动免拼发货失败: {error_msg}")
|
logger.warning(f"【{self.cookie_id}】❌ 自动免拼发货失败: {error_msg}")
|
||||||
|
|
||||||
# 如果是token相关错误,进行重试
|
return await self.auto_freeshipping(order_id, item_id, buyer_id, retry_count + 1)
|
||||||
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 {"error": error_msg, "order_id": order_id}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"【{self.cookie_id}】自动免拼发货API请求异常: {self._safe_str(e)}")
|
logger.error(f"【{self.cookie_id}】自动免拼发货API请求异常: {self._safe_str(e)}")
|
||||||
|
241
static/css/admin.css
Normal file
241
static/css/admin.css
Normal 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%);
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
@import url('items.css');
|
@import url('items.css');
|
||||||
@import url('notifications.css');
|
@import url('notifications.css');
|
||||||
@import url('components.css');
|
@import url('components.css');
|
||||||
|
@import url('admin.css');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,3 +75,73 @@
|
|||||||
.log-container::-webkit-scrollbar-thumb:hover {
|
.log-container::-webkit-scrollbar-thumb:hover {
|
||||||
background: #5a5a5c;
|
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;
|
||||||
|
}
|
||||||
|
@ -85,7 +85,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item">
|
<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>
|
<i class="bi bi-search"></i>
|
||||||
商品搜索
|
商品搜索
|
||||||
</a>
|
</a>
|
||||||
@ -103,19 +103,19 @@
|
|||||||
<small class="text-white-50">管理员功能</small>
|
<small class="text-white-50">管理员功能</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item">
|
<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>
|
<i class="bi bi-people"></i>
|
||||||
用户管理
|
用户管理
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item">
|
<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>
|
<i class="bi bi-file-text-fill"></i>
|
||||||
系统日志
|
系统日志
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item">
|
<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>
|
<i class="bi bi-database"></i>
|
||||||
数据管理
|
数据管理
|
||||||
</a>
|
</a>
|
||||||
@ -411,7 +411,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="itemsTableBody">
|
<tbody id="itemsTableBody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center text-muted">加载中...</td>
|
<td colspan="9" class="text-center text-muted">加载中...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -1214,58 +1214,257 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-body">
|
<div class="content-body">
|
||||||
<!-- 日志控制 -->
|
<!-- 日志控制面板 -->
|
||||||
<div class="row mb-3">
|
<div class="card mb-4">
|
||||||
<div class="col-md-3">
|
<div class="card-header">
|
||||||
<label for="logLines" class="form-label">显示行数</label>
|
<h5 class="mb-0">
|
||||||
<select class="form-select" id="logLines" onchange="refreshLogs()">
|
<i class="bi bi-sliders"></i> 日志控制
|
||||||
<option value="100">100行</option>
|
</h5>
|
||||||
<option value="200" selected>200行</option>
|
|
||||||
<option value="500">500行</option>
|
|
||||||
<option value="1000">1000行</option>
|
|
||||||
<option value="2000">2000行</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<div class="card-body">
|
||||||
<label class="form-label"> </label>
|
<div class="row align-items-center">
|
||||||
<div class="d-flex gap-2">
|
<div class="col-md-3">
|
||||||
<button class="btn btn-outline-primary" onclick="refreshLogs()">
|
<label class="form-label">显示行数</label>
|
||||||
<i class="bi bi-arrow-clockwise me-1"></i>刷新日志
|
<select class="form-select" id="logLines" onchange="loadSystemLogs()">
|
||||||
</button>
|
<option value="50">50行</option>
|
||||||
<button class="btn btn-outline-secondary" onclick="clearLogsDisplay()">
|
<option value="100" selected>100行</option>
|
||||||
<i class="bi bi-trash me-1"></i>清空显示
|
<option value="200">200行</option>
|
||||||
</button>
|
<option value="500">500行</option>
|
||||||
<button class="btn btn-outline-danger" onclick="clearLogsServer()">
|
<option value="1000">1000行</option>
|
||||||
<i class="bi bi-trash3 me-1"></i>清空服务器日志
|
</select>
|
||||||
</button>
|
</div>
|
||||||
<button class="btn btn-outline-info" onclick="showLogStats()">
|
<div class="col-md-4">
|
||||||
<i class="bi bi-bar-chart me-1"></i>统计信息
|
<label class="form-label">日志级别过滤</label>
|
||||||
</button>
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<button class="btn btn-outline-info" onclick="toggleAutoRefresh()">
|
<span class="badge bg-secondary filter-badge active" data-level="" onclick="filterLogsByLevel('')">
|
||||||
<i class="bi bi-play-circle me-1"></i><span id="autoRefreshText">开启自动刷新</span>
|
全部
|
||||||
</button>
|
</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"> </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>
|
||||||
</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">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<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">
|
<div class="d-flex gap-2">
|
||||||
<span class="badge bg-secondary" id="logCount">0 条日志</span>
|
<button class="btn btn-sm btn-outline-secondary" onclick="scrollLogToTop()">
|
||||||
<span class="badge bg-info" id="lastUpdate">未更新</span>
|
<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>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div id="logContainer" class="log-container">
|
<div id="loadingSystemLogs" class="text-center py-4">
|
||||||
<div class="text-center p-4 text-muted">
|
<div class="spinner-border" role="status">
|
||||||
<i class="bi bi-file-text fs-1"></i>
|
<span class="visually-hidden">加载中...</span>
|
||||||
<p class="mt-2">点击刷新按钮加载日志</p>
|
</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"> </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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1444,6 +1643,187 @@
|
|||||||
</div>
|
</div>
|
||||||
</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"> </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 id="about-section" class="content-section">
|
||||||
<div class="content-header">
|
<div class="content-header">
|
||||||
@ -2650,5 +3030,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</body>
|
||||||
</html>
|
</html>
|
@ -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) {
|
async function searchAllPages(keyword, totalPages) {
|
||||||
try {
|
try {
|
||||||
|
// 检查是否有有效的cookies账户
|
||||||
|
const hasValidCookies = await checkValidCookies();
|
||||||
|
if (!hasValidCookies) {
|
||||||
|
showErrorMessage('搜索失败:系统中不存在有效的账户信息。请先在Cookie管理中添加有效的闲鱼账户。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
showLoading(true);
|
showLoading(true);
|
||||||
hideResults();
|
hideResults();
|
||||||
@ -454,20 +485,36 @@
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('API返回的完整数据:', data);
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
allItems = data.data || [];
|
allItems = data.data || [];
|
||||||
totalResults = allItems.length;
|
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);
|
displayPaginatedResults(data);
|
||||||
|
console.log('调用updateFrontendPagination...');
|
||||||
updateFrontendPagination();
|
updateFrontendPagination();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.message || '搜索失败');
|
console.error('API返回success=false:', data);
|
||||||
|
throw new Error(data.message || data.error || '搜索失败');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
const errorText = await response.text();
|
||||||
|
throw new Error(`服务器错误 (${response.status}): ${errorText}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索失败:', error);
|
console.error('搜索失败:', error);
|
||||||
alert('搜索失败: ' + error.message);
|
showErrorMessage('搜索失败: ' + error.message);
|
||||||
} finally {
|
} finally {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
}
|
}
|
||||||
@ -527,7 +574,7 @@
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索失败:', error);
|
console.error('搜索失败:', error);
|
||||||
alert('搜索失败: ' + error.message);
|
showErrorMessage('搜索失败: ' + error.message);
|
||||||
} finally {
|
} finally {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
}
|
}
|
||||||
@ -538,6 +585,36 @@
|
|||||||
document.getElementById('loading').style.display = show ? 'block' : 'none';
|
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() {
|
function hideResults() {
|
||||||
document.getElementById('searchResults').innerHTML = '';
|
document.getElementById('searchResults').innerHTML = '';
|
||||||
@ -548,10 +625,15 @@
|
|||||||
|
|
||||||
// 显示分页搜索结果
|
// 显示分页搜索结果
|
||||||
function displayPaginatedResults(data) {
|
function displayPaginatedResults(data) {
|
||||||
|
console.log('displayPaginatedResults被调用,data:', data);
|
||||||
|
|
||||||
// 显示统计信息
|
// 显示统计信息
|
||||||
const statsElement = document.getElementById('searchStats');
|
const statsElement = document.getElementById('searchStats');
|
||||||
const statsText = document.getElementById('statsText');
|
const statsText = document.getElementById('statsText');
|
||||||
|
|
||||||
|
console.log('statsElement:', statsElement);
|
||||||
|
console.log('statsText:', statsText);
|
||||||
|
|
||||||
// 检查数据来源
|
// 检查数据来源
|
||||||
let dataSource = '';
|
let dataSource = '';
|
||||||
if (data.is_real_data) {
|
if (data.is_real_data) {
|
||||||
@ -563,11 +645,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
statsElement.style.display = 'block';
|
statsElement.style.display = 'block';
|
||||||
|
console.log('统计信息已显示');
|
||||||
|
|
||||||
// 使用统一的统计信息更新函数
|
// 使用统一的统计信息更新函数
|
||||||
|
console.log('调用updateStatsDisplay...');
|
||||||
updateStatsDisplay();
|
updateStatsDisplay();
|
||||||
|
|
||||||
// 显示当前页的数据
|
// 显示当前页的数据
|
||||||
|
console.log('调用displayCurrentPage...');
|
||||||
displayCurrentPage();
|
displayCurrentPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,14 +662,27 @@
|
|||||||
|
|
||||||
// 显示当前页数据(全局函数)
|
// 显示当前页数据(全局函数)
|
||||||
function displayCurrentPage() {
|
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');
|
const resultsContainer = document.getElementById('searchResults');
|
||||||
|
console.log('resultsContainer:', resultsContainer);
|
||||||
|
|
||||||
// 计算当前页的数据范围
|
// 计算当前页的数据范围
|
||||||
const startIndex = (currentPage - 1) * currentPageSize;
|
const startIndex = (currentPage - 1) * currentPageSize;
|
||||||
const endIndex = startIndex + currentPageSize;
|
const endIndex = startIndex + currentPageSize;
|
||||||
const currentItems = allItems.slice(startIndex, endIndex);
|
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) {
|
if (currentItems.length === 0) {
|
||||||
|
console.log('没有数据,显示无结果提示');
|
||||||
document.getElementById('noResults').style.display = 'block';
|
document.getElementById('noResults').style.display = 'block';
|
||||||
resultsContainer.innerHTML = '';
|
resultsContainer.innerHTML = '';
|
||||||
return;
|
return;
|
||||||
@ -593,7 +691,11 @@
|
|||||||
document.getElementById('noResults').style.display = 'none';
|
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(全局函数)
|
// 创建商品卡片HTML(全局函数)
|
||||||
function createItemCard(item) {
|
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 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 imageUrl = item.main_image || 'https://via.placeholder.com/200x200?text=暂无图片';
|
||||||
const wantCount = item.want_count || 0;
|
const wantCount = item.want_count || 0;
|
||||||
|
|
||||||
|
console.log('处理后的数据:', {
|
||||||
|
title: item.title,
|
||||||
|
price: item.price,
|
||||||
|
seller_name: item.seller_name,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
wantCount: wantCount
|
||||||
|
});
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="col-md-6 col-lg-4 col-xl-3 mb-4">
|
<div class="col-md-6 col-lg-4 col-xl-3 mb-4">
|
||||||
<div class="card item-card h-100">
|
<div class="card item-card h-100">
|
||||||
|
1181
static/js/app.js
1181
static/js/app.js
File diff suppressed because it is too large
Load Diff
@ -286,27 +286,61 @@
|
|||||||
'Authorization': `Bearer ${token}`
|
'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 => {
|
.then(data => {
|
||||||
|
console.log('API响应数据:', data);
|
||||||
loadingDiv.style.display = 'none';
|
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) {
|
if (data.logs && data.logs.length > 0) {
|
||||||
|
console.log(`显示 ${data.logs.length} 条日志`);
|
||||||
displayLogs(data.logs);
|
displayLogs(data.logs);
|
||||||
updateLogInfo(data);
|
updateLogInfo(data);
|
||||||
logContainer.style.display = 'block';
|
logContainer.style.display = 'block';
|
||||||
} else {
|
} 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';
|
noLogsDiv.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新最后更新时间
|
// 更新最后更新时间
|
||||||
document.getElementById('lastUpdate').textContent =
|
document.getElementById('lastUpdate').textContent =
|
||||||
'最后更新: ' + new Date().toLocaleTimeString('zh-CN');
|
'最后更新: ' + new Date().toLocaleTimeString('zh-CN');
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('加载日志失败:', error);
|
console.error('加载日志失败:', error);
|
||||||
loadingDiv.style.display = 'none';
|
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';
|
noLogsDiv.style.display = 'block';
|
||||||
alert('加载日志失败');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +57,74 @@ class XianyuSearcher:
|
|||||||
return default
|
return default
|
||||||
return data
|
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):
|
async def init_browser(self):
|
||||||
"""初始化浏览器"""
|
"""初始化浏览器"""
|
||||||
if not PLAYWRIGHT_AVAILABLE:
|
if not PLAYWRIGHT_AVAILABLE:
|
||||||
@ -65,60 +133,42 @@ class XianyuSearcher:
|
|||||||
if not self.browser:
|
if not self.browser:
|
||||||
playwright = await async_playwright().start()
|
playwright = await async_playwright().start()
|
||||||
logger.info("正在启动浏览器...")
|
logger.info("正在启动浏览器...")
|
||||||
# Docker环境优化的浏览器启动参数
|
# 简化的浏览器启动参数,避免冲突
|
||||||
browser_args = [
|
browser_args = [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
'--disable-setuid-sandbox',
|
'--disable-setuid-sandbox',
|
||||||
'--disable-dev-shm-usage',
|
'--disable-dev-shm-usage',
|
||||||
'--disable-accelerated-2d-canvas',
|
|
||||||
'--no-first-run',
|
'--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-extensions',
|
||||||
'--disable-default-apps',
|
'--disable-default-apps',
|
||||||
'--disable-sync',
|
'--no-default-browser-check'
|
||||||
'--disable-translate',
|
|
||||||
'--hide-scrollbars',
|
|
||||||
'--mute-audio',
|
|
||||||
'--no-default-browser-check',
|
|
||||||
'--no-pings',
|
|
||||||
'--single-process' # 在Docker中使用单进程模式
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# 在Docker环境中添加额外参数
|
# 只在确实是Docker环境时添加额外参数
|
||||||
if os.getenv('DOCKER_ENV'):
|
if os.getenv('DOCKER_ENV') == 'true':
|
||||||
browser_args.extend([
|
browser_args.extend([
|
||||||
'--disable-background-networking',
|
'--disable-gpu',
|
||||||
'--disable-background-timer-throttling',
|
'--single-process'
|
||||||
'--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'
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
logger.info("正在启动浏览器...")
|
||||||
self.browser = await playwright.chromium.launch(
|
self.browser = await playwright.chromium.launch(
|
||||||
headless=True, # 无头模式
|
headless=True, # 无头模式,后台运行
|
||||||
args=browser_args
|
args=browser_args
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info("浏览器启动成功,创建上下文...")
|
||||||
|
# 简化上下文创建,减少可能的问题
|
||||||
self.context = await self.browser.new_context(
|
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()
|
self.page = await self.context.new_page()
|
||||||
|
|
||||||
|
logger.info("浏览器初始化完成")
|
||||||
|
|
||||||
async def close_browser(self):
|
async def close_browser(self):
|
||||||
"""关闭浏览器"""
|
"""关闭浏览器"""
|
||||||
if self.browser:
|
if self.browser:
|
||||||
@ -192,8 +242,28 @@ class XianyuSearcher:
|
|||||||
logger.warning(f"响应处理异常: {str(e)}")
|
logger.warning(f"响应处理异常: {str(e)}")
|
||||||
|
|
||||||
try:
|
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("正在访问闲鱼首页...")
|
logger.info("正在访问闲鱼首页...")
|
||||||
await self.page.goto("https://www.goofish.com", timeout=30000)
|
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)
|
await self.page.wait_for_load_state("networkidle", timeout=10000)
|
||||||
|
|
||||||
logger.info(f"正在搜索关键词: {keyword}")
|
logger.info(f"正在搜索关键词: {keyword}")
|
||||||
@ -499,6 +569,7 @@ class XianyuSearcher:
|
|||||||
Returns:
|
Returns:
|
||||||
搜索结果字典,包含所有页面的items列表和总数
|
搜索结果字典,包含所有页面的items列表和总数
|
||||||
"""
|
"""
|
||||||
|
browser_initialized = False
|
||||||
try:
|
try:
|
||||||
if not PLAYWRIGHT_AVAILABLE:
|
if not PLAYWRIGHT_AVAILABLE:
|
||||||
logger.error("Playwright 不可用,无法获取真实数据")
|
logger.error("Playwright 不可用,无法获取真实数据")
|
||||||
@ -510,7 +581,15 @@ class XianyuSearcher:
|
|||||||
|
|
||||||
logger.info(f"使用 Playwright 搜索多页闲鱼商品: 关键词='{keyword}', 总页数={total_pages}")
|
logger.info(f"使用 Playwright 搜索多页闲鱼商品: 关键词='{keyword}', 总页数={total_pages}")
|
||||||
|
|
||||||
|
# 确保浏览器初始化
|
||||||
await self.init_browser()
|
await self.init_browser()
|
||||||
|
browser_initialized = True
|
||||||
|
|
||||||
|
# 验证浏览器状态
|
||||||
|
if not self.browser or not self.page:
|
||||||
|
raise Exception("浏览器初始化失败")
|
||||||
|
|
||||||
|
logger.info("浏览器初始化成功,开始搜索...")
|
||||||
|
|
||||||
# 清空之前的API响应
|
# 清空之前的API响应
|
||||||
self.api_responses = []
|
self.api_responses = []
|
||||||
@ -552,22 +631,97 @@ class XianyuSearcher:
|
|||||||
logger.warning(f"响应处理异常: {str(e)}")
|
logger.warning(f"响应处理异常: {str(e)}")
|
||||||
|
|
||||||
try:
|
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("正在访问闲鱼首页...")
|
logger.info("正在访问闲鱼首页...")
|
||||||
await self.page.goto("https://www.goofish.com", timeout=30000)
|
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}")
|
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)
|
self.page.on("response", on_response)
|
||||||
|
|
||||||
|
logger.info("🖱️ 准备点击搜索按钮...")
|
||||||
await self.page.click('button[type="submit"]')
|
await self.page.click('button[type="submit"]')
|
||||||
|
logger.info("✅ 搜索按钮已点击")
|
||||||
await self.page.wait_for_load_state("networkidle", timeout=15000)
|
await self.page.wait_for_load_state("networkidle", timeout=15000)
|
||||||
|
|
||||||
# 等待第一页API响应
|
# 等待第一页API响应
|
||||||
logger.info("等待第一页API响应...")
|
logger.info("等待第一页API响应...")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(10) # 增加等待时间
|
||||||
|
|
||||||
# 尝试处理弹窗
|
# 尝试处理弹窗
|
||||||
try:
|
try:
|
||||||
@ -661,17 +815,29 @@ class XianyuSearcher:
|
|||||||
}
|
}
|
||||||
|
|
||||||
finally:
|
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:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
logger.error(f"Playwright 多页搜索失败: {error_msg}")
|
logger.error(f"Playwright 多页搜索失败: {error_msg}")
|
||||||
|
|
||||||
# 检查是否是浏览器安装问题
|
# 检查是否是浏览器相关问题
|
||||||
if "Executable doesn't exist" in error_msg or "playwright install" in error_msg:
|
if "Executable doesn't exist" in error_msg or "playwright install" in error_msg:
|
||||||
error_msg = "浏览器未安装。请在Docker容器中运行: playwright install chromium"
|
error_msg = "浏览器未安装。请在Docker容器中运行: playwright install chromium"
|
||||||
elif "BrowserType.launch" in error_msg:
|
elif "BrowserType.launch" in error_msg:
|
||||||
error_msg = "浏览器启动失败。请确保Docker容器有足够的权限和资源"
|
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 失败,返回错误信息
|
# 如果 Playwright 失败,返回错误信息
|
||||||
return {
|
return {
|
||||||
@ -840,12 +1006,11 @@ class XianyuSearcher:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# 全局搜索器实例
|
# 搜索器工具函数
|
||||||
_searcher = None
|
|
||||||
|
|
||||||
async def search_xianyu_items(keyword: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
|
async def search_xianyu_items(keyword: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
搜索闲鱼商品的便捷函数
|
搜索闲鱼商品的便捷函数,带重试机制
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
keyword: 搜索关键词
|
keyword: 搜索关键词
|
||||||
@ -855,25 +1020,58 @@ async def search_xianyu_items(keyword: str, page: int = 1, page_size: int = 20)
|
|||||||
Returns:
|
Returns:
|
||||||
搜索结果
|
搜索结果
|
||||||
"""
|
"""
|
||||||
global _searcher
|
max_retries = 2
|
||||||
|
retry_delay = 5 # 秒,增加重试间隔
|
||||||
|
|
||||||
if not _searcher:
|
for attempt in range(max_retries + 1):
|
||||||
_searcher = XianyuSearcher()
|
searcher = None
|
||||||
|
try:
|
||||||
|
# 每次搜索都创建新的搜索器实例,避免浏览器状态混乱
|
||||||
|
searcher = XianyuSearcher()
|
||||||
|
|
||||||
try:
|
logger.info(f"开始单页搜索,尝试次数: {attempt + 1}/{max_retries + 1}")
|
||||||
return await _searcher.search_items(keyword, page, page_size)
|
result = await searcher.search_items(keyword, page, page_size)
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"搜索商品失败: {str(e)}")
|
# 如果成功获取到数据,直接返回
|
||||||
return {
|
if result.get('items') or not result.get('error'):
|
||||||
'items': [],
|
logger.info(f"单页搜索成功,获取到 {len(result.get('items', []))} 条数据")
|
||||||
'total': 0,
|
return result
|
||||||
'error': str(e)
|
|
||||||
}
|
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]:
|
async def search_multiple_pages_xianyu(keyword: str, total_pages: int = 1) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
搜索多页闲鱼商品的便捷函数
|
搜索多页闲鱼商品的便捷函数,带重试机制
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
keyword: 搜索关键词
|
keyword: 搜索关键词
|
||||||
@ -882,27 +1080,55 @@ async def search_multiple_pages_xianyu(keyword: str, total_pages: int = 1) -> Di
|
|||||||
Returns:
|
Returns:
|
||||||
搜索结果
|
搜索结果
|
||||||
"""
|
"""
|
||||||
global _searcher
|
max_retries = 2
|
||||||
|
retry_delay = 5 # 秒,增加重试间隔
|
||||||
|
|
||||||
if not _searcher:
|
for attempt in range(max_retries + 1):
|
||||||
_searcher = XianyuSearcher()
|
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]:
|
async def get_item_detail_from_api(item_id: str) -> Optional[str]:
|
||||||
|
@ -492,11 +492,11 @@ class OrderDetailFetcher:
|
|||||||
logger.warning(f"未找到或找到异常数量的 sku--u_ddZval 元素: {len(sku_elements)}")
|
logger.warning(f"未找到或找到异常数量的 sku--u_ddZval 元素: {len(sku_elements)}")
|
||||||
print(f"⚠️ 未找到或找到异常数量的元素: {len(sku_elements)}")
|
print(f"⚠️ 未找到或找到异常数量的元素: {len(sku_elements)}")
|
||||||
|
|
||||||
# 如果没有找到sku--u_ddZval元素,设置默认数量为0
|
# 如果没有找到sku--u_ddZval元素,设置默认数量为1
|
||||||
if len(sku_elements) == 0:
|
if len(sku_elements) == 0:
|
||||||
result['quantity'] = '0'
|
result['quantity'] = '1'
|
||||||
logger.info("未找到sku--u_ddZval元素,数量默认设置为0")
|
logger.info("未找到sku--u_ddZval元素,数量默认设置为1")
|
||||||
print("📦 数量默认设置为: 0")
|
print("📦 数量默认设置为: 1")
|
||||||
|
|
||||||
# 尝试获取页面的所有class包含sku的元素进行调试
|
# 尝试获取页面的所有class包含sku的元素进行调试
|
||||||
all_sku_elements = await self.page.query_selector_all('[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()
|
text_content = await element.text_content()
|
||||||
logger.info(f"SKU元素 {i+1}: class='{class_name}', text='{text_content}'")
|
logger.info(f"SKU元素 {i+1}: class='{class_name}', text='{text_content}'")
|
||||||
|
|
||||||
# 确保数量字段存在,如果不存在则设置为0
|
# 确保数量字段存在,如果不存在则设置为1
|
||||||
if 'quantity' not in result:
|
if 'quantity' not in result:
|
||||||
result['quantity'] = '0'
|
result['quantity'] = '1'
|
||||||
logger.info("未获取到数量信息,默认设置为0")
|
logger.info("未获取到数量信息,默认设置为1")
|
||||||
print("📦 数量默认设置为: 0")
|
print("📦 数量默认设置为: 1")
|
||||||
|
|
||||||
# 打印最终结果
|
# 打印最终结果
|
||||||
if result:
|
if result:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user