
每年到准备课程设计或者毕业设计的节点停车场停车缴费管理系统这类题目总会被反复拿出来讨论。原因其实藏在业务特点里——车辆进场要记时间、选车位车辆离场要算时长、收费用中间还有车位状态变化和订单记录整个链条短而完整特别适合拿来展示JavaWeb的综合能力。我手上这份基于IDEA开发的SSM版本停车场停车缴费管理系统源码覆盖了从数据库设计、后端接口到管理后台统计报表的绝大部分内容。这篇文章准备把系统的核心模块、数据表关系、关键代码逻辑和部署过程全部拆开讲不管你是正在选题的学生还是想找一个业务案例练手SSM的Java开发者都应该会有收获。1. 为什么停车缴费系统还在用SSM写——业务链路和技术选型的冷思考1.1 停车缴费这条业务链路到底完整在哪先说业务本身。停车场停车缴费系统看起来简单实际拆开之后它可以分成四个连续环节入口登记、停车计时、出口结算、后台管理。入口登记需要处理车牌号、车位状态和入场时间。停车计时需要一套稳定的计费规则能处理跨时段、超时、封顶这些情况。出口结算要按规则算钱触发支付流程再释放车位资源。后台管理则涉及车位维护、订单查询、收入统计、登录权限。每个环节都有一张数据表和一组后端接口对应缺了哪一块业务流程就接不上。这种完整性对学习过程的价值比单纯的CRUD练手大得多因为你要真正考虑数据状态流转而不是造一个摆设项目。1.2 SSM和Spring Boot课程设计阶段怎么选现在很多新项目直接Spring Boot起步为什么还会推荐SSM因为SSM把JavaWeb的底层脉络暴露得更清楚。Spring负责对象管理和事务SpringMVC负责请求转发和参数绑定MyBatis负责数据库映射。三者各管一段每一个环节出错你都要回到配置文件去排查。这个排查过程本身就是学习你会慢慢理解DispatcherServlet为什么会把请求交给某个ControllerMapper接口为什么没有实现类也能直接注入Service方法上的事务注解到底对哪些操作生效。Spring Boot把这些都封装成了约定跑起来确实很快但答辩或者面试的时候能够把IOC容器、AOP切面、Mapper代理、事务传播行为讲清楚往往比一句我用Spring Boot十分钟搭好了项目更有说服力。所以课程设计和毕业设计阶段用SSM并不落伍反而是一种刻意练习。1.3 系统的角色划分与前后台边界这套系统从使用角色上可以分成管理员和前台收费员两类。管理员登录后台管理车位信息、查看车辆记录、汇总收入报表前台收费员负责车辆入场的登记和离场时的收费操作。两者共用底层业务逻辑只是权限范围不同。从功能边界看凡是涉及车辆进出场、车位状态、费用计算的接口都放在后端Service层凡是页面展示、数据回显、状态提示都放在前端JSP页面配合Ajax请求。这个划分方式也是SSM项目的标准解法后续如果要做小程序端或者对接道闸硬件后端接口是完全可以直接复用的。2. 数据表设计先从六张表把业务边界画清楚2.1 表清单系统用户、车位、车辆记录、订单、会员数据库是这类系统的地基表建错了后面所有代码都会跟着别扭。我这边按常规课程设计的做法把核心数据拆成六张表系统用户表、车位表、车辆出入记录表、停车订单表再预留会员卡表和月卡套餐表用于扩展。表名职责核心字段system_user管理员/前台账号id, username, password, roleparking_space车位资源id, space_no, type, status, locationcar_record车辆出入记录id, plate_no, space_id, entry_time, exit_timeparking_order缴费订单id, order_no, record_id, total_amount, pay_statusmember_card会员月卡id, plate_no, card_no, begin_date, end_datecard_type卡种套餐id, type_name, duration_days, price这个设计里car_record和parking_order分开车辆记录是流水订单是财务数据。两者生命周期不同查询场景也不同。比如查粤B12345今天停了多久直接查car_record查这个月微信支付收入多少直接查parking_order。混在一张表里会越写越别扭。2.2 车辆入场记录与订单分开的根本原因先说一个实际问题车辆入场时只产生一条记录但离场时可能因为优惠、会员、支付失败等原因需要补单、改金额甚至退单。如果记录和订单挤在同一张表这些操作会让一条业务数据的含义变得混乱——它到底是记录停车事实还是记录一笔财务流水分开之后car_record负责记录车辆的物理停留事实parking_order负责记录费用计算和支付结果。order里用一个record_id指向车辆记录再冗余一个plate_no方便按车牌查询。这样设计还有一个好处同一辆车一次停车可以对应多张订单比如先交了部分费用后来补缴超时费用两张订单都指向同一条停车记录数据不冲突。这是一套对账友好的设计。系统上线之后运营人员最经常问的一句话是今天收了多少钱、有多少笔订单没支付成功订单表独立出来这些问题都用一条SQL就能回答。订单表的建表语句参考下面这段即可CREATE TABLE parking_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(50) NOT NULL COMMENT 业务订单号, record_id BIGINT NOT NULL COMMENT 关联停车记录id, plate_no VARCHAR(20) NOT NULL COMMENT 冗余车牌号, total_minutes INT NOT NULL COMMENT 停车总分钟数, total_amount INT NOT NULL COMMENT 金额单位为分, pay_method TINYINT NOT NULL DEFAULT 0 COMMENT 0现金 1微信 2支付宝, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已退款, create_time DATETIME NOT NULL, pay_time DATETIME DEFAULT NULL, KEY idx_plate_no (plate_no), KEY idx_pay_status_time (pay_status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT停车缴费订单表;2.3 字段设计与枚举状态的理由几个字段细节值得说清楚。金额字段我用INT类型存分而不是用DECIMAL或FLOAT存元。计费系统里浮点数精度损失是致命的0.1 0.2在浮点运算里可能变成0.30000000000000004。存整数分展示的时候再除以100格式化稳定又简单。状态字段统一用TINYINT整数注释写清楚含义。车位状态0空闲、1占用、2维修支付状态0待支付、1已支付、2已退款。用数字的好处是以后加状态方便而且接口返回时可以直接映射中文文案不需要动数据库结构。索引方面最常用的查询是按车牌查最近记录和按时间范围查订单汇总所以给plate_no建普通索引给(pay_status, create_time)建联合索引。这两个索引能覆盖绝大多数报表查询又不至于让插入变慢。2.4 初始化数据与库表创建顺序导入数据库脚本的时候注意先建没有外键依赖的基础表再建业务表。一般顺序是system_user、parking_space、card_type这些基础表先建car_record和parking_order后建。MySQL有外键约束时建表顺序错了会直接报错。字符集建议直接用utf8mb4别用utf8。MySQL的utf8字符集是阉割版只能存基本多语言平面字符遇到生僻字和部分特殊符号会变成乱码。utf8mb4才是完整支持和字符集相关的排查工作能省掉一大半。3. IDEA里搭建SSM工程骨架依赖、配置文件的顺序与坑3.1 环境与版本组合SSM项目对版本组合有讲究不是版本越新越好。我建议的这套组合在兼容性和资料丰富度上比较平衡组件推荐版本说明JDK1.8稳定兼容所有SSM组件Maven3.6依赖管理MySQL5.7 或 8.0注意JDBC驱动选择Tomcat8.5支持Servlet 3.1Spring5.1.x与SpringMVC同版本MyBatis3.5.x配合mybatis-spring 2.xDruid1.2.x数据库连接池如果你本机装的是MySQL 8.0JDBC驱动要用8.x版本的mysql-connector-java驱动类名也变成了com.mysql.cj.jdbc.Driver。这个差异很隐蔽后面部署部分会专门讲。3.2 pom.xml依赖清单与版本选择依赖不建议一股脑全塞进去。SSM项目最核心的依赖其实只有八个左右其他都是配套。以Spring 5.1.x为例pom.xml里的关键部分properties spring.version5.1.20.RELEASE/spring.version mybatis.version3.5.6/mybatis.version mybatis-spring.version2.0.6/mybatis-spring.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency !-- 数据库连接池和驱动 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- JSON处理 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.11.4/version /dependency !-- Servlet和JSP API提供编译期依赖 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency /dependencies有几个容易忽视的地方javax.servlet-api的scope必须是provided因为Tomcat容器自带Servlet类重复引入会导致冲突mybatis-spring版本必须和MyBatis主版本匹配MyBatis 3.5配mybatis-spring 2.0.x否则运行时会报找不到类。3.3 配置文件的加载顺序SSM的配置文件多经常有人抄过来跑不起来十有八九是加载顺序问题。先梳理一下配置全家桶web.xml应用启动的总入口applicationContext.xmlSpring根容器管理Service、Dao、事务spring-mvc.xmlSpringMVC子容器管理Controller、视图解析器、静态资源jdbc.properties数据库连接参数mybatis-config.xmlMyBatis全局设置mapper/*.xmlSQL映射文件web.xml里ContextLoaderListener先启动Spring根容器然后DispatcherServlet启动SpringMVC容器。SpringMVC容器能访问父容器里的Service和Dao所以Controller可以自动注入Service。反过来Service层不应该扫描Controller这就是为什么要用component-scan指定包路径而不是直接扫整个项目。spring-mvc.xml里最容易被忽略的是注解驱动和静态资源配置mvc:annotation-driven/ context:component-scan base-packagecom.parking.controller/ !-- 放行静态资源否则js/css会被DispatcherServlet拦截 -- mvc:resources mapping/static/** location/static// mvc:default-servlet-handler/如果不配置mvc:resources浏览器加载JS和CSS时会请求Controller结果404页面样式全部丢失。这个错误出现频率极高但排查起来也最快F12看一眼请求路径就明白了。3.4 在IDEA中创建工程并配置Tomcat的完整过程工程结构按标准Maven分层创建controller、service、dao、entity、interceptor、utilresources下放配置文件。包命名用com.parking这种简洁结构别搞com.yourname.project.parking这种多重嵌套类多了之后自己都找不着。IDEA里跑SSM项目的操作路径按照下面顺序做基本一次通过File - New - Project选择Maven勾选Create from archetype选maven-archetype-webapp模板。设置Project SDK为JDK 1.8Maven配置成自己的本地仓库。在pom.xml添加依赖等Maven下载完成。建好包目录和配置文件。Run - Edit Configurations新增Tomcat Server - Local。Deployment标签页添加Artifact为war exploded。这个模式支持改动Java代码后快速热更新不用每次重启Tomcat。Server标签页设置JRE为项目JDK打开浏览器地址设为http://localhost:8080/项目上下文路径。启动前先确认MySQL服务已启动数据库已导入脚本。这套流程走通后项目就能在IDEA里按F5完成启动后续排错也集中在控制台日志和浏览器请求上效率比打war包扔进Tomcat高很多。4. 车辆进出场主链路状态流转、计费规则与事务边界4.1 入场并发条件下怎么避免车位被重复分配入场接口的业务逻辑其实只有四步校验车牌是否在库查一个空闲车位把车位状态改成占用插入一条车辆入场记录。看起来很简单的流程并发情况下会出现大问题。停车场还剩最后一个空车位两个收费员同时操作入场如果都用先查空闲再更新占用的写法两个请求都查到同一个空闲车位然后都更新成功车位就被分配给了两辆车。解决这个问题不需要引入分布式锁数据库层面的一条原子更新就能搞定Override Transactional(rollbackFor Exception.class) public ApiResult enterParking(String plateNo, Long spaceId) { // 乐观更新状态为0才会更新成功 int updated parkingSpaceDao.updateStatusById(spaceId, 1, 0); if (updated 0) { return ApiResult.error(该车位已被占用请重试); } CarRecord record new CarRecord(); record.setPlateNo(plateNo); record.setSpaceId(spaceId); record.setEntryTime(new Date()); carRecordDao.insert(record); return ApiResult.success(record); }核心在于update parking_space set status 1 where id ? and status 0这条SQL。数据库行锁保证同一时间只有一个请求能更新成功另一个请求的影响行数为0直接返回友好提示。这种乐观更新方式在课程设计中讲出来答辩老师会觉得你对并发有概念。4.2 计费规则设计按分钟计算、向上取整、分时段费率计费是这套系统的灵魂。很多项目把规则硬写在业务代码里后面调整一次费率要改三层代码非常痛苦。正确做法是抽出一个独立的费用计算工具类输入入场时间和出场时间输出停车分钟数和应收金额。参考常见的停车场计费方案规则可以这样设计免费时长15分钟首小时收费5元不足1小时按1小时计超过首小时后按每30分钟2元累加单日封顶30元跨天重新计算。单位统一用分钟金额统一用分避免浮点数。public class ParkingFeeCalculator { private static final int FREE_MINUTES 15; private static final int FIRST_HOUR_FEE 500; // 分 private static final int EXTRA_30_MIN_FEE 200; // 分 private static final int DAILY_LIMIT 3000; // 分 public static FeeResult calculate(Date entryTime, Date exitTime) { long minutes (exitTime.getTime() - entryTime.getTime()) / (1000 * 60); if (minutes FREE_MINUTES) { return FeeResult.of((int) minutes, 0); } int payableMinutes (int) minutes - FREE_MINUTES; int fee FIRST_HOUR_FEE; if (payableMinutes 60) { int extraCount (payableMinutes - 60 29) / 30; // 向上取整 fee extraCount * EXTRA_30_MIN_FEE; } if (fee DAILY_LIMIT) { fee DAILY_LIMIT; } return FeeResult.of((int) minutes, fee); } }计算里有个典型的取整技巧(N 29) / 30是整数向上取整的写法不需要转double再Math.ceil。例如多停了61分钟额外计费段是2段而不是1段。这个细节很值得在代码注释里写清楚。4.3 离场结账与订单生成的时序离场流程比入场复杂一点因为涉及金额计算和订单生成。基本时序是按车牌查出未离场的记录计算停车费用创建订单将支付状态置为待支付支付成功后释放车位并更新出场时间。这个流程中最容易出事故的就是更新车辆记录和释放车位不在同一个事务里。如果车辆记录已更新出场时间但车位状态更新失败就会出现车已经开出去了后台显示车位还占用。解决方法是把离场动作放到同一个事务方法里任一步异常整体回滚。支付成功后的状态更新推荐用这条SQLUPDATE parking_order SET pay_status 1, pay_time NOW() WHERE order_no ? AND pay_status 0加pay_status 0条件确保支付这个动作的幂等性。哪怕因为网络抖动前端重复调用了支付确认接口第二个请求的影响行数是0不会出现重复入账的问题。4.4 模拟支付课程设计级别怎么处理支付状态真实对接微信或支付宝支付需要商户号、证书、回调地址对课程设计来说门槛偏高。一般做法是用模拟支付流程替代前端展示一个收款二维码图片用户点击我已支付按钮后端直接把这个订单状态改为已支付。如果想要演示效果更真实一点可以在订单创建后把状态设为待支付启动一个定时任务每隔30秒扫描待支付订单超过2分钟自动置为已支付。这样就能演示扫码后稍等片刻订单状态自动变更的效果同时不会引入真实支付SDK的复杂度。等到真正需要接支付时只需要替换支付确认接口的内部实现订单表和事务逻辑完全不用动。5. 管理后台的实打实功能登录拦截、车位维护与统计报表5.1 登录与权限用拦截器而不是引入复杂框架后台管理必然要控制访问权限。很多SSM课程设计一上来就集成Shiro或Spring Security结果配置量大增答辩时又讲不清楚原理。基于当前项目的规模用SpringMVC的HandlerInterceptor实现一个登录拦截器就够了代码量只有十几行而且逻辑透明。public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { // 判断是不是Ajax请求是则返回401状态码否则重定向到登录页 String requestedWith request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { response.setStatus(401); } else { response.sendRedirect(request.getContextPath() /login); } return false; } return true; } }在spring-mvc.xml里注册拦截器拦截/admin/**路径放行/login和静态资源mvc:interceptors mvc:interceptor mvc:mapping path/admin/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/static/**/ bean classcom.parking.interceptor.AdminInterceptor/ /mvc:interceptor /mvc:interceptors这里有一个很实用的细节点拦截器里区分了Ajax请求和普通页面请求。后台页面很多操作都是用Ajax发起的如果用户登录过期Ajax请求被重定向到登录页前端拿到HTML而不是JSON解析会直接报错。返回401状态码让前端统一跳转登录页体验就好很多。5.2 密码存储加盐MD5的必要性管理账号密码不能明文存数据库这是一个底线要求。最基础的做法是MD5加盐用户注册或初始化时生成一个随机盐值拼接用户名和盐值后做MD5把盐值和哈希值一起存库。校验时取出盐值重新计算比对。这样做对课程设计已经够用。虽然MD5本身不算强哈希但加了随机盐之后对常见的彩虹表攻击已经有足够抵抗力。答辩时如果被问到为什么不用明文从数据库泄露风险、彩虹表原理两个角度回答就很完整。5.3 车位管理与车辆记录查询车位管理的CRUD虽然简单但有两个业务校验不能省车位编号必须唯一删除前必须确认该车位当前是空闲状态。如果车位上停着车却允许删除数据关联立刻就乱了。车辆记录查询要支持多条件组合搜索按车牌模糊查、按时间段查、按支付状态查。这种多条件查询用MyBatis动态SQL最方便select idsearchRecords resultTypeCarRecord SELECT * FROM car_record where if testplateNo ! null and plateNo ! AND plate_no LIKE CONCAT(%, #{plateNo}, %) /if if testpayStatus ! null AND pay_status #{payStatus} /if if teststartTime ! null AND entry_time gt; #{startTime} /if if testendTime ! null AND entry_time lt; #{endTime} /if /where ORDER BY entry_time DESC /select分页在课程设计级别可以直接用PageHelper插件也可以手动写LIMIT。PageHelper原理是基于ThreadLocal和MyBatis拦截器回答得好是加分项但如果对自己的掌握程度没把握手动算offset和limit更稳至少不会被追问卡住。5.4 统计报表SQL聚合与ECharts展示管理后台首页不能只是几个空荡荡的链接数据可视化是最容易出效果的部分。常见统计项包括今日收入、今日停车次数、当前车位占用率、近七天收入趋势。这些数据不需要引入复杂的OLAP框架几条SQL就能搞定。近七天收入趋势SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, SUM(total_amount) AS amount FROM parking_order WHERE pay_status 1 AND create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY day ORDER BY day前端使用ECharts可以展示折线图和饼图比普通表格直观得多。一个建议不要把ECharts的js文件放在远程CDN而是下载到本地static目录。很多课程设计答辩现场网络不可控远程CDN加载失败图表全白演示效果直接打折扣。6. 本地部署全流程与五个很容易踩的坑6.1 部署步骤总览在IDEA里把项目拉起来跑通涉及的不只是点一个运行按钮我按踩坑概率从高到低讲几个实际问题。完整的部署步骤概括如下用IDEA导入Maven项目等依赖下载完成。修改jdbc.properties指向本地MySQL调整用户名和密码。在MySQL中创建数据库parking_db导入init.sql脚本。启动MySQL服务确认连接正常。配置Tomcat Server选择war exploded模式。启动项目访问登录页用管理员账号登录。这套流程本身不复杂真正让人卡住的往往是环境层面的一些细节。6.2 坑一MySQL版本与JDBC驱动不匹配MySQL 8.0的驱动类名已经不是com.mysql.jdbc.Driver而是com.mysql.cj.jdbc.Driver。URL也必须带时区参数否则启动报时区错误。MySQL 8.0环境下的jdbc.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/parking_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 jdbc.usernameroot jdbc.passwordyourpassword如果误用了5.x驱动类名启动日志会报ClassNotFoundException。如果你仍然用MySQL 5.7那驱动类保持com.mysql.jdbc.Driver即可URL也不需要serverTimezone参数。判断标准只有一个看你的MySQL是哪个大版本。6.3 坑二Maven依赖下载慢SSM项目依赖二三十个首次下载如果没有配置镜像在国内网络环境能卡半天。解决办法是修改Maven的settings.xml添加阿里云镜像mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirrorIDEA中配置Maven时要确保User settings file指向这个修改过的settings.xml同时勾选Override默认配置。镜像生效之后依赖下载速度会有质的提升。6.4 坑三控制台和页面中文乱码乱码问题的根源是编码不一致。Tomcat默认使用UTF-8但Windows控制台的默认编码可能是GBK于是日志里的中文变成乱码。处理要分三处IDEA的File Encoding设置为UTF-8Tomcat的VM options加上-Dfile.encodingUTF-8web.xml里配置CharacterEncodingFilter强制请求和响应都走UTF-8。filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping另外还要检查数据库连接URL中的characterEncoding参数以及建表时的CHARSET是否utf8mb4。四层编码全对齐乱码才能彻底根治。6.5 坑四8080端口被占用Tomcat默认端口8080机器上如果有其他进程占用了启动会报Port 8080 was already in use。Windows下排查端口占用netstat -ano | findstr 8080 taskkill /PID 对应的进程号 /F如果这个端口被Web应用正常占用也可以改Tomcat的server.xml端口比如改到8081或9090。建议能换端口就换杀进程容易误伤其他应用。6.6 坑五JSP页面加载不出静态资源页面没样式、没图标多半是SpringMVC把静态资源请求拦截了。DispatcherServlet的url-pattern如果是/所有请求都会经过它静态资源也不例外。解决方案就是前面spring-mvc.xml里提到的mvc:default-servlet-handler。这个handler会把SpringMVC处理不了的请求交还给容器默认Servlet从而正常加载JS、CSS和图片资源。错误现象可能原因处理方式ClassNotFoundException: DriverMySQL版本与驱动类名不匹配换8.x驱动改com.mysql.cj.jdbc.DriverPort 8080 was already in use端口被占用netstat查进程并kill或换端口页面中文乱码编码不一致三层UTF-8统一处理页面无样式静态资源被拦截mvc:default-servlet-handlerFailed to configure a DataSourcejdbc.properties连接参数错误检查URL、用户名、密码排查的通用思路是看控制台第一条异常堆栈而不是滚动到底部看最后一行。很多报错是连锁反应真正的根因往往在最前面。7. 答辩追问与进阶方向从能用到敢聊7.1 高频答辩问题与参考回答这类系统答辩时老师的问题集中在一类主题上框架原理、事务、并发、数据库设计。下面几个高频问题提前准备好思路1. 为什么把项目分成Controller、Service、Dao三层三层各有职责。Controller只负责接收参数和返回结果Service负责业务规则Dao负责数据访问。好处是当页面或接口变化时Service不用动当数据库从MySQL换成别的只需要改Dao层配置。这种分层降低了耦合也让单元测试更好做Mock掉一层就能单独测另一层。2. Spring事务在项目里怎么起作用在Service方法上标注TransactionalSpring容器用AOP机制在方法执行前开启事务、执行成功后提交、抛异常时回滚。底层调用的是事务管理器它绑定数据库连接所有DAO操作共享这个连接。这里还涉及传播行为REQUIRED如果外层方法已有事务内层方法默认加入同一个事务。3. 乐观锁是怎么实现的核心是带条件更新的SQLUPDATE ... WHERE id ? AND status 0。两个并发请求同时进来数据库行锁让它们串行执行第一个成功第二个更新0行根据影响行数判断就是失败。整个过程没有用SELECT FOR UPDATE所以死锁风险小、性能好。4. MyBatis中#{}和${}的区别#{}是预编译占位符最终生成?由PreparedStatement安全传值能有效防止SQL注入${}是字符串直接拼接传入的特殊字符可能破坏SQL结构。项目中能使用#{}就绝不使用${}。个别需要动态排序字段的场景比如传表名或列名才考虑${}但必须严格校验白名单。5. 如果停车记录表数据量增长到几千万怎么优化先建立合适的组合索引覆盖查询条件其次考虑按月份分表比如car_record_202501、car_record_202502再往后可以引入报表定时汇总把历史数据从在线查询中剥离。回答的重点不在于是否做过而是展现有数据量意识。7.2 从课程设计到生产可用的三个切入点这个系统做到能跑、能演示只是第一步真正要落地还有三个值得深入的方向。一是车牌识别的接入。真实停车场道闸会自动抓拍车牌课程设计里手工输入车牌是最大短板。可以了解OpenCV车牌识别或云平台OCR接口流程是摄像头抓拍图片OCR识别车牌调用入场接口自动登记。这部分做出来整个系统的自动化程度会完全不一样。二是支付回调与订单幂等。真实微信支付成功后平台会异步回调你的接口。回调可能重复触发所以处理逻辑必须是幂等的。做法是在回调处理前先查订单状态已支付直接返回成功不再二次改状态。这个思路和前面模拟支付里的pay_status 0条件完全一致只是回调触发方从定时任务变成了支付平台。三是异常订单的自动清理。车辆入场后如果发生异常离场比如道闸故障、逃费出场记录会一直卡在停车中状态。生产环境需要定时任务扫描超时记录比如超过24小时未离场自动标记异常并通知管理人员人工处理。这个任务用Spring Quartz或Spring Scheduled都能实现难度不高但对业务完整性提升明显。7.3 给自己的项目留点加分项很多人的代码能跑但答辩时没有亮点可聊。建议从两个方向补强。给计费规则写单元测试就是一个很好的加分项。计费逻辑是业务核心最容易出bug。用JUnit写几个测试用例覆盖免费时长、跨小时取整、封顶金额、跨天计费既证明功能可靠又能引出我知道怎么用JUnit做单元测试这个技能点。另外一个方向是日志规范。别用System.out.println输出调试信息换成slf4j logback在配置文件里设定日志级别。项目里出现异常时可以通过日志快速定位到具体方法和参数这个习惯在面试和工作中都非常受认可。我带过的学生里凡是能把计费规则完整性、事务回滚边界、并发车位分配这三点讲清楚的答辩效果都很好。因为这些点不是背出来的是真正写代码时想过的。反过来如果你只是把代码跑通就完事那这类SSM项目做得再多收获也有限。