☰
基于Web的出租车拼车系统设计与实现:从架构到并发控制全解析
2026/10/7 19:02:53 网站建设 项目流程

每年一到毕业季,后台都会收到一堆关于“Java毕设怎么选”、“源码跑不起来怎么办”的私信。我自己的习惯是,不管学生还是刚转行的朋友来问,如果手里正好没什么好题目,我一般都推荐一类系统:业务完整度够、技术栈主流、演示效果好、答辩还能讲出花来的Web项目。今天要拆解的这套“基于Web的出租车拼车系统”,就是我心目中非常典型、也很有嚼头的一个毕设题目。它不只是一个普通的增删改查,里面既有用户端、司机端、管理后台三类角色的权限区分,又有拼车匹配、订单流转、计费规则这些带点算法味道的逻辑,再加上前后端分离的架构,用来做毕设或者简历项目都非常合适。下面我按自己设计这套系统的思路,把核心模块、数据库设计、匹配算法的常见写法、以及调试部署时最容易踩的坑一次讲清楚。

1. 为什么选这个题目:毕设选题的思路与价值判断

1.1 这道题到底在考什么

很多同学挑毕设题目的时候,容易掉进两个极端:要么选了纯商城、纯博客这种太“平”的题目,做到后面发现自己只是在堆CRUD,答辩老师问一句“你的系统难点在哪”就哑火了;要么选了人脸识别、推荐系统这种看上去高大上的题目,结果一半时间耗在调模型上,代码跑不出来,最后连演示都翻车。出租车拼车系统正好卡在中间:难度适中,但每一层都有东西可讲。

从功能上说,它至少覆盖了三类角色的完整业务流程。乘客要能注册登录、发布行程、搜索拼车单、发起预约、在线支付;司机要能录入车辆信息、发布空余座位、接单、确认行程、结算;管理员要能审核司机资质、审核行程、处理投诉、看统计报表。这三条线走通,就是一个标准的多角色Web应用,能覆盖软件工程课上学到的几乎所有知识点。

从技术上说,它天然适合用Spring Boot + MyBatis做后端,Vue/Element UI做前端,MySQL存业务数据,Redis存会话和热点数据。这套组合现在就是Java岗位的日常标配,做出来之后简历上直接能写“独立设计并实现一套前后端分离的拼车平台”,比写一百遍“熟悉Spring Boot”都有说服力。

1.2 一个题目能讲出几个层面的故事

我一般会跟学生说,毕设的“深度”不是靠堆功能,而是靠把一个点挖透。这道题能挖的点其实不少:

  • 拼车匹配逻辑:虽然不用做到滴滴那么智能,但你可以实现“按出发地距离 + 目的地相似度 + 出发时间窗口”的综合打分,这就有算法含金量了。
  • 并发控制:同一个行程的座位数有限,多个乘客同时抢同一个座位怎么防止超卖?这就能引出乐观锁、Redis事务这些进阶话题。
  • 状态机设计:一条拼车订单从乘客发起、司机确认、乘客上车、行程结束到结算完成,中间有取消、超时、投诉等分支,怎么设计状态流转才能不乱?这是面试里特别爱问的东西。
  • 角色权限模型:乘客、司机、管理员三套菜单和操作权限,用拦截器或Spring Security怎么控制?

你看,光这四个点展开写,论文工作量就出来了,而且都不算超纲。

1.3 适合谁来参考

如果你是基础一般、想踏踏实实做完一个项目顺利毕业的本科生,这道题友好;如果你手里已经有一两个小项目,想冲一下更好看的简历去面试,这道题也能给你足够的话题。就算你已经工作了,想拿这套系统当脚手架改成网约车、顺手车、通勤拼车之类的产品,代码结构稍作调整就能复用,我自己做的时候就是抱着“以后要扩展”的想法去设计的。

2. 技术选型与整体架构设计

2.1 后端为什么是Spring Boot + MyBatis-Plus

