Django+Vue3构建运维管理系统:从零到部署实战
干运维的兄弟应该都体会过这种痛服务器一多信息全散在 Excel 和聊天记录里谁改过密码没人说得清线上出问题要顺着 SSH 记录一条条翻。我这次做的就是一个用 Django Vue3 从零搭起来的前后端分离运维管理系统把主机资产、账号口令、操作日志这些日常最头疼的东西统一收口。这个项目踩了不少坑也沉淀了一些比较稳的写法今天就把它完整拆开从项目设计、后端 API、前端鉴权到最后的部署上线一条线讲透适合刚学完 Django 和 Vue3 基础、想做一个真正能落地项目的人参考。1. 项目整体设计与技术选型1.1 为什么选 Django Vue3 这对组合先说后端。运维系统本质上是管理内部资源的工具核心诉求是快速开发、稳定维护对并发要求没那么夸张。Django 在这方面有天然优势ORM 省去一大堆 SQL 手写工作Admin 后台可以直接当临时管理界面用自带的认证体系和权限框架非常成熟开发效率在同类框架里算是第一梯队。我前期调研的时候也考虑过 Spring Boot但团队本身是 Python 技术栈而且 Django 生态里有现成的认证、序列化、ORM 组件从零到 MVP 的速度确实快不少。前端选 Vue3 除了技术栈延续性还有一个很实际的原因Vue3 的 Composition API 在处理复杂业务逻辑时非常舒服。运维系统里有大量的表单校验、状态切换、数据联动场景用组合式函数可以把这些逻辑全部抽离出来复用代码不会像 Options API 那样越写越臃肿。配合 Vite 的开发体验极好热更新几乎是秒级的这在频繁调整表格和表单界面的时候特别加分。前后端分离这个架构本身也有明确考量。运维系统要对接的终端用户往往分散在不同网络环境前端静态资源可以独立部署到 CDN 或者内网 Nginx后端只暴露 API 接口两者的发布节奏互不干扰。改个前端样式不需要重启后端服务新增一个接口也不用重新构建前端交付和迭代都灵活很多。1.2 运维系统的核心模块到底拆成什么很多新手拿到这种项目就急着写代码结果把主机信息和用户信息堆在一张表里后面越改越乱。我一开始就把系统拆成了五个核心模块边界尽量清晰用户与权限负责登录认证、角色权限分配后端用户不是运维对象而是系统的操作者和资产管理完全分开。主机资产记录服务器的基础信息包括 IP、操作系统、CPU 内存规格、机房位置、责任人。账号凭证管理服务器上的登录账号和密码和主机资产是多对一关系做加密存储。操作记录记录谁在什么时间对哪台主机执行了什么命令也就是审计日志。命令通道通过 WebSSH 或后端执行脚本对目标主机下发命令回收执行结果。第一版我没有直接做很复杂的编排调度功能而是先把资产和审计这两块地基打牢。原因很简单资产管理是运维工作的数据底座操作审计是安全合规的底线要求这两个模块做好之后后续再怎么扩展监控、发布、工单系统都有的放矢。ops_backend/ ├── apps/ │ ├── users/ # 用户与认证 │ ├── assets/ # 主机资产 │ ├── credentials/ # 账号凭证 │ └── audit/ # 操作审计 ├── utils/ # 通用工具 ├── config/ # 项目配置 └── manage.py这个目录结构是我后来重构的。Django 官方默认把 app 平铺在根目录但系统模块一多settings.py、urls.py全堆在顶层会非常乱。把 app 收拢进apps/包再用config目录放配置文件项目规模上来之后结构依然清晰。2. 后端核心功能实现2.1 开发环境与踩坑记录创建虚拟环境这步千万别省我见过直接把依赖装进系统 Python 然后版本冲突把环境搞崩的。我的习惯是用 virtualenv 创建独立的运行环境Python 版本统一用 3.10Django 选择长期维护的 4.2 LTS 版本这一版兼容性和稳定性都经过了充分验证。依赖安装的核心包清单pip install django4.2.* pip install djangorestframework pip install django-cors-headers pip install pymysql pip install paramiko pip install pyjwt pip install cryptography这里有几个点提醒大家注意。第一使用 PyMySQL 连接 MySQL 的话需要在项目__init__.py里加上pymysql.install_as_MySQLdb()否则 Django 找不到 MySQLdb 模块。第二cryptography是paramiko的依赖包如果漏装SSH 连接的时候会报未知的加密算法错误。第三Python 3.10 以下版本对cryptography的安装可能有点麻烦最好直接上 3.10。2.2 数据模型怎么设计才合理数据模型是整个系统的地基后面所有业务逻辑都建立在这几张表上。我第一版的设计比较朴素后来根据真实使用场景迭代过两次最终结构大概是这样# apps/assets/models.py from django.db import models class Host(models.Model): STATUS_CHOICES ( (online, 在线), (offline, 离线), (maintain, 维护中), ) hostname models.CharField(主机名, max_length128, uniqueTrue) ip_address models.GenericIPAddressField(IP地址, uniqueTrue) os_type models.CharField(操作系统, max_length64) cpu_cores models.IntegerField(CPU核数, default0) memory_size models.IntegerField(内存大小(MB), default0) disk_size models.IntegerField(磁盘大小(GB), default0) status models.CharField(状态, max_length16, choicesSTATUS_CHOICES, defaultonline) owner models.CharField(责任人, max_length64, blankTrue) remark models.TextField(备注, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: db_table hosts ordering [-created_at] def __str__(self): return f{self.hostname} ({self.ip_address})主机和账号凭证是一对多的关系一台服务器可能有 root、ops、deploy 等多个账号class Credential(models.Model): host models.ForeignKey(Host, on_deletemodels.CASCADE, related_namecredentials) username models.CharField(用户名, max_length64) password models.CharField(密码, max_length256) port models.IntegerField(SSH端口, default22) is_sudo models.BooleanField(是否具有sudo权限, defaultFalse) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: db_table credentials unique_together (host, username)密码存储这一点必须认真对待。很多练手项目直接把明文密码存进数据库这在真实系统里绝对不允许。我这边用的是cryptography的 Fernet 对称加密密钥单独放在环境变量里数据库里只存密文。读取的时候在内存中解密日志里绝对不能打印密码原文。2.3 JWT 认证体系的落地运维系统对内使用我没走 Django 自带的 Session 认证而是直接上了 JWT。前后端分离场景下JWT 天然无状态后端不需要维护会话记录前端把 token 存在 localStorage每次请求塞进 Header 就行扩展起来也方便。# apps/users/utils.py import jwt from datetime import datetime, timedelta from django.conf import settings def generate_token(user): payload { user_id: user.id, username: user.username, exp: datetime.utcnow() timedelta(hours12), iat: datetime.utcnow(), } token jwt.encode(payload, settings.SECRET_KEY, algorithmHS256) return token def verify_token(token): try: payload jwt.decode(token, settings.SECRET_KEY, algorithms[HS256]) return payload except jwt.ExpiredSignatureError: return None except jwt.InvalidTokenError: return NoneJWT 的使用有个细节token 一旦签发在过期之前无法主动作废。运维系统里如果担心员工离职后 token 还能用一段时间可以引入一个 Redis 黑名单机制也可以把 token 有效期缩短到几小时配合前端定时刷新。我目前的做法是 12 小时过期内网系统可接受。登录接口拿用户输入的用户名密码去 Django 的authenticate校验通过后签发 token同时把用户基本信息返回给前端前端再根据返回的权限码渲染菜单。2.4 命令执行通道的坑命令执行是运维系统最核心也最危险的功能。后端要连上远程主机跑命令我把执行通道封装成了一个独立服务模块便于后续扩展和维护# apps/audit/services.py import paramiko def exec_remote_command(host, port, username, password, command, timeout10): client paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: client.connect( hostnamehost, portport, usernameusername, passwordpassword, timeouttimeout, banner_timeouttimeout ) stdin, stdout, stderr client.exec_command(command, timeouttimeout) output stdout.read().decode(utf-8, errorsreplace) error stderr.read().decode(utf-8, errorsreplace) code stdout.channel.recv_exit_status() return code, output, error finally: client.close()这里踩过一个大坑exec_command默认只返回标准输出但如果命令执行失败或输出到 stderr界面上只显示空白。必须主动读 stderr而且要通过recv_exit_status()拿真正的退出码否则你根本不知道命令到底成没成。另外一个坑是stdout.channel.recv_exit_status()必须放在read()之后调用否则会阻塞或者拿不到状态码。命令通道的鉴权逻辑也很关键不是所有登录用户都能在任意机器上执行命令。我在后端做了两层校验先确认用户有执行权限再确认命令不在黑名单里比如禁止rm -rf /这类危险命令。操作记录同时落库存下用户、目标主机、完整命令和执行结果这就是前文说的审计能力。3. 前端工程构建全程3.1 从 Vite 脚手架开始的 Vue3 项目前端这边我直接用 Vite 官方脚手架生成相比 Vue CLI 来说启动速度快、配置简洁而且 Vue3 官方生态已经全面转向 Vitenpm create vitelatest ops_web -- --template vue cd ops_web npm install npm install vue-router4 pinia axios element-plusElement Plus 是 Vue3 生态里最成熟的中后台组件库拿来就用表格表单都齐活不用自己手搓 UI节约大量时间。我在项目里基本上把 Element Plus 的 Table、Form、Dialog、Message 这几个高频组件玩明白了整个前端界面就没再写多少原生样式。工程层面的关键目录src/ ├── api/ # 按模块封装的请求方法 ├── assets/ ├── components/ # 通用组件 ├── layout/ # 布局框架 ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── utils/ # 通用工具函数 └── views/ # 页面级组件3.2 axios 封装与 token 处理的细节前后端分离项目里token 处理是最容易出问题的环节。axios 如果不封装每个页面请求都要手动带 token、手动处理 401一旦接口多了起来就是灾难。我的统一封装方式// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response) { const status error.response.status if (status 401) { localStorage.removeItem(token) router.push(/login) ElMessage.error(登录已过期请重新登录) } else if (status 403) { ElMessage.error(没有操作权限) } else if (status 500) { ElMessage.error(服务器开小差了请稍后重试) } else { ElMessage.error(error.response.data.detail || 请求失败) } } else { ElMessage.error(网络异常请检查后端服务是否可用) } return Promise.reject(error) } ) export default request拦截器解决了几个问题。统一在请求前读取 localStorage不需要每个 API 调用手动设置 Header。后端返回 401 时自动清理本地存储并跳转登录页避免用户停留在已失效的页面上继续做无用操作。接口报错时统一弹提示前端业务代码里不需要到处写 try-catch 的 Message 逻辑。这里补充一个容易踩的坑刷新页面后 localStorage 里的 token 还在但 Pinia 里的用户信息会被清空。所以应用初始化的时候要从 token 解析出用户基本信息或者调用一次/api/users/me拉取最新用户资料否则页面上的用户名会变成空的。3.3 路由权限控制与页面守卫前端路由不能只做跳转还要配合权限做动态拦截。我的路由配置分为两部分公开路由登录页和受保护路由主页、资产管理、任务执行等。路由守卫的逻辑是这样// src/router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token) const isLoginPage to.path /login if (!token !isLoginPage) { next(/login) return } if (token isLoginPage) { next(/) return } next() })如果系统里要区分不同角色的菜单和权限可以再引入动态路由。后端登录接口返回用户的权限码数组前端在路由守卫里根据权限码动态addRoute注册用户有权限访问的路由没有权限的直接渲染 404。我这个项目的权限粒度没有做到按钮级主要是菜单和路由层面对内部运维系统来说已经够用。3.4 前端页面的核心逻辑写法视图层我按照模块拆分核心页面包括登录页、主机资产列表、主机详情、凭证管理、操作审计列表。以主机列表页为例关键是分页查询和状态展示script setup import { ref, onMounted } from vue import { getHostList } from ../api/assets const loading ref(false) const hostList ref([]) const total ref(0) const queryParams ref({ page: 1, pageSize: 10, keyword: }) const loadHosts async () { loading.value true try { const res await getHostList(queryParams.value) hostList.value res.results total.value res.count } finally { loading.value false } } onMounted(loadHosts) /scriptscript setup语法极大地简化了代码组织ref 声明响应式变量onMounted 做初始化加载整个过程清晰流畅。配合 Element Plus 的 el-table 和 el-pagination后端返回的分页数据直接绑定即可。4. 前后端联调与上线部署4.1 开发环境跨域问题怎么处理前后端分离开发时前端跑在 5173 端口后端跑在 8000 端口两个端口不同就是跨域。处理跨域有两个方案我开发的阶段用的是 Vite 的代理方案部署阶段用 Nginx 反向代理统一入口。Vite 配置// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8000, changeOrigin: true } } } })开发环境所有以/api开头的请求都会被 Vite 代理到后端 8000 端口浏览器看到的请求是同源的不会有 CORS 问题。如果你不想用代理方案也可以在后端配上django-cors-headers加上CORS_ALLOW_ALL_ORIGINS True但生产环境千万别这么干等于把你的 API 开放给所有来源了。4.2 项目打包与部署细节前端构建npm run build产物会生成在dist/目录静态文件推送到服务器上的/opt/ops_web/。后端用 Gunicorn 跑 Django 服务gunicorn config.wsgi:application -w 4 -b 127.0.0.1:8000Nginx 配置是部署的关键一环server { listen 80; server_name ops.example.com; # 前端静态资源 location / { root /opt/ops_web; index index.html; try_files $uri $uri/ /index.html; } # 后端 API 反向代理 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态文件 location /static/ { alias /var/www/ops_backend/static/; } }这里try_files $uri $uri/ /index.html这一行非常重要。Vue Router 默认是 history 模式如果没有这行配置用户点击浏览器刷新或直接访问/assets等子路径时Nginx 找不到对应的文件会返回 404。加了这行之后所有前端路由会统一回退到 index.html由前端路由接管。数据库迁移和静态文件收集也要按顺序执行python manage.py makemigrations python manage.py migrate python manage.py collectstatic部署环境我用的是云服务器 宝塔面板的方式宝塔面板图形化配置 Nginx 和 MySQL 很方便省去不少命令行操作。如果你用的是麒麟这类国产化操作系统命令基本兼容唯一要注意的是安装 Python 依赖时可能需要指定--index-url指向可用源。4.3 生产环境的安全加固要点系统上线前安全加固这步必须做不能省关闭 Django 的 DEBUG 模式改设DEBUG False配置ALLOWED_HOSTS为实际域名或 IP。Django SECRET_KEY 不能硬编码在代码里放到环境变量读取。数据库密码、SSH 凭证加密密钥也都从环境变量读取。Nginx 层开启 HTTPS配置 SSL 证书。操作审计日志要定期备份至少保留 180 天以上。命令执行黑名单放在服务端前端隐藏不等于后端安全。5. 开发过程中遇到的坑与排查思路5.1 线上环境高频问题速查表下面这些问题是开发过程中真实遇过的每一条背后都是数小时的排查现在整理成一个速查表大家在开发时可以直接对照排查。现象可能原因解决办法前端登录后刷新页面又跳回登录页token 存放在内存里刷新后丢失token 存 localStorage初始化时读取接口返回 403 CSRF 验证失败前后端分离场景下未关闭 CSRF对 API 请求关闭 CSRF 中间件改用 JWT 认证跨域请求被拦截后端未配置 CORS 或配置错误加django-cors-headers生产环境用 Nginx 反代中文乱码MySQL 连接未指定 utf8mb4创建数据库时设置utf8mb4PyMySQL 连接参数加上 charset上传的文件 Nginx 报 413Nginx 上传大小限制在http或server块加client_max_body_size 50m;Vue Router history 模式刷新 404服务器没有配置 try_filesNginx 配置try_files $uri $uri/ /index.html;后端改字段后前端没反应接口返回了旧的缓存检查浏览器 Network 看是否走 Service Worker或清缓存命令执行超时SSH 连接或命令执行超过设定 timeout适当调大 timeout或者命令改为异步执行5.2 排查方法论四步定位问题排查问题我有一套自己的方法论分享给大家参考。第一步先看浏览器开发者工具的 Network 面板确认请求是否发出、返回什么状态码、响应体是什么。这一步能解决大约 60% 的问题。第二步去后端看日志Django 的日志默认打到终端或日志文件重点看异常堆栈。第三步用 curl 或 Postman 直接调接口排除前端代码干扰。第四步如果还查不出来就在关键位置打日志不要靠猜。印象最深的一次前端所有接口都返回 401但登录接口正常。排查了半天最后发现是 axios 封装里把 token 拼错了位置写成了Authorization: Token ${token}而后端验证的是 Bearer 格式。类似这种细节如果一开始就规范格式约定就不会浪费这么多时间。5.3 凭证加密的工具类封装凭证加密我单拎出来讲一下因为运维系统的安全水位很大程度上取决于这块设计。我用的是 Fernet 对称加密密钥一秒钟生成一次from cryptography.fernet import Fernet # 生成密钥保存到环境变量中 key Fernet.generate_key() def encrypt_secret(plain_text): f Fernet(settings.ENCRYPT_KEY.encode()) return f.encrypt(plain_text.encode()).decode() def decrypt_secret(cipher_text): f Fernet(settings.ENCRYPT_KEY.encode()) return f.decrypt(cipher_text.encode()).decode()存储层面密码字段设计为 CharField 存密文接口返回时默认做脱敏处理前端显示******只有执行命令时后端才在内存中解密。解密后的密码绝对不能打日志Python 的日志系统要特别留意别在 debug 时顺手打了敏感字段。6. 进阶功能从 MVC 到复杂业务逻辑的扩展6.1 用 ViewSet 组织 RESTful API上面的功能基本覆盖了一个运维系统的 MVP。实际开发中随着业务复杂度上升API 组织结构会越来越重要。Django REST Framework 的 ViewSet 提供了一种优雅的解决方案# apps/assets/views.py from rest_framework import viewsets from .models import Host from .serializers import HostSerializer class HostViewSet(viewsets.ModelViewSet): queryset Host.objects.all() serializer_class HostSerializer pagination_class StandardResultsSetPagination filterset_fields [os_type, status] search_fields [hostname, ip_address]配合设置两个关键类# config/settings.py REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ apps.users.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], DEFAULT_PAGINATION_CLASS: apps.utils.pagination.StandardResultsSetPagination, PAGE_SIZE: 10, }ViewSet 一组 CRUD 接口只需要定义一次路由注册也很简单from rest_framework.routers import DefaultRouter router DefaultRouter() router.register(rhosts, HostViewSet)一个路由直接带出列表、详情、创建、更新、删除全部接口前后端对接的 URL 约定天然统一开发效率提升非常显著。6.2 异步任务与 WebSocket 场景后面我还在系统里加了异步任务属于进阶扩展。比如批量执行命令如果同步跑一个客户端请求可能要等几十秒体验很差。我的方案是后端收到批量任务后直接返回任务 ID后台用 Celery 异步执行前端通过轮询任务状态接口获知执行进度完成后把结果存入数据库供前端拉取展示。如果后续要做实时终端或强实时监控可以升级为 WebSocket 方案。Django Channels 是官方生态里比较成熟的方案但复杂度会上升不少建议先把同步任务走通再考虑。6.3 日志规范的统一日志是系统里最容易被忽略却最重要的部分。我的做法是在每个业务模块从上到下定义日志策略记录接口的访问者、访问时间、请求参数、返回状态记录命令执行的完整链路记录登录失败的次数和来源 IP日记记录要包含时间戳以便追溯。Django 的 logging 配置可以整合到 settings 里将 INFO 级别的日志输出到文件并按天轮转LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { level: INFO, class: logging.handlers.TimedRotatingFileHandler, filename: /var/log/ops_backend/app.log, when: midnight, backupCount: 30, formatter: verbose, }, }, loggers: { django: { handlers: [file], level: INFO, propagate: True, }, }, }日志文件定期切割保留 30 天避免单个文件无限膨胀。6.4 Pinia 状态管理的最佳实践前端状态管理我用了 Pinia比 Vuex 简洁很多。运维系统里的状态大概分三类用户状态登录信息、权限码、全局 UI 状态侧边栏折叠、主题、业务缓存主机列表的筛选条件。前三类的写法// src/stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: null }), actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, setUserInfo(info) { this.userInfo info }, logout() { this.token this.userInfo null localStorage.removeItem(token) } } })跟 Vuex 对比Pinia 几乎没有学习成本TypeScript 支持也更好。在大型中后台项目里按业务域拆 store 是标准实践比把所有状态堆在一个大 store 里好维护得多。7. 最后的经验总结与后续扩展方向说几个我实际做完这个项目后最深的感受。第一个是关于开发节奏的。前后端分离项目最忌讳的是全写完再联调一定要前后端并行开发、约好接口文档同步推进。我这次是先定了完整的 API 文档后端按文档实现接口并提供 mock 数据前端按文档直接开发页面联调整体非常顺畅。接口文档工具我用的 apifox支持导出、一键 mock前后端可以基于同一份文档协作。第二个是安全底线问题。运维系统掌握着服务器最高的管理权限任何安全疏漏都可能被放大。代码层面要注意 SQL 注入、SSO 鉴权、命令逃逸、越权访问这些基础问题运维层面要管控好服务器访问权限、数据库防火墙、Nginx 配置审查。密码加密和操作审计这几个模块无论如何都不能省。第三个是可扩展性。第一版能跑通不代表系统设计就结束了真实环境里监控告警、容器管理、CI/CD 发布这些需求随时会提过来。基础数据模型设计得稳、模块边界划分得清楚加新功能就是顺理成章的事。当时我把数据模型设计得足够归一化后来对接监控数据时只是增加了一张新表主机资产表完全没动。如果后续要继续扩展我建议优先做这几个方向WebSSH 在线终端、监控数据面板、告警通知钉钉/飞书/邮件、工单审批流。WebSSH 是运维系统最常用的功能Django Channels 能实现监控面板可以对接 Prometheus 的 API 拉数据告警通知需要对接各家开放平台的消息推送接口工单审批流则需要引入分布式任务和消息队列来做支持。这些功能都做进去之后这套系统基本就从一个简单的资产台账长成了真正能支撑日常运维工作的内部平台了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →