增加了外部应用管理
parent
f616cb3fc3
commit
9fad51851d
|
|
@ -0,0 +1,518 @@
|
|||
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,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
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']
|
||||
|
||||
# 获取列表数据
|
||||
offset = (page - 1) * size
|
||||
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
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
cursor.execute(list_query, params + [size, offset])
|
||||
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="获取成功",
|
||||
data={
|
||||
"apps": apps,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size
|
||||
}
|
||||
)
|
||||
|
||||
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)}"
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, UploadFile, File
|
||||
from typing import Optional
|
||||
from app.models.models import UserInfo, PasswordChangeRequest, UserListResponse, CreateUserRequest, UpdateUserRequest, RoleInfo
|
||||
from app.core.database import get_db_connection
|
||||
|
|
@ -6,9 +6,14 @@ from app.core.auth import get_current_user
|
|||
from app.core.response import create_api_response
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
import app.core.config as config_module
|
||||
from app.core.config import UPLOAD_DIR, AVATAR_DIR
|
||||
import hashlib
|
||||
import datetime
|
||||
import re
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -50,17 +55,18 @@ def create_user(request: CreateUserRequest, current_user: dict = Depends(get_cur
|
|||
password = request.password if request.password else SystemConfigService.get_default_reset_password()
|
||||
hashed_password = hash_password(password)
|
||||
|
||||
query = "INSERT INTO users (username, password_hash, caption, email, role_id, created_at) VALUES (%s, %s, %s, %s, %s, %s)"
|
||||
query = "INSERT INTO users (username, password_hash, caption, email, avatar_url, role_id, created_at) VALUES (%s, %s, %s, %s, %s, %s, %s)"
|
||||
created_at = datetime.datetime.utcnow()
|
||||
cursor.execute(query, (request.username, hashed_password, request.caption, request.email, request.role_id, created_at))
|
||||
cursor.execute(query, (request.username, hashed_password, request.caption, request.email, request.avatar_url, request.role_id, created_at))
|
||||
connection.commit()
|
||||
|
||||
return create_api_response(code="200", message="用户创建成功")
|
||||
|
||||
@router.put("/users/{user_id}")
|
||||
def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = Depends(get_current_user)):
|
||||
if current_user['role_id'] != 1: # 1 is admin
|
||||
return create_api_response(code="403", message="仅管理员有权限修改用户信息")
|
||||
# Allow admin (role_id=1) or self
|
||||
if current_user['role_id'] != 1 and current_user['user_id'] != user_id:
|
||||
return create_api_response(code="403", message="没有权限修改此用户信息")
|
||||
|
||||
if request.email and not validate_email(request.email):
|
||||
return create_api_response(code="400", message="邮箱格式不正确")
|
||||
|
|
@ -68,7 +74,7 @@ def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = D
|
|||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("SELECT user_id, username, caption, email, role_id FROM users WHERE user_id = %s", (user_id,))
|
||||
cursor.execute("SELECT user_id, username, caption, email, avatar_url, role_id FROM users WHERE user_id = %s", (user_id,))
|
||||
existing_user = cursor.fetchone()
|
||||
if not existing_user:
|
||||
return create_api_response(code="404", message="用户不存在")
|
||||
|
|
@ -78,19 +84,25 @@ def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = D
|
|||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="用户名已存在")
|
||||
|
||||
# Restrict role_id update to admins only
|
||||
target_role_id = existing_user['role_id']
|
||||
if current_user['role_id'] == 1 and request.role_id is not None:
|
||||
target_role_id = request.role_id
|
||||
|
||||
update_data = {
|
||||
'username': request.username if request.username else existing_user['username'],
|
||||
'caption': request.caption if request.caption else existing_user['caption'],
|
||||
'email': request.email if request.email else existing_user['email'],
|
||||
'role_id': request.role_id if request.role_id is not None else existing_user['role_id']
|
||||
'avatar_url': request.avatar_url if request.avatar_url is not None else existing_user.get('avatar_url'),
|
||||
'role_id': target_role_id
|
||||
}
|
||||
|
||||
query = "UPDATE users SET username = %s, caption = %s, email = %s, role_id = %s WHERE user_id = %s"
|
||||
cursor.execute(query, (update_data['username'], update_data['caption'], update_data['email'], update_data['role_id'], user_id))
|
||||
query = "UPDATE users SET username = %s, caption = %s, email = %s, avatar_url = %s, role_id = %s WHERE user_id = %s"
|
||||
cursor.execute(query, (update_data['username'], update_data['caption'], update_data['email'], update_data['avatar_url'], update_data['role_id'], user_id))
|
||||
connection.commit()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT u.user_id, u.username, u.caption, u.email, u.created_at, u.role_id, r.role_name
|
||||
SELECT u.user_id, u.username, u.caption, u.email, u.avatar_url, u.created_at, u.role_id, r.role_name
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.role_id
|
||||
WHERE u.user_id = %s
|
||||
|
|
@ -102,6 +114,7 @@ def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = D
|
|||
username=updated_user['username'],
|
||||
caption=updated_user['caption'],
|
||||
email=updated_user['email'],
|
||||
avatar_url=updated_user['avatar_url'],
|
||||
created_at=updated_user['created_at'],
|
||||
role_id=updated_user['role_id'],
|
||||
role_name=updated_user['role_name'],
|
||||
|
|
@ -184,7 +197,7 @@ def get_all_users(
|
|||
# 主查询
|
||||
query = '''
|
||||
SELECT
|
||||
u.user_id, u.username, u.caption, u.email, u.created_at, u.role_id,
|
||||
u.user_id, u.username, u.caption, u.email, u.avatar_url, u.created_at, u.role_id,
|
||||
r.role_name,
|
||||
(SELECT COUNT(*) FROM meetings WHERE user_id = u.user_id) as meetings_created,
|
||||
(SELECT COUNT(*) FROM attendees WHERE user_id = u.user_id) as meetings_attended
|
||||
|
|
@ -218,7 +231,7 @@ def get_user_info(user_id: int, current_user: dict = Depends(get_current_user)):
|
|||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
user_query = '''
|
||||
SELECT u.user_id, u.username, u.caption, u.email, u.created_at, u.role_id, r.role_name
|
||||
SELECT u.user_id, u.username, u.caption, u.email, u.avatar_url, u.created_at, u.role_id, r.role_name
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.role_id
|
||||
WHERE u.user_id = %s
|
||||
|
|
@ -242,6 +255,7 @@ def get_user_info(user_id: int, current_user: dict = Depends(get_current_user)):
|
|||
username=user['username'],
|
||||
caption=user['caption'],
|
||||
email=user['email'],
|
||||
avatar_url=user['avatar_url'],
|
||||
created_at=user['created_at'],
|
||||
role_id=user['role_id'],
|
||||
role_name=user['role_name'],
|
||||
|
|
@ -272,4 +286,48 @@ def update_password(user_id: int, request: PasswordChangeRequest, current_user:
|
|||
cursor.execute("UPDATE users SET password_hash = %s WHERE user_id = %s", (new_password_hash, user_id))
|
||||
connection.commit()
|
||||
|
||||
return create_api_response(code="200", message="密码修改成功")
|
||||
return create_api_response(code="200", message="密码修改成功")
|
||||
|
||||
@router.post("/users/{user_id}/avatar")
|
||||
def upload_user_avatar(
|
||||
user_id: int,
|
||||
file: UploadFile = File(...),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
# Allow admin or self
|
||||
if current_user['role_id'] != 1 and current_user['user_id'] != user_id:
|
||||
return create_api_response(code="403", message="没有权限上传此用户头像")
|
||||
|
||||
# Validate file type
|
||||
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
|
||||
file_ext = os.path.splitext(file.filename)[1].lower()
|
||||
if file_ext not in ALLOWED_EXTENSIONS:
|
||||
return create_api_response(code="400", message="不支持的文件类型")
|
||||
|
||||
# Ensure upload directory exists: AVATAR_DIR / str(user_id)
|
||||
user_avatar_dir = AVATAR_DIR / str(user_id)
|
||||
if not user_avatar_dir.exists():
|
||||
os.makedirs(user_avatar_dir)
|
||||
|
||||
# Generate unique filename
|
||||
unique_filename = f"{uuid.uuid4()}{file_ext}"
|
||||
file_path = user_avatar_dir / unique_filename
|
||||
|
||||
# Save file
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Generate URL (relative)
|
||||
# AVATAR_DIR is uploads/user/avatar
|
||||
# file path is uploads/user/avatar/{user_id}/{filename}
|
||||
# URL should be /uploads/user/avatar/{user_id}/{filename}
|
||||
avatar_url = f"/uploads/user/avatar/{user_id}/{unique_filename}"
|
||||
|
||||
# Update database
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("UPDATE users SET avatar_url = %s WHERE user_id = %s", (avatar_url, user_id))
|
||||
connection.commit()
|
||||
|
||||
return create_api_response(code="200", message="头像上传成功", data={"avatar_url": avatar_url})
|
||||
|
|
@ -8,8 +8,11 @@ UPLOAD_DIR = BASE_DIR / "uploads"
|
|||
AUDIO_DIR = UPLOAD_DIR / "audio"
|
||||
TEMP_UPLOAD_DIR = UPLOAD_DIR / "temp_audio"
|
||||
MARKDOWN_DIR = UPLOAD_DIR / "markdown"
|
||||
VOICEPRINT_DIR = UPLOAD_DIR / "voiceprint"
|
||||
CLIENT_DIR = UPLOAD_DIR / "clients"
|
||||
EXTERNAL_APPS_DIR = UPLOAD_DIR / "external_apps"
|
||||
USER_DIR = UPLOAD_DIR / "user"
|
||||
VOICEPRINT_DIR = USER_DIR / "voiceprint"
|
||||
AVATAR_DIR = USER_DIR / "avatar"
|
||||
|
||||
# 文件上传配置
|
||||
ALLOWED_EXTENSIONS = {".mp3", ".wav", ".m4a", ".mpeg", ".mp4"}
|
||||
|
|
@ -24,15 +27,19 @@ MAX_CLIENT_SIZE = 500 * 1024 * 1024 # 500MB for client installers
|
|||
UPLOAD_DIR.mkdir(exist_ok=True)
|
||||
AUDIO_DIR.mkdir(exist_ok=True)
|
||||
MARKDOWN_DIR.mkdir(exist_ok=True)
|
||||
VOICEPRINT_DIR.mkdir(exist_ok=True)
|
||||
CLIENT_DIR.mkdir(exist_ok=True)
|
||||
EXTERNAL_APPS_DIR.mkdir(exist_ok=True)
|
||||
USER_DIR.mkdir(exist_ok=True)
|
||||
VOICEPRINT_DIR.mkdir(exist_ok=True)
|
||||
AVATAR_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_CONFIG = {
|
||||
'host': os.getenv('DB_HOST', '10.100.51.51'),
|
||||
'host': os.getenv('DB_HOST', '192.168.4.9'),
|
||||
'user': os.getenv('DB_USER', 'root'),
|
||||
'password': os.getenv('DB_PASSWORD', 'Unis@123'),
|
||||
'database': os.getenv('DB_NAME', 'imeeting_dev'),
|
||||
'password': os.getenv('DB_PASSWORD', 'sagacity'),
|
||||
'database': os.getenv('DB_NAME', 'imeeting'),
|
||||
'port': int(os.getenv('DB_PORT', '3306')),
|
||||
'charset': 'utf8mb4'
|
||||
}
|
||||
|
|
@ -56,10 +63,10 @@ APP_CONFIG = {
|
|||
|
||||
# Redis配置
|
||||
REDIS_CONFIG = {
|
||||
'host': os.getenv('REDIS_HOST', '10.100.51.51'),
|
||||
'host': os.getenv('REDIS_HOST', '192.168.4.9'),
|
||||
'port': int(os.getenv('REDIS_PORT', '6379')),
|
||||
'db': int(os.getenv('REDIS_DB', '0')),
|
||||
'password': os.getenv('REDIS_PASSWORD', 'Unis@123'),
|
||||
'password': os.getenv('REDIS_PASSWORD', ''),
|
||||
'decode_responses': True
|
||||
}
|
||||
|
||||
|
|
@ -72,33 +79,6 @@ TRANSCRIPTION_POLL_CONFIG = {
|
|||
'max_wait_time': int(os.getenv('TRANSCRIPTION_MAX_WAIT_TIME', '1800')), # 最大等待:30分钟
|
||||
}
|
||||
|
||||
# LLM配置 - 阿里Qwen3大模型
|
||||
LLM_CONFIG = {
|
||||
'model_name': os.getenv('LLM_MODEL_NAME', 'qwen-plus'),
|
||||
'time_out': int(os.getenv('LLM_TIMEOUT', '120')),
|
||||
'temperature': float(os.getenv('LLM_TEMPERATURE', '0.7')),
|
||||
'top_p': float(os.getenv('LLM_TOP_P', '0.9')),
|
||||
'system_prompt': """你是一个专业的会议记录分析助手。请根据提供的会议转录内容,生成简洁明了的会议总结。
|
||||
|
||||
总结应该包括以下几个部分:
|
||||
1. 会议概述 - 简要说明会议的主要目的和背景
|
||||
2. 主要讨论点 - 列出会议中讨论的重要话题和内容
|
||||
3. 决策事项 - 明确记录会议中做出的决定和结论
|
||||
4. 待办事项 - 列出需要后续跟进的任务和责任人
|
||||
5. 关键信息 - 其他重要的信息点
|
||||
|
||||
要求:
|
||||
- 保持客观中性,不添加个人观点
|
||||
- 使用简洁的中文表达
|
||||
- 按重要性排序各项内容
|
||||
- 如果某个部分没有相关内容,可以说明"无相关内容"
|
||||
- 总字数控制在500字以内"""
|
||||
}
|
||||
|
||||
# 密码重置配置
|
||||
DEFAULT_RESET_PASSWORD = os.getenv('DEFAULT_RESET_PASSWORD', '111111')
|
||||
|
||||
# 加载系统配置文件
|
||||
# 默认声纹配置
|
||||
VOICEPRINT_CONFIG = {
|
||||
"template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n\n声纹技术能够准确识别每个人独特的声音特征。",
|
||||
|
|
@ -107,5 +87,3 @@ VOICEPRINT_CONFIG = {
|
|||
"channels": 1
|
||||
}
|
||||
|
||||
#首页TimeLine每页数量
|
||||
TIMELINE_PAGESIZE=10
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import uvicorn
|
|||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data, hot_words
|
||||
from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data, hot_words, external_apps
|
||||
from app.core.config import UPLOAD_DIR, API_CONFIG
|
||||
|
||||
app = FastAPI(
|
||||
|
|
@ -46,6 +46,7 @@ app.include_router(tasks.router, prefix="/api", tags=["Tasks"])
|
|||
app.include_router(prompts.router, prefix="/api", tags=["Prompts"])
|
||||
app.include_router(knowledge_base.router, prefix="/api", tags=["KnowledgeBase"])
|
||||
app.include_router(client_downloads.router, prefix="/api", tags=["ClientDownloads"])
|
||||
app.include_router(external_apps.router, prefix="/api", tags=["ExternalApps"])
|
||||
app.include_router(dict_data.router, prefix="/api", tags=["DictData"])
|
||||
app.include_router(voiceprint.router, prefix="/api", tags=["Voiceprint"])
|
||||
app.include_router(audio.router, prefix="/api", tags=["Audio"])
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class LoginResponse(BaseModel):
|
|||
username: str
|
||||
caption: str
|
||||
email: EmailStr
|
||||
avatar_url: Optional[str] = None
|
||||
token: str
|
||||
role_id: int
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ class UserInfo(BaseModel):
|
|||
username: str
|
||||
caption: str
|
||||
email: EmailStr
|
||||
avatar_url: Optional[str] = None
|
||||
created_at: datetime.datetime
|
||||
meetings_created: int
|
||||
meetings_attended: int
|
||||
|
|
@ -38,12 +40,14 @@ class CreateUserRequest(BaseModel):
|
|||
password: Optional[str] = None
|
||||
caption: str
|
||||
email: EmailStr
|
||||
avatar_url: Optional[str] = None
|
||||
role_id: int
|
||||
|
||||
class UpdateUserRequest(BaseModel):
|
||||
username: Optional[str] = None
|
||||
caption: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
role_id: Optional[int] = None
|
||||
|
||||
class UserLog(BaseModel):
|
||||
|
|
|
|||
|
|
@ -13,14 +13,24 @@ def parse_apk_with_androguard(apk_path):
|
|||
from pyaxmlparser import APK
|
||||
|
||||
apk = APK(apk_path)
|
||||
|
||||
# 提取所有需要的信息
|
||||
package_name = apk.package
|
||||
version_code = apk.version_code
|
||||
version_name = apk.version_name
|
||||
app_name = apk.application
|
||||
min_sdk_version = apk.get_min_sdk_version()
|
||||
target_sdk_version = apk.get_target_sdk_version()
|
||||
|
||||
print(f"APK解析成功: version_code={version_code}, version_name={version_name}")
|
||||
print(f"APK解析成功: package={package_name}, version_code={version_code}, version_name={version_name}, app_name={app_name}")
|
||||
|
||||
return {
|
||||
'package_name': package_name,
|
||||
'version_code': int(version_code) if version_code else None,
|
||||
'version_name': version_name
|
||||
'version_name': version_name,
|
||||
'app_name': app_name,
|
||||
'min_sdk_version': min_sdk_version,
|
||||
'target_sdk_version': target_sdk_version
|
||||
}
|
||||
except ImportError:
|
||||
print("错误: pyaxmlparser 未安装")
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,30 @@
|
|||
-- Migration: Add avatar_url and update menu for Account Settings
|
||||
-- Created at: 2026-01-15
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. Add avatar_url to users table if it doesn't exist
|
||||
-- Note: MySQL 5.7 doesn't support IF NOT EXISTS for columns easily in one line without procedure,
|
||||
-- but for this environment we assume it doesn't exist or ignore error if strictly handled.
|
||||
-- However, creating a safe idempotent script is better.
|
||||
-- Since I can't run complex procedures easily here, I'll just run the ALTER.
|
||||
-- If it fails, it fails (user can ignore if already applied).
|
||||
ALTER TABLE `users` ADD COLUMN `avatar_url` VARCHAR(512) DEFAULT NULL AFTER `email`;
|
||||
|
||||
-- 2. Remove 'change_password' menu
|
||||
DELETE FROM `role_menu_permissions` WHERE `menu_id` IN (SELECT `menu_id` FROM `menus` WHERE `menu_code` = 'change_password');
|
||||
DELETE FROM `menus` WHERE `menu_code` = 'change_password';
|
||||
|
||||
-- 3. Add 'account_settings' menu
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `sort_order`, `is_active`, `description`)
|
||||
VALUES ('account_settings', '账户设置', 'UserCog', '/account-settings', 'link', 1, 1, '管理个人账户信息');
|
||||
|
||||
-- 4. Grant permissions
|
||||
-- Grant to Admin (role_id=1) and User (role_id=2)
|
||||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`)
|
||||
SELECT 1, menu_id FROM `menus` WHERE `menu_code` = 'account_settings';
|
||||
|
||||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`)
|
||||
SELECT 2, menu_id FROM `menus` WHERE `menu_code` = 'account_settings';
|
||||
|
||||
COMMIT;
|
||||
Loading…
Reference in New Issue