From cb925f7200396ead34aa8401a1ac459b52dfa2ce Mon Sep 17 00:00:00 2001 From: fanbook-wangdage <124357765+fanbook-wangdage@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:53:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=8B=E8=BD=BD=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=92=8C=E4=B8=8B=E8=BD=BD=E8=B5=84=E6=BA=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/init.py | 2 + routes/download_resource.py | 250 ++++++++++++++++++++++++++ services/download_resource_service.py | 209 +++++++++++++++++++++ 3 files changed, 461 insertions(+) create mode 100644 routes/download_resource.py create mode 100644 services/download_resource_service.py diff --git a/app/init.py b/app/init.py index 4b35a0a..96fb2af 100644 --- a/app/init.py +++ b/app/init.py @@ -15,12 +15,14 @@ def create_app(): from routes.gacha_log import gacha_log_bp from routes.web_api import web_api_bp from routes.misc import misc_bp + from routes.download_resource import download_resource_bp app.register_blueprint(announcement_bp, url_prefix="/Announcement") app.register_blueprint(auth_bp) app.register_blueprint(gacha_log_bp) app.register_blueprint(web_api_bp) app.register_blueprint(misc_bp) + app.register_blueprint(download_resource_bp) # CORS @app.after_request diff --git a/routes/download_resource.py b/routes/download_resource.py new file mode 100644 index 0000000..4344d1a --- /dev/null +++ b/routes/download_resource.py @@ -0,0 +1,250 @@ +from flask import Blueprint, request, jsonify +from app.decorators import require_maintainer_permission +from app.extensions import logger +from services.download_resource_service import ( + create_download_resource, + get_download_resources, + get_download_resource_by_id, + update_download_resource, + delete_download_resource, + get_latest_version +) + +download_resource_bp = Blueprint("download_resource", __name__) + + +# 公开API - 获取下载资源列表 + +@download_resource_bp.route('/download-resources', methods=['GET']) +def get_public_download_resources(): + """ + 获取下载资源列表(公开API) + 可选查询参数: + - package_type: 包类型 (msi/msix),不传则返回所有 + """ + package_type = request.args.get('package_type') + + # 验证package_type参数 + if package_type and package_type not in ['msi', 'msix']: + return jsonify({ + "code": 1, + "message": "Invalid package_type, must be 'msi' or 'msix'", + "data": None + }), 400 + + # 只返回激活的资源 + resources = get_download_resources(package_type=package_type, is_active=True) + + return jsonify({ + "code": 0, + "message": "success", + "data": resources + }) + + +@download_resource_bp.route('/download-resources/latest', methods=['GET']) +def get_latest_download_resource(): + """ + 获取最新版本(公开API) + 可选查询参数: + - package_type: 包类型 (msi/msix),不传则返回最新的任意类型 + """ + package_type = request.args.get('package_type') + + # 验证package_type参数 + if package_type and package_type not in ['msi', 'msix']: + return jsonify({ + "code": 1, + "message": "Invalid package_type, must be 'msi' or 'msix'", + "data": None + }), 400 + + resource = get_latest_version(package_type=package_type) + + if resource: + return jsonify({ + "code": 0, + "message": "success", + "data": resource + }) + else: + return jsonify({ + "code": 1, + "message": "No resource found", + "data": None + }), 404 + + +# Web管理端API - 增删改查 + +@download_resource_bp.route('/web-api/download-resources', methods=['POST']) +@require_maintainer_permission +def web_api_create_download_resource(): + """创建下载资源""" + data = request.get_json() + + # 验证必需字段 + required_fields = ['version', 'package_type', 'download_url'] + if not all(k in data for k in required_fields): + return jsonify({ + "code": 1, + "message": f"Missing required fields: {', '.join(required_fields)}", + "data": None + }), 400 + + # 验证package_type + if data['package_type'] not in ['msi', 'msix']: + return jsonify({ + "code": 1, + "message": "Invalid package_type, must be 'msi' or 'msix'", + "data": None + }), 400 + + # 添加创建者信息 + data['created_by'] = str(request.current_user['_id']) + + # 创建资源 + resource_id = create_download_resource(data) + + if resource_id: + logger.info(f"Download resource created with ID: {resource_id} by user: {request.current_user['email']}") + return jsonify({ + "code": 0, + "message": "Download resource created successfully", + "data": { + "id": str(resource_id) + } + }) + else: + logger.error("Failed to create download resource") + return jsonify({ + "code": 2, + "message": "Failed to create download resource", + "data": None + }), 500 + + +@download_resource_bp.route('/web-api/download-resources', methods=['GET']) +@require_maintainer_permission +def web_api_get_download_resources(): + """获取下载资源列表(管理端,包含所有资源,包括未激活的)""" + package_type = request.args.get('package_type') + is_active_str = request.args.get('is_active') + + # 验证package_type参数 + if package_type and package_type not in ['msi', 'msix']: + return jsonify({ + "code": 1, + "message": "Invalid package_type, must be 'msi' or 'msix'", + "data": None + }), 400 + + # 处理is_active参数 + is_active = None + if is_active_str is not None: + is_active = is_active_str.lower() == 'true' + + resources = get_download_resources(package_type=package_type, is_active=is_active) + + return jsonify({ + "code": 0, + "message": "success", + "data": resources + }) + + +@download_resource_bp.route('/web-api/download-resources/', methods=['GET']) +@require_maintainer_permission +def web_api_get_download_resource(resource_id): + """获取单个下载资源详情""" + resource = get_download_resource_by_id(resource_id) + + if resource: + return jsonify({ + "code": 0, + "message": "success", + "data": resource + }) + else: + return jsonify({ + "code": 1, + "message": "Resource not found", + "data": None + }), 404 + + +@download_resource_bp.route('/web-api/download-resources/', methods=['PUT']) +@require_maintainer_permission +def web_api_update_download_resource(resource_id): + """更新下载资源""" + data = request.get_json() + + # 检查资源是否存在 + existing_resource = get_download_resource_by_id(resource_id) + if not existing_resource: + return jsonify({ + "code": 1, + "message": "Resource not found", + "data": None + }), 404 + + # 验证package_type(如果提供) + if 'package_type' in data and data['package_type'] not in ['msi', 'msix']: + return jsonify({ + "code": 1, + "message": "Invalid package_type, must be 'msi' or 'msix'", + "data": None + }), 400 + + # 添加更新者信息 + data['updated_by'] = str(request.current_user['_id']) + + # 更新资源 + success = update_download_resource(resource_id, data) + + if success: + logger.info(f"Download resource {resource_id} updated by user: {request.current_user['email']}") + return jsonify({ + "code": 0, + "message": "Download resource updated successfully", + "data": None + }) + else: + logger.error(f"Failed to update download resource {resource_id}") + return jsonify({ + "code": 2, + "message": "Failed to update download resource", + "data": None + }), 500 + + +@download_resource_bp.route('/web-api/download-resources/', methods=['DELETE']) +@require_maintainer_permission +def web_api_delete_download_resource(resource_id): + """删除下载资源""" + # 检查资源是否存在 + existing_resource = get_download_resource_by_id(resource_id) + if not existing_resource: + return jsonify({ + "code": 1, + "message": "Resource not found", + "data": None + }), 404 + + # 删除资源 + success = delete_download_resource(resource_id) + + if success: + logger.info(f"Download resource {resource_id} deleted by user: {request.current_user['email']}") + return jsonify({ + "code": 0, + "message": "Download resource deleted successfully", + "data": None + }) + else: + logger.error(f"Failed to delete download resource {resource_id}") + return jsonify({ + "code": 2, + "message": "Failed to delete download resource", + "data": None + }), 500 \ No newline at end of file diff --git a/services/download_resource_service.py b/services/download_resource_service.py new file mode 100644 index 0000000..d180cb7 --- /dev/null +++ b/services/download_resource_service.py @@ -0,0 +1,209 @@ +import datetime +from app.extensions import client, logger +from app.config import Config + + +def create_download_resource(data): + """ + 创建下载资源 + + :param data: 包含以下字段的数据 + - version: 版本号 + - package_type: 包类型 (msi/msix) + - download_url: 下载链接 + - features: 新功能描述 + - file_size: 文件大小 (可选) + - file_hash: 文件哈希 (可选) + - is_active: 是否激活 (可选,默认为True) + :return: 创建的资源ID或None + """ + try: + resource_doc = { + "version": data['version'], + "package_type": data['package_type'], + "download_url": data['download_url'], + "features": data.get('features', ''), + "file_size": data.get('file_size'), + "file_hash": data.get('file_hash'), + "is_active": data.get('is_active', True), + "created_at": datetime.datetime.utcnow(), + "created_by": data.get('created_by') + } + + result = client.ht_server.download_resources.insert_one(resource_doc) + logger.info(f"Download resource created with ID: {result.inserted_id}") + return result.inserted_id + except Exception as e: + logger.error(f"Failed to create download resource: {e}") + return None + + +def get_download_resources(package_type=None, is_active=None): + """ + 获取下载资源列表 + + :param package_type: 包类型过滤 (msi/msix),None表示获取所有 + :param is_active: 是否激活过滤,None表示获取所有 + :return: 资源列表 + """ + try: + query = {} + if package_type: + query['package_type'] = package_type + if is_active is not None: + query['is_active'] = is_active + + resources = list(client.ht_server.download_resources.find(query, sort=[("created_at", -1)])) + + # 移除 _id 字段并转换日期 + result = [] + for r in resources: + r = dict(r) + # 转换_id为字符串并存为id字段 + r['id'] = str(r.pop('_id')) + # 转换datetime为配置时区的ISO格式字符串 + if 'created_at' in r and isinstance(r['created_at'], datetime.datetime): + dt = r['created_at'].replace(tzinfo=datetime.timezone.utc) + dt = dt.astimezone(Config.TIMEZONE) + r['created_at'] = dt.isoformat() + if 'updated_at' in r and isinstance(r['updated_at'], datetime.datetime): + dt = r['updated_at'].replace(tzinfo=datetime.timezone.utc) + dt = dt.astimezone(Config.TIMEZONE) + r['updated_at'] = dt.isoformat() + result.append(r) + + return result + except Exception as e: + logger.error(f"Failed to get download resources: {e}") + return [] + + +def get_download_resource_by_id(resource_id): + """ + 根据ID获取下载资源 + + :param resource_id: 资源ID + :return: 资源对象或None + """ + try: + from bson import ObjectId + resource = client.ht_server.download_resources.find_one({"_id": ObjectId(resource_id)}) + + if resource: + resource = dict(resource) + resource.pop('_id', None) + # 转换datetime为配置时区的ISO格式字符串 + if 'created_at' in resource and isinstance(resource['created_at'], datetime.datetime): + dt = resource['created_at'].replace(tzinfo=datetime.timezone.utc) + dt = dt.astimezone(Config.TIMEZONE) + resource['created_at'] = dt.isoformat() + if 'updated_at' in resource and isinstance(resource['updated_at'], datetime.datetime): + dt = resource['updated_at'].replace(tzinfo=datetime.timezone.utc) + dt = dt.astimezone(Config.TIMEZONE) + resource['updated_at'] = dt.isoformat() + return resource + return None + except Exception as e: + logger.error(f"Failed to get download resource by ID: {e}") + return None + + +def update_download_resource(resource_id, data): + """ + 更新下载资源 + + :param resource_id: 资源ID + :param data: 要更新的字段 + :return: 是否成功 + """ + try: + from bson import ObjectId + + # 构建更新数据 + update_data = {"updated_at": datetime.datetime.utcnow()} + + if 'version' in data: + update_data['version'] = data['version'] + if 'package_type' in data: + update_data['package_type'] = data['package_type'] + if 'download_url' in data: + update_data['download_url'] = data['download_url'] + if 'features' in data: + update_data['features'] = data['features'] + if 'file_size' in data: + update_data['file_size'] = data['file_size'] + if 'file_hash' in data: + update_data['file_hash'] = data['file_hash'] + if 'is_active' in data: + update_data['is_active'] = data['is_active'] + if 'updated_by' in data: + update_data['updated_by'] = data['updated_by'] + + result = client.ht_server.download_resources.update_one( + {"_id": ObjectId(resource_id)}, + {"$set": update_data} + ) + + if result.modified_count > 0: + logger.info(f"Download resource {resource_id} updated successfully") + return True + return False + except Exception as e: + logger.error(f"Failed to update download resource: {e}") + return False + + +def delete_download_resource(resource_id): + """ + 删除下载资源 + + :param resource_id: 资源ID + :return: 是否成功 + """ + try: + from bson import ObjectId + result = client.ht_server.download_resources.delete_one({"_id": ObjectId(resource_id)}) + + if result.deleted_count > 0: + logger.info(f"Download resource {resource_id} deleted successfully") + return True + return False + except Exception as e: + logger.error(f"Failed to delete download resource: {e}") + return False + + +def get_latest_version(package_type=None): + """ + 获取最新版本 + + :param package_type: 包类型 (msi/msix),None表示获取所有类型的最新版本 + :return: 资源对象或None + """ + try: + query = {"is_active": True} + if package_type: + query['package_type'] = package_type + + resource = client.ht_server.download_resources.find_one( + query, + sort=[("created_at", -1)] + ) + + if resource: + resource = dict(resource) + resource.pop('_id', None) + # 转换datetime为配置时区的ISO格式字符串 + if 'created_at' in resource and isinstance(resource['created_at'], datetime.datetime): + dt = resource['created_at'].replace(tzinfo=datetime.timezone.utc) + dt = dt.astimezone(Config.TIMEZONE) + resource['created_at'] = dt.isoformat() + if 'updated_at' in resource and isinstance(resource['updated_at'], datetime.datetime): + dt = resource['updated_at'].replace(tzinfo=datetime.timezone.utc) + dt = dt.astimezone(Config.TIMEZONE) + resource['updated_at'] = dt.isoformat() + return resource + return None + except Exception as e: + logger.error(f"Failed to get latest version: {e}") + return None \ No newline at end of file