diff --git a/static/js/app.js b/static/js/app.js
index cdf9989..91905b2 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -7938,18 +7938,28 @@ async function loadSystemSettings() {
console.log('用户信息:', result, '是否管理员:', isAdmin);
- // 显示/隐藏注册设置和外发配置(仅管理员可见)
+ // 显示/隐藏管理员专用设置(仅管理员可见)
+ const apiSecuritySettings = document.getElementById('api-security-settings');
const registrationSettings = document.getElementById('registration-settings');
const outgoingConfigs = document.getElementById('outgoing-configs');
+ const backupManagement = document.getElementById('backup-management');
+
+ if (apiSecuritySettings) {
+ apiSecuritySettings.style.display = isAdmin ? 'block' : 'none';
+ }
if (registrationSettings) {
registrationSettings.style.display = isAdmin ? 'block' : 'none';
}
if (outgoingConfigs) {
outgoingConfigs.style.display = isAdmin ? 'block' : 'none';
}
+ if (backupManagement) {
+ backupManagement.style.display = isAdmin ? 'block' : 'none';
+ }
- // 如果是管理员,加载注册设置、登录信息设置和外发配置
+ // 如果是管理员,加载所有管理员设置
if (isAdmin) {
+ await loadAPISecuritySettings();
await loadRegistrationSettings();
await loadLoginInfoSettings();
await loadOutgoingConfigs();
@@ -7965,6 +7975,115 @@ async function loadSystemSettings() {
}
}
+// 加载API安全设置
+async function loadAPISecuritySettings() {
+ try {
+ const response = await fetch('/system-settings', {
+ headers: {
+ 'Authorization': `Bearer ${authToken}`
+ }
+ });
+
+ if (response.ok) {
+ const settings = await response.json();
+
+ // 加载QQ回复消息秘钥
+ const qqReplySecretKey = settings.qq_reply_secret_key || '';
+ const qqReplySecretKeyInput = document.getElementById('qqReplySecretKey');
+ if (qqReplySecretKeyInput) {
+ qqReplySecretKeyInput.value = qqReplySecretKey;
+ }
+ }
+ } catch (error) {
+ console.error('加载API安全设置失败:', error);
+ showToast('加载API安全设置失败', 'danger');
+ }
+}
+
+// 切换密码可见性
+function togglePasswordVisibility(inputId) {
+ const input = document.getElementById(inputId);
+ const icon = document.getElementById(inputId + '-icon');
+
+ if (input && icon) {
+ if (input.type === 'password') {
+ input.type = 'text';
+ icon.className = 'bi bi-eye-slash';
+ } else {
+ input.type = 'password';
+ icon.className = 'bi bi-eye';
+ }
+ }
+}
+
+// 生成随机秘钥
+function generateRandomSecretKey() {
+ // 生成32位随机字符串
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let result = 'xianyu_qq_';
+ for (let i = 0; i < 24; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+
+ const qqReplySecretKeyInput = document.getElementById('qqReplySecretKey');
+ if (qqReplySecretKeyInput) {
+ qqReplySecretKeyInput.value = result;
+ showToast('随机秘钥已生成', 'success');
+ }
+}
+
+// 更新QQ回复消息秘钥
+async function updateQQReplySecretKey() {
+ const qqReplySecretKey = document.getElementById('qqReplySecretKey').value.trim();
+
+ if (!qqReplySecretKey) {
+ showToast('请输入QQ回复消息API秘钥', 'warning');
+ return;
+ }
+
+ if (qqReplySecretKey.length < 8) {
+ showToast('秘钥长度至少需要8位字符', 'warning');
+ return;
+ }
+
+ try {
+ const response = await fetch('/system-settings/qq_reply_secret_key', {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`
+ },
+ body: JSON.stringify({
+ value: qqReplySecretKey,
+ description: 'QQ回复消息API秘钥'
+ })
+ });
+
+ if (response.ok) {
+ showToast('QQ回复消息API秘钥更新成功', 'success');
+
+ // 显示状态信息
+ const statusDiv = document.getElementById('qqReplySecretStatus');
+ const statusText = document.getElementById('qqReplySecretStatusText');
+ if (statusDiv && statusText) {
+ statusText.textContent = `秘钥已更新,长度: ${qqReplySecretKey.length} 位`;
+ statusDiv.style.display = 'block';
+
+ // 3秒后隐藏状态
+ setTimeout(() => {
+ statusDiv.style.display = 'none';
+ }, 3000);
+ }
+ } else {
+ const errorData = await response.json();
+ showToast(`更新失败: ${errorData.detail || '未知错误'}`, 'danger');
+ }
+ } catch (error) {
+ console.error('更新QQ回复消息秘钥失败:', error);
+ showToast('更新QQ回复消息秘钥失败', 'danger');
+ }
+}
+
// 加载外发配置
async function loadOutgoingConfigs() {
try {
diff --git a/static/update_log.txt b/static/update_log.txt
new file mode 100644
index 0000000..5ce3085
--- /dev/null
+++ b/static/update_log.txt
@@ -0,0 +1,4 @@
+🎉1.新增qq直接回复闲鱼消息,无需在进入闲鱼;
+ ①回复哪条消息引用即可
+ ②系统设置维护qq回复秘钥
+
\ No newline at end of file
From 3f42d07aa1f34f7783501ffbf2abb2bff3349e1a Mon Sep 17 00:00:00 2001
From: zhinianboke <115088296+zhinianboke@users.noreply.github.com>
Date: Sat, 30 Aug 2025 12:11:16 +0800
Subject: [PATCH 4/5] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=9A=82=E5=81=9C?=
=?UTF-8?q?=E5=9B=9E=E5=A4=8D=E6=97=B6=E9=97=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
XianyuAutoAsync.py | 5 +++++
db_manager.py | 4 ++--
reply_server.py | 11 ++++++++---
static/index.html | 2 +-
static/js/app.js | 20 ++++++++++----------
static/update_log.txt | 4 +++-
6 files changed, 29 insertions(+), 17 deletions(-)
diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py
index 3de38cb..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
diff --git a/db_manager.py b/db_manager.py
index 1924f29..a5ca8fb 100644
--- a/db_manager.py
+++ b/db_manager.py
@@ -1211,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
@@ -1272,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
diff --git a/reply_server.py b/reply_server.py
index 354155c..9f62aa1 100644
--- a/reply_server.py
+++ b/reply_server.py
@@ -1068,6 +1068,11 @@ class MessageNotificationIn(BaseModel):
class SystemSettingIn(BaseModel):
+ value: str
+ description: Optional[str] = None
+
+
+class SystemSettingCreateIn(BaseModel):
key: str
value: str
description: Optional[str] = None
@@ -2167,9 +2172,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)
diff --git a/static/index.html b/static/index.html
index d095d9c..0d32cad 100644
--- a/static/index.html
+++ b/static/index.html
@@ -319,7 +319,7 @@
+ title="检测到手动发出消息后,自动回复暂停的时间长度(分钟)。设置为0表示不暂停。如果在暂停期间再次手动发出消息,会重新开始计时。">
操作 |
diff --git a/static/js/app.js b/static/js/app.js
index 91905b2..a6cc644 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -1269,8 +1269,8 @@ async function loadCookies() {
-
- ${cookie.pause_duration || 10}分钟
+
+ ${cookie.pause_duration === 0 ? '不暂停' : (cookie.pause_duration || 10) + '分钟'}
|
@@ -7797,16 +7797,16 @@ function editPauseDuration(cookieId, currentDuration) {
const input = document.createElement('input');
input.type = 'number';
input.className = 'form-control form-control-sm';
- input.value = currentDuration || 10;
+ input.value = currentDuration !== undefined ? currentDuration : 10;
input.placeholder = '请输入暂停时间...';
input.style.fontSize = '0.875rem';
- input.min = 1;
+ input.min = 0;
input.max = 60;
input.step = 1;
// 保存原始内容和原始值
const originalContent = pauseCell.innerHTML;
- const originalValue = currentDuration || 10;
+ const originalValue = currentDuration !== undefined ? currentDuration : 10;
// 标记是否已经进行了编辑
let hasChanged = false;
@@ -7818,7 +7818,7 @@ function editPauseDuration(cookieId, currentDuration) {
// 监听输入变化
input.addEventListener('input', () => {
- const newValue = parseInt(input.value) || 10;
+ const newValue = input.value === '' ? 10 : parseInt(input.value);
hasChanged = newValue !== originalValue;
});
@@ -7827,12 +7827,12 @@ function editPauseDuration(cookieId, currentDuration) {
console.log('savePauseDuration called, isProcessing:', isProcessing, 'hasChanged:', hasChanged); // 调试信息
if (isProcessing) return; // 防止重复调用
- const newDuration = parseInt(input.value) || 10;
+ const newDuration = input.value === '' ? 10 : parseInt(input.value);
console.log('newDuration:', newDuration, 'originalValue:', originalValue); // 调试信息
// 验证范围
- if (newDuration < 1 || newDuration > 60) {
- showToast('暂停时间必须在1-60分钟之间', 'warning');
+ if (isNaN(newDuration) || newDuration < 0 || newDuration > 60) {
+ showToast('暂停时间必须在0-60分钟之间(0表示不暂停)', 'warning');
input.focus();
return;
}
@@ -7860,7 +7860,7 @@ function editPauseDuration(cookieId, currentDuration) {
// 更新显示
pauseCell.innerHTML = `
- ${newDuration}分钟
+ ${newDuration === 0 ? '不暂停' : newDuration + '分钟'}
`;
showToast('暂停时间更新成功', 'success');
diff --git a/static/update_log.txt b/static/update_log.txt
index 5ce3085..8fa0873 100644
--- a/static/update_log.txt
+++ b/static/update_log.txt
@@ -1,4 +1,6 @@
🎉1.新增qq直接回复闲鱼消息,无需在进入闲鱼;
①回复哪条消息引用即可
②系统设置维护qq回复秘钥
-
\ No newline at end of file
+ ③在qq上给机器人发送 咸鱼绑定 key:url 其中key是你系统设置配置的秘钥,url 是你公网服务器的 ip加端口,例如 zheshimiyao:http://10.12.13.14:8080
+ ④如果没有公网,可联系我购买内网穿透服务,2块一个月,物美价廉
+🎉2.支持自动回复暂停时间支持设置为0;
\ No newline at end of file
From ea44e32e32810f2ee56a8a4826fa01edc5c1d4ab Mon Sep 17 00:00:00 2001
From: zhinianboke <115088296+zhinianboke@users.noreply.github.com>
Date: Sat, 30 Aug 2025 14:40:07 +0800
Subject: [PATCH 5/5] =?UTF-8?q?=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 236 ++++++++++++++++++++++++++++++++++++++++-
README.md | 111 ++++++++++++++++++-
Start.py | 7 ++
db_manager.py | 40 ++++---
quick_test_api.py | 76 -------------
reply_server.py | 76 ++++++++++++-
simple_stats_server.py | 232 ++++++++++++++++++++++++++++++++++++++++
static/index.html | 22 ++--
static/js/app.js | 190 +++++++++++++++++++++++++++++++--
static/update_log.txt | 10 +-
static/version.txt | 2 +-
usage_statistics.py | 177 +++++++++++++++++++++++++++++++
12 files changed, 1054 insertions(+), 125 deletions(-)
delete mode 100644 quick_test_api.py
create mode 100644 simple_stats_server.py
create mode 100644 usage_statistics.py
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 5e60e3a..1cd9544 100644
--- a/README.md
+++ b/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
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/db_manager.py b/db_manager.py
index a5ca8fb..b126772 100644
--- a/db_manager.py
+++ b/db_manager.py
@@ -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._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/quick_test_api.py b/quick_test_api.py
deleted file mode 100644
index 55bff76..0000000
--- a/quick_test_api.py
+++ /dev/null
@@ -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()
diff --git a/reply_server.py b/reply_server.py
index 9f62aa1..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
# 关键字文件路径
@@ -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)
\ 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 0d32cad..fa359c8 100644
--- a/static/index.html
+++ b/static/index.html
@@ -155,7 +155,11 @@
系统概览和统计信息