43 Commits

Author SHA1 Message Date
c291e47ae8 feat(mqtt): 添加报警检查服务实现多阶段报警处理
- 实现 AlarmCheckService 提供延迟队列消费者功能
- 添加 AlarmDelayProvider 接口定义延迟检查任务
- 集成 AlarmStageConfig 支持租户配置报警阶段延迟时间
- 重构 AliyunVoiceUtil 返回完整响应对象而非字符串
- 在 AppDeviceController 中新增 AlarmList 接口查询设备告警列表
- 扩展设备相关控制器支持数据来源枚举参数传递
- 新增 Xinghan 指令控制器提供 HBY018A 设备专用接口
- 定义 DataSourceEnum 枚举区分 APP 和 Web 数据来源
- 扩展 Device 实体类增加紧急联系人和通知配置字段
- 添加 DeviceAlarm 实体类告警状态和等级属性
- 新增 DeviceContactPhoneBo 处理设备联系人信息
- 优化设备操作记录日志支持数据来源标识
- 实现设备自定义语音短信消息编辑功能
- 添加设备通知开关和紧急联系人设置接口
2026-05-26 15:38:18 +08:00
7fcbb81317 snailjob配置 2026-04-27 13:17:02 +08:00
05cb75e652 修复bug3 2026-04-01 17:52:11 +08:00
5813425be9 修复bug2 2026-04-01 17:42:14 +08:00
539caa4b5f 修复bug 2026-03-27 10:34:38 +08:00
1111e03bf4 修复bug2 2026-03-20 16:37:32 +08:00
83752b2f11 修复bug 2026-03-19 13:25:11 +08:00
f37cb405cc 提交代码9 2026-03-19 09:49:08 +08:00
cc2713618a 提交代码8 2026-03-18 15:29:42 +08:00
47d40cf9d2 提交代码7 2026-03-06 17:58:39 +08:00
83b97841ef 提交代码6 2026-03-05 17:49:55 +08:00
9d84265f57 Merge remote-tracking branch 'origin/6170' into 6170 2026-03-02 16:38:01 +08:00
fa5dfab939 2.4G设备上报技术方案 2026-03-02 16:37:50 +08:00
5fb71dd092 提交代码5 2026-03-02 15:35:19 +08:00
1ce87aaec5 Merge branch '6170' of http://47.107.152.87:3000/dyf/fys-Multi-tenant into 6170 2026-03-02 14:31:46 +08:00
4f3e7b0ed0 regis 2026-03-02 14:31:40 +08:00
b75b7ef431 提交代码4 2026-03-02 13:55:21 +08:00
c9cad751f0 提交代码3 2026-03-02 13:45:30 +08:00
34841c8704 提交代码2 2026-03-02 11:17:46 +08:00
20ac6b0baa bug修改4 2026-02-27 18:34:21 +08:00
4d5292cebc bug修改3 2026-02-27 18:31:03 +08:00
7a35baa8f0 bug修改2 2026-02-27 17:42:23 +08:00
dbb7076b50 Merge remote-tracking branch 'origin/6170' into 6170 2026-02-27 15:34:22 +08:00
15c85d4c62 bug修改 2026-02-27 15:34:15 +08:00
e036ef05ce Merge branch 'jingquan' into 6170
# Conflicts:
#	fys-modules/fys-equipment/src/main/resources/mapper/equipment/AppBusinessFileMapper.xml
2026-02-27 15:31:26 +08:00
552bf0af0a hby100japp功能,语音音量增加2 2026-02-07 09:42:16 +08:00
efad1f5a4b hby100japp功能,语音音量增加 2026-02-06 16:11:38 +08:00
f3551be093 hby100japp功能3 2026-02-06 11:50:38 +08:00
855714106e hby100japp功能3 2026-02-04 18:40:27 +08:00
04cb699081 该设备类型下已有设备,无法修改设备类型名称!!! 2026-02-04 17:28:08 +08:00
0f0dd4c6b1 hby100japp功能2 2026-02-04 15:49:53 +08:00
2db37a75d2 hby100japp功能 2026-02-04 15:27:43 +08:00
696bbb4aa4 hby100j功能语音管理 2026-02-03 16:29:46 +08:00
ee445dc0d2 获取音频时长2 2026-02-02 16:09:25 +08:00
897561ba89 获取音频时长 2026-02-02 16:08:43 +08:00
2e575f78b1 语音管理 2026-02-02 16:06:11 +08:00
6fd7f8ca94 配置文件修改 2026-01-30 16:54:25 +08:00
9936a5ad13 新增mac地址 2026-01-30 16:53:20 +08:00
c267fe0c14 配置文件 2026-01-30 16:50:01 +08:00
e80830e76c Merge remote-tracking branch 'origin/6170' into 6170 2026-01-30 16:08:07 +08:00
437ab110b7 Merge remote-tracking branch 'origin/6170-protocol' into 6170
# Conflicts:
#	fys-admin/src/main/java/com/fuyuanshen/global/mqtt/config/MqttInboundConfiguration.java
#	fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/bjq/BjqAlarmRule.java
2026-01-30 13:48:33 +08:00
88650c3d9f 代码优化 2026-01-14 18:36:23 +08:00
ec28ab1092 设备协议 2025-11-10 08:37:42 +08:00
141 changed files with 6850 additions and 264 deletions

View File

@ -133,6 +133,19 @@
<version>1.5.7</version>
<scope>compile</scope>
</dependency>
<!-- Java音频转换库 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-core</artifactId>
<version>3.3.1</version>
</dependency>
<!-- <dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-all</artifactId>
<version>3.3.1</version>
</dependency>-->
<!-- skywalking 整合 logback -->
<!-- <dependency>-->

View File

@ -0,0 +1,81 @@
package com.fuyuanshen;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
public class SimpleQR {
public static void main(String[] args) throws Exception {
String text = "HELLO";
int size = 21; // 版本1的大小
boolean[][] qr = new boolean[size][size];
// 1. 三个定位标记
for (int i = 0; i < 7; i++)
for (int j = 0; j < 7; j++) {
boolean black = i == 0 || i == 6 || j == 0 || j == 6 || (i > 1 && i < 5 && j > 1 && j < 5);
qr[i][j] = qr[i][size - 1 - j] = qr[size - 1 - i][j] = black;
}
// 2. 定时图案(简化)
for (int i = 8; i < 13; i++) qr[i][6] = qr[6][i] = (i % 2 == 0);
// 3. 编码数据(极简编码)
byte[] data = text.getBytes();
int x = 20, y = 20, up = 1; // 从右下角开始
for (byte b : data)
for (int bit = 7; bit >= 0; bit--) {
while (qr[x][y] || (x == 6 && y < 9) || (y == 6 && x < 9) || (x < 9 && y < 9) ||
(x < 9 && y > size - 10) || (y < 9 && x > size - 10)) {
if (up > 0) {
if (--y < 0) {
y = 0;
x -= 2;
up = -1;
}
} else {
if (++y >= size) {
y = size - 1;
x -= 2;
up = 1;
}
}
if (x == 6) x--;
}
qr[x][y] = ((b >> bit) & 1) == 1;
if (up > 0) {
if (--y < 0) {
y = 0;
x -= 2;
up = -1;
}
} else {
if (++y >= size) {
y = size - 1;
x -= 2;
up = 1;
}
}
if (x == 6) x--;
}
// 4. 保存图像
int scale = 10, margin = 4;
BufferedImage img = new BufferedImage(
size * scale + margin * 2, size * scale + margin * 2, BufferedImage.TYPE_INT_RGB);
for (int i = 0; i < img.getWidth(); i++)
for (int j = 0; j < img.getHeight(); j++)
img.setRGB(i, j, 0xFFFFFF); // 白色背景
for (int i = 0; i < size; i++)
for (int j = 0; j < size; j++)
if (qr[i][j])
for (int di = 0; di < scale; di++)
for (int dj = 0; dj < scale; dj++)
img.setRGB(margin + i * scale + di, margin + j * scale + dj, 0x000000);
ImageIO.write(img, "PNG", new File("qr.png"));
System.out.println("二维码已生成");
}
}

View File

@ -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);
}
/**
* 查询设备列表
*/

View File

@ -1,13 +1,20 @@
package com.fuyuanshen.app.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import com.fuyuanshen.app.domain.dto.AppAudioFileDto;
import com.fuyuanshen.app.domain.dto.AppFileDto;
import com.fuyuanshen.app.domain.dto.AppFileRenameDto;
import com.fuyuanshen.app.domain.dto.TextToSpeechRequest;
import com.fuyuanshen.app.service.AudioProcessService;
import com.fuyuanshen.app.service.VideoProcessService;
import com.fuyuanshen.common.core.domain.R;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.idempotent.annotation.RepeatSubmit;
import com.fuyuanshen.common.web.core.BaseController;
import com.fuyuanshen.equipment.domain.vo.AppFileVo;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@ -56,6 +63,7 @@ public class AppVideoController extends BaseController {
return R.ok(audioProcessService.generateStandardPcmData(text));
}
/**
* 提取文本内容只支持txt/docx
*/
@ -65,4 +73,73 @@ public class AppVideoController extends BaseController {
return R.ok("Success",audioProcessService.extract(file));
}
/**
* 上传音频文件进行处理
* 支持MP3、WAV、PCM格式
*/
@PostMapping("/uploadAudioToOss")
public R<String> uploadAudioToOss(@ModelAttribute AppAudioFileDto bo) {
try {
bo.setSource("app");
String result = audioProcessService.uploadAudioToOss(bo);
return R.ok(result);
} catch (IllegalArgumentException e) {
return R.fail("文件格式错误: " + e.getMessage());
} catch (Exception e) {
return R.fail("上传处理失败: " + e.getMessage());
}
}
/**
* 文本转语音
* 支持MP3、WAV、PCM格式
*/
@PostMapping("/ttsToOss")
public R<String> textToSpeech(@RequestBody TextToSpeechRequest request) {
try {
if (request.getDeviceId() == null) {
return R.fail("设备ID不能为空");
}
if (StringUtils.isBlank(request.getText())) {
return R.fail("文本不能为空");
}
String result = audioProcessService.textToSpeech(
request.getDeviceId(),
request.getText(),
request.getFileSuffix(),
"app"
);
return R.ok(result);
} catch (Exception e) {
return R.fail("文本转语音失败: " + e.getMessage());
}
}
/**
* 查询语音文件列表
*/
@GetMapping("/queryAudioFileList")
public R<List<AppFileVo>> queryAudioFileList(Long deviceId) {
return R.ok(audioProcessService.queryAudioFileList(deviceId,"app"));
}
/**
* 删除语音文件
*/
@GetMapping("/deleteAudioFile")
public R<Void> deleteAudioFile(Long id) {
return audioProcessService.deleteAudioFile(id);
}
/**
* 文件重命名
*/
@PostMapping("/renameAudioFile")
public R<Void> renameAudioFile(@RequestBody AppFileRenameDto bo) {
return audioProcessService.renameAudioFile(bo);
}
}

View File

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

View File

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

View File

@ -0,0 +1,104 @@
package com.fuyuanshen.app.controller.device.bjq;
import com.fuyuanshen.app.domain.vo.AppDeviceHBY100JDetailVo;
import com.fuyuanshen.common.core.domain.R;
import com.fuyuanshen.common.web.core.BaseController;
import com.fuyuanshen.web.controller.device.domain.dto.*;
import com.fuyuanshen.web.service.device.DeviceHBY100JBizService;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* HBY100J设备控制类
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/hby100j/device")
public class AppDeviceHBY100JController extends BaseController {
private final DeviceHBY100JBizService deviceHBY100JBizService;
/**
* 获取设备详细信息
*
* @param id 主键
*/
@GetMapping("/{id}")
public R<AppDeviceHBY100JDetailVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(deviceHBY100JBizService.getInfo(id));
}
/**
* 更新语音
*/
@PostMapping("/updateVoice")
public R<Void> updateVoice(@RequestBody HBY100JUpdateVoiceDto dto) {
deviceHBY100JBizService.updateVoice(dto);
return R.ok();
}
/**
* 强制报警
*
*/
@PostMapping("/forceAlarmActivation")
public R<Void> forceAlarmActivation(@RequestBody HBY100JForceAlarmActivationDto bo) {
deviceHBY100JBizService.forceAlarmActivation(bo);
return R.ok();
}
/**
* 语音播报
*
*/
@PostMapping("/voiceBroadcast")
public R<Void> voiceBroadcast(@RequestBody HBY100JVoiceBroadcastDto params) {
deviceHBY100JBizService.voiceBroadcast(params);
return R.ok();
}
/**
* 爆闪模式
*/
@PostMapping("/strobeMode")
public R<Void> strobeMode(@RequestBody HBY100JStrobeModeDto params) {
deviceHBY100JBizService.strobeMode(params);
return R.ok();
}
/**
* 灯光调节
*/
@PostMapping("/lightAdjustment")
public R<Void> lightAdjustment(@RequestBody HBY100JLightAdjustmentDto params) {
deviceHBY100JBizService.lightAdjustment(params);
return R.ok();
}
/**
* 爆闪频率
*/
@PostMapping("/strobeFrequency")
public R<Void> strobeFrequency(@RequestBody HBY100JStrobeFrequencyDto params) {
deviceHBY100JBizService.strobeFrequency(params);
return R.ok();
}
/**
* 修改音量
*/
@PostMapping("/updateVolume")
public R<Void> updateVolume(@RequestBody HBY100JUpdateVolumeDto params) {
deviceHBY100JBizService.updateVolume(params);
return R.ok();
}
}

View File

@ -0,0 +1,21 @@
package com.fuyuanshen.app.domain.dto;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
public class AppAudioFileDto {
private Long deviceId;
/**
* 文件
*/
private MultipartFile file;
/**
* 数据来源 1:app 2:web 3:其他
*/
private String source;
}

View File

@ -0,0 +1,26 @@
package com.fuyuanshen.app.domain.dto;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
public class AppFileRenameDto {
private Long id;
/**
* 文件id
*/
private Long fileId;
/**
* 设备id
*/
private Long deviceId;
/**
* 文件名称
*/
private String fileName;
}

View File

@ -0,0 +1,11 @@
package com.fuyuanshen.app.domain.dto;
import lombok.Data;
/**
* PCM生成请求实体
*/
@Data
public class PcmGenerationRequest {
private String text;
}

View File

@ -0,0 +1,19 @@
package com.fuyuanshen.app.domain.dto;
import lombok.Data;
/**
* 文本转语音请求实体
*/
@Data
public class TextToSpeechRequest {
private Long deviceId;
private String text;
/**
* 文件后缀格式
*/
private String fileSuffix;
}

View File

@ -0,0 +1,133 @@
package com.fuyuanshen.app.http;
import com.alibaba.nls.client.AccessToken;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Duration;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import static cn.dev33.satoken.SaManager.log;
public class HttpTtsClient {
private String accessKeyId;
private String accessKeySecret;
private String appKey;
/**
* 阿里云TTS服务基础URL
*/
private static final String BASE_URL = "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/tts";
public HttpTtsClient(String accessKeyId, String accessKeySecret, String appKey) {
this.accessKeyId = accessKeyId;
this.accessKeySecret = accessKeySecret;
this.appKey = appKey;
}
private String refreshAccessToken() {
try {
// 调用阿里云API获取访问令牌
AccessToken accessToken = new AccessToken(accessKeyId, accessKeySecret);
accessToken.apply();
String token = accessToken.getToken();
log.info("访问令牌刷新成功");
return token;
} catch (Exception e) {
log.error("刷新访问令牌失败: {}", e.getMessage(), e);
return null;
}
}
/**
* 使用HTTP POST方式调用阿里云TTS服务生成MP3格式语音
*/
public byte[] synthesizeTextToMp3(String text,String fileSuffix) throws IOException {
String endpoint = "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/tts";
// 构建请求体
// String requestBody = String.format(
// "{\"appkey\":\"%s\",\"text\":\"%s\",\"voice\":\"zhifeng\",\"format\":\"MP3\",\"sample_rate\":24000,\"volume\":50,\"speech_rate\":0,\"pitch_rate\":0}",
// appKey,
// text.replace("\"", "\\\"")zhide
// );
String token = refreshAccessToken();
String requestBody = " {\n" +
" \"appkey\":\""+appKey+"\",\n" +
" \"voice\":\"zhide\",\n" +
" \"text\":\""+text+"\",\n" +
" \"token\":\""+token+"\",\n" +
" \"volume\": 100,\n" +
" \"pitch_rate\": 0,\n" +
" \"format\":\""+fileSuffix+"\"\n" +
" }";
URL url = new URL(endpoint);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 设置请求方法和头部
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", buildAuthorization());
conn.setRequestProperty("Content-Length", String.valueOf(requestBody.getBytes().length));
conn.setDoOutput(true);
// 发送请求体
try (OutputStream os = conn.getOutputStream()) {
os.write(requestBody.getBytes("UTF-8"));
}
// 读取响应
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// 读取音频数据
try (InputStream is = conn.getInputStream()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[1024];
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
return buffer.toByteArray();
}
} else {
throw new IOException("HTTP请求失败状态码: " + responseCode);
}
}
/**
* 构建授权头部
*/
private String buildAuthorization() {
// 实际实现需要根据阿里云API规范构建签名
// 这里是简化示例
return "Bearer " + generateAccessToken();
}
/**
* 生成访问令牌
*/
private String generateAccessToken() {
// 实际实现需要调用阿里云获取token的API
return "your-access-token";
}
/**
* 保存MP3到文件
*/
public String saveMp3ToFile(String text, String outputPath) throws IOException {
byte[] mp3Data = synthesizeTextToMp3(text,"mp3");
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
fos.write(mp3Data);
}
return outputPath;
}
}

