perf: paginate admin audit lists

This commit is contained in:
2026-07-13 13:09:15 +08:00
parent 0864f9305b
commit 00d53c2569
14 changed files with 300 additions and 80 deletions

View File

@@ -21,6 +21,7 @@ from app.schemas.knowledge import AttentionUpdateRequest
from app.services.admin_service import OperationLogService
from app.models.logs import LogRetentionPolicy
from app.services.maintenance_service import MaintenanceService
from app.api.pagination import page_result
router = APIRouter()
@@ -48,8 +49,9 @@ def retrieval_logs(
query = query.where(KnowledgeRetrievalLog.status == statusValue)
if attentionCreated is not None:
query = query.where(KnowledgeRetrievalLog.attention_created == (1 if attentionCreated else 0))
query = query.offset((page - 1) * pageSize).limit(pageSize)
return api_success([_retrieval_dict(item, detail=False) for item in db.scalars(query).all()])
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
rows = db.scalars(query.offset((page - 1) * pageSize).limit(pageSize)).all()
return api_success(page_result([_retrieval_dict(item, detail=False) for item in rows], total=total, page=page, page_size=pageSize))
@router.post("/retrieval-log/cleanup/estimate")
@@ -133,15 +135,19 @@ def retrieval_detail(
def attention_list(
priority: str = Query(default=""),
statusValue: str = Query(default="", alias="status"),
page: int = Query(default=1, ge=1),
pageSize: int = Query(default=20, ge=10, le=100),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
query = select(HumanAttentionRecord).order_by(HumanAttentionRecord.id.desc()).limit(300)
query = select(HumanAttentionRecord)
if priority:
query = query.where(HumanAttentionRecord.priority == priority)
if statusValue:
query = query.where(HumanAttentionRecord.status == statusValue)
return api_success([_attention_dict(item) for item in db.scalars(query).all()])
total = db.scalar(select(func.count()).select_from(query.subquery())) or 0
rows = db.scalars(query.order_by(HumanAttentionRecord.id.desc()).offset((page - 1) * pageSize).limit(pageSize)).all()
return api_success(page_result([_attention_dict(item) for item in rows], total=total, page=page, page_size=pageSize))
@router.put("/attention/{attention_id}")

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
@@ -26,13 +26,42 @@ from app.schemas.admin import KnowledgeSaveRequest
from app.services.admin_service import OperationLogService
from app.services.feishu_service import FeishuKnowledgeService
from app.services.knowledge_catalog_cache_service import KnowledgeCatalogCacheService
from app.api.pagination import page_result
router = APIRouter()
@router.get("/knowledge/list")
def list_knowledge(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
items = db.scalars(select(Knowledge).order_by(Knowledge.id.desc())).all()
def list_knowledge(
keyword: str = Query(default=""),
knowledgeType: str = Query(default=""),
openStatus: int | None = Query(default=None),
sourceStatus: str = Query(default=""),
pending: bool | None = Query(default=None),
page: int = Query(default=1, ge=1),
pageSize: int = Query(default=20, ge=10, le=100),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
query = select(Knowledge)
if keyword:
query = query.where((Knowledge.name.like(f"%{keyword}%")) | (Knowledge.source_title.like(f"%{keyword}%")))
if knowledgeType:
query = query.where(Knowledge.knowledge_type == knowledgeType)
if openStatus is not None:
query = query.where(Knowledge.status == openStatus)
if sourceStatus:
query = query.where(Knowledge.source_status == sourceStatus)
if pending is not None:
query = query.where(Knowledge.pending_version_id.is_not(None) if pending else Knowledge.pending_version_id.is_(None))
total = db.scalar(select(func.count()).select_from(query.subquery())) or 0
items = db.scalars(query.order_by(Knowledge.id.desc()).offset((page - 1) * pageSize).limit(pageSize)).all()
return api_success(page_result([_knowledge_dict(item) for item in items], total=total, page=page, page_size=pageSize))
@router.get("/knowledge/options")
def knowledge_options(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
items = db.scalars(select(Knowledge).where(Knowledge.lifecycle_status == "active").order_by(Knowledge.id.desc())).all()
return api_success([_knowledge_dict(item) for item in items])

View File

@@ -367,11 +367,11 @@ def migration_summary(
db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)
) -> dict:
_require_super_admin(current_admin)
rows = db.scalars(select(Knowledge).order_by(Knowledge.id)).all()
counts: dict[str, int] = {}
for item in rows:
counts[item.migration_status] = counts.get(item.migration_status, 0) + 1
return api_success({"total": len(rows), "counts": counts, "items": [_knowledge_detail(item) for item in rows]})
rows = db.execute(
select(Knowledge.migration_status, func.count(Knowledge.id)).group_by(Knowledge.migration_status)
).all()
counts = {migration_status: count for migration_status, count in rows}
return api_success({"total": sum(counts.values()), "counts": counts})
@router.post("/knowledge-migration/run")

View File

@@ -8,8 +8,8 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import Response
from sqlalchemy import exists, or_, select
from sqlalchemy.orm import Session
from sqlalchemy import exists, func, or_, select
from sqlalchemy.orm import Session, load_only
from app.core.database import get_db
from app.core.dependencies import get_current_admin
@@ -18,6 +18,7 @@ from app.models.admin import Admin
from app.models.chat import ChatMessage, ChatSession
from app.models.logs import AiRequestLog, OperationLog
from app.models.user import User
from app.api.pagination import page_result
router = APIRouter()
@@ -29,15 +30,15 @@ def chat_list(
status: str = Query(default=""),
dateFrom: datetime | None = Query(default=None),
dateTo: datetime | None = Query(default=None),
page: int = Query(default=1, ge=1),
pageSize: int = Query(default=20, ge=10, le=100),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
rows = db.execute(
_chat_query(keyword=keyword, user_id=userId, status=status, date_from=dateFrom, date_to=dateTo)
.order_by(ChatSession.updated_at.desc())
.limit(200)
).all()
return api_success([_chat_row_dict(session, user) for session, user in rows])
query = _chat_query(keyword=keyword, user_id=userId, status=status, date_from=dateFrom, date_to=dateTo)
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
rows = db.execute(query.order_by(ChatSession.updated_at.desc()).offset((page - 1) * pageSize).limit(pageSize)).all()
return api_success(page_result([_chat_row_dict(session, user) for session, user in rows], total=total, page=page, page_size=pageSize))
@router.get("/chat/export")
@@ -118,14 +119,20 @@ def chat_detail(
@router.get("/log/list")
def operation_logs(
module: str = Query(default=""),
page: int = Query(default=1, ge=1),
pageSize: int = Query(default=20, ge=10, le=100),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
query = select(OperationLog).order_by(OperationLog.created_at.desc()).limit(200)
query = select(OperationLog)
if module:
query = query.where(OperationLog.module == module)
logs = db.scalars(query).all()
return api_success(
total = db.scalar(select(func.count()).select_from(query.subquery())) or 0
logs = db.scalars(
query.options(load_only(OperationLog.id, OperationLog.admin_id, OperationLog.module, OperationLog.action, OperationLog.target_id, OperationLog.result, OperationLog.created_at))
.order_by(OperationLog.created_at.desc()).offset((page - 1) * pageSize).limit(pageSize)
).all()
return api_success(page_result(
[
{
"id": item.id,
@@ -137,8 +144,8 @@ def operation_logs(
"createdAt": item.created_at,
}
for item in logs
]
)
], total=total, page=page, page_size=pageSize
))
@router.get("/ai-log/list")
@@ -146,18 +153,28 @@ def ai_logs(
sessionId: int | None = Query(default=None),
userId: int | None = Query(default=None),
status: str = Query(default=""),
page: int = Query(default=1, ge=1),
pageSize: int = Query(default=20, ge=10, le=100),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
query = select(AiRequestLog).order_by(AiRequestLog.created_at.desc()).limit(200)
query = select(AiRequestLog)
if sessionId is not None:
query = query.where(AiRequestLog.session_id == sessionId)
if userId is not None:
query = query.where(AiRequestLog.user_id == userId)
if status:
query = query.where(AiRequestLog.status == status)
logs = db.scalars(query).all()
return api_success([_ai_log_dict(item) for item in logs])
total = db.scalar(select(func.count()).select_from(query.subquery())) or 0
logs = db.scalars(
query.options(load_only(
AiRequestLog.id, AiRequestLog.session_id, AiRequestLog.message_id, AiRequestLog.user_id,
AiRequestLog.model_name, AiRequestLog.knowledge_ids, AiRequestLog.retrieve_count,
AiRequestLog.input_token, AiRequestLog.output_token, AiRequestLog.total_token,
AiRequestLog.cost_ms, AiRequestLog.status, AiRequestLog.error_message, AiRequestLog.created_at,
)).order_by(AiRequestLog.created_at.desc()).offset((page - 1) * pageSize).limit(pageSize)
).all()
return api_success(page_result([_ai_log_dict(item, include_chunks=False) for item in logs], total=total, page=page, page_size=pageSize))
@router.get("/ai-log/{log_id}")
@@ -241,7 +258,7 @@ def _message_dict(message: ChatMessage) -> dict:
}
def _ai_log_dict(log: AiRequestLog, *, include_prompt: bool = False) -> dict:
def _ai_log_dict(log: AiRequestLog, *, include_prompt: bool = False, include_chunks: bool = True) -> dict:
data = {
"id": log.id,
"sessionId": log.session_id,
@@ -250,7 +267,7 @@ def _ai_log_dict(log: AiRequestLog, *, include_prompt: bool = False) -> dict:
"modelName": log.model_name,
"knowledgeIds": log.knowledge_ids,
"retrieveCount": log.retrieve_count,
"retrievedChunks": _parse_retrieved_chunks(log.retrieved_chunks),
"retrievedChunks": _parse_retrieved_chunks(log.retrieved_chunks) if include_chunks else [],
"inputToken": log.input_token,
"outputToken": log.output_token,
"totalToken": log.total_token,

View File

@@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile,
from fastapi.responses import StreamingResponse
from openpyxl import Workbook, load_workbook
from pydantic import ValidationError
from sqlalchemy import select
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.core.database import get_db
@@ -19,6 +19,7 @@ from app.models.ai_config import SystemConfig
from app.models.user import User
from app.schemas.admin import AdminUserCreateRequest, AdminUserImportItem, AdminUserImportRequest, AdminUserUpdateRequest
from app.services.admin_service import OperationLogService
from app.api.pagination import page_result
router = APIRouter()
@@ -28,6 +29,8 @@ STUDENT_TEMPLATE_HEADERS = ["手机号", "姓名", "昵称", "每日聊天额度
@router.get("/user/list")
def list_users(
keyword: str = Query(default=""),
page: int = Query(default=1, ge=1),
pageSize: int = Query(default=20, ge=10, le=100),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
@@ -35,8 +38,9 @@ def list_users(
if keyword:
like = f"%{keyword}%"
query = query.where((User.phone.like(like)) | (User.name.like(like)))
users = db.scalars(query.limit(200)).all()
return api_success([_user_dict(user) for user in users])
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
users = db.scalars(query.offset((page - 1) * pageSize).limit(pageSize)).all()
return api_success(page_result([_user_dict(user) for user in users], total=total, page=page, page_size=pageSize))
@router.post("/user")

View File

@@ -0,0 +1,7 @@
from __future__ import annotations
from typing import Any
def page_result(items: list[Any], *, total: int, page: int, page_size: int) -> dict[str, Any]:
return {"items": items, "total": total, "page": page, "pageSize": page_size}

View File

@@ -0,0 +1,65 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from app.api.admin_agent_records import attention_list, retrieval_logs
from app.api.admin_records import ai_logs
from app.api.admin_users import list_users
from app.models import Base
from app.models.knowledge import HumanAttentionRecord, KnowledgeRetrievalLog
from app.models.logs import AiRequestLog
from app.models.user import User
def _database() -> Session:
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
return Session(engine)
def test_user_list_uses_database_pagination():
with _database() as db:
db.add_all([
User(id=i + 1, phone=f"1380000{i:04d}", name=f"学员{i}", daily_chat_limit=10)
for i in range(25)
])
db.commit()
response = list_users(keyword="", page=2, pageSize=10, db=db, current_admin=object())
assert response["data"]["total"] == 25
assert response["data"]["page"] == 2
assert len(response["data"]["items"]) == 10
def test_ai_log_page_does_not_load_large_detail_fields():
with _database() as db:
db.add_all([
AiRequestLog(status="success", prompt="p" * 5000, retrieved_chunks='[{"content":"large"}]')
for _ in range(23)
])
db.commit()
response = ai_logs(sessionId=None, userId=None, status="", page=2, pageSize=10, db=db, current_admin=object())
assert response["data"]["total"] == 23
assert len(response["data"]["items"]) == 10
assert all(item["retrievedChunks"] == [] for item in response["data"]["items"])
def test_retrieval_and_attention_lists_are_paginated():
with _database() as db:
for index in range(21):
db.add(KnowledgeRetrievalLog(
question=f"q{index}", catalog_snapshot="[]", catalog_hash=str(index),
safety_rule_version="v1", status="success",
))
db.add(HumanAttentionRecord(
session_id=index + 1, message_id=index + 1, user_id=index + 1,
trigger_message="test", problem_summary="test", trigger_reason="test",
priority="normal", status="pending",
))
db.commit()
retrieval = retrieval_logs(
sessionId=None, knowledgeId=None, knowledgeCalled=None, statusValue="", attentionCreated=None,
page=3, pageSize=10, db=db, current_admin=object(),
)["data"]
attention = attention_list(priority="", statusValue="", page=3, pageSize=10, db=db, current_admin=object())["data"]
assert retrieval["total"] == attention["total"] == 21
assert len(retrieval["items"]) == len(attention["items"]) == 1