更新测试一版
This commit is contained in:
parent
73800eeaa7
commit
0cd42f3401
3
.env
3
.env
@ -23,6 +23,9 @@ MAX_UNBIND_TIMES=3
|
|||||||
LICENSE_KEY_LENGTH=32
|
LICENSE_KEY_LENGTH=32
|
||||||
LICENSE_KEY_PREFIX=
|
LICENSE_KEY_PREFIX=
|
||||||
|
|
||||||
|
FRONTEND_DOMAIN=http://km.taisan.online
|
||||||
|
|
||||||
|
|
||||||
# API配置
|
# API配置
|
||||||
API_VERSION=v1
|
API_VERSION=v1
|
||||||
ITEMS_PER_PAGE=20
|
ITEMS_PER_PAGE=20
|
||||||
|
|||||||
@ -12,6 +12,7 @@ FLASK_DEBUG=True
|
|||||||
# 安全配置
|
# 安全配置
|
||||||
SECRET_KEY=your-super-secret-key-change-this-in-production
|
SECRET_KEY=your-super-secret-key-change-this-in-production
|
||||||
AUTH_SECRET_KEY=your-auth-validator-secret-key
|
AUTH_SECRET_KEY=your-auth-validator-secret-key
|
||||||
|
FRONTEND_DOMAIN=
|
||||||
|
|
||||||
# 验证器配置
|
# 验证器配置
|
||||||
OFFLINE_CACHE_DAYS=7
|
OFFLINE_CACHE_DAYS=7
|
||||||
@ -30,6 +31,8 @@ ITEMS_PER_PAGE=20
|
|||||||
# 服务器配置
|
# 服务器配置
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=5000
|
PORT=5000
|
||||||
|
FASTAPI_HOST=127.0.0.1
|
||||||
|
FASTAPI_PORT=9000
|
||||||
|
|
||||||
# 文件上传配置
|
# 文件上传配置
|
||||||
MAX_CONTENT_LENGTH=524288000
|
MAX_CONTENT_LENGTH=524288000
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
5. [启动服务](#启动服务)
|
5. [启动服务](#启动服务)
|
||||||
6. [常见问题解决](#常见问题解决)
|
6. [常见问题解决](#常见问题解决)
|
||||||
7. [维护与管理](#维护与管理)
|
7. [维护与管理](#维护与管理)
|
||||||
|
8. [前端域名配置](#前端域名配置-1)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -184,6 +185,27 @@ pip install PyMySQL==1.1.0
|
|||||||
- `localhost`:数据库地址(本地部署就用 localhost)
|
- `localhost`:数据库地址(本地部署就用 localhost)
|
||||||
- `kamaxitong`:数据库名称
|
- `kamaxitong`:数据库名称
|
||||||
|
|
||||||
|
### 第二步:配置前端域名(可选但推荐)
|
||||||
|
|
||||||
|
1. **为什么要配置前端域名**
|
||||||
|
在本地开发环境中,系统默认通过 `http://localhost:5000` 访问。当部署到服务器并通过域名访问时,如果不配置前端域名,前端页面中的接口调用可能会仍然指向 `localhost:5000`,导致接口调用失败。
|
||||||
|
|
||||||
|
2. **配置方法**
|
||||||
|
在 `.env` 文件中添加或修改以下配置:
|
||||||
|
```bash
|
||||||
|
# 前端域名配置
|
||||||
|
FRONTEND_DOMAIN=https://your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
或者通过管理后台配置:
|
||||||
|
- 登录系统管理后台
|
||||||
|
- 进入"系统设置"页面
|
||||||
|
- 在"基本设置"中找到"前端域名"配置项
|
||||||
|
- 输入你的域名,例如:`https://your-domain.com`
|
||||||
|
- 点击"保存设置"
|
||||||
|
|
||||||
|
> 注意:如果两种方式都配置了,环境变量的优先级更高。
|
||||||
|
|
||||||
### 第二步:创建数据库
|
### 第二步:创建数据库
|
||||||
|
|
||||||
#### 方法一:使用命令行(推荐)
|
#### 方法一:使用命令行(推荐)
|
||||||
@ -510,6 +532,39 @@ tail -f logs/kamaxitong.log
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 前端域名配置
|
||||||
|
|
||||||
|
### 问题描述
|
||||||
|
|
||||||
|
在本地开发环境中,系统默认通过 `http://localhost:5000` 访问。当部署到服务器并通过域名访问时,前端页面中的接口调用可能会仍然指向 `localhost:5000`,导致接口调用失败。
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
|
||||||
|
系统已通过修改前端JavaScript中的 `apiRequest` 函数来自动检测当前访问的主机并构建正确的API调用地址。现在还支持通过环境变量或管理后台配置前端域名。
|
||||||
|
|
||||||
|
前端JavaScript现在会使用配置的域名或 `window.location.origin` 来获取当前访问的完整域名,并自动构建完整的API URL。
|
||||||
|
|
||||||
|
### 配置方法
|
||||||
|
|
||||||
|
1. **通过环境变量配置(推荐)**
|
||||||
|
在 `.env` 文件中添加:
|
||||||
|
```bash
|
||||||
|
FRONTEND_DOMAIN=https://your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **通过管理后台配置**
|
||||||
|
- 登录系统管理后台
|
||||||
|
- 进入"系统设置"页面
|
||||||
|
- 在"基本设置"中找到"前端域名"配置项
|
||||||
|
- 输入你的域名,例如:`https://your-domain.com`
|
||||||
|
- 点击"保存设置"
|
||||||
|
|
||||||
|
### 验证配置
|
||||||
|
|
||||||
|
1. 在浏览器中打开应用,按F12打开开发者工具
|
||||||
|
2. 切换到Network标签页
|
||||||
|
3. 刷新页面,观察API请求的URL是否为域名地址而非localhost
|
||||||
|
|
||||||
## 🎉 部署完成!
|
## 🎉 部署完成!
|
||||||
|
|
||||||
恭喜你!如果以上步骤都成功执行,你的 KaMiXiTong 系统已经成功部署了!
|
恭喜你!如果以上步骤都成功执行,你的 KaMiXiTong 系统已经成功部署了!
|
||||||
|
|||||||
@ -1,262 +1,96 @@
|
|||||||
# MySQL数据库配置指南
|
# MySQL 数据库配置与初始化指南
|
||||||
|
|
||||||
## 概述
|
## 目录
|
||||||
|
1. [环境要求](#环境要求)
|
||||||
|
2. [安装依赖](#安装依赖)
|
||||||
|
3. [配置数据库连接](#配置数据库连接)
|
||||||
|
4. [创建数据库](#创建数据库)
|
||||||
|
5. [初始化数据库](#初始化数据库)
|
||||||
|
6. [启动应用](#启动应用)
|
||||||
|
|
||||||
系统已经配置为使用MySQL数据库,并从`.env`文件读取配置。
|
## 环境要求
|
||||||
|
|
||||||
## 配置步骤
|
- MySQL 5.7 或更高版本
|
||||||
|
- Python 3.8 或更高版本
|
||||||
|
- PyMySQL 驱动
|
||||||
|
|
||||||
### 1. 确保安装MySQL客户端依赖
|
## 安装依赖
|
||||||
|
|
||||||
安装PyMySQL(MySQL Python驱动):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install PyMySQL
|
pip install PyMySQL
|
||||||
# 或者
|
|
||||||
pip install pymysql
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 配置.env文件
|
## 配置数据库连接
|
||||||
|
|
||||||
编辑项目根目录的`.env`文件:
|
复制 [.env.example](.env.example) 文件为 .env 并修改数据库配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
在 .env 文件中修改数据库连接配置:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# 数据库配置
|
DATABASE_URL=mysql+pymysql://用户名:密码@主机:端口/数据库名
|
||||||
# 格式: mysql+pymysql://用户名:密码@主机:端口/数据库名
|
|
||||||
DATABASE_URL=mysql+pymysql://root:你的密码@localhost/kamaxitong
|
|
||||||
|
|
||||||
# 示例:
|
|
||||||
DATABASE_URL=mysql+pymysql://root:taiyi1224@localhost/kamaxitong
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 创建数据库
|
示例:
|
||||||
|
```env
|
||||||
|
DATABASE_URL=mysql+pymysql://root:password@localhost:3306/kamaxitong
|
||||||
|
```
|
||||||
|
|
||||||
在MySQL中创建数据库:
|
## 创建数据库
|
||||||
|
|
||||||
|
在 MySQL 中手动创建数据库:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- 登录MySQL
|
|
||||||
mysql -u root -p
|
|
||||||
|
|
||||||
-- 创建数据库
|
|
||||||
CREATE DATABASE kamaxitong CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
CREATE DATABASE kamaxitong CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 授权(可选)
|
|
||||||
GRANT ALL PRIVILEGES ON kamaxitong.* TO 'root'@'localhost';
|
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 更新.env文件中的数据库URL
|
或者使用提供的脚本自动创建:
|
||||||
|
|
||||||
`.env`文件中的`DATABASE_URL`格式:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# 标准格式
|
|
||||||
DATABASE_URL=mysql+pymysql://用户名:密码@主机:端口/数据库名
|
|
||||||
|
|
||||||
# 本地示例
|
|
||||||
DATABASE_URL=mysql+pymysql://root:taiyi1224@localhost/kamaxitong
|
|
||||||
|
|
||||||
# 远程示例
|
|
||||||
DATABASE_URL=mysql+pymysql://root:password@192.168.1.100:3306/kamaxitong
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 初始化数据库
|
|
||||||
|
|
||||||
#### 方式1: 使用Flask-Migrate(推荐)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 初始化迁移(如果还没有)
|
python setup_mysql.py
|
||||||
flask db init
|
|
||||||
|
|
||||||
# 生成迁移文件
|
|
||||||
flask db migrate -m "Initial migration"
|
|
||||||
|
|
||||||
# 应用迁移
|
|
||||||
flask db upgrade
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 方式2: 使用快速修复脚本
|
## 初始化数据库
|
||||||
|
|
||||||
|
有两种方式初始化数据库:
|
||||||
|
|
||||||
|
### 方式一:使用专用初始化脚本(推荐)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python quick_fix.py
|
python init_db_mysql.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 启动应用
|
该脚本将:
|
||||||
|
1. 从 .env 文件读取数据库配置
|
||||||
|
2. 清理现有表结构
|
||||||
|
3. 创建所有数据表
|
||||||
|
4. 插入初始数据(管理员账号、示例产品等)
|
||||||
|
|
||||||
|
### 方式二:使用通用配置脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python setup_mysql.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动应用
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python run.py
|
python run.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 数据库配置参数说明
|
或
|
||||||
|
|
||||||
| 参数 | 说明 | 示例 |
|
|
||||||
|------|------|------|
|
|
||||||
| 数据库类型 | mysql+pymysql | mysql+pymysql |
|
|
||||||
| 用户名 | MySQL用户名 | root |
|
|
||||||
| 密码 | MySQL密码 | taiyi1224 |
|
|
||||||
| 主机 | 数据库主机 | localhost 或 192.168.1.100 |
|
|
||||||
| 端口 | MySQL端口(可选) | 3306(默认) |
|
|
||||||
| 数据库名 | 要使用的数据库名 | kamaxitong |
|
|
||||||
|
|
||||||
## 完整的.env文件示例
|
|
||||||
|
|
||||||
```env
|
|
||||||
# 环境配置
|
|
||||||
FLASK_ENV=development
|
|
||||||
FLASK_DEBUG=True
|
|
||||||
|
|
||||||
# 数据库配置 - MySQL
|
|
||||||
DATABASE_URL=mysql+pymysql://root:taiyi1224@localhost/kamaxitong
|
|
||||||
|
|
||||||
# 安全配置
|
|
||||||
SECRET_KEY=taiyi1224
|
|
||||||
AUTH_SECRET_KEY=taiyi1224
|
|
||||||
|
|
||||||
# 验证器配置
|
|
||||||
OFFLINE_CACHE_DAYS=7
|
|
||||||
MAX_FAILED_ATTEMPTS=5
|
|
||||||
LOCKOUT_MINUTES=10
|
|
||||||
MAX_UNBIND_TIMES=3
|
|
||||||
|
|
||||||
# 卡密配置
|
|
||||||
LICENSE_KEY_LENGTH=32
|
|
||||||
LICENSE_KEY_PREFIX=
|
|
||||||
|
|
||||||
# API配置
|
|
||||||
API_VERSION=v1
|
|
||||||
ITEMS_PER_PAGE=20
|
|
||||||
|
|
||||||
# 服务器配置
|
|
||||||
HOST=0.0.0.0
|
|
||||||
PORT=5000
|
|
||||||
|
|
||||||
# 文件上传配置
|
|
||||||
MAX_CONTENT_LENGTH=16777216
|
|
||||||
UPLOAD_FOLDER=static/uploads
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
LOG_FILE=logs/kamaxitong.log
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验证配置
|
|
||||||
|
|
||||||
### 1. 检查.env文件是否被加载
|
|
||||||
|
|
||||||
启动应用时查看控制台输出:
|
|
||||||
|
|
||||||
```
|
|
||||||
成功加载.env文件
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 验证数据库连接
|
|
||||||
|
|
||||||
在Python中测试:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from app import create_app
|
|
||||||
from app import db
|
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
with app.app_context():
|
|
||||||
print("数据库URI:", app.config['SQLALCHEMY_DATABASE_URI'])
|
|
||||||
db.create_all() # 测试连接
|
|
||||||
print("数据库连接成功!")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q1: ImportError: No module named 'pymysql'
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
```bash
|
|
||||||
pip install pymysql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q2: 1049 (42000): Unknown database 'kamaxitong'
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
```sql
|
|
||||||
CREATE DATABASE kamaxitong CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q3: 1045 (28000): Access denied for user 'root'@'localhost'
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
1. 检查.env文件中的用户名和密码是否正确
|
|
||||||
2. 确保MySQL用户有权限访问数据库
|
|
||||||
3. 重置MySQL root密码
|
|
||||||
|
|
||||||
### Q4: 2003 (HY000): Can't connect to MySQL server
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
1. 确保MySQL服务已启动
|
|
||||||
2. 检查主机和端口是否正确
|
|
||||||
3. 检查防火墙设置
|
|
||||||
|
|
||||||
### Q5: .env文件没有被加载
|
|
||||||
|
|
||||||
**解决方案:**
|
|
||||||
1. 确保python-dotenv已安装:`pip install python-dotenv`
|
|
||||||
2. 确保.env文件在项目根目录
|
|
||||||
3. 重启应用
|
|
||||||
|
|
||||||
## 性能优化建议
|
|
||||||
|
|
||||||
### 1. 配置MySQL连接池
|
|
||||||
|
|
||||||
在config.py中已经配置:
|
|
||||||
|
|
||||||
```python
|
|
||||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
|
||||||
"future": True,
|
|
||||||
"pool_pre_ping": True,
|
|
||||||
"pool_size": 10,
|
|
||||||
"pool_recycle": 3600,
|
|
||||||
"max_overflow": 20
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 启用MySQL查询缓存
|
|
||||||
|
|
||||||
在MySQL配置文件中(my.cnf):
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[mysqld]
|
|
||||||
query_cache_type = 1
|
|
||||||
query_cache_size = 256M
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 设置合适的字符集
|
|
||||||
|
|
||||||
确保数据库、表、列都使用utf8mb4字符集:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE DATABASE kamaxitong
|
|
||||||
CHARACTER SET utf8mb4
|
|
||||||
COLLATE utf8mb4_unicode_ci;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 备份和恢复
|
|
||||||
|
|
||||||
### 备份数据库
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mysqldump -u root -p kamaxitong > backup.sql
|
python start.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### 恢复数据库
|
访问地址:http://localhost:5000
|
||||||
|
|
||||||
```bash
|
默认管理员账号:
|
||||||
mysql -u root -p kamaxitong < backup.sql
|
- 超级管理员: admin / admin123
|
||||||
```
|
- 普通管理员: test_admin / test123
|
||||||
|
|
||||||
## 总结
|
⚠️ 请在生产环境中修改默认密码!
|
||||||
|
|
||||||
系统已经配置为使用MySQL和.env文件:
|
|
||||||
- ✅ .env文件支持
|
|
||||||
- ✅ MySQL配置已就绪
|
|
||||||
- ✅ 从环境变量读取配置
|
|
||||||
- ✅ 支持开发/生产/测试环境
|
|
||||||
|
|
||||||
只需确保MySQL服务运行、依赖安装正确、.env文件配置正确即可!
|
|
||||||
@ -183,13 +183,20 @@ python start.py
|
|||||||
|
|
||||||
### 生产环境
|
### 生产环境
|
||||||
```bash
|
```bash
|
||||||
# 使用Gunicorn
|
# 使用一键部署脚本(推荐)
|
||||||
|
python deploy.py --setup-service --setup-nginx
|
||||||
|
|
||||||
|
# 或者手动部署
|
||||||
pip install gunicorn
|
pip install gunicorn
|
||||||
gunicorn -w 4 -b 0.0.0.0:5000 run:app
|
gunicorn -w 4 -b 0.0.0.0:5000 run:app
|
||||||
|
|
||||||
# 使用Nginx反向代理
|
# 使用Nginx反向代理
|
||||||
# 配置SSL证书
|
# 配置SSL证书
|
||||||
# 设置防火墙规则
|
# 设置防火墙规则
|
||||||
|
|
||||||
|
# 前端域名配置
|
||||||
|
# 在.env文件中设置FRONTEND_DOMAIN=https://your-domain.com
|
||||||
|
# 或通过管理后台配置
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker部署
|
### Docker部署
|
||||||
|
|||||||
41
README.md
41
README.md
@ -38,7 +38,7 @@ KaMiXiTong/
|
|||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
### 快速开始
|
### 快速开始 (SQLite)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆项目
|
# 克隆项目
|
||||||
@ -50,7 +50,7 @@ cd kamaxitong
|
|||||||
# 安装依赖
|
# 安装依赖
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# 初始化数据库
|
# 初始化SQLite数据库
|
||||||
python init_db_sqlite.py
|
python init_db_sqlite.py
|
||||||
|
|
||||||
# 启动服务
|
# 启动服务
|
||||||
@ -60,6 +60,36 @@ python run.py
|
|||||||
访问地址: http://localhost:5000
|
访问地址: http://localhost:5000
|
||||||
默认账号: admin / admin123
|
默认账号: admin / admin123
|
||||||
|
|
||||||
|
### MySQL数据库支持
|
||||||
|
|
||||||
|
系统也支持使用MySQL数据库,提供更好的性能和并发支持。
|
||||||
|
|
||||||
|
#### 初始化MySQL数据库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装MySQL依赖
|
||||||
|
pip install PyMySQL
|
||||||
|
|
||||||
|
# 配置数据库连接
|
||||||
|
# 复制 .env.example 为 .env 并修改 DATABASE_URL 配置
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 编辑 .env 文件,设置MySQL连接信息
|
||||||
|
# DATABASE_URL=mysql+pymysql://用户名:密码@主机:端口/数据库名
|
||||||
|
|
||||||
|
# 初始化MySQL数据库
|
||||||
|
python init_db_mysql.py
|
||||||
|
```
|
||||||
|
|
||||||
|
或者使用配置脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用配置脚本自动创建数据库和表
|
||||||
|
python setup_mysql.py
|
||||||
|
```
|
||||||
|
|
||||||
|
详细配置说明请参考 [MySQL配置指南](MYSQL_CONFIG_GUIDE.md)
|
||||||
|
|
||||||
### FastAPI接口
|
### FastAPI接口
|
||||||
|
|
||||||
系统还提供了现代化的FastAPI接口,具有自动生成的交互式文档:
|
系统还提供了现代化的FastAPI接口,具有自动生成的交互式文档:
|
||||||
@ -85,7 +115,10 @@ python start.py
|
|||||||
### 生产环境
|
### 生产环境
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 使用Gunicorn
|
# 使用一键部署脚本(推荐)
|
||||||
|
python deploy.py --setup-service --setup-nginx
|
||||||
|
|
||||||
|
# 或者手动部署
|
||||||
pip install gunicorn
|
pip install gunicorn
|
||||||
gunicorn -w 4 -b 0.0.0.0:5000 run:app
|
gunicorn -w 4 -b 0.0.0.0:5000 run:app
|
||||||
|
|
||||||
@ -107,6 +140,8 @@ gunicorn -w 4 -b 0.0.0.0:5000 run:app
|
|||||||
- [集成指南](docs/INTEGRATION.md)
|
- [集成指南](docs/INTEGRATION.md)
|
||||||
- [使用示例](docs/EXAMPLES.md)
|
- [使用示例](docs/EXAMPLES.md)
|
||||||
- [FastAPI接口文档](docs/FASTAPI.md)
|
- [FastAPI接口文档](docs/FASTAPI.md)
|
||||||
|
- [部署说明](docs/DEPLOYMENT.md)
|
||||||
|
- [MySQL配置指南](MYSQL_CONFIG_GUIDE.md)
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ CONFIG_MAPPING = {
|
|||||||
# 基本设置
|
# 基本设置
|
||||||
'site_name': 'SITE_NAME',
|
'site_name': 'SITE_NAME',
|
||||||
'admin_email': 'ADMIN_EMAIL',
|
'admin_email': 'ADMIN_EMAIL',
|
||||||
|
'frontend_domain': 'FRONTEND_DOMAIN',
|
||||||
'max_failed_attempts': 'MAX_FAILED_ATTEMPTS',
|
'max_failed_attempts': 'MAX_FAILED_ATTEMPTS',
|
||||||
'lockout_minutes': 'LOCKOUT_MINUTES',
|
'lockout_minutes': 'LOCKOUT_MINUTES',
|
||||||
'max_unbind_times': 'MAX_UNBIND_TIMES',
|
'max_unbind_times': 'MAX_UNBIND_TIMES',
|
||||||
|
|||||||
@ -228,34 +228,20 @@ class AuthValidator:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _validate_license_format(self, license_key: str) -> bool:
|
def _validate_license_format(self, license_key: str) -> bool:
|
||||||
"""验证卡密格式(与服务端保持一致)"""
|
"""验证卡密格式"""
|
||||||
if not license_key:
|
if not license_key:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 去除空格和制表符,并转为大写
|
|
||||||
license_key = license_key.strip().replace(' ', '').replace('\t', '').upper()
|
license_key = license_key.strip().replace(' ', '').replace('\t', '').upper()
|
||||||
|
|
||||||
# 检查是否为XXXX-XXXX-XXXX-XXXX格式
|
# 检查长度(16-32位)
|
||||||
if '-' in license_key:
|
if len(license_key) < 16 or len(license_key) > 32:
|
||||||
parts = license_key.split('-')
|
|
||||||
# 应该有4部分,每部分8个字符
|
|
||||||
if len(parts) == 4 and all(len(part) == 8 for part in parts):
|
|
||||||
# 检查所有字符是否为大写字母或数字
|
|
||||||
combined = ''.join(parts)
|
|
||||||
if len(combined) == 32:
|
|
||||||
pattern = r'^[A-Z0-9]+$'
|
|
||||||
import re
|
|
||||||
return bool(re.match(pattern, combined))
|
|
||||||
return False
|
return False
|
||||||
else:
|
|
||||||
# 兼容旧格式:检查长度(16-32位)
|
|
||||||
if len(license_key) < 16 or len(license_key) > 32:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查字符(只允许大写字母、数字和下划线)
|
# 检查字符(只允许大写字母、数字和下划线)
|
||||||
pattern = r'^[A-Z0-9_]+$'
|
import re
|
||||||
import re
|
pattern = r'^[A-Z0-9_]+$'
|
||||||
return bool(re.match(pattern, license_key))
|
return bool(re.match(pattern, license_key))
|
||||||
|
|
||||||
def _input_license_key(self) -> str:
|
def _input_license_key(self) -> str:
|
||||||
"""输入卡密"""
|
"""输入卡密"""
|
||||||
@ -407,55 +393,117 @@ class AuthValidator:
|
|||||||
max_attempts = 3 # 最多尝试3次
|
max_attempts = 3 # 最多尝试3次
|
||||||
|
|
||||||
for attempt in range(max_attempts):
|
for attempt in range(max_attempts):
|
||||||
# 获取卡密
|
# 输入卡密
|
||||||
license_key = self._input_license_key()
|
license_key = self._input_license_key()
|
||||||
if not license_key:
|
if not license_key:
|
||||||
self._show_message("验证取消", "用户取消了验证操作")
|
self._show_message("验证取消", "未输入卡密,程序退出", True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 验证卡密格式
|
# 验证卡密格式
|
||||||
if not self._validate_license_format(license_key):
|
if not self._validate_license_format(license_key):
|
||||||
self._show_message("格式错误", "卡密格式不正确,请重新输入", True)
|
self._show_message("格式错误", "卡密格式错误,请检查后重新输入", True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 在线验证
|
# 在线验证
|
||||||
success, message, auth_info = self._online_verify(license_key)
|
success, message, auth_info = self._online_verify(license_key)
|
||||||
|
|
||||||
if success:
|
if success and auth_info:
|
||||||
# 缓存授权信息
|
# 验证成功,缓存授权信息
|
||||||
if auth_info:
|
self._cache_auth_info(auth_info)
|
||||||
self._cache_auth_info(auth_info)
|
|
||||||
self._show_message("验证成功", message)
|
# 检查是否需要更新
|
||||||
|
force_update = auth_info.get('force_update', False)
|
||||||
|
download_url = auth_info.get('download_url')
|
||||||
|
new_version = auth_info.get('new_version')
|
||||||
|
|
||||||
|
if force_update and download_url:
|
||||||
|
self._show_message("需要更新", f"发现新版本 {new_version}\n请下载更新后重新启动程序", True)
|
||||||
|
# 尝试打开下载链接
|
||||||
|
try:
|
||||||
|
import webbrowser
|
||||||
|
webbrowser.open(download_url)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._show_message("验证成功", f"授权验证成功!\n卡密: {license_key}\n有效期至: {auth_info.get('expire_time', '永久')}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 记录失败尝试
|
# 验证失败
|
||||||
self.failed_attempts += 1
|
self.failed_attempts += 1
|
||||||
if self.failed_attempts >= 3:
|
self.last_attempt_time = datetime.utcnow()
|
||||||
self._lock_account(10) # 锁定10分钟
|
|
||||||
|
if self.failed_attempts >= 5: # 失败5次锁定
|
||||||
|
self._lock_account()
|
||||||
self._show_message("验证失败", self._get_lock_message(), True)
|
self._show_message("验证失败", self._get_lock_message(), True)
|
||||||
return False
|
return False
|
||||||
else:
|
|
||||||
remaining_attempts = 3 - self.failed_attempts
|
|
||||||
self._show_message(
|
|
||||||
"验证失败",
|
|
||||||
f"{message}\n\n剩余尝试次数: {remaining_attempts}",
|
|
||||||
True
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 所有尝试都失败
|
self._show_message("验证失败", f"验证失败: {message}\n剩余尝试次数: {5 - self.failed_attempts}", True)
|
||||||
self._show_message("验证失败", "已达到最大尝试次数", True)
|
|
||||||
|
# 如果不是最后一次尝试,询问是否继续
|
||||||
|
if attempt < max_attempts - 1:
|
||||||
|
if self.gui_mode:
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
result = messagebox.askyesno("继续验证", "是否继续输入卡密验证?")
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return False
|
||||||
|
except ImportError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_auth_info(self) -> Optional[Dict[str, Any]]:
|
def get_software_info(self) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""获取软件信息"""
|
||||||
获取当前授权信息
|
try:
|
||||||
|
url = f"{self.api_url}/software/info"
|
||||||
Returns:
|
params = {"software_id": self.software_id}
|
||||||
Optional[Dict[str, Any]]: 授权信息
|
|
||||||
"""
|
|
||||||
return self.cache.get_auth_info(self.software_id)
|
|
||||||
|
|
||||||
def clear_auth_cache(self):
|
response = requests.get(url, params=params, timeout=self.timeout)
|
||||||
"""清除授权缓存"""
|
if response.status_code == 200:
|
||||||
self.cache.clear_cache(self.software_id)
|
result = response.json()
|
||||||
|
if result.get('success'):
|
||||||
|
return result.get('data')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def clear_cache(self):
|
||||||
|
"""清除本地缓存"""
|
||||||
|
self.cache.clear_cache(self.software_id)
|
||||||
|
# 删除机器码缓存文件
|
||||||
|
try:
|
||||||
|
if os.path.exists(".machine_code"):
|
||||||
|
os.remove(".machine_code")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 便捷函数
|
||||||
|
def validate_license(software_id: str, **kwargs) -> bool:
|
||||||
|
"""
|
||||||
|
便捷的验证函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
software_id: 软件ID
|
||||||
|
**kwargs: 其他参数(api_url, secret_key, cache_days, timeout, gui_mode)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 验证是否成功
|
||||||
|
"""
|
||||||
|
validator = AuthValidator(software_id, **kwargs)
|
||||||
|
return validator.validate()
|
||||||
|
|
||||||
|
def get_machine_code() -> str:
|
||||||
|
"""获取当前机器码"""
|
||||||
|
return MachineCodeGenerator.generate()
|
||||||
@ -19,48 +19,55 @@ def login():
|
|||||||
username = request.form.get('username', '').strip()
|
username = request.form.get('username', '').strip()
|
||||||
password = request.form.get('password', '')
|
password = request.form.get('password', '')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
print("DEBUG: Missing username or password")
|
# 对于AJAX请求,返回JSON错误
|
||||||
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请输入用户名和密码'
|
||||||
|
}), 400
|
||||||
|
# 对于普通表单提交,使用flash消息
|
||||||
flash('请输入用户名和密码', 'error')
|
flash('请输入用户名和密码', 'error')
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
|
|
||||||
# 查找用户
|
# 查找用户
|
||||||
admin = Admin.query.filter_by(username=username).first()
|
admin = Admin.query.filter_by(username=username).first()
|
||||||
if admin:
|
if admin and admin.check_password(password) and admin.is_active:
|
||||||
if admin and admin.check_password(password) and admin.is_active:
|
# 登录成功
|
||||||
# 登录成功
|
login_user(admin, remember=True)
|
||||||
login_user(admin, remember=True)
|
|
||||||
|
|
||||||
# 更新最后登录信息
|
# 更新最后登录信息
|
||||||
admin.update_last_login(request.remote_addr)
|
admin.update_last_login(request.remote_addr)
|
||||||
|
|
||||||
# 生成简单的token(实际项目中应使用JWT)
|
# 生成简单的token(实际项目中应使用JWT)
|
||||||
import secrets
|
import secrets
|
||||||
token = secrets.token_urlsafe(32)
|
token = secrets.token_urlsafe(32)
|
||||||
|
|
||||||
# 如果是AJAX请求,返回JSON
|
# 如果是AJAX请求,返回JSON
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'token': token,
|
'token': token,
|
||||||
'user': {
|
'user': {
|
||||||
'username': admin.username,
|
'username': admin.username,
|
||||||
'role': admin.role,
|
'role': admin.role,
|
||||||
'is_super_admin': admin.is_super_admin()
|
'is_super_admin': admin.is_super_admin()
|
||||||
},
|
},
|
||||||
'redirect': url_for('web.dashboard')
|
'redirect': url_for('web.dashboard')
|
||||||
})
|
})
|
||||||
|
|
||||||
# 获取next参数
|
# 获取next参数
|
||||||
next_page = request.args.get('next')
|
next_page = request.args.get('next')
|
||||||
if next_page:
|
if next_page:
|
||||||
return redirect(next_page)
|
return redirect(next_page)
|
||||||
return redirect(url_for('web.dashboard'))
|
return redirect(url_for('web.dashboard'))
|
||||||
else:
|
|
||||||
flash('用户名或密码错误', 'error')
|
|
||||||
else:
|
else:
|
||||||
|
# 登录失败
|
||||||
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '用户名或密码错误'
|
||||||
|
}), 401
|
||||||
flash('用户名或密码错误', 'error')
|
flash('用户名或密码错误', 'error')
|
||||||
|
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
@ -86,6 +93,4 @@ def favicon():
|
|||||||
return Response(status=204) # No Content
|
return Response(status=204) # No Content
|
||||||
|
|
||||||
# 导入视图函数
|
# 导入视图函数
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
@ -53,6 +53,14 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
/* 消息通知样式 */
|
||||||
|
.notification-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{% block extra_css %}{% endblock %}
|
{% block extra_css %}{% endblock %}
|
||||||
@ -66,6 +74,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Notification Container -->
|
||||||
|
<div class="notification-container" id="notification-container"></div>
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<!-- Main Container -->
|
<!-- Main Container -->
|
||||||
@ -201,6 +212,9 @@
|
|||||||
|
|
||||||
<!-- Common Functions -->
|
<!-- Common Functions -->
|
||||||
<script>
|
<script>
|
||||||
|
// 设置前端域名全局变量
|
||||||
|
window.FRONTEND_DOMAIN = '{{ config.FRONTEND_DOMAIN or "" }}';
|
||||||
|
|
||||||
// 显示加载动画
|
// 显示加载动画
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
document.getElementById('loading').style.display = 'block';
|
document.getElementById('loading').style.display = 'block';
|
||||||
@ -213,6 +227,20 @@
|
|||||||
|
|
||||||
// AJAX请求封装
|
// AJAX请求封装
|
||||||
function apiRequest(url, options = {}) {
|
function apiRequest(url, options = {}) {
|
||||||
|
// 自动构建完整的API URL
|
||||||
|
let fullUrl = url;
|
||||||
|
|
||||||
|
// 检查是否配置了前端域名
|
||||||
|
const frontendDomain = window.FRONTEND_DOMAIN || '';
|
||||||
|
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
// 如果是相对路径,则使用配置的域名或当前主机
|
||||||
|
fullUrl = (frontendDomain || window.location.origin) + url;
|
||||||
|
} else if (!url.startsWith('http')) {
|
||||||
|
// 如果不是完整URL且不以http开头,则添加API前缀
|
||||||
|
fullUrl = (frontendDomain || window.location.origin) + '/api/v1/' + url;
|
||||||
|
}
|
||||||
|
|
||||||
showLoading();
|
showLoading();
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
@ -221,7 +249,7 @@
|
|||||||
credentials: 'same-origin' // 重要: 使用session cookies
|
credentials: 'same-origin' // 重要: 使用session cookies
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetch(url, { ...defaultOptions, ...options })
|
return fetch(fullUrl, { ...defaultOptions, ...options })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -278,19 +306,23 @@
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示通知
|
// 显示通知 - 统一使用静态JS中的函数
|
||||||
function showNotification(message, type = 'info') {
|
function showNotification(message, type = 'info') {
|
||||||
|
// 创建通知元素
|
||||||
const alertDiv = document.createElement('div');
|
const alertDiv = document.createElement('div');
|
||||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
const alertType = type === 'error' ? 'danger' : type;
|
||||||
|
alertDiv.className = `alert alert-${alertType} alert-dismissible fade show`;
|
||||||
alertDiv.innerHTML = `
|
alertDiv.innerHTML = `
|
||||||
${message}
|
${message}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
`;
|
`;
|
||||||
|
alertDiv.style.marginBottom = '10px';
|
||||||
const container = document.querySelector('main');
|
|
||||||
|
// 插入到通知容器中
|
||||||
|
const container = document.getElementById('notification-container') || document.querySelector('main');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.insertBefore(alertDiv, container.firstChild);
|
container.insertBefore(alertDiv, container.firstChild);
|
||||||
|
|
||||||
// 自动隐藏
|
// 自动隐藏
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (alertDiv.parentNode) {
|
if (alertDiv.parentNode) {
|
||||||
|
|||||||
@ -73,7 +73,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// 基础验证
|
// 基础验证
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
// 使用基础模板中的showNotification函数
|
|
||||||
showNotification('请填写用户名和密码', 'warning');
|
showNotification('请填写用户名和密码', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -83,8 +82,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (loginBtnText) loginBtnText.textContent = '登录中...';
|
if (loginBtnText) loginBtnText.textContent = '登录中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('尝试登录:', username);
|
|
||||||
|
|
||||||
// 尝试使用Web表单登录(传统Flask登录)
|
// 尝试使用Web表单登录(传统Flask登录)
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('username', username);
|
formData.append('username', username);
|
||||||
@ -107,9 +104,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
credentials: 'same-origin' // 重要:发送cookies
|
credentials: 'same-origin' // 重要:发送cookies
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('登录响应状态:', response.status);
|
|
||||||
console.log('登录响应URL:', response.url);
|
|
||||||
|
|
||||||
// 检查响应状态码
|
// 检查响应状态码
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
// 检查是否真正登录成功(重定向到dashboard)
|
// 检查是否真正登录成功(重定向到dashboard)
|
||||||
@ -119,23 +113,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
} else {
|
} else {
|
||||||
// 尝试解析响应内容
|
// 尝试解析响应内容
|
||||||
const responseData = await response.text();
|
const responseData = await response.text();
|
||||||
// 检查响应内容中是否包含成功信息
|
|
||||||
if (responseData.includes('dashboard') || responseData.includes('仪表板')) {
|
// 检查是否包含Flask flash消息
|
||||||
// 登录成功
|
if (responseData.includes('用户名或密码错误')) {
|
||||||
window.location.href = '/dashboard';
|
|
||||||
} else if (responseData.includes('用户名或密码错误')) {
|
|
||||||
showNotification('用户名或密码错误,请重试', 'error');
|
showNotification('用户名或密码错误,请重试', 'error');
|
||||||
|
} else if (responseData.includes('请输入用户名和密码')) {
|
||||||
|
showNotification('请输入用户名和密码', 'warning');
|
||||||
} else {
|
} else {
|
||||||
// 尝试解析为JSON
|
// 检查响应内容中是否包含成功信息
|
||||||
try {
|
if (responseData.includes('dashboard') || responseData.includes('仪表板')) {
|
||||||
const jsonData = JSON.parse(responseData);
|
// 登录成功
|
||||||
if (jsonData.success) {
|
window.location.href = '/dashboard';
|
||||||
window.location.href = jsonData.redirect || '/dashboard';
|
} else {
|
||||||
} else {
|
// 尝试解析为JSON
|
||||||
showNotification(jsonData.message || '登录失败,请检查用户名和密码', 'error');
|
try {
|
||||||
|
const jsonData = JSON.parse(responseData);
|
||||||
|
if (jsonData.success) {
|
||||||
|
window.location.href = jsonData.redirect || '/dashboard';
|
||||||
|
} else {
|
||||||
|
showNotification(jsonData.message || '登录失败,请检查用户名和密码', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 如果无法解析为JSON,显示通用错误消息
|
||||||
|
showNotification('登录失败,请检查用户名和密码', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
showNotification('登录失败,请检查用户名和密码', 'error');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,7 +148,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
} else {
|
} else {
|
||||||
showNotification('请求参数错误,请刷新页面后重试', 'error');
|
showNotification('请求参数错误,请刷新页面后重试', 'error');
|
||||||
}
|
}
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
// 认证失败
|
||||||
|
showNotification('用户名或密码错误,请重试', 'error');
|
||||||
|
} else if (response.status >= 500) {
|
||||||
|
// 服务器错误
|
||||||
|
showNotification('服务器内部错误,请稍后重试', 'error');
|
||||||
} else {
|
} else {
|
||||||
|
// 其他错误
|
||||||
showNotification('登录失败,请检查用户名和密码', 'error');
|
showNotification('登录失败,请检查用户名和密码', 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,4 +171,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -23,6 +23,13 @@
|
|||||||
<input type="email" class="form-control" id="admin_email" name="admin_email" value="{{ config.ADMIN_EMAIL or '' }}">
|
<input type="email" class="form-control" id="admin_email" name="admin_email" value="{{ config.ADMIN_EMAIL or '' }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="frontend_domain" class="form-label">前端域名</label>
|
||||||
|
<input type="text" class="form-control" id="frontend_domain" name="frontend_domain"
|
||||||
|
value="{{ config.FRONTEND_DOMAIN or '' }}">
|
||||||
|
<div class="form-text">前端API请求使用的域名,留空则使用当前访问域名</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="max_failed_attempts" class="form-label">最大失败尝试次数</label>
|
<label for="max_failed_attempts" class="form-label">最大失败尝试次数</label>
|
||||||
<input type="number" class="form-control" id="max_failed_attempts" name="max_failed_attempts"
|
<input type="number" class="form-control" id="max_failed_attempts" name="max_failed_attempts"
|
||||||
@ -366,6 +373,7 @@ function saveBasicSettings() {
|
|||||||
const formData = {
|
const formData = {
|
||||||
site_name: document.getElementById('site_name').value,
|
site_name: document.getElementById('site_name').value,
|
||||||
admin_email: document.getElementById('admin_email').value,
|
admin_email: document.getElementById('admin_email').value,
|
||||||
|
frontend_domain: document.getElementById('frontend_domain').value,
|
||||||
max_failed_attempts: parseInt(document.getElementById('max_failed_attempts').value),
|
max_failed_attempts: parseInt(document.getElementById('max_failed_attempts').value),
|
||||||
lockout_minutes: parseInt(document.getElementById('lockout_minutes').value),
|
lockout_minutes: parseInt(document.getElementById('lockout_minutes').value),
|
||||||
max_unbind_times: parseInt(document.getElementById('max_unbind_times').value),
|
max_unbind_times: parseInt(document.getElementById('max_unbind_times').value),
|
||||||
|
|||||||
16
config.py
16
config.py
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""基础配置类"""
|
"""基础配置类"""
|
||||||
@ -24,6 +25,7 @@ class Config:
|
|||||||
# 系统基本配置
|
# 系统基本配置
|
||||||
SITE_NAME = os.environ.get('SITE_NAME') or '软件授权管理系统'
|
SITE_NAME = os.environ.get('SITE_NAME') or '软件授权管理系统'
|
||||||
ADMIN_EMAIL = os.environ.get('ADMIN_EMAIL') or ''
|
ADMIN_EMAIL = os.environ.get('ADMIN_EMAIL') or ''
|
||||||
|
FRONTEND_DOMAIN = os.environ.get('FRONTEND_DOMAIN') or '' # 前端域名配置
|
||||||
|
|
||||||
# 验证器配置
|
# 验证器配置
|
||||||
AUTH_SECRET_KEY = os.environ.get('AUTH_SECRET_KEY') or 'auth-validator-secret-key'
|
AUTH_SECRET_KEY = os.environ.get('AUTH_SECRET_KEY') or 'auth-validator-secret-key'
|
||||||
@ -56,11 +58,12 @@ class Config:
|
|||||||
if not os.path.exists('logs'):
|
if not os.path.exists('logs'):
|
||||||
os.mkdir('logs')
|
os.mkdir('logs')
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志 - 使用时间轮转而不是大小轮转来避免Windows文件锁定问题
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
file_handler = RotatingFileHandler('logs/kamaxitong.log', maxBytes=10240, backupCount=10)
|
# 使用时间轮转日志处理器,每天轮转一次
|
||||||
|
file_handler = TimedRotatingFileHandler('logs/kamaxitong.log', when='midnight', interval=1, backupCount=10, encoding='utf-8')
|
||||||
file_handler.setFormatter(logging.Formatter(
|
file_handler.setFormatter(logging.Formatter(
|
||||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||||||
))
|
))
|
||||||
@ -86,7 +89,7 @@ class ProductionConfig(Config):
|
|||||||
|
|
||||||
# 生产环境日志配置
|
# 生产环境日志配置
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
if not app.debug:
|
if not app.debug:
|
||||||
# 确保日志目录存在
|
# 确保日志目录存在
|
||||||
@ -94,7 +97,8 @@ class ProductionConfig(Config):
|
|||||||
if not os.path.exists('logs'):
|
if not os.path.exists('logs'):
|
||||||
os.mkdir('logs')
|
os.mkdir('logs')
|
||||||
|
|
||||||
file_handler = RotatingFileHandler('logs/kamaxitong.log', maxBytes=10240, backupCount=10)
|
# 使用时间轮转日志处理器,每天轮转一次
|
||||||
|
file_handler = TimedRotatingFileHandler('logs/kamaxitong.log', when='midnight', interval=1, backupCount=10, encoding='utf-8')
|
||||||
file_handler.setFormatter(logging.Formatter(
|
file_handler.setFormatter(logging.Formatter(
|
||||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||||||
))
|
))
|
||||||
|
|||||||
@ -29,23 +29,12 @@
|
|||||||
[parameters: {'admin_id': 1, 'action': 'GENERATE_LICENSES', 'target_type': 'LICENSE', 'target_id': None, 'details': {'product_id': 'ArticleReplace', 'count': 1, 'license_type': 1, 'license_keys': ['4SGGNAPF-HPGNQC1Z-6D7OH879-9BGW32PI']}, 'ip_address': '127.0.0.1', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0', 'create_time': datetime.datetime(2025, 11, 16, 5, 7, 9, 662553)}]
|
[parameters: {'admin_id': 1, 'action': 'GENERATE_LICENSES', 'target_type': 'LICENSE', 'target_id': None, 'details': {'product_id': 'ArticleReplace', 'count': 1, 'license_type': 1, 'license_keys': ['4SGGNAPF-HPGNQC1Z-6D7OH879-9BGW32PI']}, 'ip_address': '127.0.0.1', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0', 'create_time': datetime.datetime(2025, 11, 16, 5, 7, 9, 662553)}]
|
||||||
(Background on this error at: https://sqlalche.me/e/20/f405) [in D:\work\code\python\KaMiXiTong\master\app\models\audit_log.py:59]
|
(Background on this error at: https://sqlalche.me/e/20/f405) [in D:\work\code\python\KaMiXiTong\master\app\models\audit_log.py:59]
|
||||||
2025-11-16 13:07:44,918 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 13:07:44,918 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
||||||
2025-11-16 13:10:12,691 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 17:40:11,814 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
||||||
2025-11-16 13:11:09,687 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 17:40:13,787 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
||||||
2025-11-16 13:14:50,942 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 18:01:08,089 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
||||||
2025-11-16 13:15:22,908 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 18:02:10,913 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
||||||
2025-11-16 13:15:40,597 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 18:06:15,046 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
||||||
2025-11-16 13:17:33,606 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 18:11:51,110 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
||||||
2025-11-16 13:18:26,492 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 19:05:00,160 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:74]
|
||||||
2025-11-16 13:18:38,007 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 19:05:02,504 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:74]
|
||||||
2025-11-16 13:18:49,532 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
2025-11-16 19:05:57,986 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:74]
|
||||||
2025-11-16 13:21:42,076 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 13:21:54,735 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 13:24:17,919 ERROR: 更新卡密失败: 'License' object has no attribute 'remark' [in D:\work\code\python\KaMiXiTong\master\app\api\license.py:270]
|
|
||||||
2025-11-16 13:24:20,319 ERROR: 记录审计日志失败: (pymysql.err.ProgrammingError) (1064, 'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'\'license_key\': "\'4SGGNAPF-HPGNQC1Z-6D7OH879-9BGW32PI\'"}, \'127.0.0.1\', \'Mozilla/5\' at line 1')
|
|
||||||
[SQL: INSERT INTO audit_log (admin_id, action, target_type, target_id, details, ip_address, user_agent, create_time) VALUES (%(admin_id)s, %(action)s, %(target_type)s, %(target_id)s, %(details)s, %(ip_address)s, %(user_agent)s, %(create_time)s)]
|
|
||||||
[parameters: {'admin_id': 1, 'action': 'DELETE_LICENSE', 'target_type': 'LICENSE', 'target_id': None, 'details': {'license_key': '4SGGNAPF-HPGNQC1Z-6D7OH879-9BGW32PI'}, 'ip_address': '127.0.0.1', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0', 'create_time': datetime.datetime(2025, 11, 16, 5, 24, 20, 318782)}]
|
|
||||||
(Background on this error at: https://sqlalche.me/e/20/f405) [in D:\work\code\python\KaMiXiTong\master\app\models\audit_log.py:59]
|
|
||||||
2025-11-16 13:24:53,054 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 13:26:10,998 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 13:32:27,257 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 13:32:51,116 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
2025-11-15 23:58:20,986 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-15 23:58:22,596 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 11:34:44,087 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 11:34:48,148 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 11:36:10,246 ERROR: 记录审计日志失败: (pymysql.err.ProgrammingError) (1064, 'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'\'product_name\': "\'测试产品B\'"}, \'127.0.0.1\', \'Mozilla/5.0 (Windows NT 10.0; \' at line 1')
|
|
||||||
[SQL: INSERT INTO audit_log (admin_id, action, target_type, target_id, details, ip_address, user_agent, create_time) VALUES (%(admin_id)s, %(action)s, %(target_type)s, %(target_id)s, %(details)s, %(ip_address)s, %(user_agent)s, %(create_time)s)]
|
|
||||||
[parameters: {'admin_id': 1, 'action': 'DELETE_PRODUCT', 'target_type': 'PRODUCT', 'target_id': 'PROD_23EF726D', 'details': {'product_name': '测试产品B'}, 'ip_address': '127.0.0.1', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0', 'create_time': datetime.datetime(2025, 11, 16, 3, 36, 10, 245687)}]
|
|
||||||
(Background on this error at: https://sqlalche.me/e/20/f405) [in D:\work\code\python\KaMiXiTong\master\app\models\audit_log.py:59]
|
|
||||||
2025-11-16 11:36:15,609 ERROR: 记录审计日志失败: (pymysql.err.ProgrammingError) (1064, 'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'\'product_name\': "\'测试产品A\'"}, \'127.0.0.1\', \'Mozilla/5.0 (Windows NT 10.0; \' at line 1')
|
|
||||||
[SQL: INSERT INTO audit_log (admin_id, action, target_type, target_id, details, ip_address, user_agent, create_time) VALUES (%(admin_id)s, %(action)s, %(target_type)s, %(target_id)s, %(details)s, %(ip_address)s, %(user_agent)s, %(create_time)s)]
|
|
||||||
[parameters: {'admin_id': 1, 'action': 'DELETE_PRODUCT', 'target_type': 'PRODUCT', 'target_id': 'PROD_897EF967', 'details': {'product_name': '测试产品A'}, 'ip_address': '127.0.0.1', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0', 'create_time': datetime.datetime(2025, 11, 16, 3, 36, 15, 608798)}]
|
|
||||||
(Background on this error at: https://sqlalche.me/e/20/f405) [in D:\work\code\python\KaMiXiTong\master\app\models\audit_log.py:59]
|
|
||||||
2025-11-16 11:36:28,535 ERROR: 记录审计日志失败: (pymysql.err.ProgrammingError) (1064, 'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'\'license_key\': "\'W7XGEZU0-MFLWWKUK-XWPWFW3V-0LE77N2K\'"}, \'127.0.0.1\', \'Mozilla/5\' at line 1')
|
|
||||||
[SQL: INSERT INTO audit_log (admin_id, action, target_type, target_id, details, ip_address, user_agent, create_time) VALUES (%(admin_id)s, %(action)s, %(target_type)s, %(target_id)s, %(details)s, %(ip_address)s, %(user_agent)s, %(create_time)s)]
|
|
||||||
[parameters: {'admin_id': 1, 'action': 'DELETE_LICENSE', 'target_type': 'LICENSE', 'target_id': None, 'details': {'license_key': 'W7XGEZU0-MFLWWKUK-XWPWFW3V-0LE77N2K'}, 'ip_address': '127.0.0.1', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0', 'create_time': datetime.datetime(2025, 11, 16, 3, 36, 28, 535307)}]
|
|
||||||
(Background on this error at: https://sqlalche.me/e/20/f405) [in D:\work\code\python\KaMiXiTong\master\app\models\audit_log.py:59]
|
|
||||||
2025-11-16 11:36:34,617 ERROR: 记录审计日志失败: (pymysql.err.ProgrammingError) (1064, 'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'\'product_name\': "\'测试产品C\'"}, \'127.0.0.1\', \'Mozilla/5.0 (Windows NT 10.0; \' at line 1')
|
|
||||||
[SQL: INSERT INTO audit_log (admin_id, action, target_type, target_id, details, ip_address, user_agent, create_time) VALUES (%(admin_id)s, %(action)s, %(target_type)s, %(target_id)s, %(details)s, %(ip_address)s, %(user_agent)s, %(create_time)s)]
|
|
||||||
[parameters: {'admin_id': 1, 'action': 'DELETE_PRODUCT', 'target_type': 'PRODUCT', 'target_id': 'PROD_A24B55D2', 'details': {'product_name': '测试产品C'}, 'ip_address': '127.0.0.1', 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0', 'create_time': datetime.datetime(2025, 11, 16, 3, 36, 34, 616101)}]
|
|
||||||
(Background on this error at: https://sqlalche.me/e/20/f405) [in D:\work\code\python\KaMiXiTong\master\app\models\audit_log.py:59]
|
|
||||||
2025-11-16 12:12:41,268 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:13:10,962 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:13:13,050 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:14:26,744 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:14:34,528 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:14:44,691 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:56,754 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,016 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,016 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,024 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,024 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,024 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,039 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,039 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,039 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,039 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,092 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,092 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,092 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,092 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,092 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,343 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,343 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,343 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,343 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,343 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,343 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,372 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,372 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,372 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,372 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,372 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,372 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:57,372 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:16:59,517 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:17:24,098 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:21:22,890 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:21:27,735 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:21:31,439 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:21:40,107 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:21:43,784 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:21:53,199 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:22:01,231 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:22:03,158 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:22:05,098 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:22:07,063 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:22:11,957 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:22:59,514 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:23:01,359 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:32:48,638 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
2025-11-16 12:33:31,339 INFO: KaMiXiTong startup [in D:\work\code\python\KaMiXiTong\master\config.py:71]
|
|
||||||
4
run.py
4
run.py
@ -32,7 +32,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
# 运行应用
|
# 运行应用
|
||||||
app.run(
|
app.run(
|
||||||
host='0.0.0.0',
|
host=os.environ.get('HOST', '0.0.0.0'),
|
||||||
port=5000,
|
port=int(os.environ.get('PORT', 5000)),
|
||||||
debug=app.config.get('DEBUG', False)
|
debug=app.config.get('DEBUG', False)
|
||||||
)
|
)
|
||||||
|
|||||||
21
start.py
21
start.py
@ -102,8 +102,8 @@ def start_flask_app():
|
|||||||
from run import app
|
from run import app
|
||||||
print("🚀 正在启动Flask应用...")
|
print("🚀 正在启动Flask应用...")
|
||||||
app.run(
|
app.run(
|
||||||
host='127.0.0.1',
|
host=os.environ.get('HOST', '127.0.0.1'),
|
||||||
port=5000,
|
port=int(os.environ.get('PORT', 5000)),
|
||||||
debug=True,
|
debug=True,
|
||||||
use_reloader=False # 禁用重载器以避免子进程问题
|
use_reloader=False # 禁用重载器以避免子进程问题
|
||||||
)
|
)
|
||||||
@ -120,11 +120,14 @@ def start_fastapi_app():
|
|||||||
|
|
||||||
print("🚀 正在启动FastAPI应用...")
|
print("🚀 正在启动FastAPI应用...")
|
||||||
# 使用uvicorn启动FastAPI应用
|
# 使用uvicorn启动FastAPI应用
|
||||||
|
fastapi_host = os.environ.get('FASTAPI_HOST', '127.0.0.1')
|
||||||
|
fastapi_port = os.environ.get('FASTAPI_PORT', '9000')
|
||||||
|
|
||||||
subprocess.run([
|
subprocess.run([
|
||||||
sys.executable, '-m', 'uvicorn',
|
sys.executable, '-m', 'uvicorn',
|
||||||
'fastapi_app:app',
|
'fastapi_app:app',
|
||||||
'--host', '127.0.0.1',
|
'--host', fastapi_host,
|
||||||
'--port', '9000'
|
'--port', fastapi_port
|
||||||
])
|
])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ FastAPI应用启动失败: {e}")
|
print(f"❌ FastAPI应用启动失败: {e}")
|
||||||
@ -157,9 +160,13 @@ def main():
|
|||||||
print("\n" + "=" * 50)
|
print("\n" + "=" * 50)
|
||||||
print("🚀 启动开发服务器...")
|
print("🚀 启动开发服务器...")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("📍 Flask访问地址: http://localhost:5000")
|
host = os.environ.get('HOST', '127.0.0.1')
|
||||||
print("📍 FastAPI访问地址: http://localhost:9000")
|
port = os.environ.get('PORT', '5000')
|
||||||
print("📍 FastAPI文档: http://localhost:9000/docs")
|
fastapi_port = os.environ.get('FASTAPI_PORT', '9000')
|
||||||
|
|
||||||
|
print(f"📍 Flask访问地址: http://{host}:{port}")
|
||||||
|
print(f"📍 FastAPI访问地址: http://{host}:{fastapi_port}")
|
||||||
|
print(f"📍 FastAPI文档: http://{host}:{fastapi_port}/docs")
|
||||||
print("👤 管理员账号: admin")
|
print("👤 管理员账号: admin")
|
||||||
print("🔑 管理员密码: admin123")
|
print("🔑 管理员密码: admin123")
|
||||||
print("⏹️ 按 Ctrl+C 停止服务器")
|
print("⏹️ 按 Ctrl+C 停止服务器")
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
// 自定义JavaScript函数
|
// 自定义JavaScript函数
|
||||||
|
|
||||||
|
// 设置前端域名全局变量
|
||||||
|
if (typeof FRONTEND_DOMAIN !== 'undefined') {
|
||||||
|
window.FRONTEND_DOMAIN = FRONTEND_DOMAIN;
|
||||||
|
}
|
||||||
|
|
||||||
// 显示加载动画
|
// 显示加载动画
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
const loadingElement = document.getElementById('loading');
|
const loadingElement = document.getElementById('loading');
|
||||||
@ -16,28 +21,6 @@ function hideLoading() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示通知
|
|
||||||
function showNotification(message, type = 'info') {
|
|
||||||
// 创建通知元素
|
|
||||||
const alertDiv = document.createElement('div');
|
|
||||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
|
||||||
alertDiv.innerHTML = `
|
|
||||||
${message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 插入到页面中
|
|
||||||
const container = document.querySelector('main') || document.body;
|
|
||||||
container.insertBefore(alertDiv, container.firstChild);
|
|
||||||
|
|
||||||
// 5秒后自动隐藏
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alertDiv.parentNode) {
|
|
||||||
alertDiv.remove();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
function formatDate(dateString) {
|
function formatDate(dateString) {
|
||||||
if (!dateString) return '-';
|
if (!dateString) return '-';
|
||||||
@ -56,16 +39,18 @@ function formatFileSize(bytes) {
|
|||||||
|
|
||||||
// API请求函数 - 添加认证支持
|
// API请求函数 - 添加认证支持
|
||||||
function apiRequest(url, options = {}) {
|
function apiRequest(url, options = {}) {
|
||||||
// 确保URL是完整的路径
|
// 自动构建完整的API URL
|
||||||
let fullUrl = url;
|
let fullUrl = url;
|
||||||
if (!url.startsWith('/')) {
|
|
||||||
fullUrl = '/' + url;
|
// 检查是否配置了前端域名
|
||||||
}
|
const frontendDomain = window.FRONTEND_DOMAIN || '';
|
||||||
if (!url.startsWith('/api/')) {
|
|
||||||
fullUrl = '/api/v1' + fullUrl;
|
if (url.startsWith('/')) {
|
||||||
}
|
// 如果是相对路径,则使用配置的域名或当前主机
|
||||||
if (!url.startsWith('/')) {
|
fullUrl = (frontendDomain || window.location.origin) + url;
|
||||||
fullUrl = '/' + fullUrl;
|
} else if (!url.startsWith('http')) {
|
||||||
|
// 如果不是完整URL且不以http开头,则添加API前缀
|
||||||
|
fullUrl = (frontendDomain || window.location.origin) + '/api/v1/' + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置默认选项
|
// 设置默认选项
|
||||||
|
|||||||
@ -74,7 +74,7 @@ class TestAuthValidator(unittest.TestCase):
|
|||||||
self.software_id = "TEST_SOFTWARE_ID"
|
self.software_id = "TEST_SOFTWARE_ID"
|
||||||
self.validator = AuthValidator(
|
self.validator = AuthValidator(
|
||||||
software_id=self.software_id,
|
software_id=self.software_id,
|
||||||
api_url="http://localhost:5000/api/v1",
|
api_url="http://km.taisan.online/api/v1",
|
||||||
timeout=1 # 快速超时
|
timeout=1 # 快速超时
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user