关于我们 / 企业动态
解决方案 4 分钟阅读

城市级停充一体平台全栈实现:从设备接入到业务闭环的技术解密(附源码)

本文介绍了YunParking停充一体平台的技术架构,重点解决城市智慧停车中的三大难题:1.设备协议碎片化问题:通过协议适配器模式统一不同设备的数据格式,结合RabbitMQ消息队列实现高并发处理,将设备接入延迟控制在500ms内。2.业务

城市级停充一体平台全栈实现:从设备接入到业务闭环的技术解密(附源码)

原文链接:城市级停充一体平台全栈实现:从设备接入到业务闭环的技术解密(附源码)

作为一名在智慧停车领域摸爬滚打13 年的技术老兵,我最深的体会是:城市级 “路内 + 路外 + 充电一体化” 不是 “拼积木”,而是 “用技术把分散的资源串成网”—— 从路边的地磁、视频桩,到商场的道闸、充电桩,再到用户手机里的 APP,每一个节点都需要 “会说话”。

今天我就把我们团队基于SpringCloud 微服务架构搭建的YunParking 停充一体平台的技术细节扒开来讲 —— 从底层设备接入的协议适配,到中层业务联动的规则引擎,再到上层支付分账的算法实现,全是一线踩坑后的干货。

一、底层破局:YunIOT 设备统一接入,搞定 “协议碎片化” 难题

传统停充系统的第一大痛点是设备协议不兼容:地磁用 NB-IoT 发十六进制指令,视频桩用 SDK 传图片流,充电桩用云快充协议 —— 每加一类设备就要写一套适配代码,维护成本高到离谱。

我们的解决方案是 \\“协议适配器模式 + 消息中间件”\\,把所有设备的 “语言” 翻译成统一的 “系统语”。

1. 协议适配器:让设备 “讲同一种话”

我们定义了一个DeviceAdapter接口,每类设备对应一个实现类,核心是 \\“解析原始数据”+“构建控制指令”\\

JAVA
/**
 * 设备适配器接口:统一设备数据解析与指令构建
 */
public interface DeviceAdapter {
    /**
     * 解析设备原始数据(如NB-IoT的十六进制、视频桩的SDK输出)
     * @param rawData 设备原始数据
     * @return 统一格式的设备数据DTO
     */
    DeviceDataDTO parseRawData(String rawData);

    /**
     * 构建设备控制指令(如控制道闸开/关、查询充电桩状态)
     * @param command 系统指令DTO
     * @return 设备能懂的指令字符串(如十六进制、SDK指令)
     */
    String buildCommand(DeviceCommandDTO command);
}

路内地磁(NB-IoT 协议)和路外道闸(百胜协议)为例,看具体实现:

(1)地磁适配器:解析 NB-IoT 十六进制数据

地磁的原始数据是"01 03 00 00 00 01"(表示车位占用),我们需要转成系统能理解的DeviceDataDTO

JAVA
/**
 * NB-IoT地磁适配器
 */
public class MagneticAdapter implements DeviceAdapter {
    @Override
    public DeviceDataDTO parseRawData(String rawData) {
        // 1. 去除空格,转成十六进制字节数组
        String hexStr = rawData.replace(" ", "");
        byte[] bytes = Hex.decodeHex(hexStr.toCharArray());

        // 2. 解析状态:第4位(索引3)为1表示占用,0表示空闲
        int status = bytes[3] & 0xFF;
        DeviceDataDTO data = new DeviceDataDTO();
        data.setDeviceId(new String(bytes, 0, 2)); // 设备ID取前2字节
        data.setDeviceType("magnetic");
        data.setStatus(status == 1 ? "occupied" : "free");
        data.setTimestamp(System.currentTimeMillis());
        return data;
    }

    @Override
    public String buildCommand(DeviceCommandDTO command) {
        // 构建查询指令:"01 03 00 00 00 01"(读取地磁状态)
        return "01 03 00 00 00 01";
    }
}

(2)道闸适配器:对接百胜道闸协议

百胜道闸的控制指令是"JF:OPEN"(开闸)、"JF:CLOSE"(关闸),我们需要将系统的DeviceCommandDTO转成对应字符串:

JAVA
/**
 * 百胜道闸适配器
 */
public class BaShengGateAdapter implements DeviceAdapter {
    @Override
    public DeviceDataDTO parseRawData(String rawData) {
        // 道闸的原始数据是"STATUS:OPEN"(开闸状态)或"STATUS:CLOSED"(关闸)
        String[] parts = rawData.split(":");
        DeviceDataDTO data = new DeviceDataDTO();
        data.setDeviceType("gate");
        data.setStatus(parts[1].toLowerCase());
        return data;
    }

    @Override
    public String buildCommand(DeviceCommandDTO command) {
        // 系统指令"open"转成百胜的"JF:OPEN"
        return command.getCommand().equals("open") ? "JF:OPEN" : "JF:CLOSE";
    }
}

(3)适配器工厂:一键获取设备适配器

为了避免硬编码,我们用工厂模式管理所有适配器:

JAVA
/**
 * 设备适配器工厂:根据设备类型获取对应适配器
 */
public class DeviceAdapterFactory {
    private static final Map<String, DeviceAdapter> ADAPTERS = new ConcurrentHashMap<>();

    // 静态初始化:注册所有设备适配器
    static {
        ADAPTERS.put("magnetic", new MagneticAdapter());
        ADAPTERS.put("video_pile", new VideoPileAdapter()); // 视频桩适配器
        ADAPTERS.put("gate_basheng", new BaShengGateAdapter()); // 百胜道闸
        ADAPTERS.put("charger_yunchong", new YunChongChargerAdapter()); // 云快充充电桩
    }

    /**
     * 根据设备类型获取适配器
     * @param deviceType 设备类型(如"magnetic"、"gate_basheng")
     * @return 对应的设备适配器
     */
    public static DeviceAdapter getAdapter(String deviceType) {
        return ADAPTERS.getOrDefault(deviceType, new DefaultAdapter()); // 默认适配器处理未知设备
    }
}

2. 消息中间件:用 RabbitMQ 解决 “高并发延迟”

早期我们用阿里云 MQTT接入设备,结果 1000 台设备同时上报时,消息延迟高达8 秒(MQTT 的 QoS1 模式需要确认消息)。后来换成私有化 RabbitMQ,通过 3 个优化把延迟压到了500ms 以内

(1)队列配置:持久化 + 手动 ACK

JAVA
/**
 * RabbitMQ队列配置:确保消息不丢失
 */
@Configuration
public class RabbitConfig {
    @Bean
    public Queue deviceDataQueue() {
        // durable: 持久化队列;autoDelete: 不自动删除
        return new Queue("device_data_queue", true, false, false);
    }

    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); // 手动ACK,避免消息丢失
        factory.setPrefetchCount(10); // 每次拉取10条消息,提升并发
        return factory;
    }
}

(2)消费者代码:异步处理设备数据

JAVA
/**
 * 设备数据消费者:处理所有设备的上报数据
 */
@Component
@RabbitListener(queues = "device_data_queue")
public class DeviceDataConsumer {
    @Autowired
    private DeviceService deviceService;

    @RabbitHandler
    public void process(String rawData, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws IOException {
        try {
            // 1. 解析设备类型(假设原始数据是JSON格式:{"deviceType":"magnetic","rawData":"01 03..."})
            JSONObject json = JSON.parseObject(rawData);
            String deviceType = json.getString("deviceType");
            String raw = json.getString("rawData");

            // 2. 获取适配器,解析数据
            DeviceAdapter adapter = DeviceAdapterFactory.getAdapter(deviceType);
            DeviceDataDTO data = adapter.parseRawData(raw);

            // 3. 保存数据到数据库(或触发业务逻辑)
            deviceService.saveData(data);

            // 4. 手动ACK:确认消息处理完成
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            // 异常处理:重新入队或死信队列
            channel.basicNack(deliveryTag, false, true);
            log.error("处理设备数据失败:{}", e.getMessage(), e);
        }
    }
}

二、中层联动:YunParking+YunCharge,让 “停车” 与 “充电” 对话

停充一体的核心是 \\“业务联动”:比如用户停入路内车位,系统自动推荐 50 米内的空闲充电桩;用户发起充电,系统锁定对应车位 —— 这些场景需要规则引擎 \\ 把 “设备数据” 转换成 “用户服务”。

1. 路内停车:视频桩的 “车牌识别 + 计时”

路内停车的关键是 \\“精准识别车牌 + 自动计时”,我们选用臻识 200 万像素 AI 视频桩 \\(支持宽动态、低照度),通过 SDK 对接实现车牌识别:

(1)视频桩 SDK 接入

JAVA
/**
 * 视频桩车牌识别服务:对接臻识SDK
 */
@Service
public class VideoPileService {
    // 臻识SDK初始化(需要申请APP_KEY)
    private ZSClient zsClient = new ZSClient("YOUR_APP_KEY");

    /**
     * 识别视频流中的车牌
     * @param videoStream 视频流字节数组
     * @return 车牌信息(如"粤A12345")
     */
    public String recognizeLicensePlate(byte[] videoStream) {
        try {
            // 调用SDK识别车牌
            ZSResult result = zsClient.recognize(videoStream);
            if (result.isSuccess()) {
                return result.getLicensePlate();
            }
            return null;
        } catch (Exception e) {
            log.error("视频桩识别失败:{}", e.getMessage());
            return null;
        }
    }
}

(2)自动计时逻辑

当视频桩识别到车牌时,系统会记录 “入场时间”;当车辆离开时,再次识别车牌,计算 “停车时长”—— 整个过程无需人工干预。

2. 路外停车:道闸与充电桩的 “双向锁定”

路外停车场的核心是 \\“道闸与充电桩联动”\\:用户停入带充电桩的车位,发起充电后,系统自动锁定车位(避免被其他车辆占用)。

我们用Drools 规则引擎实现这个逻辑,规则文件charge_lock_parking.drl

CODE
/**
 * 规则:充电开始时锁定对应车位
 */
rule "Lock Parking Space When Charging Starts"
when
    // 1. 捕获充电开始事件(ChargeEvent)
    $chargeEvent : ChargeEvent(status == "STARTED")
    // 2. 找到对应车位,且状态为空闲
    $parkingSpace : ParkingSpace(id == $chargeEvent.getParkingSpaceId(), status == "FREE")
then
    // 3. 锁定车位
    parkingSpaceService.lock($parkingSpace.getId());
    // 4. 发送通知给用户
    notificationService.send($chargeEvent.getUserId(), "您的车位已锁定,充电期间请勿离开");
end

Java 代码中调用规则引擎:

JAVA
/**
 * 规则引擎服务:执行停充联动规则
 */
@Service
public class RuleEngineService {
    @Autowired
    private KieContainer kieContainer;

    public void executeChargeRules(ChargeEvent chargeEvent) {
        KieSession kieSession = kieContainer.newKieSession();
        try {
            // 插入充电事件到规则引擎
            kieSession.insert(chargeEvent);
            // 执行所有匹配的规则
            kieSession.fireAllRules();
        } finally {
            kieSession.dispose();
        }
    }
}

三、上层闭环:YunPay 支付分账,解决 “多角色分润” 痛点

城市级项目的终极问题是 \\“分钱”\\:政府要抽成、停车场要收益、充电桩运营商要利润 —— 传统支付系统的 “固定比例分账” 根本扛不住复杂场景(比如充电时长超过 1 小时,运营商分润降 10%)。

1. 分账规则:JSON 配置 + 动态调整

我们用JSON 定义分账规则,支持 “固定金额”“比例分润”“条件调整”:

JSON
{
    "ruleId": "city_park_charge",
    "scenario": "parking+charging", // 场景:停车+充电
    "parties": [
        {"partyId": "government", "name": "政府", "type": "fixed", "value": 10}, // 政府固定抽10元
        {"partyId": "parking_lot", "name": "停车场", "type": "proportion", "value": 0.8}, // 停车费80%
        {"partyId": "charger_op", "name": "充电桩运营商", "type": "proportion", "value": 0.7} // 充电费70%
    ],
    "conditions": [
        // 条件:充电时长超过60分钟,运营商分润降10%
        {"field": "chargingDuration", "operator": ">", "value": "60", "adjust": {"partyId": "charger_op", "value": -0.1}}
    ]
}

2. 分账算法:从 “固定” 到 “动态” 的实现

JAVA
/**
 * 分账计算服务:根据规则计算各角色的分润金额
 */
@Service
public class ProfitShareService {
    @Autowired
    private ShareRuleDao shareRuleDao;

