From 9fad51851d6b886cef742e5db75197af3d969680 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Fri, 16 Jan 2026 10:07:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 10244 -> 10244 bytes app/api/endpoints/external_apps.py | 518 +++++++++++++++++++++ app/api/endpoints/users.py | 84 +++- app/core/config.py | 50 +- app/main.py | 3 +- app/models/models.py | 4 + app/utils/apk_parser.py | 14 +- sql/.DS_Store | Bin 6148 -> 6148 bytes sql/migrations/update_account_settings.sql | 30 ++ 9 files changed, 651 insertions(+), 52 deletions(-) create mode 100644 app/api/endpoints/external_apps.py create mode 100644 sql/migrations/update_account_settings.sql diff --git a/.DS_Store b/.DS_Store index 7ba88596e12e4de63543fb128740758ae77c4368..d10040d7111d3bda45d0afe2916d7962e623daa2 100644 GIT binary patch delta 92 zcmZn(XbIS`M?l2L$WTYY(9)z-QLoKxf^2LeW`;Tn#)g))IttZ>Mh0d&3T6hzlP?MwZw?mU$&XWZ Ia 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)}" + ) diff --git a/app/api/endpoints/users.py b/app/api/endpoints/users.py index 324a173..dd4f7e1 100644 --- a/app/api/endpoints/users.py +++ b/app/api/endpoints/users.py @@ -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="密码修改成功") \ No newline at end of file + 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}) \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index 3ae835c..f7398b3 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -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 diff --git a/app/main.py b/app/main.py index 7635c80..57e7f68 100644 --- a/app/main.py +++ b/app/main.py @@ -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"]) diff --git a/app/models/models.py b/app/models/models.py index b0a61e2..647f8e7 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -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): diff --git a/app/utils/apk_parser.py b/app/utils/apk_parser.py index dc1e157..d13faf7 100644 --- a/app/utils/apk_parser.py +++ b/app/utils/apk_parser.py @@ -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 未安装") diff --git a/sql/.DS_Store b/sql/.DS_Store index 2e46cacab7a5a13436fcb2f51b359c70b88ac912..b9606ee8485e9500cbceb76534bf6515f2236b4b 100644 GIT binary patch delta 18 acmZoMXfc?uX5+>%_K6Lwo7p-3@&f=#ng>V# delta 20 ccmZoMXfc?uhLLgO#xVAY4J@14IsWnk08aS_NdN!< diff --git a/sql/migrations/update_account_settings.sql b/sql/migrations/update_account_settings.sql new file mode 100644 index 0000000..eae974d --- /dev/null +++ b/sql/migrations/update_account_settings.sql @@ -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;