jingquan #8

Merged
dyf merged 6 commits from liwenlong/fys-Multi-tenant:jingquan into main 2025-08-22 18:26:44 +08:00
14 changed files with 1054 additions and 17 deletions

View File

@ -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<Void> registerPersonInfo(@Validated(AddGroup.class) @RequestBody AppPersonnelInfoBo bo) {
return toAjax(appDeviceService.registerPersonInfo(bo));
}
/**
* 上传设备logo图片
*/
@PostMapping("/uploadLogo")
@FunctionAccessAnnotation("uploadLogo")
public R<Void> 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<Void> DetectGradeSettings(@RequestBody DeviceInstructDto params) {
// params 转 JSONObject
appDeviceService.upDetectGradeSettings(params);
return R.ok();
}
/**
* 照明档位
* 照明档位2,1,0,分别表示弱光/强光/关闭
*/
@PostMapping("/LightGradeSettings")
public R<Void> LightGradeSettings(@RequestBody DeviceInstructDto params) {
// params 转 JSONObject
appDeviceService.upLightGradeSettings(params);
return R.ok();
}
/**
* SOS档位
* SOS档位2,1,0, 分别表示红蓝模式/爆闪模式/关闭
*/
@PostMapping("/SOSGradeSettings")
public R<Void> SOSGradeSettings(@RequestBody DeviceInstructDto params) {
// params 转 JSONObject
appDeviceService.upSOSGradeSettings(params);
return R.ok();
}
/**
* 静止报警状态
* 静止报警状态0-未静止报警1-正在静止报警。
*/
@PostMapping("/ShakeBitSettings")
public R<Void> ShakeBitSettings(@RequestBody DeviceInstructDto params) {
// params 转 JSONObject
appDeviceService.upShakeBitSettings(params);
return R.ok();
}
}

View File

@ -0,0 +1,65 @@
package com.fuyuanshen.global.mqtt.base;
import cn.hutool.core.lang.Dict;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Collections;
import org.springframework.stereotype.Component;
@Component
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);
private final int value;
XinghanCommandTypeEnum(int value) { this.value = value; }
public int getValue() { return value; }
}
private static final Map<String, XinghanCommandTypeEnum> KEY_TO_TYPE;
static {
LinkedHashMap<String, XinghanCommandTypeEnum> map = new LinkedHashMap<>();
map.put("sta_DetectGrade", XinghanCommandTypeEnum.GRADE_INFO);
map.put("sta_PowerTime", XinghanCommandTypeEnum.GRADE_INFO);
map.put("sta_longitude", XinghanCommandTypeEnum.GRADE_INFO);
map.put("sta_latitude", XinghanCommandTypeEnum.GRADE_INFO);
map.put("sta_PicTrans", XinghanCommandTypeEnum.PIC_TRANS);
map.put("sta_TexTrans", XinghanCommandTypeEnum.TEX_TRANS);
map.put("sta_BreakNews", XinghanCommandTypeEnum.BREAK_NEWS);
KEY_TO_TYPE = Collections.unmodifiableMap(map);
}
public static int computeVirtualCommandType(Dict payloadDict) {
if (payloadDict == null) {
return XinghanCommandTypeEnum.UNKNOWN.getValue();
}
try {
for (String key : KEY_TO_TYPE.keySet()) {
if (payloadDict.containsKey(key)) {
return KEY_TO_TYPE.get(key).getValue();
}
}
} catch (Exception ex) {
return XinghanCommandTypeEnum.UNKNOWN.getValue();
}
return XinghanCommandTypeEnum.UNKNOWN.getValue();
}
}

View File

