修复页面没数据的错误
This commit is contained in:
parent
ebeb56a230
commit
7f6cf6e624
164
README.md
164
README.md
@ -1,53 +1,26 @@
|
||||
# 软件及卡密管理系统
|
||||
# 软件授权管理系统 (KaMiXiTong)
|
||||
|
||||
一款面向Python开发者的轻量化软件授权控制+全生命周期管理系统,核心解决"Python软件盗版防护、卡密授权管控、软件版本迭代管理"三大核心需求。
|
||||
一款基于Python Flask的软件授权管理系统,支持卡密生成、设备绑定、在线验证等功能。
|
||||
|
||||
## 项目结构
|
||||
## 功能特性
|
||||
|
||||
- 🎯 **卡密管理**: 支持批量生成、导入导出卡密
|
||||
- 🔐 **设备绑定**: 防止账号共享,支持解绑操作
|
||||
- 🔄 **在线验证**: 实时验证卡密有效性
|
||||
- 📊 **数据统计**: 可视化展示激活趋势和统计数据
|
||||
- 🛡️ **安全防护**: AES加密传输,防抓包破解
|
||||
- 🌐 **多产品支持**: 一个系统管理多个软件产品
|
||||
- 📱 **响应式界面**: 支持PC和移动设备访问
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
KaMiXiTong/
|
||||
├── README.md # 项目说明文档
|
||||
├── requirements.txt # Python依赖包
|
||||
├── setup.py # 安装脚本
|
||||
├── config.py # 配置文件
|
||||
├── run.py # 启动入口
|
||||
├── auth_validator.py # Python验证器模块(供开发者嵌入)
|
||||
├── app/ # Web管理后台
|
||||
│ ├── __init__.py
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── product.py # 产品模型
|
||||
│ │ ├── version.py # 版本模型
|
||||
│ │ ├── license.py # 许可证(卡密)模型
|
||||
│ │ ├── device.py # 设备模型
|
||||
│ │ ├── ticket.py # 工单模型
|
||||
│ │ └── admin.py # 管理员模型
|
||||
│ ├── 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界面
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── views.py # 页面路由
|
||||
│ │ └── templates/ # HTML模板
|
||||
│ │ ├── base.html
|
||||
│ │ ├── login.html
|
||||
│ │ ├── dashboard.html
|
||||
│ │ ├── product/
|
||||
│ │ ├── version/
|
||||
│ │ ├── license/
|
||||
│ │ ├── device/
|
||||
│ │ └── ticket/
|
||||
│ └── utils/ # 工具函数
|
||||
│ ├── __init__.py
|
||||
│ ├── crypto.py # 加密工具
|
||||
│ ├── machine_code.py # 机器码生成
|
||||
│ └── validators.py # 验证工具
|
||||
├── tests/ # 测试文件
|
||||
│ ├── __init__.py
|
||||
│ ├── test_validator.py # 验证器测试
|
||||
@ -56,83 +29,84 @@ KaMiXiTong/
|
||||
├── docs/ # 文档
|
||||
│ ├── API.md # API文档
|
||||
│ ├── INTEGRATION.md # 集成文档
|
||||
│ └── EXAMPLES.md # 使用示例
|
||||
│ ├── EXAMPLES.md # 使用示例
|
||||
│ └── FASTAPI.md # FastAPI接口文档
|
||||
├── migrations/ # 数据库迁移文件
|
||||
│ └── init.sql
|
||||
└── static/ # 静态文件
|
||||
├── css/
|
||||
├── js/
|
||||
└── uploads/
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
## 使用方式
|
||||
|
||||
### 1. Python验证器 (auth_validator.py)
|
||||
- 轻量级授权验证模块,开发者可嵌入自有Python软件
|
||||
- 支持在线/离线验证模式
|
||||
- 跨平台机器码生成
|
||||
- 防篡改机制
|
||||
### 快速开始
|
||||
|
||||
### 2. Web管理后台
|
||||
- 产品管理:创建、编辑、删除软件产品
|
||||
- 版本管理:版本发布、兼容性控制
|
||||
- 卡密管理:批量生成、导入导出、状态控制
|
||||
- 设备管理:设备绑定、解绑、禁用
|
||||
- 数据统计:下载量、活跃度分析
|
||||
- 工单管理:用户反馈处理
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/yourusername/kamaxitong.git
|
||||
|
||||
# 进入项目目录
|
||||
cd kamaxitong
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 初始化数据库
|
||||
|
||||
#### SQLite数据库(默认)
|
||||
```bash
|
||||
# 初始化数据库
|
||||
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
|
||||
```
|
||||
|
||||
### 4. 嵌入验证器
|
||||
```python
|
||||
from auth_validator import AuthValidator
|
||||
访问地址: http://localhost:5000
|
||||
默认账号: admin / admin123
|
||||
|
||||
validator = AuthValidator(software_id="your_software_id")
|
||||
if not validator.validate():
|
||||
exit() # 验证失败退出程序
|
||||
### FastAPI接口
|
||||
|
||||
系统还提供了现代化的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
|
||||
- **前端**: Bootstrap + jQuery
|
||||
- **加密**: AES + SHA256
|
||||
- **验证器**: 纯Python,支持Python 3.6+
|
||||
- 后端: Flask + SQLAlchemy
|
||||
- 前端: Bootstrap 5 + jQuery
|
||||
- 数据库: SQLite/MySQL
|
||||
- 部署: Gunicorn + Nginx
|
||||
|
||||
## 安全特性
|
||||
## 文档
|
||||
|
||||
- HTTPS通信加密
|
||||
- 数据库敏感信息加密存储
|
||||
- 验证器防篡改机制
|
||||
- 机器码唯一性校验
|
||||
- 防重放攻击
|
||||
- [API文档](docs/API.md)
|
||||
- [集成指南](docs/INTEGRATION.md)
|
||||
- [使用示例](docs/EXAMPLES.md)
|
||||
- [FastAPI接口文档](docs/FASTAPI.md)
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ from flask_login import LoginManager
|
||||
from flask_migrate import Migrate
|
||||
from config import config
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
# 初始化扩展
|
||||
db = SQLAlchemy()
|
||||
@ -65,4 +67,21 @@ def create_app(config_name='default'):
|
||||
from app.web.views import register_error_handlers
|
||||
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
|
||||
|
||||
@ -4,4 +4,4 @@ from flask import Blueprint
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
# 导入所有API路由
|
||||
from . import auth, product, version, license, device, ticket, statistics, settings, admin
|
||||
from . import auth, product, version, license, device, ticket, statistics, settings, admin, log
|
||||
@ -7,6 +7,7 @@ from werkzeug.security import generate_password_hash
|
||||
import functools
|
||||
import re
|
||||
from .decorators import require_admin
|
||||
from app.utils.logger import log_operation
|
||||
|
||||
# 响应码定义
|
||||
class ResponseCode:
|
||||
@ -234,6 +235,13 @@ def create_admin():
|
||||
'role': role,
|
||||
'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='管理员创建成功')
|
||||
|
||||
@ -306,6 +314,16 @@ def update_admin(admin_id):
|
||||
'status': admin.status
|
||||
}
|
||||
})
|
||||
|
||||
# 记录操作日志
|
||||
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='管理员更新成功')
|
||||
|
||||
@ -340,6 +358,12 @@ def toggle_admin_status(admin_id):
|
||||
'old_status': old_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={
|
||||
'status': admin.status,
|
||||
@ -373,6 +397,11 @@ def delete_admin(admin_id):
|
||||
log_audit('DELETE', 'ADMIN', admin_id, {
|
||||
'username': admin.username
|
||||
})
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('DELETE_ADMIN', 'ADMIN', admin_id, {
|
||||
'username': admin.username
|
||||
})
|
||||
|
||||
return create_response(True, message='管理员删除成功')
|
||||
|
||||
|
||||
@ -3,39 +3,12 @@ from datetime import datetime
|
||||
from app import db
|
||||
from app.models import Device, Product, License
|
||||
from . import api_bp
|
||||
from .decorators import require_admin
|
||||
from .decorators import require_login, require_admin
|
||||
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'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def get_devices():
|
||||
"""获取设备列表"""
|
||||
try:
|
||||
@ -48,7 +21,8 @@ def get_devices():
|
||||
product = request.args.get('product')
|
||||
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:
|
||||
@ -72,7 +46,6 @@ def get_devices():
|
||||
)
|
||||
)
|
||||
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.order_by(db.desc(Device.last_verify_time))
|
||||
@ -100,7 +73,7 @@ def get_devices():
|
||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||||
|
||||
@api_bp.route('/devices/<int:device_id>/status', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def update_device_status(device_id):
|
||||
"""更新设备状态"""
|
||||
try:
|
||||
@ -120,6 +93,11 @@ def update_device_status(device_id):
|
||||
device.status = status
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('UPDATE_DEVICE_STATUS', 'DEVICE', device.device_id, {
|
||||
'status': status
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '设备状态更新成功',
|
||||
@ -132,7 +110,7 @@ def update_device_status(device_id):
|
||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||||
|
||||
@api_bp.route('/devices/<int:device_id>', methods=['DELETE'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def delete_device(device_id):
|
||||
"""删除设备"""
|
||||
try:
|
||||
@ -143,6 +121,11 @@ def delete_device(device_id):
|
||||
db.session.delete(device)
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('DELETE_DEVICE', 'DEVICE', device.device_id, {
|
||||
'machine_code': device.machine_code
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '设备删除成功'
|
||||
@ -155,7 +138,7 @@ def delete_device(device_id):
|
||||
|
||||
|
||||
@api_bp.route('/devices/batch', methods=['DELETE'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def batch_delete_devices():
|
||||
"""批量删除设备"""
|
||||
try:
|
||||
@ -189,6 +172,13 @@ def batch_delete_devices():
|
||||
|
||||
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({
|
||||
'success': True,
|
||||
'message': f'成功删除 {len(devices)} 个设备'
|
||||
@ -204,7 +194,7 @@ def batch_delete_devices():
|
||||
|
||||
|
||||
@api_bp.route('/devices/batch/status', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def batch_update_device_status():
|
||||
"""批量更新设备状态"""
|
||||
try:
|
||||
@ -246,6 +236,14 @@ def batch_update_device_status():
|
||||
|
||||
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_name = status_names.get(status, '未知')
|
||||
return jsonify({
|
||||
|
||||
@ -3,15 +3,16 @@ from datetime import datetime, timedelta
|
||||
from app import db
|
||||
from app.models import Product, License
|
||||
from . import api_bp
|
||||
from .decorators import require_admin
|
||||
from .decorators import require_login, require_admin
|
||||
import io
|
||||
import csv
|
||||
import xlsxwriter
|
||||
from functools import wraps
|
||||
from sqlalchemy import or_, func
|
||||
from app.utils.logger import log_operation
|
||||
|
||||
@api_bp.route('/licenses', methods=['GET'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def get_licenses():
|
||||
"""获取卡密列表"""
|
||||
try:
|
||||
@ -22,28 +23,34 @@ def get_licenses():
|
||||
status = request.args.get('status', type=int)
|
||||
license_type = request.args.get('type', type=int)
|
||||
keyword = request.args.get('keyword', '').strip()
|
||||
sort = request.args.get('sort', 'create_time') # 排序字段
|
||||
order = request.args.get('order', 'desc') # 排序方向
|
||||
|
||||
# 构建查询
|
||||
query = License.query
|
||||
# 构建查询 - 使用join预加载product关系,避免N+1查询
|
||||
query = db.session.query(License).join(Product, License.product_id == Product.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:
|
||||
query = query.filter_by(status=status)
|
||||
query = query.filter(License.status == status)
|
||||
|
||||
# 类型筛选
|
||||
if license_type is not None:
|
||||
query = query.filter_by(type=license_type)
|
||||
query = query.filter(License.type == license_type)
|
||||
|
||||
# 关键词搜索(卡密)
|
||||
if keyword:
|
||||
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(
|
||||
@ -52,7 +59,7 @@ def get_licenses():
|
||||
error_out=False
|
||||
)
|
||||
|
||||
# 格式化结果
|
||||
# 格式化结果 - product关系已经通过join加载
|
||||
licenses = [license.to_dict() for license in pagination.items]
|
||||
|
||||
return jsonify({
|
||||
@ -78,7 +85,7 @@ def get_licenses():
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/licenses', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def generate_licenses():
|
||||
"""批量生成卡密"""
|
||||
try:
|
||||
@ -153,6 +160,15 @@ def generate_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({
|
||||
'success': True,
|
||||
'message': f'成功生成 {count} 个卡密',
|
||||
@ -178,7 +194,7 @@ def generate_licenses():
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/licenses/<int:license_id>', methods=['GET'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def get_license(license_id):
|
||||
"""获取单个卡密详情"""
|
||||
try:
|
||||
@ -202,7 +218,7 @@ def get_license(license_id):
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/licenses/<int:license_id>', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def update_license(license_id):
|
||||
"""更新卡密信息"""
|
||||
try:
|
||||
@ -237,6 +253,12 @@ def update_license(license_id):
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('UPDATE_LICENSE', 'LICENSE', license_obj.license_id, {
|
||||
'status': license_obj.status,
|
||||
'remark': license_obj.remark
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '更新成功',
|
||||
@ -252,7 +274,7 @@ def update_license(license_id):
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/licenses/<int:license_id>/extend', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def extend_license(license_id):
|
||||
"""延长卡密有效期"""
|
||||
try:
|
||||
@ -284,6 +306,12 @@ def extend_license(license_id):
|
||||
'message': message
|
||||
}), 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({
|
||||
'success': True,
|
||||
'message': message,
|
||||
@ -299,7 +327,7 @@ def extend_license(license_id):
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/licenses/<int:license_id>/convert', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def convert_license(license_id):
|
||||
"""试用卡密转正式"""
|
||||
try:
|
||||
@ -331,6 +359,11 @@ def convert_license(license_id):
|
||||
'message': message
|
||||
}), 400
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('CONVERT_LICENSE', 'LICENSE', license_obj.license_id, {
|
||||
'valid_days': valid_days
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': message,
|
||||
@ -346,7 +379,7 @@ def convert_license(license_id):
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/licenses/<int:license_id>/unbind', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def unbind_license(license_id):
|
||||
"""解绑设备"""
|
||||
try:
|
||||
@ -367,6 +400,11 @@ def unbind_license(license_id):
|
||||
'message': message
|
||||
}), 400
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('UNBIND_LICENSE', 'LICENSE', license_obj.license_id, {
|
||||
'machine_code': license_obj.bind_machine_code
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': message,
|
||||
@ -382,7 +420,7 @@ def unbind_license(license_id):
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/licenses/export', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def export_licenses():
|
||||
"""导出卡密"""
|
||||
try:
|
||||
@ -509,7 +547,7 @@ def export_licenses():
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/licenses/import', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def import_licenses():
|
||||
"""导入卡密"""
|
||||
try:
|
||||
@ -623,7 +661,7 @@ def import_licenses():
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/licenses/<string:license_key>', methods=['DELETE'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def delete_license(license_key):
|
||||
"""删除卡密"""
|
||||
try:
|
||||
@ -649,6 +687,11 @@ def delete_license(license_key):
|
||||
db.session.delete(license_obj)
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('DELETE_LICENSE', 'LICENSE', None, {
|
||||
'license_key': license_key
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '卡密删除成功'
|
||||
@ -664,7 +707,7 @@ def delete_license(license_key):
|
||||
|
||||
|
||||
@api_bp.route('/licenses/batch', methods=['DELETE'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def batch_delete_licenses():
|
||||
"""批量删除卡密"""
|
||||
try:
|
||||
@ -707,6 +750,13 @@ def batch_delete_licenses():
|
||||
|
||||
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({
|
||||
'success': True,
|
||||
'message': f'成功删除 {len(licenses)} 个卡密'
|
||||
@ -722,7 +772,7 @@ def batch_delete_licenses():
|
||||
|
||||
|
||||
@api_bp.route('/licenses/batch/status', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def batch_update_license_status():
|
||||
"""批量更新卡密状态"""
|
||||
try:
|
||||
@ -774,6 +824,14 @@ def batch_update_license_status():
|
||||
|
||||
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_name = status_names.get(status, '未知')
|
||||
return jsonify({
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
from flask import request, jsonify, current_app
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
from app.models import Product
|
||||
from app.models import Product, License, Device, Version
|
||||
from . import api_bp
|
||||
from .decorators import require_admin
|
||||
from flask_login import login_required
|
||||
from .decorators import require_login, require_admin
|
||||
from sqlalchemy import func, case
|
||||
import traceback
|
||||
import sys
|
||||
from app.utils.logger import log_operation
|
||||
|
||||
@api_bp.route('/products', methods=['GET'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def get_products():
|
||||
"""获取产品列表"""
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = min(request.args.get('per_page', 20, type=int), 100)
|
||||
keyword = request.args.get('keyword', '').strip()
|
||||
include_stats = request.args.get('include_stats', 'true').lower() == 'true'
|
||||
|
||||
query = Product.query
|
||||
|
||||
@ -28,7 +30,66 @@ def get_products():
|
||||
query = query.order_by(Product.create_time.desc())
|
||||
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({
|
||||
'success': True,
|
||||
@ -56,7 +117,7 @@ def get_products():
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/products', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def create_product():
|
||||
"""创建产品"""
|
||||
try:
|
||||
@ -97,6 +158,12 @@ def create_product():
|
||||
db.session.add(product)
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('CREATE_PRODUCT', 'PRODUCT', product.product_id, {
|
||||
'product_name': product.product_name,
|
||||
'description': product.description
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '产品创建成功',
|
||||
@ -112,7 +179,7 @@ def create_product():
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/products/<product_id>', methods=['GET'])
|
||||
@login_required
|
||||
@require_login
|
||||
def get_product(product_id):
|
||||
"""获取产品详情"""
|
||||
try:
|
||||
@ -136,7 +203,7 @@ def get_product(product_id):
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/products/<product_id>', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def update_product(product_id):
|
||||
"""更新产品"""
|
||||
try:
|
||||
@ -163,6 +230,12 @@ def update_product(product_id):
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('UPDATE_PRODUCT', 'PRODUCT', product.product_id, {
|
||||
'product_name': product.product_name,
|
||||
'description': product.description
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '产品更新成功',
|
||||
@ -178,7 +251,7 @@ def update_product(product_id):
|
||||
}), 500
|
||||
|
||||
@api_bp.route('/products/<product_id>', methods=['DELETE'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def delete_product(product_id):
|
||||
"""删除产品"""
|
||||
try:
|
||||
@ -200,6 +273,11 @@ def delete_product(product_id):
|
||||
db.session.delete(product)
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('DELETE_PRODUCT', 'PRODUCT', product.product_id, {
|
||||
'product_name': product.product_name
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '产品删除成功'
|
||||
@ -215,7 +293,7 @@ def delete_product(product_id):
|
||||
|
||||
|
||||
@api_bp.route('/products/batch', methods=['DELETE'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def batch_delete_products():
|
||||
"""批量删除产品"""
|
||||
try:
|
||||
@ -267,6 +345,13 @@ def batch_delete_products():
|
||||
|
||||
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({
|
||||
'success': True,
|
||||
'message': f'成功删除 {len(products)} 个产品'
|
||||
@ -282,7 +367,7 @@ def batch_delete_products():
|
||||
|
||||
|
||||
@api_bp.route('/products/batch/status', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def batch_update_product_status():
|
||||
"""批量更新产品状态"""
|
||||
try:
|
||||
@ -324,6 +409,14 @@ def batch_update_product_status():
|
||||
|
||||
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 '禁用'
|
||||
return jsonify({
|
||||
'success': True,
|
||||
|
||||
@ -1,44 +1,67 @@
|
||||
from flask import request, jsonify, current_app
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import func, and_, not_, text
|
||||
from datetime import datetime, timedelta, time
|
||||
from sqlalchemy import func, and_, not_, text, case
|
||||
from app import db
|
||||
from app.models import Product, License, Device, Version, Ticket
|
||||
from . import api_bp
|
||||
from .license import require_admin
|
||||
from .decorators import require_login, require_admin
|
||||
|
||||
@api_bp.route('/statistics/overview', methods=['GET'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def get_overview_stats():
|
||||
"""获取总览统计"""
|
||||
try:
|
||||
# 产品统计
|
||||
total_products = Product.query.count()
|
||||
active_products = Product.query.filter_by(status=1).count()
|
||||
# 优化:使用单个查询获取所有产品统计
|
||||
product_stats = db.session.query(
|
||||
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()
|
||||
active_licenses = License.query.filter_by(status=1).count()
|
||||
trial_licenses = License.query.filter_by(type=0).count()
|
||||
# 优化:使用单个查询获取所有卡密统计
|
||||
license_stats = db.session.query(
|
||||
func.count(License.license_id).label('total'),
|
||||
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(
|
||||
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)}
|
||||
).scalar()
|
||||
{"start_date": start_date}
|
||||
).scalar() or 0
|
||||
|
||||
# 近期激活统计
|
||||
today_activations = License.query.filter(
|
||||
func.date(License.activate_time) == datetime.utcnow().date()
|
||||
).count()
|
||||
|
||||
week_activations = db.session.execute(
|
||||
text("SELECT COUNT(*) FROM license WHERE activate_time IS NOT NULL AND activate_time >= :start_date"),
|
||||
{"start_date": datetime.utcnow() - timedelta(days=7)}
|
||||
).scalar()
|
||||
# 优化:使用单个查询获取激活统计
|
||||
today = datetime.utcnow().date()
|
||||
today_start = datetime.combine(today, time.min)
|
||||
today_end = datetime.combine(today, time.max)
|
||||
|
||||
# 今日激活数(使用日期范围查询,兼容SQLite)
|
||||
today_activations = db.session.query(func.count(License.license_id)).filter(
|
||||
License.activate_time.isnot(None),
|
||||
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({
|
||||
'success': True,
|
||||
@ -73,7 +96,7 @@ def get_overview_stats():
|
||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||||
|
||||
@api_bp.route('/statistics/activations', methods=['GET'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def get_activation_trend():
|
||||
"""获取激活趋势"""
|
||||
try:
|
||||
@ -115,7 +138,7 @@ def get_activation_trend():
|
||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||||
|
||||
@api_bp.route('/statistics/products', methods=['GET'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def get_product_stats():
|
||||
"""获取产品统计信息"""
|
||||
try:
|
||||
@ -148,4 +171,4 @@ def get_product_stats():
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"获取产品统计失败: {str(e)}")
|
||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||||
@ -3,10 +3,11 @@ from datetime import datetime
|
||||
from app import db
|
||||
from app.models import Ticket, Product
|
||||
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'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def get_tickets():
|
||||
"""获取工单列表"""
|
||||
try:
|
||||
@ -80,6 +81,12 @@ def create_ticket():
|
||||
db.session.add(ticket)
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('CREATE_TICKET', 'TICKET', ticket.ticket_id, {
|
||||
'title': ticket.title,
|
||||
'product_id': ticket.product_id
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '工单创建成功',
|
||||
@ -93,7 +100,7 @@ def create_ticket():
|
||||
|
||||
|
||||
@api_bp.route('/tickets/batch/status', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def batch_update_ticket_status():
|
||||
"""批量更新工单状态"""
|
||||
try:
|
||||
@ -134,6 +141,14 @@ def batch_update_ticket_status():
|
||||
for ticket in tickets:
|
||||
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_name = status_names.get(status, '未知')
|
||||
return jsonify({
|
||||
|
||||
@ -4,15 +4,16 @@ import os
|
||||
import hashlib
|
||||
from werkzeug.utils import secure_filename
|
||||
from app import db
|
||||
from app.models import Version, Product
|
||||
from app.models import Version, Product, Device
|
||||
from . import api_bp
|
||||
from .decorators import require_admin
|
||||
from sqlalchemy import desc
|
||||
from .decorators import require_login, require_admin
|
||||
from sqlalchemy import desc, func, case, or_
|
||||
import traceback
|
||||
import sys
|
||||
from app.utils.logger import log_operation
|
||||
|
||||
@api_bp.route('/versions', methods=['GET'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def get_versions():
|
||||
"""获取版本列表"""
|
||||
try:
|
||||
@ -20,6 +21,7 @@ def get_versions():
|
||||
per_page = min(request.args.get('per_page', 20, type=int), 100)
|
||||
product_id = request.args.get('product_id')
|
||||
status = request.args.get('publish_status', type=int)
|
||||
include_stats = request.args.get('include_stats', 'true').lower() == 'true'
|
||||
|
||||
query = Version.query
|
||||
|
||||
@ -31,7 +33,45 @@ def get_versions():
|
||||
query = query.order_by(desc(Version.create_time))
|
||||
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({
|
||||
'success': True,
|
||||
@ -51,7 +91,7 @@ def get_versions():
|
||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||||
|
||||
@api_bp.route('/versions', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def create_version():
|
||||
"""创建版本"""
|
||||
try:
|
||||
@ -154,6 +194,13 @@ def create_version():
|
||||
current_app.logger.info("发布版本")
|
||||
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({
|
||||
'success': True,
|
||||
'message': '版本创建成功',
|
||||
@ -169,7 +216,7 @@ def create_version():
|
||||
return jsonify({'success': False, 'message': f'服务器内部错误: {str(e)}'}), 500
|
||||
|
||||
@api_bp.route('/versions/<int:version_id>/publish', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def publish_version(version_id):
|
||||
"""发布版本"""
|
||||
try:
|
||||
@ -178,6 +225,12 @@ def publish_version(version_id):
|
||||
return jsonify({'success': False, 'message': '版本不存在'}), 404
|
||||
|
||||
version.publish()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('PUBLISH_VERSION', 'VERSION', version.version_id, {
|
||||
'version_num': version.version_num
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '版本发布成功',
|
||||
@ -189,7 +242,7 @@ def publish_version(version_id):
|
||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||||
|
||||
@api_bp.route('/versions/<int:version_id>', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def update_version(version_id):
|
||||
"""更新版本信息"""
|
||||
try:
|
||||
@ -219,6 +272,13 @@ def update_version(version_id):
|
||||
# 保存更新
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('UPDATE_VERSION', 'VERSION', version.version_id, {
|
||||
'version_num': version.version_num,
|
||||
'platform': version.platform,
|
||||
'description': version.description
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '版本信息更新成功',
|
||||
@ -232,7 +292,7 @@ def update_version(version_id):
|
||||
|
||||
|
||||
@api_bp.route('/versions/<int:version_id>/status', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def update_version_status(version_id):
|
||||
"""更新版本状态(发布/取消发布)"""
|
||||
try:
|
||||
@ -260,6 +320,14 @@ def update_version_status(version_id):
|
||||
else:
|
||||
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({
|
||||
'success': True,
|
||||
'message': message,
|
||||
@ -271,7 +339,7 @@ def update_version_status(version_id):
|
||||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||||
|
||||
@api_bp.route('/versions/upload', methods=['POST'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def upload_version_file():
|
||||
"""上传版本文件"""
|
||||
try:
|
||||
@ -349,7 +417,7 @@ def extract_filename_from_url(download_url):
|
||||
return None
|
||||
|
||||
@api_bp.route('/versions/<int:version_id>', methods=['DELETE'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def delete_version(version_id):
|
||||
"""删除版本"""
|
||||
try:
|
||||
@ -383,6 +451,11 @@ def delete_version(version_id):
|
||||
db.session.delete(version)
|
||||
db.session.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation('DELETE_VERSION', 'VERSION', version.version_id, {
|
||||
'version_num': version.version_num
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '版本删除成功'
|
||||
@ -395,7 +468,7 @@ def delete_version(version_id):
|
||||
|
||||
|
||||
@api_bp.route('/versions/batch', methods=['DELETE'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def batch_delete_versions():
|
||||
"""批量删除版本"""
|
||||
try:
|
||||
@ -470,6 +543,13 @@ def batch_delete_versions():
|
||||
|
||||
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({
|
||||
'success': True,
|
||||
'message': f'成功删除 {len(versions)} 个版本'
|
||||
@ -485,7 +565,7 @@ def batch_delete_versions():
|
||||
|
||||
|
||||
@api_bp.route('/versions/batch/status', methods=['PUT'])
|
||||
@require_admin
|
||||
@require_login
|
||||
def batch_update_version_status():
|
||||
"""批量更新版本状态(发布/取消发布)"""
|
||||
try:
|
||||
@ -528,6 +608,14 @@ def batch_update_version_status():
|
||||
else:
|
||||
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 '取消发布'
|
||||
return jsonify({
|
||||
'success': True,
|
||||
|
||||
@ -30,6 +30,11 @@ class Product(db.Model):
|
||||
|
||||
def get_stats(self):
|
||||
"""获取产品统计信息"""
|
||||
# 如果已经有缓存的统计信息(来自批量查询),直接使用
|
||||
if hasattr(self, '_cached_stats'):
|
||||
return self._cached_stats
|
||||
|
||||
# 否则执行单个查询(用于单个产品详情等场景)
|
||||
total_licenses = self.licenses.count()
|
||||
active_licenses = self.licenses.filter_by(status=1).count()
|
||||
total_devices = self.devices.filter_by(status=1).count()
|
||||
|
||||
@ -47,7 +47,11 @@ class Version(db.Model):
|
||||
|
||||
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
|
||||
return Device.query.filter_by(
|
||||
product_id=self.product_id,
|
||||
@ -56,7 +60,11 @@ class Version(db.Model):
|
||||
|
||||
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
|
||||
return Device.query.filter_by(
|
||||
product_id=self.product_id,
|
||||
|
||||
@ -135,6 +135,12 @@
|
||||
系统设置
|
||||
</a>
|
||||
</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 %}
|
||||
</ul>
|
||||
|
||||
@ -227,7 +233,19 @@
|
||||
window.location.href = '/login';
|
||||
}, 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 => {
|
||||
throw new Error(`${response.status}: ${errorData.message || response.statusText}`);
|
||||
}).catch(() => {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
{% block content %}
|
||||
<!-- 统计卡片 -->
|
||||
<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-body">
|
||||
<div class="row">
|
||||
@ -25,7 +25,7 @@
|
||||
</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-body">
|
||||
<div class="row">
|
||||
@ -43,7 +43,7 @@
|
||||
</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-body">
|
||||
<div class="row">
|
||||
@ -61,7 +61,7 @@
|
||||
</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-body">
|
||||
<div class="row">
|
||||
@ -325,7 +325,7 @@ function initCharts() {
|
||||
|
||||
// 加载最近激活
|
||||
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 => {
|
||||
if (data.success) {
|
||||
const tbody = document.getElementById('recent-activations');
|
||||
|
||||
@ -124,26 +124,51 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 从URL参数中读取筛选条件
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('product_id')) {
|
||||
document.getElementById('search-product').value = urlParams.get('product_id');
|
||||
}
|
||||
if (urlParams.has('software_version')) {
|
||||
// 这里可以添加版本筛选,但当前UI没有对应的输入框
|
||||
}
|
||||
if (urlParams.has('status')) {
|
||||
document.getElementById('search-status').value = urlParams.get('status');
|
||||
// 使用立即执行函数确保在DOM和所有脚本加载完成后执行
|
||||
(function() {
|
||||
function init() {
|
||||
console.log('设备列表页面已加载,开始初始化...');
|
||||
console.log('apiRequest函数是否存在:', typeof apiRequest);
|
||||
|
||||
// 从URL参数中读取筛选条件
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('product_id')) {
|
||||
document.getElementById('search-product').value = urlParams.get('product_id');
|
||||
}
|
||||
if (urlParams.has('software_version')) {
|
||||
// 这里可以添加版本筛选,但当前UI没有对应的输入框
|
||||
}
|
||||
if (urlParams.has('status')) {
|
||||
document.getElementById('search-status').value = urlParams.get('status');
|
||||
}
|
||||
|
||||
if (typeof apiRequest === 'function') {
|
||||
loadDevices();
|
||||
initEventListeners();
|
||||
} else {
|
||||
console.error('apiRequest函数未定义,等待脚本加载...');
|
||||
setTimeout(function() {
|
||||
if (typeof apiRequest === 'function') {
|
||||
loadDevices();
|
||||
initEventListeners();
|
||||
} else {
|
||||
console.error('apiRequest函数仍未定义,请检查脚本加载顺序');
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
loadDevices();
|
||||
initEventListeners();
|
||||
});
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
||||
// 初始化事件监听器
|
||||
function initEventListeners() {
|
||||
@ -193,6 +218,7 @@ function initEventListeners() {
|
||||
|
||||
// 加载设备列表
|
||||
function loadDevices(page = 1) {
|
||||
console.log('loadDevices函数被调用,页码:', page);
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
per_page: 10
|
||||
@ -216,7 +242,10 @@ function loadDevices(page = 1) {
|
||||
if (productId) params.append('product_id', productId);
|
||||
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 => {
|
||||
if (data.success) {
|
||||
renderDeviceList(data.data.devices);
|
||||
@ -584,4 +613,5 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
batchDeleteBtn.addEventListener('click', showBatchDeleteModal);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -147,14 +147,38 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadLicenses();
|
||||
initEventListeners();
|
||||
});
|
||||
// 使用立即执行函数确保在DOM和所有脚本加载完成后执行
|
||||
(function() {
|
||||
function init() {
|
||||
console.log('卡密列表页面已加载,开始初始化...');
|
||||
console.log('apiRequest函数是否存在:', typeof apiRequest);
|
||||
if (typeof apiRequest === 'function') {
|
||||
loadLicenses();
|
||||
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() {
|
||||
@ -210,6 +234,7 @@ function initEventListeners() {
|
||||
|
||||
// 加载卡密列表
|
||||
function loadLicenses(page = 1) {
|
||||
console.log('loadLicenses函数被调用,页码:', page);
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
per_page: 10
|
||||
@ -226,7 +251,10 @@ function loadLicenses(page = 1) {
|
||||
if (type) params.append('type', type);
|
||||
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 => {
|
||||
if (data.success) {
|
||||
renderLicenseList(data.data.licenses);
|
||||
@ -782,4 +810,5 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
batchDeleteBtn.addEventListener('click', showBatchDeleteModal);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -52,103 +52,114 @@
|
||||
<script>
|
||||
// 自动聚焦到用户名输入框
|
||||
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) {
|
||||
e.preventDefault();
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const loginForm = document.getElementById('login-form');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const loginBtnText = document.getElementById('login-btn-text');
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const remember = document.getElementById('remember').checked;
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const loginBtnText = document.getElementById('login-btn-text');
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const remember = document.getElementById('remember').checked;
|
||||
|
||||
// 基础验证
|
||||
if (!username || !password) {
|
||||
showNotification('请填写用户名和密码', 'warning');
|
||||
return;
|
||||
}
|
||||
// 基础验证
|
||||
if (!username || !password) {
|
||||
// 使用基础模板中的showNotification函数
|
||||
showNotification('请填写用户名和密码', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
loginBtn.disabled = true;
|
||||
loginBtnText.textContent = '登录中...';
|
||||
// 显示加载状态
|
||||
if (loginBtn) loginBtn.disabled = true;
|
||||
if (loginBtnText) loginBtnText.textContent = '登录中...';
|
||||
|
||||
try {
|
||||
console.log('尝试登录:', username);
|
||||
try {
|
||||
console.log('尝试登录:', username);
|
||||
|
||||
// 尝试使用Web表单登录(传统Flask登录)
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
formData.append('remember', remember);
|
||||
|
||||
// 获取CSRF令牌
|
||||
try {
|
||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||
formData.append('csrf_token', csrfToken);
|
||||
} catch (e) {
|
||||
console.error('获取CSRF令牌失败:', e);
|
||||
}
|
||||
|
||||
const response = await fetch('/login', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin' // 重要:发送cookies
|
||||
});
|
||||
|
||||
console.log('登录响应状态:', response.status);
|
||||
console.log('登录响应URL:', response.url);
|
||||
|
||||
// 检查响应状态码
|
||||
if (response.status === 200) {
|
||||
// 检查是否真正登录成功(重定向到dashboard)
|
||||
if (response.url && response.url.includes('/dashboard')) {
|
||||
// 登录成功,重定向到dashboard
|
||||
window.location.href = '/dashboard';
|
||||
} else {
|
||||
// 尝试解析响应内容
|
||||
const responseData = await response.text();
|
||||
// 检查响应内容中是否包含成功信息
|
||||
if (responseData.includes('dashboard') || responseData.includes('仪表板')) {
|
||||
// 登录成功
|
||||
window.location.href = '/dashboard';
|
||||
} else if (responseData.includes('用户名或密码错误')) {
|
||||
showNotification('用户名或密码错误,请重试', 'error');
|
||||
} else {
|
||||
// 尝试解析为JSON
|
||||
try {
|
||||
const jsonData = JSON.parse(responseData);
|
||||
if (jsonData.success) {
|
||||
window.location.href = jsonData.redirect || '/dashboard';
|
||||
} else {
|
||||
showNotification(jsonData.message || '登录失败,请检查用户名和密码', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('登录失败,请检查用户名和密码', 'error');
|
||||
// 尝试使用Web表单登录(传统Flask登录)
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
formData.append('remember', remember);
|
||||
|
||||
// 获取CSRF令牌
|
||||
try {
|
||||
const csrfTokenInput = document.querySelector('input[name="csrf_token"]');
|
||||
if (csrfTokenInput) {
|
||||
formData.append('csrf_token', csrfTokenInput.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取CSRF令牌失败:', e);
|
||||
}
|
||||
}
|
||||
} else if (response.status === 400) {
|
||||
// CSRF错误或其他客户端错误
|
||||
const errorData = await response.text();
|
||||
if (errorData.includes('CSRF')) {
|
||||
showNotification('CSRF令牌验证失败,请刷新页面后重试', 'error');
|
||||
} else {
|
||||
showNotification('请求参数错误,请刷新页面后重试', 'error');
|
||||
}
|
||||
} else {
|
||||
showNotification('登录失败,请检查用户名和密码', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录请求失败:', error);
|
||||
showNotification('网络错误,请检查网络连接后重试', 'error');
|
||||
} finally {
|
||||
// 恢复按钮状态
|
||||
loginBtn.disabled = false;
|
||||
loginBtnText.textContent = '登录';
|
||||
const response = await fetch('/login', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin' // 重要:发送cookies
|
||||
});
|
||||
|
||||
console.log('登录响应状态:', response.status);
|
||||
console.log('登录响应URL:', response.url);
|
||||
|
||||
// 检查响应状态码
|
||||
if (response.status === 200) {
|
||||
// 检查是否真正登录成功(重定向到dashboard)
|
||||
if (response.url && response.url.includes('/dashboard')) {
|
||||
// 登录成功,重定向到dashboard
|
||||
window.location.href = '/dashboard';
|
||||
} else {
|
||||
// 尝试解析响应内容
|
||||
const responseData = await response.text();
|
||||
// 检查响应内容中是否包含成功信息
|
||||
if (responseData.includes('dashboard') || responseData.includes('仪表板')) {
|
||||
// 登录成功
|
||||
window.location.href = '/dashboard';
|
||||
} else if (responseData.includes('用户名或密码错误')) {
|
||||
showNotification('用户名或密码错误,请重试', 'error');
|
||||
} else {
|
||||
// 尝试解析为JSON
|
||||
try {
|
||||
const jsonData = JSON.parse(responseData);
|
||||
if (jsonData.success) {
|
||||
window.location.href = jsonData.redirect || '/dashboard';
|
||||
} else {
|
||||
showNotification(jsonData.message || '登录失败,请检查用户名和密码', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('登录失败,请检查用户名和密码', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (response.status === 400) {
|
||||
// CSRF错误或其他客户端错误
|
||||
const errorData = await response.text();
|
||||
if (errorData.includes('CSRF')) {
|
||||
showNotification('CSRF令牌验证失败,请刷新页面后重试', 'error');
|
||||
} else {
|
||||
showNotification('请求参数错误,请刷新页面后重试', 'error');
|
||||
}
|
||||
} else {
|
||||
showNotification('登录失败,请检查用户名和密码', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录请求失败:', error);
|
||||
showNotification('网络错误,请检查网络连接后重试', 'error');
|
||||
} finally {
|
||||
// 恢复按钮状态
|
||||
if (loginBtn) loginBtn.disabled = false;
|
||||
if (loginBtnText) loginBtnText.textContent = '登录';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -133,15 +133,41 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let currentKeyword = '';
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadProducts();
|
||||
initEventListeners();
|
||||
});
|
||||
// 使用立即执行函数确保在DOM和所有脚本加载完成后执行
|
||||
(function() {
|
||||
function init() {
|
||||
console.log('产品列表页面已加载,开始初始化...');
|
||||
console.log('apiRequest函数是否存在:', typeof apiRequest);
|
||||
if (typeof apiRequest === 'function') {
|
||||
loadProducts();
|
||||
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() {
|
||||
@ -196,6 +222,7 @@ function initEventListeners() {
|
||||
|
||||
// 加载产品列表
|
||||
function loadProducts(page = 1) {
|
||||
console.log('loadProducts函数被调用,页码:', page);
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
per_page: 10
|
||||
@ -205,8 +232,10 @@ function loadProducts(page = 1) {
|
||||
params.append('keyword', currentKeyword);
|
||||
}
|
||||
|
||||
// 使用完整的API路径
|
||||
const apiUrl = `/api/v1/products?${params}`;
|
||||
console.log('准备请求API:', apiUrl);
|
||||
|
||||
// 使用apiRequest函数,它已经处理了加载动画和错误处理
|
||||
apiRequest(apiUrl)
|
||||
.then(data => {
|
||||
if (data && data.success && data.data) {
|
||||
@ -228,22 +257,9 @@ function loadProducts(page = 1) {
|
||||
renderProductList([]);
|
||||
renderPagination({ pages: 0, page: 1, has_prev: false, has_next: false });
|
||||
|
||||
// 处理不同类型的错误
|
||||
if (error.message) {
|
||||
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');
|
||||
}
|
||||
} else {
|
||||
showNotification('加载产品列表失败: 网络连接异常', 'error');
|
||||
// apiRequest已经处理了401和403错误,这里只处理其他错误
|
||||
if (error.message && !error.message.includes('401') && !error.message.includes('403')) {
|
||||
showNotification('加载产品列表失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -414,8 +430,26 @@ function showDeleteModal(productId, productName) {
|
||||
|
||||
// 删除产品
|
||||
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 => {
|
||||
if (data && data.success) {
|
||||
@ -430,6 +464,9 @@ function deleteProduct(productId) {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// 隐藏加载动画
|
||||
hideLoading();
|
||||
|
||||
console.error('Failed to delete product:', error);
|
||||
showNotification('删除失败: ' + (error.message || '未知错误'), 'error');
|
||||
});
|
||||
@ -467,10 +504,28 @@ function batchDeleteProducts() {
|
||||
const selectedCheckboxes = document.querySelectorAll('.product-checkbox:checked');
|
||||
const productIds = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.productId);
|
||||
|
||||
apiRequest('/api/v1/products/batch', {
|
||||
// 显示加载动画
|
||||
showLoading();
|
||||
|
||||
fetch('/api/v1/products/batch', {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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 => {
|
||||
if (data && data.success) {
|
||||
showNotification(data.message || '批量删除成功', 'success');
|
||||
@ -499,6 +554,9 @@ function batchDeleteProducts() {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// 隐藏加载动画
|
||||
hideLoading();
|
||||
|
||||
console.error('Failed to batch delete products:', error);
|
||||
showNotification('批量删除失败: ' + (error.message || '未知错误'), 'error');
|
||||
|
||||
@ -518,13 +576,31 @@ function batchUpdateProductStatus(status) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiRequest('/api/v1/products/batch/status', {
|
||||
// 显示加载动画
|
||||
showLoading();
|
||||
|
||||
fetch('/api/v1/products/batch/status', {
|
||||
method: 'PUT',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_ids: productIds,
|
||||
status: status
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
// 隐藏加载动画
|
||||
hideLoading();
|
||||
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('未授权访问');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data && data.success) {
|
||||
showNotification(data.message || '批量更新状态成功', 'success');
|
||||
@ -537,6 +613,9 @@ function batchUpdateProductStatus(status) {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// 隐藏加载动画
|
||||
hideLoading();
|
||||
|
||||
console.error('Failed to batch update product status:', error);
|
||||
showNotification('批量更新状态失败: ' + (error.message || '未知错误'), 'error');
|
||||
});
|
||||
@ -550,9 +629,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -189,15 +189,39 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let batchUpdateStatus = null; // 用于存储批量更新的状态
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadTickets();
|
||||
initEventListeners();
|
||||
});
|
||||
// 使用立即执行函数确保在DOM和所有脚本加载完成后执行
|
||||
(function() {
|
||||
function init() {
|
||||
console.log('工单列表页面已加载,开始初始化...');
|
||||
console.log('apiRequest函数是否存在:', typeof apiRequest);
|
||||
if (typeof apiRequest === 'function') {
|
||||
loadTickets();
|
||||
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() {
|
||||
@ -264,6 +288,7 @@ function initEventListeners() {
|
||||
|
||||
// 加载工单列表
|
||||
function loadTickets(page = 1) {
|
||||
console.log('loadTickets函数被调用,页码:', page);
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
per_page: 10
|
||||
@ -280,7 +305,10 @@ function loadTickets(page = 1) {
|
||||
if (priority) params.append('priority', priority);
|
||||
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 => {
|
||||
if (data.success) {
|
||||
renderTicketList(data.data.tickets);
|
||||
@ -594,4 +622,5 @@ function batchUpdateTicketStatus() {
|
||||
showNotification('批量更新状态失败', 'error');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -130,14 +130,38 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadVersions();
|
||||
initEventListeners();
|
||||
});
|
||||
// 使用立即执行函数确保在DOM和所有脚本加载完成后执行
|
||||
(function() {
|
||||
function init() {
|
||||
console.log('版本列表页面已加载,开始初始化...');
|
||||
console.log('apiRequest函数是否存在:', typeof apiRequest);
|
||||
if (typeof apiRequest === 'function') {
|
||||
loadVersions();
|
||||
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() {
|
||||
@ -186,6 +210,7 @@ function initEventListeners() {
|
||||
|
||||
// 加载版本列表
|
||||
function loadVersions(page = 1) {
|
||||
console.log('loadVersions函数被调用,页码:', page);
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
per_page: 10
|
||||
@ -200,7 +225,10 @@ function loadVersions(page = 1) {
|
||||
if (product) params.append('product_id', product);
|
||||
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 => {
|
||||
if (data.success) {
|
||||
renderVersionList(data.data.versions);
|
||||
@ -535,4 +563,5 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
batchDeleteBtn.addEventListener('click', showBatchDeleteModal);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -6,7 +6,7 @@ from app import db
|
||||
from app.web import web_bp
|
||||
import sys
|
||||
import platform
|
||||
from flask import __version__ as flask_version
|
||||
from flask import __version__ as flask_version, current_app
|
||||
|
||||
|
||||
def register_error_handlers(app):
|
||||
@ -151,6 +151,11 @@ def statistics():
|
||||
@login_required
|
||||
def settings():
|
||||
"""系统设置页面"""
|
||||
# 只有超级管理员可以访问
|
||||
if not current_user.is_super_admin():
|
||||
flash('需要超级管理员权限', 'error')
|
||||
return redirect(url_for('web.dashboard'))
|
||||
|
||||
import sys
|
||||
import platform
|
||||
from flask import __version__ as flask_version, current_app
|
||||
@ -188,4 +193,16 @@ def admins():
|
||||
flash('需要超级管理员权限', 'error')
|
||||
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')
|
||||
35
config.py
35
config.py
@ -10,10 +10,15 @@ class Config:
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///kamaxitong.db'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# SQLAlchemy 2.0 兼容性设置
|
||||
# SQLAlchemy 2.0 兼容性设置和连接池优化
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
"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
|
||||
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):
|
||||
"""开发环境配置"""
|
||||
DEBUG = True
|
||||
SQLALCHEMY_ECHO = True
|
||||
SQLALCHEMY_ECHO = False # 关闭SQL日志输出以提高性能,需要调试时可临时开启
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""生产环境配置"""
|
||||
@ -67,6 +89,11 @@ class ProductionConfig(Config):
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
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.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||||
|
||||
Binary file not shown.
66
start.py
66
start.py
@ -8,6 +8,8 @@ KaMiXiTong 启动脚本
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
def check_python_version():
|
||||
@ -23,6 +25,13 @@ def install_dependencies():
|
||||
print("📦 正在安装依赖包...")
|
||||
try:
|
||||
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("✅ 依赖安装完成")
|
||||
except subprocess.CalledProcessError:
|
||||
print("❌ 依赖安装失败")
|
||||
@ -87,6 +96,39 @@ def create_directories():
|
||||
Path(directory).mkdir(parents=True, exist_ok=True)
|
||||
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():
|
||||
"""主函数"""
|
||||
print("=" * 50)
|
||||
@ -115,21 +157,27 @@ def main():
|
||||
print("\n" + "=" * 50)
|
||||
print("🚀 启动开发服务器...")
|
||||
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("🔑 管理员密码: admin123")
|
||||
print("📚 API文档: http://localhost:5000/api/v1")
|
||||
print("⏹️ 按 Ctrl+C 停止服务器")
|
||||
print("=" * 50 + "\n")
|
||||
|
||||
# 启动Flask应用
|
||||
# 启动Flask和FastAPI应用
|
||||
try:
|
||||
from run import app
|
||||
app.run(
|
||||
host='0.0.0.0',
|
||||
port=5000,
|
||||
debug=True
|
||||
)
|
||||
# 在单独的线程中启动Flask应用
|
||||
flask_thread = threading.Thread(target=start_flask_app)
|
||||
flask_thread.daemon = True
|
||||
flask_thread.start()
|
||||
|
||||
# 等待Flask启动
|
||||
time.sleep(2)
|
||||
|
||||
# 在主线程中启动FastAPI应用
|
||||
start_fastapi_app()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 服务器已停止")
|
||||
except Exception as e:
|
||||
|
||||
@ -52,4 +52,64 @@ function formatFileSize(bytes) {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
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;
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user