View File

@ -0,0 +1,32 @@
package com.fuyuanshen.app.http;
import java.io.IOException;
public class HttpTtsExample {
/**
* appKey: lbGuq5K5bEH4uxmT
* akId: LTAI5t66moCkhNC32TDJ5ReP
* akSecret: 2F3sdoBJ08bYvJcuDgSkLnJwGXsvYH
* @param args
*/
public static void main(String[] args) {
String accessKeyId = "LTAI5t66moCkhNC32TDJ5ReP";
String accessKeySecret = "2F3sdoBJ08bYvJcuDgSkLnJwGXsvYH";
String appKey = "lbGuq5K5bEH4uxmT";
try {
// 使用HTTP方式调用
HttpTtsClient httpClient = new HttpTtsClient(accessKeyId, accessKeySecret, appKey);
String text = "前方拥堵,请绕道而行。";
String outputPath = "D:\\http_output11.mp3";
String resultFile = httpClient.saveMp3ToFile(text, outputPath);
System.out.println("MP3音频已保存至: " + resultFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,58 @@
package com.fuyuanshen.app.http;
import okhttp3.*;
import java.io.IOException;
public class OkHttpTtsClient {
private OkHttpClient client = new OkHttpClient();
private String accessKeyId;
private String accessKeySecret;
private String appKey;
public OkHttpTtsClient(String accessKeyId, String accessKeySecret, String appKey) {
this.accessKeyId = accessKeyId;
this.accessKeySecret = accessKeySecret;
this.appKey = appKey;
}
/**
* 使用OkHttp调用TTS服务
*/
public byte[] synthesizeTextToMp3(String text) throws IOException {
MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String jsonPayload = String.format(
"{\"appkey\":\"%s\",\"text\":\"%s\",\"voice\":\"zhifeng\",\"format\":\"MP3\",\"sample_rate\":24000,\"volume\":50,\"speech_rate\":0,\"pitch_rate\":0}",
appKey,
text.replace("\"", "\\\"")
);
RequestBody body = RequestBody.create(JSON,jsonPayload);
Request request = new Request.Builder()
.url("https://nls-gateway.cn-shanghai.aliyuncs.com/v1/tts")
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer " + getAccessToken())
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("HTTP请求失败: " + response.code());
}
ResponseBody responseBody = response.body();
if (responseBody != null) {
return responseBody.bytes();
} else {
throw new IOException("响应体为空");
}
}
}
private String getAccessToken() {
// 实现获取访问令牌的逻辑
return "your-access-token";
}
}

View File

@ -0,0 +1,48 @@
package com.fuyuanshen.app.http;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* 获取阿里云TTS服务访问令牌
*/
public class TokenClient {
public static String getAccessToken(String accessKeyId, String accessKeySecret) throws IOException {
String endpoint = "https://nls-meta.cn-shanghai.aliyuncs.com/v1/token";
URL url = new URL(endpoint);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setDoOutput(true);
// 发送认证信息
String params = String.format("grant_type=client_credentials&client_id=%s&client_secret=%s",
accessKeyId, accessKeySecret);
try (OutputStream os = conn.getOutputStream()) {
os.write(params.getBytes("UTF-8"));
}
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
try (InputStream is = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
// 解析响应获取token实际需要使用JSON解析库
return response.toString().replaceAll(".*\"access_token\":\"([^\"]+)\".*", "$1");
}
} else {
throw new IOException("获取令牌失败,状态码: " + responseCode);
}
}
}

View File

@ -97,5 +97,4 @@ public class AppFileService {
}
return appBusinessFileService.deleteWithValidByIds(List.of(ids), true);
}
}

View File

@ -1,9 +1,30 @@
package com.fuyuanshen.app.service;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.fuyuanshen.app.domain.bo.AppDeviceBindRecordBo;
import com.fuyuanshen.app.domain.dto.AppAudioFileDto;
import com.fuyuanshen.app.domain.dto.AppFileRenameDto;
import com.fuyuanshen.app.domain.vo.AppDeviceBindRecordVo;
import com.fuyuanshen.app.http.HttpTtsClient;
import com.fuyuanshen.common.core.domain.R;
import com.fuyuanshen.common.satoken.utils.AppLoginHelper;
import com.fuyuanshen.common.satoken.utils.LoginHelper;
import com.fuyuanshen.equipment.domain.AppBusinessFile;
import com.fuyuanshen.equipment.domain.bo.AppBusinessFileBo;
import com.fuyuanshen.equipment.domain.vo.AppFileVo;
import com.fuyuanshen.equipment.mapper.AppBusinessFileMapper;
import com.fuyuanshen.equipment.service.IAppBusinessFileService;
import com.fuyuanshen.equipment.utils.AlibabaTTSUtil;
import com.fuyuanshen.equipment.utils.AudioProcessUtil;
import com.fuyuanshen.equipment.utils.FileHashUtil;
import com.fuyuanshen.equipment.utils.Mp3Duration;
import com.fuyuanshen.global.mqtt.utils.FfmpegVolumeUtil;
import com.fuyuanshen.global.mqtt.utils.FfmpegVolumeUtil3;
import com.fuyuanshen.system.domain.vo.SysOssVo;
import com.fuyuanshen.system.service.ISysOssService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@ -16,9 +37,12 @@ import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@ -39,6 +63,22 @@ public class AudioProcessService {
private final AudioProcessUtil audioProcessUtil;
private final AlibabaTTSUtil alibabaTTSUtil;
private final FileHashUtil fileHashUtil;
private final ISysOssService ossService;
private final IAppBusinessFileService appBusinessFileService;
private final AppBusinessFileMapper appBusinessFileMapper;
private final IAppDeviceBindRecordService appDeviceBindRecordService;
// String accessKeyId = "LTAI5t66moCkhNC32TDJ5ReP";
// String accessKeySecret = "2F3sdoBJ08bYvJcuDgSkLnJwGXsvYH";
// String appKey = "lbGuq5K5bEH4uxmT";
@Value("${alibaba.tts.appKey}")
private String appKey;
@Value("${alibaba.tts.akId}")
private String accessKeyId;
@Value("${alibaba.tts.akSecret}")
private String accessKeySecret;
/**
* 处理上传的音频文件
*/
@ -172,9 +212,32 @@ public class AudioProcessService {
fos.write(data);
}
return file.getAbsolutePath();
}
private String saveByteArrayToFile(InputStream inputStream, String filename) throws IOException {
// 确定保存路径(可以是临时目录或指定目录)
String directory = System.getProperty("java.io.tmpdir"); // 使用系统临时目录
File dir = new File(directory);
if (!dir.exists()) {
dir.mkdirs();
}
// 创建完整文件路径
File file = new File(dir, filename);
// 从输入流读取数据并写入文件
try (FileOutputStream fos = new FileOutputStream(file);
InputStream is = inputStream) {
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
return file.getAbsolutePath();
}
/**
@ -305,4 +368,234 @@ public class AudioProcessService {
}
public String uploadAudioToOss(AppAudioFileDto bo) {
MultipartFile file = bo.getFile();
// 校验文件格式和大小
validateAudioFileForRestful(file);
// 上传文件
// SysOssVo upload = sysOssService.upload(file);
String filename = file.getOriginalFilename();
String savedPath = null;
try {
String fileHash = fileHashUtil.hash(file);
SysOssVo upload = ossService.updateHash(file, fileHash);
// 强制将HTTP替换为HTTPS
if (upload.getUrl() != null && upload.getUrl().startsWith("http://")) {
upload.setUrl(upload.getUrl().replaceFirst("^http://", "https://"));
}
String fileSuffix = filename.substring(filename.lastIndexOf('.')).toLowerCase();
AppBusinessFileBo appBusinessFileBo = new AppBusinessFileBo();
appBusinessFileBo.setFileId(upload.getOssId());
appBusinessFileBo.setBusinessId(bo.getDeviceId());
appBusinessFileBo.setFileType(3L);
Long userId = getUserId(bo.getSource(), bo.getDeviceId());
appBusinessFileBo.setCreateBy(userId);
savedPath = saveByteArrayToFile(file.getInputStream(), generateRandomFileName(fileSuffix));
if (savedPath != null) {
log.info("MP3文件已保存: {}", savedPath);
Integer mp3Duration = Mp3Duration.getMp3Duration(savedPath);
log.info("MP3文件时长: {} 秒", mp3Duration);
appBusinessFileBo.setDuration(mp3Duration);
}
appBusinessFileService.insertByBo(appBusinessFileBo);
if (upload != null) {
return upload.getUrl();
}
} catch (Exception e){
log.error("上传音频文件失败", e);
}finally {
log.info("删除临时文件: {}", savedPath);
if(savedPath != null){
deleteTempFile(new File(savedPath));
}
}
return null;
}
private Long getUserId(String source,Long deviceId){
if("app".equals( source)){
return AppLoginHelper.getUserId();
} else if ("web".equals( source)){
// AppDeviceBindRecordBo appDeviceBindRecordBo = new AppDeviceBindRecordBo();
// appDeviceBindRecordBo.setDeviceId(deviceId);
//// appDeviceBindRecordBo.setCommunicationMode(0);
// List<AppDeviceBindRecordVo> appDeviceBindRecordVos = appDeviceBindRecordService.queryList(appDeviceBindRecordBo);
// if (appDeviceBindRecordVos != null && !appDeviceBindRecordVos.isEmpty()){
// userId = appDeviceBindRecordVos.get(0).getBindingUserId();
// }
}
return 0L;
}
/**
* 校验音频文件格式
*/
private void validateAudioFileForRestful(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
throw new IllegalArgumentException("文件名不能为空");
}
List<String> SUPPORTED_FORMATS = Arrays.asList(
".wav", ".mp3", ".pcm"
);
String ext = originalFilename.substring(originalFilename.lastIndexOf('.')).toLowerCase();
// 检查文件扩展名
if (!SUPPORTED_FORMATS.contains(ext)) {
throw new IllegalArgumentException("只允许上传MP3、WAV、PCM格式的音频文件");
}
long fileSize = 256 * 1024;
// 检查文件大小
if (file.getSize() > fileSize) {
throw new IllegalArgumentException("音频大小不能超过256kb");
}
}
/**
* 判断文件是否为支持的音频格式
*/
private boolean isSupportedFormat(String filename) {
if (filename == null || filename.lastIndexOf('.') == -1) {
return false;
}
String ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
return SUPPORTED_FORMATS.contains(ext);
}
public String textToSpeech(Long deviceId,String text, String fileSuffix,String source) {
//支持PCM/WAV/MP3格式
if (fileSuffix == null || fileSuffix.isEmpty()) {
fileSuffix = "mp3";
}
fileSuffix = fileSuffix.toLowerCase();
List<String> SUPPORTED_FORMATS = Arrays.asList(
"wav", "mp3", "pcm"
);
boolean contains = SUPPORTED_FORMATS.contains(fileSuffix);
if (!contains) {
throw new IllegalArgumentException("不支持的音频格式");
}
// String accessKeyId = "LTAI5t66moCkhNC32TDJ5ReP";
// String accessKeySecret = "2F3sdoBJ08bYvJcuDgSkLnJwGXsvYH";
// String appKey = "lbGuq5K5bEH4uxmT";
String savedPath = null;
String savedMp3VolumePath = null;
try {
// 使用HTTP方式调用
HttpTtsClient httpClient = new HttpTtsClient(accessKeyId, accessKeySecret, appKey);
//
byte[] mp3Data = httpClient.synthesizeTextToMp3(text,fileSuffix);
// byte[] mp3Data = alibabaTTSUtil.synthesizeTextToMp3(text,fileSuffix);
AppBusinessFileBo appBusinessFileBo = new AppBusinessFileBo();
savedPath = saveByteArrayToFile(mp3Data, generateRandomFileName(fileSuffix));
Integer mp3Duration = getMp3Duration2(savedPath);
appBusinessFileBo.setDuration(mp3Duration);
String directory = System.getProperty("java.io.tmpdir"); // 使用系统临时目录
String fileName = generateRandomFileName(fileSuffix);
savedMp3VolumePath = directory + "/" + fileName;
log.info("保存MP3文件: {}", savedMp3VolumePath);
FfmpegVolumeUtil.increaseMp3Volume(savedPath, savedMp3VolumePath, 8);
File file = new File(savedMp3VolumePath);
String fileHash = fileHashUtil.getFileHash(file,"SHA-256");
SysOssVo upload = ossService.updateHash(file, fileHash);
// 强制将HTTP替换为HTTPS
if (upload.getUrl() != null && upload.getUrl().startsWith("http://")) {
upload.setUrl(upload.getUrl().replaceFirst("^http://", "https://"));
}
appBusinessFileBo.setFileId(upload.getOssId());
appBusinessFileBo.setBusinessId(deviceId);
appBusinessFileBo.setFileType(3L);
Long userId = getUserId(source, deviceId);
appBusinessFileBo.setCreateBy(userId);
appBusinessFileService.insertByBo(appBusinessFileBo);
if (upload != null) {
return upload.getUrl();
}
} catch (Exception e){
log.error("上传音频文件失败", e);
} finally {
log.info("删除savedPath临时文件: {}", savedPath);
if(savedPath != null){
deleteTempFile(new File(savedPath));
}
if(savedMp3VolumePath != null){
log.info("删除savedMp3VolumePath临时文件: {}", savedMp3VolumePath);
deleteTempFile(new File(savedMp3VolumePath));
}
}
return null;
}
private Integer getMp3Duration2(String savedPath) {
if (savedPath != null) {
log.info("MP3文件已保存: {}", savedPath);
Integer mp3Duration = Mp3Duration.getMp3Duration(savedPath);
log.info("MP3文件时长: {} 秒", mp3Duration);
return mp3Duration;
}
return 0;
}
private static final Random random = new Random();
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
/**
* 生成纯随机文件名(无原文件名依赖)
*/
public static String generateRandomFileName(String extension) {
String timestamp = LocalDateTime.now().format(formatter);
int randomNum = random.nextInt(10000);
String uuidPart = UUID.randomUUID().toString().substring(0, 8);
if (!extension.startsWith(".")) {
extension = "." + extension;
}
return timestamp+extension;
}
public List<AppFileVo> queryAudioFileList(Long deviceId,String source) {
if(deviceId == null){
return null;
}
Long userId = getUserId(source, deviceId);
AppBusinessFileBo bo = new AppBusinessFileBo();
bo.setBusinessId(deviceId);
bo.setFileType(3L);
if("web".equals(source)){
bo.setCreateBy(null);
}else{
bo.setCreateBy(userId);
}
return appBusinessFileService.queryAppFileList(bo);
}
public R<Void> deleteAudioFile(Long id) {
UpdateWrapper<AppBusinessFile> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id",id);
appBusinessFileMapper.delete(updateWrapper);
return R.ok();
}
public R<Void> renameAudioFile(AppFileRenameDto bo) {
UpdateWrapper<AppBusinessFile> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id",bo.getId());
updateWrapper.set("re_name",bo.getFileName());
appBusinessFileMapper.update(updateWrapper);
return R.ok();
}
}

View File

@ -0,0 +1,10 @@
package com.fuyuanshen.global.Provider;
public interface AlarmDelayProvider {
/**
* 发送延迟检查任务
* @param alarmId 报警表主键ID
* @param delayInSeconds 延迟秒数(如 180
*/
void sendDelayCheck(Long alarmId, long delayInSeconds);
}

View File

@ -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(); // 释放后台任务和资源
}
}
}

View File

