From 4d9038567f4069c4d8b261cbaa9873b7ad287979 Mon Sep 17 00:00:00 2001 From: DragonWenLong <552045633@qq.com> Date: Thu, 18 Sep 2025 11:40:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E6=8F=90=E4=BE=9B=E7=9A=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=B7=AE=E5=BC=82=E4=BF=A1=E6=81=AF=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E4=B8=8B=E6=98=AF=E7=AC=A6=E5=90=88Angular=20commit?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E7=9A=84commit=20message=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` feat(app): 添加获取app版本接口及视频处理控制器 新增功能: - 在AppAuthController中添加了获取app版本信息的接口`/version`。- 新增AppVideoController用于处理视频上传和帧提取。 修改内容: - 在AppAuthController中引入了ISysDictTypeService服务。 - 在DeviceBizService中更新了设备通信模式的判断逻辑。 - 修改了DeviceAlarmMapper.xml和DeviceMapper.xml中的SQL查询语句以支持更多通信模式。 - 更新了DeviceXinghanBizService中的人员信息登记逻辑,并添加了获取设备详细信息的方法。 - 在DeviceXinghanController中添加了获取设备详细信息的接口`/info/{id}`。 - 更新了MqttXinghanJson类中的字段命名。 - 在pom.xml中添加了javacv相关的依赖。 修复问题: - 注释掉了AppSmsAuthStrategy中的登录检查逻辑。 ``` 这个commit message包含了类型(feat)、作用范围(app)以及简短的描述。同时在body部分详细说明了新增的功能、修改的内容以及修复的问题。 --- fys-admin/pom.xml | 11 + .../app/controller/AppAuthController.java | 26 ++ .../app/controller/AppVideoController.java | 249 ++++++++++++++++++ .../global/mqtt/base/MqttXinghanJson.java | 4 +- .../device/DeviceXinghanController.java | 15 ++ .../web/domain/vo/DeviceXinghanDetailVo.java | 103 ++++++++ .../web/service/device/DeviceBizService.java | 3 +- .../device/DeviceXinghanBizService.java | 145 ++++++++-- .../web/service/impl/AppSmsAuthStrategy.java | 2 +- .../mapper/equipment/DeviceAlarmMapper.xml | 2 +- .../mapper/equipment/DeviceMapper.xml | 2 +- 11 files changed, 541 insertions(+), 21 deletions(-) create mode 100644 fys-admin/src/main/java/com/fuyuanshen/app/controller/AppVideoController.java create mode 100644 fys-admin/src/main/java/com/fuyuanshen/web/domain/vo/DeviceXinghanDetailVo.java 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 @@