提交
This commit is contained in:
parent
3f42d07aa1
commit
ea44e32e32
234
.gitignore
vendored
234
.gitignore
vendored
@ -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
|
||||
@ -386,3 +440,181 @@ systemd/
|
||||
archive/
|
||||
old/
|
||||
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
|
||||
111
README.md
111
README.md
@ -9,9 +9,30 @@
|
||||
|
||||
我用夸克网盘给你分享了「自动发货系统源码」,点击链接或复制整段内容,打开「夸克APP」即可获取。 /~5e4237vG5B~:/ 链接:https://pan.quark.cn/s/88d118bd700e
|
||||
|
||||
## 📋 项目概述
|
||||
|
||||
一个功能完整的闲鱼自动回复和管理系统,采用现代化的技术架构,支持多用户、多账号管理,具备智能回复、自动发货、自动确认发货、商品管理等企业级功能。系统基于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 # 主管理界面(集成所有功能模块)
|
||||
@ -780,9 +802,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
|
||||
|
||||
|
||||
7
Start.py
7
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()
|
||||
|
||||
@ -1339,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')",
|
||||
@ -1349,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.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}")
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
快速测试QQ回复消息API的脚本
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
def test_api(api_key, cookie_id="test", chat_id="test", to_user_id="test", message="test"):
|
||||
"""测试API调用"""
|
||||
url = "http://localhost:8000/send-message"
|
||||
|
||||
data = {
|
||||
"api_key": api_key,
|
||||
"cookie_id": cookie_id,
|
||||
"chat_id": chat_id,
|
||||
"to_user_id": to_user_id,
|
||||
"message": message
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=data, timeout=10)
|
||||
result = response.json()
|
||||
|
||||
print(f"秘钥: {api_key}")
|
||||
print(f"状态: {response.status_code}")
|
||||
print(f"响应: {json.dumps(result, ensure_ascii=False, indent=2)}")
|
||||
print("-" * 50)
|
||||
|
||||
return result.get("success", False)
|
||||
|
||||
except Exception as e:
|
||||
print(f"请求失败: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("🚀 快速API测试")
|
||||
print("=" * 50)
|
||||
|
||||
# 测试用例
|
||||
test_cases = [
|
||||
("默认秘钥", "xianyu_qq_reply_2024"),
|
||||
("测试秘钥", "zhinina_test_key"),
|
||||
("错误秘钥", "wrong_key"),
|
||||
("空秘钥", ""),
|
||||
]
|
||||
|
||||
for name, key in test_cases:
|
||||
print(f"\n📋 测试: {name}")
|
||||
test_api(key)
|
||||
|
||||
# 测试参数验证
|
||||
print("\n📋 测试参数验证:")
|
||||
|
||||
# 测试空参数
|
||||
param_tests = [
|
||||
("空cookie_id", {"cookie_id": ""}),
|
||||
("空chat_id", {"chat_id": ""}),
|
||||
("空to_user_id", {"to_user_id": ""}),
|
||||
("空message", {"message": ""}),
|
||||
]
|
||||
|
||||
for name, params in param_tests:
|
||||
print(f"\n测试: {name}")
|
||||
default_params = {
|
||||
"cookie_id": "test",
|
||||
"chat_id": "test",
|
||||
"to_user_id": "test",
|
||||
"message": "test"
|
||||
}
|
||||
default_params.update(params)
|
||||
test_api("xianyu_qq_reply_2024", **default_params)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -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
|
||||
|
||||
# 关键字文件路径
|
||||
@ -2352,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="保存关键词失败")
|
||||
@ -4472,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)
|
||||
232
simple_stats_server.py
Normal file
232
simple_stats_server.py
Normal file
@ -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)
|
||||
@ -155,7 +155,11 @@
|
||||
</h2>
|
||||
<p class="text-muted mb-0">系统概览和统计信息</p>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<div class="version-info d-flex gap-2">
|
||||
<span class="badge bg-primary" id="projectUsers" style="cursor: pointer;" onclick="showProjectStats()" title="点击查看详细统计">
|
||||
<i class="bi bi-people me-1"></i>
|
||||
使用人数: <span id="totalUsers">加载中...</span>
|
||||
</span>
|
||||
<span class="badge bg-secondary" id="systemVersion">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
版本: <span id="versionNumber">加载中...</span>
|
||||
@ -960,7 +964,7 @@
|
||||
<th>商品关键字</th>
|
||||
<th>匹配卡券</th>
|
||||
<th>卡券类型</th>
|
||||
<th>发货数量</th>
|
||||
<!-- <th>发货数量</th> 隐藏发货数量列 -->
|
||||
<th>状态</th>
|
||||
<th>已发货次数</th>
|
||||
<th>操作</th>
|
||||
@ -2794,11 +2798,8 @@
|
||||
<option value="">请选择卡券</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">发货数量</label>
|
||||
<input type="number" class="form-control" id="deliveryCount" value="1" min="1" max="10">
|
||||
<small class="form-text text-muted">每次发货的数量</small>
|
||||
</div>
|
||||
<!-- 隐藏发货数量字段,默认为1 -->
|
||||
<input type="hidden" id="deliveryCount" value="1">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="ruleEnabled" checked>
|
||||
@ -2846,11 +2847,8 @@
|
||||
<option value="">请选择卡券</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">发货数量</label>
|
||||
<input type="number" class="form-control" id="editDeliveryCount" value="1" min="1" max="10">
|
||||
<small class="form-text text-muted">每次发货的数量</small>
|
||||
</div>
|
||||
<!-- 隐藏发货数量字段,默认为1 -->
|
||||
<input type="hidden" id="editDeliveryCount" value="1">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="editRuleEnabled">
|
||||
|
||||
190
static/js/app.js
190
static/js/app.js
@ -233,7 +233,7 @@ async function loadDashboard() {
|
||||
async function loadOrdersCount() {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const response = await fetch('/admin/data/orders', {
|
||||
const response = await fetch('/api/orders', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
@ -702,7 +702,13 @@ async function addKeyword() {
|
||||
}
|
||||
|
||||
// 检查关键词是否已存在(考虑商品ID,检查所有类型的关键词)
|
||||
const allKeywords = keywordsData[currentCookieId] || [];
|
||||
// 在编辑模式下,需要排除正在编辑的关键词本身
|
||||
let allKeywords = keywordsData[currentCookieId] || [];
|
||||
if (isEditMode && typeof window.editingIndex !== 'undefined') {
|
||||
// 创建一个副本,排除正在编辑的关键词
|
||||
allKeywords = allKeywords.filter((item, index) => index !== window.editingIndex);
|
||||
}
|
||||
|
||||
const existingKeyword = allKeywords.find(item =>
|
||||
item.keyword === keyword &&
|
||||
(item.item_id || '') === (itemId || '')
|
||||
@ -1906,6 +1912,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// 加载系统版本号
|
||||
loadSystemVersion();
|
||||
// 启动项目使用人数定时刷新
|
||||
startProjectUsersRefresh();
|
||||
// 添加Cookie表单提交
|
||||
document.getElementById('addForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
@ -4219,9 +4227,8 @@ function renderDeliveryRulesList(rules) {
|
||||
</div>
|
||||
</td>
|
||||
<td>${cardTypeBadge}</td>
|
||||
<td>
|
||||
<span class="badge bg-info">${rule.delivery_count || 1}</span>
|
||||
</td>
|
||||
<!-- 隐藏发货数量列 -->
|
||||
<!-- <td><span class="badge bg-info">${rule.delivery_count || 1}</span></td> -->
|
||||
<td>${statusBadge}</td>
|
||||
<td>
|
||||
<span class="badge bg-warning">${rule.delivery_times || 0}</span>
|
||||
@ -4330,7 +4337,7 @@ async function saveDeliveryRule() {
|
||||
try {
|
||||
const keyword = document.getElementById('productKeyword').value;
|
||||
const cardId = document.getElementById('selectedCard').value;
|
||||
const deliveryCount = document.getElementById('deliveryCount').value;
|
||||
const deliveryCount = document.getElementById('deliveryCount').value || 1;
|
||||
const enabled = document.getElementById('ruleEnabled').checked;
|
||||
const description = document.getElementById('ruleDescription').value;
|
||||
|
||||
@ -4787,7 +4794,7 @@ async function updateDeliveryRule() {
|
||||
const ruleId = document.getElementById('editRuleId').value;
|
||||
const keyword = document.getElementById('editProductKeyword').value;
|
||||
const cardId = document.getElementById('editSelectedCard').value;
|
||||
const deliveryCount = document.getElementById('editDeliveryCount').value;
|
||||
const deliveryCount = document.getElementById('editDeliveryCount').value || 1;
|
||||
const enabled = document.getElementById('editRuleEnabled').checked;
|
||||
const description = document.getElementById('editRuleDescription').value;
|
||||
|
||||
@ -8428,7 +8435,7 @@ async function loadOrderCookieFilter() {
|
||||
// 加载所有订单
|
||||
async function loadAllOrders() {
|
||||
try {
|
||||
const response = await fetch(`${apiBase}/admin/data/orders`, {
|
||||
const response = await fetch(`${apiBase}/api/orders`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
@ -8461,7 +8468,7 @@ async function loadOrdersByCookie() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBase}/admin/data/orders`, {
|
||||
const response = await fetch(`${apiBase}/api/orders`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
@ -10128,6 +10135,171 @@ function exportSearchResults() {
|
||||
// 版本管理功能
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* 加载项目使用人数
|
||||
*/
|
||||
async function loadProjectUsers() {
|
||||
try {
|
||||
const response = await fetch('http://xianyu.zhinianblog.cn/?action=stats');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
console.error('获取项目使用人数失败:', result.error);
|
||||
document.getElementById('totalUsers').textContent = '获取失败';
|
||||
return;
|
||||
}
|
||||
|
||||
const totalUsers = result.total_users || 0;
|
||||
document.getElementById('totalUsers').textContent = totalUsers;
|
||||
|
||||
// 如果用户数量大于0,可以添加一些视觉效果
|
||||
if (totalUsers > 0) {
|
||||
const usersElement = document.getElementById('projectUsers');
|
||||
usersElement.classList.remove('bg-primary');
|
||||
usersElement.classList.add('bg-success');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取项目使用人数失败:', error);
|
||||
document.getElementById('totalUsers').textContent = '网络错误';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动项目使用人数定时刷新
|
||||
*/
|
||||
function startProjectUsersRefresh() {
|
||||
// 立即加载一次
|
||||
loadProjectUsers();
|
||||
|
||||
// 每5分钟刷新一次
|
||||
setInterval(() => {
|
||||
loadProjectUsers();
|
||||
}, 5 * 60 * 1000); // 5分钟 = 5 * 60 * 1000毫秒
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示项目详细统计信息
|
||||
*/
|
||||
async function showProjectStats() {
|
||||
try {
|
||||
const response = await fetch('http://xianyu.zhinianblog.cn/?action=stats');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
showToast('获取统计信息失败: ' + data.error, 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建模态框HTML
|
||||
const modalHtml = `
|
||||
<div class="modal fade" id="projectStatsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-bar-chart me-2"></i>项目使用统计
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="text-center p-3 bg-light rounded">
|
||||
<div class="h2 text-primary mb-1">${data.total_users || 0}</div>
|
||||
<div class="text-muted">总用户数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center p-3 bg-light rounded">
|
||||
<div class="h2 text-success mb-1">${data.daily_active_users || 0}</div>
|
||||
<div class="text-muted">今日活跃</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center p-3 bg-light rounded">
|
||||
<div class="h2 text-info mb-1">${Object.keys(data.os_distribution || {}).length}</div>
|
||||
<div class="text-muted">操作系统类型</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center p-3 bg-light rounded">
|
||||
<div class="h2 text-warning mb-1">${Object.keys(data.version_distribution || {}).length}</div>
|
||||
<div class="text-muted">版本类型</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-laptop me-2"></i>操作系统分布</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${Object.entries(data.os_distribution || {}).map(([os, count]) => `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span>${os}</span>
|
||||
<span class="badge bg-primary">${count}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-tag me-2"></i>版本分布</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${Object.entries(data.version_distribution || {}).map(([version, count]) => `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span>${version}</span>
|
||||
<span class="badge bg-success">${count}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-muted text-center">
|
||||
<small>最后更新: ${data.last_updated || '未知'}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" onclick="loadProjectUsers()">刷新数据</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 移除已存在的模态框
|
||||
const existingModal = document.getElementById('projectStatsModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// 添加新模态框到页面
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
// 显示模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('projectStatsModal'));
|
||||
modal.show();
|
||||
|
||||
// 模态框关闭后移除DOM元素
|
||||
document.getElementById('projectStatsModal').addEventListener('hidden.bs.modal', function () {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取项目统计失败:', error);
|
||||
showToast('获取项目统计失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载系统版本号并检查更新
|
||||
*/
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
🎉1.新增qq直接回复闲鱼消息,无需在进入闲鱼;
|
||||
'🎉1.新增qq直接回复闲鱼消息,无需在进入闲鱼(注意只能有公网访问的才行);
|
||||
①回复哪条消息引用即可
|
||||
②系统设置维护qq回复秘钥
|
||||
③在qq上给机器人发送 咸鱼绑定 key:url 其中key是你系统设置配置的秘钥,url 是你公网服务器的 ip加端口,例如 zheshimiyao:http://10.12.13.14:8080
|
||||
④如果没有公网,可联系我购买内网穿透服务,2块一个月,物美价廉
|
||||
🎉2.支持自动回复暂停时间支持设置为0;
|
||||
④如果没有公网,可联系我购买内网穿透服务,2块一个月,物美价廉',
|
||||
'🎉2.支持自动回复暂停时间支持设置为0;',
|
||||
'🎉3.修复自动回复关键词无法修改的问题;',
|
||||
'🎉4.修复订单管理普通用户无法查看的问题;',
|
||||
'🎉5.删除自动发货界面的发货数量,防止误解,想实现多数量发货在商品管理开启;',
|
||||
'🎉6.增加项目使用人数统计;',
|
||||
@ -1 +1 @@
|
||||
v1.0.0
|
||||
v1.0.2
|
||||
|
||||
177
usage_statistics.py
Normal file
177
usage_statistics.py
Normal file
@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
用户使用统计模块
|
||||
只统计有多少人在使用这个系统
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import platform
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class UsageStatistics:
|
||||
"""用户使用统计收集器 - 只统计用户数量"""
|
||||
|
||||
def __init__(self):
|
||||
# 默认启用统计
|
||||
self.enabled = True
|
||||
self.api_endpoint = "http://xianyu.zhinianblog.cn/?action=statistics" # PHP统计接收端点
|
||||
self.timeout = 5
|
||||
self.retry_count = 1
|
||||
|
||||
# 生成持久化的匿名用户ID
|
||||
self.anonymous_id = self._get_or_create_anonymous_id()
|
||||
|
||||
def _get_or_create_anonymous_id(self) -> str:
|
||||
"""获取或创建持久化的匿名用户ID"""
|
||||
# 保存到数据库中,确保Docker重建时ID不变
|
||||
try:
|
||||
from db_manager import db_manager
|
||||
|
||||
# 尝试从数据库获取ID
|
||||
existing_id = db_manager.get_system_setting('anonymous_user_id')
|
||||
if existing_id and len(existing_id) == 16:
|
||||
return existing_id
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 生成新的匿名ID
|
||||
new_id = self._generate_anonymous_id()
|
||||
|
||||
# 保存到数据库
|
||||
try:
|
||||
from db_manager import db_manager
|
||||
db_manager.set_system_setting('anonymous_user_id', new_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return new_id
|
||||
|
||||
def _generate_anonymous_id(self) -> str:
|
||||
"""生成匿名用户ID(基于机器特征)"""
|
||||
try:
|
||||
# 使用系统信息生成唯一但匿名的ID
|
||||
machine_info = f"{platform.machine()}-{platform.processor()}-{platform.system()}"
|
||||
unique_str = f"{machine_info}-{platform.python_version()}"
|
||||
|
||||
# 生成哈希值作为匿名ID
|
||||
anonymous_id = hashlib.sha256(unique_str.encode()).hexdigest()[:16]
|
||||
return anonymous_id
|
||||
except Exception:
|
||||
# 如果获取系统信息失败,使用随机ID
|
||||
import time
|
||||
return hashlib.md5(str(time.time()).encode()).hexdigest()[:16]
|
||||
|
||||
def _get_basic_info(self) -> Dict[str, Any]:
|
||||
"""获取基本信息(只包含必要信息)"""
|
||||
try:
|
||||
# 从version.txt文件读取版本号
|
||||
version = self._get_version()
|
||||
return {
|
||||
"os": platform.system(),
|
||||
"version": version
|
||||
}
|
||||
except Exception:
|
||||
return {"os": "unknown", "version": "unknown"}
|
||||
|
||||
def _get_version(self) -> str:
|
||||
"""从static/version.txt文件获取版本号"""
|
||||
try:
|
||||
with open("static/version.txt", "r", encoding="utf-8") as f:
|
||||
version = f.read().strip()
|
||||
return version if version else "unknown"
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
def _prepare_statistics_data(self) -> Dict[str, Any]:
|
||||
"""准备统计数据(只包含用户统计)"""
|
||||
return {
|
||||
"anonymous_id": self.anonymous_id,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"project": "xianyu-auto-reply",
|
||||
"info": self._get_basic_info()
|
||||
}
|
||||
|
||||
async def _send_statistics(self, data: Dict[str, Any]) -> bool:
|
||||
"""发送统计数据到远程API"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
for attempt in range(self.retry_count):
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'XianyuAutoReply/2.2.0'
|
||||
}
|
||||
|
||||
async with session.post(
|
||||
self.api_endpoint,
|
||||
json=data,
|
||||
headers=headers
|
||||
) as response:
|
||||
if response.status in [200, 201]:
|
||||
logger.debug("统计数据上报成功")
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"统计数据上报失败,状态码: {response.status}")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug(f"统计数据上报超时,第{attempt + 1}次尝试")
|
||||
except Exception as e:
|
||||
logger.debug(f"统计数据上报异常: {e}")
|
||||
|
||||
if attempt < self.retry_count - 1:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
return False
|
||||
|
||||
async def report_usage(self) -> bool:
|
||||
"""报告用户使用统计"""
|
||||
try:
|
||||
data = self._prepare_statistics_data()
|
||||
return await self._send_statistics(data)
|
||||
except Exception as e:
|
||||
logger.debug(f"报告使用统计失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# 全局统计实例
|
||||
usage_stats = UsageStatistics()
|
||||
|
||||
|
||||
async def report_user_count():
|
||||
"""报告用户数量统计"""
|
||||
try:
|
||||
logger.info("正在上报用户统计...")
|
||||
success = await usage_stats.report_usage()
|
||||
if success:
|
||||
logger.info("✅ 用户统计上报成功")
|
||||
else:
|
||||
logger.debug("用户统计上报失败")
|
||||
except Exception as e:
|
||||
logger.debug(f"用户统计异常: {e}")
|
||||
|
||||
|
||||
def get_anonymous_id() -> str:
|
||||
"""获取匿名用户ID"""
|
||||
return usage_stats.anonymous_id
|
||||
|
||||
|
||||
# 测试函数
|
||||
async def test_statistics():
|
||||
"""测试统计功能"""
|
||||
print(f"匿名ID: {get_anonymous_id()}")
|
||||
await report_user_count()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_statistics())
|
||||
Loading…
Reference in New Issue
Block a user