1019 lines
34 KiB
Python
1019 lines
34 KiB
Python
from flask import request, jsonify, current_app, send_file
|
||
from datetime import datetime, timedelta
|
||
from app import db
|
||
from app.models import Product, License
|
||
from . import api_bp
|
||
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_login
|
||
def get_licenses():
|
||
"""获取卡密列表"""
|
||
try:
|
||
# 获取查询参数
|
||
page = request.args.get('page', 1, type=int)
|
||
per_page = min(request.args.get('per_page', 20, type=int), 100)
|
||
product_id = request.args.get('product_id')
|
||
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') # 排序方向
|
||
|
||
# 记录查询参数用于调试
|
||
current_app.logger.info(f"License search params - page: {page}, per_page: {per_page}, "
|
||
f"product_id: {product_id}, status: {status}, type: {license_type}, "
|
||
f"keyword: {keyword}")
|
||
|
||
# 构建查询 - 使用join预加载product关系,避免N+1查询
|
||
query = db.session.query(License).join(Product, License.product_id == Product.product_id)
|
||
|
||
# 产品筛选
|
||
if product_id:
|
||
query = query.filter(License.product_id == product_id)
|
||
|
||
# 状态筛选
|
||
if status is not None:
|
||
query = query.filter(License.status == status)
|
||
|
||
# 类型筛选
|
||
if license_type is not None:
|
||
query = query.filter(License.type == license_type)
|
||
|
||
# 关键词搜索(卡密或产品名称)- 使用参数化查询防止SQL注入
|
||
if keyword:
|
||
# 转义特殊字符,防止LIKE查询中的通配符攻击
|
||
escaped_keyword = keyword.replace('%', '\\%').replace('_', '\\_')
|
||
pattern = f'%{escaped_keyword.lower()}%'
|
||
query = query.filter(
|
||
db.or_(
|
||
func.lower(License.license_key).like(pattern, escape='\\'),
|
||
func.lower(Product.product_name).like(pattern, escape='\\')
|
||
)
|
||
)
|
||
|
||
# 排序处理
|
||
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(
|
||
page=page,
|
||
per_page=per_page,
|
||
error_out=False
|
||
)
|
||
|
||
# 记录查询结果用于调试
|
||
current_app.logger.info(f"License search results - total: {pagination.total}, pages: {pagination.pages}")
|
||
|
||
# 格式化结果 - product关系已经通过join加载
|
||
licenses = [license.to_dict() for license in pagination.items]
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': {
|
||
'licenses': licenses,
|
||
'pagination': {
|
||
'page': page,
|
||
'per_page': per_page,
|
||
'total': pagination.total,
|
||
'pages': pagination.pages,
|
||
'has_prev': pagination.has_prev,
|
||
'has_next': pagination.has_next
|
||
}
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"获取卡密列表失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/licenses', methods=['POST'])
|
||
@require_login
|
||
def generate_licenses():
|
||
"""批量生成卡密"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空'
|
||
}), 400
|
||
|
||
# 获取参数
|
||
product_id = data.get('product_id')
|
||
count = data.get('count', 1)
|
||
license_type = data.get('type', 1) # 0=试用, 1=正式
|
||
valid_days = data.get('valid_days') # 不设置默认值,保持None
|
||
prefix = data.get('prefix', '')
|
||
length = data.get('length', 32)
|
||
|
||
# 验证参数
|
||
if not product_id:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '产品ID不能为空'
|
||
}), 400
|
||
|
||
if count < 1 or count > 10000:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '生成数量必须在1-10000之间'
|
||
}), 400
|
||
|
||
if length < 16 or length > 35:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密长度必须在16-35之间'
|
||
}), 400
|
||
|
||
# 验证产品存在
|
||
product = Product.query.filter_by(product_id=product_id).first()
|
||
if not product:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '产品不存在'
|
||
}), 404
|
||
|
||
# 设置默认有效期
|
||
if valid_days is None:
|
||
valid_days = 1 # 默认为1天(天卡)
|
||
|
||
# 试用卡密最大有效期限制
|
||
if license_type == 0 and valid_days > 90:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '试用卡密有效期不能超过90天'
|
||
}), 400
|
||
|
||
# 验证有效期范围
|
||
if valid_days != -1 and (valid_days < 1 or valid_days > 3650):
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密有效期必须在1-3650天之间,或-1表示永久'
|
||
}), 400
|
||
|
||
# 验证永久卡不能是试用卡
|
||
if valid_days == -1 and license_type == 0:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '试用卡密不能设置为永久'
|
||
}), 400
|
||
|
||
# 生成卡密
|
||
try:
|
||
licenses = License.generate_batch(
|
||
product_id=product_id,
|
||
count=count,
|
||
license_type=license_type,
|
||
valid_days=valid_days,
|
||
prefix=prefix,
|
||
length=length
|
||
)
|
||
|
||
# 批量保存
|
||
db.session.add_all(licenses)
|
||
db.session.commit()
|
||
|
||
# 格式化结果
|
||
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} 个卡密',
|
||
'data': {
|
||
'licenses': license_data,
|
||
'count': len(licenses)
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"生成卡密失败: {str(e)}")
|
||
is_production = current_app.config.get('FLASK_ENV') == 'production' or not current_app.config.get('DEBUG', False)
|
||
error_message = '生成卡密失败,请稍后重试' if is_production else f'生成卡密失败: {str(e)}'
|
||
return jsonify({
|
||
'success': False,
|
||
'message': error_message
|
||
}), 500
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"生成卡密失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/licenses/<int:license_id>', methods=['GET'])
|
||
@require_login
|
||
def get_license(license_id):
|
||
"""获取单个卡密详情"""
|
||
try:
|
||
license_obj = License.query.get(license_id)
|
||
if not license_obj:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密不存在'
|
||
}), 404
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': license_obj.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"获取卡密详情失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/licenses/<int:license_id>', methods=['PUT'])
|
||
@require_login
|
||
def update_license(license_id):
|
||
"""更新卡密信息"""
|
||
try:
|
||
license_obj = License.query.get(license_id)
|
||
if not license_obj:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密不存在'
|
||
}), 404
|
||
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空'
|
||
}), 400
|
||
|
||
# 更新状态
|
||
if 'status' in data:
|
||
new_status = data['status']
|
||
# 已激活的卡密不能改为未激活
|
||
if license_obj.status == 1 and new_status == 0:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '已激活的卡密不能改为未激活状态'
|
||
}), 400
|
||
# 检查是否尝试将状态设置为已禁用(3)
|
||
if new_status == 3:
|
||
# 当禁用卡密时,使用专门的禁用方法
|
||
success, message = license_obj.disable()
|
||
if not success:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': message
|
||
}), 400
|
||
else:
|
||
license_obj.status = new_status
|
||
|
||
# 更新备注
|
||
if 'remark' in data:
|
||
license_obj.remark = data['remark']
|
||
|
||
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': '更新成功',
|
||
'data': license_obj.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"更新卡密失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/licenses/<int:license_id>/extend', methods=['POST'])
|
||
@require_login
|
||
def extend_license(license_id):
|
||
"""延长卡密有效期"""
|
||
try:
|
||
license_obj = License.query.get(license_id)
|
||
if not license_obj:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密不存在'
|
||
}), 404
|
||
|
||
data = request.get_json()
|
||
if not data or 'days' not in data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '缺少延期天数参数'
|
||
}), 400
|
||
|
||
days = data['days']
|
||
if days <= 0:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '延期天数必须大于0'
|
||
}), 400
|
||
|
||
success, message = license_obj.extend_validity(days)
|
||
if not success:
|
||
return jsonify({
|
||
'success': False,
|
||
'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,
|
||
'data': license_obj.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"延长卡密有效期失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/licenses/<int:license_id>/convert', methods=['POST'])
|
||
@require_login
|
||
def convert_license(license_id):
|
||
"""试用卡密转正式"""
|
||
try:
|
||
license_obj = License.query.get(license_id)
|
||
if not license_obj:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密不存在'
|
||
}), 404
|
||
|
||
data = request.get_json()
|
||
if not data or 'valid_days' not in data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '缺少有效期参数'
|
||
}), 400
|
||
|
||
valid_days = data['valid_days']
|
||
if valid_days <= 0:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '有效期必须大于0天'
|
||
}), 400
|
||
|
||
success, message = license_obj.convert_to_formal(valid_days)
|
||
if not success:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': message
|
||
}), 400
|
||
|
||
# 记录操作日志
|
||
log_operation('CONVERT_LICENSE', 'LICENSE', license_obj.license_id, {
|
||
'valid_days': valid_days
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': message,
|
||
'data': license_obj.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"转换卡密失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/licenses/<int:license_id>/unbind', methods=['POST'])
|
||
@require_login
|
||
def unbind_license(license_id):
|
||
"""解绑设备"""
|
||
try:
|
||
license_obj = License.query.get(license_id)
|
||
if not license_obj:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密不存在'
|
||
}), 404
|
||
|
||
# 获取最大解绑次数配置
|
||
max_unbind_times = current_app.config.get('MAX_UNBIND_TIMES', 3)
|
||
|
||
success, message = license_obj.unbind()
|
||
if not success:
|
||
return jsonify({
|
||
'success': False,
|
||
'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,
|
||
'data': license_obj.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"解绑设备失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/licenses/export', methods=['POST'])
|
||
@require_login
|
||
def export_licenses():
|
||
"""导出卡密"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空'
|
||
}), 400
|
||
|
||
product_id = data.get('product_id')
|
||
status = data.get('status')
|
||
license_type = data.get('type')
|
||
export_format = data.get('format', 'excel') # excel 或 csv
|
||
|
||
# 构建查询
|
||
query = License.query
|
||
|
||
if product_id:
|
||
query = query.filter_by(product_id=product_id)
|
||
if status is not None:
|
||
query = query.filter_by(status=status)
|
||
if license_type is not None:
|
||
query = query.filter_by(type=license_type)
|
||
|
||
# 获取数据
|
||
licenses = query.order_by(License.create_time.desc()).all()
|
||
|
||
if not licenses:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '没有找到符合条件的卡密'
|
||
}), 404
|
||
|
||
if export_format == 'csv':
|
||
# 导出CSV
|
||
output = io.StringIO()
|
||
writer = csv.writer(output)
|
||
|
||
# 写入表头
|
||
writer.writerow(['卡密', '产品ID', '产品名称', '类型', '状态', '有效期',
|
||
'绑定机器码', '激活时间', '过期时间', '创建时间'])
|
||
|
||
# 写入数据
|
||
for license_obj in licenses:
|
||
writer.writerow([
|
||
license_obj.license_key,
|
||
license_obj.product_id,
|
||
license_obj.product.product_name if license_obj.product else '',
|
||
'试用' if license_obj.is_trial() else '正式',
|
||
license_obj.get_status_name(),
|
||
'永久' if license_obj.valid_days == -1 else f'{license_obj.valid_days}天',
|
||
license_obj.bind_machine_code or '',
|
||
license_obj.activate_time.strftime('%Y-%m-%d %H:%M:%S') if license_obj.activate_time else '',
|
||
license_obj.expire_time.strftime('%Y-%m-%d %H:%M:%S') if license_obj.expire_time else '',
|
||
license_obj.create_time.strftime('%Y-%m-%d %H:%M:%S')
|
||
])
|
||
|
||
output.seek(0)
|
||
return send_file(
|
||
io.BytesIO(output.getvalue().encode('utf-8-sig')),
|
||
mimetype='text/csv',
|
||
as_attachment=True,
|
||
download_name=f'licenses_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
|
||
)
|
||
|
||
else:
|
||
# 导出Excel
|
||
output = io.BytesIO()
|
||
workbook = xlsxwriter.Workbook(output)
|
||
worksheet = workbook.add_worksheet('卡密列表')
|
||
|
||
# 设置表头格式
|
||
header_format = workbook.add_format({
|
||
'bold': True,
|
||
'text_wrap': True,
|
||
'valign': 'top',
|
||
'fg_color': '#D7E4BC',
|
||
'border': 1
|
||
})
|
||
|
||
# 写入表头
|
||
headers = ['卡密', '产品ID', '产品名称', '类型', '状态', '有效期',
|
||
'绑定机器码', '激活时间', '过期时间', '创建时间']
|
||
for col, header in enumerate(headers):
|
||
worksheet.write(0, col, header, header_format)
|
||
|
||
# 写入数据
|
||
for row, license_obj in enumerate(licenses, 1):
|
||
worksheet.write(row, 0, license_obj.license_key)
|
||
worksheet.write(row, 1, license_obj.product_id)
|
||
worksheet.write(row, 2, license_obj.product.product_name if license_obj.product else '')
|
||
worksheet.write(row, 3, '试用' if license_obj.is_trial() else '正式')
|
||
worksheet.write(row, 4, license_obj.get_status_name())
|
||
worksheet.write(row, 5, '永久' if license_obj.valid_days == -1 else f'{license_obj.valid_days}天')
|
||
worksheet.write(row, 6, license_obj.bind_machine_code or '')
|
||
worksheet.write(row, 7, license_obj.activate_time.strftime('%Y-%m-%d %H:%M:%S') if license_obj.activate_time else '')
|
||
worksheet.write(row, 8, license_obj.expire_time.strftime('%Y-%m-%d %H:%M:%S') if license_obj.expire_time else '')
|
||
worksheet.write(row, 9, license_obj.create_time.strftime('%Y-%m-%d %H:%M:%S'))
|
||
|
||
# 设置列宽
|
||
worksheet.set_column(0, 0, 35) # 卡密
|
||
worksheet.set_column(1, 1, 20) # 产品ID
|
||
worksheet.set_column(2, 2, 25) # 产品名称
|
||
worksheet.set_column(3, 5, 12) # 类型、状态、有效期
|
||
worksheet.set_column(6, 6, 35) # 机器码
|
||
worksheet.set_column(7, 9, 20) # 时间字段
|
||
|
||
workbook.close()
|
||
output.seek(0)
|
||
|
||
return send_file(
|
||
output,
|
||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
as_attachment=True,
|
||
download_name=f'licenses_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx'
|
||
)
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"导出卡密失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/licenses/import', methods=['POST'])
|
||
@require_login
|
||
def import_licenses():
|
||
"""导入卡密"""
|
||
try:
|
||
if 'file' not in request.files:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '没有上传文件'
|
||
}), 400
|
||
|
||
file = request.files['file']
|
||
if file.filename == '':
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '没有选择文件'
|
||
}), 400
|
||
|
||
# 验证文件格式
|
||
if not file.filename or not (file.filename.endswith('.xlsx') or file.filename.endswith('.csv')):
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '只支持Excel或CSV文件格式'
|
||
}), 400
|
||
|
||
# 读取文件内容
|
||
import pandas as pd
|
||
if file.filename.endswith('.xlsx'):
|
||
# 读取Excel文件
|
||
file.stream.seek(0) # 重置文件指针
|
||
df = pd.read_excel(file.stream)
|
||
else:
|
||
# 读取CSV文件
|
||
file.stream.seek(0) # 重置文件指针
|
||
df = pd.read_csv(file.stream)
|
||
|
||
# 验证必要列
|
||
required_columns = ['卡密', '产品ID', '有效期(天)', '类型']
|
||
missing_columns = [col for col in required_columns if col not in df.columns]
|
||
if missing_columns:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'缺少必要列: {", ".join(missing_columns)}'
|
||
}), 400
|
||
|
||
success_count = 0
|
||
error_count = 0
|
||
error_messages = []
|
||
|
||
# 逐行处理
|
||
for index, row in df.iterrows():
|
||
try:
|
||
license_key = str(row['卡密']).strip()
|
||
product_id = str(row['产品ID']).strip()
|
||
valid_days = int(row['有效期(天)'])
|
||
license_type = 1 if str(row['类型']).strip() == '正式' else 0
|
||
|
||
# 验证产品存在
|
||
product = Product.query.filter_by(product_id=product_id).first()
|
||
if not product:
|
||
error_messages.append(f'第{int(str(index))+1}行: 产品ID {product_id} 不存在')
|
||
error_count += 1
|
||
continue
|
||
|
||
# 检查卡密是否已存在
|
||
existing = License.query.filter_by(license_key=license_key).first()
|
||
if existing:
|
||
error_messages.append(f'第{int(str(index))+1}行: 卡密 {license_key} 已存在')
|
||
error_count += 1
|
||
continue
|
||
|
||
# 创建新卡密
|
||
license_obj = License(
|
||
license_key=license_key,
|
||
product_id=product_id,
|
||
type=license_type,
|
||
valid_days=valid_days,
|
||
status=0 # 未激活
|
||
)
|
||
|
||
db.session.add(license_obj)
|
||
success_count += 1
|
||
|
||
except Exception as e:
|
||
error_messages.append(f'第{int(str(index))+1}行: 处理失败 - {str(e)}')
|
||
error_count += 1
|
||
|
||
# 提交成功的记录
|
||
try:
|
||
db.session.commit()
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"保存卡密失败: {str(e)}")
|
||
is_production = current_app.config.get('FLASK_ENV') == 'production' or not current_app.config.get('DEBUG', False)
|
||
error_message = '保存失败,请稍后重试' if is_production else f'保存失败: {str(e)}'
|
||
return jsonify({
|
||
'success': False,
|
||
'message': error_message
|
||
}), 500
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': f'导入完成,成功 {success_count} 条,失败 {error_count} 条',
|
||
'data': {
|
||
'success_count': success_count,
|
||
'error_count': error_count,
|
||
'error_messages': error_messages[:10] # 只返回前10条错误信息
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"导入卡密失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/licenses/<string:license_key>', methods=['DELETE'])
|
||
@require_login
|
||
def delete_license(license_key):
|
||
"""删除卡密"""
|
||
try:
|
||
# 获取查询参数,检查是否允许强制删除
|
||
force = request.args.get('force', 'false').lower() == 'true'
|
||
|
||
# 根据卡密key查找卡密
|
||
license_obj = License.query.filter_by(license_key=license_key).first()
|
||
if not license_obj:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密不存在'
|
||
}), 404
|
||
|
||
# 检查卡密状态,已激活的卡密不能直接删除,除非强制删除
|
||
if license_obj.status == 1 and not force:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '已激活的卡密不能直接删除,请先解绑设备或禁用卡密,或者使用强制删除参数'
|
||
}), 400
|
||
|
||
# 执行删除操作
|
||
db.session.delete(license_obj)
|
||
db.session.commit()
|
||
|
||
# 记录操作日志
|
||
log_operation('DELETE_LICENSE', 'LICENSE', None, {
|
||
'license_key': license_key
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '卡密删除成功'
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"删除卡密失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
|
||
@api_bp.route('/licenses/batch', methods=['DELETE'])
|
||
@require_login
|
||
def batch_delete_licenses():
|
||
"""批量删除卡密"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data or 'license_keys' not in data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空或缺少license_keys字段'
|
||
}), 400
|
||
|
||
license_keys = data['license_keys']
|
||
if not isinstance(license_keys, list) or len(license_keys) == 0:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'license_keys必须是非空列表'
|
||
}), 400
|
||
|
||
# 查找所有要删除的卡密
|
||
licenses = License.query.filter(License.license_key.in_(license_keys)).all()
|
||
if len(licenses) != len(license_keys):
|
||
found_keys = [l.license_key for l in licenses]
|
||
missing_keys = [key for key in license_keys if key not in found_keys]
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'以下卡密不存在: {", ".join(missing_keys)}'
|
||
}), 404
|
||
|
||
# 检查是否有已激活的卡密
|
||
active_licenses = [l for l in licenses if l.status == 1]
|
||
if active_licenses:
|
||
active_keys = [l.license_key for l in active_licenses]
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'以下卡密已激活,不能直接删除: {", ".join(active_keys)},请先禁用或解绑设备,或者单独删除并选择强制删除选项'
|
||
}), 400
|
||
|
||
# 批量删除卡密
|
||
for license_obj in licenses:
|
||
db.session.delete(license_obj)
|
||
|
||
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)} 个卡密'
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"批量删除卡密失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
|
||
@api_bp.route('/licenses/batch/status', methods=['PUT'])
|
||
@require_login
|
||
def batch_update_license_status():
|
||
"""批量更新卡密状态"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data or 'license_keys' not in data or 'status' not in data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空或缺少license_keys/status字段'
|
||
}), 400
|
||
|
||
license_keys = data['license_keys']
|
||
status = data['status']
|
||
|
||
if not isinstance(license_keys, list) or len(license_keys) == 0:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'license_keys必须是非空列表'
|
||
}), 400
|
||
|
||
if status not in [0, 1, 2, 3]:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'status必须是0(未激活)、1(已激活)、2(已过期)或3(已禁用)'
|
||
}), 400
|
||
|
||
# 查找所有要更新的卡密
|
||
licenses = License.query.filter(License.license_key.in_(license_keys)).all()
|
||
if len(licenses) != len(license_keys):
|
||
found_keys = [l.license_key for l in licenses]
|
||
missing_keys = [key for key in license_keys if key not in found_keys]
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'以下卡密不存在: {", ".join(missing_keys)}'
|
||
}), 404
|
||
|
||
# 检查是否有已激活的卡密要改为未激活
|
||
if status == 0:
|
||
active_licenses = [l for l in licenses if l.status == 1]
|
||
if active_licenses:
|
||
active_keys = [l.license_key for l in active_licenses]
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'以下卡密已激活,不能改为未激活状态: {", ".join(active_keys)}'
|
||
}), 400
|
||
|
||
# 批量更新卡密状态
|
||
for license_obj in licenses:
|
||
license_obj.status = 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({
|
||
'success': True,
|
||
'message': f'成功将 {len(licenses)} 个卡密状态更新为{status_name}'
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"批量更新卡密状态失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
|
||
@api_bp.route('/licenses/check-expired', methods=['POST'])
|
||
@require_login
|
||
@require_admin
|
||
def check_expired_licenses():
|
||
"""手动触发过期卡密检查"""
|
||
try:
|
||
from app.utils.background_tasks import update_expired_licenses
|
||
|
||
result = update_expired_licenses()
|
||
|
||
# 记录操作日志
|
||
log_operation('CHECK_EXPIRED_LICENSES', 'LICENSE', None, {
|
||
'updated_count': result.get('updated_count', 0),
|
||
'success': result.get('success', False)
|
||
})
|
||
|
||
return jsonify({
|
||
'success': result['success'],
|
||
'message': result['message'],
|
||
'data': {
|
||
'updated_count': result.get('updated_count', 0)
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"手动检查过期卡密失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'服务器内部错误: {str(e)}'
|
||
}), 500
|
||
|
||
|
||
@api_bp.route('/licenses/batch-check', methods=['POST'])
|
||
@require_login
|
||
@require_admin
|
||
def batch_check_licenses():
|
||
"""批量检查所有卡密状态"""
|
||
try:
|
||
from app.utils.background_tasks import check_licenses_batch
|
||
|
||
result = check_licenses_batch()
|
||
|
||
# 记录操作日志
|
||
log_operation('BATCH_CHECK_LICENSES', 'LICENSE', None, {
|
||
'success': result.get('success', False),
|
||
'statistics': result.get('statistics', {})
|
||
})
|
||
|
||
return jsonify({
|
||
'success': result['success'],
|
||
'message': result['message'],
|
||
'data': result.get('statistics', {})
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"批量检查卡密失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'服务器内部错误: {str(e)}'
|
||
}), 500
|
||
|
||
|
||
@api_bp.route('/scheduler/status', methods=['GET'])
|
||
@require_login
|
||
@require_admin
|
||
def get_scheduler_status():
|
||
"""获取定时任务状态"""
|
||
try:
|
||
from app.utils.scheduler import get_job_status
|
||
|
||
status = get_job_status()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': status
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"获取定时任务状态失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'服务器内部错误: {str(e)}'
|
||
}), 500
|
||
|
||
|
||
@api_bp.route('/scheduler/trigger-check', methods=['POST'])
|
||
@require_login
|
||
@require_admin
|
||
def trigger_scheduler_check():
|
||
"""手动触发定时任务中的过期检查"""
|
||
try:
|
||
from app.utils.scheduler import check_and_update_expired_licenses
|
||
|
||
result = check_and_update_expired_licenses()
|
||
|
||
# 记录操作日志
|
||
log_operation('TRIGGER_SCHEDULER_CHECK', 'SCHEDULER', None, {
|
||
'success': result.get('success', False),
|
||
'message': result.get('message', '')
|
||
})
|
||
|
||
return jsonify({
|
||
'success': result['success'],
|
||
'message': result['message']
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"手动触发定时任务失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'服务器内部错误: {str(e)}'
|
||
}), 500
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|