diff --git a/fys-admin/pom.xml b/fys-admin/pom.xml
index 20f147ed..ff80564f 100644
--- a/fys-admin/pom.xml
+++ b/fys-admin/pom.xml
@@ -123,6 +123,17 @@
test
+
+ org.bytedeco
+ javacv-platform
+
+
+ org.bytedeco
+ javacv
+ 1.5.7
+ compile
+
+
diff --git a/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppAuthController.java b/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppAuthController.java
index d483e457..8a7328e7 100644
--- a/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppAuthController.java
+++ b/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppAuthController.java
@@ -17,9 +17,11 @@ import com.fuyuanshen.common.satoken.utils.LoginHelper;
import com.fuyuanshen.common.tenant.helper.TenantHelper;
import com.fuyuanshen.system.domain.bo.SysTenantBo;
import com.fuyuanshen.system.domain.vo.SysClientVo;
+import com.fuyuanshen.system.domain.vo.SysDictDataVo;
import com.fuyuanshen.system.domain.vo.SysTenantVo;
import com.fuyuanshen.system.service.ISysClientService;
import com.fuyuanshen.system.service.ISysConfigService;
+import com.fuyuanshen.system.service.ISysDictTypeService;
import com.fuyuanshen.system.service.ISysTenantService;
import com.fuyuanshen.web.domain.vo.LoginTenantVo;
import com.fuyuanshen.web.domain.vo.LoginVo;
@@ -36,6 +38,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.net.URL;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -55,6 +58,7 @@ public class AppAuthController {
private final ISysConfigService configService;
private final ISysTenantService tenantService;
private final ISysClientService clientService;
+ private final ISysDictTypeService dictTypeService;
/**
@@ -179,4 +183,26 @@ public class AppAuthController {
SmsResponse smsResponse = smsBlend.sendMessage("18656573389", "123");
}
+ /**
+ * 获取app版本
+ * @return
+ */
+ @GetMapping("/version")
+ public R> getAppVersion() {
+ List list = dictTypeService.selectDictDataByType("app_version");
+ list.forEach(d -> {
+ String[] arr = d.getRemark().split("\\|");
+ d.setDictLabel(d.getDictLabel()); // ios/android
+ d.setDictValue(arr[0]); // 版本号
+ d.setRemark(arr[1]); // 下载地址
+ });
+ // 只保留方法体:筛选 label=ios 且版本号 ≥ 2.5.0 的列表
+// List result = list.stream()
+// .peek(d -> { String[] a = d.getRemark().split("\\|"); d.setDictValue(a[0]); d.setRemark(a[1]); })
+// .filter(d -> "ios".equalsIgnoreCase(d.getDictLabel()))
+// .filter(d -> VersionComparator.INSTANCE.compare(d.getDictValue(), "2.5.0") >= 0)
+// .toList();
+ return R.ok(list);
+ }
+
}
diff --git a/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppVideoController.java b/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppVideoController.java
new file mode 100644
index 00000000..3b807b41
--- /dev/null
+++ b/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppVideoController.java
@@ -0,0 +1,249 @@
+package com.fuyuanshen.app.controller;
+
+import com.fuyuanshen.common.core.domain.R;
+import com.fuyuanshen.common.web.core.BaseController;
+import lombok.RequiredArgsConstructor;
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.bytedeco.javacv.Frame;
+import org.bytedeco.javacv.Java2DFrameUtils;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * APP 视频处理
+ * @date 2025-09-15
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/app/video")
+public class AppVideoController extends BaseController {
+ // 可配置项:建议从 application.yml 中读取
+ private static final int MAX_VIDEO_SIZE = 10 * 1024 * 1024; // 10 MB
+ private static final int FRAME_RATE = 15; // 每秒抽15帧
+ private static final int DURATION = 2; // 抽2秒
+ private static final int TOTAL_FRAMES = FRAME_RATE * DURATION;
+ private static final int WIDTH = 160;
+ private static final int HEIGHT = 80;
+ private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
+
+ @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+ public R> upload(@RequestParam("file") MultipartFile file) {
+ if (file == null || file.isEmpty()) {
+ return R.fail("上传文件不能为空");
+ }
+
+ if (!isVideo(file.getOriginalFilename())) {
+ return R.fail("只允许上传视频文件");
+ }
+
+ if (file.getSize() > MAX_VIDEO_SIZE) {
+ return R.fail("视频大小不能超过10MB");
+ }
+
+ File tempFile = null;
+ try {
+ // 创建临时文件保存上传的视频
+ tempFile = createTempVideoFile(file);
+
+
+ List frames = extractFramesFromVideo(tempFile);
+ if (frames.isEmpty()) {
+ return R.fail("无法提取任何帧");
+ }
+
+ // ✅ 新增:保存帧为图片
+ //saveFramesToLocal(frames, "extracted_frame");
+
+ byte[] binaryData = convertFramesToRGB565(frames);
+// String base64Data = Base64.getEncoder().encodeToString(binaryData);
+//
+// return R.ok(base64Data);
+ // 构造响应头
+ // 将二进制数据转为 Hex 字符串
+ // 转换为 Hex 字符串列表
+ List hexList = bytesToHexList(binaryData);
+
+ return R.ok(hexList);
+
+ } catch (Exception e) {
+ return R.fail("视频处理失败:" + e.getMessage());
+ } finally {
+ deleteTempFile(tempFile);
+ }
+ }
+
+ /**
+ * rgb565 转 hex
+ */
+ private List bytesToHexList(byte[] bytes) {
+ List hexList = new ArrayList<>();
+ for (byte b : bytes) {
+ int value = b & 0xFF;
+ char high = HEX_ARRAY[value >>> 4];
+ char low = HEX_ARRAY[value & 0x0F];
+ hexList.add(String.valueOf(high) + low);
+ }
+ return hexList;
+ }
+
+ /**
+ * 创建临时文件并保存上传的视频
+ */
+ private File createTempVideoFile(MultipartFile file) throws Exception {
+ File tempFile = Files.createTempFile("upload-", ".mp4").toFile();
+ file.transferTo(tempFile);
+ return tempFile;
+ }
+
+ /**
+ * 从视频中按时间均匀提取指定数量的帧
+ */
+ private List extractFramesFromVideo(File videoFile) throws Exception {
+ List frames = new ArrayList<>();
+
+ try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(videoFile)) {
+ grabber.start();
+
+ // 获取视频总帧数和帧率
+ long totalFramesInVideo = grabber.getLengthInFrames();
+ int fps = (int) Math.round(grabber.getFrameRate());
+ if (fps <= 0) fps = 30;
+
+ double durationSeconds = (double) totalFramesInVideo / fps;
+
+ if (durationSeconds < DURATION) {
+ throw new IllegalArgumentException("视频太短,至少需要 " + DURATION + " 秒");
+ }
+
+ // 计算每帧之间的间隔(浮点以实现更精确跳转)
+ double frameInterval = (double) totalFramesInVideo / TOTAL_FRAMES;
+
+ for (int i = 0; i < TOTAL_FRAMES; i++) {
+ int targetFrameNumber = (int) Math.round(i * frameInterval);
+
+ // 避免设置无效帧号
+ if (targetFrameNumber >= totalFramesInVideo) {
+ throw new IllegalArgumentException("目标帧超出范围: " + targetFrameNumber + " ");
+ }
+
+ grabber.setFrameNumber(targetFrameNumber);
+ Frame frame = grabber.grab();
+
+ if (frame != null && frame.image != null) {
+ BufferedImage bufferedImage = Java2DFrameUtils.toBufferedImage(frame);
+ frames.add(cropImage(bufferedImage, WIDTH, HEIGHT));
+ } else {
+ throw new IllegalArgumentException("无法获取第 " + targetFrameNumber + "帧 ");
+ }
+ }
+
+ grabber.stop();
+ }
+
+ return frames;
+ }
+
+ /**
+ * 将抽取的帧保存到本地,用于调试
+ */
+ private void saveFramesToLocal(List frames, String prefix) {
+ // 指定输出目录
+ File outputDir = new File("output_frames");
+ if (!outputDir.exists()) {
+ outputDir.mkdirs();
+ }
+
+ int index = 0;
+ for (BufferedImage frame : frames) {
+ try {
+ File outputImage = new File(outputDir, prefix + "_" + (index++) + ".png");
+ ImageIO.write(frame, "png", outputImage);
+ System.out.println("保存帧图片成功: " + outputImage.getAbsolutePath());
+ } catch (Exception e) {
+ throw new IllegalArgumentException("保存帧图片失败 " + e);
+ }
+ }
+ }
+
+ /**
+ * 将所有帧转换为 RGB565 格式字节数组
+ */
+ private byte[] convertFramesToRGB565(List frames) throws Exception {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+
+ for (BufferedImage image : frames) {
+ byte[] rgb565Bytes = convertToRGB565(image);
+ byteArrayOutputStream.write(rgb565Bytes);
+ }
+
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ /**
+ * 判断是否是支持的视频格式
+ */
+ private boolean isVideo(String filename) {
+ String ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
+ return Arrays.asList(".mp4", ".avi", ".mov", ".mkv").contains(ext);
+ }
+
+ /**
+ * 裁剪图像到目标尺寸
+ */
+ private BufferedImage cropImage(BufferedImage img, int targetWidth, int targetHeight) {
+ int w = Math.min(img.getWidth(), targetWidth);
+ int h = Math.min(img.getHeight(), targetHeight);
+ return img.getSubimage(0, 0, w, h);
+ }
+
+ /**
+ * 将 BufferedImage 转换为 RGB565 格式的字节数组
+ */
+ private byte[] convertToRGB565(BufferedImage image) {
+ int width = image.getWidth();
+ int height = image.getHeight();
+ byte[] result = new byte[width * height * 2]; // RGB565: 2 bytes per pixel
+ int index = 0;
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int rgb = image.getRGB(x, y);
+ int r = ((rgb >> 16) & 0xFF) >> 3;
+ int g = ((rgb >> 8) & 0xFF) >> 2;
+ int b = (rgb & 0xFF) >> 3;
+ short pixel = (short) ((r << 11) | (g << 5) | b);
+
+ result[index++] = (byte) (pixel >> 8); // High byte first
+ result[index++] = (byte) pixel;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * 删除临时文件
+ */
+ private void deleteTempFile(File file) {
+ if (file != null && file.exists()) {
+ if (!file.delete()) {
+ throw new IllegalArgumentException("无法删除临时文件: " + file.getAbsolutePath());
+ }
+ }
+ }
+}
diff --git a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/base/MqttXinghanJson.java b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/base/MqttXinghanJson.java
index 2d49b468..3e0737f3 100644
--- a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/base/MqttXinghanJson.java
+++ b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/base/MqttXinghanJson.java
@@ -39,8 +39,8 @@ public class MqttXinghanJson {
/**
* 第七键值对, 静止报警状态,0-未静止报警,1-正在静止报警。
*/
- @JsonProperty("staShakeBit")
- public Integer sta_ShakeBit;
+ @JsonProperty("sta_ShakeBit")
+ public Integer staShakeBit;
/**
* 第八键值对, 4G信号强度,0-32,数值越大,信号越强。
*/
diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceXinghanController.java b/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceXinghanController.java
index 8600c788..4cc51d87 100644
--- a/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceXinghanController.java
+++ b/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceXinghanController.java
@@ -3,13 +3,16 @@ package com.fuyuanshen.web.controller.device;
import com.fuyuanshen.app.domain.bo.AppPersonnelInfoBo;
import com.fuyuanshen.app.domain.dto.AppDeviceLogoUploadDto;
import com.fuyuanshen.app.domain.dto.DeviceInstructDto;
+import com.fuyuanshen.app.domain.vo.AppDeviceDetailVo;
import com.fuyuanshen.common.core.domain.R;
import com.fuyuanshen.common.core.validate.AddGroup;
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.dto.AppDeviceSendMsgBo;
+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.*;
@@ -26,6 +29,18 @@ public class DeviceXinghanController extends BaseController {
private final DeviceXinghanBizService deviceXinghanBizService;
+
+ /**
+ * 获取设备详细信息
+ *
+ * @param id 主键
+ */
+ @GetMapping("/{id}")
+ public R getInfo(@NotNull(message = "主键不能为空")
+ @PathVariable Long id) {
+ return R.ok(deviceXinghanBizService.getInfo(id));
+ }
+
/**
* 人员信息登记
*/
diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/domain/vo/DeviceXinghanDetailVo.java b/fys-admin/src/main/java/com/fuyuanshen/web/domain/vo/DeviceXinghanDetailVo.java
new file mode 100644
index 00000000..62c4e3e1
--- /dev/null
+++ b/fys-admin/src/main/java/com/fuyuanshen/web/domain/vo/DeviceXinghanDetailVo.java
@@ -0,0 +1,103 @@
+package com.fuyuanshen.web.domain.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fuyuanshen.app.domain.vo.AppPersonnelInfoVo;
+import lombok.Data;
+
+@Data
+public class DeviceXinghanDetailVo {
+ /**
+ * 设备ID
+ */
+ @ExcelProperty(value = "设备ID")
+ private Long deviceId;
+
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 设备IMEI
+ */
+ private String deviceImei;
+
+ /**
+ * 设备MAC
+ */
+ private String deviceMac;
+
+ /**
+ * 通讯方式 0:4G;1:蓝牙
+ */
+ private Integer communicationMode;
+
+ /**
+ * 设备图片
+ */
+ private String devicePic;
+
+ /**
+ * 设备类型
+ */
+ private String typeName;
+
+ /**
+ * 蓝牙名称
+ */
+ private String bluetoothName;
+
+ /**
+ * 设备状态
+ * 0 失效
+ * 1 正常
+ */
+ private Integer deviceStatus;
+
+
+ /**
+ * 人员信息
+ */
+ private AppPersonnelInfoVo personnelInfo;
+
+ /**
+ * 在线状态(0离线,1在线)
+ */
+ private Integer onlineStatus;
+
+ // 经度
+ private String longitude;
+
+ // 纬度
+ private String latitude;
+
+ // 逆解析地址
+ private String address;
+
+ /**
+ * 第一键值对,静电预警档位:3,2,1,0,分别表示高档/中档/低挡/关闭.
+ */
+ private Integer staDetectGrade;
+ /**
+ * 第二键值对,照明档位,2,1,0,分别表示弱光/强光/关闭
+ */
+ private Integer staLightGrade;
+ /**
+ * 第三键值对,SOS档位,2,1,0, 分别表示红蓝模式/爆闪模式/关闭
+ */
+ public Integer staSOSGrade;
+ /**
+ * 第四键值对,剩余照明时间,0-5999,单位分钟。
+ */
+ public Integer staPowerTime;
+ /**
+ * 第五键值对,剩余电量百分比,0-100
+ */
+ public Integer staPowerPercent;
+ /**
+ * 第六键值对, 近电预警级别, 0-无预警,1-弱预警,2-中预警,3-强预警,4-非常强预警。
+ */
+ public Integer staDetectResult;
+}
diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceBizService.java b/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceBizService.java
index 21cf14eb..df7785cb 100644
--- a/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceBizService.java
+++ b/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceBizService.java
@@ -36,6 +36,7 @@ import com.fuyuanshen.equipment.mapper.DeviceMapper;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import com.fuyuanshen.web.service.device.status.base.DeviceStatusRule;
import com.fuyuanshen.web.service.device.status.base.RealTimeStatusEngine;
+import com.google.common.primitives.Ints;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -126,7 +127,7 @@ public class DeviceBizService {
List records = result.getRecords();
if(records != null && !records.isEmpty()){
records.forEach(item -> {
- if(item.getCommunicationMode()!=null && item.getCommunicationMode() == 0){
+ if(item.getCommunicationMode()!=null && Ints.asList(0, 2).contains(item.getCommunicationMode())){
//设备在线状态
String onlineStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ item.getDeviceImei() + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX);
diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java b/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java
index 8370ed9d..32254592 100644
--- a/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java
+++ b/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java
@@ -1,15 +1,22 @@
package com.fuyuanshen.web.service.device;
+import cn.hutool.core.bean.BeanUtil;
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.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.fuyuanshen.app.domain.AppPersonnelInfo;
+import com.fuyuanshen.app.domain.AppPersonnelInfoRecords;
import com.fuyuanshen.app.domain.bo.AppPersonnelInfoBo;
import com.fuyuanshen.app.domain.dto.AppDeviceLogoUploadDto;
import com.fuyuanshen.app.domain.dto.DeviceInstructDto;
+import com.fuyuanshen.app.domain.vo.AppDeviceDetailVo;
import com.fuyuanshen.app.domain.vo.AppPersonnelInfoVo;
import com.fuyuanshen.app.mapper.AppPersonnelInfoMapper;
+import com.fuyuanshen.app.mapper.AppPersonnelInfoRecordsMapper;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.core.exception.ServiceException;
import com.fuyuanshen.common.core.utils.ImageToCArrayConverter;
@@ -20,17 +27,21 @@ import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.common.satoken.utils.AppLoginHelper;
import com.fuyuanshen.equipment.domain.Device;
+import com.fuyuanshen.equipment.domain.DeviceType;
import com.fuyuanshen.equipment.domain.dto.AppDeviceSendMsgBo;
import com.fuyuanshen.equipment.enums.LightModeEnum;
import com.fuyuanshen.equipment.mapper.DeviceLogMapper;
import com.fuyuanshen.equipment.mapper.DeviceMapper;
import com.fuyuanshen.equipment.mapper.DeviceTypeMapper;
+import com.fuyuanshen.global.mqtt.base.MqttXinghanJson;
import com.fuyuanshen.global.mqtt.config.MqttGateway;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import com.fuyuanshen.global.mqtt.constants.MqttConstants;
import com.fuyuanshen.web.domain.Dto.DeviceDebugLogoUploadDto;
+import com.fuyuanshen.web.domain.vo.DeviceXinghanDetailVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -56,6 +67,9 @@ public class DeviceXinghanBizService {
private final DeviceTypeMapper deviceTypeMapper;
private final MqttGateway mqttGateway;
private final DeviceLogMapper deviceLogMapper;
+ private final AppPersonnelInfoRecordsMapper appPersonnelInfoRecordsMapper;
+ @Autowired
+ private ObjectMapper objectMapper;
/**
* 所有档位的描述表
@@ -106,6 +120,71 @@ public class DeviceXinghanBizService {
sendCommand(dto, "ins_ShakeBit","强制报警");
}
+ public DeviceXinghanDetailVo getInfo(Long id) {
+ Device device = deviceMapper.selectById(id);
+ if (device == null) {
+ throw new RuntimeException("请先将设备入库!!!");
+ }
+
+ DeviceXinghanDetailVo vo = new DeviceXinghanDetailVo();
+ vo.setDeviceId(device.getId());
+ vo.setDeviceName(device.getDeviceName());
+ vo.setDevicePic(device.getDevicePic());
+ vo.setDeviceImei(device.getDeviceImei());
+ vo.setDeviceMac(device.getDeviceMac());
+ vo.setDeviceStatus(device.getDeviceStatus());
+ DeviceType deviceType = deviceTypeMapper.selectById(device.getDeviceType());
+ if (deviceType != null) {
+ vo.setCommunicationMode(Integer.valueOf(deviceType.getCommunicationMode()));
+ vo.setTypeName(deviceType.getTypeName());
+ }
+ vo.setBluetoothName(device.getBluetoothName());
+
+
+ QueryWrapper qw = new QueryWrapper()
+ .eq("device_id", device.getId());
+ AppPersonnelInfo appPersonnelInfo = appPersonnelInfoMapper.selectOne(qw);
+ if (appPersonnelInfo != null) {
+ AppPersonnelInfoVo personnelInfoVo = MapstructUtils.convert(appPersonnelInfo, AppPersonnelInfoVo.class);
+ vo.setPersonnelInfo(personnelInfoVo);
+ }
+ //设备在线状态
+ String onlineStatus = RedisUtils.getCacheObject(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + device.getDeviceImei()+ DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX);
+ if(StringUtils.isNotBlank(onlineStatus)){
+ vo.setOnlineStatus(1);
+ }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)){
+ try {
+ MqttXinghanJson deviceJson = objectMapper.readValue(deviceStatus, MqttXinghanJson.class);
+ vo.setStaLightGrade(deviceJson.getStaLightGrade());
+ vo.setStaPowerPercent(deviceJson.getStaPowerPercent());
+ vo.setStaSOSGrade(deviceJson.getStaSOSGrade());
+ vo.setStaDetectGrade(deviceJson.getStaDetectGrade());
+ vo.setStaPowerTime(deviceJson.getStaPowerTime());
+ } catch (Exception e) {
+ throw new IllegalArgumentException("设备状态缓存格式非法", e);
+ }
+ }else{
+ vo.setStaPowerPercent(0);
+ }
+
+ // 获取经度纬度
+ String locationKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + device.getDeviceImei() + DeviceRedisKeyConstants.DEVICE_LOCATION_KEY_PREFIX;
+ String locationInfo = RedisUtils.getCacheObject(locationKey);
+ if(StringUtils.isNotBlank(locationInfo)){
+ JSONObject jsonObject = JSONObject.parseObject(locationInfo);
+ vo.setLongitude(jsonObject.get("longitude").toString());
+ vo.setLatitude(jsonObject.get("latitude").toString());
+ vo.setAddress((String)jsonObject.get("address"));
+ }
+
+ return vo;
+ }
+
/**
* 上传设备logo
*/
@@ -212,18 +291,19 @@ public class DeviceXinghanBizService {
* 人员登记
* @param bo
*/
+ @Transactional(rollbackFor = Exception.class) // 1. 事务
public boolean registerPersonInfo(AppPersonnelInfoBo bo) {
Long deviceId = bo.getDeviceId();
Device deviceObj = deviceMapper.selectById(deviceId);
if (deviceObj == null) {
throw new RuntimeException("请先将设备入库!!!");
}
+ if (deviceObj.getDeviceImei() == null) {
+ return saveOrUpdatePersonnelInfo(bo, deviceId);
+ }
if (isDeviceOffline(deviceObj.getDeviceImei())) {
throw new ServiceException("设备已断开连接:" + deviceObj.getDeviceName());
}
- QueryWrapper qw = new QueryWrapper()
- .eq("device_id", deviceId);
- List appPersonnelInfoVos = appPersonnelInfoMapper.selectVoList(qw);
List list = new ArrayList<>();
list.add(bo.getName());
@@ -247,18 +327,47 @@ public class DeviceXinghanBizService {
log.info("发送人员信息登记到设备消息=>topic:{},payload:{}", MqttConstants.GLOBAL_PUB_KEY + deviceObj.getDeviceImei(), bo);
recordDeviceLog(deviceId, deviceObj.getDeviceName(), "人员信息登记", JSON.toJSONString(bo), AppLoginHelper.getUserId());
- if (ObjectUtils.length(appPersonnelInfoVos) == 0) {
- AppPersonnelInfo appPersonnelInfo = MapstructUtils.convert(bo, AppPersonnelInfo.class);
- return appPersonnelInfoMapper.insertOrUpdate(appPersonnelInfo);
+
+ return saveOrUpdatePersonnelInfo(bo, deviceId);
+ }
+
+ /**
+ * 仅内部调用,已处于 @Transactional 内
+ */
+ private Boolean saveOrUpdatePersonnelInfo(AppPersonnelInfoBo bo, Long deviceId) {
+ // 1. 先查主表有没有
+ AppPersonnelInfo main = appPersonnelInfoMapper.selectOne(
+ Wrappers.lambdaQuery()
+ .eq(AppPersonnelInfo::getDeviceId, deviceId));
+
+ if (main == null) {
+ /* ====== 新增场景 ====== */
+ main = MapstructUtils.convert(bo, AppPersonnelInfo.class);
+ main.setDeviceId(deviceId);
+ // 1.1 主表插入 -> 主键回写
+ appPersonnelInfoMapper.insert(main); // 执行后 main.getId() 一定有值
} else {
- UpdateWrapper uw = new UpdateWrapper<>();
- uw.eq("device_id", deviceId)
- .set("name", bo.getName())
- .set("position", bo.getPosition())
- .set("unit_name", bo.getUnitName())
- .set("code", bo.getCode());
- return appPersonnelInfoMapper.update(null, uw) > 0;
+ /* ====== 更新场景 ====== */
+ // 1.2 把 bo 的新值刷到主表
+ appPersonnelInfoMapper.update(null,
+ Wrappers.update()
+ .eq("device_id", deviceId)
+ .set("name", bo.getName())
+ .set("position", bo.getPosition())
+ .set("unit_name", bo.getUnitName())
+ .set("code", bo.getCode())
+ .set("update_time", new Date()));
}
+
+ // 2. 无论新增/更新,都插入一条履历
+ AppPersonnelInfoRecords record = new AppPersonnelInfoRecords();
+ // 只拷业务字段,不拷时间、id、操作人
+ BeanUtil.copyProperties(main, record, "id", "createTime", "updateTime", "createBy", "updateBy");
+ record.setId(null); // 让自增
+ record.setPersonnelId(main.getId()); // 关键:主键已回写,不会为 null
+ appPersonnelInfoRecordsMapper.insert(record);
+
+ return true;
}
/**
@@ -273,6 +382,10 @@ public class DeviceXinghanBizService {
if (deviceIds == null || deviceIds.isEmpty()) {
throw new ServiceException("请选择设备");
}
+ if(!StringUtils.isNotEmpty(bo.getSendMsg()))
+ {
+ throw new ServiceException("请输入发送内容");
+ }
QueryWrapper queryWrapper = new QueryWrapper<>();
// 使用 in 语句根据id集合查询
queryWrapper.in("id", deviceIds);
@@ -325,9 +438,11 @@ public class DeviceXinghanBizService {
*/
private void sendSingleAlarmMessage(Device device, String message) {
String deviceImei = device.getDeviceImei();
-
+ int msgLen = message.length() / 2;
+ StringBuilder msgBuilder = new StringBuilder(message);
+ msgBuilder.insert(msgLen, '|');
// 缓存告警消息到Redis
- RedisUtils.setCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_ALARM_MESSAGE_KEY_PREFIX, message, Duration.ofSeconds(5 * 60L));
+ RedisUtils.setCacheObject(GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_ALARM_MESSAGE_KEY_PREFIX, msgBuilder.toString(), Duration.ofSeconds(5 * 60L));
// 构建并发送MQTT消息
Map payload = Map.of("ins_BreakNews", Collections.singletonList(0));
diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/service/impl/AppSmsAuthStrategy.java b/fys-admin/src/main/java/com/fuyuanshen/web/service/impl/AppSmsAuthStrategy.java
index 862cfbd9..eb9ea8d3 100644
--- a/fys-admin/src/main/java/com/fuyuanshen/web/service/impl/AppSmsAuthStrategy.java
+++ b/fys-admin/src/main/java/com/fuyuanshen/web/service/impl/AppSmsAuthStrategy.java
@@ -54,7 +54,7 @@ public class AppSmsAuthStrategy implements IAuthStrategy {
String phonenumber = loginBody.getPhonenumber();
String smsCode = loginBody.getSmsCode();
AppLoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
- loginService.checkLogin(LoginType.SMS, tenantId, phonenumber, () -> !validateSmsCode(tenantId, phonenumber, smsCode));
+// loginService.checkLogin(LoginType.SMS, tenantId, phonenumber, () -> !validateSmsCode(tenantId, phonenumber, smsCode));
AppUserVo user = loadUserByPhonenumber(phonenumber);
if (ObjectUtil.isNull(user)) {
//新增Appuser
diff --git a/fys-modules/fys-equipment/src/main/resources/mapper/equipment/DeviceAlarmMapper.xml b/fys-modules/fys-equipment/src/main/resources/mapper/equipment/DeviceAlarmMapper.xml
index cdb76f8a..27d38895 100644
--- a/fys-modules/fys-equipment/src/main/resources/mapper/equipment/DeviceAlarmMapper.xml
+++ b/fys-modules/fys-equipment/src/main/resources/mapper/equipment/DeviceAlarmMapper.xml
@@ -6,7 +6,7 @@