基于Python Django与Vue的航空公司管理系统开发实践
1. 为什么是PythonVue航司管理系统的需求拆解与选型逻辑1.1 先从业务说起航司管理网站到底要管什么接到这个项目的时候需求方给我的描述其实很简单要一个航空公司管理网站能管航班、管乘客、管订单最好还能看几眼报表。但简单背后涉及的东西一点都不少。我习惯先把业务对象捋清楚再谈技术因为后面建表、写接口、画页面全都要靠这份清单。一个典型的航空公司管理系统核心业务对象大致是这几类航班信息航班号、起降机场、起飞到达时间、机型、票价、舱位余量、航班状态计划、起飞、到达、取消乘客信息姓名、证件号码、联系电话必要时还要区分乘客类型成人/儿童订单信息订单号、所属航班、乘客列表、座位等级、实付金额、订单状态已支付、已出票、已退票基础数据机场信息、机型信息、航线信息统计报表上座率、每日营收、热门航线排行等。从使用角色上看这类网站通常分两类一类是给普通用户查航班、订票用的门户另一类是给航司内部员工做运营管理的后台。项目标题里写的是管理网站所以我默认以内部管理系统为主兼顾查询和订票功能来设计。如果你拿到的是课程设计或毕设题目这样做也最稳妥——业务面覆盖广技术上该练的都练到了。有了这份业务清单再回头看技术选型思路就会清楚很多这套系统表多、关联复杂、后台管理操作频繁还要有统计报表。这不是一个几十行的脚本而是一个需要长期维护的中型Web系统选型必须围绕快速搭建、稳定迭代、容易招人接手这几个关键词展开。1.2 Django和Flask之争这次我为什么选Django项目标题里同时出现了Django和Flask不少人第一反应是这俩到底用哪个我的答案是做这类重后台、多表关联的管理系统默认走Django如果项目只是轻量API服务再考虑Flask。这不是说Flask不好而是两者定位不同硬要互相替换只会让自己多写一堆本来不用写的代码。先看一组直观对比维度DjangoFlaskORM自带且支持关系映射、迁移、查询集懒加载不自带通常配Flask-SQLAlchemyAdmin后台自带注册模型即可生成可用的管理界面没有现成方案需要自己写或用第三方插件用户认证自带User模型与权限体系需要配合Flask-Login、PyJWT等自行组装数据库迁移内置makemigrations/migrate需要Flask-Migrate学习曲线略陡但体系完整平坦但坑要自己填适合场景后台管理、内容系统、业务复杂的Web应用微服务接口、简单原型、定制化高的项目就这个航司项目来说Django的ORM能让我用很少的代码完成航班、订单、乘客之间的复杂关联查询Django Admin在开发阶段直接帮我顶了一个简易后台验收演示的时候也方便用户登录和Token认证用simplejwt一接就行。整套东西是电池齐全的不用到处找轮子。那Flask在这篇文章里还提不提提而且要重点说。很多课程设计或者个人项目的题目习惯写成PythonFlask是因为Flask上手快、代码量少适合答辩演示。我的处理方式是主体用Django实现但搞清楚如果换成Flask同一个业务分布要怎么改写。这样既拿到了Django开发效率的红利也保留了Flask方案的对照参考。第三节和第五节我会各留一段讲这个问题。1.3 Vue在这套系统里解决什么问题后端定了Python前端选Vue算是一个很自然的决定。Vue在国内社区活跃、中文资料多配Element Plus组件库之后后台管理类界面的开发速度非常快。具体到航司管理系统Vue解决的核心问题有三个第一交互体验。传统Django模板渲染是整页刷新操作一下航班列表就要重新加载整个页面。Vue做成单页应用SPA后切换路由只是局部刷新组件体感上流畅一大截而且查航班、筛选日期这类高频操作再也不用等白屏。第二组件复用。航班搜索框、分页表格、订单状态标签这些在管理系统里到处出现。Vue把每个模块拆成组件写一次到处用后期改样式只改一处比复制粘贴模板舒服得多。第三前后端解耦。后端只输出JSON接口前端只管渲染页面两边只要把接口约定好可以并行开发。我在这个项目里就是先定接口文档后端撸Django前端同时搭组件最后联调一天就收工。可能有人问Vue项目本身要在Node环境跑PyCharm对这种前后端混合项目支持好吗这个我在下一节详细说因为初始化环节确实藏了几个坑。2. 初始化细节PyCharm虚拟环境、Django项目结构与应用划分2.1 PyCharm里创建项目的三个易错点先声明一下我用的PyCharm Professional版但下面这套流程换成社区版也能走通只是有些前端插件和数据库工具没有不影响核心开发。**第一个易错点虚拟环境没选对。**很多新手在PyCharm里直接新建项目解释器默认选了全局的Python后续用pip装Django装到了全局环境里。结果就是多个项目互相污染今天装的包明天另一个项目也用得上再改天一个项目升级包另一个项目就莫名其妙的跑不起来了。正确做法是创建项目时选择New environment using Virtualenv指定一个专用的Python解释器版本这个项目我用的是Python 3.10兼容性最稳后面所有依赖都装进这个虚拟环境。**第二个易错点用IDE向导创建Django项目不如命令行干净。**PyCharm的Django项目向导不是不好而是它会产生一些IDE特有的配置文件初学者容易搞不清哪些文件是项目的、哪些是工具的。我习惯直接在Terminal里操作mkdir airline_management cd airline_management python -m venv venv # Windows激活方式 venv\Scripts\activate # Linux/macOS激活方式 source venv/bin/activate pip install django djangorestframework django-cors-headers djangorestframework-simplejwt django-admin startproject config .项目目录放到上一级生成的结构是这样airline_management/ ├── config/ # 项目配置文件目录 │ ├── settings.py │ ├── urls.py │ ├── asgi.py │ └── wsgi.py ├── manage.py ├── venv/用config而不是airline_management当项目配置目录名是我个人习惯。因为项目根目录本身已经叫airline_management了再嵌套一层同名目录后面import路径容易绕晕。**第三个易错点数据库驱动问题。**如果计划开发阶段就上MySQLWindows用户大概率会在安装mysqlclient时卡住需要装一堆编译工具或者下载预编译whl包非常劝退。我的建议是开发阶段先用默认的SQLite模型设计、接口调试全都跑通之后部署前再切换MySQL。SQLite和MySQL在Django里的切换成本很低只需改settings里的DATABASES配置和后端驱动依赖业务代码完全不用动。2.2 Django项目结构与应用划分Django的哲学是一个项目包含多个应用app每个app负责一块独立业务。我在这个航司系统里划分了四个app边界清清楚楚python manage.py startapp flights python manage.py startapp orders python manage.py startapp users python manage.py startapp statsflights航班、机型、机场。负责航班查询、航班增删改查、航班状态维护orders订单、乘客。负责订票、退票、订单列表与详情users用户登录认证。可以扩展员工信息、角色权限stats聚合统计报表只读接口服务前端图表。这四个app划分的逻辑是高内聚低耦合。flights不依赖ordersorders里的外键引用flights的模型users独立stats只读其他app的数据做聚合。这样一个人开发也好多人协作也好改一个模块不容易误伤另一个模块。创建完app后别忘了去config/settings.py的INSTALLED_APPS里注册同时顺手把DRF、corsheaders也加进去INSTALLED_APPS [ # Django自带app略 rest_framework, rest_framework_simplejwt, corsheaders, flights, orders, users, stats, ]另外settings里有几个关键点时区要设成Asia/Shanghai、USE_TZ保持True这个组合的含义后面有专门的坑讲允许所有host开发期设为[*]部署时再收紧加一个简单的CORS配置允许前端开发服务器访问。2.3 把Vue前端脚手架搭起来后端初始化完成之后开一个终端创建前端项目。我用的是Vite而不是官方Vue CLI原因很简单Vite启动快、配置少、生态已经是当前主流。我用Node 18以上的环境执行npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router pinia element-plus axios echarts这一套装完前端的基础依赖就齐了。frontend目录和项目根目录平级后面Vite做代理转发、部署时Nginx反代都方便。目录结构我按功能模块来组织frontend/src/ ├── api/ # 所有接口请求封装按业务模块分文件 ├── components/ # 公共组件表格封装、状态标签等 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理用户信息、Token ├── views/ # 页面级组件 │ ├── login/ │ ├── dashboard/ │ ├── flights/ │ ├── orders/ │ └── stats/ └── utils/ # axios实例、格式化工具pyCharm打开这个混合项目时建议直接把airline_management作为根目录打开前后端代码都在同一个工作区里切换文件方便。如果PyCharm提示安装Vue插件装一下就好代码高亮和语法检查会舒服很多。3. 后端实现数据模型、API与Token认证的完整落地3.1 数据模型设计航班系统的四张核心表业务表的设计是这套系统的地基。我建议动手写代码前先把ER图实体关系图画出来哪怕用纸画也行。航司系统的核心表关系不复杂但字段容易漏比如航班状态、舱位余量这种运营天天要看的数据一开始没设计进去后面补字段很麻烦。我最终的模型设计简化后如下# flights/models.py from django.db import models class Aircraft(models.Model): code models.CharField(机型编码, max_length10, uniqueTrue) name models.CharField(机型名称, max_length50) economy_seats models.IntegerField(经济舱座位数, default180) business_seats models.IntegerField(公务舱座位数, default20) def total_seats(self): return self.economy_seats self.business_seats def __str__(self): return f{self.code} {self.name} class Flight(models.Model): STATUS_CHOICES [ (scheduled, 计划), (boarding, 登机), (departed, 已起飞), (arrived, 已到达), (cancelled, 已取消), ] flight_number models.CharField(航班号, max_length10, uniqueTrue) aircraft models.ForeignKey(Aircraft, on_deletemodels.PROTECT, verbose_name执飞机型) departure_city models.CharField(出发城市, max_length30) arrival_city models.CharField(到达城市, max_length30) departure_time models.DateTimeField(起飞时间) arrival_time models.DateTimeField(到达时间) economy_price models.DecimalField(经济舱票价, max_digits8, decimal_places2) business_price models.DecimalField(公务舱票价, max_digits8, decimal_places2) status models.CharField(航班状态, max_length20, choicesSTATUS_CHOICES, defaultscheduled) sold_seats models.IntegerField(已售座位数, default0) class Meta: ordering [departure_time] def remaining_seats(self, cabin_classeconomy): # 简化计算不区分舱位时直接用总座位减已售 return self.aircraft.total_seats() - self.sold_seats# orders/models.py from django.db import models from django.conf import settings from flights.models import Flight class Passenger(models.Model): name models.CharField(姓名, max_length50) id_card models.CharField(证件号, max_length30) phone models.CharField(联系电话, max_length20) class Meta: unique_together (id_card, phone) def __str__(self): return self.name class Order(models.Model): STATUS_CHOICES [ (paid, 已支付), (ticketed, 已出票), (refunded, 已退票), (cancelled, 已取消), ] order_no models.CharField(订单号, max_length30, uniqueTrue) user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, verbose_name下单用户) flight models.ForeignKey(Flight, on_deletemodels.PROTECT, verbose_name航班) passengers models.ManyToManyField(Passenger, verbose_name乘客) cabin_class models.CharField(舱位等级, max_length10, choices[(economy, 经济舱), (business, 公务舱)]) total_amount models.DecimalField(订单金额, max_digits10, decimal_places2) status models.CharField(订单状态, max_length20, choicesSTATUS_CHOICES, defaultpaid) created_at models.DateTimeField(下单时间, auto_now_addTrue) class Meta: ordering [-created_at]简单解释几个设计决策第一订单号不要用自增ID。虽然自增主键能用但业务上订单号通常需要体现可读性比如加日期前缀20250612xxxxxx后面客服查订单、对账单都方便。我这边是下单时用日期加随机数生成。第二乘客和订单是多对多关系。一个订单可能包含多名乘客比如一家人出行一个乘客也可能多次订票。用中间表是最灵活的方案查某乘客坐过哪些航班也是一条查询搞定。第三航班已售座位数字段单独存。虽然算余票可以总座位 - 已售实时算但每次用户查航班都去count订单表量一大数据库就扛不住了。所以在Flight表上冗余一个sold_seats字段下单时在Django的F()表达式配合事务里做原子更新既保证不超卖查询又只读一个整数字段性能好很多。这也算是用空间换时间的经典做法。唯一需要注意的坑是on_deletemodels.PROTECT意思是有订单引用的航班不允许直接删。这个约束是业务上必须的——航班删了订单怎么办后端会直接拒绝删除提示前端改为取消航班操作。3.2 用DRF写航班查询与订单API模型建好之后写REST API。Django REST FrameworkDRF的核心思路是序列化器定义数据进出格式视图集定义增删改查行为路由自动注册URL。三个文件一写接口就有了。航班查询接口的序列化器# flights/serializers.py from rest_framework import serializers from .models import Flight, Aircraft class FlightSerializer(serializers.ModelSerializer): aircraft_code serializers.CharField(sourceaircraft.code, read_onlyTrue) remaining_seats serializers.SerializerMethodField() class Meta: model Flight fields [id, flight_number, aircraft_code, departure_city, arrival_city, departure_time, arrival_time, economy_price, business_price, status, remaining_seats] def get_remaining_seats(self, obj): return obj.remaining_seats()视图集加上查询参数支持# flights/views.py from rest_framework import viewsets, filters from django_filters.rest_framework import DjangoFilterBackend from .models import Flight from .serializers import FlightSerializer class FlightViewSet(viewsets.ModelViewSet): queryset Flight.objects.select_related(aircraft).all() serializer_class FlightSerializer filter_backends [DjangoFilterBackend, filters.SearchFilter] filterset_fields [departure_city, arrival_city, status] search_fields [flight_number]这里用select_related(aircraft)做一个预取是因为序列化时要用到机型编号不预取的话每条航班都会多出一条查询机型的SQL列表页数据量一大数据库直接被N1查询拖垮。这是一个非常经典的性能优化点。路由注册# config/urls.py from rest_framework.routers import DefaultRouter from flights.views import FlightViewSet router DefaultRouter() router.register(flights, FlightViewSet) urlpatterns [ path(api/, include(router.urls)), ]完成之后访问/api/flights/?departure_city北京arrival_city上海就能拿到筛选后的航班列表。DjangoFilterBackend还自动支持?statuscancelled这种精确过滤后端几乎不用写额外代码。订单接口的核心是创建订单这个动作。这里不能只做一个普通的POST它涉及三步业务逻辑生成订单号、校验余票、更新sold_seats。我把这步写在序列化器的create方法里用事务包住# orders/serializers.py 部分代码 from django.db import transaction from django.utils import timezone from rest_framework import serializers from .models import Order, Passenger from flights.models import Flight class OrderCreateSerializer(serializers.ModelSerializer): passengers PassengerSerializer(manyTrue) class Meta: model Order fields [flight, cabin_class, passengers] transaction.atomic def create(self, validated_data): flight validated_data[flight] passengers_data validated_data.pop(passengers) cabin_class validated_data[cabin_class] if flight.sold_seats flight.aircraft.total_seats(): raise serializers.ValidationError(该航班已满员无法预订) order_no timezone.now().strftime(%Y%m%d%H%M%S) str(random.randint(1000, 9999)) total_amount flight.business_price if cabin_class business else flight.economy_price order Order.objects.create( order_noorder_no, userself.context[request].user, flightflight, cabin_classcabin_class, total_amounttotal_amount, ) passengers [Passenger.objects.create(**p) for p in passengers_data] order.passengers.set(passengers) # 原子更新余票避免并发超卖 Flight.objects.filter(pkflight.pk).update(sold_seatsmodels.F(sold_seats) len(passengers)) return order这段里最有价值的就是那行update(sold_seatsF(sold_seats) len(passengers))。这是一个数据库层面的原子操作两个用户同时抢最后一张票时数据库会串行化这个UPDATE而不是各自先SELECT再UPDATE——前者安全后者必超卖。这种细节是看起来能跑和真正能上线的分水岭。3.3 Token认证与权限控制的落地管理系统必须要身份认证不然任何人都能删航班改价格。我选择用JWTJSON Web Token方案具体库是restframework-simplejwt。JWT的思路是用户登录成功后后端签发一个带签名和过期时间的Token前端每次请求带上它后端验签通过即认为用户已登录。配置很简单# config/settings.py 追加 from datetime import timedelta 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: 10, } SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(hours2), REFRESH_TOKEN_LIFETIME: timedelta(days7), }然后在urls.py里挂JWT的登录和刷新接口from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns [ path(api/auth/login/, TokenObtainPairView.as_view(), nametoken_obtain_pair), path(api/auth/refresh/, TokenRefreshView.as_view(), nametoken_refresh), ]权限控制上默认全局要求登录。航班的查询接口虽然是查航班但管理系统里也应该要求登录无需匿名开放。航班和订单的写操作可以进一步限制为只有管理员角色from rest_framework.permissions import IsAdminUser class FlightViewSet(viewsets.ModelViewSet): permission_classes [IsAdminUser] # 覆盖全局配置只允许管理员操作如果不想用Django自带的User表也可以扩展自定义用户模型继承AbstractUser加role字段区分管理员和运营人员。毕设演示时建议做这个扩展答辩老师问到权限设计能多讲几句。3.4 一个容易忽略的坑时区时区问题看着小坑起人来一点不含糊。典型现象就是我开发时把航班起飞时间存进去前端一看时间整整少了8小时。原因在于Django的USE_TZ True会把所有datetime按UTC时间存进数据库而中国在东八区浏览器里用本地时间解析就错位了。解决方案有两层。后端层面settings里设置TIME_ZONE Asia/Shanghai同时USE_TZ True。此时DRF输出的时间字段如果开启了datetime格式化会按UTC输出还是按本地时区输出答案是取决于序列化器有没有指定时区默认情况下DRF会用Django的TIME_ZONE设置来格式化输出。前端拿到的是带时区的ISO字符串再用new Date()格式化一次就会转成浏览器本地时间这个坑就绕过去了。另一种更稳妥的做法是后端直接不输出时间而输出时间戳整数前端自己格式化。时间戳没有时区歧义只是不够直观调试时看着费劲。我的建议是接口输出ISO字符串前端用dayjs统一格式化显示这样最自然。4. 前端实现Vue路由、状态管理与航班业务页面搭建4.1 前端项目骨架路由和状态管理先想清楚前端起步之前先把路由和状态管理设计好比直接写页面省心得多。这个系统的路由表大概是这个结构// frontend/src/router/index.js import { createRouter, createWebHistory } from vue-router; const routes [ { path: /login, component: () import(../views/login/LoginView.vue) }, { path: /, component: () import(../layouts/BasicLayout.vue), redirect: /dashboard, children: [ { path: dashboard, component: () import(../views/dashboard/DashboardView.vue), meta: { title: 仪表盘 } }, { path: flights, component: () import(../views/flights/FlightListView.vue), meta: { title: 航班管理 } }, { path: orders, component: () import(../views/orders/OrderListView.vue), meta: { title: 订单管理 } }, { path: stats, component: () import(../views/stats/StatsView.vue), meta: { title: 统计报表 } }, ], }, ];路由守卫里做一个非常关键的判断没有Token就踢回登录页。这是前端安全的第一道防线虽然真正的安全在后端接口但前端拦截能大幅改善体验避免用户眼睁睁看着接口报401。router.beforeEach((to, from, next) { const token localStorage.getItem(access_token); if (to.path ! /login !token) { next(/login); } else { next(); } });pinia负责存用户信息和当前登录状态。Token放localStorage里可以避免刷新页面就丢登录态但注意JWT是明文可解码的千万别把密码之类敏感信息也存进去。axios实例封装是前端联调的枢纽核心代码如下// frontend/src/utils/request.js import axios from axios; import { ElMessage } from element-plus; import router from ../router; const request axios.create({ baseURL: /api, timeout: 15000, }); 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); } );统一在这里加请求头、统一处理错误码业务页面里只管调接口拿数据代码干净很多。4.2 航班管理页从列表到表单的Element Plus写法后台管理页面90%的形态都是表格搜索弹窗表单航班管理页就是最典型的例子。用Element Plus来写前三十分钟基本就能把框架搭完。核心就两件事表格渲染和表单提交。列表页数据结构大概是!-- frontend/src/views/flights/FlightListView.vue 核心片段 -- template div el-form :inlinetrue :modelqueryParams el-form-item label出发城市 el-input v-modelqueryParams.departure_city placeholder请输入 clearable / /el-form-item el-form-item label到达城市 el-input v-modelqueryParams.arrival_city placeholder请输入 clearable / /el-form-item el-form-item el-button typeprimary clickfetchList查询/el-button el-button typesuccess clickopenDialog新增航班/el-button /el-form-item /el-form el-table :datatableData v-loadingloading border stripe el-table-column propflight_number label航班号 width120 / el-table-column propdeparture_city label出发城市 / el-table-column proparrival_city label到达城市 / el-table-column label起飞时间 width180 template #default{ row }{{ formatTime(row.departure_time) }}/template /el-table-column el-table-column propeconomy_price label经济舱票价 width110 / el-table-column propstatus label状态 width100 template #default{ row } el-tag :typestatusType(row.status){{ statusLabel(row.status) }}/el-tag /template /el-table-column el-table-column propremaining_seats label余票数 width90 / el-table-column label操作 width150 fixedright template #default{ row } el-button link typeprimary clickopenDialog(row)编辑/el-button el-button link typedanger clickcancelFlight(row)取消航班/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequeryParams.page :page-sizequeryParams.page_size :totaltotal layouttotal, prev, pager, next current-changefetchList / /div /template新增/编辑航班用el-dialog加el-form弹窗完成。表单里的机型选项从/api/aircrafts/下拉加载起降城市用输入框加校验必填、不同城市时间选择用el-date-picker的datetime类型。这套写法本身不难但我想强调两点经验第一把表格的loading态做出来。接口快的时候感觉不到区别但航班列表一旦加了筛选条件或者后端在重新生成统计数据没有loading的页面就会让用户觉得点了没反应。v-loading一行代码的事体验提升是实打实的。第二删除操作别用DELETE用取消。航班的业务规则是有订单关联的航班不允许删除所以我在前端也只暴露取消航班操作本质是PATCH把status改为cancelled。这样既符合业务语义也避免了后端PROTECT外键导致的报错弹窗。做管理系统时一定要记住操作按钮的名字要跟业务语言一致不要跟技术术语一致。用户不关心DELETE还是PATCH他们只关心取消航班按钮点了能不能用。4.3 统计报表用ECharts展示上座率与营收统计页直接裸写表格太干瘪了VueECharts的组合可以很快做出漂亮的图表。后端在stats这个app里提供聚合接口前端每加载一次页面就调一次接口拿数据丢给ECharts渲染。后端的聚合统计用Django ORM的annotate就能完成不需要额外引入复杂框架。比如按月统计订单营收# stats/views.py from django.db.models import Sum, Count, F from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response from orders.models import Order class RevenueStatsView(APIView): def get(self, request): current_year timezone.now().year monthly_revenue ( Order.objects .filter(created_at__yearcurrent_year, status__in[paid, ticketed]) .annotate(monthF(created_at__month)) .values(month) .annotate(totalSum(total_amount)) .order_by(month) ) return Response(list(monthly_revenue))前端ECharts的配置则是经典的柱状图。这里有个小技巧created_at__month在SQLite和MySQL里都能用ORM帮我们屏蔽了数据库差异这是不用Flask直接拼SQL的一个好处。统计页我还加了一块热门航线Top5的横向条形图思路完全一样只是聚合字段改成departure_city和arrival_city组合。可视化图表对管理系统的加分效果非常明显演示时领导一眼就能看懂运营状况比甩一张表格有说服力得多。4.4 前后端联调时的CORS配置与本地代理前后端分离开发时前端跑在5173端口Vite默认后端跑在8000端口Django默认浏览器直接从前端发请求到后端会触发跨域问题。解决跨域有两个环节要配合。第一个环节是Django后端开CORS白名单。安装django-cors-headers后settings里配置CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]CORS_ALLOWED_ORIGINS指定允许跨域访问的来源。开发期也可以图省事用CORS_ALLOW_ALL_ORIGINS True但上线前一定要改成白名单否则任何网站都能调你的接口。第二个环节是Vite开发服务器做代理。更优雅的方式是让前端请求同源由Vite帮忙转发到后端。在frontend/vite.config.js里配置export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true, }, }, }, });这样前端axios的baseURL直接写/api就行浏览器看到的请求是发给5173端口的同源请求没有跨域问题。这个方案开发期比CORS更干净因为上线后Nginx也是这么干的——前端静态资源和/api反代指向后端前端代码完全不用改。5. 联调、部署与实测踩坑这些细节文档里不会写5.1 排查链路本地联调时接口报错怎么一步步定位前后端联调的第一天我预期是顺利的结果一上来就遇到航班列表加载不出来。我没有急着改代码而是按一套固定排查链路走这也是我觉得新手最应该养成的习惯。链路是浏览器Network面板看请求状态 - 后端日志看异常堆栈 - 数据库工具验证数据。当时的现象是浏览器控制台报500错误。第一步打开Network面板确认接口路径是/api/flights/请求头带了Authorization: Bearer xxx响应体里有一段DRF的报错信息。第二步切到PyCharm的Run窗口找到了完整的堆栈——原来是序列化器里get_remaining_seats访问了self.aircraft.total_seats()而aircraft是外键列表查询时没做select_related预取由于某条测试数据的机型被删成了NULL访问到了空对象。第三步确认数据后修复一是在视图queryset里加上select_related(aircraft)二是给外键加on_deletePROTECT阻止未来出现空外键数据。这套链路里的关键点是用后端日志而不是用肉眼看代码去找问题。DRF和Django的报错信息其实非常详细会告诉你在哪个文件哪一行出了什么错跟着堆栈走90%的问题都能定位。5.2 如果换成Flask同样的业务怎么改虽然主体用了Django但我还是想认真回答标题里Flask这个选项。如果你因为课程要求或者个人偏好必须用Flask同一个航司管理系统应该怎么组织我给出一个对照方案。Flask的核心理念是微框架不绑定ORM、不绑定认证方案所有组件自己选型组装。同样的业务技术栈会变成Flask Flask-SQLAlchemyORM负责模型和查询Flask-Migrate数据库迁移PyJWT生成和验证TokenFlask-CORS解决跨域。用一个蓝图Blueprint组织航班相关接口大概长这样# flask_version/flights.py简化示例 from flask import Blueprint, request, jsonify from flask_jwt_extended import jwt_required from .models import Flight from .schemas import flight_schema, flights_schema flights_bp Blueprint(flights, __name__, url_prefix/api/flights) flights_bp.get(/) jwt_required() def list_flights(): departure request.args.get(departure_city) query Flight.query if departure: query query.filter(Flight.departure_city departure) flights query.all() return jsonify(flights_schema.dump(flights))对比下来Flask版本最明显的差异是没有Django Admin了想给运营人员一个管理界面要么自己写HTML页面要么再配一个如Flask-Admin的插件但成熟度相比Django Admin差一截。这也是我一再强调重后台场景选Django的原因。Flask的真正优势在于你就需要一个提供几个JSON接口的轻服务时它启动快、依赖少、部署方便放在微服务架构里很舒服。如果你是因为项目标题里同时出现Django和Flask才纠结的我的建议很直接用Django做主体写清楚技术选型对比然后口头或文档里说明如果用Flask蓝图怎么划分、模型怎么组织。这比硬用Flask拼一个Django Admin同等体验的后台要明智得多。5.3 部署时的几个要点本地跑通只是第一步真正能给别人用还得部署到服务器上。这个项目的部署架构很标准Nginx托管Vue构建出来的静态文件同时把/api开头的请求反代到Django。前端构建cd frontend npm run build构建产物在frontend/dist目录把里面的文件扔到服务器Nginx的站点目录就行。Django侧需要做几件事第一settings.py里改掉DEBUG False配置ALLOWED_HOSTS为你的域名或服务器IP。这一步忘记改页面会显示500且访问不了任何接口这是部署最常碰到的第一个报错。第二执行collectstatic收集静态文件Django Admin等后台页面需要这些文件才能正常显示样式。第三数据库切换。SQLite过渡到MySQL的步骤是装驱动、建库、改DATABASES配置、执行migrate、导数据。Django的ORM保证业务代码基本不用动但有一个坑要注意MySQL不支持SQLite的某些字段操作比如修改字段类型会提示需要手动加转换语句所以模型结构尽量在上线前定死别上线后再频繁改字段。第四生产环境用gunicorn启动Django服务别再用runserver。runserver是开发服务器既慢又不安全。启动命令大概是gunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 3Nginx反代配置片段server { listen 80; server_name your_domain.com; location / { root /var/www/frontend/dist; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里try_files $uri $uri/ /index.html是SPA部署的关键前端路由是history模式用户直接访问/flights这个地址时Nginx要返回index.html由Vue再接管路由渲染对应页面。5.4 我在这个项目里的实际体会项目收尾后复盘有几个点想分享给后来者。**先画ER图再建表这句话是真的有用。**我第一版模型设计时偷懒直接在代码里敲字段结果航班表和订单表关联做了一半才发现漏了乘客和订单多对多这个关键关系回炉重改了一次。后来老老实实花半小时把ER图画完所有表关系一目了然建表几乎一次通过。做任何管理系统ER图的时间绝对不能省。**接口文档先行的收益远比想象中大。**我当时用简单的方式列了个接口清单路径、方法、请求参数、返回字段发给协作的前端同学。虽然只是个Markdown文档但联调时两边不存在我以为你返回了这个字段的误会至少少吵了三回架。**权限设计一开始就要做别指望后面补。**刚开始我把所有接口都设成无认证可访问想着反正本地调试方便。结果加上JWT认证那天前端所有请求都要跟着改忙活了一个晚上。再重来一次的话我会在项目初始化时就带上认证哪怕先写死一个测试账号。**版本管理习惯很重要。**这个项目全程用Git管理每个功能模块一个commit比如feat: 航班列表接口、fix: 修复订单余票并发问题。后面排查bug需要回退版本或者演示完想找某个历史状态Git能帮上大忙。养成这个习惯之后做任何项目心里都有底。对我个人来说这个航司管理系统最大收获不在于用了多少框架而在于完整走了一遍业务梳理 - 表设计 - 后端API - 前端页面 - 联调测试 - 部署上线的全流程。如果你也是第一次做前后端分离的项目这套流程的每个环节都值得你亲手过一遍踩过的坑都会变成下次开发的直觉。希望这篇记录能帮你少走几步弯路。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →