Compare commits

...

6 Commits

Author SHA1 Message Date
zhinianboke
789870b334 新增多种通知渠道,支持ai模型自定义 2025-08-02 20:23:33 +08:00
zhinianboke
49693760b8 Update README.md 2025-08-02 17:59:06 +08:00
zhinianboke
2a0bb2368a
Merge pull request #13 from Amazingzl/main
新增钉钉通知渠道支持,更新数据库结构以适应新功能,并优化相关日志记录
2025-08-02 16:30:25 +08:00
amazingzl
8ac5909db1 新增钉钉通知渠道支持,更新数据库结构以适应新功能,并优化相关日志记录 2025-08-02 16:24:52 +08:00
zhinianboke
565625ab15 修复商品搜索分页bug 2025-08-02 13:38:11 +08:00
zhinianboke
06a6392cfd 修复bug 2025-08-02 12:58:04 +08:00
8 changed files with 1819 additions and 333 deletions

View File

@ -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
View File

@ -3,7 +3,9 @@
[![GitHub](https://img.shields.io/badge/GitHub-zhinianboke%2Fxianyu--auto--reply-blue?logo=github)](https://github.com/zhinianboke/xianyu-auto-reply)
[![Docker](https://img.shields.io/badge/Docker-一键部署-blue?logo=docker)](https://github.com/zhinianboke/xianyu-auto-reply#-快速开始)
[![Python](https://img.shields.io/badge/Python-3.11+-green?logo=python)](https://www.python.org/)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![License](https://img.shields.io/badge/License-仅供学习-red.svg)](#-版权声明与使用条款)
> **⚠️ 重要提示:本项目仅供学习研究使用,严禁商业用途!使用前请仔细阅读[版权声明](#-版权声明与使用条款)。**
一个功能完整的闲鱼自动回复和管理系统,支持多用户、多账号管理,具备智能回复、自动发货、自动确认发货、商品管理等企业级功能。
@ -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
- **邮箱联系**:在项目交流群中获取联系方式
我们将在收到通知后**立即处理**并删除相关内容。
### 🤝 合作与授权
如需商业使用或特殊授权,请通过项目交流群联系作者进行协商。
---
**⚖️ 使用本项目即表示您已阅读、理解并同意遵守以上所有条款。**
---
🎉 **开始使用闲鱼自动回复系统,让您的闲鱼店铺管理更加智能高效!**
**请记住:仅限学习使用,禁止商业用途!**

View File

@ -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'&timestamp={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)}")

View File

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

View File

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

View File

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

View File

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