Merge branch 'master' of http://47.109.37.87:3000/by2025/SmartParks
Some checks are pending
Gitea Actions Demo / Explore-Gitea-Actions (push) Waiting to run

# Conflicts:
#	pom.xml
#	ruoyi-modules/Property/pom.xml
This commit is contained in:
15683799673
2025-07-21 03:37:26 +08:00
38 changed files with 1021 additions and 92 deletions

View File

@@ -122,6 +122,12 @@
<version>4.5.2_1</version>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
</dependencies>
<build>

View File

@@ -1,5 +1,6 @@
package org.dromara.sis.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.sis.domain.SisAuthRecord;
import org.dromara.sis.domain.vo.SisAuthRecordVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
@@ -12,6 +13,7 @@ import java.util.List;
* @author lsm
* @since 2025-07-14
*/
@Mapper
public interface SisAuthRecordMapper extends BaseMapperPlus<SisAuthRecord, SisAuthRecordVo> {
List<SisAuthRecordVo> checkAuth(Long personId);
}

View File

@@ -0,0 +1,20 @@
package org.dromara.sis.sdk.smartDevices.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @author lsm
* @apiNote PowerFrame
* @since 2025/7/20
*/
@Data
@AllArgsConstructor
public class PowerFrame {
private byte[] address;
private byte controlCode;
private byte[] data;
}

View File

@@ -0,0 +1,139 @@
package org.dromara.sis.sdk.smartDevices.utils;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
/**
* @author lsm
* @apiNote LightingUtil
* @since 2025/7/20
*/
@Slf4j
public class LightingUtil {
private final MqttClient mqttClient;
private final String productKey;
private final String deviceName;
private final Gson gson = new Gson();
// 初始化连接参数
public LightingUtil(String brokerUrl, String productKey, String deviceName,
String username, String password) throws MqttException {
this.productKey = productKey;
this.deviceName = deviceName;
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(username);
options.setPassword(password.toCharArray());
options.setCleanSession(true);
mqttClient = new MqttClient(brokerUrl, deviceName, new MemoryPersistence());
mqttClient.connect(options);
// 订阅网关上报主题
String subscribeTopic = "/sys/" + productKey + "/+/thing/event/+/post";
mqttClient.subscribe(subscribeTopic, this::handleIncomingMessage);
}
// 基础指令构造
private JsonObject createBaseCommand(int code, String area, String address, String action) {
JsonObject command = new JsonObject();
command.addProperty("code", code);
command.addProperty("deviceName", deviceName);
command.addProperty("area", area);
command.addProperty("address", address);
command.addProperty("action", action);
command.addProperty("identity", "");
return command;
}
// 灯具控制指令
public void sendLightCommand(int code, String area, String address, String action, String params)
throws MqttException {
JsonObject command = createBaseCommand(code, area, address, action);
if (params != null) command.addProperty("params", params);
String topic = "/" + productKey + "/" + deviceName + "/user/get";
mqttClient.publish(topic, new MqttMessage(gson.toJson(command).getBytes()));
}
// 常用快捷方法
public void turnOnLight(String area, String groupAddress) throws MqttException {
sendLightCommand(200, area, groupAddress, "lightOn", null);
}
public void turnOffLight(String area, String groupAddress) throws MqttException {
sendLightCommand(200, area, groupAddress, "lightOff", null);
}
public void setBrightness(String area, String address, int brightness) throws MqttException {
sendLightCommand(200, area, address, "setHighBright", String.valueOf(brightness));
}
// 上报数据处理
private void handleIncomingMessage(String topic, MqttMessage message) {
try {
JsonObject payload = gson.fromJson(new String(message.getPayload()), JsonObject.class);
String method = payload.get("method").getAsString();
switch (method) {
case "thing.event.heartbeat.post":
processHeartbeat(payload.getAsJsonObject("params"));
break;
case "thing.event.consumption.post":
processEnergyData(payload.getAsJsonObject("params"));
break;
case "thing.event.trigger.post":
processSensorTrigger(payload.getAsJsonObject("params"));
break;
// 添加其他事件处理...
}
} catch (Exception e) {
log.error("MQTT消息处理异常topic: {}", topic, e);
}
}
// 心跳处理
private void processHeartbeat(JsonObject params) {
JsonObject value = params.getAsJsonObject("value");
String uuid = value.get("uuid").getAsString();
String area = value.get("area").getAsString();
System.out.println("设备在线: " + uuid + " | 区域: " + area);
}
// 能耗处理
private void processEnergyData(JsonObject params) {
JsonObject value = params.getAsJsonObject("value");
String uuid = value.get("uuid").getAsString();
double power = value.get("power").getAsDouble();
System.out.println("能耗报告: " + uuid + " | 功率: " + power + "W");
}
// 传感器触发处理
private void processSensorTrigger(JsonObject params) {
JsonObject value = params.getAsJsonObject("value");
long trigTime = value.get("trig_time").getAsLong();
String area = value.get("area").getAsString();
System.out.println("传感器触发: 区域=" + area + " | 时间=" + trigTime);
}
// 网关管理
public void rebootGateway(int delaySeconds) throws MqttException {
JsonObject command = createBaseCommand(400, "00 00", "FF FF", "reboot");
command.addProperty("params", String.valueOf(delaySeconds));
String topic = "/" + productKey + "/" + deviceName + "/user/get";
mqttClient.publish(topic, new MqttMessage(gson.toJson(command).getBytes()));
}
// 关闭连接
public void disconnect() throws MqttException {
mqttClient.disconnect();
}
}

