如果你今年也选了Java方向的毕设,而且题目里带着SpringBoot和校园出行这几个关键词,那我猜你大概率和我当初一样:题目看着眼熟,但不知道从哪里开始动工。我做的这个项目叫基于SpringBoot的校园顺风车平台,说白了就是把市面顺风车那套逻辑搬到高校场景里,解决的是跨校区上课、晚自习回宿舍、搬实验室、早八赶课这些固定路线的搭车需求。和商业网约车最大的区别在于,它不管"司机赚多少钱",只关心"顺路人怎么高效碰上"。
这篇文章我把整个系统的设计思路、数据库建模、匹配算法、关键代码、工程配置、答辩准备全部摊开讲一遍。适合正在为毕设发愁的计算机专业学生,也适合想用JavaWeb完整走一遍项目的朋友。我的技术栈是SpringBoot + MyBatis-Plus + MySQL + Redis,前端用了Vue3,但本文重心放在后端设计与实现,因为这才是毕业设计老师真正想看的东西。
1. 先聊两句选题:这个毕设要解决的是什么问题
1.1 校园场景和网约车场景的本质差异
做任何一个系统,第一步不是写代码,是先想清楚它服务的人和场景。校园顺风车和滴滴顺风车表面都是"乘客下单、车主接单",但细看逻辑差别很大。
校园里跑车的全是学生和教职工,没有职业司机,意思是每个人都可以是"发布者",也随时可以是"乘客"。今天A同学早八去本部,明天他可能就要从本部回校区,所以系统不能把用户固定成"司机"和"乘客"两个角色,而应该设计成"一人双身份":发布行程时你是车主,参与行程时你是乘客。这是很多第一次做类似项目的同学容易踩的坑——一上来就建了司机表和乘客表两套用户体系,后面写撮合逻辑时痛苦得要命。
另一个差异是路线极度固定。校园拼车不像城市顺风车那样"任意起点到任意终点",大多数校内出行都可以归纳为几个热门区域:宿舍区、教学楼区、图书馆、校门口、某个学院楼、高铁站接送。这意味着匹配不需要和真实地图API深度整合,用"区域名称 + 经纬度"的方式就能满足需求。这样做既降低了对接高精地图的成本,又能保证答辩时你能把匹配原理讲透。
第三个差异是信任机制。校内的交易天然带一层身份背书:学生学号、院系、宿舍楼都可以作为信用维度。商业平台靠实名和保险,校园场景就可以靠"同校身份 + 信用分 + 历史评价"来建立信任体系。这部分是项目加分的重要来源,后面我会单独展开。
1.2 技术栈定型的理由:Java + SpringBoot 的组合为什么合适
毕设选题时你可能会看到两种极端:一种是抱着SSH框架不放的老模板,一种是追求时髦用小程序云开发。我最后选了SpringBoot,原因有三条。
第一,SpringBoot解决了传统JavaWeb最折磨人的配置问题。以前写SSM要配web.xml、spring-mvc.xml、mybatis-config.xml一堆文件,少配一个扫描包跑半天才报错。SpringBoot把自动配置做进了框架里,你只需要在application.yml里写几行数据源参数,内嵌的Tomcat一启动就能跑起来。对毕设这种"时间紧、任务重、还要写论文"的项目,省配置就是省命。
第二,JavaWeb方向的课程设计和绝大多数学校的毕设要求,都默认你会SpringBoot。这点很现实。不管你们学校是要求"基于JavaWeb"还是明确写"基于SSM/SpringBoot",SpringBoot这一套知识体系都是通用答案。而且MyBatis-Plus又把这层持久层工作简化为"Mapper接口 + 注解",稍微有点JavaSE基础的人两个星期就能上手。
第三,就业向技术栈和毕设可以复用。SpringBoot、MySQL、Redis、Vue,这一套组合直接对标中小型公司的主流开发链。做毕设不只是为了过查重拿学分,答辩完挂在简历上时,面试官问的也会是SpringBoot自动装配、IOC容器、事务传播行为这些内容。用这个任务去倒逼自己把这些原理搞懂,比临时背八股文有用得多。
所以我的建议是:别在这时候去玩花活。用SpringBoot做后端,用标准的RESTful接口对接前端,数据库老老实实设计成范式,算法部分写得足够清晰,这个项目就已经超过平均水平了。
2. 数据库是系统的地基:七张表是怎么设计的
2.1 核心表拆解
数据库设计是毕设项目第一个真正考验你建模能力的环节。我设计表时遵循一个原则:每一张表只描述一类实体,每一类实体都能在现实业务里找到原型。整个系统我最终保留了七张核心表,具体字段和用途如下。
先看用户表。用户表包含id、username、password、real_name、student_no、phone、avatar、dept、role、credit_score这些字段。password用MD5加盐或者BCrypt加密存储,明文密码是绝对的低级错误,答辩时老师瞄一眼就会问。student_no对应学号,要加唯一索引,当作第二身份凭证。role字段其实不完全必要,我更倾向于用"当前操作身份"去判断,但留一个role字段方便做权限控制也没问题。credit_score就是信用分,默认100,扣到一定阈值就限制发布行程。
行程表是最核心的表。字段包括id、user_id、departure、destination、dep_lng、dep_lat、dest_lng、dest_lat、via_points、departure_time、seats、price、status、create_time。departure和destination存的是文字描述,比如"东校区南门";经纬度四个字段单独存,为匹配算法准备。va_points是途经点,我用JSON字符串存储,这个字段是整个"顺路"匹配的关键设计,下一小节细说。
订单表记录撮合结果。字段有id、trip_id、passenger_id、driver_id、status、pickup_point、dropoff_point、amount、create_time、confirm_time、complete_time。pickup_point和dropoff_point同样用文字+经纬度成对保存,因为乘客上车点和行程的起点不一定完全重合,约在校门口和约在宿舍楼下就是两个概念。
订单状态字段是status,我用整数表示更稳妥。0待确认、1已确认、2进行中、3已完成、4已取消、5已关闭。每个状态对应一组允许的操作,状态机的细节我放到2.3节展开,因为它是整个系统的业务规则核心。
另外还有评价表,字段为id、order_id、rater_id、ratee_id、score、content、create_time;消息表,用于站内通知;以及收藏/常用路线表,提高用户二次发布时的录入效率。这几张表逻辑相对独立,建议表结构设计时就预留。
2.2 "途经点"字段:顺路匹配的精髓
如果只是"起点匹配起点、终点匹配终点",那这个系统做的就是一个加上了距离计算的普通信息发布板,谈不上"智能撮合"。真正的"顺路"判定,应当允许司机在A和B之间存在中间路径,而乘客恰好能在这个路径的某个点上车或下车。
我在设计时给行程表加了一个多途经点概念。司机发布行程时可以依次添加多个途经点,比如"东门出发 → 软件园实验室 → 南门 → 商业街"。每个途经点带名称、经度、纬度。存储上我用JSON字符串,插入时把一组坐标对象序列化后存进va_points字段,读取时再解析成List。对这种低频更新的辅助字段,JSON存储比单独建一张子表更合适,查询便捷度反而更高。
匹配时,乘客的起点只要落在这条折线路径上的某个点附近(比如300米内),我们就认为这个行程可能适合他。计算方式不复杂,先判断乘客起点离哪一段路径最近,再计算点到线段的距离。这个算法加上Haversine距离公式,就是撮合引擎最核心的数学部分。你可以在论文里写清楚"基于多点路径的最近距离匹配方法",这就是一个可描述的创新点。
对应地,前端地图选点时也不需要做得太复杂。如果你不想对接完整的地图SDK,让用户在地图上点选坐标,再把点名和经纬度一起提交就行。实测中,这种点选方式比文字输入体验好很多,因为系统可以顺便完成经纬度字段的回填,减少前后端联调时数据对不齐的麻烦。
2.3 订单状态机设计
没有状态机的订单模块,代码全是一团浆糊。你在写接口时如果对着status字段一会儿拼一个1一会儿写一个3,过两天回头看自己都会乱。先定义清楚状态流转规则,再动手写代码,效率完全不同。
订单状态机的流转我定义为这样:乘客对某条行程发起"预约"时生成订单,状态是待确认;车主看到预约请求后,可以选择"确认接单"或者"拒绝",确认后订单变成已确认;双方都到了上车点,乘客点击"开始上车"或车主点击"出发"后状态变为进行中;到达目的地后任意一方点击"确认到达",系统校验后置为已完成,并触发信用分和评价流程。
任何一方想中途取消,分别走不同的扣分规则。乘客在车主确认前取消不扣分;车主确认后乘客取消,乘客扣信用分;车主在没有正当理由的情况下主动取消,车主扣分。这样设计是为了防止滥用:如果取消零成本,体验会迅速恶化。这个规则不需要写得很复杂,一个状态机类加上对应的校验方法就够了。
我在代码里用一个包private的枚举常量类去管理状态值,所有状态判断都走枚举,不直接写魔法数字。这样后期加状态、看日志、写单元测试都直观得多。论文里你也可以画一张状态流转图,老师非常吃这套设计逻辑。
3. 撮合引擎实现:Math.min 处理的问题比想象中多
3.1 Haversine 公式与经纬度计算
撮合引擎要解决的第一个基础问题,就是"两个经纬度点之间到底有多远"。球面距离计算不能用平面直角坐标系的欧氏距离,因为在地球表面,经度1度对应的实际距离在不同纬度是不一样的。我在这里用的是Haversine公式,它通过经纬度计算球面上两点间的大圆距离,误差在公里级以内,对校园场景完全够用。
具体公式是:
a = sin²(Δlat/2) + cos(lat1) * cos(lat2) * sin²(Δlng/2) c = 2 * atan2(√a, √(1−a)) d = R * cR取地球平均半径6371千米。在校园场景下,半径足够用。我把这个公式封装成了一个静态工具类,所有距离相关的地方统一调用。这里有个细节:计算前必须把经纬度从角度转为弧度,我第一次写这个工具时忘了转弧度,测试时匹配结果偏差被放大了将近57倍,排查了很久才意识到是这个低级问题。
工具类里我还多做了两个扩展方法,一个是"点到点距离",一个是"点到线段距离"。前面提过的途经点匹配,用的是点到线段距离:把乘客上车点作为点,把行程路径上相邻两个途经点组成的线段作为线段,计算最短距离。这段代码不复杂,网上有很多参考实现,把向量投影、夹逼判断写好就行。真正的工作量在于把这些方法组织好,形成一套可复用的地理计算工具集。
3.2 匹配规则:距离阈值 + 时间窗 + 方向角
有了距离计算方法,匹配规则就水到渠成了。我定的匹配策略整体是这样:
第一条是距离阈值。乘客起点和行程起点或路径的距离要小于等于300米,终点同理。校园里宿舍区和教学楼本身就集中,300米基本就是"出宿舍楼走到校门口"的步行范围,再大就容易出现"我在这头你从那头"的尴尬。
第二条是时间窗口。乘客期望出发时间和行程的出发时间的差值在±30分钟以内。顺风车本来就是"顺路带人",不是即时专车,时间上有一点弹性是合理的,但这个弹性不能太大。代码里可以写成start_time BETWEEN departure_time - 30 AND departure_time + 30。
第三条是方向校验。行程的起点终点方向和乘客的起点终点方向要一致,不能"你从东到西,他想从南到北"。最简单的判定是计算起点到终点连线的方位角,如果角度偏差超过了90度,就过滤掉。当然如果有途经点在中间勾连,方向判断就要放宽,否则会漏掉一些中间路径真的顺路的行程。
实际操作中,我把这三步组织成一个"候选行程生成"流程。先用SQL把基础条件过一遍:待匹配状态、出发时间窗口、起点终点文字匹配。再在内存里做精确的坐标计算和方向判定。为什么要分两层?因为纯SQL做经纬度距离计算需要反复调用自定义函数,索引用不上,写起来还绕;纯内存计算又要把全表的行程都捞出来,数据量一大就卡。分两层之后,SQL负责用粗糙条件快速缩小范围,Java负责精确计算,性能问题就能化解。
3.3 并发场景:如何防止一个座位被多个人抢走
毕设系统看起来是单机应用,但"多人同时抢同一个行程的座位"一定会被问到。合理的处理方式是:先保证剩余座位字段的正确性,再做并发控制。
我使用了乐观锁。在行程表里加一个version字段,每次更新座位数时,在SQL里同时校验version值,例如:
UPDATE trip SET seats_left = seats_left - 1, version = version + 1 WHERE id = ? AND seats_left > 0 AND version = ?这条SQL执行后如果返回影响行数为0,说明座位已满或版本号对不上,系统就提示"座位已被预约"。这是最简单有效的防超卖方案。更进一步,我在用户下订单时先用Redis的SETNX做一道轻量级锁,避免极端情况下同一个用户重复点击提交按钮产生两条订单。锁的key设计为"order:trip:{tripId}:{userId}",加一个几十秒的过期时间,防止锁永久占用。
状态机的并发问题也要注意。比如乘客点击取消订单的同时,车主正在确认接单,通过状态值校验来防止非法状态流转很关键。每一次状态更新都要带上期望的状态值,确保只在正确状态上做修改。这相当于把乐观锁思想用在了业务状态上,比写一堆synchronized块靠谱得多。
4. 核心代码走读与工程配置
4.1 发布行程与检索行程接口
代码实现部分,我重点讲三个接口:发布行程、搜索行程、下单接单。这三个接口走完,整个系统的核心业务就闭环了。
发布行程接口的流程是:接收前端传来的表单数据,校验必填字段,把途经点字符串解析成JSON数组存入行程表,设置初始状态为"可匹配"。行程发布时间不能晚于出发时间,这是一个容易被忽略的业务校验,后端必须做,不能只靠前端拦截,因为前后端联调时总有办法绕过前端验证。
示例代码大致是这个结构:
@PostMapping("/api/trip/publish") public Result<TripVO> publish(@RequestBody TripDTO dto) { Trip trip = new Trip(); BeanUtils.copyProperties(dto, trip); trip.setStatus(TripStatus.MATCHING.getCode()); trip.setSeatsLeft(dto.getSeats()); trip.setVersion(0); tripService.save(trip); return Result.success(TripVO.from(trip)); }搜索行程接口是核心查询接口。接收参数为起点、终点、出发时间、距离阈值。实现时先将起终点文字转成经纬度坐标,然后按时间窗口过滤,再查候选列表,最后在内存中计算精确距离和方向。返回时按"顺路程度"排序,顺路程度用"乘客起点到司机路径的最短距离 + 乘客终点到司机路径的最短距离"综合打分,距离越小越靠前。
4.2 下单与确认链路
乘客发起预约时,系统要先查询行程状态是否还是可匹配,然后检查剩余座位数,通过校验后生成订单,同时调用扣减座位数。这里要记住一个原则:创建订单和扣减座位必须放在同一个事务里,否则会出现订单创建成功但座位没扣,或者座位扣了但订单创建失败的数据不一致问题。
我在创建订单的Service方法上加了@Transactional(rollbackFor = Exception.class)注解,保证原子性。同时用乐观锁的那条SQL去更新座位数,如果返回值小于1,直接抛出业务异常,事务回滚,前面创建的订单也随之撤销。这样整个链路既保证了并发安全,又保证了数据一致。
车主确认接单的接口相对简单,更新订单状态为已确认,同时给乘客发一条站内消息。这个部分不值得铺开写,但一定要做,因为站内信功能是体现系统完整度的重要加分点。
4.3 SpringBoot 工程里那些必须提前配好的参数
SpringBoot项目搭建本身不难,但有几个配置位置容易出错。先看application.yml里最基础的部分:
server: port: 8081 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_carpool ?useUnicode=true&characterEncoding=utf8 &serverTimezone=Asia/Shanghai username: root password: xxxx redis: host: localhost port: 6379如果你用的是MySQL 8.x,驱动类必须写com.mysql.cj.jdbc.Driver,旧版com.mysql.jdbc.Driver已经不能用了。url里要带serverTimezone=Asia/Shanghai,否则会报时区错误。Redis的序列化器也建议配置一遍,把默认的JDK序列化改成Jackson或Fastjson序列化,否则在Redis里看数据是乱码,排查问题时会非常痛苦。
开发环境里SpringBoot项目默认端口是8080,如果你本机已经跑了其他项目,或者前端代理配置和端口不匹配,可以在IDEA的运行配置里覆盖。操作路径是:点击右上角运行配置下拉按钮,选择Edit Configurations,在你的SpringBoot应用配置里找到Environment变量位置,添加一行参数server.port=8081,或者在Program arguments里写--server.port=8081,效果一样。这个操作在IDEA 2026版本里依旧适用,只要项目运行方式是Spring Boot类型配置就行。
MyBatis-Plus的配置我也提一下。Mapper接口记得加@MapperScan注解,或者在启动类上标注扫描包路径。日志级别在开发阶段设置成debug,方便打印SQL语句,实机演示时再改回info,否则控制台会被SQL刷屏。
5. 一个能过答辩的加分项:报表、文件上传与分布式会话
5.1 用 MinIO 处理头像和学生证上传
做完核心业务,再打磨几个"非核心但显技术深度"的功能,答辩讲出来的效果完全不一样。比如文件上传服务,学生头像、学生证照片、平台公告图片,都涉及存储。存本地磁盘虽然简单,但不利于后续扩展,也不好讲出亮点。我建议用MinIO。
MinIO是一个开源的对象存储服务,兼容Amazon S3接口,能在本地一键启动,非常适合毕设场景。SpringBoot整合MinIO的步骤很固定:引入minio依赖,配置endpoint、accessKey、secretKey、bucketName,注入MinioClient实例,然后写一个上传接口,把文件流封装成InputStream,设置bucket和objectName,返回可访问的URL。
这里有一个提高系统安全性的细节:不要把所有桶都设成公共读权限。一般会建两个桶,一个放公开静态资源,一个放私密文件。私密文件上传后,通过生成带有效期的预签名URL给前端访问,过期时间可以设为5分钟。这样你在答辩时可以讲"我考虑了文件访问的时效安全性",瞬间拉开和普通项目的差距。
5.2 POI 生成 Word 报表的技巧
毕设系统大多要包含"数据统计"或"报表导出"功能。很多同学一听到报表就联想到POI操作Excel,能用,但如果你需要输出的是"月度拼车统计报告"这类文档型内容,Word格式反而更贴近实际使用场景。
这里有一个很容易在网上查资料时搞混的概念:POI的Word处理模块并不能像Excel那样直接在页面里生成图表对象。网上有人搜"java poi word能生成图表吗",标准答案是:POI能操作Word,也能在Word里插入图片,但"生成图表"必须先把图表渲染成图片,再嵌入Word文档。我建议用XChart库在服务端生成柱状图、折线图图片,然后把图片字节流插入到POI创建的XWPFDocument中。这样就实现了"Word报表自带统计图"的效果,答辩时打开演示文档,视觉效果非常好。
5.3 为什么建议你做"信用分"而不是积分商城
很多同类毕设喜欢做一个积分商城,发布行程送积分,签到送积分,然后兑换小礼品。这个功能不是不好,但它和系统的核心业务脱离,做完基本是一个孤立模块。我更推荐做信用分机制,因为它直接作用于撮合业务,能让整套系统有"规则感"。
我的设计是:用户初始信用分100,上限120,每次完成行程且双方互评好评,信用分加1-2分;车主取消已确认订单扣5分;乘客多次超时未上车扣3分;信用分低于70分后,不能发布行程,只能作为乘客参与;低于50分,平台限制其参与拼车,并发送警告消息。这些规则全部用一张信用记录表存明细,前端展示"信用分变化趋势"的简单折线图。
这套机制回答了两个核心问题。第一,"系统如何约束用户行为",信用分就是这个答案。第二,"评价体系如何影响后续匹配",低信用用户发布的行程会降低推荐权重。把这两点讲清楚,整个系统的闭环逻辑就完整了。
6. 实操避坑清单 + 答辩准备
6.1 我踩过的几个比较真实的坑
坑一:MySQL时区报错。项目启动时一直报The server time zone value,我花了一个小时才发现是url缺了serverTimezone参数。这个问题太典型了,几乎所有第一次用MySQL 8配合SpringBoot的人都会遇到。
坑二:前端跨域。我在Vue前端的请求端口是5173,后端是8081,没有配置CORS时浏览器直接拦截所有请求。解决办法是写一个WebConfig类实现WebMvcConfigurer,加上addCorsMappings允许跨域,指定allowedOrigins为自己前端的地址。注意不要用*去通配,因为涉及到携带凭证请求时通配符会失效。
坑三:IDEA运行SpringBoot项目时,控制台中文乱码。这是编码问题,在IDEA的Help菜单里找到Edit Custom VM Options,加一行-Dfile.encoding=UTF-8,重启后基本解决。不经处理,数据库里的中文都正常但控制台日志乱码,非常影响调试心情。
坑四:MinIO上传后访问URL打不开。这通常是因为local地址配置不一致,上传时的endpoint填了localhost,但浏览器访问时填的又是127.0.0.1。保持endpoint一致即可,尽量都用localhost。
6.2 老师最可能问的三个问题
答辩时,老师不一定真的会去运行你的系统,更多是围绕设计问逻辑和原理。我整理了被问频率最高的三个问题。
第一个问题:"顺路匹配到底怎么实现的?"回答思路:从Haversine公式开始讲球面距离计算,再讲途经点路径匹配用到点到线段距离,最后讲匹配策略的三重过滤(距离阈值、时间窗口、方向角)。可以边说边在代码里指出对应的工具类和过滤方法。这个问题的答案贯穿全文,只要逻辑顺,老师基本就会点头。
第二个问题:"为什么用SpringBoot而不用其他框架?"回答思路:自动配置简化开发流程、内嵌Tomcat快速启动、生态完善便于整合MyBatis-Plus和Redis、与当前企业主流技术栈一致。千万别只说"因为学校推荐",要把技术优势讲出来。
第三个问题:"并发情况下怎么保证不会超卖?"回答思路:乐观锁版本号更新座位数 + Redis分布式锁防止重复下单 + 状态机约束订单状态流转。把这三层都说出来,已经是一个中等偏上水平的技术回答了。
6.3 给后面做这个题目的同学一句实在话
整个系统从零到能跑,我大概用了三周半,其中第一周全在搞数据库设计和匹配规则推导。别一上来就埋头敲代码,先把业务场景想透,把表结构定下来,把状态流转图画出来,后边的开发大多是体力活。
数据库建模时一定要把经纬度字段和途经点字段预留好。我见过的很多人前期表里没有坐标字段,后期撮合算法写不下去,回过头来改表结构,牵一发动全身。阅卷老师不一定期待一个多复杂的项目,但他一定期待一个自洽的、逻辑闭环的业务系统。你只要把这套规则讲清楚,比堆砌十个功能页面有用得多。
我在实际开发里的体会是:这类毕设最值得投入的时间点有两个,一是数据库设计,二是撮合算法。前者决定了系统结构是否牢固,后者决定了项目能不能提炼出独特的业务亮点。最后再分享一个答辩小技巧:准备一张数据流程图,把"用户发布行程 → 系统计算顺路匹配 → 乘客预约 → 车主确认 → 出行完成 → 互评与信用分更新"这条主线画出来。现场讲题时,就照着这张图讲,老师不用翻代码就能明白你的设计思路,你也能从被动回答问题变成主动展示项目。剩下的时间,多测试几个真实场景,比如多人同时抢座、跨校区远距离匹配、取消订单扣分,跑通了,答辩就稳了。