@ -1,9 +1,5 @@
package com.fuyuanshen.global.mqtt.base;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.util.Comparator;
@ -16,9 +12,9 @@ import java.util.List;
@Component
public class MqttRuleEngine {
@Autowired
@Qualifier("threadPoolTaskExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
// @Autowired
// @Qualifier("threadPoolTaskExecutor")
// private ThreadPoolTaskExecutor threadPoolTaskExecutor;
private final LinkedHashMap<String, MqttMessageRule> rulesMap = new LinkedHashMap<>();
@ -41,7 +37,8 @@ public class MqttRuleEngine {
int commandType = context.getCommandType();
MqttMessageRule mqttMessageRule = rulesMap.get("Light_" + commandType);
if (mqttMessageRule != null) {
threadPoolTaskExecutor.execute(() -> mqttMessageRule.execute(context));
// threadPoolTaskExecutor.execute(() -> mqttMessageRule.execute(context));
mqttMessageRule.execute(context);
return true;
}

View File

@ -0,0 +1,26 @@
package com.fuyuanshen.global.mqtt.base;
/**
* MQTT消息处理接口
*/
public interface NewMqttMessageRule {
/**
* 获取命令类型
* @return 命令类型
*/
String getCommandType();
/**
* 执行处理
* @param context 处理上下文
*/
void execute(NewMqttRuleContext context);
/**
* 获取优先级,数值越小优先级越高
* @return 优先级
*/
default int getPriority() {
return 0;
}
}

View File

@ -0,0 +1,25 @@
package com.fuyuanshen.global.mqtt.base;
import lombok.Data;
import java.util.Map;
/**
* MQTT消息处理上下文
*/
@Data
public class NewMqttRuleContext {
/**
* 命令类型
*/
private String commandType;
/**
* 设备IMEI
*/
private String deviceImei;
/**
* MQTT消息负载字典
*/
private Map<String, Object> payloadDict;
}

View File

@ -0,0 +1,44 @@
package com.fuyuanshen.global.mqtt.base;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
/**
* MQTT消息引擎
*/
@Component
public class NewMqttRuleEngine {
// @Autowired
// @Qualifier("threadPoolTaskExecutor")
// private ThreadPoolTaskExecutor threadPoolTaskExecutor;
private final LinkedHashMap<String, NewMqttMessageRule> rulesMap = new LinkedHashMap<>();
public NewMqttRuleEngine(List<NewMqttMessageRule> rules) {
// 按优先级排序
rules.sort(Comparator.comparing(NewMqttMessageRule::getPriority));
rules.forEach(rule -> rulesMap.put(rule.getCommandType(), rule)
);
}
/**
* 执行匹配
* @param context 处理上下文
* @return
*/
public boolean executeRule(NewMqttRuleContext context) {
String commandType = context.getCommandType();
// String funcType = context.getFuncType();
NewMqttMessageRule mqttMessageRule = rulesMap.get(commandType);
if (mqttMessageRule != null) {
// threadPoolTaskExecutor.execute(() -> mqttMessageRule.execute(context));
mqttMessageRule.execute(context);
return true;
}
return false;
}
}

View File

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

View File

@ -44,12 +44,12 @@ public class MqttInboundConfiguration {
if (url == null) {
throw new IllegalStateException("MQTT服务器URL未配置");
}
String subTopic = mqttPropertiesConfig.getSubTopic();
MqttPahoMessageDrivenChannelAdapter mqttPahoMessageDrivenChannelAdapter = new MqttPahoMessageDrivenChannelAdapter(
url,
clientId,
mqttPahoClientFactory,
mqttPropertiesConfig.getSubTopic().split(",")
subTopic.split(",")
);
mqttPahoMessageDrivenChannelAdapter.setQos(1);
mqttPahoMessageDrivenChannelAdapter.setConverter(new DefaultPahoMessageConverter());

View File

@ -47,7 +47,7 @@ public class MqttOutboundConfiguration {
mqttPahoClientFactory
);
mqttPahoMessageHandler.setDefaultQos(1);
mqttPahoMessageHandler.setDefaultTopic("B/#");
mqttPahoMessageHandler.setDefaultTopic(mqttPropertiesConfig.getPubTopic());
mqttPahoMessageHandler.setAsync(true);
return mqttPahoMessageHandler;
}

View File

@ -14,6 +14,8 @@ public class MqttPropertiesConfig {
private String url;
private String subClientId;
private String subTopic;
private String subTopic2;
private String pubClientId;
private String pubTopic;
private String pubTopic2;
}

View File

@ -0,0 +1,68 @@
//package com.fuyuanshen.global.mqtt.config;
//
//
//import cn.hutool.core.lang.UUID;
//import com.fuyuanshen.global.mqtt.receiver.NewReceiverMessageHandler;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.integration.annotation.ServiceActivator;
//import org.springframework.integration.channel.DirectChannel;
//import org.springframework.integration.core.MessageProducer;
//import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
//import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
//import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
//import org.springframework.messaging.MessageChannel;
//import org.springframework.messaging.MessageHandler;
//
//
//@Configuration
//@Slf4j
//public class NewMqttInboundConfiguration {
// @Autowired
// private MqttPropertiesConfig mqttPropertiesConfig;
// @Autowired
// private MqttPahoClientFactory mqttPahoClientFactory;
// @Autowired
// private NewReceiverMessageHandler receiverMessageHandler2;
// //消息通道
// @Bean
// public MessageChannel messageInboundChannel2(){
// return new DirectChannel();
// }
//
// /**
// * 配置入站适配器
// * 作用: 设置订阅主题,以及指定消息的通道 等相关属性
// * */
// @Bean
// public MessageProducer messageProducer2(){
// // 生成一个不重复的随机数
// String clientId = mqttPropertiesConfig.getSubClientId() + "_" + UUID.fastUUID();
// String subTopic = mqttPropertiesConfig.getSubTopic2();
// log.info("订阅主题:{}", subTopic);
// MqttPahoMessageDrivenChannelAdapter mqttPahoMessageDrivenChannelAdapter = new MqttPahoMessageDrivenChannelAdapter(
// mqttPropertiesConfig.getUrl(),
// clientId,
// mqttPahoClientFactory,
// subTopic.split(",")
// );
// mqttPahoMessageDrivenChannelAdapter.setQos(1);
// mqttPahoMessageDrivenChannelAdapter.setConverter(new DefaultPahoMessageConverter());
// mqttPahoMessageDrivenChannelAdapter.setOutputChannel(messageInboundChannel2());
// return mqttPahoMessageDrivenChannelAdapter;
// }
// /** 指定处理消息来自哪个通道 */
// @Bean
// @ServiceActivator(inputChannel = "messageInboundChannel2")
// public MessageHandler messageHandler2(){
// return receiverMessageHandler2;
// }
//
// // @Bean
// // @ServiceActivator(inputChannel = "messageInboundChannel") // 确保通道名称正确
// // public MessageHandler deviceAlarmMessageHandler() {
// // return new DeviceAlrmMessageHandler();
// // }
//}

View File

@ -0,0 +1,46 @@
//package com.fuyuanshen.global.mqtt.config;
//
//import cn.hutool.core.lang.UUID;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.integration.annotation.ServiceActivator;
//import org.springframework.integration.channel.DirectChannel;
//import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
//import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
//import org.springframework.messaging.MessageChannel;
//import org.springframework.messaging.MessageHandler;
//
//@Configuration
//@Slf4j
//public class NewMqttOutboundConfiguration {
// @Autowired
// private MqttPropertiesConfig mqttPropertiesConfig;
// @Autowired
// private MqttPahoClientFactory mqttPahoClientFactory;
//
// // 消息通道
// @Bean
// public MessageChannel mqttOutboundChannel2(){
// return new DirectChannel();
// }
//
//
// /** 配置出站消息处理器 */
// @Bean
// @ServiceActivator(inputChannel = "mqttOutboundChannel2") // 指定处理器针对哪个通道的消息进行处理
// public MessageHandler mqttOutboundMessageHandler2(){
// String clientId = mqttPropertiesConfig.getPubClientId() + "_" + UUID.fastUUID();
// MqttPahoMessageHandler mqttPahoMessageHandler = new MqttPahoMessageHandler(
// mqttPropertiesConfig.getUrl(),
// clientId,
// mqttPahoClientFactory
// );
// mqttPahoMessageHandler.setDefaultQos(1);
// mqttPahoMessageHandler.setDefaultTopic(mqttPropertiesConfig.getPubTopic2());
// mqttPahoMessageHandler.setAsync(true);
// return mqttPahoMessageHandler;
// }
//
//}

View File

@ -0,0 +1,69 @@
package com.fuyuanshen.global.mqtt.config;
import cn.hutool.core.lang.UUID;
//import com.fuyuanshen.global.mqtt.receiver.NewReceiverMessageHandler;
import com.fuyuanshen.global.mqtt.receiver.RegisEquipReceiverMessageHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@Configuration
@Slf4j
public class RegisEquipMqttInboundConfiguration {
@Autowired
private MqttPropertiesConfig mqttPropertiesConfig;
@Autowired
private MqttPahoClientFactory mqttPahoClientFactory;
@Autowired
private RegisEquipReceiverMessageHandler receiverMessageHandler3;
//消息通道
@Bean
public MessageChannel messageInboundChannel3(){
return new DirectChannel();
}
/**
* 配置入站适配器
* 作用: 设置订阅主题,以及指定消息的通道 等相关属性
* */
@Bean
public MessageProducer messageProducer3(){
// 生成一个不重复的随机数
String clientId = mqttPropertiesConfig.getSubClientId() + "_" + UUID.fastUUID();
String subTopic = mqttPropertiesConfig.getSubTopic2();
log.info("订阅主题:{}", subTopic);
MqttPahoMessageDrivenChannelAdapter mqttPahoMessageDrivenChannelAdapter = new MqttPahoMessageDrivenChannelAdapter(
mqttPropertiesConfig.getUrl(),
clientId,
mqttPahoClientFactory,
subTopic.split(",")
);
mqttPahoMessageDrivenChannelAdapter.setQos(1);
mqttPahoMessageDrivenChannelAdapter.setConverter(new DefaultPahoMessageConverter());
mqttPahoMessageDrivenChannelAdapter.setOutputChannel(messageInboundChannel3());
return mqttPahoMessageDrivenChannelAdapter;
}
/** 指定处理消息来自哪个通道 */
@Bean
@ServiceActivator(inputChannel = "messageInboundChannel3")
public MessageHandler messageHandler3(){
return receiverMessageHandler3;
}
// @Bean
// @ServiceActivator(inputChannel = "messageInboundChannel") // 确保通道名称正确
// public MessageHandler deviceAlarmMessageHandler() {
// return new DeviceAlrmMessageHandler();
// }
}

View File

@ -0,0 +1,46 @@
package com.fuyuanshen.global.mqtt.config;
import cn.hutool.core.lang.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@Configuration
@Slf4j
public class RegisEquipMqttOutboundConfiguration {
@Autowired
private MqttPropertiesConfig mqttPropertiesConfig;
@Autowired
private MqttPahoClientFactory mqttPahoClientFactory;
// 消息通道
@Bean
public MessageChannel mqttOutboundChannel3(){
return new DirectChannel();
}
/** 配置出站消息处理器 */
@Bean
@ServiceActivator(inputChannel = "mqttOutboundChannel3") // 指定处理器针对哪个通道的消息进行处理
public MessageHandler mqttOutboundMessageHandler3(){
String clientId = mqttPropertiesConfig.getPubClientId() + "_" + UUID.fastUUID();
MqttPahoMessageHandler mqttPahoMessageHandler = new MqttPahoMessageHandler(
mqttPropertiesConfig.getUrl(),
clientId,
mqttPahoClientFactory
);
mqttPahoMessageHandler.setDefaultQos(1);
mqttPahoMessageHandler.setDefaultTopic(mqttPropertiesConfig.getPubTopic2());
mqttPahoMessageHandler.setAsync(true);
return mqttPahoMessageHandler;
}
}

View File

@ -13,4 +13,5 @@ public interface MqttConstants {
*/
String GLOBAL_SUB_KEY = "A/";
String GLOBAL_PUB_KEY2 = "command/";
}

View File

@ -1,20 +1,22 @@
package com.fuyuanshen.global.mqtt.publish;
import cn.dev33.satoken.annotation.SaIgnore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/")
@RequestMapping("/api")
@SaIgnore
@Slf4j
public class DeviceDataController {
@Autowired
private MqttClientTest mqttClientTest;
// @PostMapping("/{deviceId}/command")
@GetMapping("/command")
public ResponseEntity<String> sendCommand() {
mqttClientTest.sendMsg();

View File

@ -13,10 +13,10 @@ public class MqttClientTest {
private MqttGateway mqttGateway;
public void sendMsg() {
mqttGateway.sendMsgToMqtt("worker/location/1", "hello mqtt spring boot");
mqttGateway.sendMsgToMqtt("command/894078/HBY670/864865082081523", "hello mqtt spring boot");
log.info("message is send");
mqttGateway.sendMsgToMqtt("worker/alert/2", "hello mqtt spring boot2");
mqttGateway.sendMsgToMqtt("report/894078/HBY670/864865082081523", "hello mqtt spring boot2");
log.info("message is send2");
}
}

View File

@ -0,0 +1,125 @@
//package com.fuyuanshen.global.mqtt.receiver;
//
//import cn.hutool.core.lang.Dict;
//import com.baomidou.lock.LockTemplate;
//import com.fuyuanshen.common.core.constant.GlobalConstants;
//import com.fuyuanshen.common.core.utils.StringUtils;
//import com.fuyuanshen.common.json.utils.JsonUtils;
//import com.fuyuanshen.common.redis.utils.RedisUtils;
//import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
//import com.fuyuanshen.global.mqtt.base.NewMqttRuleEngine;
//import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
//import com.fuyuanshen.global.queue.MqttMessageQueueConstants;
//import lombok.extern.slf4j.Slf4j;
//import org.redisson.api.RLock;
//import org.redisson.api.RedissonClient;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.messaging.Message;
//import org.springframework.messaging.MessageHandler;
//import org.springframework.messaging.MessageHeaders;
//import org.springframework.messaging.MessagingException;
//import org.springframework.stereotype.Service;
//
//import java.time.Duration;
//import java.util.Objects;
//import java.util.concurrent.TimeUnit;
//
//import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
//
//@Service
//@Slf4j
//public class NewReceiverMessageHandler implements MessageHandler {
//
// @Autowired
// private NewMqttRuleEngine newRuleEngine;
//
// @Override
// public void handleMessage(Message<?> message) throws MessagingException {
//
// Object payload = message.getPayload();
// MessageHeaders headers = message.getHeaders();
// String receivedTopic = Objects.requireNonNull(headers.get("mqtt_receivedTopic")).toString();
// String receivedQos = Objects.requireNonNull(headers.get("mqtt_receivedQos")).toString();
// String timestamp = Objects.requireNonNull(headers.get("timestamp")).toString();
//
// log.info("MQTT2 payload= {} \n receivedTopic = {} \n receivedQos = {} \n timestamp = {}",
// payload, receivedTopic, receivedQos, timestamp);
//
// Dict payloadDict = JsonUtils.parseMap(payload.toString());
// if (receivedTopic == null || payloadDict == null) {
// return;
// }
// String imei = payloadDict.getStr("imei");
// String funcType = payloadDict.getStr("funcType");
// // 执行业务逻辑
// if(StringUtils.isNotBlank(imei)){
// String queueKey = MqttMessageQueueConstants.MQTT_MESSAGE_QUEUE_KEY;
// String dedupKey = MqttMessageQueueConstants.MQTT_MESSAGE_DEDUP_KEY;
// RedisUtils.offerDeduplicated(queueKey,dedupKey,imei, Duration.ofSeconds(900));
// //在线状态
// String deviceOnlineStatusRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ imei + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX ;
// RedisUtils.setCacheObject(deviceOnlineStatusRedisKey, "1", Duration.ofSeconds(360));
// }
//
// String[] topicArr = receivedTopic.split("/");
//
// NewMqttRuleContext context = new NewMqttRuleContext();
// context.setCommandType(topicArr[2]+"_"+funcType);
// context.setDeviceImei(imei);
// context.setPayloadDict(payloadDict);
//
// boolean ruleExecuted = newRuleEngine.executeRule(context);
//
// if (!ruleExecuted) {
// log.warn("未找到匹配的规则来处理命令类型: {}", topicArr[2] + " : " +funcType);
// }
//// final LockInfo lockInfo = lockTemplate.lock(GlobalConstants.GLOBAL_REDIS_KEY + lockKey + imei, 100L, 3000L, RedissonLockExecutor.class);
//// if (null == lockInfo) {
//// log.info("MQTT3业务处理中,请稍后再试:funcType=>{},imei=>{}",funcType,imei);
//// return;
//// }
////// 获取锁成功,处理业务
//// try {
//// if(StringUtils.isNotBlank(imei)){
//// String queueKey = MqttMessageQueueConstants.MQTT_MESSAGE_QUEUE_KEY;
//// String dedupKey = MqttMessageQueueConstants.MQTT_MESSAGE_DEDUP_KEY;
//// RedisUtils.offerDeduplicated(queueKey,dedupKey,imei, Duration.ofSeconds(900));
//// //在线状态
//// String deviceOnlineStatusRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ imei + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX ;
//// RedisUtils.setCacheObject(deviceOnlineStatusRedisKey, "1", Duration.ofSeconds(360));
//// }
////
//// String[] topicArr = receivedTopic.split("/");
////
//// NewMqttRuleContext context = new NewMqttRuleContext();
//// context.setCommandType(topicArr[2]+"_"+funcType);
//// context.setDeviceImei(imei);
//// context.setPayloadDict(payloadDict);
////
//// boolean ruleExecuted = newRuleEngine.executeRule(context);
////
//// if (!ruleExecuted) {
//// log.warn("未找到匹配的规则来处理命令类型: {}", topicArr[2] + " : " +funcType);
//// }
//// } finally {
//// //释放锁
//// lockTemplate.releaseLock(lockInfo);
//// }
//
//
// /* ===== 追加:根据报文内容识别格式并统一解析 ===== */
//// int intType = MqttXinghanCommandType.computeVirtualCommandType(payloadDict);
//// if (intType > 0) {
//// MqttRuleContext newCtx = new MqttRuleContext();
//// String commandType = "Light_"+intType;
//// newCtx.setCommandType(commandType);
//// newCtx.setDeviceImei(imei);
//// newCtx.setPayloadDict(payloadDict);
////
//// boolean ok = ruleEngine.executeRule(newCtx);
//// if (!ok) {
//// log.warn("新规则引擎未命中, imei={}", imei);
//// }
//// }
// }
//}

View File

@ -6,12 +6,12 @@ import com.fuyuanshen.common.core.utils.ImageToCArrayConverter;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
import com.fuyuanshen.global.mqtt.base.MqttRuleEngine;
import com.fuyuanshen.global.mqtt.base.MqttXinghanCommandType;
import com.fuyuanshen.global.mqtt.base.*;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import com.fuyuanshen.global.queue.MqttMessageQueueConstants;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
@ -21,6 +21,7 @@ import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
@ -31,6 +32,9 @@ public class ReceiverMessageHandler implements MessageHandler {
@Autowired
private MqttRuleEngine ruleEngine;
@Autowired
private NewMqttRuleEngine newRuleEngine;
@Override
public void handleMessage(Message<?> message) throws MessagingException {
Object payload = message.getPayload();
@ -46,8 +50,28 @@ public class ReceiverMessageHandler implements MessageHandler {
if (receivedTopic == null || payloadDict == null) {
return;
}
// 模版格式匹配
boolean flag = checkTemplateFormatMatching(payloadDict);
String[] subStr = receivedTopic.split("/");
String deviceImei = subStr[1];
String deviceImei = null;
if(flag){
deviceImei = payloadDict.getStr("imei");
} else {
deviceImei = subStr[1];
}
RedissonClient client = RedisUtils.getClient();
String lockKey = "mqtt:consumer:lock:";
String KEY = GlobalConstants.GLOBAL_REDIS_KEY + lockKey + deviceImei;
RLock lock = client.getLock(KEY);
try {
// 尝试获取锁,
boolean isLocked = lock.tryLock(60, 3000, TimeUnit.MILLISECONDS);
if (isLocked) {
String state = payloadDict.getStr("state");
Object[] convertArr = ImageToCArrayConverter.convertByteStringToMixedObjectArray(state);
if(StringUtils.isNotBlank(deviceImei)){
@ -59,7 +83,22 @@ public class ReceiverMessageHandler implements MessageHandler {
RedisUtils.setCacheObject(deviceOnlineStatusRedisKey, "1", Duration.ofSeconds(360));
}
// 新的通信协议
if(flag){
String[] topicArr = receivedTopic.split("/");
String funcType = payloadDict.getStr("funcType");
NewMqttRuleContext context = new NewMqttRuleContext();
context.setCommandType(topicArr[2]+"_"+funcType);
context.setDeviceImei(deviceImei);
context.setPayloadDict(payloadDict);
boolean ruleExecuted = newRuleEngine.executeRule(context);
if (!ruleExecuted) {
log.warn("未找到匹配的规则来处理命令类型: {}", topicArr[2] + " : " +funcType);
}
return;
}
if (convertArr.length > 0) {
Byte val1 = (Byte) convertArr[0];
@ -74,6 +113,7 @@ public class ReceiverMessageHandler implements MessageHandler {
if (!ruleExecuted) {
log.warn("未找到匹配的规则来处理命令类型: {}", val1);
}
return;
}
/* ===== 追加:根据报文内容识别格式并统一解析 ===== */
@ -89,6 +129,31 @@ public class ReceiverMessageHandler implements MessageHandler {
log.warn("新规则引擎未命中, imei={}", deviceImei);
}
}
}else{
log.warn("MQTT获取锁失败请稍后再试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private boolean checkTemplateFormatMatching(Dict payloadDict) {
// 检查是否包含指定的 key
boolean hasImei = payloadDict.containsKey("imei");
boolean hasFuncType = payloadDict.containsKey("funcType");
boolean hasStatus = payloadDict.containsKey("status");
boolean hasTimestamp = payloadDict.containsKey("timestamp");
boolean hasData = payloadDict.containsKey("data");
// 输出检查结果
log.info("包含 imei: {}, funcType: {}, status: {}, timestamp: {}, data: {}",
hasImei, hasFuncType, hasStatus, hasTimestamp, hasData);
return hasImei && hasFuncType && hasStatus && hasTimestamp && hasData;
}
}

View File

@ -0,0 +1,156 @@
package com.fuyuanshen.global.mqtt.receiver;
import cn.hutool.core.lang.Dict;
import cn.hutool.json.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.equipment.domain.Device;
import com.fuyuanshen.equipment.domain.DeviceType;
import com.fuyuanshen.equipment.domain.form.DeviceForm;
import com.fuyuanshen.equipment.domain.query.DeviceTypeQueryCriteria;
import com.fuyuanshen.equipment.mapper.DeviceMapper;
import com.fuyuanshen.equipment.mapper.DeviceTypeMapper;
import com.fuyuanshen.equipment.service.DeviceService;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleEngine;
import com.fuyuanshen.global.mqtt.config.MqttGateway;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import com.fuyuanshen.global.queue.MqttMessageQueueConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Objects;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 注册设备消息接收处理
*/
@Service
@Slf4j
public class RegisEquipReceiverMessageHandler implements MessageHandler {
@Autowired
private NewMqttRuleEngine newRuleEngine;
@Autowired
private MqttGateway mqttGateway;
@Autowired
private DeviceMapper deviceMapper;
@Autowired
private DeviceService deviceService;
@Autowired
private DeviceTypeMapper deviceTypeMapper;
@Override
public void handleMessage(Message<?> message) throws MessagingException {
Object payload = message.getPayload();
MessageHeaders headers = message.getHeaders();
String receivedTopic = Objects.requireNonNull(headers.get("mqtt_receivedTopic")).toString();
// 只处理 regis/equip/# 主题的消息
if (!receivedTopic.startsWith("regis/equip/")) {
return;
}
// 从主题中提取设备ID
String[] topicParts = receivedTopic.split("/");
if (topicParts.length < 3) {
log.warn("Invalid topic format: {}", receivedTopic);
return;
}
String deviceTypeName = topicParts[2]; // HBY100-J
String receivedQos = Objects.requireNonNull(headers.get("mqtt_receivedQos")).toString();
String timestamp = Objects.requireNonNull(headers.get("timestamp")).toString();
log.info("MQTT3 payload= {} \n receivedTopic = {} \n receivedQos = {} \n timestamp = {}",
payload, receivedTopic, receivedQos, timestamp);
// 解析JSON payload获取imei和mac
Dict payloadDict = JsonUtils.parseMap(payload.toString());
if (payloadDict == null) {
log.warn("Failed to parse payload JSON");
return;
}
String imei = payloadDict.getStr("imei");
String mac = payloadDict.getStr("mac");
log.info("Extracted IMEI: {}, MAC: {}", imei, mac);
// 验证必要字段
if (StringUtils.isEmpty(imei) || StringUtils.isEmpty(mac)) {
log.warn("Missing required fields - IMEI: {}, MAC: {}", imei, mac);
return;
}
DeviceTypeQueryCriteria criteria = new DeviceTypeQueryCriteria();
criteria.setTypeName(deviceTypeName);
DeviceType deviceType = deviceTypeMapper.queryByName(criteria);
if (deviceType == null) {
log.warn("Device type not found for name: {}", deviceTypeName);
return;
}
Device device = deviceMapper.selectDeviceByImei(imei);
if (device != null) {
log.warn("Device already exists for IMEI: {}", imei);
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
imei + ":regis";
Object value = RedisUtils.getCacheObject(redisKey);
if (value != null) {
log.warn("regis/imei发送次数频率过快: {}", imei);
return;
}
// sendSuccessMessage(imei);
RedisUtils.setCacheObject(redisKey, payload.toString(), Duration.ofSeconds(10));
return;
}
// 提取MAC地址后6位
String macSuffix = mac.replaceAll(":", "").substring(6); // 43:73:43:33:53:33 -> 335333
// 构建设备名称和蓝牙名称:设备类型+MAC后6位
String deviceName = deviceTypeName + "-" + macSuffix; // HBY100-335333
DeviceForm deviceForm = new DeviceForm();
deviceForm.setDeviceImei(imei);
deviceForm.setDeviceMac(mac);
deviceForm.setDeviceName(deviceName);
deviceForm.setBluetoothName(deviceName);
deviceForm.setDeviceType(deviceType.getId());
try {
deviceService.addDevice(deviceForm);
} catch (Exception e) {
throw new RuntimeException(e);
}
sendSuccessMessage(imei);
}
private void sendSuccessMessage(String imei) {
// 解析原始JSON数据
JSONObject originalData = new JSONObject();
originalData.put("code", 200);
// 发送到MQTT
String topic = "regis/" + imei;
mqttGateway.sendMsgToMqtt(topic, originalData.toString());
}
}

View File

@ -67,7 +67,6 @@ public class BjqActiveReportingDeviceDataRule implements MqttMessageRule {
*/
public void asyncSendDeviceDataToRedisWithFuture(String deviceImei, String mainLightMode, String laserLightMode,
String batteryPercentage, String chargeState, String batteryRemainingTime) {
CompletableFuture.runAsync(() -> {
try {
// 构造设备状态信息对象
Map<String, Object> deviceInfo = new LinkedHashMap<>();
@ -91,7 +90,6 @@ public class BjqActiveReportingDeviceDataRule implements MqttMessageRule {
} catch (Exception e) {
log.error("异步发送设备信息到Redis时出错: device={}, error={}", deviceImei, e.getMessage(), e);
}
});
}

View File

@ -121,7 +121,6 @@ public class BjqLocationDataRule implements MqttMessageRule {
* @param longitude 经度
*/
public void asyncSendLocationToRedisWithFuture(String deviceImei, String latitude, String longitude) {
CompletableFuture.runAsync(() -> {
try {
if (StringUtils.isBlank(latitude) || StringUtils.isBlank(longitude)) {
return;
@ -177,7 +176,6 @@ public class BjqLocationDataRule implements MqttMessageRule {
} catch (Exception e) {
log.error("异步发送位置信息到Redis时出错: device={}, error={}", deviceImei, e.getMessage(), e);
}
});
}
@ -189,7 +187,6 @@ public class BjqLocationDataRule implements MqttMessageRule {
* @param longitude 经度
*/
public void asyncSaveLocationToMySQLWithFuture(String deviceImei, String latitude, String longitude) {
CompletableFuture.runAsync(() -> {
try {
if (StringUtils.isBlank(latitude) || StringUtils.isBlank(longitude)) {
return;
@ -202,7 +199,6 @@ public class BjqLocationDataRule implements MqttMessageRule {
} catch (Exception e) {
log.error("异步保存位置信息到MySQL时出错: device={}, error={}", deviceImei, e.getMessage(), e);
}
});
}

View File

@ -50,6 +50,8 @@ public class BjqModeRule implements MqttMessageRule {
public void execute(MqttRuleContext context) {
String functionAccess = FUNCTION_ACCESS_KEY + context.getDeviceImei();
try {
log.info("处理灯光模式命令");
log.info("MQTT消息负载字典{}", context.getPayloadDict());
Object[] convertArr = context.getConvertArr();
String mainLightMode = convertArr[1].toString();

View File

@ -34,8 +34,8 @@ public class BjqPersonnelInfoDataRule implements MqttMessageRule {
public void execute(MqttRuleContext context) {
String functionAccess = FUNCTION_ACCESS_KEY + context.getDeviceImei();
try {
Object[] convertArr = context.getConvertArr();
RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.OK.getCode(), Duration.ofSeconds(30));
// Object[] convertArr = context.getConvertArr();
// RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.OK.getCode(), Duration.ofSeconds(30));
} catch (Exception e) {
log.error("处理定位数据命令时出错", e);
RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.FAILED.getCode(), Duration.ofSeconds(30));

View File

@ -0,0 +1,43 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 爆闪模式开启/关闭
*/
@Slf4j
@Component
public class FuncType10StrobeMode implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_10";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J爆闪模式开启/关闭,消息负载:{}", context.getPayloadDict());
try {
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":strobeMode";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
} catch (Exception e) {
log.error("HBY100J爆闪模式开启/关闭失败", e);
}
}
}

View File

@ -0,0 +1,43 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 修改警示灯爆闪频率
*/
@Slf4j
@Component
public class FuncType11Frequency implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_11";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J修改警示灯爆闪频率消息负载{}", context.getPayloadDict());
try {
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":frequency";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
} catch (Exception e) {
log.error("HBY100J修改警示灯爆闪频率失败", e);
}
}
}

View File

@ -0,0 +1,45 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 强制声光报警开启/关闭
*/
@Slf4j
@Component
public class FuncType12ForceAudio implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_12";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J强制声光报警开启/关闭,消息负载:{}", context.getPayloadDict());
try {
// 构建强制声光报警开关的Redis键
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":forceAudio";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
// 存储强制声光报警开关状态到Redis
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
} catch (Exception e) {
log.error("HBY100J强制声光报警开启/关闭失败", e);
}
}
}

View File

@ -0,0 +1,43 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 警示灯LED亮度调节
*/
@Slf4j
@Component
public class FuncType13Brightness implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_13";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J警示灯LED亮度调节消息负载{}", context.getPayloadDict());
try {
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":brightness";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
} catch (Exception e) {
log.error("HBY100J警示灯LED亮度调节失败", e);
}
}
}

View File

@ -0,0 +1,43 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 获取警示灯的当前工作方式(设备下发返回响应数据、设备定时主动上报)
*/
@Slf4j
@Component
public class FuncType14Report implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_14";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J设备定时主动上报消息负载{}", context.getPayloadDict());
try {
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":report";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
} catch (Exception e) {
log.error("HBY100J设备定时主动上报失败", e);
}
}
}

View File

@ -0,0 +1,46 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 设备复位
*/
@Slf4j
@Component
public class FuncType1Rest implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_1";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("开始处理设备复位,消息负载:{}", context.getPayloadDict());
try {
// 构建强制声光报警开关的Redis键
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":rest";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
// 存储强制声光报警开关状态到Redis
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
log.info("设备复位设备ID{}", context.getDeviceImei());
} catch (Exception e) {
log.error("处理设备复位失败", e);
}
}
}

View File

@ -0,0 +1,46 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 获取设备基础信息
*/
@Slf4j
@Component
public class FuncType2BaseInfo implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_2";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("开始处理强制声光报警开关,消息负载:{}", context.getPayloadDict());
try {
// 构建强制声光报警开关的Redis键
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":force_audio_visual_alarm_switch";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
// 存储强制声光报警开关状态到Redis
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
log.info("强制声光报警开关处理完成设备ID{}", context.getDeviceImei());
} catch (Exception e) {
log.error("处理强制声光报警开关失败", e);
}
}
}

View File

@ -0,0 +1,208 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.equipment.utils.map.GetAddressFromLatUtil;
import com.fuyuanshen.equipment.utils.map.LngLonUtil;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import com.fuyuanshen.global.mqtt.rule.hby100j.domin.FunctionType3LocationReport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_LOCATION_KEY_PREFIX;
/**
* 获取设备位置信息(设备下发返回响应数据、设备定时主动上报)
*/
@Slf4j
@Component
public class FuncType3Location implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_3";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J获取设备位置信息消息负载{}", context.getPayloadDict());
try {
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":location";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
// RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
String jsonString = JSONObject.toJSONString(payloadDict);
FunctionType3LocationReport data = JSONObject.parseObject(jsonString, FunctionType3LocationReport.class);
FunctionType3LocationReport.Data data1 = data.getData();
if (data1 != null) {
Double latitude = data1.getLatitude();
Double longitude = data1.getLongitude();
asyncSendLocationToRedisWithFuture(context.getDeviceImei(), latitude.toString(), longitude.toString());
}
}
} catch (Exception e) {
log.error("HBY100J获取设备位置信息失败", e);
}
}
/** 位置未发生明显变化的距离阈值(米),可通过配置中心动态调整 */
private final double MOVEMENT_THRESHOLD_METER = 10.0;
/**
* 异步发送位置信息到Redis使用CompletableFuture
*
* @param deviceImei 设备IMEI
* @param latitude 纬度
* @param longitude 经度
*/
public void asyncSendLocationToRedisWithFuture(String deviceImei, String latitude, String longitude) {
try {
if (latitude == null || longitude == null) {
return;
}
if (StringUtils.isAnyBlank(deviceImei, latitude, longitude)) {
log.warn("位置上报参数为空deviceImei={}", deviceImei);
return;
}
//log.info("位置上报deviceImei={}, lat={}, lon={}", deviceImei, latitude, longitude);
// 1. 解析当前上报的经纬度
Double curLat = parseDoubleSafe(latitude.trim());
Double curLon = parseDoubleSafe(longitude.trim());
if (curLat == null || curLon == null) {
log.warn("经纬度格式错误直接更新deviceImei={}, lat={}, lon={}", deviceImei, latitude, longitude);
// doSaveLocation(deviceImei, latitude, longitude);
return;
}
// 2. 读取 Redis 中缓存的上一次位置
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX;
String cachedJson = RedisUtils.getCacheObject(redisKey);
if (StringUtils.isNotBlank(cachedJson)) {
com.alibaba.fastjson2.JSONObject cachedObj = com.alibaba.fastjson2.JSONObject.parseObject(cachedJson);
String cachedWgs84Lat = cachedObj.getString("wgs84_latitude");
String cachedWgs84Lon = cachedObj.getString("wgs84_longitude");
Double oldLat = parseDoubleSafe(cachedWgs84Lat);
Double oldLon = parseDoubleSafe(cachedWgs84Lon);
if (oldLat != null && oldLon != null) {
double distance = haversine(oldLat, oldLon, curLat, curLon);
if (distance <= MOVEMENT_THRESHOLD_METER) {
log.info("位置未发生明显变化({}米 <= {}米),不更新 RedisdeviceImei={}, lat={}, lon={}",
distance, MOVEMENT_THRESHOLD_METER, deviceImei, latitude, longitude);
return;
}
}
}
// String[] latArr = latitude.split("\\.");
// String[] lonArr = longitude.split("\\.");
// // 将位置信息存储到Redis中
// String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX;
// String redisObj = RedisUtils.getCacheObject(redisKey);
// JSONObject jsonOBj = JSONObject.parseObject(redisObj);
// if(jsonOBj != null){
// String str1 = latArr[0] +"."+ latArr[1].substring(0,4);
// String str2 = lonArr[0] +"."+ lonArr[1].substring(0,4);
//
// String cacheLatitude = jsonOBj.getString("wgs84_latitude");
// String cacheLongitude = jsonOBj.getString("wgs84_longitude");
// String[] latArr1 = cacheLatitude.split("\\.");
// String[] lonArr1 = cacheLongitude.split("\\.");
//
// String cacheStr1 = latArr1[0] +"."+ latArr1[1].substring(0,4);
// String cacheStr2 = lonArr1[0] +"."+ lonArr1[1].substring(0,4);
// if(str1.equals(cacheStr1) && str2.equals(cacheStr2)){
// log.info("位置信息未发生变化: device={}, lat={}, lon={}", deviceImei, latitude, longitude);
// return;
// }
// }
// 构造位置信息对象
Map<String, Object> locationInfo = new LinkedHashMap<>();
double[] doubles = LngLonUtil.gps84_To_Gcj02(Double.parseDouble(latitude), Double.parseDouble(longitude));
locationInfo.put("deviceImei", deviceImei);
locationInfo.put("latitude", doubles[0]);
locationInfo.put("longitude", doubles[1]);
locationInfo.put("wgs84_latitude", latitude);
locationInfo.put("wgs84_longitude", longitude);
String address = GetAddressFromLatUtil.getAdd(String.valueOf(doubles[1]), String.valueOf(doubles[0]));
locationInfo.put("address", address);
locationInfo.put("timestamp", System.currentTimeMillis());
String locationJson = JsonUtils.toJsonString(locationInfo);
// 存储到Redis
RedisUtils.setCacheObject(redisKey, locationJson);
// 存储到一个列表中,保留历史位置信息
// String locationHistoryKey = GlobalConstants.GLOBAL_REDIS_KEY+DeviceRedisKeyConstants.DEVICE_LOCATION_HISTORY_KEY_PREFIX + deviceImei;
// RedisUtils.addCacheList(locationHistoryKey, locationJson);
// RedisUtils.expire(locationHistoryKey, Duration.ofDays(90));
storeDeviceTrajectoryWithSortedSet(deviceImei, locationJson);
log.info("位置信息已异步发送到Redis: device={}, lat={}, lon={}", deviceImei, latitude, longitude);
} catch (Exception e) {
log.error("异步发送位置信息到Redis时出错: device={}, error={}", deviceImei, e.getMessage(), e);
}
}
/** 安全解析 double解析失败返回 null */
private Double parseDoubleSafe(String str) {
if (StringUtils.isBlank(str)) return null;
try {
return Double.parseDouble(str.trim());
} catch (NumberFormatException e) {
return null;
}
}
/** Haversine 公式计算两点球面距离(米) */
private double haversine(double lat1, double lon1, double lat2, double lon2) {
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
/** 地球平均半径(米) */
double EARTH_RADIUS = 6371_393.0;
return EARTH_RADIUS * c;
}
/**
* 存储设备30天历史轨迹到Redis (使用Sorted Set)
*/
public void storeDeviceTrajectoryWithSortedSet(String deviceImei, String locationJson) {
try {
String trajectoryKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DeviceRedisKeyConstants.DEVICE_LOCATION_HISTORY_KEY_PREFIX;
// String trajectoryKey = "device:trajectory:zset:" + deviceImei;
// String locationJson = JsonUtils.toJsonString(locationInfo);
long timestamp = System.currentTimeMillis();
// 添加到Sorted Set使用时间戳作为score
RedisUtils.zAdd(trajectoryKey, locationJson, timestamp);
// // 设置30天过期时间
// RedisUtils.expire(trajectoryKey, Duration.ofDays(30));
// 清理30天前的数据冗余保护
long thirtyDaysAgo = System.currentTimeMillis() - (7L * 24 * 60 * 60 * 1000);
RedisUtils.zRemoveRangeByScore(trajectoryKey, 0, thirtyDaysAgo);
} catch (Exception e) {
log.error("存储设备轨迹到Redis(ZSet)失败: device={}, error={}", deviceImei, e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,69 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import com.fuyuanshen.global.mqtt.rule.hby100j.domin.FunctionType4PowerStatusReport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_STATUS_KEY_PREFIX;
/**
* 获取设备电源状态(设备下发返回响应数据、设备定时主动上报)
*/
@Slf4j
@Component
public class FuncType4PowerStatus implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_4";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J获取设备电源状态消息负载{}", context.getPayloadDict());
try {
String deviceStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + context.getDeviceImei() + DEVICE_STATUS_KEY_PREFIX);
// String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
// context.getDeviceImei() + ":powerStatus";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
String str = JSONObject.toJSONString(payloadDict);
FunctionType4PowerStatusReport powerStatusReport =JSONObject.parseObject(str,FunctionType4PowerStatusReport.class);
FunctionType4PowerStatusReport.Data data = powerStatusReport.getData();
if(data != null){
// 构造设备状态信息对象
Map<String, Object> deviceInfo = new LinkedHashMap<>();
deviceInfo.put("batteryPercentage", data.getLevel());
deviceInfo.put("chargeState", data.getCharge());
deviceInfo.put("batteryRemainingTime", data.getBatteryRemainingTime());
// 将设备状态信息存储到Redis中
String deviceRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DeviceRedisKeyConstants.DEVICE_KEY_PREFIX + context.getDeviceImei() + DEVICE_STATUS_KEY_PREFIX;
String deviceInfoJson = JsonUtils.toJsonString(deviceInfo);
// 存储到Redis
RedisUtils.setCacheObject(deviceRedisKey, deviceInfoJson);
}
}
} catch (Exception e) {
log.error("HBY100J获取设备电源状态失败", e);
}
}
}

