364e688382
主要改动: backend 业务: - feat(error-codes): 统一错误码表 E1011/E1012 拆码 - E1011 AUTH_PASSWORD_WRONG: 本地密码错误 - E1012 AUTH_FIRST_LOGIN_PASSWORD_REQUIRED: 首次登录请先设置密码 - E1015 AUTH_OLD_PASSWORD_REQUIRED: 改密需要旧密码 - E1016 AUTH_OLD_PASSWORD_WRONG: 旧密码错误 - fix(agents): P0 降级放行时,如坐席已注册但未设密码,正确 raise 1012 (修复前会撞 1011 本地密码错误,与场景不符) - feat(approval): 审批模块 (T审批/A审批) - feat(config): approval_template_resource / approval_template_device 配置 - feat(main): /ready, /metrics, /version 端点(K8s 友好) backend 测试: - test(agents): 新增 test_agents.py — 3 个 Fix-4 降级登录测试 - 错误密码拒绝 - 缺密码拒绝 - 正确密码通过 pytest tests/test_agents.py → 3/3 通过 - test(conftest): 模块级 mock + slowapi 限流重置 + UTF-8 patch 解决 Windows pytest GBK 读 .env 失败 + 降级路径无法测试 仓库治理: - chore(gitignore): 排除 .workbuddy/memory/(workbuddy 本地记忆) - chore(docs): 重命名两份 IT 文档(前缀加智能区分版本) 部署与文档: - docs: RELEASE_NOTES_v0.5.0-beta.md / dashboard.html / 需求-发版预览页面 - docs: 部署、架构、PRD、安全、评审报告等同步 v0.5.0-beta - deploy-server: 打包脚本、nginx、docker-compose 版本号 bump 前端 (frontend-h5 / frontend-agent / frontend-admin / frontend-portal): - index.html / package.json 版本号与构建号 bump 自动验收(RELEASE_NOTES L100-104): - [x] pytest tests/test_agents.py -v → 3 passed - [x] grep Bs7ucT backend frontend-h5 frontend-agent → 无输出 - [x] grep AppException(101[123]) backend → 仅 1 处(登录场景 1012) - [ ] npm run build (frontend-h5 / frontend-agent) → 合并后跑 后续: 合并 feature/t-1-t4-merge → main,tag v0.5.0-beta
173 lines
5.6 KiB
Python
173 lines
5.6 KiB
Python
# =============================================================================
|
|
# IT智能服务台 — 审批流程 API
|
|
# =============================================================================
|
|
# 说明:提供审批模板管理和跳转链接生成
|
|
# - 模板124(资源申请):跳转审批
|
|
# - 模板122(设备申请):API提交
|
|
# =============================================================================
|
|
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Depends, Query
|
|
from pydantic import BaseModel
|
|
|
|
router = APIRouter()
|
|
|
|
# =============================================================================
|
|
# 审批模板配置(可配置化,后续可存入数据库)
|
|
# =============================================================================
|
|
|
|
# =============================================================================
|
|
# 企微审批模板配置(从环境变量读取)
|
|
# =============================================================================
|
|
# 环境变量:
|
|
# APPROVAL_TEMPLATE_RESOURCE - 资源申请模板ID
|
|
# APPROVAL_TEMPLATE_DEVICE - 设备申请模板ID
|
|
|
|
import os
|
|
|
|
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", # API提交
|
|
"keywords": ["申请设备", "要设备", "电脑", "笔记本"],
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Schema 定义
|
|
# =============================================================================
|
|
|
|
|
|
class ApprovalTemplateResponse(BaseModel):
|
|
"""审批模板响应"""
|
|
id: str
|
|
name: str
|
|
type: str # "jump" 或 "api"
|
|
keywords: list[str]
|
|
|
|
|
|
class ApprovalJumpRequest(BaseModel):
|
|
"""跳转审批请求"""
|
|
template_id: str
|
|
employee_id: Optional[str] = None
|
|
|
|
|
|
class ApprovalJumpResponse(BaseModel):
|
|
"""跳转审批响应"""
|
|
url: str
|
|
template_name: str
|
|
|
|
|
|
class ApprovalSubmitRequest(BaseModel):
|
|
"""API提交审批请求"""
|
|
template_id: str
|
|
employee_id: str
|
|
content: dict # 审批内容
|
|
|
|
|
|
class ApprovalSubmitResponse(BaseModel):
|
|
"""API提交审批响应"""
|
|
sp_no: str # 审批单号
|
|
template_name: str
|
|
|
|
|
|
# =============================================================================
|
|
# 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.post("/approval/jump", response_model=ApprovalJumpResponse)
|
|
async def create_approval_jump(request: ApprovalJumpRequest):
|
|
"""生成跳转审批链接(模板124跳转方式)"""
|
|
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(企微审批链接格式)
|
|
# 实际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):
|
|
"""API提交审批(模板122 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提交")
|
|
|
|
# TODO: 调用企微API提交审批
|
|
# 这里需要使用企微access_token调用审批API
|
|
# 实际实现需要根据企微审批API文档
|
|
|
|
return ApprovalSubmitResponse(
|
|
sp_no=f"SP{request.template_id[:8]}", # 模拟审批单号
|
|
template_name=template["name"],
|
|
)
|
|
|
|
|
|
@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
|