☰
SpringBoot+Vue全栈实战:航班进出港管理系统设计与部署详解
2026/9/28 12:21:11 网站建设 项目流程

好的,我在整理手头这套源码的时候,脑子里不断浮现出当年自己在航司项目中踩过的各种坑。这套基于SpringBoot+Vue的航班进出港管理系统,虽然是个典型的全栈教学项目,但从数据库设计、接口规划到前端交互,几乎把企业级开发中常用的那套东西都过了一遍。如果你正在找一套能真正跑起来、能写进简历、能应付答辩的系统源码,这篇拆解应该能帮你省下大量摸索的时间。

说实话,“航班进出港管理”这个概念乍一听挺唬人,但实际上拆开来看,它就是一套带实时状态流转的CRUD系统,只不过业务对象从普通的“商品”“订单”换成了“航班”,并且多了一些统计看板和状态约束。做这种系统最忌讳一上来就写代码,先花半天把业务梳理清楚,后面几天会顺畅得多。我会顺着这个思路,把项目从技术选型、数据库设计、后端实现到前端页面、部署排查完整讲一遍。

1. 项目定位与技术选型

1.1 为什么是SpringBoot+Vue这套组合

这套技术栈现在几乎成了中小型管理系统的事实标准,原因很实际:SpringBoot把SSM时代那些繁琐的XML配置全部干掉,内嵌Tomcat,一个main方法就能启动服务,特别适合那种需要快速交付、快速迭代的管理后台。而Vue作为前端框架,响应式数据绑定和组件化开发能把页面的交互逻辑理得很清晰,尤其是航班列表这种需要频繁筛选、排序、状态切换的页面,用Vue写起来比传统的jQuery省太多事。

有人可能会问,为什么不用更流行的SpringCloud微服务,或者前端用React?我的观点是:这个项目的复杂度决定了它不需要微服务那一套注册中心、网关、配置中心的重量级架构,单机单体应用加前后端分离已经足够。微服务是要解决“复杂系统拆分”的问题,而不是为了炫技。同样,Vue上手曲线比React平缓,中文资料多,对于学生或者刚转行的开发者来说,踩坑成本低很多。这套组合最大的优势是“生态成熟、资料全、面试能聊的东西多”,这才是选型的核心逻辑。

1.2 核心需求拆解:航班进出港到底管什么

如果只做增删改查,这个项目是没有灵魂的。一份合格的进出港管理系统,至少要覆盖这几个业务场景:

  • 航班基础信息的维护:航班号、航空公司、机型、起降机场、计划起降时间、值机柜台、登机口等。
  • 进港与出港的分类管理:进港航班重点看“到达时间、停靠廊桥、行李转盘”,出港航班重点看“值机截止时间、登机状态、起飞状态”。
  • 航班动态状态流转:从计划、值机、登机、起飞到落地,状态的每一步变更都要有时间记录,而且不是随便哪个状态都能乱跳的。
  • 旅客数据与统计看板:当天进出港航班总数、旅客吞吐量、准点率、延误航班列表,这些数据要能在首页直观展示。
  • 多条件检索:按航班号、起降日期、机场、状态等条件组合查询,这种查询在航班量大的时候对SQL优化有一定要求。

把这些需求列清楚,你会发现这个系统本质上是一个“状态机+CRUD+统计报表”的组合体。后面所有的表结构设计、接口划分、页面组织,都是围绕着这几个核心需求展开的。很多同学做项目失败,不是代码写不出来,而是需求没想明白就开始建表,结果做到一半发现表结构不对,推倒重来,特别浪费时间。

1.3 系统整体功能模块规划

基于上面的需求,系统可以拆成几个大模块,每个模块对应一组页面和接口:

功能模块核心功能点前端页面
用户认证登录、登出、密码加密登录页
航班管理航班的增删改查、逻辑删除航班列表页
进出港管理按进港/出港分类查询、状态流转进出港看板页
动态监控当日航班状态实时更新、时间轴航班动态页
统计报表航班量趋势、准点率、吞吐量首页数据看板
系统管理用户管理、基础数据字典用户管理页

