mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 23: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.modules.device.dto.DeviceHeaderDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface DeviceService {
|
||||
|
||||
/**
|
||||
* 根据Mac地址获取设备信息
|
||||
*/
|
||||
DeviceEntity getDeviceById(String macAddress);
|
||||
|
||||
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId, DeviceReportReqDTO deviceReport);
|
||||
|
||||
DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader);
|
||||
|
||||
List<DeviceEntity> getUserDevices(Long userId);
|
||||
@@ -15,4 +25,6 @@ public interface DeviceService {
|
||||
void unbindDevice(Long userId, Long deviceId);
|
||||
|
||||
PageData<DeviceEntity> adminDeviceList(Map<String, Object> params);
|
||||
|
||||
Boolean deviceActivation(String activationCode);
|
||||
}
|
||||
+153
-4
@@ -1,28 +1,168 @@
|
||||
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.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 xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.modules.device.dao.DeviceDao;
|
||||
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.service.DeviceService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||
|
||||
private final DeviceDao deviceDao;
|
||||
|
||||
private final String frontedUrl;
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
// 添加构造函数来初始化 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.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
|
||||
public DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader) {
|
||||
DeviceEntity device = new DeviceEntity();
|
||||
@@ -54,4 +194,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
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:拥有对某个资源的权限才能访问
|
||||
role:拥有某个角色权限才能访问*/
|
||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||
filterMap.put("/ota/**", "anon");
|
||||
filterMap.put("/webjars/**", "anon");
|
||||
filterMap.put("/druid/**", "anon");
|
||||
filterMap.put("/v3/api-docs/**", "anon");
|
||||
|
||||
@@ -75,4 +75,7 @@ mybatis-plus:
|
||||
configuration-properties:
|
||||
prefix:
|
||||
blobType: BLOB
|
||||
boolValue: TRUE
|
||||
boolValue: TRUE
|
||||
|
||||
app:
|
||||
fronted-url: http://localhost:8001
|
||||
Reference in New Issue
Block a user