diff --git a/README.md b/README.md index eee20a8..2e8d1d1 100644 --- a/README.md +++ b/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) ## 许可证 diff --git a/app/__init__.py b/app/__init__.py index 60937c7..2346763 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 diff --git a/app/api/__init__.py b/app/api/__init__.py index 779a25d..cf880b6 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -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 \ No newline at end of file +from . import auth, product, version, license, device, ticket, statistics, settings, admin, log \ No newline at end of file diff --git a/app/api/admin.py b/app/api/admin.py index 638a7ed..f5058a2 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -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='管理员删除成功') diff --git a/app/api/device.py b/app/api/device.py index 2730e98..4b634ed 100644 --- a/app/api/device.py +++ b/app/api/device.py @@ -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//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/', 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({ diff --git a/app/api/license.py b/app/api/license.py index 07d5e55..123b5e6 100644 --- a/app/api/license.py +++ b/app/api/license.py @@ -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/', 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/', 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//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//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//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/', 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({ diff --git a/app/api/product.py b/app/api/product.py index 81353eb..c370545 100644 --- a/app/api/product.py +++ b/app/api/product.py @@ -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/', 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/', 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/', 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, diff --git a/app/api/statistics.py b/app/api/statistics.py index 08cc192..6cc26c1 100644 --- a/app/api/statistics.py +++ b/app/api/statistics.py @@ -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 \ No newline at end of file diff --git a/app/api/ticket.py b/app/api/ticket.py index 6289f8d..f047411 100644 --- a/app/api/ticket.py +++ b/app/api/ticket.py @@ -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({ diff --git a/app/api/version.py b/app/api/version.py index b7b0072..b80088c 100644 --- a/app/api/version.py +++ b/app/api/version.py @@ -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//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/', 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//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/', 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, diff --git a/app/models/product.py b/app/models/product.py index 9048c20..5fb16d4 100644 --- a/app/models/product.py +++ b/app/models/product.py @@ -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() diff --git a/app/models/version.py b/app/models/version.py index bbd5dc4..4fd20cf 100644 --- a/app/models/version.py +++ b/app/models/version.py @@ -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, diff --git a/app/web/templates/base.html b/app/web/templates/base.html index 3722b2b..d58b63a 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -135,6 +135,12 @@ 系统设置 + {% endif %} @@ -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(() => { diff --git a/app/web/templates/dashboard.html b/app/web/templates/dashboard.html index 7c110e7..7521173 100644 --- a/app/web/templates/dashboard.html +++ b/app/web/templates/dashboard.html @@ -7,7 +7,7 @@ {% block content %}
-
+
@@ -25,7 +25,7 @@
-
+
@@ -43,7 +43,7 @@
-
+
@@ -61,7 +61,7 @@
-
+
@@ -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'); diff --git a/app/web/templates/device/list.html b/app/web/templates/device/list.html index b7f7865..8c0ab0f 100644 --- a/app/web/templates/device/list.html +++ b/app/web/templates/device/list.html @@ -124,26 +124,51 @@
{% endblock %} +{% block extra_js %} \ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/app/web/templates/license/list.html b/app/web/templates/license/list.html index aec570a..0bef94f 100644 --- a/app/web/templates/license/list.html +++ b/app/web/templates/license/list.html @@ -147,14 +147,38 @@
{% endblock %} +{% block extra_js %} \ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/app/web/templates/login.html b/app/web/templates/login.html index e289157..5c8bc16 100644 --- a/app/web/templates/login.html +++ b/app/web/templates/login.html @@ -52,103 +52,114 @@ diff --git a/app/web/templates/product/list.html b/app/web/templates/product/list.html index 0e1152f..f0868ec 100644 --- a/app/web/templates/product/list.html +++ b/app/web/templates/product/list.html @@ -133,15 +133,41 @@
{% endblock %} +{% block extra_js %} - - - - - - +{% endblock %} diff --git a/app/web/templates/ticket/list.html b/app/web/templates/ticket/list.html index 0a8df3b..8c25ba7 100644 --- a/app/web/templates/ticket/list.html +++ b/app/web/templates/ticket/list.html @@ -189,15 +189,39 @@
{% endblock %} +{% block extra_js %} \ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/app/web/templates/version/list.html b/app/web/templates/version/list.html index 9e46b95..87ca3f5 100644 --- a/app/web/templates/version/list.html +++ b/app/web/templates/version/list.html @@ -130,14 +130,38 @@
{% endblock %} +{% block extra_js %} \ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/app/web/views.py b/app/web/views.py index c4e7ddb..12e1b94 100644 --- a/app/web/views.py +++ b/app/web/views.py @@ -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') \ No newline at end of file + 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') \ No newline at end of file diff --git a/config.py b/config.py index b785f3c..46e3a50 100644 --- a/config.py +++ b/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]' diff --git a/instance/kamaxitong.db b/instance/kamaxitong.db index 154fe26..8a7a98b 100644 Binary files a/instance/kamaxitong.db and b/instance/kamaxitong.db differ diff --git a/start.py b/start.py index b07d941..35d3fc0 100644 --- a/start.py +++ b/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: diff --git a/static/js/custom.js b/static/js/custom.js index 88b96e0..1c5c6e8 100644 --- a/static/js/custom.js +++ b/static/js/custom.js @@ -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; + }); } \ No newline at end of file