随着城市化进程的加快,停车难、收费乱成为困扰城市管理的难题。本文将介绍如何使用Java技术栈设计并实现一套完整的智能停车计费系统。
一、系统概述
1.1 业务背景
现代停车场管理面临诸多挑战:
- 人工收费效率低、易出错
- 计费规则复杂多变
- 缺乏数据分析支持
- 用户体验不佳
智能停车计费系统通过信息化手段,实现车辆入场自动识别、停车时长精准计算、多样化支付方式,显著提升停车场运营效率。
1.2 核心功能模块
本系统主要包含以下模块:
车辆管理模块:车牌识别、车辆进出记录计费
管理模块:灵活的计费策略配置
支付管理模块:支持微信、支付宝等多种支付方式
报表统计模块:营收分析、车流统计
系统管理模块:用户权限、参数配置
二、技术架构设计
2.1 技术选型
后端技术栈:
SpringBoot 2.7.x - 快速开发框架
MyBatis-Plus 3.5.x - ORM框架
MySQL 8.0 - 关系型数据库
Redis 6.x - 缓存中间件
RabbitMQ - 消息队列
JWT - 认证授权证授权
2.2 系统架构
系统采用分层架构设计,包括表现层、业务层、数据层和外部集成层。表现层提供Web界面和RESTful API,业务层封装业务逻辑(包括计费策略引擎),数据层负责数据持久化和缓存管理,外部集成层对接车牌识别、支付网关等第三方服务。
三、数据库设计
3.1 核心数据表
车辆信息表(parking_vehicle)
CREATE TABLE parking_vehicle (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
plate_number VARCHAR(20) NOT NULL COMMENT '车牌号',
vehicle_type TINYINT COMMENT '车辆类型 1-小型车 2-中型车 3-大型车',
owner_name VARCHAR(50) COMMENT '车主姓名',
owner_phone VARCHAR(20) COMMENT '车主电话',
is_monthly TINYINT DEFAULT 0 COMMENT '是否月租车 0-否 1-是',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_plate (plate_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
停车记录表(parking_record)
CREATE TABLE parking_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
plate_number VARCHAR(20) NOT NULL COMMENT '车牌号',
entry_time DATETIME NOT NULL COMMENT '入场时间',
exit_time DATETIME COMMENT '出场时间',
parking_duration INT COMMENT '停车时长(分钟)',
parking_lot_id INT COMMENT '停车场ID',
parking_space_no VARCHAR(20) COMMENT '车位号',
status TINYINT DEFAULT 1 COMMENT '状态 1-在场 2-已离场',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_plate (plate_number),
INDEX idx_entry_time (entry_time),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
计费记录表(parking_billing)
CREATE TABLE parking_billing (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
record_id BIGINT NOT NULL COMMENT '停车记录ID',
plate_number VARCHAR(20) NOT NULL COMMENT '车牌号',
parking_duration INT COMMENT '停车时长(分钟)',
original_amount DECIMAL(10,2) COMMENT '原始金额',
discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
actual_amount DECIMAL(10,2) COMMENT '实际金额',
billing_rule_id INT COMMENT '计费规则ID',
pay_status TINYINT DEFAULT 0 COMMENT '支付状态 0-未支付 1-已支付',
pay_time DATETIME COMMENT '支付时间',
pay_type TINYINT COMMENT '支付方式 1-微信 2-支付宝 3-现金',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_record (record_id),
INDEX idx_pay_status (pay_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
计费规则表(parking_billing_rule)
CREATE TABLE parking_billing_rule (
id INT PRIMARY KEY AUTO_INCREMENT,
rule_name VARCHAR(100) NOT NULL COMMENT '规则名称',
vehicle_type TINYINT COMMENT '适用车辆类型',
free_minutes INT DEFAULT 15 COMMENT '免费时长(分钟)',
first_hour_price DECIMAL(10,2) COMMENT '首小时价格',
additional_hour_price DECIMAL(10,2) COMMENT '超出后每小时价格',
max_daily_amount DECIMAL(10,2) COMMENT '单日最高金额',
effective_time TIME COMMENT '生效时间',
expiry_time TIME COMMENT '失效时间',
is_active TINYINT DEFAULT 1 COMMENT '是否启用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
四、核心代码实现
4.1 实体类设计
停车记录实体
package com.parking.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("parking_record")
public class ParkingRecord {
@TableId(type = IdType.AUTO)
private Long id;
private String plateNumber;
private LocalDateTime entryTime;
private LocalDateTime exitTime;
private Integer parkingDuration;
private Integer parkingLotId;
private String parkingSpaceNo;
/**
* 状态 1-在场 2-已离场
*/
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}
计费记录实体
package com.parking.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("parking_billing")
public class ParkingBilling {
@TableId(type = IdType.AUTO)
private Long id;
private Long recordId;
private String plateNumber;
private Integer parkingDuration;
private BigDecimal originalAmount;
private BigDecimal discountAmount;
private BigDecimal actualAmount;
private Integer billingRuleId;
/**
* 支付状态 0-未支付 1-已支付
*/
private Integer payStatus;
private LocalDateTime payTime;
/**
* 支付方式 1-微信 2-支付宝 3-现金
*/
private Integer payType;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}
4.2 计费策略引擎
计费策略接口
package com.parking.service.strategy;
import com.parking.entity.BillingRule;
import java.math.BigDecimal;
/**
* 计费策略接口
*/
public interface BillingStrategy {
/**
* 计算停车费用
* @param parkingMinutes 停车分钟数
* @param rule 计费规则
* @return 停车费用
*/
BigDecimal calculateFee(int parkingMinutes, BillingRule rule);
/**
* 获取策略类型
*/
String getStrategyType();
}
标准计费策略实现
package com.parking.service.strategy.impl;
import com.parking.entity.BillingRule;
import com.parking.service.strategy.BillingStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 标准计费策略
* 规则:免费15分钟,首小时X元,之后每小时Y元,不足1小时按1小时计算
*/
@Slf4j
@Component
public class StandardBillingStrategy implements BillingStrategy {
@Override
public BigDecimal calculateFee(int parkingMinutes, BillingRule rule) {
log.info("标准计费策略计算,停车时长:{}分钟", parkingMinutes);
// 免费时长内
if (parkingMinutes <= rule.getFreeMinutes()) {
return BigDecimal.ZERO;
}
// 计费时长
int chargeMinutes = parkingMinutes - rule.getFreeMinutes();
// 首小时内
if (chargeMinutes <= 60) {
return rule.getFirstHourPrice();
}
// 超过首小时,计算额外小时数(向上取整)
int additionalMinutes = chargeMinutes - 60;
int additionalHours = (int) Math.ceil(additionalMinutes / 60.0);
BigDecimal totalFee = rule.getFirstHourPrice()
.add(rule.getAdditionalHourPrice()
.multiply(new BigDecimal(additionalHours)));
// 检查是否超过单日最高金额
if (rule.getMaxDailyAmount() != null &&
totalFee.compareTo(rule.getMaxDailyAmount()) > 0) {
totalFee = rule.getMaxDailyAmount();
}
return totalFee.setScale(2, RoundingMode.HALF_UP);
}
@Override
public String getStrategyType() {
return "STANDARD";
}
}
分段计费策略实现
package com.parking.service.strategy.impl;
import com.parking.entity.BillingRule;
import com.parking.service.strategy.BillingStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 分段计费策略
* 规则:0-2小时每小时5元,2-6小时每小时4元,6小时以上每小时3元
*/
@Slf4j
@Component
public class TieredBillingStrategy implements BillingStrategy {
private static final int TIER1_HOURS = 2;
private static final int TIER2_HOURS = 6;
private static final BigDecimal TIER1_PRICE = new BigDecimal("5.00");
private static final BigDecimal TIER2_PRICE = new BigDecimal("4.00");
private static final BigDecimal TIER3_PRICE = new BigDecimal("3.00");
@Override
public BigDecimal calculateFee(int parkingMinutes, BillingRule rule) {
log.info("分段计费策略计算,停车时长:{}分钟", parkingMinutes);
// 免费时长内
if (parkingMinutes <= rule.getFreeMinutes()) {
return BigDecimal.ZERO;
}
// 计费时长(小时,向上取整)
int chargeMinutes = parkingMinutes - rule.getFreeMinutes();
double chargeHours = Math.ceil(chargeMinutes / 60.0);
BigDecimal totalFee = BigDecimal.ZERO;
// 计算各段费用
if (chargeHours <= TIER1_HOURS) {
totalFee = TIER1_PRICE.multiply(new BigDecimal(chargeHours));
} else if (chargeHours <= TIER2_HOURS) {
totalFee = TIER1_PRICE.multiply(new BigDecimal(TIER1_HOURS))
.add(TIER2_PRICE.multiply(
new BigDecimal(chargeHours - TIER1_HOURS)));
} else {
totalFee = TIER1_PRICE.multiply(new BigDecimal(TIER1_HOURS))
.add(TIER2_PRICE.multiply(
new BigDecimal(TIER2_HOURS - TIER1_HOURS)))
.add(TIER3_PRICE.multiply(
new BigDecimal(chargeHours - TIER2_HOURS)));
}
// 检查是否超过单日最高金额
if (rule.getMaxDailyAmount() != null &&
totalFee.compareTo(rule.getMaxDailyAmount()) > 0) {
totalFee = rule.getMaxDailyAmount();
}
return totalFee.setScale(2, RoundingMode.HALF_UP);
}
@Override
public String getStrategyType() {
return "TIERED";
}
}
策略工厂
五、核心业务服务
5.1 停车记录服务
package com.parking.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.parking.entity.ParkingRecord;
import com.parking.mapper.ParkingRecordMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 停车记录服务
*/
@Slf4j
@Service
public class ParkingRecordService
extends ServiceImpl<ParkingRecordMapper, ParkingRecord> {
/**
* 车辆入场
*/
@Transactional(rollbackFor = Exception.class)
public ParkingRecord vehicleEntry(String plateNumber,
Integer parkingLotId,
String parkingSpaceNo) {
log.info("车辆入场:{}", plateNumber);
// 检查是否有未完成的停车记录
ParkingRecord existingRecord = this.lambdaQuery()
.eq(ParkingRecord::getPlateNumber, plateNumber)
.eq(ParkingRecord::getStatus, 1)
.one();
if (existingRecord != null) {
throw new RuntimeException("该车辆已在场内,无法重复入场");
}
// 创建停车记录
ParkingRecord record = new ParkingRecord();
record.setPlateNumber(plateNumber);
record.setEntryTime(LocalDateTime.now());
record.setParkingLotId(parkingLotId);
record.setParkingSpaceNo(parkingSpaceNo);
record.setStatus(1); // 在场
this.save(record);
log.info("车辆入场成功,记录ID:{}", record.getId());
return record;
}
/**
* 车辆出场
*/
@Transactional(rollbackFor = Exception.class)
public ParkingRecord vehicleExit(String plateNumber) {
log.info("车辆出场:{}", plateNumber);
// 查询在场记录
ParkingRecord record = this.lambdaQuery()
.eq(ParkingRecord::getPlateNumber, plateNumber)
.eq(ParkingRecord::getStatus, 1)
.one();
if (record == null) {
throw new RuntimeException("未找到该车辆的入场记录");
}
// 更新出场信息
LocalDateTime exitTime = LocalDateTime.now();
record.setExitTime(exitTime);
// 计算停车时长(分钟)
Duration duration = Duration.between(record.getEntryTime(), exitTime);
int minutes = (int) duration.toMinutes();
record.setParkingDuration(minutes);
record.setStatus(2); // 已离场
this.updateById(record);
log.info("车辆出场成功,停车时长:{}分钟", minutes);
return record;
}
}
5.2 计费服务
package com.parking.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.parking.entity.BillingRule;
import com.parking.entity.ParkingBilling;
import com.parking.entity.ParkingRecord;
import com.parking.mapper.BillingRuleMapper;
import com.parking.mapper.ParkingBillingMapper;
import com.parking.service.strategy.BillingStrategy;
import com.parking.service.strategy.BillingStrategyFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 计费服务
*/
@Slf4j
@Service
public class BillingService
extends ServiceImpl<ParkingBillingMapper, ParkingBilling> {
@Autowired
private BillingRuleMapper billingRuleMapper;
@Autowired
private BillingStrategyFactory strategyFactory;
/**
* 计算停车费用
*/
@Transactional(rollbackFor = Exception.class)
public ParkingBilling calculateParkingFee(ParkingRecord record) {
log.info("开始计算停车费用,记录ID:{}", record.getId());
// 获取适用的计费规则
BillingRule rule = billingRuleMapper.selectOne(
lambdaQuery -> lambdaQuery
.eq(BillingRule::getIsActive, 1)
.orderByDesc(BillingRule::getCreateTime)
.last("LIMIT 1")
);
if (rule == null) {
throw new RuntimeException("未找到有效的计费规则");
}
// 使用策略模式计算费用
BillingStrategy strategy = strategyFactory.getStrategy("STANDARD");
BigDecimal fee = strategy.calculateFee(
record.getParkingDuration(), rule);
// 创建计费记录
ParkingBilling billing = new ParkingBilling();
billing.setRecordId(record.getId());
billing.setPlateNumber(record.getPlateNumber());
billing.setParkingDuration(record.getParkingDuration());
billing.setOriginalAmount(fee);
billing.setDiscountAmount(BigDecimal.ZERO);
billing.setActualAmount(fee);
billing.setBillingRuleId(rule.getId());
billing.setPayStatus(0); // 未支付
this.save(billing);
log.info("计费完成,应付金额:{}元", fee);
return billing;
}
/**
* 支付停车费
*/
@Transactional(rollbackFor = Exception.class)
public void payParkingFee(Long billingId, Integer payType) {
log.info("支付停车费,计费ID:{},支付方式:{}", billingId, payType);
ParkingBilling billing = this.getById(billingId);
if (billing == null) {
throw new RuntimeException("计费记录不存在");
}
if (billing.getPayStatus() == 1) {
throw new RuntimeException("该订单已支付");
}
// 更新支付状态
billing.setPayStatus(1);
billing.setPayType(payType);
billing.setPayTime(LocalDateTime.now());
this.updateById(billing);
log.info("支付成功");
}
}
六、Controller层实现
停车控制器
package com.parking.controller;
import com.parking.common.Result;
import com.parking.entity.ParkingBilling;
import com.parking.entity.ParkingRecord;
import com.parking.service.BillingService;
import com.parking.service.ParkingRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 停车管理控制器
*/
@Slf4j
@Api(tags = "停车管理")
@RestController
@RequestMapping("/api/parking")
public class ParkingController {
@Autowired
private ParkingRecordService recordService;
@Autowired
private BillingService billingService;
@ApiOperation("车辆入场")
@PostMapping("/entry")
public Result<ParkingRecord> vehicleEntry(
@RequestParam String plateNumber,
@RequestParam Integer parkingLotId,
@RequestParam(required = false) String parkingSpaceNo) {
try {
ParkingRecord record = recordService.vehicleEntry(
plateNumber, parkingLotId, parkingSpaceNo);
return Result.success(record);
} catch (Exception e) {
log.error("车辆入场失败", e);
return Result.error(e.getMessage());
}
}
@ApiOperation("车辆出场")
@PostMapping("/exit")
public Result<ParkingBilling> vehicleExit(
@RequestParam String plateNumber) {
try {
// 车辆出场
ParkingRecord record = recordService.vehicleExit(plateNumber);
// 计算费用
ParkingBilling billing =
billingService.calculateParkingFee(record);
return Result.success(billing);
} catch (Exception e) {
log.error("车辆出场失败", e);
return Result.error(e.getMessage());
}
}
@ApiOperation("支付停车费")
@PostMapping("/pay")
public Result<Void> payParkingFee(
@RequestParam Long billingId,
@RequestParam Integer payType) {
try {
billingService.payParkingFee(billingId, payType);
return Result.success();
} catch (Exception e) {
log.error("支付失败", e);
return Result.error(e.getMessage());
}
}
@ApiOperation("查询停车费用")
@GetMapping("/fee/{plateNumber}")
public Result<ParkingBilling> queryParkingFee(
@PathVariable String plateNumber) {
try {
ParkingBilling billing = billingService.lambdaQuery()
.eq(ParkingBilling::getPlateNumber, plateNumber)
.eq(ParkingBilling::getPayStatus, 0)
.orderByDesc(ParkingBilling::getCreateTime)
.one();
return Result.success(billing);
} catch (Exception e) {
log.error("查询失败", e);
return Result.error(e.getMessage());
}
}
}
统一返回结果
package com.parking.common;
import lombok.Data;
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
七、配置文件
application.yml
server:
port: 8080
spring:
application:
name: parking-system
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/parking_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root123
redis:
host: localhost
port: 6379
database: 0
timeout: 3000
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.parking.entity
logging:
level:
com.parking: debug
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
八、系统优化与扩展
8.1 性能优化
缓存优化
@Service
public class CacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String PARKING_RECORD_KEY = "parking:record:";
private static final long CACHE_EXPIRE_HOURS = 24;
/**
* 缓存停车记录
*/
public void cacheParkingRecord(ParkingRecord record) {
String key = PARKING_RECORD_KEY + record.getPlateNumber();
redisTemplate.opsForValue().set(key, record,
CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
}
/**
* 获取缓存的停车记录
*/
public ParkingRecord getCachedRecord(String plateNumber) {
String key = PARKING_RECORD_KEY + plateNumber;
return (ParkingRecord) redisTemplate.opsForValue().get(key);
}
}
异步处理
@Service
public class AsyncBillingService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 异步发送计费消息
*/
public void sendBillingMessage(ParkingRecord record) {
rabbitTemplate.convertAndSend("parking.billing.exchange",
"billing.calculate",
record);
}
}
8.2 实际应用案例
案例一:商场停车场
前2小时免费,之后每小时5元,会员车辆享受8折优惠,单日封顶50元
案例二:写字楼停车场
月租车辆24小时自由出入,访客车辆首小时10元、之后每小时8元,夜间(22:00-次日8:00)按次收费20元
案例三:医院停车场
前30分钟免费(方便快速就医),2小时内每小时4元,超过2小时每小时6元
hutool集成示例:
// 使用Hutool进行日期计算
import cn.hutool.core.date.DateUtil;
public class DateHelper {
public static int calculateMinutes(LocalDateTime start, LocalDateTime end) {
return (int) DateUtil.between(start, end, DateUnit.MINUTE);
}
}
九、部署与运维
9.1 Docker部署
Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY target/parking-system.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
EXPOSE 8080
docker-compose.yml
version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: parking_db
ports:
- "3306:3306"
redis:
image: redis:6-alpine
ports:
- "6379:6379"
parking-app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis