Files
wecom_it_smart_desk/src/backend/app/api/troubleshooting_templates.py
T

860 lines
31 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — 排查模板 API(v6.0 P0 重构版)
# =============================================================================
# 说明:提供排查模板的 CRUD 接口
# 接口列表:
# GET /api/troubleshooting-templates — 获取排查模板列表(已登录用户)
# GET /api/troubleshooting-templates/{id} — 获取排查模板详情(已登录用户)
# POST /api/troubleshooting-templates — 新增模板(仅管理员)
# PUT /api/troubleshooting-templates/{id} — 修改模板(仅管理员)
# DELETE /api/troubleshooting-templates/{id} — 删除模板(仅管理员)
#
# v6.0 P0 修复(2026-08-03):
# - 5 个端点全部加 auth 依赖(GET 走 get_current_user,写走 require_admin
# - 进程内 MOCK_TEMPLATES → PostgreSQL 持久化(troubleshooting_templates 表)
# - 容器重启不再丢数据
# - 冷启动 seed 8 套预设模板(幂等)
# =============================================================================
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.agents import get_current_agent
from app.database import get_db
from app.dependencies import get_current_user, UserInfo
from app.models.agent import Agent
from app.models.troubleshooting_template import TroubleshootingTemplate
from app.schemas.troubleshooting_template import (
TroubleshootingTemplateCreate,
TroubleshootingTemplateUpdate,
TroubleshootingTemplateResponse,
)
from app.utils.response import AppException, success_response
logger = logging.getLogger(__name__)
# 创建路由器
router = APIRouter(prefix="/troubleshooting-templates", tags=["排查模板"])
# --------------------------------------------------------------------------
# 管理员权限校验依赖(v6.0 P0 修复 - 加 auth 必备)
# --------------------------------------------------------------------------
async def require_admin(
agent: Agent = Depends(get_current_agent),
) -> Agent:
"""排查模板管理权限校验:仅 role='admin' 可访问。
镜像 admin_api.py:50 的同款依赖(避免误改项目级 401 行为)。
非管理员 → AppException(1004, "无管理权限")。
Args:
agent: 当前坐席(通过认证依赖注入)
Returns:
Agent: 具有管理权限的坐席对象
"""
if agent.role != "admin":
raise AppException(1004, "无管理权限")
return agent
# ==========================================================================
# 1. 冷启动 seed — 8 套预设模板(v6.0 P0 重构)
# ==========================================================================
# 保留原因:
# - 老 MOCK_TEMPLATES 在进程内是"8 套模板"的事实标准
# - 改为 DB 持久化后,需要"首次启动时插入 8 条预设"的能力
# - 幂等:只在表为空时插入(避免重复)
# ==========================================================================
def _build_vpn_flowchart() -> Dict[str, Any]:
"""构建 VPN 故障排查流程图。"""
return {
"id": "fc-vpn-1",
"type": "step",
"label": "确认VPN客户端版本",
"status": "done",
"children": [
{
"id": "fc-vpn-2",
"type": "decision",
"label": "版本是否为最新?",
"status": "pending",
"yes_branch": {
"id": "fc-vpn-3",
"type": "step",
"label": "清除DNS缓存并重连",
"status": "current",
"children": [
{
"id": "fc-vpn-4",
"type": "decision",
"label": "重连是否成功?",
"status": "pending",
"yes_branch": {
"id": "fc-vpn-5",
"type": "step",
"label": "回访确认",
"status": "pending",
},
"no_branch": {
"id": "fc-vpn-6",
"type": "step",
"label": "发起远程协助",
"status": "pending",
"children": [
{
"id": "fc-vpn-7",
"type": "decision",
"label": "远程能否解决?",
"status": "pending",
"yes_branch": {
"id": "fc-vpn-8",
"type": "step",
"label": "回访确认并结单",
"status": "pending",
},
"no_branch": {
"id": "fc-vpn-9",
"type": "step",
"label": "升级至二线团队",
"status": "pending",
},
},
],
},
},
],
},
"no_branch": {
"id": "fc-vpn-10",
"type": "step",
"label": "升级VPN客户端到最新版",
"status": "pending",
"children": [
{
"id": "fc-vpn-11",
"type": "step",
"label": "重试连接",
"status": "pending",
},
],
},
},
],
}
def _build_email_flowchart() -> Dict[str, Any]:
"""构建邮箱故障排查流程图。"""
return {
"id": "fc-email-1",
"type": "step",
"label": "确认邮箱账号状态",
"status": "done",
"children": [
{
"id": "fc-email-2",
"type": "decision",
"label": "账号是否被锁定?",
"status": "pending",
"yes_branch": {
"id": "fc-email-3",
"type": "step",
"label": "解锁账号并重置密码",
"status": "current",
},
"no_branch": {
"id": "fc-email-4",
"type": "step",
"label": "检查Outlook配置",
"status": "pending",
"children": [
{
"id": "fc-email-5",
"type": "decision",
"label": "配置是否正确?",
"status": "pending",
"yes_branch": {
"id": "fc-email-6",
"type": "step",
"label": "清理Outlook缓存",
"status": "pending",
},
"no_branch": {
"id": "fc-email-7",
"type": "step",
"label": "重新配置Outlook",
"status": "pending",
},
},
],
},
},
],
}
def _build_system_flowchart() -> Dict[str, Any]:
"""构建系统登录异常排查流程图。"""
return {
"id": "fc-sys-1",
"type": "step",
"label": "确认系统服务是否正常",
"status": "current",
"children": [
{
"id": "fc-sys-2",
"type": "decision",
"label": "系统服务是否正常?",
"status": "pending",
"yes_branch": {
"id": "fc-sys-3",
"type": "step",
"label": "清除浏览器缓存",
"status": "pending",
"children": [
{
"id": "fc-sys-4",
"type": "decision",
"label": "清除后是否恢复?",
"status": "pending",
"yes_branch": {
"id": "fc-sys-5",
"type": "step",
"label": "回访确认并结单",
"status": "pending",
},
"no_branch": {
"id": "fc-sys-6",
"type": "step",
"label": "更换浏览器重试",
"status": "pending",
},
},
],
},
"no_branch": {
"id": "fc-sys-7",
"type": "step",
"label": "联系运维检查服务端",
"status": "pending",
},
},
],
}
def _build_account_flowchart() -> Dict[str, Any]:
"""构建账号权限问题排查流程图。"""
return {
"id": "fc-acc-1",
"type": "step",
"label": "确认权限需求与合规性",
"status": "current",
"children": [
{
"id": "fc-acc-2",
"type": "decision",
"label": "权限是否符合策略?",
"status": "pending",
"yes_branch": {
"id": "fc-acc-3",
"type": "step",
"label": "提交权限审批流程",
"status": "pending",
"children": [
{
"id": "fc-acc-4",
"type": "step",
"label": "审批通过后配置权限",
"status": "pending",
},
],
},
"no_branch": {
"id": "fc-acc-5",
"type": "step",
"label": "建议替代方案或申请特批",
"status": "pending",
},
},
],
}
def _build_network_flowchart() -> Dict[str, Any]:
"""构建网络连接问题排查流程图。"""
return {
"id": "fc-net-1",
"type": "step",
"label": "确认网络连接状态",
"status": "current",
"children": [
{
"id": "fc-net-2",
"type": "decision",
"label": "能否ping通网关?",
"status": "pending",
"yes_branch": {
"id": "fc-net-3",
"type": "step",
"label": "检查DNS解析",
"status": "pending",
"children": [
{
"id": "fc-net-4",
"type": "decision",
"label": "DNS是否正常?",
"status": "pending",
"yes_branch": {
"id": "fc-net-5",
"type": "step",
"label": "检查防火墙规则",
"status": "pending",
},
"no_branch": {
"id": "fc-net-6",
"type": "step",
"label": "手动配置DNS服务器",
"status": "pending",
},
},
],
},
"no_branch": {
"id": "fc-net-7",
"type": "step",
"label": "检查网线和交换机端口",
"status": "pending",
},
},
],
}
def _build_printer_flowchart() -> Dict[str, Any]:
"""构建打印机故障排查流程图。"""
return {
"id": "fc-prt-1",
"type": "step",
"label": "确认打印机连接状态",
"status": "current",
"children": [
{
"id": "fc-prt-2",
"type": "decision",
"label": "打印机是否在线?",
"status": "pending",
"yes_branch": {
"id": "fc-prt-3",
"type": "step",
"label": "清除打印队列并重启打印服务",
"status": "pending",
"children": [
{
"id": "fc-prt-4",
"type": "decision",
"label": "打印是否恢复?",
"status": "pending",
"yes_branch": {
"id": "fc-prt-5",
"type": "step",
"label": "回访确认",
"status": "pending",
},
"no_branch": {
"id": "fc-prt-6",
"type": "step",
"label": "重新安装打印机驱动",
"status": "pending",
},
},
],
},
"no_branch": {
"id": "fc-prt-7",
"type": "step",
"label": "检查网络连接和打印机电源",
"status": "pending",
},
},
],
}
def _build_office_flowchart() -> Dict[str, Any]:
"""构建 Office 软件问题排查流程图。"""
return {
"id": "fc-off-1",
"type": "step",
"label": "确认Office版本和激活状态",
"status": "current",
"children": [
{
"id": "fc-off-2",
"type": "decision",
"label": "Office是否正常激活?",
"status": "pending",
"yes_branch": {
"id": "fc-off-3",
"type": "step",
"label": "修复Office安装",
"status": "pending",
"children": [
{
"id": "fc-off-4",
"type": "decision",
"label": "修复后是否正常?",
"status": "pending",
"yes_branch": {
"id": "fc-off-5",
"type": "step",
"label": "回访确认",
"status": "pending",
},
"no_branch": {
"id": "fc-off-6",
"type": "step",
"label": "卸载重装Office",
"status": "pending",
},
},
],
},
"no_branch": {
"id": "fc-off-7",
"type": "step",
"label": "重新激活Office许可证",
"status": "pending",
},
},
],
}
def _build_password_flowchart() -> Dict[str, Any]:
"""构建密码重置问题排查流程图。"""
return {
"id": "fc-pwd-1",
"type": "step",
"label": "确认账号状态和锁定原因",
"status": "current",
"children": [
{
"id": "fc-pwd-2",
"type": "decision",
"label": "账号是否被锁定?",
"status": "pending",
"yes_branch": {
"id": "fc-pwd-3",
"type": "step",
"label": "解锁账号并引导自助重置",
"status": "pending",
"children": [
{
"id": "fc-pwd-4",
"type": "decision",
"label": "自助重置是否成功?",
"status": "pending",
"yes_branch": {
"id": "fc-pwd-5",
"type": "step",
"label": "回访确认",
"status": "pending",
},
"no_branch": {
"id": "fc-pwd-6",
"type": "step",
"label": "管理员手动重置密码",
"status": "pending",
},
},
],
},
"no_branch": {
"id": "fc-pwd-7",
"type": "step",
"label": "检查SSO单点登录配置",
"status": "pending",
},
},
],
}
# 8 套预设模板的 seed payload
# 与原 MOCK_TEMPLATES 1:1 对应(保留 id 以兼容历史日志/外键引用)
# 注:原 MOCK 时间是 "2025-06-01T08:00:00Z" 等历史日期,seed 沿用
SEED_TEMPLATES: List[Dict[str, Any]] = [
{
"id": "tpl-vpn-001",
"name": "VPN连接故障",
"category": "vpn",
"path_steps": [
{"label": "确认VPN版本", "status": "done"},
{"label": "清除缓存重连", "status": "current"},
{"label": "远程排查", "status": "pending"},
{"label": "升级客户端", "status": "pending"},
{"label": "回访确认", "status": "pending"},
],
"flowchart": _build_vpn_flowchart(),
"is_active": True,
},
{
"id": "tpl-email-001",
"name": "邮箱登录故障",
"category": "email",
"path_steps": [
{"label": "确认邮箱状态", "status": "done"},
{"label": "重置密码", "status": "current"},
{"label": "检查配置", "status": "pending"},
{"label": "清理缓存", "status": "pending"},
{"label": "回访确认", "status": "pending"},
],
"flowchart": _build_email_flowchart(),
"is_active": True,
},
{
"id": "tpl-system-001",
"name": "系统登录异常",
"category": "system",
"path_steps": [
{"label": "确认系统状态", "status": "current"},
{"label": "清除浏览器缓存", "status": "pending"},
{"label": "更换浏览器", "status": "pending"},
{"label": "检查网络权限", "status": "pending"},
{"label": "回访确认", "status": "pending"},
],
"flowchart": _build_system_flowchart(),
"is_active": True,
},
{
"id": "tpl-account-001",
"name": "账号权限问题",
"category": "account",
"path_steps": [
{"label": "确认权限需求", "status": "current"},
{"label": "提交审批", "status": "pending"},
{"label": "配置权限", "status": "pending"},
{"label": "验证权限", "status": "pending"},
{"label": "回访确认", "status": "pending"},
],
"flowchart": _build_account_flowchart(),
"is_active": True,
},
{
"id": "tpl-network-001",
"name": "网络连接问题",
"category": "system",
"path_steps": [
{"label": "确认网络状态", "status": "current"},
{"label": "检查DNS配置", "status": "pending"},
{"label": "检查防火墙", "status": "pending"},
{"label": "更换网口/网线", "status": "pending"},
{"label": "回访确认", "status": "pending"},
],
"flowchart": _build_network_flowchart(),
"is_active": True,
},
{
"id": "tpl-printer-001",
"name": "打印机故障",
"category": "system",
"path_steps": [
{"label": "确认打印机状态", "status": "current"},
{"label": "清除打印队列", "status": "pending"},
{"label": "重新安装驱动", "status": "pending"},
{"label": "检查网络连接", "status": "pending"},
{"label": "回访确认", "status": "pending"},
],
"flowchart": _build_printer_flowchart(),
"is_active": True,
},
{
"id": "tpl-office-001",
"name": "Office软件问题",
"category": "system",
"path_steps": [
{"label": "确认Office版本", "status": "current"},
{"label": "修复安装", "status": "pending"},
{"label": "重新激活", "status": "pending"},
{"label": "卸载重装", "status": "pending"},
{"label": "回访确认", "status": "pending"},
],
"flowchart": _build_office_flowchart(),
"is_active": True,
},
{
"id": "tpl-password-001",
"name": "密码重置问题",
"category": "account",
"path_steps": [
{"label": "确认账号状态", "status": "current"},
{"label": "解锁账号", "status": "pending"},
{"label": "引导自助重置", "status": "pending"},
{"label": "管理员重置", "status": "pending"},
{"label": "回访确认", "status": "pending"},
],
"flowchart": _build_password_flowchart(),
"is_active": True,
},
]
async def seed_default_troubleshooting_templates(db: AsyncSession) -> int:
"""冷启动 seed 8 套预设模板(幂等:表非空则跳过)。
被 main.py 的 `_init_default_data()` 调用,确保:
- 新部署的服务器启动后立即有 8 套可用模板
- 已运行实例不会重复 seed(id 冲突会失败但我们用 limit(1) 守卫)
- 与原 MOCK_TEMPLATES 行为兼容:H5 列表、Admin 流程图管理 都能直接看到 8 套
Args:
db: 数据库会话(FastAPI 依赖注入传入)
Returns:
int: 本次实际插入的条数(0 表示已存在跳过)
"""
# 幂等检查:表非空则跳过
existing = (await db.execute(
select(TroubleshootingTemplate).limit(1)
)).scalar_one_or_none()
if existing:
logger.info("troubleshooting_templates 表已有数据,跳过 seed")
return 0
# 用 model 的 default 时间戳,而不是 payload 里的历史日期 — 时间戳代表"何时入 DB"
now = datetime.now()
for item in SEED_TEMPLATES:
template = TroubleshootingTemplate(
id=item["id"],
name=item["name"],
category=item["category"],
path_steps=item["path_steps"],
flowchart=item["flowchart"],
is_active=item["is_active"],
created_at=now,
updated_at=now,
)
db.add(template)
await db.flush() # 触发 INSERT,但未 commit(由 main.py 统一提交)
logger.info(f"troubleshooting_templates seed 完成:插入 {len(SEED_TEMPLATES)}")
return len(SEED_TEMPLATES)
# ==========================================================================
# 2. 5 个 API 端点(v6.0 P0 重构)
# ==========================================================================
# 权限矩阵:
# GET list/detail → Depends(get_current_user) 任何已登录用户
# POST/PUT/DELETE → Depends(require_admin) 仅管理员
#
# 数据源:
# 全部走 PostgreSQL troubleshooting_templates 表
# 容器重启数据不丢(替代原进程内 MOCK_TEMPLATES
# ==========================================================================
@router.get("")
async def list_troubleshooting_templates(
category: Optional[str] = Query(None, description="按分类过滤(vpn/email/system/account"),
db: AsyncSession = Depends(get_db),
current_user: UserInfo = Depends(get_current_user),
):
"""获取排查模板列表(已登录用户)。
支持按分类过滤;只返回启用的模板(is_active=True),与原行为兼容。
Args:
category: 分类过滤(可选)
db: 数据库会话
current_user: 当前已登录用户(依赖注入,401 if no token
Returns:
Dict: 统一响应格式,data.items + data.total
"""
# 构建查询
stmt = select(TroubleshootingTemplate).where(
TroubleshootingTemplate.is_active == True
)
if category:
stmt = stmt.where(TroubleshootingTemplate.category == category)
# 按 id 排序保证结果稳定(前端列表渲染)
stmt = stmt.order_by(TroubleshootingTemplate.id)
result = await db.execute(stmt)
items = result.scalars().all()
# 序列化(用 schema 的 from_attributes=True 直接转换 ORM 对象)
return success_response(data={
"items": [TroubleshootingTemplateResponse.model_validate(item).model_dump(mode="json") for item in items],
"total": len(items),
})
@router.get("/{template_id}")
async def get_troubleshooting_template(
template_id: str,
db: AsyncSession = Depends(get_db),
current_user: UserInfo = Depends(get_current_user),
):
"""获取排查模板详情(已登录用户)。
Args:
template_id: 模板唯一标识
db: 数据库会话
current_user: 当前已登录用户(依赖注入)
Returns:
Dict: 统一响应格式,data 为模板对象
Raises:
AppException(1003): 模板不存在
"""
stmt = select(TroubleshootingTemplate).where(
TroubleshootingTemplate.id == template_id
)
result = await db.execute(stmt)
template = result.scalar_one_or_none()
if not template:
raise AppException(code=1003, message=f"排查模板 {template_id} 不存在")
return success_response(
data=TroubleshootingTemplateResponse.model_validate(template).model_dump(mode="json")
)
@router.post("", status_code=201)
async def create_troubleshooting_template(
request: TroubleshootingTemplateCreate,
db: AsyncSession = Depends(get_db),
admin: Agent = Depends(require_admin),
):
"""新增排查模板(仅管理员)。
Args:
request: 创建请求体(name/category/path_steps/flowchart/is_active
db: 数据库会话
admin: 管理员(依赖注入,403 if role != admin
Returns:
Dict: 统一响应格式,data 为新建的模板对象
"""
# 透传 schema 校验后的字段到 ORMid 由 model default uuid4 自动生成)
template = TroubleshootingTemplate(
name=request.name,
category=request.category,
path_steps=request.path_steps,
flowchart=request.flowchart,
is_active=request.is_active,
)
db.add(template)
await db.flush() # 获取 id
await db.refresh(template) # 加载 DB 默认值(created_at/updated_at)
logger.info(
f"管理员 {admin.user_id} 新建排查模板 id={template.id} name={template.name}"
)
return success_response(
data=TroubleshootingTemplateResponse.model_validate(template).model_dump(mode="json")
)
@router.put("/{template_id}")
async def update_troubleshooting_template(
template_id: str,
request: TroubleshootingTemplateUpdate,
db: AsyncSession = Depends(get_db),
admin: Agent = Depends(require_admin),
):
"""修改排查模板(仅管理员)。
支持 PATCH 语义:只更新请求体里非 None 的字段。
Args:
template_id: 模板唯一标识
request: 更新请求体(所有字段可选)
db: 数据库会话
admin: 管理员(依赖注入)
Returns:
Dict: 统一响应格式,data 为更新后的模板对象
Raises:
AppException(1003): 模板不存在
"""
stmt = select(TroubleshootingTemplate).where(
TroubleshootingTemplate.id == template_id
)
result = await db.execute(stmt)
template = result.scalar_one_or_none()
if not template:
raise AppException(code=1003, message=f"排查模板 {template_id} 不存在")
# PATCH 语义:只更新显式传入的字段
if request.name is not None:
template.name = request.name
if request.category is not None:
template.category = request.category
if request.path_steps is not None:
template.path_steps = request.path_steps
if request.flowchart is not None:
template.flowchart = request.flowchart
if request.is_active is not None:
template.is_active = request.is_active
# updated_at 由 model onupdate=datetime.now 自动处理,但需要 flush 触发
await db.flush()
await db.refresh(template)
logger.info(f"管理员 {admin.user_id} 更新排查模板 id={template_id}")
return success_response(
data=TroubleshootingTemplateResponse.model_validate(template).model_dump(mode="json")
)
@router.delete("/{template_id}")
async def delete_troubleshooting_template(
template_id: str,
db: AsyncSession = Depends(get_db),
admin: Agent = Depends(require_admin),
):
"""删除排查模板(仅管理员,硬删除)。
Args:
template_id: 模板唯一标识
db: 数据库会话
admin: 管理员(依赖注入)
Returns:
Dict: 统一响应格式,data=None, message=已删除
Raises:
AppException(1003): 模板不存在
"""
stmt = select(TroubleshootingTemplate).where(
TroubleshootingTemplate.id == template_id
)
result = await db.execute(stmt)
template = result.scalar_one_or_none()
if not template:
raise AppException(code=1003, message=f"排查模板 {template_id} 不存在")
await db.delete(template)
await db.flush()
logger.info(f"管理员 {admin.user_id} 删除排查模板 id={template_id}")
return success_response(data=None, message=f"排查模板 {template_id} 已删除")