☰
Python+Vue管理系统开发:Django与Flask选型实战与部署全记录
2026/10/22 8:44:14 网站建设 项目流程

年底接了个文体俱乐部管理系统的开发需求,和之前做的几个管理系统项目类似,技术栈锁定在Python后端加Vue前端。不过在框架选型上,团队内部一开始就分了两派:一派说用Django,自带Admin和管理后台,开发效率高;另一派说项目体量不大,Flask轻量灵活,接口自己定义更自由。两边说得都有道理,但实际动工之后才发现,选错框架带来的代价比想象中大得多。

这篇文章我想从这套系统的完整实现过程讲起,包括框架选型、数据建模、接口设计、前后端联调,还有最后部署上线时踩过的坑。如果你也在用Python+Vue做这类管理系统,或者正在纠结Django和Flask怎么选,这篇文章应该能给你省下不少时间。

1. 先别急着写代码:Django和Flask在管理系统场景下的真实差异

既然标题里问到了django和flask,那就先把这个最核心的选型问题说透。文体俱乐部管理系统看起来是个小项目,但业务上该有的模块一个不少——会员信息管理、俱乐部活动发布、报名审核、场地预约、公告通知、数据统计。这些功能背后是大量关联表的增删改查,还得考虑不同角色的权限范围。

1.1 两类框架在管理系统场景下的实际表现

我拿一个具体的业务点来说明差异。比如做一个"活动报名"功能,会员在前端点报名按钮,后端要完成几件事:检查活动是否还有名额,检查这个会员是否重复报名,写入报名记录,同时更新活动已报名人数。Django这边,模型层用ORM直接定义外键和字段约束,视图层用一个ViewSet加上DRF(Django REST Framework)的序列化器就能把整套逻辑串起来,而且自带的Admin后台可以直接给运营人员用,不需要额外开发管理界面。

Flask这边实现同样功能,可以用SQLAlchemy做ORM,但没有类似DRF这样约定俗成的接口方案,路由、请求参数校验、序列化、分页、认证这些都要自己搭。不是说Flask做不到,而是很多代码本来Django已经替你做好了,换成Flask之后你得一个一个重新装配。我做过的几个管理系统项目里,Flask项目的前期搭框架时间普遍比Django多出一倍以上。

下面是两者在这类项目上的直观对比:

对比维度DjangoFlask
自带后台管理自带Admin,可直接复用无,需要自己开发或接入第三方
ORM和数据库迁移内置ORM,manage.py支持迁移需搭配SQLAlchemy,迁移用Alembic
用户认证与权限自带User模型和Permission体系需用Flask-Login或自研
接口序列化DRF生态完善需用Marshmallow或手工序列化
项目结构固定的project/app结构,规范统一自由灵活,适合极简项目
上手成本概念多,需要理解的地方多简单直观,几天就能上手

1.2 我的结论:为什么最终用了Django

如果你做的系统需要登录注册、多角色权限、数据增删改查、后台管理,这类业务特征鲜明的项目,Django是更省心的选择。Flask适合接口极其简洁、几乎没有后台管理需求、更偏定制化API的场景,比如一个只对外提供JSON数据的小工具服务。

文体俱乐部的业务恰恰命中Django的优势区。会员和活动的数据关联关系要用外键来维护,报名状态要流转,运营人员需要一个能直接录入数据的后台页面,这些Django开箱即用。所以我最终的选型用的是Python 3.10、Django 4.2、Django REST Framework、Vue 3加Element Plus。开发工具用Pycharm,它对这个组合的支持比较成熟,调试起来也顺手。

1.3 初始化和基础配置里容易被忽略的几个点

用Pycharm从零创建Django项目的时候,我建议直接用一个干净的虚拟环境,不要把系统Python环境搞乱了。创建完项目之后,有几个配置项在写业务代码之前就要改好:

  • 在settings.py里把ALLOWED_HOSTS加上本地IP和后续部署要用的域名,不然前端联调时请求直接被拒。
  • 安装并配置corsheaders中间件,前后端分离架构下跨域问题早晚要处理,早配早省事。
  • 注册rest_framework这个app,后面写接口会用。
  • 数据库如果不想用默认的SQLite,可以在这一步就切到MySQL或PostgreSQL,等数据量大了再迁移数据库反而麻烦。

我当时身边有个A同学自己搭项目,图省事直接用的SQLite,结果测试阶段塞进几千条报名记录之后,查询速度明显变慢,最后又回头折腾数据迁移。这类管理系统虽然数据量不会特别夸张,但一开始用MySQL会省掉很多后续麻烦。