@ -0,0 +1,65 @@
package com.fuyuanshen.global.mqtt.base;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonProperty;
@Data
public class MqttXinghanJson {
/**
* 第一键值对静电预警档位3,2,1,0,分别表示高档/中档/低挡/关闭.
*/
@JsonProperty("sta_DetectGrade")
private Integer staDetectGrade;
/**
* 第二键值对照明档位2,1,0,分别表示弱光/强光/关闭
*/
@JsonProperty("sta_LightGrade")
private Integer staLightGrade;
/**
* 第三键值对SOS档位2,1,0, 分别表示红蓝模式/爆闪模式/关闭
*/
@JsonProperty("sta_SOSGrade")
public Integer staSOSGrade;
/**
* 第四键值对剩余照明时间0-5999单位分钟。
*/
@JsonProperty("sta_PowerTime")
public Integer staPowerTime;
/**
* 第五键值对剩余电量百分比0-100
*/
@JsonProperty("sta_PowerPercent")
public Integer staPowerPercent;
/**
* 第六键值对, 近电预警级别, 0-无预警1-弱预警2-中预警3-强预警4-非常强预警。
*/
@JsonProperty("sta_DetectResult")
public Integer staDetectResult;
/**
* 第七键值对, 静止报警状态0-未静止报警1-正在静止报警。
*/
@JsonProperty("staShakeBit")
public Integer sta_ShakeBit;
/**
* 第八键值对, 4G信号强度0-32数值越大信号越强。
*/
@JsonProperty("sta_4gSinal")
public Integer sta4gSinal;
/**
* 第九键值对IMIE卡号
*/
@JsonProperty("sta_imei")
public String staimei;
/**
* 第十键值对,经度
*/
@JsonProperty("sta_longitude")
public String stalongitude;
/**
* 第十一键值对,纬度
*/
@JsonProperty("sta_latitude")
public String stalatitude;
}

View File

@ -0,0 +1,20 @@
package com.fuyuanshen.global.mqtt.constants;
public class XingHanCommandTypeConstants {
/**
* 星汉设备主动上报数据 (XingHan Device Data)
*/
public static final String XingHan_DEVICE_DATA = "Light_101";
/**
* 星汉开机LOGO (XingHan Boot Logo)
*/
public static final String XingHan_BOOT_LOGO = "Light_102";
/**
* 星汉设备发送消息 (XingHan send msg)
*/
public static final String XingHan_ESEND_MSG = "Light_103";
/**
* 星汉设备发送紧急通知 (XingHan break news)
*/
public static final String XingHan_BREAK_NEWS = "Light_104";
}

View File

@ -8,6 +8,7 @@ import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
import com.fuyuanshen.global.mqtt.base.MqttRuleEngine;
import com.fuyuanshen.global.mqtt.base.MqttXinghanCommandType;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -69,5 +70,19 @@ public class ReceiverMessageHandler implements MessageHandler {
log.warn("未找到匹配的规则来处理命令类型: {}", val1);
}
}
/* ===== 追加:根据报文内容识别格式并统一解析 ===== */
int intType = MqttXinghanCommandType.computeVirtualCommandType(payloadDict);
if (intType > 0) {
MqttRuleContext newCtx = new MqttRuleContext();
newCtx.setCommandType((byte) intType);
newCtx.setDeviceImei(deviceImei);
newCtx.setPayloadDict(payloadDict);
boolean ok = ruleEngine.executeRule(newCtx);
if (!ok) {
log.warn("新规则引擎未命中, imei={}", deviceImei);
}
}
}
}

View File

@ -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 下发规则:
* <p>
* 1. 设备上行 sta_PicTarns=great! => 仅标记成功<br>
* 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<Integer> dataFrame = new ArrayList<>();
dataFrame.add(blockIndex); // 块号
ImageToCArrayConverter.buildArr(convertHexToDecimal(chunk), dataFrame);
dataFrame.addAll(crc32AsList(chunk)); // CRC32
Map<String, Object> 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<Integer> 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<Integer> 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;
}
}

View File

