diff --git a/.DS_Store b/.DS_Store index 55b6e62..c8ee9a3 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app.zip b/app.zip deleted file mode 100644 index 477099a..0000000 Binary files a/app.zip and /dev/null differ diff --git a/app/api/endpoints/client_downloads.py b/app/api/endpoints/client_downloads.py index 1aaca1e..fc236fb 100644 --- a/app/api/endpoints/client_downloads.py +++ b/app/api/endpoints/client_downloads.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form from app.models.models import ( ClientDownload, CreateClientDownloadRequest, @@ -8,20 +8,30 @@ from app.models.models import ( from app.core.database import get_db_connection from app.core.auth import get_current_user, get_current_admin_user from app.core.response import create_api_response +from app.core.config import CLIENT_DIR, ALLOWED_CLIENT_EXTENSIONS, MAX_CLIENT_SIZE, APP_CONFIG +from app.utils.apk_parser import parse_apk_with_androguard from typing import Optional +from pathlib import Path +import os +import shutil router = APIRouter() @router.get("/clients", response_model=dict) async def get_client_downloads( - platform_type: Optional[str] = None, - platform_name: Optional[str] = None, + platform_code: Optional[str] = None, is_active: Optional[bool] = None, page: int = 1, size: int = 50 ): """ - 获取客户端下载列表(公开接口,所有用户可访问) + 获取客户端下载列表(管理后台接口) + + 参数: + platform_code: 平台编码(如 WIN, MAC, ANDROID等) + is_active: 是否启用 + page: 页码 + size: 每页数量 """ try: with get_db_connection() as conn: @@ -31,13 +41,9 @@ async def get_client_downloads( where_clauses = [] params = [] - if platform_type: - where_clauses.append("platform_type = %s") - params.append(platform_type) - - if platform_name: - where_clauses.append("platform_name = %s") - params.append(platform_name) + if platform_code: + where_clauses.append("platform_code = %s") + params.append(platform_code) if is_active is not None: where_clauses.append("is_active = %s") @@ -50,12 +56,12 @@ async def get_client_downloads( cursor.execute(count_query, params) total = cursor.fetchone()['total'] - # 获取列表数据 + # 获取列表数据 - 按 platform_code 和版本号排序 offset = (page - 1) * size list_query = f""" SELECT * FROM client_downloads WHERE {where_clause} - ORDER BY platform_type, platform_name, version_code DESC + ORDER BY platform_code, version_code DESC LIMIT %s OFFSET %s """ cursor.execute(list_query, params + [size, offset]) @@ -85,31 +91,48 @@ async def get_client_downloads( async def get_latest_clients(): """ 获取所有平台的最新版本客户端(公开接口,用于首页下载) + + 返回按平台类型分组的最新客户端,包含平台的中英文名称 """ try: with get_db_connection() as conn: cursor = conn.cursor(dictionary=True) + # 关联 dict_data 获取平台信息 query = """ - SELECT * FROM client_downloads - WHERE is_active = TRUE AND is_latest = TRUE - ORDER BY platform_type, platform_name + SELECT cd.*, dd.label_cn, dd.label_en, dd.parent_code, dd.extension_attr + FROM client_downloads cd + LEFT JOIN dict_data dd ON cd.platform_code = dd.dict_code + AND dd.dict_type = 'client_platform' + WHERE cd.is_active = TRUE AND cd.is_latest = TRUE + ORDER BY dd.parent_code, dd.sort_order, cd.platform_code """ cursor.execute(query) clients = cursor.fetchall() + + # 处理 JSON 字段 + for client in clients: + if client.get('extension_attr'): + try: + import json + client['extension_attr'] = json.loads(client['extension_attr']) + except: + client['extension_attr'] = {} + cursor.close() - # 按平台类型分组 + # 按 parent_code 分组 mobile_clients = [] desktop_clients = [] terminal_clients = [] for client in clients: - if client['platform_type'] == 'mobile': + parent_code = client.get('parent_code', '').upper() + if parent_code == 'MOBILE': mobile_clients.append(client) - elif client['platform_type'] == 'desktop': + elif parent_code == 'DESKTOP': desktop_clients.append(client) - elif client['platform_type'] == 'terminal': + elif parent_code == 'TERMINAL': terminal_clients.append(client) return create_api_response( @@ -130,39 +153,75 @@ async def get_latest_clients(): @router.get("/clients/latest/by-platform", response_model=dict) -async def get_latest_version_by_platform_type_and_name( - platform_type: str, - platform_name: str +async def get_latest_version_by_code( + platform_type: Optional[str] = None, + platform_name: Optional[str] = None, + platform_code: Optional[str] = None ): """ - 通过平台类型和平台名称获取最新版本(公开接口,用于客户端版本检查) + 获取最新版本客户端(公开接口,用于客户端版本检查) + + 支持两种调用方式: + 1. 旧版方式:传 platform_type 和 platform_name(兼容已发布的终端) + 2. 新版方式:传 platform_code(推荐使用) 参数: - platform_type: 平台类型 (mobile, desktop, terminal) - platform_name: 具体平台 (ios, android, windows, mac_intel, mac_m, linux, mcu) + platform_type: 平台类型 (mobile, desktop, terminal) - 旧版参数 + platform_name: 具体平台 (ios, android, windows等) - 旧版参数 + platform_code: 平台编码 (WIN, MAC, LINUX, IOS, ANDROID等) - 新版参数 """ try: with get_db_connection() as conn: cursor = conn.cursor(dictionary=True) - query = """ - SELECT * FROM client_downloads - WHERE platform_type = %s - AND platform_name = %s - AND is_active = TRUE - AND is_latest = TRUE - LIMIT 1 - """ - cursor.execute(query, (platform_type, platform_name)) - client = cursor.fetchone() - cursor.close() + # 优先使用 platform_code(新版) + if platform_code: + query = """ + SELECT * FROM client_downloads + WHERE platform_code = %s + AND is_active = TRUE + AND is_latest = TRUE + LIMIT 1 + """ + cursor.execute(query, (platform_code,)) + client = cursor.fetchone() - if not client: + if not client: + cursor.close() + return create_api_response( + code="404", + message=f"未找到平台编码 {platform_code} 的客户端" + ) + + # 使用 platform_type 和 platform_name(旧版,兼容) + elif platform_type and platform_name: + query = """ + SELECT * FROM client_downloads + WHERE platform_type = %s + AND platform_name = %s + AND is_active = TRUE + AND is_latest = TRUE + LIMIT 1 + """ + cursor.execute(query, (platform_type, platform_name)) + client = cursor.fetchone() + + if not client: + cursor.close() + return create_api_response( + code="404", + message=f"未找到平台类型 {platform_type} 下的 {platform_name} 客户端" + ) + + else: + cursor.close() return create_api_response( - code="404", - message=f"未找到平台类型 {platform_type} 下的 {platform_name} 客户端" + code="400", + message="请提供 platform_code 参数" ) + cursor.close() + return create_api_response( code="200", message="获取成功", @@ -216,6 +275,8 @@ async def create_client_download( ): """ 创建新的客户端版本(仅管理员) + + 注意: platform_type 和 platform_name 为兼容字段,可不传 """ try: with get_db_connection() as conn: @@ -226,21 +287,23 @@ async def create_client_download( update_query = """ UPDATE client_downloads SET is_latest = FALSE - WHERE platform_name = %s + WHERE platform_code = %s """ - cursor.execute(update_query, (request.platform_name,)) + cursor.execute(update_query, (request.platform_code,)) - # 插入新版本 + # 插入新版本 - platform_type 和 platform_name 允许为 NULL insert_query = """ INSERT INTO client_downloads ( - platform_type, platform_name, version, version_code, - download_url, file_size, release_notes, is_active, - is_latest, min_system_version, created_by - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + platform_type, platform_name, platform_code, + version, version_code, download_url, file_size, + release_notes, is_active, is_latest, min_system_version, + created_by + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ cursor.execute(insert_query, ( - request.platform_type, - request.platform_name, + request.platform_type, # 可为 None + request.platform_name, # 可为 None + request.platform_code, # 必填 request.version, request.version_code, request.download_url, @@ -294,17 +357,31 @@ async def update_client_download( # 如果设置为最新版本,先将同平台的其他版本设为非最新 if request.is_latest: + # 使用 platform_code (如果有更新) 或现有的 platform_code + platform_code = request.platform_code if request.platform_code else existing['platform_code'] update_query = """ UPDATE client_downloads SET is_latest = FALSE - WHERE platform_name = %s AND id != %s + WHERE platform_code = %s AND id != %s """ - cursor.execute(update_query, (existing['platform_name'], id)) + cursor.execute(update_query, (platform_code, id)) # 构建更新语句 update_fields = [] params = [] + if request.platform_type is not None: + update_fields.append("platform_type = %s") + params.append(request.platform_type) + + if request.platform_name is not None: + update_fields.append("platform_name = %s") + params.append(request.platform_name) + + if request.platform_code is not None: + update_fields.append("platform_code = %s") + params.append(request.platform_code) + if request.version is not None: update_fields.append("version = %s") params.append(request.version) @@ -403,3 +480,86 @@ async def delete_client_download( code="500", message=f"删除客户端版本失败: {str(e)}" ) + + +@router.post("/clients/upload", response_model=dict) +async def upload_client_installer( + platform_code: str = Form(...), + file: UploadFile = File(...), + current_user: dict = Depends(get_current_admin_user) +): + """ + 上传客户端安装包(仅管理员) + + 参数: + platform_code: 平台编码(如 WIN, MAC, ANDROID等) + file: 安装包文件 + + 返回: + 文件信息,包括文件大小、下载URL,以及APK的版本信息(如果是APK) + """ + try: + # 验证文件扩展名 + file_ext = Path(file.filename).suffix.lower() + if file_ext not in ALLOWED_CLIENT_EXTENSIONS: + return create_api_response( + code="400", + message=f"不支持的文件类型: {file_ext}。支持的类型: {', '.join(ALLOWED_CLIENT_EXTENSIONS)}" + ) + + # 创建平台目录 + platform_dir = CLIENT_DIR / platform_code + platform_dir.mkdir(parents=True, exist_ok=True) + + # 生成文件名(保留原始文件名) + file_path = platform_dir / file.filename + + # 检查文件大小 + file.file.seek(0, 2) # 移动到文件末尾 + file_size = file.file.tell() # 获取文件大小 + file.file.seek(0) # 移回文件开头 + + if file_size > MAX_CLIENT_SIZE: + return create_api_response( + code="400", + message=f"文件过大,最大允许 {MAX_CLIENT_SIZE / 1024 / 1024} MB" + ) + + # 保存文件 + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + # 构建下载URL + base_url = APP_CONFIG['base_url'].rstrip('/') + download_url = f"{base_url}/uploads/clients/{platform_code}/{file.filename}" + + # 准备返回数据 + result = { + "file_name": file.filename, + "file_size": file_size, + "download_url": download_url, + "platform_code": platform_code + } + + # 如果是APK文件,尝试解析版本信息 + if file_ext == '.apk': + apk_info = parse_apk_with_androguard(str(file_path)) + if apk_info: + result['version_code'] = apk_info.get('version_code') + result['version_name'] = apk_info.get('version_name') + result['note'] = apk_info.get('note', '') + else: + # APK解析失败,给出提示 + result['note'] = 'APK解析失败,请检查后台日志。您可以手动输入版本信息。' + + return create_api_response( + code="200", + message="文件上传成功", + data=result + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"文件上传失败: {str(e)}" + ) diff --git a/app/api/endpoints/dict_data.py b/app/api/endpoints/dict_data.py new file mode 100644 index 0000000..b5b2114 --- /dev/null +++ b/app/api/endpoints/dict_data.py @@ -0,0 +1,413 @@ +from fastapi import APIRouter, HTTPException, Depends +from app.core.database import get_db_connection +from app.core.auth import get_current_user, get_current_admin_user +from app.core.response import create_api_response +from pydantic import BaseModel +from typing import Optional, List +import json + +router = APIRouter() + + +class DictDataItem(BaseModel): + """码表数据项""" + id: int + dict_type: str + dict_code: str + parent_code: str + tree_path: Optional[str] = None + label_cn: str + label_en: Optional[str] = None + sort_order: int + extension_attr: Optional[dict] = None + is_default: int + status: int + create_time: str + + +class CreateDictDataRequest(BaseModel): + """创建码表数据请求""" + dict_type: str = "client_platform" + dict_code: str + parent_code: str = "ROOT" + label_cn: str + label_en: Optional[str] = None + sort_order: int = 0 + extension_attr: Optional[dict] = None + is_default: int = 0 + status: int = 1 + + +class UpdateDictDataRequest(BaseModel): + """更新码表数据请求""" + parent_code: Optional[str] = None + label_cn: Optional[str] = None + label_en: Optional[str] = None + sort_order: Optional[int] = None + extension_attr: Optional[dict] = None + is_default: Optional[int] = None + status: Optional[int] = None + + +@router.get("/dict/types", response_model=dict) +async def get_dict_types(): + """ + 获取所有字典类型(公开接口) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + query = """ + SELECT DISTINCT dict_type + FROM dict_data + WHERE status = 1 + ORDER BY dict_type + """ + cursor.execute(query) + types = cursor.fetchall() + cursor.close() + + return create_api_response( + code="200", + message="获取成功", + data={"types": [t['dict_type'] for t in types]} + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"获取字典类型失败: {str(e)}" + ) + + +@router.get("/dict/{dict_type}", response_model=dict) +async def get_dict_data_by_type(dict_type: str): + """ + 获取指定类型的所有码表数据(公开接口) + 支持树形结构 + + 参数: + dict_type: 字典类型,如 'client_platform' + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + query = """ + SELECT id, dict_type, dict_code, parent_code, tree_path, + label_cn, label_en, sort_order, extension_attr, + is_default, status, create_time + FROM dict_data + WHERE dict_type = %s AND status = 1 + ORDER BY parent_code, sort_order, dict_code + """ + cursor.execute(query, (dict_type,)) + items = cursor.fetchall() + cursor.close() + + # 处理JSON字段 + for item in items: + if item.get('extension_attr'): + try: + item['extension_attr'] = json.loads(item['extension_attr']) + except: + item['extension_attr'] = {} + + # 构建树形结构 + tree_data = [] + nodes_map = {} + + # 第一遍:创建所有节点 + for item in items: + nodes_map[item['dict_code']] = { + **item, + 'children': [] + } + + # 第二遍:构建树形关系 + for item in items: + node = nodes_map[item['dict_code']] + parent_code = item['parent_code'] + + if parent_code == 'ROOT': + tree_data.append(node) + elif parent_code in nodes_map: + nodes_map[parent_code]['children'].append(node) + + return create_api_response( + code="200", + message="获取成功", + data={ + "items": items, # 平铺数据 + "tree": tree_data # 树形数据 + } + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"获取码表数据失败: {str(e)}" + ) + + +@router.get("/dict/{dict_type}/{dict_code}", response_model=dict) +async def get_dict_data_by_code(dict_type: str, dict_code: str): + """ + 获取指定编码的码表数据(公开接口) + + 参数: + dict_type: 字典类型 + dict_code: 字典编码 + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + query = """ + SELECT id, dict_type, dict_code, parent_code, tree_path, + label_cn, label_en, sort_order, extension_attr, + is_default, status, create_time + FROM dict_data + WHERE dict_type = %s AND dict_code = %s + LIMIT 1 + """ + cursor.execute(query, (dict_type, dict_code)) + item = cursor.fetchone() + cursor.close() + + if not item: + return create_api_response( + code="404", + message=f"未找到编码 {dict_code} 的数据" + ) + + # 处理JSON字段 + if item.get('extension_attr'): + try: + item['extension_attr'] = json.loads(item['extension_attr']) + except: + item['extension_attr'] = {} + + return create_api_response( + code="200", + message="获取成功", + data=item + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"获取码表数据失败: {str(e)}" + ) + + +@router.post("/dict", response_model=dict) +async def create_dict_data( + request: CreateDictDataRequest, + current_user: dict = Depends(get_current_admin_user) +): + """ + 创建码表数据(仅管理员) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor() + + # 检查是否已存在 + cursor.execute( + "SELECT id FROM dict_data WHERE dict_type = %s AND dict_code = %s", + (request.dict_type, request.dict_code) + ) + if cursor.fetchone(): + cursor.close() + return create_api_response( + code="400", + message=f"编码 {request.dict_code} 已存在" + ) + + # 插入数据 + query = """ + INSERT INTO dict_data ( + dict_type, dict_code, parent_code, label_cn, label_en, + sort_order, extension_attr, is_default, status + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + """ + extension_json = json.dumps(request.extension_attr) if request.extension_attr else None + + cursor.execute(query, ( + request.dict_type, + request.dict_code, + request.parent_code, + request.label_cn, + request.label_en, + request.sort_order, + extension_json, + request.is_default, + request.status + )) + + new_id = cursor.lastrowid + conn.commit() + cursor.close() + + return create_api_response( + code="200", + message="创建成功", + data={"id": new_id} + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"创建码表数据失败: {str(e)}" + ) + + +@router.put("/dict/{id}", response_model=dict) +async def update_dict_data( + id: int, + request: UpdateDictDataRequest, + current_user: dict = Depends(get_current_admin_user) +): + """ + 更新码表数据(仅管理员) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + # 检查是否存在 + cursor.execute("SELECT * FROM dict_data WHERE id = %s", (id,)) + existing = cursor.fetchone() + if not existing: + cursor.close() + return create_api_response( + code="404", + message="码表数据不存在" + ) + + # 构建更新语句 + update_fields = [] + params = [] + + if request.parent_code is not None: + update_fields.append("parent_code = %s") + params.append(request.parent_code) + + if request.label_cn is not None: + update_fields.append("label_cn = %s") + params.append(request.label_cn) + + if request.label_en is not None: + update_fields.append("label_en = %s") + params.append(request.label_en) + + if request.sort_order is not None: + update_fields.append("sort_order = %s") + params.append(request.sort_order) + + if request.extension_attr is not None: + update_fields.append("extension_attr = %s") + params.append(json.dumps(request.extension_attr)) + + if request.is_default is not None: + update_fields.append("is_default = %s") + params.append(request.is_default) + + if request.status is not None: + update_fields.append("status = %s") + params.append(request.status) + + if not update_fields: + cursor.close() + return create_api_response( + code="400", + message="没有要更新的字段" + ) + + # 执行更新 + update_query = f""" + UPDATE dict_data + SET {', '.join(update_fields)} + WHERE id = %s + """ + params.append(id) + cursor.execute(update_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("/dict/{id}", response_model=dict) +async def delete_dict_data( + id: int, + current_user: dict = Depends(get_current_admin_user) +): + """ + 删除码表数据(仅管理员) + 注意:如果有子节点或被引用,应该拒绝删除 + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + # 检查是否存在 + cursor.execute("SELECT dict_code FROM dict_data WHERE id = %s", (id,)) + existing = cursor.fetchone() + if not existing: + cursor.close() + return create_api_response( + code="404", + message="码表数据不存在" + ) + + # 检查是否有子节点 + cursor.execute( + "SELECT COUNT(*) as count FROM dict_data WHERE parent_code = %s", + (existing['dict_code'],) + ) + if cursor.fetchone()['count'] > 0: + cursor.close() + return create_api_response( + code="400", + message="该节点存在子节点,无法删除" + ) + + # 检查是否被client_downloads引用 + cursor.execute( + "SELECT COUNT(*) as count FROM client_downloads WHERE platform_code = %s", + (existing['dict_code'],) + ) + if cursor.fetchone()['count'] > 0: + cursor.close() + return create_api_response( + code="400", + message="该平台编码已被客户端下载记录引用,无法删除" + ) + + # 执行删除 + cursor.execute("DELETE FROM dict_data WHERE id = %s", (id,)) + 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)}" + ) diff --git a/app/api/endpoints/meetings.py b/app/api/endpoints/meetings.py index aa350ab..fdaa93d 100644 --- a/app/api/endpoints/meetings.py +++ b/app/api/endpoints/meetings.py @@ -113,8 +113,8 @@ def get_meetings( # 构建基础查询 base_query = ''' - SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags, - m.user_id as creator_id, u.caption as creator_username, af.file_path as audio_file_path + SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags, m.access_password, + m.user_id as creator_id, u.caption as creator_username, MAX(af.file_path) as audio_file_path FROM meetings m JOIN users u ON m.user_id = u.user_id LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id @@ -139,9 +139,8 @@ def get_meetings( cursor.execute(count_query, params) total = cursor.fetchone()['total'] - # 添加GROUP BY(如果联表了attendees) - if has_attendees_join: - base_query += " GROUP BY m.meeting_id" + # 添加GROUP BY(因为使用了MAX聚合函数,总是需要GROUP BY) + base_query += " GROUP BY m.meeting_id" # 计算分页 total_pages = (total + page_size - 1) // page_size diff --git a/app/core/config.py b/app/core/config.py index e906840..39da6b3 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -9,25 +9,29 @@ 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" # 文件上传配置 ALLOWED_EXTENSIONS = {".mp3", ".wav", ".m4a", ".mpeg", ".mp4"} ALLOWED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} ALLOWED_VOICEPRINT_EXTENSIONS = {".wav"} +ALLOWED_CLIENT_EXTENSIONS = {".apk", ".exe", ".dmg", ".deb", ".rpm", ".pkg", ".msi", ".zip", ".tar.gz"} MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10MB +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) # 数据库配置 DATABASE_CONFIG = { - 'host': os.getenv('DB_HOST', '10.100.51.161'), + 'host': os.getenv('DB_HOST', '10.100.51.51'), 'user': os.getenv('DB_USER', 'root'), - 'password': os.getenv('DB_PASSWORD', 'sagacity'), + 'password': os.getenv('DB_PASSWORD', 'Unis@123'), 'database': os.getenv('DB_NAME', 'imeeting_dev'), 'port': int(os.getenv('DB_PORT', '3306')), 'charset': 'utf8mb4' @@ -52,10 +56,10 @@ APP_CONFIG = { # Redis配置 REDIS_CONFIG = { - 'host': os.getenv('REDIS_HOST', '10.100.51.161'), + 'host': os.getenv('REDIS_HOST', '10.100.51.51'), 'port': int(os.getenv('REDIS_PORT', '6379')), 'db': int(os.getenv('REDIS_DB', '0')), - 'password': os.getenv('REDIS_PASSWORD', None), + 'password': os.getenv('REDIS_PASSWORD', 'Unis@123'), 'decode_responses': True } diff --git a/app/main.py b/app/main.py index 1d45b98..4e9e3b8 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 +from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data from app.core.config import UPLOAD_DIR, API_CONFIG from app.api.endpoints.admin import load_system_config @@ -50,6 +50,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(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 5cd9894..1a6ae9a 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -167,8 +167,9 @@ class KnowledgeBaseListResponse(BaseModel): # 客户端下载相关模型 class ClientDownload(BaseModel): id: int - platform_type: str # 'mobile' or 'desktop' - platform_name: str # 'ios', 'android', 'windows', 'mac_intel', 'mac_m', 'linux' + platform_type: Optional[str] = None # 兼容旧版:'mobile', 'desktop', 'terminal' + platform_name: Optional[str] = None # 兼容旧版:'ios', 'android', 'windows', 'mac_intel', 'mac_m', 'linux' + platform_code: str # 新版平台编码,关联 dict_data.dict_code version: str version_code: int download_url: str @@ -182,8 +183,9 @@ class ClientDownload(BaseModel): created_by: Optional[int] = None class CreateClientDownloadRequest(BaseModel): - platform_type: str - platform_name: str + platform_type: Optional[str] = None # 兼容旧版 + platform_name: Optional[str] = None # 兼容旧版 + platform_code: str # 必填,关联 dict_data version: str version_code: int download_url: str @@ -194,6 +196,9 @@ class CreateClientDownloadRequest(BaseModel): min_system_version: Optional[str] = None class UpdateClientDownloadRequest(BaseModel): + platform_type: Optional[str] = None + platform_name: Optional[str] = None + platform_code: Optional[str] = None version: Optional[str] = None version_code: Optional[int] = None download_url: Optional[str] = None diff --git a/app/utils/apk_parser.py b/app/utils/apk_parser.py new file mode 100644 index 0000000..226fd0c --- /dev/null +++ b/app/utils/apk_parser.py @@ -0,0 +1,36 @@ +""" +APK解析工具 +用于从APK文件中提取版本信息 +""" +import zipfile +import xml.etree.ElementTree as ET +import struct + + +# 如果安装了 androguard,使用更可靠的解析方法 +def parse_apk_with_androguard(apk_path): + """ + 使用 androguard 库解析 APK + 需要先安装: pip install androguard + """ + try: + from androguard.core.apk import APK + + apk = APK(apk_path) + version_code = apk.get_androidversion_code() + version_name = apk.get_androidversion_name() + + print(f"APK解析成功: version_code={version_code}, version_name={version_name}") + + return { + 'version_code': int(version_code) if version_code else None, + 'version_name': version_name + } + except ImportError as ie: + print(f"androguard 导入失败: {str(ie)}") + return parse_apk(apk_path) + except Exception as e: + print(f"使用androguard解析APK失败: {str(e)}") + import traceback + traceback.print_exc() + return None diff --git a/check_db_structure.py b/check_db_structure.py new file mode 100644 index 0000000..745f056 --- /dev/null +++ b/check_db_structure.py @@ -0,0 +1,56 @@ +import mysql.connector + +# 连接数据库 +conn = mysql.connector.connect( + host="10.100.51.51", + port=3306, + user="root", + password="Unis@123", + database="imeeting_dev" +) + +cursor = conn.cursor() + +print("=" * 80) +print("dict_data 表结构:") +print("=" * 80) +cursor.execute("SHOW CREATE TABLE dict_data") +result = cursor.fetchone() +print(result[1]) +print("\n") + +print("=" * 80) +print("dict_data 表数据:") +print("=" * 80) +cursor.execute("SELECT * FROM dict_data ORDER BY dict_code, sort_order") +rows = cursor.fetchall() +cursor.execute("SHOW COLUMNS FROM dict_data") +columns = [col[0] for col in cursor.fetchall()] +print(" | ".join(columns)) +print("-" * 80) +for row in rows: + print(" | ".join(str(val) for val in row)) +print("\n") + +print("=" * 80) +print("client_download 表结构:") +print("=" * 80) +cursor.execute("SHOW CREATE TABLE client_download") +result = cursor.fetchone() +print(result[1]) +print("\n") + +print("=" * 80) +print("client_download 表数据:") +print("=" * 80) +cursor.execute("SELECT * FROM client_download") +rows = cursor.fetchall() +cursor.execute("SHOW COLUMNS FROM client_download") +columns = [col[0] for col in cursor.fetchall()] +print(" | ".join(columns)) +print("-" * 80) +for row in rows: + print(" | ".join(str(val) for val in row)) + +cursor.close() +conn.close() diff --git a/requirements-prod.txt b/requirements-prod.txt index 9bd1d4e..840a5d4 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -18,3 +18,6 @@ python-multipart # System Monitoring psutil + +# APK Parsing +androguard diff --git a/requirements.txt b/requirements.txt index 3cdd46f..7573184 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ dashscope PyJWT>=2.8.0 python-jose[cryptography]>=3.3.0 psutil +androguard diff --git a/sql/add_task_type_dict.sql b/sql/add_task_type_dict.sql new file mode 100644 index 0000000..fe6fac8 --- /dev/null +++ b/sql/add_task_type_dict.sql @@ -0,0 +1,10 @@ +-- 添加 task_type 字典数据 +-- 用于会议任务和知识库任务的分类 + +INSERT INTO dict_data (dict_type, dict_code, parent_code, label_cn, label_en, sort_order, status, extension_attr) VALUES +('task_type', 'MEETING_TASK', 'ROOT', '会议任务', 'Meeting Task', 1, 1, NULL), +('task_type', 'KNOWLEDGE_TASK', 'ROOT', '知识库任务', 'Knowledge Task', 2, 1, NULL) +ON DUPLICATE KEY UPDATE label_cn=VALUES(label_cn), label_en=VALUES(label_en); + +-- 查看结果 +SELECT * FROM dict_data WHERE dict_type='task_type'; diff --git a/sql/create_client_downloads.sql b/sql/create_client_downloads.sql new file mode 100644 index 0000000..d0097fe --- /dev/null +++ b/sql/create_client_downloads.sql @@ -0,0 +1,37 @@ +-- 客户端下载管理表 +-- 保留 platform_type 和 platform_name 字段以兼容旧终端 +-- 新增 platform_code 关联 dict_data 表的码表数据 + +CREATE TABLE IF NOT EXISTS `client_downloads` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `platform_type` VARCHAR(50) NULL COMMENT '平台类型(兼容旧版:mobile, desktop, terminal)', + `platform_name` VARCHAR(50) NULL COMMENT '平台名称(兼容旧版:ios, android, windows等)', + `platform_code` VARCHAR(64) NOT NULL COMMENT '平台编码(关联 dict_data.dict_code)', + `version` VARCHAR(50) NOT NULL COMMENT '版本号(如 1.0.0)', + `version_code` INT NOT NULL COMMENT '版本号数值(用于版本比较)', + `download_url` VARCHAR(512) NOT NULL COMMENT '下载链接', + `file_size` BIGINT NULL COMMENT '文件大小(bytes)', + `release_notes` TEXT NULL COMMENT '更新说明', + `is_active` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否启用', + `is_latest` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否为最新版本', + `min_system_version` VARCHAR(50) NULL COMMENT '最低系统版本要求', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` INT NULL COMMENT '创建者用户ID', + PRIMARY KEY (`id`), + INDEX `idx_platform_code` (`platform_code`), + INDEX `idx_platform_type_name` (`platform_type`, `platform_name`), + INDEX `idx_is_latest` (`is_latest`), + INDEX `idx_is_active` (`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户端下载管理表'; + +-- 插入测试数据示例(包含新旧字段映射) +-- 旧终端使用 platform_type + platform_name +-- 新终端使用 platform_code +-- INSERT INTO client_downloads (platform_type, platform_name, platform_code, version, version_code, download_url, file_size, release_notes, is_active, is_latest, min_system_version, created_by) +-- VALUES +-- ('desktop', 'windows', 'WIN', '1.0.0', 100, 'https://download.example.com/imeeting-win-1.0.0.exe', 52428800, '首个正式版本', TRUE, TRUE, 'Windows 10', 1), +-- ('desktop', 'mac', 'MAC', '1.0.0', 100, 'https://download.example.com/imeeting-mac-1.0.0.dmg', 48234496, '首个正式版本', TRUE, TRUE, 'macOS 11.0', 1), +-- ('mobile', 'ios', 'IOS', '1.0.0', 100, 'https://apps.apple.com/app/imeeting', 45088768, '首个正式版本', TRUE, TRUE, 'iOS 13.0', 1), +-- ('mobile', 'android', 'ANDROID', '1.0.0', 100, 'https://download.example.com/imeeting-android-1.0.0.apk', 38797312, '首个正式版本', TRUE, TRUE, 'Android 8.0', 1); + diff --git a/test_apk_parser.py b/test_apk_parser.py new file mode 100644 index 0000000..10c03f9 --- /dev/null +++ b/test_apk_parser.py @@ -0,0 +1,24 @@ +""" +测试APK解析功能 +""" +from app.utils.apk_parser import parse_apk_with_androguard + +# 测试导入是否正常 +try: + from androguard.core.apk import APK + print("✅ androguard 导入成功") + print(f" androguard 模块路径: {APK.__module__}") +except ImportError as e: + print(f"❌ androguard 导入失败: {e}") + exit(1) + +# 如果有测试APK文件,可以在这里测试解析 +# apk_path = "path/to/your/test.apk" +# result = parse_apk_with_androguard(apk_path) +# print(f"解析结果: {result}") + +print("\n📋 测试结果:") +print(" 1. androguard 库已成功安装") +print(" 2. 导入路径正确: androguard.core.apk.APK") +print(" 3. 可以正常使用 APK 解析功能") +print("\n💡 提示: 请重启后台服务以使更改生效")