简介:这是一套面向Python全栈开发者与企业级Web项目学习者的完整管理系统源码,基于Django+DRF构建后端API服务,前端采用Vue.js实现组件化交互,切实解决前后端分离架构下权限管理、组织架构、任务协同等典型企业管理场景的开发落地问题。资源包共109个文件,含38个核心Python源码(涵盖models、views、serializers、urls等DRF标准模块)、27张界面截图(JPG格式,直观展示登录页、部门管理、任务看板等真实UI效果)及7张PNG图标资源,辅以README说明文档,整体压缩包仅5.24MB,轻量易部署。已有1201人学习下载,适合中高级开发者快速掌握Django REST Framework权限控制、Vue Axios通信、JWT认证集成、数据库关系建模等关键技术,并可直接复用其分层目录结构、标准化API设计规范与前后端联调流程。
1. 项目概述与核心价值
最近在整理过往项目时,翻出了一个几年前为企业客户定制开发的管理系统源码。这套系统采用了经典的Django + Django REST Framework (DRF)作为后端,Vue.js作为前端,是完全的前后端分离架构。虽然技术栈在今天看来不算最新潮,但它的设计思路、代码结构以及解决实际业务问题的方案,依然非常扎实,堪称一个教科书级的全栈入门与进阶实战项目。无论是想学习如何将 Django 从传统的 MTV 模式转型为纯 API 服务,还是想掌握 Vue 如何与后端优雅交互,构建一个完整可用的 B 端管理系统,这个项目都能提供一条清晰的路径。
这个系统麻雀虽小,五脏俱全,涵盖了用户权限管理、部门组织架构、员工信息管理、简单的审批流程等企业通用功能模块。它没有追求酷炫的新技术,而是专注于如何用稳定、成熟的技术栈,高效、可靠地解决实际问题。对于初学者,你可以把它当作一个从零到一的脚手架;对于有一定经验的开发者,其中的权限设计、API 抽象、前后端状态管理协同等细节,也值得深入琢磨。接下来,我就把这个项目的核心设计、关键实现以及我踩过的那些坑,毫无保留地拆解给你看。
2. 技术栈选型与架构设计思路
2.1 为什么是 Django + DRF + Vue?
当初选择这个技术组合,是基于几个非常实际的考量。首先,项目周期紧,需求明确但后续可能变动,这就要求后端框架必须具备强大的快速开发能力和清晰的代码组织。Django的 “开箱即用” 特性在这里是巨大的优势:自带 Admin 后台(初期用于快速配置数据和权限)、完善的 ORM、强大的路由和中间件机制,以及严谨的安全防护(如 CSRF、XSS 防护)。这意味着我们可以把更多精力放在业务逻辑,而不是重复造轮子上。
然而,传统的 Django 模板渲染方式并不适合我们想要的现代化、动态交互强的管理后台。所以,我们引入了Django REST Framework (DRF)。DRF 不是替代 Django,而是基于 Django 构建 RESTful API 的强力工具包。它提供了序列化器(Serializer)、视图集(ViewSet)、路由器(Router)等高级抽象,能让我们用极少的代码就构建出规范、健壮的 API 接口,并且自带可浏览的 API 界面,方便前后端联调。
前端选择Vue.js,在当时(现在依然)是一个平衡了学习曲线、开发效率和生态成熟度的选择。相比于 React 的 JSX 和相对灵活(有时也意味着混乱)的架构,Vue 的模板语法对后端转前端的开发者更友好,其响应式系统和单文件组件(.vue)的概念清晰直观。对于管理系统这类以表单、表格、弹窗为主要交互形式的应用,Vue 配合 Element UI(我们当时用的 UI 库)能够非常高效地搭建出界面。前后端分离后,前端可以独立部署、独立开发,通过 API 与后端解耦,大大提升了团队的并行开发效率。
2.2 前后端分离架构的核心设计
这个项目的架构图在脑子里很简单,但实现时需要明确几条关键原则:
- 后端纯 API 化:Django 不再负责任何页面渲染,它的唯一职责就是通过 DRF 提供 JSON 格式的 RESTful API。所有业务逻辑、数据验证、权限判断都集中在后端。
- 前端独立应用:Vue 项目是一个完全独立的 SPA(单页应用)。它通过 Axios 等 HTTP 客户端调用后端 API,获取数据后在前端进行渲染和状态管理。
- 基于 Token 的认证:这是前后端分离的关键。我们放弃了 Django 默认的 Session-Cookie 机制(因为涉及跨域等问题),采用 JWT (JSON Web Token) 或 DRF 自带的 TokenAuthentication。用户登录后,后端返回一个 Token,前端将其存储在本地(如 localStorage),并在后续每次请求的 Header 中携带,后端据此识别用户身份和权限。
- 清晰的 API 契约:前后端开发前,先定义好 API 的 URL、请求方法、请求/响应体格式。DRF 的 Serializer 和 Schema 自动生成功能,在这里帮了大忙。
注意:在正式生产环境中,务必妥善处理 Token 的安全存储与刷新机制,避免 XSS 攻击导致 Token 泄露。我们当时采用了 HttpOnly 的 Cookie 存储 Access Token 并结合 Refresh Token 的方案,但这需要更复杂的配置,初期学习时可以从简单的 localStorage 开始。
3. 后端核心实现详解
3.1 Django 项目结构与模型设计
我们并没有使用 Django 默认的单models.py结构,而是采用了按应用(App)分拆的模式,这对于中大型项目更清晰。
backend/ ├── manage.py ├── core/ # 核心配置与应用 │ ├── settings.py │ ├── urls.py │ └── ... ├── apps/ │ ├── users/ # 用户认证与权限管理 │ │ ├── models.py (扩展Django User模型) │ │ ├── serializers.py │ │ ├── views.py │ │ └── ... │ ├── department/ # 部门组织架构 │ ├── employee/ # 员工信息管理 │ └── workflow/ # 简单审批流 ├── utils/ # 公共工具函数 └── requirements.txt在users/models.py中,我们通过继承AbstractUser来扩展默认用户模型,这是 Django 推荐的做法。这样可以保留 Django 强大的认证系统,同时添加我们需要的字段,比如手机号、头像、所属部门等。
# apps/users/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): mobile = models.CharField('手机号', max_length=11, unique=True, blank=True) avatar = models.ImageField('头像', upload_to='avatars/', blank=True) department = models.ForeignKey('department.Department', on_delete=models.SET_NULL, null=True, verbose_name='所属部门') # 可以添加更多自定义字段 class Meta: db_table = 'auth_user' # 可以指定表名,覆盖默认的auth_user verbose_name = '用户' verbose_name_plural = verbose_name def __str__(self): return self.username记得在settings.py中指定自定义用户模型:AUTH_USER_MODEL = 'users.User'。这个操作一定要在首次执行migrate之前完成,否则后期修改会非常麻烦。
3.2 DRF 视图、序列化器与权限控制
这是 DRF 发挥威力的地方。以员工(Employee)的增删改查 API 为例。
首先,定义序列化器(Serializer),它负责将模型实例转换为 JSON(序列化),以及将 JSON 数据验证后转换为模型实例(反序列化)。
# apps/employee/serializers.py from rest_framework import serializers from .models import Employee from apps.department.serializers import DepartmentSimpleSerializer # 嵌套序列化 class EmployeeSerializer(serializers.ModelSerializer): # 将只读的部门信息嵌套展示 department_info = DepartmentSimpleSerializer(source='department', read_only=True) # 在创建/更新时,我们只需要部门的ID department_id = serializers.PrimaryKeyRelatedField( queryset=Department.objects.all(), source='department', write_only=True ) class Meta: model = Employee fields = ['id', 'name', 'employee_id', 'gender', 'email', 'phone', 'department', 'department_id', 'department_info', 'join_date', 'status'] # 注意:`department`字段是外键关联对象,默认会输出其ID。我们通过上面的定义来精细控制。 read_only_fields = ['id', 'employee_id'] # 员工ID可能由系统生成 def validate_phone(self, value): # 自定义字段验证逻辑 if not re.match(r'^1[3-9]\d{9}$', value): raise serializers.ValidationError("手机号格式不正确") return value然后,使用视图集(ViewSet)和路由器(Router)来快速构建一组标准的 CRUD API。
# apps/employee/views.py from rest_framework import viewsets, permissions, filters from django_filters.rest_framework import DjangoFilterBackend from .models import Employee from .serializers import EmployeeSerializer from .permissions import IsHRorReadOnly # 自定义权限 class EmployeeViewSet(viewsets.ModelViewSet): """ 员工信息视图集 支持标准的 list, create, retrieve, update, partial_update, destroy 操作 """ queryset = Employee.objects.all().select_related('department') # 使用select_related优化查询 serializer_class = EmployeeSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields = ['department', 'status'] # 精确过滤字段 search_fields = ['name', 'employee_id', 'phone'] # 搜索字段 ordering_fields = ['join_date', 'id'] # 排序字段 ordering = ['-join_date'] # 默认排序 # 权限控制:HR可以增删改,普通员工只能查看 permission_classes = [permissions.IsAuthenticated, IsHRorReadOnly] def perform_create(self, serializer): # 在创建对象前可以做一些事情,比如自动生成员工编号 serializer.save(employee_id=self._generate_employee_id()) def _generate_employee_id(self): # 简单的员工号生成逻辑,例如:EMP20231025001 from django.utils import timezone date_prefix = timezone.now().strftime('%Y%m%d') last_emp = Employee.objects.filter(employee_id__startswith=f'EMP{date_prefix}').order_by('-employee_id').first() if last_emp: seq = int(last_emp.employee_id[-3:]) + 1 else: seq = 1 return f'EMP{date_prefix}{seq:03d}'最后,在urls.py中使用DefaultRouter自动注册路由,非常简洁。
# core/urls.py (主路由) from django.urls import path, include from rest_framework.routers import DefaultRouter from apps.employee.views import EmployeeViewSet router = DefaultRouter() router.register(r'employees', EmployeeViewSet) # ... 注册其他视图集 urlpatterns = [ path('api/', include(router.urls)), path('api-auth/', include('rest_framework.urls')), # DRF 的登录视图,用于可浏览API界面 # ... 其他URL ]这样,一套完整的、带过滤、搜索、排序、分页(DRF默认提供)和权限控制的员工管理 API 就完成了,访问/api/employees/即可。
3.3 认证与权限系统深度定制
DRF 提供了灵活的认证和权限类。我们通常会在settings.py中配置全局默认项,然后在需要特殊处理的视图上覆盖。
# core/settings.py REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', # 用于DRF的可浏览API 'rest_framework.authentication.TokenAuthentication', # 我们主要的Token认证 # 'rest_framework_simplejwt.authentication.JWTAuthentication', # 如果使用JWT ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', # 全局默认:所有API都需要登录 ], 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.SearchFilter', 'rest_framework.filters.OrderingFilter', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20, # 默认分页大小 }对于更细粒度的权限,比如“只有部门经理才能修改本部门员工”,我们需要自定义权限类。
# apps/employee/permissions.py from rest_framework import permissions class IsDepartmentManagerOrHR(permissions.BasePermission): """ 自定义权限:部门经理或HR可以编辑员工。 """ def has_object_permission(self, request, view, obj): # 安全方法(GET, HEAD, OPTIONS)总是允许 if request.method in permissions.SAFE_METHODS: return True # 用户必须是HR if request.user.groups.filter(name='HR').exists(): return True # 或者用户是该员工所在部门的经理 if obj.department and obj.department.manager == request.user: return True return False然后在EmployeeViewSet的permission_classes中使用这个自定义类。这种基于对象的权限控制,是复杂业务系统的核心。
4. 前端 Vue 项目关键实现
4.1 项目初始化与架构搭建
我们使用 Vue CLI 创建项目,并选择了 Vue Router 和 Vuex。项目结构大致如下:
frontend/ ├── public/ ├── src/ │ ├── api/ # 所有API请求封装 │ │ ├── index.js # 创建axios实例,配置拦截器 │ │ ├── auth.js # 登录、注销、Token刷新API │ │ ├── employee.js # 员工相关API │ │ └── ... │ ├── assets/ │ ├── components/ # 公共组件 │ ├── router/ # Vue Router配置 │ ├── store/ # Vuex状态管理 │ │ ├── index.js # 主store │ │ ├── modules/ # 模块化store │ │ │ ├── user.js │ │ │ └── ... │ ├── utils/ # 工具函数(如日期格式化、权限检查) │ ├── views/ # 页面级组件 │ │ ├── Login.vue │ │ ├── Dashboard.vue │ │ ├── employee/ │ │ │ ├── EmployeeList.vue │ │ │ ├── EmployeeForm.vue │ │ └── ... │ ├── App.vue │ └── main.js ├── .env.development # 开发环境变量 ├── .env.production # 生产环境变量 └── package.json在src/api/index.js中,我们创建了一个配置好的 Axios 实例,这是前后端通信的枢纽。
// src/api/index.js import axios from 'axios' import { Message } from 'element-ui' // 假设使用Element UI的消息提示 import router from '@/router' // 创建axios实例 const service = axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL || '/api', // 从环境变量读取后端地址 timeout: 15000 // 请求超时时间 }) // 请求拦截器 service.interceptors.request.use( config => { // 在发送请求之前做些什么:例如添加Token const token = localStorage.getItem('access_token') if (token) { config.headers['Authorization'] = `Token ${token}` // 根据后端认证类调整,可能是 `Bearer ${token}` } return config }, error => { // 对请求错误做些什么 console.error('Request Error:', error) return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response => { // 对响应数据做点什么 const res = response.data // 这里假设后端返回的统一格式为 { code: 200, data: {}, message: 'success' } if (res.code !== 200) { // 业务逻辑错误 Message.error(res.message || 'Error') // 如果是未授权或Token过期,跳转到登录页 if (res.code === 401 || res.code === 403) { localStorage.removeItem('access_token') router.push('/login') } return Promise.reject(new Error(res.message || 'Error')) } else { return res.data // 直接返回真正的数据部分 } }, error => { // 对响应错误做点什么 (HTTP状态码非2xx) console.error('Response Error:', error.response) if (error.response && error.response.status === 401) { Message.error('登录已过期,请重新登录') localStorage.removeItem('access_token') router.push('/login') } else if (error.response && error.response.status === 500) { Message.error('服务器内部错误') } else { Message.error(error.message || '网络请求失败') } return Promise.reject(error) } ) export default service4.2 状态管理与路由守卫
使用 Vuex 管理用户登录状态和全局信息。store/modules/user.js模块:
// store/modules/user.js import { login, logout, getUserInfo } from '@/api/auth' const state = { token: localStorage.getItem('access_token') || '', userInfo: null, permissions: [] // 用户权限列表 } const mutations = { SET_TOKEN: (state, token) => { state.token = token localStorage.setItem('access_token', token) }, SET_USER_INFO: (state, userInfo) => { state.userInfo = userInfo }, SET_PERMISSIONS: (state, permissions) => { state.permissions = permissions }, RESET_STATE: (state) => { state.token = '' state.userInfo = null state.permissions = [] localStorage.removeItem('access_token') } } const actions = { // 用户登录 login({ commit }, userInfo) { const { username, password } = userInfo return new Promise((resolve, reject) => { login({ username: username.trim(), password: password }) .then(response => { const { token, user } = response commit('SET_TOKEN', token) commit('SET_USER_INFO', user) resolve() }) .catch(error => { reject(error) }) }) }, // 获取用户信息 getInfo({ commit, state }) { return new Promise((resolve, reject) => { getUserInfo() .then(response => { const { user, permissions } = response commit('SET_USER_INFO', user) commit('SET_PERMISSIONS', permissions) resolve(response) }) .catch(error => { reject(error) }) }) }, // 用户退出 logout({ commit }) { return new Promise((resolve, reject) => { logout() .then(() => { commit('RESET_STATE') resolve() }) .catch(error => { reject(error) }) }) } } export default { namespaced: true, state, mutations, actions }在路由配置中,我们使用路由守卫(Navigation Guards)来实现页面级的权限控制。
// router/index.js import Vue from 'vue' import VueRouter from 'vue-router' import store from '@/store' Vue.use(VueRouter) const routes = [ { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { requiresAuth: false } // 登录页不需要认证 }, { path: '/', component: () => import('@/layouts/MainLayout.vue'), // 主布局 meta: { requiresAuth: true }, // 需要登录 children: [ { path: '/employees', name: 'EmployeeList', component: () => import('@/views/employee/EmployeeList.vue'), meta: { title: '员工管理', requiresAuth: true, permission: 'view_employee' } // 需要特定权限 }, // ... 其他路由 ] } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) // 全局前置守卫 router.beforeEach((to, from, next) => { // 设置页面标题 if (to.meta.title) { document.title = `${to.meta.title} - 企业管理系统` } const token = store.state.user.token // 判断该路由是否需要登录权限 if (to.meta.requiresAuth) { if (token) { // 如果已经登录,还需要检查是否有访问该页面的具体权限 if (to.meta.permission) { const hasPermission = store.state.user.permissions.includes(to.meta.permission) if (hasPermission) { next() } else { next({ path: '/403' }) // 跳转到无权限页面 } } else { next() } } else { // 未登录,跳转到登录页,并携带目标路径,以便登录后回跳 next({ path: '/login', query: { redirect: to.fullPath } }) } } else { // 不需要认证的页面,直接进入 next() } }) export default router4.3 典型页面组件实现:员工列表与表单
以员工列表页EmployeeList.vue为例,展示如何结合 Element UI 的表格、分页、搜索组件与后端 API 交互。
<template> <div class="employee-container"> <el-card> <!-- 搜索和操作栏 --> <div class="filter-container"> <el-input v-model="listQuery.name" placeholder="员工姓名" style="width: 200px;" @keyup.enter.native="handleFilter" /> <el-select v-model="listQuery.department" placeholder="部门" clearable style="width: 200px; margin-left: 10px;"> <el-option v-for="dept in departmentOptions" :key="dept.id" :label="dept.name" :value="dept.id" /> </el-select> <el-button type="primary" icon="el-icon-search" @click="handleFilter">搜索</el-button> <el-button type="success" icon="el-icon-plus" @click="handleCreate" v-permission="['add_employee']">新增</el-button> </div> <!-- 数据表格 --> <el-table v-loading="listLoading" :data="list" border fit highlight-current-row style="width: 100%; margin-top: 20px;" > <el-table-column prop="employee_id" label="工号" width="120" align="center" /> <el-table-column prop="name" label="姓名" width="100" align="center" /> <el-table-column prop="gender" label="性别" width="80" align="center"> <template slot-scope="{row}"> {{ row.gender | genderFilter }} </template> </el-table-column> <el-table-column prop="department_info.name" label="部门" /> <el-table-column prop="phone" label="电话" width="150" /> <el-table-column prop="status" label="状态" width="100" align="center"> <template slot-scope="{row}"> <el-tag :type="row.status | statusTagFilter">{{ row.status | statusFilter }}</el-tag> </template> </el-table-column> <el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width"> <template slot-scope="{row}"> <el-button type="primary" size="mini" @click="handleUpdate(row)" v-permission="['change_employee']">编辑</el-button> <el-button size="mini" type="info" @click="handleView(row)">查看</el-button> <el-button size="mini" type="danger" @click="handleDelete(row)" v-permission="['delete_employee']">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页组件 --> <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" /> </el-card> <!-- 新增/编辑对话框 --> <employee-form :dialog-visible="dialogVisible" :dialog-type="dialogType" :current-data="currentEmployee" @close="handleDialogClose" @success="handleFormSuccess" /> </div> </template> <script> import { fetchEmployeeList, deleteEmployee } from '@/api/employee' import { fetchDepartmentList } from '@/api/department' import Pagination from '@/components/Pagination' // 封装的分页组件 import EmployeeForm from './components/EmployeeForm' export default { name: 'EmployeeList', components: { Pagination, EmployeeForm }, filters: { genderFilter(gender) { const genderMap = { 'M': '男', 'F': '女' } return genderMap[gender] || '未知' }, statusFilter(status) { const statusMap = { 'active': '在职', 'inactive': '离职' } return statusMap[status] || status }, statusTagFilter(status) { const tagMap = { 'active': 'success', 'inactive': 'info' } return tagMap[status] || '' } }, data() { return { list: [], total: 0, listLoading: true, listQuery: { page: 1, limit: 20, name: undefined, department: undefined, ordering: '-join_date' }, departmentOptions: [], dialogVisible: false, dialogType: 'create', currentEmployee: {} } }, created() { this.getList() this.getDepartments() }, methods: { async getList() { this.listLoading = true try { const response = await fetchEmployeeList(this.listQuery) this.list = response.results // DRF分页返回的结果在results字段 this.total = response.count } catch (error) { console.error('获取员工列表失败:', error) } finally { this.listLoading = false } }, async getDepartments() { try { const response = await fetchDepartmentList({ all: true }) // 获取所有部门,不分页 this.departmentOptions = response } catch (error) { console.error('获取部门列表失败:', error) } }, handleFilter() { this.listQuery.page = 1 // 搜索时回到第一页 this.getList() }, handleCreate() { this.dialogType = 'create' this.currentEmployee = {} this.dialogVisible = true }, handleUpdate(row) { this.dialogType = 'update' this.currentEmployee = Object.assign({}, row) // 浅拷贝,避免直接修改表格数据 this.dialogVisible = true }, handleView(row) { this.$router.push(`/employees/detail/${row.id}`) }, async handleDelete(row) { try { await this.$confirm(`确定删除员工 "${row.name}" 吗?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) await deleteEmployee(row.id) this.$message.success('删除成功') this.getList() // 刷新列表 } catch (error) { if (error !== 'cancel') { this.$message.error('删除失败') } } }, handleDialogClose() { this.dialogVisible = false this.currentEmployee = {} }, handleFormSuccess() { this.dialogVisible = false this.getList() // 表单提交成功后刷新列表 } } } </script>EmployeeForm.vue组件则负责处理新增和编辑的逻辑,通过props接收父组件传递的类型和数据,通过$emit事件与父组件通信,这是一个非常清晰的组件通信模式。
5. 前后端联调与部署实战
5.1 开发环境配置与跨域处理
在开发阶段,前端运行在localhost:8080,后端运行在localhost:8000,存在跨域问题。有两种主流解决方案:
方案一:后端配置 CORS(推荐)使用 Django 的django-cors-headers库。
- 安装:
pip install django-cors-headers - 在
settings.py的INSTALLED_APPS中添加'corsheaders'。 - 在
MIDDLEWARE的最顶部添加'corsheaders.middleware.CorsMiddleware'。 - 配置白名单:
CORS_ALLOWED_ORIGINS = [ "http://localhost:8080", "http://127.0.0.1:8080", ] # 或者允许所有(仅限开发) # CORS_ALLOW_ALL_ORIGINS = True - 允许携带 Cookie(如果需要):
CORS_ALLOW_CREDENTIALS = True
方案二:前端代理在 Vue 项目的vue.config.js中配置开发服务器代理。
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8000', // 后端地址 changeOrigin: true, pathRewrite: { '^/api': '' // 如果后端API本身没有`/api`前缀,可以重写掉 } } } } }这样,前端在开发时请求/api/employees/就会被代理到http://localhost:8000/api/employees/,避免了跨域。我个人更推荐方案一,因为它更接近生产环境,且配置一次后端即可,多个前端项目都能用。
5.2 生产环境部署要点
生产部署通常将前后端分开。
后端部署:
- 服务器:Linux 服务器(如 Ubuntu/CentOS)。
- 环境:使用
virtualenv或pipenv创建虚拟环境,安装依赖。 - WSGI 服务器:使用 Gunicorn 或 uWSGI 来运行 Django 应用。例如:
gunicorn core.wsgi:application -b 0.0.0.0:8000 -w 4。 - 静态文件:使用
python manage.py collectstatic收集静态文件,并通过 Nginx 来服务。 - 反向代理:使用 Nginx 作为反向代理,处理静态文件、负载均衡,并将动态请求转发给 Gunicorn。Nginx 配置中需要设置正确的
proxy_pass和client_max_body_size(处理文件上传)等。
前端部署:
- 构建:运行
npm run build生成静态文件(在dist目录)。 - 托管:可以将
dist目录的文件直接放到 Nginx 的根目录下,或者使用对象存储(如阿里云 OSS)+ CDN。 - 路由问题:由于是 SPA,需要配置 Nginx 将所有非静态文件请求重定向到
index.html。location / { try_files $uri $uri/ /index.html; }
- 构建:运行
前后端连接:
- 前端构建时,通过环境变量
VUE_APP_API_BASE_URL设置后端 API 的绝对地址(如https://api.yourdomain.com)。 - 后端 Nginx 配置中,API 路径(如
/api/)代理到 Gunicorn。 - 确保生产环境关闭了 DEBUG 模式 (
DEBUG=False),并正确配置了ALLOWED_HOSTS、数据库连接、密钥等。
- 前端构建时,通过环境变量
5.3 常见问题与排查实录
在开发和部署这个系统的过程中,我遇到了不少典型问题,这里记录几个高频的:
跨域请求失败,尤其是带 Cookie/Token 的请求
- 现象:前端能发请求,但后端收不到认证信息,返回 401 或 403。
- 排查:
- 检查后端 CORS 配置,确保
CORS_ALLOW_CREDENTIALS = True。 - 检查前端 Axios 配置,是否设置了
withCredentials: true(如果使用 Cookie)。 - 检查请求头
Authorization是否正确携带(格式如Token xxxxxx或Bearer xxxxxx)。 - 浏览器的开发者工具 Network 面板,查看请求的Request Headers和Response Headers,确认
Access-Control-Allow-Origin、Access-Control-Allow-Credentials等头部是否正确。
- 检查后端 CORS 配置,确保
DRF 分页或过滤不生效
- 现象:API 返回所有数据,没有分页;过滤参数无效。
- 排查:
- 确认视图集(ViewSet)中是否设置了
pagination_class和filter_backends。 - 检查全局
settings.py中的REST_FRAMEWORK配置,分页和过滤后端是否设置。 - 对于过滤,检查
filterset_fields或search_fields是否正确定义。 - 查看请求的 URL 参数是否正确,例如
?page=2&search=张三&department=1。
- 确认视图集(ViewSet)中是否设置了
Vue 页面刷新后,Vuex 状态丢失,用户跳回登录页
- 现象:用户登录后,刷新页面,登录状态没了。
- 原因:Vuex 状态存储在内存中,页面刷新会重置。
- 解决:
- 将 Token 等关键信息持久化到
localStorage或sessionStorage。 - 在应用初始化时(如
main.js或 App.vue 的created钩子),从存储中读取 Token 并提交到 Vuex。 - 同时,调用获取用户信息的 API,重新填充 Vuex 中的用户状态。
- 将 Token 等关键信息持久化到
上传文件(如图片)到 Django 后端失败
- 现象:前端能获取文件,但 POST 到后端时报错或文件为空。
- 排查:
- 前端需要使用
FormData对象来包装文件数据。 - 后端视图需要使用
MultiPartParser(DRF 默认已支持)。 - 检查 Nginx 配置,是否设置了足够大的
client_max_body_size。 - 检查 Django 的
MEDIA_ROOT和MEDIA_URL配置是否正确,服务器是否有写入权限。
- 前端需要使用
Element UI 表格中显示嵌套对象属性
- 现象:后端返回的数据中有
department_info: {id: 1, name: '技术部'},但在el-table-column中直接prop="department_info.name"不显示。 - 解决:使用作用域插槽(Scoped Slot)来访问行数据。
或者,在获取数据后,先对列表数据进行一次处理,将嵌套属性展平。<el-table-column label="部门"> <template slot-scope="{row}"> {{ row.department_info ? row.department_info.name : '-' }} </template> </el-table-column>
- 现象:后端返回的数据中有
这个基于 Django + DRF + Vue 的企业管理系统项目,虽然技术栈不是最前沿的,但它完整地展示了一个现代 Web 应用从设计、开发到部署上线的核心流程。它所涉及的用户认证、权限控制、API 设计、状态管理、组件化开发、部署配置等知识点,是构建任何复杂 Web 应用的基石。无论你是想找一份全栈工作,还是打算自己独立开发一个项目,吃透这个项目的每一行代码背后的思想,都绝对会让你受益匪浅。代码是死的,但解决问题的思路是活的,希望这个拆解能给你带来启发。
本文还有配套的精品资源,点击获取