diff --git a/.gitignore b/.gitignore index d542d2d..c8bb84a 100644 --- a/.gitignore +++ b/.gitignore @@ -290,12 +290,67 @@ build/ # 测试和示例文件 test_*.py *_test.py +test_*.html +*_test.html +test_*.js +*_test.js example_*.py *_example.py demo_*.py *_demo.py fix_*.py *_fix.py +sample_*.py +*_sample.py +mock_*.py +*_mock.py +debug_*.py +*_debug.py + +# 测试相关目录 +tests/ +test/ +testing/ +__tests__/ +spec/ +specs/ + +# 测试配置文件 +pytest.ini +.pytest_cache/ +test_*.ini +*_test.ini +test_*.conf +*_test.conf + +# 测试数据文件 +test_*.json +*_test.json +test_*.csv +*_test.csv +test_*.xml +*_test.xml +test_*.xlsx +*_test.xlsx +test_*.txt +*_test.txt + +# 测试输出文件 +test_output/ +test_results/ +test_reports/ +coverage_html/ +.coverage +coverage.xml +htmlcov/ + +# 性能测试文件 +benchmark_*.py +*_benchmark.py +perf_*.py +*_perf.py +load_test_*.py +*_load_test.py # 文档文件(除了README.md) *.md @@ -339,7 +394,6 @@ setup.cfg tox.ini # 容器相关 -.dockerignore docker-compose.*.yml !docker-compose.yml !docker-compose-cn.yml @@ -385,4 +439,182 @@ systemd/ # 备份和归档 archive/ old/ -deprecated/ \ No newline at end of file +deprecated/ + +# ==================== 项目特定新增 ==================== +# 数据库文件 +xianyu_data.db +xianyu_data_backup_*.db + +# 实时日志文件 +realtime.log + +# 用户统计文件 +user_stats.db +user_stats.txt +stats.log + +# PHP测试文件 +php/ + +# 检查脚本 +check_disk_usage.py + +# Docker相关 +docker-compose.override.yml +.env.docker + +# IDE和编辑器 +.vscode/settings.json +.idea/workspace.xml +*.sublime-project +*.sublime-workspace + +# 操作系统特定 +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +desktop.ini +$RECYCLE.BIN/ + +# 网络和缓存 +.wget-hsts +.curl_sslcache + +# 临时和锁文件 +*.lock +*.pid +*.sock +*.port +.fuse_hidden* + +# 压缩和打包文件 +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# 媒体文件(如果不需要版本控制) +*.mp4 +*.avi +*.mov +*.wmv +*.flv +*.webm +*.mp3 +*.wav +*.flac +*.aac + +# 大文件和二进制文件 +*.bin +*.exe +*.dll +*.so +*.dylib + +# 文档生成 +docs/_build/ +docs/build/ +site/ +_site/ + +# 包管理器锁文件 +package-lock.json +yarn.lock +Pipfile.lock +poetry.lock + +# 环境和配置文件 +.env +.env.* +!.env.example +config.local.* +settings.local.* + +# 运行时生成的文件 +*.generated.* +*.auto.* +auto_* + +# 性能分析和调试 +*.prof +*.pstats +*.trace +*.debug +profile_* +debug_* + +# 安全相关 +*.key +*.pem +*.crt +*.cert +*.p12 +*.pfx +*.secret +*.token +*.auth +secrets/ +credentials/ +keys/ + +# 监控和统计 +monitoring/ +metrics/ +stats/ + +# 第三方工具 +.sonarqube/ +.scannerwork/ +.nyc_output/ +coverage/ +.coverage.* + +# 移动端开发 +*.apk +*.ipa +*.app +*.aab + +# 游戏开发 +*.unity +*.unitypackage + +# 科学计算 +*.mat +*.h5 +*.hdf5 + +# 地理信息系统 +*.shp +*.dbf +*.shx +*.prj + +# 3D模型 +*.obj +*.fbx +*.dae +*.3ds + +# 字体文件(如果不需要版本控制) +*.ttf +*.otf +*.woff +*.woff2 +*.eot + +# 数据文件(根据需要调整) +*.csv.bak +*.json.bak +*.xml.bak +*.sql.bak \ No newline at end of file diff --git a/README.md b/README.md index e1283d2..65d335e 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,30 @@ https://pan.baidu.com/s/1I6muOGJQYd6y3oxQSmtvrQ?pwd=gcpd +## 📋 项目概述 + +一个功能完整的闲鱼自动回复和管理系统,采用现代化的技术架构,支持多用户、多账号管理,具备智能回复、自动发货、自动确认发货、商品管理等企业级功能。系统基于Python异步编程,使用FastAPI提供RESTful API,SQLite数据库存储,支持Docker一键部署。 + > **⚠️ 重要提示:本项目仅供学习研究使用,严禁商业用途!使用前请仔细阅读[版权声明](#️-版权声明与使用条款)。** -一个功能完整的闲鱼自动回复和管理系统,支持多用户、多账号管理,具备智能回复、自动发货、自动确认发货、商品管理等企业级功能。 +## 🏗️ 技术架构 + +### 核心技术栈 +- **后端框架**: FastAPI + Python 3.11+ 异步编程 +- **数据库**: SQLite 3 + 多用户数据隔离 + 自动迁移 +- **前端**: Bootstrap 5 + Vanilla JavaScript + 响应式设计 +- **通信协议**: WebSocket + RESTful API + 实时通信 +- **部署方式**: Docker + Docker Compose + 一键部署 +- **日志系统**: Loguru + 文件轮转 + 实时收集 +- **安全认证**: JWT + 图形验证码 + 邮箱验证 + 权限控制 + +### 系统架构特点 +- **微服务设计**: 模块化架构,易于维护和扩展 +- **异步处理**: 基于asyncio的高性能异步处理 +- **多用户隔离**: 完全的数据隔离和权限控制 +- **容器化部署**: Docker容器化,支持一键部署 +- **实时监控**: WebSocket实时通信和状态监控 +- **自动化运维**: 自动重连、异常恢复、日志轮转 ## ✨ 核心特性 @@ -103,11 +124,12 @@ xianyu-auto-reply/ │ ├── xianyu_utils.py # 闲鱼API工具函数(加密、签名、解析) │ ├── message_utils.py # 消息格式化和处理工具 │ ├── ws_utils.py # WebSocket客户端封装 -│ ├── qr_login.py # 二维码登录功能 +│ ├── image_utils.py # 图片处理和管理工具 +│ ├── image_uploader.py # 图片上传到闲鱼CDN +│ ├── image_utils.py # 图片处理工具(压缩、格式转换) │ ├── item_search.py # 商品搜索功能(基于Playwright,无头模式) │ ├── order_detail_fetcher.py # 订单详情获取工具 -│ ├── image_utils.py # 图片处理工具(压缩、格式转换) -│ └── image_uploader.py # 图片上传到CDN工具 +│ └── qr_login.py # 二维码登录功能 ├── 🌐 前端界面 │ └── static/ │ ├── index.html # 主管理界面(集成所有功能模块) @@ -781,9 +803,88 @@ powershell -ExecutionPolicy Bypass -File docker-deploy.bat --- +## 📊 项目统计 + +- **代码行数**: 10,000+ 行 +- **功能模块**: 15+ 个核心模块 +- **API接口**: 50+ 个RESTful接口 +- **数据库表**: 20+ 个数据表 +- **支持平台**: Windows/Linux/macOS +- **部署方式**: Docker一键部署 +- **开发周期**: 持续更新维护 + +## 🎯 项目优势 + +### 技术优势 +- ✅ **现代化架构**: 基于FastAPI + Python 3.11+异步编程 +- ✅ **容器化部署**: Docker + Docker Compose一键部署 +- ✅ **多用户系统**: 完整的用户注册、登录、权限管理 +- ✅ **数据隔离**: 每个用户的数据完全独立,安全可靠 +- ✅ **实时通信**: WebSocket实时消息处理和状态监控 + +### 功能优势 +- ✅ **智能回复**: 关键词匹配 + AI智能回复 + 优先级策略 +- ✅ **自动发货**: 多种发货方式,支持规格匹配和延时发货 +- ✅ **商品管理**: 自动收集商品信息,支持批量操作 +- ✅ **订单管理**: 订单详情获取,支持自动确认发货 +- ✅ **安全保护**: 多层加密,防重复机制,异常恢复 + +### 运维优势 +- ✅ **日志系统**: 完整的日志记录和实时查看 +- ✅ **监控告警**: 账号状态监控和异常告警 +- ✅ **数据备份**: 自动数据备份和恢复机制 +- ✅ **性能优化**: 异步处理,高并发支持 +- ✅ **易于维护**: 模块化设计,代码结构清晰 +- ✅ **使用统计**: 匿名使用统计,帮助改进产品 + +## 📊 用户统计说明 + +### 统计目的 +为了了解有多少人在使用这个系统,系统会发送匿名的用户统计信息。 + +### 收集的信息 +- **匿名ID**: 基于机器特征生成的唯一标识符(重启不变) +- **操作系统**: 系统类型(如Windows、Linux) +- **版本信息**: 软件版本号 + +### 隐私保护 +- ✅ **完全匿名**: 不收集任何个人身份信息 +- ✅ **数据安全**: 不收集账号、密码、关键词等敏感信息 +- ✅ **本地优先**: 所有业务数据仅存储在本地 +- ✅ **持久化ID**: Docker重建时ID不变(保存在数据库中) + +### 查看统计信息 + +#### 方式1: Python统计服务器 +```bash +# 部署Python统计服务器 +python simple_stats_server.py + +# 访问统计服务器查看用户数量 +curl http://localhost:8081/stats +``` + +#### 方式2: PHP统计服务器 +```bash +# 将index.php部署到Web服务器(如Apache/Nginx) +# 访问统计接口 +curl http://localhost/php/stats + +# 测试统计功能 +python test_php_stats.py +``` + +**PHP统计服务器特点**: +- 数据保存在`user_stats.txt`文件中 +- 支持用户数据更新(anonymous_id作为key) +- 自动生成统计摘要 +- 记录操作日志到`stats.log` + +--- + 🎉 **开始使用闲鱼自动回复系统,让您的闲鱼店铺管理更加智能高效!** -**请记住:仅限学习使用,禁止商业用途!** +**⚠️ 重要提醒:本项目仅供学习研究使用,严禁商业用途!** ## Star History diff --git a/Start.py b/Start.py index 313adfb..e6c968b 100644 --- a/Start.py +++ b/Start.py @@ -27,6 +27,7 @@ from config import AUTO_REPLY, COOKIES_LIST import cookie_manager as cm from db_manager import db_manager from file_log_collector import setup_file_logging +from usage_statistics import report_user_count def _start_api_server(): @@ -143,6 +144,12 @@ async def main(): threading.Thread(target=_start_api_server, daemon=True).start() print("API 服务线程已启动") + # 上报用户统计 + try: + await report_user_count() + except Exception as e: + logger.debug(f"上报用户统计失败: {e}") + # 阻塞保持运行 print("主程序启动完成,保持运行...") await asyncio.Event().wait() diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 7c93570..ae9fcac 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -37,6 +37,11 @@ class AutoReplyPauseManager: logger.error(f"获取账号 {cookie_id} 暂停时间失败: {e},使用默认10分钟") pause_minutes = 10 + # 如果暂停时间为0,表示不暂停 + if pause_minutes == 0: + logger.info(f"【{cookie_id}】检测到手动发出消息,但暂停时间设置为0,不暂停自动回复") + return + pause_duration_seconds = pause_minutes * 60 pause_until = time.time() + pause_duration_seconds self.paused_chats[chat_id] = pause_until @@ -122,6 +127,10 @@ class XianyuLive: # 商品详情缓存(24小时有效) _item_detail_cache = {} # {item_id: {'detail': str, 'timestamp': float}} _item_detail_cache_lock = asyncio.Lock() + + # 类级别的实例管理字典,用于API调用 + _instances = {} # {cookie_id: XianyuLive实例} + _instances_lock = asyncio.Lock() def _safe_str(self, e): """安全地将异常转换为字符串""" @@ -213,7 +222,41 @@ class XianyuLive: self.max_connection_failures = 5 # 最大连续失败次数 self.last_successful_connection = 0 # 上次成功连接时间 + # 注册实例到类级别字典(用于API调用) + self._register_instance() + def _register_instance(self): + """注册当前实例到类级别字典""" + try: + # 使用同步方式注册,避免在__init__中使用async + XianyuLive._instances[self.cookie_id] = self + logger.debug(f"【{self.cookie_id}】实例已注册到全局字典") + except Exception as e: + logger.error(f"【{self.cookie_id}】注册实例失败: {self._safe_str(e)}") + + def _unregister_instance(self): + """从类级别字典中注销当前实例""" + try: + if self.cookie_id in XianyuLive._instances: + del XianyuLive._instances[self.cookie_id] + logger.debug(f"【{self.cookie_id}】实例已从全局字典中注销") + except Exception as e: + logger.error(f"【{self.cookie_id}】注销实例失败: {self._safe_str(e)}") + + @classmethod + def get_instance(cls, cookie_id: str): + """获取指定cookie_id的XianyuLive实例""" + return cls._instances.get(cookie_id) + + @classmethod + def get_all_instances(cls): + """获取所有活跃的XianyuLive实例""" + return dict(cls._instances) + + @classmethod + def get_instance_count(cls): + """获取当前活跃实例数量""" + return len(cls._instances) def is_auto_confirm_enabled(self) -> bool: """检查当前账号是否启用自动确认发货""" @@ -4991,6 +5034,10 @@ class XianyuLive: self.cookie_refresh_task.cancel() await self.close_session() # 确保关闭session + # 从全局实例字典中注销当前实例 + self._unregister_instance() + logger.info(f"【{self.cookie_id}】XianyuLive主程序已完全退出") + async def get_item_list_info(self, page_number=1, page_size=20, retry_count=0): """获取商品信息,自动处理token失效的情况 diff --git a/db_manager.py b/db_manager.py index f968481..b126772 100644 --- a/db_manager.py +++ b/db_manager.py @@ -414,7 +414,8 @@ class DBManager: ('smtp_password', '', 'SMTP登录密码/授权码'), ('smtp_from', '', '发件人显示名(留空则使用用户名)'), ('smtp_use_tls', 'true', '是否启用TLS'), - ('smtp_use_ssl', 'false', '是否启用SSL') + ('smtp_use_ssl', 'false', '是否启用SSL'), + ('qq_reply_secret_key', 'xianyu_qq_reply_2024', 'QQ回复消息API秘钥') ''') # 检查并升级数据库 @@ -1210,7 +1211,7 @@ class DBManager: 'user_id': result[2], 'auto_confirm': bool(result[3]), 'remark': result[4] or '', - 'pause_duration': result[5] if result[5] is not None else 10, + 'pause_duration': result[5] if result[5] is not None else 10, # 0是有效值,表示不暂停 'created_at': result[6] } return None @@ -1271,7 +1272,7 @@ class DBManager: self._execute_sql(cursor, "UPDATE cookies SET pause_duration = 10 WHERE id = ?", (cookie_id,)) self.conn.commit() return 10 - return result[0] # 返回实际值,不使用or操作符 + return result[0] # 返回实际值,包括0(0表示不暂停) else: logger.warning(f"账号 {cookie_id} 未找到记录,使用默认值10分钟") return 10 @@ -1338,6 +1339,29 @@ class DBManager: try: cursor = self.conn.cursor() + # 检查是否与现有图片关键词冲突 + for keyword, reply, item_id in keywords: + normalized_item_id = item_id if item_id and item_id.strip() else None + + # 检查是否存在同名的图片关键词 + if normalized_item_id: + # 有商品ID的情况:检查 (cookie_id, keyword, item_id) 是否存在图片关键词 + self._execute_sql(cursor, + "SELECT type FROM keywords WHERE cookie_id = ? AND keyword = ? AND item_id = ? AND type = 'image'", + (cookie_id, keyword, normalized_item_id)) + else: + # 通用关键词的情况:检查 (cookie_id, keyword) 是否存在图片关键词 + self._execute_sql(cursor, + "SELECT type FROM keywords WHERE cookie_id = ? AND keyword = ? AND (item_id IS NULL OR item_id = '') AND type = 'image'", + (cookie_id, keyword)) + + if cursor.fetchone(): + # 存在同名图片关键词,抛出友好的错误信息 + item_desc = f"商品ID: {normalized_item_id}" if normalized_item_id else "通用关键词" + error_msg = f"关键词 '{keyword}' ({item_desc}) 已存在(图片关键词),无法保存为文本关键词" + logger.warning(f"文本关键词与图片关键词冲突: Cookie={cookie_id}, 关键词='{keyword}', {item_desc}") + raise ValueError(error_msg) + # 只删除该cookie_id的文本类型关键字,保留图片关键词 self._execute_sql(cursor, "DELETE FROM keywords WHERE cookie_id = ? AND (type IS NULL OR type = 'text')", @@ -1348,22 +1372,15 @@ class DBManager: # 标准化item_id:空字符串转为NULL normalized_item_id = item_id if item_id and item_id.strip() else None - try: - self._execute_sql(cursor, - "INSERT INTO keywords (cookie_id, keyword, reply, item_id, type) VALUES (?, ?, ?, ?, 'text')", - (cookie_id, keyword, reply, normalized_item_id)) - except sqlite3.IntegrityError as ie: - # 如果遇到唯一约束冲突,记录详细错误信息并回滚 - item_desc = f"商品ID: {normalized_item_id}" if normalized_item_id else "通用关键词" - logger.error(f"关键词唯一约束冲突: Cookie={cookie_id}, 关键词='{keyword}', {item_desc}") - self.conn.rollback() - raise ie + self._execute_sql(cursor, + "INSERT INTO keywords (cookie_id, keyword, reply, item_id, type) VALUES (?, ?, ?, ?, 'text')", + (cookie_id, keyword, reply, normalized_item_id)) self.conn.commit() logger.info(f"文本关键字保存成功: {cookie_id}, {len(keywords)}条,图片关键词已保留") return True - except sqlite3.IntegrityError: - # 唯一约束冲突,重新抛出异常让上层处理 + except ValueError: + # 重新抛出友好的错误信息 raise except Exception as e: logger.error(f"文本关键字保存失败: {e}") diff --git a/reply_server.py b/reply_server.py index 238a230..140b1da 100644 --- a/reply_server.py +++ b/reply_server.py @@ -24,6 +24,7 @@ from ai_reply_engine import ai_reply_engine from utils.qr_login import qr_login_manager from utils.xianyu_utils import trans_cookies from utils.image_utils import image_manager + from loguru import logger # 关键字文件路径 @@ -848,6 +849,146 @@ async def register(request: RegisterRequest): ) +# ------------------------- 发送消息接口 ------------------------- + +# 固定的API秘钥(生产环境中应该从配置文件或环境变量读取) +# 注意:现在从系统设置中读取QQ回复消息秘钥 +API_SECRET_KEY = "xianyu_api_secret_2024" # 保留作为后备 + +class SendMessageRequest(BaseModel): + api_key: str + cookie_id: str + chat_id: str + to_user_id: str + message: str + + +class SendMessageResponse(BaseModel): + success: bool + message: str + + +def verify_api_key(api_key: str) -> bool: + """验证API秘钥""" + try: + # 从系统设置中获取QQ回复消息秘钥 + from db_manager import db_manager + qq_secret_key = db_manager.get_system_setting('qq_reply_secret_key') + + # 如果系统设置中没有配置,使用默认值 + if not qq_secret_key: + qq_secret_key = API_SECRET_KEY + + return api_key == qq_secret_key + except Exception as e: + logger.error(f"验证API秘钥时发生异常: {e}") + # 异常情况下使用默认秘钥验证 + return api_key == API_SECRET_KEY + + +@app.post('/send-message', response_model=SendMessageResponse) +async def send_message_api(request: SendMessageRequest): + """发送消息API接口(使用秘钥验证)""" + try: + # 清理所有参数中的换行符 + def clean_param(param_str): + """清理参数中的换行符""" + if isinstance(param_str, str): + return param_str.replace('\\n', '').replace('\n', '') + return param_str + + # 清理所有参数 + cleaned_api_key = clean_param(request.api_key) + cleaned_cookie_id = clean_param(request.cookie_id) + cleaned_chat_id = clean_param(request.chat_id) + cleaned_to_user_id = clean_param(request.to_user_id) + cleaned_message = clean_param(request.message) + + # 验证API秘钥不能为空 + if not cleaned_api_key: + logger.warning("API秘钥为空") + return SendMessageResponse( + success=False, + message="API秘钥不能为空" + ) + + # 特殊测试秘钥处理 + if cleaned_api_key == "zhinina_test_key": + logger.info("使用测试秘钥,直接返回成功") + return SendMessageResponse( + success=True, + message="接口验证成功" + ) + + # 验证API秘钥 + if not verify_api_key(cleaned_api_key): + logger.warning(f"API秘钥验证失败: {cleaned_api_key}") + return SendMessageResponse( + success=False, + message="API秘钥验证失败" + ) + + # 验证必需参数不能为空 + required_params = { + 'cookie_id': cleaned_cookie_id, + 'chat_id': cleaned_chat_id, + 'to_user_id': cleaned_to_user_id, + 'message': cleaned_message + } + + for param_name, param_value in required_params.items(): + if not param_value: + logger.warning(f"必需参数 {param_name} 为空") + return SendMessageResponse( + success=False, + message=f"参数 {param_name} 不能为空" + ) + + # 直接获取XianyuLive实例,跳过cookie_manager检查 + from XianyuAutoAsync import XianyuLive + live_instance = XianyuLive.get_instance(cleaned_cookie_id) + + if not live_instance: + logger.warning(f"账号实例不存在或未连接: {cleaned_cookie_id}") + return SendMessageResponse( + success=False, + message="账号实例不存在或未连接,请检查账号状态" + ) + + # 检查WebSocket连接状态 + if not live_instance.ws or live_instance.ws.closed: + logger.warning(f"账号WebSocket连接已断开: {cleaned_cookie_id}") + return SendMessageResponse( + success=False, + message="账号WebSocket连接已断开,请等待重连" + ) + + # 发送消息(使用清理后的所有参数) + await live_instance.send_msg( + live_instance.ws, + cleaned_chat_id, + cleaned_to_user_id, + cleaned_message + ) + + logger.info(f"API成功发送消息: {cleaned_cookie_id} -> {cleaned_to_user_id}, 内容: {cleaned_message[:50]}{'...' if len(cleaned_message) > 50 else ''}") + + return SendMessageResponse( + success=True, + message="消息发送成功" + ) + + except Exception as e: + # 使用清理后的参数记录日志 + cookie_id_for_log = clean_param(request.cookie_id) if 'clean_param' in locals() else request.cookie_id + to_user_id_for_log = clean_param(request.to_user_id) if 'clean_param' in locals() else request.to_user_id + logger.error(f"API发送消息异常: {cookie_id_for_log} -> {to_user_id_for_log}, 错误: {str(e)}") + return SendMessageResponse( + success=False, + message=f"发送消息失败: {str(e)}" + ) + + @app.post("/xianyu/reply", response_model=ResponseModel) async def xianyu_reply(req: RequestModel): msg_template = match_reply(req.cookie_id, req.send_message) @@ -928,6 +1069,11 @@ class MessageNotificationIn(BaseModel): class SystemSettingIn(BaseModel): + value: str + description: Optional[str] = None + + +class SystemSettingCreateIn(BaseModel): key: str value: str description: Optional[str] = None @@ -2027,9 +2173,9 @@ def update_cookie_pause_duration(cid: str, update_data: PauseDurationUpdate, cur if cid not in user_cookies: raise HTTPException(status_code=403, detail="无权限操作该Cookie") - # 验证暂停时间范围(1-60分钟) - if not (1 <= update_data.pause_duration <= 60): - raise HTTPException(status_code=400, detail="暂停时间必须在1-60分钟之间") + # 验证暂停时间范围(0-60分钟,0表示不暂停) + if not (0 <= update_data.pause_duration <= 60): + raise HTTPException(status_code=400, detail="暂停时间必须在0-60分钟之间(0表示不暂停)") # 更新暂停时间 success = db_manager.update_cookie_pause_duration(cid, update_data.pause_duration) @@ -2207,12 +2353,40 @@ def update_keywords_with_item_id(cid: str, body: KeywordWithItemIdIn, current_us raise HTTPException(status_code=500, detail="保存关键词失败") except Exception as e: error_msg = str(e) - if "UNIQUE constraint failed" in error_msg: - # 解析具体的冲突信息 - if "keywords.cookie_id, keywords.keyword" in error_msg: - raise HTTPException(status_code=400, detail="关键词重复!该关键词已存在(可能是图片关键词或文本关键词),请使用其他关键词") + + # 检查是否是图片关键词冲突 + if "已存在(图片关键词)" in error_msg: + # 直接使用数据库管理器提供的友好错误信息 + raise HTTPException(status_code=400, detail=error_msg) + elif "UNIQUE constraint failed" in error_msg or "唯一约束冲突" in error_msg: + # 尝试从错误信息中提取具体的冲突关键词 + conflict_keyword = None + conflict_type = None + + # 检查是否是数据库管理器抛出的详细错误 + if "关键词唯一约束冲突" in error_msg: + # 解析详细错误信息:关键词唯一约束冲突: Cookie=xxx, 关键词='xxx', 通用关键词/商品ID: xxx + import re + keyword_match = re.search(r"关键词='([^']+)'", error_msg) + if keyword_match: + conflict_keyword = keyword_match.group(1) + + if "通用关键词" in error_msg: + conflict_type = "通用关键词" + elif "商品ID:" in error_msg: + item_match = re.search(r"商品ID: ([^\s,]+)", error_msg) + if item_match: + conflict_type = f"商品关键词(商品ID: {item_match.group(1)})" + + # 构造用户友好的错误信息 + if conflict_keyword and conflict_type: + detail_msg = f'关键词 "{conflict_keyword}" ({conflict_type}) 已存在,请使用其他关键词或商品ID' + elif "keywords.cookie_id, keywords.keyword" in error_msg: + detail_msg = "关键词重复!该关键词已存在(可能是图片关键词或文本关键词),请使用其他关键词" else: - raise HTTPException(status_code=400, detail="关键词重复!请使用不同的关键词或商品ID组合") + detail_msg = "关键词重复!请使用不同的关键词或商品ID组合" + + raise HTTPException(status_code=400, detail=detail_msg) else: log_with_user('error', f"保存关键词时发生未知错误: {error_msg}", current_user) raise HTTPException(status_code=500, detail="保存关键词失败") @@ -4327,6 +4501,43 @@ def update_item_multi_quantity_delivery(cookie_id: str, item_id: str, delivery_d raise HTTPException(status_code=500, detail=str(e)) + + + +# ==================== 订单管理接口 ==================== + +@app.get('/api/orders') +def get_user_orders(current_user: Dict[str, Any] = Depends(get_current_user)): + """获取当前用户的订单信息""" + try: + from db_manager import db_manager + + user_id = current_user['user_id'] + log_with_user('info', "查询用户订单信息", current_user) + + # 获取用户的所有Cookie + user_cookies = db_manager.get_all_cookies(user_id) + + # 获取所有订单数据 + all_orders = [] + for cookie_id in user_cookies.keys(): + orders = db_manager.get_orders_by_cookie(cookie_id, limit=1000) # 增加限制数量 + # 为每个订单添加cookie_id信息 + for order in orders: + order['cookie_id'] = cookie_id + all_orders.append(order) + + # 按创建时间倒序排列 + all_orders.sort(key=lambda x: x.get('created_at', ''), reverse=True) + + log_with_user('info', f"用户订单查询成功,共 {len(all_orders)} 条记录", current_user) + return {"success": True, "data": all_orders} + + except Exception as e: + log_with_user('error', f"查询用户订单失败: {str(e)}", current_user) + raise HTTPException(status_code=500, detail=f"查询订单失败: {str(e)}") + + # 移除自动启动,由Start.py或手动启动 # if __name__ == "__main__": # uvicorn.run(app, host="0.0.0.0", port=8080) \ No newline at end of file diff --git a/simple_stats_server.py b/simple_stats_server.py new file mode 100644 index 0000000..0c6d485 --- /dev/null +++ b/simple_stats_server.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +简单的用户统计服务器 +只统计有多少人在使用闲鱼自动回复系统 +""" + +from fastapi import FastAPI +from pydantic import BaseModel +from typing import Dict, Any +import sqlite3 +from datetime import datetime +import uvicorn +from pathlib import Path + +app = FastAPI(title="闲鱼自动回复系统用户统计", version="1.0.0") + +# 数据库文件路径 +DB_PATH = Path(__file__).parent / "user_stats.db" + + +class UserStats(BaseModel): + """用户统计数据模型""" + anonymous_id: str + timestamp: str + project: str + info: Dict[str, Any] + + +def init_database(): + """初始化数据库""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # 创建用户统计表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS user_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + anonymous_id TEXT UNIQUE NOT NULL, + first_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + os TEXT, + version TEXT, + total_reports INTEGER DEFAULT 1 + ) + ''') + + # 创建索引 + cursor.execute('CREATE INDEX IF NOT EXISTS idx_anonymous_id ON user_stats(anonymous_id)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_last_seen ON user_stats(last_seen)') + + conn.commit() + conn.close() + + +def save_user_stats(data: UserStats): + """保存用户统计数据""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + try: + info = data.info + os_info = info.get('os', 'unknown') + version = info.get('version', '2.2.0') + + # 检查用户是否已存在 + cursor.execute('SELECT id, total_reports FROM user_stats WHERE anonymous_id = ?', (data.anonymous_id,)) + existing = cursor.fetchone() + + if existing: + # 更新现有用户的最后访问时间和报告次数 + cursor.execute(''' + UPDATE user_stats + SET last_seen = CURRENT_TIMESTAMP, + total_reports = total_reports + 1, + os = ?, + version = ? + WHERE anonymous_id = ? + ''', (os_info, version, data.anonymous_id)) + else: + # 插入新用户 + cursor.execute(''' + INSERT INTO user_stats (anonymous_id, os, version) + VALUES (?, ?, ?) + ''', (data.anonymous_id, os_info, version)) + + conn.commit() + return True + + except Exception as e: + print(f"保存用户统计失败: {e}") + return False + finally: + conn.close() + + +@app.post('/statistics') +async def receive_user_stats(data: UserStats): + """接收用户统计数据""" + try: + success = save_user_stats(data) + + if success: + print(f"收到用户统计: {data.anonymous_id}") + return {"status": "success", "message": "用户统计已收到"} + else: + return {"status": "error", "message": "保存统计数据失败"} + + except Exception as e: + print(f"处理用户统计失败: {e}") + return {"status": "error", "message": "处理统计数据失败"} + + +@app.get('/stats') +async def get_user_stats(): + """获取用户统计摘要""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + try: + # 总用户数 + cursor.execute('SELECT COUNT(*) FROM user_stats') + total_users = cursor.fetchone()[0] + + # 今日活跃用户 + cursor.execute(''' + SELECT COUNT(*) + FROM user_stats + WHERE DATE(last_seen) = DATE('now') + ''') + daily_active = cursor.fetchone()[0] + + # 本周活跃用户 + cursor.execute(''' + SELECT COUNT(*) + FROM user_stats + WHERE DATE(last_seen) >= DATE('now', '-7 days') + ''') + weekly_active = cursor.fetchone()[0] + + # 操作系统分布 + cursor.execute(''' + SELECT os, COUNT(*) as count + FROM user_stats + GROUP BY os + ORDER BY count DESC + ''') + os_distribution = dict(cursor.fetchall()) + + # 版本分布 + cursor.execute(''' + SELECT version, COUNT(*) as count + FROM user_stats + GROUP BY version + ORDER BY count DESC + ''') + version_distribution = dict(cursor.fetchall()) + + return { + "total_users": total_users, + "daily_active_users": daily_active, + "weekly_active_users": weekly_active, + "os_distribution": os_distribution, + "version_distribution": version_distribution, + "last_updated": datetime.now().isoformat() + } + + except Exception as e: + return {"error": f"获取统计失败: {e}"} + finally: + conn.close() + + +@app.get('/stats/recent') +async def get_recent_users(): + """获取最近活跃的用户(匿名)""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + try: + cursor.execute(''' + SELECT anonymous_id, first_seen, last_seen, os, version, total_reports + FROM user_stats + ORDER BY last_seen DESC + LIMIT 20 + ''') + + records = cursor.fetchall() + + return { + "recent_users": [ + { + "anonymous_id": record[0][:8] + "****", # 部分隐藏ID + "first_seen": record[1], + "last_seen": record[2], + "os": record[3], + "version": record[4], + "total_reports": record[5] + } + for record in records + ] + } + + except Exception as e: + return {"error": f"获取最近用户失败: {e}"} + finally: + conn.close() + + +@app.get('/') +async def root(): + """根路径""" + return { + "message": "闲鱼自动回复系统用户统计服务器", + "description": "只统计有多少人在使用这个系统", + "endpoints": { + "POST /statistics": "接收用户统计数据", + "GET /stats": "获取用户统计摘要", + "GET /stats/recent": "获取最近活跃用户" + } + } + + +if __name__ == "__main__": + # 初始化数据库 + init_database() + print("用户统计数据库初始化完成") + + # 启动服务器 + print("启动用户统计服务器...") + print("访问 http://localhost:8081/stats 查看统计信息") + uvicorn.run(app, host="0.0.0.0", port=8081) diff --git a/static/index.html b/static/index.html index cf0c6e3..fa359c8 100644 --- a/static/index.html +++ b/static/index.html @@ -155,7 +155,11 @@
系统概览和统计信息
-