修复页面没数据的错误

This commit is contained in:
taiyi 2025-11-15 23:57:05 +08:00
parent ebeb56a230
commit 7f6cf6e624
25 changed files with 1064 additions and 382 deletions

164
README.md
View File

@ -1,53 +1,26 @@
# 软件及卡密管理系统 # 软件授权管理系统 (KaMiXiTong)
一款面向Python开发者的轻量化软件授权控制+全生命周期管理系统,核心解决"Python软件盗版防护、卡密授权管控、软件版本迭代管理"三大核心需求 一款基于Python Flask的软件授权管理系统支持卡密生成、设备绑定、在线验证等功能
## 项目结构 ## 功能特性
- 🎯 **卡密管理**: 支持批量生成、导入导出卡密
- 🔐 **设备绑定**: 防止账号共享,支持解绑操作
- 🔄 **在线验证**: 实时验证卡密有效性
- 📊 **数据统计**: 可视化展示激活趋势和统计数据
- 🛡️ **安全防护**: AES加密传输防抓包破解
- 🌐 **多产品支持**: 一个系统管理多个软件产品
- 📱 **响应式界面**: 支持PC和移动设备访问
## 目录结构
``` ```
KaMiXiTong/ KaMiXiTong/
├── README.md # 项目说明文档
├── requirements.txt # Python依赖包
├── setup.py # 安装脚本
├── config.py # 配置文件
├── run.py # 启动入口
├── auth_validator.py # Python验证器模块供开发者嵌入
├── app/ # Web管理后台 ├── app/ # Web管理后台
│ ├── __init__.py
│ ├── models/ # 数据模型 │ ├── models/ # 数据模型
│ │ ├── __init__.py
│ │ ├── product.py # 产品模型
│ │ ├── version.py # 版本模型
│ │ ├── license.py # 许可证(卡密)模型
│ │ ├── device.py # 设备模型
│ │ ├── ticket.py # 工单模型
│ │ └── admin.py # 管理员模型
│ ├── api/ # API接口 │ ├── api/ # API接口
│ │ ├── __init__.py
│ │ ├── auth.py # 验证相关API
│ │ ├── product.py # 产品管理API
│ │ ├── version.py # 版本管理API
│ │ ├── license.py # 卡密管理API
│ │ ├── device.py # 设备管理API
│ │ ├── ticket.py # 工单管理API
│ │ └── statistics.py # 统计分析API
│ ├── web/ # Web界面 │ ├── web/ # Web界面
│ │ ├── __init__.py
│ │ ├── views.py # 页面路由
│ │ └── templates/ # HTML模板
│ │ ├── base.html
│ │ ├── login.html
│ │ ├── dashboard.html
│ │ ├── product/
│ │ ├── version/
│ │ ├── license/
│ │ ├── device/
│ │ └── ticket/
│ └── utils/ # 工具函数 │ └── utils/ # 工具函数
│ ├── __init__.py
│ ├── crypto.py # 加密工具
│ ├── machine_code.py # 机器码生成
│ └── validators.py # 验证工具
├── tests/ # 测试文件 ├── tests/ # 测试文件
│ ├── __init__.py │ ├── __init__.py
│ ├── test_validator.py # 验证器测试 │ ├── test_validator.py # 验证器测试
@ -56,83 +29,84 @@ KaMiXiTong/
├── docs/ # 文档 ├── docs/ # 文档
│ ├── API.md # API文档 │ ├── API.md # API文档
│ ├── INTEGRATION.md # 集成文档 │ ├── INTEGRATION.md # 集成文档
│ └── EXAMPLES.md # 使用示例 │ ├── EXAMPLES.md # 使用示例
│ └── FASTAPI.md # FastAPI接口文档
├── migrations/ # 数据库迁移文件 ├── migrations/ # 数据库迁移文件
│ └── init.sql │ └── init.sql
└── static/ # 静态文件 └── static/ # 静态文件
├── css/
├── js/
└── uploads/
``` ```
## 核心功能 ## 使用方式
### 1. Python验证器 (auth_validator.py) ### 快速开始
- 轻量级授权验证模块开发者可嵌入自有Python软件
- 支持在线/离线验证模式
- 跨平台机器码生成
- 防篡改机制
### 2. Web管理后台
- 产品管理:创建、编辑、删除软件产品
- 版本管理:版本发布、兼容性控制
- 卡密管理:批量生成、导入导出、状态控制
- 设备管理:设备绑定、解绑、禁用
- 数据统计:下载量、活跃度分析
- 工单管理:用户反馈处理
## 快速开始
### 1. 安装依赖
```bash ```bash
# 克隆项目
git clone https://github.com/yourusername/kamaxitong.git
# 进入项目目录
cd kamaxitong
# 安装依赖
pip install -r requirements.txt pip install -r requirements.txt
```
### 2. 初始化数据库 # 初始化数据库
#### SQLite数据库默认
```bash
python init_db_sqlite.py python init_db_sqlite.py
```
#### MySQL数据库 # 启动服务
1. 配置.env文件中的DATABASE_URL为MySQL连接字符串
```
DATABASE_URL=mysql+pymysql://username:password@localhost:3306/database_name
```
2. 运行MySQL初始化脚本
```bash
python init_db_mysql.py
```
### 3. 启动后台
```bash
python run.py python run.py
``` ```
### 4. 嵌入验证器 访问地址: http://localhost:5000
```python 默认账号: admin / admin123
from auth_validator import AuthValidator
validator = AuthValidator(software_id="your_software_id") ### FastAPI接口
if not validator.validate():
exit() # 验证失败退出程序 系统还提供了现代化的FastAPI接口具有自动生成的交互式文档
```bash
# 安装FastAPI依赖
pip install -r requirements-fastapi.txt
# 启动FastAPI服务
python fastapi_app.py
```
FastAPI文档地址: http://localhost:8000/docs
## 部署说明
### 开发环境
```bash
python start.py
```
### 生产环境
```bash
# 使用Gunicorn
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5000 run:app
# 使用Nginx反向代理
# 配置SSL证书
# 设置防火墙规则
``` ```
## 技术栈 ## 技术栈
- **后端**: Flask + SQLAlchemy + MySQL/SQLite - 后端: Flask + SQLAlchemy
- **前端**: Bootstrap + jQuery - 前端: Bootstrap 5 + jQuery
- **加密**: AES + SHA256 - 数据库: SQLite/MySQL
- **验证器**: 纯Python支持Python 3.6+ - 部署: Gunicorn + Nginx
## 安全特性 ## 文档
- HTTPS通信加密 - [API文档](docs/API.md)
- 数据库敏感信息加密存储 - [集成指南](docs/INTEGRATION.md)
- 验证器防篡改机制 - [使用示例](docs/EXAMPLES.md)
- 机器码唯一性校验 - [FastAPI接口文档](docs/FASTAPI.md)
- 防重放攻击
## 许可证 ## 许可证

View File

@ -5,6 +5,8 @@ from flask_login import LoginManager
from flask_migrate import Migrate from flask_migrate import Migrate
from config import config from config import config
from flask_wtf.csrf import CSRFProtect from flask_wtf.csrf import CSRFProtect
import logging
from logging.handlers import RotatingFileHandler
# 初始化扩展 # 初始化扩展
db = SQLAlchemy() db = SQLAlchemy()
@ -65,4 +67,21 @@ def create_app(config_name='default'):
from app.web.views import register_error_handlers from app.web.views import register_error_handlers
register_error_handlers(app) register_error_handlers(app)
# 配置日志
if not app.debug and not app.testing:
# 确保日志目录存在
if not os.path.exists('logs'):
os.mkdir('logs')
# 配置文件日志处理器
file_handler = RotatingFileHandler('logs/kamaxitong.log', maxBytes=10240, backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('KaMiXiTong startup')
return app return app

View File

@ -4,4 +4,4 @@ from flask import Blueprint
api_bp = Blueprint('api', __name__) api_bp = Blueprint('api', __name__)
# 导入所有API路由 # 导入所有API路由
from . import auth, product, version, license, device, ticket, statistics, settings, admin from . import auth, product, version, license, device, ticket, statistics, settings, admin, log

View File

@ -7,6 +7,7 @@ from werkzeug.security import generate_password_hash
import functools import functools
import re import re
from .decorators import require_admin from .decorators import require_admin
from app.utils.logger import log_operation
# 响应码定义 # 响应码定义
class ResponseCode: class ResponseCode:
@ -235,6 +236,13 @@ def create_admin():
'status': status 'status': status
}) })
# 记录操作日志
log_operation('CREATE_ADMIN', 'ADMIN', admin.admin_id, {
'username': username,
'role': role,
'status': status
})
return create_response(True, data=admin.to_dict(), message='管理员创建成功') return create_response(True, data=admin.to_dict(), message='管理员创建成功')
except Exception as e: except Exception as e:
@ -307,6 +315,16 @@ def update_admin(admin_id):
} }
}) })
# 记录操作日志
log_operation('UPDATE_ADMIN', 'ADMIN', admin_id, {
'old': old_data,
'new': {
'email': admin.email,
'role': admin.role,
'status': admin.status
}
})
return create_response(True, data=admin.to_dict(), message='管理员更新成功') return create_response(True, data=admin.to_dict(), message='管理员更新成功')
except Exception as e: except Exception as e:
@ -341,6 +359,12 @@ def toggle_admin_status(admin_id):
'new_status': admin.status 'new_status': admin.status
}) })
# 记录操作日志
log_operation('TOGGLE_ADMIN_STATUS', 'ADMIN', admin_id, {
'old_status': old_status,
'new_status': admin.status
})
return create_response(True, data={ return create_response(True, data={
'status': admin.status, 'status': admin.status,
'status_name': status_name 'status_name': status_name
@ -374,6 +398,11 @@ def delete_admin(admin_id):
'username': admin.username 'username': admin.username
}) })
# 记录操作日志
log_operation('DELETE_ADMIN', 'ADMIN', admin_id, {
'username': admin.username
})
return create_response(True, message='管理员删除成功') return create_response(True, message='管理员删除成功')
except Exception as e: except Exception as e:

