package com.jsowell.service; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.jsowell.common.constant.CacheConstants; import com.jsowell.common.constant.Constants; import com.jsowell.common.core.redis.RedisCache; import com.jsowell.common.enums.uniapp.OccupyOrderStatusEnum; import com.jsowell.common.util.DateUtils; import com.jsowell.common.util.StringUtils; import com.jsowell.common.util.file.AliyunOssUploadUtils; import com.jsowell.common.util.file.ImageUtils; import com.jsowell.common.util.sign.MD5Util; import com.jsowell.netty.server.mqtt.BootNettyMqttChannelInboundHandler; import com.jsowell.pile.domain.OrderPileOccupy; import com.jsowell.pile.domain.PileCameraInfo; import com.jsowell.pile.dto.GenerateOccupyOrderDTO; import com.jsowell.pile.dto.QueryOccupyOrderDTO; import com.jsowell.pile.dto.camera.Camera2GroundLockCommand; import com.jsowell.pile.dto.camera.CameraHeartBeatDTO; import com.jsowell.pile.dto.camera.CameraIdentifyResultsDTO; import com.jsowell.pile.service.MemberBasicInfoService; import com.jsowell.pile.service.OrderPileOccupyService; import com.jsowell.pile.service.PileCameraInfoService; import com.jsowell.pile.vo.uniapp.customer.MemberVO; import org.apache.commons.collections4.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; /** * 相机管理系统 Service * * @author Lemon * @Date 2023/12/5 11:11:32 */ @Service public class CameraService { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private RedisCache redisCache; @Autowired private PileCameraInfoService pileCameraInfoService; @Autowired private OrderPileOccupyService orderPileOccupyService; @Autowired private BootNettyMqttChannelInboundHandler handler; @Autowired private MemberBasicInfoService memberBasicInfoService; /** * 接收相机识别结果 * @param jsonObject * @throws InterruptedException */ public void receiveIdentifyResults(JSONObject jsonObject) throws InterruptedException { // 区分入场和出场 Integer parking_state = 0; try { parking_state = jsonObject.getJSONObject("parking").getInteger("parking_state"); }catch (Exception e) { logger.info("相机发送系统 alarmInfo:{}", jsonObject.toJSONString()); } if (parking_state == Constants.one) { // 入场 String parkingState = "ENTRY"; String result = vehicleEntry(jsonObject, parkingState); logger.info("车辆入场处理 result:{}", result); } if (parking_state == Constants.two) { // 在场 } if (parking_state == Constants.four) { // 出场 vehicleLeave(jsonObject); } // saveInfo2DataBase(jsonObject); } /** * 车辆离场 * @param jsonObject */ private void vehicleLeave(JSONObject jsonObject) { // 将信息存数据库 Map resultMap = saveInfo2DataBase(jsonObject); if (resultMap == null) { logger.error("车辆离场,将信息存入数据库 error, 源数据:{}", jsonObject); return; } String plateNumber = resultMap.get("plateNumber"); // 查出该车牌对应的挂起状态的占桩订单 QueryOccupyOrderDTO dto = QueryOccupyOrderDTO.builder() .plateNumber(plateNumber) .orderStatus(OccupyOrderStatusEnum.ORDER_HANG_UP.getCode()) .build(); List orderPileOccupyList = orderPileOccupyService.queryOccupyOrderList(dto); if (CollectionUtils.isEmpty(orderPileOccupyList)) { return; } OrderPileOccupy occupy = orderPileOccupyList.get(0); // 停止占桩订单 orderPileOccupyService.stopOccupyPileOrder(occupy); } /** * 保存心跳到Redis * @param dto */ public void saveHeartBeat2Redis(CameraHeartBeatDTO dto) { // 将基本信息存入缓存 String redisKey = CacheConstants.CAMERA_HEARTBEAT + dto.getIp(); redisCache.setCacheObject(redisKey, dto.getSn()); } /** * 车辆入场 * @param jsonObject json报文对象 * @param parkingState * @return * @throws InterruptedException */ private String vehicleEntry(JSONObject jsonObject, String parkingState) throws InterruptedException { // 先将车牌图片信息存入缓存 // boolean result = saveCarPicture2Redis(jsonObject, parkingState); // 将信息存数据库 Map resultMap = saveInfo2DataBase(jsonObject); if (resultMap == null) { logger.error("车辆入场,将信息存入数据库 error, 源数据:{}", jsonObject); return null; } String sn = resultMap.get("sn"); String zoneId = resultMap.get("zoneId"); String plateNumber = resultMap.get("plateNumber"); String devName = resultMap.get("devName"); if (StringUtils.equals("无牌车", plateNumber)) { return null; } // 先判断该车牌是否有挂起未支付的占桩订单 OrderPileOccupy occupy = OrderPileOccupy.builder() .status(OccupyOrderStatusEnum.ORDER_HANG_UP.getCode()) // 2-订单挂起 .plateNumber(plateNumber) .build(); List occupyList = orderPileOccupyService.selectOrderPileOccupyList(occupy); // todo 如果有占桩订单,则先提醒“需支付占桩订单” if (CollectionUtils.isNotEmpty(occupyList)) { return "需支付占桩订单"; } // 根据车牌号找出绑定小程序的用户 List memberList = memberBasicInfoService.getMemberInfoByPlateNumber(plateNumber); GenerateOccupyOrderDTO dto = new GenerateOccupyOrderDTO(); dto.setPileSn(devName); // TODO 设备名称 dto.setConnectorCode(String.valueOf(zoneId)); // TODO 车位id dto.setOrderStatus(OccupyOrderStatusEnum.ORDER_HANG_UP.getCode()); // 订单挂起 dto.setPayStatus(Constants.ZERO); // 未支付 String occupyPileOrderCode = null; if (CollectionUtils.isNotEmpty(memberList)) { // 如果是有小程序的用户,则先降地锁,然后生成一笔占桩订单 // 发送降锁指令 Object cacheObject = sendGroundLockMsg(sn, Integer.parseInt(zoneId)); if (cacheObject != null) { // 降锁成功,生成占桩订单(挂起、未支付) dto.setMemberId(memberList.get(0).getMemberId()); occupyPileOrderCode = orderPileOccupyService.generateOccupyPileOrder(dto); } }else { // 如果没有小程序账号,再根据此车牌是否有挂起的占桩订单 OrderPileOccupy orderPileOccupy = OrderPileOccupy.builder() .plateNumber(plateNumber) .build(); List orderPileOccupyList = orderPileOccupyService.selectOrderPileOccupyList(orderPileOccupy); // TODO 如果有已挂起的占桩订单,则不予降锁,将“已存在有未支付的占桩订单”信息返回 if (CollectionUtils.isNotEmpty(orderPileOccupyList)) { return "已存在有未支付的占桩订单"; } // 如果没有,则先降锁,再生成一笔占桩订单 (与车牌号绑定) Object cacheObject = sendGroundLockMsg(sn, Integer.parseInt(zoneId)); if (cacheObject == null) { return null; } dto.setPlateNumber(plateNumber); occupyPileOrderCode = orderPileOccupyService.generateOccupyPileOrder(dto); } return occupyPileOrderCode; } /** * 对数据进行解析并存储到数据库 * @param jsonObject * @return */ private Map saveInfo2DataBase(JSONObject jsonObject) { Map resultMap = new LinkedHashMap<>(); // 解析 jsonObject // 车牌信息 CameraIdentifyResultsDTO.ProductH.Plate plate = JSONObject.parseObject(jsonObject.getJSONObject("product_h") .getJSONObject("plate").toJSONString(), CameraIdentifyResultsDTO.ProductH.Plate.class); if (plate == null) { logger.error("车位相机解析 error, 车牌信息为空"); return null; } // 设备信息 CameraIdentifyResultsDTO.DeviceInfo deviceInfo = JSONObject.parseObject(jsonObject.getJSONObject("device_info").toJSONString(), CameraIdentifyResultsDTO.DeviceInfo.class); if (deviceInfo == null) { logger.error("车位相机解析 error, 设备信息为空"); return null; } // 停车位信息 CameraIdentifyResultsDTO.ProductH.Parking parking = JSONObject.parseObject(jsonObject.getJSONObject("product_h") .getJSONObject("parking").toJSONString(), CameraIdentifyResultsDTO.ProductH.Parking.class); if (parking == null) { logger.error("车位相机解析 error, 停车位信息为空"); return null; } // 获取背景图片 JSONArray bgImgs = jsonObject.getJSONArray("bg_img"); List bgImgList = bgImgs.toList(CameraIdentifyResultsDTO.BgImg.class); if (CollectionUtils.isEmpty(bgImgList)) { logger.error("车位相机解析 error, 背景图片信息为空"); return null; } String sn = deviceInfo.getSn(); Integer zoneId = parking.getZoneId(); String devName = deviceInfo.getDevName(); // Base64 解密 String plateNumber = cn.hutool.core.codec.Base64.decodeStr(plate.getPlate()); String zoneName = cn.hutool.core.codec.Base64.decodeStr(parking.getZoneName()); // 将解密后的值重新 set 进对象中,便于存储数据库 plate.setPlate(plateNumber); parking.setZoneName(zoneName); // 循环 bgImgList, 将图片分成单张进行存储 for (CameraIdentifyResultsDTO.BgImg bgImg : bgImgList) { // 上传到阿里云OSS,获取图片上传成功后的地址 String fileName = zoneName + "-" + System.currentTimeMillis() / 1000 + ".jpg"; String url = saveImage(bgImg.getImage(), fileName); if (StringUtils.isBlank(url)) { logger.error("车位号:{} 图片上传失败", zoneName); continue; } PileCameraInfo pileCameraInfo = PileCameraInfo.builder() .deviceName(devName) .deviceIp(deviceInfo.getIp()) .deviceSn(sn) .plateNumber(plateNumber) .parkingState(String.valueOf(parking.getParkingState())) .zoneId(zoneId) .zoneName(zoneName) .color(plate.getColor()) .plateType(plate.getType()) .image(url) .build(); // 插入数据库 pileCameraInfoService.insertPileCameraInfo(pileCameraInfo); } // 构建返回参数 resultMap.put("sn", sn); resultMap.put("zoneId", String.valueOf(zoneId)); resultMap.put("plateNumber", plateNumber); resultMap.put("devName", devName); return resultMap; } /** * 保存图像 * @param base64Image 图像的Base64编码 * @param fileName 文件名 * @return */ private String saveImage(String base64Image, String fileName) { try { // 将Base64编码的字符串解码为字节数组 byte[] imageBytes = Base64.getDecoder().decode(base64Image); // 将字节数组转换为 BufferedImage ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes); BufferedImage originalImage = ImageIO.read(inputStream); // 对图像进行压缩 double quality = 0.5; // 压缩质量 BufferedImage compressedImage = ImageUtils.compressImage(originalImage, quality); // 将压缩后的图片转换为字节数组 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(compressedImage, "jpg", outputStream); byte[] compressedImageBytes = outputStream.toByteArray(); // 上传图片到OSS String url = AliyunOssUploadUtils.upload2OSS(compressedImageBytes, fileName); if (StringUtils.isNotBlank(url)) { return url; } } catch (IOException e) { e.printStackTrace(); } return null; } /** * 发送地锁升降指令 * @param sn 相机的sn号 * @param zoneId 相机对应的车位id * @return 发送降锁指令的响应对象 * @throws InterruptedException */ private Object sendGroundLockMsg(String sn, Integer zoneId) throws InterruptedException { Camera2GroundLockCommand command = Camera2GroundLockCommand.builder() .sn(sn) .msgType("GroundlockOption") .msgPrefix("GO") .topic("/remoteCommand") .zoneId(zoneId) .option(3) // 3-降锁后不自动升锁 .force(0) // 强制操作 0:否 1:是 .build(); JSONObject msgData = new JSONObject(); msgData.put("option", command.getOption()); msgData.put("zone_id", command.getZoneId()); msgData.put("force", command.getForce()); String msgId = sendMsg2Topic(command.getSn(), command.getMsgType(), command.getMsgPrefix(), command.getTopic(), msgData); // 判断降锁是否成功 // 将此降锁指令存入缓存, 在 nettyServer 收到新消息时判断是否是此消息的回复 String redisKey = CacheConstants.SEND_DROP_LOCK_COMMOND + msgId; redisCache.setCacheObject(redisKey, command, 5, TimeUnit.MINUTES); // 延时 3 秒钟, 再查询缓存中是否有此降锁信息的回复 Thread.sleep(3); String responseRedisKey = CacheConstants.GROUND_LOCK_DEVICE_REPORT + msgId; Object cacheObject = redisCache.getCacheObject(responseRedisKey); return cacheObject; } /** * 发送具体指令到某主题 * @param sn 设备 sn * @param msgType 消息类型 * @param msgPrefix 消息前缀 * @param topic 主题 * @param msgData 消息内容 * * @throws InterruptedException * @return msg_id 相当于此消息的唯一标识,用于辨识返回报文 */ public String sendMsg2Topic(String sn, String msgType, String msgPrefix, String topic, JSONObject msgData) throws InterruptedException { JSONObject jsonObject = spliceStr(sn, msgType, msgPrefix); // 通过sn查找出对应的channelId String mqttConnectRedisKey = CacheConstants.MQTT_CONNECT_SN + sn; Object cacheObject = redisCache.getCacheObject(mqttConnectRedisKey); if (cacheObject == null) { return null; } String channelId = (String) cacheObject; if (msgData != null) { jsonObject.put("msg_data", msgData); } logger.info("给相机发送远程命令,sn:{}, 消息类型:{}, 主题:{}, 最终发送数据:{}", sn, msgType, topic, jsonObject.toJSONString()); // 发送消息 handler.sendMsg(channelId, topic, jsonObject.toJSONString()); return jsonObject.getString("msg_id"); } /** * 根据规则拼装字符串 * @param sn 设备 sn * @param msgType 消息类型 * @param msgPrefix 消息前缀 * @return 拼装好的json对象 */ private JSONObject spliceStr(String sn, String msgType, String msgPrefix) { StringBuilder sb = new StringBuilder(); String msgId = msgPrefix + DateUtils.dateTimeNow(DateUtils.YYYYMMDDHHMMSS) + "01"; String timeStamp = DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD_HH_MM_SS); sb.append("sn=").append(sn) .append("×tamp=").append(timeStamp) .append("&msg_id=").append(msgId) .append("&msg_type=").append(msgType); // 进行 32 位 MD5 计算 String sign = MD5Util.MD5Encode(sb.toString()).toUpperCase(Locale.ROOT); JSONObject jsonObject = new JSONObject(); jsonObject.put("sign", sign); jsonObject.put("sn", sn); jsonObject.put("timestamp", timeStamp); jsonObject.put("msg_id", msgId); jsonObject.put("msg_type", msgType); return jsonObject; } /** * 将车辆图片信息存入缓存 * @param jsonObject */ // private boolean saveCarPicture2Redis(JSONObject jsonObject, String parkingState) { // // 获取车牌号 // String plateNumber = jsonObject.getJSONObject("plate").getString("plate"); // if (StringUtils.isBlank(plateNumber)) { // return false; // } // // 获取背景图片 // JSONArray bgImgs = jsonObject.getJSONArray("bg_img"); // List bgImgList = bgImgs.toList(CameraIdentifyResultsDTO.BgImg.class); // if (CollectionUtils.isEmpty(bgImgList)) { // return false; // } // for (CameraIdentifyResultsDTO.BgImg bgImg : bgImgList) { // String image = bgImg.getImage(); // 图片的 base64 编码 // // String key = bgImg.getKey(); // 索引id // // key: 前缀 + 车牌号 + 日期 + 入场/出场状态 // String redisKey = CacheConstants.CAMERA_IMAGE_BY_PLATE_NUMBER + plateNumber + "_" + DateUtils.getDate() + "_" + parkingState; // // 存入缓存 // // 暂时永久保存 // redisCache.setCacheObject(redisKey, image); // } // return true; // } }