做毕设选题的时候,“基于 Web 的园区车辆出入管理系统”这串题目我见过很多个版本,有的写成“园区车辆智能出入与缴费管理系统”,有的写成“Java 园区车辆进出登记与计费平台”,实际指向的都是同一个项目。它在毕设里属于那种看着不炫,但能把 Java Web 的核心技能点都覆盖完的题目:要处理车辆进入和出去两个动作、要给临时车算停车费、要给月租车做续费管理、还要在网页上实时看到园区每一辆车的通行记录。
这篇记录我不会去堆那些“假大空”的论文架构,而是给出一套能真正跑起来、适合答辩演示的方案。后端用 Java 技术栈,我习惯用 Spring Boot + MySQL,前端只要达到 Web 操作效果即可,传统模板页或前后端分离都行。车辆进入登记、离开计费、收费记录、月卡管理、管理员后台这些模块,最终都能在一个 Web 页面上完成操作。下面从需求拆解开始,把数据库、计费逻辑、接口流程和踩坑点全部讲清楚,适合正在做 Java Web 毕设、或者想了解车辆管理类系统内部逻辑的同学直接参考。
1. 项目定位与需求拆解:这个毕设到底在做什么
1.1 从标题看核心模块
这个项目的标题组合信息量很大,我先帮大家把几个关键词拆开看:
- “园区车辆出入”:核心是入口登记和出口登记两条链路。
- “智能出入”:通常指通过车牌识别完成自动抬杆、自动匹配车辆类型。
- “缴费管理”:出场时结算临时车费用,支持线下或模拟在线支付。
- “登记与计费平台”:后台需要能看到车辆档案、车辆进出流水、收费标准、缴费记录等。
把这些关键词梳到一起,落成实际系统就是四个字:管车、算钱。管车是把车从进入到出去的完整轨迹记录下来,算钱是按照停车规则把临时车、月租车、免费车区分开,在出场时自动生成订单。
如果只做到车辆登记,项目会很单薄,答辩时没什么可讲;如果只做计费,又缺少“进出管理”这个业务闭环。所以完整系统建议至少包含这几个模块:车辆档案管理、出入记录管理、收费规则管理、缴费订单管理、系统用户登录与角色权限。
1.2 用户角色与真实业务场景
我建议按现实园区的人员结构来设计角色,不要只做一个“万能管理员”,这对于毕设而言虽然省事,但需求分析不够深入。
第一个角色是园区保安或入口值班人员。他们负责操作出入口登记页面:车辆进场时录入或识别车牌,选择车辆类型,形成入场记录;车辆出场时检索到对应入场记录,这时系统自动计算金额。
第二个角色是访客或临时车车主。临时车场景最典型:车辆进入园区时没有登记车牌档案,出场到岗亭后,保安在系统里输入车牌,系统显示入场时间和当前应收金额,车主扫码或现金缴费后抬杆放行。
第三个角色是财务人员。他们需要查看每天收了多少钱、哪些订单未支付、车场车位数是否异常,从而做对账。
第四个角色是系统管理员。除了操作权限,管理员还要维护收费规则,比如临时车首小时计费标准、单日封顶金额;也要维护月租车用户,录入车牌号和套餐到期时间。
一个模拟完整园区业务的设计,必然包含这些角色和交互场景。实际演示时不需要每个角色都单独做 PC 端应用,只需要在同一个 Web 系统里按角色显示不同菜单和按钮权限即可。
1.3 技术栈选型:为什么 Java + Web 组合好落地
毕设题目是 Java,同时要求 Web 管理端,那技术选型基本就在两个方向里选:传统 Java Web(Servlet + JSP + Tomcat)和现在更主流的 Spring Boot + 前端模板/分离开发。
我更推荐使用 Spring Boot。原因是写起来干净、独立部署方便、资料多,而且答辩老师看到 Spring Boot + Restful API 会比 JSP 项目印象分高不少。下面是我在做同类项目时常用的组合:
| 层次 | 选型 | 理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.7 或 3.x | 简化配置,内置 Tomcat,适合快速开发 Rest API |
| 持久层 | MyBatis-Plus | 单表 CRUD 不需要写 SQL,能节省大量时间 |
| 数据库 | MySQL 5.7 / 8.0 | 稳定主流,界面工具用 Navicat 或 DBeaver |
| 权限认证 | Sa-Token 或 JWT | 登录拦截容易实现,适合前后端分离 |
| 前端方案 | Vue 3 + Element Plus | 表格、表单、弹窗等管理端组件非常齐全 |
| 接口调试 | Knife4j 或 Postman | 生成在线接口文档,答辩时方便展示 |
如果不想搞前后端分离,也可以用 Thymeleaf 模板直接渲染页面,后端多写一点页面跳转,前端工作量小。但考虑到后续扩展和答辩时展示接口设计,我比较倾向于使用 Vue 管理后台,再配一套登录注销和按钮权限即可,不需要做太复杂。
前端每个页面有固定套路:车辆列表页、入场登记页、出场结算页、月卡列表页、缴费订单页。这些页面都围绕表格、表单、下拉选择、状态标签来展开,属于很标准的 Web CRUD 场景,对新手也很友好。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与计费模型:先把大头想清楚
2.1 核心表结构设计
车辆管理系统表数量不需要特别多,但如果建表不仔细,后期写计费逻辑会很痛苦,所以最好在建库前把表之间关系理清。核心是“一车一档”和“一次通行一条记录”的关系。
我建议至少建五张核心表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 后台用户 | username, password, role |
| vehicle_info | 车辆档案(固定车/月租车/内部车) | plate_no, owner_name, phone, vehicle_type |
| passage_record | 每一次进出场记录 | plate_no, entry_time, exit_time, entry_image, exit_image, fee_amount |
| pay_order | 缴费订单 | passage_id, order_no, amount, pay_type, pay_status, pay_time |
| charge_rule | 收费规则配置 | rule_type, free_minutes, unit_hours, unit_price, daily_cap |
要补充一点:很多同学会把车辆档案和进出记录合并到一张表里,乍一看字段简单,但实际会出现大量冗余。比如同一辆车一个月进园区 30 次,如果车牌信息都带在通行记录里,后续修改车主手机号就要批量 update 记录。拆成两张表后,通行记录只要保存 plate_no 这个关联字段,车辆档案独立维护,逻辑会清楚很多。
下面是一份我认为可以直接用的 passage_record 建表语句:
sql复制CREATE TABLE `passage_record` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '通行记录id',
`plate_no` VARCHAR(20) NOT NULL COMMENT '车牌号码',
`vehicle_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1临时车 2月租车 3内部车 4特殊车',
`entry_time` DATETIME NOT NULL COMMENT '入场时间',
`exit_time` DATETIME NULL COMMENT '出场时间',
`entry_image` VARCHAR(255) NULL COMMENT '入场抓拍图片路径',
`exit_image` VARCHAR(255) NULL COMMENT '出场抓拍图片路径',
`fee_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '应收金额',
`pay_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未支付 1已支付 2免费',
`remark` VARCHAR(255) NULL COMMENT '备注',
UNIQUE KEY `uk_entry` (`plate_no`, `entry_time`) COMMENT '防止同一车牌同一时间重复入场'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆进出场流水记录';
这里 set entry_time 是 DATETIME 而不是 TIMESTAMP,主要是避免 MySQL 默认时区带来的麻烦,也方便前端直接接受 LocalDateTime 格式。
2.2 计费规则:临时车、月租车、免费车怎么区分
计费是这个项目最能体现专业度的地方。基础逻辑不难,但要考虑边界情况,比如入场不到半小时是否免费、跨天是否封顶、月租车过期后按什么标准收费。
我的建议是,把车辆类型和收费规则分开设计。vehicle_info 表里记录 vehicle_type,车进来时先查一下车辆档案:
- 如果车牌存在且状态正常:
- 月租车:不生成费用,出场时直接放行。
- 内部车/特殊车:免费放行,记录流水但费用为 0。
- 如果车牌不存在或月租已过期:
- 视为临时车,按入场和出场时间差计算费用。
临时车费用模型可以参考:“首小时免费,超过首小时后每 1 小时收费 2 元,不足 1 小时按 1 小时计算,单日 24 小时封顶 20 元。”这个规则简单、容易演示,也便于用代码实现。
计算逻辑代码如下:
java复制public class ParkingFeeCalculator {
public static final BigDecimal FREE_MINUTES = BigDecimal.valueOf(60);
public static final BigDecimal UNIT_PRICE = new BigDecimal("2");
public static final BigDecimal DAILY_CAP = new BigDecimal("20");
public BigDecimal calcTemporaryFee(LocalDateTime entryTime, LocalDateTime exitTime) {
if (entryTime == null || exitTime == null || !exitTime.isAfter(entryTime)) {
return BigDecimal.ZERO;
}
long minutes = Duration.between(entryTime, exitTime).toMinutes();
if (minutes <= FREE_MINUTES.longValue()) {
return BigDecimal.ZERO;
}
long billableMinutes = minutes - FREE_MINUTES.longValue();
long hours = (billableMinutes + 59) / 60; // 向上取整,不足1小时按1小时
BigDecimal total = UNIT_PRICE.multiply(BigDecimal.valueOf(hours));
return total.min(DAILY_CAP);
}
}
关键点有两个:第一个是金额必须用 BigDecimal,不能用 double,否则会出现 0.1 + 0.2 这种精度问题;第二个是“不足一小时按一小时”不是直接除 60,而是 (分钟数 + 59) / 60 向上取整。
月租车过期判断也不能忽略。比如车主包月套餐到 2024-06-01 到期,出场时发现当前时间已经超过到期时间,就应该自动降级为临时车,按照临时车的费率收取本次费用。
2.3 车辆状态流转与关键 SQL 思路
一次完整的临时车通行,状态可以这样流转:入场登记(passage_record 插入一条 exit_time 为空记录) -> 出场结算(根据 plate_no 查最近一条未出场记录) -> 生成缴费订单(pay_order 插入待支付单) -> 支付完成(更新缴费状态,同时回写进场记录的 exit_time 和 fee_amount)。
后端在“根据车牌查最近一条未出场记录”时,SQL 要特别注意,不能简单用 WHERE plate_no = ? 然后 LIMIT 1,因为可能同一辆车同一天进出多次,查出早上的旧记录就有问题。更稳妥的方法是限定状态字段:
sql复制SELECT * FROM passage_record
WHERE plate_no = #{plateNo} AND pay_status = 0 AND exit_time IS NULL
ORDER BY entry_time DESC
LIMIT 1;
这里的 exit_time IS NULL 是核心条件,含义是这辆车还在园区内没有离场。如果把这个条件漏掉,一旦有历史数据就非常容易把入场时间搞混,费用自然也算不对。
说到 SQL,统计报表页也是这类毕设项目的加分项。每天营收统计可以用一条简单的 group by 完成:
sql复制SELECT DATE(pay_time) AS payDate, COUNT(*) AS orderCount, SUM(amount) AS totalAmount
FROM pay_order
WHERE pay_status = 1
GROUP BY DATE(pay_time)
ORDER BY payDate DESC;
现场演示的时候,只要页面能显示今日车流量、今日收入、近 7 日收入趋势,整个项目在数据层面的完整性就已经很能打了。
3. Java Web 后端核心实现:从建项目到接口跑通
3.1 Spring Boot 项目骨架与配置
这类项目从零创建 Spring Boot 工程并不难,我习惯直接把 Maven 依赖、yml 配置和启动类配好,然后按 controller/service/mapper/entity 分包。关键依赖放在 pom.xml 中:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.37.0</version>
</dependency>
application.yml 里的配置其实很固定,但有几个坑要特别留意。第一个是数据库连接串必须加上时区参数,最好写成 serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8,否则可能会遇到时间差 8 小时的问题。
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/park_system?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8
username: root
password: 123456
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
时间处理是 Java Web 毕设常见重灾区。后端实体类如果使用 LocalDateTime,和前端 JSON 字符串之间需要一套可读的序列化格式。上面 jackson 配置能解决输出问题,但引入 jackson-datatype-jsr310 也要确认存在,否则 LocalDateTime 会转成数组格式,前端就不好处理了。
3.2 出入接口:一个 Controller 搞定登记和结算
后端接口设计上,我建议把入口和出口操作做成独立接口,而不是把所有逻辑揉进一个 update 方法。下面是最基本的接口列表:
| 方法 | 路径 | 功能 |
|---|---|---|
| POST | /api/vehicle/entry | 车辆入场登记 |
| POST | /api/vehicle/exit | 车辆出场结算并生成订单 |
| GET | /api/vehicle/page | 分页查询通行记录 |
| POST | /api/pay/confirm | 确认支付(模拟扫码支付) |
| POST | /api/month/issue | 办理月租车套餐 |
| GET | /api/stat/summary | 首页运营数据汇总 |
入场接口的核心逻辑是:先根据车牌查找车辆档案,判断类型;如果查不到,按临时车处理。同时为了防止人工录入导致同一辆车短时间重复进场,可以做一次简单校验,调用结果如下:
java复制@PostMapping("/vehicle/entry")
public Result<String> entry(@RequestBody EntryDto dto) {
VehicleInfo vehicle = vehicleInfoService.findByPlateNo(dto.getPlateNo());
Integer type = vehicle != null ? vehicle.getVehicleType() : TEMP_VEHICLE;
PassageRecord record = new PassageRecord();
record.setPlateNo(dto.getPlateNo());
record.setVehicleType(type);
record.setEntryTime(LocalDateTime.now());
passageRecordService.save(record);
return Result.success("入场成功");
}
出场接口逻辑比入场复杂一点,需要先查出这条未出场记录,才能计算费用。伪流程可以这样写:
java复制@PostMapping("/vehicle/exit")
public Result<PayOrder> exit(@RequestBody ExitDto dto) {
String plateNo = dto.getPlateNo();
PassageRecord record = passageRecordService.findLastUnclosedByPlateNo(plateNo);
if (record == null) {
throw new RuntimeException("未找到该车的入场记录");
}
LocalDateTime exitTime = LocalDateTime.now();
record.setExitTime(exitTime);
// 判断车辆类型,计算费用
if (record.getVehicleType() == TEMP_VEHICLE
|| isMonthExpired(record.getPlateNo(), exitTime)) {
BigDecimal fee = parkingFeeCalculator.calcTemporaryFee(
record.getEntryTime(), exitTime);
record.setFeeAmount(fee);
record.setPayStatus(fee.compareTo(BigDecimal.ZERO) > 0 ? UNPAID : FREE);
} else {
record.setFeeAmount(BigDecimal.ZERO);
record.setPayStatus(FREE);
}
passageRecordService.updateById(record);
PayOrder order = new PayOrder();
order.setPassageId(record.getId());
order.setAmount(record.getFeeAmount());
order.setOrderNo("P" + System.currentTimeMillis());
order.setPayType(dto.getPayType());
payOrderService.save(order);
return Result.success(order);
}
这里我把“月租车过期后按临时车处理”的逻辑也写在出场里,演示时比较有说服力:车主包月到期了,系统不会让他稀里糊涂免费出场。
3.3 前端 Web 页面怎么和接口对接
如果采用 Vue 3 + Element Plus,前端页面并不需要发挥太多创意,关键是形成一套可复用的列表和表单页。我的目录习惯是:
- views/dashboard 首页运营数据
- views/vehicleRecord 车辆出入记录
- views/vehicleEntry 车辆入场弹窗
- views/vehicleExit 车辆出场结算弹窗
- views/monthCard 月租车管理
- views/payOrder 缴费订单
- views/rule 收费规则配置
页面开发时最麻烦的是下拉框数据回显。比如车辆类型有临时车、月租车、内部车、特殊车,前端后端如果不约定好,就会出现 0/1/2 和中文含义对不上的问题。我的经验是统一用数字字典,并在页面里封装一个通用函数:
javascript复制export const VEHICLE_TYPE_MAP = {
1: '临时车',
2: '月租车',
3: '内部车',
4: '特殊车'
}
列表接口返回的 vehicleType 是数字,前端直接用 map 渲染标签即可,这样接口语义清晰,也比后端拼字符串返回更规范。
如果嫌前后端分离部署麻烦,也可以把 Vue 打包后的 dist 目录放到 Spring Boot 的 static 目录下,这样最终运行效果就是一个可执行 jar 包,打开浏览器就能访问,答辩时不会因为要单独启动 node 服务而翻车。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
做这类 Java Web 项目时,很多问题其实不是因为需求有多复杂,而是环境和代码细节没注意。下面这张表可以说是我的经验总结,基本覆盖了新手最常遇到的几类报错:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 启动时报数据库连接失败 | 数据库地址、账号密码填错 | 检查 application.yml 配置,先用 Navicat 测试连接 |
| 时间差 8 小时 | jdbc url 没加时区参数 | 连接串加 serverTimezone=Asia/Shanghai |
| 金额显示 19.999999 | 用了 double 计算金额 | 字段和计算改成 BigDecimal |
| JSON 显示时间数组 | LocalDateTime 序列化问题 | 引入 jsr310 依赖并配置 jackson date-format |
| 同一辆车查询到旧入场记录 | 出场接口没过滤 exit_time is null | 增加状态条件,查找最近一条未出场记录 |
| 页面跨域报错 | 前端端口和后端端口不同 | 配置 CorsFilter 或者用代理转发 |
| 月租车到期还能免费出场 | 没判断有效期 | 出场时调用过期判断并按临时车计费 |
| 接不了真实车牌识别摄像头 | 硬件条件有限 | 先用模拟车牌号或图片上传代替,抽象车牌识别接口 |
4.2 印象深刻的两个代码级问题
第一个是入场重复记录问题。刚开始我只有一个 passage_record 表,没有对“入场时间+车牌”做去重,结果测试时手动连点两次入场按钮,产生两条入场记录,出场时就匹配到了第一条旧记录,费用计算直接乱套。
解决办法有两个层面:数据库层对 (plate_no, entry_time) 加唯一索引,程序层在入场接口里先查一下该车牌最近 10 分钟内是否已有未出场记录,如果有就不允许再次入场。对于真实园区,这能防止车辆识别摄像头抖动导致的一次入场产生多张单;对于毕设演示,也能说明你考虑过边界情况。
第二个是支付状态和出场状态没有联动。临时车出场结算后生成了一个待支付订单,但把 passage_record 的支付状态也直接改成未支付后,如果页面一直没点“确认支付”,那这辆车会一直处于“已出场但未支付”状态,和真实逻辑有出入。
真实园区通常是先抬杆放行,后续再催缴或拦截;但管理员后台需要一个“未支付订单列表”,方便财务线下对账。我建议把出场流水状态和订单状态分开看:流水负责车辆通行动作,订单负责支付动作,两者通过 passage_id 关联。如果展示一个车辆流水列表,可以根据 pay_order 是否存在且支付成功来显示订单状态,而不是直接复用流水状态。
4.3 答辩演示建议:把“智能”体现出来
这个项目标题里有“智能”两个字,如果没有真实摄像头,很容易被老师说名不副实。我建议在代码层面做一个车牌识别衔接接口,哪怕实际是用模拟数据,也要预留扩展点。
具体做法是定义一个 VehicleRecognizeService 接口,接口里只有一个方法 recognize(MultipartFile file)。项目里提供默认实现类 MockVehicleRecognizeServiceImpl,方法里直接返回一个随机或测试车牌号;同时在接口文档里写清楚,如果要接入真实识别服务,只需替换实现类或者调用第三方算法平台即可。这样一来,前端页面上传一张车辆照片,系统回车牌号、登记进场,链路完整又保留了扩展空间,答辩时能讲的东西会多很多。
5. 项目答辩时的扩展空间和我的体感
5.1 项目亮点怎么讲才不虚
答辩时不能只会说“我完成了增删改查”,要有几个能拿得出手的模块级亮点。
第一个是计费规则可配置。收费规则没有写死在代码里,而是存在 charge_rule 表中,后台管理人员可以修改首小时免费时间、每小时单价、每日封顶金额。每次修改立即生效,下次出场结算就会按新规则计算,这种细节比写死一个常量要高级不少。
第二个是月租车到期自动降级。很多基础版项目只会区分临时车和月租车,但没有处理到期以后怎么收费的问题。把“月租车在有效期内免费出场,过期后按临时车收费”写成清晰的判断逻辑,并且在页面里对即将到期的月租车高亮提示,这体现的是业务完整性。
第三个是操作留痕和统计报表。系统不只要记录最后数据,还要能看到动作历史。这里的动作历史不是所有操作日志,而是车辆每一次进出都有一个对应流水,并且每次缴费产生订单号。首页统计卡片能实时展示今日进场量、今日出场量、今日营收和未支付订单数,这个效果对答辩演示来说已经相当加分。
5.2 可以继续扩展的方向
如果时间充裕,或者想把项目包装得更完整,可以考虑继续扩展几个点。
一是增加车位管理。在车场信息表里维护总车位和已占用位数,入场时判断车位是否已满,满位时自动拒绝新车辆入场,出场时释放车位。从这个扩展点能看到实时车位数变化,系统会更像一个“智慧园区”平台。
二是缴费方式更细。目前一般只做订单支付确认,可以进一步改造成“内部余额支付”或“第三方支付回调模拟”。如果增加车主端小程序或公众号绑定车牌,就能实现车主自己离场前在线缴费,驶出时车牌识别后自动抬杆,这就是常说的无感支付场景。
三是用定时任务处理长期未缴费订单。比如临时车有订单生成后 30 分钟内未支付,系统自动在订单列表标记为异常,并由后台管理员人工处理。加入 XXL-Job 或 Spring Task 对这个项目来说也不复杂,但能展示对线上场景的理解。
5.3 最后说点实在的
我自己做这类课题时最大的感受是:题目大小不重要,重要的是有没有把一条核心链路做透。这个系统表面上是车辆进来、车辆出去、收钱,但完整做完以后,你会把 Spring Boot、MyBatis-Plus、MySQL 表设计、接口规范、前端页面联调、部署运行都串成一条线,对 Java Web 整体的掌控感会明显不一样。
如果你现在正卡在某个环节,建议先不要把功能想得太满,先把“临时车入场 -> 出场 -> 计费 -> 支付成功”这条主链路跑通,再逐步加入月租车、统计报表、规则配置。一口吃不成胖子,但先打通最核心的业务闭环,后面每一步都只是往已有的框架里填东西而已。