这里不得不提一个经验:模块划分不要贪多。很多同学喜欢把“角色权限”“操作日志”“数据字典”这些都塞进去,结果每个模块都做得半吊子,答辩的时候一问细节就露馅。这个系统把核心放在航班管理、状态流转和统计看板上,把这些做深做透,远比堆一堆凑数的功能要好。

2. 数据库设计与接口规划:整个项目最容易翻车的环节

2.1 航班信息表到底怎么设计

数据库设计这个环节,直接决定了后面开发是“顺畅”还是“到处打补丁”。我见过太多人把航班表设计成一个大宽表,所有字段全塞进去,结果查询的时候各种OR条件,索引完全失效,性能惨不忍睹。这套系统里,我用了“主表+状态扩展”的思路,核心表叫flight_info,承载的是航班的所有静态和动态信息。

CREATE TABLE `flight_info` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `flight_no` VARCHAR(20) NOT NULL COMMENT '航班号,如CA1831', `flight_type` TINYINT NOT NULL COMMENT '航班类型:1-出港,2-进港', `airline` VARCHAR(50) DEFAULT NULL COMMENT '航空公司', `aircraft_type` VARCHAR(20) DEFAULT NULL COMMENT '机型,如A320/B738', `origin_airport` VARCHAR(50) DEFAULT NULL COMMENT '起飞机场', `arrival_airport` VARCHAR(50) DEFAULT NULL COMMENT '到达机场', `scheduled_departure` DATETIME DEFAULT NULL COMMENT '计划起飞时间', `scheduled_arrival` DATETIME DEFAULT NULL COMMENT '计划到达时间', `actual_departure` DATETIME DEFAULT NULL COMMENT '实际起飞时间', `actual_arrival` DATETIME DEFAULT NULL COMMENT '实际到达时间', `gate_no` VARCHAR(20) DEFAULT NULL COMMENT '登机口', `checkin_counter` VARCHAR(20) DEFAULT NULL COMMENT '值机柜台', `flight_status` TINYINT NOT NULL DEFAULT 0 COMMENT '航班状态:0-计划,1-值机,2-登机,3-起飞,4-落地,5-取消', `passenger_count` INT DEFAULT 0 COMMENT '旅客人数', `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注,延误原因等', `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-正常,1-删除', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_flight_no` (`flight_no`), KEY `idx_flight_type` (`flight_type`), KEY `idx_flight_status` (`flight_status`), KEY `idx_scheduled_departure` (`scheduled_departure`), KEY `idx_scheduled_arrival` (`scheduled_arrival`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='航班信息表';

几个容易忽略的设计细节:

  • deleted字段做逻辑删除,而不是物理DELETE。这个在管理系统里几乎是标配,因为航班数据有审计价值,而且物理删除会破坏关联统计。
  • 四个时间字段分开存:计划起飞、计划到达、实际起飞、实际到达。很多人图省事只存“起飞时间”和“到达时间”,但一旦做准点率统计就会发现完全没法算,因为准时率=实际时间与计划时间的差值,没有计划时间就没有统计依据。
  • 索引不是越多越好,但要覆盖高频查询路径。航班号、类型、状态、计划时间是查询和统计最常见的选择条件,建上这4个索引基本够了。注意idx_scheduled_departure和idx_scheduled_arrival是给范围查询用的,如果你们项目里主要按日起飞查询,前者尤其关键。

2.2 状态流转:如何用代码约束“航班不能乱跳状态”

航班的生命周期是有序的:计划->值机->登机->起飞->落地,中间可以插入“取消”作为终态。如果用户在前端把一个已经落地的航班改成“计划”,这就是数据事故。所以后端必须要有一层状态机校验逻辑,我写了一个非常轻量的校验器:

