Java停车场管理系统实战:Spring Boot+MySQL+计费并发
发布时间:2026/9/25 4:02:30来源:尧图网络
简介这是一份基于Java的停车场管理系统设计与实现的毕业设计文档面向计算机相关专业学生、开发者及需要快速搭建同类管理系统的项目人员系统性地解决城市停车难、车位管理效率低等问题。文档涵盖课题背景与意义、国内外研究现状、可行性分析、系统流程分析、功能模块设计、数据库ER图及数据表设计、界面设计并完整描述从开发环境搭建到系统测试的全过程重点讲解Java、MySQL数据库、B/S架构、SpringBoot后端框架与VUE前端框架的实际应用。资源包为单个docx文档大小约937KB便于直接阅读与修改。目前已有53人学习浏览适合用于毕业设计参考资料、课程设计模板或停车场管理系统开发入门学习。打开文档即可获得完整的设计思路、模块划分与测试方案可直接借鉴文档结构并扩展实现。1. 停车场管理系统到底难在哪从课设题目到真实业务每年毕业季和课设季总有一批「基于 Java 的 XX 管理系统」的题目停车场是里面出镜率最高的一个。这个题目容易给人错觉不就是车辆进进出出、记个时间、收个钱吗真动手你会发现它恰好覆盖了管理系统里最刁钻的那几个点——车位是带状态、会被并发争夺的资源计费是跨天、有免费时长、有封顶的规则计算月卡和临时车还是两套逻辑。能把这个系统写顺等于把 Java 编程里面向对象、事务、集合、日期处理这几块硬骨头都啃了一遍。这篇笔记要解决的问题很具体这个系统到底要建哪些表、入场出场流程怎么设计、跨天停车费怎么算不出错、并发抢车位怎么防、答辩前要验证哪些边界。适合刚学完 Java SE 想拿管理系统练手的人也适合正在做 Java 课程设计、毕业设计选了这道题的人。整套方案不需要车牌识别、不需要物联网设备一台电脑、一个 MySQL、一个 Spring Boot 工程就能跑起来但业务闭环是完整的。2. 技术栈选型三条路线对比为什么我推荐 Spring Boot MyBatis2.1 三选一控制台、JSPServlet、Spring Boot 各自的适用场景常见做法有三种路线先看对比再决定别一上来就建工程。技术路线实现难度答辩/演示效果适合人群Java SE 控制台程序 JDBC低弱只能看命令行输出只想练 Java 基础语法的人JSP Servlet JDBC中中有网页但代码耦合重学校课程只教了 JSP 的人Spring Boot MyBatis Thymeleaf中高强接近真实项目结构想兼顾课设分数和工程能力的人控制台方案最省事一个 Main 方法循环打印菜单入场就是System.out.println出场就是读键盘输入算钱。但它的天花板很低答辩时老师看到命令行界面第一句话大概率是「你这个跟 Excel 有什么区别」。JSP 方案能出网页可业务逻辑和页面标签混在一个文件里加一个需求就要在 .jsp 里翻半天属于典型的「能跑但不想维护」。我一般给时间紧、Java 基础一般的同学推荐第三条路Spring Boot 2.7 MyBatis Thymeleaf。理由很实在Thymeleaf 是服务端渲染在 HTML 模板里写th:each就能把停车记录循环出来不用单独学 Vue 那套前后端分离也不用处理跨域问题Java 里查出来的数据直接塞进 Model 就能渲染。而 Spring Boot 自带 Tomcatmvn spring-boot:run一条命令起来配置比 SSM 那套 XML 少了一大截。2.2 Spring Boot 2.7 JDK8为什么这个组合最不容易翻车版本选择是第一个坑。Spring Boot 3.x 要求 JDK 17 起步如果你本机装的是 JDK 8直接编译报错最典型的就是那句java: 警告: 源发行版 17 需要目标发行版 17其实就是编译器版本和运行版本对不上。而 JDK 8 是目前课设环境里最普遍的版本淘宝买的二手课本、学校机房老机器、老师演示用的电脑几乎都是 JDK 8。所以选 Spring Boot 2.7.x 是最稳的它基于 JDK 8 开发依赖里用的是javax.*命名空间网上搜得到的教程、报错解决方案也几乎都围绕这个版本。另外要注意Spring Boot 2.7 的默认打包方式是 jar内置 Tomcat不用额外装 Tomcat 到系统里这对在寝室和机房两个环境切换的人非常友好。环境配置这块建议先把三件事做齐再写代码JDK 装好并配好JAVA_HOME环境变量命令行里java -version能看到 1.8Maven 配好镜像源不然第一次拉依赖能卡半小时MySQL 装好并用命令行能登录。这三个前置条件不满足就去写代码后面八成会回来返工——配置环境是最容易因为「看着差不多」就跳过的步骤结果也是最耽误时间的。2.3 包结构controller/service/mapper 三层该放什么包结构决定后面写代码顺不顺。常见的错误是按「工具类、实体类、连接类」分业务一复杂就全堆在一起。这里按业务职责拆直接对着这个结构抄就行pms ├── controller │ ├── AdminController.java │ ├── EntryController.java │ └── ExitController.java ├── service │ ├── ParkingService.java │ └── impl │ └── ParkingServiceImpl.java ├── mapper │ ├── ParkingRecordMapper.java │ └── ParkingSpaceMapper.java ├── entity │ ├── ParkingRecord.java │ ├── ParkingSpace.java │ └── FeeRule.java ├── common │ └── Result.java └── config └── WebConfig.javacontroller 层只干两件事接收 HTTP 参数、调用 service 拿结果返回。所有的业务规则——车位占用判断、费用计算、月卡判断——全部放 service 层。mapper 层是 MyBatis 的接口只写数据库操作和方法签名SQL 写在对应的 XML 文件里。entity 就是纯数据类对应数据库表字段名和表字段一一映射。这样分有一个直接好处写论文画系统模块图的时候只要把这个包结构翻译成「表示层—业务层—数据访问层」的层次架构图就可以了不用再费劲重新归纳。Service 接口和 impl 实现类分开是给后期扩展留的口子比如你现在只支持临时车后面要加月卡接口不用动在 impl 里加逻辑就行这也是答辩时老师爱问的点。3. 数据模型与核心流程五张表、入场/出场、跨天计费怎么写3.1 建表从车位到停车记录最少要五张核心表先想清楚业务实体再建表。停车场管理离不开这几个东西车位、车辆、停车记录、收费规则、管理员对应的就是五张核心表。下面是车位的建表语句字段注释都写在里面CREATE TABLE parking_space ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 车位ID, space_no VARCHAR(20) NOT NULL COMMENT 车位编号如A-01, status TINYINT NOT NULL DEFAULT 0 COMMENT 车位状态 0空闲 1占用, type TINYINT NOT NULL DEFAULT 0 COMMENT 车位类型 0普通 1新能源, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_space_no (space_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位表;字段设计有几个点值得说。space_no加唯一索引这是业务上的自然约束不允许两个车位编号一样。version字段是给并发控制用的后面抢车位那节会讲现在先留好。type字段为了扩展比如新能源车位优先分配给绿牌车答辩时可以当亮点提。停车记录表是核心表字段得按业务流程一次想全CREATE TABLE parking_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 记录ID, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, space_id BIGINT NOT NULL COMMENT 车位ID, entry_time DATETIME NOT NULL COMMENT 入场时间, exit_time DATETIME NULL COMMENT 出场时间, fee DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 应收金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 记录状态 0在场 1已离场, KEY idx_plate (plate_no), KEY idx_entry (entry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT停车记录表;plate_no和entry_time都加索引因为最频繁的查询就是「查这辆车当前停在哪」和「按时间范围统计收入」。fee用DECIMAL(10,2)不用 float/double这是 Java 开发里被念叨烂了的规矩——浮点数算金额会有精度误差存数据库更明显0.1 0.2 不等于 0.3 这种问题在金额上绝对不能出现。exit_time允许为 NULL因为车辆还没出场时这个字段就是空的。3.2 入场逻辑一次带事务的「占位 建记录」入场流程拆开就三步确认这个车位空闲、把车位状态改成占用、插入一条入场记录。第一次写的人最容易做成「先 select 查一下状态再 update 占用」但后面会发现并发一高这个写法会翻车。这里用一个带条件的 update 解决问题Service public class ParkingServiceImpl implements ParkingService { Autowired private ParkingSpaceMapper spaceMapper; Autowired private ParkingRecordMapper recordMapper; Transactional(rollbackFor Exception.class) public Long entry(String plateNo, Long spaceId) { // 乐观锁扣减车位UPDATE ... WHERE status 0 只影响空闲中的车位 int affected spaceMapper.tryOccupy(spaceId); if (affected 0) { throw new BizException(车位已被占用请刷新后重试); } ParkingRecord record new ParkingRecord(); record.setPlateNo(plateNo); record.setSpaceId(spaceId); record.setEntryTime(LocalDateTime.now()); record.setStatus(0); recordMapper.insert(record); return record.getId(); } }对应的 MyBatis 更新语句长这样UPDATE parking_space SET status 1, version version 1 WHERE id #{spaceId} AND status 0注意这段代码里的两个关键点。第一tryOccupy这个 update 把「查状态」和「改状态」合并成了一条 SQLMySQL 在 execute 这条语句时对同一行记录加锁两个线程同时执行只有一个会成功另一个的affected是 0直接抛异常。这就是乐观锁的简化实现比先 select 再 update 安全得多。第二Transactional保证车位状态和停车记录要么都成功、要么都失败——如果 insert 停车记录时抛了异常车位状态会回滚成空闲不会出现「车没进来车位却显示占用」的脏数据。3.3 计费引擎免费时长、封顶与跨天怎么算才不出错计费是全场最容易被写崩的地方。常见规则是入场后 30 分钟内免费超过后按小时收费每小时 5 元不足一小时按一小时算单日封顶 30 元。看着简单实际写的时候要处理三个边界免费时长怎么算、跨天怎么拆、封顶按天还是按总时长。public BigDecimal calcFee(LocalDateTime entry, LocalDateTime exit, FeeRule rule) { long totalMinutes Duration.between(entry, exit).toMinutes(); long billable Math.max(0, totalMinutes - rule.getFreeMinutes()); if (billable 0) { return BigDecimal.ZERO; } BigDecimal fee BigDecimal.ZERO; // 从入场时间加上免费时长后开始计费 LocalDateTime cursor entry.plusMinutes(rule.getFreeMinutes()); while (cursor.isBefore(exit)) { LocalDateTime dayEnd cursor.toLocalDate().atTime(23, 59, 59); if (exit.isBefore(dayEnd) || exit.equals(dayEnd)) { // 当天结束按剩余时长计费 fee fee.add(calcWithinDay(cursor, exit, rule)); break; } else { // 跨天先收当天封顶再继续下一天 fee fee.add(rule.getDailyCap()); cursor dayEnd.plusSeconds(1); } } return fee.setScale(2, RoundingMode.HALF_UP); } private BigDecimal calcWithinDay(LocalDateTime start, LocalDateTime end, FeeRule rule) { long minutes Duration.between(start, end).toMinutes(); long units (minutes rule.getUnitMinutes() - 1) / rule.getUnitMinutes(); // 向上取整 return rule.getUnitFee().multiply(BigDecimal.valueOf(units)); }这个实现的内存循环逻辑是从计费起点开始判断剩余时间是否还在今天如果跨天了先收今天一整天的封顶费用然后把时间指针拨到第二天零点继续循环如果没跨天按「不足一个计费单元按一个单元算」的规则收尾。参数rule.getUnitMinutes()是计费单元比如 60 分钟为一个单元calcWithinDay里(minutes unitMinutes - 1) / unitMinutes是向上取整的经典写法停了 61 分钟按 2 个单元算停了刚好 60 分钟按 1 个单元算。RoundingMode.HALF_UP是四舍五入金额保留两位小数。这套逻辑跑一遍昨天 23:50 入场、今天 00:10 出场的场景免费时长 30 分钟计费起点是 00:20已经过了出场时间所以费用是 0——这个结果一开始很多人会不信但按规则推确实如此规则设计得合理用户也能接受。3.4 把收费规则做成数据表改价格不用改代码价格规则如果写在代码常量里改一次价格就要重新编译、重新部署在答辩现场改需求会非常被动。费率和时长参数抽成一张配置表是更可靠的做法CREATE TABLE fee_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(50) NOT NULL COMMENT 规则名称如临时车/月卡车, free_minutes INT NOT NULL DEFAULT 0 COMMENT 免费时长分钟, unit_minutes INT NOT NULL DEFAULT 60 COMMENT 计费单元分钟, unit_fee DECIMAL(10,2) NOT NULL COMMENT 每个计费单元费用, daily_cap DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 单日封顶金额0表示不封顶, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收费规则表;initial 数据插入一条临时车规则免费 30 分钟每 60 分钟 5 元日封顶 30 元。系统启动的时候从这张表把启用状态的规则加载到内存缓存里每次计费从缓存取。答辩演示时改一下数据库里的unit_fee下一笔订单立刻按新价格算这种「改配置不重编译」的效果比讲十页 PPT 都有说服力。4. 工程落地分层代码、统一返回、分页与环境配置4.1 Controller 只接参数业务交给 ServiceController 层最常见的毛病是「图省事把逻辑全写进来」。一个典型的错误写法是在 Controller 里先查车位、再判断状态、再插入记录几十行代码堆在一个方法里看起来也能跑但一旦出问题定位都找不到方向。正确的习惯是 Controller 保持轻薄只做参数接收和结果封装。RestController RequestMapping(/api/parking) public class EntryController { Autowired private ParkingService parkingService; PostMapping(/entry) public ResultLong entry(RequestBody EntryReq req) { // 参数校验车牌号为空直接拒绝 if (req.getPlateNo() null || req.getPlateNo().trim().isEmpty()) { return Result.error(车牌号不能为空); } return Result.ok(parkingService.entry(req.getPlateNo(), req.getSpaceId())); } }ResultT是一个统一返回体里面至少有三个字段code业务状态码、message提示信息、data真正的数据。所有接口都返回这个结构前端或者页面拿到之后先看code再决定展示什么这是让整个系统看起来「像个正经项目」的最小改造。逻辑说明就一句话Controller 里只做参数的非空校验和格式校验涉及业务规则的校验放到 Service 里做一个方法的长度控制在 20 行以内超过就拆。4.2 分页查询记录多了之后第一步要做什么停车记录是会不断累积的表不做分页的话查一次全表数据量上来以后页面会很卡。MyBatis 的场景下最常见方案是 PageHelper 插件也可以在 XML 里手写 LIMIT。这里给出 PageHelper 的用法因为课设和实际项目里它出现频率最高public PageInfoParkingRecordVO pageRecords(int pageNum, int pageSize, String plateNo) { // PageHelper 只对紧随其后的第一条查询生效 PageHelper.startPage(pageNum, pageSize); ListParkingRecord records recordMapper.selectByPlateNo(plateNo); return new PageInfo(records); }对应的 Mapper XML 里就是一条普通的SELECT * FROM parking_record WHERE plate_no #{plateNo} ORDER BY entry_time DESC。PageHelper 的原理是在执行这条查询前自动拼接LIMIT offset, size并且自动执行一条SELECT COUNT(*)来拿总数。使用时的参数说明pageNum从 1 开始不是 0这是新手最容易踩的边界pageSize一般设 10 或 20不要超过 50否则分页就失去意义了。要特别提醒的是PageHelper.startPage只对紧接着的第一条 SQL 查询生效有人会在 startPage 和查询之间夹一行其他数据库操作结果分页癞到别的查询上去了。写完分页代码一定要真造几十条数据翻两页看看总数对不对这个步骤不要省。4.3 运行环境自查JDK版本、MySQL时区、IDEA编码三连很多系统不是代码写错是环境没对齐。跑起来之前先在命令行里做三连自查java -version mvn -v mysql --versionjava -version确认是 1.8 还是 17如果输出显示是 17而你的 Spring Boot 是 3.x那没问题如果是 1.8 而项目用了 3.x直接改回 2.7.x 版本。mvn -v看 Maven 版本和它使用的 JDK两者不一致是最隐蔽的坑。mysql --version确认客户端能连上数据库。然后看配置文件application.yml这里有几个写了能少踩很多坑的参数spring: datasource: url: jdbc:mysql://localhost:3306/parking_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/ShanghaiserverTimezoneAsia/Shanghai不写MySQL 8 的驱动会报时区错误characterEncodingutf8不写中文存进去变问号这条和 IDEA 的 File Encoding 要配合——IDEA 里把项目编码设为 UTF-8存数据库的字符集也统一成 utf8mb4两边对不上会出现「代码看着没问题但数据乱了」的玄学问题。driver-class-name在 MySQL 8 里必须是com.mysql.cj.jdbc.Driver网上老教程写的com.mysql.jdbc.Driver在 MySQL 8 下会直接 ClassNotFoundException。5. 避坑排查数据库时区、中文乱码、跨天计费与并发抢车位的翻车记录5.1 MySQL 时区报错与驱动类名变化现象项目启动时控制台报The server time zone value ??? 标准时间 is unrecognized或者报java.sql.SQLException: The server time zone ...。原因MySQL 8 的 JDBC 驱动默认使用 UTC 时区而本机 MySQL 服务器时区是中国标准时间两边对不上驱动拒绝建立连接。另一个常见伴随错误是 ClassNotFoundException驱动类没找到。解决连接 URL 末尾加serverTimezoneAsia/Shanghai驱动类名统一写com.mysql.cj.jdbc.Driver。如果两个都配了还在报错就在 MySQL 命令行执行SELECT NOW()确认数据库本身的系统时间是对的。顺带检查一下 MySQL 服务是不是真的启动了——Windows 上服务没启动时报错信息往往也是连接失败但具体错误码不同注意区分是网络不通通信链路故障还是认证失败Access denied。5.2 中文乱码URL、建表字符集、IDE编码三个地方一起查现象页面上显示的车牌号「京A12345」变成「???」数据库里直接查也是乱码。原因这是一条链路的问题不是单点。最常见的是三层第一JDBC URL 没带characterEncodingutf8第二建表时没指定CHARSETutf8mb4默认继承了库的 latin1第三IDEA 的项目文件编码是系统默认的 GBKJava 源码里的中文字符串编译时已经坏了这种乱码在页面和数据库都可能表现为「本来应该是中文的位置出现奇怪字符」。解决三层同时改。URL 加useUnicodetruecharacterEncodingutf8建表语句统一带DEFAULT CHARSETutf8mb4如果表已经建了执行ALTER TABLE 表名 CONVERT TO CHARACTER SET utf8mb4IDEA 的 Settings 里把 Global Encoding、Project Encoding、Properties Files 三项都改成 UTF-8。排查顺序建议从数据库往上游走先在命令行里 INSERT 一条中文数据看能不能存能存说明数据库没问题问题在应用层。5.3 跨天停车费算错计费逻辑没按自然日循环现象晚上 22:00 停进去第二天早上 08:00 开出去系统算出来 50 元而按单日封顶 30 元的规则应该收 30 元。或者反过来算出来 5 元明显漏了跨天后的时段。原因计费代码只算了「入场当天」的时长把exit_time和entry_time相减得到总分钟数再套一个简单的单价公式。总时长虽然是 10 小时但规则是「单日封顶 30 元」不是「总时长封顶 30 元」必须按自然日拆分——第一天 22 点到 24 点收取对应时段费用第二天 0 点到 8 点重新起算每天单独判断是否达到封顶。解决用前面 3.3 节那个按天循环的算法先判断是否跨天跨天就拆成多段逐日计算。写完之后把这两条测试用例跑一遍22:00 入场、次日 08:00 出场以及23:50 入场、次日 00:10 出场。后者尤其能检验免费时长跨天时的处理很多实现会把免费时长算在第一天头上导致第二天开始计费的时间点算不准。5.4 端口被占用先看清楚是谁占的再动手现象mvn spring-boot:run启动时报Web server failed to start. Port 8080 was already in use或者 MySQL 连接报 3306 连接失败。原因上一个没关干净的 Java 进程还占着端口或者是本机装了其他服务比如某软件的 web 管理台占了 8080、另一个 MySQL 实例占了 3306。很多人这时候直接改端口号改完发现新的端口也被占了越改越乱。解决先找到占用者再决定。Windows 命令行执行netstat -ano | findstr 8080Linux 执行lsof -i:8080拿到 PID 后用taskkill /PID 端口号对应PID /F结束进程或者kill -9。看清楚这个 PID 对应的进程名再动手确认是不是自己上次启动的 Java 进程别把系统服务清了。如果确实是别的软件在用再改自己项目的端口Spring Boot 里改server.port就行但这属于妥协方案不如把端口腾出来。5.5 并发抢车位Select-Then-Update 的经典翻车现象用 Postman 或者 Jmeter 模拟两个请求同时入场都传同一个spaceId结果两个请求都返回成功一个车位给了两台车或者明明车位已被占第二次请求返回的还是成功。原因代码写成「先 SELECT 查 status 是否为 0再 UPDATE 改成 1」。两个线程同时 SELECT 都看到 status0然后都执行 UPDATE都成功了。这就是并发里的 check-then-act 竞态条件Java 里锁没加到数据库层面单靠synchronized在集群环境下也没用。解决把检查和更新合并成一条 SQL也就是 3.2 节的UPDATE ... SET status 1 WHERE id ? AND status 0以数据库的行锁保证只有一个请求能改成功影响行数为 0 的那个直接抛业务异常。事务要加上保证车位更新和记录插入同生共死。压测方式用 Jmeter 开两个线程同时发请求断言只有一个返回成功。这种「先查再改」的写法在库存扣减、优惠券领取、秒杀场景里是同一个坑搞明白这一个这一整类问题都通。6. 再进一步用边界测试和月卡扩展验证你的系统6.1 用 JUnit 参数化测试锁死计费边界手工测试计费规则很容易漏边界。JUnit 5 的参数化测试能把一批边界用例集中维护每次改完代码一键验证这是把系统从「能跑」推向「可靠」的关键动作。ParameterizedTest CsvSource({ 2024-01-01 10:00, 2024-01-01 10:30, 0.00, 2024-01-01 10:00, 2024-01-01 10:31, 5.00, 2024-01-01 23:50, 2024-01-02 00:10, 0.00, 2024-01-01 08:00, 2024-01-01 20:00, 30.00 }) void testCalcFee(String entry, String exit, String expected) { LocalDateTime entryTime LocalDateTime.parse(entry, DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm)); LocalDateTime exitTime LocalDateTime.parse(exit, DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm)); BigDecimal fee parkingService.calcFee(entryTime, exitTime, defaultRule); assertEquals(new BigDecimal(expected), fee); }四条用例覆盖了四个典型区间免费时长内、刚好超过免费时长 1 分钟、跨天但在免费时长内、单日超过封顶。特别注意第二条「10:31 出场」它验证的是向上取整逻辑——超过免费时长 1 分钟也要按整小时收费。答辩的时候把 IDEA 里测试通过的绿色对勾截图放进论文的测试章节比空写「经测试系统运行正常」有说服力得多。6.2 给月卡留扩展位规则引擎做一处的三个步骤现在系统只有临时车收费逻辑实际场景里月卡是标配。扩展的时候不要满世界加 if 判断正确做法是基于fee_rule表的rule_name做策略分发第一步车辆表加一个card_type字段0 表示临时车1 表示月卡第二步出场计费时先查车辆类型月卡直接返回 0 元临时车走原有的calcFee第三步把calcFee方法签名改成接收FeeRule参数月卡对应的FeeRule的unit_fee设为 0 也行但单独写一个分支语义更清楚。要点是「规则变化收敛在 service 的一个方法里」而不是散落在页面模板、Controller、SQL 各处。这时候 2.3 节把 service 拆成接口和实现类的好处就体现出来了调用方只认接口你修改实现类不影响 controller 和页面。6.3 交付前检查清单从冷启动到功能自测系统写完后别急着交按这份清单过一遍每一行都是一个真实的翻车点检查项通过标准冷启动删除本地数据库从建表脚本重新执行应用能一键启动入场分配连续两次入场同一车位第二次返回「已被占用」免费时长入场后 29 分钟出场费用为 031 分钟出场按一小时计跨天计费22:00 入场、次日 08:00 出场金额等于两天的封顶叠加分页总数字段当页数据量小于页大小如最后一页只有 3 条时页码显示正确中文显示车牌含中文如京A时页面与数据库均无乱码记录持久化重启应用后在场车辆记录仍然存在车位状态仍然是占用异常输入车牌为空、车位号不存在、金额非法字符均返回友好提示冷启动这一项尤其值得做把数据库整个 drop 掉从建表脚本一条条执行再启动项目、跑一遍完整流程。这个动作能暴露一半以上的环境问题——建表脚本缺了哪张表、初始数据没插入导致计费规则查不到、密码写死在配置里换了机器就连接失败全都会现原形。我个人的习惯是做完这些检查之后再故意把数据库停掉启动一次项目看报错信息是否友好这个「黑匣子测试」能让你提前想清楚系统在故障时的表现。答辩老师不会真的去你电脑上敲命令但他会问「如果 MySQL 挂了你的系统会怎样」能现场演示一个清晰的报错页比支支吾吾说「应该不会挂」强得多。一次课设做完代码能力是其次建立这套「边界意识、持久化验证、冷启动自测」的习惯才是真正的收获希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网