Django+DRF+Vue前后端分离企业管理系统实战:RBAC权限与动态路由 简介这是一套基于Django与Django REST Framework构建的企业管理系统源码采用前后端分离架构前端使用Vue.js适合具备一定Python与Web基础的开发者学习企业级项目开发。后端涵盖用户认证、ORM数据建模、API接口设计与权限控制前端通过组件化方式实现界面渲染与接口通信完整呈现了现代Web开发中前后端协作的典型流程。资源包共109个文件包含38个py源码、35个pyc编译文件、27个jpg与7个png界面截图以及1个md说明文档压缩包约5.24MB目录结构清晰便于按模块查阅。项目涉及用户、部门、任务等数据模型设计以及CRUD接口、序列化、分页过滤、模板引擎与部署测试等知识点可作为课程设计、毕业设计或企业管理系统二次开发的参考范例。目前已有1204人学习下载适合希望打通Django后端与Vue前端全链路开发的读者对照研读。1. 从一张“能跑起来”的企业管理系统源码说起DjangoDRF 与 Vue 前后端分离到底怎么落地很多做 Python 后端的同学第一次接触“企业管理系统源码”时最容易被两件事卡住一是 Django 和 DRF 到底怎么分工二是 Vue 前端拿到接口后怎么和权限、分页、菜单这些企业系统标配功能对上。我见过太多人把 Django 当纯后端 API 用结果 admin 后台和业务接口两套权限逻辑打架也见过 Vue 这边路由写死了菜单后端一改角色前端直接白屏。这篇笔记就围绕“DjangoDRF 做后端、Vue 做前端、前后端分离”这条主线把一套企业管理系统从环境搭建、模型设计、接口鉴权到前端动态路由的完整路径拆开讲。适合已经会一点 Python 和 Vue 基础、想拿一个真实项目练手或交付给团队用的开发者。读完你能自己搭出一套带 RBAC 权限、JWT 鉴权、分页搜索、动态菜单的最小可用系统并且知道哪些参数一改就翻车。2. 后端选型与最小可运行骨架Django 项目怎么拆 app、DRF 怎么配鉴权2.1 为什么企业管理系统普遍选 DjangoDRF 而不是 FastAPI先把这个选型问题说清楚不然后面每一步你都会犹豫。企业管理系统和纯 C 端高并发接口是两种东西前者重表单、重权限、重后台管理、重数据关系后者重吞吐和异步。Django 自带 ORM、Admin、Auth、MigrationDRF 在它上面补了序列化、视图集、分页、过滤、权限类这套组合最擅长的就是“表多、角色多、CRUD 多”的场景。FastAPI 性能好、写异步舒服但你要自己搭 Admin、自己处理迁移、自己写权限体系做企业管理系统反而更慢。常见做法是核心业务表用 Django ORM 定义接口层用 DRF 的 ModelViewSet 快速暴露权限用 DRF 的 permission_classes 挂载后台运维直接用 Django Admin 兜底。这样一套下来一个中等复杂度的企业管理系统后端接口部分两三天能出雏形。2.2 创建项目与 app 拆分别把所有模型塞进一个 app环境准备先做掉。Python 3.10 以上、Django 4.2 LTS、DRF 3.14 以上是当前比较稳的组合。安装命令如下python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django djangorestframework djangorestframework-simplejwt django-cors-headers django-filter django-admin startproject enterprise_backend . python manage.py startapp accounts python manage.py startapp organization python manage.py startapp permissions这里 app 拆分的逻辑是accounts 管用户和登录organization 管部门、岗位、员工permissions 管角色、菜单、接口权限。很多新手喜欢只建一个 app 叫 api所有模型全塞进去结果迁移文件冲突、循环导入、权限类找不到模型血泪经验就是拆开。拆完之后在 settings.py 里注册INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, corsheaders, django_filters, accounts, organization, permissions, ] AUTH_USER_MODEL accounts.User REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), DEFAULT_PERMISSION_CLASSES: ( rest_framework.permissions.IsAuthenticated, ), DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 20, DEFAULT_FILTER_BACKENDS: ( django_filters.rest_framework.DjangoFilterBackend, rest_framework.filters.SearchFilter, rest_framework.filters.OrderingFilter, ), } CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ]参数说明AUTH_USER_MODEL 必须在第一次 migrate 之前就设好否则后面换用户模型会非常痛苦PAGE_SIZE 设 20 是企业管理系统的常见值列表页一屏刚好CORS_ALLOWED_ORIGINS 里写的是 Vue 开发服务器地址Vite 默认 5173用 vue-cli 的话是 8080别写错。JWT 的过期时间在 SIMPLE_JWT 里配默认 access 5 分钟太短建议改成 2 小时refresh 7 天。2.3 自定义用户模型与 RBAC 三张核心表企业管理系统几乎不会用 Django 默认 User因为要加部门、岗位、手机号、状态这些字段。自定义用户模型如下from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): phone models.CharField(max_length20, blankTrue, verbose_name手机号) department models.ForeignKey( organization.Department, nullTrue, blankTrue, on_deletemodels.SET_NULL, related_nameusers, verbose_name所属部门 ) is_active models.BooleanField(defaultTrue, verbose_name在职状态) class Meta: db_table sys_user verbose_name 用户RBAC 的核心是三张表Role、Menu、Permission外加两张关联表。Role 和 User 多对多Role 和 Menu 多对多Menu 上挂前端路由信息。这样前端登录后拉一次菜单接口就能动态生成路由。模型定义时注意 db_table 统一加前缀避免和 Django 内置表混在一起。迁移命令python manage.py makemigrations python manage.py migrate python manage.py createsuperuser如果 makemigrations 报 “no changes detected”先检查 app 是否在 INSTALLED_APPS 里再检查模型文件是否被正确导入。这是新手最高频的翻车点。3. DRF 接口层实战序列化器、视图集、JWT 登录与权限类怎么写3.1 序列化器的嵌套与字段控制别让密码字段漏出去序列化器是 DRF 的核心企业管理系统里最常见的需求是列表页返回精简字段详情页返回完整字段写操作时密码要加密。做法是拆两个序列化器from rest_framework import serializers from django.contrib.auth.hashers import make_password from .models import User class UserListSerializer(serializers.ModelSerializer): department_name serializers.CharField(sourcedepartment.name, read_onlyTrue) class Meta: model User fields [id, username, phone, department_name, is_active] class UserCreateSerializer(serializers.ModelSerializer): password serializers.CharField(write_onlyTrue, min_length6) class Meta: model User fields [id, username, password, phone, department, is_active] def validate_password(self, value): return make_password(value)逻辑说明UserListSerializer 里 department_name 用 source 跨表取名字避免前端再发一次请求UserCreateSerializer 的 password 设 write_only保证序列化输出里永远不出现密码validate_password 里直接 make_password比在 create 方法里处理更集中。参数上 min_length6 是底线企业系统建议再加正则校验。注意不要用 serializers.CharField(source*) 这种写法会把所有字段暴露出去。3.2 ModelViewSet 与路由注册一个视图集搞定增删改查DRF 的 ModelViewSet 把 list、create、retrieve、update、destroy 全包了配合 DefaultRouter 自动生成路由from rest_framework import viewsets, filters from rest_framework.decorators import action from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend from .models import User from .serializers import UserListSerializer, UserCreateSerializer class UserViewSet(viewsets.ModelViewSet): queryset User.objects.select_related(department).all() filter_backends [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields [is_active, department] search_fields [username, phone] ordering_fields [id, date_joined] def get_serializer_class(self): if self.action list: return UserListSerializer return UserCreateSerializer action(methods[post], detailTrue) def reset_password(self, request, pkNone): user self.get_object() user.set_password(123456) user.save() return Response({status: ok})逻辑说明select_related 预加载部门避免列表页 N1 查询filterset_fields 让前端可以按状态和部门筛选search_fields 支持模糊搜索get_serializer_class 根据 action 切换序列化器这是 DRF 里非常实用的模式。action 装饰器用来挂自定义接口比如重置密码。路由注册from rest_framework.routers import DefaultRouter from .views import UserViewSet router DefaultRouter() router.register(users, UserViewSet, basenameuser) urlpatterns router.urls注意 basename 一定要写否则 queryset 为空时会报错。这是很多人复制代码后遇到的玄学问题。3.3 JWT 登录接口与自定义权限类登录接口用 simplejwt 的 TokenObtainPairView 改造返回用户信息和角色from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.serializers import TokenObtainPairSerializer class CustomTokenSerializer(TokenObtainPairSerializer): classmethod def get_token(cls, user): token super().get_token(user) token[username] user.username token[roles] list(user.roles.values_list(code, flatTrue)) return token def validate(self, attrs): data super().validate(attrs) data[user] { id: self.user.id, username: self.user.username, department: self.user.department.name if self.user.department else , } return data class CustomTokenView(TokenObtainPairView): serializer_class CustomTokenSerializer逻辑说明get_token 里往 JWT payload 塞角色 code前端可以据此做按钮级权限validate 里额外返回用户信息省一次请求。注意 payload 不要塞太多数据JWT 是 base64 编码不是加密敏感信息别放。自定义权限类from rest_framework.permissions import BasePermission class HasMenuPermission(BasePermission): def has_permission(self, request, view): if not request.user.is_authenticated: return False menu_code getattr(view, menu_code, None) if not menu_code: return True return request.user.roles.filter(menus__codemenu_code).exists()在视图里设 menu_code user_manage就能实现接口级权限控制。参数上 menu_code 要和数据库菜单表里的 code 一致建议用常量管理。4. Vue 前端对接动态路由、Axios 封装与菜单权限怎么串起来4.1 Vue 项目初始化与 Axios 拦截器封装前端用 Vite 创建 Vue 3 项目npm create vitelatest enterprise_frontend -- --template vue cd enterprise_frontend npm install npm install axios vue-router pinia element-plus npm run devAxios 封装是前后端分离项目的命脉请求拦截器加 token响应拦截器统一处理 401 和错误提示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 || http://127.0.0.1:8000/api, timeout: 10000, }) request.interceptors.request.use(config { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(access_token) router.push(/login) ElMessage.error(登录已过期请重新登录) } else { ElMessage.error(error.response?.data?.detail || 请求失败) } return Promise.reject(error) } ) export default request逻辑说明baseURL 从环境变量读方便切换开发和生产请求拦截器统一加 Bearer token响应拦截器里 401 直接跳登录其他错误弹提示。注意 response 拦截器返回的是 response.data所以业务代码里不用再 .data。参数上 timeout 设 10 秒企业系统接口一般不会超过这个时间。4.2 动态路由登录后根据后端菜单生成 router.addRoute企业管理系统前端最核心的机制是动态路由。做法是登录成功后调 /api/menus/ 拿到菜单树递归转换成路由配置用 router.addRoute 动态挂载import router from /router import Layout from /layout/index.vue const modules import.meta.glob(/views/**/*.vue) function buildRoutes(menus) { return menus.map(menu { const route { path: menu.path, name: menu.code, meta: { title: menu.name, icon: menu.icon }, } if (menu.component Layout) { route.component Layout } else if (menu.component) { route.component modules[/src/views/${menu.component}.vue] } if (menu.children?.length) { route.children buildRoutes(menu.children) } return route }) } export async function loadDynamicRoutes() { const menus await request.get(/menus/) const routes buildRoutes(menus) routes.forEach(route router.addRoute(route)) return routes }逻辑说明import.meta.glob 是 Vite 的懒加载语法把 views 下所有 vue 文件映射成动态导入函数后端菜单表里 component 字段存相对路径比如 system/user/indexLayout 是父级容器。注意 addRoute 要在 router.isReady() 之后调用否则刷新页面会白屏。参数上 menu.path 建议用 /system/user 这种绝对路径避免嵌套路由拼接出错。4.3 按钮级权限用自定义指令 v-permission 控制显隐菜单级权限管的是页面能不能进按钮级权限管的是页面里的操作能不能点。用自定义指令实现import { useUserStore } from /stores/user const permission { mounted(el, binding) { const userStore useUserStore() const required binding.value if (required !userStore.permissions.includes(required)) { el.parentNode?.removeChild(el) } }, } export default { install(app) { app.directive(permission, permission) }, }用法el-button v-permissionuser:delete删除/el-button。逻辑说明mounted 时检查用户权限列表没有就移除元素。注意用 removeChild 而不是 display:none避免用户通过开发者工具改样式绕过。参数上权限码建议用「模块:操作」格式和后端权限表保持一致。5. 避坑与排查企业管理系统前后端分离最容易翻车的 5 个点5.1 跨域配置改了没生效现象前端请求报 CORS 错误明明在 settings.py 里加了 CORS_ALLOWED_ORIGINS。原因django-cors-headers 的中间件顺序不对必须在 CommonMiddleware 之前。解决检查 MIDDLEWARE 列表corsheaders.middleware.CorsMiddleware 要放在 django.middleware.common.CommonMiddleware 上面。另外如果用了 Nginx 反代要确认 Nginx 没有把 Origin 头丢掉。5.2 JWT 过期后前端疯狂弹 401现象token 过期后页面同时发多个请求每个都弹一次“登录已过期”。原因响应拦截器没有做并发控制。解决加一个 isRefreshing 标志位第一个 401 触发刷新 token其他请求挂起等待刷新成功后重发。或者简单点401 时判断当前路由是不是已经是 /login是就不弹提示。5.3 动态路由刷新后 404现象登录后菜单正常按 F5 刷新页面直接 404 或白屏。原因动态路由是运行时 addRoute 的刷新后路由表还没加载完router 已经去匹配了。解决在 router.beforeEach 里判断如果 token 存在但路由表为空先 await loadDynamicRoutes()再 next({ ...to, replace: true })。注意 next 里要传 replace否则历史记录会多一条。5.4 分页参数前后端对不上现象前端传 page1size10后端返回的却是 20 条一页。原因DRF 的 PageNumberPagination 默认参数是 page 和 page_size不是 size。解决自定义分页类把 page_size_query_param 设成 size并在 settings 里替换默认分页类。或者前端统一用 page_size别自己造参数名。5.5 删除操作没做软删除数据恢复不了现象用户误删一条部门数据关联的员工全部变成 null想恢复发现数据库里已经没了。原因直接用了 ModelViewSet 的 destroy物理删除。解决企业管理系统一律用软删除模型加 is_deleted 字段重写 destroy 方法改成 update查询时过滤 is_deletedFalse。这个后悔药一定要提前吃等出事再改就晚了。6. 进阶技巧用 DRF 的过滤器 Vue 的表格封装把列表页开发时间砍一半列表页是企业管理系统里重复度最高的页面一个系统几十个列表如果每个都手写搜索、分页、排序工作量巨大。我的习惯是后端统一用 DjangoFilterBackend SearchFilter OrderingFilter前端封装一个通用 Table 组件把列配置、搜索项、接口地址通过 props 传进去。这样新增一个列表页后端只要写一个 ViewSet 加 filterset_fields前端只要写一份配置。具体做法后端所有 ViewSet 继承一个 BaseViewSet统一挂三个 filter backend统一分页类。前端封装 useTable hookimport { ref, reactive, onMounted } from vue import request from /utils/request export function useTable(url, defaultParams {}) { const loading ref(false) const list ref([]) const total ref(0) const query reactive({ page: 1, size: 20, ...defaultParams }) async function fetchData() { loading.value true try { const res await request.get(url, { params: query }) list.value res.results total.value res.count } finally { loading.value false } } function handleSearch() { query.page 1 fetchData() } function handleReset() { Object.keys(query).forEach(key { if (![page, size].includes(key)) query[key] }) handleSearch() } onMounted(fetchData) return { loading, list, total, query, fetchData, handleSearch, handleReset } }逻辑说明query 里 page 和 size 是分页参数其他是搜索条件handleSearch 重置到第一页再查handleReset 清空搜索条件。参数上 size 默认 20和后端 PAGE_SIZE 保持一致。这个 hook 配合 Element Plus 的 el-table 和 el-pagination一个列表页的 script 部分能压到 30 行以内。验证方法新建一个部门列表页后端 DepartmentViewSet 加 filterset_fields [name, is_active]前端用 useTable(/departments/)看搜索、分页、重置是否都正常。如果分页不生效检查后端是否返回了 count 和 results 字段DRF 默认分页返回的就是这两个 key。最后说个我自己的习惯每次做完一个模块先不急着写下一个而是把接口用 Postman 或 curl 跑一遍确认分页、筛选、权限都对了再动前端。因为前后端分离项目里后端接口一改前端所有调用点都要跟着改返工成本比多测五分钟高得多。希望帮到你。本文还有配套的精品资源点击获取