SpringBoot全栈实战:校园电动车租赁系统设计与核心代码解析
2026/9/5 12:01:23 网站建设 项目流程

简介:本资源是一套完整的高校电动车租赁系统毕业设计项目代码,面向计算机专业本科生及Java全栈初学者,聚焦校园场景下的共享出行服务开发实践。系统采用前后端分离架构,后端基于SpringBoot+MyBatisPlus构建RESTful接口,前端涵盖Vue管理后台与UniApp/微信小程序双端应用,完整覆盖用户管理、车辆调度、订单租赁、素材(图片/视频)维护等核心业务模块。压缩包共531个文件,含99个Java后端逻辑文件、78个Vue组件页、42个JPG/PNG素材图、41个JS交互脚本、15个CSS样式文件及配套SQL、配置与批处理脚本,总大小16.13MB,目录结构清晰,含完整论文章节(摘要至系统实现)、数据库设计及多端部署支持。目前已有237人学习下载,可直接运行调试,适合作为毕设参考、课程设计原型或SpringBoot+Vue全栈开发实战训练样本。

1. 项目缘起:从校园痛点到一个完整的租赁系统

最近在整理过往项目时,翻到了一个几年前为某高校做的电动车租赁系统。这个项目虽然不算复杂,但麻雀虽小五脏俱全,从需求分析、技术选型到部署上线,完整地走了一遍。当时校园里电动车乱停乱放、充电安全隐患、学生出行“最后一公里”不便等问题挺突出的,学校后勤部门就想搞一个线上租赁平台来规范管理。我接手后,基于当时最熟悉的SpringBoot技术栈,快速搭建了一套系统。今天正好有空,把这个项目的核心代码、设计思路以及踩过的坑,系统地梳理一遍,希望能给想做类似校园服务系统或者刚接触SpringBoot全栈开发的朋友一些参考。

这个系统本质上是一个B/S架构的Web应用,后台用Java+SpringBoot,前端当时用的还是JSP+Thymeleaf模板引擎(现在可能更流行Vue/React了)。核心功能围绕“租赁”展开:学生用户可以在线浏览可租车辆、预约租用、在线支付(集成第三方)、查看租赁记录;管理员则负责车辆入库、维护、计费规则设置、订单管理和数据统计。技术层面,它涵盖了SpringBoot Web开发、MyBatis数据持久化、Redis缓存、Spring Security权限控制、以及一些第三方API集成等常见企业级应用技术点。下面,我就分模块拆解一下这个系统的实现。

2. 技术选型与项目骨架搭建:为什么是SpringBoot全家桶?

做项目,技术选型是第一步,它直接决定了后续的开发效率和系统稳定性。当时选择SpringBoot作为核心框架,几乎是顺理成章的事情,原因有几个。

2.1 核心框架:SpringBoot的“约定大于配置”

对于高校内部系统这类业务逻辑不算极端复杂、但要求快速迭代和稳定部署的项目,SpringBoot是绝佳选择。它最大的优势在于“开箱即用”和极简的配置。我们不需要再像传统SSH/SSM框架那样,写一大堆繁琐的XML配置文件来整合Spring、Spring MVC和MyBatis。一个@SpringBootApplication注解标注的主类,内嵌了Tomcat服务器,直接就能跑起来。这对于开发、测试和部署环节都极大地提升了效率。当时我们团队人手紧张,SpringBoot能让我们更专注于业务逻辑本身,而不是框架的整合与配置。

2.2 数据持久层:MyBatis的灵活与可控

在ORM框架的选择上,我们放弃了全自动化的Hibernate,选择了半自动的MyBatis。主要考虑是电动车租赁业务涉及的查询并不都是简单的单表CRUD。比如,统计某个月份不同车型的出租率、查询某个学生的租赁历史及费用明细,这些SQL往往需要多表关联和特定的聚合函数。MyBatis允许我们直接编写和优化原生SQL,对于复杂查询的控制力更强,也更容易进行性能调优。同时,MyBatis-Generator插件可以帮我们自动生成实体类、Mapper接口和基础的XML映射文件,减少了大量重复的样板代码。

