尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Java停车场管理系统课设指南:表结构、计费与并发控制实战

发布时间:2026/9/25 1:58:28

资讯中心
01
ARTICLE

Java停车场管理系统课设指南:表结构、计费与并发控制实战

Java停车场管理系统课设指南:表结构、计费与并发控制实战
简介一份基于Java的停车场管理系统设计与实现文档面向计算机专业学生、课程设计或毕业设计开发者也可供在职开发人员快速了解B/S架构管理系统的实现思路。无论是完成课设还是进行技术预研都具有参考意义。文档以城市停车难为切入点系统阐述了课题背景与意义、国内外研究现状、开发环境搭建、可行性分析、系统流程设计、功能模块划分、数据库E-R图设计及主要数据表字段说明、界面设计、系统测试等关键环节技术选型上覆盖了Java、MySQL、SpringBoot和VUE框架并针对用户管理、车辆信息管理、停车位管理、收费管理等模块给出了设计要点这些模块的拆分方式也值得同类系统设计时参考。资源包仅含一个DOCX文件大小约937KB已有53人学习内容预览显示了从摘要到数据库设计的章节结构便于快速查阅相关部分。整体而言这份资源既能帮助理清整个项目的设计脉络也能作为相关技术论文的写作范本实用价值较高。1. 基于 Java 的停车场管理系统这门课设到底在做什么如果你打开这个标题的文档看到的通常是一套经典的三层结构后端业务逻辑、数据库表设计、前端交互页面。停车场管理系统几乎占据了 Java 课程设计的一半选题不是因为简单而是因为它覆盖了 Java 后端开发最常被问到的知识点——面向对象建模、JDBC 或 MyBatis 操作、事务处理、并发控制。这个标题指向的系统核心要管的是车辆进场、车位分配、计费出场、月卡管理四件事。适合两类人一类是交课设的在校生需要一套能答辩、能演示、能讲清楚设计理由的完整项目另一类是刚入门 Java 服务端开发、想用一个小项目把 Spring Boot 和数据库实践串起来的人。这篇文章不帮你抄代码而是把一个能跑、能演示、能应对提问的停车场系统设计思路和落地路径讲透包括表结构、计费逻辑、并发坑点和扩展方向。2. 技术选型与架构设计为什么用 Spring Boot MyBatis 而不是纯 JSP2.1 技术栈选型课设加分和实际落地之间的平衡点标题写的是“基于 Java”但 Java 生态里能做的方案差得很远。最常见也最稳的组合是 Spring Boot MyBatis MySQL Vue或 Thymeleaf。很多在校生会选 JSP Servlet JDBC 的“纯 Java Web”方案因为这是 Java Web 课程教的路线。这个方案的优点是答辩时能讲清楚每个请求从 JSP 到 Servlet 到 DAO 的完整链路缺点是开发效率低且脱离了现在企业里实际的开发方式。我一般会建议如果是课程设计且你还没学过 Spring Boot用 JSP Servlet 没关系但要在设计文档里写清楚“后续可迁移到 Spring Boot 的理由”如果你已经会 Spring Boot直接用 Spring Boot 做答辩时反而能解释“为什么选型更合理”。这里有一个容易被问倒的问题——为什么不用 Python Flask 或 Go答案不是“Java 更好”而是“这个系统的核心价值在事务和并发控制Java 的 Spring 生态对事务管理有成熟的声明式方案”这才是技术选型的真实理由。不要为了“看起来高级”引入 Redis、RabbitMQ 这类中间件停车场管理系统在课设规模下用不上。如果答辩被问到“高并发怎么办”就把数据库索引和事务隔离级别说清楚比堆技术名词更稳。2.2 数据库表设计五张核心表与它们之间的关系停车场系统的表结构是答辩时最容易展开讲的部分。最少需要五张表车位表parking_space、车辆表vehicle、入场记录表entry_record、出场记录表exit_record也可以合并成一张进出记录表、月卡表monthly_card。下面是一个可直接建表的 SQL 设计去掉了冗余字段保留了答辩时能讲出“为什么这样设计”的关键列CREATE TABLE parking_space ( id INT PRIMARY KEY AUTO_INCREMENT, space_no VARCHAR(10) NOT NULL UNIQUE COMMENT 车位编号A-01、B-12, status TINYINT DEFAULT 0 COMMENT 0-空闲 1-占用 2-锁定, area VARCHAR(20) DEFAULT A区 COMMENT 区域划分, rate DECIMAL(6,2) DEFAULT 5.00 COMMENT 该区域每小时费率, INDEX idx_status (status) ) ENGINEInnoDB; CREATE TABLE vehicle ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL UNIQUE COMMENT 车牌号, owner_name VARCHAR(50), owner_phone VARCHAR(20), vehicle_type TINYINT DEFAULT 0 COMMENT 0-小型车 1-大型车 2-新能源, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE entry_record ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL, space_id INT COMMENT 分配的车位ID可为空, entry_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, entry_image VARCHAR(255) COMMENT 入场抓拍图片路径, is_monthly TINYINT DEFAULT 0 COMMENT 是否月卡入场, UNIQUE KEY uk_plate_entry (plate_no, entry_time) ) ENGINEInnoDB; CREATE TABLE exit_record ( id INT PRIMARY KEY AUTO_INCREMENT, entry_record_id INT NOT NULL COMMENT 关联入场记录, plate_no VARCHAR(20) NOT NULL, exit_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, duration_minutes INT COMMENT 停车时长分钟, total_amount DECIMAL(8,2) COMMENT 应收金额, actual_amount DECIMAL(8,2) COMMENT 实收金额, pay_method TINYINT DEFAULT 0 COMMENT 0-现金 1-微信 2-支付宝 3-月卡抵扣, UNIQUE KEY uk_entry (entry_record_id) ) ENGINEInnoDB; CREATE TABLE monthly_card ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL UNIQUE, start_date DATE NOT NULL, end_date DATE NOT NULL, card_type TINYINT DEFAULT 0 COMMENT 0-月卡 1-季卡 2-年卡, status TINYINT DEFAULT 1 COMMENT 1-有效 0-失效 ) ENGINEInnoDB;这套表结构有三个值得在文档里解释的设计点。第一entry_record和exit_record分离而不是合并成一张停车记录表因为入场时不知道出场时间强行合并会出现大量空字段。第二费率放在parking_space表而不单独建费率表课设规模下按区域定价已经够用单独建表反而增加联表复杂度。第三entry_record上加(plate_no, entry_time)唯一索引这是为了防止同一辆车在未出场的情况下重复入场——这个坑后面还会详细说。从关系上看entry_record通过plate_no关联vehicle表获取车辆信息通过space_id关联parking_space表获取车位和费率exit_record通过entry_record_id反查入场记录计算出停放时长后用parking_space.rate乘以小时数计算费用。车辆表和月卡表通过plate_no一一对应。这套设计的核心思路是让“一次停车”这个业务动作由一条入场记录加一条出场记录完整刻画不做多余冗余。2.3 Controller-Service-Mapper 三层拆分与包结构规划包结构直接决定答辩时讲代码的流畅度。按功能分包而不是按技术层分包更贴近实际项目。常见的结构是com.example.parking ├── controller # 接收请求参数校验 ├── service # 业务逻辑计费、车位分配、月卡校验 │ └── impl ├── mapper # MyBatis 数据访问接口 ├── entity # 数据库实体类 ├── dto # 前端交互对象如入场DTO、出场DTO ├── config # 配置类 └── common # 统一返回结果、异常处理Controller 层只做三件事接收参数、调用 Service、返回统一结果。Service 层是业务核心计费算法、车位分配策略、月卡有效性判断都写在这里不写 SQL 也不写页面逻辑。Mapper 层只写数据库操作。这个分层的意义在于答辩时被问“如果要改计费规则怎么改”你可以直接说“只改 Service 里一个方法不动表和页面”这是分层设计最直观的价值。实体类可以用 Lombok 减少样板代码但如果你还没学 Lombok建议手写 getter/setter——答辩时被问“这个注解是什么”会有点尴尬。DTO 和实体分开是我的习惯有的同学图省事直接用实体类接收前端参数这在课设阶段能跑但一旦某个字段不想暴露给前端就会很别扭。3. 核心流程落地计费、车位分配与进出场逻辑3.1 入场流程顺序是“查月卡 → 找车位 → 写记录”不能反过来入场流程最常见的实现顺序是先分配车位再判断月卡这会导致月卡车辆占用了普通车位而无需付费长期下来普通车位被挤占。正确的入场逻辑是public EntryRecordDTO entry(EntryRequest request) { // 1. 判断是否是月卡车辆月卡车无需分配固定车位设定为指定区域或虚拟车位 MonthlyCard card monthlyCardMapper.findByPlateNo(request.getPlateNo()); boolean isMonthly card ! null isCardValid(card); // 2. 非月卡车辆才需要分配空闲车位 if (!isMonthly) { ParkingSpace space parkingSpaceMapper.findFirstFreeSpace(request.getArea()); if (space null) { throw new BizException(当前区域无空闲车位请前往其他区域); } parkingSpaceMapper.updateStatus(space.getId(), 1); // 0-1 占用 } // 3. 插入入场记录 EntryRecord record new EntryRecord(); record.setPlateNo(request.getPlateNo()); record.setIsMonthly(isMonthly ? 1 : 0); entryRecordMapper.insert(record); return buildEntryDTO(record); }这段代码的顺序是有讲究的。第一步先查月卡如果数据库里没有这张卡或者卡已过期就说明这是一辆临时车需要分配车位。第二步分配车位时用findFirstFreeSpace查询然后立刻把状态改为占用这个“先查后改”其实有一个并发窗口后面第三节专门讲怎么处理。第三步插入入场记录时因为entry_time是数据库默认值这里不需要手动传入时间避免应用服务器和数据库服务器时间不一致导致的误差。入场时要生成一条“入场记录”而不是直接更新车辆状态原因在于车辆可能多次进出用一条记录对应一次停车是更清晰的设计。入口处如果对接了车牌识别摄像头plate_no来自识别结果没有摄像头就做成手动输入车牌同时保留一张“手动入场”的标记字段答辩时能体现你考虑到了设备故障场景。3.2 计费逻辑跨天要分段免费时段要入表而不是写死在代码里计费是答辩追问的重灾区。最容易翻车的实现是把费率写死在 Java 代码里比如每小时5元直接作为常量。一旦收费规则变了比如新能源车半价、夜间时段优惠就要改代码重新部署。正确做法是把规则参数化放到parking_space表里通过rate字段配置同时把免费时段做成一个配置项。public ExitRecordDTO exit(ExitRequest request) { // 1. 查出该车的入场记录按车牌查最近一条未出场的记录 EntryRecord entryRecord entryRecordMapper.findLatestUnfinished(request.getPlateNo()); if (entryRecord null) { throw new BizException(未找到入场记录无法出场); } // 2. 计算停车分钟数和跨天标记 LocalDateTime entryTime entryRecord.getEntryTime(); LocalDateTime exitTime LocalDateTime.now(); long minutes Duration.between(entryTime, exitTime).toMinutes(); boolean overnight entryTime.toLocalDate().isBefore(exitTime.toLocalDate()); // 3. 查费率计算金额 ParkingSpace space parkingSpaceMapper.findById(entryRecord.getSpaceId()); BigDecimal rate space.getRate(); // 每小时费率 BigDecimal freeMinutes configMapper.findByName(free_minutes); // 免费时长比如15分钟 BigDecimal billableMinutes BigDecimal.valueOf(Math.max(0, minutes - freeMinutes.doubleValue())); BigDecimal amount billableMinutes.divide(BigDecimal.valueOf(60), 2, RoundingMode.CEILING) .multiply(rate); // 4. 优惠规则判断新能源9折写一个独立方法 amount applyDiscount(entryRecord.getPlateNo(), amount); // 5. 写入出场记录并释放车位 ExitRecord exitRecord buildExitRecord(entryRecord, exitTime, minutes, amount); exitRecordMapper.insert(exitRecord); parkingSpaceMapper.updateStatus(entryRecord.getSpaceId(), 0); return buildExitDTO(exitRecord, amount); }计费的核心逻辑是“封顶到分钟按小时向上取整”这里用BigDecimal而不是double是为了避免浮点误差。跨天计费在这个代码里没有单独处理因为按分钟累加后乘以小时费率跨天本身不影响总额。如果计费规则里确实有“夜间时段更便宜”这类叠加逻辑正确做法是提取一个calculateAmount(entryTime, exitTime, rate)方法按时段拆分到小时每一段用对应的费率计算。不要在一个大方法里用 if-else 堆时段判断后面加规则会很痛苦。3.3 车位分配策略按区域轮流分配还是就近分配入场时分配车位的策略有很多种最简单的是“按区域优先分配”——查parking_space表时按区域分组取status0的第一条。这种做法的好处是代码短但会导致 A 区永远先满B 区闲置。更好的策略是“区域轮流分配”记录一个分配游标按区域轮询每次从上一个分配的区域往后找空闲车位。在课设里实现这个不用 Redis用一个数据库字段记录当前轮询到哪个区域即可。但要注意如果一个区域满了要跳过它轮询逻辑不能死循环。public ParkingSpace allocateSpace(String areaPreference) { ListParkingSpace spaces parkingSpaceMapper.findFreeSpaces(); if (spaces.isEmpty()) { return null; // 停车场已满 } // 按区域分组按区域序号轮转 MapString, ListParkingSpace grouped spaces.stream() .collect(Collectors.groupingBy(ParkingSpace::getArea)); // 从上次分配的区域index 1开始轮询 AtomicInteger counter new AtomicInteger(0); // 从配置表读取 ListString areas new ArrayList(grouped.keySet()); Collections.sort(areas); for (int i 0; i areas.size(); i) { int idx (counter.getAndIncrement() i) % areas.size(); ListParkingSpace areaSpaces grouped.get(areas.get(idx)); if (areaSpaces ! null !areaSpaces.isEmpty()) { return areaSpaces.get(0); // 取该区域第一个空闲车位 } } return null; }这段代码里用了stream对空闲车位按区域分组然后区域名排序后轮转。注意counter这里我用注释标了“从配置表读取”实际项目中建议把这个游标存到数据库配置表每次分配后更新。不要在内存里维护这个状态服务重启后游标就丢了。分配策略也要考虑一种边界入场请求进入时查到了空闲车位但执行 update 状态时被并发抢走。findFirstFreeSpace和updateStatus之间的时间差里另一个请求可能已经把车位占掉。最简单的兜底方案是 update 时加上条件WHERE status 0更新后检查影响行数如果为 0 说明车位已被人抢走重新调用分配逻辑。这个细节在文档里写明白答辩时非常加分。4. 避坑指南停车场系统开发中常见的 5 个翻车点4.1 同一个车牌重复入场唯一索引救了一命现象车辆入场后道闸没抬或者司机倒出去又重新进来系统生成了两条入场记录第二次入场时显示车位已满但数据库里明明有空车位。原因入场接口没有检查该车牌是否已经有未出场的记录两次请求都执行了“插入入场记录”的操作。解决在entry_record表加UNIQUE KEY uk_plate_entry (plate_no, entry_time)只能防止同一秒重复插入挡不住跨秒的重复入场。更好的做法是在 Service 层先查findLatestUnfinished(plateNo)如果存在未出场记录直接抛异常“该车辆已在场内”。同时数据库表里给entry_record加一个冗余字段status0-在场 1-已出场并建立(plate_no, status)联合索引入场时先UPDATE entry_record SET status1 WHERE plate_no? AND status0但更稳的做法是让业务逻辑保证同一时间只有一条在场记录。4.2 时间差问题应用服务器和数据库服务器时间不一致现象计费金额莫名其妙多算或少算偶尔出现停车时长为负数。原因代码里用LocalDateTime.now()获取应用服务器时间但数据库的entry_time用DEFAULT CURRENT_TIMESTAMP由数据库生成两台机器时钟不同步就会导致计算基准不同。解决统一时间来源。最简单的方式是入场时也不依赖数据库默认值而是在entry()方法里用同一个LocalDateTime now LocalDateTime.now()同时写入entry_time字段出场时也用同一个 now 与入场记录比对。如果你有 NTP 时间同步的环境当然更好但课设和中小系统中统一由应用层取时间是最不容易出错的。我的习惯是所有时间字段都不使用数据库默认值应用层显式传入这样测试时还能手动模拟时间。4.3 并发占车位两个请求同时拿到同一个空闲车位现象车位表显示有 5 个空闲车位但两辆车同时入场其中一辆被拒绝“车位已满”。原因findFirstFreeSpace和updateStatus之间不是原子的两个线程同时查到同一个空闲车位都尝试去更新更新条件没带status0导致两次更新都成功但同一车位被标记占用两次。解决更新语句带上状态条件然后检查影响行数int updated parkingSpaceMapper.occupySpace(spaceId); // SQL: UPDATE parking_space SET status 1 WHERE id #{spaceId} AND status 0 if (updated 0) { // 车位被别人抢了重新分配 return allocateSpace(areaPreference); }occupySpace的 SQL 里必须写AND status 0这个条件依靠数据库行锁来保证只有一个请求能成功。如果你们的数据库是 MySQL InnoDBUPDATE会锁住该行直到事务提交或回滚这也意味着你必须在同一个事务里完成“占车位 插入入场记录”否则锁释放后数据可能不一致。事务的写法是Transactional(rollbackFor Exception.class)但注意这个注解默认只在 RuntimeException 时回滚如果你抛的是自定义受检异常要显式声明。4.4 月卡到期了还在用只在入场时校验是骗自己现象月卡用户 1 号到期但 2 号还能正常入场出场时也没多扣钱。原因很多实现只在入场时查月卡状态入场后到出场之间跨过了到期时间出场时不再校验直接按月卡处理免单。解决出场计费时也要校验月卡有效性。比如月卡 1 号到期车辆 1 号 23:50 入场2 号 00:10 出场这 20 分钟应该按临时车计费。出场判断逻辑应该是“入场时是月卡且出场时月卡仍然有效”两者缺一不可。如果出场时月卡已过期把入场记录标记为普通车重新按时长计费。还有一种边界是“月卡当天入场出场时跨天未过期”这个没问题只要end_date大于等于出场日期即可。4.5 发票金额和实际收款对不上优惠计算链路上重复打折现象报表里统计的收费总额和数据库里actual_amount相加的结果对不上。原因出场计费时既在 Service 层打了新能源 9 折又在 Controller 层针对某类支付方式打了 95 折两层各自处理折扣没有统一入口而且打折后的金额是直接覆盖原值没有保存原始应收和优惠明细。解决把金额计算收敛到唯一入口。ExitRecordDTO里同时保留total_amount折扣前应收和actual_amount实收折扣规则统一写在一个DiscountCalculator类里不允许在其他地方修改金额。答辩时看到这个设计可以主动说“我预留了优惠明细表如果想追踪每次折扣是什么规则产生的可以再建一张 discount_log 表”他会觉得你想得比一般课设深一层。5. 进阶扩展从课设到能用的系统这四步是分水岭到这里系统已经能跑通完整的进出场流程但距离“可以部署给别人用”还有一段距离。下面再走四步每一步在文档和答辩里都是增量亮点。第一步加一个dashboard统计接口。用三个 SQL 输出今天的进场数、出场数、实时在场车辆和今日营收。-- 今日进场数 SELECT COUNT(*) FROM entry_record WHERE DATE(entry_time) CURDATE(); -- 当前在场车辆 SELECT COUNT(*) FROM entry_record WHERE status 0; -- 今日营收 SELECT COALESCE(SUM(actual_amount), 0) FROM exit_record WHERE DATE(exit_time) CURDATE();这三个查询单独看都很简单但组合起来就是一个运营看板的后端接口。如果要进一步避免全表扫描可以在entry_time和exit_time上建索引数据量过万时性能差异明显。第二步给月卡加“到期前三天自动提醒”。不需要定时任务出场时判断一次即可如果该车牌是月卡且三天内到期在出场结果里塞一个remindMsg字段前端弹个提示。这是最省事的实现但答辩时如果被问“凌晨没人出场怎么提醒”再补充说“生产环境会用定时任务扫表给用户发短信微信”点出两种方案的不同适用场景即可。第三步临时车补缴功能。很多停车场出场时才发现余额不足需要先去中央缴费亭补缴。实现上就是在exit_record里加pay_status字段0-未支付 1-已支付出场时校验支付状态未支付的不抬杆。这一步在文档中属于“考虑到真实运营场景”的设计很加印象分。第四步不要忽略“数据脱敏”的表述。虽然课设没有强制要求但车辆表里有手机号设计文档里写一句“手机号展示时中间四位打码”会让系统在观感上更完整。不用真的实现提一句边界考虑就够了。最后说一个我自己的习惯每次写完一个功能模块我会先在本地用 JUnit 写两个测试一个测正常流程入场-出场-金额正确一个测异常流程重复入场-报错-车位不占用然后才去调页面。这个习惯保证我答辩演示时不会因为测试数据没清干净而当场翻车——那种“诶怎么停不进去”的场面经历过一次就再也不想经历了。还有一个收尾技巧数据库里准备一份“演示数据”脚本包含三辆临时车、一个月卡过期车、一个新能源车每次答辩前重新执行一遍保证时间状态都是新的。比在页面上手点快得多。希望这套从表设计到并发处理再到扩展的思路能帮到你。无论你是要交课设还是纯粹练手把入场流程里“查月卡-占车位-写记录-处理重复请求”这条链路的四个坑都踩一遍你对 Java Web 事务和并发控制的理解会比看十篇八股文都深。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。