diff --git a/.DS_Store b/.DS_Store index 2b4cb54..f67d325 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app.zip b/app.zip new file mode 100644 index 0000000..f7388fc Binary files /dev/null and b/app.zip differ diff --git a/app/.DS_Store b/app/.DS_Store index 3cf41ad..f935425 100644 Binary files a/app/.DS_Store and b/app/.DS_Store differ diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py index 6f8726b..45271e7 100644 --- a/app/api/endpoints/admin.py +++ b/app/api/endpoints/admin.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends from app.core.auth import get_current_admin_user, get_current_user -from app.core.config import LLM_CONFIG, DEFAULT_RESET_PASSWORD, MAX_FILE_SIZE, MAX_IMAGE_SIZE +from app.core.config import LLM_CONFIG, DEFAULT_RESET_PASSWORD, MAX_FILE_SIZE, VOICEPRINT_CONFIG, TIMELINE_PAGESIZE from app.core.response import create_api_response from pydantic import BaseModel import json @@ -13,10 +13,10 @@ CONFIG_FILE = Path(__file__).parent.parent.parent.parent / "config" / "system_co class SystemConfigModel(BaseModel): model_name: str - system_prompt: str + template_text: str DEFAULT_RESET_PASSWORD: str MAX_FILE_SIZE: int # 字节为单位 - MAX_IMAGE_SIZE: int # 字节为单位 + TIMELINE_PAGESIZE: int # 分页数量 def load_config_from_file(): """从文件加载配置,如果文件不存在则返回默认配置""" @@ -30,10 +30,10 @@ def load_config_from_file(): # 返回默认配置 return { 'model_name': LLM_CONFIG['model_name'], - 'system_prompt': LLM_CONFIG['system_prompt'], + 'template_text': VOICEPRINT_CONFIG['template_text'], 'DEFAULT_RESET_PASSWORD': DEFAULT_RESET_PASSWORD, 'MAX_FILE_SIZE': MAX_FILE_SIZE, - 'MAX_IMAGE_SIZE': MAX_IMAGE_SIZE + 'TIMELINE_PAGESIZE': TIMELINE_PAGESIZE } def save_config_to_file(config_data): @@ -57,10 +57,10 @@ async def get_system_config(current_user=Depends(get_current_user)): config = load_config_from_file() response_data = { 'model_name': config.get('model_name', LLM_CONFIG['model_name']), - 'system_prompt': config.get('system_prompt', LLM_CONFIG['system_prompt']), + 'template_text': config.get('template_text', VOICEPRINT_CONFIG['template_text']), 'DEFAULT_RESET_PASSWORD': config.get('DEFAULT_RESET_PASSWORD', DEFAULT_RESET_PASSWORD), 'MAX_FILE_SIZE': config.get('MAX_FILE_SIZE', MAX_FILE_SIZE), - 'MAX_IMAGE_SIZE': config.get('MAX_IMAGE_SIZE', MAX_IMAGE_SIZE), + 'TIMELINE_PAGESIZE': config.get('TIMELINE_PAGESIZE', TIMELINE_PAGESIZE), } return create_api_response(code="200", message="配置获取成功", data=response_data) except Exception as e: @@ -78,10 +78,10 @@ async def update_system_config( try: config_data = { 'model_name': config.model_name, - 'system_prompt': config.system_prompt, + 'template_text': config.template_text, 'DEFAULT_RESET_PASSWORD': config.DEFAULT_RESET_PASSWORD, 'MAX_FILE_SIZE': config.MAX_FILE_SIZE, - 'MAX_IMAGE_SIZE': config.MAX_IMAGE_SIZE + 'TIMELINE_PAGESIZE': config.TIMELINE_PAGESIZE } if not save_config_to_file(config_data): @@ -89,11 +89,11 @@ async def update_system_config( # 更新运行时配置 LLM_CONFIG['model_name'] = config.model_name - LLM_CONFIG['system_prompt'] = config.system_prompt + VOICEPRINT_CONFIG['template_text'] = config.template_text import app.core.config as config_module config_module.DEFAULT_RESET_PASSWORD = config.DEFAULT_RESET_PASSWORD config_module.MAX_FILE_SIZE = config.MAX_FILE_SIZE - config_module.MAX_IMAGE_SIZE = config.MAX_IMAGE_SIZE + config_module.TIMELINE_PAGESIZE = config.TIMELINE_PAGESIZE return create_api_response( code="200", @@ -109,11 +109,11 @@ def load_system_config(): try: config = load_config_from_file() LLM_CONFIG['model_name'] = config.get('model_name', LLM_CONFIG['model_name']) - LLM_CONFIG['system_prompt'] = config.get('system_prompt', LLM_CONFIG['system_prompt']) + VOICEPRINT_CONFIG['template_text'] = config.get('template_text', VOICEPRINT_CONFIG['template_text']) import app.core.config as config_module config_module.DEFAULT_RESET_PASSWORD = config.get('DEFAULT_RESET_PASSWORD', DEFAULT_RESET_PASSWORD) config_module.MAX_FILE_SIZE = config.get('MAX_FILE_SIZE', MAX_FILE_SIZE) - config_module.MAX_IMAGE_SIZE = config.get('MAX_IMAGE_SIZE', MAX_IMAGE_SIZE) - print(f"系统配置加载成功: model={config.get('model_name')}") + config_module.TIMELINE_PAGESIZE = config.get('TIMELINE_PAGESIZE', TIMELINE_PAGESIZE) + print(f"系统配置加载成功: model={config.get('model_name')}, pagesize={config.get('TIMELINE_PAGESIZE')}") except Exception as e: print(f"加载系统配置失败,使用默认配置: {e}") diff --git a/app/api/endpoints/meetings.py b/app/api/endpoints/meetings.py index ff632bf..3a7ae2b 100644 --- a/app/api/endpoints/meetings.py +++ b/app/api/endpoints/meetings.py @@ -45,34 +45,180 @@ def _process_tags(cursor, tag_string: Optional[str], creator_id: Optional[int] = return [Tag(**tag) for tag in tags_data] @router.get("/meetings") -def get_meetings(current_user: dict = Depends(get_current_user), user_id: Optional[int] = None): +def get_meetings( + current_user: dict = Depends(get_current_user), + user_id: Optional[int] = None, + page: int = 1, + page_size: Optional[int] = None, + search: Optional[str] = None, + tags: Optional[str] = None, + filter_type: str = "all" +): + from app.core.config import TIMELINE_PAGESIZE + + # 使用配置的默认页面大小 + if page_size is None: + page_size = TIMELINE_PAGESIZE + with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) + + # 构建WHERE子句 + where_conditions = [] + params = [] + + # 用户过滤 + if user_id: + # 需要联表查询参与者 + has_attendees_join = True + else: + has_attendees_join = False + + # 按类型过滤 (created/attended/all) + if user_id: + if filter_type == "created": + where_conditions.append("m.user_id = %s") + params.append(user_id) + elif filter_type == "attended": + where_conditions.append("m.user_id != %s AND a.user_id = %s") + params.extend([user_id, user_id]) + has_attendees_join = True + else: # all + where_conditions.append("(m.user_id = %s OR a.user_id = %s)") + params.extend([user_id, user_id]) + has_attendees_join = True + + # 搜索关键词过滤 + if search and search.strip(): + search_pattern = f"%{search.strip()}%" + where_conditions.append("(m.title LIKE %s OR u.caption LIKE %s)") + params.extend([search_pattern, search_pattern]) + + # 标签过滤 + if tags and tags.strip(): + tag_list = [t.strip() for t in tags.split(',') if t.strip()] + if tag_list: + # 使用JSON_CONTAINS或LIKE查询 + tag_conditions = [] + for tag in tag_list: + tag_conditions.append("m.tags LIKE %s") + params.append(f"%{tag}%") + where_conditions.append(f"({' OR '.join(tag_conditions)})") + + # 构建基础查询 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 - 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 + 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 ''' - 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)) + + if has_attendees_join: + base_query += " LEFT JOIN attendees a ON m.meeting_id = a.meeting_id" + + # 添加WHERE子句 + if where_conditions: + base_query += f" WHERE {' AND '.join(where_conditions)}" + + # 获取总数 - 需要在添加 GROUP BY 之前 + count_base = base_query # 保存一份不含GROUP BY的查询 + if has_attendees_join: + # 如果有联表,使用子查询计数 + count_query = f"SELECT COUNT(DISTINCT m.meeting_id) as total {count_base[count_base.find('FROM'):]}" else: - query = f" {base_query} ORDER BY m.meeting_time DESC, m.created_at DESC" - cursor.execute(query) + # 没有联表,直接计数 + count_query = f"SELECT COUNT(*) as total {count_base[count_base.find('FROM'):]}" + + cursor.execute(count_query, params) + total = cursor.fetchone()['total'] + + # 添加GROUP BY(如果联表了attendees) + if has_attendees_join: + base_query += " GROUP BY m.meeting_id" + + # 计算分页 + total_pages = (total + page_size - 1) // page_size + has_more = page < total_pages + offset = (page - 1) * page_size + + # 添加排序和分页 + query = f"{base_query} ORDER BY m.meeting_time DESC, m.created_at DESC LIMIT %s OFFSET %s" + params.extend([page_size, offset]) + + cursor.execute(query, params) 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] - tags = _process_tags(cursor, meeting.get('tags')) + tags_list = _process_tags(cursor, meeting.get('tags')) 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'], audio_file_path=meeting['audio_file_path'], - attendees=attendees, creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags + attendees=attendees, creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags_list )) - return create_api_response(code="200", message="获取会议列表成功", data=meeting_list) + + return create_api_response(code="200", message="获取会议列表成功", data={ + "meetings": meeting_list, + "total": total, + "page": page, + "page_size": page_size, + "total_pages": total_pages, + "has_more": has_more + }) + +@router.get("/meetings/stats") +def get_meetings_stats( + current_user: dict = Depends(get_current_user), + user_id: Optional[int] = None +): + """ + 获取会议统计数据:全部会议、我创建的会议、我参加的会议数量 + """ + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + if not user_id: + return create_api_response(code="400", message="user_id is required") + + # 获取全部会议数量(创建的 + 参加的) + all_query = ''' + SELECT COUNT(DISTINCT m.meeting_id) as count + FROM meetings m + LEFT JOIN attendees a ON m.meeting_id = a.meeting_id + WHERE m.user_id = %s OR a.user_id = %s + ''' + cursor.execute(all_query, (user_id, user_id)) + all_count = cursor.fetchone()['count'] + + # 获取我创建的会议数量 + created_query = ''' + SELECT COUNT(*) as count + FROM meetings m + WHERE m.user_id = %s + ''' + cursor.execute(created_query, (user_id,)) + created_count = cursor.fetchone()['count'] + + # 获取我参加的会议数量(不包括我创建的) + attended_query = ''' + SELECT COUNT(DISTINCT a.meeting_id) as count + FROM attendees a + JOIN meetings m ON a.meeting_id = m.meeting_id + WHERE a.user_id = %s AND m.user_id != %s + ''' + cursor.execute(attended_query, (user_id, user_id)) + attended_count = cursor.fetchone()['count'] + + return create_api_response(code="200", message="获取会议统计成功", data={ + "all_meetings": all_count, + "created_meetings": created_count, + "attended_meetings": attended_count + }) @router.get("/meetings/{meeting_id}") def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_current_user)): @@ -477,3 +623,53 @@ def get_meeting_llm_tasks(meeting_id: int, current_user: dict = Depends(get_curr }) except Exception as e: return create_api_response(code="500", message=f"Failed to get LLM tasks: {str(e)}") + +@router.get("/meetings/{meeting_id}/preview-data") +def get_meeting_preview_data(meeting_id: int): + """ + 获取会议预览数据(无需登录认证) + 用于二维码扫描后的预览页面 + """ + try: + with get_db_connection() as connection: + cursor = connection.cursor(dictionary=True) + + # 检查会议是否存在 + query = ''' + SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.updated_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 + WHERE m.meeting_id = %s + ''' + cursor.execute(query, (meeting_id,)) + meeting = cursor.fetchone() + + if not meeting: + return create_api_response(code="404", message="会议不存在") + + # 检查是否已生成会议总结 + if not meeting['summary'] or not meeting['updated_at']: + return create_api_response(code="400", message="该会议总结尚未生成") + + # 获取参会人员信息 + 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_id,)) + attendees_data = cursor.fetchall() + attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data] + + # 组装返回数据 + preview_data = { + "meeting_id": meeting['meeting_id'], + "title": meeting['title'], + "meeting_time": meeting['meeting_time'], + "summary": meeting['summary'], + "creator_username": meeting['creator_username'], + "attendees": attendees, + "attendees_count": len(attendees) + } + + return create_api_response(code="200", message="获取会议预览数据成功", data=preview_data) + + except Exception as e: + return create_api_response(code="500", message=f"Failed to get meeting preview data: {str(e)}") diff --git a/app/core/config.py b/app/core/config.py index d50d6f9..959e027 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -95,3 +95,6 @@ VOICEPRINT_CONFIG = { "sample_rate": 16000, "channels": 1 } + +#首页TimeLine每页数量 +TIMELINE_PAGESIZE=10 diff --git a/config/system_config.json b/config/system_config.json index 92bdf2a..67b500c 100644 --- a/config/system_config.json +++ b/config/system_config.json @@ -1,7 +1,7 @@ { "model_name": "qwen-plus", - "system_prompt": "你是一个专业的会议记录分析助手。请根据提供的会议转录内容,生成简洁明了的会议总结。\n\n总结包括五个部分(名称严格一致,生成为MD二级目录):\n1. 会议概述 - 简要说明会议的主要目的和背景(生成MD引用)\n2. 主要讨论点 - 列出会议中讨论的重要话题和内容\n3. 决策事项 - 明确记录会议中做出的决定和结论\n4. 待办事项 - 列出需要后续跟进的任务和责任人\n5. 关键信息 - 其他重要的信息点\n\n输出要求:\n- 保持客观中性,不添加个人观点\n- 使用简洁、准确的中文表达\n- 按重要性排序各项内容\n- 如果某个部分没有相关内容,可以说明\"无相关内容\"\n- 总字数控制在500字以内", + "template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n声纹技术能够识别每个人独特的声音特征,用于人声识别应用。", "DEFAULT_RESET_PASSWORD": "123456", "MAX_FILE_SIZE": 208666624, - "MAX_IMAGE_SIZE": 10485760 + "TIMELINE_PAGESIZE": 10 } \ No newline at end of file