forked from dyf/fys-Multi-tenant
feat(equipment): 实现设备维修记录图片管理和报警处理功能
-重构文件哈希工具类路径并优化上传逻辑,支持秒传 - 新增维修记录图片ID列表字段及删除旧图片逻辑- 设备维修记录查询增加设备名称模糊搜索条件 -日期查询条件添加格式化注解支持 yyyy-MM-dd- MQTT规则中新增SOS与静止报警处理机制 - 实现报警生命周期管理(开始/结束)及Redis缓存控制 - 添加报警信息入库和位置解析功能 - 优化设备状态数据解析与经纬度异步存储逻辑
This commit is contained in:
@ -61,5 +61,10 @@ public class MqttXinghanJson {
|
|||||||
*/
|
*/
|
||||||
@JsonProperty("sta_latitude")
|
@JsonProperty("sta_latitude")
|
||||||
public String stalatitude;
|
public String stalatitude;
|
||||||
|
/**
|
||||||
|
* 第十二键值对,系统现状,0关机,1仅充电,2开机未充电,3,开机且充电
|
||||||
|
*/
|
||||||
|
@JsonProperty("sta_system")
|
||||||
|
public String stasystem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,41 @@
|
|||||||
package com.fuyuanshen.global.mqtt.rule.xinghan;
|
package com.fuyuanshen.global.mqtt.rule.xinghan;
|
||||||
|
|
||||||
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fuyuanshen.common.core.constant.GlobalConstants;
|
import com.fuyuanshen.common.core.constant.GlobalConstants;
|
||||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||||
|
import com.fuyuanshen.common.core.utils.date.DurationUtils;
|
||||||
import com.fuyuanshen.common.json.utils.JsonUtils;
|
import com.fuyuanshen.common.json.utils.JsonUtils;
|
||||||
import com.fuyuanshen.common.redis.utils.RedisUtils;
|
import com.fuyuanshen.common.redis.utils.RedisUtils;
|
||||||
|
import com.fuyuanshen.equipment.domain.Device;
|
||||||
|
import com.fuyuanshen.equipment.domain.bo.DeviceAlarmBo;
|
||||||
|
import com.fuyuanshen.equipment.domain.vo.DeviceAlarmVo;
|
||||||
|
import com.fuyuanshen.equipment.mapper.DeviceAlarmMapper;
|
||||||
|
import com.fuyuanshen.equipment.service.DeviceService;
|
||||||
|
import com.fuyuanshen.equipment.service.IDeviceAlarmService;
|
||||||
import com.fuyuanshen.equipment.utils.map.GetAddressFromLatUtil;
|
import com.fuyuanshen.equipment.utils.map.GetAddressFromLatUtil;
|
||||||
import com.fuyuanshen.equipment.utils.map.LngLonUtil;
|
import com.fuyuanshen.equipment.utils.map.LngLonUtil;
|
||||||
import com.fuyuanshen.global.mqtt.base.MqttMessageRule;
|
import com.fuyuanshen.global.mqtt.base.MqttMessageRule;
|
||||||
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
|
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
|
||||||
import com.fuyuanshen.global.mqtt.base.MqttXinghanJson;
|
import com.fuyuanshen.global.mqtt.base.MqttXinghanJson;
|
||||||
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
|
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
|
||||||
import com.fuyuanshen.global.mqtt.constants.LightingCommandTypeConstants;
|
|
||||||
import com.fuyuanshen.global.mqtt.constants.XingHanCommandTypeConstants;
|
import com.fuyuanshen.global.mqtt.constants.XingHanCommandTypeConstants;
|
||||||
import com.fuyuanshen.global.mqtt.listener.domain.FunctionAccessStatus;
|
import com.fuyuanshen.global.mqtt.listener.domain.FunctionAccessStatus;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY;
|
import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY;
|
||||||
|
import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
|
||||||
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.*;
|
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.*;
|
||||||
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
|
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
|
||||||
|
|
||||||
@ -57,6 +65,9 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
private final IDeviceAlarmService deviceAlarmService;
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
private final DeviceAlarmMapper deviceAlarmMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(MqttRuleContext context) {
|
public void execute(MqttRuleContext context) {
|
||||||
@ -99,12 +110,123 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
|
|||||||
// 异步发送经纬度到Redis
|
// 异步发送经纬度到Redis
|
||||||
asyncSendLocationToRedisWithFuture(deviceImei, deviceStatus.getStalatitude(), deviceStatus.getStalongitude());
|
asyncSendLocationToRedisWithFuture(deviceImei, deviceStatus.getStalatitude(), deviceStatus.getStalongitude());
|
||||||
|
|
||||||
|
// 保存报警信息
|
||||||
|
saveAlarm(deviceImei,deviceStatus);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("异步发送设备信息到Redis时出错: device={}, error={}", deviceImei, e.getMessage(), e);
|
log.error("异步发送设备信息到Redis时出错: device={}, error={}", deviceImei, e.getMessage(), e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入口:保存报警(SOS 与 Shake 完全独立)
|
||||||
|
*/
|
||||||
|
public void saveAlarm(String deviceImei, MqttXinghanJson status) {
|
||||||
|
int sos = Optional.ofNullable(status.getStaSOSGrade()).orElse(0);
|
||||||
|
// 1. 处理 SOS 报警
|
||||||
|
handleSingleAlarm(deviceImei,
|
||||||
|
sos > 0,
|
||||||
|
AlarmType.SOS);
|
||||||
|
int shake = Optional.ofNullable(status.getStaShakeBit()).orElse(0);
|
||||||
|
// 2. 处理 Shake 报警
|
||||||
|
handleSingleAlarm(deviceImei,
|
||||||
|
shake > 0,
|
||||||
|
AlarmType.SHAKE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用:对单个报警源的“开始/结束”生命周期管理
|
||||||
|
*/
|
||||||
|
private void handleSingleAlarm(String deviceImei, boolean nowAlarming, AlarmType type) {
|
||||||
|
String redisKey = buildAlarmRedisKey(deviceImei, type);
|
||||||
|
|
||||||
|
Long alarmId = RedisUtils.getCacheObject(redisKey);
|
||||||
|
|
||||||
|
// ---------- 情况 1:当前正在报警 ----------
|
||||||
|
if (nowAlarming) {
|
||||||
|
// 已存在未结束报警 -> 什么都不做(同一条报警)
|
||||||
|
if (alarmId != null) {
|
||||||
|
// key 还在 -> 同一条报警,只续期
|
||||||
|
RedisUtils.setCacheObject(redisKey, alarmId, Duration.ofMinutes(10));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 不存在 -> 新建
|
||||||
|
DeviceAlarmBo bo = createAlarmBo(deviceImei, type);
|
||||||
|
deviceAlarmService.insertByBo(bo);
|
||||||
|
RedisUtils.setCacheObject(redisKey, bo.getId(), Duration.ofMinutes(10)); // 5分钟后结束过期
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 情况 2:当前不报警 ----------
|
||||||
|
if (alarmId != null) {
|
||||||
|
// 结束它
|
||||||
|
finishAlarm(alarmId);
|
||||||
|
RedisUtils.deleteObject(redisKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束报警:写库
|
||||||
|
*/
|
||||||
|
private void finishAlarm(Long alarmId) {
|
||||||
|
DeviceAlarmVo vo = deviceAlarmService.queryById(alarmId);
|
||||||
|
if (vo == null || vo.getTreatmentState() == 0) {
|
||||||
|
return; // 已处理或已被删
|
||||||
|
}
|
||||||
|
DeviceAlarmBo bo = BeanUtil.toBean(vo, DeviceAlarmBo.class);
|
||||||
|
bo.setFinishTime(new Date());
|
||||||
|
bo.setDurationTime(DurationUtils.getDurationBetween(vo.getStartTime(), bo.getFinishTime()));
|
||||||
|
bo.setTreatmentState(0); // 已处理
|
||||||
|
deviceAlarmService.updateByBo(bo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建报警 Bo
|
||||||
|
*/
|
||||||
|
private DeviceAlarmBo createAlarmBo(String deviceImei, AlarmType type) {
|
||||||
|
Device device = deviceService.selectDeviceByImei(deviceImei);
|
||||||
|
DeviceAlarmBo bo = new DeviceAlarmBo();
|
||||||
|
bo.setDeviceId(device.getId());
|
||||||
|
bo.setDeviceImei(deviceImei);
|
||||||
|
bo.setDeviceAction(2); // 自动报警
|
||||||
|
bo.setStartTime(new Date());
|
||||||
|
bo.setTreatmentState(1); // 未处理
|
||||||
|
bo.setTenantId(device.getTenantId());
|
||||||
|
|
||||||
|
// 报警内容
|
||||||
|
bo.setContent("自动报警:" + type.getDesc());
|
||||||
|
|
||||||
|
// 位置
|
||||||
|
String location = RedisUtils.getCacheObject(
|
||||||
|
GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX);
|
||||||
|
if (StrUtil.isNotBlank(location)) {
|
||||||
|
bo.setLocation(JSONObject.parseObject(location).getString("address"));
|
||||||
|
}
|
||||||
|
return bo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key 构建
|
||||||
|
*/
|
||||||
|
private String buildAlarmRedisKey(String deviceImei, AlarmType type) {
|
||||||
|
return StrUtil.format("{}{}{}{}:alarm_id",
|
||||||
|
GLOBAL_REDIS_KEY, DEVICE_KEY_PREFIX, deviceImei, type.getSuffix());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报警类型枚举
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
enum AlarmType {
|
||||||
|
SOS("_sos", "SOS报警"),
|
||||||
|
SHAKE("_shake", "静止报警");
|
||||||
|
|
||||||
|
private final String suffix;
|
||||||
|
private final String desc;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步发送位置信息到Redis(使用CompletableFuture)
|
* 异步发送位置信息到Redis(使用CompletableFuture)
|
||||||
*
|
*
|
||||||
|
|||||||
@ -10,18 +10,15 @@ import com.fuyuanshen.app.service.IAppBusinessFileService;
|
|||||||
import com.fuyuanshen.app.service.IAppOperationVideoService;
|
import com.fuyuanshen.app.service.IAppOperationVideoService;
|
||||||
import com.fuyuanshen.common.core.exception.ServiceException;
|
import com.fuyuanshen.common.core.exception.ServiceException;
|
||||||
import com.fuyuanshen.common.satoken.utils.AppLoginHelper;
|
import com.fuyuanshen.common.satoken.utils.AppLoginHelper;
|
||||||
import com.fuyuanshen.equipment.mapper.DeviceMapper;
|
|
||||||
import com.fuyuanshen.equipment.service.DeviceService;
|
import com.fuyuanshen.equipment.service.DeviceService;
|
||||||
import com.fuyuanshen.system.domain.vo.SysOssVo;
|
import com.fuyuanshen.system.domain.vo.SysOssVo;
|
||||||
import com.fuyuanshen.system.service.ISysOssService;
|
import com.fuyuanshen.system.service.ISysOssService;
|
||||||
import com.fuyuanshen.web.domain.vo.DeviceInfoVo;
|
import com.fuyuanshen.web.domain.vo.DeviceInfoVo;
|
||||||
import com.fuyuanshen.web.util.FileHashUtil;
|
import com.fuyuanshen.equipment.utils.FileHashUtil;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import jakarta.validation.constraints.*;
|
import jakarta.validation.constraints.*;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@ -70,4 +72,6 @@ public class DeviceRepairRecordsBo extends BaseEntity {
|
|||||||
@Schema(title = "维修后图片")
|
@Schema(title = "维修后图片")
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private MultipartFile afterFile;
|
private MultipartFile afterFile;
|
||||||
|
|
||||||
|
private List<Long> imageIds;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -51,8 +52,10 @@ public class DeviceRepairRecordsQueryCriteria extends BaseEntity {
|
|||||||
private String repairPerson;
|
private String repairPerson;
|
||||||
|
|
||||||
@Schema(title = "维修开始时间")
|
@Schema(title = "维修开始时间")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date repairBeginTime;
|
private Date repairBeginTime;
|
||||||
@Schema(title = "维修结束时间")
|
@Schema(title = "维修结束时间")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date repairEndTime;
|
private Date repairEndTime;
|
||||||
|
|
||||||
@Schema(title = "所属客户")
|
@Schema(title = "所属客户")
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import com.fuyuanshen.equipment.domain.vo.DeviceRepairImagesVo;
|
|||||||
import com.fuyuanshen.equipment.enums.RepairImageType;
|
import com.fuyuanshen.equipment.enums.RepairImageType;
|
||||||
import com.fuyuanshen.equipment.mapper.DeviceMapper;
|
import com.fuyuanshen.equipment.mapper.DeviceMapper;
|
||||||
import com.fuyuanshen.equipment.mapper.DeviceRepairImagesMapper;
|
import com.fuyuanshen.equipment.mapper.DeviceRepairImagesMapper;
|
||||||
|
import com.fuyuanshen.equipment.utils.FileHashUtil;
|
||||||
import com.fuyuanshen.system.domain.vo.SysOssVo;
|
import com.fuyuanshen.system.domain.vo.SysOssVo;
|
||||||
import com.fuyuanshen.system.service.ISysOssService;
|
import com.fuyuanshen.system.service.ISysOssService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -32,6 +33,7 @@ import com.fuyuanshen.equipment.service.IDeviceRepairRecordsService;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -178,6 +180,10 @@ public class DeviceRepairRecordsServiceImpl extends ServiceImpl<DeviceRepairReco
|
|||||||
if (!updated) {
|
if (!updated) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// 3. 删除旧图片
|
||||||
|
if(bo.getImageIds() != null){
|
||||||
|
imagesMapper.deleteByIds(bo.getImageIds());
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 收集需要保存的图片
|
// 3. 收集需要保存的图片
|
||||||
List<DeviceRepairImages> images = new ArrayList<>(2);
|
List<DeviceRepairImages> images = new ArrayList<>(2);
|
||||||
@ -200,12 +206,28 @@ public class DeviceRepairRecordsServiceImpl extends ServiceImpl<DeviceRepairReco
|
|||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SysOssVo ossVo = ossService.upload(file);
|
|
||||||
|
// 1. 计算文件哈希
|
||||||
|
String hash = null;
|
||||||
|
try {
|
||||||
|
hash = FileHashUtil.hash(file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 先根据 hash 查库(秒传)
|
||||||
|
SysOssVo exist = ossService.selectByHash(hash);
|
||||||
|
if (exist == null) {
|
||||||
|
// 2.2 不存在,真正上传
|
||||||
|
exist = ossService.upload(file);
|
||||||
|
// 2.3 把 hash 写回记录(供下次去重)
|
||||||
|
ossService.updateHashById(exist.getOssId(), hash);
|
||||||
|
}
|
||||||
|
|
||||||
DeviceRepairImages image = new DeviceRepairImages();
|
DeviceRepairImages image = new DeviceRepairImages();
|
||||||
image.setRecordId(recordId);
|
image.setRecordId(recordId);
|
||||||
image.setImageType(imageType);
|
image.setImageType(imageType);
|
||||||
image.setImageUrl(ossVo.getUrl());
|
image.setImageUrl(exist.getUrl());
|
||||||
|
|
||||||
list.add(image);
|
list.add(image);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.fuyuanshen.web.util;
|
package com.fuyuanshen.equipment.utils;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@ -23,6 +23,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
FROM device_repair_records dr
|
FROM device_repair_records dr
|
||||||
JOIN device d ON dr.device_id = d.id
|
JOIN device d ON dr.device_id = d.id
|
||||||
<where>
|
<where>
|
||||||
|
<if test="criteria.searchValue != null">
|
||||||
|
and d.device_name like concat('%', TRIM(#{criteria.searchValue}), '%')
|
||||||
|
</if>
|
||||||
<if test="criteria.deviceId != null">
|
<if test="criteria.deviceId != null">
|
||||||
and dr.device_id = #{criteria.deviceId}
|
and dr.device_id = #{criteria.deviceId}
|
||||||
</if>
|
</if>
|
||||||
|
|||||||
Reference in New Issue
Block a user