@ -0,0 +1,212 @@
package com.fuyuanshen.global.mqtt.rule.xinghan;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.equipment.utils.map.GetAddressFromLatUtil;
import com.fuyuanshen.equipment.utils.map.LngLonUtil;
import com.fuyuanshen.global.mqtt.base.MqttMessageRule;
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
import com.fuyuanshen.global.mqtt.base.MqttXinghanJson;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import com.fuyuanshen.global.mqtt.constants.LightingCommandTypeConstants;
import com.fuyuanshen.global.mqtt.constants.XingHanCommandTypeConstants;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.*;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 主动上报设备数据命令处理
* 第一键值对静电预警档位3,2,1,0,分别表示高档/中档/低挡/关闭.
* 第二键值对照明档位2,1,0,分别表示弱光/强光/关闭
* 第三键值对SOS档位2,1,0, 分别表示红蓝模式/爆闪模式/关闭
* 第四键值对剩余照明时间0-5999单位分钟。
* 第五键值对, 剩余电量百分比0-100。
* 第六键值对, 近电预警级别, 0-无预警1-弱预警2-中预警3-强预警4-非常强预警。
* 第七键值对, 静止报警状态0-未静止报警1-正在静止报警。
* 第八键值对, 4G信号强度0-32数值越大信号越强。
* 第九键值对IMIE卡号
* 第十键值对,经度
* 第十一键值对,纬度
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class XinghanDeviceDataRule implements MqttMessageRule {
@Override
public String getCommandType() {
return XingHanCommandTypeConstants.XingHan_DEVICE_DATA;
}
@Autowired
private ObjectMapper objectMapper;
@Override
public void execute(MqttRuleContext context) {
try {
// Latitude, longitude
//主灯档位,激光灯档位,电量百分比,充电状态,电池剩余续航时间
MqttXinghanJson deviceStatus = objectMapper.convertValue(context.getPayloadDict(), MqttXinghanJson.class);
// 发送设备状态和位置信息到Redis
asyncSendDeviceDataToRedisWithFuture(context.getDeviceImei(),deviceStatus);
} catch (Exception e) {
log.error("处理上报数据命令时出错", e);
}
}
/**
* 发送设备状态信息和位置信息到Redis
*
* @param deviceImei 设备IMEI
* @param deviceStatus 机器主动上报的状态信息
*/
public void asyncSendDeviceDataToRedisWithFuture(String deviceImei, MqttXinghanJson deviceStatus) {
CompletableFuture.runAsync(() -> {
try {
// 将设备状态信息存储到Redis中
String deviceRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DeviceRedisKeyConstants.DEVICE_KEY_PREFIX + deviceImei + DEVICE_STATUS_KEY_PREFIX;
String deviceInfoJson = JsonUtils.toJsonString(deviceStatus);
// 存储到Redis
RedisUtils.setCacheObject(deviceRedisKey, deviceInfoJson);
log.info("设备状态信息已异步发送到Redis: device={}, deviceInfoJson={}",
deviceImei, deviceInfoJson);
//设备坐标缓存KEY
String functionAccess = FUNCTION_ACCESS_KEY + deviceImei;
// 异步发送经纬度到Redis
asyncSendLocationToRedisWithFuture(deviceImei, deviceStatus.getStalatitude(), deviceStatus.getStalongitude());
} catch (Exception e) {
log.error("异步发送设备信息到Redis时出错: device={}, error={}", deviceImei, e.getMessage(), e);
}
});
}
/**
* 异步发送位置信息到Redis使用CompletableFuture
*
* @param deviceImei 设备IMEI
* @param latitude 纬度
* @param longitude 经度
*/
public void asyncSendLocationToRedisWithFuture(String deviceImei, String latitude, String longitude) {
CompletableFuture.runAsync(() -> {
try {
if(StringUtils.isBlank(latitude) || StringUtils.isBlank(longitude)){
return;
}
String[] latArr = latitude.split("\\.");
String[] lonArr = longitude.split("\\.");
// 将位置信息存储到Redis中
String redisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX+ deviceImei + DEVICE_LOCATION_KEY_PREFIX;
String redisObj = RedisUtils.getCacheObject(redisKey);
JSONObject jsonOBj = JSONObject.parseObject(redisObj);
if(jsonOBj != null){
String str1 = latArr[0] +"."+ latArr[1].substring(0,4);
String str2 = lonArr[0] +"."+ lonArr[1].substring(0,4);
String cacheLatitude = jsonOBj.getString("wgs84_latitude");
String cacheLongitude = jsonOBj.getString("wgs84_longitude");
String[] latArr1 = cacheLatitude.split("\\.");
String[] lonArr1 = cacheLongitude.split("\\.");
String cacheStr1 = latArr1[0] +"."+ latArr1[1].substring(0,4);
String cacheStr2 = lonArr1[0] +"."+ lonArr1[1].substring(0,4);
if(str1.equals(cacheStr1) && str2.equals(cacheStr2)){
log.info("位置信息未发生变化: device={}, lat={}, lon={}", deviceImei, latitude, longitude);
return;
}
}
// 构造位置信息对象
Map<String, Object> locationInfo = new LinkedHashMap<>();
double[] doubles = LngLonUtil.gps84_To_Gcj02(Double.parseDouble(latitude), Double.parseDouble(longitude));
locationInfo.put("deviceImei", deviceImei);
locationInfo.put("latitude", doubles[0]);
locationInfo.put("longitude", doubles[1]);
locationInfo.put("wgs84_latitude", latitude);
locationInfo.put("wgs84_longitude", longitude);
String address = GetAddressFromLatUtil.getAdd(String.valueOf(doubles[1]), String.valueOf(doubles[0]));
locationInfo.put("address", address);
locationInfo.put("timestamp", System.currentTimeMillis());
String locationJson = JsonUtils.toJsonString(locationInfo);
// 存储到Redis
RedisUtils.setCacheObject(redisKey, locationJson);
// 存储到一个列表中,保留历史位置信息
// String locationHistoryKey = GlobalConstants.GLOBAL_REDIS_KEY+DeviceRedisKeyConstants.DEVICE_LOCATION_HISTORY_KEY_PREFIX + deviceImei;
// RedisUtils.addCacheList(locationHistoryKey, locationJson);
// RedisUtils.expire(locationHistoryKey, Duration.ofDays(90));
storeDeviceTrajectoryWithSortedSet(deviceImei, locationJson);
log.info("位置信息已异步发送到Redis: device={}, lat={}, lon={}", deviceImei, latitude, longitude);
} catch (Exception e) {
log.error("异步发送位置信息到Redis时出错: device={}, error={}", deviceImei, e.getMessage(), e);
}
});
}
/**
* 存储设备30天历史轨迹到Redis (使用Sorted Set)
*/
public void storeDeviceTrajectoryWithSortedSet(String deviceImei, String locationJson) {
try {
String trajectoryKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + deviceImei + DeviceRedisKeyConstants.DEVICE_LOCATION_HISTORY_KEY_PREFIX;
// String trajectoryKey = "device:trajectory:zset:" + deviceImei;
// String locationJson = JsonUtils.toJsonString(locationInfo);
long timestamp = System.currentTimeMillis();
// 添加到Sorted Set使用时间戳作为score
RedisUtils.zAdd(trajectoryKey, locationJson, timestamp);
// // 设置30天过期时间
// RedisUtils.expire(trajectoryKey, Duration.ofDays(30));
// 清理30天前的数据冗余保护
long thirtyDaysAgo = System.currentTimeMillis() - (7L * 24 * 60 * 60 * 1000);
RedisUtils.zRemoveRangeByScore(trajectoryKey, 0, thirtyDaysAgo);
} catch (Exception e) {
log.error("存储设备轨迹到Redis(ZSet)失败: device={}, error={}", deviceImei, e.getMessage(), e);
}
}
private Map<String, Object> buildLocationDataMap(String latitude, String longitude) {
String[] latArr = latitude.split("\\.");
String[] lonArr = longitude.split("\\.");
ArrayList<Integer> intData = new ArrayList<>();
intData.add(11);
intData.add(Integer.parseInt(latArr[0]));
String str1 = latArr[1];
intData.add(Integer.parseInt(str1.substring(0,4)));
String str2 = lonArr[1];
intData.add(Integer.parseInt(lonArr[0]));
intData.add(Integer.parseInt(str2.substring(0,4)));
Map<String, Object> map = new HashMap<>();
map.put("instruct", intData);
return map;
}
}

