feat: add admin permissions voice input analytics and feedback
This commit is contained in:
@@ -7,7 +7,8 @@ from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.schemas.admin import AdminLoginRequest, AdminLoginResponse, AdminRead
|
||||
from app.schemas.admin import AdminLoginRequest, AdminLoginResponse
|
||||
from app.services.admin_permission_service import permissions_for
|
||||
from app.services.admin_service import AdminAuthService
|
||||
from app.services.auth_service import AuthService
|
||||
from app.services.security_state_service import client_ip
|
||||
@@ -24,7 +25,12 @@ def login(payload: AdminLoginRequest, request: Request, db: Session = Depends(ge
|
||||
|
||||
@router.get("/profile")
|
||||
def profile(current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
return api_success(AdminRead.model_validate(current_admin).model_dump())
|
||||
return api_success({
|
||||
"id": current_admin.id, "username": current_admin.username, "name": current_admin.name,
|
||||
"status": current_admin.status, "isSuperAdmin": bool(current_admin.is_super_admin),
|
||||
"mustChangePassword": bool(current_admin.must_change_password),
|
||||
"permissions": sorted(permissions_for(current_admin)),
|
||||
})
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
|
||||
@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -16,6 +17,7 @@ from app.schemas.admin import DashboardStats
|
||||
from app.services.admin_service import AdminDashboardService
|
||||
from app.models.logs import StorageSnapshot
|
||||
from app.services.redis_client import get_sync_redis_client
|
||||
from app.services.request_traffic_service import RequestTrafficService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -37,6 +39,14 @@ def dashboard(
|
||||
return api_success(DashboardStats.model_validate(stats).model_dump())
|
||||
|
||||
|
||||
@router.get("/dashboard/traffic")
|
||||
async def peak_traffic(
|
||||
grain: Literal["minute", "hour", "day", "week"] = Query(default="hour"),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
return api_success(await RequestTrafficService.peak_traffic(grain))
|
||||
|
||||
|
||||
@router.get("/dashboard/storage")
|
||||
def storage_stats(
|
||||
refresh: bool = Query(default=False),
|
||||
|
||||
120
ai_knowledge_base_v2/apps/backend/app/api/admin_management.py
Normal file
120
ai_knowledge_base_v2/apps/backend/app/api/admin_management.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.core.security import hash_password, verify_password
|
||||
from app.models.admin import Admin, Role
|
||||
from app.schemas.admin import AdminPasswordChangeRequest, ManagedAdminCreateRequest, ManagedAdminUpdateRequest
|
||||
from app.services.admin_permission_service import ALL_PERMISSION_CODES, PERMISSION_TREE, permissions_for, require_permission
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _serialize(admin: Admin) -> dict:
|
||||
return {
|
||||
"id": admin.id, "username": admin.username, "name": admin.name, "status": admin.status,
|
||||
"isSuperAdmin": bool(admin.is_super_admin), "mustChangePassword": bool(admin.must_change_password),
|
||||
"permissions": sorted(permissions_for(admin)), "lastLoginAt": admin.last_login_at,
|
||||
"createdAt": admin.created_at,
|
||||
}
|
||||
|
||||
|
||||
def _validated_permissions(values: list[str]) -> list[str]:
|
||||
unknown = set(values) - ALL_PERMISSION_CODES
|
||||
if unknown:
|
||||
raise HTTPException(status_code=422, detail=f"包含未知权限:{', '.join(sorted(unknown))}")
|
||||
return sorted(set(values))
|
||||
|
||||
|
||||
def _require_super(admin: Admin) -> None:
|
||||
if not admin.is_super_admin:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅超级管理员可管理管理员账号")
|
||||
|
||||
|
||||
@router.get("/permission-tree")
|
||||
def permission_tree(current: Admin = Depends(get_current_admin)) -> dict:
|
||||
_require_super(current)
|
||||
return api_success(PERMISSION_TREE)
|
||||
|
||||
|
||||
@router.get("/administrator/list")
|
||||
def list_administrators(db: Session = Depends(get_db), current: Admin = Depends(get_current_admin)) -> dict:
|
||||
_require_super(current)
|
||||
rows = db.scalars(select(Admin).options(selectinload(Admin.role)).order_by(Admin.is_super_admin.desc(), Admin.id)).all()
|
||||
return api_success([_serialize(item) for item in rows])
|
||||
|
||||
|
||||
@router.post("/administrator")
|
||||
def create_administrator(payload: ManagedAdminCreateRequest, db: Session = Depends(get_db), current: Admin = Depends(get_current_admin)) -> dict:
|
||||
_require_super(current)
|
||||
if db.scalar(select(Admin.id).where(Admin.username == payload.username.strip())):
|
||||
raise HTTPException(status_code=409, detail="管理员账号已存在")
|
||||
permissions = _validated_permissions(payload.permissions)
|
||||
role = Role(code=f"admin_{payload.username.strip().lower()}", name=f"{payload.name.strip()}的权限", permissions=json.dumps(permissions))
|
||||
db.add(role)
|
||||
db.flush()
|
||||
item = Admin(username=payload.username.strip(), name=payload.name.strip(), password=hash_password(payload.initialPassword), role_id=role.id, status=payload.status, must_change_password=1, is_super_admin=0)
|
||||
db.add(item)
|
||||
db.commit()
|
||||
db.refresh(item)
|
||||
item.role = role
|
||||
return api_success(_serialize(item))
|
||||
|
||||
|
||||
@router.put("/administrator/{admin_id}")
|
||||
def update_administrator(admin_id: int, payload: ManagedAdminUpdateRequest, db: Session = Depends(get_db), current: Admin = Depends(get_current_admin)) -> dict:
|
||||
_require_super(current)
|
||||
item = db.scalar(select(Admin).options(selectinload(Admin.role)).where(Admin.id == admin_id))
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="管理员不存在")
|
||||
if item.is_super_admin:
|
||||
raise HTTPException(status_code=403, detail="超级管理员账号由环境变量管理,不可修改")
|
||||
item.name, item.status = payload.name.strip(), payload.status
|
||||
if item.role is None:
|
||||
item.role = Role(code=f"admin_{item.username.lower()}", name=f"{item.name}的权限")
|
||||
item.role.name = f"{item.name}的权限"
|
||||
item.role.permissions = json.dumps(_validated_permissions(payload.permissions))
|
||||
if payload.resetPassword:
|
||||
item.password = hash_password(payload.resetPassword)
|
||||
item.must_change_password = 1
|
||||
db.commit()
|
||||
db.refresh(item)
|
||||
return api_success(_serialize(item))
|
||||
|
||||
|
||||
@router.delete("/administrator/{admin_id}")
|
||||
def delete_administrator(admin_id: int, db: Session = Depends(get_db), current: Admin = Depends(get_current_admin)) -> dict:
|
||||
_require_super(current)
|
||||
item = db.scalar(select(Admin).options(selectinload(Admin.role)).where(Admin.id == admin_id))
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="管理员不存在")
|
||||
if item.is_super_admin:
|
||||
raise HTTPException(status_code=403, detail="超级管理员不可删除")
|
||||
role = item.role
|
||||
db.delete(item)
|
||||
db.flush()
|
||||
if role is not None:
|
||||
db.delete(role)
|
||||
db.commit()
|
||||
return api_success()
|
||||
|
||||
|
||||
@router.post("/password")
|
||||
def change_password(payload: AdminPasswordChangeRequest, db: Session = Depends(get_db), current: Admin = Depends(get_current_admin)) -> dict:
|
||||
if current.is_super_admin:
|
||||
raise HTTPException(status_code=403, detail="超级管理员密码由环境变量管理,不可在后台修改")
|
||||
if not verify_password(payload.currentPassword, current.password):
|
||||
raise HTTPException(status_code=400, detail="当前密码不正确")
|
||||
if payload.currentPassword == payload.newPassword:
|
||||
raise HTTPException(status_code=400, detail="新密码不能与当前密码相同")
|
||||
current.password = hash_password(payload.newPassword)
|
||||
current.must_change_password = 0
|
||||
db.commit()
|
||||
return api_success()
|
||||
92
ai_knowledge_base_v2/apps/backend/app/api/feedback.py
Normal file
92
ai_knowledge_base_v2/apps/backend/app/api/feedback.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.pagination import page_result
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin, get_current_user
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.chat import ChatMessage, ChatSession
|
||||
from app.models.feedback import MessageFeedback
|
||||
from app.models.user import User
|
||||
from app.services.admin_service import OperationLogService
|
||||
from app.services.admin_permission_service import require_permission
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class FeedbackCreate(BaseModel):
|
||||
messageId: int = Field(gt=0)
|
||||
content: str = Field(min_length=1, max_length=200)
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_feedback(payload: FeedbackCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
message = db.scalar(select(ChatMessage).where(ChatMessage.id == payload.messageId, ChatMessage.user_id == user.id))
|
||||
if message is None or message.role != "assistant" or message.message_status != "FINISHED":
|
||||
raise HTTPException(status_code=404, detail="反馈的回答不存在")
|
||||
content = payload.content.strip()
|
||||
if not content:
|
||||
raise HTTPException(status_code=400, detail="请填写反馈内容")
|
||||
existing = db.scalar(select(MessageFeedback).where(MessageFeedback.user_id == user.id, MessageFeedback.message_id == message.id))
|
||||
if existing:
|
||||
raise HTTPException(status_code=409, detail="这条回答已经反馈过了")
|
||||
item = MessageFeedback(user_id=user.id, session_id=message.session_id, message_id=message.id, content=content)
|
||||
db.add(item)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=409, detail="这条回答已经反馈过了") from exc
|
||||
db.refresh(item)
|
||||
return api_success({"id": item.id})
|
||||
|
||||
|
||||
@router.get("/admin/list")
|
||||
def feedback_list(readStatus: str = Query(default="all", pattern="^(all|read|unread)$"), page: int = Query(default=1, ge=1), pageSize: int = Query(default=20, ge=10, le=100), db: Session = Depends(get_db), _admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
require_permission(_admin, "feedback.view")
|
||||
query = select(MessageFeedback, User, ChatMessage).join(User, User.id == MessageFeedback.user_id).join(ChatMessage, ChatMessage.id == MessageFeedback.message_id)
|
||||
if readStatus != "all":
|
||||
query = query.where(MessageFeedback.is_read == (1 if readStatus == "read" else 0))
|
||||
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
|
||||
rows = db.execute(query.order_by(MessageFeedback.created_at.desc()).offset((page - 1) * pageSize).limit(pageSize)).all()
|
||||
return api_success(page_result([_summary(*row) for row in rows], total=total, page=page, page_size=pageSize))
|
||||
|
||||
|
||||
@router.get("/admin/{feedback_id}")
|
||||
def feedback_detail(feedback_id: int, db: Session = Depends(get_db), admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
row = db.execute(select(MessageFeedback, User, ChatMessage, ChatSession).join(User, User.id == MessageFeedback.user_id).join(ChatMessage, ChatMessage.id == MessageFeedback.message_id).join(ChatSession, ChatSession.id == MessageFeedback.session_id).where(MessageFeedback.id == feedback_id)).first()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="反馈不存在")
|
||||
require_permission(admin, "feedback.view")
|
||||
feedback, user, target, session = row
|
||||
if not feedback.is_read:
|
||||
feedback.is_read = 1
|
||||
feedback.read_by = admin.id
|
||||
feedback.read_at = datetime.now(UTC).replace(tzinfo=None)
|
||||
db.commit()
|
||||
messages = db.scalars(select(ChatMessage).where(ChatMessage.session_id == feedback.session_id, ChatMessage.id <= target.id).order_by(ChatMessage.id.asc()).limit(200)).all()
|
||||
return api_success({**_summary(feedback, user, target), "sessionTitle": session.title, "messages": [{"id": m.id, "role": m.role, "content": m.content, "createdAt": m.created_at, "isTarget": m.id == target.id} for m in messages]})
|
||||
|
||||
|
||||
@router.delete("/admin/{feedback_id}")
|
||||
def delete_feedback(feedback_id: int, db: Session = Depends(get_db), admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
require_permission(admin, "feedback.delete")
|
||||
item = db.get(MessageFeedback, feedback_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="反馈不存在")
|
||||
db.delete(item)
|
||||
OperationLogService.write(db, admin_id=admin.id, module="feedback", action="delete", target_id=feedback_id)
|
||||
db.commit()
|
||||
return api_success()
|
||||
|
||||
|
||||
def _summary(item: MessageFeedback, user: User, message: ChatMessage) -> dict:
|
||||
return {"id": item.id, "userId": user.id, "userName": user.name, "userPhone": user.phone, "messageId": message.id, "messageContent": message.content, "content": item.content, "isRead": bool(item.is_read), "createdAt": item.created_at}
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.core.dependencies import enforce_admin_access
|
||||
|
||||
from app.api import (
|
||||
admin_auth,
|
||||
@@ -10,15 +11,18 @@ from app.api import (
|
||||
admin_entitlements,
|
||||
admin_knowledge,
|
||||
admin_knowledge_lifecycle,
|
||||
admin_management,
|
||||
admin_records,
|
||||
admin_settings,
|
||||
admin_sso,
|
||||
admin_users,
|
||||
auth,
|
||||
feedback,
|
||||
chat,
|
||||
health,
|
||||
integration_sso,
|
||||
user,
|
||||
voice,
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -26,15 +30,19 @@ api_router.include_router(health.router, tags=["health"])
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(integration_sso.router, prefix="/integration/sso", tags=["integration-sso"])
|
||||
api_router.include_router(user.router, prefix="/user", tags=["user"])
|
||||
api_router.include_router(voice.router, prefix="/voice", tags=["voice"])
|
||||
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
||||
api_router.include_router(feedback.router, prefix="/feedback", tags=["feedback"])
|
||||
api_router.include_router(admin_auth.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_content_generation.router, prefix="/admin", tags=["admin-content-generation"])
|
||||
api_router.include_router(admin_agent_records.router, prefix="/admin", tags=["admin-agent-records"])
|
||||
api_router.include_router(admin_dashboard.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_entitlements.router, prefix="/admin", tags=["admin-entitlements"])
|
||||
api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_knowledge.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_knowledge_lifecycle.router, prefix="/admin", tags=["admin-knowledge-lifecycle"])
|
||||
api_router.include_router(admin_settings.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_sso.router, prefix="/admin", tags=["admin-sso"])
|
||||
api_router.include_router(admin_records.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_management.router, prefix="/admin", tags=["admin-management"])
|
||||
guard = [Depends(enforce_admin_access)]
|
||||
api_router.include_router(admin_content_generation.router, prefix="/admin", tags=["admin-content-generation"], dependencies=guard)
|
||||
api_router.include_router(admin_agent_records.router, prefix="/admin", tags=["admin-agent-records"], dependencies=guard)
|
||||
api_router.include_router(admin_dashboard.router, prefix="/admin", tags=["admin"], dependencies=guard)
|
||||
api_router.include_router(admin_entitlements.router, prefix="/admin", tags=["admin-entitlements"], dependencies=guard)
|
||||
api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"], dependencies=guard)
|
||||
api_router.include_router(admin_knowledge.router, prefix="/admin", tags=["admin"], dependencies=guard)
|
||||
api_router.include_router(admin_knowledge_lifecycle.router, prefix="/admin", tags=["admin-knowledge-lifecycle"], dependencies=guard)
|
||||
api_router.include_router(admin_settings.router, prefix="/admin", tags=["admin"], dependencies=guard)
|
||||
api_router.include_router(admin_sso.router, prefix="/admin", tags=["admin-sso"], dependencies=guard)
|
||||
api_router.include_router(admin_records.router, prefix="/admin", tags=["admin"], dependencies=guard)
|
||||
|
||||
27
ai_knowledge_base_v2/apps/backend/app/api/voice.py
Normal file
27
ai_knowledge_base_v2/apps/backend/app/api/voice.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.core.responses import api_success
|
||||
from app.models.user import User
|
||||
from app.services.voice_input_service import MAX_UPLOAD_BYTES, VoiceInputService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
def voice_config(db: Session = Depends(get_db), _user: User = Depends(get_current_user)) -> dict:
|
||||
return api_success(VoiceInputService.public_config(db))
|
||||
|
||||
|
||||
@router.post("/transcribe")
|
||||
def transcribe(
|
||||
audio: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
content = audio.file.read(MAX_UPLOAD_BYTES + 1)
|
||||
return api_success(VoiceInputService.transcribe(db, user.id, content, audio.content_type))
|
||||
Reference in New Issue
Block a user