现在毕设如果用SSH(Struts + Spring + Hibernate),答辩老师看了都会皱眉,因为这套东西已经脱离生产实践了。Spring Boot 2.x + MyBatis-Plus基本是当前Java Web毕设的最优解,理由很实在:

  • Spring Boot的自动配置让环境搭建成本很低,一个Spring Initializr就能生成可运行的骨架,不用像SSH那样写一堆XML配置。
  • MyBatis-Plus在原生MyBatis上加了通用Mapper和LambdaQueryWrapper,写单表查询基本不用手写SQL,开发效率高出一大截,尤其是我们做订单列表分页、按条件筛选这类操作,几行代码就能搞定。
  • 它对事务、拦截器、多数据源的支持很成熟,就算后面想加ShardingSphere分库分表,也能平滑扩展。

数据库我选的是MySQL 5.7+。需要注意的坑是:不要装8.0以上版本后使用默认的认证插件,否则JDBC连接会报Unable to load authentication plugin 'caching_sha2_password',要么在连接串上加allowPublicKeyRetrieval=true,要么直接建用户时指定mysql_native_password。很多同学项目跑不起来,问题不在代码,就卡在这个连接上。

2.2 前端分离方案的取舍

这题的演示效果好不好,前端占了很大比重。我建议用前后端分离:Vue 2.x + Element UI + Axios。后端只需要提供一套RESTful API,前端单独跑在8080端口,通过代理转发到后端8081。这样做的好处是:

  • 开发和调试互相不阻塞,我改后端接口不影响前端页面,反之亦然。
  • 答辩演示的时候可以一边开着Vue devtools看数据流,一边开着Swagger看接口文档,显得你非常专业。
  • 代码结构上是两个独立工程,论文里可以专门写一章“前后端交互与接口设计”,内容量很充实。

当然,如果你对Vue不熟,也可以用Thymeleaf模板引擎直接在后端渲染页面,项目结构会简单一些,但可展示的深度会打折。我的建议:既然题目是“基于Web的出租车拼车系统”,那就彻底一点,做成现在企业里最常见的前后端分离形态,这也是最容易被面试官认可的。

2.3 系统分层与包结构规划

拿到源码以后,第一件事不是急着跑,而是先看包结构。我习惯这样分:

com.example.carpool ├── controller # 接口入口,只做参数接收和响应封装 ├── service # 业务逻辑层,事务注解标注在这里 │ └── impl ├── mapper # MyBatis-Plus的Mapper接口,继承BaseMapper ├── entity # 数据库实体类,标注@TableName ├── dto # 前端传入参数对象,加上@Validated校验 ├── vo # 返回给前端的结果对象 ├── config # 跨域配置、WebMvcConfigurer、MyBatisPlus分页插件等 ├── common # 统一返回结果Result、异常处理、常量类 └── CarpoolApplication.java

这种分层的好处是职责清晰:Controller里绝对不写SQL,Service只做业务编排,Mapper只管数据库交互。答辩时被问到“高内聚低耦合怎么体现的”,直接把这套结构讲一遍就可以了。代码评审和后续扩展也省心,比如加一个“优惠券”模块,新增entity、mapper、service、controller四个类就行,其余地方不用动。

2.4 统一响应体与异常处理

一个很容易被忽略但很加分的点是统一响应结构。我定义了一个Result<T>类,所有接口都返回这种格式:

public class Result<T> implements Serializable { private Integer code; // 200成功,500业务异常,401未登录 private String msg; private T data; // 省略构造方法和静态工厂方法 // 常用:Result.success(data)、Result.error("座位不足") }

前端Axios封装了响应拦截器,判断code字段决定是弹this.$message.error(msg)还是正常渲染。这样整个系统的错误提示是统一的,不会出现有的接口返回{"error": "xxx"},有的返回{"message": "yyy"}这种杂乱情况。

全局异常处理我用@RestControllerAdvice。业务层抛出BusinessException("该行程已被预约"),全局处理器捕获后返回Result.error,HTTP状态码仍然保持200,这是很多互联网公司的做法——用业务code而不是HTTP状态码来区分业务成败,前端处理起来更简单。如果你把这个设计写进论文的“系统非功能性设计”一节,是很稳的加分项。

3. 核心功能模块与业务流程拆解

3.1 用户端:从注册到拼车成功的完整链路

用户端是功能最密集的地方,我们用一条“乘客小张想拼车”的路线来看所有用例。

小张第一次来,需要注册。注册表单里有用户名、手机号(这里建议做格式校验)、密码(用BCrypt加密存储,千万不要明文存密码)。注册成功后登录,后端用JWT签发一个token返给前端,前端存到localStorage,后续每次请求在header里带上Authorization: Bearer <token>。这个机制几乎是Java岗面试必问,你能讲清楚token无状态验证和Session的区别,已经超过大半求职者了。

登录后小张有两种拼车姿势:

第一种是“搭别人的车”。他先去行程大厅看列表,列表展示了司机的出发地、目的地、出发时间、剩余座位、拼车单价。筛选条件按他的目的地查,比如只想去“高新区软件园”的。他选中一条行程后点击“预约拼车”,后端就要做一件事:判断该行程剩余座位是否大于0,如果大于0则创建一条PENDING状态的拼车订单,并临时锁定一个座位。这个“锁座位”非常重要,我会在后面的并发小节细说。

第二种是“找人与他拼车”。小张自己发布一条行程:从“白水塘小区”到“软件园”,时间是明天早上8点半,共享座位数2个,备注可以带宠物或大件行李。发布后他在自己行程列表里能看到谁发起了拼车申请,点“同意拼车”之后,对方才算是拼车成功。

完整的链路走到这里,已经覆盖了“注册->登录->找行程->预约->发布行程->处理申请”六个核心用例,足够撑起用户端的工作量了。

3.2 司机端:车辆认证与行程管理

司机端不是单独做一个App,而是在同一个Web系统里用角色区分。司机注册后默认是乘客角色,只有提交了驾驶证、行驶证、车辆照片等信息并经过管理员审核,才能开通“发布行程”的权限。

这里有一个设计决策值得写进论文:司机与车辆的信息是一对多还是多对一?现实中一个司机可以拥有多辆车,但为了毕设简化,我采用一司机一车,直接字段挂在用户表扩展信息里。答辩如果被问到“怎么支持一个司机多辆车”,就说预留了driver_vehicle关联表,当前版本为了方便演示做了简化,这样既回答了问题,也不显得你没想到。

司机发布行程的页面里,要自动带上他车辆的座位数、车牌号、车辆品牌颜色,这些信息从司机认证记录里读取。行程发布成功后,司机端出现两个列表:我发布的行程和待处理的拼车申请。点“同意申请”,系统给乘客推送一条消息(毕设里用站内消息即可,不用接短信SDK);点“拒绝”,释放座位。

3.3 管理后台:审核、风控与数据统计

管理后台是很多同学容易只做一半的模块,但它恰恰是答辩老师第一眼会点进去看的地方。我的设计里包含这些页面:

  • 司机认证审核:查看上传的证件照片,通过或驳回,驳回要填原因。
  • 行程审核:对敏感目的地、异常定价的行程做下架处理。
  • 拼车订单管理:按订单号、乘客、司机、时间范围查询,处理用户投诉,可强制取消订单。
  • 数据统计:用ECharts画三张图——每日拼车订单量折线图、热门线路Top10柱状图、司机完单量排行榜。这些统计SQL不难,但非常能体现你的数据意识。

管理员账号我建议单独用初始化SQL插入,不要和普通用户注册通道混在一起,防止有人把自己权限改成管理员。

3.4 拼车匹配的核心逻辑:不止是模糊查询

题目叫“拼车系统”,匹配算法自然是最有含金量的部分。最简陋的写法是前端传一个关键词,SQL里LIKE '%keyword%',这样只能叫搜索,不能叫匹配。我给出一个折中但很实用的方案:出发地范围匹配 + 目的地文本相似度 + 时间窗口打分。

先说思路。用户A要找拼车,他给三个参数:出发地经纬度、目的地文本、出发时间。系统遍历所有状态为OPEN的行程:

  1. 出发地距离:计算行程发布者出发地与A出发地的球面距离,小于等于3公里才进入候选。
  2. 出发时间差:绝对值小于等于1小时进入候选。
  3. 目的地相似度:把发布者的目的地文本和A的目的地用字符串相似度算法(比如编辑距离或Jaccard相似度)计算得分。
  4. 最终排序:综合得分 = 距离得分 × 0.4 + 时间得分 × 0.3 + 目的地相似度 × 0.3。

球面距离用Haversine公式计算,Java代码不长,但很有面试话题:

public static double getDistance(double lat1, double lng1, double lat2, double lng2) { double radLat1 = Math.toRadians(lat1); double radLat2 = Math.toRadians(lat2); double a = radLat1 - radLat2; double b = Math.toRadians(lng1) - Math.toRadians(lng2); double s = 2 * Math.asin(Math.sqrt( Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); return s * 6371.008; // 返回公里数,6371是地球平均半径 }

这块可以放在Service层一个单独的MatchService里,不要写在Controller里。答辩的时候,把匹配公式一讲,老师基本就能判断你是真做了系统的,而不是只背了八股文。

4. 数据库设计与订单状态机

4.1 核心表结构设计

整套系统的表我控制在九张以内,既够用又不冗余:

表名说明关键字段
user用户表(乘客/司机/管理员)username, password(BCrypt), phone, role, status
driver_info司机认证信息user_id, license_no, car_no, car_model, car_seats, audit_status
travel行程表(司机发布)driver_id, origin, destination, origin_lnglat, dest_lnglat, depart_time, total_seats, remain_seats, price_per_seat, status
carpool_order拼车订单表order_no, travel_id, passenger_id, seats, amount, status, create_time
message站内消息表from_id, to_id, content, type, is_read
complaint投诉表order_id, user_id, content, handle_status
admin_log操作日志表admin_id, action, target_type, target_id
dict数据字典type, name, value

设计的时候有两个细节容易踩坑,必须提醒:

  • 金额字段不要用float或double,要用DECIMAL(10,2)。浮点数算金额会出现0.1+0.2=0.30000000000000004这种问题,答辩时被指出来非常尴尬。
  • 时间和origin_lnglat这种字段在MySQL里类型要选对:日期时间统一用datetime,经纬度分别用两个decimal(10,6)字段存,比存一个字符串再解析方便得多,后续计算距离也不用折腾格式。

4.2 订单状态机与异常分支

拼车订单我定义了以下状态:

PENDING(待司机确认) -> CONFIRMED(已确认) -> ON_BOARD(乘客已上车) -> FINISHED(已完成) PENDING -> CANCELED(乘客/司机取消) CONFIRMED -> CANCELED(行程取消) CONFIRMED -> COMPLAINT(投诉中) -> SETTLED(已处理)

状态流转的合法性判断写在Service里,不是前端控制。比如乘客取消订单时,后端判断订单当前状态是不是PENDING,是才能取消,如果是CONFIRMED就进入“协商取消”流程,这种细节写进论文里非常加分。

创建订单时的并发问题我觉得值得多写几句。假设一个行程剩余座位只有1,但同时来了3个乘客预约。如果用最简单的写法:SELECT remain_seats FROM travel WHERE id=1,判断大于0就UPDATE remain_seats = remain_seats - 1,在高并发下会产生“幻读”,三个人都读到1,都执行更新,最后变成-2,这就是超卖。

解决方案有两个:

  • 乐观锁:UPDATE travel SET remain_seats = remain_seats - 1 WHERE id = ? AND remain_seats > 0,更新后判断影响行数,等于0说明座位已经被抢完,返回“手慢了”。
  • Redis秒杀式:拼接座位数量Key,用DECR操作原子扣减,扣到负数就拒绝。

毕设项目没有真正的高并发流量,但答辩老师问“你怎么防止超卖”时必须答得出来。我推荐第一种,因为它没有引入额外组件,逻辑也直观,代码里就一个SQL的事。

4.3 计费逻辑与订单金额

拼车计费这块,我的简化规则是:乘客支付金额 = 拼车单价 × 座位数。但要注意,一个乘客一次性预订2个座位,单价可以打95折,这是很自然的运营策略,写进论文里又是一个小亮点。计算时机是乘客确认下单时,金额直接存储到订单表,不要每次都现算。

另外,订单生成时单号要设计一下。网上常见错误是直接用UUID.randomUUID(),字符串32位太长且无序。我用的方案:yyyyMMddHHmmss + 6位随机数 + 用户ID后四位,这样从单号就能看出下单时间,也方便查问题。

String orderNo = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + String.format("%06d", ThreadLocalRandom.current().nextInt(999999)) + String.format("%04d", userId % 10000);

4.4 事务边界怎么划

什么叫“创建拼车订单成功”?

  1. 查询并校验行程状态和剩余座位。
  2. 扣减行程剩余座位。
  3. 插入拼车订单记录。
  4. 给司机发送站内消息。

这四个操作必须在一个事务里,任何一个失败都不能留下脏数据。做法是:在Service方法上加@Transactional(rollbackFor = Exception.class),注意要指定rollbackFor,因为Spring默认只回滚运行时异常,如果方法里抛了受检异常而不指定,事务是不会回滚的。这个坑很隐蔽,我见过不少同事栽在这上面。

5. 关键功能实操:拼车匹配和预约的代码实现

5.1 实体和Mapper层的快速搭建

用MyBatis-Plus的BaseMapper,实体类只需要做字段映射声明,无需手写XML。

@Data @TableName("travel") public class Travel { @TableId(type = IdType.AUTO) private Long id; private Long driverId; private String origin; private String destination; private BigDecimal originLat; private BigDecimal originLng; private BigDecimal destLat; private BigDecimal destLng; private LocalDateTime departTime; private Integer totalSeats; private Integer remainSeats; private BigDecimal pricePerSeat; private Integer status; // 0已发布 1已满 2已取消 3已完成 }

Mapper接口:

@Mapper public interface TravelMapper extends BaseMapper<Travel> { }

分页插件在Config里注册一下,后面查列表用Page对象:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

5.2 匹配算法的Service层实现

下面这段就是我前面说的匹配逻辑的具体落地,方法入参是乘客想去的终点、出发地经纬度和时间。注意这里简化了:出发地距离、时间差、目的地文本相似度都要经过计算。

@Service @RequiredArgsConstructor public class MatchServiceImpl implements MatchService { private final TravelMapper travelMapper; @Override public List<Travel> matchTravels(String destText, double lat, double lng, LocalDateTime time) { // 1. 先粗筛:只查已发布且未满座的行程 LambdaQueryWrapper<Travel> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Travel::getStatus, 0) .gt(Travel::getRemainSeats, 0); List<Travel> candidates = travelMapper.selectList(wrapper); // 2. 打分排序 return candidates.stream() .map(t -> { double distanceScore = scoreDistance(lat, lng, t.getOriginLat().doubleValue(), t.getOriginLng().doubleValue()); double timeScore = scoreTime(time, t.getDepartTime()); double destScore = calculateSimilarity(destText, t.getDestination()); double total = distanceScore * 0.4 + timeScore * 0.3 + destScore * 0.3; t.setScore(total); // 临时字段 return t; }) .filter(t -> t.getScore() > 0.5) .sorted(Comparator.comparingDouble(Travel::getScore).reversed()) .limit(20) .collect(Collectors.toList()); } }

假设出发地3公里以外直接给0分,时间差超过1小时也给0分,目的地相似度低于0.3就给0分,这样能一次性把低质量候选过滤掉,不用在SQL里拼一堆复杂的条件。

5.3 预约订单的Controller与事务处理

Controller层保持简洁,只接收请求参数和返回统一结果。下面这是预约拼车的完整入口:

@RestController @RequestMapping("/api/order") @RequiredArgsConstructor public class OrderController { private final OrderService orderService; @PostMapping("/create") public Result<CarpoolOrder> create(@RequestBody @Valid CreateOrderDTO dto, @RequestAttribute Long userId) { return Result.success(orderService.createOrder(dto, userId)); } }

Service层的实现,我加了详细注释:

@Transactional(rollbackFor = Exception.class) @Override public CarpoolOrder createOrder(CreateOrderDTO dto, Long userId) { // 1. 查行程并加锁,防止并发超卖 Travel travel = travelMapper.selectByIdForUpdate(dto.getTravelId()); if (travel == null) { throw new BusinessException("行程不存在"); } if (travel.getStatus() != 0 || travel.getRemainSeats() < dto.getSeats()) { throw new BusinessException("该行程座位不足或已关闭"); } // 2. 扣减座位 travel.setRemainSeats(travel.getRemainSeats() - dto.getSeats()); if (travel.getRemainSeats() == 0) { travel.setStatus(1); } travelMapper.updateById(travel); // 3. 创建订单 CarpoolOrder order = new CarpoolOrder(); order.setOrderNo(generateOrderNo(userId)); order.setTravelId(travel.getId()); order.setPassengerId(userId); order.setSeats(dto.getSeats()); order.setAmount(travel.getPricePerSeat().multiply(new BigDecimal(dto.getSeats()))); order.setStatus("PENDING"); orderMapper.insert(order); // 4. 通知司机 messageService.send(travel.getDriverId(), userId, "您有一条新的拼车预约,订单号:" + order.getOrderNo()); return order; }

这里用了selectByIdForUpdate,对应SQL是SELECT ... FOR UPDATE,是用悲观锁控制并发。虽然前面推荐的乐观锁更轻量,但在这段业务里,因为事务里要读取并扣减,悲观锁更直观不易出错。两种方案都写了,你答辩时就可以说:我用了乐观锁的SQL扣减思想,同时也在关键路径用FOR UPDATE做兜底,防止极端并发。这句话一出来,老师基本不会再往下追问了。

5.4 前端联调里的几个细节

前端我用Vue2 + Element UI + Axios,起步流程就三步:

  1. npm install安装依赖。
  2. 在vue.config.js里配置代理,把/api开头的请求转发到后端地址。
  3. 写一个request.js封住Axios,统一加token、统一处理错误码。

有一个很常见的问题:前端访问http://localhost:8080/api/xxx,而后端跑在8081,如果不配置代理,浏览器会直接跨域报错。配置看起来是这样:

devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } }

同时后端也要做好跨域配置,因为可能有人绕过代理直接调试。我用一个WebMvcConfigurer注册全局CORS:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

两个解决方案其实是双保险,一个是前端代理,一个是后端放行。实际企业开发一般只保留后端放行就行,但毕设环境里大家开发方式不一样,我特意都写上,避免你哪个环节卡住。

6. 部署运行与调试避坑实录:让源码真正“能跑起来”

6.1 环境准备:JDK、Maven、MySQL、Redis

我拿到一套新的毕设源码,第一步永远是“按能跑的最小集去配环境”,你别一上来就开两个IDE调试,先确认基础软件版本没问题。建议组合与注意事项如下:

组件推荐版本注意事项
JDK1.8 / 11项目pom里的java.version要和本机一致
Maven3.6+源里如果有私服的依赖要确认拉得到,不然一直编译失败
MySQL5.7 / 8.0初始化SQL先执行,注意字符集要UTF-8,排序规则utf8mb4_general_ci
Redis5.0+如果项目里用到Redis缓存登录态,要先启动Redis,否则后端启动报连接超时
Node.js14+前端工程编译需要,太老的版本装依赖容易报错

拿到源码后不要立刻跑,先看一遍application.yml里的数据库账号、密码、端口,再执行init.sql初始化数据。我见过很多学生把账号密码写错或没执行SQL,启动起来页面一片红,这个锅真不在代码。

6.2 启动后端常见的几个报错

后端起不来,九成是以下几种原因,每一条都是我这几年远程帮人调源码时高频碰到的:

  • 端口被占用:后端8081被之前没关干净的程序占着,启动直接Port already in use。Windows下用netstat -ano | findstr 8081查PID,再taskkill /PID xxx /F杀掉即可。Mac/Linux用lsof -i:8081。
  • 数据库时区错误:报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。连接串加上serverTimezone=Asia/Shanghai,并确保MySQL全局时区是+08:00。
  • Redis连接失败:如果你把登录token存Redis,Redis没启动或者密码不对,启动日志会一直刷连接异常。本地开发可以先在配置里换成JWT本地校验,不依赖Redis,演示环节更稳。
  • MyBatis-Plus扫描不到Mapper:启动类上忘记加@MapperScan("com.example.carpool.mapper"),所有的Mapper注入都会失败。这个IDE可能会提示Field xxxMapper in ... required a bean of type。

6.3 前后端联调时的“经典三连问”

前端页面能打开但数据是空的,或者接口报跨域错、返回401,基本上就是三件事没做好:

  1. 后端接口路径对不对?打开http://localhost:8081/swagger-ui/index.html,对照接口文档看一眼。
  2. 登录token是否存上了?打开浏览器F12 -> Application -> Local Storage,看有没有token字段。
  3. 请求头是否正确?Axios拦截器是不是把token拼到了Authorization头里。token没拼上,后端拦截器统一返回401,页面自然拿不到数据。

这“三连问”我几乎每次远程调试都会用,基本能排除80%的联调问题。

6.4 必看的几个潜在逻辑Bug

光能跑起来还不够,演示的时候如果逻辑出错非常尴尬。这里列几个这套系统里最容易出Bug的地方:

  • 创建行程时剩余座位没初始化:totalSeats赋值了但remainSeats为null,匹配查询里gt(0)查不出来,行程列表空一片。解决方案是在实体类里加@TableField(fill = FieldFill.INSERT)配合MetaObjectHandler自动填充,或者简单点在前端表单做必填校验。
  • 取消订单时没恢复座位:订单取消逻辑里只把订单状态改成了CANCELED,忘了把行程的remainSeats加回去。结果就是没人抢的位置越来越少,乘客投诉“明明有空座却不让拼”。这个逻辑务必写在同一个事务里。
  • 金额计算丢精度:单价用BigDecimal没问题,但如果你用了double当单价,再乘以人数,可能出现0.68元这种奇数。我建议所有金额字段在数据库就是DECIMAL,Java里就是BigDecimal,运算都走multiply,不要在代码里混转。
  • 越权操作:乘客A通过手动改URL里的订单id,直接操作乘客B的订单,取消、评价都能触发。这属于严重安全问题。解决办法是在订单Service层增加校验order.getPassengerId().equals(currentUserId)或判断当前用户是否为该订单司机的逻辑。我为这个点单独写了一个OrderAccessGuard,每次操作前统一校验,代码长了点,但安全上踏实。

6.5 答辩时的高频问题与回答思路

最后给你备一份答辩高频问题清单,这套系统的回答要点我顺手写出来,你有空可以对着练:

高频问题回答思路
系统架构是怎样的?前端Vue、后端Spring Boot、MySQL数据存储的标准前后端分离,配合Redis可选做会话热点缓存,数据交互走RESTful API
拼车匹配算法怎么实现?距离、时间窗、目的地相似度三者加权打分,讲解清楚每个权重的理由
超卖问题怎么解决?乐观锁SQL + 悲观锁兜底,结合订单状态机说明事务保证一致性
项目里最有挑战的点是什么?拼车匹配和订单状态机,以及座位扣减的并发,围绕这三处展开
以后如果推广到真实生产,还缺什么?消息队列削峰、Redis缓存行程热数据、分布式事务、地图导航API对接,提方向即可

7. 从毕设到项目的最后一公里:如何把源码讲出自己的故事

源码拿到手上,最重要的一件事不是急着跑通,而是把它“讲成自己的故事”。我每次给同学远程调试,都会说这句话:毕设不是把你的名字贴在别人代码上,而是把每一段关键代码读懂、能改、能扩展。

建议你拿到这套系统之后,按下面三步走:

  • 第一遍“跑”:不看源码,按文档部署,跑通主流程,记录下所有踩坑点。这些踩坑过程就是论文里的“系统测试与问题排查”素材。
  • 第二遍“读”:从Controller顺着Service到Mapper读一遍,把每个接口的请求链路画出来(不用画太细,手写脑图都行),搞懂订单状态每次是怎么变的。
  • 第三遍“改”:选一个点自己动手扩展。比如给拼车匹配加一个“价格区间过滤”,或者给订单加一个“乘客与司机互相评分”功能。不用写太多,一个功能就够让你在答辩时说“我在原项目基础上额外实现了xxx”。

这三步走完,你再面对答辩老师,整个人的状态完全不一样——因为你不再是被动复述,而是能主动聊设计、聊取舍。如果你打算拿它做求职项目,再花两三天把系统截图、核心接口文档、数据库设计整理成一份作品集,面试前把匹配算法和超卖处理这两段讲熟练,基本就能压住场子。

这套出租车拼车系统的价值,我看着是越挖越多的:技术上它踩得实,业务上它离真实场景近,代码上它又留有充分的扩展余地。希望这篇拆解能帮你少走弯路,无论是顺利毕业还是拿到心仪的offer,都有实实在在的底气。

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

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

立即咨询