View File

@ -0,0 +1,43 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 更新语音
*/
@Slf4j
@Component
public class FuncType5UpdateVoice implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_5";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J更新语音消息负载{}", context.getPayloadDict());
try {
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":updateVoice";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
} catch (Exception e) {
log.error("HBY100J更新语音失败", e);
}
}
}

View File

@ -0,0 +1,43 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 语音播报开启/关闭
*/
@Slf4j
@Component
public class FuncType6VoicePlay implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_6";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J语音播报开启/关闭,消息负载:{}", context.getPayloadDict());
try {
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":voicePlay";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
} catch (Exception e) {
log.error("HBY100J语音播报开启/关闭失败", e);
}
}
}

View File

@ -0,0 +1,43 @@
package com.fuyuanshen.global.mqtt.rule.hby100j;
import com.alibaba.fastjson2.JSONObject;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.NewMqttMessageRule;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 修改音量
*/
@Slf4j
@Component
public class FuncType9UpdateVolume implements NewMqttMessageRule {
@Override
public String getCommandType() {
return "HBY100_9";
}
@Override
public void execute(NewMqttRuleContext context) {
log.info("HBY100J修改音量消息负载{}", context.getPayloadDict());
try {
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
context.getDeviceImei() + ":updateVolume";
Map<String, Object> payloadDict = context.getPayloadDict();
if (payloadDict != null) {
RedisUtils.setCacheObject(redisKey, JSONObject.toJSONString(payloadDict));
}
} catch (Exception e) {
log.error("HBY100J修改音量失败", e);
}
}
}

