v1.1.1
parent
ad16567e82
commit
41f71e649d
|
|
@ -1,391 +1,52 @@
|
|||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from app.core.auth import get_current_admin_user, get_current_user
|
||||
from app.core.response import create_api_response
|
||||
from app.core.database import get_db_connection
|
||||
from app.models.models import (
|
||||
MenuInfo,
|
||||
MenuListResponse,
|
||||
RolePermissionInfo,
|
||||
UpdateRolePermissionsRequest,
|
||||
RoleInfo,
|
||||
CreateMenuRequest,
|
||||
UpdateMenuRequest,
|
||||
CreateRoleRequest,
|
||||
UpdateMenuRequest,
|
||||
UpdateRolePermissionsRequest,
|
||||
UpdateRoleRequest,
|
||||
)
|
||||
from typing import List
|
||||
import time
|
||||
import app.services.admin_service as admin_service
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
_USER_MENU_CACHE_TTL_SECONDS = 120
|
||||
_USER_MENU_CACHE_VERSION = "menu-rules-v4"
|
||||
_user_menu_cache_by_role = {}
|
||||
|
||||
|
||||
def _get_cached_user_menus(role_id: int):
|
||||
cached = _user_menu_cache_by_role.get(role_id)
|
||||
if not cached:
|
||||
return None
|
||||
if cached.get("version") != _USER_MENU_CACHE_VERSION:
|
||||
_user_menu_cache_by_role.pop(role_id, None)
|
||||
return None
|
||||
if time.time() > cached["expires_at"]:
|
||||
_user_menu_cache_by_role.pop(role_id, None)
|
||||
return None
|
||||
return cached["menus"]
|
||||
|
||||
|
||||
def _set_cached_user_menus(role_id: int, menus):
|
||||
_user_menu_cache_by_role[role_id] = {
|
||||
"version": _USER_MENU_CACHE_VERSION,
|
||||
"menus": menus,
|
||||
"expires_at": time.time() + _USER_MENU_CACHE_TTL_SECONDS,
|
||||
}
|
||||
|
||||
|
||||
def _invalidate_user_menu_cache(role_id: int | None = None):
|
||||
if role_id is None:
|
||||
_user_menu_cache_by_role.clear()
|
||||
return
|
||||
_user_menu_cache_by_role.pop(role_id, None)
|
||||
|
||||
|
||||
def _build_menu_index(menus):
|
||||
menu_by_id = {}
|
||||
children_by_parent = {}
|
||||
for menu in menus:
|
||||
menu_id = menu["menu_id"]
|
||||
menu_by_id[menu_id] = menu
|
||||
parent_id = menu.get("parent_id")
|
||||
if parent_id is not None:
|
||||
children_by_parent.setdefault(parent_id, []).append(menu_id)
|
||||
return menu_by_id, children_by_parent
|
||||
|
||||
|
||||
def _get_descendants(menu_id, children_by_parent):
|
||||
result = set()
|
||||
stack = [menu_id]
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
for child_id in children_by_parent.get(current, []):
|
||||
if child_id in result:
|
||||
continue
|
||||
result.add(child_id)
|
||||
stack.append(child_id)
|
||||
return result
|
||||
|
||||
|
||||
def _normalize_permission_menu_ids(raw_menu_ids, all_menus):
|
||||
"""
|
||||
对权限菜单ID做归一化:
|
||||
1. 选中父节点 => 自动包含全部子孙节点
|
||||
2. 选中子节点 => 自动包含全部祖先节点
|
||||
"""
|
||||
menu_by_id, children_by_parent = _build_menu_index(all_menus)
|
||||
selected = {menu_id for menu_id in raw_menu_ids if menu_id in menu_by_id}
|
||||
|
||||
expanded = set(selected)
|
||||
|
||||
# 父 -> 子孙
|
||||
for menu_id in list(expanded):
|
||||
expanded.update(_get_descendants(menu_id, children_by_parent))
|
||||
|
||||
# 子 -> 祖先
|
||||
for menu_id in list(expanded):
|
||||
cursor = menu_by_id[menu_id].get("parent_id")
|
||||
while cursor is not None and cursor in menu_by_id:
|
||||
if cursor in expanded:
|
||||
break
|
||||
expanded.add(cursor)
|
||||
cursor = menu_by_id[cursor].get("parent_id")
|
||||
|
||||
return sorted(expanded)
|
||||
|
||||
# ========== 菜单权限管理接口 ==========
|
||||
|
||||
@router.get("/admin/menus")
|
||||
async def get_all_menus(current_user=Depends(get_current_admin_user)):
|
||||
"""
|
||||
获取所有菜单列表
|
||||
只有管理员才能访问
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT menu_id, menu_code, menu_name, menu_icon, menu_url, menu_type,
|
||||
parent_id, sort_order, is_active, description, created_at, updated_at
|
||||
FROM sys_menus
|
||||
ORDER BY
|
||||
COALESCE(parent_id, menu_id) ASC,
|
||||
CASE WHEN parent_id IS NULL THEN 0 ELSE 1 END ASC,
|
||||
sort_order ASC,
|
||||
menu_id ASC
|
||||
"""
|
||||
cursor.execute(query)
|
||||
menus = cursor.fetchall()
|
||||
|
||||
menu_list = [MenuInfo(**menu) for menu in menus]
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取菜单列表成功",
|
||||
data=MenuListResponse(menus=menu_list, total=len(menu_list))
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取菜单列表失败: {str(e)}")
|
||||
return admin_service.get_all_menus()
|
||||
|
||||
|
||||
@router.post("/admin/menus")
|
||||
async def create_menu(request: CreateMenuRequest, current_user=Depends(get_current_admin_user)):
|
||||
"""
|
||||
创建菜单
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("SELECT menu_id FROM sys_menus WHERE menu_code = %s", (request.menu_code,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="菜单编码已存在")
|
||||
|
||||
if request.parent_id is not None:
|
||||
cursor.execute("SELECT menu_id FROM sys_menus WHERE menu_id = %s", (request.parent_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="400", message="父菜单不存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO sys_menus (menu_code, menu_name, menu_icon, menu_url, menu_type, parent_id, sort_order, is_active, description)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
request.menu_code,
|
||||
request.menu_name,
|
||||
request.menu_icon,
|
||||
request.menu_url,
|
||||
request.menu_type,
|
||||
request.parent_id,
|
||||
request.sort_order,
|
||||
1 if request.is_active else 0,
|
||||
request.description,
|
||||
),
|
||||
)
|
||||
menu_id = cursor.lastrowid
|
||||
connection.commit()
|
||||
_invalidate_user_menu_cache()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT menu_id, menu_code, menu_name, menu_icon, menu_url, menu_type,
|
||||
parent_id, sort_order, is_active, description, created_at, updated_at
|
||||
FROM sys_menus
|
||||
WHERE menu_id = %s
|
||||
""",
|
||||
(menu_id,),
|
||||
)
|
||||
created = cursor.fetchone()
|
||||
return create_api_response(code="200", message="创建菜单成功", data=created)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建菜单失败: {str(e)}")
|
||||
return admin_service.create_menu(request)
|
||||
|
||||
|
||||
@router.put("/admin/menus/{menu_id}")
|
||||
async def update_menu(menu_id: int, request: UpdateMenuRequest, current_user=Depends(get_current_admin_user)):
|
||||
"""
|
||||
更新菜单
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("SELECT * FROM sys_menus WHERE menu_id = %s", (menu_id,))
|
||||
current = cursor.fetchone()
|
||||
if not current:
|
||||
return create_api_response(code="404", message="菜单不存在")
|
||||
|
||||
updates = {}
|
||||
for field in [
|
||||
"menu_code",
|
||||
"menu_name",
|
||||
"menu_icon",
|
||||
"menu_url",
|
||||
"menu_type",
|
||||
"sort_order",
|
||||
"description",
|
||||
]:
|
||||
value = getattr(request, field)
|
||||
if value is not None:
|
||||
updates[field] = value
|
||||
|
||||
if request.is_active is not None:
|
||||
updates["is_active"] = 1 if request.is_active else 0
|
||||
|
||||
fields_set = getattr(request, "model_fields_set", getattr(request, "__fields_set__", set()))
|
||||
|
||||
# parent_id 允许设为 null,且不允许设为自己
|
||||
if request.parent_id == menu_id:
|
||||
return create_api_response(code="400", message="父菜单不能为自身")
|
||||
if request.parent_id is not None:
|
||||
cursor.execute("SELECT menu_id FROM sys_menus WHERE menu_id = %s", (request.parent_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="400", message="父菜单不存在")
|
||||
|
||||
# 防止形成环:父菜单不能是当前菜单的子孙
|
||||
cursor.execute("SELECT menu_id, parent_id FROM sys_menus")
|
||||
all_menus = cursor.fetchall()
|
||||
_, children_by_parent = _build_menu_index(all_menus)
|
||||
descendants = _get_descendants(menu_id, children_by_parent)
|
||||
if request.parent_id in descendants:
|
||||
return create_api_response(code="400", message="父菜单不能设置为当前菜单的子孙菜单")
|
||||
|
||||
if request.parent_id is not None or (request.parent_id is None and "parent_id" in fields_set):
|
||||
updates["parent_id"] = request.parent_id
|
||||
|
||||
if "menu_code" in updates:
|
||||
cursor.execute(
|
||||
"SELECT menu_id FROM sys_menus WHERE menu_code = %s AND menu_id != %s",
|
||||
(updates["menu_code"], menu_id),
|
||||
)
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="菜单编码已存在")
|
||||
|
||||
if not updates:
|
||||
return create_api_response(code="200", message="没有变更内容", data=current)
|
||||
|
||||
set_sql = ", ".join([f"{k} = %s" for k in updates.keys()])
|
||||
values = list(updates.values()) + [menu_id]
|
||||
cursor.execute(f"UPDATE sys_menus SET {set_sql} WHERE menu_id = %s", tuple(values))
|
||||
connection.commit()
|
||||
_invalidate_user_menu_cache()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT menu_id, menu_code, menu_name, menu_icon, menu_url, menu_type,
|
||||
parent_id, sort_order, is_active, description, created_at, updated_at
|
||||
FROM sys_menus
|
||||
WHERE menu_id = %s
|
||||
""",
|
||||
(menu_id,),
|
||||
)
|
||||
updated = cursor.fetchone()
|
||||
return create_api_response(code="200", message="更新菜单成功", data=updated)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新菜单失败: {str(e)}")
|
||||
return admin_service.update_menu(menu_id, request)
|
||||
|
||||
|
||||
@router.delete("/admin/menus/{menu_id}")
|
||||
async def delete_menu(menu_id: int, current_user=Depends(get_current_admin_user)):
|
||||
"""
|
||||
删除菜单(有子菜单时不允许删除)
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
return admin_service.delete_menu(menu_id)
|
||||
|
||||
cursor.execute("SELECT menu_id FROM sys_menus WHERE menu_id = %s", (menu_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="菜单不存在")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) AS cnt FROM sys_menus WHERE parent_id = %s", (menu_id,))
|
||||
child_count = cursor.fetchone()["cnt"]
|
||||
if child_count > 0:
|
||||
return create_api_response(code="400", message="请先删除子菜单")
|
||||
|
||||
cursor.execute("DELETE FROM sys_role_menu_permissions WHERE menu_id = %s", (menu_id,))
|
||||
cursor.execute("DELETE FROM sys_menus WHERE menu_id = %s", (menu_id,))
|
||||
connection.commit()
|
||||
_invalidate_user_menu_cache()
|
||||
return create_api_response(code="200", message="删除菜单成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"删除菜单失败: {str(e)}")
|
||||
|
||||
@router.get("/admin/roles")
|
||||
async def get_all_roles(current_user=Depends(get_current_admin_user)):
|
||||
"""
|
||||
获取所有角色列表及其权限统计
|
||||
只有管理员才能访问
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 查询所有角色及其权限数量
|
||||
query = """
|
||||
SELECT r.role_id, r.role_name, r.created_at,
|
||||
COUNT(rmp.menu_id) as menu_count
|
||||
FROM sys_roles r
|
||||
LEFT JOIN sys_role_menu_permissions rmp ON r.role_id = rmp.role_id
|
||||
GROUP BY r.role_id
|
||||
ORDER BY r.role_id ASC
|
||||
"""
|
||||
cursor.execute(query)
|
||||
roles = cursor.fetchall()
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取角色列表成功",
|
||||
data={"roles": roles, "total": len(roles)}
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取角色列表失败: {str(e)}")
|
||||
return admin_service.get_all_roles()
|
||||
|
||||
|
||||
@router.post("/admin/roles")
|
||||
async def create_role(request: CreateRoleRequest, current_user=Depends(get_current_admin_user)):
|
||||
"""
|
||||
创建角色
|
||||
"""
|
||||
try:
|
||||
role_name = request.role_name.strip()
|
||||
if not role_name:
|
||||
return create_api_response(code="400", message="角色名称不能为空")
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT role_id FROM sys_roles WHERE role_name = %s", (role_name,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="角色名称已存在")
|
||||
|
||||
cursor.execute("INSERT INTO sys_roles (role_name) VALUES (%s)", (role_name,))
|
||||
role_id = cursor.lastrowid
|
||||
connection.commit()
|
||||
|
||||
cursor.execute("SELECT role_id, role_name, created_at FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
role = cursor.fetchone()
|
||||
return create_api_response(code="200", message="创建角色成功", data=role)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建角色失败: {str(e)}")
|
||||
return admin_service.create_role(request)
|
||||
|
||||
|
||||
@router.put("/admin/roles/{role_id}")
|
||||
async def update_role(role_id: int, request: UpdateRoleRequest, current_user=Depends(get_current_admin_user)):
|
||||
"""
|
||||
更新角色
|
||||
"""
|
||||
try:
|
||||
role_name = request.role_name.strip()
|
||||
if not role_name:
|
||||
return create_api_response(code="400", message="角色名称不能为空")
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT role_id FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="角色不存在")
|
||||
|
||||
cursor.execute("SELECT role_id FROM sys_roles WHERE role_name = %s AND role_id != %s", (role_name, role_id))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="角色名称已存在")
|
||||
|
||||
cursor.execute("UPDATE sys_roles SET role_name = %s WHERE role_id = %s", (role_name, role_id))
|
||||
connection.commit()
|
||||
|
||||
cursor.execute("SELECT role_id, role_name, created_at FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
role = cursor.fetchone()
|
||||
return create_api_response(code="200", message="更新角色成功", data=role)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新角色失败: {str(e)}")
|
||||
return admin_service.update_role(role_id, request)
|
||||
|
||||
|
||||
@router.get("/admin/roles/{role_id}/users")
|
||||
|
|
@ -395,253 +56,28 @@ async def get_role_users(
|
|||
size: int = Query(10, ge=1, le=100),
|
||||
current_user=Depends(get_current_admin_user),
|
||||
):
|
||||
"""
|
||||
获取角色下用户列表
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT role_id, role_name FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
role = cursor.fetchone()
|
||||
if not role:
|
||||
return create_api_response(code="404", message="角色不存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS total
|
||||
FROM sys_users
|
||||
WHERE role_id = %s
|
||||
""",
|
||||
(role_id,),
|
||||
)
|
||||
total = cursor.fetchone()["total"]
|
||||
offset = (page - 1) * size
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT user_id, username, caption, email, avatar_url, role_id, created_at
|
||||
FROM sys_users
|
||||
WHERE role_id = %s
|
||||
ORDER BY user_id ASC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
(role_id, size, offset),
|
||||
)
|
||||
users = cursor.fetchall()
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取角色用户成功",
|
||||
data={
|
||||
"role_id": role_id,
|
||||
"role_name": role["role_name"],
|
||||
"users": users,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取角色用户失败: {str(e)}")
|
||||
return admin_service.get_role_users(role_id, page, size)
|
||||
|
||||
|
||||
@router.get("/admin/roles/permissions/all")
|
||||
async def get_all_role_permissions(current_user=Depends(get_current_admin_user)):
|
||||
"""
|
||||
批量获取所有角色权限(用于减少N次请求)
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT rmp.role_id, rmp.menu_id
|
||||
FROM sys_role_menu_permissions rmp
|
||||
JOIN sys_menus m ON m.menu_id = rmp.menu_id
|
||||
WHERE m.is_active = 1
|
||||
ORDER BY rmp.role_id ASC, rmp.menu_id ASC
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
return admin_service.get_all_role_permissions()
|
||||
|
||||
result = {}
|
||||
for row in rows:
|
||||
result.setdefault(row["role_id"], []).append(row["menu_id"])
|
||||
return create_api_response(code="200", message="获取角色权限成功", data={"permissions": result})
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取角色权限失败: {str(e)}")
|
||||
|
||||
@router.get("/admin/roles/{role_id}/permissions")
|
||||
async def get_role_permissions(role_id: int, current_user=Depends(get_current_admin_user)):
|
||||
"""
|
||||
获取指定角色的菜单权限
|
||||
只有管理员才能访问
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
return admin_service.get_role_permissions(role_id)
|
||||
|
||||
# 检查角色是否存在
|
||||
cursor.execute("SELECT role_id, role_name FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
role = cursor.fetchone()
|
||||
if not role:
|
||||
return create_api_response(code="404", message="角色不存在")
|
||||
|
||||
# 查询该角色的所有菜单权限
|
||||
query = """
|
||||
SELECT rmp.menu_id
|
||||
FROM sys_role_menu_permissions rmp
|
||||
JOIN sys_menus m ON m.menu_id = rmp.menu_id
|
||||
WHERE rmp.role_id = %s
|
||||
AND m.is_active = 1
|
||||
"""
|
||||
cursor.execute(query, (role_id,))
|
||||
permissions = cursor.fetchall()
|
||||
|
||||
menu_ids = [p['menu_id'] for p in permissions]
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取角色权限成功",
|
||||
data=RolePermissionInfo(
|
||||
role_id=role['role_id'],
|
||||
role_name=role['role_name'],
|
||||
menu_ids=menu_ids
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取角色权限失败: {str(e)}")
|
||||
|
||||
@router.put("/admin/roles/{role_id}/permissions")
|
||||
async def update_role_permissions(
|
||||
role_id: int,
|
||||
request: UpdateRolePermissionsRequest,
|
||||
current_user=Depends(get_current_admin_user)
|
||||
current_user=Depends(get_current_admin_user),
|
||||
):
|
||||
"""
|
||||
更新指定角色的菜单权限
|
||||
只有管理员才能访问
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
return admin_service.update_role_permissions(role_id, request)
|
||||
|
||||
# 检查角色是否存在
|
||||
cursor.execute("SELECT role_id FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="角色不存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT menu_id, parent_id
|
||||
FROM sys_menus
|
||||
WHERE is_active = 1
|
||||
"""
|
||||
)
|
||||
all_menus = cursor.fetchall()
|
||||
menu_id_set = {menu["menu_id"] for menu in all_menus}
|
||||
|
||||
# 验证所有menu_id是否有效
|
||||
invalid_menu_ids = [menu_id for menu_id in request.menu_ids if menu_id not in menu_id_set]
|
||||
if invalid_menu_ids:
|
||||
return create_api_response(code="400", message="包含无效的菜单ID")
|
||||
|
||||
normalized_menu_ids = _normalize_permission_menu_ids(request.menu_ids, all_menus)
|
||||
|
||||
# 删除该角色的所有现有权限
|
||||
cursor.execute("DELETE FROM sys_role_menu_permissions WHERE role_id = %s", (role_id,))
|
||||
|
||||
# 插入新的权限
|
||||
if normalized_menu_ids:
|
||||
insert_values = [(role_id, menu_id) for menu_id in normalized_menu_ids]
|
||||
cursor.executemany(
|
||||
"INSERT INTO sys_role_menu_permissions (role_id, menu_id) VALUES (%s, %s)",
|
||||
insert_values
|
||||
)
|
||||
|
||||
connection.commit()
|
||||
_invalidate_user_menu_cache(role_id)
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="更新角色权限成功",
|
||||
data={"role_id": role_id, "menu_count": len(normalized_menu_ids)}
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新角色权限失败: {str(e)}")
|
||||
|
||||
@router.get("/menus/user")
|
||||
async def get_user_menus(current_user=Depends(get_current_user)):
|
||||
"""
|
||||
获取当前用户可访问的菜单列表(用于渲染下拉菜单)
|
||||
所有登录用户都可以访问
|
||||
"""
|
||||
try:
|
||||
role_id = current_user["role_id"]
|
||||
cached_menus = _get_cached_user_menus(role_id)
|
||||
if cached_menus is not None:
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取用户菜单成功",
|
||||
data={"menus": cached_menus}
|
||||
)
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 根据用户的role_id查询可访问的菜单
|
||||
query = """
|
||||
SELECT m.menu_id, m.menu_code, m.menu_name, m.menu_icon,
|
||||
m.menu_url, m.menu_type, m.parent_id, m.sort_order
|
||||
FROM sys_menus m
|
||||
JOIN sys_role_menu_permissions rmp ON m.menu_id = rmp.menu_id
|
||||
WHERE rmp.role_id = %s
|
||||
AND m.is_active = 1
|
||||
AND (m.is_visible = 1 OR m.is_visible IS NULL OR m.menu_code IN ('dashboard', 'desktop'))
|
||||
ORDER BY
|
||||
COALESCE(m.parent_id, m.menu_id) ASC,
|
||||
CASE WHEN m.parent_id IS NULL THEN 0 ELSE 1 END ASC,
|
||||
m.sort_order ASC,
|
||||
m.menu_id ASC
|
||||
"""
|
||||
cursor.execute(query, (role_id,))
|
||||
menus = cursor.fetchall()
|
||||
|
||||
# 仅在缺失父菜单时补查,减少不必要的SQL
|
||||
current_menu_ids = {menu["menu_id"] for menu in menus}
|
||||
missing_parent_ids = {
|
||||
menu["parent_id"] for menu in menus
|
||||
if menu.get("parent_id") is not None and menu["parent_id"] not in current_menu_ids
|
||||
}
|
||||
|
||||
if missing_parent_ids:
|
||||
format_strings = ",".join(["%s"] * len(missing_parent_ids))
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT menu_id, menu_code, menu_name, menu_icon, menu_url, menu_type, parent_id, sort_order
|
||||
FROM sys_menus
|
||||
WHERE is_active = 1 AND menu_id IN ({format_strings})
|
||||
""",
|
||||
tuple(missing_parent_ids),
|
||||
)
|
||||
parent_rows = cursor.fetchall()
|
||||
menus.extend(parent_rows)
|
||||
current_menu_ids.update(row["menu_id"] for row in parent_rows)
|
||||
|
||||
menus = sorted(
|
||||
{menu["menu_id"]: menu for menu in menus}.values(),
|
||||
key=lambda m: (
|
||||
m["parent_id"] if m["parent_id"] is not None else m["menu_id"],
|
||||
0 if m["parent_id"] is None else 1,
|
||||
m["sort_order"],
|
||||
m["menu_id"],
|
||||
),
|
||||
)
|
||||
_set_cached_user_menus(role_id, menus)
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取用户菜单成功",
|
||||
data={"menus": menus}
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取用户菜单失败: {str(e)}")
|
||||
return admin_service.get_user_menus(current_user)
|
||||
|
|
|
|||
|
|
@ -1,281 +1,25 @@
|
|||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from app.core.auth import get_current_admin_user
|
||||
from app.core.response import create_api_response
|
||||
from app.core.database import get_db_connection
|
||||
from app.services.jwt_service import jwt_service
|
||||
from app.core.config import AUDIO_DIR, REDIS_CONFIG
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
import os
|
||||
import redis
|
||||
import app.services.admin_dashboard_service as admin_dashboard_service
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Redis 客户端
|
||||
redis_client = redis.Redis(**REDIS_CONFIG)
|
||||
|
||||
# 常量定义
|
||||
AUDIO_FILE_EXTENSIONS = ('.wav', '.mp3', '.m4a', '.aac', '.flac', '.ogg', '.mpeg', '.mp4', '.webm')
|
||||
BYTES_TO_GB = 1024 ** 3
|
||||
|
||||
|
||||
def _build_status_condition(status: str) -> str:
|
||||
"""构建任务状态查询条件"""
|
||||
if status == 'running':
|
||||
return "AND (t.status = 'pending' OR t.status = 'processing')"
|
||||
elif status == 'completed':
|
||||
return "AND t.status = 'completed'"
|
||||
elif status == 'failed':
|
||||
return "AND t.status = 'failed'"
|
||||
return ""
|
||||
|
||||
|
||||
def _get_task_stats_query() -> str:
|
||||
"""获取任务统计的 SQL 查询"""
|
||||
return """
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'pending' OR status = 'processing' THEN 1 ELSE 0 END) as running,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
|
||||
"""
|
||||
|
||||
|
||||
def _get_online_user_count(redis_client) -> int:
|
||||
"""从 Redis 获取在线用户数"""
|
||||
try:
|
||||
token_keys = redis_client.keys("token:*")
|
||||
user_ids = set()
|
||||
for key in token_keys:
|
||||
if isinstance(key, bytes):
|
||||
key = key.decode("utf-8", errors="ignore")
|
||||
parts = key.split(':')
|
||||
if len(parts) >= 2:
|
||||
user_ids.add(parts[1])
|
||||
return len(user_ids)
|
||||
except Exception as e:
|
||||
print(f"获取在线用户数失败: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def _table_exists(cursor, table_name: str) -> bool:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS cnt
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = %s
|
||||
""",
|
||||
(table_name,),
|
||||
)
|
||||
return (cursor.fetchone() or {}).get("cnt", 0) > 0
|
||||
|
||||
|
||||
def _calculate_audio_storage() -> Dict[str, float]:
|
||||
"""计算音频文件存储统计"""
|
||||
audio_files_count = 0
|
||||
audio_total_size = 0
|
||||
|
||||
try:
|
||||
if os.path.exists(AUDIO_DIR):
|
||||
for root, _, files in os.walk(AUDIO_DIR):
|
||||
for file in files:
|
||||
file_extension = os.path.splitext(file)[1].lower()
|
||||
if file_extension in AUDIO_FILE_EXTENSIONS:
|
||||
audio_files_count += 1
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
audio_total_size += os.path.getsize(file_path)
|
||||
except OSError:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"统计音频文件失败: {e}")
|
||||
|
||||
return {
|
||||
"audio_file_count": audio_files_count,
|
||||
"audio_files_count": audio_files_count,
|
||||
"audio_total_size_gb": round(audio_total_size / BYTES_TO_GB, 2)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/admin/dashboard/stats")
|
||||
async def get_dashboard_stats(current_user=Depends(get_current_admin_user)):
|
||||
"""获取管理员 Dashboard 统计数据"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 1. 用户统计
|
||||
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
total_users = 0
|
||||
today_new_users = 0
|
||||
|
||||
if _table_exists(cursor, "sys_users"):
|
||||
cursor.execute("SELECT COUNT(*) as total FROM sys_users")
|
||||
total_users = (cursor.fetchone() or {}).get("total", 0)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) as count FROM sys_users WHERE created_at >= %s",
|
||||
(today_start,),
|
||||
)
|
||||
today_new_users = (cursor.fetchone() or {}).get("count", 0)
|
||||
|
||||
online_users = _get_online_user_count(redis_client)
|
||||
|
||||
# 2. 会议统计
|
||||
total_meetings = 0
|
||||
today_new_meetings = 0
|
||||
if _table_exists(cursor, "meetings"):
|
||||
cursor.execute("SELECT COUNT(*) as total FROM meetings")
|
||||
total_meetings = (cursor.fetchone() or {}).get("total", 0)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) as count FROM meetings WHERE created_at >= %s",
|
||||
(today_start,),
|
||||
)
|
||||
today_new_meetings = (cursor.fetchone() or {}).get("count", 0)
|
||||
|
||||
# 3. 任务统计
|
||||
task_stats_query = _get_task_stats_query()
|
||||
|
||||
# 转录任务
|
||||
if _table_exists(cursor, "transcript_tasks"):
|
||||
cursor.execute(f"{task_stats_query} FROM transcript_tasks")
|
||||
transcription_stats = cursor.fetchone() or {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
else:
|
||||
transcription_stats = {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
|
||||
# 总结任务
|
||||
if _table_exists(cursor, "llm_tasks"):
|
||||
cursor.execute(f"{task_stats_query} FROM llm_tasks")
|
||||
summary_stats = cursor.fetchone() or {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
else:
|
||||
summary_stats = {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
|
||||
# 知识库任务
|
||||
if _table_exists(cursor, "knowledge_base_tasks"):
|
||||
cursor.execute(f"{task_stats_query} FROM knowledge_base_tasks")
|
||||
kb_stats = cursor.fetchone() or {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
else:
|
||||
kb_stats = {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
|
||||
# 4. 音频存储统计
|
||||
storage_stats = _calculate_audio_storage()
|
||||
|
||||
# 组装返回数据
|
||||
stats = {
|
||||
"users": {
|
||||
"total": total_users,
|
||||
"today_new": today_new_users,
|
||||
"online": online_users
|
||||
},
|
||||
"meetings": {
|
||||
"total": total_meetings,
|
||||
"today_new": today_new_meetings
|
||||
},
|
||||
"tasks": {
|
||||
"transcription": {
|
||||
"total": transcription_stats['total'] or 0,
|
||||
"running": transcription_stats['running'] or 0,
|
||||
"completed": transcription_stats['completed'] or 0,
|
||||
"failed": transcription_stats['failed'] or 0
|
||||
},
|
||||
"summary": {
|
||||
"total": summary_stats['total'] or 0,
|
||||
"running": summary_stats['running'] or 0,
|
||||
"completed": summary_stats['completed'] or 0,
|
||||
"failed": summary_stats['failed'] or 0
|
||||
},
|
||||
"knowledge_base": {
|
||||
"total": kb_stats['total'] or 0,
|
||||
"running": kb_stats['running'] or 0,
|
||||
"completed": kb_stats['completed'] or 0,
|
||||
"failed": kb_stats['failed'] or 0
|
||||
}
|
||||
},
|
||||
"storage": storage_stats
|
||||
}
|
||||
|
||||
return create_api_response(code="200", message="获取统计数据成功", data=stats)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取Dashboard统计数据失败: {e}")
|
||||
return create_api_response(code="500", message=f"获取统计数据失败: {str(e)}")
|
||||
return await admin_dashboard_service.get_dashboard_stats(current_user)
|
||||
|
||||
|
||||
@router.get("/admin/online-users")
|
||||
async def get_online_users(current_user=Depends(get_current_admin_user)):
|
||||
"""获取在线用户列表"""
|
||||
try:
|
||||
token_keys = redis_client.keys("token:*")
|
||||
|
||||
# 提取用户ID并去重
|
||||
user_tokens = {}
|
||||
for key in token_keys:
|
||||
if isinstance(key, bytes):
|
||||
key = key.decode("utf-8", errors="ignore")
|
||||
parts = key.split(':')
|
||||
if len(parts) >= 3:
|
||||
user_id = int(parts[1])
|
||||
token = parts[2]
|
||||
if user_id not in user_tokens:
|
||||
user_tokens[user_id] = []
|
||||
user_tokens[user_id].append({'token': token, 'key': key})
|
||||
|
||||
# 查询用户信息
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
online_users_list = []
|
||||
for user_id, tokens in user_tokens.items():
|
||||
cursor.execute(
|
||||
"SELECT user_id, username, caption, email, role_id FROM sys_users WHERE user_id = %s",
|
||||
(user_id,)
|
||||
)
|
||||
user = cursor.fetchone()
|
||||
if user:
|
||||
ttl_seconds = redis_client.ttl(tokens[0]['key'])
|
||||
online_users_list.append({
|
||||
**user,
|
||||
'token_count': len(tokens),
|
||||
'ttl_seconds': ttl_seconds,
|
||||
'ttl_hours': round(ttl_seconds / 3600, 1) if ttl_seconds > 0 else 0
|
||||
})
|
||||
|
||||
# 按用户ID排序
|
||||
online_users_list.sort(key=lambda x: x['user_id'])
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取在线用户列表成功",
|
||||
data={"users": online_users_list, "total": len(online_users_list)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取在线用户列表失败: {e}")
|
||||
return create_api_response(code="500", message=f"获取在线用户列表失败: {str(e)}")
|
||||
return await admin_dashboard_service.get_online_users(current_user)
|
||||
|
||||
|
||||
@router.post("/admin/kick-user/{user_id}")
|
||||
async def kick_user(user_id: int, current_user=Depends(get_current_admin_user)):
|
||||
"""踢出用户(撤销该用户的所有 token)"""
|
||||
try:
|
||||
revoked_count = jwt_service.revoke_all_user_tokens(user_id)
|
||||
|
||||
if revoked_count > 0:
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message=f"已踢出用户,撤销了 {revoked_count} 个 token",
|
||||
data={"user_id": user_id, "revoked_count": revoked_count}
|
||||
)
|
||||
else:
|
||||
return create_api_response(
|
||||
code="404",
|
||||
message="该用户当前不在线或未找到 token"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"踢出用户失败: {e}")
|
||||
return create_api_response(code="500", message=f"踢出用户失败: {str(e)}")
|
||||
return await admin_dashboard_service.kick_user(user_id, current_user)
|
||||
|
||||
|
||||
@router.get("/admin/tasks/monitor")
|
||||
|
|
@ -285,207 +29,14 @@ async def monitor_tasks(
|
|||
limit: int = Query(20, ge=1, le=100, description="返回数量限制"),
|
||||
current_user=Depends(get_current_admin_user)
|
||||
):
|
||||
"""监控任务进度"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
tasks = []
|
||||
status_condition = _build_status_condition(status)
|
||||
|
||||
# 转录任务
|
||||
if task_type in ['all', 'transcription']:
|
||||
query = f"""
|
||||
SELECT
|
||||
t.task_id,
|
||||
'transcription' as task_type,
|
||||
t.meeting_id,
|
||||
m.title as meeting_title,
|
||||
t.status,
|
||||
t.progress,
|
||||
t.error_message,
|
||||
t.created_at,
|
||||
t.completed_at,
|
||||
u.username as creator_name
|
||||
FROM transcript_tasks t
|
||||
LEFT JOIN meetings m ON t.meeting_id = m.meeting_id
|
||||
LEFT JOIN sys_users u ON m.user_id = u.user_id
|
||||
WHERE 1=1 {status_condition}
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
cursor.execute(query, (limit,))
|
||||
tasks.extend(cursor.fetchall())
|
||||
|
||||
# 总结任务
|
||||
if task_type in ['all', 'summary']:
|
||||
query = f"""
|
||||
SELECT
|
||||
t.task_id,
|
||||
'summary' as task_type,
|
||||
t.meeting_id,
|
||||
m.title as meeting_title,
|
||||
t.status,
|
||||
t.progress,
|
||||
t.error_message,
|
||||
t.created_at,
|
||||
t.completed_at,
|
||||
u.username as creator_name
|
||||
FROM llm_tasks t
|
||||
LEFT JOIN meetings m ON t.meeting_id = m.meeting_id
|
||||
LEFT JOIN sys_users u ON m.user_id = u.user_id
|
||||
WHERE 1=1 {status_condition}
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
cursor.execute(query, (limit,))
|
||||
tasks.extend(cursor.fetchall())
|
||||
|
||||
# 知识库任务
|
||||
if task_type in ['all', 'knowledge_base']:
|
||||
query = f"""
|
||||
SELECT
|
||||
t.task_id,
|
||||
'knowledge_base' as task_type,
|
||||
t.kb_id as meeting_id,
|
||||
k.title as meeting_title,
|
||||
t.status,
|
||||
t.progress,
|
||||
t.error_message,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
u.username as creator_name
|
||||
FROM knowledge_base_tasks t
|
||||
LEFT JOIN knowledge_bases k ON t.kb_id = k.kb_id
|
||||
LEFT JOIN sys_users u ON k.creator_id = u.user_id
|
||||
WHERE 1=1 {status_condition}
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
cursor.execute(query, (limit,))
|
||||
tasks.extend(cursor.fetchall())
|
||||
|
||||
# 按创建时间排序并限制返回数量
|
||||
tasks.sort(key=lambda x: x['created_at'], reverse=True)
|
||||
tasks = tasks[:limit]
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取任务监控数据成功",
|
||||
data={"tasks": tasks, "total": len(tasks)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取任务监控数据失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return create_api_response(code="500", message=f"获取任务监控数据失败: {str(e)}")
|
||||
return await admin_dashboard_service.monitor_tasks(task_type, status, limit, current_user)
|
||||
|
||||
|
||||
@router.get("/admin/system/resources")
|
||||
async def get_system_resources(current_user=Depends(get_current_admin_user)):
|
||||
"""获取服务器资源使用情况"""
|
||||
try:
|
||||
import psutil
|
||||
|
||||
# CPU 使用率
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
cpu_count = psutil.cpu_count()
|
||||
|
||||
# 内存使用情况
|
||||
memory = psutil.virtual_memory()
|
||||
memory_total_gb = round(memory.total / BYTES_TO_GB, 2)
|
||||
memory_used_gb = round(memory.used / BYTES_TO_GB, 2)
|
||||
|
||||
# 磁盘使用情况
|
||||
disk = psutil.disk_usage('/')
|
||||
disk_total_gb = round(disk.total / BYTES_TO_GB, 2)
|
||||
disk_used_gb = round(disk.used / BYTES_TO_GB, 2)
|
||||
|
||||
resources = {
|
||||
"cpu": {
|
||||
"percent": cpu_percent,
|
||||
"count": cpu_count
|
||||
},
|
||||
"memory": {
|
||||
"total_gb": memory_total_gb,
|
||||
"used_gb": memory_used_gb,
|
||||
"percent": memory.percent
|
||||
},
|
||||
"disk": {
|
||||
"total_gb": disk_total_gb,
|
||||
"used_gb": disk_used_gb,
|
||||
"percent": disk.percent
|
||||
},
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return create_api_response(code="200", message="获取系统资源成功", data=resources)
|
||||
|
||||
except ImportError:
|
||||
return create_api_response(
|
||||
code="500",
|
||||
message="psutil 库未安装,请运行: pip install psutil"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"获取系统资源失败: {e}")
|
||||
return create_api_response(code="500", message=f"获取系统资源失败: {str(e)}")
|
||||
return await admin_dashboard_service.get_system_resources(current_user)
|
||||
|
||||
|
||||
@router.get("/admin/user-stats")
|
||||
async def get_user_stats(current_user=Depends(get_current_admin_user)):
|
||||
"""获取用户统计列表"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 查询所有用户及其会议统计和最后登录时间(排除没有会议的用户)
|
||||
query = """
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.username,
|
||||
u.caption,
|
||||
u.created_at,
|
||||
(SELECT MAX(created_at) FROM user_logs
|
||||
WHERE user_id = u.user_id AND action_type = 'login') as last_login_time,
|
||||
COUNT(DISTINCT m.meeting_id) as meeting_count,
|
||||
COALESCE(SUM(af.duration), 0) as total_duration_seconds
|
||||
FROM sys_users u
|
||||
INNER JOIN meetings m ON u.user_id = m.user_id
|
||||
LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id
|
||||
GROUP BY u.user_id, u.username, u.caption, u.created_at
|
||||
HAVING meeting_count > 0
|
||||
ORDER BY u.user_id ASC
|
||||
"""
|
||||
|
||||
cursor.execute(query)
|
||||
users = cursor.fetchall()
|
||||
|
||||
# 格式化返回数据
|
||||
users_list = []
|
||||
for user in users:
|
||||
total_seconds = int(user['total_duration_seconds']) if user['total_duration_seconds'] else 0
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
|
||||
users_list.append({
|
||||
'user_id': user['user_id'],
|
||||
'username': user['username'],
|
||||
'caption': user['caption'],
|
||||
'created_at': user['created_at'].isoformat() if user['created_at'] else None,
|
||||
'last_login_time': user['last_login_time'].isoformat() if user['last_login_time'] else None,
|
||||
'meeting_count': user['meeting_count'],
|
||||
'total_duration_seconds': total_seconds,
|
||||
'total_duration_formatted': f"{hours}h {minutes}m" if total_seconds > 0 else '-'
|
||||
})
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取用户统计成功",
|
||||
data={"users": users_list, "total": len(users_list)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取用户统计失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return create_api_response(code="500", message=f"获取用户统计失败: {str(e)}")
|
||||
return await admin_dashboard_service.get_user_stats(current_user)
|
||||
|
|
|
|||
|
|
@ -1,215 +1,13 @@
|
|||
import json
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.auth import get_current_admin_user
|
||||
from app.core.database import get_db_connection
|
||||
from app.core.response import create_api_response
|
||||
from app.services.async_transcription_service import AsyncTranscriptionService
|
||||
from app.services.llm_service import LLMService
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
import app.services.admin_settings_service as admin_settings_service
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
llm_service = LLMService()
|
||||
transcription_service = AsyncTranscriptionService()
|
||||
|
||||
|
||||
def _parse_json_object(value: Any) -> dict[str, Any]:
|
||||
if value is None:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
return dict(value)
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
def _normalize_string_list(value: Any) -> list[str] | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, list):
|
||||
values = [str(item).strip() for item in value if str(item).strip()]
|
||||
return values or None
|
||||
if isinstance(value, str):
|
||||
values = [item.strip() for item in value.split(",") if item.strip()]
|
||||
return values or None
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_int_list(value: Any) -> list[int] | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, list):
|
||||
items = value
|
||||
elif isinstance(value, str):
|
||||
items = [item.strip() for item in value.split(",") if item.strip()]
|
||||
else:
|
||||
return None
|
||||
|
||||
normalized = []
|
||||
for item in items:
|
||||
try:
|
||||
normalized.append(int(item))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return normalized or None
|
||||
|
||||
|
||||
def _clean_extra_config(config: dict[str, Any]) -> dict[str, Any]:
|
||||
cleaned: dict[str, Any] = {}
|
||||
for key, value in (config or {}).items():
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if stripped:
|
||||
cleaned[key] = stripped
|
||||
continue
|
||||
if isinstance(value, list):
|
||||
normalized_list = []
|
||||
for item in value:
|
||||
if item is None:
|
||||
continue
|
||||
if isinstance(item, str):
|
||||
stripped = item.strip()
|
||||
if stripped:
|
||||
normalized_list.append(stripped)
|
||||
else:
|
||||
normalized_list.append(item)
|
||||
if normalized_list:
|
||||
cleaned[key] = normalized_list
|
||||
continue
|
||||
cleaned[key] = value
|
||||
return cleaned
|
||||
|
||||
|
||||
def _merge_audio_extra_config(
|
||||
request: "AudioModelUpsertRequest",
|
||||
vocabulary_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
extra_config = _parse_json_object(request.extra_config)
|
||||
|
||||
if request.audio_scene == "asr":
|
||||
legacy_config = {
|
||||
"model": request.asr_model_name,
|
||||
"speaker_count": request.asr_speaker_count,
|
||||
"language_hints": request.asr_language_hints,
|
||||
"disfluency_removal_enabled": request.asr_disfluency_removal_enabled,
|
||||
"diarization_enabled": request.asr_diarization_enabled,
|
||||
}
|
||||
else:
|
||||
legacy_config = {
|
||||
"model": request.model_name,
|
||||
"template_text": request.vp_template_text,
|
||||
"duration_seconds": request.vp_duration_seconds,
|
||||
"sample_rate": request.vp_sample_rate,
|
||||
"channels": request.vp_channels,
|
||||
"max_size_bytes": request.vp_max_size_bytes,
|
||||
}
|
||||
|
||||
merged = {**legacy_config, **extra_config}
|
||||
|
||||
language_hints = _normalize_string_list(merged.get("language_hints"))
|
||||
if language_hints is not None:
|
||||
merged["language_hints"] = language_hints
|
||||
|
||||
channel_id = _normalize_int_list(merged.get("channel_id"))
|
||||
if channel_id is not None:
|
||||
merged["channel_id"] = channel_id
|
||||
|
||||
resolved_vocabulary_id = vocabulary_id or merged.get("vocabulary_id") or request.asr_vocabulary_id
|
||||
if request.audio_scene == "asr" and resolved_vocabulary_id:
|
||||
merged["vocabulary_id"] = resolved_vocabulary_id
|
||||
|
||||
return _clean_extra_config(merged)
|
||||
|
||||
|
||||
def _extract_legacy_audio_columns(audio_scene: str, extra_config: dict[str, Any]) -> dict[str, Any]:
|
||||
extra_config = _parse_json_object(extra_config)
|
||||
columns = {
|
||||
"asr_model_name": None,
|
||||
"asr_vocabulary_id": None,
|
||||
"asr_speaker_count": None,
|
||||
"asr_language_hints": None,
|
||||
"asr_disfluency_removal_enabled": None,
|
||||
"asr_diarization_enabled": None,
|
||||
"vp_template_text": None,
|
||||
"vp_duration_seconds": None,
|
||||
"vp_sample_rate": None,
|
||||
"vp_channels": None,
|
||||
"vp_max_size_bytes": None,
|
||||
}
|
||||
|
||||
if audio_scene == "asr":
|
||||
language_hints = extra_config.get("language_hints")
|
||||
if isinstance(language_hints, list):
|
||||
language_hints = ",".join(str(item).strip() for item in language_hints if str(item).strip())
|
||||
columns.update(
|
||||
{
|
||||
"asr_model_name": extra_config.get("model"),
|
||||
"asr_vocabulary_id": extra_config.get("vocabulary_id"),
|
||||
"asr_speaker_count": extra_config.get("speaker_count"),
|
||||
"asr_language_hints": language_hints,
|
||||
"asr_disfluency_removal_enabled": 1 if extra_config.get("disfluency_removal_enabled") is True else 0 if extra_config.get("disfluency_removal_enabled") is False else None,
|
||||
"asr_diarization_enabled": 1 if extra_config.get("diarization_enabled") is True else 0 if extra_config.get("diarization_enabled") is False else None,
|
||||
}
|
||||
)
|
||||
else:
|
||||
columns.update(
|
||||
{
|
||||
"vp_template_text": extra_config.get("template_text"),
|
||||
"vp_duration_seconds": extra_config.get("duration_seconds"),
|
||||
"vp_sample_rate": extra_config.get("sample_rate"),
|
||||
"vp_channels": extra_config.get("channels"),
|
||||
"vp_max_size_bytes": extra_config.get("max_size_bytes"),
|
||||
}
|
||||
)
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def _normalize_audio_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||
extra_config = _parse_json_object(row.get("extra_config"))
|
||||
|
||||
if row.get("audio_scene") == "asr":
|
||||
if extra_config.get("model") is None and row.get("asr_model_name") is not None:
|
||||
extra_config["model"] = row["asr_model_name"]
|
||||
if extra_config.get("vocabulary_id") is None and row.get("asr_vocabulary_id") is not None:
|
||||
extra_config["vocabulary_id"] = row["asr_vocabulary_id"]
|
||||
if extra_config.get("speaker_count") is None and row.get("asr_speaker_count") is not None:
|
||||
extra_config["speaker_count"] = row["asr_speaker_count"]
|
||||
if extra_config.get("language_hints") is None and row.get("asr_language_hints"):
|
||||
extra_config["language_hints"] = _normalize_string_list(row["asr_language_hints"])
|
||||
if extra_config.get("disfluency_removal_enabled") is None and row.get("asr_disfluency_removal_enabled") is not None:
|
||||
extra_config["disfluency_removal_enabled"] = bool(row["asr_disfluency_removal_enabled"])
|
||||
if extra_config.get("diarization_enabled") is None and row.get("asr_diarization_enabled") is not None:
|
||||
extra_config["diarization_enabled"] = bool(row["asr_diarization_enabled"])
|
||||
else:
|
||||
if extra_config.get("model") is None and row.get("model_name"):
|
||||
extra_config["model"] = row["model_name"]
|
||||
if extra_config.get("template_text") is None and row.get("vp_template_text") is not None:
|
||||
extra_config["template_text"] = row["vp_template_text"]
|
||||
if extra_config.get("duration_seconds") is None and row.get("vp_duration_seconds") is not None:
|
||||
extra_config["duration_seconds"] = row["vp_duration_seconds"]
|
||||
if extra_config.get("sample_rate") is None and row.get("vp_sample_rate") is not None:
|
||||
extra_config["sample_rate"] = row["vp_sample_rate"]
|
||||
if extra_config.get("channels") is None and row.get("vp_channels") is not None:
|
||||
extra_config["channels"] = row["vp_channels"]
|
||||
if extra_config.get("max_size_bytes") is None and row.get("vp_max_size_bytes") is not None:
|
||||
extra_config["max_size_bytes"] = row["vp_max_size_bytes"]
|
||||
|
||||
row["extra_config"] = extra_config
|
||||
row["service_model_name"] = extra_config.get("model")
|
||||
return row
|
||||
|
||||
|
||||
class ParameterUpsertRequest(BaseModel):
|
||||
|
|
@ -242,7 +40,7 @@ class LLMModelUpsertRequest(BaseModel):
|
|||
class AudioModelUpsertRequest(BaseModel):
|
||||
model_code: str
|
||||
model_name: str
|
||||
audio_scene: str # asr / voiceprint
|
||||
audio_scene: str
|
||||
provider: str | None = None
|
||||
endpoint_url: str | None = None
|
||||
api_key: str | None = None
|
||||
|
|
@ -278,89 +76,17 @@ async def list_parameters(
|
|||
keyword: str | None = Query(None),
|
||||
current_user=Depends(get_current_admin_user),
|
||||
):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT param_id, param_key, param_name, param_value, value_type, category,
|
||||
description, is_active, created_at, updated_at
|
||||
FROM sys_system_parameters
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
if category:
|
||||
query += " AND category = %s"
|
||||
params.append(category)
|
||||
if keyword:
|
||||
like_pattern = f"%{keyword}%"
|
||||
query += " AND (param_key LIKE %s OR param_name LIKE %s)"
|
||||
params.extend([like_pattern, like_pattern])
|
||||
|
||||
query += " ORDER BY category ASC, param_key ASC"
|
||||
cursor.execute(query, tuple(params))
|
||||
rows = cursor.fetchall()
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取参数列表成功",
|
||||
data={"items": rows, "total": len(rows)},
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取参数列表失败: {str(e)}")
|
||||
return admin_settings_service.list_parameters(category, keyword)
|
||||
|
||||
|
||||
@router.get("/admin/parameters/{param_key}")
|
||||
async def get_parameter(param_key: str, current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT param_id, param_key, param_name, param_value, value_type, category,
|
||||
description, is_active, created_at, updated_at
|
||||
FROM sys_system_parameters
|
||||
WHERE param_key = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(param_key,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return create_api_response(code="404", message="参数不存在")
|
||||
return create_api_response(code="200", message="获取参数成功", data=row)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取参数失败: {str(e)}")
|
||||
return admin_settings_service.get_parameter(param_key)
|
||||
|
||||
|
||||
@router.post("/admin/parameters")
|
||||
async def create_parameter(request: ParameterUpsertRequest, current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT param_id FROM sys_system_parameters WHERE param_key = %s", (request.param_key,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="参数键已存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO sys_system_parameters
|
||||
(param_key, param_name, param_value, value_type, category, description, is_active)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
request.param_key,
|
||||
request.param_name,
|
||||
request.param_value,
|
||||
request.value_type,
|
||||
request.category,
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
SystemConfigService.invalidate_cache()
|
||||
return create_api_response(code="200", message="创建参数成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建参数失败: {str(e)}")
|
||||
return admin_settings_service.create_parameter(request)
|
||||
|
||||
|
||||
@router.put("/admin/parameters/{param_key}")
|
||||
|
|
@ -369,131 +95,22 @@ async def update_parameter(
|
|||
request: ParameterUpsertRequest,
|
||||
current_user=Depends(get_current_admin_user),
|
||||
):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT param_id FROM sys_system_parameters WHERE param_key = %s", (param_key,))
|
||||
existed = cursor.fetchone()
|
||||
if not existed:
|
||||
return create_api_response(code="404", message="参数不存在")
|
||||
|
||||
new_key = request.param_key or param_key
|
||||
if new_key != param_key:
|
||||
cursor.execute("SELECT param_id FROM sys_system_parameters WHERE param_key = %s", (new_key,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="新的参数键已存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE sys_system_parameters
|
||||
SET param_key = %s, param_name = %s, param_value = %s, value_type = %s,
|
||||
category = %s, description = %s, is_active = %s
|
||||
WHERE param_key = %s
|
||||
""",
|
||||
(
|
||||
new_key,
|
||||
request.param_name,
|
||||
request.param_value,
|
||||
request.value_type,
|
||||
request.category,
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
param_key,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
SystemConfigService.invalidate_cache()
|
||||
return create_api_response(code="200", message="更新参数成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新参数失败: {str(e)}")
|
||||
return admin_settings_service.update_parameter(param_key, request)
|
||||
|
||||
|
||||
@router.delete("/admin/parameters/{param_key}")
|
||||
async def delete_parameter(param_key: str, current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT param_id FROM sys_system_parameters WHERE param_key = %s", (param_key,))
|
||||
existed = cursor.fetchone()
|
||||
if not existed:
|
||||
return create_api_response(code="404", message="参数不存在")
|
||||
|
||||
cursor.execute("DELETE FROM sys_system_parameters WHERE param_key = %s", (param_key,))
|
||||
conn.commit()
|
||||
SystemConfigService.invalidate_cache()
|
||||
return create_api_response(code="200", message="删除参数成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"删除参数失败: {str(e)}")
|
||||
return admin_settings_service.delete_parameter(param_key)
|
||||
|
||||
|
||||
@router.get("/admin/model-configs/llm")
|
||||
async def list_llm_model_configs(current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT config_id, model_code, model_name, provider, endpoint_url, api_key,
|
||||
llm_model_name, llm_timeout, llm_temperature, llm_top_p, llm_max_tokens,
|
||||
llm_system_prompt, description, is_active, is_default, created_at, updated_at
|
||||
FROM llm_model_config
|
||||
ORDER BY model_code ASC
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取LLM模型配置成功",
|
||||
data={"items": rows, "total": len(rows)},
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取LLM模型配置失败: {str(e)}")
|
||||
return admin_settings_service.list_llm_model_configs()
|
||||
|
||||
|
||||
@router.post("/admin/model-configs/llm")
|
||||
async def create_llm_model_config(request: LLMModelUpsertRequest, current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM llm_model_config WHERE model_code = %s", (request.model_code,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="模型编码已存在")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) AS total FROM llm_model_config")
|
||||
total_row = cursor.fetchone() or {"total": 0}
|
||||
is_default = bool(request.is_default) or total_row["total"] == 0
|
||||
if is_default:
|
||||
cursor.execute("UPDATE llm_model_config SET is_default = 0 WHERE is_default = 1")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO llm_model_config
|
||||
(model_code, model_name, provider, endpoint_url, api_key, llm_model_name,
|
||||
llm_timeout, llm_temperature, llm_top_p, llm_max_tokens, llm_system_prompt,
|
||||
description, is_active, is_default)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
request.model_code,
|
||||
request.model_name,
|
||||
request.provider,
|
||||
request.endpoint_url,
|
||||
request.api_key,
|
||||
request.llm_model_name,
|
||||
request.llm_timeout,
|
||||
request.llm_temperature,
|
||||
request.llm_top_p,
|
||||
request.llm_max_tokens,
|
||||
request.llm_system_prompt,
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
1 if is_default else 0,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="创建LLM模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建LLM模型配置失败: {str(e)}")
|
||||
return admin_settings_service.create_llm_model_config(request)
|
||||
|
||||
|
||||
@router.put("/admin/model-configs/llm/{model_code}")
|
||||
|
|
@ -502,54 +119,7 @@ async def update_llm_model_config(
|
|||
request: LLMModelUpsertRequest,
|
||||
current_user=Depends(get_current_admin_user),
|
||||
):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM llm_model_config WHERE model_code = %s", (model_code,))
|
||||
existed = cursor.fetchone()
|
||||
if not existed:
|
||||
return create_api_response(code="404", message="模型配置不存在")
|
||||
|
||||
new_model_code = request.model_code or model_code
|
||||
if new_model_code != model_code:
|
||||
cursor.execute("SELECT config_id FROM llm_model_config WHERE model_code = %s", (new_model_code,))
|
||||
duplicate_row = cursor.fetchone()
|
||||
if duplicate_row and duplicate_row["config_id"] != existed["config_id"]:
|
||||
return create_api_response(code="400", message="新的模型编码已存在")
|
||||
|
||||
if request.is_default:
|
||||
cursor.execute("UPDATE llm_model_config SET is_default = 0 WHERE model_code <> %s AND is_default = 1", (model_code,))
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE llm_model_config
|
||||
SET model_code = %s, model_name = %s, provider = %s, endpoint_url = %s, api_key = %s,
|
||||
llm_model_name = %s, llm_timeout = %s, llm_temperature = %s, llm_top_p = %s,
|
||||
llm_max_tokens = %s, llm_system_prompt = %s, description = %s, is_active = %s, is_default = %s
|
||||
WHERE model_code = %s
|
||||
""",
|
||||
(
|
||||
new_model_code,
|
||||
request.model_name,
|
||||
request.provider,
|
||||
request.endpoint_url,
|
||||
request.api_key,
|
||||
request.llm_model_name,
|
||||
request.llm_timeout,
|
||||
request.llm_temperature,
|
||||
request.llm_top_p,
|
||||
request.llm_max_tokens,
|
||||
request.llm_system_prompt,
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
1 if request.is_default else 0,
|
||||
model_code,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="更新LLM模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新LLM模型配置失败: {str(e)}")
|
||||
return admin_settings_service.update_llm_model_config(model_code, request)
|
||||
|
||||
|
||||
@router.get("/admin/model-configs/audio")
|
||||
|
|
@ -557,97 +127,12 @@ async def list_audio_model_configs(
|
|||
scene: str = Query("all"),
|
||||
current_user=Depends(get_current_admin_user),
|
||||
):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
sql = """
|
||||
SELECT a.config_id, a.model_code, a.model_name, a.audio_scene, a.provider, a.endpoint_url, a.api_key,
|
||||
a.asr_model_name, a.asr_vocabulary_id, a.hot_word_group_id, a.asr_speaker_count, a.asr_language_hints,
|
||||
a.asr_disfluency_removal_enabled, a.asr_diarization_enabled,
|
||||
a.vp_template_text, a.vp_duration_seconds, a.vp_sample_rate, a.vp_channels, a.vp_max_size_bytes,
|
||||
a.extra_config, a.description, a.is_active, a.is_default, a.created_at, a.updated_at,
|
||||
g.name AS hot_word_group_name, g.vocabulary_id AS hot_word_group_vocab_id
|
||||
FROM audio_model_config a
|
||||
LEFT JOIN hot_word_group g ON g.id = a.hot_word_group_id
|
||||
"""
|
||||
params = []
|
||||
if scene in ("asr", "voiceprint"):
|
||||
sql += " WHERE a.audio_scene = %s"
|
||||
params.append(scene)
|
||||
sql += " ORDER BY a.audio_scene ASC, a.model_code ASC"
|
||||
cursor.execute(sql, tuple(params))
|
||||
rows = [_normalize_audio_row(row) for row in cursor.fetchall()]
|
||||
return create_api_response(code="200", message="获取音频模型配置成功", data={"items": rows, "total": len(rows)})
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取音频模型配置失败: {str(e)}")
|
||||
return admin_settings_service.list_audio_model_configs(scene)
|
||||
|
||||
|
||||
@router.post("/admin/model-configs/audio")
|
||||
async def create_audio_model_config(request: AudioModelUpsertRequest, current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
if request.audio_scene not in ("asr", "voiceprint"):
|
||||
return create_api_response(code="400", message="audio_scene 仅支持 asr 或 voiceprint")
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM audio_model_config WHERE model_code = %s", (request.model_code,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="模型编码已存在")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) AS total FROM audio_model_config WHERE audio_scene = %s", (request.audio_scene,))
|
||||
total_row = cursor.fetchone() or {"total": 0}
|
||||
is_default = bool(request.is_default) or total_row["total"] == 0
|
||||
if is_default:
|
||||
cursor.execute("UPDATE audio_model_config SET is_default = 0 WHERE audio_scene = %s AND is_default = 1", (request.audio_scene,))
|
||||
|
||||
# 如果指定了热词组,从组中获取 vocabulary_id
|
||||
asr_vocabulary_id = request.asr_vocabulary_id
|
||||
if request.hot_word_group_id:
|
||||
cursor.execute("SELECT vocabulary_id FROM hot_word_group WHERE id = %s", (request.hot_word_group_id,))
|
||||
group_row = cursor.fetchone()
|
||||
if group_row and group_row.get("vocabulary_id"):
|
||||
asr_vocabulary_id = group_row["vocabulary_id"]
|
||||
extra_config = _merge_audio_extra_config(request, vocabulary_id=asr_vocabulary_id)
|
||||
legacy_columns = _extract_legacy_audio_columns(request.audio_scene, extra_config)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO audio_model_config
|
||||
(model_code, model_name, audio_scene, provider, endpoint_url, api_key,
|
||||
asr_model_name, asr_vocabulary_id, hot_word_group_id, asr_speaker_count, asr_language_hints,
|
||||
asr_disfluency_removal_enabled, asr_diarization_enabled,
|
||||
vp_template_text, vp_duration_seconds, vp_sample_rate, vp_channels, vp_max_size_bytes,
|
||||
extra_config, description, is_active, is_default)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
request.model_code,
|
||||
request.model_name,
|
||||
request.audio_scene,
|
||||
request.provider,
|
||||
request.endpoint_url,
|
||||
request.api_key,
|
||||
legacy_columns["asr_model_name"],
|
||||
legacy_columns["asr_vocabulary_id"],
|
||||
request.hot_word_group_id,
|
||||
legacy_columns["asr_speaker_count"],
|
||||
legacy_columns["asr_language_hints"],
|
||||
legacy_columns["asr_disfluency_removal_enabled"],
|
||||
legacy_columns["asr_diarization_enabled"],
|
||||
legacy_columns["vp_template_text"],
|
||||
legacy_columns["vp_duration_seconds"],
|
||||
legacy_columns["vp_sample_rate"],
|
||||
legacy_columns["vp_channels"],
|
||||
legacy_columns["vp_max_size_bytes"],
|
||||
json.dumps(extra_config, ensure_ascii=False),
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
1 if is_default else 0,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="创建音频模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建音频模型配置失败: {str(e)}")
|
||||
return admin_settings_service.create_audio_model_config(request)
|
||||
|
||||
|
||||
@router.put("/admin/model-configs/audio/{model_code}")
|
||||
|
|
@ -656,196 +141,34 @@ async def update_audio_model_config(
|
|||
request: AudioModelUpsertRequest,
|
||||
current_user=Depends(get_current_admin_user),
|
||||
):
|
||||
try:
|
||||
if request.audio_scene not in ("asr", "voiceprint"):
|
||||
return create_api_response(code="400", message="audio_scene 仅支持 asr 或 voiceprint")
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM audio_model_config WHERE model_code = %s", (model_code,))
|
||||
existed = cursor.fetchone()
|
||||
if not existed:
|
||||
return create_api_response(code="404", message="模型配置不存在")
|
||||
|
||||
new_model_code = request.model_code or model_code
|
||||
if new_model_code != model_code:
|
||||
cursor.execute("SELECT config_id FROM audio_model_config WHERE model_code = %s", (new_model_code,))
|
||||
duplicate_row = cursor.fetchone()
|
||||
if duplicate_row and duplicate_row["config_id"] != existed["config_id"]:
|
||||
return create_api_response(code="400", message="新的模型编码已存在")
|
||||
|
||||
if request.is_default:
|
||||
cursor.execute(
|
||||
"UPDATE audio_model_config SET is_default = 0 WHERE audio_scene = %s AND model_code <> %s AND is_default = 1",
|
||||
(request.audio_scene, model_code),
|
||||
)
|
||||
|
||||
# 如果指定了热词组,从组中获取 vocabulary_id
|
||||
asr_vocabulary_id = request.asr_vocabulary_id
|
||||
if request.hot_word_group_id:
|
||||
cursor.execute("SELECT vocabulary_id FROM hot_word_group WHERE id = %s", (request.hot_word_group_id,))
|
||||
group_row = cursor.fetchone()
|
||||
if group_row and group_row.get("vocabulary_id"):
|
||||
asr_vocabulary_id = group_row["vocabulary_id"]
|
||||
extra_config = _merge_audio_extra_config(request, vocabulary_id=asr_vocabulary_id)
|
||||
legacy_columns = _extract_legacy_audio_columns(request.audio_scene, extra_config)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE audio_model_config
|
||||
SET model_code = %s, model_name = %s, audio_scene = %s, provider = %s, endpoint_url = %s, api_key = %s,
|
||||
asr_model_name = %s, asr_vocabulary_id = %s, hot_word_group_id = %s, asr_speaker_count = %s, asr_language_hints = %s,
|
||||
asr_disfluency_removal_enabled = %s, asr_diarization_enabled = %s,
|
||||
vp_template_text = %s, vp_duration_seconds = %s, vp_sample_rate = %s, vp_channels = %s, vp_max_size_bytes = %s,
|
||||
extra_config = %s, description = %s, is_active = %s, is_default = %s
|
||||
WHERE model_code = %s
|
||||
""",
|
||||
(
|
||||
new_model_code,
|
||||
request.model_name,
|
||||
request.audio_scene,
|
||||
request.provider,
|
||||
request.endpoint_url,
|
||||
request.api_key,
|
||||
legacy_columns["asr_model_name"],
|
||||
legacy_columns["asr_vocabulary_id"],
|
||||
request.hot_word_group_id,
|
||||
legacy_columns["asr_speaker_count"],
|
||||
legacy_columns["asr_language_hints"],
|
||||
legacy_columns["asr_disfluency_removal_enabled"],
|
||||
legacy_columns["asr_diarization_enabled"],
|
||||
legacy_columns["vp_template_text"],
|
||||
legacy_columns["vp_duration_seconds"],
|
||||
legacy_columns["vp_sample_rate"],
|
||||
legacy_columns["vp_channels"],
|
||||
legacy_columns["vp_max_size_bytes"],
|
||||
json.dumps(extra_config, ensure_ascii=False),
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
1 if request.is_default else 0,
|
||||
model_code,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="更新音频模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新音频模型配置失败: {str(e)}")
|
||||
return admin_settings_service.update_audio_model_config(model_code, request)
|
||||
|
||||
|
||||
@router.delete("/admin/model-configs/llm/{model_code}")
|
||||
async def delete_llm_model_config(model_code: str, current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM llm_model_config WHERE model_code = %s", (model_code,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="模型配置不存在")
|
||||
|
||||
cursor.execute("DELETE FROM llm_model_config WHERE model_code = %s", (model_code,))
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="删除LLM模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"删除LLM模型配置失败: {str(e)}")
|
||||
return admin_settings_service.delete_llm_model_config(model_code)
|
||||
|
||||
|
||||
@router.delete("/admin/model-configs/audio/{model_code}")
|
||||
async def delete_audio_model_config(model_code: str, current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM audio_model_config WHERE model_code = %s", (model_code,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="模型配置不存在")
|
||||
|
||||
cursor.execute("DELETE FROM audio_model_config WHERE model_code = %s", (model_code,))
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="删除音频模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"删除音频模型配置失败: {str(e)}")
|
||||
return admin_settings_service.delete_audio_model_config(model_code)
|
||||
|
||||
|
||||
@router.post("/admin/model-configs/llm/test")
|
||||
async def test_llm_model_config(request: LLMModelTestRequest, current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
payload = request.model_dump() if hasattr(request, "model_dump") else request.dict()
|
||||
result = llm_service.test_model(payload, prompt=request.test_prompt)
|
||||
return create_api_response(code="200", message="LLM模型测试成功", data=result)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"LLM模型测试失败: {str(e)}")
|
||||
return admin_settings_service.test_llm_model_config(request)
|
||||
|
||||
|
||||
@router.post("/admin/model-configs/audio/test")
|
||||
async def test_audio_model_config(request: AudioModelTestRequest, current_user=Depends(get_current_admin_user)):
|
||||
try:
|
||||
if request.audio_scene != "asr":
|
||||
return create_api_response(code="400", message="当前仅支持音频识别(ASR)测试")
|
||||
|
||||
vocabulary_id = request.asr_vocabulary_id
|
||||
if request.hot_word_group_id:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT vocabulary_id FROM hot_word_group WHERE id = %s", (request.hot_word_group_id,))
|
||||
group_row = cursor.fetchone()
|
||||
cursor.close()
|
||||
if group_row and group_row.get("vocabulary_id"):
|
||||
vocabulary_id = group_row["vocabulary_id"]
|
||||
|
||||
extra_config = _merge_audio_extra_config(request, vocabulary_id=vocabulary_id)
|
||||
runtime_config = {
|
||||
"provider": request.provider,
|
||||
"endpoint_url": request.endpoint_url,
|
||||
"api_key": request.api_key,
|
||||
"audio_scene": request.audio_scene,
|
||||
"hot_word_group_id": request.hot_word_group_id,
|
||||
**extra_config,
|
||||
}
|
||||
result = transcription_service.test_asr_model(runtime_config, test_file_url=request.test_file_url)
|
||||
return create_api_response(code="200", message="音频模型测试任务已提交", data=result)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"音频模型测试失败: {str(e)}")
|
||||
return admin_settings_service.test_audio_model_config(request)
|
||||
|
||||
|
||||
@router.get("/system-config/public")
|
||||
async def get_public_system_config():
|
||||
try:
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取公开配置成功",
|
||||
data=SystemConfigService.get_public_configs()
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取公开配置失败: {str(e)}")
|
||||
return admin_settings_service.get_public_system_config()
|
||||
|
||||
|
||||
@router.get("/admin/system-config")
|
||||
async def get_system_config_compat(current_user=Depends(get_current_admin_user)):
|
||||
"""兼容旧前端的系统配置接口,数据来源为 sys_system_parameters。"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT param_key, param_value
|
||||
FROM sys_system_parameters
|
||||
WHERE is_active = 1
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
data = {row["param_key"]: row["param_value"] for row in rows}
|
||||
|
||||
# 兼容旧字段
|
||||
if "max_audio_size" in data:
|
||||
try:
|
||||
data["MAX_FILE_SIZE"] = int(data["max_audio_size"]) * 1024 * 1024
|
||||
except Exception:
|
||||
data["MAX_FILE_SIZE"] = 100 * 1024 * 1024
|
||||
if "max_image_size" in data:
|
||||
try:
|
||||
data["MAX_IMAGE_SIZE"] = int(data["max_image_size"]) * 1024 * 1024
|
||||
except Exception:
|
||||
data["MAX_IMAGE_SIZE"] = 10 * 1024 * 1024
|
||||
else:
|
||||
data.setdefault("MAX_IMAGE_SIZE", 10 * 1024 * 1024)
|
||||
|
||||
return create_api_response(code="200", message="获取系统配置成功", data=data)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取系统配置失败: {str(e)}")
|
||||
return admin_settings_service.get_system_config_compat()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,90 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.openapi.docs import get_swagger_ui_html
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.api.endpoints import (
|
||||
admin,
|
||||
admin_dashboard,
|
||||
admin_settings,
|
||||
audio,
|
||||
auth,
|
||||
client_downloads,
|
||||
dict_data,
|
||||
external_apps,
|
||||
hot_words,
|
||||
knowledge_base,
|
||||
meetings,
|
||||
prompts,
|
||||
tags,
|
||||
tasks,
|
||||
terminals,
|
||||
users,
|
||||
voiceprint,
|
||||
)
|
||||
from app.core.config import UPLOAD_DIR
|
||||
from app.core.middleware import TerminalCheckMiddleware
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(
|
||||
title="iMeeting API",
|
||||
description="iMeeting API说明",
|
||||
version="1.1.0",
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
)
|
||||
|
||||
app.add_middleware(TerminalCheckMiddleware)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
if UPLOAD_DIR.exists():
|
||||
app.mount("/uploads", StaticFiles(directory=str(UPLOAD_DIR)), name="uploads")
|
||||
|
||||
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.include_router(admin.router, prefix="/api", tags=["Admin"])
|
||||
app.include_router(admin_dashboard.router, prefix="/api", tags=["AdminDashboard"])
|
||||
app.include_router(admin_settings.router, prefix="/api", tags=["AdminSettings"])
|
||||
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(external_apps.router, prefix="/api", tags=["ExternalApps"])
|
||||
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"])
|
||||
app.include_router(hot_words.router, prefix="/api", tags=["HotWords"])
|
||||
app.include_router(terminals.router, prefix="/api", tags=["Terminals"])
|
||||
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
async def custom_swagger_ui_html():
|
||||
return get_swagger_ui_html(
|
||||
openapi_url=app.openapi_url,
|
||||
title=app.title + " - Swagger UI",
|
||||
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
|
||||
swagger_js_url="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js",
|
||||
swagger_css_url="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css",
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"message": "Welcome to iMeeting API"}
|
||||
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "iMeeting API",
|
||||
"version": "1.1.0",
|
||||
}
|
||||
|
||||
return app
|
||||
|
|
@ -1,110 +1,22 @@
|
|||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
# 无论从哪里运行,都能正确找到 app 模块
|
||||
current_file = Path(__file__).resolve()
|
||||
project_root = current_file.parent.parent # backend/
|
||||
project_root = current_file.parent.parent
|
||||
if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.openapi.docs import get_swagger_ui_html
|
||||
from app.core.middleware import TerminalCheckMiddleware
|
||||
from app.api.endpoints import (
|
||||
auth,
|
||||
users,
|
||||
meetings,
|
||||
tags,
|
||||
admin,
|
||||
admin_dashboard,
|
||||
admin_settings,
|
||||
tasks,
|
||||
prompts,
|
||||
knowledge_base,
|
||||
client_downloads,
|
||||
voiceprint,
|
||||
audio,
|
||||
dict_data,
|
||||
hot_words,
|
||||
external_apps,
|
||||
terminals,
|
||||
)
|
||||
from app.core.config import UPLOAD_DIR, API_CONFIG
|
||||
|
||||
app = FastAPI(
|
||||
title="iMeeting API",
|
||||
description="iMeeting API说明",
|
||||
version="1.1.0",
|
||||
docs_url=None, # 禁用默认docs,使用自定义CDN
|
||||
redoc_url=None
|
||||
)
|
||||
from app.app_factory import create_app
|
||||
from app.core.config import API_CONFIG
|
||||
|
||||
# 添加终端检查中间件 (在CORS之前添加,以便位于CORS内部)
|
||||
app.add_middleware(TerminalCheckMiddleware)
|
||||
|
||||
# 添加CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app = create_app()
|
||||
|
||||
# 静态文件服务 - 提供音频文件下载
|
||||
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.include_router(tags.router, prefix="/api", tags=["Tags"])
|
||||
app.include_router(admin.router, prefix="/api", tags=["Admin"])
|
||||
app.include_router(admin_dashboard.router, prefix="/api", tags=["AdminDashboard"])
|
||||
app.include_router(admin_settings.router, prefix="/api", tags=["AdminSettings"])
|
||||
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(external_apps.router, prefix="/api", tags=["ExternalApps"])
|
||||
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"])
|
||||
app.include_router(hot_words.router, prefix="/api", tags=["HotWords"])
|
||||
app.include_router(terminals.router, prefix="/api", tags=["Terminals"])
|
||||
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
async def custom_swagger_ui_html():
|
||||
"""自定义Swagger UI,使用国内可访问的CDN"""
|
||||
return get_swagger_ui_html(
|
||||
openapi_url=app.openapi_url,
|
||||
title=app.title + " - Swagger UI",
|
||||
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
|
||||
swagger_js_url="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js",
|
||||
swagger_css_url="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css",
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"message": "Welcome to iMeeting API"}
|
||||
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
"""健康检查端点"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "iMeeting API",
|
||||
"version": "1.1.0"
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 简单的uvicorn配置,避免参数冲突
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host=API_CONFIG['host'],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,482 @@
|
|||
from app.core.response import create_api_response
|
||||
from app.core.database import get_db_connection
|
||||
from app.services.jwt_service import jwt_service
|
||||
from app.core.config import AUDIO_DIR, REDIS_CONFIG
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
import os
|
||||
import redis
|
||||
|
||||
|
||||
# Redis 客户端
|
||||
redis_client = redis.Redis(**REDIS_CONFIG)
|
||||
|
||||
# 常量定义
|
||||
AUDIO_FILE_EXTENSIONS = ('.wav', '.mp3', '.m4a', '.aac', '.flac', '.ogg', '.mpeg', '.mp4', '.webm')
|
||||
BYTES_TO_GB = 1024 ** 3
|
||||
|
||||
|
||||
def _build_status_condition(status: str) -> str:
|
||||
"""构建任务状态查询条件"""
|
||||
if status == 'running':
|
||||
return "AND (t.status = 'pending' OR t.status = 'processing')"
|
||||
elif status == 'completed':
|
||||
return "AND t.status = 'completed'"
|
||||
elif status == 'failed':
|
||||
return "AND t.status = 'failed'"
|
||||
return ""
|
||||
|
||||
|
||||
def _get_task_stats_query() -> str:
|
||||
"""获取任务统计的 SQL 查询"""
|
||||
return """
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'pending' OR status = 'processing' THEN 1 ELSE 0 END) as running,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
|
||||
"""
|
||||
|
||||
|
||||
def _get_online_user_count(redis_client) -> int:
|
||||
"""从 Redis 获取在线用户数"""
|
||||
try:
|
||||
token_keys = redis_client.keys("token:*")
|
||||
user_ids = set()
|
||||
for key in token_keys:
|
||||
if isinstance(key, bytes):
|
||||
key = key.decode("utf-8", errors="ignore")
|
||||
parts = key.split(':')
|
||||
if len(parts) >= 2:
|
||||
user_ids.add(parts[1])
|
||||
return len(user_ids)
|
||||
except Exception as e:
|
||||
print(f"获取在线用户数失败: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def _table_exists(cursor, table_name: str) -> bool:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS cnt
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = %s
|
||||
""",
|
||||
(table_name,),
|
||||
)
|
||||
return (cursor.fetchone() or {}).get("cnt", 0) > 0
|
||||
|
||||
|
||||
def _calculate_audio_storage() -> Dict[str, float]:
|
||||
"""计算音频文件存储统计"""
|
||||
audio_files_count = 0
|
||||
audio_total_size = 0
|
||||
|
||||
try:
|
||||
if os.path.exists(AUDIO_DIR):
|
||||
for root, _, files in os.walk(AUDIO_DIR):
|
||||
for file in files:
|
||||
file_extension = os.path.splitext(file)[1].lower()
|
||||
if file_extension in AUDIO_FILE_EXTENSIONS:
|
||||
audio_files_count += 1
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
audio_total_size += os.path.getsize(file_path)
|
||||
except OSError:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"统计音频文件失败: {e}")
|
||||
|
||||
return {
|
||||
"audio_file_count": audio_files_count,
|
||||
"audio_files_count": audio_files_count,
|
||||
"audio_total_size_gb": round(audio_total_size / BYTES_TO_GB, 2)
|
||||
}
|
||||
|
||||
|
||||
async def get_dashboard_stats(current_user=None):
|
||||
"""获取管理员 Dashboard 统计数据"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 1. 用户统计
|
||||
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
total_users = 0
|
||||
today_new_users = 0
|
||||
|
||||
if _table_exists(cursor, "sys_users"):
|
||||
cursor.execute("SELECT COUNT(*) as total FROM sys_users")
|
||||
total_users = (cursor.fetchone() or {}).get("total", 0)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) as count FROM sys_users WHERE created_at >= %s",
|
||||
(today_start,),
|
||||
)
|
||||
today_new_users = (cursor.fetchone() or {}).get("count", 0)
|
||||
|
||||
online_users = _get_online_user_count(redis_client)
|
||||
|
||||
# 2. 会议统计
|
||||
total_meetings = 0
|
||||
today_new_meetings = 0
|
||||
if _table_exists(cursor, "meetings"):
|
||||
cursor.execute("SELECT COUNT(*) as total FROM meetings")
|
||||
total_meetings = (cursor.fetchone() or {}).get("total", 0)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) as count FROM meetings WHERE created_at >= %s",
|
||||
(today_start,),
|
||||
)
|
||||
today_new_meetings = (cursor.fetchone() or {}).get("count", 0)
|
||||
|
||||
# 3. 任务统计
|
||||
task_stats_query = _get_task_stats_query()
|
||||
|
||||
# 转录任务
|
||||
if _table_exists(cursor, "transcript_tasks"):
|
||||
cursor.execute(f"{task_stats_query} FROM transcript_tasks")
|
||||
transcription_stats = cursor.fetchone() or {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
else:
|
||||
transcription_stats = {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
|
||||
# 总结任务
|
||||
if _table_exists(cursor, "llm_tasks"):
|
||||
cursor.execute(f"{task_stats_query} FROM llm_tasks")
|
||||
summary_stats = cursor.fetchone() or {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
else:
|
||||
summary_stats = {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
|
||||
# 知识库任务
|
||||
if _table_exists(cursor, "knowledge_base_tasks"):
|
||||
cursor.execute(f"{task_stats_query} FROM knowledge_base_tasks")
|
||||
kb_stats = cursor.fetchone() or {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
else:
|
||||
kb_stats = {'total': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
||||
|
||||
# 4. 音频存储统计
|
||||
storage_stats = _calculate_audio_storage()
|
||||
|
||||
# 组装返回数据
|
||||
stats = {
|
||||
"users": {
|
||||
"total": total_users,
|
||||
"today_new": today_new_users,
|
||||
"online": online_users
|
||||
},
|
||||
"meetings": {
|
||||
"total": total_meetings,
|
||||
"today_new": today_new_meetings
|
||||
},
|
||||
"tasks": {
|
||||
"transcription": {
|
||||
"total": transcription_stats['total'] or 0,
|
||||
"running": transcription_stats['running'] or 0,
|
||||
"completed": transcription_stats['completed'] or 0,
|
||||
"failed": transcription_stats['failed'] or 0
|
||||
},
|
||||
"summary": {
|
||||
"total": summary_stats['total'] or 0,
|
||||
"running": summary_stats['running'] or 0,
|
||||
"completed": summary_stats['completed'] or 0,
|
||||
"failed": summary_stats['failed'] or 0
|
||||
},
|
||||
"knowledge_base": {
|
||||
"total": kb_stats['total'] or 0,
|
||||
"running": kb_stats['running'] or 0,
|
||||
"completed": kb_stats['completed'] or 0,
|
||||
"failed": kb_stats['failed'] or 0
|
||||
}
|
||||
},
|
||||
"storage": storage_stats
|
||||
}
|
||||
|
||||
return create_api_response(code="200", message="获取统计数据成功", data=stats)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取Dashboard统计数据失败: {e}")
|
||||
return create_api_response(code="500", message=f"获取统计数据失败: {str(e)}")
|
||||
|
||||
|
||||
async def get_online_users(current_user=None):
|
||||
"""获取在线用户列表"""
|
||||
try:
|
||||
token_keys = redis_client.keys("token:*")
|
||||
|
||||
# 提取用户ID并去重
|
||||
user_tokens = {}
|
||||
for key in token_keys:
|
||||
if isinstance(key, bytes):
|
||||
key = key.decode("utf-8", errors="ignore")
|
||||
parts = key.split(':')
|
||||
if len(parts) >= 3:
|
||||
user_id = int(parts[1])
|
||||
token = parts[2]
|
||||
if user_id not in user_tokens:
|
||||
user_tokens[user_id] = []
|
||||
user_tokens[user_id].append({'token': token, 'key': key})
|
||||
|
||||
# 查询用户信息
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
online_users_list = []
|
||||
for user_id, tokens in user_tokens.items():
|
||||
cursor.execute(
|
||||
"SELECT user_id, username, caption, email, role_id FROM sys_users WHERE user_id = %s",
|
||||
(user_id,)
|
||||
)
|
||||
user = cursor.fetchone()
|
||||
if user:
|
||||
ttl_seconds = redis_client.ttl(tokens[0]['key'])
|
||||
online_users_list.append({
|
||||
**user,
|
||||
'token_count': len(tokens),
|
||||
'ttl_seconds': ttl_seconds,
|
||||
'ttl_hours': round(ttl_seconds / 3600, 1) if ttl_seconds > 0 else 0
|
||||
})
|
||||
|
||||
# 按用户ID排序
|
||||
online_users_list.sort(key=lambda x: x['user_id'])
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取在线用户列表成功",
|
||||
data={"users": online_users_list, "total": len(online_users_list)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取在线用户列表失败: {e}")
|
||||
return create_api_response(code="500", message=f"获取在线用户列表失败: {str(e)}")
|
||||
|
||||
|
||||
async def kick_user(user_id: int, current_user=None):
|
||||
"""踢出用户(撤销该用户的所有 token)"""
|
||||
try:
|
||||
revoked_count = jwt_service.revoke_all_user_tokens(user_id)
|
||||
|
||||
if revoked_count > 0:
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message=f"已踢出用户,撤销了 {revoked_count} 个 token",
|
||||
data={"user_id": user_id, "revoked_count": revoked_count}
|
||||
)
|
||||
else:
|
||||
return create_api_response(
|
||||
code="404",
|
||||
message="该用户当前不在线或未找到 token"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"踢出用户失败: {e}")
|
||||
return create_api_response(code="500", message=f"踢出用户失败: {str(e)}")
|
||||
|
||||
|
||||
async def monitor_tasks(
|
||||
task_type: str = 'all',
|
||||
status: str = 'all',
|
||||
limit: int = 20,
|
||||
current_user=None
|
||||
):
|
||||
"""监控任务进度"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
tasks = []
|
||||
status_condition = _build_status_condition(status)
|
||||
|
||||
# 转录任务
|
||||
if task_type in ['all', 'transcription']:
|
||||
query = f"""
|
||||
SELECT
|
||||
t.task_id,
|
||||
'transcription' as task_type,
|
||||
t.meeting_id,
|
||||
m.title as meeting_title,
|
||||
t.status,
|
||||
t.progress,
|
||||
t.error_message,
|
||||
t.created_at,
|
||||
t.completed_at,
|
||||
u.username as creator_name
|
||||
FROM transcript_tasks t
|
||||
LEFT JOIN meetings m ON t.meeting_id = m.meeting_id
|
||||
LEFT JOIN sys_users u ON m.user_id = u.user_id
|
||||
WHERE 1=1 {status_condition}
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
cursor.execute(query, (limit,))
|
||||
tasks.extend(cursor.fetchall())
|
||||
|
||||
# 总结任务
|
||||
if task_type in ['all', 'summary']:
|
||||
query = f"""
|
||||
SELECT
|
||||
t.task_id,
|
||||
'summary' as task_type,
|
||||
t.meeting_id,
|
||||
m.title as meeting_title,
|
||||
t.status,
|
||||
t.progress,
|
||||
t.error_message,
|
||||
t.created_at,
|
||||
t.completed_at,
|
||||
u.username as creator_name
|
||||
FROM llm_tasks t
|
||||
LEFT JOIN meetings m ON t.meeting_id = m.meeting_id
|
||||
LEFT JOIN sys_users u ON m.user_id = u.user_id
|
||||
WHERE 1=1 {status_condition}
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
cursor.execute(query, (limit,))
|
||||
tasks.extend(cursor.fetchall())
|
||||
|
||||
# 知识库任务
|
||||
if task_type in ['all', 'knowledge_base']:
|
||||
query = f"""
|
||||
SELECT
|
||||
t.task_id,
|
||||
'knowledge_base' as task_type,
|
||||
t.kb_id as meeting_id,
|
||||
k.title as meeting_title,
|
||||
t.status,
|
||||
t.progress,
|
||||
t.error_message,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
u.username as creator_name
|
||||
FROM knowledge_base_tasks t
|
||||
LEFT JOIN knowledge_bases k ON t.kb_id = k.kb_id
|
||||
LEFT JOIN sys_users u ON k.creator_id = u.user_id
|
||||
WHERE 1=1 {status_condition}
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
cursor.execute(query, (limit,))
|
||||
tasks.extend(cursor.fetchall())
|
||||
|
||||
# 按创建时间排序并限制返回数量
|
||||
tasks.sort(key=lambda x: x['created_at'], reverse=True)
|
||||
tasks = tasks[:limit]
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取任务监控数据成功",
|
||||
data={"tasks": tasks, "total": len(tasks)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取任务监控数据失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return create_api_response(code="500", message=f"获取任务监控数据失败: {str(e)}")
|
||||
|
||||
|
||||
async def get_system_resources(current_user=None):
|
||||
"""获取服务器资源使用情况"""
|
||||
try:
|
||||
import psutil
|
||||
|
||||
# CPU 使用率
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
cpu_count = psutil.cpu_count()
|
||||
|
||||
# 内存使用情况
|
||||
memory = psutil.virtual_memory()
|
||||
memory_total_gb = round(memory.total / BYTES_TO_GB, 2)
|
||||
memory_used_gb = round(memory.used / BYTES_TO_GB, 2)
|
||||
|
||||
# 磁盘使用情况
|
||||
disk = psutil.disk_usage('/')
|
||||
disk_total_gb = round(disk.total / BYTES_TO_GB, 2)
|
||||
disk_used_gb = round(disk.used / BYTES_TO_GB, 2)
|
||||
|
||||
resources = {
|
||||
"cpu": {
|
||||
"percent": cpu_percent,
|
||||
"count": cpu_count
|
||||
},
|
||||
"memory": {
|
||||
"total_gb": memory_total_gb,
|
||||
"used_gb": memory_used_gb,
|
||||
"percent": memory.percent
|
||||
},
|
||||
"disk": {
|
||||
"total_gb": disk_total_gb,
|
||||
"used_gb": disk_used_gb,
|
||||
"percent": disk.percent
|
||||
},
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return create_api_response(code="200", message="获取系统资源成功", data=resources)
|
||||
|
||||
except ImportError:
|
||||
return create_api_response(
|
||||
code="500",
|
||||
message="psutil 库未安装,请运行: pip install psutil"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"获取系统资源失败: {e}")
|
||||
return create_api_response(code="500", message=f"获取系统资源失败: {str(e)}")
|
||||
|
||||
|
||||
async def get_user_stats(current_user=None):
|
||||
"""获取用户统计列表"""
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 查询所有用户及其会议统计和最后登录时间(排除没有会议的用户)
|
||||
query = """
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.username,
|
||||
u.caption,
|
||||
u.created_at,
|
||||
(SELECT MAX(created_at) FROM user_logs
|
||||
WHERE user_id = u.user_id AND action_type = 'login') as last_login_time,
|
||||
COUNT(DISTINCT m.meeting_id) as meeting_count,
|
||||
COALESCE(SUM(af.duration), 0) as total_duration_seconds
|
||||
FROM sys_users u
|
||||
INNER JOIN meetings m ON u.user_id = m.user_id
|
||||
LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id
|
||||
GROUP BY u.user_id, u.username, u.caption, u.created_at
|
||||
HAVING meeting_count > 0
|
||||
ORDER BY u.user_id ASC
|
||||
"""
|
||||
|
||||
cursor.execute(query)
|
||||
users = cursor.fetchall()
|
||||
|
||||
# 格式化返回数据
|
||||
users_list = []
|
||||
for user in users:
|
||||
total_seconds = int(user['total_duration_seconds']) if user['total_duration_seconds'] else 0
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
|
||||
users_list.append({
|
||||
'user_id': user['user_id'],
|
||||
'username': user['username'],
|
||||
'caption': user['caption'],
|
||||
'created_at': user['created_at'].isoformat() if user['created_at'] else None,
|
||||
'last_login_time': user['last_login_time'].isoformat() if user['last_login_time'] else None,
|
||||
'meeting_count': user['meeting_count'],
|
||||
'total_duration_seconds': total_seconds,
|
||||
'total_duration_formatted': f"{hours}h {minutes}m" if total_seconds > 0 else '-'
|
||||
})
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取用户统计成功",
|
||||
data={"users": users_list, "total": len(users_list)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取用户统计失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return create_api_response(code="500", message=f"获取用户统计失败: {str(e)}")
|
||||
|
|
@ -0,0 +1,550 @@
|
|||
import time
|
||||
from typing import Any
|
||||
|
||||
from app.core.database import get_db_connection
|
||||
from app.core.response import create_api_response
|
||||
from app.models.models import MenuInfo, MenuListResponse, RolePermissionInfo
|
||||
|
||||
|
||||
_USER_MENU_CACHE_TTL_SECONDS = 120
|
||||
_USER_MENU_CACHE_VERSION = "menu-rules-v4"
|
||||
_user_menu_cache_by_role: dict[int, dict[str, Any]] = {}
|
||||
|
||||
|
||||
def _get_cached_user_menus(role_id: int):
|
||||
cached = _user_menu_cache_by_role.get(role_id)
|
||||
if not cached:
|
||||
return None
|
||||
if cached.get("version") != _USER_MENU_CACHE_VERSION:
|
||||
_user_menu_cache_by_role.pop(role_id, None)
|
||||
return None
|
||||
if time.time() > cached["expires_at"]:
|
||||
_user_menu_cache_by_role.pop(role_id, None)
|
||||
return None
|
||||
return cached["menus"]
|
||||
|
||||
|
||||
def _set_cached_user_menus(role_id: int, menus):
|
||||
_user_menu_cache_by_role[role_id] = {
|
||||
"version": _USER_MENU_CACHE_VERSION,
|
||||
"menus": menus,
|
||||
"expires_at": time.time() + _USER_MENU_CACHE_TTL_SECONDS,
|
||||
}
|
||||
|
||||
|
||||
def _invalidate_user_menu_cache(role_id: int | None = None):
|
||||
if role_id is None:
|
||||
_user_menu_cache_by_role.clear()
|
||||
return
|
||||
_user_menu_cache_by_role.pop(role_id, None)
|
||||
|
||||
|
||||
def _build_menu_index(menus):
|
||||
menu_by_id = {}
|
||||
children_by_parent = {}
|
||||
for menu in menus:
|
||||
menu_id = menu["menu_id"]
|
||||
menu_by_id[menu_id] = menu
|
||||
parent_id = menu.get("parent_id")
|
||||
if parent_id is not None:
|
||||
children_by_parent.setdefault(parent_id, []).append(menu_id)
|
||||
return menu_by_id, children_by_parent
|
||||
|
||||
|
||||
def _get_descendants(menu_id, children_by_parent):
|
||||
result = set()
|
||||
stack = [menu_id]
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
for child_id in children_by_parent.get(current, []):
|
||||
if child_id in result:
|
||||
continue
|
||||
result.add(child_id)
|
||||
stack.append(child_id)
|
||||
return result
|
||||
|
||||
|
||||
def _normalize_permission_menu_ids(raw_menu_ids, all_menus):
|
||||
menu_by_id, children_by_parent = _build_menu_index(all_menus)
|
||||
selected = {menu_id for menu_id in raw_menu_ids if menu_id in menu_by_id}
|
||||
|
||||
expanded = set(selected)
|
||||
|
||||
for menu_id in list(expanded):
|
||||
expanded.update(_get_descendants(menu_id, children_by_parent))
|
||||
|
||||
for menu_id in list(expanded):
|
||||
cursor = menu_by_id[menu_id].get("parent_id")
|
||||
while cursor is not None and cursor in menu_by_id:
|
||||
if cursor in expanded:
|
||||
break
|
||||
expanded.add(cursor)
|
||||
cursor = menu_by_id[cursor].get("parent_id")
|
||||
|
||||
return sorted(expanded)
|
||||
|
||||
|
||||
def get_all_menus():
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT menu_id, menu_code, menu_name, menu_icon, menu_url, menu_type,
|
||||
parent_id, sort_order, is_active, description, created_at, updated_at
|
||||
FROM sys_menus
|
||||
ORDER BY
|
||||
COALESCE(parent_id, menu_id) ASC,
|
||||
CASE WHEN parent_id IS NULL THEN 0 ELSE 1 END ASC,
|
||||
sort_order ASC,
|
||||
menu_id ASC
|
||||
"""
|
||||
cursor.execute(query)
|
||||
menus = cursor.fetchall()
|
||||
menu_list = [MenuInfo(**menu) for menu in menus]
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取菜单列表成功",
|
||||
data=MenuListResponse(menus=menu_list, total=len(menu_list)),
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取菜单列表失败: {str(e)}")
|
||||
|
||||
|
||||
def create_menu(request):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("SELECT menu_id FROM sys_menus WHERE menu_code = %s", (request.menu_code,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="菜单编码已存在")
|
||||
|
||||
if request.parent_id is not None:
|
||||
cursor.execute("SELECT menu_id FROM sys_menus WHERE menu_id = %s", (request.parent_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="400", message="父菜单不存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO sys_menus (menu_code, menu_name, menu_icon, menu_url, menu_type, parent_id, sort_order, is_active, description)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
request.menu_code,
|
||||
request.menu_name,
|
||||
request.menu_icon,
|
||||
request.menu_url,
|
||||
request.menu_type,
|
||||
request.parent_id,
|
||||
request.sort_order,
|
||||
1 if request.is_active else 0,
|
||||
request.description,
|
||||
),
|
||||
)
|
||||
menu_id = cursor.lastrowid
|
||||
connection.commit()
|
||||
_invalidate_user_menu_cache()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT menu_id, menu_code, menu_name, menu_icon, menu_url, menu_type,
|
||||
parent_id, sort_order, is_active, description, created_at, updated_at
|
||||
FROM sys_menus
|
||||
WHERE menu_id = %s
|
||||
""",
|
||||
(menu_id,),
|
||||
)
|
||||
created = cursor.fetchone()
|
||||
return create_api_response(code="200", message="创建菜单成功", data=created)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建菜单失败: {str(e)}")
|
||||
|
||||
|
||||
def update_menu(menu_id: int, request):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("SELECT * FROM sys_menus WHERE menu_id = %s", (menu_id,))
|
||||
current = cursor.fetchone()
|
||||
if not current:
|
||||
return create_api_response(code="404", message="菜单不存在")
|
||||
|
||||
updates = {}
|
||||
for field in [
|
||||
"menu_code",
|
||||
"menu_name",
|
||||
"menu_icon",
|
||||
"menu_url",
|
||||
"menu_type",
|
||||
"sort_order",
|
||||
"description",
|
||||
]:
|
||||
value = getattr(request, field)
|
||||
if value is not None:
|
||||
updates[field] = value
|
||||
|
||||
if request.is_active is not None:
|
||||
updates["is_active"] = 1 if request.is_active else 0
|
||||
|
||||
fields_set = getattr(request, "model_fields_set", getattr(request, "__fields_set__", set()))
|
||||
|
||||
if request.parent_id == menu_id:
|
||||
return create_api_response(code="400", message="父菜单不能为自身")
|
||||
if request.parent_id is not None:
|
||||
cursor.execute("SELECT menu_id FROM sys_menus WHERE menu_id = %s", (request.parent_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="400", message="父菜单不存在")
|
||||
|
||||
cursor.execute("SELECT menu_id, parent_id FROM sys_menus")
|
||||
all_menus = cursor.fetchall()
|
||||
_, children_by_parent = _build_menu_index(all_menus)
|
||||
descendants = _get_descendants(menu_id, children_by_parent)
|
||||
if request.parent_id in descendants:
|
||||
return create_api_response(code="400", message="父菜单不能设置为当前菜单的子孙菜单")
|
||||
|
||||
if request.parent_id is not None or (request.parent_id is None and "parent_id" in fields_set):
|
||||
updates["parent_id"] = request.parent_id
|
||||
|
||||
if "menu_code" in updates:
|
||||
cursor.execute(
|
||||
"SELECT menu_id FROM sys_menus WHERE menu_code = %s AND menu_id != %s",
|
||||
(updates["menu_code"], menu_id),
|
||||
)
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="菜单编码已存在")
|
||||
|
||||
if not updates:
|
||||
return create_api_response(code="200", message="没有变更内容", data=current)
|
||||
|
||||
set_sql = ", ".join([f"{key} = %s" for key in updates.keys()])
|
||||
values = list(updates.values()) + [menu_id]
|
||||
cursor.execute(f"UPDATE sys_menus SET {set_sql} WHERE menu_id = %s", tuple(values))
|
||||
connection.commit()
|
||||
_invalidate_user_menu_cache()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT menu_id, menu_code, menu_name, menu_icon, menu_url, menu_type,
|
||||
parent_id, sort_order, is_active, description, created_at, updated_at
|
||||
FROM sys_menus
|
||||
WHERE menu_id = %s
|
||||
""",
|
||||
(menu_id,),
|
||||
)
|
||||
updated = cursor.fetchone()
|
||||
return create_api_response(code="200", message="更新菜单成功", data=updated)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新菜单失败: {str(e)}")
|
||||
|
||||
|
||||
def delete_menu(menu_id: int):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("SELECT menu_id FROM sys_menus WHERE menu_id = %s", (menu_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="菜单不存在")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) AS cnt FROM sys_menus WHERE parent_id = %s", (menu_id,))
|
||||
child_count = cursor.fetchone()["cnt"]
|
||||
if child_count > 0:
|
||||
return create_api_response(code="400", message="请先删除子菜单")
|
||||
|
||||
cursor.execute("DELETE FROM sys_role_menu_permissions WHERE menu_id = %s", (menu_id,))
|
||||
cursor.execute("DELETE FROM sys_menus WHERE menu_id = %s", (menu_id,))
|
||||
connection.commit()
|
||||
_invalidate_user_menu_cache()
|
||||
return create_api_response(code="200", message="删除菜单成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"删除菜单失败: {str(e)}")
|
||||
|
||||
|
||||
def get_all_roles():
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT r.role_id, r.role_name, r.created_at,
|
||||
COUNT(rmp.menu_id) as menu_count
|
||||
FROM sys_roles r
|
||||
LEFT JOIN sys_role_menu_permissions rmp ON r.role_id = rmp.role_id
|
||||
GROUP BY r.role_id
|
||||
ORDER BY r.role_id ASC
|
||||
"""
|
||||
cursor.execute(query)
|
||||
roles = cursor.fetchall()
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取角色列表成功",
|
||||
data={"roles": roles, "total": len(roles)},
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取角色列表失败: {str(e)}")
|
||||
|
||||
|
||||
def create_role(request):
|
||||
try:
|
||||
role_name = request.role_name.strip()
|
||||
if not role_name:
|
||||
return create_api_response(code="400", message="角色名称不能为空")
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT role_id FROM sys_roles WHERE role_name = %s", (role_name,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="角色名称已存在")
|
||||
|
||||
cursor.execute("INSERT INTO sys_roles (role_name) VALUES (%s)", (role_name,))
|
||||
role_id = cursor.lastrowid
|
||||
connection.commit()
|
||||
|
||||
cursor.execute("SELECT role_id, role_name, created_at FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
role = cursor.fetchone()
|
||||
return create_api_response(code="200", message="创建角色成功", data=role)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建角色失败: {str(e)}")
|
||||
|
||||
|
||||
def update_role(role_id: int, request):
|
||||
try:
|
||||
role_name = request.role_name.strip()
|
||||
if not role_name:
|
||||
return create_api_response(code="400", message="角色名称不能为空")
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT role_id FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="角色不存在")
|
||||
|
||||
cursor.execute("SELECT role_id FROM sys_roles WHERE role_name = %s AND role_id != %s", (role_name, role_id))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="角色名称已存在")
|
||||
|
||||
cursor.execute("UPDATE sys_roles SET role_name = %s WHERE role_id = %s", (role_name, role_id))
|
||||
connection.commit()
|
||||
|
||||
cursor.execute("SELECT role_id, role_name, created_at FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
role = cursor.fetchone()
|
||||
return create_api_response(code="200", message="更新角色成功", data=role)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新角色失败: {str(e)}")
|
||||
|
||||
|
||||
def get_role_users(role_id: int, page: int, size: int):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT role_id, role_name FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
role = cursor.fetchone()
|
||||
if not role:
|
||||
return create_api_response(code="404", message="角色不存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS total
|
||||
FROM sys_users
|
||||
WHERE role_id = %s
|
||||
""",
|
||||
(role_id,),
|
||||
)
|
||||
total = cursor.fetchone()["total"]
|
||||
offset = (page - 1) * size
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT user_id, username, caption, email, avatar_url, role_id, created_at
|
||||
FROM sys_users
|
||||
WHERE role_id = %s
|
||||
ORDER BY user_id ASC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
(role_id, size, offset),
|
||||
)
|
||||
users = cursor.fetchall()
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取角色用户成功",
|
||||
data={
|
||||
"role_id": role_id,
|
||||
"role_name": role["role_name"],
|
||||
"users": users,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取角色用户失败: {str(e)}")
|
||||
|
||||
|
||||
def get_all_role_permissions():
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT rmp.role_id, rmp.menu_id
|
||||
FROM sys_role_menu_permissions rmp
|
||||
JOIN sys_menus m ON m.menu_id = rmp.menu_id
|
||||
WHERE m.is_active = 1
|
||||
ORDER BY rmp.role_id ASC, rmp.menu_id ASC
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
result = {}
|
||||
for row in rows:
|
||||
result.setdefault(row["role_id"], []).append(row["menu_id"])
|
||||
return create_api_response(code="200", message="获取角色权限成功", data={"permissions": result})
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取角色权限失败: {str(e)}")
|
||||
|
||||
|
||||
def get_role_permissions(role_id: int):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT role_id, role_name FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
role = cursor.fetchone()
|
||||
if not role:
|
||||
return create_api_response(code="404", message="角色不存在")
|
||||
|
||||
query = """
|
||||
SELECT rmp.menu_id
|
||||
FROM sys_role_menu_permissions rmp
|
||||
JOIN sys_menus m ON m.menu_id = rmp.menu_id
|
||||
WHERE rmp.role_id = %s
|
||||
AND m.is_active = 1
|
||||
"""
|
||||
cursor.execute(query, (role_id,))
|
||||
permissions = cursor.fetchall()
|
||||
menu_ids = [permission["menu_id"] for permission in permissions]
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取角色权限成功",
|
||||
data=RolePermissionInfo(
|
||||
role_id=role["role_id"],
|
||||
role_name=role["role_name"],
|
||||
menu_ids=menu_ids,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取角色权限失败: {str(e)}")
|
||||
|
||||
|
||||
def update_role_permissions(role_id: int, request):
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT role_id FROM sys_roles WHERE role_id = %s", (role_id,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="角色不存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT menu_id, parent_id
|
||||
FROM sys_menus
|
||||
WHERE is_active = 1
|
||||
"""
|
||||
)
|
||||
all_menus = cursor.fetchall()
|
||||
menu_id_set = {menu["menu_id"] for menu in all_menus}
|
||||
|
||||
invalid_menu_ids = [menu_id for menu_id in request.menu_ids if menu_id not in menu_id_set]
|
||||
if invalid_menu_ids:
|
||||
return create_api_response(code="400", message="包含无效的菜单ID")
|
||||
|
||||
normalized_menu_ids = _normalize_permission_menu_ids(request.menu_ids, all_menus)
|
||||
|
||||
cursor.execute("DELETE FROM sys_role_menu_permissions WHERE role_id = %s", (role_id,))
|
||||
if normalized_menu_ids:
|
||||
insert_values = [(role_id, menu_id) for menu_id in normalized_menu_ids]
|
||||
cursor.executemany(
|
||||
"INSERT INTO sys_role_menu_permissions (role_id, menu_id) VALUES (%s, %s)",
|
||||
insert_values,
|
||||
)
|
||||
|
||||
connection.commit()
|
||||
_invalidate_user_menu_cache(role_id)
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="更新角色权限成功",
|
||||
data={"role_id": role_id, "menu_count": len(normalized_menu_ids)},
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新角色权限失败: {str(e)}")
|
||||
|
||||
|
||||
def get_user_menus(current_user: dict):
|
||||
try:
|
||||
role_id = current_user["role_id"]
|
||||
cached_menus = _get_cached_user_menus(role_id)
|
||||
if cached_menus is not None:
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取用户菜单成功",
|
||||
data={"menus": cached_menus},
|
||||
)
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT m.menu_id, m.menu_code, m.menu_name, m.menu_icon,
|
||||
m.menu_url, m.menu_type, m.parent_id, m.sort_order
|
||||
FROM sys_menus m
|
||||
JOIN sys_role_menu_permissions rmp ON m.menu_id = rmp.menu_id
|
||||
WHERE rmp.role_id = %s
|
||||
AND m.is_active = 1
|
||||
AND (m.is_visible = 1 OR m.is_visible IS NULL OR m.menu_code IN ('dashboard', 'desktop'))
|
||||
ORDER BY
|
||||
COALESCE(m.parent_id, m.menu_id) ASC,
|
||||
CASE WHEN m.parent_id IS NULL THEN 0 ELSE 1 END ASC,
|
||||
m.sort_order ASC,
|
||||
m.menu_id ASC
|
||||
"""
|
||||
cursor.execute(query, (role_id,))
|
||||
menus = cursor.fetchall()
|
||||
|
||||
current_menu_ids = {menu["menu_id"] for menu in menus}
|
||||
missing_parent_ids = {
|
||||
menu["parent_id"]
|
||||
for menu in menus
|
||||
if menu.get("parent_id") is not None and menu["parent_id"] not in current_menu_ids
|
||||
}
|
||||
|
||||
if missing_parent_ids:
|
||||
format_strings = ",".join(["%s"] * len(missing_parent_ids))
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT menu_id, menu_code, menu_name, menu_icon, menu_url, menu_type, parent_id, sort_order
|
||||
FROM sys_menus
|
||||
WHERE is_active = 1 AND menu_id IN ({format_strings})
|
||||
""",
|
||||
tuple(missing_parent_ids),
|
||||
)
|
||||
parent_rows = cursor.fetchall()
|
||||
menus.extend(parent_rows)
|
||||
|
||||
menus = sorted(
|
||||
{menu["menu_id"]: menu for menu in menus}.values(),
|
||||
key=lambda menu: (
|
||||
menu["parent_id"] if menu["parent_id"] is not None else menu["menu_id"],
|
||||
0 if menu["parent_id"] is None else 1,
|
||||
menu["sort_order"],
|
||||
menu["menu_id"],
|
||||
),
|
||||
)
|
||||
_set_cached_user_menus(role_id, menus)
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取用户菜单成功",
|
||||
data={"menus": menus},
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取用户菜单失败: {str(e)}")
|
||||
|
|
@ -0,0 +1,744 @@
|
|||
import json
|
||||
from typing import Any
|
||||
|
||||
from app.core.database import get_db_connection
|
||||
from app.core.response import create_api_response
|
||||
from app.services.async_transcription_service import AsyncTranscriptionService
|
||||
from app.services.llm_service import LLMService
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
|
||||
|
||||
llm_service = LLMService()
|
||||
transcription_service = AsyncTranscriptionService()
|
||||
|
||||
|
||||
def _parse_json_object(value: Any) -> dict[str, Any]:
|
||||
if value is None:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
return dict(value)
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
def _normalize_string_list(value: Any) -> list[str] | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, list):
|
||||
values = [str(item).strip() for item in value if str(item).strip()]
|
||||
return values or None
|
||||
if isinstance(value, str):
|
||||
values = [item.strip() for item in value.split(",") if item.strip()]
|
||||
return values or None
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_int_list(value: Any) -> list[int] | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, list):
|
||||
items = value
|
||||
elif isinstance(value, str):
|
||||
items = [item.strip() for item in value.split(",") if item.strip()]
|
||||
else:
|
||||
return None
|
||||
|
||||
normalized = []
|
||||
for item in items:
|
||||
try:
|
||||
normalized.append(int(item))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return normalized or None
|
||||
|
||||
|
||||
def _clean_extra_config(config: dict[str, Any]) -> dict[str, Any]:
|
||||
cleaned: dict[str, Any] = {}
|
||||
for key, value in (config or {}).items():
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if stripped:
|
||||
cleaned[key] = stripped
|
||||
continue
|
||||
if isinstance(value, list):
|
||||
normalized_list = []
|
||||
for item in value:
|
||||
if item is None:
|
||||
continue
|
||||
if isinstance(item, str):
|
||||
stripped = item.strip()
|
||||
if stripped:
|
||||
normalized_list.append(stripped)
|
||||
else:
|
||||
normalized_list.append(item)
|
||||
if normalized_list:
|
||||
cleaned[key] = normalized_list
|
||||
continue
|
||||
cleaned[key] = value
|
||||
return cleaned
|
||||
|
||||
|
||||
def _merge_audio_extra_config(request, vocabulary_id: str | None = None) -> dict[str, Any]:
|
||||
extra_config = _parse_json_object(request.extra_config)
|
||||
|
||||
if request.audio_scene == "asr":
|
||||
legacy_config = {
|
||||
"model": request.asr_model_name,
|
||||
"speaker_count": request.asr_speaker_count,
|
||||
"language_hints": request.asr_language_hints,
|
||||
"disfluency_removal_enabled": request.asr_disfluency_removal_enabled,
|
||||
"diarization_enabled": request.asr_diarization_enabled,
|
||||
}
|
||||
else:
|
||||
legacy_config = {
|
||||
"model": request.model_name,
|
||||
"template_text": request.vp_template_text,
|
||||
"duration_seconds": request.vp_duration_seconds,
|
||||
"sample_rate": request.vp_sample_rate,
|
||||
"channels": request.vp_channels,
|
||||
"max_size_bytes": request.vp_max_size_bytes,
|
||||
}
|
||||
|
||||
merged = {**legacy_config, **extra_config}
|
||||
|
||||
language_hints = _normalize_string_list(merged.get("language_hints"))
|
||||
if language_hints is not None:
|
||||
merged["language_hints"] = language_hints
|
||||
|
||||
channel_id = _normalize_int_list(merged.get("channel_id"))
|
||||
if channel_id is not None:
|
||||
merged["channel_id"] = channel_id
|
||||
|
||||
resolved_vocabulary_id = vocabulary_id or merged.get("vocabulary_id") or request.asr_vocabulary_id
|
||||
if request.audio_scene == "asr" and resolved_vocabulary_id:
|
||||
merged["vocabulary_id"] = resolved_vocabulary_id
|
||||
|
||||
return _clean_extra_config(merged)
|
||||
|
||||
|
||||
def _extract_legacy_audio_columns(audio_scene: str, extra_config: dict[str, Any]) -> dict[str, Any]:
|
||||
extra_config = _parse_json_object(extra_config)
|
||||
columns = {
|
||||
"asr_model_name": None,
|
||||
"asr_vocabulary_id": None,
|
||||
"asr_speaker_count": None,
|
||||
"asr_language_hints": None,
|
||||
"asr_disfluency_removal_enabled": None,
|
||||
"asr_diarization_enabled": None,
|
||||
"vp_template_text": None,
|
||||
"vp_duration_seconds": None,
|
||||
"vp_sample_rate": None,
|
||||
"vp_channels": None,
|
||||
"vp_max_size_bytes": None,
|
||||
}
|
||||
|
||||
if audio_scene == "asr":
|
||||
language_hints = extra_config.get("language_hints")
|
||||
if isinstance(language_hints, list):
|
||||
language_hints = ",".join(str(item).strip() for item in language_hints if str(item).strip())
|
||||
columns.update(
|
||||
{
|
||||
"asr_model_name": extra_config.get("model"),
|
||||
"asr_vocabulary_id": extra_config.get("vocabulary_id"),
|
||||
"asr_speaker_count": extra_config.get("speaker_count"),
|
||||
"asr_language_hints": language_hints,
|
||||
"asr_disfluency_removal_enabled": 1 if extra_config.get("disfluency_removal_enabled") is True else 0 if extra_config.get("disfluency_removal_enabled") is False else None,
|
||||
"asr_diarization_enabled": 1 if extra_config.get("diarization_enabled") is True else 0 if extra_config.get("diarization_enabled") is False else None,
|
||||
}
|
||||
)
|
||||
else:
|
||||
columns.update(
|
||||
{
|
||||
"vp_template_text": extra_config.get("template_text"),
|
||||
"vp_duration_seconds": extra_config.get("duration_seconds"),
|
||||
"vp_sample_rate": extra_config.get("sample_rate"),
|
||||
"vp_channels": extra_config.get("channels"),
|
||||
"vp_max_size_bytes": extra_config.get("max_size_bytes"),
|
||||
}
|
||||
)
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def _normalize_audio_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||
extra_config = _parse_json_object(row.get("extra_config"))
|
||||
|
||||
if row.get("audio_scene") == "asr":
|
||||
if extra_config.get("model") is None and row.get("asr_model_name") is not None:
|
||||
extra_config["model"] = row["asr_model_name"]
|
||||
if extra_config.get("vocabulary_id") is None and row.get("asr_vocabulary_id") is not None:
|
||||
extra_config["vocabulary_id"] = row["asr_vocabulary_id"]
|
||||
if extra_config.get("speaker_count") is None and row.get("asr_speaker_count") is not None:
|
||||
extra_config["speaker_count"] = row["asr_speaker_count"]
|
||||
if extra_config.get("language_hints") is None and row.get("asr_language_hints"):
|
||||
extra_config["language_hints"] = _normalize_string_list(row["asr_language_hints"])
|
||||
if extra_config.get("disfluency_removal_enabled") is None and row.get("asr_disfluency_removal_enabled") is not None:
|
||||
extra_config["disfluency_removal_enabled"] = bool(row["asr_disfluency_removal_enabled"])
|
||||
if extra_config.get("diarization_enabled") is None and row.get("asr_diarization_enabled") is not None:
|
||||
extra_config["diarization_enabled"] = bool(row["asr_diarization_enabled"])
|
||||
else:
|
||||
if extra_config.get("model") is None and row.get("model_name"):
|
||||
extra_config["model"] = row["model_name"]
|
||||
if extra_config.get("template_text") is None and row.get("vp_template_text") is not None:
|
||||
extra_config["template_text"] = row["vp_template_text"]
|
||||
if extra_config.get("duration_seconds") is None and row.get("vp_duration_seconds") is not None:
|
||||
extra_config["duration_seconds"] = row["vp_duration_seconds"]
|
||||
if extra_config.get("sample_rate") is None and row.get("vp_sample_rate") is not None:
|
||||
extra_config["sample_rate"] = row["vp_sample_rate"]
|
||||
if extra_config.get("channels") is None and row.get("vp_channels") is not None:
|
||||
extra_config["channels"] = row["vp_channels"]
|
||||
if extra_config.get("max_size_bytes") is None and row.get("vp_max_size_bytes") is not None:
|
||||
extra_config["max_size_bytes"] = row["vp_max_size_bytes"]
|
||||
|
||||
row["extra_config"] = extra_config
|
||||
row["service_model_name"] = extra_config.get("model")
|
||||
return row
|
||||
|
||||
|
||||
def _resolve_hot_word_vocabulary_id(cursor, request) -> str | None:
|
||||
vocabulary_id = request.asr_vocabulary_id
|
||||
if request.hot_word_group_id:
|
||||
cursor.execute("SELECT vocabulary_id FROM hot_word_group WHERE id = %s", (request.hot_word_group_id,))
|
||||
group_row = cursor.fetchone()
|
||||
if group_row and group_row.get("vocabulary_id"):
|
||||
vocabulary_id = group_row["vocabulary_id"]
|
||||
return vocabulary_id
|
||||
|
||||
|
||||
def list_parameters(category: str | None = None, keyword: str | None = None):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT param_id, param_key, param_name, param_value, value_type, category,
|
||||
description, is_active, created_at, updated_at
|
||||
FROM sys_system_parameters
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
if category:
|
||||
query += " AND category = %s"
|
||||
params.append(category)
|
||||
if keyword:
|
||||
like_pattern = f"%{keyword}%"
|
||||
query += " AND (param_key LIKE %s OR param_name LIKE %s)"
|
||||
params.extend([like_pattern, like_pattern])
|
||||
|
||||
query += " ORDER BY category ASC, param_key ASC"
|
||||
cursor.execute(query, tuple(params))
|
||||
rows = cursor.fetchall()
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取参数列表成功",
|
||||
data={"items": rows, "total": len(rows)},
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取参数列表失败: {str(e)}")
|
||||
|
||||
|
||||
def get_parameter(param_key: str):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT param_id, param_key, param_name, param_value, value_type, category,
|
||||
description, is_active, created_at, updated_at
|
||||
FROM sys_system_parameters
|
||||
WHERE param_key = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(param_key,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return create_api_response(code="404", message="参数不存在")
|
||||
return create_api_response(code="200", message="获取参数成功", data=row)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取参数失败: {str(e)}")
|
||||
|
||||
|
||||
def create_parameter(request):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT param_id FROM sys_system_parameters WHERE param_key = %s", (request.param_key,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="参数键已存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO sys_system_parameters
|
||||
(param_key, param_name, param_value, value_type, category, description, is_active)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
request.param_key,
|
||||
request.param_name,
|
||||
request.param_value,
|
||||
request.value_type,
|
||||
request.category,
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
SystemConfigService.invalidate_cache()
|
||||
return create_api_response(code="200", message="创建参数成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建参数失败: {str(e)}")
|
||||
|
||||
|
||||
def update_parameter(param_key: str, request):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT param_id FROM sys_system_parameters WHERE param_key = %s", (param_key,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="参数不存在")
|
||||
|
||||
new_key = request.param_key or param_key
|
||||
if new_key != param_key:
|
||||
cursor.execute("SELECT param_id FROM sys_system_parameters WHERE param_key = %s", (new_key,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="新的参数键已存在")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE sys_system_parameters
|
||||
SET param_key = %s, param_name = %s, param_value = %s, value_type = %s,
|
||||
category = %s, description = %s, is_active = %s
|
||||
WHERE param_key = %s
|
||||
""",
|
||||
(
|
||||
new_key,
|
||||
request.param_name,
|
||||
request.param_value,
|
||||
request.value_type,
|
||||
request.category,
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
param_key,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
SystemConfigService.invalidate_cache()
|
||||
return create_api_response(code="200", message="更新参数成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新参数失败: {str(e)}")
|
||||
|
||||
|
||||
def delete_parameter(param_key: str):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT param_id FROM sys_system_parameters WHERE param_key = %s", (param_key,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="参数不存在")
|
||||
|
||||
cursor.execute("DELETE FROM sys_system_parameters WHERE param_key = %s", (param_key,))
|
||||
conn.commit()
|
||||
SystemConfigService.invalidate_cache()
|
||||
return create_api_response(code="200", message="删除参数成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"删除参数失败: {str(e)}")
|
||||
|
||||
|
||||
def list_llm_model_configs():
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT config_id, model_code, model_name, provider, endpoint_url, api_key,
|
||||
llm_model_name, llm_timeout, llm_temperature, llm_top_p, llm_max_tokens,
|
||||
llm_system_prompt, description, is_active, is_default, created_at, updated_at
|
||||
FROM llm_model_config
|
||||
ORDER BY model_code ASC
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取LLM模型配置成功",
|
||||
data={"items": rows, "total": len(rows)},
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取LLM模型配置失败: {str(e)}")
|
||||
|
||||
|
||||
def create_llm_model_config(request):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM llm_model_config WHERE model_code = %s", (request.model_code,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="模型编码已存在")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) AS total FROM llm_model_config")
|
||||
total_row = cursor.fetchone() or {"total": 0}
|
||||
is_default = bool(request.is_default) or total_row["total"] == 0
|
||||
if is_default:
|
||||
cursor.execute("UPDATE llm_model_config SET is_default = 0 WHERE is_default = 1")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO llm_model_config
|
||||
(model_code, model_name, provider, endpoint_url, api_key, llm_model_name,
|
||||
llm_timeout, llm_temperature, llm_top_p, llm_max_tokens, llm_system_prompt,
|
||||
description, is_active, is_default)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
request.model_code,
|
||||
request.model_name,
|
||||
request.provider,
|
||||
request.endpoint_url,
|
||||
request.api_key,
|
||||
request.llm_model_name,
|
||||
request.llm_timeout,
|
||||
request.llm_temperature,
|
||||
request.llm_top_p,
|
||||
request.llm_max_tokens,
|
||||
request.llm_system_prompt,
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
1 if is_default else 0,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="创建LLM模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建LLM模型配置失败: {str(e)}")
|
||||
|
||||
|
||||
def update_llm_model_config(model_code: str, request):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM llm_model_config WHERE model_code = %s", (model_code,))
|
||||
existed = cursor.fetchone()
|
||||
if not existed:
|
||||
return create_api_response(code="404", message="模型配置不存在")
|
||||
|
||||
new_model_code = request.model_code or model_code
|
||||
if new_model_code != model_code:
|
||||
cursor.execute("SELECT config_id FROM llm_model_config WHERE model_code = %s", (new_model_code,))
|
||||
duplicate_row = cursor.fetchone()
|
||||
if duplicate_row and duplicate_row["config_id"] != existed["config_id"]:
|
||||
return create_api_response(code="400", message="新的模型编码已存在")
|
||||
|
||||
if request.is_default:
|
||||
cursor.execute(
|
||||
"UPDATE llm_model_config SET is_default = 0 WHERE model_code <> %s AND is_default = 1",
|
||||
(model_code,),
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE llm_model_config
|
||||
SET model_code = %s, model_name = %s, provider = %s, endpoint_url = %s, api_key = %s,
|
||||
llm_model_name = %s, llm_timeout = %s, llm_temperature = %s, llm_top_p = %s,
|
||||
llm_max_tokens = %s, llm_system_prompt = %s, description = %s, is_active = %s, is_default = %s
|
||||
WHERE model_code = %s
|
||||
""",
|
||||
(
|
||||
new_model_code,
|
||||
request.model_name,
|
||||
request.provider,
|
||||
request.endpoint_url,
|
||||
request.api_key,
|
||||
request.llm_model_name,
|
||||
request.llm_timeout,
|
||||
request.llm_temperature,
|
||||
request.llm_top_p,
|
||||
request.llm_max_tokens,
|
||||
request.llm_system_prompt,
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
1 if request.is_default else 0,
|
||||
model_code,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="更新LLM模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新LLM模型配置失败: {str(e)}")
|
||||
|
||||
|
||||
def list_audio_model_configs(scene: str = "all"):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
sql = """
|
||||
SELECT a.config_id, a.model_code, a.model_name, a.audio_scene, a.provider, a.endpoint_url, a.api_key,
|
||||
a.asr_model_name, a.asr_vocabulary_id, a.hot_word_group_id, a.asr_speaker_count, a.asr_language_hints,
|
||||
a.asr_disfluency_removal_enabled, a.asr_diarization_enabled,
|
||||
a.vp_template_text, a.vp_duration_seconds, a.vp_sample_rate, a.vp_channels, a.vp_max_size_bytes,
|
||||
a.extra_config, a.description, a.is_active, a.is_default, a.created_at, a.updated_at,
|
||||
g.name AS hot_word_group_name, g.vocabulary_id AS hot_word_group_vocab_id
|
||||
FROM audio_model_config a
|
||||
LEFT JOIN hot_word_group g ON g.id = a.hot_word_group_id
|
||||
"""
|
||||
params = []
|
||||
if scene in ("asr", "voiceprint"):
|
||||
sql += " WHERE a.audio_scene = %s"
|
||||
params.append(scene)
|
||||
sql += " ORDER BY a.audio_scene ASC, a.model_code ASC"
|
||||
cursor.execute(sql, tuple(params))
|
||||
rows = [_normalize_audio_row(row) for row in cursor.fetchall()]
|
||||
return create_api_response(code="200", message="获取音频模型配置成功", data={"items": rows, "total": len(rows)})
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取音频模型配置失败: {str(e)}")
|
||||
|
||||
|
||||
def create_audio_model_config(request):
|
||||
try:
|
||||
if request.audio_scene not in ("asr", "voiceprint"):
|
||||
return create_api_response(code="400", message="audio_scene 仅支持 asr 或 voiceprint")
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM audio_model_config WHERE model_code = %s", (request.model_code,))
|
||||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="模型编码已存在")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) AS total FROM audio_model_config WHERE audio_scene = %s", (request.audio_scene,))
|
||||
total_row = cursor.fetchone() or {"total": 0}
|
||||
is_default = bool(request.is_default) or total_row["total"] == 0
|
||||
if is_default:
|
||||
cursor.execute(
|
||||
"UPDATE audio_model_config SET is_default = 0 WHERE audio_scene = %s AND is_default = 1",
|
||||
(request.audio_scene,),
|
||||
)
|
||||
|
||||
asr_vocabulary_id = _resolve_hot_word_vocabulary_id(cursor, request)
|
||||
extra_config = _merge_audio_extra_config(request, vocabulary_id=asr_vocabulary_id)
|
||||
legacy_columns = _extract_legacy_audio_columns(request.audio_scene, extra_config)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO audio_model_config
|
||||
(model_code, model_name, audio_scene, provider, endpoint_url, api_key,
|
||||
asr_model_name, asr_vocabulary_id, hot_word_group_id, asr_speaker_count, asr_language_hints,
|
||||
asr_disfluency_removal_enabled, asr_diarization_enabled,
|
||||
vp_template_text, vp_duration_seconds, vp_sample_rate, vp_channels, vp_max_size_bytes,
|
||||
extra_config, description, is_active, is_default)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
request.model_code,
|
||||
request.model_name,
|
||||
request.audio_scene,
|
||||
request.provider,
|
||||
request.endpoint_url,
|
||||
request.api_key,
|
||||
legacy_columns["asr_model_name"],
|
||||
legacy_columns["asr_vocabulary_id"],
|
||||
request.hot_word_group_id,
|
||||
legacy_columns["asr_speaker_count"],
|
||||
legacy_columns["asr_language_hints"],
|
||||
legacy_columns["asr_disfluency_removal_enabled"],
|
||||
legacy_columns["asr_diarization_enabled"],
|
||||
legacy_columns["vp_template_text"],
|
||||
legacy_columns["vp_duration_seconds"],
|
||||
legacy_columns["vp_sample_rate"],
|
||||
legacy_columns["vp_channels"],
|
||||
legacy_columns["vp_max_size_bytes"],
|
||||
json.dumps(extra_config, ensure_ascii=False),
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
1 if is_default else 0,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="创建音频模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建音频模型配置失败: {str(e)}")
|
||||
|
||||
|
||||
def update_audio_model_config(model_code: str, request):
|
||||
try:
|
||||
if request.audio_scene not in ("asr", "voiceprint"):
|
||||
return create_api_response(code="400", message="audio_scene 仅支持 asr 或 voiceprint")
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM audio_model_config WHERE model_code = %s", (model_code,))
|
||||
existed = cursor.fetchone()
|
||||
if not existed:
|
||||
return create_api_response(code="404", message="模型配置不存在")
|
||||
|
||||
new_model_code = request.model_code or model_code
|
||||
if new_model_code != model_code:
|
||||
cursor.execute("SELECT config_id FROM audio_model_config WHERE model_code = %s", (new_model_code,))
|
||||
duplicate_row = cursor.fetchone()
|
||||
if duplicate_row and duplicate_row["config_id"] != existed["config_id"]:
|
||||
return create_api_response(code="400", message="新的模型编码已存在")
|
||||
|
||||
if request.is_default:
|
||||
cursor.execute(
|
||||
"UPDATE audio_model_config SET is_default = 0 WHERE audio_scene = %s AND model_code <> %s AND is_default = 1",
|
||||
(request.audio_scene, model_code),
|
||||
)
|
||||
|
||||
asr_vocabulary_id = _resolve_hot_word_vocabulary_id(cursor, request)
|
||||
extra_config = _merge_audio_extra_config(request, vocabulary_id=asr_vocabulary_id)
|
||||
legacy_columns = _extract_legacy_audio_columns(request.audio_scene, extra_config)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE audio_model_config
|
||||
SET model_code = %s, model_name = %s, audio_scene = %s, provider = %s, endpoint_url = %s, api_key = %s,
|
||||
asr_model_name = %s, asr_vocabulary_id = %s, hot_word_group_id = %s, asr_speaker_count = %s, asr_language_hints = %s,
|
||||
asr_disfluency_removal_enabled = %s, asr_diarization_enabled = %s,
|
||||
vp_template_text = %s, vp_duration_seconds = %s, vp_sample_rate = %s, vp_channels = %s, vp_max_size_bytes = %s,
|
||||
extra_config = %s, description = %s, is_active = %s, is_default = %s
|
||||
WHERE model_code = %s
|
||||
""",
|
||||
(
|
||||
new_model_code,
|
||||
request.model_name,
|
||||
request.audio_scene,
|
||||
request.provider,
|
||||
request.endpoint_url,
|
||||
request.api_key,
|
||||
legacy_columns["asr_model_name"],
|
||||
legacy_columns["asr_vocabulary_id"],
|
||||
request.hot_word_group_id,
|
||||
legacy_columns["asr_speaker_count"],
|
||||
legacy_columns["asr_language_hints"],
|
||||
legacy_columns["asr_disfluency_removal_enabled"],
|
||||
legacy_columns["asr_diarization_enabled"],
|
||||
legacy_columns["vp_template_text"],
|
||||
legacy_columns["vp_duration_seconds"],
|
||||
legacy_columns["vp_sample_rate"],
|
||||
legacy_columns["vp_channels"],
|
||||
legacy_columns["vp_max_size_bytes"],
|
||||
json.dumps(extra_config, ensure_ascii=False),
|
||||
request.description,
|
||||
1 if request.is_active else 0,
|
||||
1 if request.is_default else 0,
|
||||
model_code,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="更新音频模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新音频模型配置失败: {str(e)}")
|
||||
|
||||
|
||||
def delete_llm_model_config(model_code: str):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM llm_model_config WHERE model_code = %s", (model_code,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="模型配置不存在")
|
||||
|
||||
cursor.execute("DELETE FROM llm_model_config WHERE model_code = %s", (model_code,))
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="删除LLM模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"删除LLM模型配置失败: {str(e)}")
|
||||
|
||||
|
||||
def delete_audio_model_config(model_code: str):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT config_id FROM audio_model_config WHERE model_code = %s", (model_code,))
|
||||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="模型配置不存在")
|
||||
|
||||
cursor.execute("DELETE FROM audio_model_config WHERE model_code = %s", (model_code,))
|
||||
conn.commit()
|
||||
return create_api_response(code="200", message="删除音频模型配置成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"删除音频模型配置失败: {str(e)}")
|
||||
|
||||
|
||||
def test_llm_model_config(request):
|
||||
try:
|
||||
payload = request.model_dump() if hasattr(request, "model_dump") else request.dict()
|
||||
result = llm_service.test_model(payload, prompt=request.test_prompt)
|
||||
return create_api_response(code="200", message="LLM模型测试成功", data=result)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"LLM模型测试失败: {str(e)}")
|
||||
|
||||
|
||||
def test_audio_model_config(request):
|
||||
try:
|
||||
if request.audio_scene != "asr":
|
||||
return create_api_response(code="400", message="当前仅支持音频识别(ASR)测试")
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
vocabulary_id = _resolve_hot_word_vocabulary_id(cursor, request)
|
||||
|
||||
extra_config = _merge_audio_extra_config(request, vocabulary_id=vocabulary_id)
|
||||
runtime_config = {
|
||||
"provider": request.provider,
|
||||
"endpoint_url": request.endpoint_url,
|
||||
"api_key": request.api_key,
|
||||
"audio_scene": request.audio_scene,
|
||||
"hot_word_group_id": request.hot_word_group_id,
|
||||
**extra_config,
|
||||
}
|
||||
result = transcription_service.test_asr_model(runtime_config, test_file_url=request.test_file_url)
|
||||
return create_api_response(code="200", message="音频模型测试任务已提交", data=result)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"音频模型测试失败: {str(e)}")
|
||||
|
||||
|
||||
def get_public_system_config():
|
||||
try:
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="获取公开配置成功",
|
||||
data=SystemConfigService.get_public_configs(),
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取公开配置失败: {str(e)}")
|
||||
|
||||
|
||||
def get_system_config_compat():
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT param_key, param_value
|
||||
FROM sys_system_parameters
|
||||
WHERE is_active = 1
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
data = {row["param_key"]: row["param_value"] for row in rows}
|
||||
|
||||
if "max_audio_size" in data:
|
||||
try:
|
||||
data["MAX_FILE_SIZE"] = int(data["max_audio_size"]) * 1024 * 1024
|
||||
except Exception:
|
||||
data["MAX_FILE_SIZE"] = 100 * 1024 * 1024
|
||||
if "max_image_size" in data:
|
||||
try:
|
||||
data["MAX_IMAGE_SIZE"] = int(data["max_image_size"]) * 1024 * 1024
|
||||
except Exception:
|
||||
data["MAX_IMAGE_SIZE"] = 10 * 1024 * 1024
|
||||
else:
|
||||
data.setdefault("MAX_IMAGE_SIZE", 10 * 1024 * 1024)
|
||||
|
||||
return create_api_response(code="200", message="获取系统配置成功", data=data)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取系统配置失败: {str(e)}")
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -19,6 +19,11 @@ import AccountSettings from './pages/AccountSettings';
|
|||
import MeetingCenterPage from './pages/MeetingCenterPage';
|
||||
import MainLayout from './components/MainLayout';
|
||||
import menuService from './services/menuService';
|
||||
import {
|
||||
clearAuthSession,
|
||||
getStoredUser,
|
||||
setStoredAuthPayload,
|
||||
} from './services/authSessionService';
|
||||
import configService from './utils/configService';
|
||||
import './App.css';
|
||||
import './styles/console-theme.css';
|
||||
|
|
@ -84,26 +89,8 @@ function App() {
|
|||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load user from localStorage on app start
|
||||
useEffect(() => {
|
||||
const savedAuth = localStorage.getItem('iMeetingUser');
|
||||
if (savedAuth && savedAuth !== "undefined" && savedAuth !== "null") {
|
||||
try {
|
||||
const authData = JSON.parse(savedAuth);
|
||||
// 如果数据包含 user 字段,则提取 user 字段(适应新结构)
|
||||
// 否则使用整个对象(兼容旧结构)
|
||||
const userData = authData.user || authData;
|
||||
|
||||
if (userData && typeof userData === 'object' && (userData.user_id || userData.id)) {
|
||||
setUser(userData);
|
||||
} else {
|
||||
localStorage.removeItem('iMeetingUser');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing saved user:', error);
|
||||
localStorage.removeItem('iMeetingUser');
|
||||
}
|
||||
}
|
||||
setUser(getStoredUser());
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
|
|
@ -124,11 +111,7 @@ function App() {
|
|||
const handleLogin = (authData) => {
|
||||
if (authData) {
|
||||
menuService.clearCache();
|
||||
// 提取用户信息用于 UI 展示
|
||||
const userData = authData.user || authData;
|
||||
setUser(userData);
|
||||
// 存入完整 auth 数据(包含 token)供拦截器使用
|
||||
localStorage.setItem('iMeetingUser', JSON.stringify(authData));
|
||||
setUser(setStoredAuthPayload(authData));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -139,9 +122,8 @@ function App() {
|
|||
console.error('Logout API error:', error);
|
||||
} finally {
|
||||
setUser(null);
|
||||
localStorage.removeItem('iMeetingUser');
|
||||
menuService.clearCache();
|
||||
window.location.href = '/';
|
||||
clearAuthSession();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
WindowsOutlined,
|
||||
RightOutlined
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
|
@ -24,7 +24,7 @@ const ClientDownloads = () => {
|
|||
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.PUBLIC_LIST));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.PUBLIC_LIST));
|
||||
setClients(response.data.clients || []);
|
||||
} catch (error) {
|
||||
console.error('获取下载列表失败:', error);
|
||||
|
|
|
|||
|
|
@ -1,160 +1,28 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Drawer, Form, Input, Button, DatePicker, Select, Space, App, Upload, Card, Progress, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { Drawer, Form, Input, Button, DatePicker, Select, Space, Upload, Card, Progress, Typography } from 'antd';
|
||||
import { SaveOutlined, UploadOutlined, DeleteOutlined, AudioOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import configService from '../utils/configService';
|
||||
import { AUDIO_UPLOAD_ACCEPT, uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
|
||||
import { AUDIO_UPLOAD_ACCEPT } from '../services/meetingAudioService';
|
||||
import useMeetingFormDrawer from '../hooks/useMeetingFormDrawer';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [prompts, setPrompts] = useState([]);
|
||||
const [selectedAudioFile, setSelectedAudioFile] = useState(null);
|
||||
const [audioUploading, setAudioUploading] = useState(false);
|
||||
const [audioUploadProgress, setAudioUploadProgress] = useState(0);
|
||||
const [audioUploadMessage, setAudioUploadMessage] = useState('');
|
||||
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
||||
|
||||
const isEdit = Boolean(meetingId);
|
||||
|
||||
const fetchOptions = useCallback(async () => {
|
||||
try {
|
||||
const [uRes, pRes] = await Promise.all([
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK'))),
|
||||
]);
|
||||
setUsers(uRes.data.users || []);
|
||||
setPrompts(pRes.data.prompts || []);
|
||||
} catch {
|
||||
message.error('加载会议表单选项失败');
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
const loadAudioUploadConfig = useCallback(async () => {
|
||||
try {
|
||||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||
} catch {
|
||||
setMaxAudioSize(100 * 1024 * 1024);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchMeeting = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
||||
const meeting = res.data;
|
||||
form.setFieldsValue({
|
||||
title: meeting.title,
|
||||
meeting_time: dayjs(meeting.meeting_time),
|
||||
attendee_ids: meeting.attendee_ids || meeting.attendees?.map((a) => a.user_id).filter(Boolean) || [],
|
||||
prompt_id: meeting.prompt_id,
|
||||
tags: meeting.tags?.map((t) => t.name) || [],
|
||||
});
|
||||
} catch {
|
||||
message.error('加载会议数据失败');
|
||||
}
|
||||
}, [form, meetingId, message]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
fetchOptions();
|
||||
loadAudioUploadConfig();
|
||||
if (isEdit) {
|
||||
fetchMeeting();
|
||||
} else {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ meeting_time: dayjs() });
|
||||
setSelectedAudioFile(null);
|
||||
setAudioUploading(false);
|
||||
setAudioUploadProgress(0);
|
||||
setAudioUploadMessage('');
|
||||
}
|
||||
}, [fetchMeeting, fetchOptions, form, isEdit, loadAudioUploadConfig, open]);
|
||||
|
||||
const handleAudioBeforeUpload = (file) => {
|
||||
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||
if (validationMessage) {
|
||||
message.warning(validationMessage);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
setSelectedAudioFile(file);
|
||||
return false;
|
||||
};
|
||||
|
||||
const clearSelectedAudio = () => {
|
||||
setSelectedAudioFile(null);
|
||||
setAudioUploadProgress(0);
|
||||
setAudioUploadMessage('');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
const payload = {
|
||||
...values,
|
||||
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
||||
attendee_ids: values.attendee_ids || [],
|
||||
tags: values.tags?.join(',') || '',
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meetingId)), payload);
|
||||
message.success('会议更新成功');
|
||||
} else {
|
||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
||||
if (res.code === '200') {
|
||||
const newMeetingId = res.data.meeting_id;
|
||||
if (selectedAudioFile) {
|
||||
setAudioUploading(true);
|
||||
setAudioUploadProgress(0);
|
||||
setAudioUploadMessage('正在上传音频文件...');
|
||||
try {
|
||||
await uploadMeetingAudio({
|
||||
meetingId: newMeetingId,
|
||||
file: selectedAudioFile,
|
||||
promptId: values.prompt_id,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
setAudioUploadProgress(Math.min(100, Math.round((progressEvent.loaded * 100) / progressEvent.total)));
|
||||
}
|
||||
setAudioUploadMessage('正在上传音频文件...');
|
||||
},
|
||||
});
|
||||
setAudioUploadProgress(100);
|
||||
setAudioUploadMessage('上传完成,正在启动转录任务...');
|
||||
message.success('会议创建成功,音频已开始上传处理');
|
||||
} catch (uploadError) {
|
||||
message.warning(uploadError?.response?.data?.message || uploadError?.response?.data?.detail || '会议已创建,但音频上传失败,请在详情页重试');
|
||||
} finally {
|
||||
setAudioUploading(false);
|
||||
}
|
||||
} else {
|
||||
message.success('会议创建成功');
|
||||
}
|
||||
|
||||
onSuccess?.(res.data.meeting_id);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
if (!error?.errorFields) {
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.detail || '操作失败');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const {
|
||||
form,
|
||||
isEdit,
|
||||
loading,
|
||||
users,
|
||||
prompts,
|
||||
selectedAudioFile,
|
||||
audioUploading,
|
||||
audioUploadProgress,
|
||||
audioUploadMessage,
|
||||
maxAudioSize,
|
||||
handleAudioBeforeUpload,
|
||||
clearSelectedAudio,
|
||||
handleSubmit,
|
||||
} = useMeetingFormDrawer({ open, onClose, onSuccess, meetingId });
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
|
|
@ -172,7 +40,7 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
|||
</Space>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={{ meeting_time: dayjs() }}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="会议主题" name="title" rules={[{ required: true, message: '请输入会议主题' }]}>
|
||||
<Input placeholder="请输入会议主题..." />
|
||||
</Form.Item>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Tag, Space, Typography, Skeleton, Empty } from 'antd';
|
||||
import { TagsOutlined } from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
|
||||
const { CheckableTag } = Tag;
|
||||
|
|
@ -19,7 +19,7 @@ const TagCloud = ({
|
|||
const fetchAllTags = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST));
|
||||
const tags = response.data || [];
|
||||
setAllTags(limitTags ? tags.slice(0, 15) : tags);
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { App } from 'antd';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
|
||||
const AUTO_REFRESH_INTERVAL = 30;
|
||||
|
||||
export default function useAdminDashboardPage() {
|
||||
const { message, modal } = App.useApp();
|
||||
const inFlightRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
const [stats, setStats] = useState(null);
|
||||
const [onlineUsers, setOnlineUsers] = useState([]);
|
||||
const [usersList, setUsersList] = useState([]);
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [resources, setResources] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [taskLoading, setTaskLoading] = useState(false);
|
||||
const [lastUpdatedAt, setLastUpdatedAt] = useState(null);
|
||||
|
||||
const [taskType, setTaskType] = useState('all');
|
||||
const [taskStatus, setTaskStatus] = useState('all');
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
const [countdown, setCountdown] = useState(AUTO_REFRESH_INTERVAL);
|
||||
|
||||
const [showMeetingModal, setShowMeetingModal] = useState(false);
|
||||
const [meetingDetails, setMeetingDetails] = useState(null);
|
||||
const [meetingLoading, setMeetingLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.DASHBOARD_STATS));
|
||||
if (response.code === '200') {
|
||||
setStats(response.data);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchOnlineUsers = useCallback(async () => {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.ONLINE_USERS));
|
||||
if (response.code === '200') {
|
||||
setOnlineUsers(response.data.users || []);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchUsersList = useCallback(async () => {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.USER_STATS));
|
||||
if (response.code === '200') {
|
||||
setUsersList(response.data.users || []);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchTasks = useCallback(async () => {
|
||||
try {
|
||||
setTaskLoading(true);
|
||||
const params = new URLSearchParams();
|
||||
if (taskType !== 'all') params.append('task_type', taskType);
|
||||
if (taskStatus !== 'all') params.append('status', taskStatus);
|
||||
params.append('limit', '20');
|
||||
|
||||
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.ADMIN.TASKS_MONITOR}?${params.toString()}`));
|
||||
if (response.code === '200') {
|
||||
setTasks(response.data.tasks || []);
|
||||
}
|
||||
} finally {
|
||||
setTaskLoading(false);
|
||||
}
|
||||
}, [taskStatus, taskType]);
|
||||
|
||||
const fetchResources = useCallback(async () => {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_RESOURCES));
|
||||
if (response.code === '200') {
|
||||
setResources(response.data);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchAllData = useCallback(async ({ silent = false } = {}) => {
|
||||
if (inFlightRef.current) {
|
||||
return;
|
||||
}
|
||||
inFlightRef.current = true;
|
||||
|
||||
try {
|
||||
if (!silent && mountedRef.current) {
|
||||
setLoading(true);
|
||||
}
|
||||
await Promise.all([fetchStats(), fetchOnlineUsers(), fetchUsersList(), fetchTasks(), fetchResources()]);
|
||||
if (mountedRef.current) {
|
||||
setLastUpdatedAt(new Date());
|
||||
setCountdown(AUTO_REFRESH_INTERVAL);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
if (mountedRef.current && !silent) {
|
||||
message.error('加载数据失败,请稍后重试');
|
||||
}
|
||||
} finally {
|
||||
inFlightRef.current = false;
|
||||
if (mountedRef.current && !silent) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [fetchOnlineUsers, fetchResources, fetchStats, fetchTasks, fetchUsersList, message]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllData();
|
||||
}, [fetchAllData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh || showMeetingModal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
fetchAllData({ silent: true });
|
||||
return AUTO_REFRESH_INTERVAL;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [autoRefresh, fetchAllData, showMeetingModal]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
}, [fetchTasks]);
|
||||
|
||||
const handleKickUser = (user) => {
|
||||
modal.confirm({
|
||||
title: '踢出用户',
|
||||
content: `确定要踢出用户"${user.caption}"吗?该用户将被强制下线。`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.KICK_USER(user.user_id)));
|
||||
if (response.code === '200') {
|
||||
message.success('用户已被踢出');
|
||||
fetchOnlineUsers();
|
||||
}
|
||||
} catch {
|
||||
message.error('踢出用户失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewMeeting = async (meetingId) => {
|
||||
setMeetingLoading(true);
|
||||
setShowMeetingModal(true);
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
||||
if (response.code === '200') {
|
||||
setMeetingDetails(response.data);
|
||||
}
|
||||
} catch {
|
||||
message.error('获取会议详情失败');
|
||||
} finally {
|
||||
setMeetingLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadTranscript = async (meetingId) => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(`/api/meetings/${meetingId}/transcript`));
|
||||
if (response.code === '200') {
|
||||
const dataStr = JSON.stringify(response.data, null, 2);
|
||||
const blob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `transcript_${meetingId}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch {
|
||||
message.error('下载失败');
|
||||
}
|
||||
};
|
||||
|
||||
const closeMeetingModal = () => {
|
||||
setShowMeetingModal(false);
|
||||
setMeetingDetails(null);
|
||||
};
|
||||
|
||||
const taskCompletionRate = useMemo(() => {
|
||||
const all = tasks.length || 1;
|
||||
const completed = tasks.filter((item) => item.status === 'completed').length;
|
||||
return Math.round((completed / all) * 100);
|
||||
}, [tasks]);
|
||||
|
||||
return {
|
||||
stats,
|
||||
onlineUsers,
|
||||
usersList,
|
||||
tasks,
|
||||
resources,
|
||||
loading,
|
||||
taskLoading,
|
||||
lastUpdatedAt,
|
||||
taskType,
|
||||
setTaskType,
|
||||
taskStatus,
|
||||
setTaskStatus,
|
||||
autoRefresh,
|
||||
setAutoRefresh,
|
||||
countdown,
|
||||
showMeetingModal,
|
||||
meetingDetails,
|
||||
meetingLoading,
|
||||
fetchAllData,
|
||||
fetchOnlineUsers,
|
||||
handleKickUser,
|
||||
handleViewMeeting,
|
||||
handleDownloadTranscript,
|
||||
closeMeetingModal,
|
||||
taskCompletionRate,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,979 @@
|
|||
import { useEffect, useEffectEvent, useRef, useState } from 'react';
|
||||
import { App } from 'antd';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import configService from '../utils/configService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import { uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
|
||||
|
||||
const TRANSCRIPT_INITIAL_RENDER_COUNT = 80;
|
||||
const TRANSCRIPT_RENDER_STEP = 120;
|
||||
|
||||
const findTranscriptIndexByTime = (segments, timeMs) => {
|
||||
let left = 0;
|
||||
let right = segments.length - 1;
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
const segment = segments[mid];
|
||||
|
||||
if (timeMs < segment.start_time_ms) {
|
||||
right = mid - 1;
|
||||
} else if (timeMs > segment.end_time_ms) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
return mid;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
const generateRandomPassword = (length = 4) => {
|
||||
const charset = '0123456789';
|
||||
return Array.from({ length }, () => charset[Math.floor(Math.random() * charset.length)]).join('');
|
||||
};
|
||||
|
||||
const buildSpeakerState = (segments) => {
|
||||
const speakerMap = new Map();
|
||||
|
||||
segments.forEach((segment) => {
|
||||
if (segment?.speaker_id == null || speakerMap.has(segment.speaker_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
speakerMap.set(segment.speaker_id, {
|
||||
speaker_id: segment.speaker_id,
|
||||
speaker_tag: segment.speaker_tag || `发言人 ${segment.speaker_id}`,
|
||||
});
|
||||
});
|
||||
|
||||
const speakerList = Array.from(speakerMap.values()).sort((a, b) => a.speaker_id - b.speaker_id);
|
||||
const editingSpeakers = {};
|
||||
speakerList.forEach((speaker) => {
|
||||
editingSpeakers[speaker.speaker_id] = speaker.speaker_tag;
|
||||
});
|
||||
|
||||
return { speakerList, editingSpeakers };
|
||||
};
|
||||
|
||||
export default function useMeetingDetailsPage({ user }) {
|
||||
const { meeting_id: meetingId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { message, modal } = App.useApp();
|
||||
|
||||
const [meeting, setMeeting] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [transcript, setTranscript] = useState([]);
|
||||
const [transcriptLoading, setTranscriptLoading] = useState(false);
|
||||
const [audioUrl, setAudioUrl] = useState(null);
|
||||
|
||||
const [editingSpeakers, setEditingSpeakers] = useState({});
|
||||
const [speakerList, setSpeakerList] = useState([]);
|
||||
|
||||
const [transcriptionStatus, setTranscriptionStatus] = useState(null);
|
||||
const [transcriptionProgress, setTranscriptionProgress] = useState(0);
|
||||
const [currentHighlightIndex, setCurrentHighlightIndex] = useState(-1);
|
||||
|
||||
const [showSummaryDrawer, setShowSummaryDrawer] = useState(false);
|
||||
const [summaryLoading, setSummaryLoading] = useState(false);
|
||||
const [summaryResourcesLoading, setSummaryResourcesLoading] = useState(false);
|
||||
const [userPrompt, setUserPrompt] = useState('');
|
||||
const [promptList, setPromptList] = useState([]);
|
||||
const [selectedPromptId, setSelectedPromptId] = useState(null);
|
||||
const [summaryTaskProgress, setSummaryTaskProgress] = useState(0);
|
||||
const [summaryTaskMessage, setSummaryTaskMessage] = useState('');
|
||||
const [llmModels, setLlmModels] = useState([]);
|
||||
const [selectedModelCode, setSelectedModelCode] = useState(null);
|
||||
|
||||
const [showSpeakerDrawer, setShowSpeakerDrawer] = useState(false);
|
||||
const [viewingPrompt, setViewingPrompt] = useState(null);
|
||||
const [editDrawerOpen, setEditDrawerOpen] = useState(false);
|
||||
const [showQRModal, setShowQRModal] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploadStatusMessage, setUploadStatusMessage] = useState('');
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [accessPasswordEnabled, setAccessPasswordEnabled] = useState(false);
|
||||
const [accessPasswordDraft, setAccessPasswordDraft] = useState('');
|
||||
const [savingAccessPassword, setSavingAccessPassword] = useState(false);
|
||||
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
||||
|
||||
const [showTranscriptEditDrawer, setShowTranscriptEditDrawer] = useState(false);
|
||||
const [editingSegments, setEditingSegments] = useState({});
|
||||
|
||||
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||
const [editingSummaryContent, setEditingSummaryContent] = useState('');
|
||||
const [inlineSpeakerEdit, setInlineSpeakerEdit] = useState(null);
|
||||
const [inlineSpeakerEditSegmentId, setInlineSpeakerEditSegmentId] = useState(null);
|
||||
const [inlineSpeakerValue, setInlineSpeakerValue] = useState('');
|
||||
const [inlineSegmentEditId, setInlineSegmentEditId] = useState(null);
|
||||
const [inlineSegmentValue, setInlineSegmentValue] = useState('');
|
||||
const [savingInlineEdit, setSavingInlineEdit] = useState(false);
|
||||
const [transcriptVisibleCount, setTranscriptVisibleCount] = useState(TRANSCRIPT_INITIAL_RENDER_COUNT);
|
||||
|
||||
const audioRef = useRef(null);
|
||||
const transcriptRefs = useRef([]);
|
||||
const statusCheckIntervalRef = useRef(null);
|
||||
const summaryPollIntervalRef = useRef(null);
|
||||
const summaryBootstrapTimeoutRef = useRef(null);
|
||||
const activeSummaryTaskIdRef = useRef(null);
|
||||
|
||||
const isMeetingOwner = user?.user_id === meeting?.creator_id;
|
||||
const creatorName = meeting?.creator_username || '未知创建人';
|
||||
const hasUploadedAudio = Boolean(audioUrl);
|
||||
const isTranscriptionRunning = ['pending', 'processing'].includes(transcriptionStatus?.status);
|
||||
const isSummaryRunning = summaryLoading;
|
||||
const displayUploadProgress = Math.max(0, Math.min(uploadProgress, 100));
|
||||
const displayTranscriptionProgress = Math.max(0, Math.min(transcriptionProgress, 100));
|
||||
const displaySummaryProgress = Math.max(0, Math.min(summaryTaskProgress, 100));
|
||||
const summaryDisabledReason = isUploading
|
||||
? '音频上传中,暂不允许重新总结'
|
||||
: !hasUploadedAudio
|
||||
? '请先上传音频后再总结'
|
||||
: isTranscriptionRunning
|
||||
? '转录进行中,完成后会自动总结'
|
||||
: '';
|
||||
const isSummaryActionDisabled = isUploading || !hasUploadedAudio || isTranscriptionRunning || summaryLoading;
|
||||
|
||||
const clearSummaryBootstrapPolling = () => {
|
||||
if (summaryBootstrapTimeoutRef.current) {
|
||||
clearTimeout(summaryBootstrapTimeoutRef.current);
|
||||
summaryBootstrapTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const loadAudioUploadConfig = async () => {
|
||||
try {
|
||||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||
} catch {
|
||||
setMaxAudioSize(100 * 1024 * 1024);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPromptList = async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK')));
|
||||
setPromptList(res.data.prompts || []);
|
||||
const defaultPrompt = res.data.prompts?.find((prompt) => prompt.is_default) || res.data.prompts?.[0];
|
||||
if (defaultPrompt) {
|
||||
setSelectedPromptId(defaultPrompt.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('加载提示词列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLlmModels = async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LLM_MODELS));
|
||||
const models = Array.isArray(res.data) ? res.data : (res.data?.models || []);
|
||||
setLlmModels(models);
|
||||
const defaultModel = models.find((model) => model.is_default);
|
||||
if (defaultModel) {
|
||||
setSelectedModelCode(defaultModel.model_code);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('加载模型列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSummaryResources = async () => {
|
||||
setSummaryResourcesLoading(true);
|
||||
try {
|
||||
await Promise.allSettled([
|
||||
promptList.length > 0 ? Promise.resolve() : fetchPromptList(),
|
||||
llmModels.length > 0 ? Promise.resolve() : fetchLlmModels(),
|
||||
]);
|
||||
} finally {
|
||||
setSummaryResourcesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTranscript = async () => {
|
||||
setTranscriptLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meetingId)));
|
||||
const segments = Array.isArray(res.data) ? res.data : [];
|
||||
const speakerState = buildSpeakerState(segments);
|
||||
|
||||
setTranscript(segments);
|
||||
setSpeakerList(speakerState.speakerList);
|
||||
setEditingSpeakers(speakerState.editingSpeakers);
|
||||
} catch {
|
||||
setTranscript([]);
|
||||
setSpeakerList([]);
|
||||
setEditingSpeakers({});
|
||||
} finally {
|
||||
setTranscriptLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMeetingDetails = async (options = {}) => {
|
||||
const { showPageLoading = true } = options;
|
||||
try {
|
||||
if (showPageLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
||||
const meetingData = response.data;
|
||||
|
||||
setMeeting(meetingData);
|
||||
if (meetingData.prompt_id) {
|
||||
setSelectedPromptId(meetingData.prompt_id);
|
||||
}
|
||||
setAccessPasswordEnabled(Boolean(meetingData.access_password));
|
||||
setAccessPasswordDraft(meetingData.access_password || '');
|
||||
|
||||
if (meetingData.transcription_status) {
|
||||
const nextStatus = meetingData.transcription_status;
|
||||
setTranscriptionStatus(nextStatus);
|
||||
setTranscriptionProgress(nextStatus.progress || 0);
|
||||
} else {
|
||||
setTranscriptionStatus(null);
|
||||
setTranscriptionProgress(0);
|
||||
}
|
||||
|
||||
if (meetingData.llm_status) {
|
||||
const llmStatus = meetingData.llm_status;
|
||||
clearSummaryBootstrapPolling();
|
||||
setSummaryTaskProgress(llmStatus.progress || 0);
|
||||
setSummaryTaskMessage(
|
||||
llmStatus.message
|
||||
|| (llmStatus.status === 'processing'
|
||||
? 'AI 正在分析会议内容...'
|
||||
: llmStatus.status === 'pending'
|
||||
? 'AI 总结任务排队中...'
|
||||
: '')
|
||||
);
|
||||
if (!['pending', 'processing'].includes(llmStatus.status)) {
|
||||
setSummaryLoading(false);
|
||||
}
|
||||
} else if (meetingData.transcription_status?.status === 'completed' && !meetingData.summary) {
|
||||
if (!activeSummaryTaskIdRef.current) {
|
||||
setSummaryLoading(true);
|
||||
setSummaryTaskProgress(0);
|
||||
setSummaryTaskMessage('转录完成,正在启动 AI 分析...');
|
||||
}
|
||||
} else {
|
||||
clearSummaryBootstrapPolling();
|
||||
if (!activeSummaryTaskIdRef.current) {
|
||||
setSummaryLoading(false);
|
||||
setSummaryTaskProgress(0);
|
||||
setSummaryTaskMessage('');
|
||||
}
|
||||
}
|
||||
|
||||
const hasAudioFile = Boolean(meetingData.audio_file_path && String(meetingData.audio_file_path).length > 5);
|
||||
setAudioUrl(hasAudioFile ? buildApiUrl(`${API_ENDPOINTS.MEETINGS.AUDIO(meetingId)}/stream`) : null);
|
||||
|
||||
return meetingData;
|
||||
} catch {
|
||||
message.error('加载会议详情失败');
|
||||
return null;
|
||||
} finally {
|
||||
if (showPageLoading) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startStatusPolling = (taskId) => {
|
||||
if (statusCheckIntervalRef.current) {
|
||||
clearInterval(statusCheckIntervalRef.current);
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.TASKS.TRANSCRIPTION_STATUS(taskId)));
|
||||
const status = res.data;
|
||||
setTranscriptionStatus(status);
|
||||
setTranscriptionProgress(status.progress || 0);
|
||||
setMeeting((prev) => (prev ? { ...prev, transcription_status: status } : prev));
|
||||
|
||||
if (['completed', 'failed', 'error', 'cancelled'].includes(status.status)) {
|
||||
clearInterval(interval);
|
||||
statusCheckIntervalRef.current = null;
|
||||
|
||||
if (status.status === 'completed') {
|
||||
await fetchTranscript();
|
||||
await fetchMeetingDetails({ showPageLoading: false });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
clearInterval(interval);
|
||||
statusCheckIntervalRef.current = null;
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
statusCheckIntervalRef.current = interval;
|
||||
};
|
||||
|
||||
const startSummaryPolling = (taskId, options = {}) => {
|
||||
const { closeDrawerOnComplete = false } = options;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
if (summaryPollIntervalRef.current && activeSummaryTaskIdRef.current === taskId) {
|
||||
return;
|
||||
}
|
||||
if (summaryPollIntervalRef.current) {
|
||||
clearInterval(summaryPollIntervalRef.current);
|
||||
}
|
||||
|
||||
activeSummaryTaskIdRef.current = taskId;
|
||||
setSummaryLoading(true);
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const statusRes = await apiClient.get(buildApiUrl(API_ENDPOINTS.TASKS.SUMMARY_STATUS(taskId)));
|
||||
const status = statusRes.data;
|
||||
setSummaryTaskProgress(status.progress || 0);
|
||||
setSummaryTaskMessage(status.message || 'AI 正在分析会议内容...');
|
||||
setMeeting((prev) => (prev ? { ...prev, llm_status: status } : prev));
|
||||
|
||||
if (status.status === 'completed') {
|
||||
clearInterval(interval);
|
||||
summaryPollIntervalRef.current = null;
|
||||
activeSummaryTaskIdRef.current = null;
|
||||
setSummaryLoading(false);
|
||||
|
||||
if (closeDrawerOnComplete) {
|
||||
setShowSummaryDrawer(false);
|
||||
}
|
||||
|
||||
await fetchMeetingDetails({ showPageLoading: false });
|
||||
} else if (status.status === 'failed') {
|
||||
clearInterval(interval);
|
||||
summaryPollIntervalRef.current = null;
|
||||
activeSummaryTaskIdRef.current = null;
|
||||
setSummaryLoading(false);
|
||||
message.error(status.error_message || '生成总结失败');
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(interval);
|
||||
summaryPollIntervalRef.current = null;
|
||||
activeSummaryTaskIdRef.current = null;
|
||||
setSummaryLoading(false);
|
||||
message.error(error?.response?.data?.message || '获取总结状态失败');
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(poll, 3000);
|
||||
summaryPollIntervalRef.current = interval;
|
||||
poll();
|
||||
};
|
||||
|
||||
const scheduleSummaryBootstrapPolling = (attempt = 0) => {
|
||||
if (summaryPollIntervalRef.current || activeSummaryTaskIdRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearSummaryBootstrapPolling();
|
||||
if (attempt >= 10) {
|
||||
setSummaryLoading(false);
|
||||
setSummaryTaskMessage('');
|
||||
return;
|
||||
}
|
||||
|
||||
summaryBootstrapTimeoutRef.current = setTimeout(async () => {
|
||||
summaryBootstrapTimeoutRef.current = null;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
||||
const meetingData = response.data;
|
||||
if (meetingData.llm_status?.task_id) {
|
||||
startSummaryPolling(meetingData.llm_status.task_id);
|
||||
return;
|
||||
}
|
||||
if (meetingData.llm_status || meetingData.summary) {
|
||||
await fetchMeetingDetails({ showPageLoading: false });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
if (attempt >= 9) {
|
||||
setSummaryLoading(false);
|
||||
setSummaryTaskMessage('');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleSummaryBootstrapPolling(attempt + 1);
|
||||
}, attempt === 0 ? 1200 : 2000);
|
||||
};
|
||||
|
||||
const bootstrapMeetingPage = useEffectEvent(async () => {
|
||||
const meetingData = await fetchMeetingDetails();
|
||||
await fetchTranscript();
|
||||
await loadAudioUploadConfig();
|
||||
|
||||
if (meetingData?.transcription_status?.task_id && ['pending', 'processing'].includes(meetingData.transcription_status.status)) {
|
||||
startStatusPolling(meetingData.transcription_status.task_id);
|
||||
}
|
||||
|
||||
if (meetingData?.llm_status?.task_id && ['pending', 'processing'].includes(meetingData.llm_status.status)) {
|
||||
startSummaryPolling(meetingData.llm_status.task_id);
|
||||
} else if (meetingData?.transcription_status?.status === 'completed' && !meetingData.summary) {
|
||||
scheduleSummaryBootstrapPolling();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
bootstrapMeetingPage();
|
||||
|
||||
return () => {
|
||||
if (statusCheckIntervalRef.current) {
|
||||
clearInterval(statusCheckIntervalRef.current);
|
||||
}
|
||||
if (summaryPollIntervalRef.current) {
|
||||
clearInterval(summaryPollIntervalRef.current);
|
||||
}
|
||||
if (summaryBootstrapTimeoutRef.current) {
|
||||
clearTimeout(summaryBootstrapTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [bootstrapMeetingPage, meetingId]);
|
||||
|
||||
const openSummaryResources = useEffectEvent(() => {
|
||||
fetchSummaryResources();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSummaryDrawer) {
|
||||
return;
|
||||
}
|
||||
if (promptList.length > 0 && llmModels.length > 0) {
|
||||
return;
|
||||
}
|
||||
openSummaryResources();
|
||||
}, [llmModels.length, openSummaryResources, promptList.length, showSummaryDrawer]);
|
||||
|
||||
useEffect(() => {
|
||||
transcriptRefs.current = [];
|
||||
setTranscriptVisibleCount(Math.min(transcript.length, TRANSCRIPT_INITIAL_RENDER_COUNT));
|
||||
}, [transcript]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentHighlightIndex < 0 || currentHighlightIndex < transcriptVisibleCount || transcriptVisibleCount >= transcript.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTranscriptVisibleCount((prev) => Math.min(
|
||||
transcript.length,
|
||||
Math.max(prev + TRANSCRIPT_RENDER_STEP, currentHighlightIndex + 20)
|
||||
));
|
||||
}, [currentHighlightIndex, transcript.length, transcriptVisibleCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentHighlightIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
transcriptRefs.current[currentHighlightIndex]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}, [currentHighlightIndex, transcriptVisibleCount]);
|
||||
|
||||
const validateAudioBeforeUpload = (file) => {
|
||||
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||
if (validationMessage) {
|
||||
message.warning(validationMessage);
|
||||
return validationMessage;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleUploadAudio = async (file) => {
|
||||
const validationMessage = validateAudioBeforeUpload(file);
|
||||
if (validationMessage) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
setUploadProgress(0);
|
||||
setUploadStatusMessage('正在上传音频文件...');
|
||||
|
||||
try {
|
||||
await uploadMeetingAudio({
|
||||
meetingId,
|
||||
file,
|
||||
promptId: meeting?.prompt_id,
|
||||
modelCode: selectedModelCode,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
setUploadProgress(Math.min(100, Math.round((progressEvent.loaded * 100) / progressEvent.total)));
|
||||
}
|
||||
setUploadStatusMessage('正在上传音频文件...');
|
||||
},
|
||||
});
|
||||
|
||||
setUploadProgress(100);
|
||||
setUploadStatusMessage('上传完成,正在启动转录任务...');
|
||||
message.success('音频上传成功');
|
||||
setTranscript([]);
|
||||
setSpeakerList([]);
|
||||
setEditingSpeakers({});
|
||||
await fetchMeetingDetails({ showPageLoading: false });
|
||||
await fetchTranscript();
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.detail || '上传失败');
|
||||
throw error;
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
setUploadProgress(0);
|
||||
setUploadStatusMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadAudioRequest = async ({ file, onSuccess, onError }) => {
|
||||
try {
|
||||
await handleUploadAudio(file);
|
||||
onSuccess?.({}, file);
|
||||
} catch (error) {
|
||||
onError?.(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeMs = audioRef.current.currentTime * 1000;
|
||||
const nextIndex = findTranscriptIndexByTime(transcript, timeMs);
|
||||
|
||||
if (nextIndex !== -1 && nextIndex !== currentHighlightIndex) {
|
||||
setCurrentHighlightIndex(nextIndex);
|
||||
transcriptRefs.current[nextIndex]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTranscriptScroll = (event) => {
|
||||
if (transcriptVisibleCount >= transcript.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
|
||||
if (scrollHeight - scrollTop - clientHeight > 240) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTranscriptVisibleCount((prev) => Math.min(transcript.length, prev + TRANSCRIPT_RENDER_STEP));
|
||||
};
|
||||
|
||||
const jumpToTime = (ms) => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = ms / 1000;
|
||||
audioRef.current.play();
|
||||
}
|
||||
};
|
||||
|
||||
const saveAccessPassword = async () => {
|
||||
const nextPassword = accessPasswordEnabled ? accessPasswordDraft.trim() : null;
|
||||
if (accessPasswordEnabled && !nextPassword) {
|
||||
message.warning('开启访问密码后,请先输入密码');
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingAccessPassword(true);
|
||||
try {
|
||||
const res = await apiClient.put(
|
||||
buildApiUrl(API_ENDPOINTS.MEETINGS.ACCESS_PASSWORD(meetingId)),
|
||||
{ password: nextPassword }
|
||||
);
|
||||
const savedPassword = res.data?.password || null;
|
||||
setMeeting((prev) => (prev ? { ...prev, access_password: savedPassword } : prev));
|
||||
setAccessPasswordEnabled(Boolean(savedPassword));
|
||||
setAccessPasswordDraft(savedPassword || '');
|
||||
message.success(res.message || '访问密码已更新');
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '访问密码更新失败');
|
||||
} finally {
|
||||
setSavingAccessPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccessPasswordSwitchChange = async (checked) => {
|
||||
setAccessPasswordEnabled(checked);
|
||||
|
||||
if (checked) {
|
||||
const existingPassword = (meeting?.access_password || accessPasswordDraft || '').trim();
|
||||
setAccessPasswordDraft(existingPassword || generateRandomPassword());
|
||||
return;
|
||||
}
|
||||
|
||||
setAccessPasswordDraft('');
|
||||
setSavingAccessPassword(true);
|
||||
try {
|
||||
const res = await apiClient.put(
|
||||
buildApiUrl(API_ENDPOINTS.MEETINGS.ACCESS_PASSWORD(meetingId)),
|
||||
{ password: null }
|
||||
);
|
||||
setMeeting((prev) => (prev ? { ...prev, access_password: null } : prev));
|
||||
message.success(res.message || '访问密码已关闭');
|
||||
} catch (error) {
|
||||
setAccessPasswordEnabled(true);
|
||||
setAccessPasswordDraft(meeting?.access_password || '');
|
||||
message.error(error?.response?.data?.message || '访问密码更新失败');
|
||||
} finally {
|
||||
setSavingAccessPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyAccessPassword = async () => {
|
||||
if (!accessPasswordDraft) {
|
||||
message.warning('当前没有可复制的访问密码');
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(accessPasswordDraft);
|
||||
message.success('访问密码已复制');
|
||||
};
|
||||
|
||||
const openAudioUploadPicker = () => {
|
||||
document.getElementById('audio-upload-input')?.click();
|
||||
};
|
||||
|
||||
const startInlineSpeakerEdit = (speakerId, currentTag, segmentId) => {
|
||||
setInlineSpeakerEdit(speakerId);
|
||||
setInlineSpeakerEditSegmentId(`speaker-${speakerId}-${segmentId}`);
|
||||
setInlineSpeakerValue(currentTag || `发言人 ${speakerId}`);
|
||||
};
|
||||
|
||||
const cancelInlineSpeakerEdit = () => {
|
||||
setInlineSpeakerEdit(null);
|
||||
setInlineSpeakerEditSegmentId(null);
|
||||
setInlineSpeakerValue('');
|
||||
};
|
||||
|
||||
const saveInlineSpeakerEdit = async () => {
|
||||
if (inlineSpeakerEdit == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTag = inlineSpeakerValue.trim();
|
||||
if (!nextTag) {
|
||||
message.warning('发言人名称不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingInlineEdit(true);
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(`/api/meetings/${meetingId}/speaker-tags/batch`), {
|
||||
updates: [{ speaker_id: inlineSpeakerEdit, new_tag: nextTag }],
|
||||
});
|
||||
|
||||
setTranscript((prev) => prev.map((item) => (
|
||||
item.speaker_id === inlineSpeakerEdit
|
||||
? { ...item, speaker_tag: nextTag }
|
||||
: item
|
||||
)));
|
||||
setSpeakerList((prev) => prev.map((item) => (
|
||||
item.speaker_id === inlineSpeakerEdit
|
||||
? { ...item, speaker_tag: nextTag }
|
||||
: item
|
||||
)));
|
||||
setEditingSpeakers((prev) => ({ ...prev, [inlineSpeakerEdit]: nextTag }));
|
||||
message.success('发言人名称已更新');
|
||||
cancelInlineSpeakerEdit();
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '更新发言人名称失败');
|
||||
} finally {
|
||||
setSavingInlineEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startInlineSegmentEdit = (segment) => {
|
||||
setInlineSegmentEditId(segment.segment_id);
|
||||
setInlineSegmentValue(segment.text_content || '');
|
||||
};
|
||||
|
||||
const cancelInlineSegmentEdit = () => {
|
||||
setInlineSegmentEditId(null);
|
||||
setInlineSegmentValue('');
|
||||
};
|
||||
|
||||
const saveInlineSegmentEdit = async () => {
|
||||
if (inlineSegmentEditId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingInlineEdit(true);
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(`/api/meetings/${meetingId}/transcript/batch`), {
|
||||
updates: [{ segment_id: inlineSegmentEditId, text_content: inlineSegmentValue }],
|
||||
});
|
||||
setTranscript((prev) => prev.map((item) => (
|
||||
item.segment_id === inlineSegmentEditId
|
||||
? { ...item, text_content: inlineSegmentValue }
|
||||
: item
|
||||
)));
|
||||
message.success('转录内容已更新');
|
||||
cancelInlineSegmentEdit();
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '更新转录内容失败');
|
||||
} finally {
|
||||
setSavingInlineEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const changePlaybackRate = (nextRate) => {
|
||||
setPlaybackRate(nextRate);
|
||||
if (audioRef.current) {
|
||||
audioRef.current.playbackRate = nextRate;
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartTranscription = async () => {
|
||||
try {
|
||||
const res = await apiClient.post(buildApiUrl(`/api/meetings/${meetingId}/transcription/start`));
|
||||
if (res.data?.task_id) {
|
||||
message.success('转录任务已启动');
|
||||
setTranscriptionStatus({ status: 'processing' });
|
||||
startStatusPolling(res.data.task_id);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.detail || '启动转录失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteMeeting = () => {
|
||||
if (!isMeetingOwner) {
|
||||
message.warning('仅会议创建人可删除会议');
|
||||
return;
|
||||
}
|
||||
|
||||
modal.confirm({
|
||||
title: '删除会议',
|
||||
content: '确定要删除此会议吗?此操作无法撤销。',
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId)));
|
||||
navigate('/dashboard');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const generateSummary = async () => {
|
||||
if (!isMeetingOwner) {
|
||||
message.warning('仅会议创建人可重新总结');
|
||||
return;
|
||||
}
|
||||
if (isUploading) {
|
||||
message.warning('音频上传中,暂不允许重新总结');
|
||||
return;
|
||||
}
|
||||
if (!hasUploadedAudio) {
|
||||
message.warning('请先上传音频后再总结');
|
||||
return;
|
||||
}
|
||||
if (isTranscriptionRunning) {
|
||||
message.warning('转录进行中,暂不允许重新总结');
|
||||
return;
|
||||
}
|
||||
|
||||
setSummaryLoading(true);
|
||||
setSummaryTaskProgress(0);
|
||||
try {
|
||||
const res = await apiClient.post(buildApiUrl(`/api/meetings/${meetingId}/generate-summary-async`), {
|
||||
user_prompt: userPrompt,
|
||||
prompt_id: selectedPromptId,
|
||||
model_code: selectedModelCode,
|
||||
});
|
||||
startSummaryPolling(res.data.task_id, { closeDrawerOnComplete: true });
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '生成总结失败');
|
||||
setSummaryLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openSummaryDrawer = () => {
|
||||
if (!isMeetingOwner) {
|
||||
message.warning('仅会议创建人可重新总结');
|
||||
return;
|
||||
}
|
||||
if (isUploading) {
|
||||
message.warning('音频上传中,暂不允许重新总结');
|
||||
return;
|
||||
}
|
||||
if (!hasUploadedAudio) {
|
||||
message.warning('请先上传音频后再总结');
|
||||
return;
|
||||
}
|
||||
if (isTranscriptionRunning) {
|
||||
message.warning('转录进行中,完成后会自动总结');
|
||||
return;
|
||||
}
|
||||
setShowSummaryDrawer(true);
|
||||
};
|
||||
|
||||
const downloadSummaryMd = () => {
|
||||
if (!meeting?.summary) {
|
||||
message.warning('暂无总结内容');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([meeting.summary], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = `${meeting.title || 'summary'}_总结.md`;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const saveTranscriptEdits = async () => {
|
||||
try {
|
||||
const updates = Object.values(editingSegments).map((segment) => ({
|
||||
segment_id: segment.segment_id,
|
||||
text_content: segment.text_content,
|
||||
}));
|
||||
await apiClient.put(buildApiUrl(`/api/meetings/${meetingId}/transcript/batch`), { updates });
|
||||
message.success('转录内容已更新');
|
||||
setShowTranscriptEditDrawer(false);
|
||||
await fetchTranscript();
|
||||
} catch (error) {
|
||||
console.debug('批量更新转录失败:', error);
|
||||
message.error('更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openSummaryEditDrawer = () => {
|
||||
if (!isMeetingOwner) {
|
||||
message.warning('仅会议创建人可编辑总结');
|
||||
return;
|
||||
}
|
||||
setEditingSummaryContent(meeting?.summary || '');
|
||||
setIsEditingSummary(true);
|
||||
};
|
||||
|
||||
const saveSummaryContent = async () => {
|
||||
if (!isMeetingOwner) {
|
||||
message.warning('仅会议创建人可编辑总结');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meetingId)), {
|
||||
title: meeting.title,
|
||||
meeting_time: meeting.meeting_time,
|
||||
summary: editingSummaryContent,
|
||||
tags: meeting.tags?.map((tag) => tag.name).join(',') || '',
|
||||
});
|
||||
message.success('总结已保存');
|
||||
setMeeting((prev) => (prev ? { ...prev, summary: editingSummaryContent } : prev));
|
||||
setIsEditingSummary(false);
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const saveSpeakerTags = async () => {
|
||||
const updates = Object.entries(editingSpeakers).map(([id, tag]) => ({
|
||||
speaker_id: parseInt(id, 10),
|
||||
new_tag: tag,
|
||||
}));
|
||||
|
||||
await apiClient.put(buildApiUrl(`/api/meetings/${meetingId}/speaker-tags/batch`), { updates });
|
||||
setShowSpeakerDrawer(false);
|
||||
await fetchTranscript();
|
||||
message.success('更新成功');
|
||||
};
|
||||
|
||||
return {
|
||||
meetingId,
|
||||
meeting,
|
||||
loading,
|
||||
transcript,
|
||||
transcriptLoading,
|
||||
audioUrl,
|
||||
editingSpeakers,
|
||||
setEditingSpeakers,
|
||||
speakerList,
|
||||
transcriptionStatus,
|
||||
currentHighlightIndex,
|
||||
showSummaryDrawer,
|
||||
setShowSummaryDrawer,
|
||||
summaryLoading,
|
||||
summaryResourcesLoading,
|
||||
userPrompt,
|
||||
setUserPrompt,
|
||||
promptList,
|
||||
selectedPromptId,
|
||||
setSelectedPromptId,
|
||||
summaryTaskProgress,
|
||||
summaryTaskMessage,
|
||||
llmModels,
|
||||
selectedModelCode,
|
||||
setSelectedModelCode,
|
||||
showSpeakerDrawer,
|
||||
setShowSpeakerDrawer,
|
||||
viewingPrompt,
|
||||
setViewingPrompt,
|
||||
editDrawerOpen,
|
||||
setEditDrawerOpen,
|
||||
showQRModal,
|
||||
setShowQRModal,
|
||||
isUploading,
|
||||
displayUploadProgress,
|
||||
uploadStatusMessage,
|
||||
playbackRate,
|
||||
accessPasswordEnabled,
|
||||
accessPasswordDraft,
|
||||
setAccessPasswordDraft,
|
||||
savingAccessPassword,
|
||||
showTranscriptEditDrawer,
|
||||
setShowTranscriptEditDrawer,
|
||||
editingSegments,
|
||||
setEditingSegments,
|
||||
isEditingSummary,
|
||||
setIsEditingSummary,
|
||||
editingSummaryContent,
|
||||
setEditingSummaryContent,
|
||||
inlineSpeakerEdit,
|
||||
inlineSpeakerEditSegmentId,
|
||||
inlineSpeakerValue,
|
||||
setInlineSpeakerValue,
|
||||
inlineSegmentEditId,
|
||||
inlineSegmentValue,
|
||||
setInlineSegmentValue,
|
||||
savingInlineEdit,
|
||||
transcriptVisibleCount,
|
||||
audioRef,
|
||||
transcriptRefs,
|
||||
isMeetingOwner,
|
||||
creatorName,
|
||||
isTranscriptionRunning,
|
||||
isSummaryRunning,
|
||||
displayTranscriptionProgress,
|
||||
displaySummaryProgress,
|
||||
summaryDisabledReason,
|
||||
isSummaryActionDisabled,
|
||||
validateAudioBeforeUpload,
|
||||
handleUploadAudioRequest,
|
||||
fetchMeetingDetails,
|
||||
handleTimeUpdate,
|
||||
handleTranscriptScroll,
|
||||
jumpToTime,
|
||||
saveAccessPassword,
|
||||
handleAccessPasswordSwitchChange,
|
||||
copyAccessPassword,
|
||||
openAudioUploadPicker,
|
||||
startInlineSpeakerEdit,
|
||||
saveInlineSpeakerEdit,
|
||||
cancelInlineSpeakerEdit,
|
||||
startInlineSegmentEdit,
|
||||
saveInlineSegmentEdit,
|
||||
cancelInlineSegmentEdit,
|
||||
changePlaybackRate,
|
||||
handleStartTranscription,
|
||||
handleDeleteMeeting,
|
||||
generateSummary,
|
||||
openSummaryDrawer,
|
||||
downloadSummaryMd,
|
||||
saveTranscriptEdits,
|
||||
openSummaryEditDrawer,
|
||||
saveSummaryContent,
|
||||
saveSpeakerTags,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { App, Form, Upload } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import configService from '../utils/configService';
|
||||
import { uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
|
||||
|
||||
export default function useMeetingFormDrawer({ open, onClose, onSuccess, meetingId = null }) {
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [prompts, setPrompts] = useState([]);
|
||||
const [selectedAudioFile, setSelectedAudioFile] = useState(null);
|
||||
const [audioUploading, setAudioUploading] = useState(false);
|
||||
const [audioUploadProgress, setAudioUploadProgress] = useState(0);
|
||||
const [audioUploadMessage, setAudioUploadMessage] = useState('');
|
||||
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
||||
|
||||
const isEdit = Boolean(meetingId);
|
||||
|
||||
const fetchOptions = useCallback(async () => {
|
||||
try {
|
||||
const [usersResponse, promptsResponse] = await Promise.all([
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK'))),
|
||||
]);
|
||||
setUsers(usersResponse.data.users || []);
|
||||
setPrompts(promptsResponse.data.prompts || []);
|
||||
} catch {
|
||||
message.error('加载会议表单选项失败');
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
const loadAudioUploadConfig = useCallback(async () => {
|
||||
try {
|
||||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||
} catch {
|
||||
setMaxAudioSize(100 * 1024 * 1024);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchMeeting = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
||||
const meeting = response.data;
|
||||
form.setFieldsValue({
|
||||
title: meeting.title,
|
||||
meeting_time: dayjs(meeting.meeting_time),
|
||||
attendee_ids: meeting.attendee_ids || meeting.attendees?.map((attendee) => attendee.user_id).filter(Boolean) || [],
|
||||
prompt_id: meeting.prompt_id,
|
||||
tags: meeting.tags?.map((tag) => tag.name) || [],
|
||||
});
|
||||
} catch {
|
||||
message.error('加载会议数据失败');
|
||||
}
|
||||
}, [form, meetingId, message]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchOptions();
|
||||
loadAudioUploadConfig();
|
||||
|
||||
if (isEdit) {
|
||||
fetchMeeting();
|
||||
return;
|
||||
}
|
||||
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ meeting_time: dayjs() });
|
||||
setSelectedAudioFile(null);
|
||||
setAudioUploading(false);
|
||||
setAudioUploadProgress(0);
|
||||
setAudioUploadMessage('');
|
||||
}, [fetchMeeting, fetchOptions, form, isEdit, loadAudioUploadConfig, open]);
|
||||
|
||||
const handleAudioBeforeUpload = (file) => {
|
||||
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||
if (validationMessage) {
|
||||
message.warning(validationMessage);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
setSelectedAudioFile(file);
|
||||
return false;
|
||||
};
|
||||
|
||||
const clearSelectedAudio = () => {
|
||||
setSelectedAudioFile(null);
|
||||
setAudioUploadProgress(0);
|
||||
setAudioUploadMessage('');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
const payload = {
|
||||
...values,
|
||||
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
||||
attendee_ids: values.attendee_ids || [],
|
||||
tags: values.tags?.join(',') || '',
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meetingId)), payload);
|
||||
message.success('会议更新成功');
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
||||
if (response.code !== '200') {
|
||||
return;
|
||||
}
|
||||
|
||||
const newMeetingId = response.data.meeting_id;
|
||||
if (selectedAudioFile) {
|
||||
setAudioUploading(true);
|
||||
setAudioUploadProgress(0);
|
||||
setAudioUploadMessage('正在上传音频文件...');
|
||||
try {
|
||||
await uploadMeetingAudio({
|
||||
meetingId: newMeetingId,
|
||||
file: selectedAudioFile,
|
||||
promptId: values.prompt_id,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
setAudioUploadProgress(Math.min(100, Math.round((progressEvent.loaded * 100) / progressEvent.total)));
|
||||
}
|
||||
setAudioUploadMessage('正在上传音频文件...');
|
||||
},
|
||||
});
|
||||
setAudioUploadProgress(100);
|
||||
setAudioUploadMessage('上传完成,正在启动转录任务...');
|
||||
message.success('会议创建成功,音频已开始上传处理');
|
||||
} catch (uploadError) {
|
||||
message.warning(uploadError?.response?.data?.message || uploadError?.response?.data?.detail || '会议已创建,但音频上传失败,请在详情页重试');
|
||||
} finally {
|
||||
setAudioUploading(false);
|
||||
}
|
||||
} else {
|
||||
message.success('会议创建成功');
|
||||
}
|
||||
|
||||
onSuccess?.(newMeetingId);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
if (!error?.errorFields) {
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.detail || '操作失败');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
form,
|
||||
isEdit,
|
||||
loading,
|
||||
users,
|
||||
prompts,
|
||||
selectedAudioFile,
|
||||
audioUploading,
|
||||
audioUploadProgress,
|
||||
audioUploadMessage,
|
||||
maxAudioSize,
|
||||
handleAudioBeforeUpload,
|
||||
clearSelectedAudio,
|
||||
handleSubmit,
|
||||
};
|
||||
}
|
||||
|
|
@ -25,10 +25,11 @@ import {
|
|||
SaveOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
|
||||
import StatusTag from '../components/StatusTag';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import { updateStoredUser } from '../services/authSessionService';
|
||||
import './AccountSettings.css';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
|
@ -43,30 +44,6 @@ const formatDateTime = (value) => {
|
|||
return date.toLocaleString('zh-CN', { hour12: false });
|
||||
};
|
||||
|
||||
const buildUpdatedAuthPayload = (nextUser) => {
|
||||
const raw = localStorage.getItem('iMeetingUser');
|
||||
if (!raw || raw === 'undefined' || raw === 'null') {
|
||||
return nextUser;
|
||||
}
|
||||
|
||||
try {
|
||||
const savedAuth = JSON.parse(raw);
|
||||
if (savedAuth && typeof savedAuth === 'object' && savedAuth.user) {
|
||||
return {
|
||||
...savedAuth,
|
||||
user: {
|
||||
...savedAuth.user,
|
||||
...nextUser,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse saved auth payload:', error);
|
||||
}
|
||||
|
||||
return nextUser;
|
||||
};
|
||||
|
||||
const AccountSettings = ({ user, onUpdateUser }) => {
|
||||
const { message, modal } = App.useApp();
|
||||
const [profileForm] = Form.useForm();
|
||||
|
|
@ -84,7 +61,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
|||
|
||||
const fetchUserData = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
|
||||
profileForm.setFieldsValue({
|
||||
username: response.data.username,
|
||||
caption: response.data.caption,
|
||||
|
|
@ -103,7 +80,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
|||
setMcpLoading(true);
|
||||
setMcpError('');
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.MCP_CONFIG(user.user_id)));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.USERS.MCP_CONFIG(user.user_id)));
|
||||
setMcpConfig(response.data || null);
|
||||
} catch (error) {
|
||||
setMcpConfig(null);
|
||||
|
|
@ -146,21 +123,20 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
|||
if (avatarFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', avatarFile);
|
||||
const uploadRes = await apiClient.post(
|
||||
const uploadRes = await httpService.post(
|
||||
buildApiUrl(API_ENDPOINTS.USERS.AVATAR(user.user_id)),
|
||||
formData,
|
||||
);
|
||||
currentAvatarUrl = uploadRes.data.avatar_url;
|
||||
}
|
||||
|
||||
const updateRes = await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(user.user_id)), {
|
||||
const updateRes = await httpService.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(user.user_id)), {
|
||||
caption: values.caption,
|
||||
email: values.email,
|
||||
avatar_url: currentAvatarUrl,
|
||||
});
|
||||
|
||||
const nextAuthPayload = buildUpdatedAuthPayload(updateRes.data);
|
||||
localStorage.setItem('iMeetingUser', JSON.stringify(nextAuthPayload));
|
||||
const nextAuthPayload = updateStoredUser(updateRes.data);
|
||||
onUpdateUser?.(nextAuthPayload);
|
||||
setAvatarFile(null);
|
||||
setPreviewAvatar(updateRes.data.avatar_url || null);
|
||||
|
|
@ -175,7 +151,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
|||
const handlePasswordUpdate = async (values) => {
|
||||
setPasswordLoading(true);
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE_PASSWORD(user.user_id)), {
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE_PASSWORD(user.user_id)), {
|
||||
old_password: values.old_password,
|
||||
new_password: values.new_password,
|
||||
});
|
||||
|
|
@ -207,7 +183,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
|
|||
onOk: async () => {
|
||||
setMcpRefreshing(true);
|
||||
try {
|
||||
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.MCP_REGENERATE(user.user_id)));
|
||||
const response = await httpService.post(buildApiUrl(API_ENDPOINTS.USERS.MCP_REGENERATE(user.user_id)));
|
||||
setMcpConfig(response.data || null);
|
||||
setMcpError('');
|
||||
message.success('MCP Secret 已重新生成');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
|
|
@ -10,7 +10,6 @@ import {
|
|||
Button,
|
||||
Tag,
|
||||
Select,
|
||||
App,
|
||||
Modal,
|
||||
Descriptions,
|
||||
Badge,
|
||||
|
|
@ -35,16 +34,13 @@ import {
|
|||
ClockCircleOutlined,
|
||||
CloseOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import useSystemPageSize from '../hooks/useSystemPageSize';
|
||||
import useAdminDashboardPage from '../hooks/useAdminDashboardPage';
|
||||
import './AdminDashboard.css';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const AUTO_REFRESH_INTERVAL = 30;
|
||||
|
||||
const TASK_TYPE_MAP = {
|
||||
transcription: { text: '转录', color: 'blue' },
|
||||
summary: { text: '总结', color: 'green' },
|
||||
|
|
@ -61,181 +57,34 @@ const STATUS_MAP = {
|
|||
const formatResourcePercent = (value) => `${Number(value || 0).toFixed(1)}%`;
|
||||
|
||||
const AdminDashboard = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const inFlightRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
const [stats, setStats] = useState(null);
|
||||
const [onlineUsers, setOnlineUsers] = useState([]);
|
||||
const [usersList, setUsersList] = useState([]);
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [resources, setResources] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [taskLoading, setTaskLoading] = useState(false);
|
||||
const [lastUpdatedAt, setLastUpdatedAt] = useState(null);
|
||||
|
||||
const [taskType, setTaskType] = useState('all');
|
||||
const [taskStatus, setTaskStatus] = useState('all');
|
||||
const {
|
||||
stats,
|
||||
onlineUsers,
|
||||
usersList,
|
||||
tasks,
|
||||
resources,
|
||||
loading,
|
||||
taskLoading,
|
||||
lastUpdatedAt,
|
||||
taskType,
|
||||
setTaskType,
|
||||
taskStatus,
|
||||
setTaskStatus,
|
||||
autoRefresh,
|
||||
setAutoRefresh,
|
||||
countdown,
|
||||
showMeetingModal,
|
||||
meetingDetails,
|
||||
meetingLoading,
|
||||
fetchAllData,
|
||||
handleKickUser,
|
||||
handleViewMeeting,
|
||||
handleDownloadTranscript,
|
||||
closeMeetingModal,
|
||||
taskCompletionRate,
|
||||
} = useAdminDashboardPage();
|
||||
const pageSize = useSystemPageSize(10);
|
||||
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
const [countdown, setCountdown] = useState(AUTO_REFRESH_INTERVAL);
|
||||
|
||||
const [showMeetingModal, setShowMeetingModal] = useState(false);
|
||||
const [meetingDetails, setMeetingDetails] = useState(null);
|
||||
const [meetingLoading, setMeetingLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.DASHBOARD_STATS));
|
||||
if (response.code === '200') setStats(response.data);
|
||||
}, []);
|
||||
|
||||
const fetchOnlineUsers = useCallback(async () => {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.ONLINE_USERS));
|
||||
if (response.code === '200') setOnlineUsers(response.data.users || []);
|
||||
}, []);
|
||||
|
||||
const fetchUsersList = useCallback(async () => {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.USER_STATS));
|
||||
if (response.code === '200') setUsersList(response.data.users || []);
|
||||
}, []);
|
||||
|
||||
const fetchTasks = useCallback(async () => {
|
||||
try {
|
||||
setTaskLoading(true);
|
||||
const params = new URLSearchParams();
|
||||
if (taskType !== 'all') params.append('task_type', taskType);
|
||||
if (taskStatus !== 'all') params.append('status', taskStatus);
|
||||
params.append('limit', '20');
|
||||
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.ADMIN.TASKS_MONITOR}?${params.toString()}`));
|
||||
if (response.code === '200') setTasks(response.data.tasks || []);
|
||||
} finally {
|
||||
setTaskLoading(false);
|
||||
}
|
||||
}, [taskStatus, taskType]);
|
||||
|
||||
const fetchResources = useCallback(async () => {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_RESOURCES));
|
||||
if (response.code === '200') setResources(response.data);
|
||||
}, []);
|
||||
|
||||
const fetchAllData = useCallback(async ({ silent = false } = {}) => {
|
||||
if (inFlightRef.current) return;
|
||||
inFlightRef.current = true;
|
||||
|
||||
try {
|
||||
if (!silent && mountedRef.current) {
|
||||
setLoading(true);
|
||||
}
|
||||
await Promise.all([fetchStats(), fetchOnlineUsers(), fetchUsersList(), fetchTasks(), fetchResources()]);
|
||||
if (mountedRef.current) {
|
||||
setLastUpdatedAt(new Date());
|
||||
setCountdown(AUTO_REFRESH_INTERVAL);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取数据失败:', err);
|
||||
if (mountedRef.current && !silent) {
|
||||
message.error('加载数据失败,请稍后重试');
|
||||
}
|
||||
} finally {
|
||||
inFlightRef.current = false;
|
||||
if (mountedRef.current && !silent) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [fetchOnlineUsers, fetchResources, fetchStats, fetchTasks, fetchUsersList, message]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllData();
|
||||
}, [fetchAllData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh || showMeetingModal) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
fetchAllData({ silent: true });
|
||||
return AUTO_REFRESH_INTERVAL;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [autoRefresh, fetchAllData, showMeetingModal]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
}, [fetchTasks]);
|
||||
|
||||
const handleKickUser = (u) => {
|
||||
modal.confirm({
|
||||
title: '踢出用户',
|
||||
content: `确定要踢出用户"${u.caption}"吗?该用户将被强制下线。`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.KICK_USER(u.user_id)));
|
||||
if (response.code === '200') {
|
||||
message.success('用户已被踢出');
|
||||
fetchOnlineUsers();
|
||||
}
|
||||
} catch {
|
||||
message.error('踢出用户失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewMeeting = async (meetingId) => {
|
||||
setMeetingLoading(true);
|
||||
setShowMeetingModal(true);
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
||||
if (response.code === '200') setMeetingDetails(response.data);
|
||||
} catch {
|
||||
message.error('获取会议详情失败');
|
||||
} finally {
|
||||
setMeetingLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadTranscript = async (meetingId) => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(`/api/meetings/${meetingId}/transcript`));
|
||||
if (response.code === '200') {
|
||||
const dataStr = JSON.stringify(response.data, null, 2);
|
||||
const blob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `transcript_${meetingId}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch {
|
||||
message.error('下载失败');
|
||||
}
|
||||
};
|
||||
|
||||
const taskCompletionRate = useMemo(() => {
|
||||
const all = tasks.length || 1;
|
||||
const completed = tasks.filter((item) => item.status === 'completed').length;
|
||||
return Math.round((completed / all) * 100);
|
||||
}, [tasks]);
|
||||
|
||||
const resourceRows = useMemo(() => ([
|
||||
{
|
||||
key: 'cpu',
|
||||
|
|
@ -524,11 +373,8 @@ const AdminDashboard = () => {
|
|||
<Modal
|
||||
title="会议详情"
|
||||
open={showMeetingModal}
|
||||
onCancel={() => {
|
||||
setShowMeetingModal(false);
|
||||
setMeetingDetails(null);
|
||||
}}
|
||||
footer={[<Button key="close" icon={<CloseOutlined />} onClick={() => setShowMeetingModal(false)}>关闭</Button>]}
|
||||
onCancel={closeMeetingModal}
|
||||
footer={[<Button key="close" icon={<CloseOutlined />} onClick={closeMeetingModal}>关闭</Button>]}
|
||||
width={620}
|
||||
>
|
||||
{meetingLoading ? (
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import {
|
|||
RocketOutlined,
|
||||
SaveOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import AdminModuleShell from '../components/AdminModuleShell';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
|
|
@ -82,7 +82,7 @@ const ClientManagement = () => {
|
|||
|
||||
const fetchPlatforms = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')));
|
||||
if (response.code === '200') {
|
||||
const { tree = [], items = [] } = response.data || {};
|
||||
setPlatforms({ tree, items });
|
||||
|
|
@ -101,7 +101,7 @@ const ClientManagement = () => {
|
|||
const fetchClients = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST));
|
||||
if (response.code === '200') {
|
||||
setClients(response.data.clients || []);
|
||||
}
|
||||
|
|
@ -165,10 +165,10 @@ const ClientManagement = () => {
|
|||
};
|
||||
|
||||
if (isEditing) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(selectedClient.id)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(selectedClient.id)), payload);
|
||||
message.success('版本更新成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload);
|
||||
message.success('版本创建成功');
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ const ClientManagement = () => {
|
|||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.DELETE(item.id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.DELETE(item.id)));
|
||||
message.success('删除成功');
|
||||
fetchClients();
|
||||
} catch {
|
||||
|
|
@ -202,7 +202,7 @@ const ClientManagement = () => {
|
|||
const handleToggleActive = async (item, checked) => {
|
||||
setUpdatingStatusId(item.id);
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(item.id)), {
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(item.id)), {
|
||||
is_active: checked,
|
||||
});
|
||||
setClients((prev) => prev.map((client) => (
|
||||
|
|
@ -232,7 +232,7 @@ const ClientManagement = () => {
|
|||
uploadFormData.append('platform_code', platformCode);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
const response = await httpService.post(
|
||||
buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPLOAD),
|
||||
uploadFormData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import dayjs from 'dayjs';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import configService from '../utils/configService';
|
||||
import { AUDIO_UPLOAD_ACCEPT, uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
|
||||
|
|
@ -31,7 +31,7 @@ const CreateMeeting = () => {
|
|||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.USERS.LIST));
|
||||
setUsers(res.data.users || []);
|
||||
} catch {
|
||||
setUsers([]);
|
||||
|
|
@ -40,7 +40,7 @@ const CreateMeeting = () => {
|
|||
|
||||
const fetchPrompts = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK')));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK')));
|
||||
setPrompts(res.data.prompts || []);
|
||||
} catch {
|
||||
setPrompts([]);
|
||||
|
|
@ -88,7 +88,7 @@ const CreateMeeting = () => {
|
|||
attendee_ids: values.attendee_ids,
|
||||
tags: values.tags?.join(',') || ''
|
||||
};
|
||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
||||
const res = await httpService.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
||||
if (res.code === '200') {
|
||||
const meetingId = res.data.meeting_id;
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import MeetingTimeline from '../components/MeetingTimeline';
|
||||
|
|
@ -113,9 +113,9 @@ const Dashboard = ({ user }) => {
|
|||
const fetchVoiceprintData = useCallback(async () => {
|
||||
try {
|
||||
setVoiceprintLoading(true);
|
||||
const statusResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.VOICEPRINT.STATUS(user.user_id)));
|
||||
const statusResponse = await httpService.get(buildApiUrl(API_ENDPOINTS.VOICEPRINT.STATUS(user.user_id)));
|
||||
setVoiceprintStatus(statusResponse.data);
|
||||
const templateResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.VOICEPRINT.TEMPLATE));
|
||||
const templateResponse = await httpService.get(buildApiUrl(API_ENDPOINTS.VOICEPRINT.TEMPLATE));
|
||||
setVoiceprintTemplate(templateResponse.data);
|
||||
} catch (error) {
|
||||
console.error('获取声纹数据失败:', error);
|
||||
|
|
@ -148,7 +148,7 @@ const Dashboard = ({ user }) => {
|
|||
|
||||
if (isLoadMore) setLoadingMore(true); else setLoading(true);
|
||||
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), {
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), {
|
||||
params: {
|
||||
user_id: user.user_id,
|
||||
page,
|
||||
|
|
@ -182,7 +182,7 @@ const Dashboard = ({ user }) => {
|
|||
|
||||
const fetchMeetingsStats = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.STATS), {
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.STATS), {
|
||||
params: { user_id: user.user_id },
|
||||
});
|
||||
setMeetingsStats(response.data);
|
||||
|
|
@ -193,7 +193,7 @@ const Dashboard = ({ user }) => {
|
|||
|
||||
const fetchUserData = useCallback(async () => {
|
||||
try {
|
||||
const userResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
|
||||
const userResponse = await httpService.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
|
||||
setUserInfo(userResponse.data);
|
||||
} catch {
|
||||
message.error('获取用户信息失败');
|
||||
|
|
@ -218,7 +218,7 @@ const Dashboard = ({ user }) => {
|
|||
|
||||
const handleDeleteMeeting = async (meetingId) => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId)));
|
||||
meetingCacheService.clearAll();
|
||||
fetchMeetings(1, false);
|
||||
fetchMeetingsStats();
|
||||
|
|
@ -236,7 +236,7 @@ const Dashboard = ({ user }) => {
|
|||
|
||||
const handleVoiceprintUpload = async (formData) => {
|
||||
try {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.VOICEPRINT.UPLOAD(user.user_id)), formData, {
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.VOICEPRINT.UPLOAD(user.user_id)), formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
await fetchVoiceprintData();
|
||||
|
|
@ -255,7 +255,7 @@ const Dashboard = ({ user }) => {
|
|||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.VOICEPRINT.DELETE(user.user_id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.VOICEPRINT.DELETE(user.user_id)));
|
||||
await fetchVoiceprintData();
|
||||
message.success('声纹已删除');
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
ArrowLeftOutlined, SaveOutlined, EditOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import MarkdownEditor from '../components/MarkdownEditor';
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ const EditKnowledgeBase = () => {
|
|||
|
||||
const fetchKbDetail = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kb_id)));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kb_id)));
|
||||
form.setFieldsValue(response.data);
|
||||
} catch {
|
||||
message.error('加载知识库详情失败');
|
||||
|
|
@ -39,7 +39,7 @@ const EditKnowledgeBase = () => {
|
|||
const onFinish = async (values) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.UPDATE(kb_id)), values);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.UPDATE(kb_id)), values);
|
||||
message.success('更新成功');
|
||||
navigate('/knowledge-base');
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import dayjs from 'dayjs';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
|
@ -26,9 +26,9 @@ const EditMeeting = () => {
|
|||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [uRes, pRes, mRes] = await Promise.all([
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK'))),
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)))
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK'))),
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)))
|
||||
]);
|
||||
|
||||
setUsers(uRes.data.users || []);
|
||||
|
|
@ -61,7 +61,7 @@ const EditMeeting = () => {
|
|||
attendee_ids: values.attendee_ids,
|
||||
tags: values.tags?.join(',') || ''
|
||||
};
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meeting_id)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meeting_id)), payload);
|
||||
message.success('会议更新成功');
|
||||
navigate(`/meetings/${meeting_id}`);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
LockOutlined,
|
||||
ArrowRightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import menuService from '../services/menuService';
|
||||
import BrandLogo from '../components/BrandLogo';
|
||||
|
|
@ -35,24 +35,16 @@ const HomePage = ({ onLogin }) => {
|
|||
const handleLogin = async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.AUTH.LOGIN), {
|
||||
const response = await httpService.post(buildApiUrl(API_ENDPOINTS.AUTH.LOGIN), {
|
||||
username: values.username,
|
||||
password: values.password
|
||||
});
|
||||
|
||||
if (response.code === '200') {
|
||||
message.success('登录成功');
|
||||
|
||||
// 关键修复:保存整个 data 对象,因为它包含了 token 和 user 两个部分
|
||||
const authData = response.data;
|
||||
localStorage.setItem('iMeetingUser', JSON.stringify(authData));
|
||||
menuService.clearCache();
|
||||
|
||||
// 通知 App 组件更新状态
|
||||
onLogin(authData);
|
||||
|
||||
// 入口页会根据当前用户的首个菜单做动态跳转
|
||||
window.location.href = '/';
|
||||
|
||||
onLogin(response.data);
|
||||
} else {
|
||||
message.error(response.message || '登录失败');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
CheckCircleOutlined, InfoCircleOutlined, ThunderboltOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import ContentViewer from '../components/ContentViewer';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
|
|
@ -49,7 +49,7 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
|
||||
const loadKbDetail = useCallback(async (id) => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(id)));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(id)));
|
||||
setSelectedKb(res.data);
|
||||
} catch {
|
||||
message.error('加载知识库详情失败');
|
||||
|
|
@ -58,7 +58,7 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
|
||||
const fetchAllKbs = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.LIST));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.LIST));
|
||||
const sorted = (res.data.kbs || []).sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
setKbs(sorted);
|
||||
if (sorted.length > 0 && !selectedKb) {
|
||||
|
|
@ -78,7 +78,7 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
search: searchQuery || undefined,
|
||||
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined
|
||||
};
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), { params });
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), { params });
|
||||
setMeetings(res.data.meetings || []);
|
||||
setMeetingsPagination({ page: res.data.page, total: res.data.total });
|
||||
} catch {
|
||||
|
|
@ -90,7 +90,7 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
|
||||
const fetchAvailableTags = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST));
|
||||
setAvailableTags(res.data?.slice(0, 10) || []);
|
||||
} catch {
|
||||
setAvailableTags([]);
|
||||
|
|
@ -99,7 +99,7 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
|
||||
const fetchPrompts = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('KNOWLEDGE_TASK')));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('KNOWLEDGE_TASK')));
|
||||
setAvailablePrompts(res.data.prompts || []);
|
||||
const def = res.data.prompts?.find(p => p.is_default) || res.data.prompts?.[0];
|
||||
if (def) setSelectedPromptId(def.id);
|
||||
|
|
@ -130,7 +130,7 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
setGenerating(true);
|
||||
setTaskProgress(10);
|
||||
try {
|
||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.CREATE), {
|
||||
const res = await httpService.post(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.CREATE), {
|
||||
user_prompt: userPrompt,
|
||||
source_meeting_ids: selectedMeetings.join(','),
|
||||
prompt_id: selectedPromptId
|
||||
|
|
@ -138,7 +138,7 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
|
||||
const taskId = res.data.task_id;
|
||||
const interval = setInterval(async () => {
|
||||
const statusRes = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.TASK_STATUS(taskId)));
|
||||
const statusRes = await httpService.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.TASK_STATUS(taskId)));
|
||||
const s = statusRes.data;
|
||||
setTaskProgress(s.progress || 20);
|
||||
if (s.status === 'completed') {
|
||||
|
|
@ -166,7 +166,7 @@ const KnowledgeBasePage = ({ user }) => {
|
|||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DELETE(kb.kb_id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DELETE(kb.kb_id)));
|
||||
if (selectedKb?.kb_id === kb.kb_id) setSelectedKb(null);
|
||||
fetchAllKbs();
|
||||
message.success('删除成功');
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import CenterPager from '../components/CenterPager';
|
||||
|
|
@ -123,7 +123,7 @@ const MeetingCenterPage = ({ user }) => {
|
|||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), {
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), {
|
||||
params: {
|
||||
user_id: user.user_id,
|
||||
page: nextPage,
|
||||
|
|
@ -176,7 +176,7 @@ const MeetingCenterPage = ({ user }) => {
|
|||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meeting.meeting_id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meeting.meeting_id)));
|
||||
message.success('会议已删除');
|
||||
meetingCacheService.clearAll();
|
||||
const nextPage = page > 1 && meetings.length === 1 ? page - 1 : page;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|||
import { useParams, Link } from 'react-router-dom';
|
||||
import { Layout, Space, Button, App, Empty, Input, Tabs } from 'antd';
|
||||
import { LockOutlined, EyeOutlined, CopyOutlined, ShareAltOutlined, HomeOutlined, FileTextOutlined, PartitionOutlined, AudioOutlined } from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import MarkdownRenderer from '../components/MarkdownRenderer';
|
||||
import MindMap from '../components/MindMap';
|
||||
|
|
@ -38,8 +38,8 @@ const MeetingPreview = () => {
|
|||
|
||||
const fetchTranscriptAndAudio = useCallback(async () => {
|
||||
const [transcriptRes, audioRes] = await Promise.allSettled([
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meeting_id))),
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.AUDIO(meeting_id))),
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meeting_id))),
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.AUDIO(meeting_id))),
|
||||
]);
|
||||
|
||||
if (transcriptRes.status === 'fulfilled') {
|
||||
|
|
@ -60,7 +60,7 @@ const MeetingPreview = () => {
|
|||
try {
|
||||
const endpoint = API_ENDPOINTS.MEETINGS.PREVIEW_DATA(meeting_id);
|
||||
const url = buildApiUrl(`${endpoint}${pwd ? `?password=${encodeURIComponent(pwd)}` : ''}`);
|
||||
const res = await apiClient.get(url);
|
||||
const res = await httpService.get(url);
|
||||
setMeeting(res.data);
|
||||
setIsAuthorized(true);
|
||||
setPasswordRequired(false);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
SaveOutlined,
|
||||
StarFilled,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
|
||||
import PromptManagementPage from './PromptManagementPage';
|
||||
import MarkdownRenderer from '../components/MarkdownRenderer';
|
||||
|
|
@ -49,7 +49,7 @@ const PromptConfigPage = ({ user }) => {
|
|||
const loadConfig = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)));
|
||||
setAvailablePrompts(res.data.available_prompts || []);
|
||||
setSelectedPromptIds(res.data.selected_prompt_ids || []);
|
||||
} catch {
|
||||
|
|
@ -98,7 +98,7 @@ const PromptConfigPage = ({ user }) => {
|
|||
is_enabled: true,
|
||||
sort_order: index + 1,
|
||||
}));
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)), { items });
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)), { items });
|
||||
message.success('提示词配置已保存');
|
||||
loadConfig();
|
||||
} catch (error) {
|
||||
|
|
@ -126,7 +126,7 @@ const PromptConfigPage = ({ user }) => {
|
|||
items={[
|
||||
{
|
||||
key: 'config',
|
||||
label: '系统提示词配置',
|
||||
label: '客户端提示词配置',
|
||||
children: (
|
||||
<div className="console-tab-panel">
|
||||
<div className="console-tab-toolbar">
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
SaveOutlined,
|
||||
StarFilled,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import httpService from '../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
import MarkdownEditor from '../components/MarkdownEditor';
|
||||
import CenterPager from '../components/CenterPager';
|
||||
|
|
@ -72,7 +72,7 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
|
|||
setLoading(true);
|
||||
try {
|
||||
const is_active = statusFilter === 'all' ? undefined : Number(statusFilter);
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.LIST), {
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.LIST), {
|
||||
params: {
|
||||
page,
|
||||
size,
|
||||
|
|
@ -135,10 +135,10 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
|
|||
}
|
||||
setDrawerSubmitting(true);
|
||||
if (editingPrompt) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id)), payload);
|
||||
message.success('提示词已更新');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), payload);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), payload);
|
||||
message.success('提示词已创建');
|
||||
}
|
||||
setDrawerOpen(false);
|
||||
|
|
@ -160,7 +160,7 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
|
|||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(prompt.id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(prompt.id)));
|
||||
message.success('提示词已删除');
|
||||
loadPrompts();
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
FileOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
|
|
@ -28,7 +28,7 @@ const DictManagement = () => {
|
|||
// 获取所有字典类型
|
||||
const fetchDictTypes = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.TYPES));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.TYPES));
|
||||
if (response.code === '200') {
|
||||
const types = (response.data.types || []).filter((type) => type !== 'system_config');
|
||||
setDictTypes(types);
|
||||
|
|
@ -60,7 +60,7 @@ const DictManagement = () => {
|
|||
|
||||
const fetchDictData = useCallback(async (dictType) => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE(dictType)));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE(dictType)));
|
||||
if (response.code === '200') {
|
||||
setDictData(response.data.items);
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ const DictManagement = () => {
|
|||
|
||||
if (selectedNode) {
|
||||
// 更新
|
||||
const response = await apiClient.put(
|
||||
const response = await httpService.put(
|
||||
buildApiUrl(API_ENDPOINTS.DICT_DATA.UPDATE(selectedNode.id)),
|
||||
values
|
||||
);
|
||||
|
|
@ -154,7 +154,7 @@ const DictManagement = () => {
|
|||
}
|
||||
} else {
|
||||
// 新增
|
||||
const response = await apiClient.post(
|
||||
const response = await httpService.post(
|
||||
buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE),
|
||||
values
|
||||
);
|
||||
|
|
@ -174,7 +174,7 @@ const DictManagement = () => {
|
|||
const handleDelete = async () => {
|
||||
if (!selectedNode) return;
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.DICT_DATA.DELETE(selectedNode.id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.DICT_DATA.DELETE(selectedNode.id)));
|
||||
message.success('删除成功');
|
||||
setSelectedNode(null);
|
||||
setIsEditing(false);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import {
|
|||
CheckCircleOutlined,
|
||||
BlockOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
|
|
@ -90,7 +90,7 @@ const ExternalAppManagement = () => {
|
|||
const fetchApps = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST));
|
||||
if (response.code === '200') {
|
||||
setApps(response.data || []);
|
||||
}
|
||||
|
|
@ -138,10 +138,10 @@ const ExternalAppManagement = () => {
|
|||
};
|
||||
|
||||
if (isEditing) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPDATE(selectedApp.id)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPDATE(selectedApp.id)), payload);
|
||||
message.success('应用更新成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.CREATE), payload);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.CREATE), payload);
|
||||
message.success('应用创建成功');
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ const ExternalAppManagement = () => {
|
|||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.DELETE(item.id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.DELETE(item.id)));
|
||||
message.success('删除成功');
|
||||
fetchApps();
|
||||
} catch {
|
||||
|
|
@ -182,7 +182,7 @@ const ExternalAppManagement = () => {
|
|||
uploadFormData.append(fieldName, file);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(buildApiUrl(endpoint), uploadFormData, {
|
||||
const response = await httpService.post(buildApiUrl(endpoint), uploadFormData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ const ExternalAppManagement = () => {
|
|||
const handleToggleStatus = async (item, checked) => {
|
||||
try {
|
||||
const newActive = checked ? 1 : 0;
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPDATE(item.id)), { is_active: newActive });
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPDATE(item.id)), { is_active: newActive });
|
||||
setApps((prev) => prev.map((app) => (
|
||||
app.id === item.id ? { ...app, is_active: newActive } : app
|
||||
)));
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
FontSizeOutlined, SearchOutlined, ReloadOutlined,
|
||||
SaveOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
|
|
@ -54,7 +54,7 @@ const HotWordManagement = () => {
|
|||
const fetchGroups = useCallback(async () => {
|
||||
setGroupsLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUPS));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUPS));
|
||||
if (res.code === '200') {
|
||||
setGroups(res.data || []);
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ const HotWordManagement = () => {
|
|||
if (!groupId) { setItems([]); return; }
|
||||
setItemsLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEMS(groupId)));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEMS(groupId)));
|
||||
if (res.code === '200') {
|
||||
setItems(res.data || []);
|
||||
}
|
||||
|
|
@ -129,10 +129,10 @@ const HotWordManagement = () => {
|
|||
const values = await groupForm.validateFields();
|
||||
const payload = { ...values, status: values.status ? 1 : 0 };
|
||||
if (editingGroup) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUP_DETAIL(editingGroup.id)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUP_DETAIL(editingGroup.id)), payload);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUPS), payload);
|
||||
const res = await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUPS), payload);
|
||||
if (res.code === '200') {
|
||||
setSelectedGroupId(res.data?.id);
|
||||
}
|
||||
|
|
@ -155,7 +155,7 @@ const HotWordManagement = () => {
|
|||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUP_DETAIL(group.id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUP_DETAIL(group.id)));
|
||||
message.success('删除成功');
|
||||
if (selectedGroupId === group.id) setSelectedGroupId(null);
|
||||
fetchGroups();
|
||||
|
|
@ -170,7 +170,7 @@ const HotWordManagement = () => {
|
|||
e?.stopPropagation();
|
||||
setSyncLoadingId(group.id);
|
||||
try {
|
||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.SYNC_GROUP(group.id)));
|
||||
const res = await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.SYNC_GROUP(group.id)));
|
||||
if (res.code === '200') {
|
||||
message.success(`同步成功 — 词表ID: ${res.data?.vocabulary_id}`);
|
||||
fetchGroups();
|
||||
|
|
@ -208,10 +208,10 @@ const HotWordManagement = () => {
|
|||
const values = await itemForm.validateFields();
|
||||
const payload = { ...values, status: values.status ? 1 : 0 };
|
||||
if (editingItem) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEM_DETAIL(editingItem.id)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEM_DETAIL(editingItem.id)), payload);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEMS(selectedGroupId)), payload);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEMS(selectedGroupId)), payload);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setItemDrawerOpen(false);
|
||||
|
|
@ -231,7 +231,7 @@ const HotWordManagement = () => {
|
|||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEM_DETAIL(item.id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEM_DETAIL(item.id)));
|
||||
message.success('删除成功');
|
||||
fetchItems(selectedGroupId);
|
||||
fetchGroups();
|
||||
|
|
@ -244,7 +244,7 @@ const HotWordManagement = () => {
|
|||
|
||||
const toggleItemStatus = async (item, checked) => {
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEM_DETAIL(item.id)), {
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.ITEM_DETAIL(item.id)), {
|
||||
status: checked ? 1 : 0,
|
||||
});
|
||||
setItems((prev) => prev.map((it) => (it.id === item.id ? { ...it, status: checked ? 1 : 0 } : it)));
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
Tooltip,
|
||||
} from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, ExperimentOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../../config/api';
|
||||
import {
|
||||
buildManagedModelCode,
|
||||
|
|
@ -179,7 +179,7 @@ const ModelManagement = () => {
|
|||
kind === 'llm'
|
||||
? API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIG_DETAIL(modelCode)
|
||||
: API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIG_DETAIL(modelCode);
|
||||
await apiClient.delete(buildApiUrl(endpoint));
|
||||
await httpService.delete(buildApiUrl(endpoint));
|
||||
message.success(kind === 'llm' ? 'LLM模型删除成功' : '音频模型删除成功');
|
||||
fetchAll();
|
||||
} catch (error) {
|
||||
|
|
@ -191,9 +191,9 @@ const ModelManagement = () => {
|
|||
setLoading(true);
|
||||
try {
|
||||
const [llmRes, audioRes, groupsRes] = await Promise.all([
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIGS)),
|
||||
apiClient.get(buildApiUrl(`${API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIGS}?scene=all`)),
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUPS)),
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIGS)),
|
||||
httpService.get(buildApiUrl(`${API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIGS}?scene=all`)),
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.ADMIN.HOT_WORDS.GROUPS)),
|
||||
]);
|
||||
setLlmItems(llmRes.data.items || []);
|
||||
setAudioItems(audioRes.data.items || []);
|
||||
|
|
@ -298,20 +298,20 @@ const ModelManagement = () => {
|
|||
const payload = buildLlmPayload(values);
|
||||
|
||||
if (editingRow) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIG_DETAIL(editingRow.model_code)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIG_DETAIL(editingRow.model_code)), payload);
|
||||
message.success('LLM模型更新成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIGS), payload);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIGS), payload);
|
||||
message.success('LLM模型创建成功');
|
||||
}
|
||||
} else {
|
||||
const payload = buildAudioPayload(values);
|
||||
|
||||
if (editingRow) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIG_DETAIL(editingRow.model_code)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIG_DETAIL(editingRow.model_code)), payload);
|
||||
message.success('音频模型更新成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIGS), payload);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIGS), payload);
|
||||
message.success('音频模型创建成功');
|
||||
}
|
||||
}
|
||||
|
|
@ -334,7 +334,7 @@ const ModelManagement = () => {
|
|||
...buildLlmPayload(values),
|
||||
test_prompt: '请用一句中文回复:LLM测试成功。',
|
||||
};
|
||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIG_TEST), payload);
|
||||
const res = await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIG_TEST), payload);
|
||||
modal.success({
|
||||
title: 'LLM测试成功',
|
||||
width: 640,
|
||||
|
|
@ -351,7 +351,7 @@ const ModelManagement = () => {
|
|||
...buildAudioPayload(values),
|
||||
test_file_url: 'https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav',
|
||||
};
|
||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIG_TEST), payload);
|
||||
const res = await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIG_TEST), payload);
|
||||
modal.success({
|
||||
title: 'ASR测试任务已提交',
|
||||
width: 680,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, App, Button, Drawer, Form, Input, Popconfirm, Select, Space, Switch, Table, Tag, Tooltip, Typography } from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SaveOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { API_ENDPOINTS, buildApiUrl } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
|
|
@ -57,7 +57,7 @@ const ParameterManagement = () => {
|
|||
const fetchItems = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS));
|
||||
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS));
|
||||
setItems(res.data.items || []);
|
||||
} catch {
|
||||
message.error('获取参数列表失败');
|
||||
|
|
@ -98,10 +98,10 @@ const ParameterManagement = () => {
|
|||
setSubmitting(true);
|
||||
try {
|
||||
if (editing) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETER_DETAIL(editing.param_key)), values);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETER_DETAIL(editing.param_key)), values);
|
||||
message.success('参数更新成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS), values);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS), values);
|
||||
message.success('参数创建成功');
|
||||
}
|
||||
configService.clearCache();
|
||||
|
|
@ -152,7 +152,7 @@ const ParameterManagement = () => {
|
|||
okButtonProps={{ danger: true }}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETER_DELETE(row.param_key)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETER_DELETE(row.param_key)));
|
||||
configService.clearCache();
|
||||
message.success('参数删除成功');
|
||||
fetchItems();
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import {
|
|||
CompressOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { buildApiUrl } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
||||
|
|
@ -214,9 +214,9 @@ const PermissionManagement = () => {
|
|||
setLoading(true);
|
||||
try {
|
||||
const [rolesRes, menusRes, rolePermsRes] = await Promise.all([
|
||||
apiClient.get(buildApiUrl('/api/admin/roles')),
|
||||
apiClient.get(buildApiUrl('/api/admin/menus')),
|
||||
apiClient.get(buildApiUrl('/api/admin/roles/permissions/all')),
|
||||
httpService.get(buildApiUrl('/api/admin/roles')),
|
||||
httpService.get(buildApiUrl('/api/admin/menus')),
|
||||
httpService.get(buildApiUrl('/api/admin/roles/permissions/all')),
|
||||
]);
|
||||
|
||||
const rolesList = rolesRes.data.roles || [];
|
||||
|
|
@ -255,7 +255,7 @@ const PermissionManagement = () => {
|
|||
}
|
||||
setRoleUsersLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get(
|
||||
const res = await httpService.get(
|
||||
buildApiUrl(`/api/admin/roles/${selectedRoleId}/users?page=${roleUsersPage}&size=${roleUsersPageSize}`),
|
||||
);
|
||||
setRoleUsers(res.data.users || []);
|
||||
|
|
@ -280,7 +280,7 @@ const PermissionManagement = () => {
|
|||
|
||||
setSavingPermission(true);
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(`/api/admin/roles/${selectedRoleId}/permissions`), {
|
||||
await httpService.put(buildApiUrl(`/api/admin/roles/${selectedRoleId}/permissions`), {
|
||||
menu_ids: sanitizedMenuIds,
|
||||
});
|
||||
|
||||
|
|
@ -317,10 +317,10 @@ const PermissionManagement = () => {
|
|||
setRoleSubmitting(true);
|
||||
try {
|
||||
if (editingRole) {
|
||||
await apiClient.put(buildApiUrl(`/api/admin/roles/${editingRole.role_id}`), values);
|
||||
await httpService.put(buildApiUrl(`/api/admin/roles/${editingRole.role_id}`), values);
|
||||
message.success('角色更新成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl('/api/admin/roles'), values);
|
||||
await httpService.post(buildApiUrl('/api/admin/roles'), values);
|
||||
message.success('角色创建成功');
|
||||
}
|
||||
setRoleDrawerOpen(false);
|
||||
|
|
@ -397,10 +397,10 @@ const PermissionManagement = () => {
|
|||
setMenuSubmitting(true);
|
||||
try {
|
||||
if (editingMenu) {
|
||||
await apiClient.put(buildApiUrl(`/api/admin/menus/${editingMenu.menu_id}`), values);
|
||||
await httpService.put(buildApiUrl(`/api/admin/menus/${editingMenu.menu_id}`), values);
|
||||
message.success('菜单更新成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl('/api/admin/menus'), values);
|
||||
await httpService.post(buildApiUrl('/api/admin/menus'), values);
|
||||
message.success('菜单创建成功');
|
||||
}
|
||||
setMenuIconPickerOpen(false);
|
||||
|
|
@ -415,7 +415,7 @@ const PermissionManagement = () => {
|
|||
|
||||
const deleteMenu = async (menuId) => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(`/api/admin/menus/${menuId}`));
|
||||
await httpService.delete(buildApiUrl(`/api/admin/menus/${menuId}`));
|
||||
message.success('菜单删除成功');
|
||||
if (selectedManageMenuId === menuId) {
|
||||
setSelectedManageMenuId(null);
|
||||
|
|
@ -631,7 +631,7 @@ const PermissionManagement = () => {
|
|||
try {
|
||||
await Promise.all(
|
||||
updates.map((item) =>
|
||||
apiClient.put(buildApiUrl(`/api/admin/menus/${item.menu_id}`), {
|
||||
httpService.put(buildApiUrl(`/api/admin/menus/${item.menu_id}`), {
|
||||
parent_id: item.parent_id,
|
||||
sort_order: item.sort_order,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
MessageOutlined,
|
||||
DatabaseOutlined
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
import MarkdownEditor from '../../components/MarkdownEditor';
|
||||
|
|
@ -47,7 +47,7 @@ const PromptManagement = () => {
|
|||
|
||||
const fetchPrompts = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.LIST));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.LIST));
|
||||
const list = response.data.prompts || [];
|
||||
setPrompts(list);
|
||||
if (list.length > 0 && !selectedPrompt) {
|
||||
|
|
@ -81,7 +81,7 @@ const PromptManagement = () => {
|
|||
is_default: editingPrompt.is_default ? 1 : 0,
|
||||
is_active: editingPrompt.is_active ? 1 : 0
|
||||
};
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id)), payload);
|
||||
message.success('保存成功');
|
||||
fetchPrompts();
|
||||
} catch {
|
||||
|
|
@ -102,7 +102,7 @@ const PromptManagement = () => {
|
|||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(item.id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(item.id)));
|
||||
message.success('删除成功');
|
||||
setSelectedPrompt(null);
|
||||
fetchPrompts();
|
||||
|
|
@ -216,7 +216,7 @@ const PromptManagement = () => {
|
|||
onOk={() => form.submit()}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={async (v) => {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), v);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), v);
|
||||
message.success('创建成功');
|
||||
setShowCreateModal(false);
|
||||
fetchPrompts();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
ClusterOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import {
|
||||
|
|
@ -79,9 +79,9 @@ const SystemManagementOverview = () => {
|
|||
setLoading(true);
|
||||
try {
|
||||
const [clientRes, appRes, terminalRes] = await Promise.all([
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST)),
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST)),
|
||||
apiClient.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST, { page: 1, size: 10000 })),
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST)),
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST)),
|
||||
httpService.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST, { page: 1, size: 10000 })),
|
||||
]);
|
||||
|
||||
setClients(clientRes?.data?.clients || []);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import {
|
|||
CheckCircleOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
|
|
@ -61,7 +61,7 @@ const TerminalManagement = () => {
|
|||
|
||||
const fetchTerminalTypes = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')), {
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')), {
|
||||
params: { parent_code: 'TERMINAL' },
|
||||
});
|
||||
if (response.code === '200') {
|
||||
|
|
@ -75,7 +75,7 @@ const TerminalManagement = () => {
|
|||
const fetchTerminals = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST), {
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST), {
|
||||
params: { page: 1, size: 10000 },
|
||||
});
|
||||
if (response.code === '200') {
|
||||
|
|
@ -122,10 +122,10 @@ const TerminalManagement = () => {
|
|||
};
|
||||
|
||||
if (isEditing) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.TERMINALS.UPDATE(selectedTerminal.id)), payload);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.TERMINALS.UPDATE(selectedTerminal.id)), payload);
|
||||
message.success('终端更新成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.TERMINALS.CREATE), payload);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.TERMINALS.CREATE), payload);
|
||||
message.success('终端创建成功');
|
||||
}
|
||||
|
||||
|
|
@ -146,7 +146,7 @@ const TerminalManagement = () => {
|
|||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.TERMINALS.DELETE(item.id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.TERMINALS.DELETE(item.id)));
|
||||
message.success('删除成功');
|
||||
fetchTerminals();
|
||||
} catch {
|
||||
|
|
@ -159,7 +159,7 @@ const TerminalManagement = () => {
|
|||
const handleToggleStatus = async (item, checked) => {
|
||||
try {
|
||||
const newStatus = checked ? 1 : 0;
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.TERMINALS.STATUS(item.id)), null, {
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.TERMINALS.STATUS(item.id)), null, {
|
||||
params: { status: newStatus },
|
||||
});
|
||||
setTerminals((prev) => prev.map((terminal) => (
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
SafetyCertificateOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import httpService from '../../services/httpService';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import AdminModuleShell from '../../components/AdminModuleShell';
|
||||
import ActionButton from '../../components/ActionButton';
|
||||
|
|
@ -54,7 +54,7 @@ const UserManagement = () => {
|
|||
if (debouncedSearchText) {
|
||||
url += `&search=${encodeURIComponent(debouncedSearchText)}`;
|
||||
}
|
||||
const response = await apiClient.get(buildApiUrl(url));
|
||||
const response = await httpService.get(buildApiUrl(url));
|
||||
setUsers(response.data.users || []);
|
||||
setTotal(response.data.total || 0);
|
||||
} catch {
|
||||
|
|
@ -70,7 +70,7 @@ const UserManagement = () => {
|
|||
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.ROLES));
|
||||
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.USERS.ROLES));
|
||||
setRoles(response.data || []);
|
||||
} catch {
|
||||
setRoles([
|
||||
|
|
@ -98,10 +98,10 @@ const UserManagement = () => {
|
|||
try {
|
||||
const values = await form.validateFields();
|
||||
if (isEditing) {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(currentUser.user_id)), values);
|
||||
await httpService.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(currentUser.user_id)), values);
|
||||
message.success('用户修改成功');
|
||||
} else {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), values);
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), values);
|
||||
message.success('用户添加成功');
|
||||
}
|
||||
setShowUserDrawer(false);
|
||||
|
|
@ -122,7 +122,7 @@ const UserManagement = () => {
|
|||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.USERS.DELETE(user.user_id)));
|
||||
await httpService.delete(buildApiUrl(API_ENDPOINTS.USERS.DELETE(user.user_id)));
|
||||
message.success('用户删除成功');
|
||||
fetchUsers();
|
||||
} catch {
|
||||
|
|
@ -140,7 +140,7 @@ const UserManagement = () => {
|
|||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.RESET_PASSWORD(user.user_id)));
|
||||
await httpService.post(buildApiUrl(API_ENDPOINTS.USERS.RESET_PASSWORD(user.user_id)));
|
||||
message.success('密码重置成功');
|
||||
} catch {
|
||||
message.error('重置失败');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
const AUTH_STORAGE_KEY = 'iMeetingUser';
|
||||
|
||||
const isValidStoredValue = (value) => value && value !== 'undefined' && value !== 'null';
|
||||
|
||||
export const getStoredAuthPayload = () => {
|
||||
const raw = localStorage.getItem(AUTH_STORAGE_KEY);
|
||||
if (!isValidStoredValue(raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed && typeof parsed === 'object' ? parsed : null;
|
||||
} catch (error) {
|
||||
console.error('Failed to parse stored auth payload:', error);
|
||||
localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getStoredUser = () => {
|
||||
const authPayload = getStoredAuthPayload();
|
||||
const user = authPayload?.user || authPayload;
|
||||
|
||||
if (user && typeof user === 'object' && (user.user_id || user.id)) {
|
||||
return user;
|
||||
}
|
||||
|
||||
if (authPayload) {
|
||||
localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getStoredToken = () => {
|
||||
const authPayload = getStoredAuthPayload();
|
||||
return authPayload?.token || null;
|
||||
};
|
||||
|
||||
export const setStoredAuthPayload = (authPayload) => {
|
||||
if (!authPayload || typeof authPayload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(authPayload));
|
||||
return getStoredUser();
|
||||
};
|
||||
|
||||
export const clearStoredAuthPayload = () => {
|
||||
localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||
};
|
||||
|
||||
export const updateStoredUser = (nextUser) => {
|
||||
if (!nextUser || typeof nextUser !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentPayload = getStoredAuthPayload();
|
||||
if (currentPayload?.user) {
|
||||
const nextPayload = {
|
||||
...currentPayload,
|
||||
user: {
|
||||
...currentPayload.user,
|
||||
...nextUser,
|
||||
},
|
||||
};
|
||||
setStoredAuthPayload(nextPayload);
|
||||
return nextPayload;
|
||||
}
|
||||
|
||||
setStoredAuthPayload(nextUser);
|
||||
return nextUser;
|
||||
};
|
||||
|
||||
export const clearAuthSession = ({ redirectToRoot = false } = {}) => {
|
||||
clearStoredAuthPayload();
|
||||
|
||||
if (redirectToRoot && window.location.pathname !== '/') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import apiClient from '../utils/apiClient';
|
||||
|
||||
const httpService = {
|
||||
get(url, config) {
|
||||
return apiClient.get(url, config);
|
||||
},
|
||||
post(url, data, config) {
|
||||
return apiClient.post(url, data, config);
|
||||
},
|
||||
put(url, data, config) {
|
||||
return apiClient.put(url, data, config);
|
||||
},
|
||||
delete(url, config) {
|
||||
return apiClient.delete(url, config);
|
||||
},
|
||||
};
|
||||
|
||||
export default httpService;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import axios from 'axios';
|
||||
import { clearAuthSession, getStoredToken } from '../services/authSessionService';
|
||||
|
||||
// 创建axios实例
|
||||
const apiClient = axios.create();
|
||||
|
|
@ -6,20 +7,9 @@ const apiClient = axios.create();
|
|||
// 请求拦截器 - 自动添加Authorization头
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const savedUser = localStorage.getItem('iMeetingUser');
|
||||
|
||||
// 增加严格校验:排除 null, undefined 字符串以及空值
|
||||
if (savedUser && savedUser !== "undefined" && savedUser !== "null") {
|
||||
try {
|
||||
const user = JSON.parse(savedUser);
|
||||
// 确保解析出来的是对象且包含 token
|
||||
if (user && typeof user === 'object' && user.token) {
|
||||
config.headers.Authorization = `Bearer ${user.token}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse user from localStorage:', error);
|
||||
localStorage.removeItem('iMeetingUser');
|
||||
}
|
||||
const token = getStoredToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
|
@ -45,11 +35,7 @@ apiClient.interceptors.response.use(
|
|||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('iMeetingUser');
|
||||
// 避免重复跳转
|
||||
if (window.location.pathname !== '/') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
clearAuthSession({ redirectToRoot: true });
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue