codex/dev
mula.liu 2026-04-08 19:19:33 +08:00
parent ad16567e82
commit 41f71e649d
44 changed files with 5382 additions and 4700 deletions

View File

@ -1,391 +1,52 @@
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from app.core.auth import get_current_admin_user, get_current_user 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 ( from app.models.models import (
MenuInfo,
MenuListResponse,
RolePermissionInfo,
UpdateRolePermissionsRequest,
RoleInfo,
CreateMenuRequest, CreateMenuRequest,
UpdateMenuRequest,
CreateRoleRequest, CreateRoleRequest,
UpdateMenuRequest,
UpdateRolePermissionsRequest,
UpdateRoleRequest, UpdateRoleRequest,
) )
from typing import List import app.services.admin_service as admin_service
import time
router = APIRouter() 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") @router.get("/admin/menus")
async def get_all_menus(current_user=Depends(get_current_admin_user)): async def get_all_menus(current_user=Depends(get_current_admin_user)):
""" return admin_service.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)}")
@router.post("/admin/menus") @router.post("/admin/menus")
async def create_menu(request: CreateMenuRequest, current_user=Depends(get_current_admin_user)): async def create_menu(request: CreateMenuRequest, current_user=Depends(get_current_admin_user)):
""" return admin_service.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)}")
@router.put("/admin/menus/{menu_id}") @router.put("/admin/menus/{menu_id}")
async def update_menu(menu_id: int, request: UpdateMenuRequest, current_user=Depends(get_current_admin_user)): async def update_menu(menu_id: int, request: UpdateMenuRequest, current_user=Depends(get_current_admin_user)):
""" return admin_service.update_menu(menu_id, 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()))
# 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)}")
@router.delete("/admin/menus/{menu_id}") @router.delete("/admin/menus/{menu_id}")
async def delete_menu(menu_id: int, current_user=Depends(get_current_admin_user)): async def delete_menu(menu_id: int, current_user=Depends(get_current_admin_user)):
""" return admin_service.delete_menu(menu_id)
删除菜单有子菜单时不允许删除
"""
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)}")
@router.get("/admin/roles") @router.get("/admin/roles")
async def get_all_roles(current_user=Depends(get_current_admin_user)): async def get_all_roles(current_user=Depends(get_current_admin_user)):
""" return admin_service.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)}")
@router.post("/admin/roles") @router.post("/admin/roles")
async def create_role(request: CreateRoleRequest, current_user=Depends(get_current_admin_user)): async def create_role(request: CreateRoleRequest, current_user=Depends(get_current_admin_user)):
""" return admin_service.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)}")
@router.put("/admin/roles/{role_id}") @router.put("/admin/roles/{role_id}")
async def update_role(role_id: int, request: UpdateRoleRequest, current_user=Depends(get_current_admin_user)): async def update_role(role_id: int, request: UpdateRoleRequest, current_user=Depends(get_current_admin_user)):
""" return admin_service.update_role(role_id, 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)}")
@router.get("/admin/roles/{role_id}/users") @router.get("/admin/roles/{role_id}/users")
@ -395,253 +56,28 @@ async def get_role_users(
size: int = Query(10, ge=1, le=100), size: int = Query(10, ge=1, le=100),
current_user=Depends(get_current_admin_user), current_user=Depends(get_current_admin_user),
): ):
""" return admin_service.get_role_users(role_id, page, size)
获取角色下用户列表
"""
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)}")
@router.get("/admin/roles/permissions/all") @router.get("/admin/roles/permissions/all")
async def get_all_role_permissions(current_user=Depends(get_current_admin_user)): async def get_all_role_permissions(current_user=Depends(get_current_admin_user)):
""" return admin_service.get_all_role_permissions()
批量获取所有角色权限用于减少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()
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") @router.get("/admin/roles/{role_id}/permissions")
async def get_role_permissions(role_id: int, current_user=Depends(get_current_admin_user)): async def get_role_permissions(role_id: int, current_user=Depends(get_current_admin_user)):
""" return admin_service.get_role_permissions(role_id)
获取指定角色的菜单权限
只有管理员才能访问
"""
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 = [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") @router.put("/admin/roles/{role_id}/permissions")
async def update_role_permissions( async def update_role_permissions(
role_id: int, role_id: int,
request: UpdateRolePermissionsRequest, request: UpdateRolePermissionsRequest,
current_user=Depends(get_current_admin_user) current_user=Depends(get_current_admin_user),
): ):
""" return admin_service.update_role_permissions(role_id, 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}
# 验证所有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") @router.get("/menus/user")
async def get_user_menus(current_user=Depends(get_current_user)): async def get_user_menus(current_user=Depends(get_current_user)):
""" return admin_service.get_user_menus(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)}")

View File

@ -1,281 +1,25 @@
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from app.core.auth import get_current_admin_user from app.core.auth import get_current_admin_user
from app.core.response import create_api_response import app.services.admin_dashboard_service as admin_dashboard_service
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
router = APIRouter() 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") @router.get("/admin/dashboard/stats")
async def get_dashboard_stats(current_user=Depends(get_current_admin_user)): async def get_dashboard_stats(current_user=Depends(get_current_admin_user)):
"""获取管理员 Dashboard 统计数据""" return await admin_dashboard_service.get_dashboard_stats(current_user)
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)}")
@router.get("/admin/online-users") @router.get("/admin/online-users")
async def get_online_users(current_user=Depends(get_current_admin_user)): async def get_online_users(current_user=Depends(get_current_admin_user)):
"""获取在线用户列表""" return await admin_dashboard_service.get_online_users(current_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)}")
@router.post("/admin/kick-user/{user_id}") @router.post("/admin/kick-user/{user_id}")
async def kick_user(user_id: int, current_user=Depends(get_current_admin_user)): async def kick_user(user_id: int, current_user=Depends(get_current_admin_user)):
"""踢出用户(撤销该用户的所有 token""" return await admin_dashboard_service.kick_user(user_id, current_user)
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)}")
@router.get("/admin/tasks/monitor") @router.get("/admin/tasks/monitor")
@ -285,207 +29,14 @@ async def monitor_tasks(
limit: int = Query(20, ge=1, le=100, description="返回数量限制"), limit: int = Query(20, ge=1, le=100, description="返回数量限制"),
current_user=Depends(get_current_admin_user) current_user=Depends(get_current_admin_user)
): ):
"""监控任务进度""" return await admin_dashboard_service.monitor_tasks(task_type, status, limit, current_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)}")
@router.get("/admin/system/resources") @router.get("/admin/system/resources")
async def get_system_resources(current_user=Depends(get_current_admin_user)): async def get_system_resources(current_user=Depends(get_current_admin_user)):
"""获取服务器资源使用情况""" return await admin_dashboard_service.get_system_resources(current_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)}")
@router.get("/admin/user-stats") @router.get("/admin/user-stats")
async def get_user_stats(current_user=Depends(get_current_admin_user)): async def get_user_stats(current_user=Depends(get_current_admin_user)):
"""获取用户统计列表""" return await admin_dashboard_service.get_user_stats(current_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)}")

View File

@ -1,215 +1,13 @@
import json
from typing import Any from typing import Any
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel from pydantic import BaseModel
from app.core.auth import get_current_admin_user from app.core.auth import get_current_admin_user
from app.core.database import get_db_connection import app.services.admin_settings_service as admin_settings_service
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
router = APIRouter() 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): class ParameterUpsertRequest(BaseModel):
@ -242,7 +40,7 @@ class LLMModelUpsertRequest(BaseModel):
class AudioModelUpsertRequest(BaseModel): class AudioModelUpsertRequest(BaseModel):
model_code: str model_code: str
model_name: str model_name: str
audio_scene: str # asr / voiceprint audio_scene: str
provider: str | None = None provider: str | None = None
endpoint_url: str | None = None endpoint_url: str | None = None
api_key: str | None = None api_key: str | None = None
@ -278,89 +76,17 @@ async def list_parameters(
keyword: str | None = Query(None), keyword: str | None = Query(None),
current_user=Depends(get_current_admin_user), current_user=Depends(get_current_admin_user),
): ):
try: return admin_settings_service.list_parameters(category, keyword)
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)}")
@router.get("/admin/parameters/{param_key}") @router.get("/admin/parameters/{param_key}")
async def get_parameter(param_key: str, current_user=Depends(get_current_admin_user)): async def get_parameter(param_key: str, current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.get_parameter(param_key)
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)}")
@router.post("/admin/parameters") @router.post("/admin/parameters")
async def create_parameter(request: ParameterUpsertRequest, current_user=Depends(get_current_admin_user)): async def create_parameter(request: ParameterUpsertRequest, current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.create_parameter(request)
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)}")
@router.put("/admin/parameters/{param_key}") @router.put("/admin/parameters/{param_key}")
@ -369,131 +95,22 @@ async def update_parameter(
request: ParameterUpsertRequest, request: ParameterUpsertRequest,
current_user=Depends(get_current_admin_user), current_user=Depends(get_current_admin_user),
): ):
try: return admin_settings_service.update_parameter(param_key, request)
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)}")
@router.delete("/admin/parameters/{param_key}") @router.delete("/admin/parameters/{param_key}")
async def delete_parameter(param_key: str, current_user=Depends(get_current_admin_user)): async def delete_parameter(param_key: str, current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.delete_parameter(param_key)
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)}")
@router.get("/admin/model-configs/llm") @router.get("/admin/model-configs/llm")
async def list_llm_model_configs(current_user=Depends(get_current_admin_user)): async def list_llm_model_configs(current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.list_llm_model_configs()
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)}")
@router.post("/admin/model-configs/llm") @router.post("/admin/model-configs/llm")
async def create_llm_model_config(request: LLMModelUpsertRequest, current_user=Depends(get_current_admin_user)): async def create_llm_model_config(request: LLMModelUpsertRequest, current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.create_llm_model_config(request)
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)}")
@router.put("/admin/model-configs/llm/{model_code}") @router.put("/admin/model-configs/llm/{model_code}")
@ -502,54 +119,7 @@ async def update_llm_model_config(
request: LLMModelUpsertRequest, request: LLMModelUpsertRequest,
current_user=Depends(get_current_admin_user), current_user=Depends(get_current_admin_user),
): ):
try: return admin_settings_service.update_llm_model_config(model_code, request)
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)}")
@router.get("/admin/model-configs/audio") @router.get("/admin/model-configs/audio")
@ -557,97 +127,12 @@ async def list_audio_model_configs(
scene: str = Query("all"), scene: str = Query("all"),
current_user=Depends(get_current_admin_user), current_user=Depends(get_current_admin_user),
): ):
try: return admin_settings_service.list_audio_model_configs(scene)
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)}")
@router.post("/admin/model-configs/audio") @router.post("/admin/model-configs/audio")
async def create_audio_model_config(request: AudioModelUpsertRequest, current_user=Depends(get_current_admin_user)): async def create_audio_model_config(request: AudioModelUpsertRequest, current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.create_audio_model_config(request)
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)}")
@router.put("/admin/model-configs/audio/{model_code}") @router.put("/admin/model-configs/audio/{model_code}")
@ -656,196 +141,34 @@ async def update_audio_model_config(
request: AudioModelUpsertRequest, request: AudioModelUpsertRequest,
current_user=Depends(get_current_admin_user), current_user=Depends(get_current_admin_user),
): ):
try: return admin_settings_service.update_audio_model_config(model_code, request)
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)}")
@router.delete("/admin/model-configs/llm/{model_code}") @router.delete("/admin/model-configs/llm/{model_code}")
async def delete_llm_model_config(model_code: str, current_user=Depends(get_current_admin_user)): async def delete_llm_model_config(model_code: str, current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.delete_llm_model_config(model_code)
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)}")
@router.delete("/admin/model-configs/audio/{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)): async def delete_audio_model_config(model_code: str, current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.delete_audio_model_config(model_code)
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)}")
@router.post("/admin/model-configs/llm/test") @router.post("/admin/model-configs/llm/test")
async def test_llm_model_config(request: LLMModelTestRequest, current_user=Depends(get_current_admin_user)): async def test_llm_model_config(request: LLMModelTestRequest, current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.test_llm_model_config(request)
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)}")
@router.post("/admin/model-configs/audio/test") @router.post("/admin/model-configs/audio/test")
async def test_audio_model_config(request: AudioModelTestRequest, current_user=Depends(get_current_admin_user)): async def test_audio_model_config(request: AudioModelTestRequest, current_user=Depends(get_current_admin_user)):
try: return admin_settings_service.test_audio_model_config(request)
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)}")
@router.get("/system-config/public") @router.get("/system-config/public")
async def get_public_system_config(): async def get_public_system_config():
try: return admin_settings_service.get_public_system_config()
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)}")
@router.get("/admin/system-config") @router.get("/admin/system-config")
async def get_system_config_compat(current_user=Depends(get_current_admin_user)): async def get_system_config_compat(current_user=Depends(get_current_admin_user)):
"""兼容旧前端的系统配置接口,数据来源为 sys_system_parameters。""" return admin_settings_service.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

View File

@ -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

View File

@ -1,110 +1,22 @@
import sys import sys
import os
from pathlib import Path from pathlib import Path
# 添加项目根目录到 Python 路径 # 添加项目根目录到 Python 路径
# 无论从哪里运行,都能正确找到 app 模块
current_file = Path(__file__).resolve() 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: if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
import uvicorn 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( from app.app_factory import create_app
title="iMeeting API", from app.core.config import API_CONFIG
description="iMeeting API说明",
version="1.1.0",
docs_url=None, # 禁用默认docs使用自定义CDN
redoc_url=None
)
# 添加终端检查中间件 (在CORS之前添加以便位于CORS内部)
app.add_middleware(TerminalCheckMiddleware)
# 添加CORS中间件 app = create_app()
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")
# 包含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__": if __name__ == "__main__":
# 简单的uvicorn配置避免参数冲突
uvicorn.run( uvicorn.run(
"app.main:app", "app.main:app",
host=API_CONFIG['host'], host=API_CONFIG['host'],

View File

@ -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)}")

View File

@ -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)}")

View File

@ -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

View File

@ -19,6 +19,11 @@ import AccountSettings from './pages/AccountSettings';
import MeetingCenterPage from './pages/MeetingCenterPage'; import MeetingCenterPage from './pages/MeetingCenterPage';
import MainLayout from './components/MainLayout'; import MainLayout from './components/MainLayout';
import menuService from './services/menuService'; import menuService from './services/menuService';
import {
clearAuthSession,
getStoredUser,
setStoredAuthPayload,
} from './services/authSessionService';
import configService from './utils/configService'; import configService from './utils/configService';
import './App.css'; import './App.css';
import './styles/console-theme.css'; import './styles/console-theme.css';
@ -84,26 +89,8 @@ function App() {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
// Load user from localStorage on app start
useEffect(() => { useEffect(() => {
const savedAuth = localStorage.getItem('iMeetingUser'); setUser(getStoredUser());
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');
}
}
setIsLoading(false); setIsLoading(false);
}, []); }, []);
@ -124,11 +111,7 @@ function App() {
const handleLogin = (authData) => { const handleLogin = (authData) => {
if (authData) { if (authData) {
menuService.clearCache(); menuService.clearCache();
// UI setUser(setStoredAuthPayload(authData));
const userData = authData.user || authData;
setUser(userData);
// auth token使
localStorage.setItem('iMeetingUser', JSON.stringify(authData));
} }
}; };
@ -139,9 +122,8 @@ function App() {
console.error('Logout API error:', error); console.error('Logout API error:', error);
} finally { } finally {
setUser(null); setUser(null);
localStorage.removeItem('iMeetingUser');
menuService.clearCache(); menuService.clearCache();
window.location.href = '/'; clearAuthSession();
} }
}; };

View File

@ -9,7 +9,7 @@ import {
WindowsOutlined, WindowsOutlined,
RightOutlined RightOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -24,7 +24,7 @@ const ClientDownloads = () => {
const fetchClients = async () => { const fetchClients = async () => {
try { 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 || []); setClients(response.data.clients || []);
} catch (error) { } catch (error) {
console.error('获取下载列表失败:', error); console.error('获取下载列表失败:', error);

View File

@ -1,160 +1,28 @@
import React, { useCallback, useEffect, useState } from 'react'; import React from 'react';
import { Drawer, Form, Input, Button, DatePicker, Select, Space, App, Upload, Card, Progress, Typography } from 'antd'; import { Drawer, Form, Input, Button, DatePicker, Select, Space, Upload, Card, Progress, Typography } from 'antd';
import { SaveOutlined, UploadOutlined, DeleteOutlined, AudioOutlined } from '@ant-design/icons'; 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 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 { Text } = Typography;
const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => { const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
const { message } = App.useApp(); const {
const [form] = Form.useForm(); form,
const [loading, setLoading] = useState(false); isEdit,
const [users, setUsers] = useState([]); loading,
const [prompts, setPrompts] = useState([]); users,
const [selectedAudioFile, setSelectedAudioFile] = useState(null); prompts,
const [audioUploading, setAudioUploading] = useState(false); selectedAudioFile,
const [audioUploadProgress, setAudioUploadProgress] = useState(0); audioUploading,
const [audioUploadMessage, setAudioUploadMessage] = useState(''); audioUploadProgress,
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024); audioUploadMessage,
maxAudioSize,
const isEdit = Boolean(meetingId); handleAudioBeforeUpload,
clearSelectedAudio,
const fetchOptions = useCallback(async () => { handleSubmit,
try { } = useMeetingFormDrawer({ open, onClose, onSuccess, meetingId });
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);
}
};
return ( return (
<Drawer <Drawer
@ -172,7 +40,7 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
</Space> </Space>
} }
> >
<Form form={form} layout="vertical" initialValues={{ meeting_time: dayjs() }}> <Form form={form} layout="vertical">
<Form.Item label="会议主题" name="title" rules={[{ required: true, message: '请输入会议主题' }]}> <Form.Item label="会议主题" name="title" rules={[{ required: true, message: '请输入会议主题' }]}>
<Input placeholder="请输入会议主题..." /> <Input placeholder="请输入会议主题..." />
</Form.Item> </Form.Item>

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Tag, Space, Typography, Skeleton, Empty } from 'antd'; import { Tag, Space, Typography, Skeleton, Empty } from 'antd';
import { TagsOutlined } from '@ant-design/icons'; import { TagsOutlined } from '@ant-design/icons';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
const { CheckableTag } = Tag; const { CheckableTag } = Tag;
@ -19,7 +19,7 @@ const TagCloud = ({
const fetchAllTags = useCallback(async () => { const fetchAllTags = useCallback(async () => {
try { try {
setLoading(true); 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 || []; const tags = response.data || [];
setAllTags(limitTags ? tags.slice(0, 15) : tags); setAllTags(limitTags ? tags.slice(0, 15) : tags);
} catch (err) { } catch (err) {

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -25,10 +25,11 @@ import {
SaveOutlined, SaveOutlined,
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { API_ENDPOINTS, buildApiUrl } from '../config/api'; import { API_ENDPOINTS, buildApiUrl } from '../config/api';
import StatusTag from '../components/StatusTag'; import StatusTag from '../components/StatusTag';
import ActionButton from '../components/ActionButton'; import ActionButton from '../components/ActionButton';
import { updateStoredUser } from '../services/authSessionService';
import './AccountSettings.css'; import './AccountSettings.css';
const { Title, Paragraph, Text } = Typography; const { Title, Paragraph, Text } = Typography;
@ -43,30 +44,6 @@ const formatDateTime = (value) => {
return date.toLocaleString('zh-CN', { hour12: false }); 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 AccountSettings = ({ user, onUpdateUser }) => {
const { message, modal } = App.useApp(); const { message, modal } = App.useApp();
const [profileForm] = Form.useForm(); const [profileForm] = Form.useForm();
@ -84,7 +61,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
const fetchUserData = useCallback(async () => { const fetchUserData = useCallback(async () => {
try { 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({ profileForm.setFieldsValue({
username: response.data.username, username: response.data.username,
caption: response.data.caption, caption: response.data.caption,
@ -103,7 +80,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
setMcpLoading(true); setMcpLoading(true);
setMcpError(''); setMcpError('');
try { 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); setMcpConfig(response.data || null);
} catch (error) { } catch (error) {
setMcpConfig(null); setMcpConfig(null);
@ -146,21 +123,20 @@ const AccountSettings = ({ user, onUpdateUser }) => {
if (avatarFile) { if (avatarFile) {
const formData = new FormData(); const formData = new FormData();
formData.append('file', avatarFile); formData.append('file', avatarFile);
const uploadRes = await apiClient.post( const uploadRes = await httpService.post(
buildApiUrl(API_ENDPOINTS.USERS.AVATAR(user.user_id)), buildApiUrl(API_ENDPOINTS.USERS.AVATAR(user.user_id)),
formData, formData,
); );
currentAvatarUrl = uploadRes.data.avatar_url; 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, caption: values.caption,
email: values.email, email: values.email,
avatar_url: currentAvatarUrl, avatar_url: currentAvatarUrl,
}); });
const nextAuthPayload = buildUpdatedAuthPayload(updateRes.data); const nextAuthPayload = updateStoredUser(updateRes.data);
localStorage.setItem('iMeetingUser', JSON.stringify(nextAuthPayload));
onUpdateUser?.(nextAuthPayload); onUpdateUser?.(nextAuthPayload);
setAvatarFile(null); setAvatarFile(null);
setPreviewAvatar(updateRes.data.avatar_url || null); setPreviewAvatar(updateRes.data.avatar_url || null);
@ -175,7 +151,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
const handlePasswordUpdate = async (values) => { const handlePasswordUpdate = async (values) => {
setPasswordLoading(true); setPasswordLoading(true);
try { 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, old_password: values.old_password,
new_password: values.new_password, new_password: values.new_password,
}); });
@ -207,7 +183,7 @@ const AccountSettings = ({ user, onUpdateUser }) => {
onOk: async () => { onOk: async () => {
setMcpRefreshing(true); setMcpRefreshing(true);
try { 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); setMcpConfig(response.data || null);
setMcpError(''); setMcpError('');
message.success('MCP Secret 已重新生成'); message.success('MCP Secret 已重新生成');

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useMemo } from 'react';
import { import {
Card, Card,
Table, Table,
@ -10,7 +10,6 @@ import {
Button, Button,
Tag, Tag,
Select, Select,
App,
Modal, Modal,
Descriptions, Descriptions,
Badge, Badge,
@ -35,16 +34,13 @@ import {
ClockCircleOutlined, ClockCircleOutlined,
CloseOutlined, CloseOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import ActionButton from '../components/ActionButton'; import ActionButton from '../components/ActionButton';
import useSystemPageSize from '../hooks/useSystemPageSize'; import useSystemPageSize from '../hooks/useSystemPageSize';
import useAdminDashboardPage from '../hooks/useAdminDashboardPage';
import './AdminDashboard.css'; import './AdminDashboard.css';
const { Text } = Typography; const { Text } = Typography;
const AUTO_REFRESH_INTERVAL = 30;
const TASK_TYPE_MAP = { const TASK_TYPE_MAP = {
transcription: { text: '转录', color: 'blue' }, transcription: { text: '转录', color: 'blue' },
summary: { text: '总结', color: 'green' }, summary: { text: '总结', color: 'green' },
@ -61,181 +57,34 @@ const STATUS_MAP = {
const formatResourcePercent = (value) => `${Number(value || 0).toFixed(1)}%`; const formatResourcePercent = (value) => `${Number(value || 0).toFixed(1)}%`;
const AdminDashboard = () => { const AdminDashboard = () => {
const { message, modal } = App.useApp(); const {
const inFlightRef = useRef(false); stats,
const mountedRef = useRef(true); onlineUsers,
usersList,
const [stats, setStats] = useState(null); tasks,
const [onlineUsers, setOnlineUsers] = useState([]); resources,
const [usersList, setUsersList] = useState([]); loading,
const [tasks, setTasks] = useState([]); taskLoading,
const [resources, setResources] = useState(null); lastUpdatedAt,
const [loading, setLoading] = useState(true); taskType,
const [taskLoading, setTaskLoading] = useState(false); setTaskType,
const [lastUpdatedAt, setLastUpdatedAt] = useState(null); taskStatus,
setTaskStatus,
const [taskType, setTaskType] = useState('all'); autoRefresh,
const [taskStatus, setTaskStatus] = useState('all'); setAutoRefresh,
countdown,
showMeetingModal,
meetingDetails,
meetingLoading,
fetchAllData,
handleKickUser,
handleViewMeeting,
handleDownloadTranscript,
closeMeetingModal,
taskCompletionRate,
} = useAdminDashboardPage();
const pageSize = useSystemPageSize(10); 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(() => ([ const resourceRows = useMemo(() => ([
{ {
key: 'cpu', key: 'cpu',
@ -524,11 +373,8 @@ const AdminDashboard = () => {
<Modal <Modal
title="会议详情" title="会议详情"
open={showMeetingModal} open={showMeetingModal}
onCancel={() => { onCancel={closeMeetingModal}
setShowMeetingModal(false); footer={[<Button key="close" icon={<CloseOutlined />} onClick={closeMeetingModal}>关闭</Button>]}
setMeetingDetails(null);
}}
footer={[<Button key="close" icon={<CloseOutlined />} onClick={() => setShowMeetingModal(false)}>关闭</Button>]}
width={620} width={620}
> >
{meetingLoading ? ( {meetingLoading ? (

View File

@ -36,7 +36,7 @@ import {
RocketOutlined, RocketOutlined,
SaveOutlined, SaveOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import AdminModuleShell from '../components/AdminModuleShell'; import AdminModuleShell from '../components/AdminModuleShell';
import ActionButton from '../components/ActionButton'; import ActionButton from '../components/ActionButton';
@ -82,7 +82,7 @@ const ClientManagement = () => {
const fetchPlatforms = useCallback(async () => { const fetchPlatforms = useCallback(async () => {
try { 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') { if (response.code === '200') {
const { tree = [], items = [] } = response.data || {}; const { tree = [], items = [] } = response.data || {};
setPlatforms({ tree, items }); setPlatforms({ tree, items });
@ -101,7 +101,7 @@ const ClientManagement = () => {
const fetchClients = useCallback(async () => { const fetchClients = useCallback(async () => {
setLoading(true); setLoading(true);
try { 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') { if (response.code === '200') {
setClients(response.data.clients || []); setClients(response.data.clients || []);
} }
@ -165,10 +165,10 @@ const ClientManagement = () => {
}; };
if (isEditing) { 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('版本更新成功'); message.success('版本更新成功');
} else { } else {
await apiClient.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload); await httpService.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload);
message.success('版本创建成功'); message.success('版本创建成功');
} }
@ -189,7 +189,7 @@ const ClientManagement = () => {
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
try { 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('删除成功'); message.success('删除成功');
fetchClients(); fetchClients();
} catch { } catch {
@ -202,7 +202,7 @@ const ClientManagement = () => {
const handleToggleActive = async (item, checked) => { const handleToggleActive = async (item, checked) => {
setUpdatingStatusId(item.id); setUpdatingStatusId(item.id);
try { 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, is_active: checked,
}); });
setClients((prev) => prev.map((client) => ( setClients((prev) => prev.map((client) => (
@ -232,7 +232,7 @@ const ClientManagement = () => {
uploadFormData.append('platform_code', platformCode); uploadFormData.append('platform_code', platformCode);
try { try {
const response = await apiClient.post( const response = await httpService.post(
buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPLOAD), buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPLOAD),
uploadFormData, uploadFormData,
{ headers: { 'Content-Type': 'multipart/form-data' } }, { headers: { 'Content-Type': 'multipart/form-data' } },

View File

@ -9,7 +9,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import configService from '../utils/configService'; import configService from '../utils/configService';
import { AUDIO_UPLOAD_ACCEPT, uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService'; import { AUDIO_UPLOAD_ACCEPT, uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
@ -31,7 +31,7 @@ const CreateMeeting = () => {
const fetchUsers = useCallback(async () => { const fetchUsers = useCallback(async () => {
try { 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 || []); setUsers(res.data.users || []);
} catch { } catch {
setUsers([]); setUsers([]);
@ -40,7 +40,7 @@ const CreateMeeting = () => {
const fetchPrompts = useCallback(async () => { const fetchPrompts = useCallback(async () => {
try { 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 || []); setPrompts(res.data.prompts || []);
} catch { } catch {
setPrompts([]); setPrompts([]);
@ -88,7 +88,7 @@ const CreateMeeting = () => {
attendee_ids: values.attendee_ids, attendee_ids: values.attendee_ids,
tags: values.tags?.join(',') || '' 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') { if (res.code === '200') {
const meetingId = res.data.meeting_id; const meetingId = res.data.meeting_id;

View File

@ -29,7 +29,7 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { API_ENDPOINTS, buildApiUrl } from '../config/api'; import { API_ENDPOINTS, buildApiUrl } from '../config/api';
import ActionButton from '../components/ActionButton'; import ActionButton from '../components/ActionButton';
import MeetingTimeline from '../components/MeetingTimeline'; import MeetingTimeline from '../components/MeetingTimeline';
@ -113,9 +113,9 @@ const Dashboard = ({ user }) => {
const fetchVoiceprintData = useCallback(async () => { const fetchVoiceprintData = useCallback(async () => {
try { try {
setVoiceprintLoading(true); 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); 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); setVoiceprintTemplate(templateResponse.data);
} catch (error) { } catch (error) {
console.error('获取声纹数据失败:', error); console.error('获取声纹数据失败:', error);
@ -148,7 +148,7 @@ const Dashboard = ({ user }) => {
if (isLoadMore) setLoadingMore(true); else setLoading(true); 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: { params: {
user_id: user.user_id, user_id: user.user_id,
page, page,
@ -182,7 +182,7 @@ const Dashboard = ({ user }) => {
const fetchMeetingsStats = useCallback(async () => { const fetchMeetingsStats = useCallback(async () => {
try { 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 }, params: { user_id: user.user_id },
}); });
setMeetingsStats(response.data); setMeetingsStats(response.data);
@ -193,7 +193,7 @@ const Dashboard = ({ user }) => {
const fetchUserData = useCallback(async () => { const fetchUserData = useCallback(async () => {
try { 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); setUserInfo(userResponse.data);
} catch { } catch {
message.error('获取用户信息失败'); message.error('获取用户信息失败');
@ -218,7 +218,7 @@ const Dashboard = ({ user }) => {
const handleDeleteMeeting = async (meetingId) => { const handleDeleteMeeting = async (meetingId) => {
try { try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId))); await httpService.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meetingId)));
meetingCacheService.clearAll(); meetingCacheService.clearAll();
fetchMeetings(1, false); fetchMeetings(1, false);
fetchMeetingsStats(); fetchMeetingsStats();
@ -236,7 +236,7 @@ const Dashboard = ({ user }) => {
const handleVoiceprintUpload = async (formData) => { const handleVoiceprintUpload = async (formData) => {
try { 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' }, headers: { 'Content-Type': 'multipart/form-data' },
}); });
await fetchVoiceprintData(); await fetchVoiceprintData();
@ -255,7 +255,7 @@ const Dashboard = ({ user }) => {
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
try { 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(); await fetchVoiceprintData();
message.success('声纹已删除'); message.success('声纹已删除');
} catch (error) { } catch (error) {

View File

@ -7,7 +7,7 @@ import {
ArrowLeftOutlined, SaveOutlined, EditOutlined ArrowLeftOutlined, SaveOutlined, EditOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom'; 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 { buildApiUrl, API_ENDPOINTS } from '../config/api';
import MarkdownEditor from '../components/MarkdownEditor'; import MarkdownEditor from '../components/MarkdownEditor';
@ -23,7 +23,7 @@ const EditKnowledgeBase = () => {
const fetchKbDetail = useCallback(async () => { const fetchKbDetail = useCallback(async () => {
try { 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); form.setFieldsValue(response.data);
} catch { } catch {
message.error('加载知识库详情失败'); message.error('加载知识库详情失败');
@ -39,7 +39,7 @@ const EditKnowledgeBase = () => {
const onFinish = async (values) => { const onFinish = async (values) => {
setSaving(true); setSaving(true);
try { 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('更新成功'); message.success('更新成功');
navigate('/knowledge-base'); navigate('/knowledge-base');
} catch { } catch {

View File

@ -8,7 +8,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
const { Title } = Typography; const { Title } = Typography;
@ -26,9 +26,9 @@ const EditMeeting = () => {
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
try { try {
const [uRes, pRes, mRes] = await Promise.all([ const [uRes, pRes, mRes] = await Promise.all([
apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)), httpService.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK'))), httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK'))),
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id))) httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meeting_id)))
]); ]);
setUsers(uRes.data.users || []); setUsers(uRes.data.users || []);
@ -61,7 +61,7 @@ const EditMeeting = () => {
attendee_ids: values.attendee_ids, attendee_ids: values.attendee_ids,
tags: values.tags?.join(',') || '' 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('会议更新成功'); message.success('会议更新成功');
navigate(`/meetings/${meeting_id}`); navigate(`/meetings/${meeting_id}`);
} catch (error) { } catch (error) {

View File

@ -8,7 +8,7 @@ import {
LockOutlined, LockOutlined,
ArrowRightOutlined, ArrowRightOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import menuService from '../services/menuService'; import menuService from '../services/menuService';
import BrandLogo from '../components/BrandLogo'; import BrandLogo from '../components/BrandLogo';
@ -35,24 +35,16 @@ const HomePage = ({ onLogin }) => {
const handleLogin = async (values) => { const handleLogin = async (values) => {
setLoading(true); setLoading(true);
try { try {
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.AUTH.LOGIN), { const response = await httpService.post(buildApiUrl(API_ENDPOINTS.AUTH.LOGIN), {
username: values.username, username: values.username,
password: values.password password: values.password
}); });
if (response.code === '200') { if (response.code === '200') {
message.success('登录成功'); message.success('登录成功');
// data token user
const authData = response.data;
localStorage.setItem('iMeetingUser', JSON.stringify(authData));
menuService.clearCache(); menuService.clearCache();
// App onLogin(response.data);
onLogin(authData);
//
window.location.href = '/';
} else { } else {
message.error(response.message || '登录失败'); message.error(response.message || '登录失败');
} }

View File

@ -13,7 +13,7 @@ import {
CheckCircleOutlined, InfoCircleOutlined, ThunderboltOutlined CheckCircleOutlined, InfoCircleOutlined, ThunderboltOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Link, useNavigate } from 'react-router-dom'; 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 { buildApiUrl, API_ENDPOINTS } from '../config/api';
import ContentViewer from '../components/ContentViewer'; import ContentViewer from '../components/ContentViewer';
import ActionButton from '../components/ActionButton'; import ActionButton from '../components/ActionButton';
@ -49,7 +49,7 @@ const KnowledgeBasePage = ({ user }) => {
const loadKbDetail = useCallback(async (id) => { const loadKbDetail = useCallback(async (id) => {
try { 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); setSelectedKb(res.data);
} catch { } catch {
message.error('加载知识库详情失败'); message.error('加载知识库详情失败');
@ -58,7 +58,7 @@ const KnowledgeBasePage = ({ user }) => {
const fetchAllKbs = useCallback(async () => { const fetchAllKbs = useCallback(async () => {
try { 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)); const sorted = (res.data.kbs || []).sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
setKbs(sorted); setKbs(sorted);
if (sorted.length > 0 && !selectedKb) { if (sorted.length > 0 && !selectedKb) {
@ -78,7 +78,7 @@ const KnowledgeBasePage = ({ user }) => {
search: searchQuery || undefined, search: searchQuery || undefined,
tags: selectedTags.length > 0 ? selectedTags.join(',') : 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 || []); setMeetings(res.data.meetings || []);
setMeetingsPagination({ page: res.data.page, total: res.data.total }); setMeetingsPagination({ page: res.data.page, total: res.data.total });
} catch { } catch {
@ -90,7 +90,7 @@ const KnowledgeBasePage = ({ user }) => {
const fetchAvailableTags = useCallback(async () => { const fetchAvailableTags = useCallback(async () => {
try { 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) || []); setAvailableTags(res.data?.slice(0, 10) || []);
} catch { } catch {
setAvailableTags([]); setAvailableTags([]);
@ -99,7 +99,7 @@ const KnowledgeBasePage = ({ user }) => {
const fetchPrompts = useCallback(async () => { const fetchPrompts = useCallback(async () => {
try { 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 || []); setAvailablePrompts(res.data.prompts || []);
const def = res.data.prompts?.find(p => p.is_default) || res.data.prompts?.[0]; const def = res.data.prompts?.find(p => p.is_default) || res.data.prompts?.[0];
if (def) setSelectedPromptId(def.id); if (def) setSelectedPromptId(def.id);
@ -130,7 +130,7 @@ const KnowledgeBasePage = ({ user }) => {
setGenerating(true); setGenerating(true);
setTaskProgress(10); setTaskProgress(10);
try { 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, user_prompt: userPrompt,
source_meeting_ids: selectedMeetings.join(','), source_meeting_ids: selectedMeetings.join(','),
prompt_id: selectedPromptId prompt_id: selectedPromptId
@ -138,7 +138,7 @@ const KnowledgeBasePage = ({ user }) => {
const taskId = res.data.task_id; const taskId = res.data.task_id;
const interval = setInterval(async () => { 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; const s = statusRes.data;
setTaskProgress(s.progress || 20); setTaskProgress(s.progress || 20);
if (s.status === 'completed') { if (s.status === 'completed') {
@ -166,7 +166,7 @@ const KnowledgeBasePage = ({ user }) => {
okText: '删除', okText: '删除',
okType: 'danger', okType: 'danger',
onOk: async () => { 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); if (selectedKb?.kb_id === kb.kb_id) setSelectedKb(null);
fetchAllKbs(); fetchAllKbs();
message.success('删除成功'); message.success('删除成功');

View File

@ -24,7 +24,7 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useLocation, useNavigate } from 'react-router-dom'; 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 { API_ENDPOINTS, buildApiUrl } from '../config/api';
import ActionButton from '../components/ActionButton'; import ActionButton from '../components/ActionButton';
import CenterPager from '../components/CenterPager'; import CenterPager from '../components/CenterPager';
@ -123,7 +123,7 @@ const MeetingCenterPage = ({ user }) => {
setLoading(true); setLoading(true);
try { try {
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), { const res = await httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), {
params: { params: {
user_id: user.user_id, user_id: user.user_id,
page: nextPage, page: nextPage,
@ -176,7 +176,7 @@ const MeetingCenterPage = ({ user }) => {
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
try { 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('会议已删除'); message.success('会议已删除');
meetingCacheService.clearAll(); meetingCacheService.clearAll();
const nextPage = page > 1 && meetings.length === 1 ? page - 1 : page; const nextPage = page > 1 && meetings.length === 1 ? page - 1 : page;

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import { Layout, Space, Button, App, Empty, Input, Tabs } from 'antd'; import { Layout, Space, Button, App, Empty, Input, Tabs } from 'antd';
import { LockOutlined, EyeOutlined, CopyOutlined, ShareAltOutlined, HomeOutlined, FileTextOutlined, PartitionOutlined, AudioOutlined } from '@ant-design/icons'; 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 { buildApiUrl, API_ENDPOINTS } from '../config/api';
import MarkdownRenderer from '../components/MarkdownRenderer'; import MarkdownRenderer from '../components/MarkdownRenderer';
import MindMap from '../components/MindMap'; import MindMap from '../components/MindMap';
@ -38,8 +38,8 @@ const MeetingPreview = () => {
const fetchTranscriptAndAudio = useCallback(async () => { const fetchTranscriptAndAudio = useCallback(async () => {
const [transcriptRes, audioRes] = await Promise.allSettled([ const [transcriptRes, audioRes] = await Promise.allSettled([
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meeting_id))), httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meeting_id))),
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.AUDIO(meeting_id))), httpService.get(buildApiUrl(API_ENDPOINTS.MEETINGS.AUDIO(meeting_id))),
]); ]);
if (transcriptRes.status === 'fulfilled') { if (transcriptRes.status === 'fulfilled') {
@ -60,7 +60,7 @@ const MeetingPreview = () => {
try { try {
const endpoint = API_ENDPOINTS.MEETINGS.PREVIEW_DATA(meeting_id); const endpoint = API_ENDPOINTS.MEETINGS.PREVIEW_DATA(meeting_id);
const url = buildApiUrl(`${endpoint}${pwd ? `?password=${encodeURIComponent(pwd)}` : ''}`); const url = buildApiUrl(`${endpoint}${pwd ? `?password=${encodeURIComponent(pwd)}` : ''}`);
const res = await apiClient.get(url); const res = await httpService.get(url);
setMeeting(res.data); setMeeting(res.data);
setIsAuthorized(true); setIsAuthorized(true);
setPasswordRequired(false); setPasswordRequired(false);

View File

@ -22,7 +22,7 @@ import {
SaveOutlined, SaveOutlined,
StarFilled, StarFilled,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { API_ENDPOINTS, buildApiUrl } from '../config/api'; import { API_ENDPOINTS, buildApiUrl } from '../config/api';
import PromptManagementPage from './PromptManagementPage'; import PromptManagementPage from './PromptManagementPage';
import MarkdownRenderer from '../components/MarkdownRenderer'; import MarkdownRenderer from '../components/MarkdownRenderer';
@ -49,7 +49,7 @@ const PromptConfigPage = ({ user }) => {
const loadConfig = useCallback(async () => { const loadConfig = useCallback(async () => {
setLoading(true); setLoading(true);
try { 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 || []); setAvailablePrompts(res.data.available_prompts || []);
setSelectedPromptIds(res.data.selected_prompt_ids || []); setSelectedPromptIds(res.data.selected_prompt_ids || []);
} catch { } catch {
@ -98,7 +98,7 @@ const PromptConfigPage = ({ user }) => {
is_enabled: true, is_enabled: true,
sort_order: index + 1, 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('提示词配置已保存'); message.success('提示词配置已保存');
loadConfig(); loadConfig();
} catch (error) { } catch (error) {
@ -126,7 +126,7 @@ const PromptConfigPage = ({ user }) => {
items={[ items={[
{ {
key: 'config', key: 'config',
label: '系统提示词配置', label: '客户端提示词配置',
children: ( children: (
<div className="console-tab-panel"> <div className="console-tab-panel">
<div className="console-tab-toolbar"> <div className="console-tab-toolbar">

View File

@ -26,7 +26,7 @@ import {
SaveOutlined, SaveOutlined,
StarFilled, StarFilled,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../utils/apiClient'; import httpService from '../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import MarkdownEditor from '../components/MarkdownEditor'; import MarkdownEditor from '../components/MarkdownEditor';
import CenterPager from '../components/CenterPager'; import CenterPager from '../components/CenterPager';
@ -72,7 +72,7 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
setLoading(true); setLoading(true);
try { try {
const is_active = statusFilter === 'all' ? undefined : Number(statusFilter); 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: { params: {
page, page,
size, size,
@ -135,10 +135,10 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
} }
setDrawerSubmitting(true); setDrawerSubmitting(true);
if (editingPrompt) { 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('提示词已更新'); message.success('提示词已更新');
} else { } else {
await apiClient.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), payload); await httpService.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), payload);
message.success('提示词已创建'); message.success('提示词已创建');
} }
setDrawerOpen(false); setDrawerOpen(false);
@ -160,7 +160,7 @@ const PromptManagementPage = ({ user, mode = 'default', embedded = false }) => {
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
try { try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(prompt.id))); await httpService.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(prompt.id)));
message.success('提示词已删除'); message.success('提示词已删除');
loadPrompts(); loadPrompts();
} catch (error) { } catch (error) {

View File

@ -9,7 +9,7 @@ import {
FileOutlined, FileOutlined,
CloseOutlined CloseOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../../utils/apiClient'; import httpService from '../../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
const { Option } = Select; const { Option } = Select;
const { TextArea } = Input; const { TextArea } = Input;
@ -28,7 +28,7 @@ const DictManagement = () => {
// //
const fetchDictTypes = useCallback(async () => { const fetchDictTypes = useCallback(async () => {
try { 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') { if (response.code === '200') {
const types = (response.data.types || []).filter((type) => type !== 'system_config'); const types = (response.data.types || []).filter((type) => type !== 'system_config');
setDictTypes(types); setDictTypes(types);
@ -60,7 +60,7 @@ const DictManagement = () => {
const fetchDictData = useCallback(async (dictType) => { const fetchDictData = useCallback(async (dictType) => {
try { 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') { if (response.code === '200') {
setDictData(response.data.items); setDictData(response.data.items);
@ -144,7 +144,7 @@ const DictManagement = () => {
if (selectedNode) { if (selectedNode) {
// //
const response = await apiClient.put( const response = await httpService.put(
buildApiUrl(API_ENDPOINTS.DICT_DATA.UPDATE(selectedNode.id)), buildApiUrl(API_ENDPOINTS.DICT_DATA.UPDATE(selectedNode.id)),
values values
); );
@ -154,7 +154,7 @@ const DictManagement = () => {
} }
} else { } else {
// //
const response = await apiClient.post( const response = await httpService.post(
buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE), buildApiUrl(API_ENDPOINTS.DICT_DATA.CREATE),
values values
); );
@ -174,7 +174,7 @@ const DictManagement = () => {
const handleDelete = async () => { const handleDelete = async () => {
if (!selectedNode) return; if (!selectedNode) return;
try { 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('删除成功'); message.success('删除成功');
setSelectedNode(null); setSelectedNode(null);
setIsEditing(false); setIsEditing(false);

View File

@ -35,7 +35,7 @@ import {
CheckCircleOutlined, CheckCircleOutlined,
BlockOutlined, BlockOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../../utils/apiClient'; import httpService from '../../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import AdminModuleShell from '../../components/AdminModuleShell'; import AdminModuleShell from '../../components/AdminModuleShell';
import ActionButton from '../../components/ActionButton'; import ActionButton from '../../components/ActionButton';
@ -90,7 +90,7 @@ const ExternalAppManagement = () => {
const fetchApps = useCallback(async () => { const fetchApps = useCallback(async () => {
setLoading(true); setLoading(true);
try { 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') { if (response.code === '200') {
setApps(response.data || []); setApps(response.data || []);
} }
@ -138,10 +138,10 @@ const ExternalAppManagement = () => {
}; };
if (isEditing) { 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('应用更新成功'); message.success('应用更新成功');
} else { } else {
await apiClient.post(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.CREATE), payload); await httpService.post(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.CREATE), payload);
message.success('应用创建成功'); message.success('应用创建成功');
} }
@ -162,7 +162,7 @@ const ExternalAppManagement = () => {
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
try { 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('删除成功'); message.success('删除成功');
fetchApps(); fetchApps();
} catch { } catch {
@ -182,7 +182,7 @@ const ExternalAppManagement = () => {
uploadFormData.append(fieldName, file); uploadFormData.append(fieldName, file);
try { try {
const response = await apiClient.post(buildApiUrl(endpoint), uploadFormData, { const response = await httpService.post(buildApiUrl(endpoint), uploadFormData, {
headers: { 'Content-Type': 'multipart/form-data' }, headers: { 'Content-Type': 'multipart/form-data' },
}); });
@ -217,7 +217,7 @@ const ExternalAppManagement = () => {
const handleToggleStatus = async (item, checked) => { const handleToggleStatus = async (item, checked) => {
try { try {
const newActive = checked ? 1 : 0; 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) => ( setApps((prev) => prev.map((app) => (
app.id === item.id ? { ...app, is_active: newActive } : app app.id === item.id ? { ...app, is_active: newActive } : app
))); )));

View File

@ -8,7 +8,7 @@ import {
FontSizeOutlined, SearchOutlined, ReloadOutlined, FontSizeOutlined, SearchOutlined, ReloadOutlined,
SaveOutlined, SaveOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../../utils/apiClient'; import httpService from '../../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import AdminModuleShell from '../../components/AdminModuleShell'; import AdminModuleShell from '../../components/AdminModuleShell';
import ActionButton from '../../components/ActionButton'; import ActionButton from '../../components/ActionButton';
@ -54,7 +54,7 @@ const HotWordManagement = () => {
const fetchGroups = useCallback(async () => { const fetchGroups = useCallback(async () => {
setGroupsLoading(true); setGroupsLoading(true);
try { 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') { if (res.code === '200') {
setGroups(res.data || []); setGroups(res.data || []);
} }
@ -70,7 +70,7 @@ const HotWordManagement = () => {
if (!groupId) { setItems([]); return; } if (!groupId) { setItems([]); return; }
setItemsLoading(true); setItemsLoading(true);
try { 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') { if (res.code === '200') {
setItems(res.data || []); setItems(res.data || []);
} }
@ -129,10 +129,10 @@ const HotWordManagement = () => {
const values = await groupForm.validateFields(); const values = await groupForm.validateFields();
const payload = { ...values, status: values.status ? 1 : 0 }; const payload = { ...values, status: values.status ? 1 : 0 };
if (editingGroup) { 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('更新成功'); message.success('更新成功');
} else { } 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') { if (res.code === '200') {
setSelectedGroupId(res.data?.id); setSelectedGroupId(res.data?.id);
} }
@ -155,7 +155,7 @@ const HotWordManagement = () => {
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
try { 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('删除成功'); message.success('删除成功');
if (selectedGroupId === group.id) setSelectedGroupId(null); if (selectedGroupId === group.id) setSelectedGroupId(null);
fetchGroups(); fetchGroups();
@ -170,7 +170,7 @@ const HotWordManagement = () => {
e?.stopPropagation(); e?.stopPropagation();
setSyncLoadingId(group.id); setSyncLoadingId(group.id);
try { 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') { if (res.code === '200') {
message.success(`同步成功 — 词表ID: ${res.data?.vocabulary_id}`); message.success(`同步成功 — 词表ID: ${res.data?.vocabulary_id}`);
fetchGroups(); fetchGroups();
@ -208,10 +208,10 @@ const HotWordManagement = () => {
const values = await itemForm.validateFields(); const values = await itemForm.validateFields();
const payload = { ...values, status: values.status ? 1 : 0 }; const payload = { ...values, status: values.status ? 1 : 0 };
if (editingItem) { 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('更新成功'); message.success('更新成功');
} else { } 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('创建成功'); message.success('创建成功');
} }
setItemDrawerOpen(false); setItemDrawerOpen(false);
@ -231,7 +231,7 @@ const HotWordManagement = () => {
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
try { 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('删除成功'); message.success('删除成功');
fetchItems(selectedGroupId); fetchItems(selectedGroupId);
fetchGroups(); fetchGroups();
@ -244,7 +244,7 @@ const HotWordManagement = () => {
const toggleItemStatus = async (item, checked) => { const toggleItemStatus = async (item, checked) => {
try { 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, status: checked ? 1 : 0,
}); });
setItems((prev) => prev.map((it) => (it.id === item.id ? { ...it, status: checked ? 1 : 0 } : it))); setItems((prev) => prev.map((it) => (it.id === item.id ? { ...it, status: checked ? 1 : 0 } : it)));

View File

@ -20,7 +20,7 @@ import {
Tooltip, Tooltip,
} from 'antd'; } from 'antd';
import { DeleteOutlined, EditOutlined, ExperimentOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined } from '@ant-design/icons'; 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 { API_ENDPOINTS, buildApiUrl } from '../../config/api';
import { import {
buildManagedModelCode, buildManagedModelCode,
@ -179,7 +179,7 @@ const ModelManagement = () => {
kind === 'llm' kind === 'llm'
? API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIG_DETAIL(modelCode) ? API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIG_DETAIL(modelCode)
: API_ENDPOINTS.ADMIN.AUDIO_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模型删除成功' : '音频模型删除成功'); message.success(kind === 'llm' ? 'LLM模型删除成功' : '音频模型删除成功');
fetchAll(); fetchAll();
} catch (error) { } catch (error) {
@ -191,9 +191,9 @@ const ModelManagement = () => {
setLoading(true); setLoading(true);
try { try {
const [llmRes, audioRes, groupsRes] = await Promise.all([ const [llmRes, audioRes, groupsRes] = await Promise.all([
apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIGS)), httpService.get(buildApiUrl(API_ENDPOINTS.ADMIN.LLM_MODEL_CONFIGS)),
apiClient.get(buildApiUrl(`${API_ENDPOINTS.ADMIN.AUDIO_MODEL_CONFIGS}?scene=all`)), httpService.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.HOT_WORDS.GROUPS)),
]); ]);
setLlmItems(llmRes.data.items || []); setLlmItems(llmRes.data.items || []);
setAudioItems(audioRes.data.items || []); setAudioItems(audioRes.data.items || []);
@ -298,20 +298,20 @@ const ModelManagement = () => {
const payload = buildLlmPayload(values); const payload = buildLlmPayload(values);
if (editingRow) { 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模型更新成功'); message.success('LLM模型更新成功');
} else { } 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模型创建成功'); message.success('LLM模型创建成功');
} }
} else { } else {
const payload = buildAudioPayload(values); const payload = buildAudioPayload(values);
if (editingRow) { 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('音频模型更新成功'); message.success('音频模型更新成功');
} else { } 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('音频模型创建成功'); message.success('音频模型创建成功');
} }
} }
@ -334,7 +334,7 @@ const ModelManagement = () => {
...buildLlmPayload(values), ...buildLlmPayload(values),
test_prompt: '请用一句中文回复LLM测试成功。', 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({ modal.success({
title: 'LLM测试成功', title: 'LLM测试成功',
width: 640, width: 640,
@ -351,7 +351,7 @@ const ModelManagement = () => {
...buildAudioPayload(values), ...buildAudioPayload(values),
test_file_url: 'https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav', 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({ modal.success({
title: 'ASR测试任务已提交', title: 'ASR测试任务已提交',
width: 680, width: 680,

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; 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 { 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 { 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 { API_ENDPOINTS, buildApiUrl } from '../../config/api';
import AdminModuleShell from '../../components/AdminModuleShell'; import AdminModuleShell from '../../components/AdminModuleShell';
import ActionButton from '../../components/ActionButton'; import ActionButton from '../../components/ActionButton';
@ -57,7 +57,7 @@ const ParameterManagement = () => {
const fetchItems = useCallback(async () => { const fetchItems = useCallback(async () => {
setLoading(true); setLoading(true);
try { 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 || []); setItems(res.data.items || []);
} catch { } catch {
message.error('获取参数列表失败'); message.error('获取参数列表失败');
@ -98,10 +98,10 @@ const ParameterManagement = () => {
setSubmitting(true); setSubmitting(true);
try { try {
if (editing) { 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('参数更新成功'); message.success('参数更新成功');
} else { } else {
await apiClient.post(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS), values); await httpService.post(buildApiUrl(API_ENDPOINTS.ADMIN.PARAMETERS), values);
message.success('参数创建成功'); message.success('参数创建成功');
} }
configService.clearCache(); configService.clearCache();
@ -152,7 +152,7 @@ const ParameterManagement = () => {
okButtonProps={{ danger: true }} okButtonProps={{ danger: true }}
onConfirm={async () => { onConfirm={async () => {
try { 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(); configService.clearCache();
message.success('参数删除成功'); message.success('参数删除成功');
fetchItems(); fetchItems();

View File

@ -39,7 +39,7 @@ import {
CompressOutlined, CompressOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import ActionButton from '../../components/ActionButton'; import ActionButton from '../../components/ActionButton';
import apiClient from '../../utils/apiClient'; import httpService from '../../services/httpService';
import { buildApiUrl } from '../../config/api'; import { buildApiUrl } from '../../config/api';
import AdminModuleShell from '../../components/AdminModuleShell'; import AdminModuleShell from '../../components/AdminModuleShell';
import useSystemPageSize from '../../hooks/useSystemPageSize'; import useSystemPageSize from '../../hooks/useSystemPageSize';
@ -214,9 +214,9 @@ const PermissionManagement = () => {
setLoading(true); setLoading(true);
try { try {
const [rolesRes, menusRes, rolePermsRes] = await Promise.all([ const [rolesRes, menusRes, rolePermsRes] = await Promise.all([
apiClient.get(buildApiUrl('/api/admin/roles')), httpService.get(buildApiUrl('/api/admin/roles')),
apiClient.get(buildApiUrl('/api/admin/menus')), httpService.get(buildApiUrl('/api/admin/menus')),
apiClient.get(buildApiUrl('/api/admin/roles/permissions/all')), httpService.get(buildApiUrl('/api/admin/roles/permissions/all')),
]); ]);
const rolesList = rolesRes.data.roles || []; const rolesList = rolesRes.data.roles || [];
@ -255,7 +255,7 @@ const PermissionManagement = () => {
} }
setRoleUsersLoading(true); setRoleUsersLoading(true);
try { try {
const res = await apiClient.get( const res = await httpService.get(
buildApiUrl(`/api/admin/roles/${selectedRoleId}/users?page=${roleUsersPage}&size=${roleUsersPageSize}`), buildApiUrl(`/api/admin/roles/${selectedRoleId}/users?page=${roleUsersPage}&size=${roleUsersPageSize}`),
); );
setRoleUsers(res.data.users || []); setRoleUsers(res.data.users || []);
@ -280,7 +280,7 @@ const PermissionManagement = () => {
setSavingPermission(true); setSavingPermission(true);
try { try {
await apiClient.put(buildApiUrl(`/api/admin/roles/${selectedRoleId}/permissions`), { await httpService.put(buildApiUrl(`/api/admin/roles/${selectedRoleId}/permissions`), {
menu_ids: sanitizedMenuIds, menu_ids: sanitizedMenuIds,
}); });
@ -317,10 +317,10 @@ const PermissionManagement = () => {
setRoleSubmitting(true); setRoleSubmitting(true);
try { try {
if (editingRole) { 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('角色更新成功'); message.success('角色更新成功');
} else { } else {
await apiClient.post(buildApiUrl('/api/admin/roles'), values); await httpService.post(buildApiUrl('/api/admin/roles'), values);
message.success('角色创建成功'); message.success('角色创建成功');
} }
setRoleDrawerOpen(false); setRoleDrawerOpen(false);
@ -397,10 +397,10 @@ const PermissionManagement = () => {
setMenuSubmitting(true); setMenuSubmitting(true);
try { try {
if (editingMenu) { 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('菜单更新成功'); message.success('菜单更新成功');
} else { } else {
await apiClient.post(buildApiUrl('/api/admin/menus'), values); await httpService.post(buildApiUrl('/api/admin/menus'), values);
message.success('菜单创建成功'); message.success('菜单创建成功');
} }
setMenuIconPickerOpen(false); setMenuIconPickerOpen(false);
@ -415,7 +415,7 @@ const PermissionManagement = () => {
const deleteMenu = async (menuId) => { const deleteMenu = async (menuId) => {
try { try {
await apiClient.delete(buildApiUrl(`/api/admin/menus/${menuId}`)); await httpService.delete(buildApiUrl(`/api/admin/menus/${menuId}`));
message.success('菜单删除成功'); message.success('菜单删除成功');
if (selectedManageMenuId === menuId) { if (selectedManageMenuId === menuId) {
setSelectedManageMenuId(null); setSelectedManageMenuId(null);
@ -631,7 +631,7 @@ const PermissionManagement = () => {
try { try {
await Promise.all( await Promise.all(
updates.map((item) => 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, parent_id: item.parent_id,
sort_order: item.sort_order, sort_order: item.sort_order,
}), }),

View File

@ -19,7 +19,7 @@ import {
MessageOutlined, MessageOutlined,
DatabaseOutlined DatabaseOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../../utils/apiClient'; import httpService from '../../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import ActionButton from '../../components/ActionButton'; import ActionButton from '../../components/ActionButton';
import MarkdownEditor from '../../components/MarkdownEditor'; import MarkdownEditor from '../../components/MarkdownEditor';
@ -47,7 +47,7 @@ const PromptManagement = () => {
const fetchPrompts = useCallback(async () => { const fetchPrompts = useCallback(async () => {
try { 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 || []; const list = response.data.prompts || [];
setPrompts(list); setPrompts(list);
if (list.length > 0 && !selectedPrompt) { if (list.length > 0 && !selectedPrompt) {
@ -81,7 +81,7 @@ const PromptManagement = () => {
is_default: editingPrompt.is_default ? 1 : 0, is_default: editingPrompt.is_default ? 1 : 0,
is_active: editingPrompt.is_active ? 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('保存成功'); message.success('保存成功');
fetchPrompts(); fetchPrompts();
} catch { } catch {
@ -102,7 +102,7 @@ const PromptManagement = () => {
okText: '删除', okText: '删除',
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(item.id))); await httpService.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(item.id)));
message.success('删除成功'); message.success('删除成功');
setSelectedPrompt(null); setSelectedPrompt(null);
fetchPrompts(); fetchPrompts();
@ -216,7 +216,7 @@ const PromptManagement = () => {
onOk={() => form.submit()} onOk={() => form.submit()}
> >
<Form form={form} layout="vertical" onFinish={async (v) => { <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('创建成功'); message.success('创建成功');
setShowCreateModal(false); setShowCreateModal(false);
fetchPrompts(); fetchPrompts();

View File

@ -21,7 +21,7 @@ import {
ClusterOutlined, ClusterOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import apiClient from '../../utils/apiClient'; import httpService from '../../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import AdminModuleShell from '../../components/AdminModuleShell'; import AdminModuleShell from '../../components/AdminModuleShell';
import { import {
@ -79,9 +79,9 @@ const SystemManagementOverview = () => {
setLoading(true); setLoading(true);
try { try {
const [clientRes, appRes, terminalRes] = await Promise.all([ const [clientRes, appRes, terminalRes] = await Promise.all([
apiClient.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST)), httpService.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST)),
apiClient.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST)), httpService.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST)),
apiClient.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST, { page: 1, size: 10000 })), httpService.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST, { page: 1, size: 10000 })),
]); ]);
setClients(clientRes?.data?.clients || []); setClients(clientRes?.data?.clients || []);

View File

@ -30,7 +30,7 @@ import {
CheckCircleOutlined, CheckCircleOutlined,
SafetyCertificateOutlined, SafetyCertificateOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../../utils/apiClient'; import httpService from '../../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import AdminModuleShell from '../../components/AdminModuleShell'; import AdminModuleShell from '../../components/AdminModuleShell';
import ActionButton from '../../components/ActionButton'; import ActionButton from '../../components/ActionButton';
@ -61,7 +61,7 @@ const TerminalManagement = () => {
const fetchTerminalTypes = useCallback(async () => { const fetchTerminalTypes = useCallback(async () => {
try { 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' }, params: { parent_code: 'TERMINAL' },
}); });
if (response.code === '200') { if (response.code === '200') {
@ -75,7 +75,7 @@ const TerminalManagement = () => {
const fetchTerminals = useCallback(async () => { const fetchTerminals = useCallback(async () => {
setLoading(true); setLoading(true);
try { 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 }, params: { page: 1, size: 10000 },
}); });
if (response.code === '200') { if (response.code === '200') {
@ -122,10 +122,10 @@ const TerminalManagement = () => {
}; };
if (isEditing) { 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('终端更新成功'); message.success('终端更新成功');
} else { } else {
await apiClient.post(buildApiUrl(API_ENDPOINTS.TERMINALS.CREATE), payload); await httpService.post(buildApiUrl(API_ENDPOINTS.TERMINALS.CREATE), payload);
message.success('终端创建成功'); message.success('终端创建成功');
} }
@ -146,7 +146,7 @@ const TerminalManagement = () => {
okType: 'danger', okType: 'danger',
onOk: async () => { onOk: async () => {
try { try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.TERMINALS.DELETE(item.id))); await httpService.delete(buildApiUrl(API_ENDPOINTS.TERMINALS.DELETE(item.id)));
message.success('删除成功'); message.success('删除成功');
fetchTerminals(); fetchTerminals();
} catch { } catch {
@ -159,7 +159,7 @@ const TerminalManagement = () => {
const handleToggleStatus = async (item, checked) => { const handleToggleStatus = async (item, checked) => {
try { try {
const newStatus = checked ? 1 : 0; 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 }, params: { status: newStatus },
}); });
setTerminals((prev) => prev.map((terminal) => ( setTerminals((prev) => prev.map((terminal) => (

View File

@ -12,7 +12,7 @@ import {
SafetyCertificateOutlined, SafetyCertificateOutlined,
ReloadOutlined, ReloadOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import apiClient from '../../utils/apiClient'; import httpService from '../../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import AdminModuleShell from '../../components/AdminModuleShell'; import AdminModuleShell from '../../components/AdminModuleShell';
import ActionButton from '../../components/ActionButton'; import ActionButton from '../../components/ActionButton';
@ -54,7 +54,7 @@ const UserManagement = () => {
if (debouncedSearchText) { if (debouncedSearchText) {
url += `&search=${encodeURIComponent(debouncedSearchText)}`; url += `&search=${encodeURIComponent(debouncedSearchText)}`;
} }
const response = await apiClient.get(buildApiUrl(url)); const response = await httpService.get(buildApiUrl(url));
setUsers(response.data.users || []); setUsers(response.data.users || []);
setTotal(response.data.total || 0); setTotal(response.data.total || 0);
} catch { } catch {
@ -70,7 +70,7 @@ const UserManagement = () => {
const fetchRoles = async () => { const fetchRoles = async () => {
try { try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.ROLES)); const response = await httpService.get(buildApiUrl(API_ENDPOINTS.USERS.ROLES));
setRoles(response.data || []); setRoles(response.data || []);
} catch { } catch {
setRoles([ setRoles([
@ -98,10 +98,10 @@ const UserManagement = () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
if (isEditing) { 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('用户修改成功'); message.success('用户修改成功');
} else { } else {
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), values); await httpService.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), values);
message.success('用户添加成功'); message.success('用户添加成功');
} }
setShowUserDrawer(false); setShowUserDrawer(false);
@ -122,7 +122,7 @@ const UserManagement = () => {
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
try { 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('用户删除成功'); message.success('用户删除成功');
fetchUsers(); fetchUsers();
} catch { } catch {
@ -140,7 +140,7 @@ const UserManagement = () => {
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
try { 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('密码重置成功'); message.success('密码重置成功');
} catch { } catch {
message.error('重置失败'); message.error('重置失败');

View File

@ -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 = '/';
}
};

View File

@ -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;

View File

@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { clearAuthSession, getStoredToken } from '../services/authSessionService';
// 创建axios实例 // 创建axios实例
const apiClient = axios.create(); const apiClient = axios.create();
@ -6,20 +7,9 @@ const apiClient = axios.create();
// 请求拦截器 - 自动添加Authorization头 // 请求拦截器 - 自动添加Authorization头
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config) => { (config) => {
const savedUser = localStorage.getItem('iMeetingUser'); const token = getStoredToken();
if (token) {
// 增加严格校验:排除 null, undefined 字符串以及空值 config.headers.Authorization = `Bearer ${token}`;
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');
}
} }
return config; return config;
}, },
@ -45,11 +35,7 @@ apiClient.interceptors.response.use(
}, },
(error) => { (error) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
localStorage.removeItem('iMeetingUser'); clearAuthSession({ redirectToRoot: true });
// 避免重复跳转
if (window.location.pathname !== '/') {
window.location.href = '/';
}
} }
return Promise.reject(error); return Promise.reject(error);
} }