Files
wecom_it_smart_desk/backend/app/api/quiz_admin.py
T

262 lines
8.5 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — 测验题目管理 API(管理员)
# =============================================================================
# 说明:管理后台的测验题目审批 API,统一 /api/admin/quiz 前缀。
# 包含 4 个端点:
# 1. POST /generate — 手动触发 Dify 生成题目
# 2. GET /pending — 查看待审核题目列表(分页)
# 3. POST /{id}/approve — 审批通过题目(is_active → True
# 4. DELETE /{id} — 删除质量差的题目
#
# 权限:所有端点需要管理员权限(Depends(require_admin)
# =============================================================================
import logging
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.agents import get_current_agent
from app.database import get_db
from app.models.agent import Agent
from app.models.quiz import QuizQuestion
from app.services.quiz_generation_service import get_quiz_generation_service
from app.utils.response import AppException, success_response
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/quiz", tags=["测验题目管理"])
# ==========================================================================
# 权限校验依赖(复用 admin_api.py 的模式)
# ==========================================================================
async def require_admin(
agent: Agent = Depends(get_current_agent),
) -> Agent:
"""管理员权限校验:仅 role='admin' 可访问。"""
if agent.role != "admin":
raise AppException(1004, "无管理权限")
return agent
# ==========================================================================
# 请求体定义
# ==========================================================================
class GenerateRequest(BaseModel):
"""手动触发生成题目请求。"""
category: str # network/vpn/email/system/printer/security/office
question_type: str = "knowledge" # knowledge / diagnostic
count: int = 5
problem_category: Optional[str] = None # 仅 question_type=diagnostic 时使用
# ==========================================================================
# 1. POST /api/admin/quiz/generate — 手动触发 AI 生成题目
# ==========================================================================
@router.post("/generate")
async def generate_quiz_questions(
body: GenerateRequest,
admin: Agent = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""手动触发 AI 生成题目。
生成的题目 is_active=False,需通过 /approve 端点审批后激活。
Args:
body: 生成请求(category, question_type, count, problem_category
admin: 管理员(权限校验)
db: 数据库会话
Returns:
生成结果摘要(成功/失败数量 + 题目列表)
"""
service = get_quiz_generation_service()
if body.question_type == "knowledge":
result = await service.generate_knowledge_questions_batch(
db=db,
category=body.category,
count=body.count,
is_active=False, # 手动生成也需审批
)
elif body.question_type == "diagnostic":
if not body.problem_category:
raise AppException(1004, "diagnostic 类型必须提供 problem_category")
result = await service.generate_diagnostic_questions_batch(
db=db,
problem_category=body.problem_category,
count=body.count,
is_active=False,
)
else:
raise AppException(1004, f"不支持的题目类型: {body.question_type}")
await db.commit()
logger.info(
f"管理员 {admin.name} 手动生成题目: "
f"type={body.question_type}, category={body.category}, "
f"成功={result['success_count']}"
)
return success_response(data=result)
# ==========================================================================
# 2. GET /api/admin/quiz/pending — 查看待审核题目列表
# ==========================================================================
@router.get("/pending")
async def list_pending_questions(
category: Optional[str] = Query(None, description="按类别筛选"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
admin: Agent = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""获取待审核题目列表(is_active=False)。
Args:
category: 可选,按类别筛选
page: 页码(从 1 开始)
page_size: 每页数量(1-100
admin: 管理员(权限校验)
db: 数据库会话
Returns:
分页列表 {total, page, page_size, items}
"""
# 构建查询条件
conditions = [QuizQuestion.is_active == False] # noqa: E712
if category:
conditions.append(QuizQuestion.category == category)
# 总数
total = await db.scalar(
select(func.count(QuizQuestion.id)).where(*conditions)
)
total = total or 0
# 分页查询
offset = (page - 1) * page_size
stmt = (
select(QuizQuestion)
.where(*conditions)
.order_by(QuizQuestion.created_at.desc())
.offset(offset)
.limit(page_size)
)
result = await db.execute(stmt)
questions = result.scalars().all()
items = [_question_to_dict(q) for q in questions]
return success_response(data={
"total": total,
"page": page,
"page_size": page_size,
"items": items,
})
# ==========================================================================
# 3. POST /api/admin/quiz/{question_id}/approve — 审批通过题目
# ==========================================================================
@router.post("/{question_id}/approve")
async def approve_question(
question_id: str,
admin: Agent = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""审批通过一道待审核题目(is_active: False → True)。
Args:
question_id: 题目ID
admin: 管理员(权限校验)
db: 数据库会话
Returns:
更新后的题目信息
"""
result = await db.execute(
select(QuizQuestion).where(QuizQuestion.id == question_id)
)
question = result.scalar_one_or_none()
if not question:
raise AppException(1004, "题目不存在")
if question.is_active:
raise AppException(1004, "题目已激活,无需重复审批")
question.is_active = True
await db.commit()
logger.info(f"管理员 {admin.name} 审批通过题目: {question_id}")
return success_response(data=_question_to_dict(question))
# ==========================================================================
# 4. DELETE /api/admin/quiz/{question_id} — 删除题目
# ==========================================================================
@router.delete("/{question_id}")
async def delete_question(
question_id: str,
admin: Agent = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
"""删除一道题目(用于清理质量差的 AI 生成题)。
Args:
question_id: 题目ID
admin: 管理员(权限校验)
db: 数据库会话
Returns:
删除确认
"""
result = await db.execute(
select(QuizQuestion).where(QuizQuestion.id == question_id)
)
question = result.scalar_one_or_none()
if not question:
raise AppException(1004, "题目不存在")
await db.delete(question)
await db.commit()
logger.info(f"管理员 {admin.name} 删除题目: {question_id}")
return success_response(data={"deleted_id": question_id})
# ==========================================================================
# 辅助函数
# ==========================================================================
def _question_to_dict(q: QuizQuestion) -> Dict[str, Any]:
"""将 QuizQuestion 对象转为字典。"""
return {
"id": q.id,
"type": q.type,
"category": q.category,
"problem_category": q.problem_category,
"difficulty": q.difficulty,
"question": q.question,
"options": q.options,
"correct_index": q.correct_index,
"explanation": q.explanation,
"is_active": q.is_active,
"created_at": q.created_at.isoformat() if q.created_at else None,
}