SpringBoot+Vue电动车租赁系统:从毕设到完整项目实战指南 简介一份面向计算机专业本科生及前后端开发学习者的毕业设计论文资源围绕电动车租赁管理系统展开完整呈现从选题背景、需求分析到系统设计、数据库设计与实现测试的全过程。系统基于SpringBoot和Vue构建后端采用Spring、Spring MVC、Java与MySQL涵盖车辆信息管理、租赁管理、还车信息管理、评价管理等核心模块能够支撑城市电动车租赁站点的日常运营优化租赁流程并提升服务效率。压缩包仅含1个docx文档大小2.35MB属于单文件论文资料这类格式便于直接阅读、批注与二次修改。目前已有53人学习浏览。通过阅读这篇论文读者可以系统掌握前后端分离应用的模块划分与数据库设计思路了解如何将用户需求转化为可落地的系统功能对完成同类课程设计或毕业设计具有参考价值。1. 为什么“做烂了”的毕设题我劝你还是选它电动车租赁管理系统配上SpringBoot和Vue每年毕业季能冒出上千份雷同题目。你可能觉得它毫无新意但换个角度看它覆盖了一个完整业务系统需要的全部核心环节——用户认证、订单状态机、计费规则、并发控制、前后端数据交互。这些恰恰是面试时最常被追问的东西。本文要讲的不是“怎么应付答辩”而是怎么把这个题目做成一份能摆在简历上、敢打开给面试官看的完整作品。你将从技术选型、表结构设计、核心接口实现、前端对接一路读到并发抢单和计费精度这些真正让人翻车的细节。适合正在做毕设、或者想用这个题目练手并准备求职项目的开发者。2. 选型与架构设计为什么SpringBoot Vue是这套系统的最优解2.1 前后端分离的架构到底解决了什么这个选题最常见的实现方式就是前后端分离SpringBoot提供RESTful APIVue负责页面渲染和交互。学生时代很多项目还在用Thymeleaf模板引擎把页面直接塞进后端开发起来确实快但做到后面你会发现模板渲染让前后端逻辑纠缠在一起单车列表、订单详情、个人中心这些页面每改一次交互后端模板也要跟着动。分离架构的核心价值在于后端只输出JSON前端只关心渲染。电动车租赁系统的业务场景很适合这种模式——用户在小程序端或网页端选车、下单管理端在后台维护车辆、查看订单两端的交互风格差别很大但调用的接口可以完全复用。后端把“计算租赁时长、结算金额、扣减余额”这些稳定逻辑收敛成接口前端无论换成什么客户端对接成本都很低。我在实际开发中一般会用这样的分层结构Controller层只做参数接收和简单校验Service层承载订单状态流转和计费逻辑Mapper层用MyBatis-Plus做数据访问不再手写大量SQL。车辆、订单、用户三张核心表的操作频率最高事务边界要放在Service层不能散落到Controller里。2.2 核心技术栈的选型理由框架选型上SpringBoot选2.x版本配合MyBatis-Plus操作数据库避免写繁琐的JDBC模板代码。认证方案用JWT而不是Session理由是前后端分离后Session的跨域处理很别扭而JWT无状态、后端不用存会话记录移动端也能直接复用同一套token。密码存储必须用BCrypt加密明文密码是答辩时最容易被问倒的问题。数据库选MySQLRedis用于处理并发抢单场景。租赁系统的计费逻辑其实不复杂难的是同一辆车被两个人同时下单这种边界。用Redis的分布式锁或者数据库乐观锁都能解决后面会专门讲。前端用Vue 2 Element UIVue 3 Element Plus当然也可以但Element UI的成熟组件和中文文档对新手更友好网上能查到的踩坑案例也更多。前端工程结构按标准脚手架来Vue Router管理页面路由Vuex管理用户登录状态和全局信息Axios统一封装请求拦截器在请求头里自动附带token。页面访问权限用路由守卫做未登录跳转到登录页管理员角色才能进入后台管理的路由。2.3 项目目录结构与核心模块划分后端按功能模块分包每个模块内包含Controller、Service、Mapper三层。核心模块有六个用户模块处理注册登录和余额管理车辆模块处理车辆信息维护、状态查询订单模块负责下单、还车、结算的完整生命周期计费模块定时计算骑行费用管理模块提供统计报表接口充值模块对接支付逻辑毕设阶段一般用模拟支付。前端页面按角色拆分用户端包含登录注册、车辆列表、租车下单、个人中心、我的订单、钱包充值管理端包含数据看板、车辆管理、订单管理、用户管理。这种模块划分直接对应数据库的表结构设计后续开发时基本不会出现“这里应该放哪个包”的犹豫。3. 数据库设计五张核心表与三个必须注意的字段3.1 核心表结构用户、车辆、订单、计费规则、充值记录数据库是这个项目的基石。表设计的好坏直接决定后面写代码时是顺手还是别扭。我按实际开发经验给出一个比较标准的五表方案用户表sys_user存基础信息和余额余额字段用decimal(10,2)禁止用float。车辆表vehicle记录车辆的品牌、型号、车牌号、当前电量、状态、每小时租金和当前坐标。订单表rental_order是整个系统的核心存储订单号、用户ID、车辆ID、开始时间、结束时间、租用时长、订单金额、订单状态和支付状态。计费规则表charge_rule维护按小时计费的单价和最小计费单位。充值记录表recharge_record保存用户的每一笔充值流水。订单表里order_no必须有唯一索引这既是业务需要也是答辩时能讲清楚的亮点。生成订单号的方式可以用日期 随机四位 用户ID后四位保证并发下也不重复。租赁订单的状态从下单选车开始经过骑行中最后到已还车已结算中间还有已取消状态。状态字段用整型数字前端用字典翻译成中文展示不要直接在数据库里存“骑行中”这种字符串。3.2 计费规则与订单状态流转的设计要点计费规则是这个系统最需要想清楚的地方。常见的方案是按小时计费不满一小时按一小时算。为什么这么设计因为电动车租赁的时长跨度从几十分钟到几天都有按小时计费规则简单、代码好实现也方便用户理解。整点向上取整是必须做的一步否则“租了61分钟只收一小时的钱”会让运营亏到怀疑人生。计费计算我用一条SQL说清楚思路-- 计算订单的实际租用分钟数 SELECT TIMESTAMPDIFF(MINUTE, start_time, end_time) AS total_minutes FROM rental_order WHERE order_no ORD20240615001;拿到分钟数后在后端做向上取整分钟数除以60有余数就加1。这个逻辑必须写在Service层不能依赖数据库计算因为计费规则是可配置的以后如果要改成“每天封顶30元”写死在SQL里会很难扩展。订单状态流转是答辩时的高频考点。我的设计是这样用户可以取消状态为“待取车”的订单管理员把车标记为“已出库”后订单进入“骑行中”用户还车时系统计算费用、扣减余额、把订单置为“已还车”。车辆状态和订单状态要联动下单成功时车辆状态从“空闲”变“骑行中”结算完成后变回“空闲”。这里最容易出bug的就是状态没有放在同一个事务里更新导致车辆显示空闲但订单还在骑行中。3.3 索引与数据一致性的取舍查询频率最高的场景是用户查看车辆列表和订单记录。车辆列表按状态筛选订单列表按用户ID和时间排序所以vehicle表的status字段加普通索引rental_order表的user_id和start_time建联合索引。其余字段不随意加索引索引过多会让插入和更新变慢。余额扣减必须放在事务里。典型场景是用户还车时系统要同时执行“更新订单状态”“扣减用户余额”“恢复车辆状态”三个操作任何一个失败都会造成数据不一致。我在Service方法上直接加Transactional注解并用propagation Propagation.REQUIRED指定传播行为。这里有个细节事务内不能捕获异常后吞掉否则Spring感知不到异常就不会回滚这也是新手容易犯的错。4. 后端SpringBoot实现从登录鉴权到订单结算的完整链路4.1 项目初始化与基础配置后端项目建议直接使用Spring Initializr生成依赖勾选Spring Web、MySQL Driver、Lombok。创建完成后手动引入MyBatis-Plus和JWT相关依赖。配置文件里最值得注意的是数据库连接参数和MyBatis-Plus的逻辑删除配置spring: datasource: url: jdbc:mysql://localhost:3306/ebike_rental?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl逻辑删除是毕设里容易被忽略的加分项。用户注销、车辆下架这类操作物理删除会让历史订单查不到关联信息用逻辑删除字段deleted就能保留完整数据链。注意逻辑删除字段建议在每张表都加上MyBatis-Plus配置好后所有的delete操作会自动变成update。4.2 登录接口与JWT鉴权实现用户登录是第一个要写的接口。流程是前端把用户名和密码发给后端后端用BCrypt校验密码校验通过后生成JWT返回给前端。JWT里我塞入了userId、userName、role三个关键信息过期时间设置为24小时。代码实现如下PostMapping(/login) public Result login(RequestBody LoginDTO loginDTO) { // 1. 根据用户名查询用户 LambdaQueryWrapperSysUser wrapper Wrappers.lambdaQuery(); wrapper.eq(SysUser::getUsername, loginDTO.getUsername()); SysUser user userMapper.selectOne(wrapper); // 2. 用户不存在直接返回 if (user null) { return Result.error(用户不存在); } // 3. BCrypt校验密码不能直接比对明文 if (!BCrypt.checkpw(loginDTO.getPassword(), user.getPassword())) { return Result.error(密码错误); } // 4. 生成JWT过期时间24小时 String token JwtUtil.createToken(user); return Result.success(token); }参数说明登录接口接收LoginDTO包含username和password两个字段。密码校验用BCrypt.checkpw因为数据库里存的是加密后的密文直接equals比较永远是false。JwtUtil.createToken内部用jjwt库生成字符串密钥统一配置在application.yml里不要写死在代码中。登录之后其他接口通过拦截器校验token。拦截器逻辑很简单放行登录和注册接口其余接口必须从请求头Authorization中取出token并解析解析失败返回401。这里要注意Swagger相关路径要放行否则联调时没法直接在文档里测试接口。4.3 核心业务租车下单与并发控制租车接口是这个系统最核心的接口。用户传来vehicleId后端要做四件事检查车辆状态是否空闲、检查用户余额是否足够支付押金或预估费用、锁定车辆、创建订单。这四步必须保证原子性否则两个人同时租同一辆车就翻车了。我的做法是数据库乐观锁 业务校验双保险。第一步先用乐观锁更新车辆状态PostMapping(/rent) Transactional public Result rent(RequestBody RentDTO rentDTO) { // 1. 尝试锁定车辆status0空闲且未被其他事务修改 int affected vehicleMapper.updateVehicleStatus( rentDTO.getVehicleId(), 1, 0); // 2. 影响行数为0说明车辆已被抢走或被修改 if (affected 0) { return Result.error(车辆已被租用请选择其他车辆); } // 3. 创建订单初始状态为“待取车” RentalOrder order new RentalOrder(); order.setOrderNo(OrderNoGenerator.generate()); order.setUserId(rentDTO.getUserId()); order.setVehicleId(rentDTO.getVehicleId()); order.setStartTime(LocalDateTime.now()); order.setStatus(1); orderMapper.insert(order); return Result.success(下单成功); }参数说明updateVehicleStatus的三个参数分别是车辆ID、目标状态1表示骑行中、条件状态0表示空闲。MyBatis-Plus执行的是UPDATE vehicle SET status 1 WHERE id ? AND status 0这条SQL数据库的行锁会保证只有一个事务能更新成功。这种锁的实现方式比Java的synchronized可靠因为它在应用层面不依赖单机部署以后扩展成集群也不用改代码。下单和还车接口方法上的Transactional至关重要。想象一下车辆状态更新成功了但订单插入失败了如果没有事务车辆会一直卡在“骑行中”状态无法被任何人租用。这个bug在单测里很难发现但上线后一定会出现。4.4 还车结算计费逻辑与余额扣减还车接口是另一个高频翻车点。用户点击还车后端需要计算从startTime到当前时间的总分钟数再换算成小时数向上取整乘以每小时单价得到金额最后从用户余额中扣减。扣费前还要检查余额是否够不够的话订单标记为“欠费”状态用户可以后续充值再结算。PostMapping(/return) Transactional public Result returnVehicle(RequestBody ReturnDTO returnDTO) { // 1. 查询订单订单号是唯一的 RentalOrder order orderMapper.selectByOrderNo(returnDTO.getOrderNo()); if (order null || order.getStatus() ! 1) { return Result.error(订单不存在或状态不正确); } // 2. 计算租赁分钟数使用毫秒级时间戳避免跨天问题 long minutes ChronoUnit.MINUTES.between( order.getStartTime(), LocalDateTime.now()); // 3. 向上取整到小时不满1小时按1小时计费 long hours (minutes 59) / 60; // 4. 查询计费规则当前按每小时5元计算 ChargeRule rule chargeRuleMapper.selectById(1); BigDecimal amount rule.getHourlyPrice() .multiply(BigDecimal.valueOf(hours)); // 5. 扣减余额余额不足则记录欠费状态 SysUser user userMapper.selectById(order.getUserId()); int update userMapper.deductBalance( user.getId(), amount); if (update 0) { order.setStatus(4); // 4欠费待结算 } // 6. 更新订单和车辆状态 order.setEndTime(LocalDateTime.now()); order.setTotalMinutes((int) minutes); order.setTotalAmount(amount); order.setStatus(2); // 2已还车 orderMapper.updateById(order); vehicleMapper.updateVehicleStatus( order.getVehicleId(), 0, 1); // 车辆恢复空闲 return Result.success(还车成功本次费用 amount); }参数说明ChronoUnit.MINUTES.between是Java 8时间API比直接用Date的getTime()相减再除以60000更清晰而且不会因为时区设置产生偏差。金额计算全程使用BigDecimal这里还有个细节(minutes 59) / 60 是向上取整的简写方式不用Math.ceil是因为后者涉及到浮点数容易出现精度问题。车辆恢复空闲的update语句里条件带上status1防止用户对已经还过的车辆重复发起还车请求。5. 前端Vue实现从接口封装到业务页面的完整对接5.1 Axios封装与接口请求结构前端和后端的对接规范性直接影响开发效率。Axios封装的核心思路创建一个request.js文件统一配置baseURL、请求超时时间、请求拦截器和响应拦截器。请求拦截器从Vuex里读取token并添加到请求头的Authorization字段响应拦截器检查HTTP状态码和业务状态码遇到401自动跳转登录页。// src/utils/request.js import axios from axios import store from /store import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动附带token request.interceptors.request.use(config { const token store.state.user.token if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误码 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { return Promise.reject(error) } ) export default requestbaseURL配置成/api而不是完整的后端地址这是为了配合Vue CLI的代理配置。开发环境下后端端口是8080前端是8081跨域是必然的。如果用完整地址每个请求都要配CORS很麻烦。在vue.config.js里配置代理让前端服务器把/api开头的请求转发到后端生产环境再用Nginx做同样的反向代理代码不用改一行。5.2 核心页面车辆列表与租车下单车辆列表页是全站最核心的页面。用户进入页面后要能看到车辆的照片、电量、每小时租金和距离自己的位置。接口返回的数据结构是数组每个元素包含vehicleId、brand、model、battery、pricePerHour、status等字段。前端用Element UI的卡片组件渲染状态字段做字典映射。template div classvehicle-list el-row :gutter20 el-col v-forvehicle in vehicleList :keyvehicle.id :span8 el-card div classvehicle-info h3{{ vehicle.brand }} {{ vehicle.model }}/h3 p电量{{ vehicle.battery }}%/p p租金{{ vehicle.pricePerHour }}元/小时/p el-tag :typevehicle.status 0 ? success : danger {{ vehicle.status 0 ? 空闲 : 已租出 }} /el-tag /div el-button typeprimary :disabledvehicle.status ! 0 clickrentVehicle(vehicle.id) 立即租用 /el-button /el-card /el-col /el-row /div /template车辆列表的数据加载在mounted生命周期里调用fetchVehicleList方法async fetchVehicleList() { const res await request.get(/vehicle/list) if (res.code 200) { this.vehicleList res.data } }这里有一个容易踩的坑后端返回的车辆状态字段是数字0和1前端页面直接展示成“0”和“1”会让用户一脸问号。所以模板里用三元表达式映射成中文。如果状态枚举值很多建议前端建一个常量字典文件统一管理代码会整洁很多。租车下单的交互是用户点击“立即租用”后弹出确认框显示车辆信息和预估计费规则用户确认后调用/order/rent接口。下单成功后跳转到“我的订单”页面车辆从列表里消失或变为“已租出”状态。5.3 我的订单页与还车操作订单页展示当前用户的所有租赁记录按时间倒序排列。每笔订单显示订单号、车辆信息、开始时间、租用状态和金额。骑行中的订单要显示“还车”按钮已结算的订单显示费用明细。订单列表用el-table渲染状态列用tag组件el-table :dataorderList el-table-column proporderNo label订单号 width180/el-table-column el-table-column propstartTime label开始时间/el-table-column el-table-column label状态 template slot-scopescope el-tag :typestatusMap[scope.row.status].type {{ statusMap[scope.row.status].label }} /el-tag /template /el-table-column el-table-column label操作 template slot-scopescope el-button v-ifscope.row.status 1 clickreturnVehicle(scope.row) 归还车辆/el-button /template /el-table-column /el-table还车操作要处理确认逻辑防止用户误触。弹出确认框后调用接口接口返回费用金额后前端再弹一次成功消息并刷新订单列表。这里我给每个订单还车按钮加了loading状态防止用户重复点击导致重复请求。后端接口已经用订单号和状态做了幂等控制但前端配合好体验会更顺滑。6. 避坑指南电动车租赁系统最常见的六个翻车现场6.1 订单金额出现19.999999这种诡异数字现象订单结算后金额显示为19.999999元或类似的浮点误差数值数据库里存的值也有多位小数。原因金额字段用了double或float类型计费时用double做乘法。Java里0.1 * 3的结果是0.30000000000000004这是浮点数二进制存储的固有问题不是代码写错了。解决数据库金额字段一律用decimal(10,2)Java代码里全程用BigDecimal禁止用double计算金额。前端展示时调用Number.parseFloat(amount).toFixed(2)再做一次格式化双保险。后端接口返回前也会把BigDecimal转换为字符串。6.2 同一辆车被两个用户同时租走现象两个用户同时点击“租用”同一辆车后端两个请求都返回成功车辆被创建了两条有效订单。原因先查状态再更新的代码在并发下会失效。查询时车辆是空闲的两个请求都通过了检查然后都执行更新最终车辆状态还是“骑行中”但产生了两个订单。解决用前面讲到的乐观锁方案更新车辆状态的SQL必须带上AND status 0条件。影响行数为0直接返回“车辆已被租用”。如果后续车辆数量很多、并发量上来可以在更新语句前用Redis分布式锁再次校验双保险更稳妥。6.3 还车时间跨天计费直接算错现象用户在23:50租车00:20还车账单显示的费用比实际少了一倍或者直接报负数。原因时间计算用了Date.getDate()取天数再相减跨天时getDate从31变成1相减得到负数再取绝对值结果完全不对。解决全项目统一用LocalDateTime和ChronoUnit计算时间差。还车接口用ChronoUnit.MINUTES.between(startTime, endTime)得到分钟数永远不涉及“天”的位数变化。另外要留意服务器时区配置MySQL的url里加上serverTimezoneAsia/Shanghai前端传时间戳不要传字符串。6.4 前端跨域配置失效接口一直401现象前端页面能打开但所有请求都失败控制台报错显示CORS或者代理错误登录接口返回401。原因两种情况最常见。一是vue.config.js的proxy配置了但Axios的baseURL写死了完整后端地址请求根本没走代理二是代理配置的路径和后端接口前缀不匹配。解决Axios的baseURL固定写/apivue.config.js里配置/api: { target: http://localhost:8080, changeOrigin: true }后端接口的context-path也统一为/api。如果改了配置发现没生效把Vue的开发服务器重启devServer的代理改动经常需要重启才能加载。6.5 MyBatis-Plus分页查询失效返回全部数据现象用Page对象查询订单列表传入第1页每页10条结果返回了几百条全量数据且total字段始终是0。原因MyBatis-Plus从3.x开始分页功能需要手动配置分页插件没有配置MybatisPlusInterceptor的情况下Page查询会被当作普通查询执行。解决在配置类里添加分页拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }配置完后重启应用Page查询才会真正执行LIMIT语句。这个坑没有报错提示只能通过日志里打印的SQL判断分页是否生效。6.6 事务内部吞掉异常余额扣了订单没更新现象还车后用户余额被扣了但订单状态还是“骑行中”车辆也仍然是占用状态用户无法再次租车。原因Service方法上的Transactional事务注解要求异常必须抛出到方法边界才能触发回滚。代码里用try-catch捕获异常后只打印日志就返回ResultSpring感知不到异常事务不会回滚。解决事务方法内不要捕抓异常。如果确实需要捕获要手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()标记回滚。更推荐的做法是让异常向上抛在Controller层统一捕获处理。7. 答辩与验收前四个能让系统显得更专业的验证技巧系统做完不等于能验收。我自己当年在演示环节有过一次“现场翻车”的教训后来总结了一套验证流程分享给你。第一用并发脚本模拟多用户抢租同一辆车。打开浏览器控制台直接执行一段并发请求代码同时发送30个租车请求针对同一辆车的ID观察返回结果。正确的表现是只有一个请求返回“下单成功”其余全部返回“车辆已被租用”。如果出现多个成功说明乐观锁没有配置对要赶紧修。第二准备一套完整的演示数据脚本。用户账号、车辆信息、历史订单都要提前准备好。演示最忌讳现场注册账号、临时找车租网络一卡或者验证码没发出来整个答辩节奏就乱了。我会准备一个demo用户账号里预充值200元车辆列表里有空闲车辆还有两三条不同状态的订单数据这样演示时可以顺畅地走完“登录→选车→租用→还车→查看订单”全流程。第三展示订单的边界状态。除了正常流程要能主动说出系统如何处理“余额不足还车”“取消订单”“欠费结算”这些特殊情况。答辩老师最喜欢问的就是异常分支怎么处理。你把订单状态机画在纸上每个状态能转移到哪个状态、触发条件是什么讲清楚这个设计思路比单纯演示一遍正常流程加分很多。第四用Postman或JMeter打印一个接口的响应时间。还车接口包含了订单查询、费用计算、余额扣减、状态更新四个数据库操作事务提交完成后统计耗时。一般MySQL本地环境下这个接口应该在50毫秒以内完成。如果超过200毫秒大概率是SQL没有走索引或者事务范围过大值得排查一下。这四步做完系统的正确性和可靠性都有据可查答辩时面对提问心里也有底。做毕设这件事最大的收获不是那份文档和代码而是你搞清楚了一个真实业务系统从设计到落地的完整链路。当年我被问到“车辆状态和订单状态如何保持一致性”时因为提前理解了事务边界和乐观锁才没有当场卡住。希望这篇笔记里的方案和避坑经验能帮到你关键时候少走几步弯路。本文还有配套的精品资源点击获取