Kamixitong/app/api/version.py

396 lines
16 KiB
Python
Raw Normal View History

2025-11-11 21:39:12 +08:00
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