每次看到毕设选题清单,我都是同一个感受:题目那么多,真正适合拿来练手又能完整做下来的其实没几个。如果你问我在 Java Web 方向里哪个题目最“划算”,我会毫不犹豫提停车场管理系统——它看起来不起眼,但登录权限、车位状态流转、计费结算、报表统计,这些典型管理系统的核心模块它一个不落。做一个这样的系统,你不仅能串起 Spring Boot、MyBatis、MySQL 这些主流技术,还能在论文和答辩阶段拿出一套“业务逻辑完整、代码结构清晰”的作品,性价比非常高。
这篇文章就围绕“基于 Java 的停车场管理系统的设计与实现”展开,我会从选题思路、技术栈选择、数据库设计、核心功能实现,到真实开发中容易踩的坑,全部梳理一遍。内容偏实战,准备好抄作业就行。
1. 项目概述:这个系统要解决什么问题
1.1 选题定位与目标场景
停车场管理系统本质上是“资源管理 + 计费结算”的业务系统。小型停车场经营者每天最头疼的无非这么几件事:某个时段还有多少空位、车辆停了多久该收多少钱、月租车主和临时车主怎么区分、进出记录怎么留存。
这套系统就是把线下的手工登记流程搬到线上。车辆入场时登记车牌、记录入场时间;出场时根据停车时长按规则计费;管理员在后台能看到实时的车位占用情况和历史记录。想象一下:没有系统之前,保安拿个本子记车牌,下班前对着本子算账,偶尔漏记一笔就说不清楚。有了系统之后,每一步操作都有记录,钱和车都能对上账,这就是它最核心的价值。
1.2 核心功能拆解
先给整个系统的功能范围画个边界,免得做着做着就跑偏。按角色划分,一共三类用户:管理员、月租车主、临时车主(临时车主一般不自助操作,由岗亭管理员代操作)。
| 功能模块 | 需求描述 | 优先级 |
|---|---|---|
| 用户登录与权限 | 管理员登录后台,区分超级管理员和普通操作员 | 高 |
| 车位管理 | 车位编号维护、占用/空闲状态实时更新 | 高 |
| 车辆入场 | 登记车牌、选择车辆类型(临时/月租)、记录入场时间 | 高 |
| 车辆出场 | 识别车牌、计算停车时长和费用、完成缴费 | 高 |
| 计费规则配置 | 自定义首小时费用、超时单价、24小时封顶价 | 高 |
| 月租车管理 | 月租车辆绑定、有效期管理、到期提醒 | 中 |
| 数据统计 | 今日营收、进出车流量、车位利用率等基础报表 | 中 |
| 操作日志 | 记录关键操作,方便对账和追责 | 低 |
功能范围这样列出来之后,“系统边界”就清楚了。不建议再往里加会员积分、线上预约、车位导航这类功能,毕设阶段把上面这些做实做细,比堆十个半成品功能有用得多。另一个好处是,开题报告里的“研究内容”章节直接照这个功能列表展开写就行,不用绞尽脑汁编方向。
2. 技术选型:为什么这套组合最稳
2.1 开发语言与框架选型
先回答一个很多人纠结的问题:纯 Java SE 写控制台程序行不行?行,但那是十年前的做法。到了今天,Java 后端开发基本被 Spring Boot 统治,企业里在用,教程里在教,答辩时老师也认。选择一个技术栈不能只问“能不能实现”,还要问“有没有参考、有没有人答疑、面试时有没有用”。
推荐的主流组合是这样:
- 后端框架:Spring Boot 2.7.x + MyBatis-Plus。Spring Boot 负责把项目跑起来,自动配置省掉大量 XML 配置;MyBatis-Plus 在 MyBatis 基础上做了增强,单表 CRUD 不用手写 SQL,这对开发效率的提升非常明显。
- 前端方案:Vue 3 + Element Plus 做管理后台,或者直接用 Thymeleaf 服务端渲染。如果你前端基础薄弱,选 Thymeleaf 更稳,毕竟 Java 后端做服务端渲染,逻辑都在 Java 里,不用跨语言调试。
- 数据库:MySQL 8.0,稳定、教程多、云服务器上随便装。
- 构建工具:Maven,管理依赖一把好手。
- 鉴权方案:Sa-Token 或者 JWT,二选一。Sa-Token 的登录鉴权 API 对初学者友好,JWT 则更“通用”一点,面试被问到的概率高。
这套组合的核心逻辑是:每一层选的都是“生态最成熟、踩坑资料最多”的方案。你不是在搞技术研究,是在做一个能顺利交付的项目,稳定压倒一切。我在实际开发中见过太多人因为“想用点新东西”选了冷门框架,结果遇到问题连报错信息都搜不到,白白浪费时间。
2.2 存储方案设计:MySQL 够了
停车场管理系统涉及的数据量,撑死一天几千条出入场记录,MySQL 处理这种量级绰绰有余。有人说“用 Redis 做缓存提升性能”——这话本身没错,但对这个项目来说不是必须的。先把 MySQL 表结构设计好、索引建对,查询速度就已经很快了。我的建议是:Redis 可以作为加分项放在论文的“系统优化”章节里提一嘴,比如缓存车位状态、缓存计费规则,但核心业务不要依赖它。主从复制、分库分表这些,听个概念就行,硬塞进毕设反而显得不伦不类。
2.3 开发环境与版本管理
一个很实际的建议:环境最好统一。我见过有人用 JDK 8,有人用 JDK 17,最后联调时 Spring Boot 版本对不上,折腾半天。这里给一套经过验证的组合:
| 组件 | 版本/方案 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | 这两个版本跟 Spring Boot 2.x 兼容最稳 |
| IDE | IntelliJ IDEA | 社区版即可,别在这上面纠结 |
| 数据库工具 | Navicat 或 DataGrip | 可视化操作,建表时能直接看到结构 |
| 接口测试 | Postman 或 Apifox | 后端接口写完先自测,别急着联调 |
| 版本管理 | Git + Gitee/GitHub | 每完成一个模块提交一次,养成习惯 |
环境变量配置这块容易出问题,Java 装好了但java -version报错,八成是JAVA_HOME没配好或者 Path 里没加%JAVA_HOME%\bin。配置完记得新开一个命令行窗口再验证。另外注意,装了多个 JDK 版本时,Path 里的顺序决定了实际生效的是哪个版本,这不难排查但很容易被忽略。
3. 数据库设计:先把表的骨架搭对
3.1 核心表结构说明
数据库是管理系统的地基,表结构设计得好不好,直接决定后面写代码是省心还是闹心。停车场管理系统最核心的抽象就三个词:人(用户/管理员)、资源(车位)、行为(出入场记录)。围绕这三个词,我设计了下面这些表:
用户表(sys_user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 登录名,唯一 |
| password | varchar(100) | BCrypt 加密后的密码 |
| real_name | varchar(50) | 真实姓名 |
| role | varchar(20) | admin / operator |
| status | tinyint | 1启用 0禁用 |
| create_time | datetime | 创建时间 |
车位表(parking_space)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| space_no | varchar(20) | 车位编号,A-001 |
| status | tinyint | 0空闲 1占用 |
| type | tinyint | 0普通车位 1月租专用 |
| remark | varchar(255) | 备注 |
出入场记录表(parking_record)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| plate_number | varchar(20) | 车牌号 |
| space_id | bigint | 占用的车位 ID |
| car_type | tinyint | 0临时 1月租 |
| entry_time | datetime | 入场时间 |
| exit_time | datetime | 出场时间,NULL 表示仍在场 |
| fee | decimal(10,2) | 应收费用 |
| status | tinyint | 0在场 1已离场 |
计费规则表(fee_rule)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| rule_name | varchar(50) | 规则名称 |
| first_hour_fee | decimal(10,2) | 首小时费用 |
| extra_hour_fee | decimal(10,2) | 超出部分每小时费用 |
| max_daily_fee | decimal(10,2) | 24小时封顶费用 |
| enabled | tinyint | 是否启用 |
月租车辆表(monthly_car)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| plate_number | varchar(20) | 车牌号 |
| owner_name | varchar(50) | 车主姓名 |
| phone | varchar(20) | 联系电话 |
| start_date | date | 有效期开始 |
| end_date | date | 有效期结束 |
| status | tinyint | 0正常 1过期 |
一个容易忽略的细节是:计费规则一定要独立成表,别写死在代码里。写死规则意味着改一次价格就要改代码、重新打包、重新部署。独立成表后,管理员在后台把首小时费用从 5 块改成 7 块,保存就生效,这才叫“可配置的系统”。这个点放到论文里可以引申为“规则引擎的设计思想”,挺好写的。
3.2 关键字段设计的坑
说几个数据库设计阶段的实战经验,都是从教训里换来的。
第一,金额一律用decimal类型,不要用float或double。二进制浮点数在计费场景下的精度损失问题,做过金融相关开发的人都懂。假设停车费是 7.2 元,浮点数可能存成 7.1999999。用decimal(10,2)老老实实存两位小数,后面计算也不容易出幺蛾子。
第二,车牌号字段长度至少给到 10 个字符。新能源车牌比普通蓝牌多一位,还有各种临时牌照的字符组合,给少了后面扩字段非常痛苦。
第三,记录表的entry_time上加索引。这是一张表查询频率最高的条件字段——查当前在场车辆按“入场时间为空”过滤,查历史记录按时间范围过滤。没有索引的话,记录一多,查询慢的问题就来了。
第四,不要为了“省事”把场内的车辆记录和离场的历史记录拆成两张表。有些人觉得拆开后查询性能更好,但对这种数据量来说完全没必要,反而让统计报表的 SQL 变得复杂。一张parking_record表,外加一个status字段区分在场/已离场,足够了。
3.3 数据初始化与演示数据
表结构建好之后,最好写一个初始化 SQL 脚本,把管理员账号、基础计费规则、模拟车位数据(比如 50 个车位)一次性插入。这个操作在开发时能极大提高效率,因为你每次重新初始化数据库,不需要手动录一堆测试数据。
-- 初始化管理员账号,密码为 BCrypt 加密后的 "123456" INSERT INTO sys_user (username, password, real_name, role, status, create_time) VALUES ('admin', '$2a$10$X7GpFmOZ1D8yJ6cB3xHxUO2yW9Q6C3nOyG3t1AqQy8LrT2VgJ1xKq', '系统管理员', 'admin', 1, NOW()); -- 初始化 50 个普通车位 INSERT INTO parking_space (space_no, status, type, remark) SELECT CONCAT('A-', LPAD(n, 3, '0')), 0, 0, '普通车位' FROM (SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t1 CROSS JOIN (SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t2 CROSS JOIN (SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t3 CROSS JOIN (SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t4 CROSS JOIN (SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t5 LIMIT 50;上面生成车位的 SQL 用了笛卡尔积的方式快速生成 1 到 50 的序列,再配合LPAD补零成三位编号。这种写法比一条一条 insert 高效得多,也算面试时能拿出来讲两句的小技巧。
4. 核心功能实现:从登录到离场结算
4.1 登录认证与权限拦截
登录模块虽然技术上不复杂,但它是整个系统的门面,也是问答时老师爱问的点。接口设计上,提供一个POST /api/auth/login,接收用户名和密码,验证通过后返回一个 token,前端后续请求在请求头里带上这个 token。
用 Sa-Token 实现登录的逻辑非常简洁:
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private SysUserService userService; @PostMapping("/login") public Result<?> login(@RequestBody LoginDTO dto) { SysUser user = userService.login(dto.getUsername(), dto.getPassword()); if (user == null) { return Result.error("用户名或密码错误"); } // Sa-Token 登录,底层会生成 token 并维护会话 StpUtil.login(user.getId()); // 返回给前端的信息 Map<String, Object> data = new HashMap<>(); data.put("token", StpUtil.getTokenValue()); data.put("userInfo", user); return Result.ok(data); } }权限拦截这块,建议用 Sa-Token 的注解式鉴权,在需要管理员权限的接口上标@SaCheckRole("admin"),代码层面很干净。一个容易踩的坑是前端请求里 token 的传递方式,Sa-Token 默认从请求头satoken读取,如果你前端用的是Authorization头,需要在配置里改一下,前后端不一致的话会一直 401。
4.2 车位管理与空闲查询
车位管理最核心的操作就是“分配”——车辆入场时找一个空闲车位并把它置为占用,出场时把车位释放回空闲。这里一定要把“查询空闲车位”和“更新车位状态”放在一个事务里,否则高并发场景下,两辆车同时入场可能查到同一个车位。
一个实用的方案是给parking_space表加一个version字段做乐观锁,更新时带上版本号,更新影响行数为 0 说明这个车位已经被别人抢了。
@Transactional(rollbackFor = Exception.class) public Long assignSpace(String plateNumber) { // 1. 查询一个空闲车位 LambdaQueryWrapper<P parkingSpace> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ParkingSpace::getStatus, 0).last("LIMIT 1 FOR UPDATE"); ParkingSpace space = parkingSpaceMapper.selectOne(wrapper); if (space == null) { throw new BizException("停车场已满"); } // 2. 车位置为占用 space.setStatus(1); parkingSpaceMapper.updateById(space); // 3. 创建入场记录 ... }很多人会忽略last("LIMIT 1 FOR UPDATE")这一步。这在 MySQL 行锁的机制里叫悲观锁,事务 A 查到这条空闲车位并锁定,事务 B 再查时只能等 A 提交。用悲观锁还是乐观锁是面试常考题,实际开发里,车位分配这种“强一致”场景用悲观锁更稳妥,但要注意事务别开得太大,锁住的时间越短越好。
4.3 计费规则与结算流程
计费是整个系统里业务逻辑最复杂的部分,也是答辩时的核心亮点。基础的计费流程是:车辆出场时,用当前时间减去入场时间得到停车时长,再按计费规则计算费用。
先梳理一下规则:首小时内 5 元,超过 1 小时的部分每小时 3 元(不足 1 小时按 1 小时算),24 小时内封顶 30 元。这里的核心难点有两个:一是“不足 1 小时按 1 小时”的向上取整逻辑;二是跨天场景下封顶金额怎么算。
向上取整在 Java 里的实现是(totalMinutes + 59) / 60,或者用Math.ceil()。跨天封顶的逻辑要更仔细:很多系统直接算总时长,然后按首小时 + 超出小时计价,再跟封顶值比较取较小者。这种算法在“总时长超过 24 小时”时会出问题——比如停了 25 小时,按封顶 30 元算,明显不合理,应该是 30 + 5 = 35 元(第一个 24 小时封顶 30,超出的 1 小时按规则另算)。
正确的算法是“按天拆段”:
public BigDecimal calcFee(LocalDateTime entryTime, LocalDateTime exitTime, FeeRule rule) { long totalMinutes = Duration.between(entryTime, exitTime).toMinutes(); if (totalMinutes <= 0) { totalMinutes = 1; // 入场出场在同一分钟内,按 1 分钟算 } // 拆成整天 + 余数小时 long days = totalMinutes / (24 * 60); long remainMinutes = totalMinutes % (24 * 60); BigDecimal fee = rule.getMaxDailyFee().multiply(BigDecimal.valueOf(days)); if (remainMinutes > 0) { // 剩余时长按首小时 + 超出小时计算 BigDecimal remainFee; if (remainMinutes <= 60) { remainFee = rule.getFirstHourFee(); } else { long extraHours = (remainMinutes - 60 + 59) / 60; remainFee = rule.getFirstHourFee() .add(rule.getExtraHourFee().multiply(BigDecimal.valueOf(extraHours))); } // 不超过封顶值 remainFee = remainFee.min(rule.getMaxDailyFee()); fee = fee.add(remainFee); } return fee; }这段逻辑里有两个边界条件容易被忽略:一是totalMinutes为 0 的情况,必须向上兜底为 1,否则会出现“停车 0 分钟也要收费”或者“免费离场”的纠纷;二是剩余时长的费用不能超过单日封顶。这两个细节写进论文的“系统测试”部分,整篇论文的严谨度会提升不少。
4.4 数据统计报表
报表模块是很多答辩组的检查重点,因为它最能体现一个系统从“能用”到“好用”的差距。我最常写的三个统计接口是:
- 今日营收:
SELECT SUM(fee) FROM parking_record WHERE status = 1 AND exit_time >= CURDATE() - 当前在场车辆数:
SELECT COUNT(*) FROM parking_record WHERE status = 0 - 车位利用率:在场车辆数 / 总车位数量,再用百分比展示
@GetMapping("/dashboard/summary") public Result<DashboardVO> summary() { DashboardVO vo = new DashboardVO(); vo.setTodayIncome(recordMapper.sumTodayIncome()); vo.setParkingCount(recordMapper.countParking()); vo.setTotalSpaces(spaceMapper.selectCount(null)); vo.setUsageRate(BigDecimal.valueOf(vo.getParkingCount()) .multiply(BigDecimal.valueOf(100)) .divide(BigDecimal.valueOf(vo.getTotalSpaces()), 2, RoundingMode.HALF_UP)); return Result.ok(vo); }写统计接口时注意一个求和结果可能为 NULL 的问题——当天还没有任何车辆离场时,SUM(fee)返回的是 NULL 而不是 0,需要在 SQL 里用IFNULL(SUM(fee), 0)处理,否则前端拿到 null 会直接渲染错误。这种小细节,真实开发中经常遇到,很影响使用体验。
5. 实战中踩过的坑:并发、时间与精度
5.1 并发扣费与超卖问题
这是系统运行起来之后最容易暴露的问题。设想一个真实场景:停车场还剩最后一个车位,同时有两辆车开到入口,两个岗亭管理员几乎同时点击“入场确认”。如果代码没有做并发控制,两辆车都可能查询到“那个空闲车位”,然后各自创建一条入场记录——超卖了。
解决思路前面已经提过,在事务里FOR UPDATE锁行。但还有一个细节:如果parking_space表里所有车位的状态都是 0(空闲),那每次查询锁定的都是所有空闲行,并发性能会受影响。好在停车场管理系统这量级的数据量,这种锁粒度完全没问题,不用过度优化。面试官如果问到这里,你能说出悲观锁和乐观锁的适用场景,这个问题的分数就到手了。
另外一个跟并发相关的经典坑是“重复提交”。管理员快速点了两次“出场结算”,结果生成了两条离场记录。解决方案是前端按钮加 loading 状态,后端在结算接口加分布式锁或者用数据库唯一索引兜底。毕设阶段不用上 Redisson 这么重的方案,在后端用synchronized对车牌号加锁,然后在代码里判断记录状态:
synchronized (plateNumber.intern()) { ParkingRecord record = recordMapper.selectByPlateNumberAndStatus(plateNumber, 0); if (record == null) { throw new BizException("该车辆没有在场记录"); } // 正常结算流程... }这个写法简单有效,不用引入额外组件,也能挡住绝大多数重复请求。
5.2 时间计算与跨天停车
本地开发时,计费功能跑得好好的,一部署到云服务器上就出问题——费用算错了。排查下来你会发现,问题出在服务器时区。中国的服务器默认时区大多是 CST,但 MySQL 的时区设置如果不对,存进去的时间和查出来的时间可能差 8 个小时,计费自然不准。
防坑方案有两条线:第一,MySQL 连接串里显式加上serverTimezone=Asia/Shanghai;第二,Java 后端统一用LocalDateTime,不要用Date。LocalDateTime是自带时区概念的(虽然是本地时区),在跨时区场景下比Date安全得多。另外要注意,entry_time和exit_time一旦写入就不要直接改字段值,计费依据是这两个时间点的差值,任何对它的修改都会让账单变得不可信。
还有跨天场景。停车时间从今天 23:30 到明天 01:00,总计 90 分钟,这种记录如果用“自然日”来分组统计营收,就会导致费用被记到出场日而非入场日。做日报统计时,到底按入场日还是出场日算当天的营收,这个口径要在设计阶段就定下来。我的建议是统一按“出场日”统计,因为只有车辆出场时费用才确定,这样财务对账的逻辑是自洽的。
5.3 BigDecimal 与整数分
Java 的double做金额运算时出现精度丢失,是新手最容易踩的坑,而且这个坑藏得很深。比如0.1 + 0.2在浮点数体系里不等于0.3,而是一个很长的带小数位的数字。停车场几块钱一次的计费如果出现这种问题,金额对不上账,用户投诉起来非常麻烦。
两条路可选:一是全链路用BigDecimal,构造时用字符串构造器new BigDecimal("5.00"),不要用new BigDecimal(0.1)——后者照样有精度问题;二是在数据库里把金额改成整数分存储,即 1 元存成 100,展示时再除以 100。我实际开发更推荐整数分方案,因为BigDecimal在序列化和前端交互时偶尔还是会有格式上的麻烦,整数分从源头杜绝浮点数,计算统统用long,性能也好。
private Long calcFeeInCents(long totalMinutes, FeeRule rule) { long firstHourFeeCents = rule.getFirstHourFeeCents(); long extraHourFeeCents = rule.getExtraHourFeeCents(); long maxDailyFeeCents = rule.getMaxDailyFeeCents(); long days = totalMinutes / (24 * 60); long remainMinutes = totalMinutes % (24 * 60); long feeCents = maxDailyFeeCents * days; if (remainMinutes > 0) { long remainFeeCents; if (remainMinutes <= 60) { remainFeeCents = firstHourFeeCents; } else { long extraHours = (remainMinutes - 60 + 59) / 60; remainFeeCents = firstHourFeeCents + extraHourFeeCents * extraHours; } remainFeeCents = Math.min(remainFeeCents, maxDailyFeeCents); feeCents += remainFeeCents; } return feeCents; }核心思想就是“全都用整数计算,到最后一步再转成用户看得懂的元”。细心的读者会发现这个版本比前面的BigDecimal版本简洁得多,性能也更好。这算是实际开发沉淀下来的经验,能用整数解决的事,别引入浮点。
6. 系统测试与部署上线
6.1 功能测试要点
系统做完不能光对着代码傻看,得跑一轮有章法的测试。我的习惯是画一张“功能测试用例表”,把每个模块的关键路径和边界情况列出来,一项一项过。
| 模块 | 测试场景 | 预期结果 |
|---|---|---|
| 登录 | 正确账号密码 | 登录成功,返回 token |
| 登录 | 错误密码 | 提示“用户名或密码错误” |
| 车位分配 | 有空闲车位时入场 | 分配成功,车位状态变占用 |
| 车位分配 | 无空闲车位时入场 | 提示“停车场已满” |
| 计费 | 停车 30 分钟 | 按首小时收费 |
| 计费 | 停车 2 小时 10 分 | 首小时 + 2 个超出小时 |
| 计费 | 停车 25 小时 | 24 小时封顶 + 超出 1 小时费用 |
| 计费 | 入场和出场在同一分钟 | 不报错,按最小值计费 |
| 月租车 | 有效期内出场 | 不计算费用,正常放行 |
| 月租车 | 过期后出场 | 按临时车规则计费 |
这里想特别强调“停车 25 小时”这个用例,很多实现版本在这里直接算错。如果测试时能把这个场景跑对,说明计费逻辑的核心没有问题。
6.2 部署与运行
开发环境跑通了,接下来是部署。毕设答辩一般需要现场演示,稳妥的做法是本地演示为主、云服务器部署为辅。本地演示的好处是不依赖网络,不用现场连数据库、等云服务器慢吞吞地响应;云服务器部署的意义在于给老师提供一个随时能访问的线上地址,印象分会好很多。
云服务器部署的步骤并不复杂,流程是:
- 服务器安装 JDK 和 MySQL(用宝塔面板的话,一般在面板里点两下就能装好)。
- 把项目用 Maven 打包成 jar 包:
mvn clean package -DskipTests。 - 通过面板或
scp命令把 jar 包上传到服务器。 - 用
nohup java -jar parking-system.jar > app.log 2>&1 &启动。 - 确认 8080 端口能在安全组里放行,然后通过
http://服务器IP:8080访问。
启动后注意看日志文件里有没有报错。Java 项目最常见的启动问题无非三种:端口被占用(java.lang.Exception: Socket bind failed)、MySQL 连接不上(Communications link failure)、数据库账号权限不对(Access denied for user)。每个问题的日志里都带着明确的关键字,按关键字搜解决方案就行。
6.3 顺手解决 Lombok 和环境问题
开发过程中还经常碰到一些跟业务无关的环境类报错,最常见的就是 Lombok 突然不生效。IDEA 里启动项目时爆出一句you aren't using a compiler supported by lombok, so lombok will not work,这个报错的根源一般是 Lombok 版本和 JDK 版本不兼容,比如 JDK 17 配了一个老版本的 Lombok。解决办法很直接:把 Lombok 升级到 1.18.30 以上,同时在 IDEA 里装上 Lombok 插件并开启注解处理(Settings → Build → Compiler → Annotation Processors)。
另外,如果你在java.sun.misc这类包上看到找不到类的错误,比如uncaught exception java.lang.NoClassDefFoundError: java/applet/Applet,大概率是你用了非常老的 JDK 依赖,或者项目里引用的某个库使用了已删除的 JDK 内部 API。这种错误最省事的处理方式是:确认项目 JDK 版本统一为 8 或 11,然后检查依赖树,把带有sun.misc依赖的老库剔除出去。
7. 这套系统还能怎么扩展
说完了系统实现,最后聊聊扩展方向。这部分内容虽然不一定要写进开题报告,但可以放到论文结尾的“展望”里,也能在答辩时作为加分话题。
- 车牌识别接入:在入口和出口部署摄像头,通过 OpenCV 或者第三方 OCR SDK 自动识别车牌,实现“无感进出”。这个扩展点技术含量高,也是停车场系统的真实业务方向。
- 线上缴费:接入微信/支付宝支付,用户扫码支付后 15 分钟内离场不重复计费。支付回调、订单状态机、超时关单,这些逻辑写出来又是一章内容。
- 预约车位功能:用户 App 端预约空闲车位,保留 30 分钟,超时自动释放。这个功能对“车位状态”模型的要求更高,需要引入预约状态。
- 设备对接:道闸、地磁感应、LED 余位屏……这些硬件对接一般走串口或者 HTTP 协议,能打通一个就算很厉害了。
我个人的观点是,扩展方向挑一个最感兴趣的去深挖,比每个都浅尝辄止要好。答辩时老师问“如果让你继续完善,你会做什么”,你说得出一个具体方向、能讲清楚技术难点和实现思路,这道开放题就已经答得很漂亮了。
做这个系统前后,我自己最深的体会是:技术本身不难,难的是把一堆琐碎的边界情况考虑周全。从入口到出口,从车位分配到计费结算,每一步都有“看起来没错但实际会崩”的地方。希望这篇文章能帮你少走一些弯路——如果你正卡在某个具体的实现细节上,比如计费算法的边界、某个报错始终解不掉,按着上面的思路去排查,大概率能找到答案。