
做一个高校场景下的管理系统很多同学首先想到的是“这不就是增删改查吗”。但真正动手后才发现从需求拆分、数据库设计、权限控制到前后端分离联调每一步都有隐藏成本。尤其是技术栈选了 SpringBoot3 Vue3 MySQL 的组合光是版本兼容和工程结构就足以让新手卡住好几天。本文从实际开发视角拆解一个高校单车租赁管理系统从 0 到 1 的实现过程。不只讲代码还重点说明为什么这样设计、表结构有哪些坑、租还车流程如何保证数据一致、Vue3 前端如何高效对接后端接口。无论你是做课程设计、毕业设计还是想通过一个完整项目补齐前后端分离开发经验这篇文章都值得认真看完。1. 项目背景与价值判断高校校园面积普遍较大宿舍区、教学区、实验楼之间的距离常常超过步行舒适范围。共享单车进入校园后解决了出行问题但高校场景和商业场景有一个明显差异校园内的单车使用人群相对固定车辆数量有限且需要对学生身份进行核验。因此一个面向高校内部运营的单车租赁管理系统核心不是“扫码开锁”这类硬件联动而是对“谁在什么时间租了哪辆车、骑了多久、费用是多少、车辆目前在哪”进行完整记录和高效管理。从技术训练角度看这个选题非常合适原因有三个。第一业务边界清晰。用户可以拆分为学生、管理员两种角色也可以扩展出运维人员角色。业务流是发车、租车、还车、计费、维护不需要做复杂推荐算法或高并发抢购适合用来吃透一套主流技术栈。第二前后端分离架构是当下 Java 后端岗位的主流工程模式。SpringBoot3 负责后端接口和业务流转Vue3 Element Plus 负责管理界面和用户界面MySQL 负责持久化。这个项目能完整覆盖“标准企业级 Web 应用”的常见组成。第三数据一致性场景真实存在。比如一辆车被并发租用、还车时费用计算、车辆状态流转这些都需要通过数据库设计和事务控制来保证正确性而不是靠前端按钮临时拼接逻辑。这里也给出一个明确建议如果只是机械地做出页面和接口这个项目只是“管理系统”但如果把角色权限、租还车状态机、费用计算规则、车辆维护记录做成一套严谨的闭环它就足以写进简历的项目经验并能在面试中展开讲述。2. 系统需求分析与角色梳理在写代码之前需求分析比写代码更重要。高校单车租赁管理系统通常包含三类角色这里以最小可行版本为例学生用户、系统管理员、车辆维护员。角色不同看到的菜单和数据操作范围也不同。学生用户的核心诉求是注册登录、浏览可用车辆、发起租车、发起还车、查看个人骑行订单和历史费用。管理员的核心诉求是车辆信息管理、用户管理、订单查询与统计、计费规则配置。车辆维护员的核心诉求是维护车辆状态比如将故障车标记为维修中将维修完成车辆重新设为可用。角色权限如果不做系统设计直接在前端用 v-if 控制会带来安全隐患。因为前端控制只能决定“用户看不到入口”并不能阻止用户直接调用后端接口。正确做法是后端接口统一校验角色前端只是做了体验层的菜单过滤。这一点在课程设计和面试中都是加分项。2.1 业务状态流转分析单车对象在系统中不是静态数据而是有生命周期的。从投入校园开始单车会经历多种状态可用停在校园某区域等待被租用。骑行中已被用户租用尚未归还。维护中故障或需要保养禁止被租用。已下架长期损坏或报废。租车操作成功的前提是单车状态为“可用”。还车操作则将状态从“骑行中”改回“可用”同时计算费用。维护员操作的前提是单车当前不是“骑行中”否则会破坏状态一致性。另一种常见业务状态是订单状态订单和单车状态需要联动。一个合理的订单状态包括进行中、已完成、已取消。订单明细中至少记录租车时间、还车时间、租用时长、费用金额这类数据是后续做校园单车使用情况统计的基础。2.2 计费规则设计计费模块是整个系统的业务核心。最简方案是单一费率比如每小时 1 元不足一小时按一小时计算。但更合理的方案是把计费规则做成数据库配置而不是硬编码在 Java 代码中。这样当管理员调整价格时不需要重新编译部署系统。计费规则表可以设计为基础价格、单位时间内免费时长、超出后单价。例如前 30 分钟免费之后每 30 分钟 1 元。为了演示清楚本文采用简化规则按分钟计费每 30 分钟 1 元不足 30 分钟按 30 分钟计算。费用计算可以放在 Java 服务层在还车时根据租车开始时间和当前时间计算。也可以使用 MySQL 计算时间差。更推荐在服务层计算因为业务规则变化频率往往高于数据库结构变化频率集中管理便于测试和修改。3. 技术选型与项目架构技术选型要回答“为什么用这套组合”和“这套组合的边界在哪”。先看技术栈清单层次技术选型主要作用前端框架Vue3 TypeScript构建用户界面与后台管理界面前端 UIElement Plus提供表格、表单、弹窗、菜单等组件前端构建Vite开发服务器与生产构建后端框架SpringBoot3提供 RESTful API 与业务处理持久层框架Spring Data JPA 或 MyBatis-Plus数据库操作后端安全Spring Security 或 Sa-Token登录认证与接口鉴权数据库MySQL 8.x数据持久化存储接口调试RESTful API JSON前后端数据交互SpringBoot3 对 JDK 版本最低要求是 JDK17。这是和旧版 SpringBoot2 的最大区别。如果你的电脑还是 JDK8需要先升级 JDK 或使用兼容的旧版框架。选择 SpringBoot3 意味着面向未来Spring 官方对 2.x 的维护支持已经不再扩展新项目从 3.x 起步更合理。前端选择 Vue3 搭配 Vite开发体验比 Vue2 Webpack 更流畅组合式 API 也让组件逻辑复用更自然。Element Plus 是 Vue3 生态中成熟的组件库表格、表单、弹窗这类管理后台常用组件开箱即用可以节省大量样式时间。这里要特别解释一下为什么很多课程设计会写“XX 管理系统”但面试官依然不认可。原因通常是工程结构太混乱SQL 写死在 Controller业务逻辑堆在 Service前端一个页面几千行也没有异常处理。技术选型只是起点真正拉开差距的是工程组织和代码结构。4. 数据库设计表结构与关键字段数据库是整个系统的地基。先别急着写代码把表结构理清楚后面后端开发会顺利得多。4.1 数据表清单以最小完整闭环为例核心数据表包括用户表user、车辆信息表bike、订单表rent_order、计费规则表charging_rule、车辆维护记录表maintenance_record。如果系统包含站点概念还需要站点表station但这会增加还车区域校验逻辑。本文以无站点管理模式为例车辆通过状态标记区分是否可租。4.2 用户表设计用户表需要同时容纳学生和管理员更规范的设计是拆成 sys_user 和 sys_role 等表用中间表维护用户角色关系。但为了让初学项目不至于过度复杂可以简化为一张 user 表加 role 字段。创建用户表的 SQL 可以参考如下结构CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, student_no varchar(30) DEFAULT NULL COMMENT 学号, phone varchar(20) DEFAULT NULL COMMENT 手机号, role tinyint NOT NULL DEFAULT 2 COMMENT 角色1-管理员 2-学生, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-启用 0-禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码字段长度设计为 100是因为存储的并不是明文密码而是 BCrypt 加密后的哈希串。明文密码在数据库中存储是最常见的安全漏洞这个细节建议在项目中从一开始就规范起来。4.3 单车表设计单车表用于记录车辆静态信息与实时状态。车辆状态字段 status 使用 tinyint并在代码中定义枚举含义例如 1-可用、2-骑行中、3-维护中、4-已下架。创建单车表的 SQL 如下CREATE TABLE bike ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, bike_no varchar(30) NOT NULL COMMENT 车辆编号, bike_name varchar(50) DEFAULT NULL COMMENT 车辆名称/型号, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-可用 2-骑行中 3-维护中 4-已下架, location varchar(100) DEFAULT NULL COMMENT 当前停放位置, total_ride_count int DEFAULT 0 COMMENT 累计租用次数, total_ride_duration bigint DEFAULT 0 COMMENT 累计骑行时长单位秒, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_bike_no (bike_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT单车信息表;车辆编号 bike_no 使用唯一索引避免同一编号的车辆重复录入。日常开发中唯一性约束应当落在数据库层面而不是只靠代码判断。4.4 订单表设计订单表是租还车操作的中心表。订单创建时记录租车人、车辆、租车时间还车时回填还车时间和费用。为了避免每次查询订单都关联多张表可将部分冗余字段直接写入订单表例如 username、bike_no。这种“空间换时间”的冗余设计在管理系统中是合理的因为订单查询往往非常频繁。创建订单表的 SQL 可以参考CREATE TABLE rent_order ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, order_no varchar(40) NOT NULL COMMENT 订单编号, user_id bigint NOT NULL COMMENT 租车用户ID, username varchar(50) DEFAULT NULL COMMENT 租车账号冗余字段, bike_id bigint NOT NULL COMMENT 单车ID, bike_no varchar(30) DEFAULT NULL COMMENT 车辆编号冗余字段, start_time datetime NOT NULL COMMENT 租车开始时间, end_time datetime DEFAULT NULL COMMENT 还车时间, duration_seconds bigint DEFAULT NULL COMMENT 骑行时长单位秒, amount decimal(10,2) DEFAULT NULL COMMENT 费用金额, status tinyint NOT NULL DEFAULT 1 COMMENT 订单状态1-骑行中 2-已完成 3-已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_bike_id (bike_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT租车订单表;订单编号 order_no 推荐使用时间戳加随机数生成或者使用数据库时间函数加序列生成尽量避免使用自增主键作为业务编号暴露给前端。原因很简单自增 ID 容易被遍历猜测也缺乏业务含义。4.5 计费规则表计费规则表将价格参数化避免硬编码。CREATE TABLE charging_rule ( id bigint NOT NULL AUTO_INCREMENT, rule_name varchar(50) NOT NULL COMMENT 规则名称, unit_minutes int NOT NULL DEFAULT 30 COMMENT 计费单位分钟, unit_price decimal(10,2) NOT NULL DEFAULT 1.00 COMMENT 每个计费单位的价格, free_minutes int DEFAULT 0 COMMENT 免费时长分钟, status tinyint NOT NULL DEFAULT 1 COMMENT 是否启用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT计费规则表;计费逻辑可以这样理解骑行总时长先将免费时长扣除再除以计费单位向上取整最后乘以单价。向上取整的目的是处理不足一个计费单位的时间片段。例如骑行了 35 分钟免费 0按 30 分钟为一个单位实际收取 2 个单位费用。5. SpringBoot3 后端环境搭建后端环境是整套系统的“大脑”。在开始写代码前需要准备 JDK17、Maven、IDEA、MySQL 8.x。版本号请以实际安装为准但 SpringBoot3 配合 JDK17 是最稳妥的组合。5.1 创建 SpringBoot3 项目如果使用 IDEA 创建项目选择 Spring Initializr语言选择 JavaSpring Boot 版本选择 3.x。项目类型选择 Maven。在依赖中至少添加 Spring Web、Spring Data JPA 或 MyBatis 框架、MySQL Driver、Validation、Lombok。以 MyBatis-Plus 为例由于 SpringBoot3 的 Jakarta 命名空间变化需要引入mybatis-plus-spring-boot3-starter这一点和旧版不同。下面是一个最小可用的pom.xml关键依赖配置parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies5.2 配置 application.yml连接数据库的配置是后端启动的第一步。实际开发中推荐使用环境变量或配置中心管理账号密码本地演示可以直接写在配置文件中但不要把生产环境密码提交到 Git。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_bike_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意allowPublicKeyRetrievaltrue这个参数MySQL 8.x 在未配置 SSL 时可能出现公钥检索问题本地开发环境建议加上。map-underscore-to-camel-case的作用是让数据库下划线字段自动映射到 Java 驼峰属性。6. 后端核心业务代码实现后端代码的组织规律可以总结为 Controller 负责接收请求Service 负责业务逻辑Mapper 负责数据库操作Entity 负责字段映射DTO 负责接口参数。遵守这种分层后面对接 Vue 前端会非常轻松。6.1 统一返回结果封装前端需要一种统一风格的 JSON 响应结构。最常见的是包含 code、message、data 三个字段的对象。code 为 200 表示成功非 200 表示业务失败或系统异常。// 文件路径src/main/java/com/campus/bike/common/Result.java package com.campus.bike.common; import lombok.Data; Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }统一返回结果的好处有两个前端不用为每个接口单独处理字段结构后端可以通过拦截器或全局异常处理器统一把异常转成固定格式减少前端空指针判断。6.2 租车接口实现租车操作不是简单执行一条 insert。它需要保证两点单车状态是“可用”且该用户当前没有未完成的订单。如果直接 insert 订单而不加状态校验两台电脑同时请求同一辆车的接口时就会出现“同一辆车被两个人租走”的脏数据。从数据库层面看可以用乐观锁或事务内行锁来防止并发问题。最直观的实现是 Mapper 层提供条件更新只有车辆状态是可用时才更新为骑行中。先定义订单实体类// 文件路径src/main/java/com/campus/bike/entity/RentOrder.java package com.campus.bike.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; Data TableName(rent_order) public class RentOrder { TableId(type IdType.AUTO) private Long id; private String orderNo; private Long userId; private String username; private Long bikeId; private String bikeNo; private LocalDateTime startTime; private LocalDateTime endTime; private Long durationSeconds; private BigDecimal amount; private Integer status; }租车业务 Service 方法如下// 文件路径src/main/java/com/campus/bike/service/OrderService.java package com.campus.bike.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.campus.bike.common.BizException; import com.campus.bike.entity.Bike; import com.campus.bike.entity.RentOrder; import com.campus.bike.entity.User; import com.campus.bike.mapper.BikeMapper; import com.campus.bike.mapper.RentOrderMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; Service RequiredArgsConstructor public class OrderService { private final RentOrderMapper rentOrderMapper; private final BikeMapper bikeMapper; /** * 租车 */ Transactional(rollbackFor Exception.class) public RentOrder rentBike(Long userId, Long bikeId) { Bike bike bikeMapper.selectById(bikeId); if (bike null) { throw new BizException(车辆不存在); } if (bike.getStatus() ! 1) { throw new BizException(车辆当前不可租用); } Long activeCount rentOrderMapper.selectCount(new LambdaQueryWrapperRentOrder() .eq(RentOrder::getUserId, userId) .eq(RentOrder::getStatus, 1)); if (activeCount 0) { throw new BizException(您有未完成的订单不能继续租车); } // 条件更新只有 status1 时才能更新为 2 int updateRows bikeMapper.update(null, new LambdaUpdateWrapperBike() .eq(Bike::getId, bikeId) .eq(Bike::getStatus, 1) .set(Bike::getStatus, 2)); if (updateRows 0) { throw new BizException(车辆已被租用请刷新后重试); } RentOrder order new RentOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setBikeId(bikeId); order.setBikeNo(bike.getBikeNo()); order.setStartTime(LocalDateTime.now()); order.setStatus(1); rentOrderMapper.insert(order); return order; } private String generateOrderNo() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) UUID.randomUUID().toString().replace(-, ).substring(0, 6).toUpperCase(); } }这段代码的关键点是先查询车辆状态再通过条件更新将状态从可用改为骑行中。updateRows返回值如果为 0说明在查询和更新之间车辆状态已被其他事务修改此时不能创建订单。这个设计能有效避免并发下超卖比单纯的先查询再更新更可靠。事务注解Transactional(rollbackFor Exception.class)保证单车状态更新和订单创建要么同时成功要么同时回滚。如果订单插入失败也必须把车辆状态恢复成可用否则会出现车辆被占用但无订单的情况。6.3 还车接口实现还车时需要计算费用并回写订单。费用规则用“按固定时间单位计费并向上取整”的方式演示。假设单位时间为 30 分钟单价 1 元不足 30 分钟按 30 分钟计算。还车之后还要将单车数量、累计骑行次数等字段同步更新。// 文件路径src/main/java/com/campus/bike/service/OrderService.java 新增方法 Transactional(rollbackFor Exception.class) public RentOrder returnBike(Long orderId, String location) { RentOrder order rentOrderMapper.selectById(orderId); if (order null) { throw new BizException(订单不存在); } if (order.getStatus() ! 1) { throw new BizException(该订单已结束请勿重复还车); } LocalDateTime now LocalDateTime.now(); long duration java.time.Duration.between(order.getStartTime(), now).getSeconds(); // 计费规则30 分钟内 1 元超过按每 30 分钟 1 元累加分钟向上取整 long unitMinutes 30; long billUnits duration / (unitMinutes * 60); if (duration % (unitMinutes * 60) ! 0) { billUnits 1; } // 不足一个单位也要计费比如只骑了5分钟也需要按一个单位收费 if (billUnits 0) { billUnits 1; } java.math.BigDecimal amount java.math.BigDecimal.valueOf(billUnits); order.setEndTime(now); order.setDurationSeconds(duration); order.setAmount(amount); order.setStatus(2); rentOrderMapper.updateById(order); // 更新车辆状态 bikeMapper.update(null, new LambdaUpdateWrapperBike() .eq(Bike::getId, order.getBikeId()) .eq(Bike::getStatus, 2) .set(Bike::getStatus, 1) .set(Bike::getLocation, location) .setSql(total_ride_count total_ride_count 1) .setSql(total_ride_duration total_ride_duration duration)); return order; }还车时同样用条件更新保证单车只有处于“骑行中”状态时才能变回“可用”。如果用户重复提交还车请求第二次会因为订单状态已是 2 而直接抛出异常避免订单重复完结和费用被多次计算。6.4 用户认证与角色权限本项目如果使用 Spring Security配置会比较冗长。一个更轻量、也更适合课程设计的方案是使用 Sa-Token 或自研登录拦截器。自研 Token 登录适合理解原理用户登录成功后生成 UUID Token 存入 Redis 或数据库前端后续请求在 Header 中携带 Token后端拦截器解析 Token 获取用户信息。实际企业开发中更推荐 Spring Security JWT 或 Sa-Token。Sa-Token 的 API 对新手更友好文档中文齐全和 SpringBoot3 的整合也较简单。这里不做框架绑定只是提醒读者不要把用户信息直接放在前端 localStorage 就当作登录完成后端必须在每个需要鉴权的接口上校验身份和角色。为了兼顾演示简洁和代码可读性下面展示一个拦截器思路。登录后将 userId 和 role 放入 Token在 Controller 的 HandlerMethod 上通过自定义注解控制角色访问// 文件路径src/main/java/com/campus/bike/common/RequireRole.java package com.campus.bike.common; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int value(); }拦截器从请求头中取出 Token查询 Redis 或数据库得到用户信息。如果用户角色不匹配直接返回 403。前端即使露出按钮也无法调用没有权限的接口。7. Vue3 前端项目搭建与页面实现前端使用 Vite 创建工程UI 组件库选择 Element Plus。对管理系统来说最常见的页面布局是左侧菜单、右侧内容区。Vue Router 配置路由和菜单映射Pinia 负责保存用户 Token 和用户信息。7.1 创建项目与安装依赖npm create vitelatest campus-bike-web -- --template vue-ts cd campus-bike-web npm install npm install vue-router4 pinia element-plus axios创建完成后前端目录结构可以进行模块划分src/api存放所有后端接口请求。src/router路由配置。src/storePinia 状态管理。src/views页面组件。src/layout整体布局组件。结合热词“java: outofmemoryerror: insufficient memory”这里做一个额外提醒前端项目安装依赖时如果 Node 进程报出内存不足通常是因为全局包规模较大可以执行export NODE_OPTIONS--max-old-space-size2048Linux/macOS或set NODE_OPTIONS--max-old-space-size2048Windows后重试和 Java 常见的堆内存溢出思路类似都是先看运行时进程的可用内存。7.2 Axios 请求封装实际项目中不会在每个组件里重复写 axios 配置。通常会把 baseURL、超时时间、请求拦截器、响应拦截器统一封装在一个文件里。// 文件路径src/api/request.ts import axios from axios; import { ElMessage } from element-plus; import router from ../router; const request axios.create({ baseURL: http://localhost:8080/api, timeout: 10000, }); request.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization token; } return config; }); request.interceptors.response.use( (response) { const res response.data; if (res.code ! 200) { ElMessage.error(res.message || 请求失败); return Promise.reject(new Error(res.message || 请求失败)); } return res.data; }, (error) { ElMessage.error(error.response?.data?.message || 网络异常); return Promise.reject(error); } ); export default request;这里将后端返回结构中的 data 直接返回给页面组件减少了页面里的.data.data解构链但返回类型不再是 AxiosResponse因此 TypeScript 使用时需要做类型断言这属于工程实践层面的取舍。7.3 单车列表页面实现单车管理页面会展示表格数据支持按车辆编号搜索。用户登录后调用/bike/list接口获取数据并通过 ElTable 渲染。!-- 文件路径src/views/bike/BikeList.vue -- template div el-card el-form inline el-form-item label车辆编号 el-input v-modelquery.bikeNo placeholder请输入车辆编号 clearable / /el-form-item el-form-item el-button typeprimary clickloadData查询/el-button /el-form-item /el-form /el-card el-table :datatableData border stripe el-table-column propid labelID width80 / el-table-column propbikeNo label车辆编号 / el-table-column propbikeName label车辆名称 / el-table-column label状态 width120 template #default{ row } el-tag :typestatusMap[row.status]?.type {{ statusMap[row.status]?.text }} /el-tag /template /el-table-column el-table-column proplocation label停放位置 / el-table-column proptotalRideCount label累计租用次数 / /el-table /div /template script setup langts import { onMounted, reactive, ref } from vue; import request from ../../api/request; const query reactive({ bikeNo: }); const tableData ref([]); const statusMap: Recordnumber, { text: string; type: success | warning | danger | info } { 1: { text: 可用, type: success }, 2: { text: 骑行中, type: warning }, 3: { text: 维护中, type: danger }, 4: { text: 已下架, type: info }, }; const loadData async () { const data await request.get(/bike/list, { params: query }); tableData.value data; }; onMounted(loadData); /script如果读者用的是 Vue3 组合式 API script setup语法可以省去大量 Options API 的样板代码。Element Plus 的表格组件自带分页能力建议在后端做分页参数接收不要一次性把全表数据返回给前端。8. 核心业务接口实现与前后端交互租车和还车两个接口是整个系统最重要的交互点。下面完成后端 Controller 的编码并解释前端如何调用。8.1 租车 Controller// 文件路径src/main/java/com/campus/bike/controller/OrderController.java package com.campus.bike.controller; import com.campus.bike.common.Result; import com.campus.bike.entity.RentOrder; import com.campus.bike.service.OrderService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/order) RequiredArgsConstructor public class OrderController { private final OrderService orderService; PostMapping(/rent) public ResultRentOrder rent(RequestParam Long bikeId) { // 实际项目中 userId 应从登录态获取这里通过请求参数或拦截器注入 Long userId LoginContext.getUserId(); return Result.success(orderService.rentBike(userId, bikeId)); } PostMapping(/return) public ResultRentOrder returnBike(RequestParam Long orderId, RequestParam String location) { return Result.success(orderService.returnBike(orderId, location)); } }为了简化演示这里没有写完整的 LoginContext 实现。真实项目中登录拦截器将 userId 放入 ThreadLocalController 中可以直接获取避免每个接口都重复解析 Token。8.2 前端调用租车接口用户扫码或点击租车按钮后前端调用后端接口。核心逻辑是成功后提示用户同时刷新车辆列表。const rentBike async (bikeId: number) { await request.post(/order/rent, null, { params: { bikeId } }); ElMessage.success(租车成功); await loadData(); };这里使用 POST 请求但通过 params 传递参数Spring MVC 可以使用RequestParam接收。如果希望更符合 RESTful 风格可以把请求体封装成 DTO。两种方式都没问题关键是前后端约定保持一致。9. 项目联调与效果验证前后端代码完成后需要进行完整联调。建议的启动顺序是启动 MySQL执行建库建表 SQL。启动 SpringBoot 后端服务确认端口 8080 正常。启动 Vue3 前端开发服务器默认端口 5173。通过浏览器访问前端地址完成登录注册。一个值得推荐的验证方式是先通过 Swagger 或 Apifox 调通后端接口再通过页面验证。Swagger 在 SpringBoot3 中的依赖是springdoc-openapi-starter-webmvc-ui不会因为 SpringFox 与 SpringBoot3 不兼容而无法启动。9.1 预期效果管理员账号登录后可以看到车辆管理、订单查询、用户管理菜单。学生账号登录后可以看到校园车辆列表点击租车后车辆状态变为骑行中个人中心生成一条进行中订单。还车时需要填写停放位置或选择预设位置点击还车后订单状态变为已完成并显示骑行时长和费用金额。如果租车时选择了一辆已经被别人租走的车辆页面会弹出“车辆已被租用请刷新后重试”这就是后端条件更新生效的最直观验证结果。9.2 如何验证并发安全启动两个浏览器窗口用不同学生账号登录同时点击租用同一辆可用单车。理想情况下只有一个请求成功另一个得到错误提示。如果后端的租车 SQL 仍然是“先查状态再执行 update”且没有条件判断两个请求就可能同时成功。还可以通过查看 MySQL 日志或后端打印的 SQL 来确认两个请求是否分别执行了条件更新语句。update bike set status 2 where id ? and status 1这样的查询在第二个事务提交时返回影响行数为 0从而触发异常并回滚。9.3 常见联调问题前端请求后端接口出现跨域问题时常见处理方式是在后端添加 CORS 配置类。下面是一个简单配置// 文件路径src/main/java/com/campus/bike/config/CorsConfig.java package com.campus.bike.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }生产环境不建议直接开放allowedOrigins(*)而应限定为具体的前端域名。10. 常见问题与排查思路问题现象可能原因排查方式解决方案SpringBoot3 项目启动失败提示无效的源发行版本地 JDK 版本低于 17执行java -version查看 JDK 版本检查 IDEA 项目 SDK 配置安装 JDK17 并在 IDEA Project Structure 中切换 SDK启动时提示数据库连接失败MySQL 未启动、密码错误或数据库不存在先通过命令行mysql -u root -p测试连接再查看后端报错堆栈启动 MySQL创建数据库修正 application.yml 中的连接信息执行 SQL 创建表时提示字段名冲突字段使用了 MySQL 保留字如 order、user查看 SQL 报错位置将表名改为 rent_order、sys_user或使用反引号包裹前端请求接口跨域后端没有配置 CORS打开浏览器 F12 查看 Network 控制台报错添加 CorsConfig或在前端 Vite 中配置代理租车成功后车辆仍显示可用前端未刷新数据检查租车接口响应查看订单表是否有新增记录租车成功后调用 loadData 重新拉取列表还车后金额为负数或为 0时间计算或计费规则有误查看 startTime 和 endTime 是否读取正确统一使用 LocalDateTime前端的日期格式与后端保持一致同一车辆被并发租用没有使用条件更新或锁机制开启两个窗口并发测试租车使用update ... where id? and status1方式前端启动后页面空白Vue Router 或组件引入路径错误查看浏览器控制台报错检查路由配置和 import 路径大小写如果遇到 MySQL 连接时报Cant connect to local MySQL server through socket /tmp/mysql.sock在 Linux/macOS 上通常表示 MySQL 服务没有启动或 socket 文件路径不匹配。可以先执行systemctl status mysql查看服务状态或通过mysql -h 127.0.0.1 -P 3306 -u root -p强制走 TCP 连接确认服务是否真的存在。11. 最佳实践与工程建议11.1 统一异常处理后端不要只在 Service 层手动 try-catch。推荐使用RestControllerAdvice做全局异常处理。自定义业务异常BizException时返回 code 500参数校验异常返回 code 400系统异常返回 code 500 并记录日志。11.2 数据库变更要纳入版本管理开发过程中表结构一定会调整。不要只靠手写的 SQL 文件在多个环境之间传播建议引入 Flyway 或 Liquibase。对于课程设计至少也要保证项目仓库中有一份完整的 schema.sql并记录每次变更内容。11.3 密码与敏感信息处理用户密码必须使用 BCrypt 加密存储。数据库账号密码不要直接写死在公开配置中可以借助环境变量覆盖。在前端不要存储管理员账号密码也不要把接口详情暴露在打包代码中。11.4 日志与监控管理系统的订单操作属于关键业务应该在 Service 层记录操作日志包括用户ID、操作类型、订单编号、请求结果。不要依赖 System.out 打印来排查问题。生产环境至少使用 Logback 输出到文件并配置按天滚动和保留策略。11.5 前端权限动态化菜单权限可以写死在前端但更推荐的方案是后端返回当前用户拥有的菜单列表Vue Router 通过动态添加路由的方式生成侧边栏。这样当管理员新增菜单时不需要重新发布前端。12. 项目亮点总结与面试表达建议如果把这个项目写进简历要能从三个层面描述第一业务层面。说清楚系统为高校单车租赁提供了哪些能力如何管理学生用户、单车状态和订单流程。第二技术层面。说清楚技术栈是 SpringBoot3 Vue3 MySQL并说明为什么选择这些版本。强调在租车并发场景下使用了条件更新和事务控制确保同一车辆不会被重复租用。第三工程层面。说清楚自己实现了统一返回结果、全局异常处理、登录拦截器、角色权限校验、代码分层结构。如果做过数据库索引优化、前端路由懒加载甚至 Docker 部署都可以作为额外加分项。当面试官问“你这个项目有什么难点”时不要只说“实现了租车和还车功能”而要展开描述数据一致性这个切入点。例如当两个用户同时租同一辆车时如何保证只有一个人成功如果还车时计算费用和更新车辆状态不同步会有什么后果这种能体现数据库设计和并发控制意识的内容比作品本身更容易给面试官留下印象。13. 总结与后续优化方向本文以高校单车租赁管理系统为例完整演示了 SpringBoot3 Vue3 MySQL 前后端分离项目的核心开发链路。梳理了角色权限、单车状态流转、订单状态、计费规则给出了数据库建表 SQL 和关键租还车业务代码也解释了并发场景下使用条件更新保证数据一致性的必要性。如果希望继续深入可以把项目延伸到下面几个方向第一引入 Redis 缓存车辆热点数据降低数据库查询压力。第二用 RabbitMQ 或消息队列做还车后的订单异步通知模拟真实业务中的消息驱动。第三加入地图选点和还车区域校验通过经纬度判断用户是否在指定区域还车。第四使用 Docker Compose 将 MySQL、Redis、后端服务、前端站点一键编排部署。这套技术栈足够覆盖课设要求也足以作为学习 SpringBoot3 和 Vue3 的完整练手项目。如果你正在做类似的管理系统建议先把本文的数据库表和租还车流程跑通再逐步增加其他功能方向比速度更重要。