资讯详情

资讯详情

Python+Vue管理系统开发:Django与Flask选型实战与部署全记录

年底接了个文体俱乐部管理系统的开发需求和之前做的几个管理系统项目类似技术栈锁定在Python后端加Vue前端。不过在框架选型上团队内部一开始就分了两派一派说用Django自带Admin和管理后台开发效率高另一派说项目体量不大Flask轻量灵活接口自己定义更自由。两边说得都有道理但实际动工之后才发现选错框架带来的代价比想象中大得多。这篇文章我想从这套系统的完整实现过程讲起包括框架选型、数据建模、接口设计、前后端联调还有最后部署上线时踩过的坑。如果你也在用PythonVue做这类管理系统或者正在纠结Django和Flask怎么选这篇文章应该能给你省下不少时间。1. 先别急着写代码Django和Flask在管理系统场景下的真实差异既然标题里问到了django和flask那就先把这个最核心的选型问题说透。文体俱乐部管理系统看起来是个小项目但业务上该有的模块一个不少——会员信息管理、俱乐部活动发布、报名审核、场地预约、公告通知、数据统计。这些功能背后是大量关联表的增删改查还得考虑不同角色的权限范围。1.1 两类框架在管理系统场景下的实际表现我拿一个具体的业务点来说明差异。比如做一个活动报名功能会员在前端点报名按钮后端要完成几件事检查活动是否还有名额检查这个会员是否重复报名写入报名记录同时更新活动已报名人数。Django这边模型层用ORM直接定义外键和字段约束视图层用一个ViewSet加上DRFDjango REST Framework的序列化器就能把整套逻辑串起来而且自带的Admin后台可以直接给运营人员用不需要额外开发管理界面。Flask这边实现同样功能可以用SQLAlchemy做ORM但没有类似DRF这样约定俗成的接口方案路由、请求参数校验、序列化、分页、认证这些都要自己搭。不是说Flask做不到而是很多代码本来Django已经替你做好了换成Flask之后你得一个一个重新装配。我做过的几个管理系统项目里Flask项目的前期搭框架时间普遍比Django多出一倍以上。下面是两者在这类项目上的直观对比对比维度DjangoFlask自带后台管理自带Admin可直接复用无需要自己开发或接入第三方ORM和数据库迁移内置ORMmanage.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_deletemodels.CASCADE) real_name models.CharField(max_length50) phone models.CharField(max_length20) department models.CharField(max_length100) LEVEL_CHOICES [(1, 普通会员), (2, 高级会员), (3, 运营管理员)] level models.IntegerField(choicesLEVEL_CHOICES, default1) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return f{self.real_name}({self.user.username}) class Club(models.Model): name models.CharField(max_length100) category models.CharField(max_length50) # 篮球社、合唱团、摄影协会等 description models.TextField(blankTrue) manager models.ForeignKey(Member, on_deletemodels.SET_NULL, nullTrue, related_namemanaged_clubs) class Venue(models.Model): name models.CharField(max_length100) capacity models.IntegerField() is_available models.BooleanField(defaultTrue) class Activity(models.Model): title models.CharField(max_length200) club models.ForeignKey(Club, on_deletemodels.CASCADE, related_nameactivities) venue models.ForeignKey(Venue, on_deletemodels.SET_NULL, nullTrue) start_time models.DateTimeField() end_time models.DateTimeField() capacity models.IntegerField() STATUS_CHOICES [(0, 报名中), (1, 已满员), (2, 已结束), (3, 已取消)] status models.IntegerField(choicesSTATUS_CHOICES, default0) created_at models.DateTimeField(auto_now_addTrue) class Registration(models.Model): member models.ForeignKey(Member, on_deletemodels.CASCADE, related_nameregistrations) activity models.ForeignKey(Activity, on_deletemodels.CASCADE, related_nameregistrations) REG_STATUS [(0, 待审核), (1, 已通过), (2, 已取消)] status models.IntegerField(choicesREG_STATUS, default0) registered_at models.DateTimeField(auto_now_addTrue)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(sourceclub.name, read_onlyTrue) venue_name serializers.CharField(sourcevenue.name, read_onlyTrue) registered_count serializers.IntegerField(read_onlyTrue) 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_countCount(registrations, filterQ(registrations__status1)) ).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(idactivity_id) passed_count Registration.objects.filter( activityactivity, status1 ).count() if passed_count activity.capacity: return Response({detail: 活动名额已满}, statusstatus.HTTP_400_BAD_REQUEST) # 检查是否重复报名、是否已取消后重新报名等逻辑 serializer self.get_serializer(datarequest.data) serializer.is_valid(raise_exceptionTrue) self.perform_create(serializer) return Response(serializer.data, statusstatus.HTTP_201_CREATED)select_for_update在MySQL的InnoDB引擎下会锁住这一行避免同一时间两个请求都读到剩余名额是1然后都放行写入。这类系统看似小但这种细节如果忽略了活动火爆的时候很容易出线上事故。4. Vue前端如何对接Django后端接口前端部分我用的Vue 3加Vite加Element Plus。项目初始化用npm create vuelatest然后安装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 classactivity-list el-table :datatableData v-loadingloading stripe el-table-column proptitle label活动名称 / el-table-column propclub_name label发起俱乐部 / el-table-column propvenue_name label场地 / el-table-column propstart_time label开始时间 / el-table-column label状态 width100 template #default{ row } el-tag :typestatusTagType(row.status){{ statusText(row.status) }}/el-tag /template /el-table-column el-table-column label操作 width150 template #default{ row } el-button typeprimary sizesmall :disabled!canRegister(row) clickhandleRegister(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_indexTrue比如活动的start_time、报名记录的status和activity。7. 项目的可扩展方向与二次开发建议系统上线之后运营过程中一定会不断有新的需求冒出来。基于文体俱乐部的实际业务我梳理几个高频的方向你可以根据自己的情况选着做。第一个是接入更完整的通知机制。活动报名通过或者取消时会员需要收到通知。最简单的做法是接入邮件或短信服务技术上只涉及在报名状态变更时调用通知API但要注意把通知发送做成异步任务不然审核多条报名记录时接口会很慢。第二个是活动回顾和照片分享。每次活动结束后上传照片和总结沉淀下来就是俱乐部的历史档案。前端用一个画廊组件后端加一个活动相册表工作量不大但运营价值很高。第三个是数据报表。运营管理员最关心的是每月活动数量、参与人次、热门俱乐部排名。可以每天定时统计写入一张汇总表前端用图表组件展示趋势图这样管理员的日常查看就不需要在接口层做复杂聚合计算。我在做这套系统时最大的感受是管理系统类项目的难点不在某个单独的技术点上而在如何把数据关系、接口设计、权限控制、前端交互这几层串起来。框架选型只是第一步后续的模型设计、接口约定、联调排查才真正决定项目质量。你如果在做类似的项目建议先从数据建模入手把表结构理清楚再动手写代码整体效率会高不少。最后分享一个小技巧开发阶段启动Django和Vue时写一个简单的启动脚本来同时拉起前后端服务省得每次都要开两个终端手动敲命令。Windows下写一个.bat脚本macOS或Linux下用.sh脚本里面分别启动两个进程就行。这种小细节看着不起眼但实际开发中天天用确实能省下不少时间。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →