This commit is contained in:
zhinianboke 2025-08-30 14:40:07 +08:00
parent 3f42d07aa1
commit ea44e32e32
12 changed files with 1054 additions and 125 deletions

236
.gitignore vendored
View File

@ -290,12 +290,67 @@ build/
# 测试和示例文件 # 测试和示例文件
test_*.py test_*.py
*_test.py *_test.py
test_*.html
*_test.html
test_*.js
*_test.js
example_*.py example_*.py
*_example.py *_example.py
demo_*.py demo_*.py
*_demo.py *_demo.py
fix_*.py fix_*.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 # 文档文件除了README.md
*.md *.md
@ -339,7 +394,6 @@ setup.cfg
tox.ini tox.ini
# 容器相关 # 容器相关
.dockerignore
docker-compose.*.yml docker-compose.*.yml
!docker-compose.yml !docker-compose.yml
!docker-compose-cn.yml !docker-compose-cn.yml
@ -385,4 +439,182 @@ systemd/
# 备份和归档 # 备份和归档
archive/ archive/
old/ old/
deprecated/ 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
View File

@ -9,9 +9,30 @@
我用夸克网盘给你分享了「自动发货系统源码」点击链接或复制整段内容打开「夸克APP」即可获取。 /~5e4237vG5B~:/ 链接https://pan.quark.cn/s/88d118bd700e 我用夸克网盘给你分享了「自动发货系统源码」点击链接或复制整段内容打开「夸克APP」即可获取。 /~5e4237vG5B~:/ 链接https://pan.quark.cn/s/88d118bd700e
## 📋 项目概述
一个功能完整的闲鱼自动回复和管理系统采用现代化的技术架构支持多用户、多账号管理具备智能回复、自动发货、自动确认发货、商品管理等企业级功能。系统基于Python异步编程使用FastAPI提供RESTful APISQLite数据库存储支持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工具函数加密、签名、解析 │ ├── xianyu_utils.py # 闲鱼API工具函数加密、签名、解析
│ ├── message_utils.py # 消息格式化和处理工具 │ ├── message_utils.py # 消息格式化和处理工具
│ ├── ws_utils.py # WebSocket客户端封装 │ ├── ws_utils.py # WebSocket客户端封装
│ ├── qr_login.py # 二维码登录功能 │ ├── image_utils.py # 图片处理和管理工具
│ ├── image_uploader.py # 图片上传到闲鱼CDN
│ ├── image_utils.py # 图片处理工具(压缩、格式转换)
│ ├── item_search.py # 商品搜索功能基于Playwright无头模式 │ ├── item_search.py # 商品搜索功能基于Playwright无头模式
│ ├── order_detail_fetcher.py # 订单详情获取工具 │ ├── order_detail_fetcher.py # 订单详情获取工具
│ ├── image_utils.py # 图片处理工具(压缩、格式转换) │ └── qr_login.py # 二维码登录功能
│ └── image_uploader.py # 图片上传到CDN工具
├── 🌐 前端界面 ├── 🌐 前端界面
│ └── static/ │ └── static/
│ ├── index.html # 主管理界面(集成所有功能模块) │ ├── 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 ## Star History

View File

@ -27,6 +27,7 @@ from config import AUTO_REPLY, COOKIES_LIST
import cookie_manager as cm import cookie_manager as cm
from db_manager import db_manager from db_manager import db_manager
from file_log_collector import setup_file_logging from file_log_collector import setup_file_logging
from usage_statistics import report_user_count
def _start_api_server(): def _start_api_server():
@ -143,6 +144,12 @@ async def main():
threading.Thread(target=_start_api_server, daemon=True).start() threading.Thread(target=_start_api_server, daemon=True).start()
print("API 服务线程已启动") print("API 服务线程已启动")
# 上报用户统计
try:
await report_user_count()
except Exception as e:
logger.debug(f"上报用户统计失败: {e}")
# 阻塞保持运行 # 阻塞保持运行
print("主程序启动完成,保持运行...") print("主程序启动完成,保持运行...")
await asyncio.Event().wait() await asyncio.Event().wait()

View File