2. 文体俱乐部的业务实体拆解与数据库建模

想清楚业务需求再建表,这一步省不得。文体俱乐部管理系统里最核心的角色是会员,围绕会员延伸出俱乐部、活动、报名、场地、公告这几张表。整个系统的数据流动其实是一个很清晰的链路:俱乐部发起活动,活动占用某个场地,会员报名参加活动,报名记录变成后台的统计数据。

2.1 核心数据表的结构设计

会员表是系统的地基,字段包含了账号、姓名、联系方式、所属院系或单位、会员等级。等级字段不用单独建表,直接用一个choices选项就够了,这样简单也好维护。活动表是业务的核心,要关联到发起活动的俱乐部,同时关联场地表,记录活动的开始时间、结束时间、容量上限、状态。

这里有一个前后端都容易踩的坑:活动状态建议用数值或者固定字符串来存,比如"报名中""已满员""已结束""已取消",不要用0和1这种模糊标记。不然前端拿到数据还要猜0是什么意思,直接导致代码里各种魔法数字满天飞。

class Member(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) real_name = models.CharField(max_length=50) phone = models.CharField(max_length=20) department = models.CharField(max_length=100) LEVEL_CHOICES = [(1, '普通会员'), (2, '高级会员'), (3, '运营管理员')] level = models.IntegerField(choices=LEVEL_CHOICES, default=1) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.real_name}({self.user.username})" class Club(models.Model): name = models.CharField(max_length=100) category = models.CharField(max_length=50) # 篮球社、合唱团、摄影协会等 description = models.TextField(blank=True) manager = models.ForeignKey(Member, on_delete=models.SET_NULL, null=True, related_name='managed_clubs') class Venue(models.Model): name = models.CharField(max_length=100) capacity = models.IntegerField() is_available = models.BooleanField(default=True) class Activity(models.Model): title = models.CharField(max_length=200) club = models.ForeignKey(Club, on_delete=models.CASCADE, related_name='activities') venue = models.ForeignKey(Venue, on_delete=models.SET_NULL, null=True) start_time = models.DateTimeField() end_time = models.DateTimeField() capacity = models.IntegerField() STATUS_CHOICES = [(0, '报名中'), (1, '已满员'), (2, '已结束'), (3, '已取消')] status = models.IntegerField(choices=STATUS_CHOICES, default=0) created_at = models.DateTimeField(auto_now_add=True) class Registration(models.Model): member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name='registrations') activity = models.ForeignKey(Activity, on_delete=models.CASCADE, related_name='registrations') REG_STATUS = [(0, '待审核'), (1, '已通过'), (2, '已取消')] status = models.IntegerField(choices=REG_STATUS, default=0) registered_at = models.DateTimeField(auto_now_add=True)

2.2 建模时的几个关键设计决策

第一,会员和用户要不要拆成两张表。我的做法是Member和Django自带的User用OneToOne关联,这样登录认证用User,业务信息用Member,互不干扰。如果一开始就图省事往User表里塞电话和部门字段,后面要扩展成员信息时就只能改Django自带的表,风险比较大。

第二,报名记录表为什么要单独建而不直接在Activity里加一个字段存名单。因为报名记录会越来越多,而且需要记录时间点和状态流转,如果用一个逗号分隔的字符串存在活动表里,后续做统计、做取消报名、做审核都会变成灾难。

第三,字段类型要认真选择。时间和容量这类字段尤其不能凑合,起始时间用DateTimeField,容量用IntegerField,报名状态用带choices的整型。好的数据模型可以让接口层省掉大量脏数据校验的代码。

3. 后端接口开发与角色权限控制的落地

模型定义好之后,就可以开始写接口了。接口层我用的是Django REST Framework的ModelViewSet,它能根据模型自动生成增删改查的接口,配合Router类注册路由,代码量比手写函数视图少太多。但自动生成的路由不能满足所有业务需求,比如报名接口要做名额校验,场地接口要按时间查可用性,这类逻辑需要在视图类里重写create或perform_create方法。

3.1 DRF序列化器与视图实现

以活动列表接口为例,前端列表页想展示的不只是活动名字,还要看到发起俱乐部、场地名称、状态、已报名人数。这时候序列化器里需要加一个只读字段,把外键对象的名称取出来:

class ActivitySerializer(serializers.ModelSerializer): club_name = serializers.CharField(source='club.name', read_only=True) venue_name = serializers.CharField(source='venue.name', read_only=True) registered_count = serializers.IntegerField(read_only=True) class Meta: model = Activity fields = ['id', 'title', 'club_name', 'venue_name', 'start_time', 'end_time', 'capacity', 'status', 'registered_count']

对应视图里,可以用一个注解把统计字段聚合进来:

class ActivityViewSet(viewsets.ModelViewSet): queryset = Activity.objects.annotate( registered_count=Count('registrations', filter=Q(registrations__status=1)) ).order_by('-start_time') serializer_class = ActivitySerializer

这个查询因为要统计的是已通过报名的数量,所以嵌套了条件聚合。如果直接用Count('registrations'),会把已取消的报名也数进去,页面上的"已报名"/"剩余名额"显示就会出现偏差。

3.2 会员和管理员看到的接口不一样

文体俱乐部的用户分成几类:普通访客只能看公告和活动详情,登录会员可以报名和查看自己的报名记录,运营管理员可以管理俱乐部、审核报名、维护场地数据。权限控制要从接口层就拦截,不能指望前端藏着页面就完事。

DRF自带的IsAuthenticated和IsAdminUser能覆盖大部分场景。活动的查询接口需要登录才能查看,报名的创建接口要确保请求里的member就是当前登录用户,管理员的接口用自定义权限类来限制:

class IsClubManagerOrAdmin(BasePermission): def has_permission(self, request, view): if request.user.is_staff: return True member = getattr(request.user, 'member', None) return member and member.level == 3

这个自定义权限的逻辑是:运营管理员直接放行,普通会员检查是不是俱乐部管理者。项目里权限粒度不用分得太细,否则维护成本会成倍增加,够用就好。

3.3 报名接口的名额校验逻辑

报名接口是最容易出并发问题的地方。会员同时点报名按钮,后端如果不加限制,可能出现报名人数量超过活动容量的情况。我的做法是在写入数据库之前,用事务锁住活动记录,重新读取已报名人数再判断:

def create(self, request, *args, **kwargs): activity_id = request.data.get('activity') with transaction.atomic(): activity = Activity.objects.select_for_update().get(id=activity_id) passed_count = Registration.objects.filter( activity=activity, status=1 ).count() if passed_count >= activity.capacity: return Response({'detail': '活动名额已满'}, status=status.HTTP_400_BAD_REQUEST) # 检查是否重复报名、是否已取消后重新报名等逻辑 serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) return Response(serializer.data, status=status.HTTP_201_CREATED)

select_for_update在MySQL的InnoDB引擎下会锁住这一行,避免同一时间两个请求都读到剩余名额是1,然后都放行写入。这类系统看似小,但这种细节如果忽略了,活动火爆的时候很容易出线上事故。

4. Vue前端如何对接Django后端接口

前端部分我用的Vue 3加Vite加Element Plus。项目初始化用npm create vue@latest,然后安装Element Plus和axios。Pycharm对Vue项目的支持也不错,直接在IDE里打开前端目录就能写代码,调试模式可以用代理把前端的请求转发到后端服务。

4.1 前端项目的目录组织

前端代码按模块分目录,把接口请求、页面视图、公共组件拆开。比如src/api/activity.js专门管理活动相关的接口调用,所有接口函数返回Promise,组件里直接await调用。这种做法的好处是把接口URL集中管理,后端路径一改,前端只要改一个文件。

import request from '@/utils/request' export function getActivityList(params) { return request({ url: '/api/activities/', method: 'get', params }) } export function createRegistration(data) { return request({ url: '/api/registrations/', method: 'post', data }) }

axios实例的封装要注意几个细节:baseURL设置为/api,通过后端配置的代理把请求转发到http://127.0.0.1:8000,这样开发时不会有跨域问题;响应拦截器里统一处理后端返回的错误码,比如会话过期时跳回登录页;请求拦截器里从本地存储取token放到Authorization头。

4.2 活动列表页面的数据渲染与状态刷新

拿活动列表页来说,页面挂载时调用getActivityList拉取数据,通过loading状态控制表格加载效果。表格里的"报名"按钮要根据活动状态和当前用户的报名状态来决定是否可点击,这个判断逻辑可以放在计算属性里,不要每次渲染时写一堆v-if嵌套。

