增加了外部应用管理

main
mula.liu 2026-01-16 10:07:41 +08:00
parent f616cb3fc3
commit 9fad51851d
9 changed files with 651 additions and 52 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -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)}"
)

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, UploadFile, File
from typing import Optional from typing import Optional
from app.models.models import UserInfo, PasswordChangeRequest, UserListResponse, CreateUserRequest, UpdateUserRequest, RoleInfo from app.models.models import UserInfo, PasswordChangeRequest, UserListResponse, CreateUserRequest, UpdateUserRequest, RoleInfo
from app.core.database import get_db_connection 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.core.response import create_api_response
from app.services.system_config_service import SystemConfigService from app.services.system_config_service import SystemConfigService
import app.core.config as config_module import app.core.config as config_module
from app.core.config import UPLOAD_DIR, AVATAR_DIR
import hashlib import hashlib
import datetime import datetime
import re import re
import os
import shutil
import uuid
from pathlib import Path
router = APIRouter() 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() password = request.password if request.password else SystemConfigService.get_default_reset_password()
hashed_password = hash_password(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() 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() connection.commit()
return create_api_response(code="200", message="用户创建成功") return create_api_response(code="200", message="用户创建成功")
@router.put("/users/{user_id}") @router.put("/users/{user_id}")
def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = Depends(get_current_user)): def update_user(user_id: int, request: UpdateUserRequest, current_user: dict = Depends(get_current_user)):
if current_user['role_id'] != 1: # 1 is admin # Allow admin (role_id=1) or self
return create_api_response(code="403", message="仅管理员有权限修改用户信息") 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): if request.email and not validate_email(request.email):
return create_api_response(code="400", message="邮箱格式不正确") 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: with get_db_connection() as connection:
cursor = connection.cursor(dictionary=True) 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() existing_user = cursor.fetchone()
if not existing_user: if not existing_user:
return create_api_response(code="404", message="用户不存在") 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(): if cursor.fetchone():
return create_api_response(code="400", message="用户名已存在") 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 = { update_data = {
'username': request.username if request.username else existing_user['username'], 'username': request.username if request.username else existing_user['username'],
'caption': request.caption if request.caption else existing_user['caption'], 'caption': request.caption if request.caption else existing_user['caption'],
'email': request.email if request.email else existing_user['email'], '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" 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['role_id'], user_id)) cursor.execute(query, (update_data['username'], update_data['caption'], update_data['email'], update_data['avatar_url'], update_data['role_id'], user_id))
connection.commit() connection.commit()
cursor.execute(''' 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 FROM users u
LEFT JOIN roles r ON u.role_id = r.role_id LEFT JOIN roles r ON u.role_id = r.role_id
WHERE u.user_id = %s 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'], username=updated_user['username'],
caption=updated_user['caption'], caption=updated_user['caption'],
email=updated_user['email'], email=updated_user['email'],
avatar_url=updated_user['avatar_url'],
created_at=updated_user['created_at'], created_at=updated_user['created_at'],
role_id=updated_user['role_id'], role_id=updated_user['role_id'],
role_name=updated_user['role_name'], role_name=updated_user['role_name'],
@ -184,7 +197,7 @@ def get_all_users(
# 主查询 # 主查询
query = ''' query = '''
SELECT 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, r.role_name,
(SELECT COUNT(*) FROM meetings WHERE user_id = u.user_id) as meetings_created, (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 (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) cursor = connection.cursor(dictionary=True)
user_query = ''' 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 FROM users u
LEFT JOIN roles r ON u.role_id = r.role_id LEFT JOIN roles r ON u.role_id = r.role_id
WHERE u.user_id = %s 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'], username=user['username'],
caption=user['caption'], caption=user['caption'],
email=user['email'], email=user['email'],
avatar_url=user['avatar_url'],
created_at=user['created_at'], created_at=user['created_at'],
role_id=user['role_id'], role_id=user['role_id'],
role_name=user['role_name'], 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)) cursor.execute("UPDATE users SET password_hash = %s WHERE user_id = %s", (new_password_hash, user_id))
connection.commit() 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})

View File

@ -8,8 +8,11 @@ UPLOAD_DIR = BASE_DIR / "uploads"
AUDIO_DIR = UPLOAD_DIR / "audio" AUDIO_DIR = UPLOAD_DIR / "audio"
TEMP_UPLOAD_DIR = UPLOAD_DIR / "temp_audio" TEMP_UPLOAD_DIR = UPLOAD_DIR / "temp_audio"
MARKDOWN_DIR = UPLOAD_DIR / "markdown" MARKDOWN_DIR = UPLOAD_DIR / "markdown"
VOICEPRINT_DIR = UPLOAD_DIR / "voiceprint"
CLIENT_DIR = UPLOAD_DIR / "clients" 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"} 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) UPLOAD_DIR.mkdir(exist_ok=True)
AUDIO_DIR.mkdir(exist_ok=True) AUDIO_DIR.mkdir(exist_ok=True)
MARKDOWN_DIR.mkdir(exist_ok=True) MARKDOWN_DIR.mkdir(exist_ok=True)
VOICEPRINT_DIR.mkdir(exist_ok=True)
CLIENT_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 = { 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'), 'user': os.getenv('DB_USER', 'root'),
'password': os.getenv('DB_PASSWORD', 'Unis@123'), 'password': os.getenv('DB_PASSWORD', 'sagacity'),
'database': os.getenv('DB_NAME', 'imeeting_dev'), 'database': os.getenv('DB_NAME', 'imeeting'),
'port': int(os.getenv('DB_PORT', '3306')), 'port': int(os.getenv('DB_PORT', '3306')),
'charset': 'utf8mb4' 'charset': 'utf8mb4'
} }
@ -56,10 +63,10 @@ APP_CONFIG = {
# Redis配置 # Redis配置
REDIS_CONFIG = { 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')), 'port': int(os.getenv('REDIS_PORT', '6379')),
'db': int(os.getenv('REDIS_DB', '0')), 'db': int(os.getenv('REDIS_DB', '0')),
'password': os.getenv('REDIS_PASSWORD', 'Unis@123'), 'password': os.getenv('REDIS_PASSWORD', ''),
'decode_responses': True 'decode_responses': True
} }
@ -72,33 +79,6 @@ TRANSCRIPTION_POLL_CONFIG = {
'max_wait_time': int(os.getenv('TRANSCRIPTION_MAX_WAIT_TIME', '1800')), # 最大等待30分钟 '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 = { VOICEPRINT_CONFIG = {
"template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n\n声纹技术能够准确识别每个人独特的声音特征。", "template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n\n声纹技术能够准确识别每个人独特的声音特征。",
@ -107,5 +87,3 @@ VOICEPRINT_CONFIG = {
"channels": 1 "channels": 1
} }
#首页TimeLine每页数量
TIMELINE_PAGESIZE=10

View File

@ -13,7 +13,7 @@ import uvicorn
from fastapi import FastAPI, Request, HTTPException from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles 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 from app.core.config import UPLOAD_DIR, API_CONFIG
app = FastAPI( 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(prompts.router, prefix="/api", tags=["Prompts"])
app.include_router(knowledge_base.router, prefix="/api", tags=["KnowledgeBase"]) app.include_router(knowledge_base.router, prefix="/api", tags=["KnowledgeBase"])
app.include_router(client_downloads.router, prefix="/api", tags=["ClientDownloads"]) 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(dict_data.router, prefix="/api", tags=["DictData"])
app.include_router(voiceprint.router, prefix="/api", tags=["Voiceprint"]) app.include_router(voiceprint.router, prefix="/api", tags=["Voiceprint"])
app.include_router(audio.router, prefix="/api", tags=["Audio"]) app.include_router(audio.router, prefix="/api", tags=["Audio"])

View File

@ -11,6 +11,7 @@ class LoginResponse(BaseModel):
username: str username: str
caption: str caption: str
email: EmailStr email: EmailStr
avatar_url: Optional[str] = None
token: str token: str
role_id: int role_id: int
@ -23,6 +24,7 @@ class UserInfo(BaseModel):
username: str username: str
caption: str caption: str
email: EmailStr email: EmailStr
avatar_url: Optional[str] = None
created_at: datetime.datetime created_at: datetime.datetime
meetings_created: int meetings_created: int
meetings_attended: int meetings_attended: int
@ -38,12 +40,14 @@ class CreateUserRequest(BaseModel):
password: Optional[str] = None password: Optional[str] = None
caption: str caption: str
email: EmailStr email: EmailStr
avatar_url: Optional[str] = None
role_id: int role_id: int
class UpdateUserRequest(BaseModel): class UpdateUserRequest(BaseModel):
username: Optional[str] = None username: Optional[str] = None
caption: Optional[str] = None caption: Optional[str] = None
email: Optional[str] = None email: Optional[str] = None
avatar_url: Optional[str] = None
role_id: Optional[int] = None role_id: Optional[int] = None
class UserLog(BaseModel): class UserLog(BaseModel):

View File

@ -13,14 +13,24 @@ def parse_apk_with_androguard(apk_path):
from pyaxmlparser import APK from pyaxmlparser import APK
apk = APK(apk_path) apk = APK(apk_path)
# 提取所有需要的信息
package_name = apk.package
version_code = apk.version_code version_code = apk.version_code
version_name = apk.version_name 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 { return {
'package_name': package_name,
'version_code': int(version_code) if version_code else None, '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: except ImportError:
print("错误: pyaxmlparser 未安装") print("错误: pyaxmlparser 未安装")

BIN
sql/.DS_Store vendored

Binary file not shown.

View File

@ -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;