ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

停车管理系统毕设实战:Java全家桶与并发计费引擎设计

停车管理系统毕设实战:Java全家桶与并发计费引擎设计 1. 项目概述与需求拆解1.1 为什么选“万达广场停车管理系统”当毕设计算机类毕业设计每年题目都差不多XX管理系统、XX商城、XX平台真正能被评委一眼看中的不多。我当初选这个题目的第一个原因是停车管理系统听起来朴素但背后涉及的技术面非常广——Java基础、集合框架、MySQL索引、事务控制、并发处理、甚至接口对接几乎把本科阶段的核心技术点全串起来了。第二个原因很现实万达广场这类大型商业综合体是典型的停车场景车流密集、高峰集中、计费规则复杂、还有会员联动做出来的东西不是“玩具系统”是能直接讲出业务逻辑的项目。先明确一下这个系统到底要解决什么问题。万达广场的停车场通常有上千个车位工作日和周末的车流差异很大早晚高峰经常出现排队、找位难、出口拥堵这些情况。传统人工收费模式早就扛不住了所以需要一个能支撑“车牌识别入场 自动计费 会员优惠抵扣 出场快速结算”的智能管理系统。作为毕设我们不用去对接真实的摄像头硬件那需要硬件厂商开放SDK但系统架构和业务流程必须完整入场登记、车位占用标记、计费规则引擎、订单生成、收费记录、数据统计这些都要能在软件层面跑通。1.2 技术选型的核心思路为什么是Java全家桶我在选技术栈的时候没怎么犹豫就定了 Java Spring Boot MyBatis MySQL Redis。这几年凡是稍微沾点企业级的毕设项目基本绕不开这套组合。Java是学校教学主力语言大多数人从大二就开始写Java上手的心理门槛低。Spring Boot把SSM框架的繁琐配置全干了一个启动类搞定写代码的体验比传统SSM舒服太多。MyBatis是现在国内中小型项目最常用的持久层框架SQL手写可控导师问起来也容易讲清楚。MySQL存储核心业务数据Redis做车位状态缓存和热点数据加速。这套组合的优势在于第一它符合现在企业招聘里Java岗位的主流技术要求毕设做完简历上有东西写第二Spring Boot的自动配置机制大大降低了搭建成本第三MyBatis的动态SQL在实现复杂查询比如多条件筛选停车记录时非常顺手。顺带说一句我看到很多同学纠结要不要用SSHStrutsSpringHibernate我的建议是别碰那套东西在市场上基本退场了学完对找工作的帮助约等于零。2. 系统架构与核心功能模块设计2.1 整体架构前后端分离还是传统单体在做架构决策时我参考了目前主流开源项目的做法选了前后端分离。前端用Vue Element UI后端就是Spring Boot提供的RESTful API。这样分工明确前端只管页面渲染和交互后端只负责业务逻辑和数据持久化。对于毕设来说这样的架构显得更成熟答辩时也更容易展示项目的完整度。后端内部我分了四层Controller层负责接口接收参数、调用service、返回统一结果集。Service层业务逻辑的核心比如计费计算、入场校验、订单生成全部在这里处理。Mapper层DAO层定义数据库操作方法配合MyBatis的XML文件实现SQL映射。Entity层实体类对应数据库中的表结构。这个分层的好处是很直观地体现了“高内聚低耦合”的设计思想面试被问到架构问题时可以直接用项目里的代码举例。而且每一层之间只通过接口通信后续想替换实现方案比如把MySQL换成别的数据库只需要改对应层不需要动全局。2.2 功能模块到底拆成哪几块我画功能模块图的时候没搞花活直接按停车场的真实业务流转来拆。基础数据模块车位管理车位的编号、所在区域A区/B区/C区、类型普通车位、新能源充电车位、无障碍车位、当前状态空闲/占用/预约。收费标准管理不同区域、不同时段的费率配置支持自定义规则。这一步很关键因为万达广场这种商业体经常搞活动收费标准不能写死在代码里。核心业务模块车辆入场接收车牌号校验是否黑名单车辆生成停车记录车位状态置为占用记录入场时间。车辆出场根据车牌号查询停车记录计算停车时长和费用应用会员折扣完成支付后释放车位。车辆管理查询场内车辆列表、历史停车记录、月卡车辆到期提醒。会员管理会员等级、积分规则、优惠折扣、月卡绑定。这部分是为了体现万达广场这类商业综合体的“打通会员”需求。辅助支撑模块数据统计累计营收、日营收趋势、车位利用率、车辆类型分布。答辩时把这个页面展示出来直接碾压那些只做了增删改查的系统。系统管理用户登录、角色权限、操作日志。用Spring Boot拦截器实现简单的登录鉴权就够了。每个模块的功能点都要能从界面操作一路贯穿到数据库的增删改查答辩老师经常会顺着一条业务线追问比如“入场之后数据库状态怎么变的”“计费优先使用哪条规则”如果哪一环断了印象分就掉了。2.3 数据库表结构设计精讲数据库设计是整个项目的基石我建议所有准备做这个题目的同学在写代码前先花一两周时间把表设计搞清楚否则后面改起来会想哭。我设计表的思路是围绕“一条停车记录的主线”展开的。核心表有这么几张停车场表parking_lot记录停车场基本信息比如名称、总车位数、地址。虽然单体项目只有一个停车场但保留这张表让系统具备扩展性。车位表parking_space核心字段有车位编号space_no、所属区域zone、类型type、状态status、对应停车场ID。车位状态是高频更新字段在数据库里用tinyint存储0表示空闲1表示占用。车辆信息表vehicle车牌号plate_no作为业务主键还有车辆类型小型车/SUV/新能源、车主姓名、联系电话。非注册车辆入场时也要创建一条记录只是车主信息为空这样设计方便后续做“常客车辆”识别。停车记录表parking_record这是全系统最核心的表。字段包含记录ID、车牌号、车位ID、入场时间、出场时间、停车时长分钟、应收金额、实收金额、支付状态、支付方式。查询性能的关键在于给入场时间和车牌号建索引这两个字段是日常查询的高频条件。收费规则表charging_rule用规则引擎的思路设计支持配置免费时长、首小时价格、每增加一小时加收价格、单日封顶金额、生效时段工作日/周末/节假日。这样能灵活适配万达广场不同时期的运营策略。会员表member会员ID、手机号、等级、积分、绑定车牌。会员和车辆的关联通过车牌号来实现这也是停车场线下场景的常用做法——不需要额外办卡车牌即身份。支付记录表payment_record关联停车记录ID支付金额、支付时间、支付渠道微信/支付宝/余额用于财务对账。表之间的关系不复杂但一定要提前画清楚停车记录关联车位和车辆支付记录关联停车记录会员关联车辆。如果你打算在论文里画ER图这个模型完全够用。3. 关键技术难点与实现方案3.1 计费引擎看似简单实则容易翻车计费规则是停车系统的灵魂也是我写代码时返工最多的模块。万达广场的计费规则比普通路边停车复杂得多举个例子假设规则是入场前15分钟免费首小时8元不足1小时按1小时算有的商场按分钟计费有的按小时取整这里我用“按小时向上取整”演示之后每小时加收5元单日封顶40元周末/节假日费率浮动20%。同时会员还有额外折扣。如果用一堆if-else写逻辑会非常臃肿且容易遗漏组合情况。我的做法是把计费封装为一个独立的策略模块public FeeResult calculateFee(ParkingRecord record, ChargingRule rule) { long parkMinutes Duration.between(record.getEntryTime(), record.getExitTime()).toMinutes(); // 免费时长判断 if (parkMinutes rule.getFreeMinutes()) { return FeeResult.free(); } // 计费时长 总时长 - 免费时长 long billableMinutes parkMinutes - rule.getFreeMinutes(); // 按小时向上取整 int billableHours (int) Math.ceil(billableMinutes / 60.0); // 首小时 续增 BigDecimal total rule.getFirstHourPrice() .add(rule.getAdditionalHourPrice().multiply(BigDecimal.valueOf(billableHours - 1))); // 封顶控制 if (total.compareTo(rule.getDailyCap()) 0) { total rule.getDailyCap(); } // 会员折扣 if (memberDiscount ! null) { total total.multiply(BigDecimal.valueOf(1 - memberDiscount.getRate())); } return FeeResult.of(total); }这段伪代码不复杂但实际开发时会遇到几个坑我一个个说。坑一时长计算必须用“离开时间减去入场时间”不能直接除以1000然后取整。我第一次用(exitTime - entryTime) / 1000计算秒数再转分钟好不容易算出来结果遇到跨天停车直接时间差算错。后来统一用LocalDateTime和Duration来算所有问题迎刃而解。坑二金额计算必须用BigDecimal不要用double。计费涉及分分厘厘double的浮点误差在多次累加后会变大虽然单次最多差一两分钱但对账的时候会出大问题。这个知识点答辩时可以重点讲导师会很满意。坑三免费的15分钟到底是按“实际停车时长”还是“按小时向上取整后”来扣减两种逻辑都有人做我建议按实际分钟扣减后再对齐计费单位这样对用户更友好。如果先取整再扣免费时长相当于用户实际停1小时1分钟也要按2小时计费再扣掉免费部分体验很差。3.2 并发场景多个出口同时出场时数据怎么保证一致这个是我在自测阶段被自己坑出来的。当时用JMeter模拟50辆车同时出场设了50个并发请求结果数据库里出现了同一个车位被两辆车同时设置为核心占用还有个订单被扣了两遍钱——差一点就要被这个bug劝退。问题的本质是当多个请求同时读取同一车位状态、都判断为“空闲”、又同时写入“占用”时数据库在不加锁的情况下没法保证最终一致性。解决思路有三层第一在数据库层面给车位表的状态字段加上乐观锁版本号。更新之前先查一下version更新时带了版本号MySQL会拒绝同时更新同一行的操作这样至少有一个请求会失败可以返回给前端“车位已被占用”的提示。这是最简单且不会死锁的做法。第二在Service层用Redis的分布式锁SETNX 过期时间把“查询车位来源 → 检查状态 → 更新状态”这段临界区锁住。并发高的时候同一车位的请求只会放行一个其余等待。第三更彻底的做法是引入消息队列如RabbitMQ做削峰填谷但毕设场景完全没必要搞这么重。我最终采用的是“数据库乐观锁 Redis缓存车位状态 失败重试”的组合方案先把核心逻辑跑稳再在答辩中明确说出“如果后续要应对真正的高并发会引入MQ”的扩展方案。这句话能极大提升评分的上限。3.3 车牌识别不接硬件用模拟接口实现作为一个毕设我们的摄像头识别接口大概率是接不通的。但流程不能少我设计了一个PlateRecognizerService接口public interface PlateRecognizerService { // 模拟识别车牌实际场景调用摄像头SDK获取base64图像识别 PlateResult recognize(String imageUrl); }实现类里通过配置一个开关simulate: true来走模拟逻辑从请求参数拿车牌号处理逻辑后直接返回。如果硬件设备到位只需要切换实现类业务代码零改动。这个设计思路体现了“面向接口编程”也是面试官爱听的亮点。除了识别接口我们还做了黑名单校验VIP用户或月卡车从专用通道进入如果车牌在黑名单里接口会直接返回“禁止入场”并同步给管理员一个待处理任务表。这些细节虽小但让系统的业务流程显得特别完整。3.4 车位引导的轻量实现真实停车场里车位引导是靠地磁传感器和诱导屏来做的。毕设里我用了一个轻量方案在“车位地图”页面上用不同颜色的色块表示车位状态前端轮询后端接口每隔5秒请求一次车位状态列表实时刷新。数据量不大一张图只有几十个车位所以用最简单的定时轮询即可。后续如果要优化可以升级为WebSocket长连接推送。4. 实操过程从环境搭建到功能落地4.1 开发环境与工具清单先把环境列出来版本号我都标注了照着配就行JDK 1.8稳定为主别追新版本增加不确定性Maven 3.6.3MySQL 5.7 / 8.0我用8.0注意驱动依赖要对应Redis 5.x用作缓存和分布式锁IDEA 2022.1 / 2023.1Vue CLI 4.5前端脚手架Element UI前端UI库Postman接口调试环境变量配置属于基础操作但我还是说一句JDK和Maven的JAVA_HOME、MAVEN_HOME必须一致否则IDEA的Terminal里经常出现“java不是内部或外部命令”的报错这问题排查起来非常浪费时间。4.2 项目结构搭建一个能跑通的最小骨架我新建Spring Boot项目时会先把目录和基础依赖搭好而不是一上来就写业务。最简单的验证方式是新建一个TestController返回一个字符串跑起来看到接口通再进行后续开发。依赖方面核心就这几个spring-boot-starter-web提供Web能力 mybatis-spring-boot-starter数据库访问 mysql-connector-javaMySQL驱动 spring-boot-starter-data-redisRedis操作 lombok简化实体代码注意一个坑MyBatis和Spring Boot版本之间有兼容性问题。如果直接用最新版Spring Boot配旧版mybatis-spring-boot-starter启动时会报Failed to configure a DataSource看着像数据库连接问题实际是版本不匹配。我在pom.xml里把版本锁到了mybatis-spring-boot-starter 2.3.xSpring Boot用2.7.x系列这组搭配已经很成熟网上教程多出了问题也好查。数据库连接池我直接用了HikariCPSpring Boot默认连接URL要带参数jdbc:mysql://localhost:3306/wanda_parking?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueserverTimezone不加会报时区异常这个几乎新手必踩。第一次启动项目时遇到红色报错不要慌大多数是时区或版本问题逐个排查即可。4.3 核心流程实现入场与出场的完整链路入场接口的完整链路是这样的前端输入车牌号模拟摄像头识别→ 后端校验黑名单 → 创建车辆记录如果没有→ 选择空闲车位并锁定 → 生成停车记录 → 更新Redis缓存。每一步都有清晰的职责划分异常时能定位到具体环节。出场链路由两部分组成计算费用和完成支付。计算费用时调用前面讲的计费引擎支付是异步逻辑——当然毕设里也没有真的支付渠道我用了“模拟支付成功”的开关支付回调直接返回成功。但注意即使模拟也要在代码中预留出“支付成功后才能放行”的业务闭环否则整个系统就是一个漏洞百出的壳子。我把停车记录表的结构贴出来字段和类型都看得明白字段名类型说明idbigint主键自增plate_novarchar(20)车牌号space_idbigint关联车位IDentry_timedatetime入场时间exit_timedatetime出场时间初始为nullpark_minutesint停车分钟数total_feedecimal(10,2)应收金额member_discountdecimal(10,2)会员优惠金额actual_feedecimal(10,2)实收金额pay_statustinyint0未支付1已支付pay_timedatetime支付时间4.4 大屏统计页面让系统“能讲出故事”开发完核心业务后我专门花了三个晚上做了一个数据统计大屏页面今日营收、今日车流量、当前占用率、近7天营收折线图、车位利用率柱状图、车辆类型占比饼图。数据用每日定时任务汇总到统计表页面通过ECharts渲染。这个页面在答辩演示时很抓眼球而且它能体现你对“数据可视化”和“定时任务”两个技术点的理解属于性价比非常高的投入。要注意的是ECharts图表的data建议直接用后端聚合好的JSON不要在浏览器端做二次计算否则数据口径会不一致。比如“今日营收”应该只统计已支付订单不能把待支付订单算进去这涉及SQL里的 WHERE 条件设置。5. 常见问题排查与优化实录5.1 并发测试车位状态错乱用JMeter并发测试时发现两个请求同时入场会出现同一个车位被分配两次的情况。查了日志后发现两个线程都读到了“状态0”的车位然后都执行了UPDATE语句但没有版本控制。加乐观锁之后第二次UPDATE返回的影响行数为0业务层捕获到这个信号后自动重新查询车位并分配给下一辆核验的车辆。5.2 跨天后的计费金额错误半夜跨天测试时发现从前一天23:50停到第二天01:10应收金额算出来只有5元明显不对。原因是我用exitTime.getDayOfYear() - entryTime.getDayOfYear() 0判断跨天结果每年最后一天到第二年会得负数。后来全面改用LocalDateTime做差值计算彻底规避了这个问题。教训日期处理永远不要自己去手写“计算天数”用标准库的Duration或ChronoUnit。5.3 时间字段精度丢失MySQL 5.7及更早版本里datetime默认精度到秒。但我在代码里用LocalDateTime.now()写入时有时会带毫秒存入数据库后毫秒被丢掉。表面看影响不大但是当两个操作发生在同一秒内如果唯一约束建立在时间上就会冲突。我统一在写入前把毫秒清零withNano(0)并且设定业务以秒为最小单位。5.4 数据库连接被持久占用之前测试时有大量请求没有关闭连接MySQL报“Too many connections”。排查后发现是HikariCP最大连接数默认10但模拟并发设得太高导致连接池耗尽。解决方法是调大最大连接数到50并确保每个Service方法的事务边界清晰避免长事务占用连接。5.5 Redis和MySQL双写不一致车位状态同时存在Redis和MySQL会出现缓存更新失败后两者数据不一致的情况。我的策略是先更新数据库再异步同步到Redis如果同步失败Redis里的数据设置了10分钟过期时间过期后会自动从数据库回源。6. 答辩与面试经验总结如果你做的也是这个题目这章建议多看两遍。毕业答辩和找工作时评委/面试官对停车管理系统的关注点我总结下来有这么几个6.1 你为什么选这个题目项目有什么亮点别把“因为简单”挂在嘴边哪怕你心里真是这么想的。换个说法“停车管理系统的核心价值在于高频并发场景下数据一致性的处理以及计费引擎的灵活性设计。这让我在有限时间内能完整地实践一套企业级开发流程。”这句话一出来评委的注意力会直接从“是不是抄的”转为“他好像真的懂”。6.2 技术问题高频清单Spring Boot的自动配置原理是什么你怎么理解“约定大于配置”MyBatis中 #{} 和 ${} 的区别是什么为什么不能随便拼接SQL你怎么处理高并发场景下的并发安全Redis在你系统里具体用来做了什么为什么值得用MySQL索引在 parking_record 表中是怎么设计的每个问题都要能结合自己项目的实际代码回答不要背课本定义。比如谈到索引就说我在plate_no和entry_time上建了联合索引并用EXPLAIN验证了查询走索引这比单纯说“我建了索引”有说服力得多。6.3 后续扩展方向如果要做成生产级系统可以补充的方向对接真实车牌识别摄像头替换模拟识别服务。引入消息队列做异步订单处理应对早晚高峰流量冲击。接入微信/支付宝真实支付沙箱环境。升级为微服务架构车位服务、订单服务、会员服务独立部署。增加节假日前预约停车功能通过Redis预占车位并短信提醒。但务必要说明这些都是“扩展点”现阶段已完成的是一个覆盖核心业务链路的可运行系统。最后说点我自己的感受。做这套系统前后花了大概三周其中第一周全花在表设计和计费规则梳理上代码真正写起来反而快。走过最大的弯路就是过早动手写代码结果写到计费模块时发现表结构少了个字段连夜补表连带一堆接口逻辑返工。如果你也在做类似题目建议按这个顺序推进先画清楚的ER图和业务流转图 → 再写核心Service层 → 最后补Controller和前端页面。表设计没想清楚之前绝对不要碰代码。另外项目里一定要预留日志打印每个关键操作入场、出场、支付都要有日志排查问题的时候省下的时间远超记录日志的消耗。这个项目做完之后我自己对Spring Boot、MyBatis和并发控制的理解都提升了一个档次面试时也很容易把话题引到自己真正做过的功能上。祝所有正在赶毕设的同学都能顺利过关答辩不慌Offer多多。
返回列表