mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 01:23:55 +08:00
feature: 新增实现OTA功能——实现设备激活、前端输入激活码验证并保存设备。
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.device.utils.NetworkUtil;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/ota")
|
||||||
|
public class OTAController
|
||||||
|
{
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
|
@Operation(summary = "检查 OTA 版本和设备激活状态")
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<String> checkOTAVersion(
|
||||||
|
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||||
|
|
||||||
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER)
|
||||||
|
@RequestHeader("Device-Id")
|
||||||
|
String deviceId,
|
||||||
|
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER)
|
||||||
|
@RequestHeader("Client-Id")
|
||||||
|
String clientId
|
||||||
|
) {
|
||||||
|
if (StringUtils.isAnyBlank(deviceId, clientId)) {
|
||||||
|
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
||||||
|
}
|
||||||
|
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||||
|
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
||||||
|
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||||
|
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||||
|
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||||
|
}
|
||||||
|
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "设备激活")
|
||||||
|
@GetMapping("/activation")
|
||||||
|
public Result<Boolean> deviceActivation(@RequestParam String code) {
|
||||||
|
return new Result<Boolean>().ok(deviceService.deviceActivation(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private ResponseEntity<String> createResponse(DeviceReportRespDTO deviceReportRespDTO) {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||||
|
String json = objectMapper.writeValueAsString(deviceReportRespDTO);
|
||||||
|
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
return ResponseEntity
|
||||||
|
.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.contentLength(jsonBytes.length)
|
||||||
|
.body(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
@Schema(description = "设备固件信息上报求请求体")
|
||||||
|
public class DeviceReportReqDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
// region 实体属性
|
||||||
|
@Schema(description = "板子固件版本号")
|
||||||
|
private Integer version;
|
||||||
|
|
||||||
|
@Schema(description = "闪存大小(单位:字节)")
|
||||||
|
@JsonProperty("flash_size")
|
||||||
|
private Integer flashSize;
|
||||||
|
|
||||||
|
@Schema(description = "最小空闲堆内存(字节)")
|
||||||
|
@JsonProperty("minimum_free_heap_size")
|
||||||
|
private Integer minimumFreeHeapSize;
|
||||||
|
|
||||||
|
@Schema(description = "设备 MAC 地址")
|
||||||
|
@JsonProperty("mac_address")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
@Schema(description = "设备唯一标识 UUID")
|
||||||
|
private String uuid;
|
||||||
|
|
||||||
|
@Schema(description = "芯片型号名称")
|
||||||
|
@JsonProperty("chip_model_name")
|
||||||
|
private String chipModelName;
|
||||||
|
|
||||||
|
@Schema(description = "芯片详细信息")
|
||||||
|
@JsonProperty("chip_info")
|
||||||
|
private ChipInfo chipInfo;
|
||||||
|
|
||||||
|
@Schema(description = "应用程序信息")
|
||||||
|
private Application application;
|
||||||
|
|
||||||
|
@Schema(description = "分区表列表")
|
||||||
|
@JsonProperty("partition_table")
|
||||||
|
private List<Partition> partitionTable;
|
||||||
|
|
||||||
|
@Schema(description = "当前运行的 OTA 分区信息")
|
||||||
|
private OtaInfo ota;
|
||||||
|
|
||||||
|
@Schema(description = "板子配置信息")
|
||||||
|
private BoardInfo board;
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "芯片信息")
|
||||||
|
public static class ChipInfo {
|
||||||
|
@Schema(description = "芯片模型代码")
|
||||||
|
private Integer model;
|
||||||
|
|
||||||
|
@Schema(description = "核心数")
|
||||||
|
private Integer cores;
|
||||||
|
|
||||||
|
@Schema(description = "硬件修订版本")
|
||||||
|
private Integer revision;
|
||||||
|
|
||||||
|
@Schema(description = "芯片功能标志位")
|
||||||
|
private Integer features;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "板子编译信息")
|
||||||
|
public static class Application {
|
||||||
|
@Schema(description = "名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "应用版本号")
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
@Schema(description = "编译时间(UTC ISO格式)")
|
||||||
|
@JsonProperty("compile_time")
|
||||||
|
private String compileTime;
|
||||||
|
|
||||||
|
@Schema(description = "ESP-IDF 版本号")
|
||||||
|
@JsonProperty("idf_version")
|
||||||
|
private String idfVersion;
|
||||||
|
|
||||||
|
@Schema(description = "ELF 文件 SHA256 校验")
|
||||||
|
@JsonProperty("elf_sha256")
|
||||||
|
private String elfSha256;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "分区信息")
|
||||||
|
public static class Partition {
|
||||||
|
@Schema(description = "分区标签名")
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
@Schema(description = "分区类型")
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
@Schema(description = "子类型")
|
||||||
|
private Integer subtype;
|
||||||
|
|
||||||
|
@Schema(description = "起始地址")
|
||||||
|
private Integer address;
|
||||||
|
|
||||||
|
@Schema(description = "分区大小")
|
||||||
|
private Integer size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "OTA信息")
|
||||||
|
public static class OtaInfo {
|
||||||
|
@Schema(description = "当前OTA标签")
|
||||||
|
private String label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "板子连接和网络信息")
|
||||||
|
public static class BoardInfo {
|
||||||
|
@Schema(description = "板子类型")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "连接的 Wi-Fi SSID")
|
||||||
|
private String ssid;
|
||||||
|
|
||||||
|
@Schema(description = "Wi-Fi 信号强度(RSSI)")
|
||||||
|
private Integer rssi;
|
||||||
|
|
||||||
|
@Schema(description = "Wi-Fi 信道")
|
||||||
|
private Integer channel;
|
||||||
|
|
||||||
|
@Schema(description = "IP 地址")
|
||||||
|
private String ip;
|
||||||
|
|
||||||
|
@Schema(description = "MAC 地址")
|
||||||
|
private String mac;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "设备OTA检测版本返回体,包含激活码要求")
|
||||||
|
public class DeviceReportRespDTO
|
||||||
|
{
|
||||||
|
@Schema(description = "服务器时间")
|
||||||
|
private ServerTime serverTime;
|
||||||
|
|
||||||
|
@Schema(description = "激活码")
|
||||||
|
private Activation activation;
|
||||||
|
|
||||||
|
@Schema(description = "错误信息")
|
||||||
|
private String error;
|
||||||
|
|
||||||
|
@Schema(description = "固件版本信息")
|
||||||
|
private Firmware firmware;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class Firmware {
|
||||||
|
@Schema(description = "版本号")
|
||||||
|
private String version;
|
||||||
|
@Schema(description = "下载地址")
|
||||||
|
private String url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DeviceReportRespDTO createError(String message) {
|
||||||
|
DeviceReportRespDTO resp = new DeviceReportRespDTO();
|
||||||
|
resp.setError(message);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
public static class Activation
|
||||||
|
{
|
||||||
|
@Schema(description = "激活码")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Schema(description = "激活码信息: 激活地址")
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class ServerTime
|
||||||
|
{
|
||||||
|
@Schema(description = "时间戳")
|
||||||
|
private Long timestamp;
|
||||||
|
|
||||||
|
@Schema(description = "时区")
|
||||||
|
private String timeZone;
|
||||||
|
|
||||||
|
@Schema(description = "时区偏移量,单位为分钟")
|
||||||
|
private Integer timezoneOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,22 @@ package xiaozhi.modules.device.service;
|
|||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public interface DeviceService {
|
public interface DeviceService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据Mac地址获取设备信息
|
||||||
|
*/
|
||||||
|
DeviceEntity getDeviceById(String macAddress);
|
||||||
|
|
||||||
|
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId, DeviceReportReqDTO deviceReport);
|
||||||
|
|
||||||
DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader);
|
DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader);
|
||||||
|
|
||||||
List<DeviceEntity> getUserDevices(Long userId);
|
List<DeviceEntity> getUserDevices(Long userId);
|
||||||
@@ -15,4 +25,6 @@ public interface DeviceService {
|
|||||||
void unbindDevice(Long userId, Long deviceId);
|
void unbindDevice(Long userId, Long deviceId);
|
||||||
|
|
||||||
PageData<DeviceEntity> adminDeviceList(Map<String, Object> params);
|
PageData<DeviceEntity> adminDeviceList(Map<String, Object> params);
|
||||||
|
|
||||||
|
Boolean deviceActivation(String activationCode);
|
||||||
}
|
}
|
||||||
+153
-4
@@ -1,28 +1,168 @@
|
|||||||
package xiaozhi.modules.device.service.impl;
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.modules.device.dao.DeviceDao;
|
import xiaozhi.modules.device.dao.DeviceDao;
|
||||||
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Map;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||||
|
|
||||||
private final DeviceDao deviceDao;
|
private final DeviceDao deviceDao;
|
||||||
|
|
||||||
|
private final String frontedUrl;
|
||||||
|
|
||||||
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
// 添加构造函数来初始化 deviceMapper
|
// 添加构造函数来初始化 deviceMapper
|
||||||
public DeviceServiceImpl(DeviceDao deviceDao) {
|
public DeviceServiceImpl(DeviceDao deviceDao,
|
||||||
|
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
||||||
|
RedisTemplate<String, Object> redisTemplate) {
|
||||||
this.deviceDao = deviceDao;
|
this.deviceDao = deviceDao;
|
||||||
|
this.frontedUrl = frontedUrl;
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceEntity getDeviceById(String deviceId) {
|
||||||
|
LambdaQueryWrapper<DeviceEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(DeviceEntity::getId, deviceId);
|
||||||
|
return deviceDao.selectOne(queryWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean deviceActivation(String activationCode) {
|
||||||
|
if (StringUtils.isBlank(activationCode)) {
|
||||||
|
throw new RenException("激活码不能为空");
|
||||||
|
}
|
||||||
|
String deviceKey = "ota:activation:code:" + activationCode;
|
||||||
|
Object cacheDeviceId = redisTemplate.opsForValue().get(deviceKey);
|
||||||
|
if (cacheDeviceId == null) {
|
||||||
|
throw new RenException("激活码错误");
|
||||||
|
}
|
||||||
|
String deviceId = (String) cacheDeviceId;
|
||||||
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
|
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
|
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(cacheDeviceKey);
|
||||||
|
if (cacheMap == null) {
|
||||||
|
throw new RenException("激活码错误");
|
||||||
|
}
|
||||||
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
|
if (!activationCode.equals(cachedCode)) {
|
||||||
|
throw new RenException("激活码错误");
|
||||||
|
}
|
||||||
|
// 检查设备有没有被激活
|
||||||
|
if (selectById(deviceId) != null) {
|
||||||
|
throw new RenException("设备已激活");
|
||||||
|
}
|
||||||
|
|
||||||
|
String macAddress = (String) cacheMap.get("mac_address");
|
||||||
|
String board = (String) cacheMap.get("board");
|
||||||
|
String appVersion = (String) cacheMap.get("app_version");
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
if (user.getId() == null) {
|
||||||
|
throw new RenException("用户未登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date currentTime = new Date();
|
||||||
|
DeviceEntity deviceEntity = new DeviceEntity();
|
||||||
|
deviceEntity.setId(deviceId);
|
||||||
|
deviceEntity.setBoard(board);
|
||||||
|
deviceEntity.setAppVersion(appVersion);
|
||||||
|
deviceEntity.setMacAddress(macAddress);
|
||||||
|
deviceEntity.setUserId(user.getId());
|
||||||
|
deviceEntity.setCreator(user.getId());
|
||||||
|
deviceEntity.setCreateDate(currentTime);
|
||||||
|
deviceEntity.setUpdater(user.getId());
|
||||||
|
deviceEntity.setUpdateDate(currentTime);
|
||||||
|
deviceEntity.setLastConnectedAt(currentTime);
|
||||||
|
deviceDao.insert(deviceEntity);
|
||||||
|
|
||||||
|
// 清理redis缓存
|
||||||
|
redisTemplate.delete(cacheDeviceKey);
|
||||||
|
redisTemplate.delete(deviceKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId, DeviceReportReqDTO deviceReport) {
|
||||||
|
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||||
|
response.setServerTime(buildServerTime());
|
||||||
|
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
|
||||||
|
// 在未来开发了固件更新功能,需要更换此处代码,
|
||||||
|
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
|
||||||
|
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||||
|
firmware.setVersion(deviceReport.getApplication().getVersion());
|
||||||
|
firmware.setUrl("http://localhost:8002/xiaozhi-esp32-api/api/v1/ota/download");
|
||||||
|
response.setFirmware(firmware);
|
||||||
|
|
||||||
|
DeviceEntity deviceById = getDeviceById(deviceId);
|
||||||
|
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
||||||
|
deviceById.setLastConnectedAt(new Date());
|
||||||
|
deviceDao.updateById(deviceById);
|
||||||
|
} else { // 如果设备不存在,则生成激活码
|
||||||
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
|
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
|
|
||||||
|
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
|
||||||
|
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
||||||
|
|
||||||
|
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||||
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
|
code.setCode(cachedCode);
|
||||||
|
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||||
|
} else {
|
||||||
|
String newCode = RandomUtil.randomNumbers(6);
|
||||||
|
code.setCode(newCode);
|
||||||
|
code.setMessage(frontedUrl + "\n" + newCode);
|
||||||
|
|
||||||
|
Map<String, Object> dataMap = new HashMap<>();
|
||||||
|
dataMap.put("id", deviceId);
|
||||||
|
dataMap.put("mac_address", macAddress);
|
||||||
|
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
|
||||||
|
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
|
||||||
|
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
||||||
|
? deviceReport.getApplication().getVersion()
|
||||||
|
: null);
|
||||||
|
dataMap.put("deviceId", deviceId);
|
||||||
|
dataMap.put("activation_code", newCode);
|
||||||
|
|
||||||
|
// 写入主数据 key
|
||||||
|
redisTemplate.opsForHash().putAll(dataKey, dataMap);
|
||||||
|
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
|
||||||
|
|
||||||
|
// 写入反查激活码 key
|
||||||
|
String codeKey = "ota:activation:code:" + newCode;
|
||||||
|
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setActivation(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader) {
|
public DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader) {
|
||||||
DeviceEntity device = new DeviceEntity();
|
DeviceEntity device = new DeviceEntity();
|
||||||
@@ -54,4 +194,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
return new PageData<>(page.getRecords(), page.getTotal());
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DeviceReportRespDTO.ServerTime buildServerTime()
|
||||||
|
{
|
||||||
|
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
|
||||||
|
TimeZone tz = TimeZone.getDefault();
|
||||||
|
serverTime.setTimestamp(Instant.now().toEpochMilli());
|
||||||
|
serverTime.setTimeZone(tz.getID());
|
||||||
|
serverTime.setTimezoneOffset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||||
|
return serverTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package xiaozhi.modules.device.utils;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网络工具类
|
||||||
|
*/
|
||||||
|
public class NetworkUtil
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* MAC地址正则表达式
|
||||||
|
*/
|
||||||
|
private static final Pattern macPattern = Pattern.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断MAC地址是否合法
|
||||||
|
*/
|
||||||
|
public static boolean isMacAddressValid(String mac) {
|
||||||
|
if (StringUtils.isBlank(mac)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 正则校验格式
|
||||||
|
if (!macPattern.matcher(mac).matches()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 校验MAC地址是否为单播地址
|
||||||
|
String normalized = mac.toLowerCase();
|
||||||
|
String[] parts = normalized.split("[:-]");
|
||||||
|
int firstByte = Integer.parseInt(parts[0], 16);
|
||||||
|
return (firstByte & 1) == 0; // 最低位为0表示单播地址,合法
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@ public class ShiroConfig {
|
|||||||
perms:拥有对某个资源的权限才能访问
|
perms:拥有对某个资源的权限才能访问
|
||||||
role:拥有某个角色权限才能访问*/
|
role:拥有某个角色权限才能访问*/
|
||||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||||
|
filterMap.put("/ota/**", "anon");
|
||||||
filterMap.put("/webjars/**", "anon");
|
filterMap.put("/webjars/**", "anon");
|
||||||
filterMap.put("/druid/**", "anon");
|
filterMap.put("/druid/**", "anon");
|
||||||
filterMap.put("/v3/api-docs/**", "anon");
|
filterMap.put("/v3/api-docs/**", "anon");
|
||||||
|
|||||||
@@ -75,4 +75,7 @@ mybatis-plus:
|
|||||||
configuration-properties:
|
configuration-properties:
|
||||||
prefix:
|
prefix:
|
||||||
blobType: BLOB
|
blobType: BLOB
|
||||||
boolValue: TRUE
|
boolValue: TRUE
|
||||||
|
|
||||||
|
app:
|
||||||
|
fronted-url: http://localhost:8001
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :visible.sync="visible" width="400px" center>
|
||||||
|
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
|
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||||
|
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||||
|
</div>
|
||||||
|
激活设备
|
||||||
|
</div>
|
||||||
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
|
<div style="margin: 22px 15px;">
|
||||||
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||||
|
<div style="color: red;display: inline-block;">*</div> 设备激活码:
|
||||||
|
</div>
|
||||||
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
|
<el-input placeholder="请输入设备播报的激活码.." v-model="activateCode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||||
|
<div class="dialog-btn" @click="confirm">
|
||||||
|
确定
|
||||||
|
</div>
|
||||||
|
<div class="dialog-btn"
|
||||||
|
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||||
|
@click="cancel">
|
||||||
|
取消
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import userApi from '@/apis/module/user';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ActivateDeviceDialog',
|
||||||
|
props: {
|
||||||
|
visible: { type: Boolean, required: true }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { activateCode: "" }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
confirm() {
|
||||||
|
if (!this.activateCode.trim()) {
|
||||||
|
this.$message.error('请输入激活码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
userApi.activateDevice(this.activateCode, (res) => {
|
||||||
|
this.$message.success('激活成功,请重新唤醒设备即可完成激活!');
|
||||||
|
this.$emit('confirm', res);
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
console.log("res: ", res)
|
||||||
|
this.activateCode = "";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.$emit('update:visible', false)
|
||||||
|
this.activateCode = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.input-46 {
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
background: #f6f8fb;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 23px;
|
||||||
|
background: #5778ff;
|
||||||
|
height: 40px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
::v-deep .el-dialog {
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
::v-deep .el-dialog__headerbtn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
::v-deep .el-dialog__body {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
::v-deep .el-dialog__header{
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user