private static final Map<Integer, Set<Integer>> STATUS_TRANSITIONS = new HashMap<>(); static { STATUS_TRANSITIONS.put(0, new HashSet<>(Arrays.asList(1, 5))); // 计划可转为值机或取消 STATUS_TRANSITIONS.put(1, new HashSet<>(Arrays.asList(2, 5))); // 值机可转为登机或取消 STATUS_TRANSITIONS.put(2, new HashSet<>(Arrays.asList(3, 5))); // 登机可转为起飞或取消 STATUS_TRANSITIONS.put(3, new HashSet<>(Arrays.asList(4))); // 起飞只能落地 STATUS_TRANSITIONS.put(4, Collections.emptySet()); // 落地是终态 STATUS_TRANSITIONS.put(5, Collections.emptySet()); // 取消是终态 } public void validateTransition(Integer currentStatus, Integer targetStatus) { Set<Integer> allowed = STATUS_TRANSITIONS.get(currentStatus); if (allowed == null || !allowed.contains(targetStatus)) { throw new BusinessException("非法的状态流转:从" + currentStatus + "到" + targetStatus); } }

这个代码的作用不仅仅是“校验”,更重要的是把业务规则显式地写出来,而不是散落在各个Service方法里。如果后续业务扩展了“返航”“备降”等状态,只需要改这一个Map,所有调用方自动生效。这种“规则集中管理”的思路,在真实项目中非常吃香,面试聊到这个点也会加分。

2.3 接口清单:给前端提供稳定的“服务契约”

接口设计的原则是“按前端页面的数据需求来设计,而不是按表结构来设计”。很多新手直接把表字段原封不动返回给前端,导致前端拿到一堆用不上的字段,还容易把数据库结构暴露出去。我在这套系统里做了明确的VO层隔离,核心接口如下:

接口方法路径说明
POST/api/auth/login用户登录,返回JWT Token
GET/api/flight/page分页+多条件查询航班列表
GET/api/flight/{id}获取航班详情
POST/api/flight新增航班
PUT/api/flight修改航班信息
DELETE/api/flight/{id}逻辑删除航班
PUT/api/flight/status变更航班状态
GET/api/flight/statistics/overview首页统计总览
GET/api/flight/statistics/trend近7日航班量趋势
GET/api/flight/statistics/punctuality准点率统计

看到没有,查询条件是通过GET参数拼的,比如/api/flight/page?flightType=1&status=2&startDate=2024-01-01&endDate=2024-01-31&pageNum=1&pageSize=10。这里有个经验:日期范围查询一定要用>= startDate AND < endDate+1的方式来过滤,而不是用BETWEEN,因为DATETIME字段在用BETWEEN的时候容易把最后一天的边界数据漏掉或者包含到第二天。

3. 后端落地:SpringBoot里那些真正写代码的细节

3.1 项目分层与统一返回体

后端工程我用了最经典的四层结构:Controller(接收参数和返回结果)、Service(业务逻辑)、Mapper(数据访问)、Entity/VO(数据模型)。这种分层在菜鸟眼里可能觉得“多此一举”,但在多人协作或长期维护的场景下,它能有效地把职责隔离——Controller不写SQL,Mapper不写业务判断,Service不直接操作HttpServletRequest。

统一返回体的设计也是一个容易被忽略但极其重要的点。我写了一个R类,所有接口都返回这个格式:

public class R<T> { private Integer code; // 200成功,500业务异常 private String message; // 提示信息 private T data; // 业务数据 public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> R<T> fail(String message) { R<T> r = new R<>(); r.setCode(500); r.setMessage(message); return r; } }

有了这个统一返回体,前端axios拦截器就可以全局判断code字段,不用每个接口单独写错误处理。同时配合@RestControllerAdvice做全局异常捕获,所有未处理的异常都会转成R.fail("系统异常,请稍后重试"),避免把堆栈信息直接泄露给前端,这个安全细节在答辩或者面试的时候都可以提。

3.2 MyBatis的核心:XML里写动态SQL要注意什么

MyBatis在这套系统里的定位是“半自动ORM”,SQL由你掌控,映射交给框架。很多人纠结用注解还是XML,我的建议是:多条件查询和复杂SQL一律用XML,因为动态SQL标签<if>、<where>、<foreach>在XML里写才最顺手,注解里拼字符串写动态SQL简直是灾难。

拿航班多条件分页查询举例子,这是全项目最核心的一段SQL:

<select id="selectFlightPage" resultType="com.example.flight.entity.FlightInfo"> SELECT * FROM flight_info <where> AND deleted = 0 <if test="query.flightNo != null and query.flightNo != ''"> AND flight_no LIKE CONCAT('%', #{query.flightNo}, '%') </if> <if test="query.flightType != null"> AND flight_type = #{query.flightType} </if> <if test="query.flightStatus != null"> AND flight_status = #{query.flightStatus} </if> <if test="query.startDate != null"> AND scheduled_departure &gt;= #{query.startDate} </if> <if test="query.endDate != null"> AND scheduled_departure &lt; DATE_ADD(#{query.endDate}, INTERVAL 1 DAY) </if> <if test="query.originAirport != null and query.originAirport != ''"> AND origin_airport = #{query.originAirport} </if> <if test="query.arrivalAirport != null and query.arrivalAirport != ''"> AND arrival_airport = #{query.arrivalAirport} </if> </where> ORDER BY scheduled_departure DESC </select>

这里有两个细节很多人会栽跟头:

  • 模糊查询一定要用CONCAT('%', #{value}, '%'),而不是直接在Java代码里拼好"%值%"再传进去。虽然结果一样,但后者容易在传参时忘记处理特殊字符。更重要的是,#{}是预编译占位符,能防SQL注入;而如果用${}直接拼接,那等于把数据库敞开了让别人打。航班号这种场景虽然不容易被攻击,但习惯一定要养成。
  • 动态<where>标签会自动处理第一个条件前面的AND,你不需要写WHERE 1=1这种丑陋的兼容写法。<if>标签里每一个条件都要判断“不为空”,尤其是字符串类型,没判空的话MyBatis会拼出AND flight_no =这种残缺SQL,直接报语法错误。

分页用的是PageHelper,用法极其简单,在查询前调用一行代码,后面的查询自动带上LIMIT:

public PageResult<FlightVO> pageFlights(FlightQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<FlightInfo> list = flightMapper.selectFlightPage(query); PageInfo<FlightInfo> pageInfo = new PageInfo<>(list); // 把Entity转成VO,再组装PageResult返回 return PageResult.build(pageInfo.getTotal(), voList); }

注意:PageHelper.startPage必须紧挨着下一次Mapper查询,中间不能穿插其他查询,否则分页会错乱。这个坑我在生产环境见过不止一次——有人在startPage和selectList之间加了一个日志查询或者字典查询,结果分页被“截胡”,数据全乱套。

3.3 时间字段、VO转换和JSON序列化的坑

航班系统的核心数据几乎全是时间,所以时间处理是整个后端最容易出幺蛾子的地方。我的建议有三条:

  • 数据库用DATETIME,Java实体用LocalDateTime,两者配合是最好的,既没有java.util.Date的时区噩梦,也没有Timestamp的精度问题。
  • JDBC连接串必须加serverTimezone=Asia/Shanghai,否则MySQL驱动会拿默认时区(通常是UTC)来解析时间,导致存进去和查出来的时间差了8个小时。这种问题极其隐蔽,前端显示永远比实际时间少8小时,但数据看着又是“合理”的。
  • 返回给前端的JSON时间格式要统一。我习惯在application.yml里配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai

前端拿到的就是2024-06-01 08:30:00这种字符串,直接展示即可。如果配置漏了,默认序列化出来是一串时间戳数字,前端还得自己格式化,两边标准不统一就会出现“详情页显示正常、列表页显示NAN”这种莫名其妙的问题。

VO转换我用的是BeanUtils.copyProperties,虽然效率不是最优,但代码极其简洁。在航班量不大的场景下,这点性能损耗完全可接受。如果你追求极致性能或者字段特别多,可以引入MapStruct,编译期生成转换代码,效率高且类型安全,但需要多写一点接口定义。这个度大家自己把握。

3.4 登录鉴权:JWT该怎么用才不“裸奔”

虽然毕设或者课设对权限要求不高,但一个没有登录环节的管理系统总感觉差点意思。这套系统用的是JWT(JSON Web Token)做无状态认证:用户登录成功后,后端签发一个有效期2小时的Token返回给前端;前端每次请求在Header里带上Authorization: Bearer <token>;后端通过一个过滤器解析Token,顺便把用户信息放进ThreadLocal里供后续业务使用。

JWT的密钥一定要放在配置文件中,别硬编码在代码里。我还要加一层验证:用户状态被禁用时,即使Token没过期也要拒绝访问。实现方式是每次请求除了验签之外,再去数据库查一下用户状态,虽然多了一次查询,但安全性提升一个档次。对于真实系统来说,“Token过期但用户已被删号”还能继续访问,这是不可接受的漏洞。

密码存储用的是BCryptPasswordEncoder,不是MD5。原因很简单:MD5是摘要算法,撞库和彩虹表都能破解,而BCrypt内置随机盐,同一个密码每次加密结果都不同,暴力破解成本指数级上升。这个点其实一行代码就能体现专业度:

String encodedPassword = new BCryptPasswordEncoder().encode(rawPassword); boolean matches = new BCryptPasswordEncoder().matches(rawPassword, encodedPassword);

4. 前端页面与交互:Vue怎么把这套系统做得“像那么回事”

4.1 路由、Axios封装与全局拦截

前端用Vue3 + Element Plus + Vue Router + Pinia这套组合,工程初始化用Vite。启动项目前先做两件基础工作:路由配置和axios封装。

路由设计围绕顶部导航展开,分为登录页、首页看板、航班管理、航班动态、统计报表几个模块。使用懒加载模式:

const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/', component: () => import('@/layout/index.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('@/views/Dashboard.vue') }, { path: 'flight/list', component: () => import('@/views/flight/FlightList.vue') }, { path: 'flight/dynamic', component: () => import('@/views/flight/FlightDynamic.vue') }, { path: 'statistics/trend', component: () => import('@/views/statistics/TrendReport.vue') } ]} ];

懒加载的意义在于首屏只加载当前需要的页面,不会一上来就把所有组件全塞进浏览器,首屏渲染速度会快不少。

axios封装的核心是拦截器。请求拦截器负责自动带上Token,响应拦截器统一处理code !== 200的情况:

service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) config.headers['Authorization'] = `Bearer ${token}`; return config; }); service.interceptors.response.use(res => { const { code, message } = res.data; if (code === 200) return res.data.data; if (code === 401) { localStorage.removeItem('token'); router.push('/login'); } ElMessage.error(message || '请求失败'); return Promise.reject(new Error(message)); });

这段代码的精髓是:所有接口请求都走同一个出口,登录过期时全局跳转登录页并提示,不需要每个页面单独处理“Token失效”这种特殊情况。这也是前后端分离项目的基本盘。

4.2 航班列表页:表格、分页与多条件搜索的组合拳

航班列表页是系统使用频率最高的页面,交互设计要围绕“快速筛选、快速编辑”来展开。页面顶部是一排筛选条件:航班类型(下拉)、航班状态(下拉)、起飞机场(输入)、到达机场(输入)、日期范围(日期选择器)、航班号(输入),下面才是表格主体。

表格用Element Plus的el-table,列字段和接口返回的VO保持一一对应。这里有一个特别容易出问题的点:时间列的格式化。因为后端已经按yyyy-MM-dd HH:mm:ss格式返回了,前端直接渲染即可,但如果你后端返回的是时间戳,前端就需要用dayjs格式化,而且格式化函数要加容错判断,否则空值会产生“Invalid date”白字,特别难看。

分页交互我用的是el-pagination,关键点是:页码和每页条数的变化都会触发数据重新加载,但不会重置筛选条件。也就是说,用户在筛选条件面板填了一堆条件,翻到第5页后刷新,筛选条件还在。这个很基础,但很多初学者会把分页状态和筛选状态混在一起,导致一翻页条件全没了。

