SSM停车计费系统:时间切片计费与策略引擎实战

发布时间:2026/9/13 16:36:25
SSM停车计费系统:时间切片计费与策略引擎实战 简介本资源是一套基于JavaSSM框架开发的校内车辆停车计费收费系统面向计算机类专业本科生及初阶开发者聚焦校园场景下的车辆入场登记、计时计费、缴费核销与数据统计等核心业务闭环。项目已通过高分毕业答辩95分代码经Windows 10/11及macOS多环境实测运行稳定适合作为毕业设计、课程设计或教学演示原型也支持二次开发拓展功能。压缩包共1148个文件含27个Java后端逻辑类、39个JSP页面、256个HTML前端结构页、221个CSS样式文件、196个JS交互脚本以及PNG/GIF/JPG等静态资源和SQL数据库脚本整体体积18.84MB结构清晰、模块完整便于理解MVC分层设计与前后端协同流程。目前已有172人学习下载配套使用文档详尽涵盖部署步骤、数据库配置说明与功能操作指引是入门SSM实战与校园信息化系统开发的优质参考范例。1. 这不是又一个“学生信息管理系统”SSM 框架下校内停车计费系统的真实业务逻辑与工程落地价值你打开过几十个标着“基于 SSM 的 XX 管理系统”的毕业设计压缩包点开UserController.java发现它只做增删改查连分页都靠前端硬塞 100 条数据你见过太多 ER 图里“管理员-用户-订单”三张表撑起全部业务却从不处理“一辆车在 8:59 进入、9:01 离开该收 1 小时还是 2 小时”——而校内停车计费恰恰卡在这个毫秒级时间切片上。这个项目标题里的“校内车辆停车计费收费系统”核心不在“管理”而在“计费规则引擎”它必须支持按小时阶梯计费首小时 3 元、次小时 5 元、夜间免停时段22:00–6:00 不计费、教职工免费额度每月前 20 小时、黑名单车辆自动拦截、以及离场时实时生成含车牌号、进出时间、费用明细的 PDF 小票。它用 Java SSMSpring SpringMVC MyBatis实现不是因为框架多新潮而是因为这套组合能稳稳托住事务边界MyBatis 的Transactional控制扣费与状态更新原子性、灵活适配多套计费策略Spring 的Service多实现类 工厂模式、且便于嵌入校园一卡通对接接口SpringMVC 的RequestBody直接解析 IC 卡加密报文。适合正在做毕业设计、需要体现真实业务复杂度而非 CRUD 堆砌的计算机/软件工程专业本科生也适合想快速复现一个“有金额、有时效、有策略”的轻量级计费系统的 Java 初级开发者。2. 从数据库设计到 MyBatis 动态 SQL如何让停车记录既存得准又算得快2.1 校内停车场景倒逼的 5 张核心表设计逻辑普通管理系统常把“车辆”和“用户”合并为一张表但校内停车必须分离一辆私家车可能绑定多个教职工如夫妻共用车一个教职工也可能拥有不同牌照教学用车、科研用车、私家车。因此数据库必须包含t_vehicle车牌号为主键含车型、颜色、归属单位、t_user工号/学号为主键含身份类型教师/学生/访客、t_vehicle_user_rel多对多关系表记录某车牌在某用户名下生效的起止日期。最关键的是t_parking_record表它不能只存in_time和out_time—— 必须增加actual_in_time地感线圈触发时间精确到秒、actual_out_time道闸抬杆时间、fee_status0 待计算 / 1 已计费 / 2 已支付 / 3 已减免、fee_amount最终费用非空、fee_rule_id关联计费规则表t_fee_rule。t_fee_rule表则定义了rule_type1按小时阶梯、2按天封顶、3时段优惠、start_time/end_time适用时间段、fee_configJSON 字符串存储阶梯配置如{1:3,2:5,3:8}表示第1小时3元、第2小时5元、第3小时起每小时8元。这种设计让系统能回溯任意一笔费用的计算依据满足审计要求。2.2 MyBatis 中实现“时间切片计费”的动态 SQL 关键写法计费不是简单out_time - in_time而是要按实际停留分钟数映射到阶梯规则中。MyBatis 的foreach和choose是核心。在ParkingRecordMapper.xml中查询未结算记录并计算费用的 SQL 如下select idselectUnsettledRecords resultTypeParkingRecord SELECT pr.id, pr.vehicle_no, pr.actual_in_time, pr.actual_out_time, pr.fee_rule_id, fr.fee_config FROM t_parking_record pr LEFT JOIN t_fee_rule fr ON pr.fee_rule_id fr.id WHERE pr.fee_status 0 AND pr.actual_out_time IS NOT NULL /select真正的计费逻辑在 Service 层但 MyBatis 需支持按规则 ID 批量查询规则配置。更关键的是更新费用的 SQL需用CASE WHEN处理不同规则类型update idupdateFeeAmountAndStatus UPDATE t_parking_record SET fee_amount CASE WHEN fee_rule_id 1 THEN !-- 按小时阶梯计算总分钟数向上取整为小时 -- (SELECT SUM(CASE WHEN FLOOR((UNIX_TIMESTAMP(actual_out_time) - UNIX_TIMESTAMP(actual_in_time)) / 60 / 60) 1 THEN 3 WHEN FLOOR((UNIX_TIMESTAMP(actual_out_time) - UNIX_TIMESTAMP(actual_in_time)) / 60 / 60) 2 THEN 5 ELSE 8 * FLOOR((UNIX_TIMESTAMP(actual_out_time) - UNIX_TIMESTAMP(actual_in_time)) / 60 / 60) END) FROM t_parking_record pr2 WHERE pr2.id t_parking_record.id) WHEN fee_rule_id 2 THEN 15 !-- 按天封顶 15 元 -- ELSE 0 END, fee_status 1 WHERE id #{id} /update注意生产环境绝不推荐在 SQL 中硬编码阶梯逻辑。此处仅展示 MyBatis 动态能力。实际应将fee_configJSON 字段解析为 Java 对象在 Service 层用BigDecimal精确计算避免浮点误差再调用updateByPrimaryKeySelective()更新。SQL 中的UNIX_TIMESTAMP是 MySQL 特有函数若用 Oracle 需替换为EXTRACT(HOUR FROM (out_time - in_time))。2.3 防止“同一辆车重复进出场”导致计费错乱的数据库约束校内停车场入口常有两套地感线圈主备用若网络延迟导致两条in_time记录几乎同时插入后续out_time可能匹配错误。解决方案是在t_parking_record表上添加唯一索引UNIQUE KEY uk_vehicle_in (vehicle_no, fee_status)其中fee_status 0表示“待出场”。这样当第二条进场记录尝试插入时因vehicle_no相同且fee_status0已存在数据库直接报Duplicate entry错误。Service 层捕获该异常返回“车辆已在场内请勿重复入场”。此约束比应用层加锁更可靠且避免分布式环境下锁失效问题。3. Spring 事务与策略模式让“计费-扣款-开票”三步操作要么全成功要么全回滚3.1 Transactional 边界必须包裹整个计费链路而非单个 DAO 方法很多毕业设计把Transactional加在ParkingRecordDao.updateFeeAmount()上这是致命错误。计费过程包含1读取未结算记录2根据规则计算金额3更新fee_amount和fee_status4调用支付接口模拟5生成 PDF 小票并存入t_receipt表。这五步必须在一个事务内完成。正确做法是将Transactional加在 Service 方法上Service public class ParkingFeeService { Transactional(rollbackFor Exception.class) public void settleParkingFee(Long recordId) throws Exception { // 1. 查询记录 ParkingRecord record parkingRecordMapper.selectByPrimaryKey(recordId); if (record.getFeeStatus() ! 0 || record.getActualOutTime() null) { throw new RuntimeException(记录状态异常无法结算); } // 2. 计算费用调用策略工厂 BigDecimal fee feeStrategyFactory.getStrategy(record.getFeeRuleId()) .calculateFee(record.getActualInTime(), record.getActualOutTime()); // 3. 更新费用 record.setFeeAmount(fee); record.setFeeStatus(1); // 已计费 parkingRecordMapper.updateByPrimaryKeySelective(record); // 4. 模拟扣款实际对接校园一卡通 if (!deductFromCard(record.getVehicleNo(), fee)) { throw new RuntimeException(扣款失败); } // 5. 生成小票 Receipt receipt generateReceipt(record); receiptMapper.insert(receipt); } }提示rollbackFor Exception.class确保所有异常都触发回滚。若只写RuntimeExceptionIOException如 PDF 生成失败将不会回滚导致“已计费但未开票”的脏数据。3.2 用 Spring FactoryBean 实现计费策略的可插拔式扩展不同停车场可能启用不同规则教学区按小时阶梯、家属区按天封顶、临时访客车位按时长累加。硬编码if-else会让代码难以维护。标准做法是定义策略接口public interface FeeCalculationStrategy { BigDecimal calculateFee(Date inTime, Date outTime); }然后为每种规则写实现类如HourlyStepStrategy、DailyCapStrategy。关键在工厂类Component public class FeeStrategyFactory { private final MapInteger, FeeCalculationStrategy strategyMap new HashMap(); // 构造器注入所有策略实现 public FeeStrategyFactory( Qualifier(hourlyStepStrategy) FeeCalculationStrategy hourlyStep, Qualifier(dailyCapStrategy) FeeCalculationStrategy dailyCap) { strategyMap.put(1, hourlyStep); strategyMap.put(2, dailyCap); } public FeeCalculationStrategy getStrategy(Integer ruleId) { FeeCalculationStrategy strategy strategyMap.get(ruleId); if (strategy null) { throw new IllegalArgumentException(不支持的计费规则ID: ruleId); } return strategy; } }Spring 会自动将Service标注的策略实现类注入工厂。新增规则只需添加实现类、在构造器中注册无需修改工厂代码。3.3 支付回调与状态机如何应对“扣款成功但小票生成失败”的中间态校园一卡通系统返回“扣款成功”后若 PDF 生成因磁盘满失败记录会卡在fee_status1已计费但无小票。此时用户离场后无法查凭证。解决方案是引入状态机概念在t_parking_record表增加process_status字段0初始 / 1计费中 / 2扣款中 / 3开票中 / 4已完成 / 5异常。Service 方法改为Transactional public void settleParkingFee(Long recordId) { // ... 计费逻辑 ... record.setProcessStatus(1); parkingRecordMapper.updateByPrimaryKeySelective(record); // ... 扣款逻辑 ... record.setProcessStatus(2); parkingRecordMapper.updateByPrimaryKeySelective(record); // ... 开票逻辑 ... record.setProcessStatus(4); parkingRecordMapper.updateByPrimaryKeySelective(record); }后台定时任务每 5 分钟扫描process_status IN (1,2,3)的记录对超时如 30 分钟未完成的记录发送告警并提供人工干预入口。这比单纯依赖事务更适应真实支付场景。4. SpringMVC 文件上传与 PDF 生成让离场小票成为可验证的电子凭证4.1 用 MultipartFile 接收道闸抓拍图片并存入本地目录校内停车场出口需拍摄车辆离场照片作为凭证。SpringMVC Controller 接收文件PostMapping(/api/receipt/uploadPhoto) ResponseBody public ResultString uploadPhoto(RequestParam(file) MultipartFile file, RequestParam(recordId) Long recordId) { if (file.isEmpty()) { return Result.fail(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFilename recordId _ System.currentTimeMillis() ext; try { // 存入项目根目录下的 photos/ 子目录 String uploadPath photos/ newFilename; File uploadDir new File(uploadPath); uploadDir.getParentFile().mkdirs(); // 自动创建目录 file.transferTo(uploadDir); // 更新记录中的 photo_path 字段 ParkingRecord record new ParkingRecord(); record.setId(recordId); record.setPhotoPath(/photos/ newFilename); parkingRecordMapper.updateByPrimaryKeySelective(record); return Result.success(/photos/ newFilename); } catch (Exception e) { log.error(上传照片失败, e); return Result.fail(上传失败 e.getMessage()); } }注意生产环境不应直接存入 Web 应用目录有安全风险。此处为毕业设计简化实际应存入独立文件服务器或对象存储如 MinIO并在数据库中只存 URL。4.2 使用 iText7 生成含二维码的小票 PDF附完整依赖与代码小票需包含车牌号、进出时间、费用、收费员工号、二维码扫码可查该笔记录详情。iText7 是当前主流 PDF 生成库Maven 依赖dependency groupIdcom.itextpdf/groupId artifactIditext7-core/artifactId version7.2.5/version typepom/type scopecompile/scope /dependency dependency groupIdcom.itextpdf/groupId artifactIdbarcodes/artifactId version7.2.5/version /dependency生成核心代码public byte[] generateReceiptPdf(ParkingRecord record) throws Exception { ByteArrayOutputStream baos new ByteArrayOutputStream(); PdfWriter writer new PdfWriter(baos); PdfDocument pdfDoc new PdfDocument(writer); Document document new Document(pdfDoc, PageSize.A6.rotate()); // 小票用横向 A6 // 标题 Text title new Text(XX大学校内停车收费凭证).setFontSize(14).setFontColor(ColorConstants.BLUE); document.add(new Paragraph(title).setTextAlignment(TextAlignment.CENTER)); // 基本信息表格 Table table new Table(UnitValue.createPercentArray(new float[]{1, 1})) .useAllAvailableWidth(); table.addCell(new Cell().add(new Paragraph(车牌号).setBold())); table.addCell(new Cell().add(new Paragraph(record.getVehicleNo()))); table.addCell(new Cell().add(new Paragraph(入场时间))); table.addCell(new Cell().add(new Paragraph(sdf.format(record.getActualInTime())))); table.addCell(new Cell().add(new Paragraph(出场时间))); table.addCell(new Cell().add(new Paragraph(sdf.format(record.getActualOutTime())))); table.addCell(new Cell().add(new Paragraph(应收金额))); table.addCell(new Cell().add(new Paragraph(¥ record.getFeeAmount().toString()).setBold())); document.add(table); // 二维码内容为 recordId扫码跳转 /receipt/detail?idxxx BarcodeQRCode qrCode new BarcodeQRCode( https://parking.university.edu/receipt/detail?id record.getId(), 1, 1, null); Image qrImage new Image(qrCode.createFormXObject(pdfDoc)); qrImage.scaleAbsolute(80, 80); document.add(qrImage.setTextAlignment(TextAlignment.CENTER)); document.close(); return baos.toByteArray(); }生成的 PDF 字节流可直接通过ResponseEntitybyte[]返回给前端或存入t_receipt表的pdf_dataBLOB 字段。4.3 前端 Vue 组件调用小票生成接口并触发下载使用axios下载二进制 PDFasync downloadReceipt(recordId) { try { const response await axios.post(/api/receipt/generate, { recordId }, { responseType: arraybuffer } // 关键指定响应类型 ); const blob new Blob([response.data], { type: application/pdf }); const url window.URL.createObjectURL(blob); const link document.createElement(a); link.href url; link.download receipt_${recordId}.pdf; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); } catch (error) { this.$message.error(小票生成失败 error.response?.data?.message || 网络错误); } }提示若后端返回 JSON 错误如{ code: 500, message: xxx }responseType: arraybuffer会导致response.data是乱码。应在catch中检查error.response?.headers[content-type]是否为application/json再决定解析方式。5. 毕业设计答辩高频问题与 SSM 项目避坑清单从源码结构到部署细节5.1 答辩老师必问的 3 个 SSM 深度问题及回答要点问题回答要点切忌背诵用自己代码举例“为什么选 MyBatis 而不是 Hibernate”“Hibernate 自动生成 SQL 在复杂计费场景下难控制精度比如FLOOR((out-in)/3600)这种时间计算MyBatis 允许手写 SQL 或用script标签嵌入原生 SQL且SelectProvider可动态拼接不同规则的计算逻辑。我在ParkingRecordMapper.xml的selectUnsettledRecords就用了LEFT JOIN关联规则表Hibernate 的 HQL 对 JSON 字段fee_config解析支持弱。”“事务传播行为你用的是默认 REQUIRED有没有考虑过 REQUIRES_NEW”“没有。REQUIRES_NEW 会新建事务导致‘计费成功但扣款失败’时计费记录已提交无法回滚。我们严格遵循‘一个业务方法一个事务’原则。唯一用到REQUIRES_NEW的地方是日志记录——在settleParkingFee方法里我另写了一个Transactional(propagation Propagation.REQUIRES_NEW)的logOperation()方法确保即使主事务回滚操作日志仍能保存用于事后审计。”“SSM 各层职责是否清晰Controller 里有没有写业务逻辑”“绝对没有。Controller 只做三件事1参数校验用Valid注解2调用 Service 方法3封装Result返回。所有计费规则解析、金额计算、PDF 生成都在 Service 层。DAO 层只负责单表 CRUD多表关联用 MyBatis 的resultMap映射不写跨表 join 逻辑。例如ParkingRecord和Receipt的关联是在 Service 层先查 record再查 receipt而不是在 Mapper 里写JOIN。”5.2 源码包里最容易被忽略的 4 个部署细节毕业设计源码常因环境差异在答辩现场运行失败。以下配置必须显式说明MySQL 时区问题application.properties中必须添加spring.datasource.urljdbc:mysql://localhost:3306/parking?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8。否则actual_in_time存入数据库时会比系统时间少 8 小时。静态资源路径src/main/resources/static/photos/目录需在 Tomcat 的webapps/ROOT/下手动创建否则上传的照片路径 404。或在application.properties中配置spring.web.resources.static-locationsclasspath:/static/,file:./photos/。iText7 字体中文支持默认字体不支持中文需在 PDF 生成代码中加载 NotoSansCJKsc 字体PdfFont font PdfFontFactory.createFont(NotoSansCJKsc-Regular.otf, PdfEncodings.IDENTITY_H); document.setFont(font);并将字体文件放入src/main/resources/fonts/。JDK 版本兼容性SSM 项目普遍基于 JDK 8 编译。若答辩机装的是 JDK 17需在pom.xml中明确指定properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties5.3 数据库初始化脚本必须包含的 3 类基础数据仅导出表结构不够答辩演示需立即看到效果。init.sql应包含计费规则数据INSERT INTO t_fee_rule (id, rule_name, rule_type, start_time, end_time, fee_config) VALUES (1, 教学区小时阶梯, 1, 00:00:00, 23:59:59, {1:3,2:5,3:8}), (2, 家属区日封顶, 2, 00:00:00, 23:59:59, {cap:15});测试车辆与用户INSERT INTO t_user (user_id, user_name, user_type) VALUES (T001, 张教授, teacher); INSERT INTO t_vehicle (vehicle_no, vehicle_type, color) VALUES (粤B12345, SUV, 黑色); INSERT INTO t_vehicle_user_rel (vehicle_no, user_id, start_date, end_date) VALUES (粤B12345, T001, 2024-01-01, 2099-12-31);模拟停车记录用于演示结算INSERT INTO t_parking_record (id, vehicle_no, actual_in_time, actual_out_time, fee_rule_id, fee_status) VALUES (1, 粤B12345, 2024-06-01 08:30:00, 2024-06-01 09:45:00, 1, 0);执行init.sql后直接访问/parking/settle?recordId1即可触发完整计费-扣款-开票流程无需任何前置操作。本文还有配套的精品资源点击获取