智能停车计费系统设计与实现

78 阅读8分钟

随着城市化进程的加快,停车难、收费乱成为困扰城市管理的难题。本文将介绍如何使用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

10. 监控大屏