From 5e5fee9d5b8beed6d87574990ae6820b8f12d75e Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:48:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 243 ------ .gitignore | 3 - README.md | 3 +- XianyuAutoAsync.py | 92 ++- db_manager.py | 17 + docker-compose-cn.yml | 4 +- docker-compose.yml | 4 +- docker-deploy.bat | 17 +- docker-deploy.sh | 16 +- reply_server.py | 125 +++- requirements.txt | 1 - secure_confirm_decrypted.py | 6 +- secure_freeshipping_decrypted.py | 6 +- static/css/admin.css | 241 ++++++ static/css/app.css | 1 + static/css/logs.css | 70 ++ static/index.html | 527 +++++++++++-- static/item_search.html | 123 +++- static/js/app.js | 1181 +++++++++++++++++++++++++++++- static/log_management.html | 44 +- utils/item_search.py | 374 ++++++++-- utils/order_detail_fetcher.py | 16 +- 22 files changed, 2634 insertions(+), 480 deletions(-) delete mode 100644 .env create mode 100644 static/css/admin.css diff --git a/.env b/.env deleted file mode 100644 index 16a2402..0000000 --- a/.env +++ /dev/null @@ -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 diff --git a/.gitignore b/.gitignore index afae4e4..4ae8ce0 100644 --- a/.gitignore +++ b/.gitignore @@ -193,9 +193,6 @@ dmypy.json .pyre/ # ==================== 项目特定新增 ==================== -# 环境变量文件 -.env.example -.env.*.example # 数据库文件 *.db-journal diff --git a/README.md b/README.md index 3b2371e..727e598 100644 --- a/README.md +++ b/README.md @@ -409,7 +409,6 @@ python Start.py - **`docker-deploy.bat`** - Windows版本部署脚本,支持Windows环境一键部署 - **`entrypoint.sh`** - Docker容器启动脚本,处理环境初始化和服务启动 - **`nginx/nginx.conf`** - Nginx反向代理配置,支持负载均衡和SSL终端 -- **`.env`** - 环境变量配置文件,包含所有可配置的系统参数和敏感信息 - **`requirements.txt`** - Python依赖包列表,精简版本无冗余依赖,按功能分类组织 ## ⚙️ 配置说明 @@ -517,7 +516,7 @@ python Start.py ## ❓ 常见问题 ### 1. 端口被占用 -如果8080端口被占用,可以修改 `.env` 文件中的 `WEB_PORT` 配置。 +如果8080端口被占用,可以修改 `global_config.yml` 文件中的 `AUTO_REPLY.api.port` 配置,或者在 Docker 启动时通过环境变量 `WEB_PORT` 指定端口。 ### 2. 数据库连接失败 检查数据库文件权限,确保应用有读写权限。 diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index fd60754..6de35e5 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -457,6 +457,19 @@ class XianyuLive: item_id: str, chat_id: str, msg_time: str): """统一处理自动发货逻辑""" try: + # 检查商品是否属于当前cookies + if item_id and item_id != "未知商品": + try: + from db_manager import db_manager + item_info = db_manager.get_item_info(self.cookie_id, item_id) + if not item_info: + logger.warning(f'[{msg_time}] 【{self.cookie_id}】❌ 商品 {item_id} 不属于当前账号,跳过自动发货') + return + logger.debug(f'[{msg_time}] 【{self.cookie_id}】✅ 商品 {item_id} 归属验证通过') + except Exception as e: + logger.error(f'[{msg_time}] 【{self.cookie_id}】检查商品归属失败: {self._safe_str(e)},跳过自动发货') + return + # 提取订单ID order_id = self._extract_order_id(message) @@ -2183,23 +2196,28 @@ class XianyuLive: # 插入或更新订单信息到数据库 try: - success = db_manager.insert_or_update_order( - order_id=order_id, - item_id=item_id, - buyer_id=buyer_id, - spec_name=spec_name, - spec_value=spec_value, - quantity=quantity, - amount=amount, - order_status='processed', # 已处理状态 - cookie_id=self.cookie_id - ) - - if success: - logger.info(f"【{self.cookie_id}】订单信息已保存到数据库: {order_id}") - print(f"💾 【{self.cookie_id}】订单 {order_id} 信息已保存到数据库") + # 检查cookie_id是否在cookies表中存在 + cookie_info = db_manager.get_cookie_by_id(self.cookie_id) + if not cookie_info: + logger.warning(f"Cookie ID {self.cookie_id} 不存在于cookies表中,丢弃订单 {order_id}") else: - logger.warning(f"【{self.cookie_id}】订单信息保存失败: {order_id}") + success = db_manager.insert_or_update_order( + order_id=order_id, + item_id=item_id, + buyer_id=buyer_id, + spec_name=spec_name, + spec_value=spec_value, + quantity=quantity, + amount=amount, + order_status='processed', # 已处理状态 + cookie_id=self.cookie_id + ) + + if success: + logger.info(f"【{self.cookie_id}】订单信息已保存到数据库: {order_id}") + print(f"💾 【{self.cookie_id}】订单 {order_id} 信息已保存到数据库") + else: + logger.warning(f"【{self.cookie_id}】订单信息保存失败: {order_id}") except Exception as db_e: logger.error(f"【{self.cookie_id}】保存订单信息到数据库失败: {self._safe_str(db_e)}") @@ -2438,17 +2456,23 @@ class XianyuLive: # 保存订单基本信息到数据库(如果还没有详细信息) try: from db_manager import db_manager - existing_order = db_manager.get_order_by_id(order_id) - if not existing_order: - # 插入基本订单信息 - db_manager.insert_or_update_order( - order_id=order_id, - item_id=item_id, - buyer_id=send_user_id, - order_status='processing', # 处理中状态 - cookie_id=self.cookie_id - ) - logger.info(f"保存基本订单信息到数据库: {order_id}") + + # 检查cookie_id是否在cookies表中存在 + cookie_info = db_manager.get_cookie_by_id(self.cookie_id) + if not cookie_info: + logger.warning(f"Cookie ID {self.cookie_id} 不存在于cookies表中,丢弃订单 {order_id}") + else: + existing_order = db_manager.get_order_by_id(order_id) + if not existing_order: + # 插入基本订单信息 + db_manager.insert_or_update_order( + order_id=order_id, + item_id=item_id, + buyer_id=send_user_id, + order_status='processing', # 处理中状态 + cookie_id=self.cookie_id + ) + logger.info(f"保存基本订单信息到数据库: {order_id}") except Exception as db_e: logger.error(f"保存基本订单信息失败: {self._safe_str(db_e)}") @@ -3346,6 +3370,20 @@ class XianyuLive: # 检查是否为"我已小刀,待刀成" if card_title == "我已小刀,待刀成": logger.info(f'[{msg_time}] 【{self.cookie_id}】【系统】检测到"我已小刀,待刀成",即使在暂停期间也继续处理') + + # 检查商品是否属于当前cookies + if item_id and item_id != "未知商品": + try: + from db_manager import db_manager + item_info = db_manager.get_item_info(self.cookie_id, item_id) + if not item_info: + logger.warning(f'[{msg_time}] 【{self.cookie_id}】❌ 商品 {item_id} 不属于当前账号,跳过免拼发货') + return + logger.debug(f'[{msg_time}] 【{self.cookie_id}】✅ 商品 {item_id} 归属验证通过') + except Exception as e: + logger.error(f'[{msg_time}] 【{self.cookie_id}】检查商品归属失败: {self._safe_str(e)},跳过免拼发货') + return + # 提取订单ID order_id = self._extract_order_id(message) if order_id: diff --git a/db_manager.py b/db_manager.py index 593624b..5e69a34 100644 --- a/db_manager.py +++ b/db_manager.py @@ -1120,6 +1120,8 @@ class DBManager: logger.error(f"获取所有Cookie失败: {e}") return {} + + def get_cookie_by_id(self, cookie_id: str) -> Optional[Dict[str, str]]: """根据ID获取Cookie信息 @@ -4055,6 +4057,14 @@ class DBManager: try: cursor = self.conn.cursor() + # 检查cookie_id是否在cookies表中存在(如果提供了cookie_id) + if cookie_id: + cursor.execute("SELECT id FROM cookies WHERE id = ?", (cookie_id,)) + cookie_exists = cursor.fetchone() + if not cookie_exists: + logger.warning(f"Cookie ID {cookie_id} 不存在于cookies表中,拒绝插入订单 {order_id}") + return False + # 检查订单是否已存在 cursor.execute("SELECT order_id FROM orders WHERE order_id = ?", (order_id,)) existing = cursor.fetchone() @@ -4189,14 +4199,21 @@ class DBManager: primary_key_map = { 'users': 'id', 'cookies': 'id', + 'cookie_status': 'id', 'keywords': 'id', 'default_replies': 'id', + 'default_reply_records': 'id', + 'item_replay': 'item_id', 'ai_reply_settings': 'id', + 'ai_conversations': 'id', + 'ai_item_cache': 'id', + 'item_info': 'id', 'message_notifications': 'id', 'cards': 'id', 'delivery_rules': 'id', 'notification_channels': 'id', 'user_settings': 'id', + 'system_settings': 'id', 'email_verifications': 'id', 'captcha_codes': 'id', 'orders': 'order_id' diff --git a/docker-compose-cn.yml b/docker-compose-cn.yml index b20559a..9d46c6d 100644 --- a/docker-compose-cn.yml +++ b/docker-compose-cn.yml @@ -57,9 +57,7 @@ services: - TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600} - TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300} - MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000} - env_file: - - path: .env - required: false + networks: - xianyu-network healthcheck: diff --git a/docker-compose.yml b/docker-compose.yml index 64d4b60..f45795c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,9 +57,7 @@ services: - TOKEN_REFRESH_INTERVAL=${TOKEN_REFRESH_INTERVAL:-3600} - TOKEN_RETRY_INTERVAL=${TOKEN_RETRY_INTERVAL:-300} - MESSAGE_EXPIRE_TIME=${MESSAGE_EXPIRE_TIME:-300000} - env_file: - - path: .env - required: false + networks: - xianyu-network healthcheck: diff --git a/docker-deploy.bat b/docker-deploy.bat index 0043a51..961af36 100644 --- a/docker-deploy.bat +++ b/docker-deploy.bat @@ -10,7 +10,6 @@ title 闲鱼自动回复系统 Docker 部署 REM 项目配置 set PROJECT_NAME=xianyu-auto-reply set COMPOSE_FILE=docker-compose.yml -set ENV_FILE=.env REM 颜色定义(Windows CMD不支持ANSI颜色,使用echo代替) set "INFO_PREFIX=[INFO]" @@ -41,13 +40,6 @@ echo %SUCCESS_PREFIX% 系统依赖检查通过 REM 初始化配置 echo %INFO_PREFIX% 初始化配置文件... -if not exist "%ENV_FILE%" ( - echo %WARNING_PREFIX% %ENV_FILE% 文件不存在,将使用默认配置 - echo %INFO_PREFIX% 如需自定义配置,请创建 %ENV_FILE% 文件 -) else ( - echo %SUCCESS_PREFIX% %ENV_FILE% 配置文件已存在 -) - REM 检查关键文件 if not exist "entrypoint.sh" ( echo %ERROR_PREFIX% entrypoint.sh 文件不存在,Docker容器将无法启动 @@ -58,6 +50,15 @@ if not exist "entrypoint.sh" ( echo %SUCCESS_PREFIX% entrypoint.sh 文件已存在 ) +if not exist "global_config.yml" ( + echo %ERROR_PREFIX% global_config.yml 配置文件不存在 + echo %INFO_PREFIX% 请确保配置文件存在 + pause + exit /b 1 +) else ( + echo %SUCCESS_PREFIX% global_config.yml 配置文件已存在 +) + REM 创建必要的目录 if not exist "data" mkdir data if not exist "logs" mkdir logs diff --git a/docker-deploy.sh b/docker-deploy.sh index b1e2426..7c45749 100644 --- a/docker-deploy.sh +++ b/docker-deploy.sh @@ -15,7 +15,6 @@ NC='\033[0m' # No Color # 项目配置 PROJECT_NAME="xianyu-auto-reply" COMPOSE_FILE="docker-compose.yml" -ENV_FILE=".env" # 打印带颜色的消息 print_info() { @@ -55,13 +54,6 @@ check_dependencies() { init_config() { print_info "初始化配置文件..." - if [ ! -f "$ENV_FILE" ]; then - print_warning "$ENV_FILE 文件不存在,将使用默认配置" - print_info "如需自定义配置,请创建 $ENV_FILE 文件" - else - print_success "$ENV_FILE 配置文件已存在" - fi - # 检查关键文件 if [ ! -f "entrypoint.sh" ]; then print_error "entrypoint.sh 文件不存在,Docker容器将无法启动" @@ -71,6 +63,14 @@ init_config() { print_success "entrypoint.sh 文件已存在" fi + if [ ! -f "global_config.yml" ]; then + print_error "global_config.yml 配置文件不存在" + print_info "请确保配置文件存在" + exit 1 + else + print_success "global_config.yml 配置文件已存在" + fi + # 创建必要的目录 mkdir -p data logs backups static/uploads/images print_success "已创建必要的目录" diff --git a/reply_server.py b/reply_server.py index 8895cad..cabc6e7 100644 --- a/reply_server.py +++ b/reply_server.py @@ -2742,7 +2742,11 @@ async def search_items( current_user: Optional[Dict[str, Any]] = Depends(get_current_user_optional) ): """搜索闲鱼商品""" + user_info = f"【{current_user.get('username', 'unknown')}#{current_user.get('user_id', 'unknown')}】" if current_user else "【未登录】" + try: + logger.info(f"{user_info} 开始单页搜索: 关键词='{search_request.keyword}', 页码={search_request.page}, 每页={search_request.page_size}") + from utils.item_search import search_xianyu_items # 执行搜索 @@ -2752,18 +2756,84 @@ async def search_items( page_size=search_request.page_size ) - return { + # 检查是否有错误 + has_error = result.get("error") + items_count = len(result.get("items", [])) + + logger.info(f"{user_info} 单页搜索完成: 获取到 {items_count} 条数据" + + (f", 错误: {has_error}" if has_error else "")) + + response_data = { "success": True, "data": result.get("items", []), "total": result.get("total", 0), "page": search_request.page, "page_size": search_request.page_size, - "keyword": search_request.keyword + "keyword": search_request.keyword, + "is_real_data": result.get("is_real_data", False), + "source": result.get("source", "unknown") } - except Exception as e: - logger.error(f"商品搜索失败: {str(e)}") - raise HTTPException(status_code=500, detail=f"商品搜索失败: {str(e)}") + # 如果有错误信息,也包含在响应中 + if has_error: + response_data["error"] = has_error + + return response_data + + except Exception as e: + error_msg = str(e) + logger.error(f"{user_info} 商品搜索失败: {error_msg}") + raise HTTPException(status_code=500, detail=f"商品搜索失败: {error_msg}") + + +@app.get("/cookies/check") +async def check_valid_cookies( + current_user: Optional[Dict[str, Any]] = Depends(get_current_user_optional) +): + """检查是否有有效的cookies账户(必须是启用状态)""" + try: + if cookie_manager.manager is None: + return { + "success": True, + "hasValidCookies": False, + "validCount": 0, + "enabledCount": 0, + "totalCount": 0 + } + + from db_manager import db_manager + + # 获取所有cookies + all_cookies = db_manager.get_all_cookies() + + # 检查启用状态和有效性 + valid_cookies = [] + enabled_cookies = [] + + for cookie_id, cookie_value in all_cookies.items(): + # 检查是否启用 + is_enabled = cookie_manager.manager.get_cookie_status(cookie_id) + if is_enabled: + enabled_cookies.append(cookie_id) + # 检查是否有效(长度大于50) + if len(cookie_value) > 50: + valid_cookies.append(cookie_id) + + return { + "success": True, + "hasValidCookies": len(valid_cookies) > 0, + "validCount": len(valid_cookies), + "enabledCount": len(enabled_cookies), + "totalCount": len(all_cookies) + } + + except Exception as e: + logger.error(f"检查cookies失败: {str(e)}") + return { + "success": False, + "hasValidCookies": False, + "error": str(e) + } @app.post("/items/search_multiple") async def search_multiple_pages( @@ -2771,7 +2841,11 @@ async def search_multiple_pages( current_user: Optional[Dict[str, Any]] = Depends(get_current_user_optional) ): """搜索多页闲鱼商品""" + user_info = f"【{current_user.get('username', 'unknown')}#{current_user.get('user_id', 'unknown')}】" if current_user else "【未登录】" + try: + logger.info(f"{user_info} 开始多页搜索: 关键词='{search_request.keyword}', 页数={search_request.total_pages}") + from utils.item_search import search_multiple_pages_xianyu # 执行多页搜索 @@ -2780,7 +2854,14 @@ async def search_multiple_pages( total_pages=search_request.total_pages ) - return { + # 检查是否有错误 + has_error = result.get("error") + items_count = len(result.get("items", [])) + + logger.info(f"{user_info} 多页搜索完成: 获取到 {items_count} 条数据" + + (f", 错误: {has_error}" if has_error else "")) + + response_data = { "success": True, "data": result.get("items", []), "total": result.get("total", 0), @@ -2790,9 +2871,17 @@ async def search_multiple_pages( "is_fallback": result.get("is_fallback", False), "source": result.get("source", "unknown") } + + # 如果有错误信息,也包含在响应中 + if has_error: + response_data["error"] = has_error + + return response_data + except Exception as e: - logger.error(f"多页商品搜索失败: {str(e)}") - raise HTTPException(status_code=500, detail=f"多页商品搜索失败: {str(e)}") + error_msg = str(e) + logger.error(f"{user_info} 多页商品搜索失败: {error_msg}") + raise HTTPException(status_code=500, detail=f"多页商品搜索失败: {error_msg}") @app.get("/items/detail/{item_id}") @@ -3373,43 +3462,55 @@ def get_system_logs(admin_user: Dict[str, Any] = Depends(require_admin), # 查找日志文件 log_files = glob.glob("logs/xianyu_*.log") + logger.info(f"找到日志文件: {log_files}") + if not log_files: - return {"logs": [], "message": "未找到日志文件"} + logger.warning("未找到日志文件") + return {"logs": [], "message": "未找到日志文件", "success": False} # 获取最新的日志文件 latest_log_file = max(log_files, key=os.path.getctime) + logger.info(f"使用最新日志文件: {latest_log_file}") logs = [] try: with open(latest_log_file, 'r', encoding='utf-8') as f: all_lines = f.readlines() + logger.info(f"读取到 {len(all_lines)} 行日志") # 如果指定了日志级别,进行过滤 if level: filtered_lines = [line for line in all_lines if f"| {level.upper()} |" in line] + logger.info(f"按级别 {level} 过滤后剩余 {len(filtered_lines)} 行") else: filtered_lines = all_lines # 获取最后N行 recent_lines = filtered_lines[-lines:] if len(filtered_lines) > lines else filtered_lines + logger.info(f"取最后 {len(recent_lines)} 行日志") for line in recent_lines: logs.append(line.strip()) except Exception as e: + logger.error(f"读取日志文件失败: {str(e)}") log_with_user('error', f"读取日志文件失败: {str(e)}", admin_user) - return {"logs": [], "message": f"读取日志文件失败: {str(e)}"} + return {"logs": [], "message": f"读取日志文件失败: {str(e)}", "success": False} log_with_user('info', f"返回日志记录 {len(logs)} 条", admin_user) + logger.info(f"成功返回 {len(logs)} 条日志记录") + return { "logs": logs, "log_file": latest_log_file, - "total_lines": len(logs) + "total_lines": len(logs), + "success": True } except Exception as e: + logger.error(f"获取系统日志失败: {str(e)}") log_with_user('error', f"获取系统日志失败: {str(e)}", admin_user) - raise HTTPException(status_code=500, detail=str(e)) + return {"logs": [], "message": f"获取系统日志失败: {str(e)}", "success": False} @app.get('/admin/stats') def get_system_stats(admin_user: Dict[str, Any] = Depends(require_admin)): diff --git a/requirements.txt b/requirements.txt index 0317c68..bb7edc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,6 @@ httpx>=0.25.0 # ==================== 配置文件处理 ==================== PyYAML>=6.0.0 -python-dotenv>=1.0.1 # ==================== JavaScript执行引擎 ==================== PyExecJS>=1.5.1 diff --git a/secure_confirm_decrypted.py b/secure_confirm_decrypted.py index 8d1490e..2641559 100644 --- a/secure_confirm_decrypted.py +++ b/secure_confirm_decrypted.py @@ -165,12 +165,8 @@ class SecureConfirm: error_msg = res_json.get('ret', ['未知错误'])[0] if res_json.get('ret') else '未知错误' logger.warning(f"【{self.cookie_id}】❌ 自动确认发货失败: {error_msg}") - # 如果是token相关错误,进行重试 - if 'token' in error_msg.lower() or 'sign' in error_msg.lower(): - logger.info(f"【{self.cookie_id}】检测到token错误,准备重试...") - return await self.auto_confirm(order_id, item_id, retry_count + 1) + return await self.auto_confirm(order_id, item_id, retry_count + 1) - return {"error": error_msg, "order_id": order_id} except Exception as e: logger.error(f"【{self.cookie_id}】自动确认发货API请求异常: {self._safe_str(e)}") diff --git a/secure_freeshipping_decrypted.py b/secure_freeshipping_decrypted.py index d8e21f9..84bd1b5 100644 --- a/secure_freeshipping_decrypted.py +++ b/secure_freeshipping_decrypted.py @@ -115,12 +115,8 @@ class SecureFreeshipping: error_msg = res_json.get('ret', ['未知错误'])[0] if res_json.get('ret') else '未知错误' logger.warning(f"【{self.cookie_id}】❌ 自动免拼发货失败: {error_msg}") - # 如果是token相关错误,进行重试 - if 'token' in error_msg.lower() or 'sign' in error_msg.lower(): - logger.info(f"【{self.cookie_id}】检测到token错误,准备重试...") - return await self.auto_freeshipping(order_id, item_id, buyer_id, retry_count + 1) + return await self.auto_freeshipping(order_id, item_id, buyer_id, retry_count + 1) - return {"error": error_msg, "order_id": order_id} except Exception as e: logger.error(f"【{self.cookie_id}】自动免拼发货API请求异常: {self._safe_str(e)}") diff --git a/static/css/admin.css b/static/css/admin.css new file mode 100644 index 0000000..b41a14f --- /dev/null +++ b/static/css/admin.css @@ -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%); +} diff --git a/static/css/app.css b/static/css/app.css index 9a1db23..947a07d 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -7,6 +7,7 @@ @import url('items.css'); @import url('notifications.css'); @import url('components.css'); +@import url('admin.css'); diff --git a/static/css/logs.css b/static/css/logs.css index 7a592c2..f8de02f 100644 --- a/static/css/logs.css +++ b/static/css/logs.css @@ -75,3 +75,73 @@ .log-container::-webkit-scrollbar-thumb:hover { background: #5a5a5c; } + +/* 日志过滤标签样式 */ +.filter-badge { + cursor: pointer; + transition: all 0.2s; + margin-right: 0.5rem; + margin-bottom: 0.25rem; +} + +.filter-badge:hover { + transform: scale(1.05); +} + +.filter-badge.active { + box-shadow: 0 0 0 2px rgba(255,255,255,0.5); +} + +/* 自动刷新指示器 */ +.auto-refresh-indicator { + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +/* 日志行悬停效果 */ +.log-entry:hover { + background: rgba(255,255,255,0.05); +} + +/* 商品搜索相关样式 */ +.item-card { + transition: transform 0.2s; + border: none; + border-radius: 15px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.item-card:hover { + transform: translateY(-2px); +} + +.item-image { + width: 100%; + height: 200px; + object-fit: cover; + border-radius: 10px; +} + +.price { + color: #e74c3c; + font-weight: bold; + font-size: 1.2em; +} + +.seller-name { + color: #6c757d; + font-size: 0.9em; +} + +.want-count { + margin-top: 10px; +} + +.want-count .badge { + font-size: 0.8em; +} diff --git a/static/index.html b/static/index.html index 8670597..a790a14 100644 --- a/static/index.html +++ b/static/index.html @@ -85,7 +85,7 @@
- -
-
- - + +
+
+
+ 日志控制 +
-
- -
- - - - - +
+
+
+ + +
+
+ +
+ + 全部 + + + INFO + + + WARNING + + + ERROR + + + DEBUG + +
+
+
+ +
+ + +
+
+
+ +
+ +
+
- + +
+
+
+ 日志信息 +
+ 最后更新: - +
+
+
+
+ 日志文件: + - +
+
+ 显示行数: + - +
+
+ 当前过滤: + 全部 +
+
+
+
+ +
- 系统日志 +
+ 日志内容 +
- 0 条日志 - 未更新 + +
-
-
- -