2.3 缓存与会话:引入Redis

租赁系统有一个典型场景:车辆状态(是否可租、电量、位置)是高频访问且需要快速更新的数据。如果每次查询都去读数据库,在并发稍高时数据库压力会很大。因此,我们引入了Redis作为缓存层。例如,将热门站点的车辆列表信息缓存到Redis,并设置合理的过期时间。用户登录后的会话信息(Session)我们也选择用Redis来分布式存储,这样后续如果做集群部署,会话共享问题就自然解决了。

2.4 安全与权限:Spring Security

系统涉及支付和用户隐私信息,安全必须重视。Spring Security提供了强大且灵活的安全控制能力。我们用它来实现基于角色的访问控制(RBAC):普通学生、维修员、财务管理员、系统超级管理员,各自拥有不同的菜单权限和操作权限。通过配置HttpSecurity,可以精细地控制哪些URL需要认证、哪些需要特定角色。用户密码我们使用BCryptPasswordEncoder进行加密存储,这是目前公认的安全做法。

2.5 其他关键依赖

  • 数据库连接池:使用HikariCP,它是SpringBoot 2.x后的默认连接池,性能非常好。
  • API文档:集成Swagger2(现为SpringDoc OpenAPI),自动生成RESTful API文档,前后端协作和测试非常方便。
  • 单元测试:JUnit 5 + Mockito,保证核心业务逻辑的可靠性。
  • 构建工具:Maven,管理项目依赖和构建生命周期。

注意:技术选型不是越新越好。当时Spring Cloud生态已经兴起,但对于这个单体的、内部使用的管理系统,引入微服务只会徒增复杂度。选择最适合当前团队和项目规模的技术栈,才是明智的。

3. 核心领域模型与数据库设计

任何系统的核心都是其数据模型。设计良好的数据库结构是项目成功的基石。我们围绕“用户”、“车辆”、“订单”这几个核心实体展开。

3.1 实体关系分析

  1. 用户 (User):包括学生和各类管理员。核心字段有学号/工号(作为登录账号)、密码(加密)、姓名、手机号、所属院系/部门、角色标识等。
  2. 车辆 (Bike):这是系统的核心资产。字段包括车辆编号(唯一)、车型、电池ID、当前电量、所属租赁点、状态(可租、租赁中、维修中、已报废)、购入时间、当前经纬度(如果支持GPS)等。
  3. 租赁点 (Station):校园内设置的固定停车和充电区域。字段包括站点ID、名称、位置描述、可停放容量、当前车辆数等。
  4. 订单 (Order):连接用户和车辆的纽带,是最重要的业务表。字段包括订单号、用户ID、车辆ID、租赁站点ID、归还站点ID、开始时间、结束时间、订单总金额、支付状态、订单状态(进行中、已完成、已取消)等。
  5. 支付记录 (Payment):与订单关联,记录支付流水。包括支付流水号、订单号、支付方式、支付金额、支付时间、第三方支付平台返回的交易号等。

它们之间的关系是:一个用户可以拥有多个订单,一个订单只属于一个用户;一辆车可以有多个订单(在不同时间段),一个订单只对应一辆车;一个租赁点可以停放多辆车,一辆车在某一时刻属于一个租赁点。

3.2 数据库表结构设计示例(部分关键表)

这里给出bike(车辆)表和rental_order(租赁订单)表的简化版DDL,以说明设计思路:

