feat(equipment): 实现设备维修记录图片管理和报警处理功能

-重构文件哈希工具类路径并优化上传逻辑,支持秒传
- 新增维修记录图片ID列表字段及删除旧图片逻辑- 设备维修记录查询增加设备名称模糊搜索条件
-日期查询条件添加格式化注解支持 yyyy-MM-dd- MQTT规则中新增SOS与静止报警处理机制
- 实现报警生命周期管理(开始/结束)及Redis缓存控制
- 添加报警信息入库和位置解析功能
- 优化设备状态数据解析与经纬度异步存储逻辑
This commit is contained in:
2025-09-23 11:31:11 +08:00
parent 5f36c25550
commit 49e9066033
8 changed files with 168 additions and 12 deletions

View File

@ -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;
} }

View File

@ -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
* *

View File

@ -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;

View File

@ -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;
} }

View File

@ -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 = "所属客户")

View File

@ -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);
} }

View File

@ -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;

View File

@ -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>