diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..96a084f Binary files /dev/null and b/.DS_Store differ diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py new file mode 100644 index 0000000..a8cab44 --- /dev/null +++ b/app/api/endpoints/auth.py @@ -0,0 +1,37 @@ + +from fastapi import APIRouter, HTTPException +from app.models.models import LoginRequest, LoginResponse +from app.core.database import get_db_connection +import hashlib +import datetime + +router = APIRouter() + +def hash_password(password: str) -> str: + return hashlib.sha256(password.encode()).hexdigest() + +@router.post("/auth/login", response_model=LoginResponse) +def login(request: LoginRequest): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + query = "SELECT user_id, username, caption, email, password_hash FROM users WHERE username = %s" + cursor.execute(query, (request.username,)) + user = cursor.fetchone() + + if not user: + raise HTTPException(status_code=401, detail="用户名或密码错误") + + hashed_input = hash_password(request.password) + if user['password_hash'] != hashed_input and user['password_hash'] != request.password: + raise HTTPException(status_code=401, detail="用户名或密码错误") + + token = f"token_{user['user_id']}_{hash_password(str(datetime.datetime.now()))[:16]}" + + return LoginResponse( + user_id=user['user_id'], + username=user['username'], + caption=user['caption'], + email=user['email'], + token=token + ) diff --git a/app/api/endpoints/meetings.py b/app/api/endpoints/meetings.py new file mode 100644 index 0000000..bf3e8ec --- /dev/null +++ b/app/api/endpoints/meetings.py @@ -0,0 +1,394 @@ + +from fastapi import APIRouter, HTTPException, UploadFile, File, Form +from app.models.models import Meeting, TranscriptSegment, CreateMeetingRequest, UpdateMeetingRequest +from app.core.database import get_db_connection +from app.core.config import UPLOAD_DIR, AUDIO_DIR, ALLOWED_EXTENSIONS, MAX_FILE_SIZE +from typing import Optional +import os +import uuid +import shutil + +router = APIRouter() + +@router.get("/meetings", response_model=list[Meeting]) +def get_meetings(user_id: Optional[int] = None): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + base_query = ''' + SELECT + m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, + m.user_id as creator_id, u.caption as creator_username + FROM meetings m + JOIN users u ON m.user_id = u.user_id + ''' + + if user_id: + query = f''' + {base_query} + LEFT JOIN attendees a ON m.meeting_id = a.meeting_id + WHERE m.user_id = %s OR a.user_id = %s + GROUP BY m.meeting_id + ORDER BY m.meeting_time DESC, m.created_at DESC + ''' + cursor.execute(query, (user_id, user_id)) + else: + query = f" {base_query} ORDER BY m.meeting_time DESC, m.created_at DESC" + cursor.execute(query) + + meetings = cursor.fetchall() + + meeting_list = [] + for meeting in meetings: + attendees_query = ''' + SELECT u.user_id, u.caption + FROM attendees a + JOIN users u ON a.user_id = u.user_id + WHERE a.meeting_id = %s + ''' + cursor.execute(attendees_query, (meeting['meeting_id'],)) + attendees_data = cursor.fetchall() + attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + + meeting_list.append(Meeting( + meeting_id=meeting['meeting_id'], + title=meeting['title'], + meeting_time=meeting['meeting_time'], + summary=meeting['summary'], + created_at=meeting['created_at'], + attendees=attendees, + creator_id=meeting['creator_id'], + creator_username=meeting['creator_username'] + )) + + return meeting_list + +@router.get("/meetings/{meeting_id}", response_model=Meeting) +def get_meeting_details(meeting_id: int): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + query = ''' + SELECT + m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, + m.user_id as creator_id, u.caption as creator_username, + 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 + WHERE m.meeting_id = %s + ''' + cursor.execute(query, (meeting_id,)) + meeting = cursor.fetchone() + + if not meeting: + raise HTTPException(status_code=404, detail="Meeting not found") + + attendees_query = ''' + SELECT u.user_id, u.caption + FROM attendees a + JOIN users u ON a.user_id = u.user_id + WHERE a.meeting_id = %s + ''' + cursor.execute(attendees_query, (meeting['meeting_id'],)) + attendees_data = cursor.fetchall() + attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + + meeting_data = Meeting( + meeting_id=meeting['meeting_id'], + title=meeting['title'], + meeting_time=meeting['meeting_time'], + summary=meeting['summary'], + created_at=meeting['created_at'], + attendees=attendees, + creator_id=meeting['creator_id'], + creator_username=meeting['creator_username'] + ) + + # Add audio file path if exists + if meeting['audio_file_path']: + meeting_data.audio_file_path = meeting['audio_file_path'] + + return meeting_data + +@router.get("/meetings/{meeting_id}/transcript", response_model=list[TranscriptSegment]) +def get_meeting_transcript(meeting_id: int): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # First check if meeting exists + meeting_query = "SELECT meeting_id FROM meetings WHERE meeting_id = %s" + cursor.execute(meeting_query, (meeting_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail="Meeting not found") + + # Get transcript segments + transcript_query = ''' + SELECT segment_id, meeting_id, speaker_tag, start_time_ms, end_time_ms, text_content + FROM transcript_segments + WHERE meeting_id = %s + ORDER BY start_time_ms ASC + ''' + cursor.execute(transcript_query, (meeting_id,)) + segments = cursor.fetchall() + + return [TranscriptSegment(**segment) for segment in segments] + +@router.post("/meetings") +def create_meeting(meeting_request: CreateMeetingRequest): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # Create meeting + meeting_query = ''' + INSERT INTO meetings (user_id, title, meeting_time, summary) + VALUES (%s, %s, %s, %s) + ''' + # Note: You'll need to pass user_id, for now using hardcoded value + cursor.execute(meeting_query, (1, meeting_request.title, meeting_request.meeting_time, None)) + meeting_id = cursor.lastrowid + + # Add attendees + for attendee_id in meeting_request.attendee_ids: + attendee_query = ''' + INSERT INTO attendees (meeting_id, user_id) + VALUES (%s, %s) + ON DUPLICATE KEY UPDATE meeting_id = meeting_id + ''' + cursor.execute(attendee_query, (meeting_id, attendee_id)) + + connection.commit() + return {"meeting_id": meeting_id, "message": "Meeting created successfully"} + +@router.put("/meetings/{meeting_id}") +def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # Check if meeting exists + cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail="Meeting not found") + + # Update meeting + update_query = ''' + UPDATE meetings + SET title = %s, meeting_time = %s, summary = %s + WHERE meeting_id = %s + ''' + cursor.execute(update_query, ( + meeting_request.title, + meeting_request.meeting_time, + meeting_request.summary, + meeting_id + )) + + # Update attendees - remove existing ones and add new ones + cursor.execute("DELETE FROM attendees WHERE meeting_id = %s", (meeting_id,)) + + for attendee_id in meeting_request.attendee_ids: + attendee_query = ''' + INSERT INTO attendees (meeting_id, user_id) + VALUES (%s, %s) + ''' + cursor.execute(attendee_query, (meeting_id, attendee_id)) + + connection.commit() + return {"message": "Meeting updated successfully"} + +@router.delete("/meetings/{meeting_id}") +def delete_meeting(meeting_id: int): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # Check if meeting exists + cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail="Meeting not found") + + # Delete related records first (foreign key constraints) + cursor.execute("DELETE FROM transcript_segments WHERE meeting_id = %s", (meeting_id,)) + cursor.execute("DELETE FROM audio_files WHERE meeting_id = %s", (meeting_id,)) + cursor.execute("DELETE FROM attachments WHERE meeting_id = %s", (meeting_id,)) + cursor.execute("DELETE FROM attendees WHERE meeting_id = %s", (meeting_id,)) + + # Delete meeting + cursor.execute("DELETE FROM meetings WHERE meeting_id = %s", (meeting_id,)) + + connection.commit() + return {"message": "Meeting deleted successfully"} + +@router.post("/meetings/{meeting_id}/regenerate-summary") +def regenerate_summary(meeting_id: int): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # Check if meeting exists + cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail="Meeting not found") + + # For now, return a mock summary + # In a real implementation, this would call an AI service + mock_summary = """# AI 生成摘要 + +## 主要议题 +- 项目进度回顾 +- 技术方案讨论 +- 下阶段规划 + +## 关键决策 +- 采用新的技术架构 +- 调整项目时间节点 +- 分配任务责任 + +## 后续行动 +- [ ] 完成技术方案文档 +- [ ] 安排下次会议时间 +- [ ] 跟进项目进度""" + + # Update meeting summary + cursor.execute( + "UPDATE meetings SET summary = %s WHERE meeting_id = %s", + (mock_summary, meeting_id) + ) + connection.commit() + + return {"summary": mock_summary} + +@router.get("/meetings/{meeting_id}/edit", response_model=Meeting) +def get_meeting_for_edit(meeting_id: int): + """Get meeting details with full attendee information for editing""" + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + query = ''' + SELECT + m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, + m.user_id as creator_id, u.caption as creator_username, + 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 + WHERE m.meeting_id = %s + ''' + cursor.execute(query, (meeting_id,)) + meeting = cursor.fetchone() + + if not meeting: + raise HTTPException(status_code=404, detail="Meeting not found") + + # Get attendees with full info for editing + attendees_query = ''' + SELECT u.user_id, u.caption + FROM attendees a + JOIN users u ON a.user_id = u.user_id + WHERE a.meeting_id = %s + ''' + cursor.execute(attendees_query, (meeting['meeting_id'],)) + attendees_data = cursor.fetchall() + attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + + meeting_data = Meeting( + meeting_id=meeting['meeting_id'], + title=meeting['title'], + meeting_time=meeting['meeting_time'], + summary=meeting['summary'], + created_at=meeting['created_at'], + attendees=attendees, + creator_id=meeting['creator_id'], + creator_username=meeting['creator_username'] + ) + + # Add audio file path if exists + if meeting['audio_file_path']: + meeting_data.audio_file_path = meeting['audio_file_path'] + + return meeting_data + +@router.post("/meetings/upload-audio") +async def upload_audio( + audio_file: UploadFile = File(...), + meeting_id: int = Form(...) +): + # Validate file extension + file_extension = os.path.splitext(audio_file.filename)[1].lower() + if file_extension not in ALLOWED_EXTENSIONS: + raise HTTPException( + status_code=400, + detail=f"Unsupported file type. Allowed types: {', '.join(ALLOWED_EXTENSIONS)}" + ) + + # Check file size + if audio_file.size > MAX_FILE_SIZE: + raise HTTPException( + status_code=400, + detail="File size exceeds 100MB limit" + ) + + # Generate unique filename + unique_filename = f"{uuid.uuid4()}{file_extension}" + file_path = AUDIO_DIR / unique_filename + + # Store only relative path for database (audio/filename) + relative_path = f"audio/{unique_filename}" + + # Save file + try: + with open(file_path, "wb") as buffer: + shutil.copyfileobj(audio_file.file, buffer) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}") + + # Save file info to database + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # Check if meeting exists + cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,)) + if not cursor.fetchone(): + # Clean up uploaded file if meeting doesn't exist + os.remove(file_path) + raise HTTPException(status_code=404, detail="Meeting not found") + + # Insert audio file record + insert_query = ''' + INSERT INTO audio_files (meeting_id, file_name, file_path, file_size, upload_time) + VALUES (%s, %s, %s, %s, NOW()) + ON DUPLICATE KEY UPDATE + file_name = VALUES(file_name), + file_path = VALUES(file_path), + file_size = VALUES(file_size), + upload_time = VALUES(upload_time) + ''' + cursor.execute(insert_query, (meeting_id, audio_file.filename, relative_path, audio_file.size)) + connection.commit() + + return { + "message": "Audio file uploaded successfully", + "file_name": audio_file.filename, + "file_path": relative_path + } + +@router.get("/meetings/{meeting_id}/audio") +def get_audio_file(meeting_id: int): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + query = ''' + SELECT file_name, file_path, file_size, upload_time + FROM audio_files + WHERE meeting_id = %s + ''' + cursor.execute(query, (meeting_id,)) + audio_file = cursor.fetchone() + + if not audio_file: + raise HTTPException(status_code=404, detail="Audio file not found for this meeting") + + return { + "file_name": audio_file['file_name'], + "file_path": audio_file['file_path'], + "file_size": audio_file['file_size'], + "upload_time": audio_file['upload_time'] + } diff --git a/app/api/endpoints/users.py b/app/api/endpoints/users.py new file mode 100644 index 0000000..627617d --- /dev/null +++ b/app/api/endpoints/users.py @@ -0,0 +1,54 @@ + +from fastapi import APIRouter, HTTPException +from app.models.models import UserInfo +from app.core.database import get_db_connection + +router = APIRouter() + +@router.get("/users", response_model=list[UserInfo]) +def get_all_users(): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + query = ''' + SELECT + user_id, username, caption, email, created_at, + (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 + FROM users u + ORDER BY caption ASC + ''' + cursor.execute(query) + users = cursor.fetchall() + + return [UserInfo(**user) for user in users] + +@router.get("/users/{user_id}", response_model=UserInfo) +def get_user_info(user_id: int): + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + user_query = "SELECT user_id, username, caption, email, created_at FROM users WHERE user_id = %s" + cursor.execute(user_query, (user_id,)) + user = cursor.fetchone() + + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + created_query = "SELECT COUNT(*) as count FROM meetings WHERE user_id = %s" + cursor.execute(created_query, (user_id,)) + meetings_created = cursor.fetchone()['count'] + + attended_query = "SELECT COUNT(*) as count FROM attendees WHERE user_id = %s" + cursor.execute(attended_query, (user_id,)) + meetings_attended = cursor.fetchone()['count'] + + return UserInfo( + user_id=user['user_id'], + username=user['username'], + caption=user['caption'], + email=user['email'], + created_at=user['created_at'], + meetings_created=meetings_created, + meetings_attended=meetings_attended + ) diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..7e0968d --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,32 @@ +import os +from pathlib import Path + +# 基础路径配置 +BASE_DIR = Path(__file__).parent.parent.parent +UPLOAD_DIR = BASE_DIR / "uploads" +AUDIO_DIR = UPLOAD_DIR / "audio" + +# 文件上传配置 +ALLOWED_EXTENSIONS = {".mp3", ".wav", ".m4a", ".mpeg", ".mp4"} +MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB + +# 确保上传目录存在 +UPLOAD_DIR.mkdir(exist_ok=True) +AUDIO_DIR.mkdir(exist_ok=True) + +# 数据库配置 +DATABASE_CONFIG = { + 'host': os.getenv('DB_HOST', 'localhost'), + 'user': os.getenv('DB_USER', 'root'), + 'password': os.getenv('DB_PASSWORD', ''), + 'database': os.getenv('DB_NAME', 'imeeting'), + 'port': int(os.getenv('DB_PORT', '3306')), + 'charset': 'utf8mb4' +} + +# API配置 +API_CONFIG = { + 'host': os.getenv('API_HOST', '0.0.0.0'), + 'port': int(os.getenv('API_PORT', '8000')), + 'cors_origins': os.getenv('CORS_ORIGINS', 'http://localhost:5173').split(',') +} \ No newline at end of file diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..dc42fbf --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,27 @@ + +from fastapi import HTTPException +import mysql.connector +from mysql.connector import Error +from contextlib import contextmanager + +DB_CONFIG = { + 'host': 'localhost', + 'database': 'imeeting', + 'user': 'root', + 'password': 'sagacity', + 'port': 3306, + 'charset': 'utf8mb4' +} + +@contextmanager +def get_db_connection(): + connection = None + try: + connection = mysql.connector.connect(**DB_CONFIG) + yield connection + except Error as e: + print(f"数据库连接错误: {e}") + raise HTTPException(status_code=500, detail="数据库连接失败") + finally: + if connection and connection.is_connected(): + connection.close() diff --git a/app/models/models.py b/app/models/models.py new file mode 100644 index 0000000..3c387aa --- /dev/null +++ b/app/models/models.py @@ -0,0 +1,58 @@ + +from pydantic import BaseModel, EmailStr +from typing import Optional, Union, List +import datetime + +class LoginRequest(BaseModel): + username: str + password: str + +class LoginResponse(BaseModel): + user_id: int + username: str + caption: str + email: EmailStr + token: str + +class UserInfo(BaseModel): + user_id: int + username: str + caption: str + email: EmailStr + created_at: datetime.datetime + meetings_created: int + meetings_attended: int + +class AttendeeInfo(BaseModel): + user_id: int + caption: str + +class Meeting(BaseModel): + meeting_id: int + title: str + meeting_time: Optional[datetime.datetime] + summary: Optional[str] + created_at: datetime.datetime + attendees: Union[List[str], List[AttendeeInfo]] # Support both formats + creator_id: int + creator_username: str + audio_file_path: Optional[str] = None + +class TranscriptSegment(BaseModel): + segment_id: int + meeting_id: int + speaker_tag: str + start_time_ms: int + end_time_ms: int + text_content: str + +class CreateMeetingRequest(BaseModel): + title: str + meeting_time: Optional[datetime.datetime] + attendee_ids: list[int] + +class UpdateMeetingRequest(BaseModel): + title: str + meeting_time: Optional[datetime.datetime] + summary: Optional[str] + attendee_ids: list[int] diff --git a/main.py b/main.py new file mode 100644 index 0000000..7be2e59 --- /dev/null +++ b/main.py @@ -0,0 +1,38 @@ +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from app.api.endpoints import auth, users, meetings +from app.core.config import UPLOAD_DIR, API_CONFIG +import os + +app = FastAPI( + title="iMeeting API", + description="智慧会议系统API", + version="1.0.0" +) + +# 添加CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=API_CONFIG['cors_origins'], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 静态文件服务 - 提供音频文件下载 +if UPLOAD_DIR.exists(): + app.mount("/uploads", StaticFiles(directory=str(UPLOAD_DIR)), name="uploads") + +# 包含API路由 +app.include_router(auth.router, prefix="/api", tags=["Authentication"]) +app.include_router(users.router, prefix="/api", tags=["Users"]) +app.include_router(meetings.router, prefix="/api", tags=["Meetings"]) + +@app.get("/") +def read_root(): + return {"message": "Welcome to iMeeting API"} + +if __name__ == "__main__": + uvicorn.run(app, host=API_CONFIG['host'], port=API_CONFIG['port']) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8e764a7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi +mysql-connector-python +uvicorn[standard] +python-multipart +pydantic[email] +passlib[bcrypt] \ No newline at end of file diff --git a/uploads/.DS_Store b/uploads/.DS_Store new file mode 100644 index 0000000..73c98ca Binary files /dev/null and b/uploads/.DS_Store differ diff --git a/uploads/audio/31ce039a-f619-4869-91c8-eab934bbd1d4.m4a b/uploads/audio/31ce039a-f619-4869-91c8-eab934bbd1d4.m4a new file mode 100644 index 0000000..c87a525 Binary files /dev/null and b/uploads/audio/31ce039a-f619-4869-91c8-eab934bbd1d4.m4a differ diff --git a/uploads/audio/809605cc-504c-4e08-aa49-4ed3d95fa8f9.m4a b/uploads/audio/809605cc-504c-4e08-aa49-4ed3d95fa8f9.m4a new file mode 100644 index 0000000..a8d86b3 Binary files /dev/null and b/uploads/audio/809605cc-504c-4e08-aa49-4ed3d95fa8f9.m4a differ