@ -1339,6 +1339,29 @@ class DBManager:
try: try:
cursor = self.conn.cursor() 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的文本类型关键字保留图片关键词 # 只删除该cookie_id的文本类型关键字保留图片关键词
self._execute_sql(cursor, self._execute_sql(cursor,
"DELETE FROM keywords WHERE cookie_id = ? AND (type IS NULL OR type = 'text')", "DELETE FROM keywords WHERE cookie_id = ? AND (type IS NULL OR type = 'text')",
@ -1349,22 +1372,15 @@ class DBManager:
# 标准化item_id空字符串转为NULL # 标准化item_id空字符串转为NULL
normalized_item_id = item_id if item_id and item_id.strip() else None normalized_item_id = item_id if item_id and item_id.strip() else None
try: self._execute_sql(cursor,
self._execute_sql(cursor, "INSERT INTO keywords (cookie_id, keyword, reply, item_id, type) VALUES (?, ?, ?, ?, 'text')",
"INSERT INTO keywords (cookie_id, keyword, reply, item_id, type) VALUES (?, ?, ?, ?, 'text')", (cookie_id, keyword, reply, normalized_item_id))
(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() self.conn.commit()
logger.info(f"文本关键字保存成功: {cookie_id}, {len(keywords)}条,图片关键词已保留") logger.info(f"文本关键字保存成功: {cookie_id}, {len(keywords)}条,图片关键词已保留")
return True return True
except sqlite3.IntegrityError: except ValueError:
# 唯一约束冲突,重新抛出异常让上层处理 # 重新抛出友好的错误信息
raise raise
except Exception as e: except Exception as e:
logger.error(f"文本关键字保存失败: {e}") logger.error(f"文本关键字保存失败: {e}")

View File

@ -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()

View File

@ -24,6 +24,7 @@ from ai_reply_engine import ai_reply_engine
from utils.qr_login import qr_login_manager from utils.qr_login import qr_login_manager
from utils.xianyu_utils import trans_cookies from utils.xianyu_utils import trans_cookies
from utils.image_utils import image_manager from utils.image_utils import image_manager
from loguru import logger 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="保存关键词失败") raise HTTPException(status_code=500, detail="保存关键词失败")
except Exception as e: except Exception as e:
error_msg = str(e) error_msg = str(e)
if "UNIQUE constraint failed" in error_msg:
# 解析具体的冲突信息 # 检查是否是图片关键词冲突
if "keywords.cookie_id, keywords.keyword" in error_msg: if "已存在(图片关键词)" in error_msg:
raise HTTPException(status_code=400, detail="关键词重复!该关键词已存在(可能是图片关键词或文本关键词),请使用其他关键词") # 直接使用数据库管理器提供的友好错误信息
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: else:
raise HTTPException(status_code=400, detail="关键词重复请使用不同的关键词或商品ID组合") detail_msg = "关键词重复请使用不同的关键词或商品ID组合"
raise HTTPException(status_code=400, detail=detail_msg)
else: else:
log_with_user('error', f"保存关键词时发生未知错误: {error_msg}", current_user) log_with_user('error', f"保存关键词时发生未知错误: {error_msg}", current_user)
raise HTTPException(status_code=500, detail="保存关键词失败") 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)) 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或手动启动 # 移除自动启动由Start.py或手动启动
# if __name__ == "__main__": # if __name__ == "__main__":
# uvicorn.run(app, host="0.0.0.0", port=8080) # uvicorn.run(app, host="0.0.0.0", port=8080)

232
simple_stats_server.py Normal file
View 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)

View File

@ -155,7 +155,11 @@
</h2> </h2>
<p class="text-muted mb-0">系统概览和统计信息</p> <p class="text-muted mb-0">系统概览和统计信息</p>
</div> </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"> <span class="badge bg-secondary" id="systemVersion">
<i class="bi bi-info-circle me-1"></i> <i class="bi bi-info-circle me-1"></i>
版本: <span id="versionNumber">加载中...</span> 版本: <span id="versionNumber">加载中...</span>
@ -960,7 +964,7 @@
<th>商品关键字</th> <th>商品关键字</th>
<th>匹配卡券</th> <th>匹配卡券</th>
<th>卡券类型</th> <th>卡券类型</th>
<th>发货数量</th> <!-- <th>发货数量</th> 隐藏发货数量列 -->
<th>状态</th> <th>状态</th>
<th>已发货次数</th> <th>已发货次数</th>
<th>操作</th> <th>操作</th>
@ -2794,11 +2798,8 @@
<option value="">请选择卡券</option> <option value="">请选择卡券</option>
</select> </select>
</div> </div>
<div class="mb-3"> <!-- 隐藏发货数量字段默认为1 -->
<label class="form-label">发货数量</label> <input type="hidden" id="deliveryCount" value="1">
<input type="number" class="form-control" id="deliveryCount" value="1" min="1" max="10">
<small class="form-text text-muted">每次发货的数量</small>
</div>
<div class="mb-3"> <div class="mb-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="ruleEnabled" checked> <input class="form-check-input" type="checkbox" id="ruleEnabled" checked>
@ -2846,11 +2847,8 @@
<option value="">请选择卡券</option> <option value="">请选择卡券</option>
</select> </select>
</div> </div>
<div class="mb-3"> <!-- 隐藏发货数量字段默认为1 -->
<label class="form-label">发货数量</label> <input type="hidden" id="editDeliveryCount" value="1">
<input type="number" class="form-control" id="editDeliveryCount" value="1" min="1" max="10">
<small class="form-text text-muted">每次发货的数量</small>
</div>
<div class="mb-3"> <div class="mb-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="editRuleEnabled"> <input class="form-check-input" type="checkbox" id="editRuleEnabled">

View File

@ -233,7 +233,7 @@ async function loadDashboard() {
async function loadOrdersCount() { async function loadOrdersCount() {
try { try {
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
const response = await fetch('/admin/data/orders', { const response = await fetch('/api/orders', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@ -702,7 +702,13 @@ async function addKeyword() {
} }
// 检查关键词是否已存在考虑商品ID检查所有类型的关键词 // 检查关键词是否已存在考虑商品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 => const existingKeyword = allKeywords.find(item =>
item.keyword === keyword && item.keyword === keyword &&
(item.item_id || '') === (itemId || '') (item.item_id || '') === (itemId || '')
@ -1906,6 +1912,8 @@ document.addEventListener('DOMContentLoaded', async () => {
// 加载系统版本号 // 加载系统版本号
loadSystemVersion(); loadSystemVersion();
// 启动项目使用人数定时刷新
startProjectUsersRefresh();
// 添加Cookie表单提交 // 添加Cookie表单提交
document.getElementById('addForm').addEventListener('submit', async (e) => { document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
@ -4219,9 +4227,8 @@ function renderDeliveryRulesList(rules) {
</div> </div>
</td> </td>
<td>${cardTypeBadge}</td> <td>${cardTypeBadge}</td>
<td> <!-- 隐藏发货数量列 -->
<span class="badge bg-info">${rule.delivery_count || 1}</span> <!-- <td><span class="badge bg-info">${rule.delivery_count || 1}</span></td> -->
</td>
<td>${statusBadge}</td> <td>${statusBadge}</td>
<td> <td>
<span class="badge bg-warning">${rule.delivery_times || 0}</span> <span class="badge bg-warning">${rule.delivery_times || 0}</span>
@ -4330,7 +4337,7 @@ async function saveDeliveryRule() {
try { try {
const keyword = document.getElementById('productKeyword').value; const keyword = document.getElementById('productKeyword').value;
const cardId = document.getElementById('selectedCard').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 enabled = document.getElementById('ruleEnabled').checked;
const description = document.getElementById('ruleDescription').value; const description = document.getElementById('ruleDescription').value;
@ -4787,7 +4794,7 @@ async function updateDeliveryRule() {
const ruleId = document.getElementById('editRuleId').value; const ruleId = document.getElementById('editRuleId').value;
const keyword = document.getElementById('editProductKeyword').value; const keyword = document.getElementById('editProductKeyword').value;
const cardId = document.getElementById('editSelectedCard').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 enabled = document.getElementById('editRuleEnabled').checked;
const description = document.getElementById('editRuleDescription').value; const description = document.getElementById('editRuleDescription').value;
@ -8428,7 +8435,7 @@ async function loadOrderCookieFilter() {
// 加载所有订单 // 加载所有订单
async function loadAllOrders() { async function loadAllOrders() {
try { try {
const response = await fetch(`${apiBase}/admin/data/orders`, { const response = await fetch(`${apiBase}/api/orders`, {
headers: { headers: {
'Authorization': `Bearer ${authToken}` 'Authorization': `Bearer ${authToken}`
} }
@ -8461,7 +8468,7 @@ async function loadOrdersByCookie() {
} }
try { try {
const response = await fetch(`${apiBase}/admin/data/orders`, { const response = await fetch(`${apiBase}/api/orders`, {
headers: { headers: {
'Authorization': `Bearer ${authToken}` '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');
}
}
/** /**
* 加载系统版本号并检查更新 * 加载系统版本号并检查更新
*/ */

View File

@ -1,6 +1,10 @@
🎉1.新增qq直接回复闲鱼消息无需在进入闲鱼 '🎉1.新增qq直接回复闲鱼消息无需在进入闲鱼(注意只能有公网访问的才行)
①回复哪条消息引用即可 ①回复哪条消息引用即可
②系统设置维护qq回复秘钥 ②系统设置维护qq回复秘钥
③在qq上给机器人发送 咸鱼绑定 key:url 其中key是你系统设置配置的秘钥url 是你公网服务器的 ip加端口例如 zheshimiyao:http://10.12.13.14:8080 ③在qq上给机器人发送 咸鱼绑定 key:url 其中key是你系统设置配置的秘钥url 是你公网服务器的 ip加端口例如 zheshimiyao:http://10.12.13.14:8080
④如果没有公网可联系我购买内网穿透服务2块一个月物美价廉 ④如果没有公网可联系我购买内网穿透服务2块一个月物美价廉',
🎉2.支持自动回复暂停时间支持设置为0 '🎉2.支持自动回复暂停时间支持设置为0',
'🎉3.修复自动回复关键词无法修改的问题;',
'🎉4.修复订单管理普通用户无法查看的问题;',
'🎉5.删除自动发货界面的发货数量,防止误解,想实现多数量发货在商品管理开启;',
'🎉6.增加项目使用人数统计;',

View File

@ -1 +1 @@
v1.0.0 v1.0.2

177
usage_statistics.py Normal file
View 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())