简介:基于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 和 Bootstrap,EasyUI 主要负责后台管理界面的表格、弹窗、表单校验,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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='收费标准表';计费时逻辑是:先判断车辆是临时车还是月卡,月卡不按小时计费只校验有效期;临时车从停车记录里取进场时间,再根据收费标准分段计算。这里的单日封顶字段很关键,它解决的是“停了一天一夜费用太高引发投诉”的问题,很多校园停车场都有封顶规则。标准表支持配置多条规则并启用或停用,意味着系统可以针对不同区域、不同车型设置差异化的收费策略。
2.4 数据库脚本的导入与初始化
数据库文件通常是一个.sql脚本,里面包含建库、建表、插入初始数据的完整语句。导入时先建库再导入数据,顺序不要反。导入完成后检查三件套:系统管理员账号有没有初始化、收费标准有没有默认数据、表之间的外键关系有没有问题。默认管理员账号一般在脚本里通过 INSERT 语句写入,密码大多是 MD5 加密后的值,登录不了先查是不是加密方式不匹配。
3. 登录认证与权限拦截:从拦截器配置到 Session 失效处理
3.1 登录模块的实现思路
停车计费系统的后台管理需要登录才能访问,这是毕设里最基础但答辩必问的功能。这个项目采用传统 Session 方式的登录认证:用户提交用户名和密码,后端校验通过后把用户信息放入 Session,受保护的页面通过拦截器检查 Session 是否存在用户对象。相比 JWT,Session 方案在单体 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:interceptors>XML 配置里的 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.0,8.0 下要注意驱动版本,mysql-connector-java 5.x 连 8.0 会报认证协议错误,改用 8.x 驱动并把 URL 里的useSSL=false参数加上。
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.Driver,URL 里还需要加serverTimezone=Asia/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:PermSize=128M -XX:MaxPermSize=256M(JDK8 之前的写法)或-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m(JDK8 及以后)。如果你用的是 JDK8,PermGen 参数已经不生效,必须写 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 PageBean<T> { private int pageNum; // 当前页码 private int pageSize; // 每页条数 private long total; // 总记录数 private int pages; // 总页数 private List<T> 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; filename=parking_records.xlsx"); workbook.write(response.getOutputStream()); }这段代码的核心有三处:XSSFWorkbook 是 POI 里对应 xlsx 格式的工作簿对象,创建后往里面加 Sheet 和 Row;响应头的 Content-Type 必须设成 Excel 对应的 MIME 类型,否则浏览器可能无法识别下载文件的格式;Content-Disposition 里的 attachment 参数告诉浏览器这是一个需要下载的附件而不是直接在页面打开的内容。
加上这个功能之后,系统的完整度会明显高出一个档次:入场、计费、出场、月卡管理、收费记录、数据导出,一个真实的停车场管理系统该有的功能全齐了。我做毕设评审时看过几百份项目,含金量高的项目有一个共同特征——不只是把增删改查做完了,而是把业务闭环跑通了,尤其是类似数据导出这种细节功能对项目完整度的提升非常明显。
从那以后我拆项目、改别人的毕设源码,都会强制走一遍三件事:数据库脚本能不能一次跑通、静态资源有没有被拦截、计费结果手算和代码算是否一致。这三个点过了,项目基本就能稳定运行。希望帮到你,动手跑一遍比看十遍文章都管用。
本文还有配套的精品资源,点击获取