2026-01-19 11:03:08 +00:00
|
|
|
|
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
|
|
|
|
|
|
from app.core.database import get_db_connection
|
|
|
|
|
|
from app.core.auth import get_current_admin_user
|
|
|
|
|
|
from app.core.response import create_api_response
|
|
|
|
|
|
from app.core.config import BASE_DIR, EXTERNAL_APPS_DIR, ALLOWED_IMAGE_EXTENSIONS, MAX_IMAGE_SIZE
|
|
|
|
|
|
from app.utils.apk_parser import parse_apk_with_androguard
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
import os
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import json
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
import hashlib
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
# APK上传配置
|
|
|
|
|
|
ALLOWED_APK_EXTENSIONS = {'.apk'}
|
|
|
|
|
|
MAX_APK_SIZE = 200 * 1024 * 1024 # 200MB
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CreateExternalAppRequest(BaseModel):
|
|
|
|
|
|
app_name: str
|
|
|
|
|
|
app_type: str # 'native' or 'web'
|
|
|
|
|
|
app_info: str # JSON string
|
|
|
|
|
|
icon_url: Optional[str] = None
|
|
|
|
|
|
description: Optional[str] = None
|
|
|
|
|
|
sort_order: int = 0
|
|
|
|
|
|
is_active: bool = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateExternalAppRequest(BaseModel):
|
|
|
|
|
|
app_name: Optional[str] = None
|
|
|
|
|
|
app_type: Optional[str] = None
|
|
|
|
|
|
app_info: Optional[str] = None
|
|
|
|
|
|
icon_url: Optional[str] = None
|
|
|
|
|
|
description: Optional[str] = None
|
|
|
|
|
|
sort_order: Optional[int] = None
|
|
|
|
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/external-apps", response_model=dict)
|
|
|
|
|
|
async def get_external_apps(
|
|
|
|
|
|
app_type: Optional[str] = None,
|
|
|
|
|
|
is_active: Optional[bool] = None,
|
|
|
|
|
|
current_user: dict = Depends(get_current_admin_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取外部应用列表(管理后台接口)
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with get_db_connection() as conn:
|
|
|
|
|
|
cursor = conn.cursor(dictionary=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 构建查询条件
|
|
|
|
|
|
where_clauses = []
|
|
|
|
|
|
params = []
|
|
|
|
|
|
|
|
|
|
|
|
if app_type:
|
|
|
|
|
|
where_clauses.append("app_type = %s")
|
|
|
|
|
|
params.append(app_type)
|
|
|
|
|
|
|
|
|
|
|
|
if is_active is not None:
|
|
|
|
|
|
where_clauses.append("is_active = %s")
|
|
|
|
|
|
params.append(is_active)
|
|
|
|
|
|
|
|
|
|
|
|
where_clause = " AND ".join(where_clauses) if where_clauses else "1=1"
|
|
|
|
|
|
|
|
|
|
|
|
# 获取总数
|
|
|
|
|
|
count_query = f"SELECT COUNT(*) as total FROM external_apps WHERE {where_clause}"
|
|
|
|
|
|
cursor.execute(count_query, params)
|
|
|
|
|
|
total = cursor.fetchone()['total']
|
|
|
|
|
|
|
|
|
|
|
|
# 获取列表数据
|
|
|
|
|
|
list_query = f"""
|
|
|
|
|
|
SELECT ea.*, u.username as creator_username
|
|
|
|
|
|
FROM external_apps ea
|
|
|
|
|
|
LEFT JOIN users u ON ea.created_by = u.user_id
|
|
|
|
|
|
WHERE {where_clause}
|
|
|
|
|
|
ORDER BY ea.sort_order ASC, ea.created_at DESC
|
|
|
|
|
|
"""
|
2026-01-19 13:25:14 +00:00
|
|
|
|
cursor.execute(list_query, params)
|
2026-01-19 11:03:08 +00:00
|
|
|
|
apps = cursor.fetchall()
|
|
|
|
|
|
|
|
|
|
|
|
# 解析 app_info JSON
|
|
|
|
|
|
for app in apps:
|
|
|
|
|
|
if app.get('app_info'):
|
|
|
|
|
|
try:
|
|
|
|
|
|
app['app_info'] = json.loads(app['app_info'])
|
|
|
|
|
|
except:
|
|
|
|
|
|
app['app_info'] = {}
|
|
|
|
|
|
|
|
|
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="200",
|
|
|
|
|
|
message="获取成功",
|
2026-01-19 13:25:14 +00:00
|
|
|
|
data=apps
|
2026-01-19 11:03:08 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="500",
|
|
|
|
|
|
message=f"获取外部应用列表失败: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/external-apps/active", response_model=dict)
|
|
|
|
|
|
async def get_active_external_apps():
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取所有启用的外部应用(公开接口,供客户端调用)
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with get_db_connection() as conn:
|
|
|
|
|
|
cursor = conn.cursor(dictionary=True)
|
|
|
|
|
|
|
|
|
|
|
|
query = """
|
|
|
|
|
|
SELECT id, app_name, app_type, app_info, icon_url, description, sort_order
|
|
|
|
|
|
FROM external_apps
|
|
|
|
|
|
WHERE is_active = TRUE
|
|
|
|
|
|
ORDER BY sort_order ASC, created_at DESC
|
|
|
|
|
|
"""
|
|
|
|
|
|
cursor.execute(query)
|
|
|
|
|
|
apps = cursor.fetchall()
|
|
|
|
|
|
|
|
|
|
|
|
# 解析 app_info JSON
|
|
|
|
|
|
for app in apps:
|
|
|
|
|
|
if app.get('app_info'):
|
|
|
|
|
|
try:
|
|
|
|
|
|
app['app_info'] = json.loads(app['app_info'])
|
|
|
|
|
|
except:
|
|
|
|
|
|
app['app_info'] = {}
|
|
|
|
|
|
|
|
|
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
|
|
|
|
# 按类型分组
|
|
|
|
|
|
native_apps = [app for app in apps if app['app_type'] == 'native']
|
|
|
|
|
|
web_apps = [app for app in apps if app['app_type'] == 'web']
|
|
|
|
|
|
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="200",
|
|
|
|
|
|
message="获取成功",
|
|
|
|
|
|
data={
|
|
|
|
|
|
"native": native_apps,
|
|
|
|
|
|
"web": web_apps,
|
|
|
|
|
|
"all": apps
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="500",
|
|
|
|
|
|
message=f"获取外部应用失败: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/external-apps", response_model=dict)
|
|
|
|
|
|
async def create_external_app(
|
|
|
|
|
|
request: CreateExternalAppRequest,
|
|
|
|
|
|
current_user: dict = Depends(get_current_admin_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
创建外部应用
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 验证 app_info 是否为有效JSON
|
|
|
|
|
|
try:
|
|
|
|
|
|
json.loads(request.app_info)
|
|
|
|
|
|
except:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="400",
|
|
|
|
|
|
message="app_info 必须是有效的JSON格式"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
with get_db_connection() as conn:
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
query = """
|
|
|
|
|
|
INSERT INTO external_apps
|
|
|
|
|
|
(app_name, app_type, app_info, icon_url, description, sort_order, is_active, created_by)
|
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
|
|
|
|
|
"""
|
|
|
|
|
|
cursor.execute(query, (
|
|
|
|
|
|
request.app_name,
|
|
|
|
|
|
request.app_type,
|
|
|
|
|
|
request.app_info,
|
|
|
|
|
|
request.icon_url,
|
|
|
|
|
|
request.description,
|
|
|
|
|
|
request.sort_order,
|
|
|
|
|
|
request.is_active,
|
|
|
|
|
|
current_user['user_id']
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
app_id = cursor.lastrowid
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="200",
|
|
|
|
|
|
message="创建成功",
|
|
|
|
|
|
data={"id": app_id}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="500",
|
|
|
|
|
|
message=f"创建外部应用失败: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/external-apps/{app_id}", response_model=dict)
|
|
|
|
|
|
async def update_external_app(
|
|
|
|
|
|
app_id: int,
|
|
|
|
|
|
request: UpdateExternalAppRequest,
|
|
|
|
|
|
current_user: dict = Depends(get_current_admin_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
更新外部应用
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 验证 app_info 是否为有效JSON
|
|
|
|
|
|
if request.app_info:
|
|
|
|
|
|
try:
|
|
|
|
|
|
json.loads(request.app_info)
|
|
|
|
|
|
except:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="400",
|
|
|
|
|
|
message="app_info 必须是有效的JSON格式"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
with get_db_connection() as conn:
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
# 构建更新字段
|
|
|
|
|
|
update_fields = []
|
|
|
|
|
|
params = []
|
|
|
|
|
|
|
|
|
|
|
|
if request.app_name is not None:
|
|
|
|
|
|
update_fields.append("app_name = %s")
|
|
|
|
|
|
params.append(request.app_name)
|
|
|
|
|
|
|
|
|
|
|
|
if request.app_type is not None:
|
|
|
|
|
|
update_fields.append("app_type = %s")
|
|
|
|
|
|
params.append(request.app_type)
|
|
|
|
|
|
|
|
|
|
|
|
if request.app_info is not None:
|
|
|
|
|
|
update_fields.append("app_info = %s")
|
|
|
|
|
|
params.append(request.app_info)
|
|
|
|
|
|
|
|
|
|
|
|
if request.icon_url is not None:
|
|
|
|
|
|
update_fields.append("icon_url = %s")
|
|
|
|
|
|
params.append(request.icon_url)
|
|
|
|
|
|
|
|
|
|
|
|
if request.description is not None:
|
|
|
|
|
|
update_fields.append("description = %s")
|
|
|
|
|
|
params.append(request.description)
|
|
|
|
|
|
|
|
|
|
|
|
if request.sort_order is not None:
|
|
|
|
|
|
update_fields.append("sort_order = %s")
|
|
|
|
|
|
params.append(request.sort_order)
|
|
|
|
|
|
|
|
|
|
|
|
if request.is_active is not None:
|
|
|
|
|
|
update_fields.append("is_active = %s")
|
|
|
|
|
|
params.append(request.is_active)
|
|
|
|
|
|
|
|
|
|
|
|
if not update_fields:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="400",
|
|
|
|
|
|
message="没有需要更新的字段"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
params.append(app_id)
|
|
|
|
|
|
query = f"""
|
|
|
|
|
|
UPDATE external_apps
|
|
|
|
|
|
SET {', '.join(update_fields)}
|
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
|
"""
|
|
|
|
|
|
cursor.execute(query, params)
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="200",
|
|
|
|
|
|
message="更新成功"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="500",
|
|
|
|
|
|
message=f"更新外部应用失败: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/external-apps/{app_id}", response_model=dict)
|
|
|
|
|
|
async def delete_external_app(
|
|
|
|
|
|
app_id: int,
|
|
|
|
|
|
current_user: dict = Depends(get_current_admin_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
删除外部应用
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with get_db_connection() as conn:
|
|
|
|
|
|
cursor = conn.cursor(dictionary=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取应用信息(用于删除APK文件)
|
|
|
|
|
|
cursor.execute("SELECT * FROM external_apps WHERE id = %s", (app_id,))
|
|
|
|
|
|
app = cursor.fetchone()
|
|
|
|
|
|
|
|
|
|
|
|
if not app:
|
|
|
|
|
|
cursor.close()
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="404",
|
|
|
|
|
|
message="应用不存在"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 删除数据库记录
|
|
|
|
|
|
cursor.execute("DELETE FROM external_apps WHERE id = %s", (app_id,))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
# 删除相关文件
|
|
|
|
|
|
files_to_delete = []
|
|
|
|
|
|
|
|
|
|
|
|
# 如果是原生应用,添加APK文件到删除列表
|
|
|
|
|
|
if app['app_type'] == 'native' and app.get('app_info'):
|
|
|
|
|
|
try:
|
|
|
|
|
|
app_info = json.loads(app['app_info']) if isinstance(app['app_info'], str) else app['app_info']
|
|
|
|
|
|
apk_url = app_info.get('apk_url', '')
|
|
|
|
|
|
if apk_url and apk_url.startswith('/uploads/'):
|
|
|
|
|
|
apk_path = BASE_DIR / apk_url.lstrip('/')
|
|
|
|
|
|
files_to_delete.append(('APK', apk_path))
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Failed to parse app_info for APK deletion: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# 添加图标文件到删除列表
|
|
|
|
|
|
if app.get('icon_url'):
|
|
|
|
|
|
icon_url = app['icon_url']
|
|
|
|
|
|
if icon_url and icon_url.startswith('/uploads/'):
|
|
|
|
|
|
icon_path = BASE_DIR / icon_url.lstrip('/')
|
|
|
|
|
|
files_to_delete.append(('Icon', icon_path))
|
|
|
|
|
|
|
|
|
|
|
|
# 执行文件删除
|
|
|
|
|
|
for file_type, file_path in files_to_delete:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if file_path.exists():
|
|
|
|
|
|
os.remove(file_path)
|
|
|
|
|
|
print(f"Deleted {file_type} file: {file_path}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Failed to delete {file_type} file: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="200",
|
|
|
|
|
|
message="删除成功"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="500",
|
|
|
|
|
|
message=f"删除外部应用失败: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/external-apps/upload-apk", response_model=dict)
|
|
|
|
|
|
async def upload_apk(
|
|
|
|
|
|
apk_file: UploadFile = File(...),
|
|
|
|
|
|
current_user: dict = Depends(get_current_admin_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
上传APK文件并解析信息
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 验证文件类型
|
|
|
|
|
|
file_extension = os.path.splitext(apk_file.filename)[1].lower()
|
|
|
|
|
|
if file_extension not in ALLOWED_APK_EXTENSIONS:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="400",
|
|
|
|
|
|
message=f"不支持的文件类型。仅支持: {', '.join(ALLOWED_APK_EXTENSIONS)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 验证文件大小
|
|
|
|
|
|
apk_file.file.seek(0, 2) # 移动到文件末尾
|
|
|
|
|
|
file_size = apk_file.file.tell()
|
|
|
|
|
|
apk_file.file.seek(0) # 重置到文件开头
|
|
|
|
|
|
|
|
|
|
|
|
if file_size > MAX_APK_SIZE:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="400",
|
|
|
|
|
|
message=f"文件大小超过 {MAX_APK_SIZE // (1024 * 1024)}MB 限制"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用原始文件名
|
|
|
|
|
|
original_filename = apk_file.filename
|
|
|
|
|
|
file_path = EXTERNAL_APPS_DIR / original_filename
|
|
|
|
|
|
|
|
|
|
|
|
# 如果同名文件已存在,先删除
|
|
|
|
|
|
if file_path.exists():
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.remove(file_path)
|
|
|
|
|
|
print(f"删除已存在的同名文件: {file_path}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"删除同名文件失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# 保存文件
|
|
|
|
|
|
with open(file_path, "wb") as buffer:
|
|
|
|
|
|
shutil.copyfileobj(apk_file.file, buffer)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算MD5
|
|
|
|
|
|
md5_hash = hashlib.md5()
|
|
|
|
|
|
with open(file_path, "rb") as f:
|
|
|
|
|
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
|
|
|
|
md5_hash.update(chunk)
|
|
|
|
|
|
apk_md5 = md5_hash.hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
# 解析APK
|
|
|
|
|
|
try:
|
|
|
|
|
|
apk_info = parse_apk_with_androguard(str(file_path))
|
|
|
|
|
|
print(f"APK解析成功: {apk_info}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
# 删除已上传的文件
|
|
|
|
|
|
if file_path.exists():
|
|
|
|
|
|
os.remove(file_path)
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="400",
|
|
|
|
|
|
message=f"APK解析失败: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算相对路径
|
|
|
|
|
|
relative_path = file_path.relative_to(EXTERNAL_APPS_DIR.parent.parent)
|
|
|
|
|
|
|
|
|
|
|
|
# 返回解析结果
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="200",
|
|
|
|
|
|
message="APK上传并解析成功",
|
|
|
|
|
|
data={
|
|
|
|
|
|
"apk_url": "/" + str(relative_path).replace("\\", "/"),
|
|
|
|
|
|
"apk_size": file_size,
|
|
|
|
|
|
"apk_md5": apk_md5,
|
|
|
|
|
|
"package_name": apk_info.get('package_name'),
|
|
|
|
|
|
"version_name": apk_info.get('version_name'),
|
|
|
|
|
|
"version_code": apk_info.get('version_code'),
|
|
|
|
|
|
"app_name": apk_info.get('app_name'),
|
|
|
|
|
|
"min_sdk_version": apk_info.get('min_sdk_version'),
|
|
|
|
|
|
"target_sdk_version": apk_info.get('target_sdk_version')
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="500",
|
|
|
|
|
|
message=f"上传APK失败: {str(e)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/external-apps/upload-icon", response_model=dict)
|
|
|
|
|
|
async def upload_icon(
|
|
|
|
|
|
icon_file: UploadFile = File(...),
|
|
|
|
|
|
current_user: dict = Depends(get_current_admin_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
上传应用图标
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 验证文件类型
|
|
|
|
|
|
file_extension = os.path.splitext(icon_file.filename)[1].lower()
|
|
|
|
|
|
if file_extension not in ALLOWED_IMAGE_EXTENSIONS:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="400",
|
|
|
|
|
|
message=f"不支持的文件类型。仅支持: {', '.join(ALLOWED_IMAGE_EXTENSIONS)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 验证文件大小
|
|
|
|
|
|
icon_file.file.seek(0, 2)
|
|
|
|
|
|
file_size = icon_file.file.tell()
|
|
|
|
|
|
icon_file.file.seek(0)
|
|
|
|
|
|
|
|
|
|
|
|
if file_size > MAX_IMAGE_SIZE:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="400",
|
|
|
|
|
|
message=f"文件大小超过 {MAX_IMAGE_SIZE // (1024 * 1024)}MB 限制"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 生成唯一文件名(图标使用UUID避免冲突)
|
|
|
|
|
|
unique_filename = f"icon_{uuid.uuid4()}{file_extension}"
|
|
|
|
|
|
file_path = EXTERNAL_APPS_DIR / unique_filename
|
|
|
|
|
|
|
|
|
|
|
|
# 保存文件
|
|
|
|
|
|
with open(file_path, "wb") as buffer:
|
|
|
|
|
|
shutil.copyfileobj(icon_file.file, buffer)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算相对路径
|
|
|
|
|
|
relative_path = file_path.relative_to(EXTERNAL_APPS_DIR.parent.parent)
|
|
|
|
|
|
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="200",
|
|
|
|
|
|
message="图标上传成功",
|
|
|
|
|
|
data={
|
|
|
|
|
|
"icon_url": "/" + str(relative_path).replace("\\", "/"),
|
|
|
|
|
|
"file_size": file_size
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return create_api_response(
|
|
|
|
|
|
code="500",
|
|
|
|
|
|
message=f"上传图标失败: {str(e)}"
|
|
|
|
|
|
)
|