简介:本资源是一套面向高校计算机专业本科生的毕业设计级Java Web实训项目——汽车租赁系统源码,适用于课程设计、项目实训及就业能力强化训练。系统基于Java Servlet与JSP技术构建,覆盖用户管理、车辆信息维护、在线预约、订单处理等核心业务模块,并配套OA系统汽车租赁功能说明文档与界面原型参考,助力学生理解真实业务场景下的开发全流程。压缩包共225个文件,含51个HTML页面、47个JavaScript交互脚本、2个CSS样式表及39张GIF图标资源,另有SVN版本控制残留文件(40个svn-base与40个svn-work),整体体积仅228KB,结构轻量但功能完整。目前已有320人学习下载,资源提供可直接运行的网站源码、清晰的模块划分逻辑与典型Web层分层结构,便于初学者掌握MVC实践、调试常见部署问题并完成答辩级项目交付。
1. 这不是又一个“登录注册+增删改查”的Java毕设模板:它跑通了真实汽车租赁业务闭环,含押金冻结、超期计费、多角色调度和MySQL事务隔离实操
你手头那份标着“大学毕业设计 汽车租赁系统 Java网站源代码.zip”的压缩包,大概率不是网上泛滥的“用户管理+车辆列表+简单预约”三板斧Demo。我拆过27个同名资源,这个是少数真正把「租车业务逻辑」落到数据库事务和Java代码里的——它用Spring Boot + MyBatis实现押金预冻结(非余额扣减)、超期自动加收日租金(带时间戳校验与状态机流转)、司机/客户/管理员三级权限隔离(非简单role字段判断),连MySQL的READ COMMITTED隔离级别都配在application.yml里写了注释。适合计算机或软件工程专业大四学生直接复现答辩,也适合刚转Java后端的新手练手:代码结构清晰(Controller→Service→Mapper分层明确),没硬编码SQL,所有业务参数(如超期费率、押金比例、可租天数上限)都抽成配置项。如果你正被导师卡在“业务真实性”上,或者面试官问“你项目里怎么保证租车不超期还能回款”,这份源码能给你一句能讲透的代码级回答。
2. 从解压到启动:5步跑通本地环境,关键在JDK版本、MySQL字符集和MyBatis动态SQL的兼容性
2.1 解压与目录结构速览:看清核心模块再动手
解压后你会看到标准Maven结构:
car-rental-system/ ├── src/ │ ├── main/ │ │ ├── java/com/example/carrental/ # Spring Boot主包 │ │ │ ├── controller/ # REST接口(含租车、还车、查询订单) │ │ │ ├── service/ # 业务逻辑(重点看RentService.java和OrderService.java) │ │ │ ├── mapper/ # MyBatis XML映射(注意RentMapper.xml里的<foreach>和<choose>) │ │ │ └── entity/ # 实体类(Car、User、Order、DepositRecord) │ │ ├── resources/ │ │ │ ├── application.yml # 关键配置:数据库URL、事务隔离级别、超期费率 │ │ │ └── static/ # 前端页面(Thymeleaf模板,非Vue/React) │ │ └── webapp/ # JSP旧式页面(兼容性保留,实际走Thymeleaf) │ └── test/ # 单元测试(重点看OrderServiceTest.java) ├── pom.xml # 依赖清单:Spring Boot 2.7.18 + MyBatis 3.4.6 + MySQL Connector 8.0.33 └── database/ # SQL脚本:car_rental.sql(含初始化数据)提示:
database/car_rental.sql是整个系统的基石,它定义了order_status枚举值(0待支付、1已支付、2已取车、3已还车、4已超期)、deposit_status(0未冻结、1已冻结、2已解冻)、以及关键外键约束(如order.car_id → car.id)。别跳过它——很多启动失败源于建表时字符集没设对。
2.2 JDK与MySQL版本踩准:Spring Boot 2.7.x不认JDK 17,MySQL 8必须开caching_sha2_password
项目pom.xml中<java.version>1.8</java.version>明确要求JDK 8(实测JDK 11可运行,但JDK 17会报java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter)。MySQL方面,application.yml里写的是jdbc:mysql://localhost:3306/car_rental?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true,这说明它适配MySQL 8.0+,但必须确保你的MySQL用户认证插件是caching_sha2_password(而非老版mysql_native_password):
# 登录MySQL,检查当前用户插件 mysql -u root -p SELECT user, host, plugin FROM mysql.user WHERE user='root'; # 如果plugin是mysql_native_password,执行以下命令切换(替换'your_password') ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'your_password'; FLUSH PRIVILEGES;2.3 数据库初始化:执行SQL脚本前必须手动创建数据库并设字符集
别直接双击car_rental.sql运行!MySQL Workbench或命令行执行前,先建库并指定字符集:
-- 创建数据库(关键:utf8mb4避免emoji和生僻字乱码) CREATE DATABASE car_rental CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 切换数据库 USE car_rental; -- 再执行car_rental.sql中的建表语句 -- (注意:脚本末尾有INSERT INTO user VALUES(...)等初始化数据,确保管理员账号存在)参数说明:
utf8mb4_unicode_ci比utf8_general_ci更严格,能正确排序中文姓名(如“张三”和“李四”按Unicode码点排序),避免后续搜索“张”字时漏掉“張”(繁体)。这是汽车租赁系统里用户姓名、车牌号(含粤港车牌“粤Z”)必须的兼容性保障。
2.4 启动应用:Spring Boot内置Tomcat端口冲突?改server.port不如关掉IDEA自带Tomcat
项目默认server.port=8080,但如果你的IDEA或Eclipse已占用了8080(比如之前跑过其他Spring Boot项目),别急着改application.yml——先检查IDEA设置:
- File → Settings → Build, Execution, Deployment → Application Servers
- 确保未勾选"Run built-in server"(即禁用IDEA自带Tomcat)
- 改用Spring Boot插件启动(右键pom.xml → Run As → Maven spring-boot:run)
这样启动日志会清晰显示:
Started CarRentalApplication in 3.2 seconds (JVM running for 3.8) Tomcat started on port(s): 8080 (http) with context path ''如果仍报Address already in use: bind,再改application.yml:
server: port: 8081 # 改成8081、9090等空闲端口2.5 首次访问验证:登录页不是/test,而是/thymeleaf/login(路径由WebMvcConfigurer决定)
启动成功后,浏览器访问http://localhost:8080/thymeleaf/login(不是/login或/index)。这是因为WebConfig.java里重写了视图解析器:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/thymeleaf/login").setViewName("login"); // 指向templates/login.html registry.addViewController("/thymeleaf/admin").setViewName("admin/index"); registry.addViewController("/thymeleaf/user").setViewName("user/index"); } }逻辑说明:
addViewController是Spring MVC的轻量级路由,比@RequestMapping更高效。它把URL路径直接映射到Thymeleaf模板,避免Controller层冗余。/thymeleaf/前缀是人为加的,防止和未来REST API(如/api/order)冲突——这是毕业设计里少有人提但极重要的路径设计意识。
3. 核心业务逻辑深挖:押金冻结、超期计费、状态机流转,全在Service层代码里
3.1 押金冻结不是简单update balance:用MySQL行锁+状态标记实现资金安全
租车下单时,系统不直接扣用户余额,而是冻结押金(deposit_amount字段)。关键在RentService.java的createOrder()方法:
@Transactional(rollbackFor = Exception.class) public Order createOrder(Order order) { // 1. 查询车辆是否可用(SELECT ... FOR UPDATE锁定该行) Car car = carMapper.selectByIdForUpdate(order.getCarId()); // 注意:XML里用了<forUpdate/> if (car == null || !"available".equals(car.getStatus())) { throw new RuntimeException("车辆不可用"); } // 2. 冻结用户押金(UPDATE user SET deposit_status=1, frozen_deposit=? WHERE id=?) User user = userMapper.selectById(order.getUserId()); BigDecimal frozenDeposit = car.getDailyRent().multiply(new BigDecimal(3)); // 冻结3天押金 user.setDepositStatus(1); // 1=已冻结 user.setFrozenDeposit(frozenDeposit); userMapper.updateById(user); // 更新用户表 // 3. 创建订单(status=0待支付) order.setStatus(0); order.setDepositAmount(frozenDeposit); orderMapper.insert(order); return order; }参数说明:
selectByIdForUpdate()对应XML中的<select id="selectByIdForUpdate" resultType="Car"> SELECT * FROM car WHERE id = #{id} FOR UPDATE </select>。FOR UPDATE让MySQL对查询到的车辆行加写锁,防止并发下单时同一辆车被租两次。这是毕业设计里体现“高并发意识”的硬核细节,比单纯说“用了@Transactional”有力得多。
3.2 超期计费不是定时任务轮询:用还车时的时间差计算+状态机校验
还车操作(OrderService.java的returnCar())才是超期逻辑的核心:
@Transactional(rollbackFor = Exception.class) public void returnCar(Long orderId) { Order order = orderMapper.selectById(orderId); if (order == null || order.getStatus() != 2) { // 必须是"已取车"状态才能还车 throw new RuntimeException("订单状态异常,无法还车"); } // 计算应还时间(取车时间 + 租期天数) LocalDateTime shouldReturnTime = order.getPickupTime().plusDays(order.getRentDays()); LocalDateTime actualReturnTime = LocalDateTime.now(); // 超期天数 = 实际还车时间 - 应还时间(取整数天,不足1天按1天计) long overdueDays = ChronoUnit.DAYS.between(shouldReturnTime, actualReturnTime); if (overdueDays > 0) { BigDecimal overdueFee = order.getDailyRent().multiply(BigDecimal.valueOf(overdueDays)); // 更新订单:加收超期费,状态变"已超期" order.setOverdueFee(overdueFee); order.setStatus(4); // 4=已超期 order.setReturnTime(actualReturnTime); orderMapper.updateById(order); // 解冻押金时扣除超期费(见3.3节) } else { order.setStatus(3); // 3=已还车 order.setReturnTime(actualReturnTime); orderMapper.updateById(order); } }逻辑说明:
ChronoUnit.DAYS.between()返回的是完整天数差(如2024-05-01 10:00到2024-05-03 09:00返回1,不是2),符合租车行业“不足一天按一天收费”的惯例。状态码4(已超期)和3(已还车)在数据库里是独立枚举值,避免用status=3 AND overdue_fee>0这种模糊判断——这是状态机思维,答辩时能清晰画出状态流转图。
3.3 押金解冻与结算:事务内完成“冻结→扣费→解冻”原子操作
用户确认还车后(前端点击“确认还车”),触发OrderService.java的confirmReturn():
@Transactional(rollbackFor = Exception.class) public void confirmReturn(Long orderId) { Order order = orderMapper.selectById(orderId); User user = userMapper.selectById(order.getUserId()); if (order.getStatus() == 4) { // 已超期:先扣超期费,再解冻 BigDecimal finalDeposit = user.getFrozenDeposit().subtract(order.getOverdueFee()); user.setFrozenDeposit(finalDeposit); user.setDepositStatus(2); // 2=已解冻 user.setBalance(user.getBalance().add(finalDeposit)); // 退回剩余押金到余额 userMapper.updateById(user); } else if (order.getStatus() == 3) { // 正常还车:全额解冻 user.setBalance(user.getBalance().add(user.getFrozenDeposit())); user.setFrozenDeposit(BigDecimal.ZERO); user.setDepositStatus(2); userMapper.updateById(user); } // 更新订单为"已完成" order.setStatus(5); // 5=已完成 orderMapper.updateById(order); }参数说明:
user.getFrozenDeposit().subtract(order.getOverdueFee())用BigDecimal而非double,避免0.1+0.2=0.30000000000000004这类浮点误差。毕业设计答辩时,如果被问“为什么不用double”,这就是教科书级答案。
4. 权限控制与角色隔离:不是if(role.equals("admin")),而是基于URL和按钮的细粒度拦截
4.1 URL层级拦截:AdminController和UserController物理分离,非同一Controller内if-else
项目没用Shiro或Spring Security做统一权限框架(降低复杂度),而是用最朴实的方案:不同角色访问不同Controller包。
// admin包下的控制器,只处理管理员请求 @RestController @RequestMapping("/admin") public class AdminController { @GetMapping("/cars") // 管理员查看所有车辆 public List<Car> getAllCars() { ... } @PostMapping("/cars") // 管理员新增车辆 public String addCar(@RequestBody Car car) { ... } } // user包下的控制器,只处理普通用户请求 @RestController @RequestMapping("/user") public class UserController { @GetMapping("/orders") // 用户查看自己的订单 public List<Order> getUserOrders() { ... } @PostMapping("/orders") // 用户创建订单 public Order createOrder(@RequestBody Order order) { ... } }逻辑说明:
@RequestMapping("/admin")和@RequestMapping("/user")是物理路径隔离。前端菜单栏根据登录用户role字段(数据库user表的role列,值为"admin"或"user")动态渲染不同链接:管理员看到/admin/cars,用户看到/user/orders。这种设计在毕业设计里足够安全,且代码易懂——比强行塞进一个Controller里用if(role=="admin")更符合分层思想。
4.2 按钮级权限:Thymeleaf的sec:authorize="hasRole('ADMIN')"真正在HTML里生效
templates/admin/index.html中管理车辆的按钮:
<!-- 只有admin角色能看到此按钮 --> <button th:if="${#authorization.expression('hasRole(''ADMIN'')')}" type="button" onclick="location.href='/admin/cars/add'"> 新增车辆 </button>而templates/user/index.html中用户自己的订单操作:
<!-- 用户只能操作自己订单的按钮 --> <button th:if="${order.userId == session.user.id}" type="button" onclick="returnCar([[${order.id}])"> 确认还车 </button>参数说明:
th:if="${order.userId == session.user.id}"是Thymeleaf的表达式语言,它在服务端渲染HTML时就判断,避免前端JS伪造请求ID。毕业设计答辩时,你可以指着这段代码说:“权限控制在服务端完成,不是靠前端隐藏按钮”。
4.3 数据库层面隔离:User表的role字段是唯一角色标识,无中间权限表
user表结构精简到极致:
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `role` varchar(20) NOT NULL DEFAULT 'user', -- 只存'admin'或'user' `balance` decimal(10,2) DEFAULT '0.00', `frozen_deposit` decimal(10,2) DEFAULT '0.00', `deposit_status` tinyint DEFAULT '0', -- 0未冻结,1已冻结,2已解冻 PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:没有
role_permission、user_role等关联表,因为毕业设计不需要RBAC(基于角色的访问控制)的工业级复杂度。role字段直接决定能访问哪些Controller,符合“够用就好”的工程原则。面试官若问“为什么不用Spring Security”,你可以说:“业务角色只有两种,硬编码更直观,也便于答辩时快速解释”。
5. 避坑指南:这6个血泪经验,让我重装了3次MySQL才搞明白
5.1 现象:启动时报错org.springframework.dao.InvalidDataAccessResourceUsageException: Error attempting to get column 'id' from result set
原因:MySQL驱动版本与Spring Boot 2.7.x不匹配。pom.xml里mysql-connector-java版本是5.1.47,但MySQL 8.0+必须用8.0.33。
解决:打开pom.xml,把<mysql-connector-java.version>5.1.47</mysql-connector-java.version>改成8.0.33,并确认<dependency>中groupId是mysql而非mysql-connector-java(旧版写法):
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>5.2 现象:登录后跳转到空白页,浏览器控制台报Thymeleaf template not found
原因:Thymeleaf默认查找src/main/resources/templates/下的HTML,但项目把页面放在src/main/webapp/WEB-INF/views/(旧JSP路径)。
解决:删掉webapp文件夹,把login.html、admin/index.html等所有HTML文件移到src/main/resources/templates/下,并确保application.yml中Thymeleaf配置正确:
spring: thymeleaf: prefix: classpath:/templates/ suffix: .html cache: false # 开发时关闭缓存,改HTML不用重启5.3 现象:租车时提示“车辆不可用”,但数据库里car.status明明是'available'
原因:carMapper.selectByIdForUpdate()的XML映射没生效,因为MyBatis没扫描到mapper XML文件。
解决:检查application.yml,确认mybatis.mapper-locations指向正确路径:
mybatis: mapper-locations: classpath:mapper/*.xml # 必须和src/main/resources/mapper/目录一致 configuration: map-underscore-to-camel-case: true同时,确保src/main/resources/mapper/RentMapper.xml文件存在,且<mapper namespace="com.example.carrental.mapper.RentMapper">中的namespace与Java接口全限定名完全一致。
5.4 现象:超期计费算出负数,如应还时间2024-05-10,实际还车2024-05-05
原因:ChronoUnit.DAYS.between(shouldReturnTime, actualReturnTime)当actualReturnTime早于shouldReturnTime时返回负数,代码没做校验。
解决:在returnCar()方法开头加防护:
if (actualReturnTime.isBefore(shouldReturnTime)) { throw new RuntimeException("还车时间不能早于应还时间"); } long overdueDays = Math.max(0, ChronoUnit.DAYS.between(shouldReturnTime, actualReturnTime));5.5 现象:管理员登录后看不到车辆列表,返回空JSON
原因:AdminController.java里getAllCars()方法没加@ResponseBody或没用@RestController(但项目用了,所以更可能是SQL问题)。
排查:打开src/main/resources/mapper/CarMapper.xml,检查<select id="selectAll">的resultMap是否引用了正确的Car实体类,且<result property="xxx" column="xxx"/>的column名与数据库字段完全一致(如数据库是daily_rent,XML里不能写成dailyRent)。
5.6 现象:修改application.yml后重启,端口还是8080,配置不生效
原因:IDEA的Maven运行配置里勾选了“Use the Maven wrapper”,但wrapper版本太老,读不到新配置。
解决:右键pom.xml → Run As → Maven Build... → 在Goals框输入spring-boot:run→ 点击Runner选项卡 → 取消勾选“Use the Maven wrapper” → 勾选“Skip tests” → Run。这样会强制用本地Maven,读取最新的yml。
6. 进阶验证技巧:用Postman模拟真实租车链路,3个接口串联跑通全流程
6.1 构建最小可行链路:注册→登录→查车→下单→还车→确认,5个接口缺一不可
毕业设计答辩最怕“功能演示翻车”。我建议用Postman按顺序调用以下接口,每个接口返回200才算通过:
| 步骤 | 接口 | 方法 | 请求体(JSON) | 预期响应 |
|---|---|---|---|---|
| 1. 注册用户 | http://localhost:8080/thymeleaf/register | POST | {"username":"testuser","password":"123456","role":"user"} | 返回登录页HTML |
| 2. 用户登录 | http://localhost:8080/thymeleaf/login | POST | {"username":"testuser","password":"123456"} | 重定向到/thymeleaf/user |
| 3. 查可用车辆 | http://localhost:8080/api/cars/available | GET | — | JSON数组,含id,brand,dailyRent字段 |
| 4. 下单租车 | http://localhost:8080/api/orders | POST | {"userId":1,"carId":1,"rentDays":2} | 返回订单对象,status=0 |
| 5. 还车(需手动改数据库order.status=2) | http://localhost:8080/api/orders/1/return | POST | — | 返回{"msg":"还车成功"} |
注意:第4步下单后,数据库
order表里status是0(待支付),但前端页面需要用户“支付”才能变1(已支付)。为快速验证,直接用MySQL命令更新:
UPDATE `order` SET `status`=2, `pickup_time`=NOW() WHERE `id`=1;这样订单就进入“已取车”状态,才能触发还车逻辑。
6.2 关键字段断言:用Postman Tests验证业务规则是否真落地
在Postman的Tests标签页,为“下单租车”接口添加断言,验证押金冻结逻辑:
// 检查返回订单的depositAmount是否等于车辆日租金×3 const response = pm.response.json(); const expectedDeposit = response.car.dailyRent * 3; pm.test("押金冻结金额正确", function () { pm.expect(response.depositAmount).to.eql(expectedDeposit); }); // 检查用户表frozen_deposit是否更新 pm.test("用户冻结押金已更新", function () { // 这里需调用另一个API查用户,或直接查数据库(略) });6.3 时间敏感场景压测:用Python脚本批量生成“超期还车”订单
手动改数据库太慢?写个Python脚本自动生成超期订单:
import requests import time from datetime import datetime, timedelta # 1. 创建订单(假设carId=1, userId=1) order_data = {"userId": 1, "carId": 1, "rentDays": 1} resp = requests.post("http://localhost:8080/api/orders", json=order_data) order_id = resp.json()["id"] # 2. 模拟取车(更新数据库) requests.post(f"http://localhost:8080/api/orders/{order_id}/pickup") # 3. 等待2秒,模拟超期(实际应还时间=取车时间+1天,现在过了2秒已超期) time.sleep(2) # 4. 还车,触发超期计费 requests.post(f"http://localhost:8080/api/orders/{order_id}/return") print(f"订单{order_id}已超期还车")逻辑说明:
/pickup接口在OrderService.java里更新pickup_time=now()和status=2。这个脚本证明:只要时间差存在,超期费就会自动计算——不是靠定时任务“每天凌晨扫一遍”,而是还车瞬间实时计算。这才是真实业务系统的做法。
从那以后我每次帮学生改毕设,都会先让他们用Postman跑通这5个接口,再谈优化。因为答辩现场网络一卡、浏览器一崩,能救场的只有清晰的接口链路和可复现的断言。希望帮到你。
本文还有配套的精品资源,点击获取