<template> <div class="activity-list"> <el-table :data="tableData" v-loading="loading" stripe> <el-table-column prop="title" label="活动名称" /> <el-table-column prop="club_name" label="发起俱乐部" /> <el-table-column prop="venue_name" label="场地" /> <el-table-column prop="start_time" label="开始时间" /> <el-table-column label="状态" width="100"> <template #default="{ row }"> <el-tag :type="statusTagType(row.status)">{{ statusText(row.status) }}</el-tag> </template> </el-table-column> <el-table-column label="操作" width="150"> <template #default="{ row }"> <el-button type="primary" size="small" :disabled="!canRegister(row)" @click="handleRegister(row)" > 报名 </el-button> </template> </el-table-column> </el-table> </div> </template>

报名成功后要同步刷新两处数据:当前活动的已报名人数,按钮的禁用状态。最简单的做法是重新调用活动列表接口,但这样会有一定的请求开销。实测中我选择了手动更新列表里对应项的字段,这样页面响应更快,体验也更流畅。

4.3 Element Plus表格分页与后端DRF分页对齐

这里有个很典型的坑。DRF默认的分页接口返回结构是{ count, next, previous, results },而Element Plus的el-pagination组件给的参数是current-page和page-size,两者参数名完全不一样。如果不做适配,前端拿不到分页数据,表格直接空白。

我的处理方式是在api层把请求参数转成DRF能认的page和page_size,同时把响应体里的results和count取出来赋值给表格和分页组件。类似这种小适配几乎每个管理系统的前端都会写,习惯了就好。

5. 从开发环境到部署上线:联调阶段的坑和排查过程

开发阶段一切顺利往往意味着联调阶段要出点幺蛾子。这里把我实际经历过的几个问题按排查链路写下来,方便你对照检查。

5.1 CORS跨域配置不是加一行代码就完事

第一个问题是前端页面打开后接口请求全部被浏览器拦截。排查第一步打开浏览器开发者工具,看到的错误是CORS error。检查Django的settings.py,发现CORS_ALLOW_ALL_ORIGINS只设成了True,但访问的时候带了Authorization头。浏览器预检请求要识别这个自定义头,所以还需要配置:

CORS_ALLOW_ALL_ORIGINS = False CORS_ALLOWED_ORIGINS = [ 'http://localhost:5173', 'http://127.0.0.1:5173', ] CORS_ALLOW_HEADERS = [ 'authorization', 'content-type', 'x-requested-with', ]

这个问题的根因在于ACCESS-CONTROL-ALLOW-ORIGIN响应头如果没有返回,浏览器会单方面杀掉请求。配置完正确白名单之后,重新启动Django服务,前端请求就正常了。这里有个经验:不要图省事把ALLOW_ALL_ORIGINS开成True,尤其后续要带cookie认证的时候,这种宽松配置会直接导致凭证模式下禁止跨域通配符问题。

5.2 日期时间字段的时区差导致活动时间显示错位

第二个问题是活动发布后,前端页面上显示的时间比实际创建时间晚了8小时。排查过程是从接口返回数据开始的,先用接口调试工具直接看响应体的start_time字段,发现后端返回的时间确实是正确的UTC时间。问题出在Django的USE_TZ = True配置上——数据库里存的是UTC时间,序列化返回的也是UTC时间,但前端直接把它当本地时间渲染了。

解决方法是前端在展示之前做一次本地时区转换:

function formatLocalTime(utcTime) { const date = new Date(utcTime) return date.toLocaleString('zh-CN', { hour12: false }) }

注意,new Date(utcTime)拿到的是毫秒级时间戳,如果后端返回的字符串没有时区标记,JavaScript会默认按本地时区解析。稳妥的做法是后端在序列化时直接用带时区偏移的ISO格式,或者在接口里转为本地时间再返回。

5.3 页面表格操作完的数据同步问题

第三个问题比较常见。页面上审核通过一条报名记录后,按逻辑活动已报名人数要加1,但列表页因为缓存了旧数据,人数没有刷新。我当时第一版处理方式是审核成功后手动刷新整个活动列表,体验很生硬。后来改成在操作成功的回调里更新当前行数据:

const handleApprove = async (row) => { await updateRegistration(row.id, { status: 1 }) const target = tableData.value.find(item => item.id === row.id) if (target) { target.status = 1 } }

这种方式在数据量不大、单页操作的时候完全够用,还能避免整页刷新带来的闪烁。如果表格数据特别大,可以考虑用Vuex或Pinia来管理共享数据状态,但就这类系统而言,手动同步数据其实更实惠。

5.4 Django模板和Vue模板分隔符的冲突问题

这个坑在纯前后端分离的架构下不容易碰到,但如果你决定用Django自带模板渲染Vue入口文件(有些部署方案会这么干),就得注意Django模板引擎和Vue都把{{ }}作为变量插值语法。页面里只要混写了Django模板变量和Vue模板表达式,其中一个就会渲染失败。