View File

@@ -0,0 +1,179 @@
package org.dromara.sis.sdk.smartDevices.utils;
import org.dromara.sis.sdk.smartDevices.domain.PowerFrame;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* @author lsm
* @apiNote PowerMeterUtil
* @since 2025/7/20
*/
public class PowerMeterUtil {
// 协议常量定义
public static final byte FRAME_START = 0x68;
public static final byte FRAME_END = 0x16;
public static final byte[] PREAMBLE = {(byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE};
public static final int ADDR_LENGTH = 6;
public static final int MAX_READ_DATA_LEN = 200;
public static final int MAX_WRITE_DATA_LEN = 50;
// 控制码功能定义
public static final byte CTRL_BROADCAST_TIME = 0x08;
public static final byte CTRL_READ_DATA = 0x11;
public static final byte CTRL_READ_FOLLOW_DATA = 0x12;
public static final byte CTRL_WRITE_DATA = 0x14;
public static final byte CTRL_TRIP_CONTROL = 0x1C;
public static final byte CTRL_OUTPUT_CONTROL = 0x1D;
// 地址通配符
public static final byte ADDR_WILDCARD = (byte) 0xAA;
/**
* 构建基础帧结构
*
* @param address 6字节地址(高位在前,低位在后)
* @param ctrlCode 控制码
* @param data 原始数据域(未加33H)
* @param isEncode 是否进行数据域处理
* @return 完整帧数据
*/
public byte[] buildFrame(byte[] address, byte ctrlCode, byte[] data, boolean isEncode) {
if (address.length != ADDR_LENGTH) {
throw new IllegalArgumentException("Address must be 6 bytes");
}
// 处理数据域每个字节加0x33
byte[] processedData = processDataDomain(data, isEncode);
// 计算数据域长度
int dataLen = (data != null) ? data.length : 0;
if (dataLen > MAX_READ_DATA_LEN) {
throw new IllegalArgumentException("Data length exceeds max limit");
}
// 计算总帧长度: 起始符(1) + 地址(6) + 起始符(1) + 控制码(1) + 长度(1) + 数据域 + 校验(1) + 结束符(1)
int totalLength = 11 + dataLen;
ByteBuffer buffer = ByteBuffer.allocate(totalLength)
.order(ByteOrder.LITTLE_ENDIAN);
// 地址域处理 (传输顺序: 低字节在前)
byte[] reversedAddr = reverseAddress(address);
// 构建帧
buffer.put(FRAME_START)
.put(reversedAddr)
.put(FRAME_START)
.put(ctrlCode)
.put((byte) dataLen);
if (dataLen > 0) {
buffer.put(processedData);
}
// 计算校验码 (从第一个0x68到数据域结束)
byte[] frameWithoutCs = Arrays.copyOf(buffer.array(), buffer.position());
byte cs = calculateChecksum(frameWithoutCs);
buffer.put(cs)
.put(FRAME_END);
return buffer.array();
}
/**
* 解析接收到的帧
* @param frame 完整帧数据(包含前导符)
* @return 解析结果对象
*/
public PowerFrame parseFrame(byte[] frame) {
// 跳过前导符 (0-3)
int startIndex = findFrameStart(frame);
if (startIndex == -1) {
throw new IllegalArgumentException("无效帧:未找到起始标记");
}
// 基本长度检查
if (frame.length < startIndex + 12) {
throw new IllegalArgumentException("接受帧太短");
}
// 提取地址域 (传输顺序: 低字节在前)
byte[] reversedAddr = Arrays.copyOfRange(frame, startIndex + 1, startIndex + 7);
byte[] address = reverseAddress(reversedAddr);
// 控制码
byte ctrlCode = frame[startIndex + 8];
// 数据域长度
int dataLen = frame[startIndex + 9] & 0xFF;
// 数据域位置
int dataStart = startIndex + 10;
int dataEnd = dataStart + dataLen;
// 校验位位置
int endPos = dataEnd + 1;
// 验证结束符
if (frame[endPos] != FRAME_END) {
throw new IllegalArgumentException("无效的帧结束标记");
}
// 提取原始数据域 (含33H处理)
byte[] rawData = Arrays.copyOfRange(frame, dataStart, dataEnd);
byte[] processedData = processDataDomain(rawData, false);
// 验证校验和
byte calculatedCs = calculateChecksum(Arrays.copyOfRange(frame, startIndex, dataEnd));
byte receivedCs = frame[dataEnd];
if (calculatedCs != receivedCs) {
throw new IllegalArgumentException("校验和不匹配");
}
return new PowerFrame(address, ctrlCode, processedData);
}
// 数据处理域:加/减33H
private byte[] processDataDomain(byte[] data, boolean isEncode) {
if (data == null || data.length == 0) return data;
byte[] result = new byte[data.length];
for (int i = 0; i < data.length; i++) {
result[i] = (byte) (isEncode ? (data[i] + 0x33) : (data[i] - 0x33));
}
return result;
}
// 地址反转 (传输顺序处理)
private byte[] reverseAddress(byte[] address) {
byte[] reversed = new byte[address.length];
for (int i = 0; i < address.length; i++) {
reversed[i] = address[address.length - 1 - i];
}
return reversed;
}
// 计算校验和 (模256和)
private byte calculateChecksum(byte[] data) {
int sum = 0;
for (byte b : data) {
sum = (sum + (b & 0xFF)) & 0xFF;
}
return (byte) sum;
}
// 在帧数据中查找起始符
private int findFrameStart(byte[] data) {
for (int i = 0; i < data.length - 1; i++) {
if (data[i] == FRAME_START && data[i + 1] != FRAME_START) {
return i;
}
}
return -1;
}
}

View File

@@ -0,0 +1,168 @@
package org.dromara.sis.sdk.smartDevices.utils;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* @author lsm
* @apiNote WaterMeterUtil
* @since 2025/7/20
*/
public class WaterMeterUtil {
// 协议常量定义
public static final byte PREAMBLE = (byte) 0xFE;
public static final byte FRAME_START = 0x68;
public static final byte FRAME_END = 0x16;
public static final byte WATER_METER_TYPE = 0x10;
public static final byte CTRL_READ = 0x01;
public static final byte CTRL_RESPONSE = (byte) 0x81;
public static final byte UNIT_TON = 0x2C;
public static final int ADDRESS_LENGTH = 7;
/**
* 构建读表数据命令帧
*
* @param meterAddress 12位表计地址字符串如"000000000000012"
* @param diHighFirst 数据标识字节序true=901Fh(高字节在前), false=1F90h(低字节在前)
* @return 完整的命令帧字节数组
*/
public static byte[] buildReadCommand(String meterAddress, boolean diHighFirst) {
// 1. 地址转换12位字符串 -> 7字节BCD码逆序分组
byte[] addressBytes = convertAddress(meterAddress);
// 2. 构建帧主体(不含前导符和帧尾)
ByteBuffer buffer = ByteBuffer.allocate(32);
buffer.put(FRAME_START);
buffer.put(WATER_METER_TYPE);
buffer.put(addressBytes);
buffer.put(CTRL_READ);
buffer.put((byte) 0x03); // 数据域长度
// 数据标识处理
if (diHighFirst) {
buffer.put((byte) 0x90);
buffer.put((byte) 0x1F);
} else {
buffer.put((byte) 0x1F);
buffer.put((byte) 0x90);
}
buffer.put((byte) 0x00); // 序列号
// 3. 计算校验码从FRAME_START到序列号
byte[] frameBody = Arrays.copyOf(buffer.array(), buffer.position());
byte cs = calculateChecksum(frameBody, 0, frameBody.length);
// 4. 组装完整帧
buffer.put(cs);
buffer.put(FRAME_END);
// 5. 添加前导符
byte[] fullFrame = Arrays.copyOf(buffer.array(), buffer.position());
return addPreamble(fullFrame);
}
/**
* 解析读表响应数据
*
* @param response 完整响应帧(含前导符)
* @return 解析后的累积流量值(单位:吨)
* @throws IllegalArgumentException 响应格式错误
*/
public static double parseReadResponse(byte[] response) {
// 1. 跳过前导符(0xFE x3)
int startIndex = 3;
if (response[startIndex] != FRAME_START) {
throw new IllegalArgumentException("无效帧起始符");
}
// 2. 基础信息解析
int pos = startIndex + 1;
byte meterType = response[pos++];
byte[] address = Arrays.copyOfRange(response, pos, pos + ADDRESS_LENGTH);
pos += ADDRESS_LENGTH;
byte ctrlCode = response[pos++];
if (ctrlCode != CTRL_RESPONSE) {
throw new IllegalArgumentException("无效控制码");
}
// 3. 数据域解析
int dataLen = response[pos++] & 0xFF;
byte[] di = {response[pos++], response[pos++]}; // 数据标识
byte ser = response[pos++]; // 序列号
// 4. 累积流量解析 (4字节BCD)
byte[] currentFlow = Arrays.copyOfRange(response, pos, pos + 4);
pos += 4;
// 5. 单位校验
if (response[pos++] != UNIT_TON) {
throw new IllegalArgumentException("无效计量单位");
}
// 6. 流量值转换
return parseFlowValue(currentFlow);
}
/**
* 计算校验码 (CJ/T188-2004标准)
*
* @param data 待计算数据
* @param offset 起始位置
* @param length 数据长度
* @return 校验码
*/
public static byte calculateChecksum(byte[] data, int offset, int length) {
int sum = 0;
for (int i = offset; i < offset + length; i++) {
sum += (data[i] & 0xFF);
}
return (byte) (sum % 256);
}
// 地址转换12位字符串 -> 7字节BCD码逆序分组
private static byte[] convertAddress(String address) {
if (address.length() != 12) {
throw new IllegalArgumentException("地址长度必须为12位");
}
// 填充为14位7字节*2
String padded = "00" + address;
byte[] result = new byte[ADDRESS_LENGTH];
// 逆序分组转换
for (int i = 0; i < ADDRESS_LENGTH; i++) {
int end = padded.length() - i * 2;
int start = end - 2;
String segment = padded.substring(start, end);
result[i] = (byte) Integer.parseInt(segment, 16);
}
return result;
}
// 添加前导符 0xFE x3
private static byte[] addPreamble(byte[] frame) {
byte[] result = new byte[frame.length + 3];
result[0] = PREAMBLE;
result[1] = PREAMBLE;
result[2] = PREAMBLE;
System.arraycopy(frame, 0, result, 3, frame.length);
return result;
}
// 解析BCD流量值4字节 -> 浮点数)
private static double parseFlowValue(byte[] data) {
// 拼接BCD数字串
StringBuilder sb = new StringBuilder();
for (byte b : data) {
sb.append(String.format("%02X", b));
}
// 转换为数值最后2位是小数位
String numStr = sb.toString();
return Double.parseDouble(numStr.substring(0, numStr.length() - 2) +
Double.parseDouble(numStr.substring(numStr.length() - 2)) / 100.0);
}
}

View File

@@ -2,6 +2,7 @@ package org.dromara.sis.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import org.apache.dubbo.config.annotation.DubboReference;
import org.dromara.common.core.domain.TreeNode;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -11,6 +12,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.property.api.RemoteFloorService;
import org.dromara.property.api.domain.vo.RemoteFloorVo;
import org.dromara.sis.domain.bo.SisAccessControlBo;
import org.dromara.sis.domain.bo.SisElevatorInfoBo;
import org.dromara.sis.domain.vo.SisAccessControlVo;
@@ -25,6 +28,7 @@ import org.dromara.sis.mapper.SisAuthRecordMapper;
import org.dromara.sis.service.ISisAuthRecordService;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Collection;
@@ -45,6 +49,9 @@ public class SisAuthRecordServiceImpl implements ISisAuthRecordService {
private final ISisAccessControlService accessControlService;
private final ISisElevatorInfoService elevatorInfoService;
@DubboReference
private RemoteFloorService remoteFloorService;
/**
* 查询授权记录
*
@@ -247,19 +254,36 @@ public class SisAuthRecordServiceImpl implements ISisAuthRecordService {
return node;
}).toList();
List<TreeNode<Long>> eleChildrenList = eleVoList.stream().map(item -> {
TreeNode<Long> node = new TreeNode<>();
node.setLevel(2);
node.setCode(item.getElevatorId());
node.setParentCode(2L);
node.setLabel(item.getElevatorName());
return node;
}).toList();
List<TreeNode<Long>> eleChildrenList = new ArrayList<>();
eleVoList.forEach(item -> {
// 电梯子节点
TreeNode<Long> eleNode = new TreeNode<>();
eleNode.setLevel(2);
eleNode.setParentCode(2L);
eleNode.setCode(item.getElevatorId());
eleNode.setLabel(item.getElevatorName());
// 楼层节点
List<TreeNode<Long>> floorTree = new ArrayList<>();
// 获取楼层
List<RemoteFloorVo> floorInfoList = remoteFloorService.queryByUnitId(item.getUnitId());
floorInfoList.forEach(floor -> {
TreeNode<Long> floorNode = new TreeNode<>();
floorNode.setLevel(3);
floorNode.setCode(floor.getId());
floorNode.setLabel(floor.getFloorName());
floorNode.setParentCode(item.getElevatorId());
floorTree.add(floorNode);
});
eleNode.setChildren(floorTree);
eleChildrenList.add(eleNode);
});
// 将子节点列表分别添加到对应的父节点
accessNode.setChildren(acChildrenList);
elevatorNode.setChildren(eleChildrenList);
// 最后将两个父节点添加到根节点
root.setChildren(List.of(accessNode, elevatorNode));
return List.of(root);