View File

@ -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;
/**
* 星汉设备发送消息 下发规则:
* <p>
* 1. 设备上行 sta_TexTarns=genius! => 仅标记成功<br>
* 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<String>形式存储在Redis中
List<String> data = RedisUtils.getCacheList(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + ctx.getDeviceImei() + ":app_send_message_data");
if (data.isEmpty()) {
return;
}
//
ArrayList<Integer> 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<String, Object> 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;
}
}

View File

@ -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<Integer,String> 值 -> 描述
*/
private static final Map<String, Map<Integer, String>> 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<String, Object> 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<AppPersonnelInfo> qw = new QueryWrapper<AppPersonnelInfo>()
.eq("device_id", deviceId);
List<AppPersonnelInfoVo> appPersonnelInfoVos = appPersonnelInfoMapper.selectVoList(qw);
List<String> 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<String, Object> 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<AppPersonnelInfo> 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<String, Object> 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);
}
}

View File

@ -65,4 +65,9 @@ public class DeviceQueryCriteria extends BaseEntity {
/* app绑定用户id */
private Long bindingUserId;
/* 是否为管理员 */
private Boolean isAdmin = false;
}

View File

@ -35,4 +35,10 @@ public class DeviceTypeQueryCriteria extends BaseEntity implements Serializable
@Schema(name = "每页数据量", example = "10")
private Integer pageSize = 10;
/* 是否为管理员 */
private Boolean isAdmin = false;
}

