Files
wecom_it_smart_desk/backend/app/api/approval_queue.py
Simon ead5f83bee feat(backend): knowledge iteration + vision + neo4j + response contract source
dependencies.py 拆分为 dependencies/ 包; 新增 vision/ragflow_ingestion/neo4j 客户端与 h5_ai_task; alembic 045 图置信度迁移; 响应契约统一收尾。
2026-07-09 11:47:16 +08:00

208 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微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),
}