尧图精选

SpringBoot+MySQL智能停车场管理系统:表结构、预约并发与计费规则

🕒 发布时间:2026/10/2 2:56:57 📁 来源:尧图网络
简介基于SpringBoot和MySQL构建的智能停车场管理系统源码包面向商业综合体、写字楼及住宅小区等停车管理场景覆盖车位预约、停车费动态计算、车辆进出记录、用户权限分级控制及运营数据统计等核心模块适合Java学习者和需要快速搭建停车场项目的开发者参考。该压缩包共77个文件大小约3.29MB包含Java源码、XML/MyBatis配置、JSP视图页面、JavaScript与CSS前端资源以及说明文档和附赠的docx资料便于从代码到部署全链路阅读。目前已有91人在线学习下载资源结构清晰附带的README和说明文件有助于理解项目启动方式。通过研读源码可以掌握SpringBoot与MySQL整合的业务开发思路、角色权限设计以及停车计费逻辑的实现细节为二次开发或课程设计提供完整范本。1. 智能停车场管理系统到底要解决什么先看清状态流转很多团队拿到“基于SpringBoot和MySQL的智能停车场管理系统”这个标题时第一反应是摄像头识别、道闸联动。真正进过现场的人会告诉你最容易翻车的是两件事车位从“预约”到“占用”之间的状态一致性以及停车费规则在免费时长、夜间价、跨天封顶叠加后的计算准确度。商业综合体要免停时长写字楼要按小时加单日封顶住宅小区还要给月租车绕开临停计费。SpringBoot负责把预约、进出、计费、权限拆成清晰的服务边界MySQL用事务和行锁保证同一车位的预约不超卖。这套方案适合毕设、中小物业系统和小型SaaS项目但动手前必须先把表结构和事务边界想清楚。2. 从表结构开始用 MySQL 设计车位、预约订单和计费规则数据表是这套系统的地基。很多开发喜欢先写代码再补表做到一半发现预约状态和进出记录对不上被迫改表结构那是成本最高的一条路。按“业务模块 - 实体关系 - 建表 SQL - 索引与统计表”的顺序来后面写 SpringBoot 时基本不用回头。2.1 模块拆分与实体关系标题里列了五个功能车位预约、停车费计算、车辆进出记录、用户权限控制、数据统计分析。落到 MySQL 里实际核心表就六张车位表t_parking_space车位编号、状态、车位类型、计费规则关联。计费规则表t_fee_rule免费分钟、单价、日封顶、夜间时段。用户表t_user账号、密码、用户类型用户类型直接控制后台权限。车辆档案表t_car车牌号、月租有效期、车辆类型。预约订单表t_reserve_order预约人、车牌、车位、预约时段、状态。车辆进出记录表t_parking_record入场时间、出场时间、时长、费用、支付状态。其中最关键的关系不是外键而是状态流转。一个车位的状态只有四种0 空闲、1 预约、2 占用、3 锁定。预约产生时车位从 0 变为 1入场拍照后车位从 1 变为 2出场结算后车位从 2 变为 0。如果没有这一层状态机就会出现“预约了车位但进场时发现车位被别人占了”的现场投诉。三种业务场景的差异主要落在计费规则上。商业综合体常见的规则是免费 2 小时、超出按小时计费、单日封顶写字楼往往是按小时计费、夜间有包段价住宅小区则是月租车直接走车辆档案出场时判断车牌是否在月租白名单内不走临停计费。所以t_fee_rule里需要有park_id做隔离同一条规则不能硬套所有停车场。2.2 核心建表 SQL可直接复制的六张表建表时统一用utf8mb4避免车牌录入生僻字或特殊字符时出现中文乱码。金额字段必须用DECIMAL(10,2)不要用DOUBLE这是停车费对账不翻车的第一步。-- 计费规则表按停车场区分规则 CREATE TABLE t_fee_rule ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, park_id BIGINT NOT NULL DEFAULT 0 COMMENT 停车场ID0表示通用规则, rule_name VARCHAR(50) NOT NULL COMMENT 规则名称如商业综合体/写字楼/住宅, billing_type TINYINT NOT NULL DEFAULT 1 COMMENT 1按时计费 2按次 3阶梯, free_minutes INT NOT NULL DEFAULT 0 COMMENT 免费分钟数, unit_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 每小时单价按次时表示单次价格, max_daily_price DECIMAL(10,2) DEFAULT NULL COMMENT 单日封顶金额NULL不封顶, night_start_time TIME DEFAULT NULL COMMENT 夜间开始如22:00, night_end_time TIME DEFAULT NULL COMMENT 夜间结束如06:00, night_unit_price DECIMAL(10,2) DEFAULT NULL COMMENT 夜间单价NULL表示与白天一致, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_park_id (park_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT停车计费规则表; -- 车位表 CREATE TABLE t_parking_space ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, space_no VARCHAR(20) NOT NULL COMMENT 车位编号如A-101, park_id BIGINT NOT NULL DEFAULT 0 COMMENT 停车场ID, space_type TINYINT NOT NULL DEFAULT 0 COMMENT 0普通 1新能源 2无障碍, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1预约 2占用 3锁定, fee_rule_id BIGINT DEFAULT NULL COMMENT 关联计费规则NULL走park_id默认规则, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_space_no (park_id, space_no), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位表;status用TINYINT而不是字符串是为了索引更小、查询更快。UNIQUE KEY uk_space_no保证同一个停车场内车位编号不重复这是预约和进出的引用的基础。version字段先留好后面实现并发控制时有用。-- 用户表user_type 直接决定角色权限 CREATE TABLE t_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt哈希不存明文, real_name VARCHAR(50) DEFAULT NULL COMMENT 姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, user_type TINYINT NOT NULL DEFAULT 1 COMMENT 1车主 2管理员 3物业, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 车辆档案表月租车白名单 CREATE TABLE t_car ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, user_id BIGINT NOT NULL COMMENT 车主用户ID, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, car_type TINYINT NOT NULL DEFAULT 0 COMMENT 0临时 1月租 2免费, valid_start_date DATE DEFAULT NULL COMMENT 月租生效日期, valid_end_date DATE DEFAULT NULL COMMENT 月租失效日期, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_plate_no (plate_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆档案表;car_type 1的月租车出场时直接检查valid_end_date只要在有效期内就不计算费用。car_type 2的免费车通常是救护车、消防车或内部工作车出场时走免费通道。-- 预约订单表 CREATE TABLE t_reserve_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(32) NOT NULL COMMENT 预约单号, user_id BIGINT NOT NULL COMMENT 预约人, plate_no VARCHAR(20) NOT NULL COMMENT 预约车牌, space_id BIGINT NOT NULL COMMENT 预约车位ID, reserve_start_time DATETIME NOT NULL COMMENT 预约开始时间, reserve_end_time DATETIME NOT NULL COMMENT 预约结束时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待入场 1已入场 2已取消 3已超时, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_space_time (space_id, reserve_start_time, reserve_end_time), KEY idx_user_id (user_id), KEY idx_status_time (status, reserve_start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位预约表; -- 车辆进出记录表一条记录是一辆车的一次完整停车 CREATE TABLE t_parking_record ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, record_no VARCHAR(32) NOT NULL COMMENT 记录流水号, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, space_id BIGINT DEFAULT NULL COMMENT 占用车位ID, entry_time DATETIME DEFAULT NULL COMMENT 入场时间, exit_time DATETIME DEFAULT NULL COMMENT 出场时间, duration_seconds BIGINT DEFAULT NULL COMMENT 停车时长秒, fee_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 应收金额, paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 实收金额, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付 2免费, record_type TINYINT NOT NULL DEFAULT 0 COMMENT 0临时 1预约 2月租, entry_image VARCHAR(255) DEFAULT NULL COMMENT 入场抓拍图片地址, exit_image VARCHAR(255) DEFAULT NULL COMMENT 出场抓拍图片地址, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_record_no (record_no), KEY idx_plate_time (plate_no, entry_time), KEY idx_entry_time (entry_time), KEY idx_exit_time (exit_time), KEY idx_pay_status (pay_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆进出记录表;这里刻意不加外键约束只用逻辑外键。原因为了高并发场景下插入订单和更新车位时不产生额外的锁竞争也为了避免删除车位时被历史预约单挡住。数据一致性交给 SpringBoot 的事务控制。预约订单表上的idx_space_time很重要。查询“某个车位在某个时间段有没有被预约”时如果只有space_id索引reserve_start_time的范围筛选会走全表。加上联合索引范围查询能直接命中索引。进出记录表上的idx_entry_time和idx_exit_time是给统计分析用的后面第 4.5 小节的日报 SQL 全靠这两个索引。2.3 要不要做统计表提前留好每日汇总标题里的“数据统计分析”如果每次都直接对t_parking_record全表GROUP BY数据量到十万级之后响应速度会明显下降。更常见的做法是建一张每日统计表定时任务在凌晨把前一天的数据聚合进去报表接口只查统计表。CREATE TABLE t_daily_stats ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, stats_date DATE NOT NULL COMMENT 统计日期, park_id BIGINT NOT NULL DEFAULT 0 COMMENT 停车场ID, total_cars INT NOT NULL DEFAULT 0 COMMENT 全天进场车辆数, total_fee DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 全天应收金额, avg_duration_minutes INT NOT NULL DEFAULT 0 COMMENT 平均停车时长分钟, peak_hour TINYINT DEFAULT NULL COMMENT 进场高峰小时, occupy_rate DECIMAL(5,2) DEFAULT NULL COMMENT 日均占用率%, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_stats_date_park (stats_date, park_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT每日统计表;occupy_rate不要依赖瞬时值一般取“当天每个小时占用率的平均值”。如果没有车位传感器可以用“进场车辆数 / 车位总数”做近似值。这一列是给商业综合体运营看的数据精确度要求没那么高但趋势要能看到。3. 用 SpringBoot 把后端骨架搭起来依赖、配置和预约链路表结构定下来之后下一步就是把 SpringBoot 工程跑起来。很多新手在这一步被 MySQL 版本、驱动、时区、连接池参数卡住。先把依赖和配置调对后面写业务代码才不会反复重启。3.1 项目结构、依赖引入与自动装配拿到的 zip 包如果是标准 Maven 工程第一件事先看pom.xml里的 SpringBoot parent 版本。我一般建议用 SpringBoot 2.7.x 配 MySQL 8.0这是目前中小型项目最保守的组合遇到问题搜到的资料也最多。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesSpringBoot 的自动装配在这里起了很大作用只要引入了mybatis-spring-boot-starter框架会自动创建SqlSessionFactory只要引入了mysql-connector-j自动配置就能识别spring.datasource.url并创建数据源。自动装配并非黑匣子关键逻辑在spring-boot-autoconfigure包中的DataSourceAutoConfiguration和MybatisAutoConfiguration里出现异常时可以先查这两个类的条件注解。项目内部分层建议固定下来不要随意混com.parking ├── controller // 接口层只做参数接收和结果返回 ├── service // 业务逻辑层 ├── mapper // MyBatis Mapper接口 ├── entity // 数据库实体 ├── dto // 请求/响应对象 ├── config // 配置类如拦截器、异步任务 └── common // 统一返回结果、异常、工具类这样做的好处是预约、计费、进出记录这些模块之间不会有循环依赖。比如ReserveService只依赖SpaceMapper和ReserveOrderMapper不会去依赖摄像头 SDK 或支付回调。3.2 application.yml 里的 MySQL 连接池和时区参数MySQL 连接不上、启动报时区错误十有八九是application.yml配得不完整。贴一段可以直接用的配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/parking_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 connection-test-query: SELECT 1 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.parking.entity configuration: map-underscore-to-camel-case: true pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: trueserverTimezoneAsia/Shanghai解决The server time zone value的启动报错。allowPublicKeyRetrievaltrue是 MySQL 8 使用caching_sha2_password认证时需要的少了它会报Public Key Retrieval is not allowed。useSSLfalse是因为本地开发一般没有配置证书生产环境再按需打开。HikariCP 的参数有两个最值得调。maximum-pool-size不是越大越好小型项目 20 就够设成 100 反而会让数据库连接数被打满。connection-test-query: SELECT 1用来定期检测连接是否可用防止 MySQL 主动断开空闲连接后应用还在用旧连接。map-underscore-to-camel-case开启后数据库的fee_amount能自动映射到 Java 的feeAmount少写一堆Results。3.3 从 Controller 到 Mapper一个预约接口的完整链路拿“创建预约”这个接口串一遍三层结构。Controller 只接收参数和返回统一结果真正的校验和状态变更在 Service。RestController RequestMapping(/api/reserve) public class ReserveController { Resource private ReserveService reserveService; PostMapping(/create) public ResultReserveOrder create(RequestBody Valid ReserveRequest request, RequestHeader(userId) Long userId) { return Result.ok(reserveService.createReservation(userId, request)); } }Service 层的核心是事务和行锁Service public class ReserveService { Resource private ParkingSpaceMapper spaceMapper; Resource private ReserveOrderMapper reserveMapper; Transactional(rollbackFor Exception.class) public ReserveOrder createReservation(Long userId, ReserveRequest req) { ParkingSpace space spaceMapper.selectByIdForUpdate(req.getSpaceId()); if (space null || space.getStatus() ! 0) { throw new BizException(该车位当前不可预约); } // 校验预约时间不跨月、结束时间晚于开始时间 validateReserveTime(req); spaceMapper.updateStatus(req.getSpaceId(), 1, space.getVersion()); ReserveOrder order buildOrder(userId, req); reserveMapper.insert(order); return order; } }对应的 Mapper 接口public interface ParkingSpaceMapper { ParkingSpace selectByIdForUpdate(Long id); int updateStatus(Param(id) Long id, Param(status) Integer status, Param(version) Integer version); }Mappe r XML 里的FOR UPDATE是这一节的关键select idselectByIdForUpdate resultTypecom.parking.entity.ParkingSpace SELECT * FROM t_parking_space WHERE id #{id} FOR UPDATE /select /xmlFOR UPDATE必须在事务内执行才有意义。事务开启后这一行被锁住其他事务再执行同样的FOR UPDATE只能等待。事务提交后锁释放。updateStatus里带上version条件即使锁失效也有乐观锁兜底UPDATE t_parking_space SET status #{status}, version version 1 WHERE id #{id} AND version #{version}这里要注意FOR UPDATE如果命中非主键索引或没有索引可能锁住多行甚至全表。所以selectByIdForUpdate必须用主键id查询这也是表结构设计时保留主键索引的意义。4. 核心业务逐个实现预约并发、计费精度、进出记录和权限表结构和骨架搭好之后剩下的工作就是把标题里的功能点一个个填进去。这一章是整套系统的关键也是部署到现场后最容易出问题的部分。4.1 车位预约事务、行锁和超时状态预约模块的难点不是“插入一条订单”而是“同一车位在同一时间段不能被两个人同时预约”。除了第 3.3 小节的FOR UPDATE还需要处理预约超时。常见做法是预约成功后保留 30 分钟入场时间超过 30 分钟未入场订单状态改为“已超时”车位释放。释放逻辑不要等到用户查的时候才触发用一个定时任务每 5 分钟扫一次Component public class ReserveTimeoutTask { Resource private ReserveOrderMapper reserveMapper; Resource private ParkingSpaceMapper spaceMapper; Scheduled(fixedDelay 300000) Transactional(rollbackFor Exception.class) public void releaseTimeoutOrder() { ListReserveOrder orders reserveMapper.selectTimeoutOrders(30); for (ReserveOrder order : orders) { int count reserveMapper.updateStatus(order.getId(), 3); // 待入场 - 已超时 if (count 1) { spaceMapper.releaseSpace(order.getSpaceId()); // 1预约 - 0空闲 } } } }selectTimeoutOrders(30)的 SQL 条件大概是status 0 AND reserve_start_time DATE_SUB(NOW(), INTERVAL 30 MINUTE)。注意扫描条件要建在status和reserve_start_time的联合索引上否则表大了每次扫描都是全表。分配车位时尽量选择预约开始时间临近的车位减少因提前太久预约导致车位被无效锁定的情况。4.2 停车费计算用 BigDecimal 而不是 Double停车费计算的规则各地差异很大但代码结构可以统一入参是进出记录和计费规则返回值是BigDecimal。免费时长、按时计费、日封顶是最基础的三段式先写这一段public BigDecimal calcParkingFee(ParkingRecord record, FeeRule rule) { if (record.getEntryTime() null || record.getExitTime() null) { throw new BizException(出入场时间不完整无法计费); } long seconds Duration.between(record.getEntryTime(), record.getExitTime()).getSeconds(); long freeSeconds rule.getFreeMinutes() * 60L; if (seconds freeSeconds) { return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); } long billableSeconds seconds - freeSeconds; BigDecimal hours BigDecimal.valueOf(billableSeconds) .divide(BigDecimal.valueOf(3600), 0, RoundingMode.UP); BigDecimal fee hours.multiply(rule.getUnitPrice()).setScale(2, RoundingMode.HALF_UP); if (rule.getMaxDailyPrice() ! null fee.compareTo(rule.getMaxDailyPrice()) 0) { fee rule.getMaxDailyPrice(); } return fee; }RoundingMode.UP表示不满一个小时按一个小时计费这是停车场常见的“向上取整”逻辑。setScale(2, RoundingMode.HALF_UP)保证金额只保留两位小数并且四舍五入。金额计算全程不使用doubleMySQL 表字段也用DECIMAL(10,2)否则累计到月底一定对不上账。如果规则里有夜间价格计算前需要把停车时段按“白天段”和“夜间段”切开。我一般会写一个TimeSegmentSplitter把入场到出场的连续区间拆成多个计费段每段套用对应单价最后累加。这个函数不复杂但务必用边界用例测一遍入场 23:59、出场次日 00:01跨天、跨夜间、跨封顶三条逻辑同时触发最容易算错。4.3 车辆进出记录入场建档、出场结算和异常车牌车辆进出的标准流程是道闸相机识别车牌后调用入场接口系统查t_car判断是否月租车再写一条t_parking_record并占用车位出场时再调出场接口结算费用。Transactional(rollbackFor Exception.class) public Long entry(String plateNo, Long spaceId, String imageUrl) { Car car carMapper.selectByPlateNo(plateNo); int recordType 0; if (car ! null car.getCarType() 1 car.getValidEndDate() ! null car.getValidEndDate().isAfter(LocalDate.now())) { recordType 2; // 月租车 } ParkingRecord record new ParkingRecord(); record.setRecordNo(generateRecordNo()); record.setPlateNo(plateNo); record.setSpaceId(spaceId); record.setEntryTime(LocalDateTime.now()); record.setRecordType(recordType); record.setPayStatus(0); record.setEntryImage(imageUrl); parkingRecordMapper.insert(record); spaceMapper.occupySpace(spaceId); return record.getId(); }出场接口的逻辑刚好相反根据车牌查出未离场的记录补上exit_time计算秒数和费用更新车位状态为空闲。这里有个现场坑识别到“无入场记录”的车辆时不要直接拒绝放行。常见做法是给它补一条“异常入场”记录入场时间取当前时间但要在记录里打标记方便人工核查。否则出口排队蔓延投诉马上到物业办公室。4.4 用户权限控制Spring Security 还是轻量拦截器权限控制是小系统最容易做重也最容易做漏的地方。如果只有“车主、管理员、物业”三类角色用 Spring Security 有点重一个拦截器加角色注解就够了。如果以后要接入多端、多子系统再切换到 Spring Security 也不迟。轻量方案示例Component public class AuthInterceptor implements HandlerInterceptor { private static final ListString WHITE_LIST List.of( /api/auth/login, /api/space/list ); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String uri request.getRequestURI(); if (WHITE_LIST.contains(uri)) { return true; } String userType request.getHeader(userType); if (userType null) { response.setStatus(401); return false; } // 需要登录才能访问的接口至少要是有效用户 if (!1.equals(userType) !2.equals(userType) !3.equals(userType)) { response.setStatus(403); return false; } // 管理员和物业可以访问管理接口普通车主只能访问自己的数据 if (uri.startsWith(/api/admin/) !2.equals(userType) !3.equals(userType)) { response.setStatus(403); return false; } return true; } }拦截器能拦住外部请求但拦不住内部批量任务。所以写 Service 时还要有意识地区分“管理端方法”和“车主方法”。管理端方法不要把userId从请求参数里读应该从登录态里取车主端方法查询数据时必须带userId条件防止水平越权。这个方案虽然没用 Spring Security但密码存储不能省。密码要经过 BCrypt 哈希再入库登录时用BCryptPasswordEncoder.matches校验不能把明文密码摆在t_user里。4.5 数据统计分析SQL 聚合、索引和定时任务统计报表最常见的需求是每天进场多少车、应收多少钱、平均停多久、高峰小时是哪一段。用一条分组 SQL 就能拿到SELECT DATE_FORMAT(exit_time, %Y-%m-%d) AS bizDate, COUNT(*) AS totalCars, SUM(fee_amount) AS totalFee, ROUND(AVG(TIMESTAMPDIFF(MINUTE, entry_time, exit_time)), 1) AS avgMinutes FROM t_parking_record WHERE exit_time #{beginTime} AND exit_time #{endTime} AND pay_status IN (1, 2) GROUP BY DATE_FORMAT(exit_time, %Y-%m-%d) ORDER BY bizDate;这段 SQL 必须配合exit_time索引。pay_status IN (1, 2)是“已支付 免费”把未支付的异常记录排除掉避免统计出虚高的应收。区间条件用 和 是左闭右开不会漏数据也不会重复统计边界时间。日报的生成用定时任务更合适而不是让前端每次查报表都扫全表。注意Scheduled默认是单机单线程执行如果项目将来做多实例部署同一个任务会在多个节点同时跑。需要给统计表加唯一键用INSERT ... ON DUPLICATE KEY UPDATE做幂等写入。Component public class DailyStatsTask { Resource private StatsMapper statsMapper; Scheduled(cron 0 5 0 * * ?) // 每天凌晨 00:05 执行 public void statsYesterday() { LocalDate yesterday LocalDate.now().minusDays(1); statsMapper.statsByDate(yesterday.toString(), yesterday.plusDays(1).toString()); } }cron 0 5 0 * * ?表示零点零五分跑避开零点整的数据库备份高峰。如果系统里有多个停车场聚合 SQL 要按park_id分组写入统计表唯一键也包含park_id。5. SpringBoot MySQL 落地的避坑记录现象、原因、解决这一章全部来自实际落地时的血泪经验。每一条都按“现象 - 原因 - 解决”的次序写遇到同类型问题可以直接对照排查。5.1 启动报时区错误或 MySQL 连接失败现象SpringBoot 启动时报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者连接池初始化报Communications link failure。原因MySQL 8 的时区默认值不兼容 JDBC 的时区解析也可能是连接 URL 缺少必要参数导致驱动用了错误协议重试。解决在 JDBC URL 中显式加serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue并确认driver-class-name是com.mysql.cj.jdbc.Driver。如果是 Linux 上的 MySQL还可以执行SET GLOBAL time_zone 08:00从服务端源头统一时区。5.2 金额字段用 DOUBLE 导致月底对账不平现象停车费计算出现 0.01、0.02 的差异单条订单不明显月底汇总差几十块。原因DOUBLE是浮点数二进制无法精确表示 0.1、0.2 这样的十进制小数多次乘除后误差累积。解决MySQL 金额用DECIMAL(10,2)Java 实体用BigDecimal所有计算入口统一走BigDecimal禁止用double接收金额参数。MyBatis 映射时注意不要因为实体字段是Double而把DECIMAL转成浮点。5.3 并发预约同一车位导致超卖现象两个用户同时点击预约同一个车位系统返回两次成功。原因Service 里先select车位状态再update车位状态两个操作之间没有锁。事务隔离级别默认是REPEATABLE READ快照读看到的都是旧状态。解决预约接口必须用SELECT ... FOR UPDATE锁住车位行再执行状态更新。注意FOR UPDATE要在一个事务里且查询条件用主键。更稳妥的做法是同时给预约订单表加唯一索引比如限制“同一个车位、同一分钟只能有一条有效预约”。5.4 事务自调用导致预约数据写一半现象在同一个 Service 类里createReservation调用另一个方法updateSpaceStatus后者上有Transactional但方法内部更新失败时前一条预约订单没有回滚。原因Spring 的事务基于 AOP 代理自调用不经过代理对象Transactional直接失效。解决事务方法放到独立 Service或者从 Spring 容器里重新拿到代理对象调用。最简单的是把“查询车位 更新车位 插入预约”全部放在同一个 public 方法里只在这一层加Transactional不要拆出私有方法再自己调。5.5 PageHelper 分页把统计 SQL 一起截断现象列表接口用了PageHelper.startPage()后紧接着的一条统计 SQL 也返回了分页后的条数。原因PageHelper.startPage()只对接下来执行的“第一条查询”生效统计查询和列表查询共用 Mapper 导致线程上下文里的分页参数没有被清理。解决统计查询和分页查询分成两个 Mapper 方法startPage()之后只执行目标查询不要在同一方法里连续查询两条 SQL。更保险的做法是在 finally 里调用PageHelper.clearPage()。5.6 MySQL 8 新增认证插件导致老客户端连不上现象用 Navicat 或旧版 JDBC 驱动连接 MySQL 8报Public Key Retrieval is not allowed或Authentication plugin caching_sha2_password cannot be loaded。原因MySQL 8 默认认证插件是caching_sha2_password旧驱动不认识这个插件。解决驱动更换为com.mysql.cj.jdbc.Driver并在连接 URL 增加allowPublicKeyRetrievaltrue。如果内网环境必须用旧客户端可以在 MySQL 里把该用户的认证插件改成mysql_native_password但新项目不建议这么干应该升级客户端而不是降低数据库安全级别。5.7 慢查询排查打开 MySQL 慢日志引入大数据量之后进出记录列表可能越查越慢。怀疑 SQL 慢时不要猜直接开慢查询日志。SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SHOW VARIABLES LIKE slow_query_log_file;long_query_time 1表示执行时间超过 1 秒的 SQL 会被记录。拿到慢日志后对慢 SQL 执行EXPLAIN看type是不是ALL、key有没有走索引。最常见的修复是给entry_time、exit_time、status这些查询条件字段加联合索引。6. 从能跑到敢上线用真实数据验证预约、计费和统计最后一个环节也是很多项目交付时最吃亏的环节只测了“正常流程”没有测“边界流程”。我自己的习惯是拿到整套代码后先不在真实车场跑而是用 Docker 起一个干净的 MySQL导完表结构后灌一批构造数据把三条核心链路完整走一遍。docker run -d --name parking-mysql \ -e MYSQL_ROOT_PASSWORD123456 \ -e TZAsia/Shanghai \ -p 3306:3306 \ mysql:8.0MySQL 起来之后导入第 2 章的表结构然后造三类测试数据商业综合体的按小时计费规则、住宅小区的月租车档案、两条横跨夜间和免费时长的进出记录。接着用 JMeter 模拟 50 个用户并发抢同一个车位看返回成功的订单数是否只有一个。这一步能同时验证FOR UPDATE行锁、事务回滚和接口幂等性。费用计算重点验证这几个边界停车 19 分钟但免费 20 分钟应收 0停车 20 分零 1 秒应收 1 小时费用入场 23:30、出场次日 00:30跨天封顶是否生效。这些用例任何一条算错上线后都会变成用户投诉。统计报表的验证也不要只看当天数据。把t_parking_record里插入一条exit_time在昨天的记录再手动执行一次日报任务确认t_daily_stats里能聚合出对应数据。定时任务的cron表达式最好留成可配置项而不是硬编码在注解里方便不同项目切换执行时间。慢查询日志打开后跑一遍统计页面的接口确认GROUP BY走了idx_exit_time。如果EXPLAIN显示Using filesort就要检查ORDER BY字段是否和GROUP BY字段一致必要时改成先聚合再排序。这个系统真正做到生产可用不是看功能列表有多长而是看状态流转是否闭环、计费逻辑是否经得起边界测试。我现在的习惯是任何一次计费规则调整先把前一天的真实流水复制到测试库重算一遍再放上线。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →