2026-06-15 09:32:41 +08:00
|
|
|
# =============================================================================
|
|
|
|
|
# IT智能服务台 — 审批流程 API
|
|
|
|
|
# =============================================================================
|
2026-07-05 17:03:36 +08:00
|
|
|
# 说明:提供审批模板管理和API提交功能
|
|
|
|
|
# - 模板详情获取
|
|
|
|
|
# - API提交审批申请
|
|
|
|
|
# - 审批状态回调处理
|
2026-06-15 09:32:41 +08:00
|
|
|
# =============================================================================
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
import logging
|
|
|
|
|
import os
|
2026-06-15 09:32:41 +08:00
|
|
|
from typing import Optional
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
|
|
|
import httpx
|
2026-06-15 09:32:41 +08:00
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
|
|
|
from pydantic import BaseModel
|
2026-07-05 17:03:36 +08:00
|
|
|
import redis.asyncio as aioredis
|
2026-06-15 09:32:41 +08:00
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
from app.config import settings
|
|
|
|
|
from app.utils.token_manager import ApprovalTokenManager
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2026-06-15 09:32:41 +08:00
|
|
|
router = APIRouter()
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
# Redis客户端(依赖注入)
|
|
|
|
|
async def get_redis() -> aioredis.Redis:
|
|
|
|
|
"""获取Redis客户端依赖"""
|
|
|
|
|
from app.main import redis_client
|
|
|
|
|
return redis_client
|
|
|
|
|
|
2026-06-15 09:32:41 +08:00
|
|
|
|
2026-06-15 14:14:58 +08:00
|
|
|
# =============================================================================
|
2026-07-05 17:03:36 +08:00
|
|
|
# 审批模板配置(从环境变量读取)
|
2026-06-15 14:14:58 +08:00
|
|
|
# =============================================================================
|
|
|
|
|
# 环境变量:
|
|
|
|
|
# APPROVAL_TEMPLATE_RESOURCE - 资源申请模板ID
|
|
|
|
|
# APPROVAL_TEMPLATE_DEVICE - 设备申请模板ID
|
|
|
|
|
|
|
|
|
|
APPROVAL_TEMPLATE_RESOURCE = os.getenv("APPROVAL_TEMPLATE_RESOURCE", "")
|
|
|
|
|
APPROVAL_TEMPLATE_DEVICE = os.getenv("APPROVAL_TEMPLATE_DEVICE", "")
|
|
|
|
|
|
|
|
|
|
# 动态构建审批模板配置
|
|
|
|
|
APPROVAL_TEMPLATES = {}
|
|
|
|
|
|
|
|
|
|
if APPROVAL_TEMPLATE_RESOURCE:
|
|
|
|
|
APPROVAL_TEMPLATES[APPROVAL_TEMPLATE_RESOURCE] = {
|
|
|
|
|
"id": APPROVAL_TEMPLATE_RESOURCE,
|
2026-06-15 09:32:41 +08:00
|
|
|
"name": "资源申请",
|
2026-07-05 17:03:36 +08:00
|
|
|
"type": "jump",
|
2026-06-15 09:32:41 +08:00
|
|
|
"keywords": ["申请资源", "要资源", "申请"],
|
2026-06-15 14:14:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if APPROVAL_TEMPLATE_DEVICE:
|
|
|
|
|
APPROVAL_TEMPLATES[APPROVAL_TEMPLATE_DEVICE] = {
|
|
|
|
|
"id": APPROVAL_TEMPLATE_DEVICE,
|
2026-06-15 09:32:41 +08:00
|
|
|
"name": "设备申请",
|
2026-07-05 17:03:36 +08:00
|
|
|
"type": "api",
|
2026-06-15 09:32:41 +08:00
|
|
|
"keywords": ["申请设备", "要设备", "电脑", "笔记本"],
|
2026-06-15 14:14:58 +08:00
|
|
|
}
|
2026-06-15 09:32:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Schema 定义
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
class ApprovalTemplateResponse(BaseModel):
|
|
|
|
|
"""审批模板响应"""
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
2026-07-05 17:03:36 +08:00
|
|
|
type: str
|
2026-06-15 09:32:41 +08:00
|
|
|
keywords: list[str]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApprovalJumpRequest(BaseModel):
|
|
|
|
|
"""跳转审批请求"""
|
|
|
|
|
template_id: str
|
|
|
|
|
employee_id: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApprovalJumpResponse(BaseModel):
|
|
|
|
|
"""跳转审批响应"""
|
|
|
|
|
url: str
|
|
|
|
|
template_name: str
|
|
|
|
|
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
class ApprovalContentItem(BaseModel):
|
|
|
|
|
"""审批表单控件内容"""
|
|
|
|
|
control: str # 控件类型: Text, Textarea, Number, Money, Date, Selector, Contact, etc.
|
|
|
|
|
id: str # 控件ID
|
|
|
|
|
value: dict # 控件值
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 09:32:41 +08:00
|
|
|
class ApprovalSubmitRequest(BaseModel):
|
|
|
|
|
"""API提交审批请求"""
|
|
|
|
|
template_id: str
|
2026-07-05 17:03:36 +08:00
|
|
|
employee_id: str # 申请人userid
|
|
|
|
|
contents: list[ApprovalContentItem] # 表单内容
|
|
|
|
|
use_template_approver: int = 1 # 1-使用模板预设流程
|
2026-06-15 09:32:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApprovalSubmitResponse(BaseModel):
|
|
|
|
|
"""API提交审批响应"""
|
|
|
|
|
sp_no: str # 审批单号
|
|
|
|
|
template_name: str
|
|
|
|
|
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
class ApprovalCallbackRequest(BaseModel):
|
|
|
|
|
"""审批回调请求(XML解析后的模型)"""
|
|
|
|
|
sp_no: str
|
|
|
|
|
sp_name: str
|
|
|
|
|
template_id: str
|
|
|
|
|
apply_time: int
|
|
|
|
|
applyer_userid: str
|
|
|
|
|
sp_status: int # 1-审批中 2-已通过 3-已驳回 4-已撤销 6-通过后撤销 7-已删除 10-已支付
|
|
|
|
|
status_change_event: int # 1-提单 2-同意 3-驳回 4-转审 5-催办 6-撤销 8-通过后撤销 10-添加备注
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# 企微API调用辅助函数
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
async def get_approval_token(redis: aioredis.Redis) -> str:
|
|
|
|
|
"""获取审批应用access_token"""
|
|
|
|
|
manager = ApprovalTokenManager(redis)
|
|
|
|
|
return await manager.get_token()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_template_detail(access_token: str, template_id: str) -> dict:
|
|
|
|
|
"""获取审批模板详情
|
|
|
|
|
|
|
|
|
|
对应企微API:
|
|
|
|
|
POST https://qyapi.weixin.qq.com/cgi-bin/oa/gettemplatedetail
|
|
|
|
|
|
|
|
|
|
返回模板内的控件构成及控件ID
|
|
|
|
|
"""
|
|
|
|
|
url = "https://qyapi.weixin.qq.com/cgi-bin/oa/gettemplatedetail"
|
|
|
|
|
params = {"access_token": access_token}
|
|
|
|
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(connect=10.0, read=30.0)) as client:
|
|
|
|
|
response = await client.post(url, params=params, json={"template_id": template_id})
|
|
|
|
|
result = response.json()
|
|
|
|
|
|
|
|
|
|
if result.get("errcode") != 0:
|
|
|
|
|
logger.error(f"获取模板详情失败: {result.get('errmsg')}")
|
|
|
|
|
raise Exception(f"获取模板详情失败: {result.get('errmsg')}")
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def submit_approval_api(
|
|
|
|
|
access_token: str,
|
|
|
|
|
template_id: str,
|
|
|
|
|
creator_userid: str,
|
|
|
|
|
contents: list[dict],
|
|
|
|
|
use_template_approver: int = 1
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""提交审批申请
|
|
|
|
|
|
|
|
|
|
对应企微API:
|
|
|
|
|
POST https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
access_token: 审批应用access_token
|
|
|
|
|
template_id: 模板ID
|
|
|
|
|
creator_userid: 申请人userid
|
|
|
|
|
contents: 表单控件内容列表
|
|
|
|
|
use_template_approver: 1-使用模板预设流程
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
{"sp_no": "审批单号"}
|
|
|
|
|
"""
|
|
|
|
|
url = "https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent"
|
|
|
|
|
params = {"access_token": access_token}
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"creator_userid": creator_userid,
|
|
|
|
|
"template_id": template_id,
|
|
|
|
|
"use_template_approver": use_template_approver,
|
|
|
|
|
"apply_data": {
|
|
|
|
|
"contents": contents
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(connect=10.0, read=30.0)) as client:
|
|
|
|
|
response = await client.post(url, params=params, json=payload)
|
|
|
|
|
result = response.json()
|
|
|
|
|
|
|
|
|
|
if result.get("errcode") != 0:
|
|
|
|
|
logger.error(f"提交审批失败: {result.get('errmsg')}")
|
|
|
|
|
raise Exception(f"提交审批失败: {result.get('errmsg')}")
|
|
|
|
|
|
|
|
|
|
return {"sp_no": result.get("sp_no")}
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 09:32:41 +08:00
|
|
|
# =============================================================================
|
|
|
|
|
# API 端点
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
@router.get("/approval/templates", response_model=list[ApprovalTemplateResponse])
|
|
|
|
|
async def get_approval_templates():
|
|
|
|
|
"""获取所有审批模板列表"""
|
|
|
|
|
return list(APPROVAL_TEMPLATES.values())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/approval/templates/{template_id}", response_model=ApprovalTemplateResponse)
|
|
|
|
|
async def get_approval_template(template_id: str):
|
|
|
|
|
"""获取指定审批模板详情"""
|
|
|
|
|
if template_id not in APPROVAL_TEMPLATES:
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
|
|
|
return APPROVAL_TEMPLATES[template_id]
|
|
|
|
|
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
@router.get("/approval/templates/{template_id}/detail")
|
|
|
|
|
async def get_template_full_detail(
|
|
|
|
|
template_id: str,
|
|
|
|
|
redis: aioredis.Redis = Depends(get_redis)
|
|
|
|
|
):
|
|
|
|
|
"""获取审批模板完整详情(控件结构)"""
|
|
|
|
|
if template_id not in APPROVAL_TEMPLATES:
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
token = await get_approval_token(redis)
|
|
|
|
|
detail = await get_template_detail(token, template_id)
|
|
|
|
|
return detail
|
|
|
|
|
except Exception as e:
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-06-15 09:32:41 +08:00
|
|
|
@router.post("/approval/jump", response_model=ApprovalJumpResponse)
|
|
|
|
|
async def create_approval_jump(request: ApprovalJumpRequest):
|
2026-07-05 17:03:36 +08:00
|
|
|
"""生成跳转审批链接"""
|
2026-06-15 09:32:41 +08:00
|
|
|
template = APPROVAL_TEMPLATES.get(request.template_id)
|
|
|
|
|
if not template:
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
|
|
|
|
|
|
|
|
if template["type"] != "jump":
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
raise HTTPException(status_code=400, detail="该模板不支持跳转方式")
|
|
|
|
|
|
|
|
|
|
# 生成跳转URL(企微审批链接格式)
|
|
|
|
|
jump_url = f"https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=TOKEN&template_id={request.template_id}"
|
|
|
|
|
|
|
|
|
|
return ApprovalJumpResponse(
|
|
|
|
|
url=jump_url,
|
|
|
|
|
template_name=template["name"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/approval/submit", response_model=ApprovalSubmitResponse)
|
2026-07-05 17:03:36 +08:00
|
|
|
async def submit_approval(
|
|
|
|
|
request: ApprovalSubmitRequest,
|
|
|
|
|
redis: aioredis.Redis = Depends(get_redis)
|
|
|
|
|
):
|
|
|
|
|
"""API提交审批申请"""
|
2026-06-15 09:32:41 +08:00
|
|
|
template = APPROVAL_TEMPLATES.get(request.template_id)
|
|
|
|
|
if not template:
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
|
|
|
|
|
|
|
|
if template["type"] != "api":
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
raise HTTPException(status_code=400, detail="该模板不支持API提交")
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
try:
|
|
|
|
|
# 1. 获取审批token
|
|
|
|
|
token = await get_approval_token(redis)
|
2026-06-15 09:32:41 +08:00
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
# 2. 转换contents格式
|
|
|
|
|
contents = [item.model_dump() for item in request.contents]
|
|
|
|
|
|
|
|
|
|
# 3. 提交审批
|
|
|
|
|
result = await submit_approval_api(
|
|
|
|
|
access_token=token,
|
|
|
|
|
template_id=request.template_id,
|
|
|
|
|
creator_userid=request.employee_id,
|
|
|
|
|
contents=contents,
|
|
|
|
|
use_template_approver=request.use_template_approver
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return ApprovalSubmitResponse(
|
|
|
|
|
sp_no=result["sp_no"],
|
|
|
|
|
template_name=template["name"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/approval/callback")
|
|
|
|
|
async def approval_callback(
|
|
|
|
|
sp_no: str = Query(...),
|
|
|
|
|
sp_name: str = Query(...),
|
|
|
|
|
template_id: str = Query(...),
|
|
|
|
|
apply_time: int = Query(...),
|
|
|
|
|
applyer_userid: str = Query(...),
|
|
|
|
|
sp_status: int = Query(...),
|
|
|
|
|
status_change_event: int = Query(...)
|
|
|
|
|
):
|
|
|
|
|
"""审批状态变化回调处理
|
|
|
|
|
|
|
|
|
|
对应企微审批回调事件: sys_approval_change
|
|
|
|
|
|
|
|
|
|
状态变化类型 (status_change_event):
|
|
|
|
|
1 - 提单
|
|
|
|
|
2 - 同意
|
|
|
|
|
3 - 驳回
|
|
|
|
|
4 - 转审
|
|
|
|
|
5 - 催办
|
|
|
|
|
6 - 撤销
|
|
|
|
|
8 - 通过后撤销
|
|
|
|
|
10 - 添加备注
|
|
|
|
|
|
|
|
|
|
审批单状态 (sp_status):
|
|
|
|
|
1 - 审批中
|
|
|
|
|
2 - 已通过
|
|
|
|
|
3 - 已驳回
|
|
|
|
|
4 - 已撤销
|
|
|
|
|
6 - 通过后撤销
|
|
|
|
|
7 - 已删除
|
|
|
|
|
10 - 已支付
|
|
|
|
|
"""
|
|
|
|
|
logger.info(f"审批回调: sp_no={sp_no}, status={sp_status}, event={status_change_event}")
|
|
|
|
|
|
|
|
|
|
# TODO: 根据业务需求处理审批状态变化
|
|
|
|
|
# 例如:
|
|
|
|
|
# - 审批通过后,更新IT服务台待办状态
|
|
|
|
|
# - 审批驳回后,通知申请人
|
|
|
|
|
# - 审批撤销后,关闭相关工单
|
|
|
|
|
|
|
|
|
|
event_map = {
|
|
|
|
|
1: "submitted",
|
|
|
|
|
2: "approved",
|
|
|
|
|
3: "rejected",
|
|
|
|
|
4: "transferred",
|
|
|
|
|
5: "reminded",
|
|
|
|
|
6: "revoked",
|
|
|
|
|
8: "revoked_after_approved",
|
|
|
|
|
10: "commented"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event_type = event_map.get(status_change_event, f"unknown_{status_change_event}")
|
|
|
|
|
logger.info(f"审批事件类型: {event_type}")
|
|
|
|
|
|
|
|
|
|
return {"errcode": 0, "errmsg": "ok"}
|
2026-06-15 09:32:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/approval/keywords")
|
|
|
|
|
async def get_approval_keywords():
|
|
|
|
|
"""获取所有审批关键词(用于前端关键词检测)"""
|
|
|
|
|
keywords = []
|
|
|
|
|
for template in APPROVAL_TEMPLATES.values():
|
|
|
|
|
for kw in template["keywords"]:
|
2026-07-05 17:03:36 +08:00
|
|
|
keywords.append({
|
|
|
|
|
"keyword": kw,
|
|
|
|
|
"template_id": template["id"],
|
|
|
|
|
"template_name": template["name"],
|
|
|
|
|
"type": template["type"],
|
|
|
|
|
})
|
2026-06-15 09:32:41 +08:00
|
|
|
return keywords
|