From 4965d78c5123c426f2980776976412a149a47899 Mon Sep 17 00:00:00 2001 From: daiyongfei <974332738@qq.com> Date: Fri, 22 Aug 2025 09:40:52 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E6=89=80=E5=B1=9E?= =?UTF-8?q?=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/query/DeviceQueryCriteria.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/query/DeviceQueryCriteria.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/query/DeviceQueryCriteria.java index d8a9505e..e0e51be5 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/query/DeviceQueryCriteria.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/query/DeviceQueryCriteria.java @@ -66,8 +66,22 @@ public class DeviceQueryCriteria extends BaseEntity { /* app绑定用户id */ private Long bindingUserId; - - /* 是否为管理员 */ + /** + * 是否为管理员 + */ + @Schema(name = "是否为管理员") private Boolean isAdmin = false; + /** + * 设备所属分组 + */ + @Schema(name = "设备所属分组") + private Long groupId; + + /** + * 设备地区 + */ + @Schema(name = "设备地区") + private String area; + } From b83be496b61c3295f26b0ce8ffde6f5546e9cdb9 Mon Sep 17 00:00:00 2001 From: daiyongfei <974332738@qq.com> Date: Fri, 22 Aug 2025 09:58:27 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E5=87=BA=E5=8E=82=E6=97=A5=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fuyuanshen/equipment/domain/Device.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/Device.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/Device.java index ae1a00d3..902874c1 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/Device.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/Device.java @@ -1,9 +1,6 @@ package com.fuyuanshen.equipment.domain; -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.*; import com.fasterxml.jackson.annotation.JsonInclude; import com.fuyuanshen.common.tenant.core.TenantEntity; import io.swagger.v3.oas.annotations.media.Schema; @@ -32,6 +29,13 @@ public class Device extends TenantEntity { @TableField(exist = false) private Long assignId; + /** + * 设备分组 + * group_id + */ + @Schema(name = "设备分组") + private Long groupId; + /** * device_type */ @@ -143,4 +147,11 @@ public class Device extends TenantEntity { */ private String subTopic; + /** + * 出厂日期 + * production_date + */ + @Schema(name = "出厂日期") + private Date productionDate; + } From 2965b454cf534f794f7261aeeb37b17f179c434c Mon Sep 17 00:00:00 2001 From: daiyongfei <974332738@qq.com> Date: Fri, 22 Aug 2025 16:37:58 +0800 Subject: [PATCH 3/4] =?UTF-8?q?web:=E8=AE=BE=E5=A4=87=E5=88=86=E7=BB=84=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/device/DeviceGroupController.java | 8 ++++++++ fys-admin/src/main/resources/application.yml | 2 ++ .../com/fuyuanshen/equipment/domain/bo/DeviceGroupBo.java | 3 +++ 3 files changed, 13 insertions(+) diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceGroupController.java b/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceGroupController.java index 0f0eafab..28601a1a 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceGroupController.java +++ b/fys-admin/src/main/java/com/fuyuanshen/web/controller/device/DeviceGroupController.java @@ -3,6 +3,8 @@ package com.fuyuanshen.web.controller.device; import java.util.List; import com.fuyuanshen.common.mybatis.core.page.TableDataInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.*; @@ -27,6 +29,7 @@ import com.fuyuanshen.equipment.service.IDeviceGroupService; * @author Lion Li * @date 2025-08-08 */ +@Tag(name = "web:设备分组", description = "web:设备分组") @Validated @RequiredArgsConstructor @RestController @@ -39,6 +42,7 @@ public class DeviceGroupController extends BaseController { /** * 查询设备分组列表 */ + @Operation(summary = "查询设备分组列表") @SaCheckPermission("fys-equipment:group:list") @GetMapping("/list") public R> list(DeviceGroupBo bo) { @@ -64,6 +68,7 @@ public class DeviceGroupController extends BaseController { * * @param id 主键 */ + @Operation(summary = "获取设备分组详细信息") @SaCheckPermission("fys-equipment:group:query") @GetMapping("/{id}") public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { @@ -74,6 +79,7 @@ public class DeviceGroupController extends BaseController { /** * 新增设备分组 */ + @Operation(summary = "新增设备分组") @SaCheckPermission("fys-equipment:group:add") @Log(title = "设备分组", businessType = BusinessType.INSERT) @RepeatSubmit() @@ -86,6 +92,7 @@ public class DeviceGroupController extends BaseController { /** * 修改设备分组 */ + @Operation(summary = "修改设备分组") @SaCheckPermission("fys-equipment:group:edit") @Log(title = "设备分组", businessType = BusinessType.UPDATE) @RepeatSubmit() @@ -99,6 +106,7 @@ public class DeviceGroupController extends BaseController { * * @param ids 主键串 */ + @Operation(summary = "删除设备分组") @SaCheckPermission("fys-equipment:group:remove") @Log(title = "设备分组", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") diff --git a/fys-admin/src/main/resources/application.yml b/fys-admin/src/main/resources/application.yml index 3b4dce13..21924a61 100644 --- a/fys-admin/src/main/resources/application.yml +++ b/fys-admin/src/main/resources/application.yml @@ -219,6 +219,8 @@ springdoc: packages-to-scan: com.fuyuanshen.customer - group: APP模块 packages-to-scan: com.fuyuanshen.app + - group: 设备分组 + packages-to-scan: com.fuyuanshen.web.controller.device # 防止XSS攻击 xss: diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/DeviceGroupBo.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/DeviceGroupBo.java index 37cff3b9..064c5aa2 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/DeviceGroupBo.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/domain/bo/DeviceGroupBo.java @@ -30,18 +30,21 @@ public class DeviceGroupBo extends BaseEntity { /** * 分组名称 */ + @Schema(name = "分组名称") @NotBlank(message = "分组名称不能为空", groups = { AddGroup.class, EditGroup.class }) private String groupName; /** * 状态:0-禁用,1-正常 */ + @Schema(name = "状态:0-禁用,1-正常") // @NotNull(message = "状态:0-禁用,1-正常不能为空", groups = { AddGroup.class, EditGroup.class }) private Long status; /** * 父分组ID */ + @Schema(name = "父分组ID") private Long parentId; /** From 95aa01e1c253777166836659704963415617abe8 Mon Sep 17 00:00:00 2001 From: DragonWenLong <552045633@qq.com> Date: Fri, 22 Aug 2025 18:09:08 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(device):=20=E6=96=B0=E5=A2=9E=E6=98=9F?= =?UTF-8?q?=E6=B1=89=E8=AE=BE=E5=A4=87=E6=8E=A7=E5=88=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加星汉设备控制器 AppDeviceXinghanController- 实现星汉设备业务逻辑 DeviceXinghanBizService - 增加开机 LOGO 下发规则 XinghanBootLogoRule - 添加设备发送消息规则 XinghanSendMsgRule - 更新 MQTT 命令类型常量 XingHanCommandTypeConstants - 修改设备状态 JSON 结构 MqttXinghanJson --- .../device/AppDeviceXinghanController.java | 96 +++++++ .../mqtt/base/MqttXinghanCommandType.java | 9 + .../global/mqtt/base/MqttXinghanJson.java | 14 +- .../XingHanCommandTypeConstants.java | 4 + .../rule/xinghan/XinghanBootLogoRule.java | 143 ++++++++++ .../mqtt/rule/xinghan/XinghanSendMsgRule.java | 117 ++++++++ .../device/DeviceXinghanBizService.java | 269 ++++++++++++++++++ 7 files changed, 645 insertions(+), 7 deletions(-) create mode 100644 fys-admin/src/main/java/com/fuyuanshen/app/controller/device/AppDeviceXinghanController.java create mode 100644 fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanBootLogoRule.java create mode 100644 fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendMsgRule.java create mode 100644 fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java diff --git a/fys-admin/src/main/java/com/fuyuanshen/app/controller/device/AppDeviceXinghanController.java b/fys-admin/src/main/java/com/fuyuanshen/app/controller/device/AppDeviceXinghanController.java new file mode 100644 index 00000000..b4f53c51 --- /dev/null +++ b/fys-admin/src/main/java/com/fuyuanshen/app/controller/device/AppDeviceXinghanController.java @@ -0,0 +1,96 @@ +package com.fuyuanshen.app.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.common.core.domain.R; +import com.fuyuanshen.common.core.validate.AddGroup; +import com.fuyuanshen.common.ratelimiter.annotation.FunctionAccessAnnotation; +import com.fuyuanshen.common.web.core.BaseController; +import com.fuyuanshen.web.service.device.DeviceBJQBizService; +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; + +/** + * HBY670设备控制类 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/app/xinghan/device") +public class AppDeviceXinghanController extends BaseController { + + private final DeviceXinghanBizService appDeviceService; + /** + * 人员信息登记 + */ + @PostMapping(value = "/registerPersonInfo") +// @FunctionAccessAnnotation("registerPersonInfo") + public R registerPersonInfo(@Validated(AddGroup.class) @RequestBody AppPersonnelInfoBo bo) { + return toAjax(appDeviceService.registerPersonInfo(bo)); + } + + /** + * 上传设备logo图片 + */ + @PostMapping("/uploadLogo") + @FunctionAccessAnnotation("uploadLogo") + public R upload(@Validated @ModelAttribute AppDeviceLogoUploadDto bo) { + + MultipartFile file = bo.getFile(); + if(file.getSize()>1024*1024*2){ + return R.warn("图片不能大于2M"); + } + appDeviceService.uploadDeviceLogo(bo); + + return R.ok(); + } + + /** + * 静电预警档位 + * 3,2,1,0,分别表示高档/中档/低挡/关闭 + */ + @PostMapping("/DetectGradeSettings") + public R DetectGradeSettings(@RequestBody DeviceInstructDto params) { + // params 转 JSONObject + appDeviceService.upDetectGradeSettings(params); + return R.ok(); + } + + /** + * 照明档位 + * 照明档位,2,1,0,分别表示弱光/强光/关闭 + */ + @PostMapping("/LightGradeSettings") + public R LightGradeSettings(@RequestBody DeviceInstructDto params) { + // params 转 JSONObject + appDeviceService.upLightGradeSettings(params); + return R.ok(); + } + + /** + * SOS档位 + * SOS档位,2,1,0, 分别表示红蓝模式/爆闪模式/关闭 + */ + @PostMapping("/SOSGradeSettings") + public R SOSGradeSettings(@RequestBody DeviceInstructDto params) { + // params 转 JSONObject + appDeviceService.upSOSGradeSettings(params); + return R.ok(); + } + + /** + * 静止报警状态 + * 静止报警状态,0-未静止报警,1-正在静止报警。 + */ + @PostMapping("/ShakeBitSettings") + public R ShakeBitSettings(@RequestBody DeviceInstructDto params) { + // params 转 JSONObject + appDeviceService.upShakeBitSettings(params); + return R.ok(); + } + +} diff --git a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/base/MqttXinghanCommandType.java b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/base/MqttXinghanCommandType.java index 6f6a6a81..99e7bc78 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/base/MqttXinghanCommandType.java +++ b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/base/MqttXinghanCommandType.java @@ -13,8 +13,17 @@ public final class MqttXinghanCommandType { private MqttXinghanCommandType() {} public enum XinghanCommandTypeEnum { + /** + * 星汉设备主动上报数据 + */ GRADE_INFO(101), + /** + * 星汉开机LOGO + */ PIC_TRANS(102), + /** + * 星汉设备发送消息 (XingHan send msg) + */ TEX_TRANS(103), BREAK_NEWS(104), UNKNOWN(0); 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 fde03ac9..2d49b468 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 @@ -20,37 +20,37 @@ public class MqttXinghanJson { * 第三键值对,SOS档位,2,1,0, 分别表示红蓝模式/爆闪模式/关闭 */ @JsonProperty("sta_SOSGrade") - public int staSOSGrade; + public Integer staSOSGrade; /** * 第四键值对,剩余照明时间,0-5999,单位分钟。 */ @JsonProperty("sta_PowerTime") - public int staPowerTime; + public Integer staPowerTime; /** * 第五键值对,剩余电量百分比,0-100 */ @JsonProperty("sta_PowerPercent") - public int staPowerPercent; + public Integer staPowerPercent; /** * 第六键值对, 近电预警级别, 0-无预警,1-弱预警,2-中预警,3-强预警,4-非常强预警。 */ @JsonProperty("sta_DetectResult") - public int staDetectResult; + public Integer staDetectResult; /** * 第七键值对, 静止报警状态,0-未静止报警,1-正在静止报警。 */ @JsonProperty("staShakeBit") - public int sta_ShakeBit; + public Integer sta_ShakeBit; /** * 第八键值对, 4G信号强度,0-32,数值越大,信号越强。 */ @JsonProperty("sta_4gSinal") - public int sta4gSinal; + public Integer sta4gSinal; /** * 第九键值对,IMIE卡号 */ @JsonProperty("sta_imei") - public int staimei; + public String staimei; /** * 第十键值对,经度 */ diff --git a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/constants/XingHanCommandTypeConstants.java b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/constants/XingHanCommandTypeConstants.java index a7f4ba28..0b6f3d7c 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/constants/XingHanCommandTypeConstants.java +++ b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/constants/XingHanCommandTypeConstants.java @@ -13,4 +13,8 @@ public class XingHanCommandTypeConstants { * 星汉设备发送消息 (XingHan send msg) */ public static final String XingHan_ESEND_MSG = "Light_103"; + /** + * 星汉设备发送紧急通知 (XingHan break news) + */ + public static final String XingHan_BREAK_NEWS = "Light_104"; } diff --git a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanBootLogoRule.java b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanBootLogoRule.java new file mode 100644 index 00000000..96faf308 --- /dev/null +++ b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanBootLogoRule.java @@ -0,0 +1,143 @@ +package com.fuyuanshen.global.mqtt.rule.xinghan; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +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.MqttMessageRule; +import com.fuyuanshen.global.mqtt.base.MqttRuleContext; +import com.fuyuanshen.global.mqtt.config.MqttGateway; +import com.fuyuanshen.global.mqtt.constants.LightingCommandTypeConstants; +import com.fuyuanshen.global.mqtt.constants.MqttConstants; +import com.fuyuanshen.global.mqtt.constants.XingHanCommandTypeConstants; +import com.fuyuanshen.global.mqtt.listener.domain.FunctionAccessStatus; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.CRC32; + +import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY; +import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY; +import static com.fuyuanshen.common.core.utils.ImageToCArrayConverter.convertHexToDecimal; +import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_BOOT_LOGO_KEY_PREFIX; +import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX; + +/** + * 星汉设备开机 LOGO 下发规则: + *

+ * 1. 设备上行 sta_PicTarns=great! => 仅标记成功
+ * 2. 设备上行 sta_PicTarns=数字 => 下发第 N 块数据(256B/块,带 CRC32) + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class XinghanBootLogoRule implements MqttMessageRule { + + private final MqttGateway mqttGateway; + private final ObjectMapper objectMapper; + + @Override + public String getCommandType() { + return XingHanCommandTypeConstants.XingHan_BOOT_LOGO; + } + + @Override + public void execute(MqttRuleContext ctx) { + final String functionAccessKey = FUNCTION_ACCESS_KEY + ctx.getDeviceImei(); + try { + MqttXinghanLogoJson payload = objectMapper.convertValue( + ctx.getPayloadDict(), MqttXinghanLogoJson.class); + + String respText = payload.getStaPicTrans(); + log.warn("设备上报LOGO:{}", respText); + + // 1. great! —— 成功标记 + if ("great!".equalsIgnoreCase(respText)) { + RedisUtils.setCacheObject(functionAccessKey, + FunctionAccessStatus.OK.getCode(), Duration.ofSeconds(20)); + log.info("设备 {} 开机 LOGO 写入成功", ctx.getDeviceImei()); + return; + } + + // 2. 数字 —— 下发数据块 + int blockIndex; + try { + blockIndex = Integer.parseInt(respText); + } catch (NumberFormatException ex) { + log.warn("设备 {} LOGO 上报非法块号:{}", ctx.getDeviceImei(), respText); + return; + } + String hexImage = RedisUtils.getCacheObject( + GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + ctx.getDeviceImei() + DEVICE_BOOT_LOGO_KEY_PREFIX); + if (StringUtils.isEmpty(hexImage)) { + return; + } + + byte[] fullBin = ImageToCArrayConverter.convertStringToByteArray(hexImage); + byte[] chunk = ImageToCArrayConverter.getChunk(fullBin, blockIndex - 1, CHUNK_SIZE); + log.info("设备 {} 第 {} 块数据长度: {} bytes", ctx.getDeviceImei(), blockIndex, chunk.length); + + // 组装下发数据 + ArrayList dataFrame = new ArrayList<>(); + dataFrame.add(blockIndex); // 块号 + ImageToCArrayConverter.buildArr(convertHexToDecimal(chunk), dataFrame); + dataFrame.addAll(crc32AsList(chunk)); // CRC32 + + Map pub = new HashMap<>(); + pub.put("ins_PicTrans", dataFrame); + + String topic = MqttConstants.GLOBAL_PUB_KEY + ctx.getDeviceImei(); + String json = JsonUtils.toJsonString(pub); + mqttGateway.sendMsgToMqtt(topic, 1, json); + + log.info("下发开机 LOGO 数据 => topic:{}, payload:{}", topic, json); + + } catch (Exception e) { + log.error("处理设备 {} 开机 LOGO 失败", ctx.getDeviceImei(), e); + RedisUtils.setCacheObject(functionAccessKey, + FunctionAccessStatus.FAILED.getCode(), Duration.ofSeconds(20)); + } + } + + /* ---------- 内部工具 ---------- */ + + private static final int CHUNK_SIZE = 256; + + private static ArrayList crc32AsList(byte[] data) { + CRC32 crc = new CRC32(); + crc.update(data); + byte[] crcBytes = ByteBuffer.allocate(4) + .order(ByteOrder.BIG_ENDIAN) + .putInt((int) crc.getValue()) + .array(); + ArrayList list = new ArrayList<>(4); + for (byte b : crcBytes) { + list.add(Byte.toUnsignedInt(b)); + } + return list; + } + + /* ---------- DTO ---------- */ + + @Data + private static class MqttXinghanLogoJson { + /** + * 设备上行: + * 数字 -> 请求对应块号 + * great! -> 写入成功 + */ + @JsonProperty("sta_PicTrans") + private String staPicTrans; + } +} diff --git a/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendMsgRule.java b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendMsgRule.java new file mode 100644 index 00000000..68acb099 --- /dev/null +++ b/fys-admin/src/main/java/com/fuyuanshen/global/mqtt/rule/xinghan/XinghanSendMsgRule.java @@ -0,0 +1,117 @@ +package com.fuyuanshen.global.mqtt.rule.xinghan; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fuyuanshen.common.json.utils.JsonUtils; +import com.fuyuanshen.common.redis.utils.RedisUtils; +import com.fuyuanshen.global.mqtt.base.MqttMessageRule; +import com.fuyuanshen.global.mqtt.base.MqttRuleContext; +import com.fuyuanshen.global.mqtt.config.MqttGateway; +import com.fuyuanshen.global.mqtt.constants.MqttConstants; +import com.fuyuanshen.global.mqtt.constants.XingHanCommandTypeConstants; +import com.fuyuanshen.global.mqtt.listener.domain.FunctionAccessStatus; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY; +import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY; +import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX; + +/** + * 星汉设备发送消息 下发规则: + *

+ * 1. 设备上行 sta_TexTarns=genius! => 仅标记成功
+ * 2. 设备上行 sta_TexTarns=数字 => GBK编码,每行文字为一包,一共4包,第一字节为包序号 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class XinghanSendMsgRule implements MqttMessageRule { + + private final MqttGateway mqttGateway; + private final ObjectMapper objectMapper; + + @Override + public String getCommandType() { + return XingHanCommandTypeConstants.XingHan_ESEND_MSG; + } + + @Override + public void execute(MqttRuleContext ctx) { + String functionAccess = FUNCTION_ACCESS_KEY + ctx.getDeviceImei(); + try { + XinghanSendMsgRule.MqttXinghanMsgJson payload = objectMapper.convertValue( + ctx.getPayloadDict(), XinghanSendMsgRule.MqttXinghanMsgJson.class); + + String respText = payload.getStaTexTrans(); + log.info("设备上报人员信息: {} ", respText); + + // 1. genius! —— 成功标记 + if ("genius!".equalsIgnoreCase(respText)) { + RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.FAILED.getCode(), Duration.ofSeconds(20)); + log.info("设备 {} 发送消息完成", ctx.getDeviceImei()); + return; + } + // 2. 数字 —— 下发数据块 + int blockIndex; + try { + blockIndex = Integer.parseInt(respText); + } catch (NumberFormatException ex) { + log.warn("设备 {} 消息上报非法块号:{}", ctx.getDeviceImei(), respText); + return; + } + // 将发送的信息原文本以List形式存储在Redis中 + List data = RedisUtils.getCacheList(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + ctx.getDeviceImei() + ":app_send_message_data"); + if (data.isEmpty()) { + return; + } + // + ArrayList intData = new ArrayList<>(); + intData.add(blockIndex); + // 获取块原内容 转成GBK 再转成无符号十进制整数 + String blockTxt = data.get(blockIndex-1); + // 再按 GBK 编码把字符串转成字节数组,并逐个转为无符号十进制整数 + for (byte b : blockTxt.getBytes(GBK)) { + intData.add(b & 0xFF); // b & 0xFF 得到 0~255 的整数 + } + + Map map = new HashMap<>(); + map.put("ins_TexTrans", intData); + + String topic = MqttConstants.GLOBAL_PUB_KEY + ctx.getDeviceImei(); + String json = JsonUtils.toJsonString(map); + mqttGateway.sendMsgToMqtt(topic, 1, json); + log.info("发送设备信息数据到设备消息=>topic:{},payload:{}", + MqttConstants.GLOBAL_PUB_KEY + ctx.getDeviceImei(), + JsonUtils.toJsonString(map)); + + } catch (Exception e) { + log.error("处理发送设备信息时出错", e); + RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.FAILED.getCode(), Duration.ofSeconds(20)); + } + } + + private static final Charset GBK = Charset.forName("GBK"); + + /* ---------- DTO ---------- */ + + @Data + private static class MqttXinghanMsgJson { + /** + * 设备上行: + * 数字 -> 请求对应块号 + * genius! -> 写入成功 + */ + @JsonProperty("sta_TexTrans") + private String staTexTrans; + } +} 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 new file mode 100644 index 00000000..40c2d7af --- /dev/null +++ b/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceXinghanBizService.java @@ -0,0 +1,269 @@ +package com.fuyuanshen.web.service.device; + +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.fuyuanshen.app.domain.AppPersonnelInfo; +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.AppPersonnelInfoVo; +import com.fuyuanshen.app.mapper.AppPersonnelInfoMapper; +import com.fuyuanshen.common.core.constant.GlobalConstants; +import com.fuyuanshen.common.core.exception.ServiceException; +import com.fuyuanshen.common.core.utils.ImageToCArrayConverter; +import com.fuyuanshen.common.core.utils.MapstructUtils; +import com.fuyuanshen.common.core.utils.ObjectUtils; +import com.fuyuanshen.common.core.utils.StringUtils; +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.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.config.MqttGateway; +import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants; +import com.fuyuanshen.global.mqtt.constants.MqttConstants; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.time.Duration; +import java.util.*; + +import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY; +import static com.fuyuanshen.common.core.utils.Bitmap80x12Generator.buildArr; +import static com.fuyuanshen.common.core.utils.Bitmap80x12Generator.generateFixedBitmapData; +import static com.fuyuanshen.common.core.utils.ImageToCArrayConverter.convertHexToDecimal; +import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_BOOT_LOGO_KEY_PREFIX; +import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DeviceXinghanBizService { + + private final DeviceMapper deviceMapper; + private final AppPersonnelInfoMapper appPersonnelInfoMapper; + private final DeviceTypeMapper deviceTypeMapper; + private final MqttGateway mqttGateway; + private final DeviceLogMapper deviceLogMapper; + + /** + * 所有档位的描述表 + * key : 指令类型,如 "ins_DetectGrade"、"ins_LightGrade" …… + * value : Map 值 -> 描述 + */ + private static final Map> GRADE_DESC = Map.of( + "ins_DetectGrade", Map.of(1, "低档", 2, "中档", 3, "高档"), + "ins_LightGrade", Map.of(1, "强光", 2, "弱光"), + "ins_SOSGrade", Map.of(1, "爆闪模式", 2, "红蓝模式"), + "ins_ShakeBit", Map.of(0, "未静止报警", 1, "正在静止报警") + // 再加 4、5、6…… 档,直接往 Map 里塞即可 + ); + + /** + * 根据指令类型和值,返回中文描述 + */ + private static String resolveGradeDesc(String type, int value) { + return GRADE_DESC.getOrDefault(type, Map.of()) + .getOrDefault(value, "关闭"); + } + + /** + * 设置静电预警档位 + */ + public void upDetectGradeSettings(DeviceInstructDto dto) { + sendCommand(dto, "ins_DetectGrade","静电预警档位"); + } + + /** + * 设置照明档位 + */ + public void upLightGradeSettings(DeviceInstructDto dto) { + sendCommand(dto, "ins_LightGrade","照明档位"); + } + + /** + * 设置SOS档位 + */ + public void upSOSGradeSettings(DeviceInstructDto dto) { + sendCommand(dto, "ins_SOSGrade","SOS档位"); + } + + /** + * 设置强制报警 + */ + public void upShakeBitSettings(DeviceInstructDto dto) { + sendCommand(dto, "ins_ShakeBit","强制报警"); + } + + /** + * 上传设备logo + */ + public void uploadDeviceLogo(AppDeviceLogoUploadDto bo) { + try { + Device device = deviceMapper.selectById(bo.getDeviceId()); + if (device == null) { + throw new ServiceException("设备不存在"); + } + if (isDeviceOffline(device.getDeviceImei())) { + throw new ServiceException("设备已断开连接:" + device.getDeviceName()); + } + MultipartFile file = bo.getFile(); + + byte[] largeData = ImageToCArrayConverter.convertImageToCArray(file.getInputStream(), 160, 80, 25600); + log.info("长度:" + largeData.length); + + log.info("原始数据大小: {} 字节", largeData.length); + + int[] ints = convertHexToDecimal(largeData); + RedisUtils.setCacheObject(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + device.getDeviceImei() +DEVICE_BOOT_LOGO_KEY_PREFIX, Arrays.toString(ints), Duration.ofSeconds(5 * 60L)); + + Map payload = Map.of("ins_PicTrans", + Collections.singletonList(0)); + String topic = MqttConstants.GLOBAL_PUB_KEY + device.getDeviceImei(); + String json = JsonUtils.toJsonString(payload); + + try { + mqttGateway.sendMsgToMqtt(topic, 1, json); + } catch (Exception e) { + log.error("上传开机画面失败, topic={}, payload={}", topic, json, e); + throw new ServiceException("上传LOGO失败:" + e.getMessage()); + } + log.info("发送上传开机画面到设备消息=>topic:{},payload:{}", MqttConstants.GLOBAL_PUB_KEY+device.getDeviceImei(),json); + + recordDeviceLog(device.getId(), device.getDeviceName(), "上传开机画面", "上传开机画面", AppLoginHelper.getUserId()); + } catch (Exception e){ + throw new ServiceException("发送指令失败"); + } + } + + /** + * 人员登记 + * @param bo + */ + public boolean registerPersonInfo(AppPersonnelInfoBo bo) { + Long deviceId = bo.getDeviceId(); + Device deviceObj = deviceMapper.selectById(deviceId); + if (deviceObj == null) { + throw new RuntimeException("请先将设备入库!!!"); + } + 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.getUnitName()); + list.add(bo.getName()); + list.add(bo.getPosition()); + list.add(bo.getCode()); + RedisUtils.setCacheList(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + deviceObj.getDeviceImei() + ":app_send_message_data", list); + + Map payload = Map.of("ins_TexTrans", + Collections.singletonList(0)); + String topic = MqttConstants.GLOBAL_PUB_KEY + deviceObj.getDeviceImei(); + String json = JsonUtils.toJsonString(payload); + + try { + mqttGateway.sendMsgToMqtt(topic, 1, json); + } catch (Exception e) { + log.error("人员信息登记失败, topic={}, payload={}", topic, json, e); + throw new ServiceException("人员信息登记失败:" + e.getMessage()); + } + 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); + } 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; + } + } + + /* ---------------------------------- 私有通用方法 ---------------------------------- */ + + private void sendCommand(DeviceInstructDto dto, + String payloadKey,String deviceAction) { + long deviceId = dto.getDeviceId(); + Device device = deviceMapper.selectById(deviceId); + if (device == null) { + throw new ServiceException("设备不存在"); + } + if (isDeviceOffline(device.getDeviceImei())) { + throw new ServiceException("设备已断开连接:" + device.getDeviceName()); + } + + Integer value = Integer.parseInt(dto.getInstructValue()); + + Map payload = Map.of(payloadKey, + Collections.singletonList(value)); + + String topic = MqttConstants.GLOBAL_PUB_KEY + device.getDeviceImei(); + String json = JsonUtils.toJsonString(payload); + + try { + mqttGateway.sendMsgToMqtt(topic, 1, json); + } catch (Exception e) { + log.error("发送指令失败, topic={}, payload={}", topic, json, e); + throw new ServiceException("发送指令失败:" + e.getMessage()); + } + + log.info("发送指令成功 => topic:{}, payload:{}", topic, json); + String content = resolveGradeDesc("ins_DetectGrade", value); + recordDeviceLog(device.getId(), + device.getDeviceName(), + deviceAction, + content, + AppLoginHelper.getUserId()); + } + + private boolean isDeviceOffline(String imei) { + // 原方法名语义相反,这里取反,使含义更清晰 + return getDeviceStatus(imei); + } + + /** + * 记录设备操作日志 + * @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); + } + } + + private boolean getDeviceStatus(String deviceImei) { + String deviceOnlineStatusRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ deviceImei + DeviceRedisKeyConstants.DEVICE_ONLINE_STATUS_KEY_PREFIX ; + return StringUtils.isBlank(deviceOnlineStatusRedisKey); + } + +}