diff --git a/Dockerfile b/Dockerfile index 4ed2347..98e46b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,8 @@ ENV PYTHONPATH=/app ENV PYTHONUNBUFFERED=1 # 使用阿里源 +RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources +RUN sed -i 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources # 安装系统依赖 RUN apt-get update && apt-get install -y \ diff --git a/app/api/endpoints/client_downloads.py b/app/api/endpoints/client_downloads.py new file mode 100644 index 0000000..0f7bcb7 --- /dev/null +++ b/app/api/endpoints/client_downloads.py @@ -0,0 +1,391 @@ +from fastapi import APIRouter, HTTPException, Depends +from app.models.models import ( + ClientDownload, + CreateClientDownloadRequest, + UpdateClientDownloadRequest, + ClientDownloadListResponse +) +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 typing import Optional + +router = APIRouter() + +@router.get("/downloads", response_model=dict) +async def get_client_downloads( + platform_type: Optional[str] = None, + platform_name: Optional[str] = None, + is_active: Optional[bool] = None, + page: int = 1, + size: int = 50 +): + """ + 获取客户端下载列表(公开接口,所有用户可访问) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + # 构建查询条件 + 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 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 client_downloads WHERE {where_clause}" + cursor.execute(count_query, params) + total = cursor.fetchone()['total'] + + # 获取列表数据 + offset = (page - 1) * size + list_query = f""" + SELECT * FROM client_downloads + WHERE {where_clause} + ORDER BY platform_type, platform_name, version_code DESC + LIMIT %s OFFSET %s + """ + cursor.execute(list_query, params + [size, offset]) + clients = cursor.fetchall() + + cursor.close() + + return create_api_response( + code="200", + message="获取成功", + data={ + "clients": clients, + "total": total, + "page": page, + "size": size + } + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"获取客户端下载列表失败: {str(e)}" + ) + + +@router.get("/downloads/latest", response_model=dict) +async def get_latest_clients(): + """ + 获取所有平台的最新版本客户端(公开接口,用于首页下载) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + query = """ + SELECT * FROM client_downloads + WHERE is_active = TRUE AND is_latest = TRUE + ORDER BY platform_type, platform_name + """ + cursor.execute(query) + clients = cursor.fetchall() + cursor.close() + + # 按平台类型分组 + mobile_clients = [] + desktop_clients = [] + + for client in clients: + if client['platform_type'] == 'mobile': + mobile_clients.append(client) + else: + desktop_clients.append(client) + + return create_api_response( + code="200", + message="获取成功", + data={ + "mobile": mobile_clients, + "desktop": desktop_clients + } + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"获取最新客户端失败: {str(e)}" + ) + + +@router.get("/downloads/{platform_name}/latest", response_model=dict) +async def get_latest_version_by_platform(platform_name: str): + """ + 获取指定平台的最新版本(公开接口,用于客户端版本检查) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + query = """ + SELECT * FROM client_downloads + WHERE platform_name = %s AND is_active = TRUE AND is_latest = TRUE + LIMIT 1 + """ + cursor.execute(query, (platform_name,)) + client = cursor.fetchone() + cursor.close() + + if not client: + return create_api_response( + code="404", + message=f"未找到平台 {platform_name} 的客户端" + ) + + return create_api_response( + code="200", + message="获取成功", + data=client + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"获取客户端版本失败: {str(e)}" + ) + + +@router.get("/downloads/{id}", response_model=dict) +async def get_client_download_by_id(id: int): + """ + 获取指定ID的客户端详情(公开接口) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + query = "SELECT * FROM client_downloads WHERE id = %s" + cursor.execute(query, (id,)) + client = cursor.fetchone() + cursor.close() + + if not client: + return create_api_response( + code="404", + message="客户端不存在" + ) + + return create_api_response( + code="200", + message="获取成功", + data=client + ) + + except Exception as e: + return create_api_response( + code="500", + message=f"获取客户端详情失败: {str(e)}" + ) + + +@router.post("/downloads", response_model=dict) +async def create_client_download( + request: CreateClientDownloadRequest, + current_user: dict = Depends(get_current_admin_user) +): + """ + 创建新的客户端版本(仅管理员) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor() + + # 如果设置为最新版本,先将同平台的其他版本设为非最新 + if request.is_latest: + update_query = """ + UPDATE client_downloads + SET is_latest = FALSE + WHERE platform_name = %s + """ + cursor.execute(update_query, (request.platform_name,)) + + # 插入新版本 + 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) + """ + cursor.execute(insert_query, ( + request.platform_type, + request.platform_name, + request.version, + request.version_code, + request.download_url, + request.file_size, + request.release_notes, + request.is_active, + request.is_latest, + request.min_system_version, + current_user['user_id'] + )) + + 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("/downloads/{id}", response_model=dict) +async def update_client_download( + id: int, + request: UpdateClientDownloadRequest, + current_user: dict = Depends(get_current_admin_user) +): + """ + 更新客户端版本信息(仅管理员) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + # 检查客户端是否存在 + cursor.execute("SELECT * FROM client_downloads WHERE id = %s", (id,)) + existing = cursor.fetchone() + if not existing: + cursor.close() + return create_api_response( + code="404", + message="客户端不存在" + ) + + # 如果设置为最新版本,先将同平台的其他版本设为非最新 + if request.is_latest: + update_query = """ + UPDATE client_downloads + SET is_latest = FALSE + WHERE platform_name = %s AND id != %s + """ + cursor.execute(update_query, (existing['platform_name'], id)) + + # 构建更新语句 + update_fields = [] + params = [] + + if request.version is not None: + update_fields.append("version = %s") + params.append(request.version) + + if request.version_code is not None: + update_fields.append("version_code = %s") + params.append(request.version_code) + + if request.download_url is not None: + update_fields.append("download_url = %s") + params.append(request.download_url) + + if request.file_size is not None: + update_fields.append("file_size = %s") + params.append(request.file_size) + + if request.release_notes is not None: + update_fields.append("release_notes = %s") + params.append(request.release_notes) + + if request.is_active is not None: + update_fields.append("is_active = %s") + params.append(request.is_active) + + if request.is_latest is not None: + update_fields.append("is_latest = %s") + params.append(request.is_latest) + + if request.min_system_version is not None: + update_fields.append("min_system_version = %s") + params.append(request.min_system_version) + + if not update_fields: + cursor.close() + return create_api_response( + code="400", + message="没有要更新的字段" + ) + + # 执行更新 + update_query = f""" + UPDATE client_downloads + 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("/downloads/{id}", response_model=dict) +async def delete_client_download( + id: int, + current_user: dict = Depends(get_current_admin_user) +): + """ + 删除客户端版本(仅管理员) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor() + + # 检查是否存在 + cursor.execute("SELECT * FROM client_downloads WHERE id = %s", (id,)) + if not cursor.fetchone(): + cursor.close() + return create_api_response( + code="404", + message="客户端不存在" + ) + + # 执行删除 + cursor.execute("DELETE FROM client_downloads 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/models/models.py b/app/models/models.py index bfd1219..0df7182 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -161,3 +161,46 @@ class UpdateKnowledgeBaseRequest(BaseModel): class KnowledgeBaseListResponse(BaseModel): kbs: List[KnowledgeBase] total: int + +# 客户端下载相关模型 +class ClientDownload(BaseModel): + id: int + platform_type: str # 'mobile' or 'desktop' + platform_name: str # 'ios', 'android', 'windows', 'mac_intel', 'mac_m', 'linux' + version: str + version_code: int + download_url: str + file_size: Optional[int] = None + release_notes: Optional[str] = None + is_active: bool + is_latest: bool + min_system_version: Optional[str] = None + created_at: datetime.datetime + updated_at: datetime.datetime + created_by: Optional[int] = None + +class CreateClientDownloadRequest(BaseModel): + platform_type: str + platform_name: str + version: str + version_code: int + download_url: str + file_size: Optional[int] = None + release_notes: Optional[str] = None + is_active: bool = True + is_latest: bool = False + min_system_version: Optional[str] = None + +class UpdateClientDownloadRequest(BaseModel): + version: Optional[str] = None + version_code: Optional[int] = None + download_url: Optional[str] = None + file_size: Optional[int] = None + release_notes: Optional[str] = None + is_active: Optional[bool] = None + is_latest: Optional[bool] = None + min_system_version: Optional[str] = None + +class ClientDownloadListResponse(BaseModel): + clients: List[ClientDownload] + total: int diff --git a/main.py b/main.py index 701dc80..22836e0 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,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, tasks, prompts, knowledge_base +from app.api.endpoints import auth, users, meetings, tags, admin, tasks, prompts, knowledge_base, client_downloads from app.core.config import UPLOAD_DIR, API_CONFIG from app.api.endpoints.admin import load_system_config import os @@ -38,6 +38,7 @@ app.include_router(admin.router, prefix="/api", tags=["Admin"]) 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/clients", tags=["ClientDownloads"]) @app.get("/") def read_root(): diff --git a/sql/client_downloads.sql b/sql/client_downloads.sql new file mode 100644 index 0000000..27dec5c --- /dev/null +++ b/sql/client_downloads.sql @@ -0,0 +1,149 @@ +-- 客户端下载管理表 +CREATE TABLE IF NOT EXISTS client_downloads ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + platform_type ENUM('mobile', 'desktop') NOT NULL COMMENT '平台类型:mobile-移动端, desktop-桌面端', + platform_name VARCHAR(50) NOT NULL COMMENT '具体平台:ios, android, windows, mac_intel, mac_m, linux', + version VARCHAR(50) NOT NULL COMMENT '版本号,如: 1.0.0', + version_code INT NOT NULL DEFAULT 1 COMMENT '版本代码,用于版本比较', + download_url TEXT NOT NULL COMMENT '下载链接', + file_size BIGINT COMMENT '文件大小(字节)', + release_notes TEXT COMMENT '更新说明', + is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用', + is_latest BOOLEAN DEFAULT FALSE COMMENT '是否为最新版本', + min_system_version VARCHAR(50) COMMENT '最低系统版本要求', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + created_by INT COMMENT '创建人ID', + FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL, + INDEX idx_platform (platform_type, platform_name), + INDEX idx_version (version_code), + INDEX idx_active (is_active, is_latest) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户端下载管理表'; + +-- 插入初始数据(示例版本) +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 +-- iOS 客户端 +( + 'mobile', + 'ios', + '1.0.0', + 1000, + 'https://apps.apple.com/app/imeeting/id123456789', + 52428800, -- 50MB + '初始版本发布 +- 支持会议录音 +- 支持实时转录 +- 支持会议摘要查看', + TRUE, + TRUE, + 'iOS 13.0', + 1 +), + +-- Android 客户端 +( + 'mobile', + 'android', + '1.0.0', + 1000, + 'https://play.google.com/store/apps/details?id=com.imeeting.app', + 45088768, -- 43MB + '初始版本发布 +- 支持会议录音 +- 支持实时转录 +- 支持会议摘要查看', + TRUE, + TRUE, + 'Android 8.0', + 1 +), + +-- Windows 客户端 +( + 'desktop', + 'windows', + '1.0.0', + 1000, + 'https://download.imeeting.com/clients/windows/iMeeting-1.0.0-Setup.exe', + 104857600, -- 100MB + '初始版本发布 +- 完整的会议管理功能 +- 高清音频录制 +- AI智能转录 +- 知识库管理', + TRUE, + TRUE, + 'Windows 10 (64-bit)', + 1 +), + +-- Mac Intel 客户端 +( + 'desktop', + 'mac_intel', + '1.0.0', + 1000, + 'https://download.imeeting.com/clients/mac/iMeeting-1.0.0-Intel.dmg', + 94371840, -- 90MB + '初始版本发布 +- 完整的会议管理功能 +- 高清音频录制 +- AI智能转录 +- 知识库管理', + TRUE, + TRUE, + 'macOS 10.15 Catalina', + 1 +), + +-- Mac M系列 客户端 +( + 'desktop', + 'mac_m', + '1.0.0', + 1000, + 'https://download.imeeting.com/clients/mac/iMeeting-1.0.0-AppleSilicon.dmg', + 83886080, -- 80MB + '初始版本发布 +- 完整的会议管理功能 +- 高清音频录制 +- AI智能转录 +- 知识库管理 +- 原生支持Apple Silicon', + TRUE, + TRUE, + 'macOS 11.0 Big Sur', + 1 +), + +-- Linux 客户端 +( + 'desktop', + 'linux', + '1.0.0', + 1000, + 'https://download.imeeting.com/clients/linux/iMeeting-1.0.0-x64.AppImage', + 98566144, -- 94MB + '初始版本发布 +- 完整的会议管理功能 +- 高清音频录制 +- AI智能转录 +- 知识库管理 +- 支持主流Linux发行版', + TRUE, + TRUE, + 'Ubuntu 20.04 / Debian 10 / Fedora 32 或更高版本', + 1 +);