a20606f328
P0 安全修复(连续 2 次巡检标记): 1. 5 端点全部加 auth 依赖 - GET list/detail → Depends(get_current_user)(任何已登录用户) - POST/PUT/DELETE → Depends(require_admin)(仅 admin) 2. 进程内 MOCK_TEMPLATES → PostgreSQL troubleshooting_templates 表 - ORM model 早已注册但缺迁移 → 新建 057_troubleshooting_templates.py - 8 套预设模板在冷启动时通过 seed_default_troubleshooting_templates 插入 - 容器重启不再丢数据 3. 5 源调用方审计通过(与 voice_asr.py P0 修复同款教训): - H5 端 api/troubleshooting-templates.ts:只读 - Admin 端 api/troubleshooting.ts + Flowcharts.vue:5 端 CRUD 全套 - Agent 端 api/troubleshooting.ts:只读 - service_routes.py:仅注册无反向调用 - tests:无相关调用 4. 端到端验证(生产环境实测): - GET no-auth → 403 - GET invalid-token → 401 - GET admin token → 200 + 8 items - POST admin (sxn) → 201 + 新建 UUID - POST 非 admin → 拒绝(业务 code 1002) - PUT/DELETE admin → 200 + 字段更新/删除 - 容器重启后 → 8 套数据仍在(DB 持久化生效) 文件改动: - alembic/versions/057_troubleshooting_templates.py(新建, 79 行) - app/api/troubleshooting_templates.py(重写 720→859 行) - app/main.py(1071→1082, +11 行 seed 调用) 相关 P1 跟进(已同步滴答清单): - 053-056 迁移脱节(生产 alembic_version=052) - 缺 2 索引 idx_tpl_category / idx_tpl_active Refs: voice_asr.py P0 修复(5 来源审计教训)
860 lines
31 KiB
Python
860 lines
31 KiB
Python
# =============================================================================
|
||
# 企微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 校验后的字段到 ORM(id 由 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} 已删除")
|