Files
jsowell-charger-web/jsowell-admin/src/main/java/com/jsowell/service/CameraService.java
2024-07-16 14:07:06 +08:00

451 lines
18 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String, String> 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<OrderPileOccupy> 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<String, String> 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<OrderPileOccupy> occupyList = orderPileOccupyService.selectOrderPileOccupyList(occupy);
// todo 如果有占桩订单,则先提醒“需支付占桩订单”
if (CollectionUtils.isNotEmpty(occupyList)) {
return "需支付占桩订单";
}
// 根据车牌号找出绑定小程序的用户
List<MemberVO> 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<OrderPileOccupy> 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<String, String> saveInfo2DataBase(JSONObject jsonObject) {
Map<String, String> 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<CameraIdentifyResultsDTO.BgImg> 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("&timestamp=").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<CameraIdentifyResultsDTO.BgImg> 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;
// }
}