ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot校园电动车租赁系统:从订单表设计到扫码租车实践

2026/9/19 14:02:42 拓冰建站 浏览量
Spring Boot校园电动车租赁系统:从订单表设计到扫码租车实践 简介一份面向计算机专业学生的校园电动车租赁系统毕业设计论文文档以Spring Boot与Vue.js前后端分离方案为主线系统阐述基于MySQL的电动车租赁平台的设计与实现。文档包含摘要、关键词、目录、绪论、需求分析、系统设计、功能实现等完整章节覆盖用户注册登录、电动车租赁与还车、扫码操作、报障反馈、管理员信息管理、数据统计及在线客服等核心模块可作为同类课题选题、技术选型和论文架构的直接参考。整个资源共1个docx文件压缩包大小6.71MB内容集中、结构清晰便于按章节查阅和二次修改。目前已有72人学习下载适合毕业设计开题、系统开发与论文撰写阶段参考借助该文档可快速梳理业务场景与功能模块并借鉴其论述方式和目录组织提高论文写作效率。1. 校园电动车的租赁需求最终落在一张订单表上校园面积越扩越大从宿舍楼到实验楼动辄一公里以上高峰期共享单车又常常无车可扫电动车租赁就成了校园出行的刚需。这个基于 Spring Boot 的校园电动车租赁系统后端用 Spring Boot 暴露 REST 接口前端用 Vue.js 渲染页面MySQL 存车辆台账与订单记录定位就是一套前后端分离的典型管理类项目面试里常被问到的 springboot 项目场景也和它高度重合。它没有复杂算法真正的难点是把租车、还车、计费、报障这条状态链路做对。本文按“数据表 → 扫码租车 → 统计调度 → 部署排查”的顺序把一台电动车从上线到归还的全过程拆开讲。2. Spring Boot MySQL 数据建模车辆台账与租赁订单状态刚拿到这类需求大多数人先写用户管理再写车辆管理最后才补租赁记录结果发现租赁记录里不知道该存哪些字段。我在这个项目里建议反过来先定义租赁订单表因为用户、车辆、报障都是围绕订单展开的。订单表设计定了其他表都只是它的关联维度。2.1 核心表结构从电动车台账到租赁订单2.1.1 电动车台账表ebike电动车表的字段并不需要很多关键是位置、状态与车辆编号具有唯一性。下面是去掉冗余字段后的建表 SQL。CREATE TABLE ebike ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, bike_no varchar(32) NOT NULL COMMENT 车辆编号, type_name varchar(64) DEFAULT standard COMMENT 车型standard/comfort/large, lat decimal(10,6) DEFAULT NULL COMMENT 纬度, lng decimal(10,6) DEFAULT NULL COMMENT 经度, status tinyint NOT NULL DEFAULT 0 COMMENT 0可租 1租赁中 2维修中 3下线, qr_code varchar(128) DEFAULT NULL COMMENT 车身二维码内容, last_maintain_time datetime DEFAULT NULL COMMENT 最近维保时间, PRIMARY KEY (id), UNIQUE KEY uk_bike_no (bike_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status 用 tinyint 而不是字符串是为了 Java 侧能用枚举映射、前端再翻译成中文标签也方便在 SQL 里直接WHERE status 0做筛选。qr_code 存二维码原始内容通常是一个带车辆 ID 的 URL扫码时前端解析出 bikeId 再调后端不在二维码里塞多余信息。last_maintain_time 是给管理员调度用的连续 30 天没有维保记录的车要在管理列表里标红。2.1.2 租赁订单表rental_order订单表是租赁状态机的载体。CREATE TABLE rental_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint NOT NULL COMMENT 用户ID, ebike_id bigint NOT NULL COMMENT 车辆ID, start_time datetime NOT NULL COMMENT 租车时间, end_time datetime DEFAULT NULL COMMENT 还车时间, amount decimal(10,2) DEFAULT 0.00 COMMENT 费用, status tinyint NOT NULL DEFAULT 0 COMMENT 0租赁中 1已归还 2异常关闭, fault_desc varchar(255) DEFAULT NULL COMMENT 还车时上报的故障, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_ebike_id (ebike_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;order_no 不用自增主键直接对外暴露因为订单号会出现在二维码回跳地址和客服记录里用时间戳加随机数生成更安全。end_time 允许为空空值代表这次租赁还没结束后续统计活跃订单时WHERE end_time IS NULL就能捞出来。amount 在还车时才计算租车时只记录 start_time。2.2 订单状态机的流转规则订单状态是全局最容易出 bug 的地方。我一般会把状态和流转规则列成一张表开发时对照着写状态数值允许流入状态触发场景租赁中0租车成功用户扫码确认租车已归还1租赁中用户正常还车异常关闭2租赁中上报故障或超时未还租车时ebike.status和rental_order.status要在同一个事务里更新还车时反过来恢复。如果两个表分开写库并且没加Transactional会出现订单已归还、车辆仍是租赁中的脏数据。这一点在 springboot 面试题里经常以“订单超时未还怎么处理”的方式出现提前把状态机定义清楚回答时就能直接说该把哪个字段从 0 改成 2。2.3 MyBatis-Plus 实体映射与条件查询实体用 MyBatis-Plus 注解映射到表。Data TableName(ebike) public class Ebike { TableId(type IdType.AUTO) private Long id; private String bikeNo; private String typeName; private BigDecimal lat; private BigDecimal lng; private Integer status; private String qrCode; private LocalDateTime lastMaintainTime; }查询用户附近可租车辆时用 LambdaQueryWrapper 组装条件比手写 XML 更直观ListEbike list ebikeMapper.selectList( new LambdaQueryWrapperEbike() .eq(Ebike::getStatus, 0) .between(Ebike::getLat, lat - 0.01, lat 0.01) .between(Ebike::getLng, lng - 0.01, lng 0.01) .last(limit 20));eq 过滤车辆状态为可租between 以用户当前经纬度为中心圈出一公里左右的范围last 拼接 limit 防止接口一次返回过多空闲车。这里没有用 ge、le 分别写四行between 两个字段就能表达同样的语义。校园场景下车辆数量有限这种轻量查询完全够用如果以后扩展到多校区再考虑引入 Redis GEO 也不迟。3. 扫码租车与还车API实现二维码、鉴权与状态校验用户在前台看到车辆位置只是第一步真正决定项目质量的是扫码租车和还车这两个动作。二维码里存什么、登录态怎么传递、租车接口如何防止重复提交、还车时如何保证费用与状态一致下面按这四个问题展开。3.1 二维码内容与登录态传递二维码内容不要存 JSON 字符串扫码工具对特殊字符兼容性差。常见做法是存一个短链接例如https://api.campus-ebike.com/rent?bikeId123。用户扫码后前端先判断本地有没有 JWT token没有就跳登录页有则进入确认页。后端用一个拦截器统一解析请求头里的 Authorization 并写入当前用户上下文租车和还车接口就不必重复写解析逻辑。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !jwtUtil.validate(token)) { response.setStatus(401); return false; } UserContext.set(jwtUtil.getUserId(token)); return true; } }token 过期返回 401前端收到后自动跳回登录页重新登录。这个拦截器只作用于需要登录的接口公告资讯这类公开接口单独放行。bikeId 放在 URL 里本身没有问题后端接口会校验车辆存在性和状态不依赖前端传值所以即使有人拿着 URL 直接调接口也绕不过业务校验。3.2 租车接口的幂等与并发控制连续点击“立即租车”两次不能生成两笔租赁中的订单。租车接口的防御手段不是靠前端按钮置灰而是后端在事务里做状态校验。Transactional(rollbackFor Exception.class) public RentResult rent(Long userId, Long ebikeId) { Ebike ebike ebikeMapper.selectById(ebikeId); if (ebike null || ebike.getStatus() ! 0) { throw new BizException(400, 车辆不可租); } // 防重同一辆车只允许存在一笔进行中的订单 Long active rentalOrderMapper.selectCount( new LambdaQueryWrapperRentalOrder() .eq(RentalOrder::getEbikeId, ebikeId) .eq(RentalOrder::getStatus, 0)); if (active 0) { throw new BizException(400, 车辆正在租赁中); } Ebike update new Ebike(); update.setId(ebikeId); update.setStatus(1); ebikeMapper.updateById(update); RentalOrder order new RentalOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setEbikeId(ebikeId); order.setStartTime(LocalDateTime.now()); order.setStatus(0); rentalOrderMapper.insert(order); return new RentResult(order.getOrderNo()); }这段代码有三道防线第一查询车辆状态状态不是 0 直接拒绝第二统计该车状态为租赁中的订单数大于 0 说明这台车已经被租走第三先更新车辆状态再插入订单全程在同一个事务里任何一个步骤抛异常都会回滚。generateOrderNo() 我习惯用yyyyMMddHHmmss加 4 位随机数拼成字符串并发量不大的场景基本不会碰撞。3.3 还车接口费用计算与状态回写还车时有两条分支正常归还或者上报故障归还。上报故障的车辆要直接进入维修流程不能按普通还车把车辆状态恢复成可租。Transactional(rollbackFor Exception.class) public void returnBike(Long userId, Long orderId, ReturnRequest req) { RentalOrder order rentalOrderMapper.selectById(orderId); if (order null || !order.getUserId().equals(userId) || order.getStatus() ! 0) { throw new BizException(400, 订单不存在或已归还); } LocalDateTime now LocalDateTime.now(); if (req.getFaultFlag()) { order.setStatus(2); order.setFaultDesc(req.getFaultDesc()); rentalOrderMapper.updateById(order); Ebike update new Ebike(); update.setId(order.getEbikeId()); update.setStatus(2); ebikeMapper.updateById(update); return; } BigDecimal amount calcAmount(order.getStartTime(), now); order.setEndTime(now); order.setAmount(amount); order.setStatus(1); rentalOrderMapper.updateById(order); Ebike update new Ebike(); update.setId(order.getEbikeId()); update.setStatus(0); update.setLat(req.getLat()); update.setLng(req.getLng()); ebikeMapper.updateById(update); }细读这段代码正常还车时会把前端上报的经纬度回写到车辆表管理员端看到的车辆位置就是最近一次归还地而不是车辆出厂坐标。费用计算按分钟向上取整避免出现“骑了 1 分 20 秒收两分钟钱”的投诉。故障还车时订单状态置为 2车辆状态置为 2维修中同时记录故障描述供管理员后续派单处理。接口请求方式核心入参关键校验/rentPOSTuserId, ebikeId车辆状态为 0 且无进行中订单/returnPOSTuserId, orderId, faultFlag订单存在且属于当前用户提示单校园规模下不要为了并发控制而引入 Redis 分布式锁数据库自带的行锁配合状态校验已经够了。锁超时反而会带来误判把正常租车请求当成重复请求拒绝掉。4. 管理端数据统计与报障处理调度决策的数据支撑管理员端的价值不在增删改查而在统计。没有统计管理员不知道哪几辆车天天满负荷、哪几辆停在角落里吃灰、报障集中发生在哪个区域。这一章把统计 SQL 和报障闭环分开讲清楚。4.1 车辆使用率统计一条 SQL 看懂全局统计每辆车的租赁次数和累计骑行时长用 group by 加两个聚合函数就够了。SELECT ebike_id, COUNT(*) AS rent_count, SUM(TIMESTAMPDIFF(MINUTE, start_time, COALESCE(end_time, NOW()))) AS total_minutes FROM rental_order WHERE start_time DATE_FORMAT(CURDATE(), %Y-%m-01) GROUP BY ebike_id ORDER BY rent_count DESC;重点在COALESCE(end_time, NOW())订单没还时 end_time 为空统计时长就用当前时间兜底。TIMESTAMPDIFF 返回分钟数前端展示时再除以 60 转成小时。rent_count 是最直接的调度指标连续两周排在后 20% 的车辆应该被人为挪到人流更密集的停车点。4.2 闲置车辆与异常订单定位使用率统计面向活跃车辆闲置车辆还得换一个查询思路。SELECT e.id, e.bike_no, MAX(o.start_time) AS last_rent_time, DATEDIFF(NOW(), MAX(o.start_time)) AS idle_days FROM ebike e LEFT JOIN rental_order o ON e.id o.ebike_id GROUP BY e.id, e.bike_no HAVING idle_days 7 OR idle_days IS NULL;LEFT JOIN 保证从未被租过的新车也能出现在结果里HAVING 里的idle_days IS NULL专门捞那些上线一周多还没有订单的车。管理员把这份列表导出后结合线下停车点的实际客流决定是换位置还是下线维护。统计数据不需要实时算。常见做法是凌晨用定时任务把前一天的数据汇总到结果表白天前端只读结果表避免聚合查询拖慢在线订单库。springboot 项目里加一个Scheduled方法即可Component public class StatTask { Scheduled(cron 0 0 2 * * ?) public void collectDailyStat() { // 按车辆、按天写入使用率汇总表保留最近 30 天 statService.collectDaily(); } }cron 表达式0 0 2 * * ?表示每天凌晨两点执行这个时间订单量最低即使任务跑慢也不会影响正常租车。任务调度的核心是错峰执行不要在白天业务高峰期跑大批量聚合统计。注意定时任务里做的写操作要带重试统计任务偶尔失败一次可以接受但不能连续失败还无人感知。建议任务结束时的成功、失败状态落到一张任务日志表管理员第二天打开后台先看日志表有没有红条。4.3 报障反馈的闭环设计报障不是“用户提交、管理员看一眼”就结束而要形成可追踪的闭环。用户提交时关联订单号和车辆编号管理员处理时更新处理备注用户端实时看到当前进度。状态含义后续动作待处理用户刚提交系统通知管理员指派人员处理中维修人员已接单更新处理备注已解决车辆恢复正常车辆状态从维修中改为可租报障反馈表的结构可以这样建CREATE TABLE fault_report ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint DEFAULT NULL COMMENT 关联订单, ebike_id bigint NOT NULL COMMENT 车辆ID, user_id bigint NOT NULL COMMENT 上报人, fault_type varchar(32) COMMENT 刹车/电池/轮胎/其他, description varchar(255), status tinyint DEFAULT 0 COMMENT 0待处理 1处理中 2已解决, handle_remark varchar(255) COMMENT 处理备注, create_time datetime, handle_time datetime, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;管理员把 status 改成已解决时要同步判断车辆状态。如果 fault_type 是“电池”而车辆还停在维修中说明问题并未真正处理完不能直接恢复可租。这个判断放在 Service 层而不是依赖管理员手动操作能少踩很多次误操作。5. 部署联调与排错application.yml 配置与常见坑开发环境跑通不代表能上线实际部署里问题往往集中在配置文件、跨域和时区这几处。这一章给出关键配置和排查顺序照着做能省去大半联调时间。5.1 application.yml 里的三个关键配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_ebike?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0url 里必须写 serverTimezone否则连接 MySQL 8.x 会直接报时区相关异常characterEncodingutf8保证中文不会乱码。log-impl 打开后控制台会打印每条 SQL联调时看日志比盲猜快得多。logic-delete 是 MyBatis-Plus 的逻辑删除配置表里有 deleted 字段时删除操作自动变成 update防止车辆台账被物理删除。5.2 前后端分离联调跨域与请求路径Vue 开发服务器默认跑在 5173后端是 8080浏览器默认拦截跨域请求。本地联调建议直接加一个 CORS 配置类。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:5173); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }addAllowedOrigin 只放行本地开发服务器不要图省事写成*否则部署后任何网页都能回调接口。如果后续要对接小程序把小程序合法域名加进白名单即可不需要改业务代码。5.3 排查顺序先看 SQL 再看状态联调时遇到“租车失败”不要急着改业务代码按三步检查先看控制台有没有打印出租车接口的 update SQL再查数据库里这辆车的 status 是否已经是 1如果是 1 说明上一笔订单没有正常释放最后看异常消息里有没有 BizException 抛出的中文提示。绝大多数问题都出在订单未释放或车辆状态被前一次测试污染而不是接口逻辑写错。扫码页面在微信里打开时还容易出现一个坑二维码内容里存的是内网 IP外网用户根本访问不到。处理办法是把二维码内容做成可配置项上线后替换成公网域名不要把 IP 写死在车辆初始化数据里。本文还有配套的精品资源点击获取