我从大四做毕设到后来带学弟学妹做课设,汽车票预订这种“票务类管理系统”可以说是Java Web方向最经典、最容易出效果、也最适合写进论文的一类题目。选题本身不新颖,但胜在业务逻辑完整、技术栈通用、可扩展空间大——SpringBoot+Vue+MySQL这套组合,面试的时候也拿得出手。下面我把这套系统的设计思路、核心实现、踩坑记录完整拆一遍,打算做毕设或者想拿它练手提升的同学可以直接照着复现。
1. 项目整体设计与技术选型思路
1.1 这类票务系统真正要解决的是什么
很多同学拿到“汽车票网上预订”这个题目,第一反应是做几个页面:登录、查车次、下单、后台列表,完事。但实际做下来你会发现,票务系统的核心难点不是页面多不多,而是三个词:状态、并发、一致性。
先说状态。一张订单从用户下单到最终乘车,中间要经历“待支付、已支付、已出票、已检票、已退票”这些环节,每个环节对应数据库里某个字段的变化。如果状态设计得太随意,会出现“钱付了票没出”“后台改了车次用户不知道”这种问题。再说并发。热门线路在节假日高峰期,同一时刻可能有好几个人在抢同一个座位,如果库存扣减逻辑写得不对,就会出现超卖——车上一共40个座,结果卖出41张票。最后是一致性,涉及到支付回调、退票退款这种操作,必须保证数据库里的数据和用户实际的操作结果是一致的。
这套SpringBoot+Vue的汽车票预订系统,本质上就是围绕这三点展开的:用合理的表结构承载状态,用事务和乐观锁解决并发,用清晰的接口设计保证前后端数据流转一致。理解了这一点,你再看下面的模块拆解和代码,思路会顺很多。
1.2 技术栈选型的真实理由
SpringBoot:选它不是因为“大家都在用”,而是因为它确实最适合这种单体重度业务系统。自动配置帮你省掉一大半XML配置,内嵌Tomcat让启动和部署变得极其简单,Spring生态自带的事务管理、MyBatis整合、参数校验这些都开箱即用。对于毕设来说,SpringBoot能让你把精力放在业务逻辑而不是环境配置上,这太重要了。
Vue:前端选Vue的原因有两个。一是它和SpringBoot搭配做前后端分离,天然就是目前企业的主流开发模式,面试讲起来有东西可说;二是Vue的学习曲线相对平缓,就算你之前只写过JSP或者静态HTML,花两周也能上手。而且Vue配合Element-UI做后台管理页面非常快,表格、表单、分页、弹窗这些现成组件一套就有,比手写jQuery舒服太多。
MySQL:市面上最普及的关系型数据库,没有之一。8.0版本安装配置简单,和SpringBoot的兼容性也好,网上参考资料最多,出了问题基本都能搜到解决方案。虽然理论上Oracle或者PostgreSQL也能做,但毕设场景下MySQL就是最优解——指导老师熟悉,答辩评委也认可,本地跑起来资源占用还低。
这套组合还有一个隐藏优势:招人认可度。你去翻招聘网站的Java后端岗位,十个里面有六七个都写着“熟悉SpringBoot”“了解Vue”“掌握MySQL”,做过一个完整的全栈项目,简历上写项目和面试聊项目的时候都有素材。
1.3 系统整体蓝图:三类角色一条主线
我把这个系统拆成三条线来讲,你脑子里先搭个框架:
- 旅客(前端用户):注册登录、搜索车次、在线选座购票、查看订单、退票改签、查看个人信息和乘车记录。
- 管理员(后台用户):登录、车次管理、车辆管理、站点管理、订单管理、用户管理、数据统计。
- 系统主线业务:发布车次 → 用户搜索 → 选择班次下单 → 支付 → 出票 → 站务检票 → 退票退款。
架构上就是标准的前后端分离:Vue项目跑在8080端口(dev模式),通过axios发请求到SpringBoot的8081端口;后端Controller层统一返回JSON,Service层写业务逻辑,Mapper层用MyBatis去操作MySQL。中间用跨域配置把前后端打通,部署的时候前端打包后的dist目录也可以扔进后端的static目录,一个Tomcat全搞定。
这个架构我画过很多次给学弟学妹讲,最核心的一句话:前端只管展示和交互,后端只管数据和业务,数据库只管持久化。记住这个边界,写代码的时候就不会乱。
2. 核心功能模块拆解与业务流程设计
2.1 用户端:从注册到出票的完整业务闭环
用户端的核心流程是:注册 → 登录 → 搜索车次 → 下单购票 → 支付 → 查看订单 → 退票。每一步都有对应的后端接口和数据表支撑,我按顺序拆给你看。
注册登录这块,密码存储我建议用BCrypt加密而不是明文或者MD5。MD5虽然也能用,但彩虹表攻击防不住,正规项目早就淘汰了。SpringSecurity里自带BCryptPasswordEncoder,单独引入一个spring-security-crypto依赖也能用,加密后的字符串形如$2a$10$...,同一密码每次加密结果都不同,安全等级明显不一样。对于毕设答辩,评委问到密码安全,你答出这两个字“BCrypt”,印象分会高不少。
搜索车次是用户端流量最大的接口,也是优化空间最大的地方。核心SQL是按起点站、终点站、出发日期三个条件去查班次表,关联查询出对应的车辆信息和剩余票数。这里有一个细节要注意:不要每次都对全表扫描,对出发日期、线路ID这些查询频率高的字段要建索引。我有一个学弟就是这个环节没做索引,测试数据塞了5000条车次记录后列表接口直接从100毫秒涨到3秒,加上一条普通索引就降回80毫秒。
下单购票是整个系统的业务核心,也是最容易出错的地方。我把它设计成两段式:
- 前端预下单:用户选择车次和座位后,前端先调一个“预下单”接口。后端在这里检查该车次剩余票数是否充足,如果充足,就在订单表插入一条“待支付”状态的订单,同时把可售票数锁住。
- 前端发起支付:用户点击确认支付后,前端调“支付确认”接口,后端在这里把订单状态从“待支付”改为“已支付”,扣减剩余票数,生成乘车凭证。
为了模拟真实支付,我提供一个简单的策略:测试环境下设置一个“模拟支付开关”,打开后点击支付按钮直接走支付成功逻辑,不需要接微信支付宝的SDK。这样就避开了申请商户号、域名备案、回调签名验证这一堆麻烦事,又能把完整的支付状态流转跑通。论文里写“接入真实支付网关”就是一两句话的事,答辩时也能讲清楚这是为了演示做的简化。
退票模块要同步处理两件事:订单状态改为“已退票”,同时把车次的剩余票数加回来(要判断是否超过该车次的发车时间,发车后不允许退票)。有些同学只改了订单状态忘了回补库存,导致的结果就是可售票越来越少,最后变成负数。这种bug在测试阶段很容易漏掉,因为你自己测试一般是“买一张退一张”,数量少看不出问题。所以我在订单状态变更的工具类里,把“退票回补库存”逻辑写成了一个独立方法,在Service层强制调用。
2.2 管理端:车次、车辆、订单的运营视角
管理端面向的是“汽车客运站运营人员”,核心诉求是三个:管车、管班次、管订单。我把它拆成5个菜单,每个菜单对应一组接口:
- 用户管理:查询所有注册用户,支持按用户名/手机号模糊搜索,可以禁用恶意用户。这个模块很简单,就是一个分页列表加状态字段,但别忘了做用户状态枚举,不然“该用户已被禁用”这种提示没法优雅实现。
- 车辆管理:维护车辆信息,车牌号、车型、座位数(比如49座、53座)、所属公司、车辆状态。注意车辆的座位数直接决定了可售票总数,要保证车辆关联的班次剩余票数不能超过座位数,这个校验我在Service层做了。
- 站点管理:维护所有客运站名称,比如“成都东站汽车客运站”“绵阳汽车总站”。它本质是一张字典表,被车次表的起点站终点站字段外键引用。
- 车次管理:这是管理端最核心的模块。管理员发布一个新班次时,要选择车辆、选择线路(起点站→终点站)、设置出发时间和到达时间、定票价。我设计车次表时把“线路”单独拆了一张表,因为现实中同一条线路(比如成都→绵阳)一天可能发很多班次,共用同一个起点终点和基础票价,拆出来就避免了大量冗余数据。
- 订单管理:管理端能看到所有订单列表,支持按订单号、用户名、车次ID查询,可以对异常订单进行取消操作(比如某班次因故停运,管理员需要批量取消未检票的订单)。批量取消这里我用了事务循环处理,中途任何一条失败都会整体回滚,保证不出现“一半订单取消了另一半还是已支付”的情况。
还有一个模块必须单独说:数据统计。我用ECharts做了一个简单的可视化面板,左边显示近7天订单量折线图,右边显示各线路售票占比饼图,下面放一个“今日营收总金额”和“总订单量”的卡片。数据源就是从订单表做GROUP BY按日期/线路聚合,SQL两句搞定,但视觉效果好,答辩时评委通常会在这个页面多停留几秒。
2.3 权限设计与订单状态机设计
权限这块,我没有引入SpringSecurity的完整权限体系,而是做了一个轻量的“登录拦截器+角色判断”:后端定义一个@RequireRole("ADMIN")注解,加在管理端的Controller方法上,拦截器在请求进来时解析token里的角色字段,不是管理员就统一返回403。用户在登录成功时,后端返回一个JWT令牌,前端的axios请求拦截器把token放到Header里,后端每次请求都验证token的签名。Vue前端用vue-router的导航守卫,在路由跳转前检查当前用户的角色,不是管理员就重定向到登录页。
这种轻量方案对毕设来说够用,而且代码量少,容易讲清楚。你要是想整得更“专业”,也可以用Sa-Token或者SpringSecurity+JWT,但说实话对于这个系统的复杂度——只有两个角色、几十个接口——拦截器方案已经绰绰有余,重框架反而是过度设计。
订单状态机我强烈建议你用常量类或枚举统一管理,不要到处写数字。我是这样定义的:
public enum OrderStatus { PENDING(0, "待支付"), PAID(1, "已支付"), COMPLETED(2, "已完成"), CANCELLED(3, "已取消"), REFUNDED(4, "已退票"); private final Integer code; private final String desc; // 构造函数、getter省略 }状态流转必须合法:待支付可以变成已支付,也可以变成已取消;已支付可以变成已完成,也可以变成已退票;但已退票绝对不能变成已支付。我在Service层写了一个状态变更校验方法,非法流转直接抛业务异常。这个设计在答辩时特别加分——你体现的不仅是“能跑”,而是“设计过”。
3. 数据库设计与核心代码实现细节
3.1 五张核心表的结构设计与关系
数据库设计是这类系统的地基。我把表拆成5张核心表,这是最精简又能支撑全部业务的方案:
用户表(sys_user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 用户名,唯一索引 |
| password | varchar(100) | BCrypt加密后的密码 |
| real_name | varchar(50) | 真实姓名 |
| phone | varchar(20) | 手机号 |
| role | varchar(20) | USER/ADMIN |
| status | tinyint | 1正常 0禁用 |
| create_time | datetime | 注册时间 |
车辆表(bus_vehicle)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| license_plate | varchar(20) | 车牌号,唯一 |
| vehicle_type | varchar(30) | 车型,如大型高一 |
| seat_count | int | 座位数 |
| company | varchar(50) | 所属客运公司 |
| status | tinyint | 1可用 0维护中 |
线路表(bus_route)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| start_station | varchar(50) | 起点站 |
| end_station | varchar(50) | 终点站 |
| distance | int | 里程(公里) |
| base_price | decimal(10,2) | 基础票价 |
车次表(bus_schedule)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| route_id | bigint | 外键,关联线路表 |
| vehicle_id | bigint | 外键,关联车辆表 |
| depart_date | date | 出发日期 |
| depart_time | time | 发车时间 |
| arrive_time | time | 到达时间 |
| price | decimal(10,2) | 实际票价(可调整) |
| remain_seats | int | 剩余票数 |
| status | tinyint | 1正常 0停运 |
订单表(bus_order)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单号(业务唯一) |
| user_id | bigint | 下单用户 |
| schedule_id | bigint | 关联车次 |
| passenger_name | varchar(50) | 乘车人姓名 |
| id_card | varchar(18) | 乘车人身份证号 |
| seat_no | varchar(10) | 座位号 |
| price | decimal(10,2) | 实付金额 |
| status | tinyint | 订单状态码 |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 支付时间 |
表关系一句话概括:车次表是枢纽,一端关联线路和车辆,一端被订单关联。订单表拿到车次的线路信息后,就能拼出起点→终点、发车日期时间、票价这些关键信息。
建表时我给order_no加了唯一索引,给bus_order.schedule_id和user_id都加了普通索引。因为查询订单列表最常见的场景就是“按用户查”和“按车次查”,没有索引的话订单量一大就会慢。
3.2 购票扣库存的事务处理与超卖防护
这一段是整个系统技术的含金量所在,也是答辩时最值得展开讲的点。我先说简单地扣库存为什么有问题。
最简单的写法是这样:
Schedule schedule = scheduleMapper.selectById(scheduleId); if (schedule.getRemainSeats() > 0) { schedule.setRemainSeats(schedule.getRemainSeats() - 1); scheduleMapper.updateById(schedule); // 创建订单 }这个写法在单用户测试时没有任何问题,但一旦两个用户同时抢同一车次的票,就会出现经典的“并发脏读”:A读到剩1张,B也读到剩1张,A扣完变0,B继续用旧值1去扣,结果是0,但两个订单都创建成功了。这就是超卖。
解决办法有两个层面,我在系统里两层都做了:
第一层:数据库层面的乐观锁。给bus_schedule表加一个version字段,更新时带上版本号判断:
UPDATE bus_schedule SET remain_seats = remain_seats - 1, version = version + 1 WHERE id = #{scheduleId} AND remain_seats > 0 AND version = #{oldVersion}如果更新的影响行数是0,说明这一秒已经有其他请求把库存扣掉了,当前请求必须撤销订单并提示“余票不足”。
第二层:Service层的事务控制。用@Transactional把“扣库存 + 创建订单”绑在同一个事务里,任何一步失败都整体回滚。
@Transactional(rollbackFor = Exception.class) public OrderResult createOrder(OrderCreateDTO dto) { // 1. 乐观锁扣减库存 int rows = scheduleMapper.deductSeats(dto.getScheduleId(), dto.getVersion()); if (rows == 0) { throw new BusinessException("余票不足或车次已变更,请刷新后重试"); } // 2. 构建订单数据,生成唯一订单号 // 3. 插入订单记录 // 4. 返回预下单结果 }这里还有一个容易被忽略的细节:乐观锁冲突后要保证用户的体验。实际线上场景中,两个用户抢票只有一个能成功,失败的那个我不建议直接抛500,而是捕获后提示“当前班次余票紧张,请尝试其他班次”,前端同时刷新一下余票信息。代码层面把这个体验优化写在全局异常处理器里就好。
有的同学问要不要用Redis分布式锁?我的回答是:毕设场景没必要。你就一台应用服务器、一个MySQL实例,乐观锁完全够用。引入了Redis反而多一个环境依赖,部署成本更高,答辩时候你得额外解释为什么要用它。把乐观锁和事务讲清楚,已经能证明你理解并发问题。
3.3 前端Vue的页面结构与核心API对接
前端我用的Vue3 + Vite + Element-Plus,Vue Router使用Hash模式,状态管理用了Pinia(比Vuex写起来简单很多)。整个前端的src结构是这样规划的:
src/ ├── api/ │ ├── auth.js # 登录、注册、验证码 │ ├── schedule.js # 车次查询、购票 │ ├── order.js # 订单列表、退票 │ └── admin/ # 后台管理相关接口 ├── router/ │ └── index.js # 路由表,含角色守卫 ├── store/ │ └── user.js # 用户状态:token、角色、用户信息 ├── views/ │ ├── Home.vue # 首页 + 车次搜索 │ ├── OrderList.vue # 我的订单 │ ├── Login.vue │ ├── Register.vue │ └── admin/ # 后台五个管理页面 └── utils/ └── request.js # axios 封装,拦截器前端对接后端的核心在utils/request.js。我在这里做了三件事:请求时把token从Pinia取出来挂到Header;响应时统一解析后端返回的{code, message, data}结构,code不是200就直接弹ElMessage提示;捕获到401状态码时自动清空本地登录态并跳回登录页。这三件套几乎是每个真实前端项目的标配,做了它你的前端工程质量感会上一个台阶。
车次查询页有一个交互细节值得说:用户在首页输入起点、终点和出发日期后,点击搜索应该跳转到车次列表页,列表页根据路由query参数去拉接口。这个做法比在首页直接内嵌列表体验好很多,因为它把“搜索条件”和“结果展示”分离开,以后想加筛选、排序都方便扩展。我实际开发时还加了“加载中”的骨架屏状态,虽然是小事,但能给答辩评委留下“这个学生关注用户体验”的印象。
车次卡片上每个班次我放了一个“预订”按钮,点击后弹出抽屉展示:车辆信息、座位数、票价,下面是一个座位图(按车辆座位数生成网格,已售座位灰色禁用,可选座位绿色高亮)。座位图这块是前端最复杂的组件,用Element-Plus的Popconfirm和动态class就能实现,不需要引入任何第三方座位组件。因为理想情况下座位图应该实时反映后端库存,但真实项目里你不可能每个座位都同步一次数据库,所以我的策略是:进入选座抽屉时拉取一次“该班次已售座位号集合”,前端渲染时排除掉,提交订单时后端再做一次最终校验。
3.4 后端核心接口一览
我把后端Controller层的接口整理一张表,你把它当checklist用——写代码前先把这些接口定义好,再去做实现,事半功倍:
| 模块 | 接口 | 方法 | 说明 |
|---|---|---|---|
| 认证 | /api/auth/register | POST | 用户注册 |
| 认证 | /api/auth/login | POST | 用户登录,返回JWT |
| 车次 | /api/schedule/search | GET | 按站点+日期查车次 |
| 车次 | /api/schedule/seats/{id} | GET | 获取已售座位号 |
| 订单 | /api/order/pre-create | POST | 预下单购票 |
| 订单 | /api/order/pay/{orderNo} | POST | 模拟支付 |
| 订单 | /api/order/refund/{orderNo} | POST | 退票 |
| 订单 | /api/order/my-list | GET | 我的订单分页列表 |
| 管理 | /api/admin/schedule/page | GET | 后台车次分页 |
| 管理 | /api/admin/schedule/upsert | POST | 新增/修改车次 |
| 管理 | /api/admin/order/page | GET | 全部订单分页 |
| 管理 | /api/admin/stats/summary | GET | 运营数据统计 |
统一返回结构我建议定义为Result<T>,包含code、message、data三个字段。Controller层的每个接口都返回它,不要再出现各地返回值不统一的情况。这个规范越早定下来,后面联调时越省心。
另外,参数校验建议用Spring的@Valid注解加上@NotNull、@Size这类注解,在DTO上直接标注合法性要求。比如注册接口的DTO上,用户名标@NotBlank,手机号标@Pattern(regexp="^1[3-9]\\d{9}$")。这样就能自动拦截非法请求,不用在Service层写一堆if判断。这点很多同学会忽略,但代码规范里是加分项。
4. 部署启动与高频问题排查
4.1 本地开发环境搭建与启动顺序
从零开始跑通这个项目,我的建议顺序是“数据库 → 后端 → 前端”。
第一步,装MySQL并初始化。我用的是MySQL 8.0,Windows下安装时有个容易被坑的地方:8.0默认的认证插件是caching_sha2_password,有些老版本的连接驱动不认识,会报“Unable to load authentication plugin”。解决办法是在连接URL里指定allowPublicKeyRetrieval=true&useSSL=false,或者创建用户时指定mysql_native_password。我先说结论:用MySQL官方的Connector/J 8.0以上版本就不会有这个问题。
初始化数据库时执行项目里的init.sql,里面包含建库语句、建表语句和一批演示数据。演示数据非常关键,我塞了4辆车、6条线路、20多个班次、几个测试账号,否则你前端页面上什么都没得看。测试账号我留了一个管理员admin/123456和普通用户test/123456。
第二步,启动后端。用IDEA打开后等待Maven把依赖拉完,修改application.yml里的数据库账号密码,然后运行主类。SpringBoot默认端口8081,在配置文件里通过server.port指定。启动日志打到“Started ... in x.xx seconds”就说明后端起来了,可以在浏览器直接访问http://localhost:8081/api/schedule/search?startStation=成都&endStation=绵阳来验证接口是否通。
第三步,启动前端。前端用Vite脚手架创建,进入项目目录后先npm install装依赖,然后npm run dev启动开发服务器,默认端口是5173。Vite启动非常快,基本秒开。要注意的是前端开发环境通过VITE_API_BASE_URL这个环境变量指向http://localhost:8081,axios的baseURL就从这个变量读取。跨域问题我是这样解决的:后端写了一个CorsConfig配置类,添加CorsFilter允许所有来源访问,同时前端不额外设置代理。实际开发时我更推荐用Vite的proxy代理,配置写在vite.config.js里,这样前端请求的就是同源地址,更接近生产环境的表现。两种方案你选一种就行。
4.2 打包上线的两种典型方式
毕设最后一般要部署一台服务器给答辩评委看,或者提交一个能一键运行的包,我提供两个方案:
方案一:前后端分开部署,适合服务器上有Nginx的情况。后端先mvn clean package -DskipTests打成jar包,java -jar app.jar运行。前端在项目根目录执行npm run build,生成dist目录,把dist里的文件扔到Nginx的html目录,并配置一个反向代理把/api开头的请求转发到后端的8081端口。Nginx配置核心就这一段:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8081; proxy_set_header Host $host; } }注意try_files那行必须有,否则直接访问前端路由比如/admin/orders会404,因为Vue是单页应用,刷新页面时要全部落到index.html再走前端路由。
方案二:单jar包全家桶部署,适合云服务器配置低、不爱折腾Nginx的情况。把前端dist目录复制到后端的src/main/resources/static下,然后重新打包。SpringBoot会把static下的文件当作静态资源直接暴露,这样一台服务器一个8081端口就同时托管了前端页面和后端接口。启动后访问http://服务器IP:8081就能看到首页。这个方案我在带学弟做课设演示时经常用,部署成本最低,稳定性最高。缺点是没有Nginx层的高并发承载能力,但毕设演示场景完全够。
4.3 高频踩坑实录与排查清单
我把做这个系统前后踩过的坑整理成了一张速查表,大部分问题都是开发环境或者基础配置相关,提前看一眼能帮你省下不少排查时间:
| 现象 | 原因 | 解决 |
|---|---|---|
| 前端请求后端报CORS错误 | 后端没配跨域过滤器 | 在Config里加CorsFilter,或前端用Vite proxy |
| 启动报端口被占用 | 上次进程没关干净 | 换端口,或netstat -ano找到PID后taskkill |
| 接口能通但登录后没权限 | JWT token没传或已过期 | 检查axios请求拦截器,打印Authorization头确认 |
| 查询中文条件查不到 | 连接URL没指定UTF-8 | 加characterEncoding=utf8,且建库时指定utf8mb4 |
| 时间显示差8小时 | 时区未配置 | 连接URL加serverTimezone=Asia/Shanghai |
| 打包后前端页面白屏 | 前端资源路径不对 | 静态资源上传到static目录并配置spring.web.resources路径 |
| 退票后余票没回补 | 只改了状态没调回补方法 | Service层退票方法里先改状态再回补库存,必须在同一事务 |
| 前端调用后台接口返回401 | Token校验拦截器把登录接口也拦截了 | 配置拦截器时放行/api/auth/login和/api/auth/register两个地址 |
这里我重点说两个最隐蔽的问题。
**第一个是时间字段的时区问题。**MySQL连接URL里如果不加serverTimezone=Asia/Shanghai,很可能出现数据库存的时间比实际快8小时或慢8小时的情况。原因很简单:驱动和数据库默认时区不一致导致的。这个问题的坑是它不一定会报错,只有你查出来的时间对不上才会发现。
**第二个是Vue打包后刷新404。**如果你采用单jar包部署方式,后端直接托管前端dist,不需要Nginx的try_files,所以一般没这个问题。但如果用Nginx部署,刷新任何非首页的路由(比如刷新/order/list)就会404。原因是Nginx默认找不到这个路径对应的真实文件,必须加上try_files $uri $uri/ /index.html;配置。这个坑几乎每个用Vue的兄弟都会踩一次。
还有一个容易被新手忽略的小事:后端接口的日期参数格式。前端传“2025-06-01”这种字符串,后端如果直接用LocalDate接收,有时会报格式转换错误。我在全局配置里注册了一个LocalDate的转换器,或者更省事的办法是DTO里用String接收,Service层再转换成LocalDate。两种方式都用过,第二种更直观,适合新手,还不容易出乱子。
开发过程中如果遇到报错,我给你一个定位思路:先看控制台日志堆栈的第一行,不要直接翻到最后。找到是哪个类哪个方法抛的异常,再去对应的Service层断点,基本两三分钟就能定位。很多新手一看到红字就慌,其实Java的异常信息已经告诉了你足够多的信息,关键是要练出“看异常找代码”的条件反射。
5. 一点过来人的建议
这套系统从设计到跑通我前后用了三周,其中第一周基本都花在熟悉Vue3和Element-Plus上。如果你也是从零开始,我给你两个切实际的建议。
第一个建议是:先画表结构,再写接口,最后写页面。很多同学上来就写前端页面,结果发现字段对不上、接口定得不合理,翻工量大得惊人。而先把数据库五张表建好,你就已经完成了整个系统一半的设计——前端需要什么数据、后端要提供什么接口,全都从表结构推导出来。后端接口先按我上面那张表列出来,每个接口的入参和返回类型先在纸上写明白,再动手写Controller和Service,顺序对了效率至少翻倍。
第二个建议是:给自己留一个“炫技点”。这个系统技术栈虽然常用,但答辩时大家都做一样的题目,靠什么拉开差距?就是看你在某个点上有超出课程要求的思考。我这里说的炫技不是花架子,而是真实的工程实践,比如乐观锁解决超卖、BCrypt加密存储密码、JWT无状态认证、Vue路由守卫控制权限、Nginx静态资源缓存。这些点不需要额外引入复杂框架,就是把你已经用到的技术做得更规范、更深入。答辩时候,评委问你“为什么这样设计”,你能答出背后的理由,分数自然就上去了。
项目做到最后,你会发现自己最受益的不是写完了一个系统,而是经历了“设计 → 开发 → 调试 → 部署”的完整闭环。这套能力远远超出某个技术点的本身,它会成为你做下一个项目时底气的一部分。