我的建议是彻底避开这种混合渲染方式,前端构建后生成的dist/index.html直接由nginx独立托管,Django只提供API接口。两边各自独立部署,风格分隔干净,出问题的面也小了很多。

6. 部署到生产环境的完整步骤与常用配置

部署上线这个环节,很多初学者觉得难,实际上就是几件事:后端服务跑起来、前端静态文件被访问到、数据库迁移、进程守护。我用的方案是nginx + gunicorn + MySQL,这套组合在Linux服务器上非常常见。

6.1 后端服务的启动配置

先安装依赖并收集静态文件。Django项目的静态文件要让nginx能直接访问,需要在settings.py里配置STATIC_ROOT,然后执行python manage.py collectstatic把所有静态文件集中到一个目录。媒体文件也要单独指定目录,后续用户上传的头像、公告图片都会存到这里。

gunicorn启动命令里工作进程数可以先设为2到4,不要贪多。工作进程数不等于并发上限,进程太多反而会消耗内存。我用的是:

gunicorn myclub.wsgi:application --bind 127.0.0.1:8000 --workers 3 --timeout 120

这里绑定的地址是127.0.0.1而不是0.0.0.0,是因为访客请求不应该直接打到Django,应该统一由nginx转发。这样配置也方便nginx层统一做HTTPS、限流和日志记录。

6.2 nginx配置需要注意的点

nginx配置里有几个地方比较容易错。第一是前端路由的try_files配置,Vue用的是history模式路由,访问/activities/3这种地址时,nginx如果找不到对应文件返回404,所以必须加上try_files $uri $uri/ /index.html。第二是API接口的proxy_pass路径要注意尾部斜杠,配上代理头信息把客户端的真实IP和协议传过去。

server { listen 80; server_name your_domain_or_ip; # 前端静态文件 root /var/www/myclub_frontend/dist; index index.html; location / { 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 /media/ { alias /var/www/myclub_backend/media/; } }

6.3 部署完成后必须做的几件事

第一,把Django的DEBUG改成False,不然出错时会直接把堆栈信息暴露给访客,这不仅是信息泄露风险,也会暴露服务器目录结构。第二,用python manage.py migrate在正式数据库上执行迁移,别漏这一步。第三,测试一遍完整业务流程,从注册登录到报名审核,不要只看接口通不通就完事。

当时部署完我还发现一个性能层面的小问题:活动列表接口在数据量上来之后响应变慢,排查后定位是外键关联的字段没有加索引。Django在ForeignKey字段上默认会创建索引,但联合查询时还是建议给常用的过滤条件加上db_index=True,比如活动的start_time、报名记录的status和activity。

7. 项目的可扩展方向与二次开发建议

系统上线之后,运营过程中一定会不断有新的需求冒出来。基于文体俱乐部的实际业务,我梳理几个高频的方向,你可以根据自己的情况选着做。

第一个是接入更完整的通知机制。活动报名通过或者取消时,会员需要收到通知。最简单的做法是接入邮件或短信服务,技术上只涉及在报名状态变更时调用通知API,但要注意把通知发送做成异步任务,不然审核多条报名记录时接口会很慢。

第二个是活动回顾和照片分享。每次活动结束后上传照片和总结,沉淀下来就是俱乐部的历史档案。前端用一个画廊组件,后端加一个活动相册表,工作量不大但运营价值很高。

第三个是数据报表。运营管理员最关心的是每月活动数量、参与人次、热门俱乐部排名。可以每天定时统计写入一张汇总表,前端用图表组件展示趋势图,这样管理员的日常查看就不需要在接口层做复杂聚合计算。

我在做这套系统时,最大的感受是:管理系统类项目的难点不在某个单独的技术点上,而在如何把数据关系、接口设计、权限控制、前端交互这几层串起来。框架选型只是第一步,后续的模型设计、接口约定、联调排查才真正决定项目质量。你如果在做类似的项目,建议先从数据建模入手,把表结构理清楚,再动手写代码,整体效率会高不少。

最后分享一个小技巧:开发阶段启动Django和Vue时,写一个简单的启动脚本来同时拉起前后端服务,省得每次都要开两个终端手动敲命令。Windows下写一个.bat脚本,macOS或Linux下用.sh脚本,里面分别启动两个进程就行。这种小细节看着不起眼,但实际开发中天天用,确实能省下不少时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询