View File

@ -0,0 +1,58 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 爆闪模式开启/关闭
* 对应 funcType="10"
* mode:
* 0 红色爆闪1 蓝色爆闪2 黄色爆闪3红色顺时针旋转爆闪4黄色顺时针旋转爆闪5红蓝顺时针旋转爆闪6 红蓝交替爆闪
*/
@Data
public class FuncType10StrobeModeRequest {
/**
* 请求唯一标识符(用于链路追踪或幂等处理)
*/
@JsonProperty("requestId")
private String requestId;
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码
*/
private String funcType;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 语音配置数据
*/
private Data data;
/**
* 语音配置详情内部类
*/
@lombok.Data
public static class Data {
/**
* 爆闪模式是否启用
* 0 - 禁用
* 1 - 启用
*/
private Integer enable;
/**
* 爆闪模式类型 0 红色爆闪1 蓝色爆闪2 黄色爆闪3红色顺时针旋转爆闪4黄色顺时针旋转爆闪5红蓝顺时针旋转爆闪6 红蓝交替爆闪
**/
private Integer mode;
}
}

View File

@ -0,0 +1,47 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 修改警示灯爆闪频率
* 对应 funcType="11"
*/
@Data
public class FuncType11FrequencyRequest {
/**
* 请求唯一标识符(用于链路追踪或幂等处理)
*/
@JsonProperty("requestId")
private String requestId;
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码
*/
private String funcType;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 语音配置数据
*/
private Data data;
/**
* 语音配置详情内部类
*/
@lombok.Data
public static class Data {
//"frequency": 1-12
private Integer frequency;
}
}

View File

@ -0,0 +1,52 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 强制声光报警开启/关闭
* 对应 funcType="12"
*/
@Data
public class FuncType12ForceAudioRequest {
/**
* 请求唯一标识符(用于链路追踪或幂等处理)
*/
@JsonProperty("requestId")
private String requestId;
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码
*/
private String funcType;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 语音配置数据
*/
private Data data;
/**
* 语音配置详情内部类
*/
@lombok.Data
public static class Data {
/**
* 语音报警0关闭,1开启
*/
@JsonProperty("voice_strobe_alarm")
private Integer voiceStrobeAlarm;
//语音模式0公安,1消防,2应急,3交警4市政5铁路6医疗7部队8水利
private Integer mode;
}
}

View File

@ -0,0 +1,59 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 警示灯LED亮度调节
* 对应 funcType="13"
*/
@Data
public class FuncType13BrightnessRequest {
/**
* 请求唯一标识符(用于链路追踪或幂等处理)
*/
@JsonProperty("requestId")
private String requestId;
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码
*/
private String funcType;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 语音配置数据
*/
private Data data;
/**
* 语音配置详情内部类
*/
@lombok.Data
public static class Data {
/**
* 红色LED亮度值0-100
*/
private Integer red;
/**
* 蓝色LED亮度值0-100
*/
private Integer blue;
/**
* 黄色LED亮度值0-100
*/
private Integer yellow;
}
}

View File

@ -0,0 +1,143 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 设备状态上报数据DTO
* 对应MQTT消息中设备上报的完整状态数据结构
*/
@Data
public class FuncType14StatusReport {
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码(如"14"表示特定功能指令)
*/
private String funcType;
/**
* 响应状态码(如"200"表示成功)
*/
private String status;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 设备详细状态数据
*/
private Data data;
/**
* 设备详细状态数据内部类
*/
@lombok.Data
public static class Data {
/**
* 语音播报开关0-关闭1-开启
*/
@JsonProperty("voice_broadcast")
private Integer voiceBroadcast;
/**
* 警报器(警笛)设置
*/
@JsonProperty("siren_alarm")
private SirenAlarm sirenAlarm;
/**
* LED爆闪设置
*/
@JsonProperty("led_strobe")
private LedStrobe ledStrobe;
/**
* 音量设置0-100范围
*/
@JsonProperty("volume")
private Integer volume;
/**
* 亮度设置RGB三色亮度值
*/
@JsonProperty("brightness")
private Brightness brightness;
}
/**
* 警报器(警笛)设置
*/
@lombok.Data
public static class SirenAlarm {
/**
* 是否启用0-禁用1-启用
*/
@JsonProperty("enable")
private Integer enable;
/**
* 工作模式0-默认模式,其他值表示不同报警模式
*/
@JsonProperty("mode")
private Integer mode;
}
/**
* LED爆闪设置
*/
@lombok.Data
public static class LedStrobe {
/**
* 是否启用0-禁用1-启用
*/
@JsonProperty("enable")
private Integer enable;
/**
* 工作模式0-默认模式,其他值表示不同爆闪模式
*/
@JsonProperty("mode")
private Integer mode;
/**
* 爆闪频率单位Hz或自定义单位
*/
@JsonProperty("frequency")
private Integer frequency;
}
/**
* 亮度设置RGB三色
*/
@lombok.Data
public static class Brightness {
/**
* 红色通道亮度值0-255或百分比
*/
@JsonProperty("red")
private Integer red;
/**
* 绿色通道亮度值0-255或百分比
*/
@JsonProperty("green")
private Integer green;
/**
* 蓝色通道亮度值0-255或百分比
*/
@JsonProperty("blue")
private Integer blue;
}
}

View File

@ -0,0 +1,59 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 更新设备语音
*/
@Data
public class FuncType5UpdateVoiceReport {
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码("5" 表示操作状态上报)
*/
private String funcType;
/**
* 响应状态码("200" 表示请求成功)
*/
private String status;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 操作详细状态数据
*/
private Data data;
/**
* 操作状态数据内部类
*/
@lombok.Data
public static class Data {
/**
* 操作执行状态:
* 0 - 未开始 / 失败
* 1 - 执行中
* 2 - 成功完成
* (具体含义需结合业务协议定义)
*/
@JsonProperty("status")
private Integer status;
/**
* 操作进度百分比0-100仅在 status=1执行中时有效
*/
@JsonProperty("progress")
private Integer progress;
}
}

View File

@ -0,0 +1,62 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 语音资源配置请求DTO
* 对应 funcType="5" 的语音资源下发指令(如语音播报文件配置)
*/
@Data
public class FuncType5UpdateVoiceRequest {
/**
* 请求唯一标识符(用于链路追踪或幂等处理)
*/
@JsonProperty("requestId")
private String requestId;
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码("5" 表示语音资源配置)
*/
private String funcType;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 语音配置数据
*/
private Data data;
/**
* 语音配置详情内部类
*/
@lombok.Data
public static class Data {
/**
* 语音类型:
* 0 - 默认语音(如标准提示音)
* 1 - 自定义语音
* 2 - 紧急语音
* (具体含义需依据设备协议定义)
*/
@JsonProperty("voice_type")
private Integer voiceType;
/**
* 语音资源URLMP3等音频文件地址
* 示例http://8.129.5.250:10001/voice/01.mp3
*/
@JsonProperty("voice_resource")
private String voiceResource;
}
}

