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//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/', 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//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/', 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