Files
jsowell-charger-web/jsowell-admin/src/main/java/com/jsowell/service/CameraService.java

451 lines
18 KiB
Java
Raw Normal View History

2024-01-06 10:57:30 +08:00
package com.jsowell.service;
2023-12-05 15:44:59 +08:00
2023-12-06 11:23:02 +08:00
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.jsowell.common.constant.CacheConstants;
2024-02-22 15:37:25 +08:00
import com.jsowell.common.constant.Constants;
2023-12-06 11:23:02 +08:00
import com.jsowell.common.core.redis.RedisCache;
2024-02-22 15:37:25 +08:00
import com.jsowell.common.enums.uniapp.OccupyOrderStatusEnum;
2024-01-06 10:57:30 +08:00
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;
2024-01-06 10:57:30 +08:00
import com.jsowell.common.util.sign.MD5Util;
import com.jsowell.netty.server.mqtt.BootNettyMqttChannelInboundHandler;
2024-01-09 17:30:30 +08:00
import com.jsowell.pile.domain.OrderPileOccupy;
import com.jsowell.pile.domain.PileCameraInfo;
2024-02-22 15:37:25 +08:00
import com.jsowell.pile.dto.GenerateOccupyOrderDTO;
import com.jsowell.pile.dto.QueryOccupyOrderDTO;
2024-01-06 15:10:31 +08:00
import com.jsowell.pile.dto.camera.Camera2GroundLockCommand;
import com.jsowell.pile.dto.camera.CameraHeartBeatDTO;
2023-12-06 11:23:02 +08:00
import com.jsowell.pile.dto.camera.CameraIdentifyResultsDTO;
2024-01-06 15:20:28 +08:00
import com.jsowell.pile.service.MemberBasicInfoService;
import com.jsowell.pile.service.OrderPileOccupyService;
2024-03-19 16:22:40 +08:00
import com.jsowell.pile.service.PileCameraInfoService;
2024-07-16 14:07:06 +08:00
import com.jsowell.pile.vo.uniapp.customer.MemberVO;
2023-12-06 11:23:02 +08:00
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
2023-12-06 11:23:02 +08:00
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
2024-01-06 10:57:30 +08:00
2024-03-19 16:22:40 +08:00
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
2024-02-22 15:37:25 +08:00
import java.util.*;
import java.util.concurrent.TimeUnit;
2023-12-06 11:23:02 +08:00
2023-12-05 15:44:59 +08:00
/**
2023-12-06 11:23:02 +08:00
* 相机管理系统 Service
2023-12-05 15:44:59 +08:00
*
* @author Lemon
* @Date 2023/12/5 11:11:32
*/
2023-12-06 11:23:02 +08:00
@Service
2023-12-05 15:44:59 +08:00
public class CameraService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
2023-12-05 15:44:59 +08:00
2023-12-06 11:23:02 +08:00
@Autowired
private RedisCache redisCache;
@Autowired
2024-01-06 15:20:28 +08:00
private PileCameraInfoService pileCameraInfoService;
@Autowired
private OrderPileOccupyService orderPileOccupyService;
2024-01-06 10:57:30 +08:00
@Autowired
private BootNettyMqttChannelInboundHandler handler;
@Autowired
2024-01-06 15:20:28 +08:00
private MemberBasicInfoService memberBasicInfoService;
2024-01-06 10:57:30 +08:00
2023-12-06 11:23:02 +08:00
2024-02-23 08:30:58 +08:00
/**
* 接收相机识别结果
* @param jsonObject
* @throws InterruptedException
*/
2024-01-06 15:10:31 +08:00
public void receiveIdentifyResults(JSONObject jsonObject) throws InterruptedException {
// 区分入场和出场
2024-02-29 14:28:03 +08:00
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);
}
2024-02-23 08:30:58 +08:00
/**
* 保存心跳到Redis
* @param dto
*/
public void saveHeartBeat2Redis(CameraHeartBeatDTO dto) {
// 将基本信息存入缓存
String redisKey = CacheConstants.CAMERA_HEARTBEAT + dto.getIp();
redisCache.setCacheObject(redisKey, dto.getSn());
}
/**
* 车辆入场
2024-02-23 08:30:58 +08:00
* @param jsonObject json报文对象
* @param parkingState
* @return
* @throws InterruptedException
*/
2024-02-22 15:37:25 +08:00
private String vehicleEntry(JSONObject jsonObject, String parkingState) throws InterruptedException {
// 先将车牌图片信息存入缓存
// boolean result = saveCarPicture2Redis(jsonObject, parkingState);
2024-02-22 15:37:25 +08:00
// 将信息存数据库
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");
2024-02-24 14:31:10 +08:00
if (StringUtils.equals("无牌车", plateNumber)) {
return null;
}
2024-02-22 15:37:25 +08:00
// 先判断该车牌是否有挂起未支付的占桩订单
OrderPileOccupy occupy = OrderPileOccupy.builder()
.status(OccupyOrderStatusEnum.ORDER_HANG_UP.getCode()) // 2-订单挂起
2024-02-22 15:37:25 +08:00
.plateNumber(plateNumber)
.build();
List<OrderPileOccupy> occupyList = orderPileOccupyService.selectOrderPileOccupyList(occupy);
2024-02-22 15:37:25 +08:00
// todo 如果有占桩订单,则先提醒“需支付占桩订单”
if (CollectionUtils.isNotEmpty(occupyList)) {
return "需支付占桩订单";
}
// 根据车牌号找出绑定小程序的用户
List<MemberVO> memberList = memberBasicInfoService.getMemberInfoByPlateNumber(plateNumber);
GenerateOccupyOrderDTO dto = new GenerateOccupyOrderDTO();
2024-02-24 14:31:10 +08:00
dto.setPileSn(devName); // TODO 设备名称
dto.setConnectorCode(String.valueOf(zoneId)); // TODO 车位id
2024-02-22 15:37:25 +08:00
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);
2024-02-22 15:37:25 +08:00
// 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<>();
2024-01-06 15:10:31 +08:00
// 解析 jsonObject
// 车牌信息
2024-02-22 15:37:25 +08:00
CameraIdentifyResultsDTO.ProductH.Plate plate = JSONObject.parseObject(jsonObject.getJSONObject("product_h")
.getJSONObject("plate").toJSONString(),
2024-01-06 10:57:30 +08:00
CameraIdentifyResultsDTO.ProductH.Plate.class);
if (plate == null) {
2024-02-22 15:37:25 +08:00
logger.error("车位相机解析 error, 车牌信息为空");
return null;
2024-01-06 10:57:30 +08:00
}
2024-01-06 15:10:31 +08:00
// 设备信息
CameraIdentifyResultsDTO.DeviceInfo deviceInfo = JSONObject.parseObject(jsonObject.getJSONObject("device_info").toJSONString(),
CameraIdentifyResultsDTO.DeviceInfo.class);
if (deviceInfo == null) {
2024-02-22 15:37:25 +08:00
logger.error("车位相机解析 error, 设备信息为空");
return null;
2024-01-06 15:10:31 +08:00
}
// 停车位信息
2024-02-22 15:37:25 +08:00
CameraIdentifyResultsDTO.ProductH.Parking parking = JSONObject.parseObject(jsonObject.getJSONObject("product_h")
.getJSONObject("parking").toJSONString(),
2024-01-06 15:10:31 +08:00
CameraIdentifyResultsDTO.ProductH.Parking.class);
if (parking == null) {
2024-02-22 15:37:25 +08:00
logger.error("车位相机解析 error, 停车位信息为空");
return null;
2024-01-06 15:10:31 +08:00
}
// 获取背景图片
JSONArray bgImgs = jsonObject.getJSONArray("bg_img");
List<CameraIdentifyResultsDTO.BgImg> bgImgList = bgImgs.toList(CameraIdentifyResultsDTO.BgImg.class);
if (CollectionUtils.isEmpty(bgImgList)) {
2024-02-22 15:37:25 +08:00
logger.error("车位相机解析 error, 背景图片信息为空");
return null;
2024-01-06 15:10:31 +08:00
}
2024-02-22 15:37:25 +08:00
String sn = deviceInfo.getSn();
Integer zoneId = parking.getZoneId();
String devName = deviceInfo.getDevName();
2024-01-06 10:57:30 +08:00
// Base64 解密
String plateNumber = cn.hutool.core.codec.Base64.decodeStr(plate.getPlate());
2024-01-06 15:10:31 +08:00
String zoneName = cn.hutool.core.codec.Base64.decodeStr(parking.getZoneName());
// 将解密后的值重新 set 进对象中,便于存储数据库
plate.setPlate(plateNumber);
parking.setZoneName(zoneName);
2024-02-22 15:37:25 +08:00
// 循环 bgImgList, 将图片分成单张进行存储
2023-12-06 11:23:02 +08:00
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()
2024-02-22 15:37:25 +08:00
.deviceName(devName)
.deviceIp(deviceInfo.getIp())
2024-02-22 15:37:25 +08:00
.deviceSn(sn)
.plateNumber(plateNumber)
.parkingState(String.valueOf(parking.getParkingState()))
2024-02-22 15:37:25 +08:00
.zoneId(zoneId)
.zoneName(zoneName)
.color(plate.getColor())
.plateType(plate.getType())
.image(url)
.build();
// 插入数据库
pileCameraInfoService.insertPileCameraInfo(pileCameraInfo);
2023-12-06 11:23:02 +08:00
}
2024-02-22 15:37:25 +08:00
// 构建返回参数
resultMap.put("sn", sn);
resultMap.put("zoneId", String.valueOf(zoneId));
resultMap.put("plateNumber", plateNumber);
resultMap.put("devName", devName);
return resultMap;
2023-12-06 11:23:02 +08:00
}
/**
* 保存图像
* @param base64Image 图像的Base64编码
2024-01-06 10:57:30 +08:00
* @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;
}
2024-01-06 15:10:31 +08:00
/**
* 发送地锁升降指令
2024-02-22 15:37:25 +08:00
* @param sn 相机的sn号
* @param zoneId 相机对应的车位id
* @return 发送降锁指令的响应对象
* @throws InterruptedException
2024-01-06 15:10:31 +08:00
*/
2024-02-22 15:37:25 +08:00
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
2024-01-06 15:10:31 +08:00
2024-02-22 15:37:25 +08:00
.build();
2024-01-06 15:10:31 +08:00
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);
2024-02-22 15:37:25 +08:00
// 判断降锁是否成功
// 将此降锁指令存入缓存, 在 nettyServer 收到新消息时判断是否是此消息的回复
String redisKey = CacheConstants.SEND_DROP_LOCK_COMMOND + msgId;
2024-02-22 15:37:25 +08:00
redisCache.setCacheObject(redisKey, command, 5, TimeUnit.MINUTES);
// 延时 3 秒钟, 再查询缓存中是否有此降锁信息的回复
Thread.sleep(3);
String responseRedisKey = CacheConstants.GROUND_LOCK_DEVICE_REPORT + msgId;
2024-02-22 15:37:25 +08:00
Object cacheObject = redisCache.getCacheObject(responseRedisKey);
return cacheObject;
2024-01-06 15:10:31 +08:00
}
2024-01-06 10:57:30 +08:00
/**
* 发送具体指令到某主题
* @param sn 设备 sn
* @param msgType 消息类型
* @param msgPrefix 消息前缀
* @param topic 主题
* @param msgData 消息内容
2024-01-06 15:10:31 +08:00
*
2024-01-06 10:57:30 +08:00
* @throws InterruptedException
2024-01-06 15:10:31 +08:00
* @return msg_id 相当于此消息的唯一标识用于辨识返回报文
2024-01-06 10:57:30 +08:00
*/
2024-01-06 15:10:31 +08:00
public String sendMsg2Topic(String sn, String msgType, String msgPrefix, String topic, JSONObject msgData) throws InterruptedException {
2024-01-06 10:57:30 +08:00
JSONObject jsonObject = spliceStr(sn, msgType, msgPrefix);
// 通过sn查找出对应的channelId
String mqttConnectRedisKey = CacheConstants.MQTT_CONNECT_SN + sn;
Object cacheObject = redisCache.getCacheObject(mqttConnectRedisKey);
if (cacheObject == null) {
2024-01-06 15:10:31 +08:00
return null;
2024-01-06 10:57:30 +08:00
}
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());
2024-01-06 15:10:31 +08:00
return jsonObject.getString("msg_id");
2024-01-06 10:57:30 +08:00
}
/**
* 根据规则拼装字符串
* @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;
// }
2023-12-05 15:44:59 +08:00
}