    /**
     * 计算分账金额
     * @param order 订单(包含停车费、充电费、充电时长等信息)
     * @return 分润结果列表
     */
    public List<ProfitShareDTO> calculateShare(Order order) {
        List<ProfitShareDTO> result = new ArrayList<>();
        // 1. 获取订单对应的分账规则
        ShareRule rule = shareRuleDao.getByScenario(order.getScenario());
        if (rule == null) {
            throw new IllegalArgumentException("未找到分账规则");
        }

        // 2. 处理固定金额分账(如政府抽10元)
        handleFixedShare(rule, result);

        // 3. 处理比例分账(如停车费80%给停车场)
        handleProportionShare(rule, order, result);

        // 4. 处理条件调整(如充电超过60分钟,运营商分润降10%)
        handleConditionAdjust(rule, order, result);

        return result;
    }

    /**
     * 处理固定金额分账
     */
    private void handleFixedShare(ShareRule rule, List<ProfitShareDTO> result) {
        for (ShareParty party : rule.getParties()) {
            if ("fixed".equals(party.getType())) {
                BigDecimal amount = new BigDecimal(party.getValue());
                result.add(new ProfitShareDTO(party.getPartyId(), amount));
            }
        }
    }

    /**
     * 处理比例分账
     */
    private void handleProportionShare(ShareRule rule, Order order, List<ProfitShareDTO> result) {
        BigDecimal parkingFee = order.getParkingFee();
        BigDecimal chargingFee = order.getChargingFee();

        for (ShareParty party : rule.getParties()) {
            if (!"proportion".equals(party.getType())) continue;

            BigDecimal amount = BigDecimal.ZERO;
            if ("parking_lot".equals(party.getPartyId())) {
                amount = parkingFee.multiply(new BigDecimal(party.getValue()));
            } else if ("charger_op".equals(party.getPartyId())) {
                amount = chargingFee.multiply(new BigDecimal(party.getValue()));
            }
            result.add(new ProfitShareDTO(party.getPartyId(), amount));
        }
    }

    /**
     * 处理条件调整
     */
    private void handleConditionAdjust(ShareRule rule, Order order, List<ProfitShareDTO> result) {
        for (Condition condition : rule.getConditions()) {
            // 检查条件是否满足(如充电时长>60分钟)
            boolean meet = checkCondition(condition, order);
            if (!meet) continue;

            // 调整对应角色的分润
            for (ProfitShareDTO share : result) {
                if (share.getPartyId().equals(condition.getAdjust().getPartyId())) {
                    BigDecimal adjust = new BigDecimal(condition.getAdjust().getValue());
                    share.setAmount(share.getAmount().add(adjust));
                }
            }
        }
    }

    /**
     * 检查条件是否满足
     */
    private boolean checkCondition(Condition condition, Order order) {
        String field = condition.getField();
        String operator = condition.getOperator();
        String value = condition.getValue();

        if ("chargingDuration".equals(field)) {
            int duration = order.getChargingDuration();
            int val = Integer.parseInt(value);
            return ">".equals(operator) && duration > val;
        }
        return false;
    }
}

想看更多与您场景匹配的落地案例?

立即咨询