状态变更操作建议做成下拉选择框放在表格行内,而不是弹窗修改。这样用户点一下就能切换状态,配合状态机校验,交互效率很高:

<el-table-column label="航班状态" width="130"> <template #default="{ row }"> <el-select :model-value="row.flightStatus" @change="handleStatusChange(row, $event)"> <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </template> </el-table-column>

handleStatusChange里调用后端状态变更接口,成功之后用ElMessage.success提示,然后刷新当前页数据。注意不要像有些人那样整个页面重新查询第一页,那样会把用户正在看的页码跳回去,体验很差。正确做法是重新请求当前页码。

4.3 首页看板与航班动态:数据可视化怎么做才好看

首页看板放的是“数字总览+图表”,数字卡片展示今天的进出港航班总数、出港数、进港数、取消数、旅客总人数;图表部分用ECharts画近7日航班量趋势和准点率环图。

这里要提醒:ECharts初始化之后,组件销毁时一定要dispose,否则内存泄漏。如果在Vue里频繁切换菜单,泄漏积累多了页面会越来越卡。正确范例:

onMounted(() => { chart = echarts.init(domRef.value); chart.setOption(option); }); onBeforeUnmount(() => { chart.dispose(); });

航班动态页是我个人比较喜欢的一块设计:按时间轴展示今日航班,每条数据是一张卡片,卡片上显示航班号、起降机场、计划时间、实际时间、当前状态,状态用不同颜色标签标注。这块页面用el-timeline组件实现很合适,数据量控制在当天的前50条,避免拉取全量数据拖慢页面。高亮当前正在登机或起飞的航班,就能做出一块类似机场大屏的效果。

5. 部署上线与常见问题排查:这是我踩坑最多的地方

5.1 本地开发环境搭建要点

搭环境的顺序别乱:先装JDK(要求8以上,我建议17)、Maven(3.6+),再装MySQL(8.0),最后装Node.js(16+)。每一步都要验证成功再进行下一步,别一口气全装完才发现版本不兼容。

MySQL初始化时最需要留意的就是字符集和时区。字符集一定要是utf8mb4,不然存中文或者生僻字会变成问号;时区设置Asia/Shanghai。初始化数据库后执行建表SQL或者导入flight_management.sql,再在application.yml里改数据库账号密码。

后端启动前我习惯用Postman先测试/api/auth/login和/api/flight/page两个接口,确认后端没问题之后再启动前端。前端启动比较麻烦的就是安装依赖,npm install经常会慢,可以用npm config set registry https://registry.npmmirror.com切换镜像源。如果遇到node-sass编译报错,优先检查Node版本——node-sass和Node版本是强绑定关系,版本对不上怎么装都报错,现在Vue3项目一般都用dart-sass替代了,会省心很多。

启动完成后,浏览器访问http://localhost:5173,这是Vite默认端口;后端接口是http://localhost:8080。两个端口不同就会触发跨域问题,所以必须配置前端代理:

// vite.config.js server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

5.2 生产部署:jar包和Nginx的组合拳

开发环境跑通后,部署生产环境就是另一个世界。前端执行npm run build,产物在dist目录,包含静态的HTML/CSS/JS;后端执行mvn clean package,生成可执行jar包。

  • 后端部署:java -jar flight-management.jar --spring.profiles.active=prod,用nohup挂后台运行。如果是服务器资源紧张,可以加-Xms256m -Xmx512m限制内存,SpringBoot+MyBatis这种轻量应用512M内存足够跑得很欢。
  • 前端部署:把dist目录复制到Nginx的html目录下,配置一个server块监听80端口,root指向该目录。

这里最容易踩的坑是路由刷新404问题。因为Vue是SPA单页应用,所有路由都是前端路由,直接刷新/flight/list时,Nginx会去磁盘找/flight/list这个文件,找不到就报404。解决方案是配置try_files回退到index.html:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

还有一个常见坑是后端接口地址写死。前端在生产环境请求/api时,Nginx要把/api反向代理到后端服务的8080端口:

location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

这条配好,前后端才真正打通。

5.3 高频报错速查表

我把开发过程中最常遇到的十几个问题整理成一张速查表,这些问题每一条都是我一边开发一边记录的,真的非常实用:

