写论文、搞课程设计、应付毕设答辩的时候很多同学一听到“Java项目源码”第一反应就是去下载一个成品然后改个名字交上去。但说句实话作为一个这些年看过无数份毕业设计代码的老开发停车缴费管理系统这个题目属于“看着简单、做起来全是细节”的典型代表。它考的不只是CRUD还牵扯到计费规则、状态流转、并发场景下的数据一致性这些实战中躲不开的问题。今天我就拿一个基于SSMSpring SpringMVC MyBatis、用IDEA开发的停车场停车缴费管理系统项目当例子把我个人认为怎么拆解、怎么落地、怎么避开那些坑一次性讲透。这篇文章适合三类人一是正在做Java课程设计或者毕业设计的学生想要一份能讲清楚原理、能应付答辩的项目参考二是准备面试Java开发岗、想拿一个完整项目练手的新手三是工作了几年但一直没正经碰过SSM这套老组合、想快速补齐经验的人。我会从需求拆解、数据库设计、IDEA工程搭建、核心计费逻辑实现到典型报错排查一路讲下来尽量做到你能照着文章把项目跑起来并且能跟面试官把每个设计决策讲出道理。1. 项目到底在做什么需求梳理与技术选型很多同学拿到“停车场停车缴费管理系统”这个题目后第一反应就是赶紧建表、写增删改查。其实这种思路很容易把自己带沟里。你一上来就写代码写了一半发现需求没想清楚数据库字段改来改去Mapper重写了好几遍最后整个项目变成一坨浆糊。我建议所有做项目的人无论题目多简单先停下来把需求讲清楚。1.1 一个停车管理系统要解决哪些事停车缴费管理系统本质上是一个“状态机 计费引擎 管理后台”的组合体。我们拆开来看车辆入场车主开车进停车场系统记录车辆车牌号、入场时间、分配车位并把车位状态从“空闲”改成“占用”。车辆出场车主开车离场系统根据入场时间和出场时间计算停车时长再根据计费规则算出费用记录缴费订单把车位状态改回“空闲”。车位管理管理员能维护车位的编号、区域、类型普通车位、VIP车位、新能源车位等能查看当前哪些车位被占用。缴费记录每一笔出场收费都要形成一条记录方便日后的对账、查询和统计。用户管理系统分管理员和普通操作员角色管理员负责配置计费规则、管理用户操作员负责日常的入场出场登记。这个需求列表看着不复杂但你要注意里面藏着几个真正考人的点一是计费时长的计算不是简单的时间差涉及跨天、免费时长、封顶价格二是车位状态的流转必须和订单状态保持一致不能出现车停着但车位显示空闲的情况三是并发场景比如两个车牌号完全不同的车同时要进入同一个车位系统不能都放行四是缴费记录的准确性一旦金额算错对账就是灾难。我见过太多版本的这个项目功能表拉得很长页面也很花哨结果一问核心的计费逻辑发现就是拿时间差乘了个单价免费时长和封顶金额压根没实现。这种项目在答辩现场非常容易被问穿。1.2 为什么选了SSM而不是Spring Boot这个点我必须单独拿出来说因为你在答辩的时候大概率会被问到“现在都2025年了为什么你还用SSM而不是Spring Boot”我自己给学生的解释是SSM虽然配置繁琐但它能让你看清框架之间是怎么整合的。Spring管理对象、SpringMVC处理请求、MyBatis操作数据库这三层之间的边界非常清楚。你亲手把spring.xml、spring-mvc.xml、mybatis-config.xml一个个配出来才会理解Bean是怎么注入的、Mapper代理是怎么生成的、DispatcherServlet是怎么把请求分发到Controller的。换成Spring Boot这些东西全是自动装配初学者很容易迷失在“注解一加就能跑”的假象里。当然从实际开发角度讲Spring Boot确实是主流所以我也建议在项目里体现一点“进化”思维。比如在答辩时你可以说“我这次用SSM是为了把框架底层机制吃透同时我在配置上做了类似Spring Boot的约定优化比如把数据源参数抽离到properties文件、用包扫描减少Bean配置。”这种话一亮出来老师会觉得你是真懂而不是只会照着教程敲。从技术方案选型角度这个项目最终确定的技术栈是层技术说明前端展示JSP Bootstrap jQuery课程设计经典组合够用且容易演示控制层SpringMVC负责请求路由和参数绑定业务层Spring事务管理、业务逻辑持久层MyBatisSQL灵活可控便于调优数据库MySQL 5.7关系型数据存储容器Tomcat 8.5Servlet运行环境构建工具Maven依赖管理和打包开发工具IntelliJ IDEA主力IDE选JSP不选前后端分离不是因为我不会Vue而是因为这个项目的核心是后端业务逻辑JSP配合JSTL可以快速把管理后台的增删改查页面做完避免在答辩时被“跨域、Token鉴权、前端工程化”这些偏离主题的问题带偏。你在项目里表达的应该是“我知道更现代的方案但在课程设计这个场景下我的选型是为了聚焦核心问题”。1.3 整体架构SSM三层在项目里怎么分工这个项目的包结构我建议这样设计com.parking.controller // 控制器 com.parking.service // 业务接口 com.parking.service.impl // 业务实现 com.parking.dao // MyBatis Mapper接口 com.parking.entity // 实体类 com.parking.common // 公共工具类、常量类 com.parking.interceptor // 登录拦截器请求流转路径是JSP页面发起请求 → DispatcherServlet → Controller → Service接口 → ServiceImpl → Mapper接口 → MyBatis映射文件 → MySQL数据库。数据再一层层返回到页面。我在项目中刻意保持了Controller层的“薄”Controller里面只做参数接收、简单校验、调用Service、返回视图不写任何业务逻辑。业务逻辑全部放在Service层这样才能用Spring的声明式事务来管理。比如入场操作要同时“插入车辆记录”和“更新车位状态”如果这两步不放在同一个事务里万一插入成功但更新失败就会留下脏数据。我把这些逻辑写进一个有Transactional注解的Service方法数据库层面就能保证原子性。MyBatis的Mapper层只负责SQL。很多新手喜欢在Service里用MyBatis Plus那种自带的方法但这个项目我建议手写SQL因为你要对表结构、字段、关联查询有清清楚楚的认识。答辩时老师随口问一句“你现在统计停车场收入是用的哪张表、怎么关联的”你要是答不出来就尴尬了。2. 数据库设计与核心表结构落地数据库设计是这种管理系统的地基地基建不好后面全是灾难。我见过不少项目把所有信息塞进一张大表字段冗余严重统计报表根本没法写。这个项目我用五张核心表把数据按业务边界拆得清清楚楚。2.1 五张核心表的设计思路我们来过一遍这五张表的字段设计以及每个字段背后的思考。第一张是用户表CREATE TABLE sys_user ( id INT NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码(密文), real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, role TINYINT DEFAULT 2 COMMENT 角色 1管理员 2操作员, status TINYINT DEFAULT 1 COMMENT 状态 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码字段注意不要存明文哪怕是用MD5加个盐也比明文强。很多课程设计项目不重视这个点但你在简历上写了“管理系统”面试官就会问安全问题。第二张是车位表CREATE TABLE parking_space ( id INT NOT NULL AUTO_INCREMENT, space_no VARCHAR(20) NOT NULL COMMENT 车位编号如A001, area VARCHAR(20) DEFAULT A区 COMMENT 区域, type TINYINT DEFAULT 1 COMMENT 车位类型 1普通 2新能源 3VIP, status TINYINT DEFAULT 0 COMMENT 当前状态 0空闲 1占用 2维护, remark VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_space_no (space_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;车位表里的status字段非常关键它代表车位当前的状态。你在页面展示车位图就是查这张表按区域分组。车位状态变更的时机要特别注意入场成功后立即置为1出场结算完成后置为0。第三张是车辆入场记录表也可以叫停车记录表CREATE TABLE parking_record ( id INT NOT NULL AUTO_INCREMENT, plate_number VARCHAR(20) NOT NULL COMMENT 车牌号, space_id INT NOT NULL COMMENT 车位ID, entry_time DATETIME NOT NULL COMMENT 入场时间, exit_time DATETIME DEFAULT NULL COMMENT 出场时间, status TINYINT DEFAULT 0 COMMENT 状态 0停车中 1已出场, PRIMARY KEY (id), KEY idx_plate (plate_number), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表是系统的核心表。每辆车入场就插入一条status为0的记录出场时更新exit_time并把status改为1。查询“当前停车场里有哪些车”就是查status等于0的记录联表查出车位编号。第四张是订单表CREATE TABLE order_info ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, record_id INT NOT NULL COMMENT 停车记录ID, plate_number VARCHAR(20) NOT NULL, entry_time DATETIME NOT NULL, exit_time DATETIME NOT NULL, duration_minutes INT NOT NULL COMMENT 停车时长分钟, total_amount DECIMAL(10,2) NOT NULL COMMENT 应收金额, pay_status TINYINT DEFAULT 0 COMMENT 支付状态 0未支付 1已支付, pay_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_record_id (record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单号和停车记录是一一对应的。订单号我习惯用时间戳加随机数生成格式类似yyyyMMddHHmmss 四位随机数这样既方便排序又能保证不重复。第五张是计费规则表。计费规则可以作为系统配置项而不是硬编码在代码里。这样管理员在前台改个单价系统里的计费逻辑立刻生效不需要重启项目。CREATE TABLE charge_rule ( id INT NOT NULL AUTO_INCREMENT, rule_name VARCHAR(50) COMMENT 规则名称, free_minutes INT DEFAULT 15 COMMENT 免费停车时长分钟, unit_price DECIMAL(10,2) DEFAULT 5.00 COMMENT 每半小时价格, max_daily_amount DECIMAL(10,2) DEFAULT 50.00 COMMENT 单日封顶金额, enable_status TINYINT DEFAULT 1 COMMENT 是否启用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 车位状态与订单状态的设计细节这里有个很多初学者容易忽略的设计问题车位表里有status字段停车记录和订单也各自有状态字段那它们之间怎么保持一致我在项目里定的规则是状态以停车记录和订单为准车位表的status只是一个冗余状态。也就是说当一辆车入场时我插入一条停车记录status0然后把对应车位status改成1。当车辆出场时我更新停车记录status1生成订单更新订单状态再把车位status改成0。这个过程必须在同一个事务里完成。那为什么不让车位表作为唯一状态来源呢因为车位表只存“当前状态”它没有历史轨迹。如果你要查询“过去七天停车场每天有多少辆车进来”光看车位表是看不出来的必须查停车记录。所以车位表的作用仅仅是页面展示时的“快照”真正的业务数据分布在停车记录和订单里。为了避免状态出现“车出场了但车位还是占用”这类脏数据我在出场业务逻辑里加了一步校验更新车位状态时传入的space_id必须和停车记录里的space_id一致并且在update语句里带上WHERE status 1。如果影响行数是0说明车位状态本身就不对抛出异常回滚事务。这个技巧叫乐观锁的简化版面试时能说出来绝对加分。2.3 计费规则怎么让系统按时间自动算钱计费算法是这个项目最容易出彩也最容易写错的地方。我来讲一个比较完善的实现思路。第一步判断是否在免费时段内。假设免费时长为15分钟停车总时长小于等于15分钟费用为0。第二步按“半小时”计费。这里不是简单的总时长除以30因为你跨过31分钟和正好30分钟费用是不同的。我推荐的算法是先把免费时长扣除剩下的分钟数按向上取整到半小时。比如停45分钟先扣15分钟免费剩余30分钟计费单位数为1。如果停50分钟剩余35分钟向上取整得到2个单位。第三步考虑封顶。单日封顶50元意思是车辆一天之内最多收50元。这里的“一天”按自然日算还是按24小时滚动算需要定义清楚。我采用的是自然日封顶也就是跨天时重新计算封顶额度。这是因为自然日符合大众认知也好答辩解释。为什么计费公式看起来简单还要强调“向上取整”因为数据库里存时长是整数分钟如果你直接用Decimal的除法很容易出现1.7个计费单位这种情况再乘单价就会出现小数位数很长的问题。所以计费单位必须用整数。我把计费逻辑封装成一个独立的chargeUtil类里面定义了一个方法calculateAmount(Date entryTime, Date exitTime, ChargeRule rule)方法内部先用(exitTime.getTime() - entryTime.getTime()) / 60000算出总分钟数再按上面的规则计算。这样如果把车库改成按小时计费只需要改这一个方法。3. IDEA里的实操搭建从零把项目跑起来这一部分我按实际动手的顺序走一遍从环境准备到工程配置再到核心功能代码实现。你照着做少走弯路。3.1 环境准备IDEA配置、Maven、Tomcat这里默认你已经装好了JDK 1.8。为什么强调JDK8因为SSM这套组合最成熟的版本就在JDK8上你换JDK11反而可能碰到一些老库的兼容问题。IDEA推荐用2022版以上社区版也可以跑这个项目因为不涉及Spring Boot专属插件。Maven的话建议用IDEA内置的Bundled Maven不要自己额外配因为版本兼容问题很烦。IDEA里打开Settings → Build Tools → Maven查看Maven home path是否指向了自带路径。还要设置一下User settings file指向自己的settings.xml或者直接用默认的镜像源。国内网络环境下建议在settings.xml里配阿里云镜像否则下依赖能让你怀疑人生。Tomcat方面我用的Tomcat 8.5。在IDEA中点击Run → Edit Configurations左上角加号选择Tomcat Server → Local然后在Application server那里点Configure选择本地的Tomcat目录Deployment选项卡里点加号把项目的war包加进去。这里有个小技巧Application context建议填/parking这样访问路径就是http://localhost:8080/parking/清晰好记。强烈建议先创建一个空的Maven WebApp项目验证环境再往里面加SSM依赖。别一上来就把完整的pom.xml贴进去万一环境出问题你都不知道是依赖冲突还是Tomcat没配好。3.2 项目骨架与配置文件怎么写创建好Maven项目后第一件事不是写代码而是把目录结构建完整src/main/java src/main/resources src/main/webapp/WEB-INF/jsp src/main/webapp/staticresources目录下放四个核心配置文件jdbc.properties、spring.xml、spring-mvc.xml、mybatis-config.xml。还有一个log4j.properties不看日志调不了错。jdbc.properties的内容大概是这样的jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/parking_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456这里我特别提醒一下url里的参数serverTimezoneAsia/Shanghai是必须的不然MySQL 8的驱动会报时区错误。useSSLfalse是因为本地开发不需要SSL免得控制台刷一堆警告。spring.xml负责配置数据源、SqlSessionFactory、Mapper扫描、Service扫描和事务管理器。核心片段长这样context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.parking.dao/ /bean tx:annotation-driven transaction-managertransactionManager/很多新手在这里会漏了mapperLocations导致项目启动成功但一访问接口就报“Invalid bound statement (not found)”。这个坑太经典了我后面排查章节会单独讲。spring-mvc.xml里核心就是开启注解驱动、配置视图解析器、扫描Controller、放行静态资源mvc:annotation-driven/ context:component-scan base-packagecom.parking.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean mvc:default-servlet-handler/视图解析器的prefix和suffix一定要确认和你的JSP目录对应否则Controller返回一个字符串页面死活渲染不出来报404你都不知道去哪查。3.3 核心代码实现车辆入场、出场计费、记录查询准备好配置文件后核心功能代码就是重头戏。我先说车辆入场这个功能的完整链路。Controller层我设计是这样的Controller RequestMapping(/parking) public class ParkingController { Resource private ParkingService parkingService; RequestMapping(/entry) ResponseBody public Result entry(RequestParam String plateNumber, RequestParam Integer spaceId) { return parkingService.carEntry(plateNumber, spaceId); } RequestMapping(/exit) ResponseBody public Result exit(RequestParam Integer recordId) { return parkingService.carExit(recordId); } RequestMapping(/list) public String list(Model model, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { PageResult pageResult parkingService.queryRecordPage(pageNum, pageSize); model.addAttribute(page, pageResult); return recordList; } }Service层入场逻辑是这样的Override Transactional(rollbackFor Exception.class) public Result carEntry(String plateNumber, Integer spaceId) { // 1.校验车位是否存在且空闲 ParkingSpace space spaceMapper.selectById(spaceId); if (space null || space.getStatus() ! 0) { return Result.error(车位不可用); } // 2.校验车辆是否已经在场内防止重复入场 ParkingRecord record recordMapper.selectUnfinishedByPlate(plateNumber); if (record ! null) { return Result.error(该车辆已在停车场内); } // 3.插入停车记录 ParkingRecord newRecord new ParkingRecord(); newRecord.setPlateNumber(plateNumber); newRecord.setSpaceId(spaceId); newRecord.setEntryTime(new Date()); newRecord.setStatus(0); recordMapper.insert(newRecord); // 4.更新车位状态为占用 int rows spaceMapper.updateStatus(spaceId, 1); if (rows 0) { throw new RuntimeException(车位状态更新失败); } return Result.success(入场成功); }这里有意思的是我在做“重复入场”校验。很多基础版项目只做了车库空位判断没做车牌去重结果同一个车牌扫两次系统就生成了两条停车记录。这在真实场景里是个致命问题。出场计费逻辑是核心中的核心Override Transactional(rollbackFor Exception.class) public Result carExit(Integer recordId) { ParkingRecord record recordMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { return Result.error(停车记录不存在或已出场); } Date exitTime new Date(); int totalMinutes (int)((exitTime.getTime() - record.getEntryTime().getTime()) / 60000); ChargeRule rule chargeRuleMapper.selectActiveRule(); if (rule null) { throw new RuntimeException(未配置有效的计费规则); } int billableMinutes Math.max(0, totalMinutes - rule.getFreeMinutes()); int units (int) Math.ceil(billableMinutes / 30.0); double amount units * rule.getUnitPrice(); // 封顶逻辑按自然日计算 // 这里简化处理实际应累加每个自然日的费用后取封顶 double finalAmount Math.min(amount, rule.getMaxDailyAmount()); // 创建订单 OrderInfo order new OrderInfo(); order.setOrderNo(generateOrderNo()); order.setRecordId(record.getId()); order.setPlateNumber(record.getPlateNumber()); order.setEntryTime(record.getEntryTime()); order.setExitTime(exitTime); order.setDurationMinutes(totalMinutes); order.setTotalAmount(finalAmount); order.setPayStatus(1); // 线下支付场景出场即支付完成 order.setPayTime(exitTime); orderMapper.insert(order); // 更新停车记录状态 recordMapper.updateExitStatus(recordId, exitTime); // 释放车位 spaceMapper.updateStatus(record.getSpaceId(), 0); return Result.success(order); }注意我在出场逻辑里把支付状态直接设为1因为这是“停车缴费管理系统”线下收费场景出场即结清。如果你的项目要求做“先出场后线上支付”那这里就需要区分支付状态车位释放也要跟支付成功绑定。不同需求的差异点答辩时你要能分析清楚。记录查询功能主要是一个带条件的分页查询支持按车牌号模糊查询、按时间段查询停车记录。MyBatis里写动态SQL用where标签加if判断前端做一个简单的分页导航条配合PageHelper插件。PageHelper引用很简单引入依赖后在Service层调用PageHelper.startPage(pageNum, pageSize)紧接着的Mapper查询就会自动分页最后封装一个PageResult返回就行。4. 坑与解法实训中踩过的常见问题这一节我写点实在的都是这个项目里最常出现的问题。我把排查思路和解决方案写清楚相当于是项目调通的经验手册。4.1 JDBC驱动和数据库连接问题最常见的报错是ClassNotFoundException: com.mysql.jdbc.Driver。原因很简单pom.xml里没引入MySQL驱动或者引入的版本和数据库不匹配。我的建议是直接用mysql-connector-java5.1.49跟MySQL 5.7配合非常稳。如果你用的是MySQL 8需要引入8.x版本驱动并把driver改成com.mysql.cj.jdbc.Driver。另一个高发问题是“Access denied for user ‘root’‘localhost’”。这个绝大多数情况是密码写错。注意jdbc.properties文件里不能有空格jdbc.password123456写成jdbc.password 123456虽然看着差不多但取出来的值可能带空格导致认证失败。我的习惯是数据库密码先用纯数字或纯字母测试确认连接没问题再换复杂密码不然出问题都不知道是配置还是密码的问题。还有一个时区相关的隐藏坑MySQL 8默认时区不是北京时间如果你url里没加serverTimezoneAsia/Shanghai查询时间字段会差8个小时。停车系统的入场时间出场时间差8小时计费直接乱套。4.2 Tomcat启动异常排查Tomcat启动时报各种奇怪的错先别急着搜报错原文第一步是看日志。IDEA控制台里如果你看到类似Caused by: org.springframework.beans.factory.BeanCreationException那基本是Spring配置问题。常见的几种情况一种是Error creating bean with name sqlSessionFactory这种通常是数据库连接不上或者mybatis-config.xml里配置的mapperLocations路径不对。你先检查资源文件是否被Maven编译到了classes目录IDEA里如果一个文件放在resources下但还是找不到去File → Project Structure → Modules里看看mark as resources有没有设置对。一种是NoClassDefFoundError这是典型的依赖缺失。你pom.xml里如果用了scopeprovided/scope的servlet-api同时又引了别的版本的servlet-api启动就冲突。解决方法是统一依赖版本或者干脆不显式引入servlet-api让Tomcat提供。还有一种情况很气人Tomcat能启动但访问http://localhost:8080/parking/报404。这时候去查看Tomcat的conf目录下server.xml看看Context配置的docBase和path是否和IDEA里的Application context一致。更简单粗暴的方式是清掉Tomcat缓存把work目录下的Catalina目录删掉重新构建项目。4.3 乱码问题的统一处理SSM项目乱码率极高因为它涉及请求、响应、数据库三层字符集。先说数据库层面建库的时候你用Navicat图形界面建默认字符集很可能不是utf8mb4。如果数据表已经建好了用ALTER TABLE parking_record CONVERT TO CHARACTER SET utf8mb4;改一下。我的习惯是建库语句里直接指定CREATE DATABASE parking_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后是连接层面jdbc.url里必须带useUnicodetruecharacterEncodingutf8。然后是Tomcat层面conf/server.xml里Connector标签加URIEncodingUTF-8。然后是Spring层面spring-mvc.xml里配置一个CharacterEncodingFilter强制所有请求和响应都走UTF-8。这三层全部统一成UTF-8后绝大多数乱码都能解决。如果JSP页面还是有乱码记得在JSP文件最上面加% page contentTypetext/html;charsetUTF-8 languagejava %并且保证文件本身是UTF-8编码保存的。IDEA右下角可以看文件编码如果显示GBK就转成UTF-8。4.4 MyBatis映射的经典报错这个项目里MyBatis的报错会占掉所有报错的一小半。新手最爱遇到的是这个Invalid bound statement (not found): com.parking.dao.ParkingRecordMapper.selectUnfinishedByPlate这个报错几乎可以断定是mapperLocations没配对。你在spring.xml里配置的classpath:mapper/*.xml那么你的Mapper XML文件必须放在src/main/resources/mapper/目录下并且文件名要和接口名对应比如ParkingRecordMapper.xml对应ParkingRecordMapper接口。还要检查XML文件里的namespace是不是写了接口的全限定名mapper namespacecom.parking.dao.ParkingRecordMapper如果namespace写错或者接口方法名和XML里的id不一致也会报这个错。还有一个坑是RT参数映射。MyBatis在写动态SQL的时候如果接口方法参数没有加Param(plateNumber)注解XML里写#{plateNumber}会报“Parameter plateNumber not found”。解决办法是参数多的方法统一加Param注解参数少的用单个实体对象。我习惯是Controller传给Service一些基本类型Service传给Mapper时统一封装成实体或者Map这样XML中引用字段名不会乱。4.5 前端页面常见状态显示问题管理后台有一个经典需求停车的车位要在页面上用不同颜色标出来。有的同学用JavaScript给每个车位状态赋值刷新后状态丢失或者车位状态和数据不同步。我的建议是后端渲染时直接把车位状态带出来JSP里用JSTL判断${space.status 1}来渲染颜色不用前端二次请求。这样刷新页面状态天然一致。还有一个和后端无关但很折磨人的细节Bootstrap的modal弹窗里嵌套日期选择器在移动端会弹不出输入法。如果你在车场门口用平板操作这个问题会让你很崩溃。解决方案是用readonly属性让输入框只读用日期插件选择时直接赋值不弹软键盘。5. 项目扩展方向与个人体会这个项目做到这个程度作为课程设计和毕业设计已经很能打了。但你如果想让它在简历和面试里更有竞争力我建议再补两个能力点。一是数据可视化增加一个统计页面用ECharts把每日收入、车辆进出场趋势、车位利用率画出来。实现方式不复杂后端写一个统计接口返回近7天每日收入数组前端用Ajax拿到数据渲染成折线图。这个功能一旦加上项目瞬间显得完整很多答辩时老师特别喜欢看图表。二是Spring Boot迁移思考SSM项目调试通过后可以尝试把Spring和SpringMVC的XML配置改造成Spring Boot的方式通过启动类和注解配置把项目跑起来。这个过程能让你真正理解“约定优于配置”是什么意思。我在面试候选人时如果他说自己做过SSM项目我通常会追问一句“如果现在用Spring Boot重写哪些配置可以省略为什么”能说清楚的人真的不多说清楚了基本可以认定他是真做过。我个人在实际操作中的最大体会是这个项目的代码量不算大但业务逻辑设计的思考量很大。核心难点不在增删改查而在于如何设计表结构来保证数据一致性、如何处理计费规则的边界情况、如何在并发访问下保持车位状态正确。你在写代码之前把这些想清楚写出来的代码质量和直接抄模板完全是两个层次。最后再分享一个小技巧答辩前把项目从零到一在另一台电脑上部署一遍。数据库建库、导入SQL、IDEA配置、Tomcat启动、浏览器访问每一步都重新验证一次。我见过太多人自己电脑上能跑换台电脑就挂原因无非是环境变量没配、JDK版本不对、MySQL没起来。提前演练一遍现场演示才不会翻车。这也是做项目的基本功——交付的关键永远是“能跑”而不是“我写了多少代码”。