View File

@ -3,39 +3,12 @@ from datetime import datetime
from app import db from app import db
from app.models import Device, Product, License from app.models import Device, Product, License
from . import api_bp from . import api_bp
from .decorators import require_admin from .decorators import require_login, require_admin
from sqlalchemy import func, or_ from sqlalchemy import func, or_
from app.utils.logger import log_operation
# 简化版权限验证装饰器
def check_admin_permission(f):
"""简化版管理员权限验证"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
from flask_login import current_user
from flask import current_app, jsonify
# 检查用户是否已认证
if not hasattr(current_user, 'is_authenticated') or not current_user.is_authenticated:
return jsonify({
'success': False,
'message': '需要登录'
}), 401
# 检查是否为管理员
if not hasattr(current_user, 'is_super_admin') or not current_user.is_super_admin():
return jsonify({
'success': False,
'message': '需要管理员权限'
}), 403
return f(*args, **kwargs)
return decorated_function
# 将所有使用require_admin的地方替换为check_admin_permission
@api_bp.route('/devices', methods=['GET']) @api_bp.route('/devices', methods=['GET'])
@require_admin @require_login
def get_devices(): def get_devices():
"""获取设备列表""" """获取设备列表"""
try: try:
@ -48,7 +21,8 @@ def get_devices():
product = request.args.get('product') product = request.args.get('product')
license_key = request.args.get('license') license_key = request.args.get('license')
query = Device.query.join(Product, Device.product_id == Product.product_id) # 使用join预加载Product和License关系避免N+1查询
query = db.session.query(Device).join(Product, Device.product_id == Product.product_id).outerjoin(License, Device.license_id == License.license_id)
# 添加搜索条件 # 添加搜索条件
if product_id: if product_id:
@ -72,7 +46,6 @@ def get_devices():
) )
) )
if license_key and license_key.strip(): if license_key and license_key.strip():
query = query.join(License, Device.license_id == License.license_id)
query = query.filter(db.cast(License.license_key, db.String).like(f'%{license_key}%')) query = query.filter(db.cast(License.license_key, db.String).like(f'%{license_key}%'))
query = query.order_by(db.desc(Device.last_verify_time)) query = query.order_by(db.desc(Device.last_verify_time))
@ -100,7 +73,7 @@ def get_devices():
return jsonify({'success': False, 'message': '服务器内部错误'}), 500 return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/devices/<int:device_id>/status', methods=['PUT']) @api_bp.route('/devices/<int:device_id>/status', methods=['PUT'])
@require_admin @require_login
def update_device_status(device_id): def update_device_status(device_id):
"""更新设备状态""" """更新设备状态"""
try: try:
@ -120,6 +93,11 @@ def update_device_status(device_id):
device.status = status device.status = status
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('UPDATE_DEVICE_STATUS', 'DEVICE', device.device_id, {
'status': status
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '设备状态更新成功', 'message': '设备状态更新成功',
@ -132,7 +110,7 @@ def update_device_status(device_id):
return jsonify({'success': False, 'message': '服务器内部错误'}), 500 return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/devices/<int:device_id>', methods=['DELETE']) @api_bp.route('/devices/<int:device_id>', methods=['DELETE'])
@require_admin @require_login
def delete_device(device_id): def delete_device(device_id):
"""删除设备""" """删除设备"""
try: try:
@ -143,6 +121,11 @@ def delete_device(device_id):
db.session.delete(device) db.session.delete(device)
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('DELETE_DEVICE', 'DEVICE', device.device_id, {
'machine_code': device.machine_code
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '设备删除成功' 'message': '设备删除成功'
@ -155,7 +138,7 @@ def delete_device(device_id):
@api_bp.route('/devices/batch', methods=['DELETE']) @api_bp.route('/devices/batch', methods=['DELETE'])
@require_admin @require_login
def batch_delete_devices(): def batch_delete_devices():
"""批量删除设备""" """批量删除设备"""
try: try:
@ -189,6 +172,13 @@ def batch_delete_devices():
db.session.commit() db.session.commit()
# 记录操作日志
device_ids = [device.device_id for device in devices]
log_operation('BATCH_DELETE_DEVICES', 'DEVICE', None, {
'device_ids': device_ids,
'count': len(devices)
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'成功删除 {len(devices)} 个设备' 'message': f'成功删除 {len(devices)} 个设备'
@ -204,7 +194,7 @@ def batch_delete_devices():
@api_bp.route('/devices/batch/status', methods=['PUT']) @api_bp.route('/devices/batch/status', methods=['PUT'])
@require_admin @require_login
def batch_update_device_status(): def batch_update_device_status():
"""批量更新设备状态""" """批量更新设备状态"""
try: try:
@ -246,6 +236,14 @@ def batch_update_device_status():
db.session.commit() db.session.commit()
# 记录操作日志
device_ids = [device.device_id for device in devices]
log_operation('BATCH_UPDATE_DEVICE_STATUS', 'DEVICE', None, {
'device_ids': device_ids,
'status': status,
'count': len(devices)
})
status_names = {0: '禁用', 1: '正常', 2: '离线'} status_names = {0: '禁用', 1: '正常', 2: '离线'}
status_name = status_names.get(status, '未知') status_name = status_names.get(status, '未知')
return jsonify({ return jsonify({

View File

@ -3,15 +3,16 @@ from datetime import datetime, timedelta
from app import db from app import db
from app.models import Product, License from app.models import Product, License
from . import api_bp from . import api_bp
from .decorators import require_admin from .decorators import require_login, require_admin
import io import io
import csv import csv
import xlsxwriter import xlsxwriter
from functools import wraps from functools import wraps
from sqlalchemy import or_, func from sqlalchemy import or_, func
from app.utils.logger import log_operation
@api_bp.route('/licenses', methods=['GET']) @api_bp.route('/licenses', methods=['GET'])
@require_admin @require_login
def get_licenses(): def get_licenses():
"""获取卡密列表""" """获取卡密列表"""
try: try:
@ -22,28 +23,34 @@ def get_licenses():
status = request.args.get('status', type=int) status = request.args.get('status', type=int)
license_type = request.args.get('type', type=int) license_type = request.args.get('type', type=int)
keyword = request.args.get('keyword', '').strip() keyword = request.args.get('keyword', '').strip()
sort = request.args.get('sort', 'create_time') # 排序字段
order = request.args.get('order', 'desc') # 排序方向
# 构建查询 # 构建查询 - 使用join预加载product关系避免N+1查询
query = License.query query = db.session.query(License).join(Product, License.product_id == Product.product_id)
# 产品筛选 # 产品筛选
if product_id: if product_id:
query = query.filter_by(product_id=product_id) query = query.filter(License.product_id == product_id)
# 状态筛选 # 状态筛选
if status is not None: if status is not None:
query = query.filter_by(status=status) query = query.filter(License.status == status)
# 类型筛选 # 类型筛选
if license_type is not None: if license_type is not None:
query = query.filter_by(type=license_type) query = query.filter(License.type == license_type)
# 关键词搜索(卡密) # 关键词搜索(卡密)
if keyword: if keyword:
query = query.filter(func.lower(License.license_key).like(f'%{keyword.lower()}%')) query = query.filter(func.lower(License.license_key).like(f'%{keyword.lower()}%'))
# 按创建时间倒序 # 排序处理
query = query.order_by(License.create_time.desc()) sort_field = getattr(License, sort, License.create_time) # 默认按创建时间
if order.lower() == 'asc':
query = query.order_by(sort_field.asc())
else:
query = query.order_by(sort_field.desc())
# 分页 # 分页
pagination = query.paginate( pagination = query.paginate(
@ -52,7 +59,7 @@ def get_licenses():
error_out=False error_out=False
) )
# 格式化结果 # 格式化结果 - product关系已经通过join加载
licenses = [license.to_dict() for license in pagination.items] licenses = [license.to_dict() for license in pagination.items]
return jsonify({ return jsonify({
@ -78,7 +85,7 @@ def get_licenses():
}), 500 }), 500
@api_bp.route('/licenses', methods=['POST']) @api_bp.route('/licenses', methods=['POST'])
@require_admin @require_login
def generate_licenses(): def generate_licenses():
"""批量生成卡密""" """批量生成卡密"""
try: try:
@ -153,6 +160,15 @@ def generate_licenses():
# 格式化结果 # 格式化结果
license_data = [license.to_dict() for license in licenses] license_data = [license.to_dict() for license in licenses]
# 记录操作日志
license_keys = [license.license_key for license in licenses]
log_operation('GENERATE_LICENSES', 'LICENSE', None, {
'product_id': product_id,
'count': count,
'license_type': license_type,
'license_keys': license_keys
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'成功生成 {count} 个卡密', 'message': f'成功生成 {count} 个卡密',
@ -178,7 +194,7 @@ def generate_licenses():
}), 500 }), 500
@api_bp.route('/licenses/<int:license_id>', methods=['GET']) @api_bp.route('/licenses/<int:license_id>', methods=['GET'])
@require_admin @require_login
def get_license(license_id): def get_license(license_id):
"""获取单个卡密详情""" """获取单个卡密详情"""
try: try:
@ -202,7 +218,7 @@ def get_license(license_id):
}), 500 }), 500
@api_bp.route('/licenses/<int:license_id>', methods=['PUT']) @api_bp.route('/licenses/<int:license_id>', methods=['PUT'])
@require_admin @require_login
def update_license(license_id): def update_license(license_id):
"""更新卡密信息""" """更新卡密信息"""
try: try:
@ -237,6 +253,12 @@ def update_license(license_id):
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('UPDATE_LICENSE', 'LICENSE', license_obj.license_id, {
'status': license_obj.status,
'remark': license_obj.remark
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '更新成功', 'message': '更新成功',
@ -252,7 +274,7 @@ def update_license(license_id):
}), 500 }), 500
@api_bp.route('/licenses/<int:license_id>/extend', methods=['POST']) @api_bp.route('/licenses/<int:license_id>/extend', methods=['POST'])
@require_admin @require_login
def extend_license(license_id): def extend_license(license_id):
"""延长卡密有效期""" """延长卡密有效期"""
try: try:
@ -284,6 +306,12 @@ def extend_license(license_id):
'message': message 'message': message
}), 400 }), 400
# 记录操作日志
log_operation('EXTEND_LICENSE', 'LICENSE', license_obj.license_id, {
'days': days,
'new_expire_time': license_obj.expire_time.strftime('%Y-%m-%d %H:%M:%S') if license_obj.expire_time else None
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': message, 'message': message,
@ -299,7 +327,7 @@ def extend_license(license_id):
}), 500 }), 500
@api_bp.route('/licenses/<int:license_id>/convert', methods=['POST']) @api_bp.route('/licenses/<int:license_id>/convert', methods=['POST'])
@require_admin @require_login
def convert_license(license_id): def convert_license(license_id):
"""试用卡密转正式""" """试用卡密转正式"""
try: try:
@ -331,6 +359,11 @@ def convert_license(license_id):
'message': message 'message': message
}), 400 }), 400
# 记录操作日志
log_operation('CONVERT_LICENSE', 'LICENSE', license_obj.license_id, {
'valid_days': valid_days
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': message, 'message': message,
@ -346,7 +379,7 @@ def convert_license(license_id):
}), 500 }), 500
@api_bp.route('/licenses/<int:license_id>/unbind', methods=['POST']) @api_bp.route('/licenses/<int:license_id>/unbind', methods=['POST'])
@require_admin @require_login
def unbind_license(license_id): def unbind_license(license_id):
"""解绑设备""" """解绑设备"""
try: try:
@ -367,6 +400,11 @@ def unbind_license(license_id):
'message': message 'message': message
}), 400 }), 400
# 记录操作日志
log_operation('UNBIND_LICENSE', 'LICENSE', license_obj.license_id, {
'machine_code': license_obj.bind_machine_code
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': message, 'message': message,
@ -382,7 +420,7 @@ def unbind_license(license_id):
}), 500 }), 500
@api_bp.route('/licenses/export', methods=['POST']) @api_bp.route('/licenses/export', methods=['POST'])
@require_admin @require_login
def export_licenses(): def export_licenses():
"""导出卡密""" """导出卡密"""
try: try:
@ -509,7 +547,7 @@ def export_licenses():
}), 500 }), 500
@api_bp.route('/licenses/import', methods=['POST']) @api_bp.route('/licenses/import', methods=['POST'])
@require_admin @require_login
def import_licenses(): def import_licenses():
"""导入卡密""" """导入卡密"""
try: try:
@ -623,7 +661,7 @@ def import_licenses():
}), 500 }), 500
@api_bp.route('/licenses/<string:license_key>', methods=['DELETE']) @api_bp.route('/licenses/<string:license_key>', methods=['DELETE'])
@require_admin @require_login
def delete_license(license_key): def delete_license(license_key):
"""删除卡密""" """删除卡密"""
try: try:
@ -649,6 +687,11 @@ def delete_license(license_key):
db.session.delete(license_obj) db.session.delete(license_obj)
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('DELETE_LICENSE', 'LICENSE', None, {
'license_key': license_key
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '卡密删除成功' 'message': '卡密删除成功'
@ -664,7 +707,7 @@ def delete_license(license_key):
@api_bp.route('/licenses/batch', methods=['DELETE']) @api_bp.route('/licenses/batch', methods=['DELETE'])
@require_admin @require_login
def batch_delete_licenses(): def batch_delete_licenses():
"""批量删除卡密""" """批量删除卡密"""
try: try:
@ -707,6 +750,13 @@ def batch_delete_licenses():
db.session.commit() db.session.commit()
# 记录操作日志
license_keys = [license_obj.license_key for license_obj in licenses]
log_operation('BATCH_DELETE_LICENSES', 'LICENSE', None, {
'license_keys': license_keys,
'count': len(licenses)
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'成功删除 {len(licenses)} 个卡密' 'message': f'成功删除 {len(licenses)} 个卡密'
@ -722,7 +772,7 @@ def batch_delete_licenses():
@api_bp.route('/licenses/batch/status', methods=['PUT']) @api_bp.route('/licenses/batch/status', methods=['PUT'])
@require_admin @require_login
def batch_update_license_status(): def batch_update_license_status():
"""批量更新卡密状态""" """批量更新卡密状态"""
try: try:
@ -774,6 +824,14 @@ def batch_update_license_status():
db.session.commit() db.session.commit()
# 记录操作日志
license_keys = [license_obj.license_key for license_obj in licenses]
log_operation('BATCH_UPDATE_LICENSE_STATUS', 'LICENSE', None, {
'license_keys': license_keys,
'status': status,
'count': len(licenses)
})
status_names = {0: '未激活', 1: '已激活', 2: '已过期', 3: '已禁用'} status_names = {0: '未激活', 1: '已激活', 2: '已过期', 3: '已禁用'}
status_name = status_names.get(status, '未知') status_name = status_names.get(status, '未知')
return jsonify({ return jsonify({

View File

@ -1,21 +1,23 @@
from flask import request, jsonify, current_app from flask import request, jsonify, current_app
from datetime import datetime from datetime import datetime
from app import db from app import db
from app.models import Product from app.models import Product, License, Device, Version
from . import api_bp from . import api_bp
from .decorators import require_admin from .decorators import require_login, require_admin
from flask_login import login_required from sqlalchemy import func, case
import traceback import traceback
import sys import sys
from app.utils.logger import log_operation
@api_bp.route('/products', methods=['GET']) @api_bp.route('/products', methods=['GET'])
@require_admin @require_login
def get_products(): def get_products():
"""获取产品列表""" """获取产品列表"""
try: try:
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 20, type=int), 100) per_page = min(request.args.get('per_page', 20, type=int), 100)
keyword = request.args.get('keyword', '').strip() keyword = request.args.get('keyword', '').strip()
include_stats = request.args.get('include_stats', 'true').lower() == 'true'
query = Product.query query = Product.query
@ -28,7 +30,66 @@ def get_products():
query = query.order_by(Product.create_time.desc()) query = query.order_by(Product.create_time.desc())
pagination = query.paginate(page=page, per_page=per_page, error_out=False) pagination = query.paginate(page=page, per_page=per_page, error_out=False)
products = [product.to_dict(include_stats=True) for product in pagination.items] products = pagination.items
# 优化批量查询统计信息避免N+1查询问题
if include_stats and products:
product_ids = [p.product_id for p in products]
# 批量查询license统计
license_stats = db.session.query(
License.product_id,
func.count(License.license_id).label('total_licenses'),
func.sum(case((License.status == 1, 1), else_=0)).label('active_licenses')
).filter(License.product_id.in_(product_ids)).group_by(License.product_id).all()
license_dict = {pid: {'total': 0, 'active': 0} for pid in product_ids}
for stat in license_stats:
license_dict[stat.product_id] = {
'total': stat.total_licenses or 0,
'active': stat.active_licenses or 0
}
# 批量查询device统计
device_stats = db.session.query(
Device.product_id,
func.count(Device.device_id).label('total_devices')
).filter(
Device.product_id.in_(product_ids),
Device.status == 1
).group_by(Device.product_id).all()
device_dict = {pid: 0 for pid in product_ids}
for stat in device_stats:
device_dict[stat.product_id] = stat.total_devices or 0
# 批量查询最新版本
version_stats = db.session.query(
Version.product_id,
Version.version_num,
Version.create_time
).filter(
Version.product_id.in_(product_ids),
Version.publish_status == 1
).order_by(Version.create_time.desc()).all()
version_dict = {}
for v in version_stats:
if v.product_id not in version_dict:
version_dict[v.product_id] = v.version_num
# 为每个产品添加统计信息
for product in products:
pid = product.product_id
stats = license_dict.get(pid, {'total': 0, 'active': 0})
product._cached_stats = {
'total_licenses': stats['total'],
'active_licenses': stats['active'],
'total_devices': device_dict.get(pid, 0),
'latest_version': version_dict.get(pid)
}
products = [product.to_dict(include_stats=include_stats) for product in products]
return jsonify({ return jsonify({
'success': True, 'success': True,
@ -56,7 +117,7 @@ def get_products():
}), 500 }), 500
@api_bp.route('/products', methods=['POST']) @api_bp.route('/products', methods=['POST'])
@require_admin @require_login
def create_product(): def create_product():
"""创建产品""" """创建产品"""
try: try:
@ -97,6 +158,12 @@ def create_product():
db.session.add(product) db.session.add(product)
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('CREATE_PRODUCT', 'PRODUCT', product.product_id, {
'product_name': product.product_name,
'description': product.description
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '产品创建成功', 'message': '产品创建成功',
@ -112,7 +179,7 @@ def create_product():
}), 500 }), 500
@api_bp.route('/products/<product_id>', methods=['GET']) @api_bp.route('/products/<product_id>', methods=['GET'])
@login_required @require_login
def get_product(product_id): def get_product(product_id):
"""获取产品详情""" """获取产品详情"""
try: try:
@ -136,7 +203,7 @@ def get_product(product_id):
}), 500 }), 500
@api_bp.route('/products/<product_id>', methods=['PUT']) @api_bp.route('/products/<product_id>', methods=['PUT'])
@require_admin @require_login
def update_product(product_id): def update_product(product_id):
"""更新产品""" """更新产品"""
try: try:
@ -163,6 +230,12 @@ def update_product(product_id):
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('UPDATE_PRODUCT', 'PRODUCT', product.product_id, {
'product_name': product.product_name,
'description': product.description
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '产品更新成功', 'message': '产品更新成功',
@ -178,7 +251,7 @@ def update_product(product_id):
}), 500 }), 500
@api_bp.route('/products/<product_id>', methods=['DELETE']) @api_bp.route('/products/<product_id>', methods=['DELETE'])
@require_admin @require_login
def delete_product(product_id): def delete_product(product_id):
"""删除产品""" """删除产品"""
try: try:
@ -200,6 +273,11 @@ def delete_product(product_id):
db.session.delete(product) db.session.delete(product)
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('DELETE_PRODUCT', 'PRODUCT', product.product_id, {
'product_name': product.product_name
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '产品删除成功' 'message': '产品删除成功'
@ -215,7 +293,7 @@ def delete_product(product_id):
@api_bp.route('/products/batch', methods=['DELETE']) @api_bp.route('/products/batch', methods=['DELETE'])
@require_admin @require_login
def batch_delete_products(): def batch_delete_products():
"""批量删除产品""" """批量删除产品"""
try: try:
@ -267,6 +345,13 @@ def batch_delete_products():
db.session.commit() db.session.commit()
# 记录操作日志
product_ids = [p.product_id for p in products]
log_operation('BATCH_DELETE_PRODUCTS', 'PRODUCT', None, {
'product_ids': product_ids,
'count': len(products)
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'成功删除 {len(products)} 个产品' 'message': f'成功删除 {len(products)} 个产品'
@ -282,7 +367,7 @@ def batch_delete_products():
@api_bp.route('/products/batch/status', methods=['PUT']) @api_bp.route('/products/batch/status', methods=['PUT'])
@require_admin @require_login
def batch_update_product_status(): def batch_update_product_status():
"""批量更新产品状态""" """批量更新产品状态"""
try: try:
@ -324,6 +409,14 @@ def batch_update_product_status():
db.session.commit() db.session.commit()
# 记录操作日志
product_ids = [p.product_id for p in products]
log_operation('BATCH_UPDATE_PRODUCT_STATUS', 'PRODUCT', None, {
'product_ids': product_ids,
'status': status,
'count': len(products)
})
status_name = '启用' if status == 1 else '禁用' status_name = '启用' if status == 1 else '禁用'
return jsonify({ return jsonify({
'success': True, 'success': True,

View File

@ -1,44 +1,67 @@
from flask import request, jsonify, current_app from flask import request, jsonify, current_app
from datetime import datetime, timedelta from datetime import datetime, timedelta, time
from sqlalchemy import func, and_, not_, text from sqlalchemy import func, and_, not_, text, case
from app import db from app import db
from app.models import Product, License, Device, Version, Ticket from app.models import Product, License, Device, Version, Ticket
from . import api_bp from . import api_bp
from .license import require_admin from .decorators import require_login, require_admin
@api_bp.route('/statistics/overview', methods=['GET']) @api_bp.route('/statistics/overview', methods=['GET'])
@require_admin @require_login
def get_overview_stats(): def get_overview_stats():
"""获取总览统计""" """获取总览统计"""
try: try:
# 产品统计 # 优化:使用单个查询获取所有产品统计
total_products = Product.query.count() product_stats = db.session.query(
active_products = Product.query.filter_by(status=1).count() func.count(Product.product_id).label('total'),
func.sum(case((Product.status == 1, 1), else_=0)).label('active')
).first()
total_products = product_stats.total or 0
active_products = product_stats.active or 0
# 卡密统计 # 优化:使用单个查询获取所有卡密统计
total_licenses = License.query.count() license_stats = db.session.query(
active_licenses = License.query.filter_by(status=1).count() func.count(License.license_id).label('total'),
trial_licenses = License.query.filter_by(type=0).count() func.sum(case((License.status == 1, 1), else_=0)).label('active'),
func.sum(case((License.type == 0, 1), else_=0)).label('trial')
).first()
total_licenses = license_stats.total or 0
active_licenses = license_stats.active or 0
trial_licenses = license_stats.trial or 0
# 设备统计 # 优化:使用单个查询获取设备统计
total_devices = Device.query.count() device_stats = db.session.query(
func.count(Device.device_id).label('total')
).first()
total_devices = device_stats.total or 0
# 在线设备统计7天内验证过
start_date = datetime.utcnow() - timedelta(days=7)
online_devices = db.session.execute( online_devices = db.session.execute(
text("SELECT COUNT(*) FROM device WHERE last_verify_time IS NOT NULL AND last_verify_time >= :start_date"), text("SELECT COUNT(*) FROM device WHERE last_verify_time IS NOT NULL AND last_verify_time >= :start_date"),
{"start_date": datetime.utcnow() - timedelta(days=7)} {"start_date": start_date}
).scalar() ).scalar() or 0
# 近期激活统计 # 优化:使用单个查询获取激活统计
today_activations = License.query.filter( today = datetime.utcnow().date()
func.date(License.activate_time) == datetime.utcnow().date() today_start = datetime.combine(today, time.min)
).count() today_end = datetime.combine(today, time.max)
week_activations = db.session.execute( # 今日激活数使用日期范围查询兼容SQLite
text("SELECT COUNT(*) FROM license WHERE activate_time IS NOT NULL AND activate_time >= :start_date"), today_activations = db.session.query(func.count(License.license_id)).filter(
{"start_date": datetime.utcnow() - timedelta(days=7)} License.activate_time.isnot(None),
).scalar() License.activate_time >= today_start,
License.activate_time <= today_end
).scalar() or 0
# 本周激活数
week_activations = db.session.query(func.count(License.license_id)).filter(
License.activate_time.isnot(None),
License.activate_time >= start_date
).scalar() or 0
# 工单统计 # 工单统计
total_tickets = Ticket.query.count() total_tickets = db.session.query(func.count(Ticket.ticket_id)).scalar() or 0
return jsonify({ return jsonify({
'success': True, 'success': True,
@ -73,7 +96,7 @@ def get_overview_stats():
return jsonify({'success': False, 'message': '服务器内部错误'}), 500 return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/statistics/activations', methods=['GET']) @api_bp.route('/statistics/activations', methods=['GET'])
@require_admin @require_login
def get_activation_trend(): def get_activation_trend():
"""获取激活趋势""" """获取激活趋势"""
try: try:
@ -115,7 +138,7 @@ def get_activation_trend():
return jsonify({'success': False, 'message': '服务器内部错误'}), 500 return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/statistics/products', methods=['GET']) @api_bp.route('/statistics/products', methods=['GET'])
@require_admin @require_login
def get_product_stats(): def get_product_stats():
"""获取产品统计信息""" """获取产品统计信息"""
try: try:

View File

@ -3,10 +3,11 @@ from datetime import datetime
from app import db from app import db
from app.models import Ticket, Product from app.models import Ticket, Product
from . import api_bp from . import api_bp
from .decorators import require_admin from .decorators import require_login, require_admin
from app.utils.logger import log_operation
@api_bp.route('/tickets', methods=['GET']) @api_bp.route('/tickets', methods=['GET'])
@require_admin @require_login
def get_tickets(): def get_tickets():
"""获取工单列表""" """获取工单列表"""
try: try:
@ -80,6 +81,12 @@ def create_ticket():
db.session.add(ticket) db.session.add(ticket)
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('CREATE_TICKET', 'TICKET', ticket.ticket_id, {
'title': ticket.title,
'product_id': ticket.product_id
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '工单创建成功', 'message': '工单创建成功',
@ -93,7 +100,7 @@ def create_ticket():
@api_bp.route('/tickets/batch/status', methods=['PUT']) @api_bp.route('/tickets/batch/status', methods=['PUT'])
@require_admin @require_login
def batch_update_ticket_status(): def batch_update_ticket_status():
"""批量更新工单状态""" """批量更新工单状态"""
try: try:
@ -134,6 +141,14 @@ def batch_update_ticket_status():
for ticket in tickets: for ticket in tickets:
ticket.update_status(status, remark) ticket.update_status(status, remark)
# 记录操作日志
ticket_ids = [ticket.ticket_id for ticket in tickets]
log_operation('BATCH_UPDATE_TICKET_STATUS', 'TICKET', None, {
'ticket_ids': ticket_ids,
'status': status,
'count': len(tickets)
})
status_names = {0: '待处理', 1: '处理中', 2: '已解决', 3: '已关闭'} status_names = {0: '待处理', 1: '处理中', 2: '已解决', 3: '已关闭'}
status_name = status_names.get(status, '未知') status_name = status_names.get(status, '未知')
return jsonify({ return jsonify({

View File

@ -4,15 +4,16 @@ import os
import hashlib import hashlib
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from app import db from app import db
from app.models import Version, Product from app.models import Version, Product, Device
from . import api_bp from . import api_bp
from .decorators import require_admin from .decorators import require_login, require_admin
from sqlalchemy import desc from sqlalchemy import desc, func, case, or_
import traceback import traceback
import sys import sys
from app.utils.logger import log_operation
@api_bp.route('/versions', methods=['GET']) @api_bp.route('/versions', methods=['GET'])
@require_admin @require_login
def get_versions(): def get_versions():
"""获取版本列表""" """获取版本列表"""
try: try:
@ -20,6 +21,7 @@ def get_versions():
per_page = min(request.args.get('per_page', 20, type=int), 100) per_page = min(request.args.get('per_page', 20, type=int), 100)
product_id = request.args.get('product_id') product_id = request.args.get('product_id')
status = request.args.get('publish_status', type=int) status = request.args.get('publish_status', type=int)
include_stats = request.args.get('include_stats', 'true').lower() == 'true'
query = Version.query query = Version.query
@ -31,7 +33,45 @@ def get_versions():
query = query.order_by(desc(Version.create_time)) query = query.order_by(desc(Version.create_time))
pagination = query.paginate(page=page, per_page=per_page, error_out=False) pagination = query.paginate(page=page, per_page=per_page, error_out=False)
versions = [version.to_dict(include_stats=True) for version in pagination.items] versions = pagination.items
# 优化批量查询统计信息避免N+1查询问题
if include_stats and versions:
# 构建版本查询条件
version_conditions = []
for v in versions:
version_conditions.append(
(Device.product_id == v.product_id) &
(Device.software_version == v.version_num)
)
if version_conditions:
# 批量查询设备统计
device_stats = db.session.query(
Device.product_id,
Device.software_version,
func.count(Device.device_id).label('download_count'),
func.sum(case((Device.status == 1, 1), else_=0)).label('active_device_count')
).filter(
or_(*version_conditions)
).group_by(Device.product_id, Device.software_version).all()
# 构建统计字典
stats_dict = {}
for stat in device_stats:
key = (stat.product_id, stat.software_version)
stats_dict[key] = {
'download_count': stat.download_count or 0,
'active_device_count': stat.active_device_count or 0
}
# 为每个版本添加统计信息
for version in versions:
key = (version.product_id, version.version_num)
stats = stats_dict.get(key, {'download_count': 0, 'active_device_count': 0})
version._cached_stats = stats
versions = [version.to_dict(include_stats=include_stats) for version in versions]
return jsonify({ return jsonify({
'success': True, 'success': True,
@ -51,7 +91,7 @@ def get_versions():
return jsonify({'success': False, 'message': '服务器内部错误'}), 500 return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/versions', methods=['POST']) @api_bp.route('/versions', methods=['POST'])
@require_admin @require_login
def create_version(): def create_version():
"""创建版本""" """创建版本"""
try: try:
@ -154,6 +194,13 @@ def create_version():
current_app.logger.info("发布版本") current_app.logger.info("发布版本")
version.publish() version.publish()
# 记录操作日志
log_operation('CREATE_VERSION', 'VERSION', version.version_id, {
'product_id': version.product_id,
'version_num': version.version_num,
'publish_now': publish_now
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '版本创建成功', 'message': '版本创建成功',
@ -169,7 +216,7 @@ def create_version():
return jsonify({'success': False, 'message': f'服务器内部错误: {str(e)}'}), 500 return jsonify({'success': False, 'message': f'服务器内部错误: {str(e)}'}), 500
@api_bp.route('/versions/<int:version_id>/publish', methods=['POST']) @api_bp.route('/versions/<int:version_id>/publish', methods=['POST'])
@require_admin @require_login
def publish_version(version_id): def publish_version(version_id):
"""发布版本""" """发布版本"""
try: try:
@ -178,6 +225,12 @@ def publish_version(version_id):
return jsonify({'success': False, 'message': '版本不存在'}), 404 return jsonify({'success': False, 'message': '版本不存在'}), 404
version.publish() version.publish()
# 记录操作日志
log_operation('PUBLISH_VERSION', 'VERSION', version.version_id, {
'version_num': version.version_num
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '版本发布成功', 'message': '版本发布成功',
@ -189,7 +242,7 @@ def publish_version(version_id):
return jsonify({'success': False, 'message': '服务器内部错误'}), 500 return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/versions/<int:version_id>', methods=['PUT']) @api_bp.route('/versions/<int:version_id>', methods=['PUT'])
@require_admin @require_login
def update_version(version_id): def update_version(version_id):
"""更新版本信息""" """更新版本信息"""
try: try:
@ -219,6 +272,13 @@ def update_version(version_id):
# 保存更新 # 保存更新
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('UPDATE_VERSION', 'VERSION', version.version_id, {
'version_num': version.version_num,
'platform': version.platform,
'description': version.description
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '版本信息更新成功', 'message': '版本信息更新成功',
@ -232,7 +292,7 @@ def update_version(version_id):
@api_bp.route('/versions/<int:version_id>/status', methods=['PUT']) @api_bp.route('/versions/<int:version_id>/status', methods=['PUT'])
@require_admin @require_login
def update_version_status(version_id): def update_version_status(version_id):
"""更新版本状态(发布/取消发布)""" """更新版本状态(发布/取消发布)"""
try: try:
@ -260,6 +320,14 @@ def update_version_status(version_id):
else: else:
return jsonify({'success': False, 'message': '无效的状态值'}), 400 return jsonify({'success': False, 'message': '无效的状态值'}), 400
# 记录操作日志
status_names = {0: '取消发布', 1: '发布', 2: '回滚'}
log_operation('UPDATE_VERSION_STATUS', 'VERSION', version.version_id, {
'version_num': version.version_num,
'status': status,
'status_name': status_names.get(status, '未知')
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': message, 'message': message,
@ -271,7 +339,7 @@ def update_version_status(version_id):
return jsonify({'success': False, 'message': '服务器内部错误'}), 500 return jsonify({'success': False, 'message': '服务器内部错误'}), 500
@api_bp.route('/versions/upload', methods=['POST']) @api_bp.route('/versions/upload', methods=['POST'])
@require_admin @require_login
def upload_version_file(): def upload_version_file():
"""上传版本文件""" """上传版本文件"""
try: try:
@ -349,7 +417,7 @@ def extract_filename_from_url(download_url):
return None return None
@api_bp.route('/versions/<int:version_id>', methods=['DELETE']) @api_bp.route('/versions/<int:version_id>', methods=['DELETE'])
@require_admin @require_login
def delete_version(version_id): def delete_version(version_id):
"""删除版本""" """删除版本"""
try: try:
@ -383,6 +451,11 @@ def delete_version(version_id):
db.session.delete(version) db.session.delete(version)
db.session.commit() db.session.commit()
# 记录操作日志
log_operation('DELETE_VERSION', 'VERSION', version.version_id, {
'version_num': version.version_num
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '版本删除成功' 'message': '版本删除成功'
@ -395,7 +468,7 @@ def delete_version(version_id):
@api_bp.route('/versions/batch', methods=['DELETE']) @api_bp.route('/versions/batch', methods=['DELETE'])
@require_admin @require_login
def batch_delete_versions(): def batch_delete_versions():
"""批量删除版本""" """批量删除版本"""
try: try:
@ -470,6 +543,13 @@ def batch_delete_versions():
db.session.commit() db.session.commit()
# 记录操作日志
version_ids = [version.version_id for version in versions]
log_operation('BATCH_DELETE_VERSIONS', 'VERSION', None, {
'version_ids': version_ids,
'count': len(versions)
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'成功删除 {len(versions)} 个版本' 'message': f'成功删除 {len(versions)} 个版本'
@ -485,7 +565,7 @@ def batch_delete_versions():
@api_bp.route('/versions/batch/status', methods=['PUT']) @api_bp.route('/versions/batch/status', methods=['PUT'])
@require_admin @require_login
def batch_update_version_status(): def batch_update_version_status():
"""批量更新版本状态(发布/取消发布)""" """批量更新版本状态(发布/取消发布)"""
try: try:
@ -528,6 +608,14 @@ def batch_update_version_status():
else: else:
version.unpublish() version.unpublish()
# 记录操作日志
version_ids = [version.version_id for version in versions]
log_operation('BATCH_UPDATE_VERSION_STATUS', 'VERSION', None, {
'version_ids': version_ids,
'status': status,
'count': len(versions)
})
status_name = '发布' if status == 1 else '取消发布' status_name = '发布' if status == 1 else '取消发布'
return jsonify({ return jsonify({
'success': True, 'success': True,

View File

@ -30,6 +30,11 @@ class Product(db.Model):
def get_stats(self): def get_stats(self):
"""获取产品统计信息""" """获取产品统计信息"""
# 如果已经有缓存的统计信息(来自批量查询),直接使用
if hasattr(self, '_cached_stats'):
return self._cached_stats
# 否则执行单个查询(用于单个产品详情等场景)
total_licenses = self.licenses.count() total_licenses = self.licenses.count()
active_licenses = self.licenses.filter_by(status=1).count() active_licenses = self.licenses.filter_by(status=1).count()
total_devices = self.devices.filter_by(status=1).count() total_devices = self.devices.filter_by(status=1).count()

View File

@ -47,7 +47,11 @@ class Version(db.Model):
def get_download_count(self): def get_download_count(self):
"""获取下载次数""" """获取下载次数"""
# 通过 software_version 字段查找使用此版本的设备 # 如果已经有缓存的统计信息(来自批量查询),直接使用
if hasattr(self, '_cached_stats'):
return self._cached_stats.get('download_count', 0)
# 否则执行单个查询(用于单个版本详情等场景)
from app.models.device import Device from app.models.device import Device
return Device.query.filter_by( return Device.query.filter_by(
product_id=self.product_id, product_id=self.product_id,
@ -56,7 +60,11 @@ class Version(db.Model):
def get_active_device_count(self): def get_active_device_count(self):
"""获取活跃设备数""" """获取活跃设备数"""
# 通过 software_version 字段查找使用此版本的活跃设备 # 如果已经有缓存的统计信息(来自批量查询),直接使用
if hasattr(self, '_cached_stats'):
return self._cached_stats.get('active_device_count', 0)
# 否则执行单个查询(用于单个版本详情等场景)
from app.models.device import Device from app.models.device import Device
return Device.query.filter_by( return Device.query.filter_by(
product_id=self.product_id, product_id=self.product_id,

View File

@ -135,6 +135,12 @@
系统设置 系统设置
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint and 'log' in request.endpoint }}" href="{{ url_for('web.logs') }}">
<i class="fas fa-file-alt me-2"></i>
日志管理
</a>
</li>
{% endif %} {% endif %}
</ul> </ul>
@ -227,7 +233,19 @@
window.location.href = '/login'; window.location.href = '/login';
}, 1500); }, 1500);
} }
// 返回错误信息而不是直接抛出异常 // 对于403错误显示权限不足的提示
else if (response.status === 403) {
// 先尝试解析错误信息
return response.json().then(errorData => {
showNotification(errorData.message || '权限不足,无法执行此操作', 'error');
throw new Error(`403: ${errorData.message || '权限不足'}`);
}).catch(() => {
// 如果无法解析JSON使用默认消息
showNotification('权限不足,无法执行此操作', 'error');
throw new Error('403: 权限不足');
});
}
// 其他错误状态码
return response.json().then(errorData => { return response.json().then(errorData => {
throw new Error(`${response.status}: ${errorData.message || response.statusText}`); throw new Error(`${response.status}: ${errorData.message || response.statusText}`);
}).catch(() => { }).catch(() => {

View File

@ -7,7 +7,7 @@
{% block content %} {% block content %}
<!-- 统计卡片 --> <!-- 统计卡片 -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4"> <div class="col-xl-3 col-md-4 mb-4">
<div class="card card-stats"> <div class="card card-stats">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="col-xl-3 col-md-6 mb-4"> <div class="col-xl-3 col-md-4 mb-4">
<div class="card card-stats"> <div class="card card-stats">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -43,7 +43,7 @@
</div> </div>
</div> </div>
<div class="col-xl-3 col-md-6 mb-4"> <div class="col-xl-3 col-md-4 mb-4">
<div class="card card-stats"> <div class="card card-stats">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -61,7 +61,7 @@
</div> </div>
</div> </div>
<div class="col-xl-3 col-md-6 mb-4"> <div class="col-xl-3 col-md-4 mb-4">
<div class="card card-stats"> <div class="card card-stats">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -325,7 +325,7 @@ function initCharts() {
// 加载最近激活 // 加载最近激活
function loadRecentActivations() { function loadRecentActivations() {
apiRequest('/api/v1/licenses?status=1&per_page=5') apiRequest('/api/v1/licenses?status=1&per_page=5&sort=activate_time&order=desc')
.then(data => { .then(data => {
if (data.success) { if (data.success) {
const tbody = document.getElementById('recent-activations'); const tbody = document.getElementById('recent-activations');

View File

@ -124,11 +124,17 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script> <script>
let currentPage = 1; let currentPage = 1;
// 页面加载完成后初始化 // 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() { // 使用立即执行函数确保在DOM和所有脚本加载完成后执行
(function() {
function init() {
console.log('设备列表页面已加载,开始初始化...');
console.log('apiRequest函数是否存在:', typeof apiRequest);
// 从URL参数中读取筛选条件 // 从URL参数中读取筛选条件
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('product_id')) { if (urlParams.has('product_id')) {
@ -141,9 +147,28 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('search-status').value = urlParams.get('status'); document.getElementById('search-status').value = urlParams.get('status');
} }
if (typeof apiRequest === 'function') {
loadDevices(); loadDevices();
initEventListeners(); initEventListeners();
}); } else {
console.error('apiRequest函数未定义等待脚本加载...');
setTimeout(function() {
if (typeof apiRequest === 'function') {
loadDevices();
initEventListeners();
} else {
console.error('apiRequest函数仍未定义请检查脚本加载顺序');
}
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// 初始化事件监听器 // 初始化事件监听器
function initEventListeners() { function initEventListeners() {
@ -193,6 +218,7 @@ function initEventListeners() {
// 加载设备列表 // 加载设备列表
function loadDevices(page = 1) { function loadDevices(page = 1) {
console.log('loadDevices函数被调用页码:', page);
const params = new URLSearchParams({ const params = new URLSearchParams({
page: page, page: page,
per_page: 10 per_page: 10
@ -216,7 +242,10 @@ function loadDevices(page = 1) {
if (productId) params.append('product_id', productId); if (productId) params.append('product_id', productId);
if (softwareVersion) params.append('software_version', softwareVersion); if (softwareVersion) params.append('software_version', softwareVersion);
apiRequest(`/api/v1/devices?${params}`) const apiUrl = `/api/v1/devices?${params}`;
console.log('准备请求API:', apiUrl);
apiRequest(apiUrl)
.then(data => { .then(data => {
if (data.success) { if (data.success) {
renderDeviceList(data.data.devices); renderDeviceList(data.data.devices);
@ -585,3 +614,4 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
</script> </script>
{% endblock %}

View File

@ -147,14 +147,38 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script> <script>
let currentPage = 1; let currentPage = 1;
// 页面加载完成后初始化 // 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() { // 使用立即执行函数确保在DOM和所有脚本加载完成后执行
(function() {
function init() {
console.log('卡密列表页面已加载,开始初始化...');
console.log('apiRequest函数是否存在:', typeof apiRequest);
if (typeof apiRequest === 'function') {
loadLicenses(); loadLicenses();
initEventListeners(); initEventListeners();
}); } else {
console.error('apiRequest函数未定义等待脚本加载...');
setTimeout(function() {
if (typeof apiRequest === 'function') {
loadLicenses();
initEventListeners();
} else {
console.error('apiRequest函数仍未定义请检查脚本加载顺序');
}
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// 初始化事件监听器 // 初始化事件监听器
function initEventListeners() { function initEventListeners() {
@ -210,6 +234,7 @@ function initEventListeners() {
// 加载卡密列表 // 加载卡密列表
function loadLicenses(page = 1) { function loadLicenses(page = 1) {
console.log('loadLicenses函数被调用页码:', page);
const params = new URLSearchParams({ const params = new URLSearchParams({
page: page, page: page,
per_page: 10 per_page: 10
@ -226,7 +251,10 @@ function loadLicenses(page = 1) {
if (type) params.append('type', type); if (type) params.append('type', type);
if (product) params.append('product', product); if (product) params.append('product', product);
apiRequest(`/api/v1/licenses?${params}`) const apiUrl = `/api/v1/licenses?${params}`;
console.log('准备请求API:', apiUrl);
apiRequest(apiUrl)
.then(data => { .then(data => {
if (data.success) { if (data.success) {
renderLicenseList(data.data.licenses); renderLicenseList(data.data.licenses);
@ -783,3 +811,4 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
</script> </script>
{% endblock %}

View File

@ -52,11 +52,17 @@
<script> <script>
// 自动聚焦到用户名输入框 // 自动聚焦到用户名输入框
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.getElementById('username').focus(); const usernameInput = document.getElementById('username');
if (usernameInput) {
usernameInput.focus();
}
}); });
// 登录表单提交处理 // 登录表单提交处理
document.getElementById('login-form').addEventListener('submit', async function(e) { document.addEventListener('DOMContentLoaded', function() {
const loginForm = document.getElementById('login-form');
if (loginForm) {
loginForm.addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
const loginBtn = document.getElementById('login-btn'); const loginBtn = document.getElementById('login-btn');
@ -67,13 +73,14 @@ document.getElementById('login-form').addEventListener('submit', async function(
// 基础验证 // 基础验证
if (!username || !password) { if (!username || !password) {
// 使用基础模板中的showNotification函数
showNotification('请填写用户名和密码', 'warning'); showNotification('请填写用户名和密码', 'warning');
return; return;
} }
// 显示加载状态 // 显示加载状态
loginBtn.disabled = true; if (loginBtn) loginBtn.disabled = true;
loginBtnText.textContent = '登录中...'; if (loginBtnText) loginBtnText.textContent = '登录中...';
try { try {
console.log('尝试登录:', username); console.log('尝试登录:', username);
@ -86,8 +93,10 @@ document.getElementById('login-form').addEventListener('submit', async function(
// 获取CSRF令牌 // 获取CSRF令牌
try { try {
const csrfToken = document.querySelector('input[name="csrf_token"]').value; const csrfTokenInput = document.querySelector('input[name="csrf_token"]');
formData.append('csrf_token', csrfToken); if (csrfTokenInput) {
formData.append('csrf_token', csrfTokenInput.value);
}
} catch (e) { } catch (e) {
console.error('获取CSRF令牌失败:', e); console.error('获取CSRF令牌失败:', e);
} }
@ -147,8 +156,10 @@ document.getElementById('login-form').addEventListener('submit', async function(
showNotification('网络错误,请检查网络连接后重试', 'error'); showNotification('网络错误,请检查网络连接后重试', 'error');
} finally { } finally {
// 恢复按钮状态 // 恢复按钮状态
loginBtn.disabled = false; if (loginBtn) loginBtn.disabled = false;
loginBtnText.textContent = '登录'; if (loginBtnText) loginBtnText.textContent = '登录';
}
});
} }
}); });
</script> </script>

View File

@ -133,15 +133,41 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script> <script>
let currentPage = 1; let currentPage = 1;
let currentKeyword = ''; let currentKeyword = '';
// 页面加载完成后初始化 // 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() { // 使用立即执行函数确保在DOM和所有脚本加载完成后执行
(function() {
function init() {
console.log('产品列表页面已加载,开始初始化...');
console.log('apiRequest函数是否存在:', typeof apiRequest);
if (typeof apiRequest === 'function') {
loadProducts(); loadProducts();
initEventListeners(); initEventListeners();
}); } else {
console.error('apiRequest函数未定义等待脚本加载...');
// 如果apiRequest未定义等待一段时间后重试
setTimeout(function() {
if (typeof apiRequest === 'function') {
loadProducts();
initEventListeners();
} else {
console.error('apiRequest函数仍未定义请检查脚本加载顺序');
}
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// DOM已经加载完成直接执行
init();
}
})();
// 初始化事件监听器 // 初始化事件监听器
function initEventListeners() { function initEventListeners() {
@ -196,6 +222,7 @@ function initEventListeners() {
// 加载产品列表 // 加载产品列表
function loadProducts(page = 1) { function loadProducts(page = 1) {
console.log('loadProducts函数被调用页码:', page);
const params = new URLSearchParams({ const params = new URLSearchParams({
page: page, page: page,
per_page: 10 per_page: 10
@ -205,8 +232,10 @@ function loadProducts(page = 1) {
params.append('keyword', currentKeyword); params.append('keyword', currentKeyword);
} }
// 使用完整的API路径
const apiUrl = `/api/v1/products?${params}`; const apiUrl = `/api/v1/products?${params}`;
console.log('准备请求API:', apiUrl);
// 使用apiRequest函数它已经处理了加载动画和错误处理
apiRequest(apiUrl) apiRequest(apiUrl)
.then(data => { .then(data => {
if (data && data.success && data.data) { if (data && data.success && data.data) {
@ -228,23 +257,10 @@ function loadProducts(page = 1) {
renderProductList([]); renderProductList([]);
renderPagination({ pages: 0, page: 1, has_prev: false, has_next: false }); renderPagination({ pages: 0, page: 1, has_prev: false, has_next: false });
// 处理不同类型的错误 // apiRequest已经处理了401和403错误这里只处理其他错误
if (error.message) { if (error.message && !error.message.includes('401') && !error.message.includes('403')) {
if (error.message.includes('401')) {
showNotification('会话已过期,请重新登录', 'warning');
setTimeout(() => {
window.location.href = '/login';
}, 1500);
} else if (error.message.includes('403')) {
showNotification('权限不足,无法访问产品列表', 'error');
} else if (error.message.includes('500')) {
showNotification('服务器内部错误,请联系管理员', 'error');
} else {
showNotification('加载产品列表失败: ' + error.message, 'error'); showNotification('加载产品列表失败: ' + error.message, 'error');
} }
} else {
showNotification('加载产品列表失败: 网络连接异常', 'error');
}
}); });
} }
@ -414,8 +430,26 @@ function showDeleteModal(productId, productName) {
// 删除产品 // 删除产品
function deleteProduct(productId) { function deleteProduct(productId) {
apiRequest(`/api/v1/products/${productId}`, { // 显示加载动画
method: 'DELETE' showLoading();
fetch(`/api/v1/products/${productId}`, {
method: 'DELETE',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
// 隐藏加载动画
hideLoading();
if (response.status === 401) {
window.location.href = '/login';
throw new Error('未授权访问');
}
return response.json();
}) })
.then(data => { .then(data => {
if (data && data.success) { if (data && data.success) {
@ -430,6 +464,9 @@ function deleteProduct(productId) {
} }
}) })
.catch(error => { .catch(error => {
// 隐藏加载动画
hideLoading();
console.error('Failed to delete product:', error); console.error('Failed to delete product:', error);
showNotification('删除失败: ' + (error.message || '未知错误'), 'error'); showNotification('删除失败: ' + (error.message || '未知错误'), 'error');
}); });
@ -467,10 +504,28 @@ function batchDeleteProducts() {
const selectedCheckboxes = document.querySelectorAll('.product-checkbox:checked'); const selectedCheckboxes = document.querySelectorAll('.product-checkbox:checked');
const productIds = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.productId); const productIds = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.productId);
apiRequest('/api/v1/products/batch', { // 显示加载动画
showLoading();
fetch('/api/v1/products/batch', {
method: 'DELETE', method: 'DELETE',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ product_ids: productIds }) body: JSON.stringify({ product_ids: productIds })
}) })
.then(response => {
// 隐藏加载动画
hideLoading();
if (response.status === 401) {
window.location.href = '/login';
throw new Error('未授权访问');
}
return response.json();
})
.then(data => { .then(data => {
if (data && data.success) { if (data && data.success) {
showNotification(data.message || '批量删除成功', 'success'); showNotification(data.message || '批量删除成功', 'success');
@ -499,6 +554,9 @@ function batchDeleteProducts() {
} }
}) })
.catch(error => { .catch(error => {
// 隐藏加载动画
hideLoading();
console.error('Failed to batch delete products:', error); console.error('Failed to batch delete products:', error);
showNotification('批量删除失败: ' + (error.message || '未知错误'), 'error'); showNotification('批量删除失败: ' + (error.message || '未知错误'), 'error');
@ -518,13 +576,31 @@ function batchUpdateProductStatus(status) {
return; return;
} }
apiRequest('/api/v1/products/batch/status', { // 显示加载动画
showLoading();
fetch('/api/v1/products/batch/status', {
method: 'PUT', method: 'PUT',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: JSON.stringify({
product_ids: productIds, product_ids: productIds,
status: status status: status
}) })
}) })
.then(response => {
// 隐藏加载动画
hideLoading();
if (response.status === 401) {
window.location.href = '/login';
throw new Error('未授权访问');
}
return response.json();
})
.then(data => { .then(data => {
if (data && data.success) { if (data && data.success) {
showNotification(data.message || '批量更新状态成功', 'success'); showNotification(data.message || '批量更新状态成功', 'success');
@ -537,6 +613,9 @@ function batchUpdateProductStatus(status) {
} }
}) })
.catch(error => { .catch(error => {
// 隐藏加载动画
hideLoading();
console.error('Failed to batch update product status:', error); console.error('Failed to batch update product status:', error);
showNotification('批量更新状态失败: ' + (error.message || '未知错误'), 'error'); showNotification('批量更新状态失败: ' + (error.message || '未知错误'), 'error');
}); });
@ -550,9 +629,4 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
</script> </script>
{% endblock %}

View File

@ -189,15 +189,39 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script> <script>
let currentPage = 1; let currentPage = 1;
let batchUpdateStatus = null; // 用于存储批量更新的状态 let batchUpdateStatus = null; // 用于存储批量更新的状态
// 页面加载完成后初始化 // 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() { // 使用立即执行函数确保在DOM和所有脚本加载完成后执行
(function() {
function init() {
console.log('工单列表页面已加载,开始初始化...');
console.log('apiRequest函数是否存在:', typeof apiRequest);
if (typeof apiRequest === 'function') {
loadTickets(); loadTickets();
initEventListeners(); initEventListeners();
}); } else {
console.error('apiRequest函数未定义等待脚本加载...');
setTimeout(function() {
if (typeof apiRequest === 'function') {
loadTickets();
initEventListeners();
} else {
console.error('apiRequest函数仍未定义请检查脚本加载顺序');
}
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// 初始化事件监听器 // 初始化事件监听器
function initEventListeners() { function initEventListeners() {
@ -264,6 +288,7 @@ function initEventListeners() {
// 加载工单列表 // 加载工单列表
function loadTickets(page = 1) { function loadTickets(page = 1) {
console.log('loadTickets函数被调用页码:', page);
const params = new URLSearchParams({ const params = new URLSearchParams({
page: page, page: page,
per_page: 10 per_page: 10
@ -280,7 +305,10 @@ function loadTickets(page = 1) {
if (priority) params.append('priority', priority); if (priority) params.append('priority', priority);
if (product) params.append('product_id', product); if (product) params.append('product_id', product);
apiRequest(`/api/v1/tickets?${params}`) const apiUrl = `/api/v1/tickets?${params}`;
console.log('准备请求API:', apiUrl);
apiRequest(apiUrl)
.then(data => { .then(data => {
if (data.success) { if (data.success) {
renderTicketList(data.data.tickets); renderTicketList(data.data.tickets);
@ -595,3 +623,4 @@ function batchUpdateTicketStatus() {
}); });
} }
</script> </script>
{% endblock %}

View File

@ -130,14 +130,38 @@
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script> <script>
let currentPage = 1; let currentPage = 1;
// 页面加载完成后初始化 // 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() { // 使用立即执行函数确保在DOM和所有脚本加载完成后执行
(function() {
function init() {
console.log('版本列表页面已加载,开始初始化...');
console.log('apiRequest函数是否存在:', typeof apiRequest);
if (typeof apiRequest === 'function') {
loadVersions(); loadVersions();
initEventListeners(); initEventListeners();
}); } else {
console.error('apiRequest函数未定义等待脚本加载...');
setTimeout(function() {
if (typeof apiRequest === 'function') {
loadVersions();
initEventListeners();
} else {
console.error('apiRequest函数仍未定义请检查脚本加载顺序');
}
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// 初始化事件监听器 // 初始化事件监听器
function initEventListeners() { function initEventListeners() {
@ -186,6 +210,7 @@ function initEventListeners() {
// 加载版本列表 // 加载版本列表
function loadVersions(page = 1) { function loadVersions(page = 1) {
console.log('loadVersions函数被调用页码:', page);
const params = new URLSearchParams({ const params = new URLSearchParams({
page: page, page: page,
per_page: 10 per_page: 10
@ -200,7 +225,10 @@ function loadVersions(page = 1) {
if (product) params.append('product_id', product); if (product) params.append('product_id', product);
if (status) params.append('status', status); if (status) params.append('status', status);
apiRequest(`/api/v1/versions?${params}`) const apiUrl = `/api/v1/versions?${params}`;
console.log('准备请求API:', apiUrl);
apiRequest(apiUrl)
.then(data => { .then(data => {
if (data.success) { if (data.success) {
renderVersionList(data.data.versions); renderVersionList(data.data.versions);
@ -536,3 +564,4 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
</script> </script>
{% endblock %}

View File

@ -6,7 +6,7 @@ from app import db
from app.web import web_bp from app.web import web_bp
import sys import sys
import platform import platform
from flask import __version__ as flask_version from flask import __version__ as flask_version, current_app
def register_error_handlers(app): def register_error_handlers(app):
@ -151,6 +151,11 @@ def statistics():
@login_required @login_required
def settings(): def settings():
"""系统设置页面""" """系统设置页面"""
# 只有超级管理员可以访问
if not current_user.is_super_admin():
flash('需要超级管理员权限', 'error')
return redirect(url_for('web.dashboard'))
import sys import sys
import platform import platform
from flask import __version__ as flask_version, current_app from flask import __version__ as flask_version, current_app
@ -189,3 +194,15 @@ def admins():
return redirect(url_for('web.dashboard')) return redirect(url_for('web.dashboard'))
return render_template('admin/list.html') return render_template('admin/list.html')
# 日志管理页面
@web_bp.route('/logs')
@login_required
def logs():
"""日志管理页面"""
# 只有超级管理员可以访问
if not current_user.is_super_admin():
flash('需要超级管理员权限', 'error')
return redirect(url_for('web.dashboard'))
return render_template('log/list.html')

View File

@ -10,10 +10,15 @@ class Config:
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///kamaxitong.db' SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///kamaxitong.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
# SQLAlchemy 2.0 兼容性设置 # SQLAlchemy 2.0 兼容性设置和连接池优化
SQLALCHEMY_ENGINE_OPTIONS = { SQLALCHEMY_ENGINE_OPTIONS = {
"future": True, "future": True,
"pool_pre_ping": True, "pool_pre_ping": True, # 连接前检查连接是否有效
"pool_size": int(os.environ.get('DB_POOL_SIZE', 10)), # 连接池大小
"max_overflow": int(os.environ.get('DB_MAX_OVERFLOW', 20)), # 最大溢出连接数
"pool_recycle": int(os.environ.get('DB_POOL_RECYCLE', 3600)), # 连接回收时间(秒)
"pool_timeout": int(os.environ.get('DB_POOL_TIMEOUT', 30)), # 获取连接超时时间(秒)
"echo": False, # 关闭SQL日志输出生产环境
} }
# 系统基本配置 # 系统基本配置
@ -46,12 +51,29 @@ class Config:
@staticmethod @staticmethod
def init_app(app): def init_app(app):
"""初始化应用配置""" """初始化应用配置"""
pass # 确保日志目录存在
import os
if not os.path.exists('logs'):
os.mkdir('logs')
# 配置日志
import logging
from logging.handlers import RotatingFileHandler
file_handler = RotatingFileHandler('logs/kamaxitong.log', maxBytes=10240, backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('KaMiXiTong startup')
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
"""开发环境配置""" """开发环境配置"""
DEBUG = True DEBUG = True
SQLALCHEMY_ECHO = True SQLALCHEMY_ECHO = False # 关闭SQL日志输出以提高性能需要调试时可临时开启
class ProductionConfig(Config): class ProductionConfig(Config):
"""生产环境配置""" """生产环境配置"""
@ -67,6 +89,11 @@ class ProductionConfig(Config):
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
if not app.debug: if not app.debug:
# 确保日志目录存在
import os
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/kamaxitong.log', maxBytes=10240, backupCount=10) file_handler = RotatingFileHandler('logs/kamaxitong.log', maxBytes=10240, backupCount=10)
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]'

Binary file not shown.

View File

@ -8,6 +8,8 @@ KaMiXiTong 启动脚本
import os import os
import sys import sys
import subprocess import subprocess
import threading
import time
from pathlib import Path from pathlib import Path
def check_python_version(): def check_python_version():
@ -23,6 +25,13 @@ def install_dependencies():
print("📦 正在安装依赖包...") print("📦 正在安装依赖包...")
try: try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt']) subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt'])
# 检查并安装FastAPI依赖
if os.path.exists('requirements-fastapi.txt'):
try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements-fastapi.txt'])
print("✅ FastAPI依赖安装完成")
except subprocess.CalledProcessError:
print("⚠️ FastAPI依赖安装失败将跳过FastAPI服务")
print("✅ 依赖安装完成") print("✅ 依赖安装完成")
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
print("❌ 依赖安装失败") print("❌ 依赖安装失败")
@ -87,6 +96,39 @@ def create_directories():
Path(directory).mkdir(parents=True, exist_ok=True) Path(directory).mkdir(parents=True, exist_ok=True)
print("✅ 目录结构创建完成") print("✅ 目录结构创建完成")
def start_flask_app():
"""启动Flask应用"""
try:
from run import app
print("🚀 正在启动Flask应用...")
app.run(
host='127.0.0.1',
port=5000,
debug=True,
use_reloader=False # 禁用重载器以避免子进程问题
)
except Exception as e:
print(f"❌ Flask应用启动失败: {e}")
def start_fastapi_app():
"""启动FastAPI应用"""
try:
# 检查FastAPI应用文件是否存在
if not os.path.exists('fastapi_app.py'):
print("⚠️ FastAPI应用文件不存在跳过FastAPI服务启动")
return
print("🚀 正在启动FastAPI应用...")
# 使用uvicorn启动FastAPI应用
subprocess.run([
sys.executable, '-m', 'uvicorn',
'fastapi_app:app',
'--host', '127.0.0.1',
'--port', '9000'
])
except Exception as e:
print(f"❌ FastAPI应用启动失败: {e}")
def main(): def main():
"""主函数""" """主函数"""
print("=" * 50) print("=" * 50)
@ -115,21 +157,27 @@ def main():
print("\n" + "=" * 50) print("\n" + "=" * 50)
print("🚀 启动开发服务器...") print("🚀 启动开发服务器...")
print("=" * 50) print("=" * 50)
print("📍 访问地址: http://localhost:5000") print("📍 Flask访问地址: http://localhost:5000")
print("📍 FastAPI访问地址: http://localhost:9000")
print("📍 FastAPI文档: http://localhost:9000/docs")
print("👤 管理员账号: admin") print("👤 管理员账号: admin")
print("🔑 管理员密码: admin123") print("🔑 管理员密码: admin123")
print("📚 API文档: http://localhost:5000/api/v1")
print("⏹️ 按 Ctrl+C 停止服务器") print("⏹️ 按 Ctrl+C 停止服务器")
print("=" * 50 + "\n") print("=" * 50 + "\n")
# 启动Flask应用 # 启动Flask和FastAPI应用
try: try:
from run import app # 在单独的线程中启动Flask应用
app.run( flask_thread = threading.Thread(target=start_flask_app)
host='0.0.0.0', flask_thread.daemon = True
port=5000, flask_thread.start()
debug=True
) # 等待Flask启动
time.sleep(2)
# 在主线程中启动FastAPI应用
start_fastapi_app()
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n👋 服务器已停止") print("\n👋 服务器已停止")
except Exception as e: except Exception as e:

View File

@ -53,3 +53,63 @@ function formatFileSize(bytes) {
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
} }
// API请求函数 - 添加认证支持
function apiRequest(url, options = {}) {
// 确保URL是完整的路径
let fullUrl = url;
if (!url.startsWith('/')) {
fullUrl = '/' + url;
}
if (!url.startsWith('/api/')) {
fullUrl = '/api/v1' + fullUrl;
}
if (!url.startsWith('/')) {
fullUrl = '/' + fullUrl;
}
// 设置默认选项
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin' // 确保发送cookies进行认证
};
// 合并选项
const mergedOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...options.headers
}
};
// 显示加载动画
showLoading();
// 发起请求
return fetch(fullUrl, mergedOptions)
.then(response => {
// 隐藏加载动画
hideLoading();
// 检查响应状态
if (response.status === 401) {
// 未授权,重定向到登录页面
window.location.href = '/login';
throw new Error('未授权访问');
}
return response.json().catch(() => ({}));
})
.catch(error => {
// 隐藏加载动画
hideLoading();
// 处理网络错误
console.error('API请求失败:', error);
throw error;
});
}