View File

@ -0,0 +1,50 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 语音播报开启/关闭
* 对应 funcType="6"
*/
@Data
public class FuncType6VoicePlayRequest {
/**
* 请求唯一标识符(用于链路追踪或幂等处理)
*/
@JsonProperty("requestId")
private String requestId;
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码
*/
private String funcType;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 语音配置数据
*/
private Data data;
/**
* 语音配置详情内部类
*/
@lombok.Data
public static class Data {
/**
* 语音报警0关闭,1开启
*/
@JsonProperty("voice_broadcast")
private Integer voiceBroadcast;
}
}

View File

@ -0,0 +1,47 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 修改音量
* 对应 funcType="9"
*/
@Data
public class FuncType9UpdateVolumeRequest {
/**
* 请求唯一标识符(用于链路追踪或幂等处理)
*/
@JsonProperty("requestId")
private String requestId;
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码
*/
private String funcType;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 语音配置数据
*/
private Data data;
/**
* 语音配置详情内部类
*/
@lombok.Data
public static class Data {
//"volume": 1-100(app端可根据需求把40作为低音量, 70作为中音量100作为高音量)
private Integer volume;
}
}

View File

@ -0,0 +1,56 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 设备位置上报DTO
* 对应MQTT消息中设备上报的地理位置数据
*/
@Data
public class FunctionType3LocationReport {
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码(如"3"表示位置上报)
*/
private String funcType;
/**
* 响应状态码(如"200"表示成功)
*/
private String status;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 位置详细数据
*/
private Data data;
/**
* 位置详细数据内部类
*/
@lombok.Data
public static class Data {
/**
* 经度WGS-84坐标系如22.543100
*/
@JsonProperty("longitude")
private Double longitude;
/**
* 纬度WGS-84坐标系如114.057900
*/
@JsonProperty("latitude")
private Double latitude;
}
}

View File

@ -0,0 +1,77 @@
package com.fuyuanshen.global.mqtt.rule.hby100j.domin;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 设备电源状态上报DTO
* 对应MQTT消息中设备上报的电源相关状态数据
*/
@Data
public class FunctionType4PowerStatusReport {
/**
* 设备IMEI号国际移动设备识别码
*/
private String imei;
/**
* 功能类型编码(如"4"表示电源状态上报)
*/
private String funcType;
/**
* 响应状态码(如"200"表示成功)
*/
private String status;
/**
* 时间戳毫秒级Unix时间戳
*/
private Long timestamp;
/**
* 电源详细状态数据
*/
private Data data;
/**
* 电源详细状态数据内部类
*/
@lombok.Data
public static class Data {
/**
* 电池容量(如"5000mAh"
*/
@JsonProperty("capacity")
private String capacity;
/**
* 电压值单位V0表示未检测或无电
*/
@JsonProperty("voltage")
private Integer voltage;
/**
* 电量百分比0-1000表示低电量或未检测
*/
@JsonProperty("level")
private Integer level;
/**
* 充电状态0-未充电1-正在充电
*/
@JsonProperty("charge")
private Integer charge;
/**
* 12V电源状态0-关闭/无输出1-开启/有输出
*/
@JsonProperty("12v_power")
private Integer twelveVPower;
@JsonProperty("battery_remaining_time")
private Integer batteryRemainingTime;
}
}

View File

@ -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) {
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());
// 报警内容

View File

@ -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 不存在 -> 视为0incrementAndGet 返回 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);
}
}

View File

@ -0,0 +1,139 @@
package com.fuyuanshen.global.mqtt.utils;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
public class FfmpegVolumeUtil {
/**
* 使用FFmpeg增加MP3文件的音量
*
* @param inputFilePath 输入MP3文件路径
* @param outputFilePath 输出MP3文件路径
* @param volumeGain 音量增益例如1.5表示增加50%音量2.0表示翻倍)
*/
public static void increaseMp3Volume(String inputFilePath, String outputFilePath, int volumeGain) {
boolean ffmpegAvailable = isFfmpegAvailable();
if (!ffmpegAvailable) {
System.err.println("FFmpeg未安装或未找到请安装FFmpeg并确保在系统路径中。");
return;
}
// 检查输入文件
File inputFile = new File(inputFilePath);
if (!inputFile.exists()) {
System.err.println("输入文件不存在:" + inputFilePath);
return;
}
if (!inputFile.canRead()) {
System.err.println("输入文件不可读:" + inputFilePath);
return;
}
// 创建输出目录
File outputFile = new File(outputFilePath);
File parentDir = outputFile.getParentFile();
if (parentDir != null) {
parentDir.mkdirs();
if (!parentDir.canWrite()) {
System.err.println("输出目录不可写:" + parentDir.getAbsolutePath());
return;
}
}
Process process = null;
try {
String command = String.format(
"ffmpeg -y -i \"%s\" -af \"volume=%sdB\" \"%s\"",
inputFilePath,
volumeGain,
outputFilePath
);
System.out.println("执行命令: " + command);
// 使用 ProcessBuilder 提供更好的控制
ProcessBuilder processBuilder = new ProcessBuilder("ffmpeg", "-y",
"-i", inputFilePath, "-af", "volume=" + volumeGain + "dB", outputFilePath);
processBuilder.redirectErrorStream(false);
process = processBuilder.start();
// 同时读取标准输出和错误输出
Process finalProcess = process;
Thread stdOutThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(finalProcess.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[FFmpeg STDOUT] " + line);
}
} catch (IOException e) {
System.err.println("读取标准输出时出错: " + e.getMessage());
}finally {
finalProcess.destroy();
}
});
Process finalProcess1 = process;
Thread stdErrThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(finalProcess1.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println("[FFmpeg STDERR] " + line);
}
} catch (IOException e) {
System.err.println("读取错误输出时出错: " + e.getMessage());
}finally {
finalProcess1.destroy();
}
});
stdOutThread.start();
stdErrThread.start();
int exitCode = process.waitFor();
stdOutThread.join();
stdErrThread.join();
if (exitCode == 0) {
System.out.println("音量调整成功!输出文件:" + outputFilePath);
} else {
System.err.println("FFmpeg命令执行失败退出码" + exitCode);
}
} catch (IOException | InterruptedException e) {
System.err.println("执行FFmpeg命令时发生错误: " + e.getMessage());
e.printStackTrace();
} finally {
if (process != null) {
System.out.println("已销毁进程");
process.destroy();
}
}
}
public static void main(String[] args) {
// 示例用法
String inputPath = "/app/input.mp3";
String outputPath = "/app/output17.mp3";
int volumeGain = 12;
increaseMp3Volume(inputPath, outputPath, volumeGain);
}
private static boolean isFfmpegAvailable() {
try {
Process process = Runtime.getRuntime().exec("ffmpeg -version");
int exitCode = process.waitFor();
return exitCode == 0;
} catch (Exception e) {
return false;
}
}
}

View File

@ -0,0 +1,92 @@
package com.fuyuanshen.global.mqtt.utils;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
public class FfmpegVolumeUtil3 {
/**
* 使用FFmpeg增加MP3文件的音量
*
* @param inputFilePath 输入MP3文件路径
* @param outputFilePath 输出MP3文件路径
* @param volumeGain 音量增益例如1.5表示增加50%音量2.0表示翻倍)
*/
public static void increaseMp3Volume(String inputFilePath, String outputFilePath, int volumeGain) {
boolean ffmpegAvailable = isFfmpegAvailable();
if (!ffmpegAvailable) {
System.err.println("FFmpeg未安装或未找到请安装FFmpeg并确保在系统路径中。");
return;
}
// 创建输出文件的父目录
File outputFile = new File(outputFilePath);
File parentDir = outputFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
boolean created = parentDir.mkdirs();
if (!created) {
System.err.println("无法创建输出目录:" + parentDir.getAbsolutePath());
return;
}
}
Process process = null;
try {
// 构建FFmpeg命令
String command = String.format(
"ffmpeg -y -i \"%s\" -af \"volume=%sdB\" \"%s\"",
inputFilePath,
volumeGain,
outputFilePath
);
System.out.println("执行命令: " + command);
// 执行FFmpeg命令
process = Runtime.getRuntime().exec(command);
// 读取命令执行结果
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 等待命令执行完成
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("音量调整成功!输出文件:" + outputFilePath);
} else {
System.err.println("FFmpeg命令执行失败退出码" + exitCode);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
if (process != null) {
process.destroy();
}
}
}
public static void main(String[] args) {
// 示例用法
String inputPath = "/app/input.mp3";
String outputPath = "/app/output18.mp3";
int volumeGain = 12;
increaseMp3Volume(inputPath, outputPath, volumeGain);
}
private static boolean isFfmpegAvailable() {
try {
Process process = Runtime.getRuntime().exec("ffmpeg -version");
int exitCode = process.waitFor();
return exitCode == 0;
} catch (Exception e) {
return false;
}
}
}

View File

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

View File

@ -0,0 +1,24 @@
package com.fuyuanshen.global.mqtt.utils;
import java.util.Random;
import java.util.UUID;
public class GenerateIdUtil {
/**
* 生成数字唯一ID - 基于UUID
*/
public static String generateNumericId() {
UUID uuid = UUID.randomUUID();
return Math.abs(uuid.toString().hashCode()) + "";
}
/**
* 生成数字唯一ID - 基于时间戳+随机数
*/
public static long generateTimestampBasedId() {
long timestamp = System.currentTimeMillis();
int random = new Random().nextInt(1000); // 0-999随机数
return timestamp * 1000 + random;
}
}

View File

@ -7,14 +7,18 @@ import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.equipment.domain.Device;
import com.fuyuanshen.equipment.mapper.DeviceMapper;
import com.fuyuanshen.global.mqtt.base.NewMqttRuleContext;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@ -95,40 +99,18 @@ public class MqttMessageConsumer {
}
// 处理具体业务逻辑的方法
private void processMessage(String message) {
private void processMessage(String imei) {
String threadName = Thread.currentThread().getName();
try {
log.info("业务处理线程 {} 开始处理消息: {}", threadName, message);
// String deviceOnlineStatusRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ message + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX ;
// String deviceOnlineStatusRedis = RedisUtils.getCacheObject(deviceOnlineStatusRedisKey);
// if(StringUtils.isBlank(deviceOnlineStatusRedis)){
// UpdateWrapper<Device> updateWrapper = new UpdateWrapper<>();
// updateWrapper.eq("device_imei", message)
// .set("online_status", 1);
// deviceMapper.update(updateWrapper);
// }
// QueryWrapper<Device> queryWrapper = new QueryWrapper<>();
// queryWrapper.eq("device_imei", message);
// queryWrapper.eq("online_status", 1);
// Long count = deviceMapper.selectCount(queryWrapper);
// if(count == 0){
// UpdateWrapper<Device> updateWrapper = new UpdateWrapper<>();
// updateWrapper.eq("device_imei", message)
// .eq("online_status", 0)
// .set("online_status", 1);
// deviceMapper.update(updateWrapper);
// }
// 执行业务逻辑
UpdateWrapper<Device> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("device_imei", message)
updateWrapper.eq("device_imei", imei)
.in("online_status", 0,2)
.set("online_status", 1);
int update = deviceMapper.update(updateWrapper);
// 模拟业务处理耗时
// Thread.sleep(200);
log.info("业务处理线程 {} 完成消息处理: {}", threadName, message);
} catch (Exception e) {
log.error("业务处理线程 {} 处理消息时发生错误: {}", threadName, message, e);
log.error("业务处理线程 {} 处理消息时发生错误: {}", threadName, imei, e);
}
}

View File

@ -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 报警消费者已关闭");
}
}

View File

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

View File

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

View File

