208 lines
6.8 KiB
Python
208 lines
6.8 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微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),
|
|||
|
|
}
|