From 9fb07bb4358bdf19227efb71f932e26ee12b8d29 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Fri, 19 Sep 2025 16:51:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.zip | Bin 43248 -> 43320 bytes app/api/endpoints/meetings.py | 68 ++++++++++++++++++++++++++++------ app/api/endpoints/tags.py | 45 ++++++++++++++++++++++ app/models/models.py | 11 +++++- main.py | 3 +- 5 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 app/api/endpoints/tags.py diff --git a/app.zip b/app.zip index 79533815cad78adbdab47bac2a2dbcb6a7cc4304..ea49c8b62f90f4bd7714af5209b9cce689937eb2 100644 GIT binary patch delta 7463 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 AKL7v# diff --git a/app/api/endpoints/meetings.py b/app/api/endpoints/meetings.py index 1def460..a6c507e 100644 --- a/app/api/endpoints/meetings.py +++ b/app/api/endpoints/meetings.py @@ -1,5 +1,6 @@ + from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Depends, BackgroundTasks -from app.models.models import Meeting, TranscriptSegment, TranscriptionTaskStatus, CreateMeetingRequest, UpdateMeetingRequest, SpeakerTagUpdateRequest, BatchSpeakerTagUpdateRequest, TranscriptUpdateRequest, BatchTranscriptUpdateRequest +from app.models.models import Meeting, TranscriptSegment, TranscriptionTaskStatus, CreateMeetingRequest, UpdateMeetingRequest, SpeakerTagUpdateRequest, BatchSpeakerTagUpdateRequest, TranscriptUpdateRequest, BatchTranscriptUpdateRequest, Tag from app.core.database import get_db_connection from app.core.config import BASE_DIR, UPLOAD_DIR, AUDIO_DIR, MARKDOWN_DIR, ALLOWED_EXTENSIONS, ALLOWED_IMAGE_EXTENSIONS, MAX_FILE_SIZE, MAX_IMAGE_SIZE from app.services.qiniu_service import qiniu_service @@ -7,7 +8,7 @@ from app.services.llm_service import LLMService from app.services.async_transcription_service import AsyncTranscriptionService from app.services.async_llm_service import async_llm_service from app.core.auth import get_current_user, get_optional_current_user -from typing import Optional +from typing import List, Optional from datetime import datetime from pydantic import BaseModel import os @@ -27,6 +28,24 @@ transcription_service = AsyncTranscriptionService() class GenerateSummaryRequest(BaseModel): user_prompt: Optional[str] = "" +def _process_tags(cursor, tag_string: Optional[str]) -> List[Tag]: + if not tag_string: + return [] + + tag_names = [name.strip() for name in tag_string.split(',') if name.strip()] + if not tag_names: + return [] + + # Ensure all tags exist in the 'tags' table + insert_ignore_query = "INSERT IGNORE INTO tags (name) VALUES (%s)" + cursor.executemany(insert_ignore_query, [(name,) for name in tag_names]) + + # Fetch the full tag objects + format_strings = ', '.join(['%s'] * len(tag_names)) + cursor.execute(f"SELECT id, name, color FROM tags WHERE name IN ({format_strings})", tuple(tag_names)) + tags_data = cursor.fetchall() + return [Tag(**tag) for tag in tags_data] + @router.get("/meetings", response_model=list[Meeting]) def get_meetings(current_user: dict = Depends(get_current_user), user_id: Optional[int] = None): with get_db_connection() as connection: @@ -34,7 +53,7 @@ def get_meetings(current_user: dict = Depends(get_current_user), user_id: Option base_query = ''' SELECT - m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, + 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 FROM meetings m JOIN users u ON m.user_id = u.user_id @@ -67,6 +86,8 @@ def get_meetings(current_user: dict = Depends(get_current_user), user_id: Option attendees_data = cursor.fetchall() attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + tags = _process_tags(cursor, meeting.get('tags')) + meeting_list.append(Meeting( meeting_id=meeting['meeting_id'], title=meeting['title'], @@ -75,7 +96,8 @@ def get_meetings(current_user: dict = Depends(get_current_user), user_id: Option created_at=meeting['created_at'], attendees=attendees, creator_id=meeting['creator_id'], - creator_username=meeting['creator_username'] + creator_username=meeting['creator_username'], + tags=tags )) return meeting_list @@ -87,7 +109,7 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren query = ''' SELECT - m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, + 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 FROM meetings m @@ -112,6 +134,8 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren attendees_data = cursor.fetchall() attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + tags = _process_tags(cursor, meeting.get('tags')) + # 关闭游标,避免与转录服务的数据库连接冲突 cursor.close() @@ -123,7 +147,8 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren created_at=meeting['created_at'], attendees=attendees, creator_id=meeting['creator_id'], - creator_username=meeting['creator_username'] + creator_username=meeting['creator_username'], + tags=tags ) # Add audio file path if exists @@ -182,16 +207,24 @@ def create_meeting(meeting_request: CreateMeetingRequest, current_user: dict = D with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) + # Process tags + if meeting_request.tags: + tag_names = [name.strip() for name in meeting_request.tags.split(',') if name.strip()] + if tag_names: + insert_ignore_query = "INSERT IGNORE INTO tags (name) VALUES (%s)" + cursor.executemany(insert_ignore_query, [(name,) for name in tag_names]) + # Create meeting meeting_query = ''' - INSERT INTO meetings (user_id, title, meeting_time, summary,created_at) - VALUES (%s, %s, %s, %s, %s) + INSERT INTO meetings (user_id, title, meeting_time, summary, tags, created_at) + VALUES (%s, %s, %s, %s, %s, %s) ''' cursor.execute(meeting_query, ( meeting_request.user_id, meeting_request.title, meeting_request.meeting_time, None, + meeting_request.tags, datetime.now().isoformat() )) @@ -221,17 +254,25 @@ def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest, curre if meeting['user_id'] != current_user['user_id']: raise HTTPException(status_code=403, detail="Permission denied") + + # Process tags + if meeting_request.tags: + tag_names = [name.strip() for name in meeting_request.tags.split(',') if name.strip()] + if tag_names: + insert_ignore_query = "INSERT IGNORE INTO tags (name) VALUES (%s)" + cursor.executemany(insert_ignore_query, [(name,) for name in tag_names]) # Update meeting update_query = ''' UPDATE meetings - SET title = %s, meeting_time = %s, summary = %s + SET title = %s, meeting_time = %s, summary = %s, tags = %s WHERE meeting_id = %s ''' cursor.execute(update_query, ( meeting_request.title, meeting_request.meeting_time, meeting_request.summary, + meeting_request.tags, meeting_id )) @@ -282,7 +323,7 @@ def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_curre query = ''' SELECT - m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, + 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 FROM meetings m @@ -308,6 +349,8 @@ def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_curre attendees_data = cursor.fetchall() attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + tags = _process_tags(cursor, meeting.get('tags')) + # 关闭游标,避免与转录服务的数据库连接冲突 cursor.close() @@ -319,7 +362,8 @@ def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_curre created_at=meeting['created_at'], attendees=attendees, creator_id=meeting['creator_id'], - creator_username=meeting['creator_username'] + creator_username=meeting['creator_username'], + tags=tags ) # Add audio file path if exists @@ -906,4 +950,4 @@ def get_meeting_llm_tasks(meeting_id: int, current_user: dict = Depends(get_curr # } # except Exception as e: -# raise HTTPException(status_code=500, detail=f"Failed to get latest LLM task: {str(e)}") \ No newline at end of file +# raise HTTPException(status_code=500, detail=f"Failed to get latest LLM task: {str(e)}") diff --git a/app/api/endpoints/tags.py b/app/api/endpoints/tags.py new file mode 100644 index 0000000..fffd419 --- /dev/null +++ b/app/api/endpoints/tags.py @@ -0,0 +1,45 @@ + +from fastapi import APIRouter, HTTPException, Depends +from app.core.database import get_db_connection +from app.models.models import Tag +from typing import List +import mysql.connector + +router = APIRouter() + +@router.get("/tags/", response_model=List[Tag]) +def get_all_tags(): + """_summary_ + 获取所有标签 + """ + query = "SELECT id, name, color FROM tags ORDER BY name" + try: + with get_db_connection() as connection: + with connection.cursor(dictionary=True) as cursor: + cursor.execute(query) + tags = cursor.fetchall() + return tags + except mysql.connector.Error as err: + print(f"Error: {err}") + raise HTTPException(status_code=500, detail="Failed to retrieve tags from database.") + +@router.post("/tags/", response_model=Tag) +def create_tag(tag_in: Tag): + """_summary_ + 创建一个新标签 + """ + query = "INSERT INTO tags (name, color) VALUES (%s, %s)" + try: + with get_db_connection() as connection: + with connection.cursor(dictionary=True) as cursor: + try: + cursor.execute(query, (tag_in.name, tag_in.color)) + connection.commit() + tag_id = cursor.lastrowid + return {"id": tag_id, "name": tag_in.name, "color": tag_in.color} + except mysql.connector.IntegrityError: + connection.rollback() + raise HTTPException(status_code=400, detail=f"Tag '{tag_in.name}' already exists.") + except mysql.connector.Error as err: + print(f"Error: {err}") + raise HTTPException(status_code=500, detail="Failed to create tag in database.") diff --git a/app/models/models.py b/app/models/models.py index 5a2a808..6669d47 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,4 +1,3 @@ - from pydantic import BaseModel, EmailStr from typing import Optional, Union, List import datetime @@ -51,6 +50,11 @@ class AttendeeInfo(BaseModel): user_id: int caption: str +class Tag(BaseModel): + id: int + name: str + color: str + class TranscriptionTaskStatus(BaseModel): task_id: str status: str # 'pending', 'processing', 'completed', 'failed' @@ -72,6 +76,7 @@ class Meeting(BaseModel): creator_username: str audio_file_path: Optional[str] = None transcription_status: Optional[TranscriptionTaskStatus] = None + tags: Optional[List[Tag]] = [] class TranscriptSegment(BaseModel): segment_id: int @@ -87,12 +92,14 @@ class CreateMeetingRequest(BaseModel): title: str meeting_time: Optional[datetime.datetime] attendee_ids: list[int] + tags: Optional[str] = None class UpdateMeetingRequest(BaseModel): title: str meeting_time: Optional[datetime.datetime] summary: Optional[str] attendee_ids: list[int] + tags: Optional[str] = None class SpeakerTagUpdateRequest(BaseModel): speaker_id: int # 使用原始speaker_id(整数) @@ -110,4 +117,4 @@ class BatchTranscriptUpdateRequest(BaseModel): class PasswordChangeRequest(BaseModel): old_password: str - new_password: str + new_password: str \ No newline at end of file diff --git a/main.py b/main.py index 8d48397..8ef5fc9 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 +from app.api.endpoints import auth, users, meetings, tags from app.core.config import UPLOAD_DIR, API_CONFIG, MAX_FILE_SIZE from app.services.async_llm_service import async_llm_service import os @@ -30,6 +30,7 @@ if UPLOAD_DIR.exists(): 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.include_router(tags.router, prefix="/api", tags=["Tags"]) @app.get("/") def read_root():