@ -0,0 +1,137 @@
package com.fuyuanshen.web.controller.device;
import com.fuyuanshen.app.domain.dto.AppAudioFileDto;
import com.fuyuanshen.app.domain.dto.AppFileRenameDto;
import com.fuyuanshen.app.domain.dto.TextToSpeechRequest;
import com.fuyuanshen.app.service.AudioProcessService;
import com.fuyuanshen.app.service.VideoProcessService;
import com.fuyuanshen.common.core.domain.R;
import com.fuyuanshen.common.idempotent.annotation.RepeatSubmit;
import com.fuyuanshen.common.web.core.BaseController;
import com.fuyuanshen.equipment.domain.vo.AppFileVo;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* APP 视频处理控制器
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/video")
public class WebVideoController extends BaseController {
private final VideoProcessService videoProcessService;
private final AudioProcessService audioProcessService;
/**
* 上传视频转码code默认1RGB565 2BGR565
*/
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@RepeatSubmit(interval = 2, timeUnit = TimeUnit.SECONDS,message = "请勿重复提交!")
public R<List<String>> uploadVideo(@RequestParam("file") MultipartFile file, @RequestParam(defaultValue = "1") int code) {
return R.ok(videoProcessService.processVideo(file, code));
}
/**
* 上传音频文件并转码
*/
@PostMapping(value = "/audio", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@RepeatSubmit(interval = 2, timeUnit = TimeUnit.SECONDS,message = "请勿重复提交!")
public R<List<String>> uploadAudio(@RequestParam("file") MultipartFile file) {
return R.ok(audioProcessService.processAudio(file));
}
/**
* 文字转音频TTS服务
*/
@GetMapping("/audioTTS")
@RepeatSubmit(interval = 2, timeUnit = TimeUnit.SECONDS,message = "请勿重复提交!")
public R<List<String>> uploadAudioTTS(@RequestParam String text) throws IOException {
return R.ok(audioProcessService.generateStandardPcmData(text));
}
/**
* 提取文本内容只支持txt/docx
*/
@PostMapping(value = "/extract", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@RepeatSubmit(interval = 2, timeUnit = TimeUnit.SECONDS,message = "请勿重复提交!")
public R<String> extract(@RequestParam("file") MultipartFile file) throws Exception {
return R.ok("Success",audioProcessService.extract(file));
}
/**
* 上传音频文件进行处理
* 支持MP3、WAV、PCM格式
*/
@PostMapping("/uploadAudioToOss")
public R<String> uploadAudioToOss(@ModelAttribute AppAudioFileDto bo) {
try {
bo.setSource("web");
String result = audioProcessService.uploadAudioToOss(bo);
return R.ok(result);
} catch (IllegalArgumentException e) {
return R.fail("文件格式错误: " + e.getMessage());
} catch (Exception e) {
return R.fail("上传处理失败: " + e.getMessage());
}
}
/**
* 文本转语音
* 支持MP3、WAV、PCM格式
*/
@PostMapping("/ttsToOss")
public R<String> textToSpeech(@RequestBody TextToSpeechRequest request) {
try {
if (request.getDeviceId() == null) {
return R.fail("设备ID不能为空");
}
String result = audioProcessService.textToSpeech(
request.getDeviceId(),
request.getText(),
request.getFileSuffix(),
"web"
);
return R.ok(result);
} catch (Exception e) {
return R.fail("文本转语音失败: " + e.getMessage());
}
}
/**
* 查询语音文件列表
*/
@GetMapping("/queryAudioFileList")
public R<List<AppFileVo>> queryAudioFileList(Long deviceId) {
return R.ok(audioProcessService.queryAudioFileList(deviceId,"web"));
}
/**
* 删除语音文件
*/
@GetMapping("/deleteAudioFile")
public R<Void> deleteAudioFile(Long id) {
return audioProcessService.deleteAudioFile(id);
}
/**
* 文件重命名
*/
@PostMapping("/renameAudioFile")
public R<Void> renameAudioFile(@RequestBody AppFileRenameDto bo) {
return audioProcessService.renameAudioFile(bo);
}
}

View File

@ -0,0 +1,107 @@
package com.fuyuanshen.web.controller.device.bjq;
import com.fuyuanshen.app.domain.vo.AppDeviceHBY100JDetailVo;
import com.fuyuanshen.common.core.domain.R;
import com.fuyuanshen.common.web.core.BaseController;
import com.fuyuanshen.web.controller.device.domain.dto.*;
import com.fuyuanshen.web.service.device.DeviceHBY100JBizService;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* HBY100J设备控制类
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/hby100j/device")
public class WebDeviceHBY100JController extends BaseController {
private final DeviceHBY100JBizService deviceHBY100JBizService;
/**
* 获取设备详细信息
*
* @param id 主键
*/
@GetMapping("/{id}")
public R<AppDeviceHBY100JDetailVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(deviceHBY100JBizService.getInfo(id));
}
/**
* 更新语音
*/
@PostMapping("/updateVoice")
public R<Void> updateVoice(@RequestBody HBY100JUpdateVoiceDto dto) {
dto.setCommunicationMode(0);
deviceHBY100JBizService.updateVoice(dto);
return R.ok();
}
/**
* 强制报警
*
*/
@PostMapping("/forceAlarmActivation")
public R<Void> forceAlarmActivation(@RequestBody HBY100JForceAlarmActivationDto bo) {
deviceHBY100JBizService.forceAlarmActivation(bo);
return R.ok();
}
/**
* 语音播报
*
*/
@PostMapping("/voiceBroadcast")
public R<Void> voiceBroadcast(@RequestBody HBY100JVoiceBroadcastDto params) {
deviceHBY100JBizService.voiceBroadcast(params);
return R.ok();
}
/**
* 爆闪模式
*/
@PostMapping("/strobeMode")
public R<Void> strobeMode(@RequestBody HBY100JStrobeModeDto params) {
deviceHBY100JBizService.strobeMode(params);
return R.ok();
}
/**
* 灯光调节
*/
@PostMapping("/lightAdjustment")
public R<Void> lightAdjustment(@RequestBody HBY100JLightAdjustmentDto params) {
deviceHBY100JBizService.lightAdjustment(params);
return R.ok();
}
/**
* 爆闪频率
*/
@PostMapping("/strobeFrequency")
public R<Void> strobeFrequency(@RequestBody HBY100JStrobeFrequencyDto params) {
deviceHBY100JBizService.strobeFrequency(params);
return R.ok();
}
/**
* 修改音量
*/
@PostMapping("/updateVolume")
public R<Void> updateVolume(@RequestBody HBY100JUpdateVolumeDto params) {
deviceHBY100JBizService.updateVolume(params);
return R.ok();
}
}

View File

@ -0,0 +1,23 @@
package com.fuyuanshen.web.controller.device.domain.dto;
import lombok.Data;
import java.util.List;
@Data
public class HBY100JForceAlarmActivationDto {
/**
* 设备ID
*/
List<Long> deviceIds;
/**
* 0 关闭, 1开启
*/
private Integer voiceStrobeAlarm;
/**
* 0 公安,1消防,2应急,3交警4 市政5 铁路6 医疗7语音
*/
private Integer mode;
}

View File

@ -0,0 +1,31 @@
package com.fuyuanshen.web.controller.device.domain.dto;
import lombok.Data;
@Data
public class HBY100JLightAdjustmentDto{
/**
* 设备ID
*/
private Long deviceId;
/**
* 亮度值0-100
*/
private Integer brightness;
// /**
// * 红色LED亮度值0-100
// */
// private Integer red;
//
// /**
// * 蓝色LED亮度值0-100
// */
// private Integer blue;
//
// /**
// * 黄色LED亮度值0-100
// */
// private Integer yellow;
}

View File

@ -0,0 +1,16 @@
package com.fuyuanshen.web.controller.device.domain.dto;
import lombok.Data;
@Data
public class HBY100JStrobeFrequencyDto{
/**
* 设备ID
*/
private Long deviceId;
/**
* "frequency": 1-12
*/
private Integer frequency;
}

View File

@ -0,0 +1,21 @@
package com.fuyuanshen.web.controller.device.domain.dto;
import lombok.Data;
@Data
public class HBY100JStrobeModeDto{
/**
* 设备ID
*/
private Long deviceId;
/**
* 0 关闭 1 开启
*/
private Integer enable;
/**
* 0 红色爆闪1 蓝色爆闪2 黄色爆闪3红色顺时针旋转爆闪4黄色顺时针旋转爆闪5红蓝顺时针旋转爆闪6 红蓝交替爆闪
*/
private Integer mode;
}

View File

@ -0,0 +1,14 @@
package com.fuyuanshen.web.controller.device.domain.dto;
import lombok.Data;
@Data
public class HBY100JUpdateVoiceDto {
private Long id;
/**
* 通讯方式 0:4G;1:蓝牙
*/
private Integer communicationMode;
}

View File

@ -0,0 +1,16 @@
package com.fuyuanshen.web.controller.device.domain.dto;
import lombok.Data;
@Data
public class HBY100JUpdateVolumeDto{
/**
* 设备ID
*/
private Long deviceId;
/**
* "volume": 1-100(app端可根据需求把40作为低音量, 70作为中音量100作为高音量)
*/
private Integer volume;
}

View File

@ -0,0 +1,17 @@
package com.fuyuanshen.web.controller.device.domain.dto;
import lombok.Data;
@Data
public class HBY100JVoiceBroadcastDto {
/**
* 设备ID
*/
Long deviceId;
/**
* 0 关闭, 1开启
*/
private Integer voiceBroadcast;
}

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ public class DeviceBizService {
List<AppDeviceVo> records = result.getRecords();
if (records != null && !records.isEmpty()) {
records.forEach(item -> {
if (item.getCommunicationMode() != null && item.getCommunicationMode() == 0) {
if (item.getCommunicationMode() != null && (item.getCommunicationMode() == 0 || item.getCommunicationMode() == 2)) {
// 设备在线状态
String onlineStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + item.getDeviceImei() + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX);
@ -97,6 +97,7 @@ public class DeviceBizService {
} else {
item.setOnlineStatus(0);
}
String deviceStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + item.getDeviceImei() + DEVICE_STATUS_KEY_PREFIX);
// 获取电量
if (StringUtils.isNotBlank(deviceStatus)) {
@ -270,12 +271,12 @@ public class DeviceBizService {
if (type == 0) {
QueryWrapper<AppDeviceBindRecord> bindRecordQueryWrapper = new QueryWrapper<>();
bindRecordQueryWrapper.eq("device_id", device.getId());
bindRecordQueryWrapper.eq("binding_user_id", userId);
// bindRecordQueryWrapper.eq("binding_user_id", userId);
Long count = appDeviceBindRecordMapper.selectCount(bindRecordQueryWrapper);
if (count == 0) {
throw new RuntimeException("请先绑定设备!!!");
}
if (count < 2) {
if (count <= 1) {
UpdateWrapper<Device> deviceUpdateWrapper = new UpdateWrapper<>();
deviceUpdateWrapper.eq("id", device.getId())
.set("binding_user_id", null)

View File

@ -0,0 +1,441 @@
package com.fuyuanshen.web.service.device;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.fuyuanshen.app.domain.vo.AppDeviceHBY100JDetailVo;
import com.fuyuanshen.app.mapper.AppDeviceShareMapper;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.core.domain.model.AppLoginUser;
import com.fuyuanshen.common.core.domain.model.LoginUser;
import com.fuyuanshen.common.core.exception.ServiceException;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.common.satoken.utils.AppLoginHelper;
import com.fuyuanshen.common.satoken.utils.LoginHelper;
import com.fuyuanshen.equipment.domain.AppBusinessFile;
import com.fuyuanshen.equipment.domain.Device;
import com.fuyuanshen.equipment.domain.DeviceType;
import com.fuyuanshen.equipment.domain.vo.AppBusinessFileVo;
import com.fuyuanshen.equipment.mapper.AppBusinessFileMapper;
import com.fuyuanshen.equipment.mapper.DeviceLogMapper;
import com.fuyuanshen.equipment.mapper.DeviceMapper;
import com.fuyuanshen.equipment.mapper.DeviceTypeMapper;
import com.fuyuanshen.equipment.service.IAppBusinessFileService;
import com.fuyuanshen.global.mqtt.config.MqttGateway;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import com.fuyuanshen.global.mqtt.constants.MqttConstants;
import com.fuyuanshen.global.mqtt.rule.hby100j.domin.*;
import com.fuyuanshen.global.mqtt.utils.GenerateIdUtil;
import com.fuyuanshen.system.domain.vo.SysOssVo;
import com.fuyuanshen.system.mapper.SysOssMapper;
import com.fuyuanshen.web.controller.device.domain.dto.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_LOCATION_KEY_PREFIX;
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceHBY100JBizService {
private final DeviceMapper deviceMapper;
private final DeviceTypeMapper deviceTypeMapper;
private final MqttGateway mqttGateway;
private final DeviceLogMapper deviceLogMapper;
private final IAppBusinessFileService appBusinessFileService;
private final AppBusinessFileMapper appBusinessFileMapper;
private final SysOssMapper sysOssMapper;
private final AppDeviceShareMapper appDeviceShareMapper;
private static final String DEVICE_TYPE = "HBY100/";
private static final Long pcUserId = 0L;
/**
* 记录设备操作日志
*
* @param deviceId 设备ID
* @param content 日志内容
* @param operator 操作人
*/
private void recordDeviceLog(Long deviceId, String deviceName, String deviceAction, String content, Long operator) {
try {
// 创建设备日志实体
com.fuyuanshen.equipment.domain.DeviceLog deviceLog = new com.fuyuanshen.equipment.domain.DeviceLog();
deviceLog.setDeviceId(deviceId);
deviceLog.setDeviceAction(deviceAction);
deviceLog.setContent(content);
deviceLog.setCreateBy(operator);
deviceLog.setDeviceName(deviceName);
deviceLog.setCreateTime(new Date());
// 插入日志记录
deviceLogMapper.insert(deviceLog);
} catch (Exception e) {
log.error("记录设备操作日志失败: {}", e.getMessage(), e);
}
}
public AppDeviceHBY100JDetailVo getInfo(Long id) {
Device device = deviceMapper.selectById(id);
if (device == null) {
throw new RuntimeException("请先将设备入库!!!");
}
AppDeviceHBY100JDetailVo vo = new AppDeviceHBY100JDetailVo();
vo.setDeviceId(device.getId());
vo.setDeviceName(device.getDeviceName());
vo.setDevicePic(device.getDevicePic());
vo.setDeviceImei(device.getDeviceImei());
vo.setDeviceMac(device.getDeviceMac());
DeviceType deviceType = deviceTypeMapper.selectById(device.getDeviceType());
if (deviceType != null) {
vo.setCommunicationMode(Integer.valueOf(deviceType.getCommunicationMode()));
vo.setTypeName(deviceType.getTypeName());
}
vo.setBluetoothName(device.getBluetoothName());
// 设备在线状态
String onlineStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + device.getDeviceImei() + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX);
// 设备在线状态
if ("1".equals(onlineStatus)) {
vo.setOnlineStatus(1);
} else if ("2".equals(onlineStatus)) {
vo.setOnlineStatus(2);
} else {
vo.setOnlineStatus(0);
}
String deviceStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + device.getDeviceImei() + DeviceRedisKeyConstants.DEVICE_STATUS_KEY_PREFIX);
// 获取电量
if (StringUtils.isNotBlank(deviceStatus)) {
JSONObject jsonObject = JSONObject.parseObject(deviceStatus);
vo.setBatteryPercentage(jsonObject.getInteger("batteryPercentage"));
vo.setChargeState(jsonObject.getInteger("chargeState"));
vo.setBatteryRemainingTime(jsonObject.getInteger("batteryRemainingTime"));
} else {
vo.setBatteryPercentage(0);
vo.setBatteryRemainingTime(0);
}
String reportStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + device.getDeviceImei() + ":report");
if (StringUtils.isNotBlank(reportStatus)) {
FuncType14StatusReport report = JSONObject.parseObject(reportStatus,FuncType14StatusReport.class);
FuncType14StatusReport.Data data = report.getData();
FuncType14StatusReport.Brightness brightness = data.getBrightness();
if(brightness != null){
vo.setLightBrightness(brightness.getRed());
}
vo.setVolume(data.getVolume());
FuncType14StatusReport.LedStrobe ledStrobe = data.getLedStrobe();
if(ledStrobe != null){
vo.setStrobeFrequency(ledStrobe.getFrequency());
vo.setStrobeMode(ledStrobe.getMode());
vo.setStrobeEnable(ledStrobe.getEnable());
}
Integer voiceBroadcast = data.getVoiceBroadcast();
if(voiceBroadcast != null){
vo.setVoiceBroadcast(voiceBroadcast);
}
FuncType14StatusReport.SirenAlarm sirenAlarm = data.getSirenAlarm();
if(sirenAlarm != null){
vo.setVoiceStrobeAlarm(sirenAlarm.getEnable());
vo.setAlarmMode(sirenAlarm.getMode());
}
}
String voicePlayRedisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
device.getDeviceImei() + ":voicePlay";
String voicePlayStatus = RedisUtils.getCacheObject(voicePlayRedisKey);
if(StringUtils.isNotBlank(voicePlayStatus)){
FuncType6VoicePlayRequest funcType6VoicePlayRequest = JSONObject.parseObject(voicePlayStatus, FuncType6VoicePlayRequest.class);
if(funcType6VoicePlayRequest.getData() != null){
vo.setVoiceBroadcast(funcType6VoicePlayRequest.getData().getVoiceBroadcast());
}
}
String strobeModeRedisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
device.getDeviceImei() + ":strobeMode";
String strobeModeStatus = RedisUtils.getCacheObject(strobeModeRedisKey);
if(StringUtils.isNotBlank(strobeModeStatus)){
FuncType10StrobeModeRequest strobeModeRequest = JSONObject.parseObject(strobeModeStatus, FuncType10StrobeModeRequest.class);
FuncType10StrobeModeRequest.Data data = strobeModeRequest.getData();
if(data != null){
vo.setStrobeMode(strobeModeRequest.getData().getMode());
vo.setStrobeEnable(strobeModeRequest.getData().getEnable());
}
}
String updateVolumeRedisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
device.getDeviceImei() + ":updateVolume";
String updateVolumeStatus = RedisUtils.getCacheObject(updateVolumeRedisKey);
if(StringUtils.isNotBlank(updateVolumeStatus)){
FuncType9UpdateVolumeRequest updateVolumeRequest = JSONObject.parseObject(updateVolumeStatus, FuncType9UpdateVolumeRequest.class);
if(updateVolumeRequest.getData() != null){
vo.setVolume(updateVolumeRequest.getData().getVolume());
}
}
String frequencyRedisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
device.getDeviceImei() + ":frequency";
String frequencyStatus = RedisUtils.getCacheObject(frequencyRedisKey);
if(StringUtils.isNotBlank(frequencyStatus)){
FuncType11FrequencyRequest frequencyRequest = JSONObject.parseObject(frequencyStatus, FuncType11FrequencyRequest.class);
if(frequencyRequest.getData() != null){
vo.setStrobeFrequency(frequencyRequest.getData().getFrequency());
}
}
String forceAudioRedisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX +
device.getDeviceImei() + ":forceAudio";
String forceAudioStatus = RedisUtils.getCacheObject(forceAudioRedisKey);
if(StringUtils.isNotBlank(forceAudioStatus)){
FuncType12ForceAudioRequest forceAudioRequest = JSONObject.parseObject(forceAudioStatus, FuncType12ForceAudioRequest.class);
if(forceAudioRequest.getData() != null){
vo.setVoiceStrobeAlarm(forceAudioRequest.getData().getVoiceStrobeAlarm());
vo.setAlarmMode(forceAudioRequest.getData().getMode());
}
}
// 获取经度纬度
String location = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + device.getDeviceImei() + DEVICE_LOCATION_KEY_PREFIX);
if (StringUtils.isNotBlank(location)) {
JSONObject jsonObject = JSONObject.parseObject(location);
vo.setLatitude(jsonObject.getString("latitude"));
vo.setLongitude(jsonObject.getString("longitude"));
vo.setAddress(jsonObject.getString("address"));
}
QueryWrapper<AppBusinessFile> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("business_id", device.getId());
queryWrapper.eq("file_type", 3);
queryWrapper.eq("use_status", 1);
List<AppBusinessFile> appBusinessFiles = appBusinessFileMapper.selectList(queryWrapper);
if(appBusinessFiles != null && appBusinessFiles.size() > 0){
AppBusinessFile appBusinessFile = appBusinessFiles.get(0);
SysOssVo sysOssVo = sysOssMapper.selectVoById(appBusinessFile.getFileId());
if(sysOssVo != null){
vo.setVoiceResource(sysOssVo.getUrl());
}
}
return vo;
}
private boolean getDeviceStatus(String deviceImei) {
String deviceOnlineStatusRedisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX;
return RedisUtils.getCacheObject(deviceOnlineStatusRedisKey) == null;
}
public void forceAlarmActivation(HBY100JForceAlarmActivationDto bo) {
List<Long> deviceIds = bo.getDeviceIds();
if (deviceIds == null || deviceIds.isEmpty()) {
throw new ServiceException("请选择设备");
}
AppLoginUser loginUser = AppLoginHelper.getLoginUser();
bo.getDeviceIds().forEach(deviceId -> {
Device deviceObj = deviceMapper.selectById(deviceId);
if (getDeviceStatus(deviceObj.getDeviceImei())) {
throw new ServiceException(deviceObj.getDeviceName() + ",设备已断开连接");
}
FuncType12ForceAudioRequest request = new FuncType12ForceAudioRequest();
request.setRequestId(GenerateIdUtil.generateNumericId());
request.setImei(deviceObj.getDeviceImei());
request.setFuncType("12");
request.setTimestamp(System.currentTimeMillis());
FuncType12ForceAudioRequest.Data data = new FuncType12ForceAudioRequest.Data();
data.setVoiceStrobeAlarm(bo.getVoiceStrobeAlarm());
data.setMode(bo.getMode());
request.setData(data);
log.info("HBY100J强制报警下发设备参数{}", request);
mqttGateway.sendMsgToMqtt(buildMqttTopic(deviceObj.getDeviceImei()), 1, JSON.toJSONString(request));
Long userId = loginUser == null ? pcUserId : loginUser.getUserId();
recordDeviceLog(deviceId, deviceObj.getDeviceName(), "强制报警激活", "强制报警激活", userId);
});
}
public void updateVoice(HBY100JUpdateVoiceDto dto) {
AppBusinessFileVo appBusinessFileVo = appBusinessFileMapper.selectVoById(dto.getId());
if(appBusinessFileVo == null){
throw new ServiceException("文件不存在");
}
Device deviceObj = deviceMapper.selectById(appBusinessFileVo.getBusinessId());
if (getDeviceStatus(deviceObj.getDeviceImei())) {
throw new ServiceException(deviceObj.getDeviceName() + ",设备已断开连接");
}
AppLoginUser loginUser = AppLoginHelper.getLoginUser();
if(dto.getCommunicationMode()!=null && dto.getCommunicationMode()==0){
SysOssVo sysOssVo = sysOssMapper.selectVoById(appBusinessFileVo.getFileId());
FuncType5UpdateVoiceRequest request = new FuncType5UpdateVoiceRequest();
request.setRequestId(GenerateIdUtil.generateNumericId());
request.setImei(deviceObj.getDeviceImei());
request.setFuncType("5");
request.setTimestamp(System.currentTimeMillis());
FuncType5UpdateVoiceRequest.Data data = new FuncType5UpdateVoiceRequest.Data();
data.setVoiceResource(sysOssVo.getUrl());
data.setVoiceType(0);
request.setData(data);
log.info("HBY100J更新语音参数{}", request);
mqttGateway.sendMsgToMqtt(buildMqttTopic(deviceObj.getDeviceImei()), 1, JSON.toJSONString(request));
}
UpdateWrapper<AppBusinessFile> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("business_id", appBusinessFileVo.getBusinessId());
updateWrapper.set("use_status", 0);
appBusinessFileMapper.update(updateWrapper);
UpdateWrapper<AppBusinessFile> updateWrapper2 = new UpdateWrapper<>();
updateWrapper2.eq("id", appBusinessFileVo.getId());
updateWrapper2.set("use_status", 1);
appBusinessFileMapper.update(updateWrapper2);
Long userId = loginUser == null ? pcUserId : loginUser.getUserId();
recordDeviceLog(deviceObj.getId(), deviceObj.getDeviceName(), "更新语音", "更新语音", userId);
}
private String buildMqttTopic(String deviceImei) {
String tenantId = LoginHelper.getTenantId();
return MqttConstants.GLOBAL_PUB_KEY2 +tenantId + "/" + DEVICE_TYPE + deviceImei;
}
public void strobeMode(HBY100JStrobeModeDto params) {
Device deviceObj = deviceMapper.selectById(params.getDeviceId());
if (getDeviceStatus(deviceObj.getDeviceImei())) {
throw new ServiceException(deviceObj.getDeviceName() + ",设备已断开连接");
}
AppLoginUser loginUser = AppLoginHelper.getLoginUser();
FuncType10StrobeModeRequest request = new FuncType10StrobeModeRequest();
request.setRequestId(GenerateIdUtil.generateNumericId());
request.setImei(deviceObj.getDeviceImei());
request.setFuncType("10");
request.setTimestamp(System.currentTimeMillis());
FuncType10StrobeModeRequest.Data data = new FuncType10StrobeModeRequest.Data();
data.setMode(params.getMode());
data.setEnable(params.getEnable());
request.setData(data);
log.info("HBY100J爆闪模式开启/关闭,下发设备参数:{}", request);
mqttGateway.sendMsgToMqtt(buildMqttTopic(deviceObj.getDeviceImei()), 1, JSON.toJSONString(request));
String content = params.getEnable() != null && params.getEnable() == 1 ? "爆闪模式开启" : "爆闪模式关闭";
Long userId = loginUser == null ? pcUserId : loginUser.getUserId();
recordDeviceLog(deviceObj.getId(), deviceObj.getDeviceName(), content, content, userId);
}
public void lightAdjustment(HBY100JLightAdjustmentDto params) {
log.info("HBY100J灯光调节请求参数{}", params);
Device deviceObj = deviceMapper.selectById(params.getDeviceId());
if (deviceObj == null) {
throw new ServiceException("设备不存在");
}
if (getDeviceStatus(deviceObj.getDeviceImei())) {
throw new ServiceException(deviceObj.getDeviceName() + ",设备已断开连接");
}
AppLoginUser loginUser = AppLoginHelper.getLoginUser();
FuncType13BrightnessRequest request = new FuncType13BrightnessRequest();
request.setRequestId(GenerateIdUtil.generateNumericId());
request.setImei(deviceObj.getDeviceImei());
request.setFuncType("13");
request.setTimestamp(System.currentTimeMillis());
FuncType13BrightnessRequest.Data data = new FuncType13BrightnessRequest.Data();
data.setRed(params.getBrightness());
data.setBlue(params.getBrightness());
data.setYellow(params.getBrightness());
request.setData(data);
log.info("HBY100J灯光调节下发设备参数{}", request);
mqttGateway.sendMsgToMqtt(buildMqttTopic(deviceObj.getDeviceImei()), 1, JSON.toJSONString(request));
Long userId = loginUser == null ? pcUserId : loginUser.getUserId();
recordDeviceLog(deviceObj.getId(), deviceObj.getDeviceName(), "灯光调节", "灯光调节", userId);
}
public void strobeFrequency(HBY100JStrobeFrequencyDto params) {
Device deviceObj = deviceMapper.selectById(params.getDeviceId());
if (deviceObj == null) {
throw new ServiceException("设备不存在");
}
if (getDeviceStatus(deviceObj.getDeviceImei())) {
throw new ServiceException(deviceObj.getDeviceName() + ",设备已断开连接");
}
AppLoginUser loginUser = AppLoginHelper.getLoginUser();
FuncType11FrequencyRequest request = new FuncType11FrequencyRequest();
request.setRequestId(GenerateIdUtil.generateNumericId());
request.setImei(deviceObj.getDeviceImei());
request.setFuncType("11");
request.setTimestamp(System.currentTimeMillis());
FuncType11FrequencyRequest.Data data = new FuncType11FrequencyRequest.Data();
data.setFrequency(params.getFrequency());
request.setData(data);
log.info("HBY100J爆闪频率下发设备参数{}", request);
mqttGateway.sendMsgToMqtt(buildMqttTopic(deviceObj.getDeviceImei()), 1, JSON.toJSONString(request));
Long userId = loginUser == null ? pcUserId : loginUser.getUserId();
recordDeviceLog(deviceObj.getId(), deviceObj.getDeviceName(), "爆闪频率", "爆闪频率", userId);
}
public void updateVolume(HBY100JUpdateVolumeDto params) {
Device deviceObj = deviceMapper.selectById(params.getDeviceId());
if (deviceObj == null) {
throw new ServiceException("设备不存在");
}
if (getDeviceStatus(deviceObj.getDeviceImei())) {
throw new ServiceException(deviceObj.getDeviceName() + ",设备已断开连接");
}
AppLoginUser loginUser = AppLoginHelper.getLoginUser();
FuncType9UpdateVolumeRequest request = new FuncType9UpdateVolumeRequest();
request.setRequestId(GenerateIdUtil.generateNumericId());
request.setImei(deviceObj.getDeviceImei());
request.setFuncType("9");
request.setTimestamp(System.currentTimeMillis());
FuncType9UpdateVolumeRequest.Data data = new FuncType9UpdateVolumeRequest.Data();
data.setVolume(params.getVolume());
request.setData(data);
log.info("HBY100J更新音量下发设备参数{}", JSON.toJSONString(request));
mqttGateway.sendMsgToMqtt(buildMqttTopic(deviceObj.getDeviceImei()), 1, JSON.toJSONString(request));
Long userId = loginUser == null ? pcUserId : loginUser.getUserId();
recordDeviceLog(deviceObj.getId(), deviceObj.getDeviceName(), "更新音量", "更新音量", userId);
}
public void voiceBroadcast(HBY100JVoiceBroadcastDto params) {
Device deviceObj = deviceMapper.selectById(params.getDeviceId());
if (deviceObj == null) {
throw new ServiceException("设备不存在");
}
if (getDeviceStatus(deviceObj.getDeviceImei())) {
throw new ServiceException(deviceObj.getDeviceName() + ",设备已断开连接");
}
AppLoginUser loginUser = AppLoginHelper.getLoginUser();
FuncType6VoicePlayRequest request = new FuncType6VoicePlayRequest();
request.setRequestId(GenerateIdUtil.generateNumericId());
request.setImei(deviceObj.getDeviceImei());
request.setFuncType("6");
request.setTimestamp(System.currentTimeMillis());
FuncType6VoicePlayRequest.Data data = new FuncType6VoicePlayRequest.Data();
data.setVoiceBroadcast(params.getVoiceBroadcast());
request.setData(data);
log.info("HBY100J语音播报下发设备参数{}", request);
mqttGateway.sendMsgToMqtt(buildMqttTopic(deviceObj.getDeviceImei()), 1, JSON.toJSONString(request));
Long userId = loginUser == null ? pcUserId : loginUser.getUserId();
recordDeviceLog(deviceObj.getId(), deviceObj.getDeviceName(), "语音播报", "语音播报", userId);
}
}

View File

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

View File

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

View File

@ -279,11 +279,13 @@ justauth:
# MQTT配置
mqtt:
username: admin
password: fys123456
url: tcp://47.107.152.87:1883
password: #YtvpSfCNG
url: tcp://47.120.79.150:2883
subClientId: fys_subClient
subTopic: A/#,status/tenantCode/#,report/tenantCode/#
pubTopic: B/#,command/tenantCode/#
subTopic: A/#,status/#
pubTopic: B/#,command/#
subTopic2: regis/equip/#
pubTopic2: regis/#
pubClientId: fys_pubClient

View File

@ -16,7 +16,7 @@ spring.boot.admin.client:
--- # snail-job 配置
snail-job:
enabled: false
enabled: true
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
group: "fys_group"
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config`表
@ -283,14 +283,16 @@ mqtt:
password: #YtvpSfCNG
url: tcp://47.120.79.150:3883
subClientId: fys_subClient
subTopic: A/#,status/tenantCode/#,report/tenantCode/#
pubTopic: B/#,command/tenantCode/#
subTopic: A/#,status/#
pubTopic: B/#,command/#
subTopic2: regis/equip/#
pubTopic2: regis/#
pubClientId: fys_pubClient
# TTS语音交互配置
alibaba:
tts:
appKey: KTwSUKMrf2olFfjC
akId: LTAI5t6RsfCvQh57qojzbEoe
akSecret: MTqvK2mXYeCRkl1jVPndiNumyaad0R
appKey: lbGuq5K5bEH4uxmT
akId: LTAI5t66moCkhNC32TDJ5ReP
akSecret: 2F3sdoBJ08bYvJcuDgSkLnJwGXsvYH

View File

@ -39,8 +39,8 @@ public class EncryptUtilsTest {
loginBody.setClientId("e5cd7e4891bf95d1d19206ce24a7b32e");
loginBody.setGrantType("password");
loginBody.setTenantId("894078");
loginBody.setCode("15");
loginBody.setUuid("28ecf3d396ce4e6db8eb414992235fad");
loginBody.setCode("6");
loginBody.setUuid("399d45194f3d4c7e9cbfb3294b198a9b");
// loginBody.setUsername("admin");
// loginBody.setPassword("admin123");
loginBody.setUsername("dyf");

View File

@ -4,7 +4,7 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
password: Jq_123456#
hikari:
connection-timeout: 30000
validation-timeout: 5000

View File

@ -2,9 +2,9 @@ spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
url: jdbc:mysql://localhost:3306/fys-vue?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
password: Jq_123456#
hikari:
connection-timeout: 30000
validation-timeout: 5000
@ -29,7 +29,7 @@ snail-job:
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
enabled: false
url: http://localhost:9090/admin
instance:
service-host-type: IP

View File

@ -46,6 +46,9 @@ public class AppDeviceBindRecordBo extends BaseEntity {
* 绑定时间
*/
private Date bindingTime;
/**
* 通讯方式 0:4G;1:蓝牙,2 4G&蓝牙
*/
private Integer communicationMode;
}

View File

@ -0,0 +1,145 @@
package com.fuyuanshen.app.domain.vo;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class AppDeviceHBY100JDetailVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 设备ID
*/
@ExcelProperty(value = "设备ID")
private Long deviceId;
/**
* 设备名称
*/
private String deviceName;
/**
* 设备IMEI
*/
private String deviceImei;
/**
* 设备MAC
*/
private String deviceMac;
/**
* 通讯方式 0:4G;1:蓝牙,2 4G&蓝牙
*/
private Integer communicationMode;
/**
* 设备图片
*/
private String devicePic;
/**
* 设备类型
*/
private String typeName;
/**
* 蓝牙名称
*/
private String bluetoothName;
//电量百分比
/**
* 电量百分比
*/
private Integer batteryPercentage;
//充电状态0没有充电1正在充电2为已充满
/**
* 充电状态0没有充电1正在充电2为已充满
*/
private Integer chargeState;
/**
* 在线状态(0离线1在线)
*/
private Integer onlineStatus;
// 经度
/**
* 经度
*/
private String longitude;
// 纬度
/**
* 纬度
*/
private String latitude;
// 逆解析地址
/**
* 逆解析地址
*/
private String address;
// 亮度
/**
* 亮度
*/
private Integer lightBrightness;
// 音量
/**
* 音量
*/
private Integer volume;
/**
* 语音资源URLMP3等音频文件地址
*/
private String voiceResource;
// 报警模式 0公安,1 消防,2应急,3交警4 市政5 铁路6 医疗7 自定义语音
/**
* 报警模式 0公安,1 消防,2应急,3交警4 市政5 铁路6 医疗7 自定义语音
*/
private Integer alarmMode;
// 强制报警开关: 0 关闭, 1开启
/**
* 强制报警开关: 0 关闭, 1开启
*/
private Integer voiceStrobeAlarm;
/**
* 0 红色爆闪1 蓝色爆闪2 黄色爆闪3红色顺时针旋转爆闪4黄色顺时针旋转爆闪5红蓝顺时针旋转爆闪6 红蓝交替爆闪
*/
private Integer strobeMode;
/**
* "enable": 0爆闪关闭, 1爆闪开启
*/
private Integer strobeEnable;
/**
* 爆闪频率
*/
private Integer strobeFrequency;
/**
* 语音播报0 关闭, 1开启
*/
private Integer voiceBroadcast;
/**
* 续航时间(分钟)
*/
private Integer batteryRemainingTime;
}

View File

@ -51,6 +51,11 @@ public class AppDeviceShareVo implements Serializable {
@ExcelProperty(value = "设备名称")
private String deviceName;
/**
* 设备mac地址
*/
private String deviceMac;
/**
* 手机号
*/

View File

@ -76,6 +76,7 @@ public class AppDeviceBindRecordServiceImpl implements IAppDeviceBindRecordServi
lqw.eq(bo.getDeviceId() != null, AppDeviceBindRecord::getDeviceId, bo.getDeviceId());
lqw.eq(bo.getBindingUserId() != null, AppDeviceBindRecord::getBindingUserId, bo.getBindingUserId());
lqw.eq(bo.getBindingTime() != null, AppDeviceBindRecord::getBindingTime, bo.getBindingTime());
lqw.eq(bo.getCommunicationMode() != null, AppDeviceBindRecord::getCommunicationMode, bo.getCommunicationMode());
return lqw;
}

View File

@ -16,10 +16,8 @@ import com.fuyuanshen.customer.domain.vo.ConsumerVo;
import com.fuyuanshen.customer.mapper.CustomerMapper;
import com.fuyuanshen.customer.service.CustomerService;
import com.fuyuanshen.system.domain.SysUserRole;
import com.fuyuanshen.system.domain.bo.SysUserBo;
import com.fuyuanshen.system.mapper.SysRoleMapper;
import com.fuyuanshen.system.mapper.SysUserRoleMapper;
import com.fuyuanshen.system.service.ISysUserService;
import com.fuyuanshen.system.service.impl.SysUserServiceImpl;
import io.undertow.util.BadRequestException;
import lombok.RequiredArgsConstructor;

Some files were not shown because too many files have changed in this diff Show More