SSM停车计费系统毕设源码:六张表与计费算法全解析

发布时间:2026/9/23 10:41:36
SSM停车计费系统毕设源码:六张表与计费算法全解析 简介基于Java与SSM框架的校内车辆停车计费收费系统毕业设计源码包涵盖前端展示、后端业务与数据库设计面向计算机相关专业学生、课程设计者及需要快速搭建毕设项目的开发者能帮助掌握SSM框架整合、搭建分层项目结构并理解停车计费收费管理流程。压缩包共包含1148个文件整体大小约18.84MB内有Java源码、JSP页面、Spring与MyBatis相关配置、SQL数据库脚本以及HTML、CSS、JavaScript等前端资源并附带jar依赖与使用说明文档目录层次清楚便于导入开发环境运行调试适合直接学习或二次扩展。项目已通过导师指导认可答辩评审分95分代码在macOS和Windows10/11系统上均测试成功功能完整可靠。目前已有172人学习下载资源包含完整业务模块例如用户登录、车辆信息登记、计费规则配置、停车收费记录等配套数据库脚本和使用文档能大幅降低部署成本是毕业设计、课程设计以及SSM框架综合练习的优质参考也可以在此基础上增加其他校园管理功能。1. 为什么我建议毕业设计选 SSM 停车计费系统一份可答辩可扩展的源码每年毕业季Java 方向的选题里总有那么几类高频题目图书管理、学生选课、校园报修再就是今天要拆的校内车辆停车计费。说实话停车计费这个题比图书管理有意思得多因为它天然带了一组需要动脑子才能做好的业务规则——按小时计费、跨天结算、月卡和临时车怎么区分、出场时金额怎么算。这些规则往代码里一落答辩时老师一问业务逻辑你就有话可说。这份资源是一个基于 Java SSM 的校内停车计费收费系统完整源码包包含数据库脚本和使用文档项目的核心功能是管理车辆进出记录、自动计算停车费用、维护收费标准。适合三类人一是计算机相关专业准备毕设或课设的学生拿它做底子改一改就能用二是想快速了解 SSM 框架在真实业务里怎么组织分层的人三是刚开始学 SSM 整合、想找个完整项目练手的初学者。下面我从架构、数据库、计费逻辑到踩坑完整拆一遍。2. 系统架构与数据库设计六张核心表的字段、索引与关系2.1 SSM 分层架构在停车系统里怎么落这个项目是典型的 SSM 三层架构Spring 负责对象管理和事务控制SpringMVC 负责请求路由和参数绑定MyBatis 负责数据库操作。表现层用的是 JSP 加 EasyUI 和 BootstrapEasyUI 主要负责后台管理界面的表格、弹窗、表单校验Bootstrap 负责前台页面的布局和响应式样式。项目里能看到 easyui.css、bootstrap.css 这些静态资源文件说明前端没有用前后端分离方案仍然是传统的服务端渲染。这种选型放在毕设场景里有现实原因SSM 是很多高校 Java 课程的主线资料多、老师熟、答辩时框架选型不会被挑战JSP 加 EasyUI 的方式虽然看着不够现代但它的好处是页面逻辑直观表格分页、弹窗编辑这些后台管理常用能力 EasyUI 都封装好了自己写 JS 的量很少。如果你想把这份源码改成 Spring Boot 版本分层结构几乎可以平移只是把 XML 配置换成注解和自动配置。2.2 数据库表结构与建表 SQL我从项目的数据库脚本里梳理出六张核心表分别是系统用户表、车辆信息表、停车记录表、收费标准表、月卡信息表和收费记录表。这些表之间的关系很清晰车辆信息关联用户和月卡停车记录关联车辆和收费标准收费记录是每次出场结算的流水。先看一下核心表的结构-- 车辆信息表 CREATE TABLE t_vehicle ( id INT(11) NOT NULL AUTO_INCREMENT, plate_number VARCHAR(20) NOT NULL COMMENT 车牌号, owner_name VARCHAR(50) DEFAULT NULL COMMENT 车主姓名, vehicle_type TINYINT(4) DEFAULT 1 COMMENT 车型1-小型车 2-大型车, card_type TINYINT(4) DEFAULT 0 COMMENT 卡类型0-临时车 1-月卡, card_no VARCHAR(50) DEFAULT NULL COMMENT 月卡编号, owner_phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_plate_number (plate_number) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT车辆信息表; -- 停车记录表 CREATE TABLE t_parking_record ( id INT(11) NOT NULL AUTO_INCREMENT, record_no VARCHAR(32) DEFAULT NULL COMMENT 记录编号, vehicle_id INT(11) NOT NULL COMMENT 车辆ID, plate_number VARCHAR(20) NOT NULL COMMENT 车牌号, entry_time DATETIME NOT NULL COMMENT 进场时间, exit_time DATETIME DEFAULT NULL COMMENT 出场时间, park_duration INT(11) DEFAULT 0 COMMENT 停车时长分钟, fee DECIMAL(10,2) DEFAULT 0.00 COMMENT 应收金额, actual_fee DECIMAL(10,2) DEFAULT 0.00 COMMENT 实收金额, status TINYINT(4) DEFAULT 0 COMMENT 状态0-在停 1-已出场, PRIMARY KEY (id), KEY idx_plate_number (plate_number), KEY idx_entry_time (entry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT停车记录表;这两张表是系统的核心。车辆信息表里有个值得注意的设计点车牌号加了唯一索引保证同一辆车不会重复建档这是出入场操作的约束基础。停车记录表既存车辆 ID 又冗余了车牌号字段按照数据库第三范式这算冗余但实际业务里出场结算时频繁按车牌查询记录冗余字段避免了每次都 JOIN 车辆表这种取舍在毕设答辩里很加分。2.3 收费标准表的规则设计收费标准表是计费逻辑的数据来源。我见过很多初学者的做法把收费标准硬编码在 Java 代码里导致改一次价格就要重新编译部署。这个项目把标准放进了数据库这是一个正确得多的设计。表里核心字段包括收费类型临时/月卡、首小时费用、续时费用、单日封顶金额、免费停车时长。-- 收费标准表 CREATE TABLE t_fee_rule ( id INT(11) NOT NULL AUTO_INCREMENT, rule_name VARCHAR(50) NOT NULL COMMENT 规则名称, rule_type TINYINT(4) NOT NULL COMMENT 类型1-临时车 2-月卡, free_minutes INT(11) DEFAULT 0 COMMENT 免费时长分钟, first_hour_fee DECIMAL(10,2) DEFAULT 0.00 COMMENT 首小时费用, hourly_fee DECIMAL(10,2) DEFAULT 0.00 COMMENT 续时每小时费用, daily_cap DECIMAL(10,2) DEFAULT 0.00 COMMENT 单日封顶, enabled TINYINT(4) DEFAULT 1 COMMENT 是否启用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT收费标准表;计费时逻辑是先判断车辆是临时车还是月卡月卡不按小时计费只校验有效期临时车从停车记录里取进场时间再根据收费标准分段计算。这里的单日封顶字段很关键它解决的是“停了一天一夜费用太高引发投诉”的问题很多校园停车场都有封顶规则。标准表支持配置多条规则并启用或停用意味着系统可以针对不同区域、不同车型设置差异化的收费策略。2.4 数据库脚本的导入与初始化数据库文件通常是一个.sql脚本里面包含建库、建表、插入初始数据的完整语句。导入时先建库再导入数据顺序不要反。导入完成后检查三件套系统管理员账号有没有初始化、收费标准有没有默认数据、表之间的外键关系有没有问题。默认管理员账号一般在脚本里通过 INSERT 语句写入密码大多是 MD5 加密后的值登录不了先查是不是加密方式不匹配。3. 登录认证与权限拦截从拦截器配置到 Session 失效处理3.1 登录模块的实现思路停车计费系统的后台管理需要登录才能访问这是毕设里最基础但答辩必问的功能。这个项目采用传统 Session 方式的登录认证用户提交用户名和密码后端校验通过后把用户信息放入 Session受保护的页面通过拦截器检查 Session 是否存在用户对象。相比 JWTSession 方案在单体 JSP 项目里更自然也不需要额外处理 Token 续期问题。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录相关的请求 String uri request.getRequestURI(); if (uri.contains(/login) || uri.contains(/logout)) { return true; } // 检查 Session 中是否存在已登录用户 Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { // 未登录则重定向到登录页 response.sendRedirect(request.getContextPath() /login.jsp); return false; } return true; }这段代码是 SpringMVC 拦截器的核心逻辑。第一块判断当前请求的 URI 是否包含 login 或 logout 路径包含则直接放行不进入权限校验。第二块从 Session 中取出 loginUser 属性如果为空说明没有登录或 Session 已失效重定向到登录页。在项目的配置里拦截器需要注册到 SpringMVC 的配置文件中并且要指定拦截路径。常见做法是配置拦截/**但排除静态资源和登录相关路径。静态资源如果不排除CSS 和 JS 文件也会被拦截页面样式全部丢失这是初学者最容易踩的坑。mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/static/**/ mvc:exclude-mapping path/css/**/ mvc:exclude-mapping path/js/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/user/login/ /mvc:interceptor /mvc:interceptorsXML 配置里的 mapping 和 exclude-mapping 是一对搭配mapping 声明拦截哪些路径exclude-mapping 声明哪些路径不拦截。项目里如果静态资源放在 webapp 下的 static 目录就排除 /static/ 前缀登录接口如果走的是 /user/login也要在这里排除否则会出现“登录接口本身被拦截导致永远登录不了”的死循环。加了 Session 超时时间后用户在页面停留时间过长再点击操作会被重新弹回登录页这是正常现象不是 Bug。3.2 密码加密存储与校验毕设项目里密码加密是个高频扣分点。很多学生的代码直接把明文密码存进数据库答辩时老师问“密码安全性怎么保证”答不上来就尴尬了。这个项目的用户表里密码字段存储的是加密后的值登录时把用户输入的密码用相同算法加密再与数据库里的密文比对。public SysUser login(String username, String rawPassword) { SysUser user userDao.findByUsername(username); if (user ! null) { // 对用户输入的明文密码进行加密再比对 String encrypted MD5Util.md5(rawPassword); if (encrypted.equals(user.getPassword())) { return user; } } return null; }这里的 MD5Util 是项目里的一个工具类封装了 MD5 加密逻辑。虽然从现在的安全标准看 MD5 已经不够安全但在毕设场景里用了加密总比明文好而且如果引入盐值salt会更好。答辩时可以这样回答老师的追问“当前项目使用了 MD5 加密存储用户密码如果实际生产部署我会在 MD5 基础上增加随机盐值或者改用 BCrypt 这类自适应哈希算法。”这段话说出来老师就知道你懂安全设计的演进过程。3.3 Session 超时与并发登录控制Session 超时时间在 web.xml 里通过 session-config 配置单位是分钟。项目里一般配置 30 分钟超过这个时间用户没有操作Session 自动失效再次请求就会被拦截器踢回登录页。这里有个细节拦截器里放行了 /logout 路径但 logout 接口内部还要主动调用 Session 的 invalidate 方法把会话彻底销毁不能只做页面上跳转。并发登录控制是另一层逻辑。如果需求允许同一账号在多个浏览器同时登录代码不需要特殊处理但如果要求同一账号只能在一个地方登录常见方案是在用户登录成功后把旧的 Session 置为失效或者把 Session 和用户绑定后登录的把先登录的挤下线。这个项目在基础版本里没做复杂控制如果你要加可以在用户表里增加一个 login_token 字段登录成功后生成新 Token 并更新其他请求校验 Token 一致才放行。4. 停车计费核心流程按小时计费、跨天结算与并发处理4.1 入场流程与数据结构流转车辆入场时系统要做三件事根据车牌号查询车辆信息判断车辆是临时车还是月卡创建一条新的停车记录状态为“在停”月卡车辆要额外校验有效期。这里对临时车的处理是入场的车辆如果没有在车辆信息表里建档系统会自动创建一个临时车档案方便出场结算时关联。入场操作的代码逻辑不复杂但要注意事务边界。创建停车记录和更新车辆信息如上次入场时间这两个操作必须在一个事务里否则可能出现记录了停车但车辆状态没更新的情况。项目的 Service 层方法加上了 Transactional 注解来保证这一点。Transactional public ParkingRecord entry(String plateNumber) { // 查找车辆信息不存在则创建临时车档案 Vehicle vehicle vehicleDao.findByPlateNumber(plateNumber); if (vehicle null) { vehicle new Vehicle(); vehicle.setPlateNumber(plateNumber); vehicle.setCardType(0); // 临时车 vehicleDao.insert(vehicle); } // 创建停车记录 ParkingRecord record new ParkingRecord(); record.setPlateNumber(plateNumber); record.setVehicleId(vehicle.getId()); record.setEntryTime(new Date()); record.setStatus(0); parkingRecordDao.insert(record); return record; }这段代码里有几个参数值得注意cardType 为 0 表示临时车1 表示月卡status 为 0 是在停状态1 是已出场。入场时先查车辆查不到就自动建档这是停车场系统的常见做法避免了在入口处先做车辆信息登记再放行的繁琐流程。注意这段代码处理的是单个车辆入场实际的入口道闸场景可能还有摄像头识别车牌的前置步骤但代码层面只需要把识别到的车牌号传进来即可。4.2 出场结算与计费算法出场结算是整个系统最核心的业务逻辑也是答辩时老师大概率会问的部分。车辆出场时系统根据车牌号查到当前在停的记录计算停车时长再根据收费标准计算费用。Transactional public BigDecimal exit(String plateNumber) { // 查找在停记录 ParkingRecord record parkingRecordDao.findActiveByPlateNumber(plateNumber); if (record null) { throw new RuntimeException(未找到进场记录); } Date now new Date(); long minutes (now.getTime() - record.getEntryTime().getTime()) / (1000 * 60); // 查询车辆类型和对应收费标准 Vehicle vehicle vehicleDao.findById(record.getVehicleId()); FeeRule rule feeRuleDao.findByType(vehicle.getCardType() 1 ? 2 : 1); BigDecimal fee calculateFee(minutes, rule); record.setExitTime(now); record.setParkDuration(minutes); record.setFee(fee); record.setActualFee(fee); record.setStatus(1); parkingRecordDao.update(record); return fee; }这段代码的关键在 calculateFee 方法。计费算法要处理几个边界情况停车时间不足免费时长的不收费超过免费时长但不足一小时的按一小时算跨天时单日封顶规则要怎么分段计算。这些规则看着简单但落在代码里就涉及取整方式的问题——用 Math.ceil 向上取整还是用 Math.floor 向下取整直接决定用户是吃亏还是占便宜。private BigDecimal calculateFee(long minutes, FeeRule rule) { // 免费时长内不收费 if (minutes rule.getFreeMinutes()) { return BigDecimal.ZERO; } long billableMinutes minutes - rule.getFreeMinutes(); // 不足一小时按一小时计算 long hours billableMinutes / 60; if (billableMinutes % 60 ! 0) { hours; } // 首小时单独计费后续按续时费率 BigDecimal fee rule.getFirstHourFee(); if (hours 1) { BigDecimal extra rule.getHourlyFee() .multiply(BigDecimal.valueOf(hours - 1)); fee fee.add(extra); } // 单日封顶 if (rule.getDailyCap() ! null rule.getDailyCap().compareTo(BigDecimal.ZERO) 0 fee.compareTo(rule.getDailyCap()) 0) { fee rule.getDailyCap(); } return fee; }这段计费代码的逻辑顺序是先判断是否需要收费再取了免费时长后的可计费分钟数不足一小时的向上取整然后首小时和续时分别计费最后做单日封顶截断。这里面最容易出错的是 BigDecimal 的用法。金额计算绝对不能使用 double 或 float否则会出现 0.1 加 0.2 不等于 0.3 的精度问题。项目里全程使用 BigDecimal配合 setScale 方法保留两位小数这是正确的做法。单日封顶这里有一个细节很多停车系统的封顶规则是“24 小时内封顶”如果车辆停了 30 个小时封顶应该按两天的上限算而不是只收一次封顶费。这个项目的代码处理的是单日封顶的简化版本实际场景中还需要判断停车时长是否跨天并把每天的费用叠加。如果你要把这个逻辑改严谨可以按天拆分停车时长每天分别计算封顶费用后累加。4.3 月卡车辆的到期校验月卡车辆在入场时走的是另一套逻辑先查月卡信息校验当前日期是否在有效期内过期则提示续费或按临时车收费。月卡表里通常有 start_date 和 end_date 两个日期字段有效期判断就是判断当前时间是否在两者之间。月卡过期的处理方式各系统不一样。有的系统要求过期后必须先续费才能出场有的系统出场时按临时车标准收费。这个项目的基础逻辑是入场时月卡过期车辆仍然可以入场但卡类型被标记为临时车出场时按临时车标准计算费用。这个设计在逻辑上说得通但你要在答辩时能讲清楚为什么这样做——因为门禁系统无法在入场时强制阻止过期月卡进入只能通过出场时的费用计算来体现规则差异。4.4 并发出入场的处理策略停车场出口在高峰时段可能出现多辆车同时出场的情况。如果系统部署后没有做并发处理两辆车同时出场时可能读到同一条在停记录导致一辆车结算成功后另一辆拿到的是空记录或者重复计费。这个问题在毕设演示时不容易暴露但如果答辩老师懂并发可能会问“高并发场景下怎么保证数据一致性”。本项目里处理方式比较简单出场方法加了 Transactional 注解数据库层面的更新操作在 InnoDB 引擎下会走行锁。也就是说同一条停车记录在事务提交前另一个并发的出场请求会被阻塞等待。// 查询在停记录时使用行锁防止并发出场重复结算 Select(SELECT * FROM t_parking_record WHERE plate_number #{plateNumber} AND status 0 FOR UPDATE) ParkingRecord findActiveByPlateNumberForUpdate(String plateNumber);FOR UPDATE 是 MySQL InnoDB 引擎的行级锁语法在查询时就把这条记录锁住直到事务结束。加了锁之后两个同时到达的出场请求会串行执行第二个请求等第一个提交后才能查询此时记录的 status 已经变成 1查不到在停记录就会提示“未找到进场记录”而不会重复计费。这个问题的完整解决还要在 Service 层配合。如果你对并发有追求可以在入场时也加同样的锁避免同一辆车在极端情况下同时入场两次。不过毕设场景下做到行锁这一步已经够用了答辩时说出 FOR UPDATE 这个关键词老师就知道你对并发控制有概念。5. 避坑指南SSM 项目迁移与运行中的五个高频问题5.1 数据库脚本导入失败字符集与版本不兼容现象执行数据库脚本时报错常见的有乱码、字段类型不识别、建表失败。原因脚本里可能包含了DEFAULT CURRENT_TIMESTAMP这类 MySQL 5.6 才支持的语法如果本地是 MySQL 5.5 以下就跑不通字符集方面如果脚本里有中文注释但连接编码不是 utf8导入时直接乱码。解决导入前在客户端先执行SET NAMES utf8mb4;再 source 导入。MySQL 版本建议用 5.7 或 8.08.0 下要注意驱动版本mysql-connector-java 5.x 连 8.0 会报认证协议错误改用 8.x 驱动并把 URL 里的useSSLfalse参数加上。5.2 项目启动报 404静态资源被拦截现象Tomcat 启动成功登录页能打开但 CSS 全部失效页面光秃秃的只有文字。原因SpringMVC 的 DispatcherServlet 拦截了/路径静态资源的访问请求也走了一遍 HandlerMapping找不到对应的 Controller直接 404。配置里如果没有放行静态资源浏览器请求 css 文件时拿到的就是 404 状态。解决在 SpringMVC 配置里加mvc:default-servlet-handler/或者用mvc:resources mapping/css/** location/css//显式映射静态资源目录。加了之后还要确认前端页面引用的路径和实际路径一致——项目里引入 bootstrap.css 时如果写的是/static/bootstrap.css而文件实际在/css/bootstrap.css仍然 404。5.3 数据库连接失败驱动版本和 URL 参数不匹配现象项目启动时控制台报Cannot create PoolableConnectionFactory或者Communications link failure。原因数据库版本、JDBC 驱动版本、连接 URL 三者的匹配关系没对应上。常见组合错误MySQL 8.0 数据库 mysql-connector-java 5.1.x 驱动 URL 里没有指定时区参数。MySQL 8.0 以后对驱动类名的要求从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.DriverURL 里还需要加serverTimezoneAsia/Shanghai否则直接报时区异常。解决检查三个位置——pom.xml 或 lib 目录下的驱动 jar 版本、jdbc.properties 里的 driverClassName、数据库的版本。我的习惯是统一用 MySQL 5.7 配 mysql-connector-java 5.1.49这套组合最稳各种兼容问题最少。5.4 Tomcat 启动内存溢出PermGen 或 Metaspace 不够现象启动过程中报java.lang.OutOfMemoryError: PermGen space或Metaspace溢出。原因项目引用的 jar 包较多开发环境的 Tomcat 默认内存参数不够用。Eclipse 或 IDEA 启动 Tomcat 时用的是 JVM 默认参数老版本 Tomcat 的 PermGen 空间只有几十 MB项目一旦加载大量类就溢出。解决在 Tomcat 的启动配置里增加 JVM 参数。IDEA 里在 Run Configuration 的 VM options 填-XX:PermSize128M -XX:MaxPermSize256MJDK8 之前的写法或-XX:MetaspaceSize128m -XX:MaxMetaspaceSize512mJDK8 及以后。如果你用的是 JDK8PermGen 参数已经不生效必须写 Metaspace。5.5 登录验证码不显示Response 输出被拦截或路径错误现象登录页能打开但验证码图片的位置是个红叉或者空白。原因验证码图片是由后端动态生成并输出的前端img标签的 src 指向一个生成验证码的接口。如果验证码接口返回的 Content-Type 不对或者生成验证码的 Servlet 路径被权限拦截器拦住了图片就加载不出来导致的结果就是登录功能被卡在验证码这一关上。解决先在浏览器直接访问验证码接口的地址看能不能显示图片。不能显示则检查两个点一是生成图片的代码里是否正确设置了response.setContentType(image/jpeg)二是 SpringMVC 拦截器是否把验证码接口路径也拦截了需要在 exclude-mapping 里把它排除。6. 答辩加分技巧手写分页与导出 Excel 的两个高频改造点6.1 手写一个不依赖插件的分页工具类项目里停车记录的查询列表用了分页。很多 SSM 项目在 MyBatis 里引入 PageHelper 插件来完成分页一行代码搞定。但这个项目如果想体现你手写底层能力建议自己实现分页创建一个 PageBean 类包含当前页码、每页条数、总记录数、总页数、数据列表再用一个拦截器或手动拼接 LIMIT 语句完成分页。public class PageBeanT { private int pageNum; // 当前页码 private int pageSize; // 每页条数 private long total; // 总记录数 private int pages; // 总页数 private ListT data; // 当前页数据 public static int getOffset(int pageNum, int pageSize) { return (pageNum - 1) * pageSize; } }在 MyBatis 的 XML 里查询列表的 SQL 末尾加上LIMIT #{offset}, #{pageSize}查询总数为另一条不带 LIMIT 的计数 SQL。两层 Service 各查一次把结果封装进 PageBean 返回给前端。这样做的价值在于你不依赖 PageHelper 的插件机制也能完全理解分页的原理——LIMIT 后面的两个参数第一个是偏移量从第几条开始取第二个是取几条数据。答辩时老师问“分页怎么实现的”你直接回答“手写了一个 PageBean 工具类在 SQL 里用 LIMIT 控制查询范围然后用 count 查询拿到总记录数”比说“我用的 PageHelper”更能体现你对 MyBatis 执行机制的理解。前端接分页数据后EasyUI 的 datagrid 组件有现成的分页条直接把 total 和 rows 返回就能自动渲染页码。6.2 用 POI 导出停车记录 Excel停车计费系统做一个数据导出功能在答辩演示时非常实用——老师会看到这个系统不只是能录数据、算费用还能把数据以文档形式输出这是生产系统的标配能力。如果你基础好可以在项目里加一个导出功能把停车记录列表导出成 Excel 文件用 Apache POI 实现。核心逻辑是创建 Workbook、创建 Sheet、创建行和单元格把数据逐行填入最后通过 HttpServletResponse 把文件流输出给浏览器下载。// 导出停车记录为 Excel try (Workbook workbook new XSSFWorkbook()) { Sheet sheet workbook.createSheet(停车记录); String[] headers {车牌号, 入场时间, 出场时间, 停车时长分钟, 费用}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } int rowIndex 1; for (ParkingRecord record : records) { Row row sheet.createRow(rowIndex); row.createCell(0).setCellValue(record.getPlateNumber()); row.createCell(1).setCellValue( new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(record.getEntryTime())); row.createCell(2).setCellValue( record.getExitTime() null ? : new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(record.getExitTime())); row.createCell(3).setCellValue(record.getParkDuration()); row.createCell(4).setCellValue(record.getFee().doubleValue()); } // 设置下载响应头前端弹出下载 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameparking_records.xlsx); workbook.write(response.getOutputStream()); }这段代码的核心有三处XSSFWorkbook 是 POI 里对应 xlsx 格式的工作簿对象创建后往里面加 Sheet 和 Row响应头的 Content-Type 必须设成 Excel 对应的 MIME 类型否则浏览器可能无法识别下载文件的格式Content-Disposition 里的 attachment 参数告诉浏览器这是一个需要下载的附件而不是直接在页面打开的内容。加上这个功能之后系统的完整度会明显高出一个档次入场、计费、出场、月卡管理、收费记录、数据导出一个真实的停车场管理系统该有的功能全齐了。我做毕设评审时看过几百份项目含金量高的项目有一个共同特征——不只是把增删改查做完了而是把业务闭环跑通了尤其是类似数据导出这种细节功能对项目完整度的提升非常明显。从那以后我拆项目、改别人的毕设源码都会强制走一遍三件事数据库脚本能不能一次跑通、静态资源有没有被拦截、计费结果手算和代码算是否一致。这三个点过了项目基本就能稳定运行。希望帮到你动手跑一遍比看十遍文章都管用。本文还有配套的精品资源点击获取