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

208 lines
6.8 KiB
Python
Raw Normal View History

# =============================================================================
# 企微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),
}