-- 车辆表 CREATE TABLE `bike` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `bike_sn` varchar(32) NOT NULL COMMENT '车辆唯一编号,如SN20240001', `model` varchar(50) NOT NULL COMMENT '车型', `battery_id` varchar(32) DEFAULT NULL COMMENT '电池编号', `power_level` int(3) DEFAULT 100 COMMENT '当前电量百分比', `station_id` bigint(20) DEFAULT NULL COMMENT '当前所在站点ID', `status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '状态:0-可租,1-租赁中,2-维修中,3-已报废', `gps_lng` decimal(10,7) DEFAULT NULL COMMENT '经度', `gps_lat` decimal(10,7) 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_sn` (`bike_sn`), KEY `idx_station_status` (`station_id`,`status`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电动车信息表'; -- 租赁订单表 CREATE TABLE `rental_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` varchar(32) NOT NULL COMMENT '订单号,业务唯一', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `bike_id` bigint(20) NOT NULL COMMENT '车辆ID', `start_station_id` bigint(20) NOT NULL COMMENT '取车站点ID', `end_station_id` bigint(20) DEFAULT NULL COMMENT '还车站点ID', `start_time` datetime NOT NULL COMMENT '实际开始时间', `end_time` datetime DEFAULT NULL COMMENT '实际结束时间', `estimated_duration` int(11) DEFAULT NULL COMMENT '预计租赁时长(分钟)', `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单总金额', `pay_status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '支付状态:0-未支付,1-已支付,2-已退款', `order_status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '订单状态:0-进行中,1-已完成,2-用户取消,3-系统取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_bike_id` (`bike_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租赁订单表';

设计要点解析:

  • 索引策略:在bike表上,我们建立了(station_id, status)的联合索引。因为最频繁的查询场景就是“查询某个站点下所有可租状态的车辆”。这个联合索引能极大提升查询效率。status的单列索引用于全局状态筛选。
  • 字段选择order_no(订单号)是业务唯一标识,通常由规则生成(如时间戳+随机数),用于对外展示和交互。id是主键,用于内部关联和分页。
  • 状态字段:使用tinyint表示状态,并在代码中定义枚举类,避免魔法数字。
  • 时间字段create_timeupdate_time是审计字段,便于问题追踪和数据统计。MySQL 5.6+支持CURRENT_TIMESTAMPON UPDATE CURRENT_TIMESTAMP自动更新。

4. 核心业务逻辑实现与代码剖析

有了清晰的数据模型,接下来就是实现业务逻辑。我挑几个最核心的流程来讲,包括车辆租赁、支付回调、以及车辆状态同步。

4.1 车辆租赁流程:事务与锁的运用

租赁是一个典型的分布式事务场景(虽然我们是单体应用,但涉及多个数据表的更新),必须保证原子性。核心步骤如下:

  1. 用户选择车辆,提交租赁请求。
  2. 后端校验车辆状态是否为“可租”。
  3. 生成订单,状态为“进行中”。
  4. 更新车辆状态为“租赁中”。
  5. 调用第三方支付预下单。

这里的关键是,步骤2到步骤4必须在同一个数据库事务中,并且要对选中的车辆记录加锁,防止超租。我们使用Spring的@Transactional注解来管理事务,并在查询车辆时使用SELECT ... FOR UPDATE进行悲观锁。

@Service @Slf4j public class RentalServiceImpl implements RentalService { @Autowired private BikeMapper bikeMapper; @Autowired private OrderMapper orderMapper; @Override @Transactional(rollbackFor = Exception.class) // 开启事务 public OrderDTO startRental(Long userId, Long bikeId, Long stationId) { // 1. 悲观锁锁定车辆记录 Bike bike = bikeMapper.selectForUpdate(bikeId); // 对应的SQL是 SELECT * FROM bike WHERE id = #{id} FOR UPDATE if (bike == null) { throw new BizException("车辆不存在"); } if (!BikeStatus.AVAILABLE.getCode().equals(bike.getStatus())) { throw new BizException("车辆当前不可租"); } // 2. 生成订单 RentalOrder order = new RentalOrder(); order.setOrderNo(OrderNoGenerator.generate()); // 订单号生成器 order.setUserId(userId); order.setBikeId(bikeId); order.setStartStationId(stationId); order.setOrderStatus(OrderStatus.ONGOING.getCode()); order.setPayStatus(PayStatus.UNPAID.getCode()); orderMapper.insert(order); // 3. 更新车辆状态 bike.setStatus(BikeStatus.RENTED.getCode()); bike.setUpdateTime(new Date()); bikeMapper.updateByPrimaryKeySelective(bike); // 选择性更新,避免覆盖未修改字段 log.info("用户{}成功租赁车辆{},订单号:{}", userId, bikeId, order.getOrderNo()); // 4. 组装DTO返回,后续前端引导支付 return convertToDTO(order, bike); } }

踩坑提示:这里SELECT ... FOR UPDATE会在事务提交前一直持有行锁。如果后续支付接口调用耗时很长,会导致该车辆记录被长时间锁定,影响并发。我们的优化方案是:在车辆状态更新后,立即提交事务,释放锁。支付流程通过消息队列或异步任务来处理,即使支付失败,也有后续的补偿机制(如定时任务检查超时未支付订单并自动取消)来更新车辆状态。这实际上是将“租赁”这个长事务拆分为“占位”短事务和“支付”异步任务。

4.2 支付回调处理:幂等性与安全性

我们接入了支付宝的当面付接口。用户扫码支付后,支付宝服务器会异步通知我们的回调接口。处理回调是重中之重,必须做到幂等安全

@RestController @RequestMapping("/api/payment") @Slf4j public class PaymentCallbackController { @Autowired private PaymentService paymentService; @PostMapping("/alipay/notify") public String alipayNotify(HttpServletRequest request) { // 1. 参数验签:验证请求是否来自支付宝,防止伪造通知 Map<String, String> params = convertRequestToMap(request); boolean signVerified = AlipaySignature.rsaCheckV1(params, ALIPAY_PUBLIC_KEY, "UTF-8", "RSA2"); if (!signVerified) { log.error("支付宝回调验签失败, params: {}", params); return "failure"; } // 2. 验证通知参数 String tradeStatus = params.get("trade_status"); String outTradeNo = params.get("out_trade_no"); // 我们的订单号 String tradeNo = params.get("trade_no"); // 支付宝交易号 if (!"TRADE_SUCCESS".equals(tradeStatus)) { log.warn("收到非成功交易状态回调: {}", tradeStatus); return "success"; // 仍需返回success,支付宝才不会重复发送 } // 3. 业务处理:更新订单支付状态 try { paymentService.handlePaymentSuccess(outTradeNo, tradeNo, params); } catch (Exception e) { log.error("处理支付成功回调业务异常, outTradeNo: {}", outTradeNo, e); // 此处不能返回failure,否则支付宝会重试。应记录异常,通过人工或定时任务补偿。 // 我们记录了详细的错误日志和通知参数,便于后续排查。 } // 4. 返回成功标识 return "success"; } // 业务服务层方法,保证幂等 @Service public class PaymentServiceImpl implements PaymentService { @Transactional(rollbackFor = Exception.class) public void handlePaymentSuccess(String orderNo, String alipayTradeNo, Map<String, String> params) { // 先查询订单当前支付状态 RentalOrder order = orderMapper.selectByOrderNo(orderNo); if (order == null) { log.error("订单不存在: {}", orderNo); throw new BizException("订单不存在"); } // 幂等处理:如果已经是已支付状态,直接返回,不做重复操作 if (PayStatus.PAID.getCode().equals(order.getPayStatus())) { log.info("订单{}已支付,跳过重复处理", orderNo); return; } // 更新订单支付状态 order.setPayStatus(PayStatus.PAID.getCode()); order.setUpdateTime(new Date()); orderMapper.updateByPrimaryKeySelective(order); // 插入支付记录(同样需要幂等,可根据支付宝交易号做唯一约束) PaymentRecord record = new PaymentRecord(); record.setOrderNo(orderNo); record.setAlipayTradeNo(alipayTradeNo); // ... 设置其他字段 paymentRecordMapper.insert(record); // 可能触发其他业务:发送租赁成功短信、更新用户权益等 // ... } } }

关键点:

  • 验签:这是安全底线,确保回调来自可信的支付平台。
  • 幂等:通过先查询再判断状态来实现。更严谨的做法是在数据库层为alipay_trade_no(支付宝交易号)加唯一索引,从根源防止重复记录。
  • 异常处理:回调接口必须捕获所有异常,并在绝大多数情况下返回success(或支付平台规定的成功字符串),否则支付平台会认为通知失败而不断重试。真正的业务异常需要记录日志并通过其他机制补偿。
  • 日志:支付相关的所有操作,尤其是回调参数和业务处理结果,必须详细记录,这是线上排查问题的唯一依据。

4.3 车辆状态同步:缓存与定时任务

车辆状态(特别是电量、位置)需要准实时展示给用户。我们采用“缓存为主,数据库为辅,定时同步”的策略。

  1. Redis数据结构设计:我们为每辆在线车辆在Redis中维护一个Hash结构。

    key: bike:status:{bikeId} value: { “power”: 85, “lng”: 116.403847, “lat”: 39.915526, “status”: 0, “lastUpdate”: 1672531200000 }

    当用户通过App扫码开锁时,车载IoT设备会上报最新状态到后端一个接口,该接口直接更新Redis中对应的Hash。

  2. 前端查询:用户在地图上查看车辆或列表时,后端接口直接从Redis读取状态信息,响应速度极快(毫秒级)。

  3. 数据持久化:我们有一个每5分钟执行一次的Spring Scheduled定时任务。这个任务遍历Redis中所有的bike:status:*键,将电量、位置信息批量更新到数据库的bike表中。这样既保证了前端体验的流畅性,又将数据库的写压力从高频的IoT上报中解耦出来,变成了低频的批量操作。

@Component @Slf4j public class BikeStatusSyncTask { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private BikeMapper bikeMapper; @Scheduled(cron = "0 */5 * * * ?") // 每5分钟执行一次 public void syncBikeStatusToDB() { log.info("开始同步车辆状态到数据库..."); Set<String> keys = redisTemplate.keys("bike:status:*"); if (CollectionUtils.isEmpty(keys)) { return; } List<Bike> updateList = new ArrayList<>(); for (String key : keys) { Map<Object, Object> entries = redisTemplate.opsForHash().entries(key); if (!entries.isEmpty()) { Long bikeId = extractBikeIdFromKey(key); // 从key中解析出车辆ID Bike bike = new Bike(); bike.setId(bikeId); bike.setPowerLevel((Integer) entries.get("power")); bike.setGpsLng(new BigDecimal((String)entries.get("lng"))); bike.setGpsLat(new BigDecimal((String)entries.get("lat"))); // 注意:这里不更新status,因为车辆的业务状态(可租/租赁中)由订单流程控制,IoT只上报物理状态。 updateList.add(bike); } } if (!updateList.isEmpty()) { // 批量更新,提升效率 bikeMapper.batchUpdateStatus(updateList); } log.info("车辆状态同步完成,共处理{}条记录", updateList.size()); } }

实操心得:这种缓存+定时落地的模式,在物联网、状态频繁更新的场景下非常实用。但要注意缓存数据的过期和清理策略。我们为每个状态Key设置了TTL(例如1小时),防止因设备异常离线导致Redis中残留过时的“僵尸”数据。定时任务在同步后,也会删除那些超过一定时间未更新的Key。

5. 系统安全与防御实践

校园系统直接面向学生,虽然流量不如互联网应用,但安全一点都不能马虎。除了前面提到的支付安全,我们还做了以下几方面的工作。

5.1 接口防刷与限流

租赁和查询接口容易被脚本频繁调用。我们使用Spring Boot整合Guava RateLimiter或Redis来实现简单的限流。

@Aspect @Component public class RateLimitAspect { @Autowired private RedisTemplate<String, Integer> redisTemplate; // 定义限流注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { String key(); // 限流key,如 'rent:start:{userId}' int limit(); // 时间窗口内最大请求数 int timeout(); // 时间窗口,秒 } @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String userId = getCurrentUserId(); // 从会话或token中获取用户ID String redisKey = rateLimit.key().replace("{userId}", userId); // 使用Redis实现滑动窗口计数 Long current = redisTemplate.opsForValue().increment(redisKey, 1); if (current == 1) { // 第一次访问,设置过期时间 redisTemplate.expire(redisKey, rateLimit.timeout(), TimeUnit.SECONDS); } if (current > rateLimit.limit()) { throw new BizException("操作过于频繁,请稍后再试"); } return joinPoint.proceed(); } } // 在Controller方法上使用 @PostMapping("/start") @RateLimit(key = "rent:start:{userId}", limit = 5, timeout = 60) // 同一用户60秒内最多发起5次租赁请求 public Result startRental(@RequestBody RentalRequest request) { // ... }

5.2 SQL注入与XSS防护

  • SQL注入:坚持使用MyBatis的#{}预编译占位符,绝不拼接SQL字符串。MyBatis会将#{}替换为?,然后由数据库驱动进行参数化查询,从根本上杜绝注入。
  • XSS防护:对于前端提交的富文本内容(如意见反馈),我们使用了Jsoup库进行白名单过滤。对于普通的文本显示,在Thymeleaf模板中,使用th:text(自动转义)而非th:utext(不转义)来输出动态内容。
// 使用Jsoup进行HTML过滤 public String cleanHtml(String html) { if (StringUtils.isBlank(html)) { return html; } // 定义允许的标签和属性白名单 Whitelist whitelist = Whitelist.basicWithImages() .addAttributes("a", "href", "title", "target") // 允许的标签和属性 .addProtocols("a", "href", "http", "https"); return Jsoup.clean(html, whitelist); }

5.3 敏感数据脱敏与日志安全

用户的手机号、身份证号在日志和返回给前端的数据中必须脱敏。我们通过Jackson的JsonSerializer自定义序列化规则,在返回JSON时自动处理。

public class SensitiveInfoSerializer extends JsonSerializer<String> { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (StringUtils.isBlank(value)) { gen.writeString(value); return; } // 手机号脱敏:138****1234 if (value.matches("^1[3-9]\\d{9}$")) { gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")); } // 其他敏感信息处理... else { gen.writeString(value); } } } // 在实体类字段上使用 public class UserDTO { @JsonSerialize(using = SensitiveInfoSerializer.class) private String phone; // ... }

同时,在打印日志时,要避免直接打印完整的敏感信息、支付参数等。

6. 部署上线与监控排查

项目开发完,部署是临门一脚。我们采用了当时比较主流的“GitLab CI/CD + Docker + 单台云服务器”的部署方式。

6.1 持续集成与部署流水线

gitlab-ci.yml中定义了几个阶段:

  1. build:使用Maven打包,生成可执行的jar文件。
  2. test:运行单元测试和集成测试(如果测试失败,流水线中止)。
  3. docker-build:将jar包和Dockerfile一起构建成Docker镜像,推送到私有的Docker Registry。
  4. deploy:通过SSH登录到生产服务器,拉取最新镜像,停止旧容器,启动新容器。
# Dockerfile FROM openjdk:8-jre-alpine VOLUME /tmp COPY target/campus-bike-rental-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

6.2 生产环境配置与启动

使用application-prod.yml文件管理生产环境配置,通过--spring.profiles.active=prod激活。关键配置包括:

  • 数据库连接池参数(增大最大连接数)。
  • Redis连接信息。
  • 日志级别调整为WARNERROR,并配置日志滚动策略(使用Logback)。
  • 关闭Swagger等开发工具。

启动命令示例:java -Xms512m -Xmx1024m -jar app.jar --spring.profiles.active=prod > /dev/null 2>&1 &

6.3 基础监控与日志排查

对于小型项目,我们没上全套的APM,但做了最基础的监控:

  • Spring Boot Actuator:启用health,metrics,info端点,配合简单的脚本检查应用健康状态。
  • 日志:这是最重要的排查工具。我们按照业务模块划分日志文件,并使用ELK(Elasticsearch, Logstash, Kibana)的简化版——其实就用了Filebeat将日志收集到一台集中的服务器上,方便搜索。在代码中关键分支(如订单状态变更、支付回调、异常捕获)都打了详细的日志。
  • 数据库慢查询:在MySQL中开启慢查询日志,定期分析,优化索引。

6.4 遇到的一个典型问题:数据库连接池耗尽

上线初期,在午间高峰时段,偶尔会出现“Cannot get a connection, pool error”的异常。排查过程如下:

  1. 看日志:发现错误日志集中在几个特定的、涉及复杂报表查询的Controller。
  2. 分析代码:检查这些接口,发现其中有一个查询,会循环调用多个Mapper方法,而每个方法都是一个独立的数据库查询,且没有使用@Transactional,导致每个查询都会从连接池获取一个新连接,用完后才释放。在并发高时,连接很快被占满。
  3. 使用监控:通过Actuator的/actuator/metrics/hikaricp.connections.active端点,观察到活动连接数在高峰时确实接近配置的最大值。
  4. 解决方案
    • 短期:优化那个循环查询的代码,改为通过一个更复杂的SQL语句,一次查询出所有需要的数据,减少数据库交互次数。
    • 长期:调整HikariCP配置,适当增大maximumPoolSize(但不宜过大,一般建议在(核心数 * 2) + 磁盘数左右),并设置合理的connectionTimeoutidleTimeout。同时,为所有只读的复杂查询,在Service方法上添加@Transactional(readOnly = true),这能给数据库驱动一些优化提示。

这个坑让我深刻体会到,连接池不是越大越好,不合理的业务代码才是根源。优化代码逻辑往往比调大连接池参数更有效。

7. 项目总结与可扩展性思考

回顾这个高校电动车租赁系统,它很好地完成了从0到1的搭建,稳定运行了几年。技术栈的选择(SpringBoot + MyBatis + Redis)对于此类管理型系统来说是非常成熟和高效的组合。项目中的事务控制、缓存设计、支付集成、安全防护等模块,都具有普遍的参考价值。

如果这个系统需要进一步发展,我觉得可以从以下几个方向考虑扩展:

  1. 微服务化拆分:如果业务量增长,可以考虑将“用户中心”、“车辆IoT接入”、“订单交易”、“支付清算”拆分成独立的微服务,通过Spring Cloud Alibaba(Nacos, Sentinel, Seata)进行治理,提升系统弹性和开发并行度。
  2. 引入消息队列:将“支付成功通知用户”、“车辆状态更新”等非核心、可异步的操作通过RabbitMQ或RocketMQ解耦,提升主流程的响应速度。
  3. 更智能的调度:结合历史订单数据,利用简单的机器学习模型预测各站点在不同时段的车辆需求,实现智能调度提示,甚至自动派发运维任务去平衡车辆分布。
  4. 多端适配:当时主要面向Web和H5。现在可以开发更体验的原生App或小程序,并利用蓝牙或NFC实现更便捷的开关锁流程。

做项目,尤其是这种业务驱动型的项目,技术是为业务服务的。最开始可能只是一个简单的CRUD系统,但在解决一个个具体问题的过程中,你会自然地去学习和应用缓存、队列、安全、监控等更深的知识。这个电动车租赁系统对我来说,就是这样一个很好的练手和成长的项目。希望我的这些代码片段和思路拆解,能帮你少走一些弯路。如果你在实现类似系统时遇到问题,欢迎交流讨论。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询