# ============================================================================= # 企微IT智能服务台 — 独立审批队列 API(Tier1 新增) # ============================================================================= # 说明:独立审批队列接口,管理超出会话上下文的待审批提案。 # 1. GET /queued — 获取队列中的提案列表 # 2. GET /queued/stats — 获取队列统计 # 3. POST /queued/{id}/dequeue-approve — 队列中审批通过提案 # # D7 硬约束: # - 提案默认 status=pending,不自动 applied # - 未处理的进入独立队列(queued) # - 72 小时超时 → expired # ============================================================================= import logging from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import get_current_user, require_admin, UserInfo from app.models.knowledge_suggestion import KnowledgeSuggestion from app.schemas.knowledge_suggestion import KnowledgeSuggestionResponse from app.schemas.enums import SuggestionStatusEnum from app.services.knowledge_iteration_service import ( KnowledgeIterationService, dep_knowledge_iteration_service, ) from app.services.neo4j_client import get_neo4j_client logger = logging.getLogger(__name__) router = APIRouter() # ----------------------------------------------------------------------------- # 获取独立队列列表(Tier1 新增) # ----------------------------------------------------------------------------- # GET /api/admin/approval-queue/queued @router.get("/queued") @require_admin async def list_queued_suggestions( status: Optional[str] = Query( default=None, description="筛选状态:pending/queued(不传则返回 pending+queued)", ), audience: Optional[str] = Query( default=None, description="筛选受众:employee_quick_reply/engineer_workguide", ), page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=20, ge=1, le=100, description="每页数量"), current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取独立队列中的提案列表。 默认返回 status=pending 和 status=queued 的提案。 支持按 audience 筛选和分页。 - **status**: 筛选状态(pending/queued) - **audience**: 按受众类型筛选 - **page**: 页码 - **page_size**: 每页数量 **需要管理员权限。** """ # 构建查询:pending 或 queued 状态的提案 target_statuses = [status] if status else [ SuggestionStatusEnum.pending.value, SuggestionStatusEnum.queued.value, ] stmt = ( select(KnowledgeSuggestion) .where(KnowledgeSuggestion.status.in_(target_statuses)) .order_by( # 按入队时间降序(queued 的提案在前),然后按创建时间 KnowledgeSuggestion.queued_at.desc().nullslast(), KnowledgeSuggestion.created_at.desc(), ) ) if audience: stmt = stmt.where(KnowledgeSuggestion.audience == audience) # 分页 offset = (page - 1) * page_size stmt = stmt.offset(offset).limit(page_size) result = await db.execute(stmt) suggestions = result.scalars().all() # 统计总数 count_stmt = ( select(func.count()) .select_from(KnowledgeSuggestion) .where(KnowledgeSuggestion.status.in_(target_statuses)) ) if audience: count_stmt = count_stmt.where(KnowledgeSuggestion.audience == audience) total_result = await db.execute(count_stmt) total = total_result.scalar() return { "code": 0, "message": "success", "data": { "total": total, "items": [ KnowledgeSuggestionResponse.model_validate(s) for s in suggestions ], }, } # ----------------------------------------------------------------------------- # 获取队列统计(Tier1 新增) # ----------------------------------------------------------------------------- # GET /api/admin/approval-queue/queued/stats @router.get("/queued/stats") @require_admin async def get_queue_stats( current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """获取独立审批队列统计信息。 返回: - queued_total: 队列中提案数 - pending_total: 待审核提案数 - by_audience: 按受众分组统计 - by_source_type: 按来源分组统计 **需要管理员权限。** """ stats = await service.get_queue_stats(db) # 补充按来源分组统计 source_stats_stmt = ( select( KnowledgeSuggestion.source_type, func.count(), ) .where( KnowledgeSuggestion.status.in_([ SuggestionStatusEnum.pending.value, SuggestionStatusEnum.queued.value, ]) ) .group_by(KnowledgeSuggestion.source_type) ) source_result = await db.execute(source_stats_stmt) by_source_type = {row[0]: row[1] for row in source_result.fetchall()} stats["by_source_type"] = by_source_type return { "code": 0, "message": "success", "data": stats, } # ----------------------------------------------------------------------------- # 队列中审批通过(Tier1 新增) # ----------------------------------------------------------------------------- # POST /api/admin/approval-queue/queued/{id}/dequeue-approve @router.post("/queued/{suggestion_id}/dequeue-approve") @require_admin async def dequeue_approve( suggestion_id: str, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """从独立队列中审批通过提案。 流程:queued → approved → applied → graph_synced(同 approve_suggestion)。 - **suggestion_id**: 建议ID **需要管理员权限。** """ logger.info( f"管理员 {current_user.name} 从独立队列审批通过: {suggestion_id}" ) neo4j_client = await get_neo4j_client() suggestion = await service.dequeue_approve( db, suggestion_id, current_user.employee_id, neo4j_client=neo4j_client, ) if not suggestion: return {"code": 404, "message": "建议不存在或状态转换无效", "data": None} return { "code": 0, "message": "队列审批通过,已应用到知识库并同步至知识图谱", "data": KnowledgeSuggestionResponse.model_validate(suggestion), }