1. 项目概述:基于SpringBoot与MyBatis的车辆租赁管理系统
去年帮朋友公司改造旧系统时,发现他们还在用Excel管理200多台车辆的租赁记录。每次对账都要3个财务加班到凌晨,这种场景让我决定开发一套轻量级租赁管理系统。本文分享的正是这个实战项目——采用SpringBoot+MyBatis技术栈的车辆租赁信息管理平台,包含从技术选型到部署上线的完整过程。
这个系统主要解决三类核心问题:
- 车辆状态实时追踪:通过车牌号可秒查车辆当前租赁状态、历史记录及维修情况
- 合同自动化管理:电子合同生成、到期自动提醒、违约金计算等功能
- 多维度数据统计:按车型/地区/时间等维度分析租赁收益和车辆利用率
提示:系统采用约定优于配置的设计理念,80%的基础功能通过SpringBoot Starter实现开箱即用,特别适合需要快速搭建业务系统的中小型租赁公司。
2. 核心技术选型解析
2.1 为什么选择SpringBoot+MyBatis组合
在技术选型阶段,我们对比了三种主流方案:
| 技术组合 | 开发效率 | 性能表现 | 学习成本 | 生态支持 |
|---|---|---|---|---|
| SpringBoot+MyBatis | ★★★★★ | ★★★★ | ★★★ | ★★★★★ |
| SpringCloud+JPA | ★★★ | ★★★★ | ★★ | ★★★★ |
| Django ORM | ★★★★ | ★★★ | ★★★★★ | ★★★ |
最终选择SpringBoot+MyBatis主要基于以下考量:
- 快速迭代需求:SpringBoot的自动配置特性让开发人员能专注于业务逻辑
- 复杂SQL处理:汽车租赁涉及多表关联查询(如车辆-合同-客户关系),MyBatis的动态SQL更灵活
- 历史兼容性:旧系统有大量存储过程,MyBatis支持直接调用
2.2 MyBatis的深度优化实践
在车辆批量入库场景下,我们发现原生MyBatis的批量插入性能较差。通过以下优化手段将1000条车辆数据的插入时间从12秒降至0.8秒:
java复制// 采用BatchExecutor并调整JDBC参数
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
Properties props = new Properties();
props.setProperty("jdbc.batch.size", "500"); // 每500条提交一次
props.setProperty("rewriteBatchedStatements", "true"); // MySQL批处理优化
sessionFactory.setConfigurationProperties(props);
return sessionFactory.getObject();
}
注意:rewriteBatchedStatements参数需要MySQL Connector/J 5.1.13+版本支持
3. 系统核心模块实现
3.1 车辆状态机设计
租赁车辆有6种状态转换关系(空闲→预定→租赁中→维修中→停用→报废),我们采用状态模式实现:
java复制public interface VehicleState {
void reserve(Vehicle vehicle);
void rent(Vehicle vehicle);
void returnVehicle(Vehicle vehicle);
void maintenance(Vehicle vehicle);
}
// 具体状态实现
public class AvailableState implements VehicleState {
@Override
public void reserve(Vehicle vehicle) {
vehicle.setState(new ReservedState());
// 写入预定日志
logService.addLog(vehicle.getId(), "状态变更:空闲 → 预定");
}
// 其他方法抛出IllegalStateException...
}
3.2 动态定价策略实现
根据市场需求动态调整租金是系统的核心业务逻辑。我们采用策略模式+规则引擎:
java复制public interface PricingStrategy {
BigDecimal calculatePrice(LocalDate date, VehicleType type);
}
// 旺季定价策略
public class PeakSeasonStrategy implements PricingStrategy {
private static final Map<VehicleType, BigDecimal> COEFFICIENTS = Map.of(
VehicleType.ECONOMY, new BigDecimal("1.2"),
VehicleType.LUXURY, new BigDecimal("1.5")
);
@Override
public BigDecimal calculatePrice(LocalDate date, VehicleType type) {
BigDecimal basePrice = priceService.getBasePrice(type);
return basePrice.multiply(COEFFICIENTS.get(type));
}
}
在Spring中通过@Conditional实现策略自动切换:
java复制@Bean
@ConditionalOnExpression("#{T(java.time.LocalDate).now().getMonthValue() in {5,6,7,8,10}}")
public PricingStrategy peakSeasonStrategy() {
return new PeakSeasonStrategy();
}
4. 典型问题排查实录
4.1 MyBatis一对多查询性能优化
在实现"车辆+所有租赁记录"查询时,初期采用嵌套结果映射导致N+1查询问题:
xml复制<!-- 错误示范 -->
<resultMap id="vehicleWithRentals" type="Vehicle">
<collection property="rentalRecords" ofType="RentalRecord"
select="selectRentalsByVehicleId" column="id"/>
</resultMap>
优化方案:
- 使用join查询+结果集映射
- 启用MyBatis二级缓存
- 对分页查询添加@Transactional(readOnly=true)
最终优化后的XML配置:
xml复制<resultMap id="vehicleWithRentals" type="Vehicle">
<id property="id" column="v_id"/>
<result property="plateNumber" column="v_plate"/>
<!-- 其他车辆字段 -->
<collection property="rentalRecords" ofType="RentalRecord">
<id property="id" column="r_id"/>
<result property="startDate" column="r_start_date"/>
<!-- 其他租赁记录字段 -->
</collection>
</resultMap>
<select id="selectVehicleWithRentals" resultMap="vehicleWithRentals">
SELECT v.id as v_id, v.plate_number as v_plate,
r.id as r_id, r.start_date as r_start_date
FROM vehicle v LEFT JOIN rental_record r ON v.id = r.vehicle_id
WHERE v.id = #{id}
</select>
4.2 日期重叠校验陷阱
车辆租赁中最易出错的业务逻辑是日期冲突校验。初期采用简单SQL导致边界条件错误:
sql复制-- 错误方案:无法处理起始日期相等的情况
SELECT COUNT(*) FROM rental_record
WHERE vehicle_id = #{vehicleId}
AND end_date > #{startDate}
AND start_date < #{endDate}
正确做法应包含四种边界条件判断:
java复制public boolean isPeriodAvailable(Long vehicleId, LocalDate start, LocalDate end) {
return rentalMapper.countOverlaps(vehicleId,
start.minusDays(1), // 包含当天租车情况
end.plusDays(1)) == 0;
}
<!-- MyBatis映射 -->
<select id="countOverlaps" resultType="int">
SELECT COUNT(*) FROM rental_record
WHERE vehicle_id = #{vehicleId}
AND (
(start_date BETWEEN #{adjustedStart} AND #{adjustedEnd}) OR
(end_date BETWEEN #{adjustedStart} AND #{adjustedEnd}) OR
(start_date <= #{adjustedStart} AND end_date >= #{adjustedEnd})
)
</select>
5. 部署与监控方案
5.1 多环境配置管理
通过Spring Profiles实现开发/测试/生产环境隔离:
yaml复制# application-dev.yml
spring:
datasource:
url: jdbc:h2:mem:rental_dev
username: sa
password: ""
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db:3306/rental?useSSL=false
username: ${DB_USER}
password: ${DB_PASSWORD}
redis:
host: redis-cluster
启动时通过VM参数指定环境:
bash复制java -jar rental-system.jar --spring.profiles.active=prod
5.2 健康检查与Metrics
集成Spring Boot Actuator暴露关键指标:
java复制@Configuration
public class ActuatorConfig {
@Bean
public MeterBinders vehicleMetrics(VehicleRepository repository) {
return registry -> Gauge.builder("vehicles.count", repository::count)
.description("当前车辆总数")
.register(registry);
}
}
访问端点示例:
/actuator/health系统健康状态/actuator/metrics/vehicles.count当前车辆数量/actuator/prometheusPrometheus格式指标
6. 开发经验总结
在三个月开发周期中,有几个关键决策显著提升了项目质量:
-
领域驱动设计:早期花2周时间与业务方梳理出车辆、合同、客户三个核心领域,避免后期频繁重构
-
契约测试:采用Spring Cloud Contract确保前端与后端接口的一致性,减少联调时间
-
增量式缓存:对车辆列表采用分级缓存策略
- 第一层:Redis缓存热门车型(TTL 5分钟)
- 第二层:Caffeine本地缓存全量数据(TTL 1小时)
- 缓存击穿防护:采用BloomFilter过滤无效查询
对于想借鉴本项目的开发者,建议特别注意车辆状态变更的并发控制。我们最终采用乐观锁+重试机制:
java复制@Transactional
public RentalRecord rentVehicle(Long vehicleId, RentalRequest request) {
Vehicle vehicle = vehicleRepository.findById(vehicleId)
.orElseThrow(() -> new VehicleNotFoundException(vehicleId));
// 乐观锁验证
if (vehicle.getStatus() != VehicleStatus.AVAILABLE) {
throw new IllegalVehicleStateException();
}
// 使用版本号防止并发修改
int updated = vehicleRepository.updateStatus(
vehicleId,
VehicleStatus.AVAILABLE,
VehicleStatus.RENTED,
vehicle.getVersion());
if (updated == 0) {
throw new ConcurrentModificationException();
}
// 创建租赁记录...
}
