SpringBoot+Vue高校电动车租赁系统设计与部署全解析
1. 项目概述与技术选型拆解1.1 这个项目到底是什么先把这个项目说透。高校电动车租赁系统本质是一个面向校园场景的短途出行服务平台。学生通过手机或电脑端查看附近可用电动车、在线下单租车、到点还车、在线支付费用管理员在后台管理车辆信息、处理订单、查看营收数据。这类系统在高校里非常常见因为校园面积大、宿舍到教学楼距离远电动车是最方便的短途代步工具。单看技术栈这套系统用SpringBoot做后端、Vue做前端、MySQL存数据是当前Java Web领域最经典的一套组合。市面上绝大多数Java方向的毕业设计、课程设计、求职项目都是围绕这套技术栈展开的。换句话说你把这个项目啃下来等于同时打通了后端开发、前端开发、数据库设计这三条线。为什么这套组合能成为事实标准SpringBoot解决了过去Spring项目配置繁琐的问题内嵌Tomcat一个java -jar命令就能启动服务Vue用组件化方式组织页面配合Element UI能快速搭建出像样的后台界面MySQL开源免费在Windows和Linux上都能跑特别适合学生做本地开发。三者配合开发效率非常高。1.2 为什么选这套技术栈而不是其他方案很多同学在选题时会纠结用SSM还是SpringBoot用JSP还是Vue用MySQL还是Oracle。我直接给结论如果你是做毕业设计、课程设计或者想通过一个完整项目入门Java Web开发这套组合是目前性价比最高的选择没有之一。对比一下你就明白了。SSMSpringSpringMVCMyBatis是上一代主流配置繁琐要写一堆XML配置文件开发体验和SpringBoot差了不止一个档次。JSP属于服务端渲染前后端代码耦合在一起页面一复杂就乱成一团。Oracle虽然功能强大但安装包几个G起步对电脑配置要求高学生根本没必要用。Vue则让前端开发变得模块化组件可以复用页面跳转不刷新用户体验完全不是传统模板页面能比的。这套组合还能做到前后端分离。后端只负责提供接口返回JSON数据前端只负责页面渲染和交互通过HTTP请求调用后端接口。开发时可以前端跑在8080端口、后端跑在8081端口互不干扰。分工明确也方便后期扩展——以后想加个小程序端直接复用同一套后端接口就行。从求职面试的角度看SpringBoot、Vue、MySQL、Redis、MyBatis-Plus这些关键词出现频率极高。做完这个项目你对后端如何分层、接口如何设计、数据库如何建模、前后端如何联调会有非常具体的认知面试官问项目经验时你才有话可说。1.3 适合谁来学习和参考如果你是计算机相关专业的本科生正在为毕业设计或课程设计发愁这个项目是一个很好的选择。它的业务规模不大不小前端页面在10~15个左右后端接口在30~40个左右一个人完全能搞定但又能把增删改查、权限控制、订单状态流转、文件上传、数据统计这些核心知识点全部覆盖到。如果你是非科班转行、想通过实战项目学Java这个项目同样有价值。它的业务规则非常明确不像电商那种动辄几十张表的系统让人无从下手。你跟着本文的设计思路把表建出来、把接口写出来、把页面搭出来对全栈开发的整个流程就会有一个清晰的认知。如果你是准备求职的应届生想给简历上加一个有分量的项目这个项目也能胜任。不过要在做完的基础上做一些亮点扩展比如引入Redis缓存热点数据、用RabbitMQ处理订单超时关闭这些我在后文会专门讲。提示这个项目的难点不在技术本身而在“体系感”。很多初学者单看SpringBoot能看懂单看Vue也能看懂但一说把两者打通就懵了。本文会重点带你走一遍前后端联调的全过程包括跨域处理、接口对接、联调调试这些书上不细讲、但实际开发绕不开的环节。2. 数据库设计与核心功能模块拆解2.1 数据表结构设计做管理系统第一件事永远是设计数据库。表结构设计得好后面写代码就顺设计得烂后期全是坑。这一节根据我多年做这类项目的经验给你一套可直接用的核心表结构覆盖电动车租赁业务的所有关键实体。先梳理业务角色。这个系统里有三类人学生用户、管理员、运营人员。围绕这三类角色核心业务链条是用户注册登录、浏览车辆、下单租车、用车结束还车、在线支付、管理员审核管理。基于这个链条数据库核心表设计如下user用户表存储学生基本信息字段包括id、username、password、real_name、student_no、phone、balance账户余额、avatar、role、status、create_time。ebike电动车表存储车辆信息字段包括id、bike_no车辆编号、name、image、type、price_per_hour每小时的租金、deposit押金、status、description、create_time。rental_order租赁订单表核心业务表字段包括id、order_no订单编号、user_id、bike_id、start_time、end_time、total_price、deposit_amount、status、create_time。recharge_record充值记录表记录用户的余额充值流水字段包括id、user_id、amount、pay_type、create_time。violation_record违章记录表可选扩展记录用户骑行过程中的违规行为。repair_record报修记录表记录车辆故障和学生上报的维修请求。admin管理员表与用户表分离避免把管理员和普通用户混在一起字段包括id、username、password、real_name、role、create_time。comment评论反馈表记录用户对车辆和服务的评价。这里有几个设计要点是踩过坑才懂的。第一个要点订单表不要只存金额还要把车辆单价、时长、押金这些冗余字段一起存进去。为什么因为车辆价格可能调整如果订单里不存快照等一个月后做对账金额就算不清楚了。这是电商领域的前台价格快照思想在租赁场景同样适用。第二个要点所有涉及金额的字段用DECIMAL(10,2)类型绝不用float或double。浮点数是二进制存储0.1在二进制里是无限循环小数算到后面会出现精度丢失账目对不上。这个坑在支付相关的功能里很容易踩而且一旦上线再改只能重建表。第三个要点用户表和订单表是大数据量的核心位id建议用bigint自增主键订单号用时间戳随机数生成避免并发时重复。不能用用户手机号做订单号因为手机号会变而且长度不适合做索引。2.2 核心功能模块划分按系统角色划分功能模块整个项目可以拆成两大端、七个功能模块。用户端的功能流程是注册登录、首页浏览电动车列表、查看车辆详情、在线租车选择租用时长、提交订单、个人中心充值余额、查看我的订单、报修车辆、提交评论。管理端的功能流程是登录管理员独立登录入口、车辆管理新增车辆、编辑信息、上下架、查看车辆当前状态、订单管理查看全部订单、按状态筛选、处理超时订单、用户管理查看注册用户、禁用异常账号、报修管理处理学生的维修请求、标记维修完成、数据统计查看总营收、订单量、热门车型排行。可能有人会问用户端和管理端是两套前端页面还是共用一个我建议是拆成两个独立页面入口用户端面向普通学生、走前台页面风格管理端面向管理员、走后台表格风格这样职责清晰代码也好维护。前端项目可以采用同一个工程、多路由模块的方式组织也能拆成两个独立工程。状态设计是这类业务系统的灵魂。电动车状态我建议定义成四个1-可用、2-已租出、3-维修中、4-已下架。订单状态定义五个0-待支付、1-进行中已支付待还车、2-已完成、3-已取消、4-超时关闭。这里要强调一个细节订单不要做物理删除用status字段做逻辑删除标记比如-1代表已删除。原因是订单是业务流水删了之后对账审计就没了依据。2.3 计费规则与状态流转计费是租赁系统的业务核心这里要讲清楚计算逻辑。我的设计是按小时计费用户下单时选择租用时长前端计算展示预估费用后端在提交订单时再次计算总价。计算公式是总费用 小时数 x 每小时单价 押金金额。押金在还车时退还到用户余额如果超时还车超出的部分按小时单价再扣费。具体计算逻辑放在Service层核心方法大概是这样的// 计算订单总额 public BigDecimal calculateOrderAmount(OrderCreateDTO dto) { Ebike bike ebikeMapper.selectById(dto.getBikeId()); if (bike null || bike.getStatus() ! 1) { throw new BizException(车辆不存在或不可用); } BikeTypeConfig config typeConfigMapper.selectById(bike.getTypeId()); // 租用时长小时进一法取整 int hours dto.getRentHours(); BigDecimal rentAmount config.getPricePerHour().multiply(new BigDecimal(hours)); BigDecimal deposit config.getDeposit(); return rentAmount.add(deposit); }状态流转是这个系统最容易被忽略、但面试又最爱问的点。租车流程的状态推进是下单时生成待支付订单用户模拟支付或对接微信/支付宝支付回调后订单变为进行中用户在管理端或客户端点击还车系统计算费用并退回押金订单变为已完成。超时未支付的订单由定时任务每分钟扫描一次超过15分钟未支付自动关闭这样可以释放车辆资源。车辆状态和订单状态是联动的。用户下单成功支付后车辆状态由可用变为已租出还车成功后车辆状态由已租出变回可用。如果用户报修车辆状态变为维修中管理员处理完成后恢复可用。这里要注意并发问题如果两个用户同时看到同一辆车可用来下单如何避免重复租出最简单的方案是在更新车辆状态的SQL里加上条件判断比如UPDATE ebike SET status2 WHERE id? AND status1通过数据库行锁天然保证只有一个请求能更新成功。3. 后端SpringBoot核心实现与关键代码3.1 工程结构搭建使用SpringBoot开发建议遵循标准的Maven多级目录结构。前端工程和后端工程分开后端代码按controller、service、mapper、entity、dto、vo、config、common、util九个包组织。这里我用一个实际跑通的工程结构来展示你可以直接当模板用ebike-backend ├── pom.xml ├── src/main/java/com/example/ebike │ ├── EbikeApplication.java # 启动类 │ ├── common # 通用类 │ │ ├── Result.java # 统一返回结果 │ │ ├── PageResult.java # 分页返回结果 │ │ └── BizException.java # 自定义业务异常 │ ├── config # 配置类 │ │ ├── MybatisPlusConfig.java # MyBatis-Plus 配置 │ │ ├── WebMvcConfig.java # 拦截器/跨域配置 │ │ └── Knife4jConfig.java # 接口文档配置 │ ├── controller # 控制层 │ │ ├── AuthController.java │ │ ├── EbikeController.java │ │ ├── OrderController.java │ │ └── RechargeController.java │ ├── entity # 实体类 │ │ ├── User.java │ │ ├── Ebike.java │ │ └── RentalOrder.java │ ├── mapper # 数据访问层 │ │ ├── UserMapper.java │ │ ├── EbikeMapper.java │ │ └── RentalOrderMapper.java │ ├── service # 业务逻辑层 │ │ ├── UserService.java │ │ ├── EbikeService.java │ │ ├── OrderService.java │ │ └── impl │ │ ├── UserServiceImpl.java │ │ ├── EbikeServiceImpl.java │ │ └── OrderServiceImpl.java │ ├── dto # 前端入参对象 │ │ ├── LoginDTO.java │ │ └── OrderCreateDTO.java │ └── vo # 出参对象 │ ├── UserVO.java │ └── OrderVO.javaSpringBoot版本建议选2.7.x系列不要一上来就用3.0以上版本。原因很实际3.x版本基于JDK 17要求Java版本较高并且很多第三方中间件的兼容性还在磨合期学生电脑上通常装的是JDK 82.7.x JDK 8是最稳妥的搭配。MyBatis-Plus用3.5.x版本它对单表CRUD做了极大简化内置了分页插件配合SpringBoot用起来非常高效。3.2 统一返回结果与全局异常处理接口设计的规范程度直接决定前后端联调是否顺畅。我强烈建议从一开始就定义一个统一的返回结果类所有接口的响应结构完全一致。定义如下public class ResultT { private Integer code; // 状态码200-成功500-业务异常401-未登录 private String message; // 提示信息 private T data; // 返回数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }有同学可能会想每个接口都手动构造Result太麻烦。是的所以要用全局异常处理器兜底。定义一个类加上RestControllerAdvice注解里面捕获业务异常、参数校验异常、未知异常返回统一的错误格式。这样Service中只需要在业务出错时抛出BizException(库存不足)前端就能拿到结构完整的错误信息不需要每个Controller单独写try-catch。JWT做登录认证是当前的主流方案。用户登录成功后后端生成一个JWT token返回给前端前端把token存到localStorage或sessionStorage中后续每一次请求在请求头加上Authorization字段。后端通过自定义拦截器解析token、识别当前登录用户注入到请求上下文里。Redis这一节先不用加做毕设够用如果想加亮点把token存到Redis里做单点登录这个加分项我在后面单独说。3.3 核心业务接口的完整实现后端接口按业务模块归纳核心接口清单如下模块接口路径功能说明访问角色认证POST /api/auth/login用户登录公开认证POST /api/auth/register用户注册公开车辆GET /api/ebike/list分页查询可租车辆已登录车辆GET /api/ebike/detail/{id}车辆详情已登录车辆POST /api/ebike/add新增车辆管理员车辆PUT /api/ebike/update修改车辆管理员车辆PUT /api/ebike/status/{id}上下架/维修管理员订单POST /api/order/create创建订单已登录订单POST /api/order/pay/{id}模拟支付已登录订单POST /api/order/return/{id}还车已登录订单GET /api/order/my我的订单列表已登录订单GET /api/order/admin/list管理端订单列表管理员订单POST /api/order/cancel/{id}取消订单已登录充值POST /api/recharge余额充值已登录用户GET /api/user/info用户信息已登录统计GET /api/statistics/overview首页数据面板管理员订单模块是业务重头我把核心的下单-支付-还车链路完整代码贴出来代码经过精简但仍然可直接复用。首先是创建订单Service RequiredArgsConstructor public class OrderServiceImpl implements OrderService { private final EbikeMapper ebikeMapper; private final RentalOrderMapper rentalOrderMapper; private final UserMapper userMapper; Override Transactional(rollbackFor Exception.class) public RentalOrder createOrder(OrderCreateDTO dto, Long userId) { // 1. 校验车辆存在且可用 Ebike bike ebikeMapper.selectById(dto.getBikeId()); if (bike null || bike.getStatus() ! 1) { throw new BizException(车辆不存在或已被租用); } // 2. 用乐观更新锁抢占车辆防止并发重复租用 int updated ebikeMapper.updateStatusWithCondition( bike.getId(), 2, 1); if (updated 0) { throw new BizException(手慢了车辆刚刚被人租走); } // 3. 生成订单并计算金额 RentalOrder order new RentalOrder(); order.setOrderNo(OrderNoGenerator.generate()); order.setUserId(userId); order.setBikeId(bike.getId()); order.setBikeNo(bike.getBikeNo()); order.setStartTime(LocalDateTime.now()); order.setRentHours(dto.getRentHours()); order.setPricePerHour(bike.getPricePerHour()); order.setRentAmount(bike.getPricePerHour() .multiply(BigDecimal.valueOf(dto.getRentHours()))); order.setDepositAmount(bike.getDeposit()); order.setTotalAmount(order.getRentAmount().add(order.getDepositAmount())); order.setStatus(0); // 待支付 rentalOrderMapper.insert(order); return order; } }这里最考验功底的是第2步。为什么不先查询再判断再更新因为先查再改存在时间窗口两个请求同时查到status1同时执行update就可能都认为成功了。用UPDATE ebike SET status2 WHERE id? AND status1这样的条件更新数据库的行锁机制会保证同一时间最多只有一个请求能把状态从1改成2这就是通过数据库保证并发安全代码几乎不用加锁。然后是支付和还车。我做了一个模拟支付接口生产环境对接微信/支付宝的流程也是一样的前端调起支付回调后端接口后端收到支付成功的异步通知后再更新订单状态。模拟支付逻辑如下Override Transactional(rollbackFor Exception.class) public void payOrder(Long orderId, Long userId) { RentalOrder order rentalOrderMapper.selectById(orderId); if (order null || !order.getUserId().equals(userId)) { throw new BizException(订单不存在); } if (order.getStatus() ! 0) { throw new BizException(订单状态异常); } // 扣减用户余额余额不足则抛出异常回滚 User user userMapper.selectById(userId); if (user.getBalance().compareTo(order.getTotalAmount()) 0) { throw new BizException(余额不足请先充值); } userMapper.deductBalance(userId, order.getTotalAmount()); // 更新订单状态为进行中 order.setStatus(1); order.setPayTime(LocalDateTime.now()); rentalOrderMapper.updateById(order); }还车逻辑要计算是否存在超时费用还车时间超过预计结束时间时超时部分按单价计费并从押金中扣除剩余押金退回余额。整个逻辑用事务包起来要么全部成功要么全部回滚不可能出现还车了但钱没退的中间状态。3.4 后端开发中的重要避坑说明用MyBatis-Plus时有一个很隐蔽的坑实体类的id字段如果叫id、用的是数据库自增主键那么insert后MyBatis-Plus会自动把生成的主键回填到实体的id属性上这是它默认的行为很多新人不知道。但如果你的id策略配置成了INPUT时没有在插入前手动设置id数据库自增会失效插入报错。建议在实体类的id字段上写清楚TableId(type IdType.AUTO)。金额计算必须用BigDecimal这在前面强调过。如果用了double两个0.1相加结果会是0.20000000000000004这样的诡异数字前端展示总分就会很奇怪。用BigDecimal的时候有一点要注意构造时用new BigDecimal(19.9)用字符串构造千万不要用new BigDecimal(19.9)后者同样会产生精度误差。逻辑删除功能也要配置一下。MyBatis-Plus支持逻辑删除在application.yml里配置logic-delete-field: deleted实体类上加TableLogic注解。这样执行deleteById时操作的其实是UPDATE把deleted置为1但查询时又自动过滤掉已删除数据既安全又能做数据恢复面试时可以主动提。4. 前端Vue核心实现与前后端联调4.1 前端工程搭建与路由设计前端使用Vue 2 Element UI Axios Vue Router这是当前生态最稳定、资料最丰富的组合。如果用Vue 3也可以Vue 3 Element Plus的组合也很成熟但Vue 2的学习资料多、踩坑记录多遇到问题更容易找到答案毕设阶段求稳优先。前端工程结构建议这样组织ebike-frontend ├── package.json ├── vue.config.js # 开发代理配置 ├── public/ │ └── index.html └── src/ ├── main.js # 入口文件 ├── App.vue # 根组件 ├── utils/ │ └── request.js # Axios 封装 ├── router/ │ └── index.js # 路由配置文件 ├── store/ # Vuex 状态管理 │ └── modules/user.js ├── views/ # 页面组件 │ ├── user/ # 用户端页面 │ │ ├── Login.vue │ │ ├── Register.vue │ │ ├── BikeList.vue │ │ ├── BikeDetail.vue │ │ ├── CreateOrder.vue │ │ └── MyOrders.vue │ └── admin/ # 管理端页面 │ ├── AdminLogin.vue │ ├── Dashboard.vue │ ├── BikeManage.vue │ ├── OrderManage.vue │ ├── UserManage.vue │ └── Statistics.vue └── components/ # 公共组件 ├── BikeCard.vue └── StatusTag.vue路由设计要考虑权限控制。我的设计思路是前端路由分为公开路由和需要登录的路由两块所有页面放在路由配置中路由守卫统一检查。用户未登录访问需要权限的页面时跳转到登录页并记录原本想去的路由地址登录成功后自动跳回这个细节很提升体验。// router/index.js 核心配置 const router new VueRouter({ mode: history, routes: [ { path: /login, component: Login, meta: { title: 登录 } }, { path: /register, component: Register, meta: { title: 注册 } }, { path: /, component: UserLayout, children: [ { path: , component: BikeList, meta: { title: 车辆列表, requiresAuth: true } }, { path: detail/:id, component: BikeDetail, meta: { title: 车辆详情, requiresAuth: true } }, { path: order/create/:bikeId, component: CreateOrder, meta: { title: 下单, requiresAuth: true } }, { path: my-orders, component: MyOrders, meta: { title: 我的订单, requiresAuth: true } } ] }, { path: /admin, component: AdminLayout, meta: { requiresAuth: true, requiresAdmin: true }, children: [ { path: , component: Dashboard, meta: { title: 数据概览 } }, { path: bikes, component: BikeManage, meta: { title: 车辆管理 } }, { path: orders, component: OrderManage, meta: { title: 订单管理 } } ] } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.matched.some(record record.meta.requiresAuth) !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })4.2 Axios请求封装与统一拦截前后端能顺畅联调请求封装是基础。我封装request.js工具文件时核心做了三件事创建axios实例、设置baseURL和超时时间在请求拦截器中从localStorage取token放进请求头在响应拦截器中统一处理错误状态码。这段代码前后端联调不可或缺直接放出来import axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器附加token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }, error Promise.reject(error)) // 响应拦截器统一处理错误 request.interceptors.response.use(response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(未登录或登录已过期)) } if (res.code ! 200) { Message.error(res.message || 请求错误) return Promise.reject(new Error(res.message)) } return res }, error { Message.error(error.message) return Promise.reject(error) }) export default request开发者模式下最大的坑就是跨域。前端跑在localhost:8080后端跑在localhost:8081浏览器出于同源策略会阻止跨域请求。解决方式有两种后端加CORS配置允许跨域或者前端用webpack代理转发请求。实际项目中我推荐前端配置代理这样前端写的请求地址不用带完整域名后端也不需要改代码// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, // 后端服务地址 changeOrigin: true } } } }配置好代理后前端的axios请求写/api/user/info就能自动转发到后端的http://localhost:8081/api/user/info前后端联调的跨域问题随之解决。4.3 典型页面实现示例拿车辆列表页举例看一下Vue页面是怎么对接后端数据的。这里是核心逻辑部分用Element UI的卡片组件展示车辆信息Axios请求后端接口获取数据template div classbike-list el-row :gutter20 el-col :span8 v-forbike in bikeList :keybike.id el-card classbike-card img :srcbike.image classbike-img / div classbike-info h3{{ bike.name }}/h3 p classprice¥{{ bike.pricePerHour }}/小时/p p押金¥{{ bike.deposit }}/p el-button typeprimary :disabledbike.status ! 1 clickgoRent(bike.id) {{ bike.status 1 ? 立即租用 : 不可租用 }} /el-button /div /el-card /el-col /el-row /div /template script import request from /utils/request export default { data() { return { bikeList: [] } }, created() { this.fetchBikes() }, methods: { async fetchBikes() { const res await request.get(/ebike/list, { params: { current: 1, size: 12 } }) this.bikeList res.data.records }, goRent(id) { this.$router.push(/order/create/${id}) } } } /script下单页面要做租时选择组件、费用实时计算、余额展示和提交按钮。这个页面能体现前端的交互基本功。租时我用el-input-number组件通过change事件实时计算展示预估费用提交订单时把所选时长传到后端。还车前端的确认弹窗要清晰显示费用明细。4.4 联调阶段最实用的断点调试法前后端联调是最耗时的环节我分享几个实测好用的方法。第一时间戳对齐法。后端日志开启SQL打印mybatis-plus的configuration.log-impl配置成StdOutImpl前端在axios请求拦截器里打印请求地址和参数。当某接口数据不对时先对时间戳找到这条请求的后端日志看出参返回了什么。这是排查接口返回结构不对的最快方式。第二状态码记忆法。200是成功401是未登录或token失效500是后端抛异常。前端拦截器会根据状态码做差异化处理如果页面一直报错但后端日志又没有打出来八成是请求根本没到达后端问题出在代理配置或地址写错。第三字段命名对照法。前端通常用驼峰命名bikeName数据库字段是下划线bike_name如果后端没开启驼峰映射前端拿到的bikeName就是undefined。SpringBoot里要配置mybatis-plus.configuration.map-underscore-to-camel-casetrueVue取字段时也要注意大小写完全一致。5. 项目部署运行与常见问题排查实录5.1 本地环境搭建与运行步骤整个项目在本地跑通按下面的步骤走很快。先准备环境JDK 8或11、Maven 3.6、MySQL 5.7或8.0、Node.js 14以上、IDEA开发工具可以用社区版功能足够。第一步导入数据库。在MySQL中创建一个名为ebike的数据库字符集选utf8mb4能存emoji表情并且不乱码然后执行项目里的db.sql脚本建表并写入初始数据。建议初始数据里至少造10辆电动车、2个管理员账号、2个普通用户账号方便后续测试。第二步启动后端。用IDEA打开ebike-backend工程Maven会自动下载依赖等待加载完成后在application.yml中确认数据库连接信息无误直接运行EbikeApplication类的main方法。看到Spring Boot的启动日志输出“Started EbikeApplication”说明后端启动成功默认端口8081。第三步启动前端。在ebike-frontend目录下打开终端先执行npm install安装依赖这个步骤比较慢可以配置淘宝镜像加速再执行npm run serve启动开发服务器终端会输出访问地址http://localhost:8080。浏览器打开这个地址注册一个账号登录进去就能看到车辆列表页面。第四步联调验证。在浏览器打开开发者工具切到Network面板操作几个功能观察接口请求是否都能正常返回数据。重点测试登录、车辆列表、下单支付、还车这四条核心链路。5.2 高频报错排查对照表按照我帮学生解决问题的经验以下七类问题是出现频率最高的整理成速查表报错现象根本原因解决方案后端启动报数据库连接失败MySQL没启动或密码不对检查本地MySQL服务核对url中的数据库名和账号密码前端npm install超时npm源在国外网络太慢执行npm config set registry https://registry.npmmirror.com登录报401token未传或过期前端拦截器自动处理确认localStorage里能拿到token车辆列表为空数据库没初始化车辆数据确认db.sql已执行车表有数据提交订单失败车辆状态不对或余额不足查看后端日志具体异常提示CRC校验失败的import语句报错Vite/Webpack缓存问题关闭终端重跑npm run serve或删除node_modules后重装页面样式错乱版本兼容性问题确认Element UI版本和Vue版本匹配Vue2配Element UI 2.x这里有一个特别值得单独说的问题端口冲突。后端启动时报“Port 8081 was already in use”是因为之前启动的后端进程没关干净。Windows上可以在命令行输入netstat -ano | findstr 8081查到占用端口的PID然后taskkill /PID /F强杀。这个操作在开发中会用得上学会能省不少时间。5.3 部署上线环境与系统优化方案做毕设只需要本地演示但如果想把项目完完整整地部署到Linux服务器上展示给老师看也可以走通这条路。后端用Maven打成jar包执行mvn clean package -DskipTests生成可执行jar。服务器上装好JDK和MySQL后把jar包和数据库脚本上传上去执行nohup java -jar ebike-backend.jar app.log 21 让服务在后台运行。前端打包更简单执行npm run build后会生成dist目录里面是纯静态文件。可以用Nginx托管这些文件同时Nginx配置反向代理把/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; # 处理history路由刷新404问题 } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署时容易忽略一个细节打包后端时如果前端用的axios baseURL是/api而后端接口本身不带/api前缀需要在代理配置里把/api前缀去掉否则会404。正确的写法是proxy_pass http://127.0.0.1:8081/后面的斜杠表示把匹配到的/api替换为空。这一块多花一分钟搞清楚能少踩一个大坑。6. 项目亮点扩展与面试经验提炼6.1 用Redis为项目加分基本功能做完后横向扩展一些中间件能让项目在毕设答辩或面试时更有竞争力。我给过很多学生一个建议优先引入Redis因为它在项目中能用到两个很自然的场景。第一个场景是缓存电动车列表。课设阶段用户量小每次查询车辆列表都直接查数据库没问题。但面试官问你“如果用户量大了怎么办”时你可以说引入Redis缓存把热门的电动车列表缓存起来设置5分钟过期查询时先查缓存再查数据库。这体现了缓存思想。第二个场景是存登录态。前面用JWT做认证时token是客户端自己保存后端完全无状态。引入Redis后可以做一个变动用户登录时把token作为key、用户信息作为value存进Redis设置有效期30分钟。每次请求时后端去Redis里校验token是否存在这样管理员就可以主动踢人下线、统一管理登录态这就是单点登录的雏形。面试时讲到这个点能让面试官觉得你考虑问题更深入。第三个加分方向是处理订单超时关闭。除了用定时任务每分钟扫描超时订单更优雅的做法是使用RabbitMQ的延迟队列或者Redisson的延迟队列用户下单后往队列发一条延迟15分钟的消息到期后触发关闭逻辑。这个方案能体现消息队列的运用能力是面试的加分项。6.2 多角色权限控制的实现技巧前面提到了登录权限控制如果要更进一步可以在用户实体里加role字段区分普通用户和管理员。做法是自定义权限注解和AOP切面在方法上标注PreAuthorize(hasRole(ADMIN))拦截器解析token后把用户角色放进请求上下文AOP切面判断方法权限不匹配则抛出异常。这个方案能让你在答辩时清楚解释RBAC基于角色的访问控制模型的基本思路。一个便宜的方案是在拦截器里直接判断。如果请求路径以/admin开头就要求用户角色必须是ADMIN这样管理端的接口天然被保护。这种基于路径的权限控制虽然简单但胜在直观好理解应付答辩完全够用。6.3 答辩时个人项目阐述应当注意的四个关键点答辩和面试时项目介绍的时间一般在3到5分钟我建议按这条主线来讲项目背景、系统架构、核心难点、个人贡献。背景讲为什么做电动车租赁架构讲前后端分离、SpringBoot Vue MySQL的整体结构难点讲订单并发状态控制、金额精度处理、JWT认证流程这些细节贡献讲自己在项目里负责了哪些模块、解决了什么问题。讲项目时容易犯的错误是背业务比如堆砌“我做了用户管理、订单管理、车辆管理”。这么说面试官毫无记忆点。更好的方式是选一个点深挖拿下单业务举例你可以说“订单模块里最复杂的是并发控制我通过数据库条件更新保证同一辆车不会被重复租出通过事务保证支付和订单状态变更的原子性。”这句话一出来面试官基本能判断你确实做过、想清楚了。另一个容易被追问的点是为什么选MySQL而不选别的。可以直接从ACID事务、B树索引、InnoDB行锁、社区生态几个角度展开展示自己对于数据库选型有过思考而不只是碰巧用了这个库。6.4 从毕设项目到面试项目的进阶路线如果你把这个项目当成求职的敲门砖光把代码跑通还不够我建议做三件进阶的事。第一件是把项目部署到线上服务器并挂一个域名哪怕只是最低配置的云服务器。面试官问“项目上线过吗”时你报出域名那一刻真实性直接拉满。线上系统还能自己压力测试观察到QPS瓶颈再针对性优化这些都是宝贵的实战经验。第二件是把前端代码补一个单元测试和自动化测试的简单场景。前端至少补一个登录页面的表单校验测试后端补一个订单创建的单元测试。能讲出测试思路的人在初级岗位候选人里并不多见。第三件是写一篇完整的技术总结文档把系统架构图画出来、把核心表结构、关键接口的时序图画出来。这一步不仅帮你在讲项目时有条理还能加深自己对这些设计的理解。写文档的过程会让你发现自己理解的漏洞及时补上。我在给学生的辅导中反复强调一个观念毕业设计项目不是终点而是你从课本走向工业级开发的第一级台阶。这个电动车租赁系统麻雀虽小但五脏俱全它让你真实体验了一个软件产品从需求分析、数据库设计、前后端开发到测试部署的完整生命周期。把这段经历沉淀成自己的东西它带给你的价值会远超一份毕业设计的分数。