View File

@ -106,6 +106,13 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
criteria.setDeviceType(deviceTypeGrant.getDeviceTypeId());
}
}
// 管理员
String username = LoginHelper.getUsername();
if (username.equals("admin")) {
criteria.setIsAdmin(true);
}
IPage<Device> devices = deviceMapper.findAll(criteria, page);
List<Device> records = devices.getRecords();

View File

@ -53,8 +53,12 @@ public class DeviceTypeServiceImpl extends ServiceImpl<DeviceTypeMapper, DeviceT
*/
@Override
public TableDataInfo<DeviceType> queryAll(DeviceTypeQueryCriteria criteria, Page<DeviceType> page) {
// 管理员
String username = LoginHelper.getUsername();
if (!username.equals("admin")) {
criteria.setCustomerId(LoginHelper.getUserId());
// return
}
IPage<DeviceType> deviceTypeIPage = deviceTypeMapper.findAll(criteria, page);
return new TableDataInfo<DeviceType>(deviceTypeIPage.getRecords(), deviceTypeIPage.getTotal());
}
@ -74,8 +78,16 @@ public class DeviceTypeServiceImpl extends ServiceImpl<DeviceTypeMapper, DeviceT
@Override
public List<DeviceType> queryDeviceTypes() {
DeviceTypeQueryCriteria criteria = new DeviceTypeQueryCriteria();
// 管理员
String username = LoginHelper.getUsername();
if (!username.equals("admin")) {
criteria.setCustomerId(LoginHelper.getUserId());
Long userId = LoginHelper.getUserId();
criteria.setCustomerId(userId);
}
return deviceTypeMapper.findAll(criteria);
}

View File

@ -74,8 +74,11 @@
<if test="criteria.params.beginTime != null and criteria.params.endTime != null">
and da.create_time between #{criteria.params.beginTime} and #{criteria.params.endTime}
</if>
<!-- 管理员可以查看所有设备,普通用户只能查看自己的设备 -->
<if test="criteria.isAdmin != true">
AND da.assignee_id = #{criteria.currentOwnerId}
AND dg.customer_id = #{criteria.currentOwnerId}
</if>
</where>
) AS ranked
WHERE rn = 1
@ -213,7 +216,9 @@
WHERE original_device_id = #{originalDeviceId}
</select>
<select id="getDeviceInfo" resultType="com.fuyuanshen.equipment.domain.vo.AppDeviceVo">
select d.id, d.device_name, d.device_name,
select d.id,
d.device_name,
d.device_name,
d.device_name,
d.device_mac,
d.device_sn,