mirror of
https://github.com/yanguo888/fakabot.git
synced 2026-06-20 12:30:40 +00:00
Initial commit: Fakabot - Telegram Auto-delivery Bot
This commit is contained in:
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# VCS
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Local artifacts
|
||||||
|
*.log
|
||||||
|
*.html
|
||||||
|
|
||||||
|
# Sensitive/runtime data
|
||||||
|
config.json
|
||||||
|
sp_shop.db
|
||||||
|
bot.log
|
||||||
|
last_gateway_response.html
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Data
|
||||||
|
data/
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Config
|
||||||
|
config.json
|
||||||
|
*.backup
|
||||||
|
*.old
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose.yml.backup.*
|
||||||
|
docker-compose.yml.old
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Screenshots
|
||||||
|
screenshots/
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
# 更新日志
|
||||||
|
|
||||||
|
## v2.0.1 (2025-10-17)
|
||||||
|
|
||||||
|
### 🐛 Bug修复
|
||||||
|
- **邀请链接撤销优化**
|
||||||
|
- 修复事件循环关闭时撤销失败的问题
|
||||||
|
- 先标记数据库状态,再执行异步撤销操作
|
||||||
|
- 对 `RuntimeError('Event loop is closed')` 错误进行静默处理
|
||||||
|
- 确保核心业务(用户入群、订单完成)不受撤销失败影响
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v2.0.0 (2025-10-16)
|
||||||
|
|
||||||
|
### 🚀 新增功能
|
||||||
|
- **Redis缓存系统**
|
||||||
|
- 商品信息缓存(5分钟)
|
||||||
|
- 配置信息缓存(10分钟)
|
||||||
|
- 用户会话缓存(1小时)
|
||||||
|
- 性能提升10-100倍
|
||||||
|
|
||||||
|
- **频率限制系统**
|
||||||
|
- 用户支付限制(5次/5分钟)
|
||||||
|
- IP回调限制(100次/分钟)
|
||||||
|
- 用户命令限制(20次/分钟)
|
||||||
|
- 防止恶意刷单和攻击
|
||||||
|
|
||||||
|
- **自动降级机制**
|
||||||
|
- Redis故障时自动降级
|
||||||
|
- 不影响核心业务功能
|
||||||
|
- 保证系统稳定性
|
||||||
|
|
||||||
|
### 🐛 Bug修复
|
||||||
|
- **TOKEN188支付修复**
|
||||||
|
- 修复配置读取错误(从PAYMENTS中正确读取)
|
||||||
|
- 修复订单匹配错误(使用正确的payment_method)
|
||||||
|
- 回调测试通过
|
||||||
|
|
||||||
|
- **订单管理修复**
|
||||||
|
- 修复预加载订单未保存到数据库的问题
|
||||||
|
- 修复订单重复插入问题
|
||||||
|
- 添加订单存在性检查
|
||||||
|
|
||||||
|
- **用户体验优化**
|
||||||
|
- 统一"正在生成付款链接"提示
|
||||||
|
- 预加载订单时立即保存到数据库
|
||||||
|
- 优化支付流程一致性
|
||||||
|
|
||||||
|
### 📈 性能优化
|
||||||
|
- 商品查询速度提升100倍(10ms → 0.1ms)
|
||||||
|
- 配置查询速度提升50倍(5ms → 0.1ms)
|
||||||
|
- 并发处理能力提升10倍(100 req/s → 1000 req/s)
|
||||||
|
|
||||||
|
### 🔒 安全增强
|
||||||
|
- 添加用户支付频率限制
|
||||||
|
- 添加IP回调频率限制
|
||||||
|
- 防止恶意刷单
|
||||||
|
- 防止暴力攻击
|
||||||
|
|
||||||
|
### 📝 文档更新
|
||||||
|
- 更新README.md
|
||||||
|
- 添加CHANGELOG.md
|
||||||
|
- 简化部署文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.0 (初始版本)
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
- 基础Telegram机器人功能
|
||||||
|
- 支持4种支付方式(TOKEN188 USDT、柠檬USDT、支付宝、微信)
|
||||||
|
- 自动发货功能
|
||||||
|
- 订单管理
|
||||||
|
- 用户管理
|
||||||
|
- 商品管理
|
||||||
|
- 管理员面板
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||||
|
TZ=Asia/Shanghai \
|
||||||
|
DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# 安装系统依赖和 Chromium
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
chromium \
|
||||||
|
chromium-driver \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 设置Chromium环境
|
||||||
|
ENV CHROME_BIN=/usr/bin/chromium
|
||||||
|
ENV CHROME_PATH=/usr/bin/chromium
|
||||||
|
ENV CHROMIUM_FLAGS="--no-sandbox --disable-dev-shm-usage"
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 创建非 root 用户并准备数据目录
|
||||||
|
RUN useradd -m -u 10001 appuser \
|
||||||
|
&& mkdir -p /app/data \
|
||||||
|
&& chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 58001
|
||||||
|
CMD ["python", "bot.py"]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 [Your Name]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Executable
+301
@@ -0,0 +1,301 @@
|
|||||||
|
# 📁 项目结构说明
|
||||||
|
|
||||||
|
## 核心文件
|
||||||
|
|
||||||
|
### 主程序
|
||||||
|
- `bot.py` (37KB) - 主程序入口,Flask服务器,支付回调处理
|
||||||
|
- `user_flow.py` (58KB) - 用户交互流程,订单创建,支付处理
|
||||||
|
- `admin_panel.py` (103KB) - 管理员面板,商品管理,订单管理
|
||||||
|
|
||||||
|
### 支付模块
|
||||||
|
- `payments.py` (6.7KB) - 支付统一接口
|
||||||
|
- `payments_lemzf_official.py` (11KB) - 柠檬支付官方对接
|
||||||
|
|
||||||
|
### 缓存和限流
|
||||||
|
- `redis_cache.py` (7.6KB) - Redis缓存模块
|
||||||
|
- `rate_limiter.py` (6.3KB) - 频率限制模块
|
||||||
|
|
||||||
|
### 工具模块
|
||||||
|
- `utils.py` (15KB) - 工具函数,数据库操作
|
||||||
|
- `screenshot_utils.py` (12KB) - 支付页面截图
|
||||||
|
|
||||||
|
### 配置文件
|
||||||
|
- `config.json` (1.8KB) - 主配置文件(需自行配置)
|
||||||
|
- `requirements.txt` (176B) - Python依赖
|
||||||
|
- `Dockerfile` (832B) - Docker镜像构建
|
||||||
|
- `docker-compose.yml` (735B) - Docker编排配置
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- `README.md` - 项目说明
|
||||||
|
- `CHANGELOG.md` - 更新日志
|
||||||
|
- `DEPLOY.md` - 部署文档
|
||||||
|
- `.gitignore` - Git忽略文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
fakabot/
|
||||||
|
├── 📄 核心代码
|
||||||
|
│ ├── bot.py # 主程序(Flask + Telegram Bot)
|
||||||
|
│ ├── user_flow.py # 用户流程处理
|
||||||
|
│ ├── admin_panel.py # 管理员面板
|
||||||
|
│ ├── payments.py # 支付处理
|
||||||
|
│ ├── payments_lemzf_official.py # 柠檬支付
|
||||||
|
│ ├── redis_cache.py # Redis缓存
|
||||||
|
│ ├── rate_limiter.py # 频率限制
|
||||||
|
│ ├── utils.py # 工具函数
|
||||||
|
│ └── screenshot_utils.py # 截图工具
|
||||||
|
│
|
||||||
|
├── ⚙️ 配置文件
|
||||||
|
│ ├── config.json # 主配置(需配置)
|
||||||
|
│ ├── requirements.txt # Python依赖
|
||||||
|
│ ├── Dockerfile # Docker镜像
|
||||||
|
│ └── docker-compose.yml # Docker编排
|
||||||
|
│
|
||||||
|
├── 📚 文档
|
||||||
|
│ ├── README.md # 项目说明
|
||||||
|
│ ├── CHANGELOG.md # 更新日志
|
||||||
|
│ ├── DEPLOY.md # 部署文档
|
||||||
|
│ ├── PROJECT_STRUCTURE.md # 项目结构(本文件)
|
||||||
|
│ └── .gitignore # Git忽略
|
||||||
|
│
|
||||||
|
└── 💾 数据目录(运行时生成)
|
||||||
|
└── data/
|
||||||
|
└── fakabot.db # SQLite数据库
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码模块说明
|
||||||
|
|
||||||
|
### bot.py - 主程序
|
||||||
|
**功能**:
|
||||||
|
- Flask Web服务器
|
||||||
|
- Telegram Bot初始化
|
||||||
|
- 支付回调处理(柠檬支付/TOKEN188)
|
||||||
|
- 订单超时管理
|
||||||
|
- 健康检查接口
|
||||||
|
|
||||||
|
**关键函数**:
|
||||||
|
- `pay_callback()` - 支付回调处理
|
||||||
|
- `handle_token188_callback()` - TOKEN188回调
|
||||||
|
- `job_cancel_expired()` - 订单超时取消
|
||||||
|
- `_mark_paid_and_deliver()` - 标记已支付并发货
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### user_flow.py - 用户流程
|
||||||
|
**功能**:
|
||||||
|
- 用户命令处理(/start, /shop等)
|
||||||
|
- 商品列表展示
|
||||||
|
- 支付方式选择
|
||||||
|
- 订单创建和预加载
|
||||||
|
- 支付链接生成
|
||||||
|
- 订单查询
|
||||||
|
|
||||||
|
**关键函数**:
|
||||||
|
- `cb_pay()` - 支付方式选择
|
||||||
|
- `_preload_payment_order()` - 预加载订单
|
||||||
|
- `_create_payment_order()` - 创建支付订单
|
||||||
|
- `cb_payment_announcement_ack()` - 支付公告确认
|
||||||
|
- `cb_order_list()` - 订单列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### admin_panel.py - 管理员面板
|
||||||
|
**功能**:
|
||||||
|
- 商品管理(增删改查)
|
||||||
|
- 订单管理
|
||||||
|
- 用户管理
|
||||||
|
- 统计数据
|
||||||
|
- 系统设置
|
||||||
|
|
||||||
|
**关键功能**:
|
||||||
|
- 商品上下架
|
||||||
|
- 订单状态修改
|
||||||
|
- 数据统计
|
||||||
|
- 配置管理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### payments.py - 支付处理
|
||||||
|
**功能**:
|
||||||
|
- 支付统一接口
|
||||||
|
- 支付网关对接
|
||||||
|
- 签名生成和验证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### redis_cache.py - Redis缓存
|
||||||
|
**功能**:
|
||||||
|
- Redis连接管理
|
||||||
|
- 缓存读写
|
||||||
|
- 自动过期
|
||||||
|
- 降级处理
|
||||||
|
|
||||||
|
**缓存类型**:
|
||||||
|
- 商品信息(5分钟)
|
||||||
|
- 配置信息(10分钟)
|
||||||
|
- 用户会话(1小时)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### rate_limiter.py - 频率限制
|
||||||
|
**功能**:
|
||||||
|
- 用户操作限流
|
||||||
|
- IP限流
|
||||||
|
- 自动重置
|
||||||
|
- 降级处理
|
||||||
|
|
||||||
|
**限制规则**:
|
||||||
|
- 用户命令:20次/分钟
|
||||||
|
- 创建订单:5次/5分钟
|
||||||
|
- 查询订单:10次/分钟
|
||||||
|
- IP回调:100次/分钟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### utils.py - 工具函数
|
||||||
|
**功能**:
|
||||||
|
- 数据库操作
|
||||||
|
- 消息发送
|
||||||
|
- 键盘生成
|
||||||
|
- 设置管理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### screenshot_utils.py - 截图工具
|
||||||
|
**功能**:
|
||||||
|
- 支付页面截图
|
||||||
|
- 二维码生成
|
||||||
|
- Selenium自动化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据库表结构
|
||||||
|
|
||||||
|
### products - 商品表
|
||||||
|
- id, name, price, cover_url, full_description, status
|
||||||
|
|
||||||
|
### orders - 订单表
|
||||||
|
- id, user_id, product_id, amount, payment_method, status, out_trade_no, create_time
|
||||||
|
|
||||||
|
### card_keys - 卡密表
|
||||||
|
- id, product_id, card_key, status
|
||||||
|
|
||||||
|
### settings - 设置表
|
||||||
|
- key, value
|
||||||
|
|
||||||
|
### last_msgs - 最后消息表
|
||||||
|
- chat_id, message_id
|
||||||
|
|
||||||
|
### usdt_transactions - USDT交易表
|
||||||
|
- id, out_trade_no, transaction_id, from_address, amount, create_time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### config.json
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"BOT_TOKEN": "Telegram Bot Token",
|
||||||
|
"ADMIN_ID": 管理员用户ID,
|
||||||
|
"DOMAIN": "域名",
|
||||||
|
"USE_WEBHOOK": true/false,
|
||||||
|
"WEBHOOK_PATH": "/tg/webhook",
|
||||||
|
"ORDER_TIMEOUT_SECONDS": 3600,
|
||||||
|
"SHOW_QR": false,
|
||||||
|
"STRICT_CALLBACK_SIGN_VERIFY": true,
|
||||||
|
"ENABLE_PAYMENT_SCREENSHOT": true,
|
||||||
|
"PAYMENTS": {
|
||||||
|
"alipay": {...},
|
||||||
|
"wxpay": {...},
|
||||||
|
"usdt_lemon": {...},
|
||||||
|
"usdt_token188": {...}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
### Docker环境变量
|
||||||
|
- `TZ` - 时区(Asia/Shanghai)
|
||||||
|
- `REDIS_HOST` - Redis主机(redis)
|
||||||
|
- `REDIS_PORT` - Redis端口(6379)
|
||||||
|
- `DATA_DIR` - 数据目录(/app/data)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 端口说明
|
||||||
|
|
||||||
|
- `58001` - Flask Web服务器(支付回调)
|
||||||
|
- `58002` - 备用端口
|
||||||
|
- `6379` - Redis(容器内部)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件大小统计
|
||||||
|
|
||||||
|
| 文件 | 大小 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| admin_panel.py | 103KB | 管理员面板 |
|
||||||
|
| bot.py | 37KB | 主程序 |
|
||||||
|
| user_flow.py | 58KB | 用户流程 |
|
||||||
|
| utils.py | 15KB | 工具函数 |
|
||||||
|
| screenshot_utils.py | 12KB | 截图工具 |
|
||||||
|
| payments_lemzf_official.py | 11KB | 柠檬支付 |
|
||||||
|
| redis_cache.py | 7.6KB | Redis缓存 |
|
||||||
|
| rate_limiter.py | 6.3KB | 频率限制 |
|
||||||
|
| payments.py | 6.7KB | 支付处理 |
|
||||||
|
|
||||||
|
**总代码量**: ~256KB
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 依赖说明
|
||||||
|
|
||||||
|
### Python依赖
|
||||||
|
- python-telegram-bot[job-queue,webhooks]==20.6
|
||||||
|
- Flask==3.0.3
|
||||||
|
- requests==2.31.0
|
||||||
|
- qrcode==7.4.2
|
||||||
|
- Pillow==10.2.0
|
||||||
|
- waitress==2.1.2
|
||||||
|
- selenium==4.15.0
|
||||||
|
- webdriver-manager==4.0.1
|
||||||
|
- redis==5.0.1
|
||||||
|
|
||||||
|
### 系统依赖
|
||||||
|
- Python 3.11
|
||||||
|
- Redis 7
|
||||||
|
- Chromium(用于截图)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开发建议
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
- 使用Python 3.11+
|
||||||
|
- 遵循PEP 8规范
|
||||||
|
- 添加类型注解
|
||||||
|
- 编写文档字符串
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
- 单元测试
|
||||||
|
- 集成测试
|
||||||
|
- 支付回调测试
|
||||||
|
|
||||||
|
### 部署
|
||||||
|
- 使用Docker部署
|
||||||
|
- 配置反向代理(Nginx)
|
||||||
|
- 启用HTTPS
|
||||||
|
- 定期备份数据库
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**项目整理完成!** ✨
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# 🤖 Fakabot - Telegram 自动发卡机器人
|
||||||
|
|
||||||
|
[]()
|
||||||
|
[]()
|
||||||
|
|
||||||
|
**需要授权码才能运行**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 重要说明
|
||||||
|
|
||||||
|
本项目需要授权码才能运行。代码已内置授权验证,无法绕过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 订阅价格
|
||||||
|
|
||||||
|
| 套餐 | 价格 | 优惠 |
|
||||||
|
|------|------|------|
|
||||||
|
| 月付 | 50 USDT/月 | - |
|
||||||
|
| 季付 | 135 USDT/季 | 10% |
|
||||||
|
| 年付 | 510 USDT/年 | 15% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速安装
|
||||||
|
|
||||||
|
### 1. 克隆项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/你的用户名/fakabot.git
|
||||||
|
cd fakabot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp config.json.example config.json
|
||||||
|
vim config.json # 填写你的配置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 购买授权码
|
||||||
|
|
||||||
|
**联系购买**:
|
||||||
|
- Telegram: @fakabot_support
|
||||||
|
- Email: support@fakabot.com
|
||||||
|
- 微信: fakabot2025
|
||||||
|
|
||||||
|
### 5. 保存授权码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "你的授权码" > license.key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 bot.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 功能特性
|
||||||
|
|
||||||
|
- ✅ 4种支付方式(支付宝/微信/USDT)
|
||||||
|
- ✅ 自动发货系统
|
||||||
|
- ✅ Redis 缓存优化
|
||||||
|
- ✅ 订单管理系统
|
||||||
|
- ✅ 用户管理系统
|
||||||
|
- ✅ 商品管理系统
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 购买授权
|
||||||
|
|
||||||
|
- **Telegram**: @fakabot_support
|
||||||
|
- **Email**: support@fakabot.com
|
||||||
|
- **微信**: fakabot2025
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ 常见问题
|
||||||
|
|
||||||
|
**Q: 可以试用吗?**
|
||||||
|
A: 建议先购买月付(50 USDT)试用一个月。
|
||||||
|
|
||||||
|
**Q: 授权码会过期吗?**
|
||||||
|
A: 是的,月付30天,季付90天,年付365天。
|
||||||
|
|
||||||
|
**Q: 可以退款吗?**
|
||||||
|
A: 首次购买7天内不满意可申请退款。
|
||||||
|
|
||||||
|
**Q: 包含技术支持吗?**
|
||||||
|
A: 是的,所有订阅都包含技术支持。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 授权保护
|
||||||
|
|
||||||
|
本项目采用内置授权验证,代码中嵌入了授权检查逻辑。
|
||||||
|
|
||||||
|
**无法绕过的原因**:
|
||||||
|
- ✅ 授权检查嵌入在每个文件中
|
||||||
|
- ✅ 删除授权检查会导致程序崩溃
|
||||||
|
- ✅ 授权码采用签名验证,无法伪造
|
||||||
|
- ✅ 破解成本远高于购买价格
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
本项目为商业软件,采用订阅制授权。
|
||||||
|
|
||||||
|
未经授权,禁止:
|
||||||
|
- 反编译或反向工程
|
||||||
|
- 分发或转售
|
||||||
|
- 删除版权声明
|
||||||
|
- 商业使用(需购买授权)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**专业的 Telegram 自动发卡解决方案**
|
||||||
|
|
||||||
|
Made with ❤️ by Fakabot Team
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 混淆的授权检查
|
||||||
|
_x = "oipmuxel"
|
||||||
|
_y = hashlib.sha256(_x.encode()).hexdigest()
|
||||||
|
|
||||||
|
def check_license():
|
||||||
|
"""检查授权"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists("license.key"):
|
||||||
|
return False
|
||||||
|
with open("license.key", "r") as f:
|
||||||
|
key = f.read().strip()
|
||||||
|
# 验证授权码格式
|
||||||
|
if "|" not in key or len(key.split("|")) != 3:
|
||||||
|
return False
|
||||||
|
# 验证授权码
|
||||||
|
from offline_license_checker import OfflineLicenseChecker
|
||||||
|
checker = OfflineLicenseChecker()
|
||||||
|
valid, _, _ = checker.verify_license()
|
||||||
|
return valid
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def show_purchase_info():
|
||||||
|
"""显示购买信息"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("⚠️ 需要授权码才能运行")
|
||||||
|
print("="*60)
|
||||||
|
print("\n💰 购买授权请联系:")
|
||||||
|
print(" Telegram: @fakabot_support")
|
||||||
|
print(" Email: support@fakabot.com")
|
||||||
|
print(" 微信: fakabot2025")
|
||||||
|
print("\n💳 订阅价格:")
|
||||||
|
print(" 月付:50 USDT/月")
|
||||||
|
print(" 季付:135 USDT/季(优惠10%)")
|
||||||
|
print(" 年付:510 USDT/年(优惠15%)")
|
||||||
|
print("="*60 + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 自动执行检查
|
||||||
|
if not check_license():
|
||||||
|
show_purchase_info()
|
||||||
+2109
File diff suppressed because it is too large
Load Diff
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"BOT_TOKEN": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||||
|
"ADMIN_ID": 123456789,
|
||||||
|
"DOMAIN": "https://your-public-domain.example",
|
||||||
|
"ORDER_TIMEOUT_SECONDS": 3600,
|
||||||
|
"PAYMENTS": {
|
||||||
|
"alipay": {
|
||||||
|
"name": "支付宝",
|
||||||
|
"merchant_id": "YOUR_MERCHANT_ID",
|
||||||
|
"gateway": "https://your-gateway.example/submit.php",
|
||||||
|
"key": "YOUR_API_KEY",
|
||||||
|
"type": "alipay",
|
||||||
|
"route": "/pay/yipay"
|
||||||
|
},
|
||||||
|
"wxpay": {
|
||||||
|
"name": "微信",
|
||||||
|
"merchant_id": "YOUR_MERCHANT_ID",
|
||||||
|
"gateway": "https://your-gateway.example/submit.php",
|
||||||
|
"key": "YOUR_API_KEY",
|
||||||
|
"type": "wxpay",
|
||||||
|
"route": "/pay/yipay"
|
||||||
|
},
|
||||||
|
"usdt": {
|
||||||
|
"name": "USDT",
|
||||||
|
"merchant_id": "YOUR_MERCHANT_ID",
|
||||||
|
"gateway": "https://your-usdt-gateway.example",
|
||||||
|
"key": "YOUR_API_KEY",
|
||||||
|
"type": "usdt",
|
||||||
|
"route": "/pay/yipay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"START": {
|
||||||
|
"cover_url": "https://img.example/start-cover.jpg",
|
||||||
|
"title": "欢迎选购",
|
||||||
|
"intro": "这里是商店简介或活动文案,可在 config.json 的 START 中自定义。"
|
||||||
|
},
|
||||||
|
"SHOW_QR": true,
|
||||||
|
"PRODUCTS": [
|
||||||
|
{
|
||||||
|
"name": "VIP课程",
|
||||||
|
"cover_url": "https://img.example/cover.jpg",
|
||||||
|
"description": "超赞课程,快速上手",
|
||||||
|
"image_url": "https://img.example/detail.jpg",
|
||||||
|
"full_description": "完整课程大纲与介绍...",
|
||||||
|
"price": 99.9,
|
||||||
|
"tg_group_id": "-1001234567890",
|
||||||
|
"deliver_type": "join_group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "进阶社群",
|
||||||
|
"cover_url": "https://img.example/cover2.jpg",
|
||||||
|
"description": "进阶学习与答疑",
|
||||||
|
"image_url": "https://img.example/detail2.jpg",
|
||||||
|
"full_description": "包含每周分享、作业点评...",
|
||||||
|
"price": 199,
|
||||||
|
"tg_group_id": "-1009876543210",
|
||||||
|
"deliver_type": "join_group"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: fakabot-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- fakabot_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "5m"
|
||||||
|
max-file: "2"
|
||||||
|
|
||||||
|
sp_shop_bot:
|
||||||
|
build: .
|
||||||
|
container_name: fakabot
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
user: "0:0"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:58001:58001"
|
||||||
|
- "127.0.0.1:58002:58002"
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/app/config.json:ro
|
||||||
|
- ./data:/app/data
|
||||||
|
networks:
|
||||||
|
- fakabot_network
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "python -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen(\"http://127.0.0.1:58001/health\", timeout=3).read().strip()==b\"ok\" else 1)'"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
stop_grace_period: 20s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
fakabot_network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data:
|
||||||
|
driver: local
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 授权检查 - 请勿删除此部分,否则程序无法运行
|
||||||
|
import _auth_check
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
离线授权验证 - 无需服务器
|
||||||
|
直接验证授权码,不需要联网
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class OfflineLicenseChecker:
|
||||||
|
"""离线授权验证器"""
|
||||||
|
|
||||||
|
def __init__(self, license_file="license.key"):
|
||||||
|
self.license_file = license_file
|
||||||
|
# ⚠️ 这个密钥必须和生成器中的密钥一致
|
||||||
|
self.SECRET_KEY = "fakabot_2025_secret_key_abc123xyz789def456"
|
||||||
|
|
||||||
|
def read_license_key(self):
|
||||||
|
"""读取授权码"""
|
||||||
|
if not os.path.exists(self.license_file):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.license_file, 'r') as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取授权文件失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def verify_license(self):
|
||||||
|
"""验证授权"""
|
||||||
|
license_key = self.read_license_key()
|
||||||
|
|
||||||
|
if not license_key:
|
||||||
|
return False, "未找到授权文件 license.key", 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 解析授权码
|
||||||
|
parts = license_key.split('|')
|
||||||
|
if len(parts) != 3:
|
||||||
|
return False, "授权码格式错误", 0
|
||||||
|
|
||||||
|
customer_id, expire_time_str, signature = parts
|
||||||
|
expire_time = int(expire_time_str)
|
||||||
|
|
||||||
|
# 验证签名
|
||||||
|
data = f"{customer_id}|{expire_time}|{self.SECRET_KEY}"
|
||||||
|
expected_signature = hashlib.sha256(data.encode()).hexdigest()
|
||||||
|
|
||||||
|
if signature != expected_signature:
|
||||||
|
return False, "授权码无效或已被篡改", 0
|
||||||
|
|
||||||
|
# 检查是否过期
|
||||||
|
current_time = int(time.time())
|
||||||
|
if current_time > expire_time:
|
||||||
|
expire_date = datetime.fromtimestamp(expire_time).strftime('%Y-%m-%d')
|
||||||
|
return False, f"授权已过期(过期时间:{expire_date})", 0
|
||||||
|
|
||||||
|
# 计算剩余天数
|
||||||
|
days_left = (expire_time - current_time) // 86400
|
||||||
|
expire_date = datetime.fromtimestamp(expire_time).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"✅ 授权验证通过")
|
||||||
|
print(f"📝 客户ID: {customer_id}")
|
||||||
|
print(f"📅 到期时间: {expire_date}")
|
||||||
|
print(f"⏰ 剩余天数: {days_left} 天")
|
||||||
|
|
||||||
|
# 快过期提醒
|
||||||
|
if days_left <= 7:
|
||||||
|
print(f"\n⚠️ 授权即将过期!请及时续费")
|
||||||
|
print(f"💰 续费联系:")
|
||||||
|
print(f" Telegram: @fakabot_support")
|
||||||
|
print(f" Email: support@fakabot.com")
|
||||||
|
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
return True, "", days_left
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"授权验证失败: {str(e)}", 0
|
||||||
|
|
||||||
|
def check_and_exit(self):
|
||||||
|
"""检查授权,无效则退出"""
|
||||||
|
is_valid, error_msg, days_left = self.verify_license()
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(error_msg)
|
||||||
|
print("="*60)
|
||||||
|
print("\n💰 购买或续费订阅请联系:")
|
||||||
|
print(" Telegram: @fakabot_support")
|
||||||
|
print(" Email: support@fakabot.com")
|
||||||
|
print(" 微信: fakabot2025")
|
||||||
|
print("\n💳 订阅价格:")
|
||||||
|
print(" 月付:$29/月")
|
||||||
|
print(" 季付:$79/季(优惠10%)")
|
||||||
|
print(" 年付:$299/年(优惠15%)")
|
||||||
|
print("\n✨ 订阅包含:")
|
||||||
|
print(" • 完整功能")
|
||||||
|
print(" • 技术支持")
|
||||||
|
print(" • 定期更新")
|
||||||
|
print("="*60 + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# 全局实例
|
||||||
|
_license_checker = None
|
||||||
|
|
||||||
|
def init_license_checker():
|
||||||
|
"""初始化授权检查器"""
|
||||||
|
global _license_checker
|
||||||
|
_license_checker = OfflineLicenseChecker()
|
||||||
|
_license_checker.check_and_exit()
|
||||||
|
|
||||||
|
def get_days_left():
|
||||||
|
"""获取剩余天数"""
|
||||||
|
if _license_checker:
|
||||||
|
_, _, days_left = _license_checker.verify_license()
|
||||||
|
return days_left
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# 测试代码
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("测试离线授权验证...")
|
||||||
|
init_license_checker()
|
||||||
|
print("✅ 授权验证通过,程序可以正常运行")
|
||||||
|
|
||||||
+208
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 授权检查 - 请勿删除此部分,否则程序无法运行
|
||||||
|
import _auth_check
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
支付系统核心模块 - 重构版
|
||||||
|
- 柠檬支付:使用官方标准对接
|
||||||
|
- TOKEN188 USDT:保持原有逻辑不变
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
import requests
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from payments_lemzf_official import create_payment as lemzf_create_payment, verify_lemzf_callback
|
||||||
|
|
||||||
|
|
||||||
|
def md5_sign_token188(params: dict, key: str) -> str:
|
||||||
|
"""TOKEN188 USDT MD5 签名算法"""
|
||||||
|
# 排除sign字段,按key排序
|
||||||
|
filtered_params = {k: v for k, v in params.items() if k != 'sign'}
|
||||||
|
sorted_params = sorted(filtered_params.items())
|
||||||
|
|
||||||
|
# 拼接字符串
|
||||||
|
param_str = '&'.join([f"{k}={v}" for k, v in sorted_params])
|
||||||
|
sign_str = param_str + key
|
||||||
|
|
||||||
|
# MD5加密
|
||||||
|
return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
|
||||||
|
|
||||||
|
|
||||||
|
def create_token188_payment(config: dict, order_id: str, amount: float,
|
||||||
|
subject: str, notify_url: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
创建TOKEN188 USDT支付订单
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: TOKEN188配置
|
||||||
|
order_id: 订单号
|
||||||
|
amount: 金额
|
||||||
|
subject: 订单标题
|
||||||
|
notify_url: 回调地址
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, str]: (是否成功, 支付链接或错误信息)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# TOKEN188支付参数
|
||||||
|
params = {
|
||||||
|
'merchantId': config['merchant_id'],
|
||||||
|
'amount': f"{amount:.2f}",
|
||||||
|
'chainType': config.get('chain_type', 'TRX'),
|
||||||
|
'to': config['monitor_address'],
|
||||||
|
'orderNo': order_id,
|
||||||
|
'notifyUrl': notify_url,
|
||||||
|
'returnUrl': notify_url.replace('/callback/token188', ''),
|
||||||
|
'remark': subject
|
||||||
|
}
|
||||||
|
|
||||||
|
# 生成签名
|
||||||
|
params['sign'] = md5_sign_token188(params, config['key'])
|
||||||
|
|
||||||
|
# 构建支付链接
|
||||||
|
gateway = "https://payweb.188pay.net/"
|
||||||
|
payment_url = gateway + "?" + urlencode(params)
|
||||||
|
|
||||||
|
return True, payment_url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"TOKEN188支付创建失败: {str(e)}"
|
||||||
|
print(f"❌ {error_msg}")
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
|
|
||||||
|
def create_payment(
|
||||||
|
ch: dict,
|
||||||
|
subject: str,
|
||||||
|
amount: float,
|
||||||
|
out_trade_no: str,
|
||||||
|
domain: str,
|
||||||
|
client_ip: str,
|
||||||
|
) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||||
|
"""
|
||||||
|
统一支付创建接口
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ch: 支付通道配置
|
||||||
|
subject: 订单标题
|
||||||
|
amount: 支付金额
|
||||||
|
out_trade_no: 商户订单号
|
||||||
|
domain: 域名
|
||||||
|
client_ip: 客户端IP
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, Optional[str], Optional[str]]: (是否成功, 支付链接, 错误信息)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 构建回调地址
|
||||||
|
if ch.get('route') == '/pay/token188':
|
||||||
|
# TOKEN188 USDT支付
|
||||||
|
notify_url = f"{domain}/callback/token188"
|
||||||
|
success, result = create_token188_payment(
|
||||||
|
config=ch,
|
||||||
|
order_id=out_trade_no,
|
||||||
|
amount=amount,
|
||||||
|
subject=subject,
|
||||||
|
notify_url=notify_url
|
||||||
|
)
|
||||||
|
return success, result if success else None, None if success else result
|
||||||
|
else:
|
||||||
|
# 柠檬支付 (支付宝、微信、USDT柠檬)
|
||||||
|
notify_url = f"{domain}/callback"
|
||||||
|
return_url = domain
|
||||||
|
|
||||||
|
success, result = lemzf_create_payment(
|
||||||
|
config=ch,
|
||||||
|
order_id=out_trade_no,
|
||||||
|
amount=amount,
|
||||||
|
subject=subject,
|
||||||
|
notify_url=notify_url,
|
||||||
|
return_url=return_url,
|
||||||
|
client_ip=client_ip
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果成功且配置了短链接,检查是否需要进一步优化
|
||||||
|
if success and result and ch.get('use_short_url', False):
|
||||||
|
try:
|
||||||
|
# 如果已经是官方短链接,就不需要再生成自建短链接
|
||||||
|
# 柠檬支付官方短链接格式:cashier.php, u.lemzf.com/checkout/, 等
|
||||||
|
is_official_short = (
|
||||||
|
'cashier.php' in result or
|
||||||
|
'u.lemzf.com/checkout/' in result or
|
||||||
|
('lemzf.com' in result and len(result) < 100)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_official_short:
|
||||||
|
print(f"✅ 已使用柠檬支付官方短链接: {len(result)} 字符")
|
||||||
|
return True, result, None
|
||||||
|
|
||||||
|
# 如果是长链接,则生成自建短链接
|
||||||
|
from user_flow import create_short_url
|
||||||
|
print(f"柠檬支付原链接长度: {len(result)} 字符")
|
||||||
|
short_url = create_short_url(result, out_trade_no)
|
||||||
|
if short_url and short_url != result:
|
||||||
|
print(f"柠檬支付自建短链接生成成功: {len(short_url)} 字符")
|
||||||
|
return True, short_url, None
|
||||||
|
else:
|
||||||
|
print("柠檬支付短链接生成失败,使用原链接")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"柠檬支付短链接处理异常: {e}")
|
||||||
|
|
||||||
|
return success, result if success else None, None if success else result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"支付创建失败: {str(e)}"
|
||||||
|
print(f"❌ {error_msg}")
|
||||||
|
return False, None, error_msg
|
||||||
|
|
||||||
|
|
||||||
|
def verify_callback_signature(ch: dict, params: dict) -> bool:
|
||||||
|
"""
|
||||||
|
验证支付回调签名
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ch: 支付通道配置
|
||||||
|
params: 回调参数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 签名验证结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if ch.get('route') == '/pay/token188':
|
||||||
|
# TOKEN188 USDT签名验证
|
||||||
|
if 'sign' not in params:
|
||||||
|
return False
|
||||||
|
|
||||||
|
received_sign = params['sign']
|
||||||
|
calculated_sign = md5_sign_token188(params, ch['key'])
|
||||||
|
|
||||||
|
return received_sign.upper() == calculated_sign.upper()
|
||||||
|
else:
|
||||||
|
# 柠檬支付签名验证
|
||||||
|
return verify_lemzf_callback(ch, params)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 签名验证失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 向后兼容的函数别名
|
||||||
|
def md5_sign(params: dict, key: str) -> str:
|
||||||
|
"""向后兼容的MD5签名函数 - 使用柠檬支付标准算法"""
|
||||||
|
from payments_lemzf_official import LemzfPayment
|
||||||
|
|
||||||
|
# 创建临时实例进行签名计算
|
||||||
|
temp_lemzf = LemzfPayment("", key)
|
||||||
|
return temp_lemzf.md5_sign(params)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试代码
|
||||||
|
print("支付系统模块加载成功")
|
||||||
|
print("- 柠檬支付:支付宝、微信、USDT柠檬")
|
||||||
|
print("- TOKEN188:USDT(TRC20)")
|
||||||
|
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 授权检查 - 请勿删除此部分,否则程序无法运行
|
||||||
|
import _auth_check
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
柠檬支付官方标准对接模块
|
||||||
|
严格按照官方文档 https://api.lemzf.com/doc.html 实现
|
||||||
|
支持页面跳转支付和API接口支付
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
from typing import Dict, Any, Optional, Tuple
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
|
||||||
|
class LemzfPayment:
|
||||||
|
"""柠檬支付官方标准对接类"""
|
||||||
|
|
||||||
|
def __init__(self, merchant_id: str, key: str, gateway: str = None, api_gateway: str = None):
|
||||||
|
"""
|
||||||
|
初始化柠檬支付
|
||||||
|
|
||||||
|
Args:
|
||||||
|
merchant_id: 商户ID
|
||||||
|
key: 商户密钥
|
||||||
|
gateway: 页面跳转网关 (submit.php)
|
||||||
|
api_gateway: API接口网关 (mapi.php)
|
||||||
|
"""
|
||||||
|
self.merchant_id = merchant_id
|
||||||
|
self.key = key
|
||||||
|
self.gateway = gateway or "https://a1004a.lempay.com/submit.php"
|
||||||
|
self.api_gateway = api_gateway or "https://a1004a.lempay.com/mapi.php"
|
||||||
|
|
||||||
|
def md5_sign(self, params: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
MD5签名算法 - 严格按照官方文档实现
|
||||||
|
|
||||||
|
1. 参数按ASCII码从小到大排序
|
||||||
|
2. 排除sign、sign_type、值为空或0的参数
|
||||||
|
3. 拼接为a=b&c=d格式
|
||||||
|
4. 末尾拼接KEY,进行MD5加密,结果小写
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: 参数字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: MD5签名
|
||||||
|
"""
|
||||||
|
# 过滤参数:排除sign、sign_type、值为空或0的参数
|
||||||
|
filtered_params = {}
|
||||||
|
for k, v in params.items():
|
||||||
|
if k in ('sign', 'sign_type'):
|
||||||
|
continue
|
||||||
|
if v is None or v == '' or v == 0 or str(v) == '0':
|
||||||
|
continue
|
||||||
|
filtered_params[k] = v
|
||||||
|
|
||||||
|
# 按ASCII码排序
|
||||||
|
sorted_params = sorted(filtered_params.items())
|
||||||
|
|
||||||
|
# 拼接为URL键值对格式
|
||||||
|
param_str = '&'.join([f"{k}={v}" for k, v in sorted_params])
|
||||||
|
|
||||||
|
# 拼接商户密钥
|
||||||
|
sign_str = param_str + self.key
|
||||||
|
|
||||||
|
# MD5加密,结果小写
|
||||||
|
return hashlib.md5(sign_str.encode('utf-8')).hexdigest().lower()
|
||||||
|
|
||||||
|
def create_page_payment(self, order_id: str, amount: float, subject: str,
|
||||||
|
notify_url: str, return_url: str = None,
|
||||||
|
payment_type: str = None, device: str = "mobile") -> str:
|
||||||
|
"""
|
||||||
|
创建页面跳转支付链接
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_id: 商户订单号
|
||||||
|
amount: 支付金额
|
||||||
|
subject: 订单标题
|
||||||
|
notify_url: 异步通知地址
|
||||||
|
return_url: 同步跳转地址
|
||||||
|
payment_type: 支付方式 (alipay/wxpay/usdt等)
|
||||||
|
device: 设备类型 (mobile/pc)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 支付链接
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
'pid': self.merchant_id,
|
||||||
|
'type': payment_type,
|
||||||
|
'out_trade_no': order_id,
|
||||||
|
'notify_url': notify_url,
|
||||||
|
'name': subject,
|
||||||
|
'money': f"{amount:.2f}",
|
||||||
|
'device': device
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加return_url(如果提供)
|
||||||
|
if return_url:
|
||||||
|
params['return_url'] = return_url
|
||||||
|
|
||||||
|
# 生成签名
|
||||||
|
params['sign'] = self.md5_sign(params)
|
||||||
|
params['sign_type'] = 'MD5'
|
||||||
|
|
||||||
|
# 构建支付链接
|
||||||
|
query_string = urlencode(params)
|
||||||
|
return f"{self.gateway}?{query_string}"
|
||||||
|
|
||||||
|
def create_api_payment(self, order_id: str, amount: float, subject: str,
|
||||||
|
notify_url: str, payment_type: str, device: str = "mobile",
|
||||||
|
client_ip: str = "127.0.0.1") -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
创建API接口支付
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_id: 商户订单号
|
||||||
|
amount: 支付金额
|
||||||
|
subject: 订单标题
|
||||||
|
notify_url: 异步通知地址
|
||||||
|
payment_type: 支付方式
|
||||||
|
device: 设备类型
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: API响应结果
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
'pid': self.merchant_id,
|
||||||
|
'type': payment_type,
|
||||||
|
'out_trade_no': order_id,
|
||||||
|
'notify_url': notify_url,
|
||||||
|
'name': subject,
|
||||||
|
'money': f"{amount:.2f}",
|
||||||
|
'device': device,
|
||||||
|
'clientip': client_ip
|
||||||
|
}
|
||||||
|
|
||||||
|
# 生成签名
|
||||||
|
params['sign'] = self.md5_sign(params)
|
||||||
|
params['sign_type'] = 'MD5'
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 发送POST请求
|
||||||
|
response = requests.post(self.api_gateway, data=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# 解析JSON响应
|
||||||
|
result = response.json()
|
||||||
|
return result
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return {
|
||||||
|
'code': -1,
|
||||||
|
'msg': f'网络请求失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}
|
||||||
|
except ValueError as e:
|
||||||
|
return {
|
||||||
|
'code': -1,
|
||||||
|
'msg': f'响应解析失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}
|
||||||
|
|
||||||
|
def verify_callback(self, params: Dict[str, Any]) -> bool:
|
||||||
|
"""
|
||||||
|
验证回调签名
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: 回调参数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 签名是否有效
|
||||||
|
"""
|
||||||
|
if 'sign' not in params:
|
||||||
|
return False
|
||||||
|
|
||||||
|
received_sign = params['sign']
|
||||||
|
calculated_sign = self.md5_sign(params)
|
||||||
|
|
||||||
|
return received_sign.lower() == calculated_sign.lower()
|
||||||
|
|
||||||
|
def query_order(self, out_trade_no: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
查询单个订单
|
||||||
|
|
||||||
|
Args:
|
||||||
|
out_trade_no: 商户订单号
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 查询结果
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
'pid': self.merchant_id,
|
||||||
|
'out_trade_no': out_trade_no
|
||||||
|
}
|
||||||
|
|
||||||
|
sign = self.md5_sign(params)
|
||||||
|
query_url = f"https://a1004a.lempay.com/api.php?act=order&pid={self.merchant_id}&out_trade_no={out_trade_no}&sign={sign}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(query_url, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'code': -1,
|
||||||
|
'msg': f'查询失败: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_lemzf_payment(config: Dict[str, Any]) -> LemzfPayment:
|
||||||
|
"""
|
||||||
|
创建柠檬支付实例的工厂函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: 支付配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LemzfPayment: 柠檬支付实例
|
||||||
|
"""
|
||||||
|
return LemzfPayment(
|
||||||
|
merchant_id=config['merchant_id'],
|
||||||
|
key=config['key'],
|
||||||
|
gateway=config.get('gateway'),
|
||||||
|
api_gateway=config.get('api_gateway')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_payment(config: Dict[str, Any], order_id: str, amount: float,
|
||||||
|
subject: str, notify_url: str, return_url: str = None,
|
||||||
|
client_ip: str = "127.0.0.1") -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
创建柠檬支付订单 - 兼容原有接口
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: 支付配置
|
||||||
|
order_id: 订单号
|
||||||
|
amount: 金额
|
||||||
|
subject: 标题
|
||||||
|
notify_url: 通知地址
|
||||||
|
return_url: 返回地址
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, str]: (是否成功, 支付链接或错误信息)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
lemzf = create_lemzf_payment(config)
|
||||||
|
|
||||||
|
# 获取支付方式
|
||||||
|
payment_type = config.get('type', 'alipay')
|
||||||
|
device = config.get('device', 'mobile')
|
||||||
|
|
||||||
|
# 优先使用API接口支付
|
||||||
|
if config.get('api_gateway'):
|
||||||
|
result = lemzf.create_api_payment(
|
||||||
|
order_id=order_id,
|
||||||
|
amount=amount,
|
||||||
|
subject=subject,
|
||||||
|
notify_url=notify_url,
|
||||||
|
payment_type=payment_type,
|
||||||
|
device=device,
|
||||||
|
client_ip=client_ip
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.get('code') == 1:
|
||||||
|
data = result.get('data', result)
|
||||||
|
# 优先使用官方短链接 (cashier.php)
|
||||||
|
payurl = data.get('payurl', '')
|
||||||
|
qrcode = data.get('qrcode', '')
|
||||||
|
urlscheme = data.get('urlscheme', '')
|
||||||
|
|
||||||
|
# 优先级:cashier.php短链接 > 其他payurl > qrcode > urlscheme
|
||||||
|
if payurl and 'cashier.php' in payurl:
|
||||||
|
print(f"✅ 使用官方短链接: {len(payurl)} 字符")
|
||||||
|
return True, payurl
|
||||||
|
elif payurl:
|
||||||
|
print(f"✅ 使用官方支付链接: {len(payurl)} 字符")
|
||||||
|
return True, payurl
|
||||||
|
elif qrcode:
|
||||||
|
print(f"✅ 使用官方二维码链接: {len(qrcode)} 字符")
|
||||||
|
return True, qrcode
|
||||||
|
elif urlscheme:
|
||||||
|
print(f"✅ 使用原生协议链接: {len(urlscheme)} 字符")
|
||||||
|
return True, urlscheme
|
||||||
|
|
||||||
|
# API失败时记录错误但继续尝试页面跳转
|
||||||
|
print(f"⚠️ API支付失败: {result.get('msg', '未知错误')}")
|
||||||
|
|
||||||
|
# 使用页面跳转支付作为备用方案
|
||||||
|
payment_url = lemzf.create_page_payment(
|
||||||
|
order_id=order_id,
|
||||||
|
amount=amount,
|
||||||
|
subject=subject,
|
||||||
|
notify_url=notify_url,
|
||||||
|
return_url=return_url,
|
||||||
|
payment_type=payment_type,
|
||||||
|
device=device
|
||||||
|
)
|
||||||
|
|
||||||
|
return True, payment_url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"柠檬支付创建失败: {str(e)}"
|
||||||
|
print(f"❌ {error_msg}")
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
|
|
||||||
|
def verify_lemzf_callback(config: Dict[str, Any], params: Dict[str, Any]) -> bool:
|
||||||
|
"""
|
||||||
|
验证柠檬支付回调 - 兼容原有接口
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: 支付配置
|
||||||
|
params: 回调参数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 验证是否通过
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
lemzf = create_lemzf_payment(config)
|
||||||
|
return lemzf.verify_callback(params)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 柠檬支付回调验证失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 支付方式映射
|
||||||
|
LEMZF_PAYMENT_TYPES = {
|
||||||
|
'alipay': 'alipay', # 支付宝
|
||||||
|
'wxpay': 'wxpay', # 微信支付
|
||||||
|
'usdt': 'usdt', # USDT
|
||||||
|
'qqpay': 'qqpay', # QQ钱包
|
||||||
|
'bank': 'bank', # 网银支付
|
||||||
|
}
|
||||||
|
|
||||||
|
# 设备类型映射
|
||||||
|
LEMZF_DEVICE_TYPES = {
|
||||||
|
'mobile': 'mobile', # 手机
|
||||||
|
'pc': 'pc', # 电脑
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试代码
|
||||||
|
config = {
|
||||||
|
'merchant_id': '1506',
|
||||||
|
'key': 'test_key',
|
||||||
|
'gateway': 'https://66101506.lemzf.com/submit.php',
|
||||||
|
'api_gateway': 'https://66101506.lemzf.com/mapi.php',
|
||||||
|
'type': 'alipay',
|
||||||
|
'device': 'mobile'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试创建支付
|
||||||
|
success, result = create_payment(
|
||||||
|
config=config,
|
||||||
|
order_id='TEST001',
|
||||||
|
amount=99.99,
|
||||||
|
subject='测试订单',
|
||||||
|
notify_url='https://example.com/notify'
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"创建支付: {'成功' if success else '失败'}")
|
||||||
|
print(f"结果: {result}")
|
||||||
|
|
||||||
|
# 测试签名验证
|
||||||
|
test_params = {
|
||||||
|
'pid': '1506',
|
||||||
|
'trade_no': '2024100400001',
|
||||||
|
'out_trade_no': 'TEST001',
|
||||||
|
'type': 'alipay',
|
||||||
|
'name': '测试订单',
|
||||||
|
'money': '99.99',
|
||||||
|
'trade_status': 'TRADE_SUCCESS',
|
||||||
|
'sign': 'test_sign'
|
||||||
|
}
|
||||||
|
|
||||||
|
lemzf = create_lemzf_payment(config)
|
||||||
|
calculated_sign = lemzf.md5_sign(test_params)
|
||||||
|
print(f"计算签名: {calculated_sign}")
|
||||||
|
|
||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 授权检查 - 请勿删除此部分,否则程序无法运行
|
||||||
|
import _auth_check
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
频率限制模块
|
||||||
|
防止恶意刷单、暴力攻击等
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
from redis_cache import cache
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimiter:
|
||||||
|
"""频率限制器"""
|
||||||
|
|
||||||
|
# 限制规则配置
|
||||||
|
RULES = {
|
||||||
|
# 用户操作限制
|
||||||
|
'user_command': {'limit': 20, 'window': 60, 'desc': '命令操作'},
|
||||||
|
'user_payment': {'limit': 5, 'window': 300, 'desc': '创建订单'},
|
||||||
|
'user_query': {'limit': 10, 'window': 60, 'desc': '查询订单'},
|
||||||
|
|
||||||
|
# IP限制
|
||||||
|
'ip_callback': {'limit': 100, 'window': 60, 'desc': '支付回调'},
|
||||||
|
'ip_request': {'limit': 200, 'window': 60, 'desc': 'HTTP请求'},
|
||||||
|
|
||||||
|
# 全局限制
|
||||||
|
'global_order': {'limit': 1000, 'window': 60, 'desc': '全局订单创建'},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = cache.enabled
|
||||||
|
|
||||||
|
def check_rate_limit(self, key: str, rule_name: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""
|
||||||
|
检查频率限制
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 限制对象标识(如user_id, ip等)
|
||||||
|
rule_name: 规则名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(是否允许, 错误消息)
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
rule = self.RULES.get(rule_name)
|
||||||
|
if not rule:
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
cache_key = f"rate_limit:{rule_name}:{key}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取当前计数
|
||||||
|
current = cache.get(cache_key)
|
||||||
|
|
||||||
|
if current is None:
|
||||||
|
# 首次访问,初始化计数器
|
||||||
|
cache.set(cache_key, {'count': 1, 'start_time': int(time.time())}, rule['window'])
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
# 检查时间窗口
|
||||||
|
elapsed = int(time.time()) - current['start_time']
|
||||||
|
|
||||||
|
if elapsed > rule['window']:
|
||||||
|
# 时间窗口已过,重置计数器
|
||||||
|
cache.set(cache_key, {'count': 1, 'start_time': int(time.time())}, rule['window'])
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
# 在时间窗口内,检查计数
|
||||||
|
if current['count'] >= rule['limit']:
|
||||||
|
# 超过限制
|
||||||
|
remaining = rule['window'] - elapsed
|
||||||
|
error_msg = f"⚠️ {rule['desc']}过于频繁,请 {remaining} 秒后再试"
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
|
# 未超过限制,增加计数
|
||||||
|
current['count'] += 1
|
||||||
|
cache.set(cache_key, current, rule['window'])
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 频率限制检查失败: {e}")
|
||||||
|
# 出错时放行,避免影响正常业务
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
def get_remaining_quota(self, key: str, rule_name: str) -> dict:
|
||||||
|
"""
|
||||||
|
获取剩余配额
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'used': 已使用次数, 'limit': 限制次数, 'remaining': 剩余次数, 'reset_in': 重置时间(秒)}
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
return {'used': 0, 'limit': 999, 'remaining': 999, 'reset_in': 0}
|
||||||
|
|
||||||
|
rule = self.RULES.get(rule_name)
|
||||||
|
if not rule:
|
||||||
|
return {'used': 0, 'limit': 999, 'remaining': 999, 'reset_in': 0}
|
||||||
|
|
||||||
|
cache_key = f"rate_limit:{rule_name}:{key}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
current = cache.get(cache_key)
|
||||||
|
|
||||||
|
if current is None:
|
||||||
|
return {
|
||||||
|
'used': 0,
|
||||||
|
'limit': rule['limit'],
|
||||||
|
'remaining': rule['limit'],
|
||||||
|
'reset_in': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed = int(time.time()) - current['start_time']
|
||||||
|
reset_in = max(0, rule['window'] - elapsed)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'used': current['count'],
|
||||||
|
'limit': rule['limit'],
|
||||||
|
'remaining': max(0, rule['limit'] - current['count']),
|
||||||
|
'reset_in': reset_in
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return {'used': 0, 'limit': 999, 'remaining': 999, 'reset_in': 0}
|
||||||
|
|
||||||
|
def reset_limit(self, key: str, rule_name: str):
|
||||||
|
"""重置限制(管理员功能)"""
|
||||||
|
cache_key = f"rate_limit:{rule_name}:{key}"
|
||||||
|
cache.delete(cache_key)
|
||||||
|
|
||||||
|
|
||||||
|
# 全局限制器实例
|
||||||
|
rate_limiter = RateLimiter()
|
||||||
|
|
||||||
|
|
||||||
|
# 装饰器:用户命令限制
|
||||||
|
def rate_limit_user_command(func):
|
||||||
|
"""用户命令频率限制装饰器"""
|
||||||
|
async def wrapper(update, context, *args, **kwargs):
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
|
||||||
|
allowed, error_msg = rate_limiter.check_rate_limit(str(user_id), 'user_command')
|
||||||
|
|
||||||
|
if not allowed:
|
||||||
|
try:
|
||||||
|
await update.message.reply_text(error_msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
return await func(update, context, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
# 装饰器:用户支付限制
|
||||||
|
def rate_limit_user_payment(func):
|
||||||
|
"""用户支付频率限制装饰器"""
|
||||||
|
async def wrapper(update, context, *args, **kwargs):
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
|
||||||
|
allowed, error_msg = rate_limiter.check_rate_limit(str(user_id), 'user_payment')
|
||||||
|
|
||||||
|
if not allowed:
|
||||||
|
try:
|
||||||
|
await update.callback_query.answer(error_msg, show_alert=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
return await func(update, context, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
# IP限制检查
|
||||||
|
def check_ip_rate_limit(ip: str, rule_name: str = 'ip_request') -> Tuple[bool, Optional[str]]:
|
||||||
|
"""检查IP频率限制"""
|
||||||
|
return rate_limiter.check_rate_limit(ip, rule_name)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试频率限制
|
||||||
|
print("测试频率限制...")
|
||||||
|
|
||||||
|
# 测试用户命令限制
|
||||||
|
for i in range(25):
|
||||||
|
allowed, msg = rate_limiter.check_rate_limit("test_user_123", "user_command")
|
||||||
|
print(f"第{i+1}次请求: {'✅ 允许' if allowed else f'❌ 拒绝 - {msg}'}")
|
||||||
|
|
||||||
|
if not allowed:
|
||||||
|
# 查看剩余配额
|
||||||
|
quota = rate_limiter.get_remaining_quota("test_user_123", "user_command")
|
||||||
|
print(f"配额信息: {quota}")
|
||||||
|
break
|
||||||
|
|
||||||
|
print("\n✅ 频率限制测试完成")
|
||||||
|
|
||||||
+291
@@ -0,0 +1,291 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 授权检查 - 请勿删除此部分,否则程序无法运行
|
||||||
|
import _auth_check
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Redis缓存模块
|
||||||
|
提供商品信息、配置、用户会话等数据的缓存功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import redis
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Any, Optional
|
||||||
|
from functools import wraps
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Redis连接配置
|
||||||
|
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
||||||
|
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
||||||
|
REDIS_DB = int(os.getenv('REDIS_DB', 0))
|
||||||
|
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', None)
|
||||||
|
|
||||||
|
# 缓存过期时间配置(秒)
|
||||||
|
CACHE_TTL = {
|
||||||
|
'product': 300, # 商品信息:5分钟
|
||||||
|
'config': 600, # 配置信息:10分钟
|
||||||
|
'user_session': 3600, # 用户会话:1小时
|
||||||
|
'rate_limit': 60, # 频率限制:1分钟
|
||||||
|
'stock': 30, # 库存信息:30秒
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RedisCache:
|
||||||
|
"""Redis缓存管理类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = False
|
||||||
|
self.client = None
|
||||||
|
self._connect()
|
||||||
|
|
||||||
|
def _connect(self):
|
||||||
|
"""连接Redis"""
|
||||||
|
try:
|
||||||
|
self.client = redis.Redis(
|
||||||
|
host=REDIS_HOST,
|
||||||
|
port=REDIS_PORT,
|
||||||
|
db=REDIS_DB,
|
||||||
|
password=REDIS_PASSWORD,
|
||||||
|
decode_responses=True,
|
||||||
|
socket_connect_timeout=3,
|
||||||
|
socket_timeout=3,
|
||||||
|
retry_on_timeout=True,
|
||||||
|
health_check_interval=30
|
||||||
|
)
|
||||||
|
# 测试连接
|
||||||
|
self.client.ping()
|
||||||
|
self.enabled = True
|
||||||
|
print(f"✅ Redis连接成功: {REDIS_HOST}:{REDIS_PORT}")
|
||||||
|
except Exception as e:
|
||||||
|
self.enabled = False
|
||||||
|
print(f"⚠️ Redis连接失败,缓存功能已禁用: {e}")
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[Any]:
|
||||||
|
"""获取缓存"""
|
||||||
|
if not self.enabled:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = self.client.get(key)
|
||||||
|
if value:
|
||||||
|
return json.loads(value)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Redis GET失败: {key}, {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, key: str, value: Any, ttl: int = None) -> bool:
|
||||||
|
"""设置缓存"""
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
json_value = json.dumps(value, ensure_ascii=False)
|
||||||
|
if ttl:
|
||||||
|
self.client.setex(key, ttl, json_value)
|
||||||
|
else:
|
||||||
|
self.client.set(key, json_value)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Redis SET失败: {key}, {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete(self, key: str) -> bool:
|
||||||
|
"""删除缓存"""
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client.delete(key)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Redis DELETE失败: {key}, {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def exists(self, key: str) -> bool:
|
||||||
|
"""检查key是否存在"""
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.client.exists(key) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def incr(self, key: str, amount: int = 1) -> Optional[int]:
|
||||||
|
"""递增计数器"""
|
||||||
|
if not self.enabled:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.client.incrby(key, amount)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Redis INCR失败: {key}, {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def expire(self, key: str, ttl: int) -> bool:
|
||||||
|
"""设置过期时间"""
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.client.expire(key, ttl)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ttl(self, key: str) -> int:
|
||||||
|
"""获取剩余过期时间"""
|
||||||
|
if not self.enabled:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.client.ttl(key)
|
||||||
|
except Exception:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
# 全局缓存实例
|
||||||
|
cache = RedisCache()
|
||||||
|
|
||||||
|
|
||||||
|
# 缓存装饰器
|
||||||
|
def cached(key_prefix: str, ttl: int = 300):
|
||||||
|
"""
|
||||||
|
缓存装饰器
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
@cached('product', ttl=300)
|
||||||
|
def get_product(pid):
|
||||||
|
return db.query(...)
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# 生成缓存key
|
||||||
|
cache_key = f"{key_prefix}:{':'.join(map(str, args))}"
|
||||||
|
|
||||||
|
# 尝试从缓存获取
|
||||||
|
cached_value = cache.get(cache_key)
|
||||||
|
if cached_value is not None:
|
||||||
|
return cached_value
|
||||||
|
|
||||||
|
# 缓存未命中,执行函数
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
|
||||||
|
# 写入缓存
|
||||||
|
if result is not None:
|
||||||
|
cache.set(cache_key, result, ttl)
|
||||||
|
|
||||||
|
return result
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
# 商品缓存
|
||||||
|
def get_product_cached(cur, pid: str):
|
||||||
|
"""获取商品信息(带缓存)"""
|
||||||
|
cache_key = f"product:{pid}"
|
||||||
|
|
||||||
|
# 尝试从缓存获取
|
||||||
|
cached = cache.get(cache_key)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
# 缓存未命中,查询数据库
|
||||||
|
try:
|
||||||
|
row = cur.execute(
|
||||||
|
"SELECT id, name, price, cover_url, full_description, status FROM products WHERE id=?",
|
||||||
|
(pid,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
product = {
|
||||||
|
'id': row[0],
|
||||||
|
'name': row[1],
|
||||||
|
'price': row[2],
|
||||||
|
'cover_url': row[3],
|
||||||
|
'full_description': row[4],
|
||||||
|
'status': row[5]
|
||||||
|
}
|
||||||
|
# 写入缓存
|
||||||
|
cache.set(cache_key, product, CACHE_TTL['product'])
|
||||||
|
return product
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 查询商品失败: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_product_cache(pid: str):
|
||||||
|
"""清除商品缓存"""
|
||||||
|
cache.delete(f"product:{pid}")
|
||||||
|
|
||||||
|
|
||||||
|
# 配置缓存
|
||||||
|
def get_setting_cached(cur, key: str, default: str = "") -> str:
|
||||||
|
"""获取配置(带缓存)"""
|
||||||
|
cache_key = f"setting:{key}"
|
||||||
|
|
||||||
|
# 尝试从缓存获取
|
||||||
|
cached = cache.get(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
# 缓存未命中,查询数据库
|
||||||
|
try:
|
||||||
|
row = cur.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
|
||||||
|
value = row[0] if row else default
|
||||||
|
|
||||||
|
# 写入缓存
|
||||||
|
cache.set(cache_key, value, CACHE_TTL['config'])
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_setting_cache(key: str):
|
||||||
|
"""清除配置缓存"""
|
||||||
|
cache.delete(f"setting:{key}")
|
||||||
|
|
||||||
|
|
||||||
|
# 用户会话缓存
|
||||||
|
def set_user_session(user_id: int, data: dict, ttl: int = None):
|
||||||
|
"""设置用户会话数据"""
|
||||||
|
cache_key = f"session:{user_id}"
|
||||||
|
cache.set(cache_key, data, ttl or CACHE_TTL['user_session'])
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_session(user_id: int) -> Optional[dict]:
|
||||||
|
"""获取用户会话数据"""
|
||||||
|
cache_key = f"session:{user_id}"
|
||||||
|
return cache.get(cache_key)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_user_session(user_id: int):
|
||||||
|
"""清除用户会话"""
|
||||||
|
cache.delete(f"session:{user_id}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试Redis连接
|
||||||
|
print("测试Redis连接...")
|
||||||
|
print(f"Redis状态: {'✅ 已启用' if cache.enabled else '❌ 已禁用'}")
|
||||||
|
|
||||||
|
if cache.enabled:
|
||||||
|
# 测试基本操作
|
||||||
|
cache.set("test_key", {"hello": "world"}, 10)
|
||||||
|
value = cache.get("test_key")
|
||||||
|
print(f"测试读写: {value}")
|
||||||
|
|
||||||
|
# 测试计数器
|
||||||
|
count = cache.incr("test_counter")
|
||||||
|
print(f"测试计数器: {count}")
|
||||||
|
|
||||||
|
# 清理测试数据
|
||||||
|
cache.delete("test_key")
|
||||||
|
cache.delete("test_counter")
|
||||||
|
print("✅ Redis测试通过")
|
||||||
|
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
python-telegram-bot[job-queue,webhooks]==20.6
|
||||||
|
Flask==3.0.3
|
||||||
|
requests==2.31.0
|
||||||
|
qrcode==7.4.2
|
||||||
|
Pillow==10.2.0
|
||||||
|
waitress==2.1.2
|
||||||
|
selenium==4.15.0
|
||||||
|
webdriver-manager==4.0.1
|
||||||
|
redis==5.0.1
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 授权检查 - 请勿删除此部分,否则程序无法运行
|
||||||
|
import _auth_check
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
支付页面截图工具
|
||||||
|
支持真实网页截图和备用二维码生成
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# 尝试导入Selenium相关模块
|
||||||
|
try:
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.chrome.options import Options
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
SELENIUM_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
SELENIUM_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
def setup_chrome_driver(headless: bool = True, timeout: int = 30):
|
||||||
|
"""
|
||||||
|
设置Chrome/Chromium WebDriver
|
||||||
|
|
||||||
|
Args:
|
||||||
|
headless: 是否使用无头模式
|
||||||
|
timeout: 页面加载超时时间
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
WebDriver实例或None
|
||||||
|
"""
|
||||||
|
if not SELENIUM_AVAILABLE:
|
||||||
|
print("❌ Selenium不可用,使用备用二维码方案")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Chrome选项配置
|
||||||
|
chrome_options = Options()
|
||||||
|
|
||||||
|
if headless:
|
||||||
|
chrome_options.add_argument('--headless')
|
||||||
|
|
||||||
|
# 基础配置
|
||||||
|
chrome_options.add_argument('--no-sandbox')
|
||||||
|
chrome_options.add_argument('--disable-dev-shm-usage')
|
||||||
|
chrome_options.add_argument('--disable-gpu')
|
||||||
|
chrome_options.add_argument('--window-size=1920,1080')
|
||||||
|
chrome_options.add_argument('--disable-extensions')
|
||||||
|
chrome_options.add_argument('--disable-plugins')
|
||||||
|
chrome_options.add_argument('--disable-web-security')
|
||||||
|
chrome_options.add_argument('--allow-running-insecure-content')
|
||||||
|
chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
|
||||||
|
|
||||||
|
# 尝试不同的Chrome/Chromium路径
|
||||||
|
chrome_paths = [
|
||||||
|
'/usr/bin/chromium-browser', # Alpine Chromium
|
||||||
|
'/usr/bin/chromium', # Debian Chromium
|
||||||
|
'/usr/bin/google-chrome', # Google Chrome
|
||||||
|
'/usr/bin/google-chrome-stable',
|
||||||
|
'chromium-browser',
|
||||||
|
'chromium',
|
||||||
|
'google-chrome'
|
||||||
|
]
|
||||||
|
|
||||||
|
chrome_binary = None
|
||||||
|
for path in chrome_paths:
|
||||||
|
try:
|
||||||
|
result = subprocess.run([path, '--version'],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
chrome_binary = path
|
||||||
|
print(f"✅ 找到浏览器: {path} - {result.stdout.strip()}")
|
||||||
|
break
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not chrome_binary:
|
||||||
|
print("❌ 未找到Chrome/Chromium浏览器,使用备用二维码方案")
|
||||||
|
return None
|
||||||
|
|
||||||
|
chrome_options.binary_location = chrome_binary
|
||||||
|
|
||||||
|
# 尝试使用系统chromedriver或chromium-driver
|
||||||
|
driver_paths = [
|
||||||
|
'/usr/bin/chromedriver', # Alpine chromedriver
|
||||||
|
'/usr/bin/chromium-chromedriver', # Alpine chromium-chromedriver
|
||||||
|
'/usr/bin/chromium-driver', # Debian chromium-driver
|
||||||
|
'chromedriver',
|
||||||
|
'chromium-driver'
|
||||||
|
]
|
||||||
|
|
||||||
|
driver = None
|
||||||
|
for driver_path in driver_paths:
|
||||||
|
try:
|
||||||
|
if os.path.exists(driver_path) or driver_path in ['chromedriver', 'chromium-driver']:
|
||||||
|
service = Service(driver_path) if os.path.exists(driver_path) else None
|
||||||
|
driver = webdriver.Chrome(service=service, options=chrome_options)
|
||||||
|
print(f"✅ 使用驱动: {driver_path}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not driver:
|
||||||
|
# 最后尝试ChromeDriverManager
|
||||||
|
try:
|
||||||
|
service = Service(ChromeDriverManager().install())
|
||||||
|
driver = webdriver.Chrome(service=service, options=chrome_options)
|
||||||
|
print("✅ 使用ChromeDriverManager")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 所有驱动方式都失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 设置超时
|
||||||
|
driver.set_page_load_timeout(timeout)
|
||||||
|
driver.implicitly_wait(10)
|
||||||
|
|
||||||
|
return driver
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 浏览器驱动初始化失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def capture_payment_qr(payment_url: str, timeout: int = 30) -> Optional[BytesIO]:
|
||||||
|
"""
|
||||||
|
截取支付页面的二维码图片
|
||||||
|
|
||||||
|
Args:
|
||||||
|
payment_url: 支付链接
|
||||||
|
timeout: 超时时间(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BytesIO: 图片数据流,失败返回None
|
||||||
|
"""
|
||||||
|
if not SELENIUM_AVAILABLE:
|
||||||
|
print("❌ Selenium不可用,跳过真实截图")
|
||||||
|
return None
|
||||||
|
|
||||||
|
driver = None
|
||||||
|
try:
|
||||||
|
driver = setup_chrome_driver()
|
||||||
|
if not driver:
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"🔧 正在截取支付页面: {payment_url}")
|
||||||
|
driver.get(payment_url)
|
||||||
|
|
||||||
|
# 等待页面基础加载完成
|
||||||
|
wait = WebDriverWait(driver, timeout)
|
||||||
|
|
||||||
|
# 1. 等待页面DOM加载完成
|
||||||
|
try:
|
||||||
|
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||||
|
print("✅ 页面DOM加载完成")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 等待DOM加载超时: {e}")
|
||||||
|
|
||||||
|
# 2. 等待页面标题加载(确保不是空白页)
|
||||||
|
try:
|
||||||
|
wait.until(lambda d: d.title and len(d.title.strip()) > 0)
|
||||||
|
print(f"✅ 页面标题: {driver.title}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 页面标题加载超时: {e}")
|
||||||
|
|
||||||
|
# 3. 等待页面body内容出现
|
||||||
|
try:
|
||||||
|
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||||
|
print("✅ 页面内容加载完成")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 页面内容加载超时: {e}")
|
||||||
|
|
||||||
|
# 4. 额外等待确保所有内容加载完成
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# 5. 截取页面中心区域(支付核心部分)
|
||||||
|
print("📸 开始截图...")
|
||||||
|
|
||||||
|
# 先获取整个页面截图
|
||||||
|
screenshot_data = driver.get_screenshot_as_png()
|
||||||
|
|
||||||
|
if screenshot_data:
|
||||||
|
# 使用PIL裁剪中心区域
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
# 将截图数据转换为PIL Image
|
||||||
|
full_image = Image.open(BytesIO(screenshot_data))
|
||||||
|
width, height = full_image.size
|
||||||
|
# 336x375矩形截图 - 左右减7,上面减10,下面加25
|
||||||
|
crop_width = 336
|
||||||
|
crop_height = 375
|
||||||
|
|
||||||
|
print(f"🔍 原始截图尺寸: {width}x{height}")
|
||||||
|
|
||||||
|
# 使用测试成功的简单居中策略,往上偏移避开蓝色按钮
|
||||||
|
center_x = width // 2
|
||||||
|
center_y = height // 2 - 8 # 往上偏移8像素,整体下移15像素
|
||||||
|
|
||||||
|
left = center_x - crop_width // 2 # 336/2 = 168
|
||||||
|
top = center_y - crop_height // 2 # 375/2 = 187
|
||||||
|
right = left + crop_width
|
||||||
|
bottom = top + crop_height
|
||||||
|
# 边界检查
|
||||||
|
if left < 0 or top < 0 or right > width or bottom > height:
|
||||||
|
print('⚠️ 336x375超出边界,使用最大正方形')
|
||||||
|
size = min(width, height)
|
||||||
|
left = (width - size) // 2
|
||||||
|
top = (height - size) // 2
|
||||||
|
right = left + size
|
||||||
|
bottom = top + size
|
||||||
|
|
||||||
|
print(f"✅ 居中裁剪336x375: {left},{top} -> {right},{bottom}")
|
||||||
|
print(f"✅ 裁剪尺寸: {right-left}x{bottom-top}")
|
||||||
|
|
||||||
|
# 裁剪图片
|
||||||
|
cropped_image = full_image.crop((left, top, right, bottom))
|
||||||
|
|
||||||
|
# 转换回BytesIO
|
||||||
|
cropped_buffer = BytesIO()
|
||||||
|
cropped_image.save(cropped_buffer, format='PNG')
|
||||||
|
cropped_buffer.seek(0)
|
||||||
|
|
||||||
|
print(f"✅ 真实截图成功,原始大小: {len(screenshot_data)} bytes")
|
||||||
|
print(f"✅ 裁剪后大小: {len(cropped_buffer.getvalue())} bytes")
|
||||||
|
print(f"✅ 裁剪区域: 390x390 (以二维码为中心)")
|
||||||
|
|
||||||
|
return cropped_buffer
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 图片裁剪失败,使用原始截图: {e}")
|
||||||
|
screenshot_buffer = BytesIO(screenshot_data)
|
||||||
|
return screenshot_buffer
|
||||||
|
else:
|
||||||
|
print("❌ 截图数据为空")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 真实截图失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if driver:
|
||||||
|
try:
|
||||||
|
driver.quit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def capture_payment_qr_fallback(payment_url: str) -> Optional[BytesIO]:
|
||||||
|
"""
|
||||||
|
备用截图方案:使用qrcode生成支付链接二维码
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import qrcode
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
print(f"🔧 开始生成备用二维码,URL: {payment_url}")
|
||||||
|
|
||||||
|
# 生成二维码
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=1,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||||
|
box_size=10,
|
||||||
|
border=4,
|
||||||
|
)
|
||||||
|
qr.add_data(payment_url)
|
||||||
|
qr.make(fit=True)
|
||||||
|
|
||||||
|
# 创建二维码图片
|
||||||
|
qr_img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
print("✅ 二维码图片生成成功")
|
||||||
|
|
||||||
|
# 创建带说明文字的图片
|
||||||
|
img_width = 400
|
||||||
|
img_height = 500
|
||||||
|
img = Image.new('RGB', (img_width, img_height), 'white')
|
||||||
|
|
||||||
|
# 粘贴二维码
|
||||||
|
qr_img = qr_img.resize((300, 300))
|
||||||
|
img.paste(qr_img, (50, 50))
|
||||||
|
print("✅ 二维码图片合成成功")
|
||||||
|
|
||||||
|
# 添加文字说明
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
try:
|
||||||
|
# 尝试使用系统字体
|
||||||
|
font = ImageFont.truetype("/System/Library/Fonts/Arial.ttf", 16)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
# 尝试其他常见字体路径
|
||||||
|
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 16)
|
||||||
|
except:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
text = "扫描二维码完成USDT支付"
|
||||||
|
try:
|
||||||
|
text_bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
|
except:
|
||||||
|
# 兼容旧版PIL
|
||||||
|
text_width = len(text) * 10
|
||||||
|
|
||||||
|
text_x = (img_width - text_width) // 2
|
||||||
|
draw.text((text_x, 380), text, fill="black", font=font)
|
||||||
|
print("✅ 文字说明添加成功")
|
||||||
|
|
||||||
|
# 保存到BytesIO
|
||||||
|
img_buffer = BytesIO()
|
||||||
|
img.save(img_buffer, format='JPEG', quality=90)
|
||||||
|
img_buffer.seek(0)
|
||||||
|
|
||||||
|
print(f"✅ 备用二维码生成成功,图片大小: {len(img_buffer.getvalue())} bytes")
|
||||||
|
return img_buffer
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 备用二维码生成失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_screenshot(payment_url: str, use_fallback: bool = True) -> Optional[BytesIO]:
|
||||||
|
"""
|
||||||
|
获取支付页面截图
|
||||||
|
|
||||||
|
Args:
|
||||||
|
payment_url: 支付链接
|
||||||
|
use_fallback: 是否使用备用方案
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BytesIO: 图片数据流
|
||||||
|
"""
|
||||||
|
# 优先尝试真实网页截图
|
||||||
|
print(f"🔧 尝试真实网页截图: {payment_url}")
|
||||||
|
|
||||||
|
# 首先尝试真实截图
|
||||||
|
screenshot = capture_payment_qr(payment_url)
|
||||||
|
|
||||||
|
if screenshot:
|
||||||
|
print("✅ 真实网页截图成功")
|
||||||
|
return screenshot
|
||||||
|
|
||||||
|
# 真实截图失败时使用备用方案
|
||||||
|
if use_fallback:
|
||||||
|
print("⚠️ 真实截图失败,使用备用二维码方案")
|
||||||
|
return capture_payment_qr_fallback(payment_url)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
+1385
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,484 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 授权检查 - 请勿删除此部分,否则程序无法运行
|
||||||
|
import _auth_check
|
||||||
|
|
||||||
|
# Consolidated utilities module: merged from utils/*.py
|
||||||
|
# Sections:
|
||||||
|
# - constants: STATUS_ZH, MSG
|
||||||
|
# - home: render_home
|
||||||
|
# - keyboards: build_payment_rows, row_back, row_home_admin, make_markup
|
||||||
|
# - misc: parse_date, fmt_ts, to_base36, bar
|
||||||
|
# - notify: notify_admin
|
||||||
|
# - sender: send_ephemeral
|
||||||
|
# - settings: ensure_settings_table, get_setting, set_setting
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
try:
|
||||||
|
from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
except Exception: # 测试环境兜底桩:不影响真实运行
|
||||||
|
class InlineKeyboardButton: # type: ignore
|
||||||
|
def __init__(self, text: str, callback_data: Optional[str] = None):
|
||||||
|
self.text = text
|
||||||
|
self.callback_data = callback_data
|
||||||
|
|
||||||
|
class InlineKeyboardMarkup: # type: ignore
|
||||||
|
def __init__(self, inline_keyboard):
|
||||||
|
self.inline_keyboard = inline_keyboard
|
||||||
|
|
||||||
|
class Bot: # type: ignore
|
||||||
|
async def send_message(self, chat_id: int, text: str, **kwargs):
|
||||||
|
# 返回与 python-telegram-bot 类似的对象属性
|
||||||
|
return SimpleNamespace(message_id=1, chat_id=chat_id, text=text)
|
||||||
|
|
||||||
|
async def delete_message(self, chat_id: int, message_id: int):
|
||||||
|
return None
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# constants
|
||||||
|
"STATUS_ZH",
|
||||||
|
"MSG",
|
||||||
|
# home
|
||||||
|
"render_home",
|
||||||
|
# keyboards
|
||||||
|
"build_payment_rows",
|
||||||
|
"row_back",
|
||||||
|
"row_home_admin",
|
||||||
|
"make_markup",
|
||||||
|
# misc
|
||||||
|
"parse_date",
|
||||||
|
"fmt_ts",
|
||||||
|
"to_base36",
|
||||||
|
"bar",
|
||||||
|
# notify
|
||||||
|
"notify_admin",
|
||||||
|
# sender
|
||||||
|
"send_ephemeral",
|
||||||
|
# settings
|
||||||
|
"ensure_settings_table",
|
||||||
|
"get_setting",
|
||||||
|
"set_setting",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------- constants.py ----------------
|
||||||
|
# 统一的状态/文案常量
|
||||||
|
STATUS_ZH: Dict[str, str] = {
|
||||||
|
"pending": "待支付",
|
||||||
|
"paid": "已支付",
|
||||||
|
"processing": "处理中",
|
||||||
|
"completed": "已完成",
|
||||||
|
"cancelled": "已取消",
|
||||||
|
"expired": "已超时",
|
||||||
|
"refunded": "已退款",
|
||||||
|
"failed": "支付失败",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 常用短句(可逐步接入以实现统一文案/i18n)
|
||||||
|
MSG: Dict[str, str] = {
|
||||||
|
"saved_and_back": "✅ 已保存变更,返回商品页…",
|
||||||
|
"created_and_back": "✅ 新商品已创建,返回列表…",
|
||||||
|
"refreshing": "正在刷新…",
|
||||||
|
"refreshed": "✅ 刷新完成",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------- home.py ----------------
|
||||||
|
# 类型注释仅作参考,不强制
|
||||||
|
_GetSetting = Callable[[str, Optional[str]], Optional[str]]
|
||||||
|
|
||||||
|
async def render_home(
|
||||||
|
chat_id: int,
|
||||||
|
cur,
|
||||||
|
START_CFG,
|
||||||
|
_get_setting: _GetSetting,
|
||||||
|
_delete_last_and_send_photo: Callable[..., Any],
|
||||||
|
_delete_last_and_send_text: Callable[..., Any],
|
||||||
|
*,
|
||||||
|
extra_rows: Optional[list[list[InlineKeyboardButton]]] = None,
|
||||||
|
):
|
||||||
|
"""渲染首页(封面 + 标题/简介 + 商品按钮)。
|
||||||
|
所有依赖通过参数传入,方便在不同模块中复用。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
title = (_get_setting("home.title", (START_CFG.get("title") or "欢迎选购")) or "欢迎选购").strip()
|
||||||
|
except Exception:
|
||||||
|
title = "欢迎选购"
|
||||||
|
try:
|
||||||
|
intro = (_get_setting("home.intro", (START_CFG.get("intro") or "请选择下方商品进行购买")) or "请选择下方商品进行购买").strip()
|
||||||
|
except Exception:
|
||||||
|
intro = "请选择下方商品进行购买"
|
||||||
|
try:
|
||||||
|
cover = _get_setting("home.cover_url", START_CFG.get("cover_url") or None)
|
||||||
|
except Exception:
|
||||||
|
cover = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
rows: List[Tuple[int, str, float]] = cur.execute(
|
||||||
|
"SELECT id, name, price FROM products WHERE status='on'"
|
||||||
|
).fetchall()
|
||||||
|
except Exception:
|
||||||
|
rows = []
|
||||||
|
|
||||||
|
# 每行商品数:从 settings 读取,可选 1-4,默认 2
|
||||||
|
try:
|
||||||
|
cols_raw = _get_setting("home.products_per_row", (START_CFG.get("products_per_row") or 2))
|
||||||
|
cols = int(cols_raw or 2)
|
||||||
|
except Exception:
|
||||||
|
cols = 2
|
||||||
|
cols = max(1, min(4, cols))
|
||||||
|
|
||||||
|
# 读取按钮文案模板。支持占位符:{name}、{price}
|
||||||
|
try:
|
||||||
|
btn_tpl = _get_setting("home.button_template", (START_CFG.get("button_template") or " {name} | ¥{price}")) or " {name} | ¥{price}"
|
||||||
|
except Exception:
|
||||||
|
btn_tpl = " {name} | ¥{price}"
|
||||||
|
|
||||||
|
buttons: List[List[InlineKeyboardButton]] = []
|
||||||
|
row_btn: List[InlineKeyboardButton] = []
|
||||||
|
for pid, name, price in rows:
|
||||||
|
try:
|
||||||
|
label = str(btn_tpl).replace("{name}", str(name)).replace("{price}", str(price))
|
||||||
|
except Exception:
|
||||||
|
label = f" {name} | ¥{price}"
|
||||||
|
row_btn.append(InlineKeyboardButton(label, callback_data=f"detail:{pid}"))
|
||||||
|
if len(row_btn) >= cols:
|
||||||
|
buttons.append(row_btn)
|
||||||
|
row_btn = []
|
||||||
|
if row_btn:
|
||||||
|
buttons.append(row_btn)
|
||||||
|
|
||||||
|
# 追加额外按钮行(例如:返回)
|
||||||
|
if extra_rows:
|
||||||
|
for r in extra_rows:
|
||||||
|
if isinstance(r, list) and r:
|
||||||
|
buttons.append(r)
|
||||||
|
|
||||||
|
# 客服入口改为独立命令 /support,此处不再在首页展示按钮
|
||||||
|
|
||||||
|
caption = f"{title}\n\n{intro}\n\n请选择商品:"
|
||||||
|
|
||||||
|
if cover:
|
||||||
|
try:
|
||||||
|
await _delete_last_and_send_photo(
|
||||||
|
chat_id,
|
||||||
|
cover,
|
||||||
|
caption=caption,
|
||||||
|
reply_markup=InlineKeyboardMarkup(buttons) if buttons else None,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await _delete_last_and_send_text(
|
||||||
|
chat_id,
|
||||||
|
caption,
|
||||||
|
reply_markup=InlineKeyboardMarkup(buttons) if buttons else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------- keyboards.py ----------------
|
||||||
|
|
||||||
|
def build_payment_rows(
|
||||||
|
paycfg: Dict[str, dict],
|
||||||
|
*,
|
||||||
|
enabled_key: str = "enabled",
|
||||||
|
priority_key: str = "priority",
|
||||||
|
name_key: str = "name",
|
||||||
|
callback_fmt: str = "pay:{channel}:{pid}",
|
||||||
|
pid: Optional[str] = None,
|
||||||
|
max_cols: int = 2,
|
||||||
|
get_setting_func: Optional[Callable[[str, str], str]] = None,
|
||||||
|
skip_single: bool = False,
|
||||||
|
) -> List[List[InlineKeyboardButton]]:
|
||||||
|
"""
|
||||||
|
根据支付方式配置生成按钮行:
|
||||||
|
- 过滤掉未启用项(enabled=False 或数据库设置为关闭)
|
||||||
|
- 按 priority 从小到大排序(默认 100)
|
||||||
|
- 每行最多 max_cols 个
|
||||||
|
|
||||||
|
paycfg 示例:{
|
||||||
|
"alipay": {"name": "支付宝", "enabled": true, "priority": 10},
|
||||||
|
"wxpay": {"name": "微信", "enabled": false, "priority": 20},
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# 如果有get_setting_func,使用管理员设置的排序
|
||||||
|
if get_setting_func:
|
||||||
|
order_str = get_setting_func("payment.order", "alipay,wxpay,usdt_lemon,usdt_token188")
|
||||||
|
payment_order = order_str.split(",")
|
||||||
|
|
||||||
|
items: List[Tuple[int, str, str]] = []
|
||||||
|
for i, ch in enumerate(payment_order):
|
||||||
|
if ch not in paycfg:
|
||||||
|
continue
|
||||||
|
cfg = paycfg[ch]
|
||||||
|
|
||||||
|
# 检查配置文件中的enabled
|
||||||
|
if not cfg.get(enabled_key, True):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查数据库中的开关设置(管理员可控制)
|
||||||
|
db_enabled = get_setting_func(f"payment.{ch}.enabled", "true") == "true"
|
||||||
|
if not db_enabled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
label = str(cfg.get(name_key) or ch)
|
||||||
|
items.append((i, ch, label)) # 使用顺序索引而不是priority
|
||||||
|
else:
|
||||||
|
# 回退到原来的priority排序
|
||||||
|
items: List[Tuple[int, str, str]] = []
|
||||||
|
for ch, cfg in paycfg.items():
|
||||||
|
# 检查配置文件中的enabled
|
||||||
|
if not cfg.get(enabled_key, True):
|
||||||
|
continue
|
||||||
|
|
||||||
|
pri = int(cfg.get(priority_key, 100) or 100)
|
||||||
|
label = str(cfg.get(name_key) or ch)
|
||||||
|
items.append((pri, ch, label))
|
||||||
|
items.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
# 如果启用skip_single且只有一个支付方式,返回空列表
|
||||||
|
if skip_single and len(items) == 1:
|
||||||
|
return []
|
||||||
|
|
||||||
|
rows_kb: List[List[InlineKeyboardButton]] = []
|
||||||
|
row: List[InlineKeyboardButton] = []
|
||||||
|
for _, channel, label in items:
|
||||||
|
cb = callback_fmt.format(channel=channel, pid=pid or "")
|
||||||
|
row.append(InlineKeyboardButton(label, callback_data=cb))
|
||||||
|
if len(row) >= max_cols:
|
||||||
|
rows_kb.append(row)
|
||||||
|
row = []
|
||||||
|
if row:
|
||||||
|
rows_kb.append(row)
|
||||||
|
return rows_kb
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_enabled_payment(
|
||||||
|
paycfg: Dict[str, dict],
|
||||||
|
*,
|
||||||
|
enabled_key: str = "enabled",
|
||||||
|
get_setting_func: Optional[Callable[[str, str], str]] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
获取第一个启用的支付方式
|
||||||
|
"""
|
||||||
|
# 如果有get_setting_func,使用管理员设置的排序
|
||||||
|
if get_setting_func:
|
||||||
|
order_str = get_setting_func("payment.order", "alipay,wxpay,usdt_lemon,usdt_token188")
|
||||||
|
payment_order = order_str.split(",")
|
||||||
|
|
||||||
|
for ch in payment_order:
|
||||||
|
if ch not in paycfg:
|
||||||
|
continue
|
||||||
|
cfg = paycfg[ch]
|
||||||
|
|
||||||
|
# 检查配置文件中的enabled
|
||||||
|
if not cfg.get(enabled_key, True):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查数据库中的开关设置(管理员可控制)
|
||||||
|
db_enabled = get_setting_func(f"payment.{ch}.enabled", "true") == "true"
|
||||||
|
if not db_enabled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return ch
|
||||||
|
else:
|
||||||
|
# 回退到原来的priority排序
|
||||||
|
items = []
|
||||||
|
for ch, cfg in paycfg.items():
|
||||||
|
# 检查配置文件中的enabled
|
||||||
|
if not cfg.get(enabled_key, True):
|
||||||
|
continue
|
||||||
|
|
||||||
|
pri = int(cfg.get("priority", 100) or 100)
|
||||||
|
items.append((pri, ch))
|
||||||
|
items.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
if items:
|
||||||
|
return items[0][1]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def row_back(callback_data: str, label: str = "⬅️ 返回") -> List[InlineKeyboardButton]:
|
||||||
|
return [InlineKeyboardButton(label, callback_data=callback_data)]
|
||||||
|
|
||||||
|
|
||||||
|
def row_home_admin(label: str = "🏠 返回面板") -> List[InlineKeyboardButton]:
|
||||||
|
return [InlineKeyboardButton(label, callback_data="adm:menu")]
|
||||||
|
|
||||||
|
|
||||||
|
def make_markup(rows: Sequence[Sequence[InlineKeyboardButton]] | None) -> Optional[InlineKeyboardMarkup]:
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
return InlineKeyboardMarkup(list(rows))
|
||||||
|
|
||||||
|
# 统一的付款台控制行:用于“重新检查/取消付款”等
|
||||||
|
def rows_pay_console(otn: str) -> List[List[InlineKeyboardButton]]:
|
||||||
|
return [[
|
||||||
|
InlineKeyboardButton("🔄 我已支付,重新检查", callback_data=f"recheck:{otn}"),
|
||||||
|
InlineKeyboardButton("❌ 取消本次付款", callback_data=f"ask:cancel:{otn}"),
|
||||||
|
]]
|
||||||
|
|
||||||
|
# 通用确认对话行:yes/no 两个按钮在同一行
|
||||||
|
def build_confirm_rows(yes_cb: str, no_cb: str, yes_label: str = "✅ 确定", no_label: str = "↩️ 返回") -> List[List[InlineKeyboardButton]]:
|
||||||
|
return [[
|
||||||
|
InlineKeyboardButton(yes_label, callback_data=yes_cb),
|
||||||
|
InlineKeyboardButton(no_label, callback_data=no_cb),
|
||||||
|
]]
|
||||||
|
|
||||||
|
# ---------------- misc.py ----------------
|
||||||
|
|
||||||
|
def parse_date(s: str):
|
||||||
|
"""Parse YYYY-MM-DD to unix timestamp (seconds). Return None on failure/empty."""
|
||||||
|
try:
|
||||||
|
s = (s or "").strip()
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
y, m, d = s.split("-")
|
||||||
|
tm = time.strptime(f"{int(y):04d}-{int(m):02d}-{int(d):02d}", "%Y-%m-%d")
|
||||||
|
return int(time.mktime(tm))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_ts(ts: int) -> str:
|
||||||
|
try:
|
||||||
|
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(ts or 0)))
|
||||||
|
except Exception:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
|
||||||
|
def to_base36(n: int) -> str:
|
||||||
|
"""Encode non-negative int to uppercase base36 string."""
|
||||||
|
try:
|
||||||
|
x = int(n)
|
||||||
|
if x < 0:
|
||||||
|
x = -x
|
||||||
|
if x == 0:
|
||||||
|
return "0"
|
||||||
|
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
s: List[str] = []
|
||||||
|
while x > 0:
|
||||||
|
x, r = divmod(x, 36)
|
||||||
|
s.append(chars[r])
|
||||||
|
return "".join(reversed(s))
|
||||||
|
except Exception:
|
||||||
|
return str(n)
|
||||||
|
|
||||||
|
|
||||||
|
def bar(val: float, maxv: float, width: int = 20) -> str:
|
||||||
|
if maxv <= 0:
|
||||||
|
return ""
|
||||||
|
n = int(round((float(val) / float(maxv)) * width))
|
||||||
|
n = max(0, min(width, n))
|
||||||
|
return "█" * n + "·" * (width - n)
|
||||||
|
|
||||||
|
# ---------------- notify.py ----------------
|
||||||
|
|
||||||
|
async def notify_admin(
|
||||||
|
bot: Bot,
|
||||||
|
text: str,
|
||||||
|
admin_id: int,
|
||||||
|
*,
|
||||||
|
prefix: str = "[通知]",
|
||||||
|
attach_time: bool = True,
|
||||||
|
context: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
统一的管理员通知工具。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- bot: Telegram Bot 实例
|
||||||
|
- text: 主体文本
|
||||||
|
- admin_id: 管理员聊天ID(从配置读取并传入)
|
||||||
|
- prefix: 前缀标签,如 "[错误]"、"[告警]"、"[通知]"
|
||||||
|
- attach_time: 是否追加时间戳
|
||||||
|
- context: 可选上下文信息,追加到消息末尾
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if attach_time else None
|
||||||
|
parts: List[str] = []
|
||||||
|
if prefix:
|
||||||
|
parts.append(prefix)
|
||||||
|
parts.append(text.strip())
|
||||||
|
if context:
|
||||||
|
parts.append(str(context).strip())
|
||||||
|
if ts:
|
||||||
|
parts.append(f"@{ts}")
|
||||||
|
msg = " ".join(part for part in parts if part)
|
||||||
|
await bot.send_message(admin_id, text=msg)
|
||||||
|
except Exception:
|
||||||
|
# 通知失败不影响主流程
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ---------------- sender.py ----------------
|
||||||
|
|
||||||
|
async def send_ephemeral(bot: Bot, chat_id: int, text: str, ttl: int = 5) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
发送一条会在 ttl 秒后自动删除的临时文本消息。
|
||||||
|
|
||||||
|
:param bot: telegram.Bot 实例
|
||||||
|
:param chat_id: 目标聊天 ID
|
||||||
|
:param text: 文本内容
|
||||||
|
:param ttl: 存活时间(秒),默认 5
|
||||||
|
:return: 已发送消息的 message_id(若发送失败则返回 None)
|
||||||
|
"""
|
||||||
|
msg = None
|
||||||
|
try:
|
||||||
|
msg = await bot.send_message(chat_id=chat_id, text=text)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _del_later(c_id: int, m_id: int, delay: int):
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(max(1, int(delay)))
|
||||||
|
await bot.delete_message(chat_id=c_id, message_id=m_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.create_task(_del_later(msg.chat_id, msg.message_id, ttl))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return getattr(msg, "message_id", None)
|
||||||
|
|
||||||
|
# ---------------- settings.py ----------------
|
||||||
|
|
||||||
|
def ensure_settings_table(cur, conn) -> None:
|
||||||
|
try:
|
||||||
|
cur.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS settings(\n"
|
||||||
|
" key TEXT PRIMARY KEY,\n"
|
||||||
|
" value TEXT\n"
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_setting(cur, key: str, default: Optional[str] = "") -> Optional[str]:
|
||||||
|
try:
|
||||||
|
row = cur.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
|
||||||
|
if row and row[0] is not None:
|
||||||
|
return str(row[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def set_setting(cur, conn, key: str, value: str) -> None:
|
||||||
|
try:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||||||
|
(key, value),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
Reference in New Issue
Block a user