点击刷新按钮加载日志

+
+
+ 加载中... +
+

正在加载日志...

+
+ + +
+
+
+
+ + +
+
+

+ + 商品搜索 +

+

搜索闲鱼商品,获取真实商品数据

+
+
+ +
+
+
+ + 闲鱼商品搜索 +
+
+
+
+ + 功能说明: +
    +
  • 查询总页数:输入要获取的页数(1-20页),系统会一次性获取所有页面的数据
  • +
  • 每页显示:前端分页显示的每页商品数量
  • +
  • 数据来源:使用真实的闲鱼数据,通过浏览器自动化技术获取
  • +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+
+ + + + + + + + + + + +
@@ -1444,6 +1643,187 @@
+ +
+
+

+ + 用户管理 +

+

管理系统中的所有用户账号

+
+
+ +
+
+
+
+

-

+

总用户数

+
+
+
+
+
+
+

-

+

总Cookie数

+
+
+
+
+
+
+

-

+

总卡券数

+
+
+
+
+
+
+

运行中

+

系统状态

+
+
+
+
+ + +
+
+
+ 用户列表 +
+ +
+
+
+
+ 加载中... +
+

正在加载用户信息...

+
+ + +
+
+
+
+ + +
+
+

+ + 数据管理 +

+

查看和管理数据库中的所有表数据

