forked from dyf/fys-Multi-tenant
feat(mqtt): 添加报警检查服务实现多阶段报警处理
- 实现 AlarmCheckService 提供延迟队列消费者功能 - 添加 AlarmDelayProvider 接口定义延迟检查任务 - 集成 AlarmStageConfig 支持租户配置报警阶段延迟时间 - 重构 AliyunVoiceUtil 返回完整响应对象而非字符串 - 在 AppDeviceController 中新增 AlarmList 接口查询设备告警列表 - 扩展设备相关控制器支持数据来源枚举参数传递 - 新增 Xinghan 指令控制器提供 HBY018A 设备专用接口 - 定义 DataSourceEnum 枚举区分 APP 和 Web 数据来源 - 扩展 Device 实体类增加紧急联系人和通知配置字段 - 添加 DeviceAlarm 实体类告警状态和等级属性 - 新增 DeviceContactPhoneBo 处理设备联系人信息 - 优化设备操作记录日志支持数据来源标识 - 实现设备自定义语音短信消息编辑功能 - 添加设备通知开关和紧急联系人设置接口
This commit is contained in:
@ -9,9 +9,12 @@ import com.fuyuanshen.common.mybatis.core.page.TableDataInfo;
|
||||
import com.fuyuanshen.common.satoken.utils.AppLoginHelper;
|
||||
import com.fuyuanshen.common.web.core.BaseController;
|
||||
import com.fuyuanshen.equipment.domain.Device;
|
||||
import com.fuyuanshen.equipment.domain.bo.DeviceAlarmBo;
|
||||
import com.fuyuanshen.equipment.domain.dto.AppDeviceBo;
|
||||
import com.fuyuanshen.equipment.domain.query.DeviceQueryCriteria;
|
||||
import com.fuyuanshen.equipment.domain.vo.AppDeviceVo;
|
||||
import com.fuyuanshen.equipment.domain.vo.DeviceAlarmVo;
|
||||
import com.fuyuanshen.equipment.service.IDeviceAlarmService;
|
||||
import com.fuyuanshen.web.service.device.DeviceBizService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@ -30,8 +33,18 @@ import java.util.Map;
|
||||
public class AppDeviceController extends BaseController {
|
||||
|
||||
private final DeviceBizService appDeviceService;
|
||||
private final IDeviceAlarmService deviceAlarmService;
|
||||
|
||||
|
||||
/**
|
||||
* 查询设备告警列表
|
||||
*/
|
||||
// @SaCheckPermission("equipment:alarm:list")
|
||||
@GetMapping("/AlarmList")
|
||||
public TableDataInfo<DeviceAlarmVo> AlarmList(DeviceAlarmBo bo, PageQuery pageQuery) {
|
||||
return deviceAlarmService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询设备列表
|
||||
*/
|
||||
|
||||
@ -13,6 +13,7 @@ import com.fuyuanshen.customer.mapper.CustomerMapper;
|
||||
import com.fuyuanshen.equipment.domain.DeviceType;
|
||||
import com.fuyuanshen.equipment.domain.dto.AppDeviceSendMsgBo;
|
||||
import com.fuyuanshen.equipment.domain.form.DeviceForm;
|
||||
import com.fuyuanshen.equipment.enums.DataSourceEnum;
|
||||
import com.fuyuanshen.equipment.mapper.DeviceMapper;
|
||||
import com.fuyuanshen.equipment.service.DeviceService;
|
||||
import com.fuyuanshen.equipment.service.DeviceTypeService;
|
||||
@ -48,7 +49,7 @@ public class AppDeviceXinghanController extends BaseController {
|
||||
@PostMapping(value = "/registerPersonInfo")
|
||||
// @FunctionAccessAnnotation("registerPersonInfo")
|
||||
public R<Void> registerPersonInfo(@Validated(AddGroup.class) @RequestBody AppPersonnelInfoBo bo) {
|
||||
return toAjax(appDeviceService.registerPersonInfo(bo));
|
||||
return toAjax(appDeviceService.registerPersonInfo(bo, DataSourceEnum.APP));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,7 +59,16 @@ public class AppDeviceXinghanController extends BaseController {
|
||||
@PostMapping(value = "/sendAlarmMessage")
|
||||
@FunctionAccessBatcAnnotation(value = "sendAlarmMessage", timeOut = 5, batchMaxTimeOut = 10)
|
||||
public R<Void> sendAlarmMessage(@RequestBody AppDeviceSendMsgBo bo) {
|
||||
return toAjax(appDeviceService.sendAlarmMessage(bo));
|
||||
return toAjax(appDeviceService.sendAlarmMessage(bo, DataSourceEnum.APP));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存设备日志
|
||||
*/
|
||||
@PostMapping(value = "/saveDeviceLog")
|
||||
public R<Void> saveRecordDeviceLog(@RequestBody AppPersonnelInfoBo bo) {
|
||||
appDeviceService.saveRecordDeviceLog(bo, DataSourceEnum.APP);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,7 +83,7 @@ public class AppDeviceXinghanController extends BaseController {
|
||||
if (file.getSize() > 1024 * 1024 * 2) {
|
||||
return R.warn("图片不能大于2M");
|
||||
}
|
||||
appDeviceService.uploadDeviceLogo(bo);
|
||||
appDeviceService.uploadDeviceLogo(bo, DataSourceEnum.APP);
|
||||
|
||||
return R.ok();
|
||||
}
|
||||
@ -86,7 +96,7 @@ public class AppDeviceXinghanController extends BaseController {
|
||||
@PostMapping("/DetectGradeSettings")
|
||||
public R<Void> DetectGradeSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
appDeviceService.upDetectGradeSettings(params);
|
||||
appDeviceService.upDetectGradeSettings(params, DataSourceEnum.APP);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ -98,7 +108,7 @@ public class AppDeviceXinghanController extends BaseController {
|
||||
@PostMapping("/LightGradeSettings")
|
||||
public R<Void> LightGradeSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
appDeviceService.upLightGradeSettings(params);
|
||||
appDeviceService.upLightGradeSettings(params, DataSourceEnum.APP);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ -110,7 +120,7 @@ public class AppDeviceXinghanController extends BaseController {
|
||||
@PostMapping("/SOSGradeSettings")
|
||||
public R<Void> SOSGradeSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
appDeviceService.upSOSGradeSettings(params);
|
||||
appDeviceService.upSOSGradeSettings(params, DataSourceEnum.APP);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ -122,7 +132,7 @@ public class AppDeviceXinghanController extends BaseController {
|
||||
@PostMapping("/ShakeBitSettings")
|
||||
public R<Void> ShakeBitSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
appDeviceService.upShakeBitSettings(params);
|
||||
appDeviceService.upShakeBitSettings(params, DataSourceEnum.APP);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
package com.fuyuanshen.app.controller.device.Xinghan;
|
||||
|
||||
|
||||
import com.fuyuanshen.app.domain.bo.AppPersonnelInfoBo;
|
||||
import com.fuyuanshen.app.domain.dto.AppDeviceLogoUploadDto;
|
||||
import com.fuyuanshen.common.core.domain.R;
|
||||
import com.fuyuanshen.common.core.validate.AddGroup;
|
||||
import com.fuyuanshen.common.log.annotation.Log;
|
||||
import com.fuyuanshen.common.ratelimiter.annotation.FunctionAccessAnnotation;
|
||||
import com.fuyuanshen.common.ratelimiter.annotation.FunctionAccessBatcAnnotation;
|
||||
import com.fuyuanshen.common.web.core.BaseController;
|
||||
import com.fuyuanshen.equipment.domain.bo.DeviceContactPhoneBo;
|
||||
import com.fuyuanshen.equipment.domain.dto.AppDeviceSendMsgBo;
|
||||
import com.fuyuanshen.equipment.enums.DataSourceEnum;
|
||||
import com.fuyuanshen.equipment.service.DeviceService;
|
||||
import com.fuyuanshen.web.domain.Dto.DeviceXinghanInstructDto;
|
||||
import com.fuyuanshen.web.service.device.DeviceXinghanBizService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/app/hby018a/device")
|
||||
public class AppHBY018AController extends BaseController {
|
||||
private final DeviceXinghanBizService appDeviceService;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
/**
|
||||
* 照明档位
|
||||
* 照明档位,2,1,0,分别表示弱光/强光/关闭
|
||||
*/
|
||||
@Log(title = "xinghan指令-照明档位")
|
||||
@PostMapping("/SideLightSettings")
|
||||
public R<Void> SideLightSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
appDeviceService.upSideLightSettings(params, DataSourceEnum.APP);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制报警状态
|
||||
* 强制报警状态,0-未报警,1-正在报警。
|
||||
*/
|
||||
@Log(title = "xinghan指令-强制报警状态")
|
||||
@PostMapping("/ShakeBitSettings")
|
||||
public R<Void> ShakeBitSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
appDeviceService.upShakeBitSettings(params, DataSourceEnum.APP);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义语音消息
|
||||
*/
|
||||
@PostMapping("/SetVoiceMsg")
|
||||
public R<Void> editSosVoiceMsg(@RequestBody AppDeviceSendMsgBo bo) {
|
||||
return toAjax(appDeviceService.sendAlarmMessage(bo, DataSourceEnum.APP));
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义短信消息
|
||||
*/
|
||||
@PostMapping("/SetSmsMsg")
|
||||
public R<Void> editSosSmsMsg(@RequestBody AppDeviceSendMsgBo bo) {
|
||||
return toAjax(appDeviceService.editSosSmsMsg(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置消息通知开关
|
||||
*/
|
||||
@PostMapping("/SetNotificationEnabled")
|
||||
public R<Void> editNotificationEnabled(@RequestBody DeviceContactPhoneBo bo) {
|
||||
return toAjax(appDeviceService.editNotificationEnabled(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加设备紧急联系人
|
||||
*/
|
||||
@PostMapping("/SetContactPhone")
|
||||
public R<Void> editContactPhone(@RequestBody DeviceContactPhoneBo bo) {
|
||||
return toAjax(appDeviceService.editContactPhone(bo));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.fuyuanshen.global.Provider;
|
||||
|
||||
public interface AlarmDelayProvider {
|
||||
/**
|
||||
* 发送延迟检查任务
|
||||
* @param alarmId 报警表主键ID
|
||||
* @param delayInSeconds 延迟秒数(如 180)
|
||||
*/
|
||||
void sendDelayCheck(Long alarmId, long delayInSeconds);
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package com.fuyuanshen.global.Provider;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RBlockingQueue;
|
||||
import org.redisson.api.RDelayedQueue;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
@Primary
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class RedissonAlarmDelayProvider implements AlarmDelayProvider {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
public static final String QUEUE_NAME = "PROD:ALARM:DELAY:CHECK";
|
||||
|
||||
private RDelayedQueue<Long> delayedQueue;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 初始化一次,内部转移任务会持续运行
|
||||
RBlockingQueue<Long> blockingQueue = redissonClient.getBlockingQueue(QUEUE_NAME);
|
||||
delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
|
||||
log.info("Redisson 延迟报警队列初始化完成:{}", QUEUE_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendDelayCheck(Long alarmId, long delayInSeconds) {
|
||||
// 复用已初始化的实例,避免创建多余对象
|
||||
delayedQueue.offer(alarmId, delayInSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
log.info("正在销毁 Redisson 延迟报警队列...");
|
||||
if (delayedQueue != null) {
|
||||
delayedQueue.destroy(); // 释放后台任务和资源
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.fuyuanshen.global.mqtt.config;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fuyuanshen.common.mybatis.helper.DataBaseHelper;
|
||||
import com.fuyuanshen.common.satoken.utils.LoginHelper;
|
||||
import com.fuyuanshen.system.domain.SysDictData;
|
||||
import com.fuyuanshen.system.mapper.SysDictDataMapper;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.Getter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.annotation.SessionScope;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class AlarmStageConfig {
|
||||
@Autowired
|
||||
private SysDictDataMapper dictDataMapper;
|
||||
|
||||
// 租户ID -> (阶段编号 -> 延迟秒数)
|
||||
private final Map<String, Map<Integer, Long>> tenantConfigCache = new ConcurrentHashMap<>();
|
||||
|
||||
public int getTotalStages(String tenantId) {
|
||||
return getConfig(tenantId).size();
|
||||
}
|
||||
|
||||
public long getDelayByStage(String tenantId, int stage) {
|
||||
return getConfig(tenantId).getOrDefault(stage, 5 * 60L);
|
||||
}
|
||||
|
||||
private Map<Integer, Long> getConfig(String tenantId) {
|
||||
if (tenantId == null || tenantId.isBlank()) {
|
||||
throw new IllegalArgumentException("租户ID不能为空");
|
||||
}
|
||||
return tenantConfigCache.computeIfAbsent(tenantId, this::loadConfigForTenant);
|
||||
}
|
||||
|
||||
private Map<Integer, Long> loadConfigForTenant(String tenantId) {
|
||||
// 显式使用租户ID作为查询条件,不依赖任何 ThreadLocal
|
||||
List<SysDictData> delays = dictDataMapper.selectList(
|
||||
new LambdaQueryWrapper<SysDictData>()
|
||||
.eq(SysDictData::getDictType, "alarm_stage_delay")
|
||||
.eq(SysDictData::getTenantId, tenantId) // 假设实体有 getTenantId 字段
|
||||
.orderByAsc(SysDictData::getDictSort)
|
||||
);
|
||||
Map<Integer, Long> config = new HashMap<>();
|
||||
for (SysDictData d : delays) {
|
||||
config.put(d.getDictSort(), Long.parseLong(d.getDictValue()));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,7 @@ import com.fuyuanshen.equipment.service.IDeviceGeoFenceService;
|
||||
import com.fuyuanshen.equipment.utils.map.AmapTrackUtil;
|
||||
import com.fuyuanshen.equipment.utils.map.GetAddressFromLatUtil;
|
||||
import com.fuyuanshen.equipment.utils.map.LngLonUtil;
|
||||
import com.fuyuanshen.global.Provider.AlarmDelayProvider;
|
||||
import com.fuyuanshen.global.mqtt.base.MqttMessageRule;
|
||||
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
|
||||
import com.fuyuanshen.global.mqtt.base.MqttXinghanJson;
|
||||
@ -93,6 +94,8 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
|
||||
private final IDeviceFenceAccessRecordService deviceFenceAccessRecordService;
|
||||
private final DeviceFenceAccessRecordMapper deviceFenceAccessRecordMapper;
|
||||
private final DeviceGeoFenceMapper deviceGeoFenceMapper;
|
||||
private final RedissonClient redissonClient;
|
||||
private final AlarmDelayProvider alarmDelayProvider; // 注入你实现的延迟提供者接口
|
||||
/** 位置未发生明显变化的距离阈值(米),可通过配置中心动态调整 */
|
||||
private final double MOVEMENT_THRESHOLD_METER = 10.0;
|
||||
|
||||
@ -103,7 +106,9 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
|
||||
// Latitude, longitude
|
||||
//主灯档位,激光灯档位,电量百分比,充电状态,电池剩余续航时间
|
||||
MqttXinghanJson deviceStatus = objectMapper.convertValue(context.getPayloadDict(), MqttXinghanJson.class);
|
||||
if (deviceStatus.getStaPowerPercent() != null) {
|
||||
deviceStatus.setBatteryPercentage(deviceStatus.getStaPowerPercent().toString());
|
||||
}
|
||||
// 发送设备状态和位置信息到Redis
|
||||
asyncSendDeviceDataToRedisWithFuture(context.getDeviceImei(),deviceStatus);
|
||||
RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.OK.getCode(), Duration.ofSeconds(20));
|
||||
@ -171,44 +176,46 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
|
||||
*/
|
||||
private void handleSingleAlarm(String deviceImei, boolean nowAlarming, AlarmTypeEnum type) {
|
||||
String redisKey = buildAlarmRedisKey(deviceImei, type);
|
||||
|
||||
Long alarmId = RedisUtils.getCacheObject(redisKey);
|
||||
|
||||
String lockKey = redisKey + ":lock"; // 分布式锁 key
|
||||
RedissonClient client = RedisUtils.getClient(); // 唯一用到的“旧”入口
|
||||
RLock lock = client.getLock(lockKey);
|
||||
|
||||
// ---------- 情况 1:当前正在报警 ----------
|
||||
if (nowAlarming) {
|
||||
// 已存在未结束报警 -> 什么都不做(同一条报警)
|
||||
if (alarmId != null) {
|
||||
// key 还在 -> 同一条报警,只续期
|
||||
RedisUtils.setCacheObject(redisKey, alarmId, Duration.ofMinutes(10));
|
||||
return;
|
||||
}
|
||||
// 需要新建,抢锁
|
||||
|
||||
String lockKey = redisKey + ":lock";
|
||||
RedissonClient client = RedisUtils.getClient();
|
||||
RLock lock = client.getLock(lockKey);
|
||||
boolean locked = false;
|
||||
try {
|
||||
locked = lock.tryLock(3, TimeUnit.SECONDS); // 最多等 3 s
|
||||
if (!locked) { // 抢不到直接放弃
|
||||
locked = lock.tryLock(3, TimeUnit.SECONDS);
|
||||
if (!locked) {
|
||||
log.warn("抢锁失败,放弃创建报警 device={}, type={}", deviceImei, type.getDesc());
|
||||
return;
|
||||
}
|
||||
// 锁内二次校验(double-check)
|
||||
|
||||
// 二次校验
|
||||
alarmId = RedisUtils.getCacheObject(redisKey);
|
||||
if (alarmId != null) {
|
||||
return; // 并发线程已建好
|
||||
return;
|
||||
}
|
||||
|
||||
// 不存在 -> 新建
|
||||
DeviceAlarmBo bo = createAlarmBo(deviceImei, type);
|
||||
if (bo == null){
|
||||
if (bo == null) {
|
||||
return;
|
||||
}
|
||||
deviceAlarmService.insertByBo(bo);
|
||||
RedisUtils.setCacheObject(redisKey, bo.getId(), Duration.ofMinutes(10)); // 5分钟后结束过期
|
||||
}catch (InterruptedException ignore) {
|
||||
// 立即中断并退出,禁止继续往下走
|
||||
|
||||
// 初始化阶段并投递第一个3分钟检查
|
||||
redissonClient.getBucket("alarm:stage:" + bo.getId()).set(0L, Duration.ofMinutes(10));
|
||||
alarmDelayProvider.sendDelayCheck(bo.getId(), 180); // 3分钟
|
||||
RedisUtils.setCacheObject(redisKey, bo.getId(), Duration.ofMinutes(10));
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("线程中断,放弃创建报警 device={}, type={}", deviceImei, type.getDesc());
|
||||
return; // 明确返回
|
||||
} finally {
|
||||
if (locked && lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
@ -217,11 +224,11 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- 情况 2:当前不报警 ----------
|
||||
// 不报警
|
||||
if (alarmId != null) {
|
||||
// 结束它
|
||||
finishAlarm(alarmId);
|
||||
RedisUtils.deleteObject(redisKey);
|
||||
log.info("设备[{}] {}警报已解除", deviceImei, type.getDesc());
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,6 +244,7 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
|
||||
bo.setFinishTime(new Date());
|
||||
bo.setDurationTime(DurationUtils.getDurationBetween(vo.getStartTime(), bo.getFinishTime()));
|
||||
bo.setTreatmentState(0); // 已处理
|
||||
bo.setAlarmState(1);
|
||||
deviceAlarmService.updateByBo(bo);
|
||||
}
|
||||
|
||||
@ -250,10 +258,12 @@ public class XinghanDeviceDataRule implements MqttMessageRule {
|
||||
}
|
||||
DeviceAlarmBo bo = new DeviceAlarmBo();
|
||||
bo.setDeviceId(device.getId());
|
||||
bo.setDeviceName(device.getDeviceName());
|
||||
bo.setDeviceImei(deviceImei);
|
||||
bo.setDeviceAction(2); // 自动报警
|
||||
bo.setStartTime(new Date());
|
||||
bo.setTreatmentState(1); // 未处理
|
||||
bo.setAlarmState(0);
|
||||
bo.setTenantId(device.getTenantId());
|
||||
|
||||
// 报警内容
|
||||
|
||||
@ -0,0 +1,318 @@
|
||||
package com.fuyuanshen.global.mqtt.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.aliyun.dyvmsapi20170525.models.SingleCallByTtsResponse;
|
||||
import com.fuyuanshen.common.core.utils.date.DurationUtils;
|
||||
import com.fuyuanshen.equipment.domain.Device;
|
||||
import com.fuyuanshen.equipment.domain.SmsSendRecord;
|
||||
import com.fuyuanshen.equipment.domain.bo.DeviceAlarmBo;
|
||||
import com.fuyuanshen.equipment.domain.vo.DeviceAlarmVo;
|
||||
import com.fuyuanshen.equipment.mapper.SmsSendRecordMapper;
|
||||
import com.fuyuanshen.equipment.service.DeviceService;
|
||||
import com.fuyuanshen.equipment.service.IDeviceAlarmService;
|
||||
import com.fuyuanshen.global.Provider.AlarmDelayProvider;
|
||||
import com.fuyuanshen.global.mqtt.config.AlarmStageConfig;
|
||||
import com.fuyuanshen.web.enums.NotificationSwitchEnum;
|
||||
import com.fuyuanshen.web.service.device.DeviceXinghanBizService;
|
||||
import com.fuyuanshen.web.util.AliyunVoiceUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.redisson.api.RAtomicLong;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AlarmCheckService {
|
||||
|
||||
private final IDeviceAlarmService deviceAlarmService;
|
||||
private final DeviceXinghanBizService appDeviceService;
|
||||
private final RedissonClient redissonClient;
|
||||
private final AlarmDelayProvider alarmDelayProvider;
|
||||
private final SmsSendRecordMapper smsSendRecordMapper;
|
||||
private final DeviceService deviceService;
|
||||
private final AlarmStageConfig stageConfig;
|
||||
private final AliyunVoiceUtil voiceUtil;
|
||||
|
||||
private static final String STAGE_KEY_PREFIX = "alarm:stage:";
|
||||
private static final String SMS_SENT_PREFIX = "alarm:sms:sent:";
|
||||
private static final String SMS_TEMPLATE_ID = "SMS_506445365";
|
||||
private static final String TTS_TEMPLATE_ID = "TTS_328730104";
|
||||
/**
|
||||
* 延迟队列消费者入口
|
||||
*/
|
||||
public void executeCheck(Long alarmId) {
|
||||
// 1. 加载报警记录,判断是否仍有效
|
||||
DeviceAlarmVo alarm = deviceAlarmService.queryById(alarmId);
|
||||
if (alarm == null || alarm.getTreatmentState() != 1 || alarm.getFinishTime() != null) {
|
||||
cleanStage(alarmId);
|
||||
log.info("报警[{}]已失效或已处理,流程终止", alarmId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 设备离线 → 自动结束报警
|
||||
if (appDeviceService.isDeviceOffline(alarm.getDeviceImei())) {
|
||||
finishAlarm(alarm);
|
||||
cleanStage(alarmId);
|
||||
log.info("设备[{}]离线,报警[{}]自动结束", alarm.getDeviceImei(), alarmId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 原子推进阶段
|
||||
int currentStage = advanceStage(alarmId);
|
||||
int totalStages = stageConfig.getTotalStages(alarm.getTenantId());
|
||||
log.info("报警[{}]进入第{}/{}阶段", alarmId, currentStage, totalStages);
|
||||
|
||||
// 超出总阶段数 → 清理并结束
|
||||
if (currentStage > totalStages) {
|
||||
cleanStage(alarmId);
|
||||
log.info("报警[{}]已完成全部{}个阶段", alarmId, totalStages);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 执行当前阶段的动作
|
||||
executeStageAction(currentStage, totalStages, alarm);
|
||||
|
||||
// 5. 如果不是最后一个阶段,则投递下一阶段延迟任务
|
||||
if (currentStage < totalStages) {
|
||||
long delaySeconds = stageConfig.getDelayByStage(alarm.getTenantId(), currentStage);
|
||||
scheduleNext(alarmId, currentStage + 1, delaySeconds);
|
||||
} else {
|
||||
// 最后一个阶段执行完毕,清理 Redis 标记
|
||||
cleanStage(alarmId);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 通用阶段动作:阶段1固定,中间阶段统一,最后阶段固定 ----------
|
||||
/**
|
||||
* 阶段动作规则:
|
||||
* - 阶段1:仅升级报警等级(如升至2)
|
||||
* - 中间阶段(2 <= stage < totalStages):升级等级 + 发送短信
|
||||
* - 最后阶段(stage == totalStages):仅发送短信(不再升级,并结束流程)
|
||||
*/
|
||||
private void executeStageAction(int stage, int totalStages, DeviceAlarmVo alarm) {
|
||||
boolean isFirst = (stage == 1);
|
||||
boolean isLast = (stage == totalStages);
|
||||
boolean isMiddle = (!isFirst && !isLast);
|
||||
|
||||
// 升级等级(阶段1和中间阶段升级)
|
||||
if (isFirst || isMiddle) {
|
||||
// 等级递增:阶段1升到2,阶段2升到3,阶段3升到4...
|
||||
int newLevel = stage + 1; // 因为 stage 从1开始,newLevel = stage+1
|
||||
updateAlarmLevel(alarm, newLevel);
|
||||
log.info("报警[{}]等级上升至{}", alarm.getId(), newLevel);
|
||||
}
|
||||
|
||||
// 发送短信(中间阶段和最后阶段发短信)
|
||||
if (isMiddle || isLast) {
|
||||
if (sendSmsIfNeeded(alarm, stage)) {
|
||||
log.info("报警[{}]第{}阶段短信已发送", alarm.getId(), stage);
|
||||
}
|
||||
}
|
||||
|
||||
if (isLast) {
|
||||
log.info("报警[{}]最后阶段执行完毕", alarm.getId());
|
||||
}
|
||||
}
|
||||
// ---------- 辅助方法 ----------
|
||||
|
||||
/**
|
||||
* 原子推进阶段:Redis 中 key = alarm:stage:{alarmId} 的值 +1,返回新值。
|
||||
* 如果 key 不存在则初始化为 1。使用 Lua 保证原子并返回推进后的值。
|
||||
*/
|
||||
private int advanceStage(Long alarmId) {
|
||||
String key = STAGE_KEY_PREFIX + alarmId;
|
||||
RAtomicLong stageCounter = redissonClient.getAtomicLong(key);
|
||||
// 第一次调用:key 不存在 -> 视为0,incrementAndGet 返回 1
|
||||
// 后续调用:依次返回 2, 3, 4...
|
||||
long newStage = stageCounter.incrementAndGet();
|
||||
return (int) newStage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅当该阶段未发送过短信时执行发送,并记录已发送标记。
|
||||
*/
|
||||
private boolean sendSmsIfNeeded(DeviceAlarmVo alarm, int stage) {
|
||||
String sentKey = SMS_SENT_PREFIX + alarm.getId() + ":stage" + stage;
|
||||
boolean success = redissonClient.getBucket(sentKey).setIfAbsent("1", java.time.Duration.ofHours(2));
|
||||
if (!success) {
|
||||
log.debug("报警[{}]阶段{}短信已发送过,跳过", alarm.getId(), stage);
|
||||
return false;
|
||||
}
|
||||
sendTemplateSms(alarm, stage);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 投递下一个延迟检查任务
|
||||
*/
|
||||
private void scheduleNext(Long alarmId, int nextStage, long delaySeconds) {
|
||||
// 投递下一个延迟检查任务(队列中依旧是 alarmId)
|
||||
alarmDelayProvider.sendDelayCheck(alarmId, delaySeconds);
|
||||
log.debug("报警[{}]已投递阶段{}延时{}秒", alarmId, nextStage, delaySeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送模板短信(自动重试到备用联系人)
|
||||
*/
|
||||
private void sendTemplateSms(DeviceAlarmVo alarm, int stage) {
|
||||
Device device = deviceService.selectDeviceByImei(alarm.getDeviceImei());
|
||||
if (device == null) return;
|
||||
|
||||
// 把数据库的数字转成枚举
|
||||
NotificationSwitchEnum notifyStatus = NotificationSwitchEnum.getByCode(device.getNotificationEnabled());
|
||||
// 判断是否关闭(0=关闭)
|
||||
if (notifyStatus.isClosed()) {
|
||||
log.info("设备{}已关闭通知,跳过发送", device.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
String primaryPhone = device.getContact1Phone();
|
||||
String backupPhone = device.getContact2Phone();
|
||||
|
||||
// 下面你可以分别判断短信/语音
|
||||
if (notifyStatus.hasSms()) {
|
||||
// 先尝试发送给主联系人
|
||||
boolean primarySuccess = sendSmsToPhone(primaryPhone, device, alarm, stage);
|
||||
if (!primarySuccess && backupPhone != null && !backupPhone.isEmpty()) {
|
||||
// 主联系人失败且有备用联系人,则发送给备用联系人
|
||||
sendSmsToPhone(backupPhone, device, alarm, stage);
|
||||
}
|
||||
}
|
||||
|
||||
if (notifyStatus.hasVoice()) {
|
||||
// 发送语音逻辑
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定手机号发送短信并记录发送结果
|
||||
* @return true-发送成功,false-发送失败
|
||||
*/
|
||||
private boolean sendSmsToPhone(String phone, Device device, DeviceAlarmVo alarm, int stage) {
|
||||
if (phone == null || phone.isEmpty()) {
|
||||
log.warn("手机号为空,无法发送报警[{}]的短信", alarm.getId());
|
||||
return false;
|
||||
}
|
||||
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
|
||||
// 预创建待发送记录
|
||||
SmsSendRecord record = new SmsSendRecord();
|
||||
record.setAlarmId(alarm.getId());
|
||||
record.setDeviceImei(alarm.getDeviceImei());
|
||||
record.setNotifyType("SMS");
|
||||
record.setPhone(phone);
|
||||
record.setContent("报警阶段" + stage);
|
||||
record.setTemplateId(SMS_TEMPLATE_ID);
|
||||
record.setStatus(0L); // 待发送
|
||||
record.setCreateTime(new Date());
|
||||
record.setTenantId(alarm.getTenantId());
|
||||
record.setCreateTime(new Date());
|
||||
smsSendRecordMapper.insert(record);
|
||||
|
||||
boolean success = false;
|
||||
String responseMsg = null;
|
||||
try {
|
||||
String location = alarm.getLocation() != null ? alarm.getLocation() : "";
|
||||
LinkedHashMap<String, String> vars = new LinkedHashMap<>();
|
||||
vars.put("name", device.getDeviceName());
|
||||
vars.put("address", String.format("%tT开始持续报警(第%d阶段)%s", alarm.getStartTime(), stage, location));
|
||||
vars.put("msg", device.getSosSmsMsg());
|
||||
SmsResponse response = smsBlend.sendMessage(phone, SMS_TEMPLATE_ID, vars);
|
||||
success = response.isSuccess();
|
||||
responseMsg = success ? "成功" : "失败" + response.getData();
|
||||
} catch (Exception e) {
|
||||
responseMsg = "异常:" + e.getMessage();
|
||||
log.error("短信发送异常,手机号:{}", phone, e);
|
||||
}
|
||||
|
||||
// 更新发送记录
|
||||
record.setStatus(success ? 1L : 2L);
|
||||
record.setResponseMsg(responseMsg);
|
||||
record.setSendTime(new Date());
|
||||
smsSendRecordMapper.updateById(record);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定手机号发送语音并记录发送结果
|
||||
* @return true-发送成功,false-发送失败
|
||||
*/
|
||||
private boolean sendVoiceToPhone(String phone, Device device, DeviceAlarmVo alarm, int stage) {
|
||||
if (phone == null || phone.isEmpty()) {
|
||||
log.warn("手机号为空,无法发送报警[{}]的语音", alarm.getId());
|
||||
return false;
|
||||
}
|
||||
// 预创建待发送记录
|
||||
SmsSendRecord record = new SmsSendRecord();
|
||||
record.setAlarmId(alarm.getId());
|
||||
record.setDeviceImei(alarm.getDeviceImei());
|
||||
record.setNotifyType("VOICE");
|
||||
record.setPhone(phone);
|
||||
record.setContent("报警阶段" + stage);
|
||||
record.setTemplateId(TTS_TEMPLATE_ID);
|
||||
record.setStatus(0L); // 待发送
|
||||
record.setCreateTime(new Date());
|
||||
record.setTenantId(alarm.getTenantId());
|
||||
record.setCreateTime(new Date());
|
||||
smsSendRecordMapper.insert(record);
|
||||
|
||||
boolean success = false;
|
||||
String responseMsg = null;
|
||||
try {
|
||||
Map<String, String> params = Map.of("device", alarm.getDeviceName());
|
||||
SingleCallByTtsResponse response = voiceUtil.sendTtsSync(phone, TTS_TEMPLATE_ID, params);
|
||||
success = "OK".equalsIgnoreCase(response.getBody().getCode());
|
||||
responseMsg = success ? "成功" : "失败" + response.getBody().getMessage();
|
||||
} catch (Exception e) {
|
||||
responseMsg = "异常:" + e.getMessage();
|
||||
log.error("语音发送异常,手机号:{}", phone, e);
|
||||
}
|
||||
|
||||
// 更新发送记录
|
||||
record.setStatus(success ? 1L : 2L);
|
||||
record.setResponseMsg(responseMsg);
|
||||
record.setSendTime(new Date());
|
||||
smsSendRecordMapper.updateById(record);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新报警等级
|
||||
*/
|
||||
private void updateAlarmLevel(DeviceAlarmVo alarm, int level) {
|
||||
// 调用 deviceAlarmService 更新等级字段
|
||||
log.info("报警[{}]等级已更新为{}", alarm.getId(), level);
|
||||
if (alarm == null || alarm.getTreatmentState() == 0) return;
|
||||
DeviceAlarmBo bo = BeanUtil.toBean(alarm, DeviceAlarmBo.class);
|
||||
bo.setAlarmLevel(level);
|
||||
deviceAlarmService.updateByBo(bo);
|
||||
}
|
||||
|
||||
private void cleanStage(Long alarmId) {
|
||||
redissonClient.getBucket(STAGE_KEY_PREFIX + alarmId).deleteAsync();
|
||||
}
|
||||
|
||||
private void finishAlarm(DeviceAlarmVo alarm) {
|
||||
if (alarm == null || alarm.getTreatmentState() == 0) return;
|
||||
DeviceAlarmBo bo = BeanUtil.toBean(alarm, DeviceAlarmBo.class);
|
||||
bo.setFinishTime(new Date());
|
||||
bo.setDurationTime(DurationUtils.getDurationBetween(alarm.getStartTime(), bo.getFinishTime()));
|
||||
bo.setTreatmentState(0); // 已处理
|
||||
bo.setAlarmState(1);
|
||||
deviceAlarmService.updateByBo(bo);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,262 @@
|
||||
package com.fuyuanshen.global.mqtt.utils;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 生产环境 - 楼层计算工具(优化版)
|
||||
* 核心改进:直接使用Open-Meteo返回的surface_pressure地面气压
|
||||
* 无需再用海拔反推,简化计算且精度更高
|
||||
*/
|
||||
public class FloorCalculateUtil {
|
||||
|
||||
private static final double FLOOR_HEIGHT = 3.0;
|
||||
private static final int HTTP_TIMEOUT = 3000;
|
||||
|
||||
// API地址:同时获取地面气压和海平面气压
|
||||
private static final String WEATHER_API =
|
||||
"https://api.open-meteo.com/v1/forecast?current=pressure_msl,surface_pressure&latitude=%s&longitude=%s";
|
||||
|
||||
private static final Map<String, LocationBaseline> BASELINE_CACHE = new ConcurrentHashMap<>();
|
||||
private static final long CACHE_EXPIRE_TIME_MS = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* 内部类:存储位置的基准数据
|
||||
*/
|
||||
private static class LocationBaseline {
|
||||
double groundPressure; // 地面真实气压(hPa) - 直接来自API的surface_pressure
|
||||
double seaPressure; // 海平面气压(hPa) - 保留备用
|
||||
double elevation; // 海拔高度(米) - 新增
|
||||
long updateTime;
|
||||
|
||||
LocationBaseline(double groundPressure, double seaPressure, double elevation) {
|
||||
this.groundPressure = groundPressure;
|
||||
this.seaPressure = seaPressure;
|
||||
this.elevation = elevation;
|
||||
this.updateTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return (System.currentTimeMillis() - this.updateTime) > CACHE_EXPIRE_TIME_MS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 楼层计算结果类(包含海拔信息)
|
||||
*/
|
||||
public static class FloorResult {
|
||||
public final int floor; // 楼层号
|
||||
public final double elevation; // 海拔高度(米)
|
||||
public final double relativeHeight; // 相对地面高度(米)
|
||||
|
||||
public FloorResult(int floor, double elevation, double relativeHeight) {
|
||||
this.floor = floor;
|
||||
this.elevation = elevation;
|
||||
this.relativeHeight = relativeHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("楼层=%d, 海拔=%.1f米, 相对高度=%.1f米",
|
||||
floor, elevation, relativeHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前楼层(带海拔输出)
|
||||
* @param pressureList 设备采集的气压值列表(单位:hPa)
|
||||
* @param latitude 纬度
|
||||
* @param longitude 经度
|
||||
* @return FloorResult 包含楼层、海拔、相对高度的结果对象
|
||||
*/
|
||||
public static FloorResult getCurrentFloorWithElevation(List<Double> pressureList, double latitude, double longitude) {
|
||||
try {
|
||||
// 1. 计算设备采集的实时平均气压
|
||||
double avgPressure = getAndCheckAveragePressure(pressureList);
|
||||
|
||||
// 2. 硬件校准:扣除设备固有的硬件正偏差
|
||||
double calibratedPressure = avgPressure - 2.1;
|
||||
|
||||
// 3. 从API获取该位置的地面真实气压和海拔
|
||||
LocationBaseline baseline = getBaselineWithCache(latitude, longitude);
|
||||
double groundPressure = baseline.groundPressure;
|
||||
double elevation = baseline.elevation; // 获取海拔
|
||||
|
||||
System.out.printf("设备气压: %.2f hPa | 校准后: %.2f hPa | 地面气压: %.2f hPa | 海拔: %.1f米%n",
|
||||
avgPressure, calibratedPressure, groundPressure, elevation);
|
||||
|
||||
// 4. 核心计算:根据气压差计算相对高度
|
||||
double relativeHeight = 19411.7 * Math.log10(groundPressure / calibratedPressure);
|
||||
|
||||
System.out.printf("相对高度:%.2f 米%n", relativeHeight);
|
||||
|
||||
// 5. 将相对高度转换为楼层
|
||||
int floor = convertHeightToFloor(relativeHeight);
|
||||
|
||||
// 6. 返回包含海拔的结果
|
||||
return new FloorResult(floor, elevation, relativeHeight);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println(String.format("楼层计算异常,坐标:%.4f,%.4f,错误:%s",
|
||||
latitude, longitude, e.getMessage()));
|
||||
return new FloorResult(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧版本:只返回楼层号
|
||||
*/
|
||||
public static int getCurrentFloor(List<Double> pressureList, double latitude, double longitude) {
|
||||
return getCurrentFloorWithElevation(pressureList, latitude, longitude).floor;
|
||||
}
|
||||
|
||||
private static double getAndCheckAveragePressure(List<Double> pressureList) {
|
||||
if (pressureList == null || pressureList.isEmpty()) {
|
||||
throw new IllegalArgumentException("气压采样数据不能为空");
|
||||
}
|
||||
return pressureList.stream()
|
||||
.filter(p -> p > 800 && p < 1100)
|
||||
.mapToDouble(Double::doubleValue)
|
||||
.average()
|
||||
.orElseThrow(() -> new IllegalArgumentException("气压数据无效"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基准数据(带本地缓存)- 现在包含海拔
|
||||
*/
|
||||
private static LocationBaseline getBaselineWithCache(double lat, double lng) throws Exception {
|
||||
String cacheKey = String.format("%.6f,%.6f", lat, lng);
|
||||
LocationBaseline cached = BASELINE_CACHE.get(cacheKey);
|
||||
|
||||
if (cached != null && !cached.isExpired()) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 请求API获取数据
|
||||
WeatherData weatherData = requestWeatherData(lat, lng);
|
||||
|
||||
LocationBaseline newBaseline = new LocationBaseline(
|
||||
weatherData.surfacePressure,
|
||||
weatherData.pressureMsl,
|
||||
weatherData.elevation // 新增海拔参数
|
||||
);
|
||||
BASELINE_CACHE.put(cacheKey, newBaseline);
|
||||
return newBaseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求 Open-Meteo API
|
||||
*/
|
||||
private static WeatherData requestWeatherData(double lat, double lng) throws Exception {
|
||||
String apiUrl = String.format(WEATHER_API, lat, lng);
|
||||
String json = executeHttpGet(apiUrl);
|
||||
|
||||
// 解析JSON - elevation 在根对象,不在 current 块内
|
||||
double elevation = parseJsonValue(json, "\"elevation\":", null);
|
||||
double pressureMsl = parseJsonValue(json, "\"pressure_msl\":", "\"current\":");
|
||||
double surfacePressure = parseJsonValue(json, "\"surface_pressure\":", "\"current\":");
|
||||
|
||||
System.out.printf("API返回: 海拔=%.1f米, 海平面气压=%.1f hPa, 地面气压=%.1f hPa%n",
|
||||
elevation, pressureMsl, surfacePressure);
|
||||
|
||||
return new WeatherData(pressureMsl, surfacePressure, elevation);
|
||||
}
|
||||
|
||||
private static double parseJsonValue(String json, String targetKey, String blockStartKey) {
|
||||
int start;
|
||||
|
||||
if (blockStartKey != null && json.contains(blockStartKey)) {
|
||||
int blockStart = json.indexOf(blockStartKey);
|
||||
start = json.indexOf(targetKey, blockStart);
|
||||
} else {
|
||||
start = json.indexOf(targetKey);
|
||||
}
|
||||
|
||||
if (start == -1) {
|
||||
throw new RuntimeException("解析JSON失败,未找到键: " + targetKey);
|
||||
}
|
||||
|
||||
start += targetKey.length();
|
||||
int end = json.indexOf(",", start);
|
||||
if (end == -1) end = json.indexOf("}", start);
|
||||
if (end == -1) end = json.indexOf("]", start);
|
||||
|
||||
String rawVal = json.substring(start, end);
|
||||
String cleanVal = rawVal.replaceAll("[^0-9.\\-]", "").trim();
|
||||
|
||||
if (cleanVal.isEmpty()) {
|
||||
throw new RuntimeException("清洗JSON数值后结果为空,原始片段: " + rawVal);
|
||||
}
|
||||
|
||||
return Double.parseDouble(cleanVal);
|
||||
}
|
||||
|
||||
private static String executeHttpGet(String apiUrl) throws Exception {
|
||||
HttpURLConnection conn = null;
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
URL url = new URL(apiUrl);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(HTTP_TIMEOUT);
|
||||
conn.setReadTimeout(HTTP_TIMEOUT);
|
||||
conn.setRequestProperty("User-Agent", "FloorCalculateUtil/1.0");
|
||||
|
||||
reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
return response.toString();
|
||||
} finally {
|
||||
if (reader != null) reader.close();
|
||||
if (conn != null) conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private static int convertHeightToFloor(double realHeight) {
|
||||
if (realHeight < -0.5) {
|
||||
return (int) Math.floor(realHeight / FLOOR_HEIGHT);
|
||||
}
|
||||
int floor = (int) Math.round(realHeight / FLOOR_HEIGHT);
|
||||
return Math.max(floor, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部数据类 - 新增 elevation 字段
|
||||
*/
|
||||
private static class WeatherData {
|
||||
double pressureMsl;
|
||||
double surfacePressure;
|
||||
double elevation; // 新增
|
||||
|
||||
WeatherData(double pressureMsl, double surfacePressure, double elevation) {
|
||||
this.pressureMsl = pressureMsl;
|
||||
this.surfacePressure = surfacePressure;
|
||||
this.elevation = elevation;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 测试验证 ======================
|
||||
public static void main(String[] args) {
|
||||
// 测试数据
|
||||
List<Double> pressureList = List.of(1001.591, 1001.593, 1001.584, 1001.589, 1001.591);
|
||||
double lat = 30.490020;
|
||||
double lng = 114.415369;
|
||||
|
||||
// 新方法:获取包含海拔的完整结果
|
||||
FloorResult result = getCurrentFloorWithElevation(pressureList, lat, lng);
|
||||
System.out.println("【优化版】计算结果:" + result);
|
||||
|
||||
// 也可以单独获取海拔
|
||||
System.out.println("海拔高度:" + result.elevation + " 米");
|
||||
System.out.println("相对高度:" + result.relativeHeight + " 米");
|
||||
System.out.println("楼层:" + result.floor);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
package com.fuyuanshen.global.queue;
|
||||
|
||||
import com.fuyuanshen.global.Provider.RedissonAlarmDelayProvider;
|
||||
import com.fuyuanshen.global.mqtt.service.AlarmCheckService;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RBlockingQueue;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class RedissonAlarmConsumer implements CommandLineRunner {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
private final AlarmCheckService alarmCheckService;
|
||||
|
||||
private volatile boolean running = true;
|
||||
private Thread consumerThread;
|
||||
private ExecutorService bizExecutor;
|
||||
private static final int BIZ_THREADS = 4; // 业务处理线程数
|
||||
private static final int BIZ_QUEUE_CAPACITY = 200; // 有界队列容量
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
// 初始化业务处理线程池(有界队列 + 调用者运行拒绝策略,避免 OOM)
|
||||
bizExecutor = Executors.newFixedThreadPool(
|
||||
BIZ_THREADS,
|
||||
new ThreadFactory() {
|
||||
private final AtomicInteger counter = new AtomicInteger(1);
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
Thread t = new Thread(r, "Alarm-Biz-" + counter.getAndIncrement());
|
||||
t.setDaemon(false);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 启动消费线程
|
||||
consumerThread = new Thread(() -> {
|
||||
RBlockingQueue<Long> blockingQueue = redissonClient.getBlockingQueue(RedissonAlarmDelayProvider.QUEUE_NAME);
|
||||
log.info("Redisson 延迟报警监听线程已启动...");
|
||||
while (running && !Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
Long alarmId = blockingQueue.poll(1, TimeUnit.SECONDS); // 改用带超时的 poll,可响应中断
|
||||
if (alarmId != null) {
|
||||
// 提交到业务线程池异步处理,避免阻塞队列拉取
|
||||
bizExecutor.submit(() -> processAlarm(alarmId));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.info("Redisson 消费线程被中断,退出循环");
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
log.error("Redisson 延迟队列消费异常", e);
|
||||
// 发生非中断异常时短暂休眠,避免日志风暴
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(1);
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Redisson 延迟报警消费线程结束");
|
||||
}, "Alarm-Consumer-Thread");
|
||||
consumerThread.setDaemon(false);
|
||||
consumerThread.start();
|
||||
}
|
||||
|
||||
private void processAlarm(Long alarmId) {
|
||||
try {
|
||||
alarmCheckService.executeCheck(alarmId);
|
||||
} catch (Exception e) {
|
||||
log.error("处理报警 ID [{}] 时发生异常", alarmId, e);
|
||||
// 可在此补充重试或死信逻辑
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
log.info("开始关闭 Redisson 报警消费者...");
|
||||
running = false;
|
||||
if (consumerThread != null) {
|
||||
consumerThread.interrupt(); // 中断阻塞在 poll 上的线程
|
||||
}
|
||||
if (bizExecutor != null) {
|
||||
bizExecutor.shutdown();
|
||||
try {
|
||||
if (!bizExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
|
||||
bizExecutor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
bizExecutor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
log.info("Redisson 报警消费者已关闭");
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import com.fuyuanshen.common.ratelimiter.annotation.FunctionAccessAnnotation;
|
||||
import com.fuyuanshen.common.web.core.BaseController;
|
||||
import com.fuyuanshen.equipment.domain.query.DeviceQueryCriteria;
|
||||
import com.fuyuanshen.equipment.domain.vo.WebDeviceVo;
|
||||
import com.fuyuanshen.equipment.enums.DataSourceEnum;
|
||||
import com.fuyuanshen.web.domain.Dto.DeviceDebugEditDto;
|
||||
import com.fuyuanshen.web.domain.Dto.DeviceDebugLogoUploadDto;
|
||||
import com.fuyuanshen.web.domain.vo.DeviceInfoVo;
|
||||
@ -79,7 +80,7 @@ public class DeviceDebugController extends BaseController {
|
||||
if(file.getSize()>1024*1024*2){
|
||||
return R.warn("图片不能大于2M");
|
||||
}
|
||||
deviceXinghanBizService.uploadDeviceLogoBatch(bo);
|
||||
deviceXinghanBizService.uploadDeviceLogoBatch(bo, DataSourceEnum.Web);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import com.fuyuanshen.common.ratelimiter.annotation.FunctionAccessBatcAnnotation
|
||||
import com.fuyuanshen.common.redis.utils.RedisUtils;
|
||||
import com.fuyuanshen.common.web.core.BaseController;
|
||||
import com.fuyuanshen.equipment.domain.dto.AppDeviceSendMsgBo;
|
||||
import com.fuyuanshen.equipment.enums.DataSourceEnum;
|
||||
import com.fuyuanshen.web.domain.Dto.DeviceXinghanInstructDto;
|
||||
import com.fuyuanshen.web.domain.Dto.SystemVersionDto;
|
||||
import com.fuyuanshen.web.domain.vo.DeviceXinghanDetailVo;
|
||||
@ -60,7 +61,7 @@ public class DeviceXinghanController extends BaseController {
|
||||
@PostMapping(value = "/registerPersonInfo")
|
||||
// @FunctionAccessAnnotation("registerPersonInfo")
|
||||
public R<Void> registerPersonInfo(@Validated(AddGroup.class) @RequestBody AppPersonnelInfoBo bo) {
|
||||
return toAjax(deviceXinghanBizService.registerPersonInfo(bo));
|
||||
return toAjax(deviceXinghanBizService.registerPersonInfo(bo, DataSourceEnum.Web));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,7 +70,7 @@ public class DeviceXinghanController extends BaseController {
|
||||
@PostMapping(value = "/sendAlarmMessage")
|
||||
@FunctionAccessBatcAnnotation(value = "sendAlarmMessage", timeOut = 5, batchMaxTimeOut = 10)
|
||||
public R<Void> sendAlarmMessage(@RequestBody AppDeviceSendMsgBo bo) {
|
||||
return toAjax(deviceXinghanBizService.sendAlarmMessage(bo));
|
||||
return toAjax(deviceXinghanBizService.sendAlarmMessage(bo, DataSourceEnum.Web));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,7 +84,7 @@ public class DeviceXinghanController extends BaseController {
|
||||
if(file.getSize()>1024*1024*2){
|
||||
return R.warn("图片不能大于2M");
|
||||
}
|
||||
deviceXinghanBizService.uploadDeviceLogo(bo);
|
||||
deviceXinghanBizService.uploadDeviceLogo(bo, DataSourceEnum.Web);
|
||||
|
||||
return R.ok();
|
||||
}
|
||||
@ -95,7 +96,7 @@ public class DeviceXinghanController extends BaseController {
|
||||
@PostMapping("/DetectGradeSettings")
|
||||
public R<Void> DetectGradeSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
deviceXinghanBizService.upDetectGradeSettings(params);
|
||||
deviceXinghanBizService.upDetectGradeSettings(params, DataSourceEnum.Web);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ -106,7 +107,7 @@ public class DeviceXinghanController extends BaseController {
|
||||
@PostMapping("/LightGradeSettings")
|
||||
public R<Void> LightGradeSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
deviceXinghanBizService.upLightGradeSettings(params);
|
||||
deviceXinghanBizService.upLightGradeSettings(params, DataSourceEnum.Web);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ -117,7 +118,7 @@ public class DeviceXinghanController extends BaseController {
|
||||
@PostMapping("/SOSGradeSettings")
|
||||
public R<Void> SOSGradeSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
deviceXinghanBizService.upSOSGradeSettings(params);
|
||||
deviceXinghanBizService.upSOSGradeSettings(params, DataSourceEnum.Web);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ -128,7 +129,7 @@ public class DeviceXinghanController extends BaseController {
|
||||
@PostMapping("/SOSGradeSettingsBatch")
|
||||
public R<Void> SOSGradeSettingsBatch(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
deviceXinghanBizService.sendCommandBatch(params,"ins_SOSGrade","SOS档位");
|
||||
deviceXinghanBizService.sendCommandBatch(params,"ins_SOSGrade","SOS档位", DataSourceEnum.Web);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ -139,7 +140,7 @@ public class DeviceXinghanController extends BaseController {
|
||||
@PostMapping("/ShakeBitSettings")
|
||||
public R<Void> ShakeBitSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
deviceXinghanBizService.upShakeBitSettings(params);
|
||||
deviceXinghanBizService.upShakeBitSettings(params, DataSourceEnum.Web);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
package com.fuyuanshen.web.controller.device.xinghan;
|
||||
|
||||
|
||||
import com.fuyuanshen.common.core.domain.R;
|
||||
import com.fuyuanshen.common.log.annotation.Log;
|
||||
import com.fuyuanshen.common.web.core.BaseController;
|
||||
import com.fuyuanshen.equipment.domain.bo.DeviceContactPhoneBo;
|
||||
import com.fuyuanshen.equipment.domain.dto.AppDeviceSendMsgBo;
|
||||
import com.fuyuanshen.equipment.enums.DataSourceEnum;
|
||||
import com.fuyuanshen.web.domain.Dto.DeviceXinghanInstructDto;
|
||||
import com.fuyuanshen.web.domain.vo.DeviceXinghanDetailVo;
|
||||
import com.fuyuanshen.web.service.device.DeviceXinghanBizService;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 设备控制类 HBY018A
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/hby018a/device")
|
||||
public class WebHBY018AController extends BaseController {
|
||||
|
||||
private final DeviceXinghanBizService deviceXinghanBizService;
|
||||
|
||||
/**
|
||||
* 获取设备详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public R<DeviceXinghanDetailVo> getInfo(@NotNull(message = "主键不能为空")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(deviceXinghanBizService.getInfo(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 照明档位
|
||||
* 照明档位,2,1,0,分别表示弱光/强光/关闭
|
||||
*/
|
||||
@PostMapping("/SideLightSettings")
|
||||
public R<Void> SideLightSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
deviceXinghanBizService.upSideLightSettings(params, DataSourceEnum.Web);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制报警状态
|
||||
* 强制报警状态,0-未报警,1-正在报警。
|
||||
*/
|
||||
@PostMapping("/ShakeBitSettings")
|
||||
public R<Void> ShakeBitSettings(@RequestBody DeviceXinghanInstructDto params) {
|
||||
// params 转 JSONObject
|
||||
deviceXinghanBizService.upShakeBitSettings(params, DataSourceEnum.Web);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义语音消息
|
||||
*/
|
||||
@PostMapping("/SetVoiceMsg")
|
||||
public R<Void> editSosVoiceMsg(@RequestBody AppDeviceSendMsgBo bo) {
|
||||
return toAjax(deviceXinghanBizService.sendAlarmMessage(bo, DataSourceEnum.Web));
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义短信消息
|
||||
*/
|
||||
@PostMapping("/SetSmsMsg")
|
||||
public R<Void> editSosSmsMsg(@RequestBody AppDeviceSendMsgBo bo) {
|
||||
return toAjax(deviceXinghanBizService.editSosSmsMsg(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置消息通知开关
|
||||
*/
|
||||
@PostMapping("/SetNotificationEnabled")
|
||||
public R<Void> editNotificationEnabled(@RequestBody DeviceContactPhoneBo bo) {
|
||||
return toAjax(deviceXinghanBizService.editNotificationEnabled(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加设备紧急联系人
|
||||
*/
|
||||
@PostMapping("/SetContactPhone")
|
||||
public R<Void> editContactPhone(@RequestBody DeviceContactPhoneBo bo) {
|
||||
return toAjax(deviceXinghanBizService.editContactPhone(bo));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -100,4 +100,21 @@ public class DeviceXinghanDetailVo {
|
||||
* 第六键值对, 近电预警级别, 0-无预警,1-弱预警,2-中预警,3-强预警,4-非常强预警。
|
||||
*/
|
||||
public Integer staDetectResult;
|
||||
|
||||
/**
|
||||
* 联系人1手机
|
||||
*/
|
||||
private String contact1Phone;
|
||||
/**
|
||||
* 联系人2手机
|
||||
*/
|
||||
private String contact2Phone;
|
||||
/**
|
||||
* 自定义语音文本
|
||||
*/
|
||||
private String sosVoiceMsg;
|
||||
/**
|
||||
* 自定义短信文本
|
||||
*/
|
||||
private String sosSmsMsg;
|
||||
}
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
package com.fuyuanshen.web.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 设备通知开关枚举
|
||||
* 0=关闭 1=短信 2=语音 3=全开
|
||||
*/
|
||||
@Getter
|
||||
public enum NotificationSwitchEnum {
|
||||
|
||||
CLOSED(0, "关闭通知"),
|
||||
SMS_ONLY(1, "仅短信"),
|
||||
VOICE_ONLY(2, "仅语音"),
|
||||
SMS_VOICE(3, "短信+语音全开");
|
||||
|
||||
private final Integer code;
|
||||
private final String desc;
|
||||
|
||||
NotificationSwitchEnum(Integer code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code获取枚举
|
||||
*/
|
||||
public static NotificationSwitchEnum getByCode(Integer code) {
|
||||
if (code == null) {
|
||||
return CLOSED; // 空值默认关闭
|
||||
}
|
||||
for (NotificationSwitchEnum status : values()) {
|
||||
if (status.getCode().equals(code)) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return CLOSED; // 非法值默认关闭
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否完全关闭通知
|
||||
*/
|
||||
public boolean isClosed() {
|
||||
return this == CLOSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否开启短信
|
||||
*/
|
||||
public boolean hasSms() {
|
||||
return this == SMS_ONLY || this == SMS_VOICE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否开启语音
|
||||
*/
|
||||
public boolean hasVoice() {
|
||||
return this == VOICE_ONLY || this == SMS_VOICE;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import cn.hutool.core.lang.UUID;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.aliyun.dyvmsapi20170525.models.SingleCallByTtsResponse;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
@ -18,8 +19,9 @@ import com.fuyuanshen.app.domain.dto.AppDeviceLogoUploadDto;
|
||||
import com.fuyuanshen.app.domain.vo.AppPersonnelInfoVo;
|
||||
import com.fuyuanshen.app.mapper.AppPersonnelInfoMapper;
|
||||
import com.fuyuanshen.app.mapper.AppPersonnelInfoRecordsMapper;
|
||||
import com.fuyuanshen.equipment.service.IAppBusinessFileService;
|
||||
import com.fuyuanshen.equipment.service.IAppOperationVideoService;
|
||||
import com.fuyuanshen.equipment.domain.bo.DeviceContactPhoneBo;
|
||||
import com.fuyuanshen.equipment.enums.DataSourceEnum;
|
||||
import com.fuyuanshen.equipment.service.*;
|
||||
import com.fuyuanshen.common.core.constant.GlobalConstants;
|
||||
import com.fuyuanshen.common.core.domain.model.AppLoginUser;
|
||||
import com.fuyuanshen.common.core.exception.BadRequestException;
|
||||
@ -41,8 +43,6 @@ import com.fuyuanshen.equipment.mapper.DeviceLogMapper;
|
||||
import com.fuyuanshen.equipment.mapper.DeviceMapper;
|
||||
import com.fuyuanshen.equipment.mapper.DeviceTypeGrantsMapper;
|
||||
import com.fuyuanshen.equipment.mapper.DeviceTypeMapper;
|
||||
import com.fuyuanshen.equipment.service.DeviceAssignmentsService;
|
||||
import com.fuyuanshen.equipment.service.IDeviceAlarmService;
|
||||
import com.fuyuanshen.global.mqtt.base.MqttXinghanJson;
|
||||
import com.fuyuanshen.global.mqtt.config.MqttGateway;
|
||||
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
|
||||
@ -64,6 +64,7 @@ import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
|
||||
import static com.fuyuanshen.common.core.utils.ImageToCArrayConverter.convertHexToDecimal;
|
||||
@ -89,6 +90,7 @@ public class DeviceXinghanBizService {
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
private final AliyunVoiceUtil voiceUtil;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
/**
|
||||
* 所有档位的描述表
|
||||
@ -114,21 +116,28 @@ public class DeviceXinghanBizService {
|
||||
/**
|
||||
* 设置静电预警档位
|
||||
*/
|
||||
public void upDetectGradeSettings(DeviceXinghanInstructDto dto) {
|
||||
sendCommand(dto, "ins_DetectGrade","静电预警档位");
|
||||
public void upDetectGradeSettings(DeviceXinghanInstructDto dto, DataSourceEnum sourceEnum) {
|
||||
sendCommand(dto, "ins_DetectGrade","静电预警档位", sourceEnum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 670设置照明档位
|
||||
*/
|
||||
public void upLightGradeSettings(DeviceXinghanInstructDto dto, DataSourceEnum sourceEnum) {
|
||||
sendCommand(dto, "ins_LightGrade","照明档位", sourceEnum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置照明档位
|
||||
*/
|
||||
public void upLightGradeSettings(DeviceXinghanInstructDto dto) {
|
||||
sendCommand(dto, "ins_LightGrade","照明档位");
|
||||
public void upSideLightSettings(DeviceXinghanInstructDto dto, DataSourceEnum sourceEnum) {
|
||||
sendCommand(dto, "ins_Side_Light","照明档位", sourceEnum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置SOS档位
|
||||
*/
|
||||
public void upSOSGradeSettings(DeviceXinghanInstructDto dto) {
|
||||
public void upSOSGradeSettings(DeviceXinghanInstructDto dto, DataSourceEnum sourceEnum) {
|
||||
if(dto.getIsBluetooth()){
|
||||
long deviceId = dto.getDeviceId();
|
||||
// 1. 使用Optional简化空值检查,使代码更简洁
|
||||
@ -138,7 +147,7 @@ public class DeviceXinghanBizService {
|
||||
// 6. 新建报警信息
|
||||
createAlarm(device.getId(),device.getDeviceImei(),"ins_SOSGrade",sosGrade);
|
||||
}else {
|
||||
sendCommand(dto, "ins_SOSGrade","SOS档位");
|
||||
sendCommand(dto, "ins_SOSGrade","SOS档位", sourceEnum);
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,10 +159,10 @@ public class DeviceXinghanBizService {
|
||||
public void executeSosCall(String phone) {
|
||||
log.info("[SOS业务] 准备发起语音拨号 -> 目标: {}", phone);
|
||||
Map<String, String> params = Map.of("device", "670");
|
||||
String callId = voiceUtil.sendTtsSync(phone, "TTS_328730104", params);
|
||||
SingleCallByTtsResponse response = voiceUtil.sendTtsSync(phone, "TTS_328730104", params);
|
||||
|
||||
if (callId != null) {
|
||||
log.info("[SOS业务] 拨号指令下发成功, callId: {}", callId);
|
||||
if ("OK".equalsIgnoreCase(response.getBody().getCode())) {
|
||||
log.info("[SOS业务] 拨号指令下发成功, callId: {}", response.getBody().getCallId());
|
||||
// 这里可以记录拨打日志到数据库
|
||||
} else {
|
||||
log.error("[SOS业务] 拨号指令下发失败,请检查配置或余额");
|
||||
@ -163,8 +172,8 @@ public class DeviceXinghanBizService {
|
||||
/**
|
||||
* 设置强制报警
|
||||
*/
|
||||
public void upShakeBitSettings(DeviceXinghanInstructDto dto) {
|
||||
sendCommand(dto, "ins_ShakeBit","强制报警");
|
||||
public void upShakeBitSettings(DeviceXinghanInstructDto dto, DataSourceEnum sourceEnum) {
|
||||
sendCommand(dto, "ins_ShakeBit","强制报警", sourceEnum);
|
||||
}
|
||||
|
||||
public DeviceXinghanDetailVo getInfo(Long id) {
|
||||
@ -180,6 +189,11 @@ public class DeviceXinghanBizService {
|
||||
vo.setDeviceImei(device.getDeviceImei());
|
||||
vo.setDeviceMac(device.getDeviceMac());
|
||||
vo.setDeviceStatus(device.getDeviceStatus());
|
||||
// 2026-04-13 新增 紧急联系人与紧急联系信息
|
||||
vo.setSosSmsMsg(device.getSosSmsMsg());
|
||||
vo.setSosVoiceMsg(device.getSosVoiceMsg());
|
||||
vo.setContact1Phone(device.getContact1Phone());
|
||||
vo.setContact2Phone(device.getContact2Phone());
|
||||
DeviceType deviceType = deviceTypeMapper.selectById(device.getDeviceType());
|
||||
if (deviceType != null) {
|
||||
vo.setCommunicationMode(Integer.valueOf(deviceType.getCommunicationMode()));
|
||||
@ -235,7 +249,7 @@ public class DeviceXinghanBizService {
|
||||
/**
|
||||
* 上传设备logo
|
||||
*/
|
||||
public void uploadDeviceLogo(AppDeviceLogoUploadDto bo) {
|
||||
public void uploadDeviceLogo(AppDeviceLogoUploadDto bo, DataSourceEnum sourceEnum) {
|
||||
try {
|
||||
Device device = deviceMapper.selectById(bo.getDeviceId());
|
||||
if (device == null) {
|
||||
@ -267,7 +281,7 @@ public class DeviceXinghanBizService {
|
||||
}
|
||||
log.info("发送上传开机画面到设备消息=>topic:{},payload:{}", MqttConstants.GLOBAL_PUB_KEY+device.getDeviceImei(),json);
|
||||
|
||||
recordDeviceLog(device.getId(), device.getDeviceName(), "上传开机画面", "上传开机画面", AppLoginHelper.getUserId());
|
||||
recordDeviceLog(device.getId(), device.getDeviceName(), "上传开机画面", "上传开机画面", AppLoginHelper.getUserId(), sourceEnum.getName());
|
||||
} catch (Exception e){
|
||||
throw new ServiceException("发送指令失败");
|
||||
}
|
||||
@ -277,7 +291,7 @@ public class DeviceXinghanBizService {
|
||||
* 批量上传设备logo
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void uploadDeviceLogoBatch(DeviceDebugLogoUploadDto batchDto) {
|
||||
public void uploadDeviceLogoBatch(DeviceDebugLogoUploadDto batchDto, DataSourceEnum sourceEnum) {
|
||||
if (CollectionUtils.isEmpty(batchDto.getDeviceIds())) {
|
||||
throw new ServiceException("设备列表为空");
|
||||
}
|
||||
@ -329,7 +343,7 @@ public class DeviceXinghanBizService {
|
||||
throw new ServiceException("上传LOGO失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
recordDeviceLog(d.getId(), d.getDeviceName(), "上传开机画面", "上传开机画面", AppLoginHelper.getUserId());
|
||||
recordDeviceLog(d.getId(), d.getDeviceName(), "上传开机画面", "上传开机画面", AppLoginHelper.getUserId(), sourceEnum.getName());
|
||||
|
||||
});
|
||||
}
|
||||
@ -339,7 +353,7 @@ public class DeviceXinghanBizService {
|
||||
* @param bo
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class) // 1. 事务
|
||||
public boolean registerPersonInfo(AppPersonnelInfoBo bo) {
|
||||
public boolean registerPersonInfo(AppPersonnelInfoBo bo, DataSourceEnum sourceEnum) {
|
||||
Long deviceId = bo.getDeviceId();
|
||||
Device deviceObj = deviceMapper.selectById(deviceId);
|
||||
if (deviceObj == null) {
|
||||
@ -375,7 +389,7 @@ public class DeviceXinghanBizService {
|
||||
}
|
||||
log.info("发送人员信息登记到设备消息=>topic:{},payload:{}", MqttConstants.GLOBAL_PUB_KEY + deviceObj.getDeviceImei(), bo);
|
||||
|
||||
recordDeviceLog(deviceId, deviceObj.getDeviceName(), "人员信息登记", JSON.toJSONString(bo), AppLoginHelper.getUserId());
|
||||
recordDeviceLog(deviceId, deviceObj.getDeviceName(), "人员信息登记", JSON.toJSONString(bo), AppLoginHelper.getUserId(), sourceEnum.getName());
|
||||
|
||||
return saveOrUpdatePersonnelInfo(bo, deviceId);
|
||||
}
|
||||
@ -424,7 +438,7 @@ public class DeviceXinghanBizService {
|
||||
* @param bo
|
||||
* @return
|
||||
*/
|
||||
public int sendAlarmMessage(AppDeviceSendMsgBo bo) {
|
||||
public int sendAlarmMessage(AppDeviceSendMsgBo bo, DataSourceEnum sourceEnum) {
|
||||
List<Long> deviceIds = bo.getDeviceIds();
|
||||
|
||||
// 1. 简化非空检查和抛出异常
|
||||
@ -467,7 +481,7 @@ public class DeviceXinghanBizService {
|
||||
deviceMapper.update(updateWrapper);
|
||||
|
||||
// 6. 记录操作日志
|
||||
recordDeviceLog(device.getId(), deviceName, "发送紧急通知", bo.getSendMsg(), AppLoginHelper.getUserId());
|
||||
recordDeviceLog(device.getId(), deviceName, "发送紧急通知", bo.getSendMsg(), AppLoginHelper.getUserId(),sourceEnum.getName());
|
||||
}
|
||||
} catch (ServiceException e) {
|
||||
// 捕获并重新抛出自定义异常,避免内层异常被外层泛化捕获
|
||||
@ -480,8 +494,123 @@ public class DeviceXinghanBizService {
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* *
|
||||
* 保存设备日志
|
||||
*/
|
||||
public void saveRecordDeviceLog(AppPersonnelInfoBo bo, DataSourceEnum sourceEnum) {
|
||||
Long deviceId = bo.getDeviceId();
|
||||
Device deviceObj = deviceMapper.selectById(deviceId);
|
||||
if(!StringUtils.isNotEmpty(bo.getSendMsg()))
|
||||
{
|
||||
throw new ServiceException("请输入保存内容");
|
||||
}
|
||||
if (deviceObj == null) {
|
||||
throw new RuntimeException("请先将设备入库!!!");
|
||||
}
|
||||
bo.setSendMsg("APP:"+bo.getSendMsg());
|
||||
recordDeviceLog(deviceObj.getId(), deviceObj.getDeviceName(), bo.getName(), bo.getSendMsg(), AppLoginHelper.getUserId(),sourceEnum.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义语音内容
|
||||
* @param bo
|
||||
* @return
|
||||
*/
|
||||
public int editSosVoiceMsg(AppDeviceSendMsgBo bo) {
|
||||
// 1. 参数校验
|
||||
List<Long> deviceIds = bo.getDeviceIds();
|
||||
validateMsg(deviceIds, bo.getSendMsg(), "短信提示内容");
|
||||
|
||||
// 不查数据库,直接构造只有 id 和待更新字段的实体
|
||||
List<Device> devices = deviceIds.stream()
|
||||
.map(id -> {
|
||||
Device d = new Device();
|
||||
d.setId(id);
|
||||
d.setSosVoiceMsg(bo.getSendMsg());
|
||||
return d;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 此时生成的 SQL 只有 sos_voice_msg 一个 SET 字段
|
||||
deviceService.updateBatchById(devices);
|
||||
return devices.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义短信内容
|
||||
* @param bo
|
||||
* @return
|
||||
*/
|
||||
public int editSosSmsMsg(AppDeviceSendMsgBo bo) {
|
||||
// 1. 参数校验
|
||||
List<Long> deviceIds = bo.getDeviceIds();
|
||||
validateMsg(deviceIds, bo.getSendMsg(), "短信提示内容");
|
||||
// 不查数据库,直接构造只有 id 和待更新字段的实体
|
||||
List<Device> devices = deviceIds.stream()
|
||||
.map(id -> {
|
||||
Device d = new Device();
|
||||
d.setId(id);
|
||||
d.setSosSmsMsg(bo.getSendMsg());
|
||||
return d;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 此时生成的 SQL 只有 sos_sms_msg 一个 SET 字段
|
||||
deviceService.updateBatchById(devices);
|
||||
return devices.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改通知开关设置
|
||||
* @param bo
|
||||
* @return
|
||||
*/
|
||||
public boolean editNotificationEnabled(DeviceContactPhoneBo bo){
|
||||
if(bo.getDeviceId() == null){
|
||||
throw new ServiceException("请选择设备");
|
||||
}
|
||||
Device d = new Device();
|
||||
d.setId(bo.getDeviceId());
|
||||
d.setNotificationEnabled(bo.getNotificationEnabled());
|
||||
return deviceService.updateById(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改紧急联系人手机
|
||||
* @param bo
|
||||
* @return
|
||||
*/
|
||||
public boolean editContactPhone(DeviceContactPhoneBo bo){
|
||||
if(bo.getDeviceId() == null){
|
||||
throw new ServiceException("请选择设备");
|
||||
}
|
||||
Device d = new Device();
|
||||
d.setId(bo.getDeviceId());
|
||||
d.setContact1Phone(bo.getContact1Phone());
|
||||
d.setContact2Phone(bo.getContact2Phone());
|
||||
return deviceService.updateById(d);
|
||||
}
|
||||
|
||||
/* ---------------------------------- 私有通用方法 ---------------------------------- */
|
||||
|
||||
/**
|
||||
* 统一校验:设备ID + 消息内容
|
||||
*/
|
||||
private void validateMsg(List<Long> deviceIds, String sendMsg, String typeName) {
|
||||
// 校验设备
|
||||
if (deviceIds == null || deviceIds.isEmpty()) {
|
||||
throw new ServiceException("请选择设备");
|
||||
}
|
||||
// 校验消息内容
|
||||
if (!StringUtils.isNotEmpty(sendMsg)) {
|
||||
throw new ServiceException("请输入" + typeName);
|
||||
}
|
||||
if (sendMsg.length() > 20) {
|
||||
throw new ServiceException(typeName + "不能超过20字");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装单个设备发送告警信息的逻辑
|
||||
*/
|
||||
@ -514,7 +643,7 @@ public class DeviceXinghanBizService {
|
||||
* @param payloadKey 指令负载数据的键名
|
||||
* @param deviceAction 设备操作类型描述
|
||||
*/
|
||||
private void sendCommand(DeviceXinghanInstructDto dto, String payloadKey, String deviceAction) {
|
||||
private void sendCommand(DeviceXinghanInstructDto dto, String payloadKey, String deviceAction, DataSourceEnum sourceEnum) {
|
||||
long deviceId = dto.getDeviceId();
|
||||
|
||||
// 1. 使用Optional简化空值检查,使代码更简洁
|
||||
@ -557,7 +686,8 @@ public class DeviceXinghanBizService {
|
||||
deviceName,
|
||||
deviceAction,
|
||||
content,
|
||||
AppLoginHelper.getUserId());
|
||||
AppLoginHelper.getUserId(),
|
||||
sourceEnum.getName());
|
||||
|
||||
// 6. 新建报警信息
|
||||
createAlarm(device.getId(),deviceImei,payloadKey,value);
|
||||
@ -571,7 +701,7 @@ public class DeviceXinghanBizService {
|
||||
* @param deviceAction 设备操作类型描述
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class) // 1. 事务注解
|
||||
public void sendCommandBatch(DeviceXinghanInstructDto dto, String payloadKey, String deviceAction) {
|
||||
public void sendCommandBatch(DeviceXinghanInstructDto dto, String payloadKey, String deviceAction, DataSourceEnum sourceEnum) {
|
||||
List<String> errorMessages = Collections.synchronizedList(new ArrayList<>());
|
||||
int value;
|
||||
try {
|
||||
@ -609,6 +739,7 @@ public class DeviceXinghanBizService {
|
||||
DeviceLog deviceLog = new DeviceLog();
|
||||
deviceLog.setDeviceId(device.getId());
|
||||
deviceLog.setDeviceAction(deviceAction);
|
||||
deviceLog.setDataSource(sourceEnum.getName());
|
||||
deviceLog.setContent(contentText);
|
||||
deviceLog.setCreateBy(AppLoginHelper.getUserId());
|
||||
deviceLog.setDeviceName(deviceName);
|
||||
@ -662,6 +793,7 @@ public class DeviceXinghanBizService {
|
||||
bo.setDeviceAction(0); // 强制报警
|
||||
bo.setStartTime(new Date());
|
||||
bo.setTreatmentState(1); // 未处理
|
||||
bo.setAlarmState(0);
|
||||
bo.setContent("强制报警:" + type.getDesc());
|
||||
String location = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX);
|
||||
if (StrUtil.isNotBlank(location)) {
|
||||
@ -685,12 +817,13 @@ public class DeviceXinghanBizService {
|
||||
* @param content 日志内容
|
||||
* @param operator 操作人
|
||||
*/
|
||||
private void recordDeviceLog(Long deviceId,String deviceName, String deviceAction, String content, Long operator) {
|
||||
private void recordDeviceLog(Long deviceId,String deviceName, String deviceAction, String content, Long operator, String source) {
|
||||
try {
|
||||
// 创建设备日志实体
|
||||
com.fuyuanshen.equipment.domain.DeviceLog deviceLog = new com.fuyuanshen.equipment.domain.DeviceLog();
|
||||
deviceLog.setDeviceId(deviceId);
|
||||
deviceLog.setDeviceAction(deviceAction);
|
||||
deviceLog.setDataSource(source);
|
||||
deviceLog.setContent(content);
|
||||
deviceLog.setCreateBy(operator);
|
||||
deviceLog.setDeviceName(deviceName);
|
||||
@ -703,7 +836,7 @@ public class DeviceXinghanBizService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDeviceOffline(String deviceImei) {
|
||||
public boolean isDeviceOffline(String deviceImei) {
|
||||
String deviceOnlineStatusRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ deviceImei + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX ;
|
||||
return RedisUtils.getCacheObject(deviceOnlineStatusRedisKey)==null;
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ public class AliyunVoiceUtil {
|
||||
/**
|
||||
* 同步发送方法:由异步架构调用
|
||||
*/
|
||||
public String sendTtsSync(String phone, String templateCode, Map<String, String> params) {
|
||||
public SingleCallByTtsResponse sendTtsSync(String phone, String templateCode, Map<String, String> params) {
|
||||
|
||||
try {
|
||||
// 1. 获取(或初始化)单例客户端
|
||||
@ -72,13 +72,8 @@ public class AliyunVoiceUtil {
|
||||
runtime.setConnectTimeout(5000);
|
||||
runtime.setReadTimeout(10000);
|
||||
|
||||
SingleCallByTtsResponse response = voiceClient.singleCallByTtsWithOptions(request, runtime);
|
||||
return voiceClient.singleCallByTtsWithOptions(request, runtime);
|
||||
|
||||
if ("OK".equalsIgnoreCase(response.getBody().getCode())) {
|
||||
return response.getBody().getCallId();
|
||||
} else {
|
||||
log.error("[AliyunVoice] 拨号失败: {}", response.getBody().getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[AliyunVoice] 接口异常", e);
|
||||
}
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
package com.fuyuanshen.equipment.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.fuyuanshen.common.core.domain.R;
|
||||
import com.fuyuanshen.common.core.validate.AddGroup;
|
||||
import com.fuyuanshen.common.core.validate.EditGroup;
|
||||
import com.fuyuanshen.common.excel.utils.ExcelUtil;
|
||||
import com.fuyuanshen.common.idempotent.annotation.RepeatSubmit;
|
||||
import com.fuyuanshen.common.log.annotation.Log;
|
||||
import com.fuyuanshen.common.log.enums.BusinessType;
|
||||
import com.fuyuanshen.common.mybatis.core.page.PageQuery;
|
||||
import com.fuyuanshen.common.mybatis.core.page.TableDataInfo;
|
||||
import com.fuyuanshen.common.web.core.BaseController;
|
||||
import com.fuyuanshen.equipment.domain.bo.SmsSendRecordBo;
|
||||
import com.fuyuanshen.equipment.domain.vo.SmsSendRecordVo;
|
||||
import com.fuyuanshen.equipment.service.ISmsSendRecordService;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 报警通知记录(短信/语音)
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2026-05-08
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/system/sendRecord")
|
||||
public class SmsSendRecordController extends BaseController {
|
||||
|
||||
private final ISmsSendRecordService smsSendRecordService;
|
||||
|
||||
/**
|
||||
* 查询报警通知记录(短信/语音)列表
|
||||
*/
|
||||
@SaCheckPermission("system:sendRecord:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<SmsSendRecordVo> list(SmsSendRecordBo bo, PageQuery pageQuery) {
|
||||
return smsSendRecordService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 报警通知记录(短信/语音)列表
|
||||
*/
|
||||
@SaCheckPermission("system:sendRecord:export")
|
||||
@Log(title = "报警通知记录(短信/语音)", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(SmsSendRecordBo bo, HttpServletResponse response) {
|
||||
List<SmsSendRecordVo> list = smsSendRecordService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "报警通知记录(短信/语音)", SmsSendRecordVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取报警通知记录(短信/语音)详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@SaCheckPermission("system:sendRecord:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<SmsSendRecordVo> getInfo(@NotNull(message = "主键不能为空")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(smsSendRecordService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增报警通知记录(短信/语音)
|
||||
*/
|
||||
@SaCheckPermission("system:sendRecord:add")
|
||||
@Log(title = "报警通知记录(短信/语音)", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping()
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody SmsSendRecordBo bo) {
|
||||
return toAjax(smsSendRecordService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改报警通知记录(短信/语音)
|
||||
*/
|
||||
@SaCheckPermission("system:sendRecord:edit")
|
||||
@Log(title = "报警通知记录(短信/语音)", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping()
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody SmsSendRecordBo bo) {
|
||||
return toAjax(smsSendRecordService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除报警通知记录(短信/语音)
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("system:sendRecord:remove")
|
||||
@Log(title = "报警通知记录(短信/语音)", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@NotEmpty(message = "主键不能为空")
|
||||
@PathVariable Long[] ids) {
|
||||
return toAjax(smsSendRecordService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
}
|
||||
@ -183,4 +183,25 @@ public class Device extends TenantEntity {
|
||||
@Schema(title = "轨迹ID(高德)")
|
||||
private Long trid;
|
||||
|
||||
/**
|
||||
* 联系人1手机
|
||||
*/
|
||||
private String contact1Phone;
|
||||
/**
|
||||
* 联系人2手机
|
||||
*/
|
||||
private String contact2Phone;
|
||||
/**
|
||||
* 自定义语音文本
|
||||
*/
|
||||
private String sosVoiceMsg;
|
||||
/**
|
||||
* 自定义短信文本
|
||||
*/
|
||||
private String sosSmsMsg;
|
||||
/**
|
||||
* 通知开关,0关闭,1短信,2语音,3全开
|
||||
*/
|
||||
private Integer notificationEnabled;
|
||||
|
||||
}
|
||||
|
||||
@ -109,5 +109,13 @@ public class DeviceAlarm extends TenantEntity {
|
||||
@Schema(title = "设备IMEI")
|
||||
@AutoMapping(target = "deviceImei")
|
||||
private String deviceImei;
|
||||
/**
|
||||
* 告警状态,0 解除告警, 1 告警中
|
||||
*/
|
||||
private Integer alarmState;
|
||||
/**
|
||||
* 警报等级
|
||||
*/
|
||||
private Integer alarmLevel;
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
package com.fuyuanshen.equipment.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fuyuanshen.common.tenant.core.TenantEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 报警通知记录(短信/语音)对象 sms_send_record
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2026-05-08
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sms_send_record")
|
||||
public class SmsSendRecord extends TenantEntity {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 关联的报警记录ID
|
||||
*/
|
||||
private Long alarmId;
|
||||
|
||||
/**
|
||||
* 设备IMEI(冗余方便查询)
|
||||
*/
|
||||
private String deviceImei;
|
||||
|
||||
/**
|
||||
* 通知类型:SMS-短信, VOICE-语音呼叫
|
||||
*/
|
||||
private String notifyType;
|
||||
|
||||
/**
|
||||
* 接收通知的手机号
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 消息内容(短信内容或语音模板参数)
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 短信/语音模板ID
|
||||
*/
|
||||
private String templateId;
|
||||
|
||||
/**
|
||||
* 发送状态:0-待发送, 1-成功, 2-失败
|
||||
*/
|
||||
private Long status;
|
||||
|
||||
/**
|
||||
* 平台返回的状态码
|
||||
*/
|
||||
private String responseCode;
|
||||
|
||||
/**
|
||||
* 平台返回的描述信息
|
||||
*/
|
||||
private String responseMsg;
|
||||
|
||||
/**
|
||||
* 实际发送时间(调用平台时间)
|
||||
*/
|
||||
private Date sendTime;
|
||||
|
||||
|
||||
}
|
||||
@ -119,4 +119,9 @@ public class DeviceAlarmBo extends TenantEntity {
|
||||
*/
|
||||
private Integer alarmState;
|
||||
|
||||
/**
|
||||
* 警报等级
|
||||
*/
|
||||
private Integer alarmLevel;
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
package com.fuyuanshen.equipment.domain.bo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DeviceContactPhoneBo {
|
||||
private Long deviceId;
|
||||
/**
|
||||
* 联系人1手机
|
||||
*/
|
||||
private String contact1Phone;
|
||||
/**
|
||||
* 联系人2手机
|
||||
*/
|
||||
private String contact2Phone;
|
||||
/**
|
||||
* 通知开关
|
||||
*/
|
||||
private Integer notificationEnabled;
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package com.fuyuanshen.equipment.domain.bo;
|
||||
|
||||
import com.fuyuanshen.common.core.validate.AddGroup;
|
||||
import com.fuyuanshen.common.core.validate.EditGroup;
|
||||
import com.fuyuanshen.common.mybatis.core.domain.BaseEntity;
|
||||
import com.fuyuanshen.equipment.domain.DeviceLog;
|
||||
import com.fuyuanshen.equipment.domain.SmsSendRecord;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = SmsSendRecord.class, reverseConvertGenerate = false)
|
||||
public class SmsSendRecordBo extends BaseEntity {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@NotNull(message = "主键不能为空", groups = { EditGroup.class })
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 关联的报警记录ID
|
||||
*/
|
||||
@NotNull(message = "关联的报警记录ID不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long alarmId;
|
||||
|
||||
/**
|
||||
* 设备IMEI(冗余方便查询)
|
||||
*/
|
||||
@NotBlank(message = "设备IMEI(冗余方便查询)不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String deviceImei;
|
||||
|
||||
/**
|
||||
* 通知类型:SMS-短信, VOICE-语音呼叫
|
||||
*/
|
||||
@NotBlank(message = "通知类型:SMS-短信, VOICE-语音呼叫不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String notifyType;
|
||||
|
||||
/**
|
||||
* 接收通知的手机号
|
||||
*/
|
||||
@NotBlank(message = "接收通知的手机号不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 消息内容(短信内容或语音模板参数)
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 短信/语音模板ID
|
||||
*/
|
||||
private String templateId;
|
||||
|
||||
/**
|
||||
* 发送状态:0-待发送, 1-成功, 2-失败
|
||||
*/
|
||||
@NotNull(message = "发送状态:0-待发送, 1-成功, 2-失败不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long status;
|
||||
|
||||
/**
|
||||
* 平台返回的状态码
|
||||
*/
|
||||
private String responseCode;
|
||||
|
||||
/**
|
||||
* 平台返回的描述信息
|
||||
*/
|
||||
private String responseMsg;
|
||||
|
||||
/**
|
||||
* 实际发送时间(调用平台时间)
|
||||
*/
|
||||
private Date sendTime;
|
||||
}
|
||||
@ -136,4 +136,13 @@ public class DeviceAlarmVo implements Serializable {
|
||||
@Schema(name = "设备图片")
|
||||
private String devicePic;
|
||||
|
||||
/**
|
||||
* 警报等级
|
||||
*/
|
||||
private Integer alarmLevel;
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
private String tenantId;
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
package com.fuyuanshen.equipment.domain.vo;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import com.fuyuanshen.common.excel.annotation.ExcelDictFormat;
|
||||
import com.fuyuanshen.common.excel.convert.ExcelDictConvert;
|
||||
import com.fuyuanshen.equipment.domain.DeviceLog;
|
||||
import com.fuyuanshen.equipment.domain.SmsSendRecord;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AutoMapper(target = SmsSendRecord.class)
|
||||
public class SmsSendRecordVo implements Serializable {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@ExcelProperty(value = "主键")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 关联的报警记录ID
|
||||
*/
|
||||
@ExcelProperty(value = "关联的报警记录ID")
|
||||
private Long alarmId;
|
||||
|
||||
/**
|
||||
* 设备IMEI(冗余方便查询)
|
||||
*/
|
||||
@ExcelProperty(value = "设备IMEI", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(readConverterExp = "冗=余方便查询")
|
||||
private String deviceImei;
|
||||
|
||||
/**
|
||||
* 通知类型:SMS-短信, VOICE-语音呼叫
|
||||
*/
|
||||
@ExcelProperty(value = "通知类型:SMS-短信, VOICE-语音呼叫")
|
||||
private String notifyType;
|
||||
|
||||
/**
|
||||
* 接收通知的手机号
|
||||
*/
|
||||
@ExcelProperty(value = "接收通知的手机号")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 消息内容(短信内容或语音模板参数)
|
||||
*/
|
||||
@ExcelProperty(value = "消息内容", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(readConverterExp = "短=信内容或语音模板参数")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 短信/语音模板ID
|
||||
*/
|
||||
@ExcelProperty(value = "短信/语音模板ID")
|
||||
private String templateId;
|
||||
|
||||
/**
|
||||
* 发送状态:0-待发送, 1-成功, 2-失败
|
||||
*/
|
||||
@ExcelProperty(value = "发送状态:0-待发送, 1-成功, 2-失败")
|
||||
private Long status;
|
||||
|
||||
/**
|
||||
* 平台返回的状态码
|
||||
*/
|
||||
@ExcelProperty(value = "平台返回的状态码")
|
||||
private String responseCode;
|
||||
|
||||
/**
|
||||
* 平台返回的描述信息
|
||||
*/
|
||||
@ExcelProperty(value = "平台返回的描述信息")
|
||||
private String responseMsg;
|
||||
|
||||
/**
|
||||
* 实际发送时间(调用平台时间)
|
||||
*/
|
||||
@ExcelProperty(value = "实际发送时间", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(readConverterExp = "调=用平台时间")
|
||||
private Date sendTime;
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.fuyuanshen.equipment.enums;
|
||||
|
||||
/**
|
||||
* 数据来源枚举
|
||||
*/
|
||||
public enum DataSourceEnum {
|
||||
/**
|
||||
* 默认数据源
|
||||
*/
|
||||
APP(0, "app"),
|
||||
/**
|
||||
* 默认数据源
|
||||
*/
|
||||
Web(1, "web");
|
||||
|
||||
|
||||
private final Integer code;
|
||||
private final String name;
|
||||
|
||||
DataSourceEnum(Integer code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.fuyuanshen.equipment.mapper;
|
||||
|
||||
import com.fuyuanshen.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import com.fuyuanshen.equipment.domain.SmsSendRecord;
|
||||
import com.fuyuanshen.equipment.domain.vo.SmsSendRecordVo;
|
||||
|
||||
public interface SmsSendRecordMapper extends BaseMapperPlus<SmsSendRecord, SmsSendRecordVo> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package com.fuyuanshen.equipment.service;
|
||||
|
||||
import com.fuyuanshen.common.mybatis.core.page.PageQuery;
|
||||
import com.fuyuanshen.common.mybatis.core.page.TableDataInfo;
|
||||
import com.fuyuanshen.equipment.domain.bo.SmsSendRecordBo;
|
||||
import com.fuyuanshen.equipment.domain.vo.SmsSendRecordVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 报警通知记录(短信/语音)Service接口
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2026-05-08
|
||||
*/
|
||||
public interface ISmsSendRecordService {
|
||||
|
||||
/**
|
||||
* 查询报警通知记录(短信/语音)
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 报警通知记录(短信/语音)
|
||||
*/
|
||||
SmsSendRecordVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询报警通知记录(短信/语音)列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 报警通知记录(短信/语音)分页列表
|
||||
*/
|
||||
TableDataInfo<SmsSendRecordVo> queryPageList(SmsSendRecordBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的报警通知记录(短信/语音)列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 报警通知记录(短信/语音)列表
|
||||
*/
|
||||
List<SmsSendRecordVo> queryList(SmsSendRecordBo bo);
|
||||
|
||||
/**
|
||||
* 新增报警通知记录(短信/语音)
|
||||
*
|
||||
* @param bo 报警通知记录(短信/语音)
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(SmsSendRecordBo bo);
|
||||
|
||||
/**
|
||||
* 修改报警通知记录(短信/语音)
|
||||
*
|
||||
* @param bo 报警通知记录(短信/语音)
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(SmsSendRecordBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除报警通知记录(短信/语音)信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
package com.fuyuanshen.equipment.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fuyuanshen.common.core.utils.MapstructUtils;
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
import com.fuyuanshen.common.mybatis.core.page.PageQuery;
|
||||
import com.fuyuanshen.common.mybatis.core.page.TableDataInfo;
|
||||
import com.fuyuanshen.equipment.domain.SmsSendRecord;
|
||||
import com.fuyuanshen.equipment.domain.bo.SmsSendRecordBo;
|
||||
import com.fuyuanshen.equipment.domain.vo.SmsSendRecordVo;
|
||||
import com.fuyuanshen.equipment.mapper.SmsSendRecordMapper;
|
||||
import com.fuyuanshen.equipment.service.ISmsSendRecordService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 报警通知记录(短信/语音)Service业务层处理
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2026-05-08
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class SmsSendRecordServiceImpl implements ISmsSendRecordService {
|
||||
|
||||
private final SmsSendRecordMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询报警通知记录(短信/语音)
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 报警通知记录(短信/语音)
|
||||
*/
|
||||
@Override
|
||||
public SmsSendRecordVo queryById(Long id){
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询报警通知记录(短信/语音)列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 报警通知记录(短信/语音)分页列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<SmsSendRecordVo> queryPageList(SmsSendRecordBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<SmsSendRecord> lqw = buildQueryWrapper(bo);
|
||||
Page<SmsSendRecordVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的报警通知记录(短信/语音)列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 报警通知记录(短信/语音)列表
|
||||
*/
|
||||
@Override
|
||||
public List<SmsSendRecordVo> queryList(SmsSendRecordBo bo) {
|
||||
LambdaQueryWrapper<SmsSendRecord> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<SmsSendRecord> buildQueryWrapper(SmsSendRecordBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<SmsSendRecord> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(SmsSendRecord::getId);
|
||||
lqw.eq(bo.getAlarmId() != null, SmsSendRecord::getAlarmId, bo.getAlarmId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getDeviceImei()), SmsSendRecord::getDeviceImei, bo.getDeviceImei());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getNotifyType()), SmsSendRecord::getNotifyType, bo.getNotifyType());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getPhone()), SmsSendRecord::getPhone, bo.getPhone());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getContent()), SmsSendRecord::getContent, bo.getContent());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getTemplateId()), SmsSendRecord::getTemplateId, bo.getTemplateId());
|
||||
lqw.eq(bo.getStatus() != null, SmsSendRecord::getStatus, bo.getStatus());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getResponseCode()), SmsSendRecord::getResponseCode, bo.getResponseCode());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getResponseMsg()), SmsSendRecord::getResponseMsg, bo.getResponseMsg());
|
||||
lqw.eq(bo.getSendTime() != null, SmsSendRecord::getSendTime, bo.getSendTime());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增报警通知记录(短信/语音)
|
||||
*
|
||||
* @param bo 报警通知记录(短信/语音)
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(SmsSendRecordBo bo) {
|
||||
SmsSendRecord add = MapstructUtils.convert(bo, SmsSendRecord.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改报警通知记录(短信/语音)
|
||||
*
|
||||
* @param bo 报警通知记录(短信/语音)
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(SmsSendRecordBo bo) {
|
||||
SmsSendRecord update = MapstructUtils.convert(bo, SmsSendRecord.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(SmsSendRecord entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除报警通知记录(短信/语音)信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,9 @@
|
||||
<if test="bo.deviceName != null">
|
||||
and d.device_name like concat('%', #{bo.deviceName}, '%')
|
||||
</if>
|
||||
<if test="bo.deviceId != null">
|
||||
and da.device_id = #{bo.deviceId}
|
||||
</if>
|
||||
<if test="bo.deviceType != null">
|
||||
and dt.id = #{bo.deviceType}
|
||||
</if>
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.fuyuanshen.system.mapper.SmsSendRecordMapper">
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,145 @@
|
||||
package com.fuyuanshen.job.integration;
|
||||
|
||||
import com.aizuda.snailjob.client.job.core.openapi.SnailJobOpenApi;
|
||||
import com.aizuda.snailjob.client.job.core.enums.AllocationAlgorithmEnum;
|
||||
import com.aizuda.snailjob.common.core.enums.JobBlockStrategyEnum;
|
||||
import com.aizuda.snailjob.client.job.core.enums.TriggerTypeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* SnailJob 1.8.1 业务集成客户端
|
||||
* 优化点:增强健壮性、参数校验、日志标准化
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SnailJobClient {
|
||||
|
||||
//执行超时时间
|
||||
private static final int DEFAULT_TIMEOUT = 60;
|
||||
//如果重试开启,两次重试之间间隔几秒
|
||||
private static final int RETRY_INTERVAL = 30;
|
||||
|
||||
/**
|
||||
* 创建离线自动关单任务
|
||||
* 优化:引入 Assert 校验,防止非法参数进入 OpenAPI 导致报错
|
||||
*/
|
||||
public void addRetryTask(Long alarmId, String businessNo, String executorName, int delayMinutes) {
|
||||
Assert.notNull(alarmId, "alarmId cannot be null");
|
||||
Assert.hasText(businessNo, "businessNo cannot be empty");
|
||||
|
||||
try {
|
||||
// 计算新的延迟秒数
|
||||
String triggerInterval = String.valueOf(delayMinutes * 60L);
|
||||
// 1. 计算 5 分钟后时间戳
|
||||
LocalDateTime execTime = LocalDateTime.now().plusMinutes(delayMinutes);
|
||||
long triggerTime = execTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
|
||||
SnailJobOpenApi.addClusterJob()
|
||||
// 设置任务路由策略:随机选择执行器节点
|
||||
.setRouteKey(AllocationAlgorithmEnum.RANDOM)
|
||||
// 设置任务名称(使用业务编号唯一标识)
|
||||
.setJobName(businessNo)
|
||||
// 设置执行器名称(指定任务由哪个执行器执行)
|
||||
.setExecutorInfo(executorName)
|
||||
// 设置执行器超时时间(使用系统默认超时配置)
|
||||
.setExecutorTimeout(DEFAULT_TIMEOUT)
|
||||
// 设置任务描述:设备报警离线自动关单任务
|
||||
.setDescription("设备报警离线自动关单:" + businessNo)
|
||||
// 设置任务阻塞策略:丢弃后续并发请求
|
||||
.setBlockStrategy(JobBlockStrategyEnum.OVERLAY)
|
||||
// 设置最大重试次数:0 代表不重试
|
||||
.setMaxRetryTimes(0)
|
||||
// 设置任务触发类型:定时触发
|
||||
// 改为 CRON 触发
|
||||
.setTriggerType(TriggerTypeEnum.SCHEDULED_TIME)
|
||||
// 毫秒级时间戳
|
||||
.setTriggerInterval(triggerInterval)
|
||||
// 添加任务入参:传入报警ID字符串
|
||||
.addArgsStr("alarmId", alarmId.toString())
|
||||
// 设置重试间隔时间(重试时的等待时间)
|
||||
.setRetryInterval(RETRY_INTERVAL)
|
||||
// 执行任务创建/提交操作
|
||||
.execute();
|
||||
|
||||
log.info("[SnailJob] 创建关单任务成功 | businessNo: {} | delay: {}m", businessNo, delayMinutes);
|
||||
} catch (Exception e) {
|
||||
log.error("[SnailJob] 创建关单任务异常 | businessNo: {}", businessNo, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务触发时间(续期)
|
||||
* 优化:增加原子性思考。虽然 1.8.1 必须删后再加,但通过 try-catch 确保删除失败不中断逻辑(可能任务本就不存在)
|
||||
*/
|
||||
public void updateRetryTaskNextTriggerTime(Long alarmId, String businessNo, String executorName, int delayMinutes) {
|
||||
try {
|
||||
// 计算新的延迟秒数
|
||||
String triggerInterval = String.valueOf(delayMinutes * 60L);
|
||||
|
||||
// 只要 JobName (businessNo) 一致,SnailJob 服务端会识别为同一个任务进行更新
|
||||
SnailJobOpenApi.addClusterJob()
|
||||
.setJobName(businessNo) // 关键:保持 JobName 不变
|
||||
.setExecutorInfo(executorName)
|
||||
.setTriggerType(TriggerTypeEnum.SCHEDULED_TIME)
|
||||
.setTriggerInterval(triggerInterval)
|
||||
.addArgsStr("alarmId", alarmId.toString())
|
||||
.setRouteKey(AllocationAlgorithmEnum.RANDOM)
|
||||
.setMaxRetryTimes(0)
|
||||
// 强制开启覆盖更新(如果 SDK 支持,部分版本需显式指定,1.8.1 默认通常为 saveOrUpdate 逻辑)
|
||||
.execute();
|
||||
|
||||
log.info("[SnailJob] 任务续期成功(覆盖方式) → businessNo:{}", businessNo);
|
||||
} catch (Exception e) {
|
||||
log.error("[SnailJob] 任务续期失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
* 优化:针对 hashCode() 可能产生的负值进行处理,并使用更稳健的 Set 构造
|
||||
*/
|
||||
public void deleteRetryTask(String businessNo) {
|
||||
if (businessNo == null) return;
|
||||
|
||||
try {
|
||||
// 注意:hashCode 存在冲突可能。在 SnailJob 中若需绝对精确删除,建议存储 addClusterJob 返回的 ID
|
||||
// 这里保留你的逻辑,但对负数哈希取绝对值以符合一般 ID 预期(取决于服务端接收逻辑)
|
||||
long jobId = Math.abs((long) businessNo.hashCode());
|
||||
|
||||
SnailJobOpenApi.deleteJob(Set.of(jobId)).execute();
|
||||
log.info("[SnailJob] 删除任务成功 | businessNo: {} | jobId: {}", businessNo, jobId);
|
||||
} catch (Exception e) {
|
||||
// 删除通常作为补偿操作,记录 warn 即可,无需抛出异常中断业务
|
||||
log.warn("[SnailJob] 删除任务异常 | businessNo: {} | msg: {}", businessNo, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发工作流
|
||||
* 优化:增加对 args 内容的空值保护
|
||||
*/
|
||||
public void startWorkflow(String workflowId, Map<String, Object> args) {
|
||||
Assert.hasText(workflowId, "workflowId cannot be empty");
|
||||
|
||||
try {
|
||||
var request = SnailJobOpenApi.triggerWorkFlow(Long.parseLong(workflowId));
|
||||
|
||||
if (args != null) {
|
||||
args.forEach((k, v) -> request.addArgsStr(k, Objects.toString(v, "")));
|
||||
}
|
||||
|
||||
request.execute();
|
||||
log.info("[SnailJob] 触发工作流成功 | workflowId: {}", workflowId);
|
||||
} catch (Exception e) {
|
||||
log.error("[SnailJob] 触发工作流失败 | workflowId: {}", workflowId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user