尧图精选

校园电动车租赁系统实战:SpringBoot+Vue全栈开发指南

🕒 发布时间:2026/10/2 2:56:18 📁 来源:尧图网络
校园里电动车越来越多但真正用起来的人却不多。买一辆三四千毕业带不走只能低价转让维修充电又麻烦。如果能按小时、按天租通勤上课随用随走这个需求在高校里非常真实。我基于SpringBootVue做了一套校园电动车租赁系统前后端分离覆盖了车辆管理、在线租车、计费结算、订单追踪这几个核心环节。这篇文章把整个项目的设计思路、核心代码逻辑和踩坑经历完整梳理一遍给正在做Java全栈项目或者拿这个选题做毕设的同学一份能直接参考的实战记录。1. 校园场景下电动车租赁的痛点与业务边界1.1 为什么这个需求在校园里成立先聊业务。很多人一听租赁系统就觉得是老生常谈但校园电动车租赁和城市的共享单车、共享电动车完全是两回事。城市的共享电驴是平台统一投放、统一运维用户随扫随走校园场景里车辆归属可能是学校后勤、可能是车行商家、也可能是毕业学长留下的二手车。运营方需要的是把闲置车辆盘活而不是重新造一批车投放。这就决定了系统的核心不是开锁骑行而是车辆资产的管理、租赁订单的生命周期和计费规则的可配置。实际调研下来校园用户有几个典型诉求短时需求去两公里外的实验室、去地铁站接人用一两个小时。按天需求周末郊游、外出办事租一整天。长租需求考研党、实习党按月包车上下班通勤。三种需求对应三种不同的计费模式而且还会叠加。如果一套系统只支持固定单价运营方就得天天改后台不现实。所以我在设计时就明确了业务边界车辆管理、用户租还、订单计费、后台统计四个核心域其他的先不做。1.2 角色权限怎么划分这个系统的用户角色比较简单清晰角色主要操作核心诉求游客浏览车辆、查看价格低门槛了解信息学生用户注册登录、租车、还车、续租、查看订单、在线支付流程顺畅、价格透明运营管理员车辆上下架、审核订单、处理异常、查看经营数据高效管理、纠纷可追溯超级管理员用户管理、管理员分配、计费规则配置权限收敛、灵活配置权限上我没有做得太复杂就是基于角色的访问控制后端用Spring Security的注解做接口拦截前端配合Vue Router的守卫做页面控制。这里要提醒一点前端路由权限只是体验层面的控制真正的权限校验必须落在后端接口上。我见过很多毕设项目只在Vue里判断了个角色就完事结果接口直接裸奔别人用Postman照样能调管理接口这个在答辩的时候被问出来会很尴尬。1.3 核心业务闭环整个系统要跑通一条完整的链路车辆展示 → 用户选车 → 创建订单 → 在线支付 → 车辆出库/锁定 → 骑行使用 → 归还车辆 → 订单结算 → 费用清算。这里面有个容易忽略的点租车和还车之间车辆的物理状态和系统状态必须保持一致。比如一辆车被用户租走了系统里它的状态就不能还是可租还车之后必须触发结算动作而不是仅仅把状态改回可租。我的做法是给订单状态和车辆状态分别建模用状态机约束流转而不是靠零散的if else去改。这个后面详聊。2. 技术选型SpringBoot Vue的角色分工与架构逻辑2.1 为什么是这两件套先说我选型的思考过程。后端用Java生态候选是SpringBoot、SpringCloud太重、SSH老框架没必要。SpringBoot的优势不用我多吹自动配置、内嵌Tomcat、起步依赖做这种单体全栈业务系统效率最高。校园租赁系统撑死几百个并发单体完全够用不需要一上来就微服务。前端用Vue我当时主要考虑三点。第一Vue对渐进式开发很友好像管理后台这种中后台界面可以用Vue Router Vuex或者现在的Pinia快速搭出工程化骨架第二Vue生态里Element Plus、Ant Design Vue这种组件库非常成熟表格、表单、弹窗、日期选择器开箱即用管理端的开发速度能快一倍第三市场上Vue的招聘需求和资料丰富以后扩展维护找人接手也容易。当然这里不是说其他组合不行。如果你的项目偏实时交互、团队又熟React选React完全没问题。关键是你得能说清楚为什么选答辩和工作中都是一个道理。2.2 前端工程与后端工程的目录设计我用的是前后端完全分离的结构两个独立工程通过RESTful接口通信。后端标准SpringBoot分层src/main/java/com/campus/ebike/ ├── controller/ # 接口层只做参数接收和结果封装 ├── service/ # 业务层事务、状态流转、计费等核心逻辑 ├── mapper/ # MyBatis-Plus持久层 ├── entity/ # 实体类 ├── dto/ # 前端入参对象和出参对象 ├── config/ # 安全配置、Web配置、CORS配置 ├── common/ # 统一返回结果、异常处理、常量 └── utils/ # JWT、日期、金额处理工具前端Vue工程结构src/ ├── api/ # 接口请求模块按业务域拆分 ├── router/ # 路由配置 路由守卫 ├── store/ # 全局状态管理 ├── views/ # 页面组件 │ ├── user/ # 用户端页面 │ ├── admin/ # 管理端页面 │ └── common/ # 通用页面 ├── components/ # 公共组件 └── utils/ # axios封装、格式化工具这个分层的好处是职责清晰controller薄、service厚后续出问题好定位。很多同学喜欢把业务逻辑全写在controller里一个方法几百行看起来能跑但后期加需求的时候非常痛苦。我建议从一开始就养成controller只负责接参数和响应service负责业务规则的习惯。2.3 接口风格与统一返回结构前后端联调最怕的就是各搞各的返回格式。我在项目第一天就定了统一返回结构public class RT { private Integer code; // 200成功其他为业务错误码 private String message; // 提示信息 private T data; // 业务数据 }举一个实际接口的例子查询车辆列表GetMapping(/vehicle/list) public RPageResultVehicleVO list(RequestParam Integer page, RequestParam Integer size, VehicleQuery query) { return R.ok(vehicleService.pageQuery(page, size, query)); }所有接口都走这个格式前端统一在axios拦截器里处理code不等于200的情况弹提示、跳登录页一套逻辑通吃所有接口。这个习惯能省掉大量联调时间。3. 数据库设计订单状态机与核心表结构3.1 核心表清单数据库设计我最有心得因为这个项目的灵魂全在表结构和状态流转上。核心表大概十张左右我把最关键的列出来表名用途核心字段user用户表id, phone, password, nickname, role, status, balancevehicle车辆表id, name, plate_no, type, status, hourly_price, daily_price, monthly_price, location, battery, imagevehicle_type车辆类型表id, type_name, config_descrental_order租赁订单表id, order_no, user_id, vehicle_id, start_time, expect_end_time, actual_end_time, rent_type, deposit_status, order_status, total_amountpayment_record支付流水表id, order_id, user_id, amount, pay_type, pay_status, out_trade_norecharge_record充值记录表id, user_id, amount, balance_before, balance_aftercoupon优惠券表id, user_id, amount, threshold, status, expire_timeoperation_log操作日志表id, admin_id, action, target_id, detail, create_time车辆状态字段status我用了整数枚举0表示下架、1表示可租、2表示已租出、3表示维护中、4表示已预约锁定。注意可租和已租出之间还有一个锁定状态是为了处理用户下单后、还没实际取车的那段时间防止别人同时下单同一辆车。3.2 订单状态机的流转设计订单状态是整个系统最容易出错的地方。我用状态机来约束订单状态status定义0待支付1待取车已支付2使用中已取车3待归还用户发起还车系统等待管理员确认4已完成已结算5已取消支付前或超时未支付自动取消6异常关闭超时未还、损坏、纠纷等允许的流转路径0 - 1 (支付成功) 0 - 5 (超时未支付/主动取消) 1 - 2 (用户扫码取车/管理员确认出库) 2 - 3 (用户发起还车) 3 - 4 (管理员确认归还并结算) 2 - 6 (恶意不还/超时未还触发) 1 - 6 (支付后长时间未取车管理员强制关闭)在代码层面我会写一个状态流转校验的方法任何修改订单状态的操作都走这个方法不合法就抛异常public void changeOrderStatus(RentalOrder order, Integer targetStatus) { SetInteger allowed TRANSITION_MAP.get(order.getOrderStatus()); if (allowed null || !allowed.contains(targetStatus)) { throw new BizException(非法的订单状态流转: order.getOrderStatus() - targetStatus); } order.setOrderStatus(targetStatus); }这个设计看起来很简单但实际效果非常好。一次我在测试时想当然地把一个使用中的订单直接改成已完成被这个校验拦下来才发现中间漏了待归还这个节点。状态机不只是为了安全更是为了逼你把业务流程想完整。3.3 我对表设计的几条取舍第一订单号和支付流水号都用了独立的业务编号而不是直接用自增id。自增id容易暴露业务量又有生成时序问题我都是用yyyyMMddHHmmss 随机数生成订单号支付流水用更长的唯一字符串。第二金额字段用decimal(10,2)绝不用float/double这个属于Java后端基础中的基础了涉及钱的计算用浮点数会出大问题。第三所有表都加了create_time和update_time两个公共字段MyBatis-Plus的字段自动填充一顿配置省心且排查问题必用。4. 计费引擎时租、日租、月租与超时补差的计算实现4.1 计费规则的可配置设计计费是租赁系统的核心利润来源一定要做得灵活。我在vehicle表里直接放了三个价格字段hourly_price、daily_price、monthly_price另外还有deposit押金字段。这样做有一个好处每种车型可以独立定价普通小电驴和学生款大功率车的价格不一样。租赁方式rent_type我分了三种HOURLY时租、DAILY日租、MONTHLY月租。下单选类型时就必须确定不允许中途切换。这个约束要在后端校验前端只是展示层面。种情况比如时租从下午2点到5点就是3个小时但如果用户中途超了两小时该怎么算不能简单地时租单价乘以时长因为超时涉及占用资源的机会成本。我的规则是前N小时按时租价超时部分按超时单价 时租价 × 1.5累计不足一小时按一小时算同时封顶为当日日租价避免极端费用让用户崩溃。日租则简单一些超过一天按天累加不足一天的按天算因为日租已经是最细颗粒度不存在按小时折算。月租最小单位为月不足一个月按一个月算这个规则要在下单时前端就明文展示减少纠纷。4.2 超时计费的并发与精度问题这里有一个很容易踩的坑租车订单的计费发生在还车那一刻但超时提醒需要定时扫描。如果用户租的车超时了系统得自动通知他续费或者还车这需要定时任务配合。我用Spring的Scheduled注解实现了一个每5分钟扫描一次的定时任务找出所有超时且状态仍为使用中的订单异步推送站内通知和短信短信网关这块我用的是短信服务商的API预留接口没有真配置。到还车结算的时候要避免两个动作同时改动同一笔订单。我的做法是用MySQL的行锁配合乐观锁在结算方法上使用select ... for update锁定订单行Transactional public void settleOrder(Long orderId) { RentalOrder order rentalOrderMapper.selectForUpdate(orderId); // 行锁 // 计算费用、更新状态、生成支付流水 }为什么加行锁因为定时任务扫到超时订单时会尝试做提醒甚至强改状态用户端同时又在点还车两个事务同时读到status2的订单都觉得自己能操作不加锁就会出现状态错乱和重复结算。这个我在前期联调时真实遇到过一辆车的订单被结算了两次支出和收入对不上排查了半天最后用行锁解决。4.3 押金与余额的账务处理押金单独做成deposit_status字段0未付、1已付、2已退。押金不进入订单金额而是挂账在用户身上。还车时先判断有没有违章、超时未处理的历史订单有的话先扣异常费用再退押金。这里我用了一个简单的记账思路所有涉及用户余额变动的操作都落一条recharge_record或payment_record绝对不直接在user表的balance上加减就完事。后面查账、对账全靠流水表。5. 后端核心实现JWT鉴权、下单事务、接口安全5.1 JWT鉴权方案与token刷新认证这块我选了JWT用Spring Security JJWT实现。用户登录成功后后端生成一个token返回给前端前端存到本地存储每次请求在axios拦截器里带上Authorization: Bearer token头。private String generateToken(User user) { return Jwts.builder() .setSubject(user.getId().toString()) .claim(role, user.getRole()) .claim(nickname, user.getNickname()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }token过期处理是重点。我以前不做刷新用户坐那写个半小时的帖子再提交发现token过期被踢回登录页体验很差。后来加了刷新机制token有效期2小时后端提供一个/auth/refresh接口前端在axios响应拦截器里判断401若当前有refreshToken就静默刷新重新请求一次用户在无感知的情况下续期。这个机制在移动端和PC端的体验提升非常明显。5.2 下单与锁车的原子性设计下单是整个系统并发压力最大的点。两三个人同时盯着同一辆可租的车如果都提交订单最后车辆状态只能有一个成功。我的做法是先锁车、后下单用数据库的原子更新来实现Transactional public Long createOrder(RentOrderCreateDTO dto) { // 1. 原子更新车辆状态只有当前状态为1(可租)才允许改成4(锁定) int count vehicleMapper.lockVehicle(dto.getVehicleId()); if (count 0) { throw new BizException(车辆已被预订或不可租); } // 2. 创建订单 // 3. 设置超时自动取消时间 // 4. 返回订单号 }对应的MyBatis更新语句是UPDATE vehicle SET status 4, update_time NOW() WHERE id #{vehicleId} AND status 1这个UPDATE ... WHERE status 1非常关键它本身就是一把乐观锁。数据库的行锁保证同一时刻只有一个事务能把这辆车从1改成4其他并发请求update影响行数为0直接返回车辆已被预订。这样就避免了先查询再更新的竞态条件。5.3 事务控制的关键点一个下单流程涉及车辆表、订单表、支付记录表三张表必须放到同一个事务里。我上面用了Transactional但这里要注意几个细节事务方法不能是private必须通过Spring代理调用否则注解失效。异常要被Spring的事务机制感知所以我自定义的BizException要继承RuntimeException这样Transactional才会默认回滚。如果throws了一个受检异常默认情况下事务不会回滚。不要在大事务里做远程调用或耗时操作。初期我傻乎乎地在下单事务里加了短信通知逻辑结果通知服务超时整个下单事务回滚了用户看到下单失败其实是短信超时。后来我把通知、日志这些非核心逻辑全部改成事务同步、异步执行核心业务只操作数据库。5.4 接口安全的几个底线除了JWT鉴权我还做了几层防护。第一越权校验。比如用户A不能通过直接拼接口修改订单B的订单所有涉及订单/车辆归属的操作Service层第一步必须是校验当前登录用户与资源归属是否一致。第二参数校验。用Validated配合JSR规范给DTO加约束注解比如手机号格式、金额上限避免脏数据入库。第三操作日志。管理端的车辆上下架、订单强关、价格修改都落operation_log出现问题能追溯到人。6. 前端Vue实现要点权限路由、状态展示与接口联调6.1 管理后台的权限路由与侧边栏管理端用Vue Router 动态路由加载。用户登录后后端根据角色返回可访问的路由表前端用router.addRoute动态挂载。以管理员为例他只能看到车辆管理、订单管理、用户管理、租还管理这些菜单看不到系统配置、管理员管理这些超管菜单。// 路由守卫核心逻辑 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (token store.state.user.role ADMIN to.path.startsWith(/user)) { // 管理员访问用户端页面重定向 next(/admin/index) } else { next() } })前端路由守卫别写得太重只做一层粗略控制细粒度权限依赖后端接口返回的403/401来兜底。6.2 车辆展示与租车流程页面用户端首页的车辆卡片列表我用了一个简单的VehicleCard组件展示车辆图片、型号、价格、续航、当前状态。状态颜色做了区分可租绿色、已租灰色、维护中橙色。点击后可看到车辆详情并跳转下单页。租车流程是三步选择租赁方式时租/日租/月租页面实时计算预估费用展示押金金额。选择期望取车时间和预计归还时间后端会校验这个时间窗内车辆是否可租如果车辆已被别人锁定到某个时间段就不能租。确认订单支付押金或整单金额我做了押金与租金分离押金冻结不划扣还车后自动解冻。这个流程里有一个体验上的细节订单列表的状态标签。我用了一个状态映射组件把订单状态码翻译成中文标签和对应的操作按钮比如状态为使用中的订单显示申请还车按钮状态为待归还的显示等待管理员确认。状态与按钮一一对应避免用户对着一堆状态字段不知所措。6.3 axios封装与文件上传的坑axios封装这一步挺关键的。我在utils文件夹里写了一个request.js统一配了baseURL、超时时间、请求拦截器带token、响应拦截器统一处理业务异常和401。顺便说一句前端请求超时时间一定要设不然后端接口如果挂了浏览器默认超时时间很久用户会一直转圈以为系统卡死。车辆图片上传我用的是Element Plus的el-upload组件直接上传到后端上传时把token放进header。这里我踩过一个特别典型的坑el-upload组件默认的action属性会自行发起请求如果请求头没带token后端返回401图片怎么传都失败。后来我改成自定义http-request用自己封装的axios实例上传问题就解决了。7. 部署上线与实战避坑本地跑通到服务器可用7.1 前端构建与Nginx配置开发完成后的构建部署我用的是前端打包静态文件、Java后端打成jar包、Nginx做反向代理的方式。前端npm run build生成dist目录放到Nginx的html目录下后端mvn package打成jar用java -jar启动。Nginx配置的核心是server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /home/www/ebike-frontend/dist; index index.html; # Vue Router history模式刷新页面不能404 try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files这行必须有否则Vue Router用了history模式后直接访问/admin/index这种二级路径会404。不少同学部署完发现点菜单能跳转一刷新就404就是漏了这一行。7.2 我踩过的三个典型问题第一个跨域问题搭个代理就能解决。开发环境我是用Vue CLI的devServer配了proxy代理把前端的/api请求转发到后端localhost:8080这样前端页面访问的域名和接口域名一致没有跨域。但上线后如果用Nginx做同域反向代理前后端都在同一个域名下也不需要CORS。只要记住一点能用反向代理解决的跨域问题尽量不要用后端CORS框架去开放跨域后者的安全风险更高。第二个数据库时区问题导致时间差8小时。我第一次连接MySQL时没在连接串里加serverTimezoneAsia/Shanghai结果所有时间字段在插入后都比真实时间多了8小时订单的预计归还时间全错了。排查了很久才发现是时区问题。链接串里必须显式指定spring.datasource.urljdbc:mysql://localhost:3306/ebike?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai第三个MyBatis-Plus的分页插件不生效。这个属于绕不过去的坑。新版MyBatis-Plus需要手动配置PaginationInnerInterceptor如果忘了配selectPage查出来的total永远是0。我一开始没配拦截器以为分页代码写错了折腾了一晚上。7.3 上线前后的性能与安全建议系统上线前我做了几件花不了多少时间但很值得的事MySQL连接池配置合理的最小空闲数和最大连接数默认值在低配服务器上可能撑不住。接口层做了简单的限流用拦截器对/api/order这些写操作接口做了IP维度的频率限制防止有人恶意刷单。密码存储用了BCrypt哈希加密后的密码即使数据库泄露也没法直接登录。对上传的图片做了类型和大小校验防止恶意上传脚本文件。关于性能这块其实不用过度设计。校园租赁系统的真实并发不会太高单体应用 关系型数据库 适当的索引优化完全够用。索引方面我给订单表的user_id、order_status、create_time分别建了索引车辆表的status字段建了索引常见查询都在索引覆盖范围内。真到了量大的那天再考虑缓存或读写分离也不迟。最后再分享一点个人的体会。做这个项目最深的感触是业务规则的设计远比堆砌技术点重要。一开始我很在意用了什么新框架、什么酷炫功能后来发现真正让这个系统能跑起来的是那些藏在细节里的状态流转、事务边界和并发控制逻辑。状态机、行锁、事务回滚这些Java基础里的东西学的时候觉得抽象放到租赁这个具体场景里一下就理解了。如果你也正在做类似的Java全栈项目建议先把业务流程捋清楚画出状态流转图设计好表结构再动写接口的念头这样后面每一步都会顺畅很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →