diff --git a/.idea/KaMiXiTong.iml b/.idea/KaMiXiTong.iml index 8b8c395..fa7a615 100644 --- a/.idea/KaMiXiTong.iml +++ b/.idea/KaMiXiTong.iml @@ -2,7 +2,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9de2865 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/api/device.py b/app/api/device.py index 0e3b68f..afb97e2 100644 --- a/app/api/device.py +++ b/app/api/device.py @@ -123,4 +123,112 @@ def delete_device(device_id): except Exception as e: db.session.rollback() current_app.logger.error(f"删除设备失败: {str(e)}") - return jsonify({'success': False, 'message': '服务器内部错误'}), 500 \ No newline at end of file + return jsonify({'success': False, 'message': '服务器内部错误'}), 500 + + +@api_bp.route('/devices/batch', methods=['DELETE']) +@require_admin +def batch_delete_devices(): + """批量删除设备""" + try: + data = request.get_json() + if not data or 'device_ids' not in data: + return jsonify({ + 'success': False, + 'message': '请求数据为空或缺少device_ids字段' + }), 400 + + device_ids = data['device_ids'] + if not isinstance(device_ids, list) or len(device_ids) == 0: + return jsonify({ + 'success': False, + 'message': 'device_ids必须是非空列表' + }), 400 + + # 查找所有要删除的设备 + devices = Device.query.filter(Device.device_id.in_(device_ids)).all() + if len(devices) != len(device_ids): + found_ids = [d.device_id for d in devices] + missing_ids = [did for did in device_ids if did not in found_ids] + return jsonify({ + 'success': False, + 'message': f'以下设备不存在: {", ".join(map(str, missing_ids))}' + }), 404 + + # 批量删除设备 + for device in devices: + db.session.delete(device) + + db.session.commit() + + return jsonify({ + 'success': True, + 'message': f'成功删除 {len(devices)} 个设备' + }) + + except Exception as e: + db.session.rollback() + current_app.logger.error(f"批量删除设备失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': '服务器内部错误' + }), 500 + + +@api_bp.route('/devices/batch/status', methods=['PUT']) +@require_admin +def batch_update_device_status(): + """批量更新设备状态""" + try: + data = request.get_json() + if not data or 'device_ids' not in data or 'status' not in data: + return jsonify({ + 'success': False, + 'message': '请求数据为空或缺少device_ids/status字段' + }), 400 + + device_ids = data['device_ids'] + status = data['status'] + + if not isinstance(device_ids, list) or len(device_ids) == 0: + return jsonify({ + 'success': False, + 'message': 'device_ids必须是非空列表' + }), 400 + + if status not in [0, 1, 2]: + return jsonify({ + 'success': False, + 'message': 'status必须是0(禁用)、1(正常)或2(离线)' + }), 400 + + # 查找所有要更新的设备 + devices = Device.query.filter(Device.device_id.in_(device_ids)).all() + if len(devices) != len(device_ids): + found_ids = [d.device_id for d in devices] + missing_ids = [did for did in device_ids if did not in found_ids] + return jsonify({ + 'success': False, + 'message': f'以下设备不存在: {", ".join(map(str, missing_ids))}' + }), 404 + + # 批量更新设备状态 + for device in devices: + device.status = status + + db.session.commit() + + status_names = {0: '禁用', 1: '正常', 2: '离线'} + status_name = status_names.get(status, '未知') + return jsonify({ + 'success': True, + 'message': f'成功将 {len(devices)} 个设备状态更新为{status_name}' + }) + + except Exception as e: + db.session.rollback() + current_app.logger.error(f"批量更新设备状态失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': '服务器内部错误' + }), 500 \ No newline at end of file diff --git a/app/api/license.py b/app/api/license.py index f22b209..0b7ecc7 100644 --- a/app/api/license.py +++ b/app/api/license.py @@ -719,3 +719,130 @@ def delete_license(license_key): 'success': False, 'message': '服务器内部错误' }), 500 + + +@api_bp.route('/licenses/batch', methods=['DELETE']) +@require_admin +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() + + 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_admin +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() + + 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 diff --git a/app/api/product.py b/app/api/product.py index 835d8a1..e68d2d7 100644 --- a/app/api/product.py +++ b/app/api/product.py @@ -220,6 +220,131 @@ def delete_product(product_id): except Exception as e: db.session.rollback() current_app.logger.error(f"删除产品失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': '服务器内部错误' + }), 500 + + +@api_bp.route('/products/batch', methods=['DELETE']) +@require_admin +def batch_delete_products(): + """批量删除产品""" + try: + data = request.get_json() + if not data or 'product_ids' not in data: + return jsonify({ + 'success': False, + 'message': '请求数据为空或缺少product_ids字段' + }), 400 + + product_ids = data['product_ids'] + if not isinstance(product_ids, list) or len(product_ids) == 0: + return jsonify({ + 'success': False, + 'message': 'product_ids必须是非空列表' + }), 400 + + # 查找所有要删除的产品 + products = Product.query.filter(Product.product_id.in_(product_ids)).all() + if len(products) != len(product_ids): + found_ids = [p.product_id for p in products] + missing_ids = [pid for pid in product_ids if pid not in found_ids] + return jsonify({ + 'success': False, + 'message': f'以下产品不存在: {", ".join(missing_ids)}' + }), 404 + + # 检查是否有产品有关联的卡密 + undeletable_products = [] + for product in products: + license_count = product.licenses.count() + if license_count > 0: + undeletable_products.append({ + 'product_id': product.product_id, + 'product_name': product.product_name, + 'license_count': license_count + }) + + if undeletable_products: + return jsonify({ + 'success': False, + 'message': '部分产品无法删除,因为有关联的卡密', + 'undeletable_products': undeletable_products + }), 400 + + # 批量删除产品 + for product in products: + db.session.delete(product) + + db.session.commit() + + return jsonify({ + 'success': True, + 'message': f'成功删除 {len(products)} 个产品' + }) + + except Exception as e: + db.session.rollback() + current_app.logger.error(f"批量删除产品失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': '服务器内部错误' + }), 500 + + +@api_bp.route('/products/batch/status', methods=['PUT']) +@require_admin +def batch_update_product_status(): + """批量更新产品状态""" + try: + data = request.get_json() + if not data or 'product_ids' not in data or 'status' not in data: + return jsonify({ + 'success': False, + 'message': '请求数据为空或缺少product_ids/status字段' + }), 400 + + product_ids = data['product_ids'] + status = data['status'] + + if not isinstance(product_ids, list) or len(product_ids) == 0: + return jsonify({ + 'success': False, + 'message': 'product_ids必须是非空列表' + }), 400 + + if status not in [0, 1]: + return jsonify({ + 'success': False, + 'message': 'status必须是0(禁用)或1(启用)' + }), 400 + + # 查找所有要更新的产品 + products = Product.query.filter(Product.product_id.in_(product_ids)).all() + if len(products) != len(product_ids): + found_ids = [p.product_id for p in products] + missing_ids = [pid for pid in product_ids if pid not in found_ids] + return jsonify({ + 'success': False, + 'message': f'以下产品不存在: {", ".join(missing_ids)}' + }), 404 + + # 批量更新产品状态 + for product in products: + product.status = status + + db.session.commit() + + status_name = '启用' if status == 1 else '禁用' + return jsonify({ + 'success': True, + 'message': f'成功{status_name} {len(products)} 个产品' + }) + + except Exception as e: + db.session.rollback() + current_app.logger.error(f"批量更新产品状态失败: {str(e)}") return jsonify({ 'success': False, 'message': '服务器内部错误' diff --git a/app/api/ticket.py b/app/api/ticket.py index 6c9c113..a8b8ca6 100644 --- a/app/api/ticket.py +++ b/app/api/ticket.py @@ -14,7 +14,7 @@ def get_tickets(): per_page = min(request.args.get('per_page', 20, type=int), 100) status = request.args.get('status', type=int) priority = request.args.get('priority', type=int) - product_id = request.args.get('product_id') + product_id = request.args.get('product_id') # 修复:确保参数名与前端一致 query = Ticket.query @@ -89,4 +89,62 @@ def create_ticket(): except Exception as e: db.session.rollback() current_app.logger.error(f"创建工单失败: {str(e)}") - return jsonify({'success': False, 'message': '服务器内部错误'}), 500 \ No newline at end of file + return jsonify({'success': False, 'message': '服务器内部错误'}), 500 + + +@api_bp.route('/tickets/batch/status', methods=['PUT']) +@require_admin +def batch_update_ticket_status(): + """批量更新工单状态""" + try: + data = request.get_json() + if not data or 'ticket_ids' not in data or 'status' not in data: + return jsonify({ + 'success': False, + 'message': '请求数据为空或缺少ticket_ids/status字段' + }), 400 + + ticket_ids = data['ticket_ids'] + status = data['status'] + remark = data.get('remark', '') + + if not isinstance(ticket_ids, list) or len(ticket_ids) == 0: + return jsonify({ + 'success': False, + 'message': 'ticket_ids必须是非空列表' + }), 400 + + if status not in [0, 1, 2, 3]: + return jsonify({ + 'success': False, + 'message': 'status必须是0(待处理)、1(处理中)、2(已解决)或3(已关闭)' + }), 400 + + # 查找所有要更新的工单 + tickets = Ticket.query.filter(Ticket.ticket_id.in_(ticket_ids)).all() + if len(tickets) != len(ticket_ids): + found_ids = [t.ticket_id for t in tickets] + missing_ids = [tid for tid in ticket_ids if tid not in found_ids] + return jsonify({ + 'success': False, + 'message': f'以下工单不存在: {", ".join(map(str, missing_ids))}' + }), 404 + + # 批量更新工单状态 + for ticket in tickets: + ticket.update_status(status, remark) + + status_names = {0: '待处理', 1: '处理中', 2: '已解决', 3: '已关闭'} + status_name = status_names.get(status, '未知') + return jsonify({ + 'success': True, + 'message': f'成功将 {len(tickets)} 个工单状态更新为{status_name}' + }) + + except Exception as e: + db.session.rollback() + current_app.logger.error(f"批量更新工单状态失败: {str(e)}") + return jsonify({ + 'success': False, + 'message': '服务器内部错误' + }), 500 \ No newline at end of file diff --git a/app/api/version.py b/app/api/version.py index ca869dd..5a50e81 100644 --- a/app/api/version.py +++ b/app/api/version.py @@ -393,3 +393,152 @@ def delete_version(version_id): 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 diff --git a/app/models/admin.py b/app/models/admin.py index 64c31a2..d55202c 100644 --- a/app/models/admin.py +++ b/app/models/admin.py @@ -31,7 +31,13 @@ class Admin(UserMixin, db.Model): def check_password(self, password): """验证密码""" - return check_password_hash(self.password_hash, password) + from werkzeug.security import check_password_hash + print(f"DEBUG: Checking password for user {self.username} (ID: {self.admin_id})") + print(f"DEBUG: Stored hash: {self.password_hash}") + print(f"DEBUG: Provided password: {password}") + result = check_password_hash(self.password_hash, password) + print(f"DEBUG: Password check result: {result}") + return result def is_super_admin(self): """是否为超级管理员""" diff --git a/app/models/ticket.py b/app/models/ticket.py index 096b0d5..0c54c63 100644 --- a/app/models/ticket.py +++ b/app/models/ticket.py @@ -129,8 +129,8 @@ class Ticket(db.Model): 'operator': self.operator, 'remark': self.remark, 'processing_days': self.get_processing_days(), - 'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S'), - 'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S'), + 'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else None, + 'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S') if self.update_time else None, 'resolve_time': self.resolve_time.strftime('%Y-%m-%d %H:%M:%S') if self.resolve_time else None, 'close_time': self.close_time.strftime('%Y-%m-%d %H:%M:%S') if self.close_time else None } diff --git a/app/web/__init__.py b/app/web/__init__.py index e8e8f8d..0f1f6b4 100644 --- a/app/web/__init__.py +++ b/app/web/__init__.py @@ -1,9 +1,9 @@ +# 创建Web蓝图 from flask import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify from flask_login import login_user, logout_user, login_required, current_user from app.models.admin import Admin from app import db -# 创建Web蓝图 web_bp = Blueprint('web', __name__) @web_bp.route('/') @@ -19,17 +19,25 @@ def login(): username = request.form.get('username', '').strip() password = request.form.get('password', '') + print(f"DEBUG: Login attempt - Username: '{username}', Password: '{password}'") + print(f"DEBUG: Password length: {len(password) if password else 0}") + print(f"DEBUG: Password repr: {repr(password) if password else 'None'}") + if not username or not password: flash('请输入用户名和密码', 'error') return render_template('login.html') # 查找用户 admin = Admin.query.filter_by(username=username).first() - - # 查找用户 - admin = Admin.query.filter_by(username=username).first() - - if admin and admin.verify_password(password) and admin.is_active(): + print(f"DEBUG: Admin found: {admin.username if admin else 'None'} (ID: {admin.admin_id if admin else 'N/A'})") + if admin: + print(f"DEBUG: Admin details - Role: {admin.role}, Status: {admin.status}, Deleted: {admin.is_deleted}") + password_check_result = admin.check_password(password) + print(f"DEBUG: Password check: {password_check_result}") + print(f"DEBUG: Active check: {admin.is_active}") + print(f"DEBUG: All conditions: admin={bool(admin)}, password={password_check_result}, active={admin.is_active}") + if admin and admin.check_password(password) and admin.is_active: + print("DEBUG: Authentication successful") # 登录成功 login_user(admin, remember=True) @@ -55,10 +63,12 @@ def login(): # 获取next参数 next_page = request.args.get('next') + print(f"DEBUG: Next page: {next_page}") if next_page: return redirect(next_page) return redirect(url_for('web.dashboard')) else: + print("DEBUG: Authentication failed") flash('用户名或密码错误', 'error') return render_template('login.html') diff --git a/app/web/templates/device/list.html b/app/web/templates/device/list.html index 9e62792..b7f7865 100644 --- a/app/web/templates/device/list.html +++ b/app/web/templates/device/list.html @@ -5,14 +5,16 @@ {% block page_title %}设备管理{% endblock %} {% block content %} - +
- + +
+
- + +
- + +
-
- +
+
+ + +
+ +
+
+
+
+ + +
+
+ 已选择 0 项 +
+
+
+
+
- + + - + - +
+ + 设备ID 产品/卡密 机器码 IP地址 状态 激活时间最后在线最后验证 操作
-
- 加载中... -
加载中...
- + -
+ + + {% endblock %} -{% block extra_js %} -{% endblock %} \ No newline at end of file + +// 更新批量操作按钮状态 +function updateBatchButtons() { + const selectedCount = document.querySelectorAll('.device-checkbox:checked').length; + const batchDeleteBtn = document.getElementById('batch-delete-btn'); + const batchStatusBtn = document.getElementById('batch-status-btn'); + + if (selectedCount > 0) { + batchDeleteBtn.style.display = 'inline-block'; + batchStatusBtn.style.display = 'inline-block'; + } else { + batchDeleteBtn.style.display = 'none'; + batchStatusBtn.style.display = 'none'; + } + + // 更新选中数量显示 + document.getElementById('selected-count').textContent = `已选择 ${selectedCount} 项`; +} + +// 显示批量删除确认模态框 +function showBatchDeleteModal() { + const selectedCount = document.querySelectorAll('.device-checkbox:checked').length; + document.getElementById('batch-delete-count').textContent = selectedCount; + + const modal = new bootstrap.Modal(document.getElementById('batchDeleteModal')); + modal.show(); +} + +// 批量删除设备 +function batchDeleteDevices() { + const selectedCheckboxes = document.querySelectorAll('.device-checkbox:checked'); + const deviceIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.deviceId)); + + apiRequest('/api/v1/devices/batch', { + method: 'DELETE', + body: JSON.stringify({ device_ids: deviceIds }) + }) + .then(data => { + if (data.success) { + showNotification(data.message || '批量删除成功', 'success'); + loadDevices(currentPage); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal')); + modal.hide(); + + // 重置全选复选框 + document.getElementById('select-all-checkbox').checked = false; + } else { + showNotification(data.message || '批量删除失败', 'error'); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal')); + modal.hide(); + } + }) + .catch(error => { + console.error('Failed to batch delete devices:', error); + showNotification('批量删除失败', 'error'); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal')); + modal.hide(); + }); +} + +// 批量更新设备状态 +function batchUpdateDeviceStatus(status) { + const selectedCheckboxes = document.querySelectorAll('.device-checkbox:checked'); + const deviceIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.deviceId)); + + if (deviceIds.length === 0) { + showNotification('请至少选择一个设备', 'warning'); + return; + } + + apiRequest('/api/v1/devices/batch/status', { + method: 'PUT', + body: JSON.stringify({ + device_ids: deviceIds, + status: status + }) + }) + .then(data => { + if (data.success) { + showNotification(data.message || '批量更新状态成功', 'success'); + loadDevices(currentPage); + + // 重置全选复选框 + document.getElementById('select-all-checkbox').checked = false; + } else { + showNotification(data.message || '批量更新状态失败', 'error'); + } + }) + .catch(error => { + console.error('Failed to batch update device status:', error); + showNotification('批量更新状态失败', 'error'); + }); +} + +// 在页面加载完成后为批量删除按钮绑定事件 +document.addEventListener('DOMContentLoaded', function() { + const batchDeleteBtn = document.getElementById('batch-delete-btn'); + if (batchDeleteBtn) { + batchDeleteBtn.addEventListener('click', showBatchDeleteModal); + } +}); + \ No newline at end of file diff --git a/app/web/templates/license/list.html b/app/web/templates/license/list.html index 472e7d1..aec570a 100644 --- a/app/web/templates/license/list.html +++ b/app/web/templates/license/list.html @@ -6,24 +6,30 @@ {% block page_actions %} - + 生成卡密 - + 导入卡密 + + + 导出卡密 + {% endblock %} {% block content %} - +
- + +
+
+
- + +
-
- +
+
+ + +
+ +
+
+
+
+ + +
+
+ 已选择 0 项 +
+
+
+
+
- + + - + @@ -71,26 +113,40 @@ - +
+ + 卡密 产品 类型 状态设备信息绑定信息 激活时间 过期时间 操作
-
- 加载中... -
加载中...
- + -
+ + + {% endblock %} -{% block extra_js %} -{% endblock %} \ No newline at end of file +// 更新批量操作按钮状态 +function updateBatchButtons() { + const selectedCount = document.querySelectorAll('.license-checkbox:checked').length; + const batchDeleteBtn = document.getElementById('batch-delete-btn'); + const batchStatusBtn = document.getElementById('batch-status-btn'); + + if (selectedCount > 0) { + batchDeleteBtn.style.display = 'inline-block'; + batchStatusBtn.style.display = 'inline-block'; + } else { + batchDeleteBtn.style.display = 'none'; + batchStatusBtn.style.display = 'none'; + } + + // 更新选中数量显示 + document.getElementById('selected-count').textContent = `已选择 ${selectedCount} 项`; +} + +// 显示批量删除确认模态框 +function showBatchDeleteModal() { + const selectedCount = document.querySelectorAll('.license-checkbox:checked').length; + document.getElementById('batch-delete-count').textContent = selectedCount; + + const modal = new bootstrap.Modal(document.getElementById('batchDeleteModal')); + modal.show(); +} + +// 批量删除卡密 +function batchDeleteLicenses() { + const selectedCheckboxes = document.querySelectorAll('.license-checkbox:checked'); + const licenseKeys = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.licenseKey); + + apiRequest('/api/v1/licenses/batch', { + method: 'DELETE', + body: JSON.stringify({ license_keys: licenseKeys }) + }) + .then(data => { + if (data.success) { + showNotification(data.message || '批量删除成功', 'success'); + loadLicenses(currentPage); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal')); + modal.hide(); + + // 重置全选复选框 + document.getElementById('select-all-checkbox').checked = false; + } else { + showNotification(data.message || '批量删除失败', 'error'); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal')); + modal.hide(); + } + }) + .catch(error => { + console.error('Failed to batch delete licenses:', error); + showNotification('批量删除失败', 'error'); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal')); + modal.hide(); + }); +} + +// 批量更新卡密状态 +function batchUpdateLicenseStatus(status) { + const selectedCheckboxes = document.querySelectorAll('.license-checkbox:checked'); + const licenseKeys = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.licenseKey); + + if (licenseKeys.length === 0) { + showNotification('请至少选择一个卡密', 'warning'); + return; + } + + apiRequest('/api/v1/licenses/batch/status', { + method: 'PUT', + body: JSON.stringify({ + license_keys: licenseKeys, + status: status + }) + }) + .then(data => { + if (data.success) { + showNotification(data.message || '批量更新状态成功', 'success'); + loadLicenses(currentPage); + + // 重置全选复选框 + document.getElementById('select-all-checkbox').checked = false; + } else { + showNotification(data.message || '批量更新状态失败', 'error'); + } + }) + .catch(error => { + console.error('Failed to batch update license status:', error); + showNotification('批量更新状态失败', 'error'); + }); +} + +// 在页面加载完成后为批量删除按钮绑定事件 +document.addEventListener('DOMContentLoaded', function() { + const batchDeleteBtn = document.getElementById('batch-delete-btn'); + if (batchDeleteBtn) { + batchDeleteBtn.addEventListener('click', showBatchDeleteModal); + } +}); + \ No newline at end of file diff --git a/app/web/templates/product/list.html b/app/web/templates/product/list.html index d991b18..762c619 100644 --- a/app/web/templates/product/list.html +++ b/app/web/templates/product/list.html @@ -12,34 +12,62 @@ {% endblock %} {% block content %} - +
-
- -
- - + + +
+
+
+ + +
+ +
+
+
+
+ + +
+
+ 已选择 0 项 +
+
+
+
+
- + + @@ -52,19 +80,15 @@ - +
+ + 产品ID 产品名称 描述
-
- 加载中... -
加载中...
- + -
@@ -79,19 +103,36 @@
+
+
+ + + + {% endblock %} -{% block extra_js %} -{% endblock %} \ No newline at end of file diff --git a/app/web/templates/ticket/list.html b/app/web/templates/ticket/list.html index 207d9d2..0a8df3b 100644 --- a/app/web/templates/ticket/list.html +++ b/app/web/templates/ticket/list.html @@ -5,21 +5,23 @@ {% block page_title %}工单管理{% endblock %} {% block page_actions %} - {% endblock %} {% block content %} - +
- + +
+
+
- + +
-
- +
+
+ + +
+ +
+
+
+
+ +
+
+ 已选择 0 项 +
+
+
+
+
- + + - + - +
+ + 工单ID 标题 产品 优先级 状态 创建时间最后更新更新时间 操作
-
- 加载中... -
加载中...
- + -
@@ -98,46 +132,66 @@
- +
-
- - -
- -
- - +
+
+ + +
+
+ + +
- +
+
+
+ + + + {% endblock %} -{% block extra_js %} -{% endblock %} \ No newline at end of file + +// 更新批量操作按钮状态 +function updateBatchButtons() { + const selectedCount = document.querySelectorAll('.ticket-checkbox:checked').length; + const batchStatusBtn = document.getElementById('batch-status-btn'); + + if (selectedCount > 0) { + batchStatusBtn.style.display = 'inline-block'; + } else { + batchStatusBtn.style.display = 'none'; + } + + // 更新选中数量显示 + document.getElementById('selected-count').textContent = `已选择 ${selectedCount} 项`; +} + +// 显示批量更新状态模态框 +function showBatchUpdateStatusModal(status, statusText) { + const selectedCount = document.querySelectorAll('.ticket-checkbox:checked').length; + if (selectedCount === 0) { + showNotification('请至少选择一个工单', 'warning'); + return; + } + + batchUpdateStatus = status; + document.getElementById('batch-update-count').textContent = selectedCount; + document.getElementById('batch-update-status-text').textContent = statusText; + document.getElementById('batch-update-remark').value = ''; + + const modal = new bootstrap.Modal(document.getElementById('batchUpdateStatusModal')); + modal.show(); +} + +// 批量更新工单状态 +function batchUpdateTicketStatus() { + const selectedCheckboxes = document.querySelectorAll('.ticket-checkbox:checked'); + const ticketIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.ticketId)); + const remark = document.getElementById('batch-update-remark').value.trim(); + + if (ticketIds.length === 0) { + showNotification('请至少选择一个工单', 'warning'); + return; + } + + if (batchUpdateStatus === null) { + showNotification('请选择要更新的状态', 'warning'); + return; + } + + apiRequest('/api/v1/tickets/batch/status', { + method: 'PUT', + body: JSON.stringify({ + ticket_ids: ticketIds, + status: batchUpdateStatus, + remark: remark + }) + }) + .then(data => { + if (data.success) { + showNotification(data.message || '批量更新状态成功', 'success'); + loadTickets(currentPage); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchUpdateStatusModal')); + modal.hide(); + + // 重置全选复选框 + document.getElementById('select-all-checkbox').checked = false; + } else { + showNotification(data.message || '批量更新状态失败', 'error'); + } + }) + .catch(error => { + console.error('Failed to batch update ticket status:', error); + showNotification('批量更新状态失败', 'error'); + }); +} + \ No newline at end of file diff --git a/app/web/templates/version/list.html b/app/web/templates/version/list.html index 972dae6..9e46b95 100644 --- a/app/web/templates/version/list.html +++ b/app/web/templates/version/list.html @@ -12,14 +12,16 @@ {% endblock %} {% block content %} - +
- + +
+
+
-
- +
+
+ + +
+ +
+
+
+
+ + +
+
+ 已选择 0 项 +
+
+
+
+
- + + @@ -62,26 +96,40 @@ - +
+ + 版本号 产品 描述
-
- 加载中... -
加载中...
- + -
+ + + {% endblock %} -{% block extra_js %} -{% endblock %} \ No newline at end of file + +// 更新批量操作按钮状态 +function updateBatchButtons() { + const selectedCount = document.querySelectorAll('.version-checkbox:checked').length; + const batchDeleteBtn = document.getElementById('batch-delete-btn'); + const batchStatusBtn = document.getElementById('batch-status-btn'); + + if (selectedCount > 0) { + batchDeleteBtn.style.display = 'inline-block'; + batchStatusBtn.style.display = 'inline-block'; + } else { + batchDeleteBtn.style.display = 'none'; + batchStatusBtn.style.display = 'none'; + } + + // 更新选中数量显示 + document.getElementById('selected-count').textContent = `已选择 ${selectedCount} 项`; +} + +// 显示批量删除确认模态框 +function showBatchDeleteModal() { + const selectedCount = document.querySelectorAll('.version-checkbox:checked').length; + document.getElementById('batch-delete-count').textContent = selectedCount; + + const modal = new bootstrap.Modal(document.getElementById('batchDeleteModal')); + modal.show(); +} + +// 批量删除版本 +function batchDeleteVersions() { + const selectedCheckboxes = document.querySelectorAll('.version-checkbox:checked'); + const versionIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.versionId)); + + apiRequest('/api/v1/versions/batch', { + method: 'DELETE', + body: JSON.stringify({ version_ids: versionIds }) + }) + .then(data => { + if (data.success) { + showNotification(data.message || '批量删除成功', 'success'); + loadVersions(currentPage); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal')); + modal.hide(); + + // 重置全选复选框 + document.getElementById('select-all-checkbox').checked = false; + } else { + showNotification(data.message || '批量删除失败', 'error'); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal')); + modal.hide(); + } + }) + .catch(error => { + console.error('Failed to batch delete versions:', error); + showNotification('批量删除失败', 'error'); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal')); + modal.hide(); + }); +} + +// 批量更新版本状态 +function batchUpdateVersionStatus(status) { + const selectedCheckboxes = document.querySelectorAll('.version-checkbox:checked'); + const versionIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.versionId)); + + if (versionIds.length === 0) { + showNotification('请至少选择一个版本', 'warning'); + return; + } + + apiRequest('/api/v1/versions/batch/status', { + method: 'PUT', + body: JSON.stringify({ + version_ids: versionIds, + status: status + }) + }) + .then(data => { + if (data.success) { + showNotification(data.message || '批量更新状态成功', 'success'); + loadVersions(currentPage); + + // 重置全选复选框 + document.getElementById('select-all-checkbox').checked = false; + } else { + showNotification(data.message || '批量更新状态失败', 'error'); + } + }) + .catch(error => { + console.error('Failed to batch update version status:', error); + showNotification('批量更新状态失败', 'error'); + }); +} + +// 在页面加载完成后为批量删除按钮绑定事件 +document.addEventListener('DOMContentLoaded', function() { + const batchDeleteBtn = document.getElementById('batch-delete-btn'); + if (batchDeleteBtn) { + batchDeleteBtn.addEventListener('click', showBatchDeleteModal); + } +}); + \ No newline at end of file diff --git a/app/web/views.py b/app/web/views.py index 0952888..c4e7ddb 100644 --- a/app/web/views.py +++ b/app/web/views.py @@ -80,6 +80,13 @@ def import_license(): """导入卡密页面""" return render_template('license/import.html') +@web_bp.route('/licenses/export') +@login_required +def export_license(): + """导出卡密页面""" + products = Product.query.filter_by(status=1).all() + return render_template('license/export.html', products=products) + # 版本管理页面 @web_bp.route('/versions') @login_required diff --git a/check_routes.py b/check_routes.py index b59542a..443c272 100644 --- a/check_routes.py +++ b/check_routes.py @@ -1,16 +1,14 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - from app import create_app -def check_routes(): - """检查路由注册情况""" - app = create_app() - - with app.app_context(): - print("=== 路由注册情况 ===") - for rule in app.url_map.iter_rules(): - print(f"{rule.rule} -> {rule.endpoint}") +app = create_app() -if __name__ == "__main__": - check_routes() \ No newline at end of file +print("API routes:") +routes = [] +for rule in app.url_map.iter_rules(): + methods = rule.methods if rule.methods else set() + routes.append((rule.rule, ','.join(sorted(methods)), rule.endpoint)) + +routes.sort() +for r in routes: + if r[2].startswith('api'): + print(f'{r[0]:30} {r[1]:20} {r[2]}') \ No newline at end of file diff --git a/instance/kamaxitong.db b/instance/kamaxitong.db index 80f2235..1ab2743 100644 Binary files a/instance/kamaxitong.db and b/instance/kamaxitong.db differ diff --git a/test.py b/test.py new file mode 100644 index 0000000..0a7b1b1 --- /dev/null +++ b/test.py @@ -0,0 +1,220 @@ +import json +import zipfile +import os +import shutil +from typing import Optional, Dict, Any + + +class JianYingConverter: + def __init__(self, input_cmp: str, output_cmp: str, target_version: str = "4.0"): + """ + 初始化剪映草稿转换器 + :param input_cmp: 输入高版本.cmp草稿路径 + :param output_cmp: 输出低版本.cmp草稿路径 + :param target_version: 目标低版本(支持 "3.0" / "4.0") + """ + self.input_cmp = input_cmp + self.output_cmp = output_cmp + self.target_version = target_version + self.temp_dir = "temp_jianying" # 临时解压目录 + self.core_json = "project.json" # 核心配置文件 + + def _unzip_cmp(self) -> bool: + """解压.cmp文件到临时目录""" + if not os.path.exists(self.input_cmp): + print(f"错误:输入文件不存在 → {self.input_cmp}") + return False + + # 清空并创建临时目录 + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + os.makedirs(self.temp_dir) + + try: + with zipfile.ZipFile(self.input_cmp, 'r') as zip_ref: + zip_ref.extractall(self.temp_dir) + print(f"成功解压到临时目录 → {self.temp_dir}") + return True + except Exception as e: + print(f"解压失败:{str(e)}") + return False + + def _load_project_json(self) -> Optional[Dict[str, Any]]: + """读取project.json""" + json_path = os.path.join(self.temp_dir, self.core_json) + if not os.path.exists(json_path): + print(f"错误:未找到核心配置文件 → {json_path}") + return None + + try: + with open(json_path, 'r', encoding='utf-8') as f: + data = json.load(f) + print("成功读取project.json") + return data + except json.JSONDecodeError: + # 部分高版本草稿可能有简单加密(如Base64),尝试解密 + try: + import base64 + with open(json_path, 'r', encoding='utf-8') as f: + encrypted = f.read() + decrypted = base64.b64decode(encrypted).decode('utf-8') + data = json.loads(decrypted) + print("成功解密并读取project.json") + return data + except Exception as e: + print(f"读取/解密JSON失败:{str(e)}") + return None + + def _downgrade_json(self, data: Dict[str, Any]) -> Dict[str, Any]: + """根据目标版本降级JSON结构""" + print(f"正在降级到剪映 v{self.target_version} 格式...") + + # 1. 移除高版本新增的顶层字段(基于逆向分析) + high_version_fields = [ + "meta_info", "plugin_version", "advanced_settings", + "ai_editor_info", "cloud_project_info", "multi_track_info" + ] + for field in high_version_fields: + data.pop(field, None) + + # 2. 降级项目配置(强制设置兼容版本号) + if "project_config" in data: + if self.target_version == "3.0": + data["project_config"]["version"] = "3.9.0" + data["project_config"]["compatible_version"] = "3.0.0" + else: # 4.0 + data["project_config"]["version"] = "4.9.0" + data["project_config"]["compatible_version"] = "4.0.0" + + # 3. 处理轨道数据(移除高版本特效/转场) + if "tracks" in data: + self._downgrade_tracks(data["tracks"]) + + # 4. 处理资源列表(移除云资源引用) + if "resources" in data: + data["resources"] = [res for res in data["resources"] if not res.get("is_cloud", False)] + + print("JSON降级完成") + return data + + def _downgrade_tracks(self, tracks: list): + """降级轨道数据(移除低版本不支持的效果)""" + for track in tracks: + if "clips" not in track: + continue + + for clip in track["clips"]: + # 移除高版本转场(保留基础转场) + if "transition" in clip: + transition_type = clip["transition"].get("type", "") + # 低版本支持的基础转场列表(可根据需求扩展) + supported_transitions = ["none", "fade", "slide", "push", "zoom"] + if transition_type not in supported_transitions: + clip["transition"] = {"type": "none", "duration": 0.3} + + # 移除高版本特效(如AI特效、高级滤镜) + if "effects" in clip: + clip["effects"] = [ + eff for eff in clip["effects"] + if eff.get("type") in ["filter", "adjust", "text", "sticker"] # 保留基础效果 + ] + + # 降级音频效果(移除3D音效等高级功能) + if "audio_effects" in clip: + clip["audio_effects"] = [eff for eff in clip["audio_effects"] if eff.get("type") == "volume"] + + def _save_project_json(self, data: Dict[str, Any]) -> bool: + """保存降级后的JSON到临时目录""" + json_path = os.path.join(self.temp_dir, self.core_json) + try: + with open(json_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print("成功保存降级后的project.json") + return True + except Exception as e: + print(f"保存JSON失败:{str(e)}") + return False + + def _zip_to_cmp(self) -> bool: + """将临时目录重新压缩为.cmp文件""" + try: + with zipfile.ZipFile(self.output_cmp, 'w', zipfile.ZIP_DEFLATED) as zip_ref: + # 遍历临时目录所有文件,添加到压缩包 + for root, dirs, files in os.walk(self.temp_dir): + for file in files: + file_path = os.path.join(root, file) + # 保持压缩包内的相对路径 + arcname = os.path.relpath(file_path, self.temp_dir) + zip_ref.write(file_path, arcname) + print(f"成功生成低版本草稿 → {self.output_cmp}") + return True + except Exception as e: + print(f"压缩失败:{str(e)}") + return False + + def clean_temp(self): + """清理临时目录""" + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + print("临时目录已清理") + + def convert(self) -> bool: + """执行完整转换流程""" + print("=" * 50) + print("剪映草稿高版本转低版本工具") + print(f"输入:{self.input_cmp}") + print(f"输出:{self.output_cmp}") + print(f"目标版本:v{self.target_version}") + print("=" * 50) + + try: + # 1. 解压 + if not self._unzip_cmp(): + return False + + # 2. 读取JSON + data = self._load_project_json() + if not data: + return False + + # 3. 降级JSON + downgraded_data = self._downgrade_json(data) + + # 4. 保存JSON + if not self._save_project_json(downgraded_data): + return False + + # 5. 重新压缩 + if not self._zip_to_cmp(): + return False + + # 6. 清理临时文件 + self.clean_temp() + + print("=" * 50) + print("转换成功!请用目标版本剪映打开输出文件") + print("注意:复杂特效/AI功能可能已降级为基础效果") + print("=" * 50) + return True + except Exception as e: + print(f"转换异常:{str(e)}") + self.clean_temp() + return False + + +# ------------------------------ +# 用法示例 +# ------------------------------ +if __name__ == "__main__": + # 请修改以下参数 + INPUT_CMP = "high_version_project.cmp" # 高版本草稿路径 + OUTPUT_CMP = "low_version_project.cmp" # 输出低版本草稿路径 + TARGET_VERSION = "4.0" # 目标低版本(3.0 或 4.0) + + # 创建转换器并执行 + converter = JianYingConverter( + input_cmp=INPUT_CMP, + output_cmp=OUTPUT_CMP, + target_version=TARGET_VERSION + ) + converter.convert() \ No newline at end of file