+
+
+ +
+
+
+ 数据表选择 +
+
+
+
+
+ + +
+
+ +
+
+
-
+ 条记录 +
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+
+ 数据表 +
+
+ +
+
+
+ +
+ +

请选择要查看的数据表

+
+ + +
+
+
+
+
@@ -2650,5 +3030,68 @@
+ + + + + + \ No newline at end of file diff --git a/static/item_search.html b/static/item_search.html index f1ae4ff..d137867 100644 --- a/static/item_search.html +++ b/static/item_search.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) { try { + // 检查是否有有效的cookies账户 + const hasValidCookies = await checkValidCookies(); + if (!hasValidCookies) { + showErrorMessage('搜索失败:系统中不存在有效的账户信息。请先在Cookie管理中添加有效的闲鱼账户。'); + return; + } + // 显示加载状态 showLoading(true); hideResults(); @@ -454,20 +485,36 @@ if (response.ok) { const data = await response.json(); + console.log('API返回的完整数据:', data); + if (data.success) { allItems = data.data || []; totalResults = allItems.length; + + console.log('设置allItems:', allItems); + console.log('allItems长度:', allItems.length); + console.log('totalResults:', totalResults); + + // 检查是否有错误信息 + if (data.error) { + showErrorMessage(`搜索完成,但遇到问题: ${data.error}`); + } + + console.log('调用displayPaginatedResults...'); displayPaginatedResults(data); + console.log('调用updateFrontendPagination...'); updateFrontendPagination(); } else { - throw new Error(data.message || '搜索失败'); + console.error('API返回success=false:', data); + throw new Error(data.message || data.error || '搜索失败'); } } else { - throw new Error(`HTTP ${response.status}`); + const errorText = await response.text(); + throw new Error(`服务器错误 (${response.status}): ${errorText}`); } } catch (error) { console.error('搜索失败:', error); - alert('搜索失败: ' + error.message); + showErrorMessage('搜索失败: ' + error.message); } finally { showLoading(false); } @@ -527,7 +574,7 @@ } } catch (error) { console.error('搜索失败:', error); - alert('搜索失败: ' + error.message); + showErrorMessage('搜索失败: ' + error.message); } finally { showLoading(false); } @@ -538,6 +585,36 @@ document.getElementById('loading').style.display = show ? 'block' : 'none'; } + // 显示错误消息 + function showErrorMessage(message) { + // 创建或更新错误提示 + let errorAlert = document.getElementById('errorAlert'); + if (!errorAlert) { + errorAlert = document.createElement('div'); + errorAlert.id = 'errorAlert'; + errorAlert.className = 'alert alert-danger alert-dismissible fade show'; + errorAlert.innerHTML = ` + + + + `; + + // 插入到搜索表单后面 + const searchForm = document.querySelector('.search-form'); + searchForm.parentNode.insertBefore(errorAlert, searchForm.nextSibling); + } + + document.getElementById('errorMessage').textContent = message; + errorAlert.style.display = 'block'; + + // 5秒后自动隐藏 + setTimeout(() => { + if (errorAlert) { + errorAlert.style.display = 'none'; + } + }, 5000); + } + // 隐藏结果 function hideResults() { document.getElementById('searchResults').innerHTML = ''; @@ -548,10 +625,15 @@ // 显示分页搜索结果 function displayPaginatedResults(data) { + console.log('displayPaginatedResults被调用,data:', data); + // 显示统计信息 const statsElement = document.getElementById('searchStats'); const statsText = document.getElementById('statsText'); + console.log('statsElement:', statsElement); + console.log('statsText:', statsText); + // 检查数据来源 let dataSource = ''; if (data.is_real_data) { @@ -563,11 +645,14 @@ } statsElement.style.display = 'block'; + console.log('统计信息已显示'); // 使用统一的统计信息更新函数 + console.log('调用updateStatsDisplay...'); updateStatsDisplay(); // 显示当前页的数据 + console.log('调用displayCurrentPage...'); displayCurrentPage(); } @@ -577,14 +662,27 @@ // 显示当前页数据(全局函数) function displayCurrentPage() { + console.log('displayCurrentPage被调用'); + console.log('allItems:', allItems); + console.log('allItems.length:', allItems.length); + console.log('currentPage:', currentPage); + console.log('currentPageSize:', currentPageSize); + const resultsContainer = document.getElementById('searchResults'); + console.log('resultsContainer:', resultsContainer); // 计算当前页的数据范围 const startIndex = (currentPage - 1) * currentPageSize; const endIndex = startIndex + currentPageSize; const currentItems = allItems.slice(startIndex, endIndex); + console.log('startIndex:', startIndex); + console.log('endIndex:', endIndex); + console.log('currentItems:', currentItems); + console.log('currentItems.length:', currentItems.length); + if (currentItems.length === 0) { + console.log('没有数据,显示无结果提示'); document.getElementById('noResults').style.display = 'block'; resultsContainer.innerHTML = ''; return; @@ -593,7 +691,11 @@ document.getElementById('noResults').style.display = 'none'; // 生成商品卡片 - resultsContainer.innerHTML = currentItems.map(item => createItemCard(item)).join(''); + console.log('生成商品卡片...'); + const cardsHtml = currentItems.map(item => createItemCard(item)).join(''); + console.log('生成的HTML长度:', cardsHtml.length); + resultsContainer.innerHTML = cardsHtml; + console.log('商品卡片已设置到容器中'); } // 显示搜索结果(保留原有功能) @@ -751,10 +853,21 @@ // 创建商品卡片HTML(全局函数) function createItemCard(item) { + console.log('createItemCard被调用,item数据:', item); + console.log('item的所有字段:', Object.keys(item)); + const tags = item.tags ? item.tags.map(tag => `${escapeHtml(tag)}`).join('') : ''; const imageUrl = item.main_image || 'https://via.placeholder.com/200x200?text=暂无图片'; const wantCount = item.want_count || 0; + console.log('处理后的数据:', { + title: item.title, + price: item.price, + seller_name: item.seller_name, + imageUrl: imageUrl, + wantCount: wantCount + }); + return `
diff --git a/static/js/app.js b/static/js/app.js index 546cad9..ca6eb39 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -102,13 +102,22 @@ function showSection(sectionName) { loadSystemSettings(); break; case 'logs': // 【日志管理菜单】 - // 如果没有日志数据,则加载 + // 自动加载系统日志 setTimeout(() => { - if (!window.allLogs || window.allLogs.length === 0) { - refreshLogs(); - } + // 检查是否在正确的页面并且元素存在 + const systemLogContainer = document.getElementById('systemLogContainer'); + if (systemLogContainer) { + console.log('首次进入日志页面,自动加载日志...'); + loadSystemLogs(); + } }, 100); break; + case 'user-management': // 【用户管理菜单】 + loadUserManagement(); + break; + case 'data-management': // 【数据管理菜单】 + loadDataManagement(); + break; } // 如果切换到非日志页面,停止自动刷新 @@ -1848,6 +1857,9 @@ document.addEventListener('DOMContentLoaded', async () => { // 初始化商品搜索功能 initItemsSearch(); + // 初始化商品搜索界面功能 + initItemSearch(); + // 点击侧边栏外部关闭移动端菜单 document.addEventListener('click', function(e) { const sidebar = document.getElementById('sidebar'); @@ -5091,7 +5103,7 @@ function displayCurrentPageItems() { const tbody = document.getElementById('itemsTableBody'); if (!filteredItemsData || filteredItemsData.length === 0) { - tbody.innerHTML = '暂无商品数据'; + tbody.innerHTML = '暂无商品数据'; resetItemsSelection(); return; } @@ -5133,6 +5145,7 @@ function displayCurrentPageItems() { ${escapeHtml(item.item_id)} ${escapeHtml(itemTitleDisplay)} ${escapeHtml(itemDetailDisplay)} + ${escapeHtml(item.item_price || '未设置')} ${multiSpecDisplay} ${formatDateTime(item.updated_at)} @@ -6110,27 +6123,34 @@ window.filteredLogs = []; // 刷新日志 async function refreshLogs() { try { - const lines = document.getElementById('logLines').value; - - const response = await fetch(`${apiBase}/logs?lines=${lines}`, { - headers: { - 'Authorization': `Bearer ${authToken}` + const logLinesElement = document.getElementById('logLines'); + if (!logLinesElement) { + console.warn('logLines 元素不存在'); + showToast('页面元素缺失,请刷新页面', 'warning'); + return; } - }); - if (response.ok) { - const data = await response.json(); - window.allLogs = data.logs || []; - window.filteredLogs = window.allLogs; // 不再过滤,直接显示所有日志 - displayLogs(); - updateLogStats(); - showToast('日志已刷新', 'success'); - } else { - throw new Error(`HTTP ${response.status}`); - } + const lines = logLinesElement.value; + + const response = await fetch(`${apiBase}/logs?lines=${lines}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const data = await response.json(); + window.allLogs = data.logs || []; + window.filteredLogs = window.allLogs; // 不再过滤,直接显示所有日志 + displayLogs(); + updateLogStats(); + showToast('日志已刷新', 'success'); + } else { + throw new Error(`HTTP ${response.status}`); + } } catch (error) { - console.error('刷新日志失败:', error); - showToast('刷新日志失败', 'danger'); + console.error('刷新日志失败:', error); + showToast(`刷新日志失败: ${error.message}`, 'danger'); } } @@ -6140,7 +6160,17 @@ async function refreshLogs() { function displayLogs() { const container = document.getElementById('logContainer'); - if (window.filteredLogs.length === 0) { + // 检查容器是否存在 + if (!container) { + // 只在特定页面显示警告,避免在其他页面产生无用的警告 + const currentPath = window.location.pathname; + if (currentPath.includes('log') || currentPath.includes('admin')) { + console.warn('logContainer 元素不存在,无法显示日志'); + } + return; + } + + if (!window.filteredLogs || window.filteredLogs.length === 0) { container.innerHTML = `
@@ -6187,8 +6217,17 @@ function formatLogTimestamp(timestamp) { // 更新日志统计信息 function updateLogStats() { - document.getElementById('logCount').textContent = `${window.filteredLogs.length} 条日志`; - document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('zh-CN'); + const logCountElement = document.getElementById('logCount'); + const lastUpdateElement = document.getElementById('lastUpdate'); + + if (logCountElement) { + const count = window.filteredLogs ? window.filteredLogs.length : 0; + logCountElement.textContent = `${count} 条日志`; + } + + if (lastUpdateElement) { + lastUpdateElement.textContent = new Date().toLocaleTimeString('zh-CN'); + } } // 清空日志显示 @@ -8164,3 +8203,1093 @@ document.addEventListener('DOMContentLoaded', function() { }); }, 100); }); + +// ================================ +// 用户管理功能 +// ================================ + +// 加载用户管理页面 +async function loadUserManagement() { + console.log('加载用户管理页面'); + + // 检查管理员权限 + try { + const response = await fetch(`${apiBase}/verify`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const result = await response.json(); + if (!result.is_admin) { + showToast('您没有权限访问用户管理功能', 'danger'); + showSection('dashboard'); // 跳转回仪表盘 + return; + } + } else { + showToast('权限验证失败', 'danger'); + return; + } + } catch (error) { + console.error('权限验证失败:', error); + showToast('权限验证失败', 'danger'); + return; + } + + // 加载数据 + await loadUserSystemStats(); + await loadUsers(); +} + +// 加载用户系统统计信息 +async function loadUserSystemStats() { + try { + const token = localStorage.getItem('auth_token'); + + // 获取用户统计 + const usersResponse = await fetch('/admin/users', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (usersResponse.ok) { + const usersData = await usersResponse.json(); + document.getElementById('totalUsers').textContent = usersData.users.length; + } + + // 获取Cookie统计 + const cookiesResponse = await fetch(`${apiBase}/admin/data/cookies`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (cookiesResponse.ok) { + const cookiesData = await cookiesResponse.json(); + document.getElementById('totalUserCookies').textContent = cookiesData.data ? cookiesData.data.length : 0; + } + + // 获取卡券统计 + const cardsResponse = await fetch(`${apiBase}/admin/data/cards`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (cardsResponse.ok) { + const cardsData = await cardsResponse.json(); + document.getElementById('totalUserCards').textContent = cardsData.data ? cardsData.data.length : 0; + } + + } catch (error) { + console.error('加载系统统计失败:', error); + } +} + +// 加载用户列表 +async function loadUsers() { + const loadingDiv = document.getElementById('loadingUsers'); + const usersListDiv = document.getElementById('usersList'); + const noUsersDiv = document.getElementById('noUsers'); + + // 显示加载状态 + loadingDiv.style.display = 'block'; + usersListDiv.style.display = 'none'; + noUsersDiv.style.display = 'none'; + + try { + const token = localStorage.getItem('auth_token'); + const response = await fetch('/admin/users', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const data = await response.json(); + loadingDiv.style.display = 'none'; + + if (data.users && data.users.length > 0) { + usersListDiv.style.display = 'block'; + displayUsers(data.users); + } else { + noUsersDiv.style.display = 'block'; + } + } else { + throw new Error('获取用户列表失败'); + } + } catch (error) { + console.error('加载用户列表失败:', error); + loadingDiv.style.display = 'none'; + noUsersDiv.style.display = 'block'; + showToast('加载用户列表失败', 'danger'); + } +} + +// 显示用户列表 +function displayUsers(users) { + const usersListDiv = document.getElementById('usersList'); + usersListDiv.innerHTML = ''; + + users.forEach(user => { + const userCard = createUserCard(user); + usersListDiv.appendChild(userCard); + }); +} + +// 创建用户卡片 +function createUserCard(user) { + const col = document.createElement('div'); + col.className = 'col-md-6 col-lg-4 mb-3'; + + const isAdmin = user.username === 'admin'; + const badgeClass = isAdmin ? 'bg-danger' : 'bg-primary'; + const badgeText = isAdmin ? '管理员' : '普通用户'; + + col.innerHTML = ` +
+
+
+
${user.username}
+ ${badgeText} +
+

+ ${user.email || '未设置邮箱'} +

+

+ 注册时间:${formatDateTime(user.created_at)} +

+
+ + Cookie数: ${user.cookie_count || 0} | + 卡券数: ${user.card_count || 0} + + ${!isAdmin ? ` + + ` : ''} +
+
+
+ `; + + return col; +} + +// 全局变量用于存储当前要删除的用户信息 +let currentDeleteUserId = null; +let currentDeleteUserName = null; +let deleteUserModal = null; + +// 删除用户 +function deleteUser(userId, username) { + if (username === 'admin') { + showToast('不能删除管理员账号', 'warning'); + return; + } + + // 存储要删除的用户信息 + currentDeleteUserId = userId; + currentDeleteUserName = username; + + // 初始化模态框(如果还没有初始化) + if (!deleteUserModal) { + deleteUserModal = new bootstrap.Modal(document.getElementById('deleteUserModal')); + } + + // 显示确认模态框 + deleteUserModal.show(); +} + +// 确认删除用户 +async function confirmDeleteUser() { + if (!currentDeleteUserId) return; + + try { + const token = localStorage.getItem('auth_token'); + + const response = await fetch(`/admin/users/${currentDeleteUserId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const data = await response.json(); + deleteUserModal.hide(); + showToast(data.message || '用户删除成功', 'success'); + + // 刷新页面数据 + await loadUserSystemStats(); + await loadUsers(); + } else { + const errorData = await response.json(); + showToast(`删除失败: ${errorData.detail || '未知错误'}`, 'danger'); + } + } catch (error) { + console.error('删除用户失败:', error); + showToast('删除用户失败', 'danger'); + } finally { + // 清理状态 + currentDeleteUserId = null; + currentDeleteUserName = null; + } +} + +// 刷新用户列表 +async function refreshUsers() { + await loadUserSystemStats(); + await loadUsers(); + showToast('用户列表已刷新', 'success'); +} + +// ================================ +// 数据管理功能 +// ================================ + +// 全局变量 +let currentTable = ''; +let currentData = []; + +// 表的中文描述 +const tableDescriptions = { + 'users': '用户表', + 'cookies': 'Cookie账号表', + 'cookie_status': 'Cookie状态表', + 'keywords': '关键字表', + 'item_replay': '指定商品回复表', + 'default_replies': '默认回复表', + 'default_reply_records': '默认回复记录表', + 'ai_reply_settings': 'AI回复设置表', + 'ai_conversations': 'AI对话历史表', + 'ai_item_cache': 'AI商品信息缓存表', + 'item_info': '商品信息表', + 'message_notifications': '消息通知表', + 'cards': '卡券表', + 'delivery_rules': '发货规则表', + 'notification_channels': '通知渠道表', + 'user_settings': '用户设置表', + 'system_settings': '系统设置表', + 'email_verifications': '邮箱验证表', + 'captcha_codes': '验证码表', + 'orders': '订单表' +}; + +// 加载数据管理页面 +async function loadDataManagement() { + console.log('加载数据管理页面'); + + // 检查管理员权限 + try { + const response = await fetch(`${apiBase}/verify`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const result = await response.json(); + if (!result.is_admin) { + showToast('您没有权限访问数据管理功能', 'danger'); + showSection('dashboard'); // 跳转回仪表盘 + return; + } + } else { + showToast('权限验证失败', 'danger'); + return; + } + } catch (error) { + console.error('权限验证失败:', error); + showToast('权限验证失败', 'danger'); + return; + } + + // 重置状态 + currentTable = ''; + currentData = []; + + // 重置界面 + showNoTableSelected(); + + // 重置表格选择器 + const tableSelect = document.getElementById('tableSelect'); + if (tableSelect) { + tableSelect.value = ''; + } +} + +// 显示未选择表格状态 +function showNoTableSelected() { + document.getElementById('loadingTable').style.display = 'none'; + document.getElementById('noTableSelected').style.display = 'block'; + document.getElementById('noTableData').style.display = 'none'; + document.getElementById('tableContainer').style.display = 'none'; + + // 重置统计信息 + document.getElementById('recordCount').textContent = '-'; + document.getElementById('tableTitle').innerHTML = ' 数据表'; + + // 禁用按钮 + document.getElementById('clearBtn').disabled = true; +} + +// 显示加载状态 +function showLoading() { + document.getElementById('loadingTable').style.display = 'block'; + document.getElementById('noTableSelected').style.display = 'none'; + document.getElementById('noTableData').style.display = 'none'; + document.getElementById('tableContainer').style.display = 'none'; +} + +// 显示无数据状态 +function showNoData() { + document.getElementById('loadingTable').style.display = 'none'; + document.getElementById('noTableSelected').style.display = 'none'; + document.getElementById('noTableData').style.display = 'block'; + document.getElementById('tableContainer').style.display = 'none'; +} + +// 加载表数据 +async function loadTableData() { + const tableSelect = document.getElementById('tableSelect'); + const selectedTable = tableSelect.value; + + if (!selectedTable) { + showNoTableSelected(); + return; + } + + currentTable = selectedTable; + showLoading(); + + const token = localStorage.getItem('auth_token'); + + try { + const response = await fetch(`/admin/data/${selectedTable}`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + const data = await response.json(); + + if (data.success) { + currentData = data.data; + displayTableData(data.data, data.columns); + updateTableInfo(selectedTable, data.data.length); + } else { + showToast('加载数据失败: ' + data.message, 'danger'); + showNoData(); + } + } catch (error) { + console.error('加载数据失败:', error); + showToast('加载数据失败', 'danger'); + showNoData(); + } +} + +// 显示表格数据 +function displayTableData(data, columns) { + if (!data || data.length === 0) { + showNoData(); + return; + } + + // 显示表格容器 + document.getElementById('loadingTable').style.display = 'none'; + document.getElementById('noTableSelected').style.display = 'none'; + document.getElementById('noTableData').style.display = 'none'; + document.getElementById('tableContainer').style.display = 'block'; + + // 生成表头(添加操作列) + const tableHeaders = document.getElementById('tableHeaders'); + const headerHtml = columns.map(col => `${col}`).join('') + '操作'; + tableHeaders.innerHTML = headerHtml; + + // 生成表格内容(添加删除按钮) + const tableBody = document.getElementById('tableBody'); + tableBody.innerHTML = data.map((row, index) => { + const dataCells = columns.map(col => { + let value = row[col]; + if (value === null || value === undefined) { + value = 'NULL'; + } else if (typeof value === 'string' && value.length > 50) { + value = `${escapeHtml(value.substring(0, 50))}...`; + } else { + value = escapeHtml(String(value)); + } + return `${value}`; + }).join(''); + + // 添加操作列(删除按钮) + const recordId = row.id || row.user_id || index; + const actionCell = ` + + `; + + return `${dataCells}${actionCell}`; + }).join(''); +} + +// HTML转义函数 +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// 更新表格信息 +function updateTableInfo(tableName, recordCount) { + const description = tableDescriptions[tableName] || tableName; + document.getElementById('tableTitle').innerHTML = ` ${description}`; + document.getElementById('recordCount').textContent = recordCount; + + // 启用清空按钮 + document.getElementById('clearBtn').disabled = false; +} + +// 刷新表格数据 +function refreshTableData() { + if (currentTable) { + loadTableData(); + showToast('数据已刷新', 'success'); + } else { + showToast('请先选择数据表', 'warning'); + } +} + +// 导出表格数据 +async function exportTableData() { + if (!currentTable || !currentData || currentData.length === 0) { + showToast('没有可导出的数据', 'warning'); + return; + } + + try { + const token = localStorage.getItem('auth_token'); + const response = await fetch(`/admin/data/${currentTable}/export`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `${currentTable}_${new Date().toISOString().slice(0, 10)}.xlsx`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + showToast('数据导出成功', 'success'); + } else { + showToast('导出失败', 'danger'); + } + } catch (error) { + console.error('导出数据失败:', error); + showToast('导出数据失败', 'danger'); + } +} + +// 清空表格数据 +async function clearTableData() { + if (!currentTable) { + showToast('请先选择数据表', 'warning'); + return; + } + + const description = tableDescriptions[currentTable] || currentTable; + const confirmed = confirm(`确定要清空 "${description}" 的所有数据吗?\n\n此操作不可撤销!`); + + if (!confirmed) return; + + try { + const token = localStorage.getItem('auth_token'); + const response = await fetch(`/admin/data/${currentTable}/clear`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const data = await response.json(); + showToast(data.message || '数据清空成功', 'success'); + // 重新加载数据 + loadTableData(); + } else { + const errorData = await response.json(); + showToast(`清空失败: ${errorData.detail || '未知错误'}`, 'danger'); + } + } catch (error) { + console.error('清空数据失败:', error); + showToast('清空数据失败', 'danger'); + } +} + +// 删除记录相关变量 +let currentDeleteId = null; +let deleteRecordModal = null; + +// 初始化删除记录模态框 +function initDeleteRecordModal() { + if (!deleteRecordModal) { + deleteRecordModal = new bootstrap.Modal(document.getElementById('deleteRecordModal')); + } +} + +// 通过索引删除记录 +function deleteRecordByIndex(index) { + console.log('deleteRecordByIndex被调用,index:', index); + console.log('currentData:', currentData); + console.log('当前currentTable:', currentTable); + + if (!currentData || index >= currentData.length) { + console.error('无效的索引或数据不存在'); + showToast('删除失败:数据不存在', 'danger'); + return; + } + + const record = currentData[index]; + console.log('获取到的record:', record); + + deleteRecord(record, index); +} + +// 删除记录 +function deleteRecord(record, index) { + console.log('deleteRecord被调用'); + console.log('record:', record); + console.log('index:', index); + console.log('当前currentTable:', currentTable); + + initDeleteRecordModal(); + + // 尝试多种方式获取记录ID + currentDeleteId = record.id || record.user_id || record.cookie_id || record.keyword_id || + record.card_id || record.item_id || record.order_id || index; + + console.log('设置currentDeleteId为:', currentDeleteId); + console.log('record的所有字段:', Object.keys(record)); + console.log('record的所有值:', record); + + // 显示记录信息 + const deleteRecordInfo = document.getElementById('deleteRecordInfo'); + deleteRecordInfo.innerHTML = ''; + + Object.keys(record).forEach(key => { + const div = document.createElement('div'); + div.innerHTML = `${key}: ${record[key] || '-'}`; + deleteRecordInfo.appendChild(div); + }); + + deleteRecordModal.show(); +} + +// 确认删除记录 +async function confirmDeleteRecord() { + console.log('confirmDeleteRecord被调用'); + console.log('currentDeleteId:', currentDeleteId); + console.log('currentTable:', currentTable); + + if (!currentDeleteId || !currentTable) { + console.error('缺少必要参数:', { currentDeleteId, currentTable }); + showToast('删除失败:缺少必要参数', 'danger'); + return; + } + + try { + const token = localStorage.getItem('auth_token'); + const url = `/admin/data/${currentTable}/${currentDeleteId}`; + console.log('发送删除请求到:', url); + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + console.log('删除响应状态:', response.status); + + if (response.ok) { + const data = await response.json(); + console.log('删除成功响应:', data); + deleteRecordModal.hide(); + showToast(data.message || '删除成功', 'success'); + loadTableData(); // 重新加载数据 + } else { + const errorData = await response.json(); + console.error('删除失败响应:', errorData); + showToast(`删除失败: ${errorData.detail || '未知错误'}`, 'danger'); + } + } catch (error) { + console.error('删除记录失败:', error); + showToast('删除记录失败: ' + error.message, 'danger'); + } +} + +// ================================ +// 系统日志管理功能 +// ================================ +let logAutoRefreshInterval = null; +let currentLogLevel = ''; + +// 加载系统日志 +async function loadSystemLogs() { + const token = localStorage.getItem('auth_token'); + const lines = document.getElementById('logLines').value; + const level = currentLogLevel; + + const loadingDiv = document.getElementById('loadingSystemLogs'); + const logContainer = document.getElementById('systemLogContainer'); + const noLogsDiv = document.getElementById('noSystemLogs'); + + loadingDiv.style.display = 'block'; + logContainer.style.display = 'none'; + noLogsDiv.style.display = 'none'; + + let url = `/admin/logs?lines=${lines}`; + if (level) { + url += `&level=${level}`; + } + + try { + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + const data = await response.json(); + loadingDiv.style.display = 'none'; + + if (data.logs && data.logs.length > 0) { + displaySystemLogs(data.logs); + updateLogInfo(data); + logContainer.style.display = 'block'; + } else { + noLogsDiv.style.display = 'block'; + } + + // 更新最后更新时间 + document.getElementById('logLastUpdate').textContent = + '最后更新: ' + new Date().toLocaleTimeString('zh-CN'); + } catch (error) { + console.error('加载日志失败:', error); + loadingDiv.style.display = 'none'; + noLogsDiv.style.display = 'block'; + showToast('加载日志失败', 'danger'); + } +} + +// 显示系统日志 +function displaySystemLogs(logs) { + const logContainer = document.getElementById('systemLogContainer'); + logContainer.innerHTML = ''; + + // 反转日志数组,让最新的日志显示在最上面 + const reversedLogs = [...logs].reverse(); + + reversedLogs.forEach(log => { + const logLine = document.createElement('div'); + logLine.className = 'log-entry'; + + // 根据日志级别添加颜色类 + if (log.includes('| INFO |')) { + logLine.classList.add('INFO'); + } else if (log.includes('| WARNING |')) { + logLine.classList.add('WARNING'); + } else if (log.includes('| ERROR |')) { + logLine.classList.add('ERROR'); + } else if (log.includes('| DEBUG |')) { + logLine.classList.add('DEBUG'); + } else if (log.includes('| CRITICAL |')) { + logLine.classList.add('CRITICAL'); + } + + logLine.textContent = log; + logContainer.appendChild(logLine); + }); + + // 自动滚动到顶部(显示最新日志) + scrollLogToTop(); +} + +// 更新日志信息 +function updateLogInfo(data) { + document.getElementById('logFileName').textContent = data.log_file || '-'; + document.getElementById('logDisplayLines').textContent = data.total_lines || '-'; +} + +// 按级别过滤日志 +function filterLogsByLevel(level) { + currentLogLevel = level; + + // 更新过滤按钮状态 + document.querySelectorAll('.filter-badge').forEach(badge => { + badge.classList.remove('active'); + }); + document.querySelector(`[data-level="${level}"]`).classList.add('active'); + + // 更新当前过滤显示 + const filterText = level ? level.toUpperCase() : '全部'; + document.getElementById('logCurrentFilter').textContent = filterText; + + // 重新加载日志 + loadSystemLogs(); +} + +// 切换日志自动刷新 +function toggleLogAutoRefresh() { + const autoRefresh = document.getElementById('autoRefreshLogs'); + const label = document.getElementById('autoRefreshLogLabel'); + const icon = document.getElementById('autoRefreshLogIcon'); + + if (autoRefresh.checked) { + // 开启自动刷新 + logAutoRefreshInterval = setInterval(loadSystemLogs, 5000); // 每5秒刷新 + label.textContent = '开启 (5s)'; + icon.style.display = 'inline'; + icon.classList.add('auto-refresh-indicator'); + } else { + // 关闭自动刷新 + if (logAutoRefreshInterval) { + clearInterval(logAutoRefreshInterval); + logAutoRefreshInterval = null; + } + label.textContent = '关闭'; + icon.style.display = 'none'; + icon.classList.remove('auto-refresh-indicator'); + } +} + +// 滚动到日志顶部 +function scrollLogToTop() { + const logContainer = document.getElementById('systemLogContainer'); + logContainer.scrollTop = 0; +} + +// 滚动到日志底部 +function scrollLogToBottom() { + const logContainer = document.getElementById('systemLogContainer'); + logContainer.scrollTop = logContainer.scrollHeight; +} + +// ================================ +// 商品搜索功能 +// ================================ +let searchResultsData = []; +let currentSearchPage = 1; +let searchPageSize = 20; +let totalSearchPages = 0; + +// 初始化商品搜索功能 +function initItemSearch() { + const searchForm = document.getElementById('itemSearchForm'); + if (searchForm) { + searchForm.addEventListener('submit', handleItemSearch); + } +} + +// 处理商品搜索 +async function handleItemSearch(event) { + event.preventDefault(); + + const keyword = document.getElementById('searchKeyword').value.trim(); + const totalPages = parseInt(document.getElementById('searchTotalPages').value) || 1; + const pageSize = parseInt(document.getElementById('searchPageSize').value) || 20; + + if (!keyword) { + showToast('请输入搜索关键词', 'warning'); + return; + } + + // 显示搜索状态 + showSearchStatus(true); + hideSearchResults(); + + try { + // 检查是否有有效的cookies账户 + const cookiesCheckResponse = await fetch('/cookies/check', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` + } + }); + + if (cookiesCheckResponse.ok) { + const cookiesData = await cookiesCheckResponse.json(); + if (!cookiesData.hasValidCookies) { + showToast('搜索失败:系统中不存在有效的账户信息。请先在Cookie管理中添加有效的闲鱼账户。', 'warning'); + showSearchStatus(false); + return; + } + } + + const token = localStorage.getItem('auth_token'); + const response = await fetch('/items/search_multiple', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + keyword: keyword, + total_pages: totalPages + }) + }); + + console.log('API响应状态:', response.status); + + if (response.ok) { + const data = await response.json(); + console.log('API返回的完整数据:', data); + + // 修复字段名:使用data.data而不是data.items + searchResultsData = data.data || []; + console.log('设置searchResultsData:', searchResultsData); + console.log('searchResultsData长度:', searchResultsData.length); + + searchPageSize = pageSize; + currentSearchPage = 1; + totalSearchPages = Math.ceil(searchResultsData.length / searchPageSize); + + if (data.error) { + showToast(`搜索完成,但遇到问题: ${data.error}`, 'warning'); + } + + showSearchStatus(false); + displaySearchResults(); + updateSearchStats(data); + } else { + const errorData = await response.json(); + showSearchStatus(false); + showToast(`搜索失败: ${errorData.detail || '未知错误'}`, 'danger'); + showNoSearchResults(); + } + } catch (error) { + console.error('搜索商品失败:', error); + showSearchStatus(false); + showToast('搜索商品失败', 'danger'); + showNoSearchResults(); + } +} + +// 显示搜索状态 +function showSearchStatus(isSearching) { + const statusDiv = document.getElementById('searchStatus'); + const progressDiv = document.getElementById('searchProgress'); + + if (isSearching) { + statusDiv.style.display = 'block'; + progressDiv.textContent = '正在搜索商品数据...'; + } else { + statusDiv.style.display = 'none'; + } +} + +// 隐藏搜索结果 +function hideSearchResults() { + document.getElementById('searchResults').style.display = 'none'; + document.getElementById('searchResultStats').style.display = 'none'; + document.getElementById('noSearchResults').style.display = 'none'; +} + +// 显示搜索结果 +function displaySearchResults() { + if (searchResultsData.length === 0) { + showNoSearchResults(); + return; + } + + const startIndex = (currentSearchPage - 1) * searchPageSize; + const endIndex = startIndex + searchPageSize; + const pageItems = searchResultsData.slice(startIndex, endIndex); + + const container = document.getElementById('searchResultsContainer'); + container.innerHTML = ''; + + pageItems.forEach(item => { + const itemCard = createItemCard(item); + container.appendChild(itemCard); + }); + + updateSearchPagination(); + document.getElementById('searchResults').style.display = 'block'; +} + +// 创建商品卡片 +function createItemCard(item) { + console.log('createItemCard被调用,item数据:', item); + console.log('item的所有字段:', Object.keys(item)); + + const col = document.createElement('div'); + col.className = 'col-md-6 col-lg-4 col-xl-3 mb-4'; + + // 修复字段映射:使用main_image而不是image_url + const imageUrl = item.main_image || item.image_url || 'https://via.placeholder.com/200x200?text=图片加载失败'; + const wantCount = item.want_count || 0; + + console.log('处理后的数据:', { + title: item.title, + price: item.price, + seller_name: item.seller_name, + imageUrl: imageUrl, + wantCount: wantCount, + url: item.item_url || item.url + }); + + col.innerHTML = ` +
+ ${escapeHtml(item.title)} +
+
+ ${escapeHtml(item.title.length > 50 ? item.title.substring(0, 50) + '...' : item.title)} +
+
+ ${escapeHtml(item.price)} +
+
+ + ${escapeHtml(item.seller_name)} +
+ ${wantCount > 0 ? `
+ + ${wantCount}人想要 +
` : ''} + +
+
+ `; + + return col; +} + +// 更新搜索统计 +function updateSearchStats(data) { + document.getElementById('totalItemsFound').textContent = searchResultsData.length; + document.getElementById('totalPagesSearched').textContent = data.total_pages || 0; + document.getElementById('currentDisplayPage').textContent = currentSearchPage; + document.getElementById('totalDisplayPages').textContent = totalSearchPages; + document.getElementById('searchResultStats').style.display = 'block'; +} + +// 更新搜索分页 +function updateSearchPagination() { + const paginationContainer = document.getElementById('searchPagination'); + paginationContainer.innerHTML = ''; + + if (totalSearchPages <= 1) return; + + const pagination = document.createElement('nav'); + pagination.innerHTML = ` + + `; + + paginationContainer.appendChild(pagination); +} + +// 生成搜索分页页码 +function generateSearchPageNumbers() { + let pageNumbers = ''; + const maxVisiblePages = 5; + let startPage = Math.max(1, currentSearchPage - Math.floor(maxVisiblePages / 2)); + let endPage = Math.min(totalSearchPages, startPage + maxVisiblePages - 1); + + if (endPage - startPage + 1 < maxVisiblePages) { + startPage = Math.max(1, endPage - maxVisiblePages + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pageNumbers += ` +
  • + ${i} +
  • + `; + } + + return pageNumbers; +} + +// 切换搜索页面 +function changeSearchPage(page) { + if (page < 1 || page > totalSearchPages || page === currentSearchPage) return; + + currentSearchPage = page; + displaySearchResults(); + updateSearchStats({ total_pages: document.getElementById('totalPagesSearched').textContent }); +} + +// 显示无搜索结果 +function showNoSearchResults() { + document.getElementById('noSearchResults').style.display = 'block'; + document.getElementById('searchResults').style.display = 'none'; + document.getElementById('searchResultStats').style.display = 'none'; +} + +// 导出搜索结果 +function exportSearchResults() { + if (searchResultsData.length === 0) { + showToast('没有可导出的搜索结果', 'warning'); + return; + } + + try { + // 准备导出数据 + const exportData = searchResultsData.map(item => ({ + '商品标题': item.title, + '价格': item.price, + '卖家': item.seller_name, + '想要人数': item.want_count || 0, + '商品链接': item.url, + '图片链接': item.image_url + })); + + // 转换为CSV格式 + const headers = Object.keys(exportData[0]); + const csvContent = [ + headers.join(','), + ...exportData.map(row => headers.map(header => `"${row[header] || ''}"`).join(',')) + ].join('\n'); + + // 创建下载链接 + const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', `商品搜索结果_${new Date().toISOString().slice(0, 10)}.csv`); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + showToast('搜索结果导出成功', 'success'); + } catch (error) { + console.error('导出搜索结果失败:', error); + showToast('导出搜索结果失败', 'danger'); + } +} diff --git a/static/log_management.html b/static/log_management.html index 38def28..7060b15 100644 --- a/static/log_management.html +++ b/static/log_management.html @@ -286,27 +286,61 @@ 'Authorization': `Bearer ${token}` } }) - .then(response => response.json()) + .then(response => { + console.log('API响应状态:', response.status); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.json(); + }) .then(data => { + console.log('API响应数据:', data); loadingDiv.style.display = 'none'; - + + if (data.success === false) { + console.error('API返回错误:', data.message); + noLogsDiv.innerHTML = ` +
    + +

    加载日志失败

    +

    ${data.message || '未知错误'}

    +
    + `; + noLogsDiv.style.display = 'block'; + return; + } + if (data.logs && data.logs.length > 0) { + console.log(`显示 ${data.logs.length} 条日志`); displayLogs(data.logs); updateLogInfo(data); logContainer.style.display = 'block'; } else { + console.log('没有日志数据'); + noLogsDiv.innerHTML = ` +
    + +

    暂无日志数据

    +
    + `; noLogsDiv.style.display = 'block'; } - + // 更新最后更新时间 - document.getElementById('lastUpdate').textContent = + document.getElementById('lastUpdate').textContent = '最后更新: ' + new Date().toLocaleTimeString('zh-CN'); }) .catch(error => { console.error('加载日志失败:', error); loadingDiv.style.display = 'none'; + noLogsDiv.innerHTML = ` +
    + +

    加载日志失败

    +

    ${error.message}

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