mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-09-02 02:38:48 +08:00
Compare commits
6 Commits
1243b7c17e
...
789870b334
Author | SHA1 | Date | |
---|---|---|---|
![]() |
789870b334 | ||
![]() |
49693760b8 | ||
![]() |
2a0bb2368a | ||
![]() |
8ac5909db1 | ||
![]() |
565625ab15 | ||
![]() |
06a6392cfd |
58
Dockerfile
58
Dockerfile
@ -2,10 +2,12 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# 设置标签信息
|
||||
LABEL maintainer="Xianyu Auto Reply System"
|
||||
LABEL maintainer="zhinianboke"
|
||||
LABEL version="2.0.0"
|
||||
LABEL description="闲鱼自动回复系统 - 企业级多用户版本"
|
||||
LABEL repository="https://github.com/zhinianboke/xianyu-auto-reply"
|
||||
LABEL license="仅供学习使用,禁止商业用途"
|
||||
LABEL author="zhinianboke"
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
@ -20,14 +22,18 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
# 安装系统依赖(包括Playwright浏览器依赖)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# 基础工具
|
||||
nodejs \
|
||||
npm \
|
||||
tzdata \
|
||||
curl \
|
||||
ca-certificates \
|
||||
# 图像处理依赖
|
||||
libjpeg-dev \
|
||||
libpng-dev \
|
||||
libfreetype6-dev \
|
||||
fonts-dejavu-core \
|
||||
fonts-liberation \
|
||||
# Playwright浏览器依赖
|
||||
libnss3 \
|
||||
libnspr4 \
|
||||
@ -50,31 +56,15 @@ RUN apt-get update && \
|
||||
libx11-6 \
|
||||
libxft2 \
|
||||
libxinerama1 \
|
||||
libxrandr2 \
|
||||
libxss1 \
|
||||
libxtst6 \
|
||||
ca-certificates \
|
||||
fonts-liberation \
|
||||
libappindicator3-1 \
|
||||
libasound2 \
|
||||
libatk-bridge2.0-0 \
|
||||
libdrm2 \
|
||||
libgtk-3-0 \
|
||||
libnspr4 \
|
||||
libnss3 \
|
||||
libx11-xcb1 \
|
||||
libxcomposite1 \
|
||||
libxcursor1 \
|
||||
libxdamage1 \
|
||||
libxfixes3 \
|
||||
libxi6 \
|
||||
libxrandr2 \
|
||||
libxrender1 \
|
||||
libxss1 \
|
||||
libxtst6 \
|
||||
xdg-utils \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /tmp/* \
|
||||
&& rm -rf /var/tmp/*
|
||||
|
||||
# 设置时区
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
@ -110,18 +100,22 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# 创建启动脚本
|
||||
RUN echo '#!/bin/bash' > /app/entrypoint.sh && \
|
||||
echo 'set -e' >> /app/entrypoint.sh && \
|
||||
echo '' >> /app/entrypoint.sh && \
|
||||
echo 'echo "🚀 启动闲鱼自动回复系统..."' >> /app/entrypoint.sh && \
|
||||
echo '' >> /app/entrypoint.sh && \
|
||||
echo '# 数据库将在应用启动时自动初始化' >> /app/entrypoint.sh && \
|
||||
echo 'echo "📊 数据库将在应用启动时自动初始化..."' >> /app/entrypoint.sh && \
|
||||
echo '' >> /app/entrypoint.sh && \
|
||||
echo '# 启动主应用' >> /app/entrypoint.sh && \
|
||||
echo 'echo "🎯 启动主应用..."' >> /app/entrypoint.sh && \
|
||||
echo 'exec python Start.py' >> /app/entrypoint.sh && \
|
||||
chmod +x /app/entrypoint.sh
|
||||
COPY <<EOF /app/entrypoint.sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 启动闲鱼自动回复系统..."
|
||||
echo "📊 数据库将在应用启动时自动初始化..."
|
||||
echo "🎯 启动主应用..."
|
||||
|
||||
# 确保数据目录存在
|
||||
mkdir -p /app/data /app/logs /app/backups
|
||||
|
||||
# 启动主应用
|
||||
exec python Start.py
|
||||
EOF
|
||||
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
# 启动命令
|
||||
CMD ["/app/entrypoint.sh"]
|
294
README.md
294
README.md
@ -3,7 +3,9 @@
|
||||
[](https://github.com/zhinianboke/xianyu-auto-reply)
|
||||
[](https://github.com/zhinianboke/xianyu-auto-reply#-快速开始)
|
||||
[](https://www.python.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](#️-版权声明与使用条款)
|
||||
|
||||
> **⚠️ 重要提示:本项目仅供学习研究使用,严禁商业用途!使用前请仔细阅读[版权声明](#️-版权声明与使用条款)。**
|
||||
|
||||
一个功能完整的闲鱼自动回复和管理系统,支持多用户、多账号管理,具备智能回复、自动发货、自动确认发货、商品管理等企业级功能。
|
||||
|
||||
@ -72,6 +74,9 @@
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
<details>
|
||||
<summary>点击展开查看详细项目结构</summary>
|
||||
|
||||
```
|
||||
xianyu-auto-reply/
|
||||
├── 📄 核心文件
|
||||
@ -119,6 +124,8 @@ xianyu-auto-reply/
|
||||
└── realtime.log # 实时日志文件
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方式一:Docker 一键部署(最简单)
|
||||
@ -153,14 +160,132 @@ cd xianyu-auto-reply
|
||||
# http://localhost:8080
|
||||
```
|
||||
|
||||
#### 🔧 Docker部署故障排除
|
||||
### 方式三:本地部署(开发环境)
|
||||
|
||||
如果遇到构建问题,请参考 [Docker修复指南](DOCKER_FIX.md)
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone https://github.com/zhinianboke/xianyu-auto-reply.git
|
||||
cd xianyu-auto-reply
|
||||
|
||||
**常见问题**:
|
||||
- **sqlite3错误**:已修复,sqlite3是Python内置模块,无需安装
|
||||
- **Docker未运行**:确保Docker Desktop正在运行
|
||||
- **端口冲突**:修改docker-compose.yml中的端口映射为其他端口
|
||||
# 2. 创建虚拟环境(推荐)
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/macOS
|
||||
# 或 venv\Scripts\activate # Windows
|
||||
|
||||
# 3. 安装Python依赖
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 4. 安装Playwright浏览器
|
||||
playwright install chromium
|
||||
playwright install-deps chromium # Linux需要
|
||||
|
||||
# 5. 启动系统
|
||||
python Start.py
|
||||
|
||||
# 6. 访问系统
|
||||
# http://localhost:8080
|
||||
```
|
||||
|
||||
### 📋 环境要求
|
||||
|
||||
- **Python**: 3.11+
|
||||
- **Node.js**: 16+ (用于JavaScript执行)
|
||||
- **系统**: Windows/Linux/macOS
|
||||
- **内存**: 建议2GB+
|
||||
- **存储**: 建议10GB+
|
||||
- **Docker**: 20.10+ (Docker部署)
|
||||
- **Docker Compose**: 2.0+ (Docker部署)
|
||||
|
||||
### 🌐 访问系统
|
||||
|
||||
部署完成后,您可以通过以下方式访问系统:
|
||||
|
||||
- **Web管理界面**:http://localhost:8080
|
||||
- **默认管理员账号**:
|
||||
- 用户名:`admin`
|
||||
- 密码:`admin123`
|
||||
- **API文档**:http://localhost:8080/docs
|
||||
- **健康检查**:http://localhost:8080/health
|
||||
|
||||
> ⚠️ **安全提示**:首次登录后请立即修改默认密码!
|
||||
|
||||
### 🔧 Docker部署管理
|
||||
|
||||
使用 `docker-deploy.sh` 脚本可以方便地管理Docker部署:
|
||||
|
||||
```bash
|
||||
# 查看所有可用命令
|
||||
./docker-deploy.sh help
|
||||
|
||||
# 初始化配置
|
||||
./docker-deploy.sh init
|
||||
|
||||
# 构建镜像
|
||||
./docker-deploy.sh build
|
||||
|
||||
# 启动服务
|
||||
./docker-deploy.sh start
|
||||
|
||||
# 启动包含Nginx的完整服务
|
||||
./docker-deploy.sh start with-nginx
|
||||
|
||||
# 查看服务状态
|
||||
./docker-deploy.sh status
|
||||
|
||||
# 查看实时日志
|
||||
./docker-deploy.sh logs
|
||||
|
||||
# 备份数据
|
||||
./docker-deploy.sh backup
|
||||
|
||||
# 更新部署
|
||||
./docker-deploy.sh update
|
||||
|
||||
# 停止服务
|
||||
./docker-deploy.sh stop
|
||||
|
||||
# 重启服务
|
||||
./docker-deploy.sh restart
|
||||
|
||||
# 清理环境
|
||||
./docker-deploy.sh cleanup
|
||||
```
|
||||
|
||||
### 🛠️ 故障排除
|
||||
|
||||
**常见问题及解决方案**:
|
||||
|
||||
1. **Docker未运行**
|
||||
```bash
|
||||
# 启动Docker Desktop或Docker服务
|
||||
sudo systemctl start docker # Linux
|
||||
```
|
||||
|
||||
2. **端口冲突**
|
||||
```bash
|
||||
# 修改.env文件中的WEB_PORT
|
||||
WEB_PORT=8081
|
||||
```
|
||||
|
||||
3. **权限问题**
|
||||
```bash
|
||||
# 确保数据目录有正确权限
|
||||
sudo chown -R $USER:$USER ./data ./logs ./backups
|
||||
```
|
||||
|
||||
4. **内存不足**
|
||||
```bash
|
||||
# 调整.env文件中的资源限制
|
||||
MEMORY_LIMIT=1024
|
||||
CPU_LIMIT=1.0
|
||||
```
|
||||
|
||||
5. **Playwright浏览器安装失败**
|
||||
```bash
|
||||
# 手动安装浏览器
|
||||
playwright install chromium --with-deps
|
||||
```
|
||||
- **权限问题**:Linux系统下使用 `sudo ./docker-deploy.sh`
|
||||
|
||||
### 方式三:本地部署
|
||||
@ -263,6 +388,59 @@ docker rm -f xianyu-auto-reply
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## ✨ 核心功能特性
|
||||
|
||||
### 🚀 自动回复系统
|
||||
- **智能关键词匹配** - 支持精确匹配和模糊匹配,灵活配置回复规则
|
||||
- **AI智能回复** - 集成多种AI模型(通义千问、GPT等),智能理解用户意图
|
||||
- **多账号管理** - 支持同时管理多个闲鱼账号,独立配置和运行
|
||||
- **实时消息处理** - WebSocket长连接,毫秒级响应用户消息
|
||||
- **自定义回复模板** - 支持占位符和动态内容,个性化回复体验
|
||||
|
||||
### 🛒 自动发货系统
|
||||
- **智能订单识别** - 自动识别虚拟商品订单,精准匹配发货规则
|
||||
- **多重安全验证** - 超级加密保护,防止误操作和数据泄露
|
||||
- **批量处理能力** - 支持批量确认发货,提高处理效率
|
||||
- **异常处理机制** - 完善的错误处理和重试机制,确保发货成功
|
||||
- **多渠道通知** - 支持QQ、钉钉、邮件等多种发货通知方式
|
||||
|
||||
### 👥 多用户系统
|
||||
- **用户注册登录** - 支持邮箱验证和图形验证码,安全可靠
|
||||
- **权限管理** - 管理员和普通用户权限分离,精细化权限控制
|
||||
- **数据隔离** - 每个用户的数据完全隔离,保护隐私安全
|
||||
- **会话管理** - JWT Token认证,支持自动续期和安全登出
|
||||
|
||||
### 📊 数据管理
|
||||
- **商品信息管理** - 自动获取和同步商品信息,实时更新状态
|
||||
- **订单数据统计** - 详细的订单数据分析和可视化图表
|
||||
- **关键词管理** - 灵活的关键词配置,支持正则表达式
|
||||
- **数据导入导出** - 支持Excel格式的批量数据操作
|
||||
- **自动备份** - 定期自动备份重要数据,防止数据丢失
|
||||
|
||||
### 🔍 商品搜索
|
||||
- **真实数据获取** - 基于Playwright技术,获取真实闲鱼商品数据
|
||||
- **多页搜索** - 支持分页搜索和批量获取,无限制数据采集
|
||||
- **数据可视化** - 美观的商品展示界面,支持排序和筛选
|
||||
- **搜索历史** - 保存搜索历史和结果,方便数据分析
|
||||
|
||||
### 📱 通知系统
|
||||
- **多渠道支持** - QQ、钉钉、邮件、微信、Telegram等6种通知方式
|
||||
- **智能配置** - 可视化配置界面,支持复杂参数和加密设置
|
||||
- **实时推送** - 重要事件实时通知,及时了解系统状态
|
||||
- **通知模板** - 自定义通知内容和格式,个性化消息推送
|
||||
|
||||
### 🔐 安全特性
|
||||
- **Cookie安全管理** - 加密存储用户凭证,定期自动刷新
|
||||
- **Token自动刷新** - 智能检测和刷新过期Token,保持连接稳定
|
||||
- **操作日志** - 详细记录所有操作日志,支持审计和追踪
|
||||
- **异常监控** - 实时监控系统异常和错误,主动预警
|
||||
|
||||
### 🎨 用户界面
|
||||
- **现代化设计** - 基于Bootstrap 5的响应式界面,美观易用
|
||||
- **多主题支持** - 支持明暗主题切换,个性化界面体验
|
||||
- **移动端适配** - 完美适配手机和平板设备,随时随地管理
|
||||
- **实时更新** - 界面数据实时更新,无需手动刷新
|
||||
|
||||
## 📁 核心文件功能说明
|
||||
|
||||
### 🚀 启动和核心模块
|
||||
@ -314,19 +492,6 @@ docker rm -f xianyu-auto-reply
|
||||
- **默认密码**:`admin123`
|
||||
- **初始化机制**:首次创建数据库时自动创建admin用户
|
||||
|
||||
#### 修改密码方式
|
||||
|
||||
**方式一:Web界面修改(推荐)**
|
||||
1. 使用默认密码登录系统
|
||||
2. 进入系统设置页面
|
||||
3. 在"修改密码"区域输入当前密码和新密码
|
||||
4. 点击"修改密码"按钮完成修改
|
||||
|
||||
**密码管理机制**:
|
||||
- 数据库初始化时创建admin用户,密码为 `admin123`
|
||||
- 重启时如果用户表已存在,不重新初始化
|
||||
- 所有用户(包括admin)统一使用用户表验证
|
||||
- 密码修改后立即生效,无需重启
|
||||
|
||||
### 全局配置文件
|
||||
`global_config.yml` 包含详细的系统配置,支持:
|
||||
@ -363,23 +528,6 @@ docker rm -f xianyu-auto-reply
|
||||
- **日志文件**:`logs/` 目录下的按日期分割的日志文件
|
||||
- **日志级别**:支持DEBUG、INFO、WARNING、ERROR级别
|
||||
|
||||
### 数据备份
|
||||
```bash
|
||||
# 手动备份
|
||||
./docker-deploy.sh backup
|
||||
|
||||
# 查看备份
|
||||
ls backups/
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
```bash
|
||||
# 检查服务状态
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# 查看系统状态
|
||||
./docker-deploy.sh status
|
||||
```
|
||||
|
||||
## 🔒 安全特性
|
||||
|
||||
@ -434,19 +582,9 @@ curl http://localhost:8080/health
|
||||
- 推送分支:`git push origin feature/your-feature`
|
||||
- 提交 Pull Request
|
||||
|
||||
### 📖 文档贡献
|
||||
- 改进现有文档
|
||||
- 添加使用示例
|
||||
- 翻译文档到其他语言
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
### 🔧 故障排除
|
||||
|
||||
**1. 问题排查**
|
||||
1. 查看日志:`docker-compose logs -f`
|
||||
2. 检查状态:`./docker-deploy.sh status`
|
||||
3. 健康检查:`curl http://localhost:8080/health`
|
||||
|
||||
### 💬 交流群组
|
||||
|
||||
@ -469,9 +607,71 @@ curl http://localhost:8080/health
|
||||
|
||||
- **[XianYuApis](https://github.com/cv-cat/XianYuApis)** - 提供了闲鱼API接口的技术参考
|
||||
- **[XianyuAutoAgent](https://github.com/shaxiu/XianyuAutoAgent)** - 提供了自动化处理的实现思路
|
||||
- **[myfish](https://github.com/Kaguya233qwq/myfish)** - 提供了扫码登录的实现思路
|
||||
|
||||
|
||||
感谢这些优秀的开源项目为本项目的开发提供了宝贵的参考和启发!
|
||||
|
||||
## ⚖️ 版权声明与使用条款
|
||||
|
||||
### 📋 重要声明
|
||||
|
||||
**本项目仅供学习和研究使用,严禁商业用途!**
|
||||
|
||||
### 🚫 使用限制
|
||||
|
||||
- ❌ **禁止商业使用** - 本项目及其衍生作品不得用于任何商业目的
|
||||
- ❌ **禁止销售** - 不得以任何形式销售本项目或基于本项目的服务
|
||||
- ❌ **禁止盈利** - 不得通过本项目进行任何形式的盈利活动
|
||||
- ❌ **禁止违法使用** - 不得将本项目用于任何违法违规活动
|
||||
|
||||
### ✅ 允许使用
|
||||
|
||||
- ✅ **学习研究** - 可用于个人学习和技术研究
|
||||
- ✅ **非商业分享** - 可在非商业环境下分享和讨论
|
||||
- ✅ **开源贡献** - 欢迎为项目贡献代码和改进
|
||||
|
||||
### 📝 使用要求
|
||||
|
||||
如果您使用、修改或分发本项目,必须:
|
||||
|
||||
1. **保留原作者信息** - 必须在显著位置标注原作者和项目来源
|
||||
2. **保留版权声明** - 不得删除或修改本版权声明
|
||||
3. **注明修改内容** - 如有修改,需明确标注修改部分
|
||||
4. **遵守开源协议** - 严格遵守项目的开源许可协议
|
||||
|
||||
### 👤 原作者信息
|
||||
|
||||
- **项目作者**:zhinianboke
|
||||
- **项目地址**:https://github.com/zhinianboke/xianyu-auto-reply
|
||||
- **联系方式**:通过GitHub Issues或项目交流群
|
||||
|
||||
### ⚠️ 免责声明
|
||||
|
||||
1. **使用风险自负** - 使用本项目产生的任何风险由使用者自行承担
|
||||
2. **无质量保证** - 本项目按"现状"提供,不提供任何明示或暗示的保证
|
||||
3. **责任限制** - 作者不对使用本项目造成的任何损失承担责任
|
||||
4. **合规使用** - 使用者需确保使用行为符合当地法律法规
|
||||
|
||||
### 📞 侵权处理
|
||||
|
||||
如发现本项目存在侵权内容,请通过以下方式联系:
|
||||
|
||||
- **GitHub Issues**:https://github.com/zhinianboke/xianyu-auto-reply/issues
|
||||
- **邮箱联系**:在项目交流群中获取联系方式
|
||||
|
||||
我们将在收到通知后**立即处理**并删除相关内容。
|
||||
|
||||
### 🤝 合作与授权
|
||||
|
||||
如需商业使用或特殊授权,请通过项目交流群联系作者进行协商。
|
||||
|
||||
---
|
||||
|
||||
**⚖️ 使用本项目即表示您已阅读、理解并同意遵守以上所有条款。**
|
||||
|
||||
---
|
||||
|
||||
🎉 **开始使用闲鱼自动回复系统,让您的闲鱼店铺管理更加智能高效!**
|
||||
|
||||
**请记住:仅限学习使用,禁止商业用途!**
|
||||
|
@ -918,10 +918,24 @@ class XianyuLive:
|
||||
channel_config = notification.get('channel_config')
|
||||
|
||||
try:
|
||||
if channel_type == 'qq':
|
||||
await self._send_qq_notification(channel_config, notification_msg)
|
||||
else:
|
||||
logger.warning(f"不支持的通知渠道类型: {channel_type}")
|
||||
# 解析配置数据
|
||||
config_data = self._parse_notification_config(channel_config)
|
||||
|
||||
match channel_type:
|
||||
case 'qq':
|
||||
await self._send_qq_notification(config_data, notification_msg)
|
||||
case 'ding_talk' | 'dingtalk':
|
||||
await self._send_dingtalk_notification(config_data, notification_msg)
|
||||
case 'email':
|
||||
await self._send_email_notification(config_data, notification_msg)
|
||||
case 'webhook':
|
||||
await self._send_webhook_notification(config_data, notification_msg)
|
||||
case 'wechat':
|
||||
await self._send_wechat_notification(config_data, notification_msg)
|
||||
case 'telegram':
|
||||
await self._send_telegram_notification(config_data, notification_msg)
|
||||
case _:
|
||||
logger.warning(f"不支持的通知渠道类型: {channel_type}")
|
||||
|
||||
except Exception as notify_error:
|
||||
logger.error(f"发送通知失败 ({notification.get('channel_name', 'Unknown')}): {self._safe_str(notify_error)}")
|
||||
@ -929,13 +943,25 @@ class XianyuLive:
|
||||
except Exception as e:
|
||||
logger.error(f"处理消息通知失败: {self._safe_str(e)}")
|
||||
|
||||
async def _send_qq_notification(self, config: str, message: str):
|
||||
def _parse_notification_config(self, config: str) -> dict:
|
||||
"""解析通知配置数据"""
|
||||
try:
|
||||
import json
|
||||
# 尝试解析JSON格式的配置
|
||||
return json.loads(config)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# 兼容旧格式(直接字符串)
|
||||
return {"config": config}
|
||||
|
||||
async def _send_qq_notification(self, config_data: dict, message: str):
|
||||
"""发送QQ通知"""
|
||||
try:
|
||||
import aiohttp
|
||||
|
||||
# 解析配置(QQ号码)
|
||||
qq_number = config.strip()
|
||||
qq_number = config_data.get('qq_number') or config_data.get('config', '')
|
||||
qq_number = qq_number.strip() if qq_number else ''
|
||||
|
||||
if not qq_number:
|
||||
logger.warning("QQ通知配置为空")
|
||||
return
|
||||
@ -958,6 +984,205 @@ class XianyuLive:
|
||||
except Exception as e:
|
||||
logger.error(f"发送QQ通知异常: {self._safe_str(e)}")
|
||||
|
||||
async def _send_dingtalk_notification(self, config_data: dict, message: str):
|
||||
"""发送钉钉通知"""
|
||||
try:
|
||||
import aiohttp
|
||||
import json
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import time
|
||||
|
||||
# 解析配置
|
||||
webhook_url = config_data.get('webhook_url') or config_data.get('config', '')
|
||||
secret = config_data.get('secret', '')
|
||||
|
||||
webhook_url = webhook_url.strip() if webhook_url else ''
|
||||
if not webhook_url:
|
||||
logger.warning("钉钉通知配置为空")
|
||||
return
|
||||
|
||||
# 如果有加签密钥,生成签名
|
||||
if secret:
|
||||
timestamp = str(round(time.time() * 1000))
|
||||
secret_enc = secret.encode('utf-8')
|
||||
string_to_sign = f'{timestamp}\n{secret}'
|
||||
string_to_sign_enc = string_to_sign.encode('utf-8')
|
||||
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
|
||||
sign = base64.b64encode(hmac_code).decode('utf-8')
|
||||
webhook_url += f'×tamp={timestamp}&sign={sign}'
|
||||
|
||||
data = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": "闲鱼自动回复通知",
|
||||
"text": message
|
||||
}
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(webhook_url, json=data, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
logger.info(f"钉钉通知发送成功")
|
||||
else:
|
||||
logger.warning(f"钉钉通知发送失败: {response.status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送钉钉通知异常: {self._safe_str(e)}")
|
||||
|
||||
async def _send_email_notification(self, config_data: dict, message: str):
|
||||
"""发送邮件通知"""
|
||||
try:
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
# 解析配置
|
||||
smtp_server = config_data.get('smtp_server', '')
|
||||
smtp_port = int(config_data.get('smtp_port', 587))
|
||||
email_user = config_data.get('email_user', '')
|
||||
email_password = config_data.get('email_password', '')
|
||||
recipient_email = config_data.get('recipient_email', '')
|
||||
|
||||
if not all([smtp_server, email_user, email_password, recipient_email]):
|
||||
logger.warning("邮件通知配置不完整")
|
||||
return
|
||||
|
||||
# 创建邮件
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = email_user
|
||||
msg['To'] = recipient_email
|
||||
msg['Subject'] = "闲鱼自动回复通知"
|
||||
|
||||
# 添加邮件正文
|
||||
msg.attach(MIMEText(message, 'plain', 'utf-8'))
|
||||
|
||||
# 发送邮件
|
||||
server = smtplib.SMTP(smtp_server, smtp_port)
|
||||
server.starttls()
|
||||
server.login(email_user, email_password)
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
|
||||
logger.info(f"邮件通知发送成功: {recipient_email}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送邮件通知异常: {self._safe_str(e)}")
|
||||
|
||||
async def _send_webhook_notification(self, config_data: dict, message: str):
|
||||
"""发送Webhook通知"""
|
||||
try:
|
||||
import aiohttp
|
||||
import json
|
||||
|
||||
# 解析配置
|
||||
webhook_url = config_data.get('webhook_url', '')
|
||||
http_method = config_data.get('http_method', 'POST').upper()
|
||||
headers_str = config_data.get('headers', '{}')
|
||||
|
||||
if not webhook_url:
|
||||
logger.warning("Webhook通知配置为空")
|
||||
return
|
||||
|
||||
# 解析自定义请求头
|
||||
try:
|
||||
custom_headers = json.loads(headers_str) if headers_str else {}
|
||||
except json.JSONDecodeError:
|
||||
custom_headers = {}
|
||||
|
||||
# 设置默认请求头
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
headers.update(custom_headers)
|
||||
|
||||
# 构建请求数据
|
||||
data = {
|
||||
'message': message,
|
||||
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'source': 'xianyu-auto-reply'
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if http_method == 'POST':
|
||||
async with session.post(webhook_url, json=data, headers=headers, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
logger.info(f"Webhook通知发送成功")
|
||||
else:
|
||||
logger.warning(f"Webhook通知发送失败: {response.status}")
|
||||
elif http_method == 'PUT':
|
||||
async with session.put(webhook_url, json=data, headers=headers, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
logger.info(f"Webhook通知发送成功")
|
||||
else:
|
||||
logger.warning(f"Webhook通知发送失败: {response.status}")
|
||||
else:
|
||||
logger.warning(f"不支持的HTTP方法: {http_method}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送Webhook通知异常: {self._safe_str(e)}")
|
||||
|
||||
async def _send_wechat_notification(self, config_data: dict, message: str):
|
||||
"""发送微信通知"""
|
||||
try:
|
||||
import aiohttp
|
||||
import json
|
||||
|
||||
# 解析配置
|
||||
webhook_url = config_data.get('webhook_url', '')
|
||||
|
||||
if not webhook_url:
|
||||
logger.warning("微信通知配置为空")
|
||||
return
|
||||
|
||||
data = {
|
||||
"msgtype": "text",
|
||||
"text": {
|
||||
"content": message
|
||||
}
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(webhook_url, json=data, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
logger.info(f"微信通知发送成功")
|
||||
else:
|
||||
logger.warning(f"微信通知发送失败: {response.status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送微信通知异常: {self._safe_str(e)}")
|
||||
|
||||
async def _send_telegram_notification(self, config_data: dict, message: str):
|
||||
"""发送Telegram通知"""
|
||||
try:
|
||||
import aiohttp
|
||||
|
||||
# 解析配置
|
||||
bot_token = config_data.get('bot_token', '')
|
||||
chat_id = config_data.get('chat_id', '')
|
||||
|
||||
if not all([bot_token, chat_id]):
|
||||
logger.warning("Telegram通知配置不完整")
|
||||
return
|
||||
|
||||
# 构建API URL
|
||||
api_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
||||
|
||||
data = {
|
||||
'chat_id': chat_id,
|
||||
'text': message,
|
||||
'parse_mode': 'HTML'
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(api_url, json=data, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
logger.info(f"Telegram通知发送成功")
|
||||
else:
|
||||
logger.warning(f"Telegram通知发送失败: {response.status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送Telegram通知异常: {self._safe_str(e)}")
|
||||
|
||||
async def send_token_refresh_notification(self, error_message: str, notification_type: str = "token_refresh"):
|
||||
"""发送Token刷新异常通知(带防重复机制)"""
|
||||
try:
|
||||
@ -1004,11 +1229,30 @@ class XianyuLive:
|
||||
channel_config = notification.get('channel_config')
|
||||
|
||||
try:
|
||||
if channel_type == 'qq':
|
||||
await self._send_qq_notification(channel_config, notification_msg)
|
||||
notification_sent = True
|
||||
else:
|
||||
logger.warning(f"不支持的通知渠道类型: {channel_type}")
|
||||
# 解析配置数据
|
||||
config_data = self._parse_notification_config(channel_config)
|
||||
|
||||
match channel_type:
|
||||
case 'qq':
|
||||
await self._send_qq_notification(config_data, notification_msg)
|
||||
notification_sent = True
|
||||
case 'ding_talk' | 'dingtalk':
|
||||
await self._send_dingtalk_notification(config_data, notification_msg)
|
||||
notification_sent = True
|
||||
case 'email':
|
||||
await self._send_email_notification(config_data, notification_msg)
|
||||
notification_sent = True
|
||||
case 'webhook':
|
||||
await self._send_webhook_notification(config_data, notification_msg)
|
||||
notification_sent = True
|
||||
case 'wechat':
|
||||
await self._send_wechat_notification(config_data, notification_msg)
|
||||
notification_sent = True
|
||||
case 'telegram':
|
||||
await self._send_telegram_notification(config_data, notification_msg)
|
||||
notification_sent = True
|
||||
case _:
|
||||
logger.warning(f"不支持的通知渠道类型: {channel_type}")
|
||||
|
||||
except Exception as notify_error:
|
||||
logger.error(f"发送Token刷新通知失败 ({notification.get('channel_name', 'Unknown')}): {self._safe_str(notify_error)}")
|
||||
@ -1074,9 +1318,34 @@ class XianyuLive:
|
||||
channel_type = notification.get('channel_type', 'qq')
|
||||
channel_config = notification.get('channel_config', '')
|
||||
|
||||
if channel_type == 'qq':
|
||||
await self._send_qq_notification(channel_config, notification_message)
|
||||
logger.info(f"已发送自动发货通知到QQ: {channel_config}")
|
||||
try:
|
||||
# 解析配置数据
|
||||
config_data = self._parse_notification_config(channel_config)
|
||||
|
||||
match channel_type:
|
||||
case 'qq':
|
||||
await self._send_qq_notification(config_data, notification_message)
|
||||
logger.info(f"已发送自动发货通知到QQ")
|
||||
case 'ding_talk' | 'dingtalk':
|
||||
await self._send_dingtalk_notification(config_data, notification_message)
|
||||
logger.info(f"已发送自动发货通知到钉钉")
|
||||
case 'email':
|
||||
await self._send_email_notification(config_data, notification_message)
|
||||
logger.info(f"已发送自动发货通知到邮箱")
|
||||
case 'webhook':
|
||||
await self._send_webhook_notification(config_data, notification_message)
|
||||
logger.info(f"已发送自动发货通知到Webhook")
|
||||
case 'wechat':
|
||||
await self._send_wechat_notification(config_data, notification_message)
|
||||
logger.info(f"已发送自动发货通知到微信")
|
||||
case 'telegram':
|
||||
await self._send_telegram_notification(config_data, notification_message)
|
||||
logger.info(f"已发送自动发货通知到Telegram")
|
||||
case _:
|
||||
logger.warning(f"不支持的通知渠道类型: {channel_type}")
|
||||
|
||||
except Exception as notify_error:
|
||||
logger.error(f"发送自动发货通知失败: {self._safe_str(notify_error)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送自动发货通知异常: {self._safe_str(e)}")
|
||||
|
182
db_manager.py
182
db_manager.py
@ -295,6 +295,16 @@ class DBManager:
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建系统设置表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建消息通知配置表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS message_notifications (
|
||||
@ -310,16 +320,6 @@ class DBManager:
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建系统设置表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建用户设置表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
@ -341,6 +341,52 @@ class DBManager:
|
||||
('theme_color', 'blue', '主题颜色')
|
||||
''')
|
||||
|
||||
# 检查并升级数据库
|
||||
self.check_and_upgrade_db(cursor)
|
||||
|
||||
self.conn.commit()
|
||||
logger.info("数据库初始化完成")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库初始化失败: {e}")
|
||||
self.conn.rollback()
|
||||
raise
|
||||
|
||||
def check_and_upgrade_db(self, cursor):
|
||||
"""检查数据库版本并执行必要的升级"""
|
||||
try:
|
||||
# 获取当前数据库版本
|
||||
current_version = self.get_system_setting("db_version") or "1.0"
|
||||
logger.info(f"当前数据库版本: {current_version}")
|
||||
|
||||
if current_version == "1.0":
|
||||
logger.info("开始升级数据库到版本1.0...")
|
||||
self.update_admin_user_id(cursor)
|
||||
self.set_system_setting("db_version", "1.0", "数据库版本号")
|
||||
logger.info("数据库升级到版本1.0完成")
|
||||
|
||||
# 如果版本低于需要升级的版本,执行升级
|
||||
if current_version < "1.1":
|
||||
logger.info("开始升级数据库到版本1.1...")
|
||||
self.upgrade_notification_channels_table(cursor)
|
||||
self.set_system_setting("db_version", "1.1", "数据库版本号")
|
||||
logger.info("数据库升级到版本1.1完成")
|
||||
|
||||
# 升级到版本1.2 - 支持更多通知渠道类型
|
||||
if current_version < "1.2":
|
||||
logger.info("开始升级数据库到版本1.2...")
|
||||
self.upgrade_notification_channels_types(cursor)
|
||||
self.set_system_setting("db_version", "1.2", "数据库版本号")
|
||||
logger.info("数据库升级到版本1.2完成")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"数据库版本检查或升级失败: {e}")
|
||||
raise
|
||||
|
||||
def update_admin_user_id(self, cursor):
|
||||
"""更新admin用户ID"""
|
||||
try:
|
||||
logger.info("开始更新admin用户ID...")
|
||||
# 创建默认admin用户(只在首次初始化时创建)
|
||||
cursor.execute('SELECT COUNT(*) FROM users WHERE username = ?', ('admin',))
|
||||
admin_exists = cursor.fetchone()[0] > 0
|
||||
@ -438,11 +484,119 @@ class DBManager:
|
||||
self._migrate_keywords_table_constraints(cursor)
|
||||
|
||||
self.conn.commit()
|
||||
logger.info(f"数据库初始化成功: {self.db_path}")
|
||||
logger.info(f"admin用户ID更新完成")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库初始化失败: {e}")
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
logger.error(f"更新admin用户ID失败: {e}")
|
||||
raise
|
||||
|
||||
def upgrade_notification_channels_table(self, cursor):
|
||||
"""升级notification_channels表的type字段约束"""
|
||||
try:
|
||||
logger.info("开始升级notification_channels表...")
|
||||
|
||||
# 检查表是否存在
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='notification_channels'")
|
||||
if not cursor.fetchone():
|
||||
logger.info("notification_channels表不存在,无需升级")
|
||||
return True
|
||||
|
||||
# 检查表中是否有数据
|
||||
cursor.execute("SELECT COUNT(*) FROM notification_channels")
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
# 创建临时表
|
||||
cursor.execute('''
|
||||
CREATE TABLE notification_channels_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('qq','ding_talk')),
|
||||
config TEXT NOT NULL,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 复制数据
|
||||
if count > 0:
|
||||
logger.info(f"复制 {count} 条通知渠道数据到新表")
|
||||
cursor.execute("INSERT INTO notification_channels_new SELECT * FROM notification_channels")
|
||||
|
||||
# 删除旧表
|
||||
cursor.execute("DROP TABLE notification_channels")
|
||||
|
||||
# 重命名新表
|
||||
cursor.execute("ALTER TABLE notification_channels_new RENAME TO notification_channels")
|
||||
|
||||
logger.info("notification_channels表升级完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"升级notification_channels表失败: {e}")
|
||||
raise
|
||||
|
||||
def upgrade_notification_channels_types(self, cursor):
|
||||
"""升级notification_channels表支持更多渠道类型"""
|
||||
try:
|
||||
logger.info("开始升级notification_channels表支持更多渠道类型...")
|
||||
|
||||
# 检查表是否存在
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='notification_channels'")
|
||||
if not cursor.fetchone():
|
||||
logger.info("notification_channels表不存在,无需升级")
|
||||
return True
|
||||
|
||||
# 检查表中是否有数据
|
||||
cursor.execute("SELECT COUNT(*) FROM notification_channels")
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
# 获取现有数据
|
||||
existing_data = []
|
||||
if count > 0:
|
||||
cursor.execute("SELECT * FROM notification_channels")
|
||||
existing_data = cursor.fetchall()
|
||||
logger.info(f"备份 {count} 条通知渠道数据")
|
||||
|
||||
# 创建新表,支持更多渠道类型
|
||||
cursor.execute('''
|
||||
CREATE TABLE notification_channels_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('qq','ding_talk','dingtalk','email','webhook','wechat','telegram')),
|
||||
config TEXT NOT NULL,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 复制数据,同时处理类型映射
|
||||
if existing_data:
|
||||
logger.info(f"迁移 {len(existing_data)} 条通知渠道数据到新表")
|
||||
for row in existing_data:
|
||||
# 处理类型映射:ding_talk -> dingtalk
|
||||
channel_type = row[3] # type字段
|
||||
if channel_type == 'ding_talk':
|
||||
channel_type = 'dingtalk'
|
||||
|
||||
# 插入到新表
|
||||
cursor.execute('''
|
||||
INSERT INTO notification_channels_new
|
||||
(id, name, user_id, type, config, enabled, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (row[0], row[1], row[2], channel_type, row[4], row[5], row[6], row[7]))
|
||||
|
||||
# 删除旧表
|
||||
cursor.execute("DROP TABLE notification_channels")
|
||||
|
||||
# 重命名新表
|
||||
cursor.execute("ALTER TABLE notification_channels_new RENAME TO notification_channels")
|
||||
|
||||
logger.info("notification_channels表类型升级完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"升级notification_channels表类型失败: {e}")
|
||||
raise
|
||||
|
||||
def _migrate_keywords_table_constraints(self, cursor):
|
||||
|
148
requirements.txt
148
requirements.txt
@ -2,68 +2,174 @@
|
||||
# 闲鱼自动回复系统 - Python依赖包
|
||||
# ================================
|
||||
|
||||
# 核心Web框架
|
||||
# ==================== 核心Web框架 ====================
|
||||
fastapi>=0.111.0
|
||||
uvicorn[standard]>=0.29.0
|
||||
pydantic>=2.7.0
|
||||
|
||||
# 日志记录
|
||||
# ==================== 日志记录 ====================
|
||||
loguru>=0.7.0
|
||||
|
||||
# 网络通信
|
||||
# ==================== 网络通信 ====================
|
||||
websockets>=10.0,<13.0
|
||||
aiohttp>=3.9.0
|
||||
requests>=2.31.0
|
||||
httpx>=0.25.0
|
||||
|
||||
# 配置文件处理
|
||||
# ==================== 配置文件处理 ====================
|
||||
PyYAML>=6.0.0
|
||||
python-dotenv>=1.0.1
|
||||
|
||||
# JavaScript执行引擎
|
||||
# ==================== JavaScript执行引擎 ====================
|
||||
PyExecJS>=1.5.1
|
||||
|
||||
# 协议缓冲区解析
|
||||
# ==================== 协议缓冲区解析 ====================
|
||||
blackboxprotobuf>=1.0.1
|
||||
|
||||
# 系统监控
|
||||
# ==================== 系统监控 ====================
|
||||
psutil>=5.9.0
|
||||
|
||||
# 文件上传支持
|
||||
# ==================== 文件上传支持 ====================
|
||||
python-multipart>=0.0.6
|
||||
|
||||
# AI回复引擎
|
||||
# ==================== AI回复引擎 ====================
|
||||
openai>=1.65.5
|
||||
|
||||
# 图像处理(验证码生成、二维码生成)
|
||||
# ==================== 图像处理 ====================
|
||||
# 验证码生成、二维码生成
|
||||
Pillow>=10.0.0
|
||||
qrcode[pil]>=7.4.2
|
||||
|
||||
# 浏览器自动化(商品搜索、订单详情获取)
|
||||
# ==================== 浏览器自动化 ====================
|
||||
# 商品搜索、订单详情获取
|
||||
playwright>=1.40.0
|
||||
|
||||
# 加密和安全
|
||||
# ==================== 加密和安全 ====================
|
||||
PyJWT>=2.8.0
|
||||
passlib[bcrypt]>=1.7.4
|
||||
cryptography>=41.0.0
|
||||
|
||||
# 时间处理
|
||||
# ==================== 时间处理 ====================
|
||||
python-dateutil>=2.8.2
|
||||
|
||||
# 正则表达式增强
|
||||
# ==================== 正则表达式增强 ====================
|
||||
regex>=2023.10.3
|
||||
|
||||
# Excel文件处理(数据导入导出)
|
||||
# ==================== Excel文件处理 ====================
|
||||
# 数据导入导出功能
|
||||
pandas>=2.0.0
|
||||
openpyxl>=3.1.0
|
||||
|
||||
# 邮件发送(用户注册验证)
|
||||
# ==================== 邮件发送 ====================
|
||||
# 用户注册验证
|
||||
email-validator>=2.0.0
|
||||
|
||||
# 其他工具库
|
||||
# ==================== 其他工具库 ====================
|
||||
typing-extensions>=4.7.0
|
||||
|
||||
# 注意:
|
||||
# - sqlite3 是Python内置模块,无需安装
|
||||
# - smtplib 是Python内置模块,无需安装
|
||||
# - email 是Python内置模块,无需安装
|
||||
# ==================== 说明 ====================
|
||||
# 以下模块是Python内置模块,无需安装:
|
||||
# - sqlite3 (数据库)
|
||||
# - smtplib (邮件发送)
|
||||
# - email (邮件处理)
|
||||
# - json (JSON处理)
|
||||
# - base64 (编码解码)
|
||||
# - hashlib (哈希算法)
|
||||
# - hmac (消息认证码)
|
||||
# - time (时间处理)
|
||||
# - datetime (日期时间)
|
||||
# - os (操作系统接口)
|
||||
# - sys (系统相关)
|
||||
# - re (正则表达式)
|
||||
# - urllib (URL处理)
|
||||
# - asyncio (异步编程)
|
||||
# - threading (多线程)
|
||||
# - multiprocessing (多进程)
|
||||
# - pathlib (路径处理)
|
||||
# - uuid (UUID生成)
|
||||
# - random (随机数)
|
||||
# - secrets (安全随机数)
|
||||
# - traceback (异常追踪)
|
||||
# - logging (日志记录)
|
||||
# - collections (集合类型)
|
||||
# - itertools (迭代工具)
|
||||
# - functools (函数工具)
|
||||
# - operator (操作符函数)
|
||||
# - copy (对象复制)
|
||||
# - pickle (对象序列化)
|
||||
# - gzip (压缩)
|
||||
# - zipfile (ZIP文件)
|
||||
# - tarfile (TAR文件)
|
||||
# - shutil (文件操作)
|
||||
# - tempfile (临时文件)
|
||||
# - io (输入输出)
|
||||
# - csv (CSV文件)
|
||||
# - xml (XML处理)
|
||||
# - html (HTML处理)
|
||||
# - http (HTTP客户端/服务器)
|
||||
# - socket (网络编程)
|
||||
# - ssl (SSL/TLS)
|
||||
# - ftplib (FTP客户端)
|
||||
# - poplib (POP3客户端)
|
||||
# - imaplib (IMAP客户端)
|
||||
# - telnetlib (Telnet客户端)
|
||||
# - subprocess (子进程)
|
||||
# - signal (信号处理)
|
||||
# - atexit (退出处理)
|
||||
# - weakref (弱引用)
|
||||
# - gc (垃圾回收)
|
||||
# - inspect (对象检查)
|
||||
# - ast (抽象语法树)
|
||||
# - dis (字节码反汇编)
|
||||
# - keyword (关键字)
|
||||
# - token (令牌)
|
||||
# - tokenize (词法分析)
|
||||
# - parser (语法分析)
|
||||
# - symbol (符号)
|
||||
# - code (代码对象)
|
||||
# - codeop (代码编译)
|
||||
# - py_compile (Python编译)
|
||||
# - compileall (批量编译)
|
||||
# - importlib (导入机制)
|
||||
# - pkgutil (包工具)
|
||||
# - modulefinder (模块查找)
|
||||
# - runpy (运行Python模块)
|
||||
# - argparse (命令行参数)
|
||||
# - getopt (命令行选项)
|
||||
# - optparse (选项解析)
|
||||
# - configparser (配置文件)
|
||||
# - fileinput (文件输入)
|
||||
# - linecache (行缓存)
|
||||
# - glob (文件名模式匹配)
|
||||
# - fnmatch (文件名匹配)
|
||||
# - difflib (差异比较)
|
||||
# - textwrap (文本包装)
|
||||
# - string (字符串)
|
||||
# - struct (二进制数据)
|
||||
# - codecs (编解码器)
|
||||
# - unicodedata (Unicode数据)
|
||||
# - stringprep (字符串预处理)
|
||||
# - readline (行编辑)
|
||||
# - rlcompleter (自动补全)
|
||||
# - pprint (美化打印)
|
||||
# - reprlib (repr替代)
|
||||
# - enum (枚举)
|
||||
# - numbers (数字抽象基类)
|
||||
# - math (数学函数)
|
||||
# - cmath (复数数学)
|
||||
# - decimal (十进制浮点)
|
||||
# - fractions (分数)
|
||||
# - statistics (统计函数)
|
||||
# - array (数组)
|
||||
# - bisect (二分查找)
|
||||
# - heapq (堆队列)
|
||||
# - queue (队列)
|
||||
# - types (动态类型)
|
||||
# - contextlib (上下文管理)
|
||||
# - abc (抽象基类)
|
||||
# - atexit (退出处理)
|
||||
# - traceback (异常追踪)
|
||||
# - __future__ (未来特性)
|
||||
# - warnings (警告)
|
||||
# - dataclasses (数据类)
|
||||
# - typing (类型提示)
|
File diff suppressed because it is too large
Load Diff
@ -206,7 +206,7 @@
|
||||
</div>
|
||||
</div> <!-- 结束 mainContent -->
|
||||
|
||||
<script src="/lib/bootstrap/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 全局变量
|
||||
let currentPage = 1;
|
||||
@ -571,7 +571,11 @@
|
||||
displayCurrentPage();
|
||||
}
|
||||
|
||||
// 显示当前页数据
|
||||
} // 结束 initializePage 函数
|
||||
|
||||
// ========================= 全局函数 =========================
|
||||
|
||||
// 显示当前页数据(全局函数)
|
||||
function displayCurrentPage() {
|
||||
const resultsContainer = document.getElementById('searchResults');
|
||||
|
||||
@ -623,12 +627,134 @@
|
||||
resultsContainer.innerHTML = items.map(item => createItemCard(item)).join('');
|
||||
}
|
||||
|
||||
// 创建商品卡片HTML
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 更新分页提示(保留原有功能)
|
||||
function updatePagination() {
|
||||
const paginationContainer = document.getElementById('paginationContainer');
|
||||
const pagination = document.getElementById('pagination');
|
||||
|
||||
// 显示页码选择提示
|
||||
paginationContainer.style.display = 'block';
|
||||
|
||||
let paginationHtml = `
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
要查看其他页面,请在上方选择页码后重新搜索
|
||||
</span>
|
||||
</li>
|
||||
`;
|
||||
|
||||
pagination.innerHTML = paginationHtml;
|
||||
} // 结束 initializePage 函数
|
||||
|
||||
// ========================= 全局函数 =========================
|
||||
|
||||
// 跳转到指定页(全局函数,供分页按钮调用)
|
||||
function goToPage(page) {
|
||||
currentPage = page;
|
||||
displayCurrentPage();
|
||||
updateFrontendPagination();
|
||||
|
||||
// 更新统计信息
|
||||
updateStatsDisplay();
|
||||
|
||||
// 滚动到页面顶部
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// 更新统计信息显示(全局函数)
|
||||
function updateStatsDisplay() {
|
||||
const statsText = document.getElementById('statsText');
|
||||
let dataSource = '';
|
||||
|
||||
if (allItems.length > 0) {
|
||||
dataSource = ' [真实数据]';
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(allItems.length / currentPageSize);
|
||||
statsText.textContent = `搜索"${currentKeyword}",共获取 ${allItems.length} 个商品${dataSource},当前显示第 ${currentPage}/${totalPages} 页(每页${currentPageSize}条)`;
|
||||
}
|
||||
|
||||
// 更新前端分页(全局函数)
|
||||
function updateFrontendPagination() {
|
||||
const totalPages = Math.ceil(allItems.length / currentPageSize);
|
||||
const paginationContainer = document.getElementById('paginationContainer');
|
||||
const pagination = document.getElementById('pagination');
|
||||
|
||||
if (totalPages <= 1) {
|
||||
paginationContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
paginationContainer.style.display = 'block';
|
||||
|
||||
let paginationHtml = '';
|
||||
|
||||
// 上一页
|
||||
if (currentPage > 1) {
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${currentPage - 1}); return false;">上一页</a></li>`;
|
||||
}
|
||||
|
||||
// 页码显示逻辑
|
||||
if (totalPages <= 7) {
|
||||
// 如果总页数不超过7页,显示所有页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a></li>`;
|
||||
}
|
||||
} else {
|
||||
// 如果总页数超过7页,使用省略号显示
|
||||
if (currentPage <= 4) {
|
||||
// 当前页在前面,显示 1 2 3 4 5 ... 最后页
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a></li>`;
|
||||
}
|
||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${totalPages}); return false;">${totalPages}</a></li>`;
|
||||
} else if (currentPage >= totalPages - 3) {
|
||||
// 当前页在后面,显示 1 ... 倒数5页
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(1); return false;">1</a></li>`;
|
||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
for (let i = totalPages - 4; i <= totalPages; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a></li>`;
|
||||
}
|
||||
} else {
|
||||
// 当前页在中间,显示 1 ... 当前页前后2页 ... 最后页
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(1); return false;">1</a></li>`;
|
||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
for (let i = currentPage - 2; i <= currentPage + 2; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a></li>`;
|
||||
}
|
||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${totalPages}); return false;">${totalPages}</a></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 下一页
|
||||
if (currentPage < totalPages) {
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${currentPage + 1}); return false;">下一页</a></li>`;
|
||||
}
|
||||
|
||||
pagination.innerHTML = paginationHtml;
|
||||
}
|
||||
|
||||
// 创建商品卡片HTML(全局函数)
|
||||
function createItemCard(item) {
|
||||
const tags = item.tags ? item.tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('') : '';
|
||||
const imageUrl = item.main_image || 'https://via.placeholder.com/200x200?text=暂无图片';
|
||||
const wantCount = item.want_count || 0;
|
||||
|
||||
|
||||
return `
|
||||
<div class="col-md-6 col-lg-4 col-xl-3 mb-4">
|
||||
<div class="card item-card h-100">
|
||||
@ -667,120 +793,49 @@
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 更新前端分页
|
||||
function updateFrontendPagination() {
|
||||
const totalPages = Math.ceil(allItems.length / currentPageSize);
|
||||
const paginationContainer = document.getElementById('paginationContainer');
|
||||
const pagination = document.getElementById('pagination');
|
||||
|
||||
if (totalPages <= 1) {
|
||||
paginationContainer.style.display = 'none';
|
||||
// 显示商品详情(全局函数)
|
||||
function showItemDetail(itemId) {
|
||||
// 查找对应的商品数据
|
||||
const item = allItems.find(i => i.item_id === itemId);
|
||||
if (!item) {
|
||||
alert('商品信息不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
paginationContainer.style.display = 'block';
|
||||
|
||||
let paginationHtml = '';
|
||||
|
||||
// 上一页
|
||||
if (currentPage > 1) {
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${currentPage - 1})">上一页</a></li>`;
|
||||
}
|
||||
|
||||
// 页码显示逻辑
|
||||
if (totalPages <= 7) {
|
||||
// 如果总页数不超过7页,显示所有页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i})">${i}</a></li>`;
|
||||
}
|
||||
} else {
|
||||
// 如果总页数超过7页,使用省略号显示
|
||||
if (currentPage <= 4) {
|
||||
// 当前页在前面,显示 1 2 3 4 5 ... 最后页
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i})">${i}</a></li>`;
|
||||
}
|
||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${totalPages})">${totalPages}</a></li>`;
|
||||
} else if (currentPage >= totalPages - 3) {
|
||||
// 当前页在后面,显示 1 ... 倒数5页
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(1)">1</a></li>`;
|
||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
for (let i = totalPages - 4; i <= totalPages; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i})">${i}</a></li>`;
|
||||
}
|
||||
} else {
|
||||
// 当前页在中间,显示 1 ... 当前页前后2页 ... 最后页
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(1)">1</a></li>`;
|
||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
for (let i = currentPage - 2; i <= currentPage + 2; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i})">${i}</a></li>`;
|
||||
}
|
||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${totalPages})">${totalPages}</a></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 下一页
|
||||
if (currentPage < totalPages) {
|
||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${currentPage + 1})">下一页</a></li>`;
|
||||
}
|
||||
|
||||
pagination.innerHTML = paginationHtml;
|
||||
}
|
||||
|
||||
// 跳转到指定页
|
||||
function goToPage(page) {
|
||||
currentPage = page;
|
||||
displayCurrentPage();
|
||||
updateFrontendPagination();
|
||||
|
||||
// 更新统计信息
|
||||
updateStatsDisplay();
|
||||
}
|
||||
|
||||
// 更新统计信息显示
|
||||
function updateStatsDisplay() {
|
||||
const statsText = document.getElementById('statsText');
|
||||
let dataSource = '';
|
||||
|
||||
if (allItems.length > 0) {
|
||||
dataSource = ' [真实数据]';
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(allItems.length / currentPageSize);
|
||||
statsText.textContent = `搜索"${currentKeyword}",共获取 ${allItems.length} 个商品${dataSource},当前显示第 ${currentPage}/${totalPages} 页(每页${currentPageSize}条)`;
|
||||
}
|
||||
|
||||
// 更新分页提示(保留原有功能)
|
||||
function updatePagination() {
|
||||
const paginationContainer = document.getElementById('paginationContainer');
|
||||
const pagination = document.getElementById('pagination');
|
||||
|
||||
// 显示页码选择提示
|
||||
paginationContainer.style.display = 'block';
|
||||
|
||||
let paginationHtml = `
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
要查看其他页面,请在上方选择页码后重新搜索
|
||||
</span>
|
||||
</li>
|
||||
// 填充模态框内容
|
||||
const modalBody = document.querySelector('#itemDetailModal .modal-body');
|
||||
modalBody.innerHTML = `
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<img src="${escapeHtml(item.main_image || 'https://via.placeholder.com/400x400?text=暂无图片')}"
|
||||
class="img-fluid rounded" alt="${escapeHtml(item.title)}"
|
||||
onerror="this.src='https://via.placeholder.com/400x400?text=图片加载失败'">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>${escapeHtml(item.title)}</h5>
|
||||
<p class="text-primary fs-4 fw-bold">${escapeHtml(item.price)}</p>
|
||||
<p><strong>卖家:</strong>${escapeHtml(item.seller_name)}</p>
|
||||
${item.want_count ? `<p><strong>想要人数:</strong>${item.want_count}人</p>` : ''}
|
||||
${item.publish_time ? `<p><strong>发布时间:</strong>${escapeHtml(item.publish_time)}</p>` : ''}
|
||||
${item.tags && item.tags.length > 0 ? `
|
||||
<p><strong>标签:</strong></p>
|
||||
<div class="mb-3">
|
||||
${item.tags.map(tag => `<span class="badge bg-secondary me-1">${escapeHtml(tag)}</span>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
<p><strong>商品链接:</strong></p>
|
||||
<a href="${escapeHtml(item.item_url)}" target="_blank" class="btn btn-primary">
|
||||
<i class="bi bi-link-45deg me-1"></i>
|
||||
在闲鱼中查看
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
pagination.innerHTML = paginationHtml;
|
||||
// 显示模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('itemDetailModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
} // 结束 initializePage 函数
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -48,7 +48,7 @@ class QRLoginSession:
|
||||
|
||||
def __init__(self, session_id: str):
|
||||
self.session_id = session_id
|
||||
self.status = 'waiting' # waiting, scanned, success, expired, cancelled
|
||||
self.status = 'waiting' # waiting, scanned, success, expired, cancelled, verification_required
|
||||
self.qr_code_url = None
|
||||
self.qr_content = None
|
||||
self.cookies = {}
|
||||
@ -56,6 +56,7 @@ class QRLoginSession:
|
||||
self.created_time = time.time()
|
||||
self.expire_time = 300 # 5分钟过期
|
||||
self.params = {} # 存储登录参数
|
||||
self.verification_url = None # 风控验证URL
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""检查是否过期"""
|
||||
@ -281,13 +282,14 @@ class QRLoginManager:
|
||||
is True
|
||||
):
|
||||
# 账号被风控,需要手机验证
|
||||
session.status = 'cancelled'
|
||||
session.status = 'verification_required'
|
||||
iframe_url = (
|
||||
resp.json()
|
||||
.get("content", {})
|
||||
.get("data", {})
|
||||
.get("iframeRedirectUrl")
|
||||
)
|
||||
session.verification_url = iframe_url
|
||||
logger.warning(f"账号被风控,需要手机验证: {session_id}, URL: {iframe_url}")
|
||||
break
|
||||
else:
|
||||
@ -331,7 +333,7 @@ class QRLoginManager:
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 超时处理
|
||||
if session.status not in ['success', 'expired', 'cancelled']:
|
||||
if session.status not in ['success', 'expired', 'cancelled', 'verification_required']:
|
||||
session.status = 'expired'
|
||||
logger.info(f"二维码监控超时,标记为过期: {session_id}")
|
||||
|
||||
@ -354,6 +356,11 @@ class QRLoginManager:
|
||||
'session_id': session_id
|
||||
}
|
||||
|
||||
# 如果需要验证,返回验证URL
|
||||
if session.status == 'verification_required' and session.verification_url:
|
||||
result['verification_url'] = session.verification_url
|
||||
result['message'] = '账号被风控,需要手机验证'
|
||||
|
||||
# 如果登录成功,返回Cookie信息
|
||||
if session.status == 'success' and session.cookies and session.unb:
|
||||
result['cookies'] = self._cookie_marshal(session.cookies)
|
||||
|
Loading…
x
Reference in New Issue
Block a user