545 lines
22 KiB
Python
545 lines
22 KiB
Python
from flask import request, jsonify, current_app
|
||
from datetime import datetime
|
||
import os
|
||
import hashlib
|
||
from werkzeug.utils import secure_filename
|
||
from app import db
|
||
from app.models import Version, Product
|
||
from . import api_bp
|
||
from .license import require_admin
|
||
from sqlalchemy import desc
|
||
import traceback
|
||
import sys
|
||
|
||
@api_bp.route('/versions', methods=['GET'])
|
||
@require_admin
|
||
def get_versions():
|
||
"""获取版本列表"""
|
||
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('publish_status', type=int)
|
||
|
||
query = Version.query
|
||
|
||
if product_id:
|
||
query = query.filter_by(product_id=product_id)
|
||
if status is not None:
|
||
query = query.filter_by(publish_status=status)
|
||
|
||
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]
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': {
|
||
'versions': versions,
|
||
'pagination': {
|
||
'page': page,
|
||
'per_page': per_page,
|
||
'total': pagination.total,
|
||
'pages': pagination.pages
|
||
}
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"获取版本列表失败: {str(e)}")
|
||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||
|
||
@api_bp.route('/versions', methods=['POST'])
|
||
@require_admin
|
||
def create_version():
|
||
"""创建版本"""
|
||
try:
|
||
current_app.logger.info("开始创建版本")
|
||
current_app.logger.info(f"请求的Content-Type: {request.content_type}")
|
||
current_app.logger.info(f"请求方法: {request.method}")
|
||
current_app.logger.info(f"请求URL: {request.url}")
|
||
current_app.logger.info(f"请求头: {dict(request.headers)}")
|
||
|
||
# 处理文件上传和表单数据
|
||
if request.content_type and ('multipart/form-data' in request.content_type or 'application/x-www-form-urlencoded' in request.content_type):
|
||
current_app.logger.info("处理表单数据请求")
|
||
# 处理表单数据
|
||
product_id = request.form.get('product_id')
|
||
version_num = request.form.get('version_num')
|
||
update_log = request.form.get('update_notes', '') # 修改字段名映射
|
||
download_url = request.form.get('download_url', '')
|
||
file_hash = request.form.get('file_hash', '')
|
||
force_update = request.form.get('force_update', 0)
|
||
download_status = request.form.get('download_status', 1)
|
||
min_license_version = request.form.get('min_license_version')
|
||
# 添加对新增字段的处理
|
||
platform = request.form.get('platform', '')
|
||
description = request.form.get('description', '')
|
||
publish_now = request.form.get('publish_now') == 'true' or request.form.get('publish_now') == 'on'
|
||
else:
|
||
current_app.logger.info("处理JSON请求")
|
||
# 处理JSON数据
|
||
data = request.get_json()
|
||
if not data:
|
||
current_app.logger.warning("请求数据为空")
|
||
return jsonify({'success': False, 'message': '请求数据为空'}), 400
|
||
|
||
product_id = data.get('product_id')
|
||
version_num = data.get('version_num')
|
||
update_log = data.get('update_notes', '') # 修改字段名映射
|
||
download_url = data.get('download_url', '')
|
||
file_hash = data.get('file_hash', '')
|
||
force_update = data.get('force_update', 0)
|
||
download_status = data.get('download_status', 1)
|
||
min_license_version = data.get('min_license_version')
|
||
# 添加对新增字段的处理
|
||
platform = data.get('platform', '')
|
||
description = data.get('description', '')
|
||
publish_now = data.get('publish_now', False)
|
||
|
||
current_app.logger.info(f"收到的参数: product_id={product_id}, version_num={version_num}")
|
||
|
||
if not all([product_id, version_num]):
|
||
current_app.logger.warning(f"缺少必要参数: product_id={product_id}, version_num={version_num}")
|
||
return jsonify({'success': False, 'message': '缺少必要参数'}), 400
|
||
|
||
current_app.logger.info(f"验证产品是否存在: product_id={product_id}")
|
||
# 验证产品存在
|
||
current_app.logger.info(f"执行产品查询...")
|
||
|
||
# 先查询所有产品进行调试
|
||
all_products = Product.query.all()
|
||
current_app.logger.info(f"数据库中所有产品: {[(p.product_id, p.product_name) for p in all_products]}")
|
||
|
||
# 执行实际查询
|
||
product = Product.query.filter_by(product_id=product_id).first()
|
||
current_app.logger.info(f"产品查询结果: {product}")
|
||
if not product:
|
||
current_app.logger.warning(f"产品不存在: product_id={product_id}")
|
||
return jsonify({'success': False, 'message': '产品不存在'}), 404
|
||
|
||
current_app.logger.info(f"检查版本号是否重复: product_id={product_id}, version_num={version_num}")
|
||
# 检查版本号是否重复
|
||
existing = Version.query.filter_by(
|
||
product_id=product_id,
|
||
version_num=version_num
|
||
).first()
|
||
if existing:
|
||
current_app.logger.warning(f"版本号已存在: product_id={product_id}, version_num={version_num}")
|
||
return jsonify({'success': False, 'message': '版本号已存在'}), 400
|
||
|
||
current_app.logger.info(f"创建版本对象: product_id={product_id}, version_num={version_num}")
|
||
version = Version(
|
||
product_id=product_id,
|
||
version_num=version_num,
|
||
platform=platform, # 添加新字段
|
||
description=description, # 添加新字段
|
||
update_log=update_log,
|
||
download_url=download_url,
|
||
file_hash=file_hash,
|
||
force_update=force_update,
|
||
download_status=download_status,
|
||
min_license_version=min_license_version
|
||
)
|
||
|
||
current_app.logger.info("添加版本到数据库")
|
||
db.session.add(version)
|
||
current_app.logger.info("提交数据库事务")
|
||
db.session.commit()
|
||
|
||
current_app.logger.info(f"检查是否立即发布: publish_now={publish_now}")
|
||
# 如果选择了立即发布,则发布版本
|
||
if publish_now:
|
||
current_app.logger.info("发布版本")
|
||
version.publish()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '版本创建成功',
|
||
'data': version.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"创建版本失败: {str(e)}")
|
||
# 添加更详细的错误信息
|
||
import traceback
|
||
current_app.logger.error(f"创建版本失败详细信息: {traceback.format_exc()}")
|
||
return jsonify({'success': False, 'message': f'服务器内部错误: {str(e)}'}), 500
|
||
|
||
@api_bp.route('/versions/<int:version_id>/publish', methods=['POST'])
|
||
@require_admin
|
||
def publish_version(version_id):
|
||
"""发布版本"""
|
||
try:
|
||
version = Version.query.get(version_id)
|
||
if not version:
|
||
return jsonify({'success': False, 'message': '版本不存在'}), 404
|
||
|
||
version.publish()
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '版本发布成功',
|
||
'data': version.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"发布版本失败: {str(e)}")
|
||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||
|
||
@api_bp.route('/versions/<int:version_id>', methods=['PUT'])
|
||
@require_admin
|
||
def update_version(version_id):
|
||
"""更新版本信息"""
|
||
try:
|
||
version = Version.query.get(version_id)
|
||
if not version:
|
||
return jsonify({'success': False, 'message': '版本不存在'}), 404
|
||
|
||
# 获取请求数据
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({'success': False, 'message': '请求数据为空'}), 400
|
||
|
||
# 更新版本信息(只允许更新未发布或已回滚的版本的部分信息)
|
||
# 已发布的版本只能更新描述信息
|
||
version.platform = data.get('platform', version.platform)
|
||
version.description = data.get('description', version.description)
|
||
version.update_log = data.get('update_log', version.update_log)
|
||
version.min_license_version = data.get('min_license_version', version.min_license_version)
|
||
version.force_update = data.get('force_update', version.force_update)
|
||
version.download_status = data.get('download_status', version.download_status)
|
||
|
||
# 只有未发布或已回滚的版本才能更新版本号和产品
|
||
if version.publish_status != 1: # 不是已发布状态
|
||
version.version_num = data.get('version_num', version.version_num)
|
||
version.product_id = data.get('product_id', version.product_id)
|
||
|
||
# 保存更新
|
||
db.session.commit()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '版本信息更新成功',
|
||
'data': version.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('/versions/<int:version_id>/status', methods=['PUT'])
|
||
@require_admin
|
||
def update_version_status(version_id):
|
||
"""更新版本状态(发布/取消发布)"""
|
||
try:
|
||
version = Version.query.get(version_id)
|
||
if not version:
|
||
return jsonify({'success': False, 'message': '版本不存在'}), 404
|
||
|
||
# 获取请求数据
|
||
data = request.get_json()
|
||
status = data.get('status')
|
||
|
||
if status is None:
|
||
return jsonify({'success': False, 'message': '缺少状态参数'}), 400
|
||
|
||
# 更新状态
|
||
if status == 1: # 发布
|
||
version.publish()
|
||
message = '版本发布成功'
|
||
elif status == 0: # 取消发布
|
||
version.unpublish()
|
||
message = '版本取消发布成功'
|
||
elif status == 2: # 回滚
|
||
version.rollback()
|
||
message = '版本回滚成功'
|
||
else:
|
||
return jsonify({'success': False, 'message': '无效的状态值'}), 400
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': message,
|
||
'data': version.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"更新版本状态失败: {str(e)}")
|
||
return jsonify({'success': False, 'message': '服务器内部错误'}), 500
|
||
|
||
@api_bp.route('/versions/upload', methods=['POST'])
|
||
@require_admin
|
||
def upload_version_file():
|
||
"""上传版本文件"""
|
||
try:
|
||
# 检查是否有文件上传
|
||
if 'version_file' not in request.files:
|
||
return jsonify({'success': False, 'message': '没有文件被上传'}), 400
|
||
|
||
file = request.files['version_file']
|
||
if file.filename == '' or file.filename is None:
|
||
return jsonify({'success': False, 'message': '没有选择文件'}), 400
|
||
|
||
if file:
|
||
# 确保上传目录存在
|
||
upload_folder = current_app.config['UPLOAD_FOLDER']
|
||
# 确保使用绝对路径
|
||
upload_folder = os.path.abspath(upload_folder)
|
||
os.makedirs(upload_folder, exist_ok=True)
|
||
|
||
# 生成安全的文件名
|
||
filename = secure_filename(str(file.filename))
|
||
# 在文件名前添加时间戳以避免重复
|
||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||
filename = f"{timestamp}_{filename}"
|
||
|
||
# 保存文件
|
||
file_path = os.path.join(upload_folder, filename)
|
||
file.save(file_path)
|
||
|
||
# 计算文件哈希值
|
||
file_hash = calculate_file_hash(file_path)
|
||
|
||
# 生成文件访问URL(相对于static目录)
|
||
# 修复URL生成逻辑
|
||
file_url = f"/static/uploads/{filename}"
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '文件上传成功',
|
||
'data': {
|
||
'file_name': filename,
|
||
'file_url': file_url,
|
||
'file_hash': file_hash,
|
||
'file_size': os.path.getsize(file_path)
|
||
}
|
||
})
|
||
else:
|
||
return jsonify({'success': False, 'message': '文件上传失败'}), 400
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"文件上传失败: {str(e)}")
|
||
import traceback
|
||
current_app.logger.error(f"文件上传失败详细信息: {traceback.format_exc()}")
|
||
return jsonify({'success': False, 'message': f'文件上传失败: {str(e)}'}), 500
|
||
|
||
def calculate_file_hash(file_path):
|
||
"""计算文件SHA256哈希值"""
|
||
try:
|
||
hash_sha256 = hashlib.sha256()
|
||
with open(file_path, "rb") as f:
|
||
for chunk in iter(lambda: f.read(4096), b""):
|
||
hash_sha256.update(chunk)
|
||
return hash_sha256.hexdigest()
|
||
except Exception as e:
|
||
raise Exception(f"计算文件哈希值失败: {str(e)}")
|
||
|
||
def extract_filename_from_url(download_url):
|
||
"""从下载URL中提取文件名"""
|
||
if not download_url:
|
||
return None
|
||
|
||
# 从URL中提取文件名,假设URL格式为 /static/uploads/filename
|
||
if '/static/uploads/' in download_url:
|
||
filename = download_url.split('/static/uploads/')[-1]
|
||
return filename
|
||
return None
|
||
|
||
@api_bp.route('/versions/<int:version_id>', methods=['DELETE'])
|
||
@require_admin
|
||
def delete_version(version_id):
|
||
"""删除版本"""
|
||
try:
|
||
version = Version.query.get(version_id)
|
||
if not version:
|
||
return jsonify({'success': False, 'message': '版本不存在'}), 404
|
||
|
||
# 检查版本是否已被发布,已发布的版本不能直接删除
|
||
if version.is_published():
|
||
return jsonify({'success': False, 'message': '已发布的版本不能直接删除,请先取消发布后再删除'}), 400
|
||
|
||
# 检查是否有设备正在使用此版本
|
||
active_device_count = version.get_active_device_count()
|
||
if active_device_count > 0:
|
||
return jsonify({'success': False, 'message': f'有{active_device_count}个活跃设备正在使用此版本,为保护数据安全,不能直接删除。请先将这些设备解绑或禁用后再删除版本'}), 400
|
||
|
||
# 删除版本相关的文件
|
||
if version.download_url:
|
||
filename = extract_filename_from_url(version.download_url)
|
||
if filename:
|
||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
|
||
# 检查文件是否存在,如果存在则删除
|
||
if os.path.exists(file_path):
|
||
try:
|
||
os.remove(file_path)
|
||
current_app.logger.info(f"已删除版本文件: {file_path}")
|
||
except Exception as e:
|
||
current_app.logger.error(f"删除版本文件失败: {str(e)}")
|
||
# 即使文件删除失败也继续删除版本记录
|
||
|
||
# 删除版本
|
||
db.session.delete(version)
|
||
db.session.commit()
|
||
|
||
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('/versions/batch', methods=['DELETE'])
|
||
@require_admin
|
||
def batch_delete_versions():
|
||
"""批量删除版本"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data or 'version_ids' not in data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空或缺少version_ids字段'
|
||
}), 400
|
||
|
||
version_ids = data['version_ids']
|
||
if not isinstance(version_ids, list) or len(version_ids) == 0:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'version_ids必须是非空列表'
|
||
}), 400
|
||
|
||
# 查找所有要删除的版本
|
||
versions = Version.query.filter(Version.version_id.in_(version_ids)).all()
|
||
if len(versions) != len(version_ids):
|
||
found_ids = [v.version_id for v in versions]
|
||
missing_ids = [vid for vid in version_ids if vid not in found_ids]
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'以下版本不存在: {", ".join(map(str, missing_ids))}'
|
||
}), 404
|
||
|
||
# 检查是否有已发布的版本
|
||
published_versions = [v for v in versions if v.is_published()]
|
||
if published_versions:
|
||
published_ids = [str(v.version_id) for v in published_versions]
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'以下版本已发布,不能直接删除: {", ".join(published_ids)},请先取消发布后再删除'
|
||
}), 400
|
||
|
||
# 检查是否有设备正在使用这些版本
|
||
undeletable_versions = []
|
||
for version in versions:
|
||
active_device_count = version.get_active_device_count()
|
||
if active_device_count > 0:
|
||
undeletable_versions.append({
|
||
'version_id': version.version_id,
|
||
'version_num': version.version_num,
|
||
'active_device_count': active_device_count
|
||
})
|
||
|
||
if undeletable_versions:
|
||
undeletable_info = [f'{v["version_num"]}({v["active_device_count"]}个设备)' for v in undeletable_versions]
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'以下版本有活跃设备正在使用,不能直接删除: {", ".join(undeletable_info)},请先将这些设备解绑或禁用后再删除版本'
|
||
}), 400
|
||
|
||
# 批量删除版本
|
||
for version in versions:
|
||
# 删除版本相关的文件
|
||
if version.download_url:
|
||
filename = extract_filename_from_url(version.download_url)
|
||
if filename:
|
||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
|
||
# 检查文件是否存在,如果存在则删除
|
||
if os.path.exists(file_path):
|
||
try:
|
||
os.remove(file_path)
|
||
current_app.logger.info(f"已删除版本文件: {file_path}")
|
||
except Exception as e:
|
||
current_app.logger.error(f"删除版本文件失败: {str(e)}")
|
||
# 即使文件删除失败也继续删除版本记录
|
||
|
||
db.session.delete(version)
|
||
|
||
db.session.commit()
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': f'成功删除 {len(versions)} 个版本'
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"批量删除版本失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
|
||
@api_bp.route('/versions/batch/status', methods=['PUT'])
|
||
@require_admin
|
||
def batch_update_version_status():
|
||
"""批量更新版本状态(发布/取消发布)"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data or 'version_ids' not in data or 'status' not in data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空或缺少version_ids/status字段'
|
||
}), 400
|
||
|
||
version_ids = data['version_ids']
|
||
status = data['status']
|
||
|
||
if not isinstance(version_ids, list) or len(version_ids) == 0:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'version_ids必须是非空列表'
|
||
}), 400
|
||
|
||
if status not in [0, 1]:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'status必须是0(草稿)或1(已发布)'
|
||
}), 400
|
||
|
||
# 查找所有要更新的版本
|
||
versions = Version.query.filter(Version.version_id.in_(version_ids)).all()
|
||
if len(versions) != len(version_ids):
|
||
found_ids = [v.version_id for v in versions]
|
||
missing_ids = [vid for vid in version_ids if vid not in found_ids]
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'以下版本不存在: {", ".join(map(str, missing_ids))}'
|
||
}), 404
|
||
|
||
# 批量更新版本状态
|
||
for version in versions:
|
||
if status == 1:
|
||
version.publish()
|
||
else:
|
||
version.unpublish()
|
||
|
||
status_name = '发布' if status == 1 else '取消发布'
|
||
return jsonify({
|
||
'success': True,
|
||
'message': f'成功{status_name} {len(versions)} 个版本'
|
||
})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
current_app.logger.error(f"批量更新版本状态失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|