ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java汽车租赁系统:押金冻结与超期计费的事务实现

Java汽车租赁系统:押金冻结与超期计费的事务实现 简介本资源是一套面向高校计算机专业本科生的毕业设计级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_status0未冻结、1已冻结、2已解冻、以及关键外键约束如order.car_id → car.id。别跳过它——很多启动失败源于建表时字符集没设对。2.2 JDK与MySQL版本踩准Spring Boot 2.7.x不认JDK 17MySQL 8必须开caching_sha2_password项目pom.xml中java.version1.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?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue这说明它适配MySQL 8.0但必须确保你的MySQL用户认证插件是caching_sha2_password而非老版mysql_native_password# 登录MySQL检查当前用户插件 mysql -u root -p SELECT user, host, plugin FROM mysql.user WHERE userroot; # 如果plugin是mysql_native_password执行以下命令切换替换your_password ALTER USER rootlocalhost 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.port8080但如果你的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.ymlserver: 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_status1, 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. 创建订单status0待支付 order.setStatus(0); order.setDepositAmount(frozenDeposit); orderMapper.insert(order); return order; }参数说明selectByIdForUpdate()对应XML中的select idselectByIdForUpdate resultTypeCar 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已还车在数据库里是独立枚举值避免用status3 AND overdue_fee0这种模糊判断——这是状态机思维答辩时能清晰画出状态流转图。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.10.20.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 ListCar getAllCars() { ... } PostMapping(/cars) // 管理员新增车辆 public String addCar(RequestBody Car car) { ... } } // user包下的控制器只处理普通用户请求 RestController RequestMapping(/user) public class UserController { GetMapping(/orders) // 用户查看自己的订单 public ListOrder getUserOrders() { ... } PostMapping(/orders) // 用户创建订单 public Order createOrder(RequestBody Order order) { ... } }逻辑说明RequestMapping(/admin)和RequestMapping(/user)是物理路径隔离。前端菜单栏根据登录用户role字段数据库user表的role列值为admin或user动态渲染不同链接管理员看到/admin/cars用户看到/user/orders。这种设计在毕业设计里足够安全且代码易懂——比强行塞进一个Controller里用if(roleadmin)更符合分层思想。4.2 按钮级权限Thymeleaf的sec:authorizehasRole(ADMIN)真正在HTML里生效templates/admin/index.html中管理车辆的按钮!-- 只有admin角色能看到此按钮 -- button th:if${#authorization.expression(hasRole(ADMIN))} typebutton onclicklocation.href/admin/cars/add 新增车辆 /button而templates/user/index.html中用户自己的订单操作!-- 用户只能操作自己订单的按钮 -- button th:if${order.userId session.user.id} typebutton onclickreturnCar([[${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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明没有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.version5.1.47/mysql-connector-java.version改成8.0.33并确认dependency中groupId是mysql而非mysql-connector-java旧版写法dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency5.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 namespacecom.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 idselectAll的resultMap是否引用了正确的Car实体类且result propertyxxx columnxxx/的column名与数据库字段完全一致如数据库是daily_rentXML里不能写成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/registerPOST{username:testuser,password:123456,role:user}返回登录页HTML2. 用户登录http://localhost:8080/thymeleaf/loginPOST{username:testuser,password:123456}重定向到/thymeleaf/user3. 查可用车辆http://localhost:8080/api/cars/availableGET—JSON数组含id,brand,dailyRent字段4. 下单租车http://localhost:8080/api/ordersPOST{userId:1,carId:1,rentDays:2}返回订单对象status05. 还车需手动改数据库order.status2http://localhost:8080/api/orders/1/returnPOST—返回{msg:还车成功}注意第4步下单后数据库order表里status是0待支付但前端页面需要用户“支付”才能变1已支付。为快速验证直接用MySQL命令更新UPDATE order SET status2, pickup_timeNOW() WHERE id1;这样订单就进入“已取车”状态才能触发还车逻辑。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. 创建订单假设carId1, userId1 order_data {userId: 1, carId: 1, rentDays: 1} resp requests.post(http://localhost:8080/api/orders, jsonorder_data) order_id resp.json()[id] # 2. 模拟取车更新数据库 requests.post(fhttp://localhost:8080/api/orders/{order_id}/pickup) # 3. 等待2秒模拟超期实际应还时间取车时间1天现在过了2秒已超期 time.sleep(2) # 4. 还车触发超期计费 requests.post(fhttp://localhost:8080/api/orders/{order_id}/return) print(f订单{order_id}已超期还车)逻辑说明/pickup接口在OrderService.java里更新pickup_timenow()和status2。这个脚本证明只要时间差存在超期费就会自动计算——不是靠定时任务“每天凌晨扫一遍”而是还车瞬间实时计算。这才是真实业务系统的做法。从那以后我每次帮学生改毕设都会先让他们用Postman跑通这5个接口再谈优化。因为答辩现场网络一卡、浏览器一崩能救场的只有清晰的接口链路和可复现的断言。希望帮到你。本文还有配套的精品资源点击获取
返回列表