错误现象根本原因解决方案
数据库连接失败Access denied账号密码错误或远程权限未授权核对密码,给用户授权grant all on *.* to 'root'@'%'
中文乱码成???建库时字符集不是utf8mb4ALTER DATABASE xxx CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
页面时间差8小时JDBC连接串缺serverTimezone连接串添加?serverTimezone=Asia/Shanghai
XML里<符号报错XML规范不能直接解析小于号用&lt;或者<![CDATA[<]]>
Mapper接口找不到接口和XML路径不一致检查@MapperScan路径和XML的namespace
分页数据不对PageHelper被其他查询截胡确保startPage后紧接目标Mapper调用
前端跨域报错端口不一致没有代理配置Vite proxy或Nginx反向代理
刷新页面404SPA路由没有try_files回退Nginx增加try_files $uri $uri/ /index.html
MyBatis查询返回null表字段下划线和Java驼峰没有映射map-underscore-to-camel-case: true
表格时间显示Invalid Date后端返回时间戳或null,前端直接格式化后端统一格式,前端格式化前判空
新增航班后列表不刷新前端没有重新加载数据新增成功后重新调用查询接口
打包后XML没打进jarMaven默认不包含mapper目录pom中配置resources包含**/*.xml

5.4 查询性能优化:航班量大了怎么办

虽然毕设数据量不会大,但既然做的是航班系统,以后面试官问到“百万数据怎么优化”,你得能说出点东西。三个方向:

  • 索引优化:上面那张表已经建了四个索引,但要注意“复合索引”的使用。如果高频查询是“按日期范围+航班类型”,那建一个(scheduled_departure, flight_type)的复合索引远比两个独立索引效果好,因为数据库可以走索引的最左前缀匹配。
  • 规避LIKE '%xxx%':模糊查询如果通配符在开头,索引会失效,全表扫描。对于航班号这种前缀固定的字段,LIKE 'CA%'就能走索引。这是面试的高频考点。
  • 分页深翻页:LIMIT 100000, 10这种分页越翻越慢,因为MySQL要先丢弃前10万行。优化方式是记住上一页的最大ID,用WHERE id > lastMaxId ORDER BY id LIMIT 10来翻页,这是游标分页的思路。在航班动态这种按时间倒序的场景下尤其好用。

6. 代码质量、安全性与后续扩展方向

最后再聊聊代码层面的几个加分项。虽然管理系统“能用就行”,但作为简历项目,多几个亮点能让面试官在10分钟内记住你。

在安全性上,除了MyBatis的#{}防SQL注入之外,还要注意XSS攻击。这个系统里航班备注、机场名都是用户可输入字段,如果原样存原样回显,就能在页面上注入恶意脚本。我加了一个全局过滤器,对请求参数中的<script>、onerror=等关键字做转义处理。如果你的项目里涉及文件上传,更应该做文件类型白名单校验,别让用户传个.jsp上去直接执行。

密码加密、JWT鉴权、逻辑删除、全局异常处理、统一返回体、乐观锁版本号字段——这些点每一个都是企业级开发的基本功。我把flight_info表加了一个version字段,更新时用UPDATE ... SET ... WHERE id=? AND version=?,如果更新行数为0就说明数据被其他人改过,抛出并发冲突提示。这种乐观锁机制在业务系统里几乎是标配。

项目后续要扩展的话,优先级从高到低排列:

  • 增加角色权限(管理员/操作员/只读用户),基于Spring Security + JWT做RBAC。
  • 接入消息队列模拟航班信息推送,比如Kafka或者RabbitMQ,让前端通过WebSocket实时收到状态变更。
  • 把统计报表做成多维分析,按航空公司、航线、小时维度下钻。
  • 引入Redis缓存机场基础数据、航班字典,缓解数据库压力。

我个人在实际开发这个系统过程中,最大的体会是:这个项目帮我重新串了一遍“为什么这么设计”的逻辑。比如状态机为什么用Map存规则而不是if-else,统一返回体为什么能减少前端大量重复代码,逻辑删除为什么比物理删除更安全——这些单个看起来都很小的知识点,连在一起就是一个完整的工程思维。如果你准备拿这个项目去面试,不要只背名词,要把每个设计决策背后的“为什么”想清楚,面试官往往就爱追着这个点一直问到底。

最后再分享一个小技巧:多条件查询的DTO不要命名成FlightQueryDTO这么泛,直接在类上注明用途,加个@ApiModelProperty注解描述每个字段的语义,等过几个月你自己回来看代码时,会感谢当初那个写清楚注释的自己。工程能力往往就是在这些琐碎的细节里慢慢积累起来的。

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

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

立即咨询