# ============================================================================= # IT智能服务台 — 审批流程 API # ============================================================================= # 说明:提供审批模板管理和API提交功能 # - 模板详情获取 # - API提交审批申请 # - 审批状态回调处理 # ============================================================================= import logging import os from typing import Optional import httpx from fastapi import APIRouter, Depends, Query from pydantic import BaseModel import redis.asyncio as aioredis from app.config import settings from app.utils.token_manager import ApprovalTokenManager logger = logging.getLogger(__name__) router = APIRouter() # Redis客户端(依赖注入) async def get_redis() -> aioredis.Redis: """获取Redis客户端依赖""" from app.main import redis_client return redis_client # ============================================================================= # 审批模板配置(从环境变量读取) # ============================================================================= # 环境变量: # 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, "name": "资源申请", "type": "jump", "keywords": ["申请资源", "要资源", "申请"], } if APPROVAL_TEMPLATE_DEVICE: APPROVAL_TEMPLATES[APPROVAL_TEMPLATE_DEVICE] = { "id": APPROVAL_TEMPLATE_DEVICE, "name": "设备申请", "type": "api", "keywords": ["申请设备", "要设备", "电脑", "笔记本"], } # ============================================================================= # Schema 定义 # ============================================================================= class ApprovalTemplateResponse(BaseModel): """审批模板响应""" id: str name: str type: str keywords: list[str] class ApprovalJumpRequest(BaseModel): """跳转审批请求""" template_id: str employee_id: Optional[str] = None class ApprovalJumpResponse(BaseModel): """跳转审批响应""" url: str template_name: str class ApprovalContentItem(BaseModel): """审批表单控件内容""" control: str # 控件类型: Text, Textarea, Number, Money, Date, Selector, Contact, etc. id: str # 控件ID value: dict # 控件值 class ApprovalSubmitRequest(BaseModel): """API提交审批请求""" template_id: str employee_id: str # 申请人userid contents: list[ApprovalContentItem] # 表单内容 use_template_approver: int = 1 # 1-使用模板预设流程 class ApprovalSubmitResponse(BaseModel): """API提交审批响应""" sp_no: str # 审批单号 template_name: str 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")} # ============================================================================= # 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] @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)) @router.post("/approval/jump", response_model=ApprovalJumpResponse) async def create_approval_jump(request: ApprovalJumpRequest): """生成跳转审批链接""" 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) async def submit_approval( request: ApprovalSubmitRequest, redis: aioredis.Redis = Depends(get_redis) ): """API提交审批申请""" 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提交") try: # 1. 获取审批token token = await get_approval_token(redis) # 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"} @router.get("/approval/keywords") async def get_approval_keywords(): """获取所有审批关键词(用于前端关键词检测)""" keywords = [] for template in APPROVAL_TEMPLATES.values(): for kw in template["keywords"]: keywords.append({ "keyword": kw, "template_id": template["id"], "template_name": template["name"], "type": template["type"], }) return keywords