{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"我会快速查看当前 git diff 的文件清单和统计,确认这次提交的主线,避免只根据截断 diff 猜测。"}]}}{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"item_1","name":"bash","input":{"command":"\"C:\\\\Users\\\\admin\\\\AppData\\\\Local\\\\Microsoft\\\\WindowsApps\\\\pwsh.exe\" -Command 'git diff --stat'","description":"Run \"C:\\\\Users\\\\admin\\\\AppData\\\\Local\\\\Microsoft\\\\WindowsApps\\\\pwsh.exe\""}}]}}
This commit is contained in:
14
pom.xml
14
pom.xml
@@ -39,6 +39,8 @@
|
|||||||
<justauth.version>1.16.7</justauth.version>
|
<justauth.version>1.16.7</justauth.version>
|
||||||
<!-- 离线IP地址定位库 -->
|
<!-- 离线IP地址定位库 -->
|
||||||
<ip2region.version>3.3.4</ip2region.version>
|
<ip2region.version>3.3.4</ip2region.version>
|
||||||
|
<!-- ZXing 二维码 -->
|
||||||
|
<zxing.version>3.5.3</zxing.version>
|
||||||
<!-- OSS 配置 -->
|
<!-- OSS 配置 -->
|
||||||
<aws.sdk.version>2.28.22</aws.sdk.version>
|
<aws.sdk.version>2.28.22</aws.sdk.version>
|
||||||
<!-- SMS 配置 -->
|
<!-- SMS 配置 -->
|
||||||
@@ -151,6 +153,18 @@
|
|||||||
<version>${fastexcel.version}</version>
|
<version>${fastexcel.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ZXing 二维码库(hutool-extra QrCodeUtil 底层依赖,hutool 声明为 optional) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>core</artifactId>
|
||||||
|
<version>${zxing.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>javase</artifactId>
|
||||||
|
<version>${zxing.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- velocity代码生成使用模板 -->
|
<!-- velocity代码生成使用模板 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.velocity</groupId>
|
<groupId>org.apache.velocity</groupId>
|
||||||
|
|||||||
@@ -96,6 +96,15 @@
|
|||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
<artifactId>water-app</artifactId>
|
<artifactId>water-app</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- 二维码生成运行依赖,确保启动包包含 ZXing 类 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>javase</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 工作流模块 -->
|
<!-- 工作流模块 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
|
|||||||
private MqttClientManager mqttClientManager;
|
private MqttClientManager mqttClientManager;
|
||||||
private final MqttProperties mqttProperties;
|
private final MqttProperties mqttProperties;
|
||||||
private final AppDeviceMapper appDeviceMapper;
|
private final AppDeviceMapper appDeviceMapper;
|
||||||
|
private static final String DEVICE_STATUS_LOCK_PREFIX = "lock:mqtt:device:status:";
|
||||||
|
|
||||||
public void savePending(DeviceCommand command) {
|
public void savePending(DeviceCommand command) {
|
||||||
RedisUtils.setCacheObject(pendingKey(command.getCommandId()), command, Duration.ofSeconds(mqttProperties.getCommandAck().getPendingTtlSeconds()));
|
RedisUtils.setCacheObject(pendingKey(command.getCommandId()), command, Duration.ofSeconds(mqttProperties.getCommandAck().getPendingTtlSeconds()));
|
||||||
@@ -46,7 +47,7 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
|
|||||||
if (StringUtils.isBlank(commandId)) {
|
if (StringUtils.isBlank(commandId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deletePending(commandId);
|
withCommandLock(commandId, () -> deletePending(commandId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -74,8 +75,10 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ack.setDeviceNo(deviceNo);
|
ack.setDeviceNo(deviceNo);
|
||||||
|
withCommandLock(ack.getCommandId(), () -> {
|
||||||
deletePending(ack.getCommandId());
|
deletePending(ack.getCommandId());
|
||||||
RedisUtils.setCacheObject(ackKey(ack.getCommandId()), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds()));
|
RedisUtils.setCacheObject(ackKey(ack.getCommandId()), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds()));
|
||||||
|
});
|
||||||
refreshDeviceOnline(deviceNo);
|
refreshDeviceOnline(deviceNo);
|
||||||
log.info("[MQTT] 收到命令确认 设备编号={} 命令编号={} 状态={}", deviceNo, ack.getCommandId(), ack.getStatus());
|
log.info("[MQTT] 收到命令确认 设备编号={} 命令编号={} 状态={}", deviceNo, ack.getCommandId(), ack.getStatus());
|
||||||
}
|
}
|
||||||
@@ -96,8 +99,10 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
|
|||||||
ack.setStatus("1");
|
ack.setStatus("1");
|
||||||
ack.setMessage(payload);
|
ack.setMessage(payload);
|
||||||
|
|
||||||
|
withCommandLock(commandId, () -> {
|
||||||
deletePending(commandId);
|
deletePending(commandId);
|
||||||
RedisUtils.setCacheObject(ackKey(commandId), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds()));
|
RedisUtils.setCacheObject(ackKey(commandId), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds()));
|
||||||
|
});
|
||||||
log.info("[MQTT] 收到非 JSON 命令确认 设备编号={} 命令编号={} 消息体={}", deviceNo, commandId, payload);
|
log.info("[MQTT] 收到非 JSON 命令确认 设备编号={} 命令编号={} 消息体={}", deviceNo, commandId, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,24 +126,7 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void retryCommandIfLocked(String commandId, long now) {
|
private void retryCommandIfLocked(String commandId, long now) {
|
||||||
RLock lock = RedisUtils.getClient().getLock(mqttProperties.getCommandAck().getRetryLockKeyPrefix() + commandId);
|
withCommandLock(commandId, () -> retryCommand(commandId, now));
|
||||||
boolean locked = false;
|
|
||||||
try {
|
|
||||||
locked = lock.tryLock(0, mqttProperties.getCommandAck().getRetryLockTtlMs(), TimeUnit.MILLISECONDS);
|
|
||||||
if (!locked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
retryCommand(commandId, now);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
log.warn("[MQTT] 命令重试锁等待被中断 命令编号={}", commandId);
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
log.error("[MQTT] 命令重试失败 命令编号={}", commandId, e);
|
|
||||||
} finally {
|
|
||||||
if (locked && lock.isHeldByCurrentThread()) {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void retryCommand(String commandId, long now) {
|
private void retryCommand(String commandId, long now) {
|
||||||
@@ -186,6 +174,11 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refreshDeviceOnline(String deviceNo) {
|
private void refreshDeviceOnline(String deviceNo) {
|
||||||
|
RLock lock = RedisUtils.getClient().getLock(DEVICE_STATUS_LOCK_PREFIX + deviceNo);
|
||||||
|
boolean locked = false;
|
||||||
|
try {
|
||||||
|
lock.lock(10, TimeUnit.SECONDS);
|
||||||
|
locked = true;
|
||||||
Map<String, Object> statusCache = new HashMap<>();
|
Map<String, Object> statusCache = new HashMap<>();
|
||||||
statusCache.put("deviceNo", deviceNo);
|
statusCache.put("deviceNo", deviceNo);
|
||||||
statusCache.put("status", "1");
|
statusCache.put("status", "1");
|
||||||
@@ -200,6 +193,11 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
|
|||||||
.set(AppDevice::getStatus, "1")
|
.set(AppDevice::getStatus, "1")
|
||||||
.eq(AppDevice::getDeviceNo, deviceNo)
|
.eq(AppDevice::getDeviceNo, deviceNo)
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
if (locked && lock.isHeldByCurrentThread()) {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RSet<String> pendingIds() {
|
private RSet<String> pendingIds() {
|
||||||
@@ -211,6 +209,27 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
|
|||||||
pendingIds().remove(commandId);
|
pendingIds().remove(commandId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void withCommandLock(String commandId, Runnable action) {
|
||||||
|
RLock lock = RedisUtils.getClient().getLock(mqttProperties.getCommandAck().getRetryLockKeyPrefix() + commandId);
|
||||||
|
boolean locked = false;
|
||||||
|
try {
|
||||||
|
locked = lock.tryLock(0, mqttProperties.getCommandAck().getRetryLockTtlMs(), TimeUnit.MILLISECONDS);
|
||||||
|
if (!locked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
action.run();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.warn("[MQTT] 命令锁等待被中断 命令编号={}", commandId);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.error("[MQTT] 命令锁内处理失败 命令编号={}", commandId, e);
|
||||||
|
} finally {
|
||||||
|
if (locked && lock.isHeldByCurrentThread()) {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String pendingKey(String commandId) {
|
private String pendingKey(String commandId) {
|
||||||
return mqttProperties.getCommandAck().getPendingKeyPrefix() + commandId;
|
return mqttProperties.getCommandAck().getPendingKeyPrefix() + commandId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public class OssClient {
|
|||||||
.contentType(contentType)
|
.contentType(contentType)
|
||||||
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
|
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
|
||||||
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
|
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
|
||||||
//.acl(getAccessPolicy().getObjectCannedACL())
|
.acl(getAccessPolicy().getObjectCannedACL())
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
@@ -203,7 +203,7 @@ public class OssClient {
|
|||||||
.contentType(contentType)
|
.contentType(contentType)
|
||||||
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
|
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
|
||||||
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
|
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
|
||||||
//.acl(getAccessPolicy().getObjectCannedACL())
|
.acl(getAccessPolicy().getObjectCannedACL())
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public enum AccessPolicyType {
|
|||||||
/**
|
/**
|
||||||
* public
|
* public
|
||||||
*/
|
*/
|
||||||
PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE),
|
PUBLIC("1", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* custom
|
* custom
|
||||||
|
|||||||
@@ -102,6 +102,23 @@
|
|||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
<artifactId>water-common-websocket</artifactId>
|
<artifactId>water-common-websocket</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>water-common-oss</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-extra</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- hutool-extra QrCodeUtil 底层需要 zxing(hutool 声明为 optional,不会传递) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>javase</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
<artifactId>water-system</artifactId>
|
<artifactId>water-system</artifactId>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public class AppController extends BaseController {
|
|||||||
for (String deviceNo : deviceNos) {
|
for (String deviceNo : deviceNos) {
|
||||||
assertDeviceOwned(deviceNo);
|
assertDeviceOwned(deviceNo);
|
||||||
}
|
}
|
||||||
return toAjax(appDeviceService.deleteWithValidByIds(List.of(deviceNos), true));
|
return toAjax(appDeviceService.unbindDevices(List.of(deviceNos), LoginHelper.getUserId()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return fail(e);
|
return fail(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
package org.dromara.app.controller;
|
package org.dromara.app.controller;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import jakarta.validation.constraints.*;
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import org.dromara.common.log.annotation.Log;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.dromara.common.web.core.BaseController;
|
import org.dromara.app.domain.bo.AppDeviceBo;
|
||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
import org.dromara.app.domain.vo.AppDeviceVo;
|
||||||
|
import org.dromara.app.domain.vo.DashboardStatsVo;
|
||||||
|
import org.dromara.app.service.IAppDeviceService;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.core.validate.AddGroup;
|
import org.dromara.common.core.validate.AddGroup;
|
||||||
import org.dromara.common.core.validate.EditGroup;
|
import org.dromara.common.core.validate.EditGroup;
|
||||||
import org.dromara.common.log.enums.BusinessType;
|
|
||||||
import org.dromara.common.excel.utils.ExcelUtil;
|
import org.dromara.common.excel.utils.ExcelUtil;
|
||||||
import org.dromara.app.domain.vo.AppDeviceVo;
|
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
||||||
import org.dromara.app.domain.bo.AppDeviceBo;
|
import org.dromara.common.log.annotation.Log;
|
||||||
import org.dromara.app.service.IAppDeviceService;
|
import org.dromara.common.log.enums.BusinessType;
|
||||||
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
|
import org.dromara.common.web.core.BaseController;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设备信息
|
* 设备信息
|
||||||
@@ -47,6 +49,15 @@ public class AppDeviceController extends BaseController {
|
|||||||
return appDeviceService.queryPageList(bo, pageQuery);
|
return appDeviceService.queryPageList(bo, pageQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪表盘统计 — 首页 8 个统计卡片聚合数据。
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("app:device:query")
|
||||||
|
@GetMapping("/dashboard")
|
||||||
|
public R<DashboardStatsVo> dashboard() {
|
||||||
|
return R.ok(appDeviceService.getDashboardStats());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出设备信息
|
* 导出设备信息
|
||||||
列表
|
列表
|
||||||
@@ -63,7 +74,7 @@ public class AppDeviceController extends BaseController {
|
|||||||
* 获取设备信息
|
* 获取设备信息
|
||||||
详细信息
|
详细信息
|
||||||
*
|
*
|
||||||
* @param id 主键
|
* @param
|
||||||
*/
|
*/
|
||||||
@SaCheckPermission("app:device:query")
|
@SaCheckPermission("app:device:query")
|
||||||
@GetMapping("/{deviceNo}")
|
@GetMapping("/{deviceNo}")
|
||||||
@@ -81,6 +92,8 @@ public class AppDeviceController extends BaseController {
|
|||||||
@RepeatSubmit()
|
@RepeatSubmit()
|
||||||
@PostMapping()
|
@PostMapping()
|
||||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody AppDeviceBo bo) {
|
public R<Void> add(@Validated(AddGroup.class) @RequestBody AppDeviceBo bo) {
|
||||||
|
bo.setStatus("0");
|
||||||
|
bo.setWorkStatus("2");
|
||||||
return toAjax(appDeviceService.insertByBo(bo));
|
return toAjax(appDeviceService.insertByBo(bo));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +109,24 @@ public class AppDeviceController extends BaseController {
|
|||||||
return toAjax(appDeviceService.updateByBo(bo));
|
return toAjax(appDeviceService.updateByBo(bo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成设备二维码
|
||||||
|
*
|
||||||
|
* @param ids 设备编号集合
|
||||||
|
*/
|
||||||
|
@SaCheckPermission("app:device:edit")
|
||||||
|
@Log(title = "设备二维码", businessType = BusinessType.UPDATE)
|
||||||
|
@PostMapping("/generateQrCode/{ids}")
|
||||||
|
public R<Void> generateQrCode(@NotEmpty(message = "设备编号不能为空")
|
||||||
|
@PathVariable String[] ids) {
|
||||||
|
return toAjax(appDeviceService.generateQrCode(List.of(ids)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除设备信息
|
* 删除设备信息
|
||||||
|
|
||||||
*
|
*
|
||||||
* @param ids 主键串
|
* @param
|
||||||
*/
|
*/
|
||||||
@SaCheckPermission("app:device:remove")
|
@SaCheckPermission("app:device:remove")
|
||||||
@Log(title = "设备信息", businessType = BusinessType.DELETE)
|
@Log(title = "设备信息", businessType = BusinessType.DELETE)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.dromara.app.domain;
|
package org.dromara.app.domain;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -18,7 +17,7 @@ public class AppDevice extends BaseEntity {
|
|||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@TableId(value = "device_no", type = IdType.INPUT)
|
@TableId(value = "device_no")
|
||||||
private String deviceNo;
|
private String deviceNo;
|
||||||
|
|
||||||
private String bindTokenHash;
|
private String bindTokenHash;
|
||||||
@@ -29,6 +28,8 @@ public class AppDevice extends BaseEntity {
|
|||||||
|
|
||||||
private String deviceImg;
|
private String deviceImg;
|
||||||
|
|
||||||
|
private String qrcode;
|
||||||
|
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
private String workStatus;
|
private String workStatus;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package org.dromara.app.domain;
|
package org.dromara.app.domain;
|
||||||
|
|
||||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import java.util.Date;
|
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 浇水记录对象 app_watering_log
|
* 浇水记录对象 app_watering_log
|
||||||
@@ -74,6 +74,11 @@ public class AppWateringLog extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private String triggerType;
|
private String triggerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
private String remark;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.dromara.app.domain.bo;
|
package org.dromara.app.domain.bo;
|
||||||
|
|
||||||
import io.github.linpeilie.annotations.AutoMapper;
|
import io.github.linpeilie.annotations.AutoMapper;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.dromara.app.domain.AppDevice;
|
import org.dromara.app.domain.AppDevice;
|
||||||
@@ -8,8 +9,6 @@ import org.dromara.common.core.validate.AddGroup;
|
|||||||
import org.dromara.common.core.validate.EditGroup;
|
import org.dromara.common.core.validate.EditGroup;
|
||||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -17,14 +16,13 @@ import java.util.Date;
|
|||||||
@AutoMapper(target = AppDevice.class, reverseConvertGenerate = false)
|
@AutoMapper(target = AppDevice.class, reverseConvertGenerate = false)
|
||||||
public class AppDeviceBo extends BaseEntity {
|
public class AppDeviceBo extends BaseEntity {
|
||||||
|
|
||||||
@NotBlank(message = "设备编号不能为空", groups = { AddGroup.class, EditGroup.class })
|
@NotBlank(message = "设备编号不能为空", groups = { EditGroup.class })
|
||||||
private String deviceNo;
|
private String deviceNo;
|
||||||
|
|
||||||
private String bindToken;
|
private String bindToken;
|
||||||
|
|
||||||
private String bindTokenHash;
|
private String bindTokenHash;
|
||||||
|
|
||||||
@NotNull(message = "用户id不能为空", groups = { AddGroup.class, EditGroup.class })
|
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,6 +35,11 @@ public class AppDeviceBo extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private String deviceImg;
|
private String deviceImg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二维码
|
||||||
|
*/
|
||||||
|
private String qrcode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态 1-在线 0-离线 2-到期 3-故障
|
* 状态 1-在线 0-离线 2-到期 3-故障
|
||||||
*/
|
*/
|
||||||
@@ -85,6 +88,7 @@ public class AppDeviceBo extends BaseEntity {
|
|||||||
/**
|
/**
|
||||||
* mac地址
|
* mac地址
|
||||||
*/
|
*/
|
||||||
|
@NotBlank(message = "MAC地址不能为空", groups = { AddGroup.class })
|
||||||
private String macAddress;
|
private String macAddress;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package org.dromara.app.domain.bo;
|
package org.dromara.app.domain.bo;
|
||||||
|
|
||||||
import org.dromara.app.domain.AppWateringLog;
|
|
||||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
|
||||||
import org.dromara.common.core.validate.AddGroup;
|
|
||||||
import org.dromara.common.core.validate.EditGroup;
|
|
||||||
import io.github.linpeilie.annotations.AutoMapper;
|
import io.github.linpeilie.annotations.AutoMapper;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import jakarta.validation.constraints.*;
|
import org.dromara.app.domain.AppWateringLog;
|
||||||
|
import org.dromara.common.core.validate.EditGroup;
|
||||||
|
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 浇水记录业务对象 app_watering_log
|
* 浇水记录业务对象 app_watering_log
|
||||||
@@ -73,6 +72,11 @@ public class AppWateringLogBo extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private String triggerType;
|
private String triggerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
private String remark;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ public class AppDeviceVo implements Serializable {
|
|||||||
@ExcelProperty(value = "设备图片")
|
@ExcelProperty(value = "设备图片")
|
||||||
private String deviceImg;
|
private String deviceImg;
|
||||||
|
|
||||||
|
@ExcelProperty(value = "二维码")
|
||||||
|
private String qrcode;
|
||||||
|
|
||||||
@ExcelProperty(value = "状态")
|
@ExcelProperty(value = "状态")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@@ -59,6 +62,9 @@ public class AppDeviceVo implements Serializable {
|
|||||||
@ExcelProperty(value = "mac地址")
|
@ExcelProperty(value = "mac地址")
|
||||||
private String macAddress;
|
private String macAddress;
|
||||||
|
|
||||||
|
@ExcelProperty(value = "用户名称")
|
||||||
|
private String nickName;
|
||||||
|
|
||||||
@ExcelProperty(value = "到期时间")
|
@ExcelProperty(value = "到期时间")
|
||||||
private Date expirationTime;
|
private Date expirationTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package org.dromara.app.domain.vo;
|
package org.dromara.app.domain.vo;
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
import org.dromara.app.domain.AppWateringLog;
|
|
||||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
import org.dromara.common.excel.annotation.ExcelDictFormat;
|
|
||||||
import org.dromara.common.excel.convert.ExcelDictConvert;
|
|
||||||
import io.github.linpeilie.annotations.AutoMapper;
|
import io.github.linpeilie.annotations.AutoMapper;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.dromara.app.domain.AppWateringLog;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -91,6 +87,12 @@ public class AppWateringLogVo implements Serializable {
|
|||||||
private String triggerType;
|
private String triggerType;
|
||||||
private String deviceName;
|
private String deviceName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
@ExcelProperty(value = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.dromara.app.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪表盘统计指标 VO。
|
||||||
|
* <p>
|
||||||
|
* 对应前端首页 8 个统计卡片的数据。
|
||||||
|
*
|
||||||
|
* @author water team
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DashboardStatsVo implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 设备总数 */
|
||||||
|
private Long totalDevices;
|
||||||
|
|
||||||
|
/** 在线设备数(status='1') */
|
||||||
|
private Long onlineDevices;
|
||||||
|
|
||||||
|
/** 待机设备数(status='2') */
|
||||||
|
private Long standbyDevices;
|
||||||
|
|
||||||
|
/** 离线设备数(status='0') */
|
||||||
|
private Long offlineDevices;
|
||||||
|
|
||||||
|
/** 故障设备数(status='3') */
|
||||||
|
private Long faultDevices;
|
||||||
|
|
||||||
|
/** 工作中设备数(workStatus='1') */
|
||||||
|
private Long workingDevices;
|
||||||
|
|
||||||
|
/** 空闲设备数(workStatus='0') */
|
||||||
|
private Long idleDevices;
|
||||||
|
|
||||||
|
/** 未绑定用户设备数(user_id IS NULL) */
|
||||||
|
private Long unboundDevices;
|
||||||
|
}
|
||||||
@@ -18,9 +18,11 @@ public class DeviceCommandAckHandler implements MqttTopicHandler {
|
|||||||
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/ack$");
|
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/ack$");
|
||||||
|
|
||||||
private final IDeviceCommandAckHandler delegate;
|
private final IDeviceCommandAckHandler delegate;
|
||||||
|
private final DeviceIdentityResolver deviceIdentityResolver;
|
||||||
|
|
||||||
public DeviceCommandAckHandler(IDeviceCommandAckHandler delegate) {
|
public DeviceCommandAckHandler(IDeviceCommandAckHandler delegate, DeviceIdentityResolver deviceIdentityResolver) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
|
this.deviceIdentityResolver = deviceIdentityResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -29,7 +31,12 @@ public class DeviceCommandAckHandler implements MqttTopicHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(String deviceNo, String payload) {
|
public void handle(String deviceIdentity, String payload) {
|
||||||
|
String deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
|
||||||
|
if (deviceNo == null) {
|
||||||
|
log.warn("[MQTT] ACK 未找到设备 设备标识={} 消息体={}", deviceIdentity, payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
delegate.handleAck(deviceNo, payload);
|
delegate.handleAck(deviceNo, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.dromara.app.mqtt.MqttTopicHandler;
|
|||||||
import org.dromara.app.service.IAppDeviceService;
|
import org.dromara.app.service.IAppDeviceService;
|
||||||
import org.dromara.common.json.utils.JsonUtils;
|
import org.dromara.common.json.utils.JsonUtils;
|
||||||
import org.dromara.common.redis.utils.RedisUtils;
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.redisson.api.RLock;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ import java.time.Instant;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,12 +33,14 @@ public class DeviceDataHandler implements MqttTopicHandler {
|
|||||||
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/power$");
|
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/power$");
|
||||||
private final IAppDeviceService appDeviceService;
|
private final IAppDeviceService appDeviceService;
|
||||||
private final AppDeviceMapper appDeviceMapper;
|
private final AppDeviceMapper appDeviceMapper;
|
||||||
|
private static final String DEVICE_STATUS_LOCK_PREFIX = "lock:mqtt:device:status:";
|
||||||
|
|
||||||
@Value("${mqtt.command-ack.device-status-cache-prefix}")
|
@Value("${mqtt.command-ack.device-status-cache-prefix}")
|
||||||
private String deviceStatusCachePrefix;
|
private String deviceStatusCachePrefix;
|
||||||
|
|
||||||
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds}")
|
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds}")
|
||||||
private int deviceStatusCacheTtlSeconds;
|
private int deviceStatusCacheTtlSeconds;
|
||||||
|
private final DeviceIdentityResolver deviceIdentityResolver;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pattern topicPattern() {
|
public Pattern topicPattern() {
|
||||||
@@ -44,8 +48,15 @@ public class DeviceDataHandler implements MqttTopicHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(String deviceNo, String payload) {
|
public void handle(String deviceIdentity, String payload) {
|
||||||
|
String deviceNo = null;
|
||||||
try {
|
try {
|
||||||
|
deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
|
||||||
|
if (deviceNo == null) {
|
||||||
|
log.warn("[MQTT] 设备电量上报未找到设备 时间={} 设备标识={} 消息体={}",
|
||||||
|
HandlerLogTime.now(), deviceIdentity, payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
||||||
if (dto == null || dto.get("powerLevel") == null) {
|
if (dto == null || dto.get("powerLevel") == null) {
|
||||||
log.warn("[MQTT] 设备电量上报缺少电量字段 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
log.warn("[MQTT] 设备电量上报缺少电量字段 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||||
@@ -63,7 +74,8 @@ public class DeviceDataHandler implements MqttTopicHandler {
|
|||||||
refreshDeviceOnline(deviceNo);
|
refreshDeviceOnline(deviceNo);
|
||||||
log.info("[MQTT] 设备电量更新 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
log.info("[MQTT] 设备电量更新 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[MQTT] 设备电量更新失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
|
log.error("[MQTT] 设备电量更新失败 时间={} 设备标识={} 设备编号={} 消息体={}",
|
||||||
|
HandlerLogTime.now(), deviceIdentity, deviceNo, payload, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +84,11 @@ public class DeviceDataHandler implements MqttTopicHandler {
|
|||||||
* 写入 Redis 后 TTL 到期自动过期 = 离线
|
* 写入 Redis 后 TTL 到期自动过期 = 离线
|
||||||
*/
|
*/
|
||||||
private void refreshDeviceOnline(String deviceNo) {
|
private void refreshDeviceOnline(String deviceNo) {
|
||||||
|
RLock lock = RedisUtils.getClient().getLock(DEVICE_STATUS_LOCK_PREFIX + deviceNo);
|
||||||
|
boolean locked = false;
|
||||||
|
try {
|
||||||
|
lock.lock(10, TimeUnit.SECONDS);
|
||||||
|
locked = true;
|
||||||
Map<String, Object> statusCache = new HashMap<>();
|
Map<String, Object> statusCache = new HashMap<>();
|
||||||
statusCache.put("deviceNo", deviceNo);
|
statusCache.put("deviceNo", deviceNo);
|
||||||
statusCache.put("status", "1");
|
statusCache.put("status", "1");
|
||||||
@@ -86,5 +103,10 @@ public class DeviceDataHandler implements MqttTopicHandler {
|
|||||||
.set(AppDevice::getStatus, "1")
|
.set(AppDevice::getStatus, "1")
|
||||||
.eq(AppDevice::getDeviceNo, deviceNo)
|
.eq(AppDevice::getDeviceNo, deviceNo)
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
if (locked && lock.isHeldByCurrentThread()) {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.dromara.app.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.dromara.app.domain.AppDevice;
|
||||||
|
import org.dromara.app.mapper.AppDeviceMapper;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 MQTT Topic 中的设备标识。
|
||||||
|
* <p>
|
||||||
|
* Topic 第一段允许传设备编号或 MAC 地址。业务处理统一转换为数据库中的设备编号。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeviceIdentityResolver {
|
||||||
|
|
||||||
|
private final AppDeviceMapper appDeviceMapper;
|
||||||
|
|
||||||
|
public AppDevice resolve(String identity) {
|
||||||
|
if (StringUtils.isBlank(identity)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String text = identity.trim();
|
||||||
|
AppDevice device = appDeviceMapper.selectById(text);
|
||||||
|
if (device != null) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
return appDeviceMapper.selectByMac(normalizeMacAddress(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveDeviceNo(String identity) {
|
||||||
|
AppDevice device = resolve(identity);
|
||||||
|
return device == null ? null : device.getDeviceNo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String normalizeMacAddress(String macAddress) {
|
||||||
|
if (StringUtils.isBlank(macAddress)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return macAddress.trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,38 +5,50 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.app.domain.AppDevice;
|
import org.dromara.app.domain.AppDevice;
|
||||||
import org.dromara.app.domain.bo.AppDeviceBo;
|
import org.dromara.app.domain.bo.AppDeviceBo;
|
||||||
|
import org.dromara.app.domain.mqtt.DeviceCommand;
|
||||||
import org.dromara.app.mapper.AppDeviceMapper;
|
import org.dromara.app.mapper.AppDeviceMapper;
|
||||||
import org.dromara.app.mqtt.MqttTopicHandler;
|
import org.dromara.app.mqtt.MqttTopicHandler;
|
||||||
import org.dromara.app.service.IAppDeviceService;
|
import org.dromara.app.service.IAppDeviceService;
|
||||||
|
import org.dromara.app.service.IDeviceCommandPublisher;
|
||||||
import org.dromara.common.json.utils.JsonUtils;
|
import org.dromara.common.json.utils.JsonUtils;
|
||||||
import org.dromara.common.redis.utils.RedisUtils;
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.redisson.api.RLock;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Device registration handler, matches /{deviceNo}/publish/register.
|
* 设备注册处理器,匹配 /{deviceMac}/publish/register。
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DeviceRegisterHandler implements MqttTopicHandler {
|
public class
|
||||||
|
DeviceRegisterHandler implements MqttTopicHandler {
|
||||||
|
|
||||||
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/register$");
|
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/register$");
|
||||||
|
|
||||||
private final IAppDeviceService appDeviceService;
|
private final IAppDeviceService appDeviceService;
|
||||||
private final AppDeviceMapper appDeviceMapper;
|
private final AppDeviceMapper appDeviceMapper;
|
||||||
|
private static final String DEVICE_STATUS_LOCK_PREFIX = "lock:mqtt:device:status:";
|
||||||
|
private final ObjectProvider<IDeviceCommandPublisher> commandPublisherProvider;
|
||||||
|
|
||||||
@Value("${mqtt.command-ack.device-status-cache-prefix}")
|
@Value("${mqtt.command-ack.device-status-cache-prefix}")
|
||||||
private String deviceStatusCachePrefix;
|
private String deviceStatusCachePrefix;
|
||||||
|
|
||||||
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds}")
|
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds}")
|
||||||
private int deviceStatusCacheTtlSeconds;
|
private int deviceStatusCacheTtlSeconds;
|
||||||
|
private final DeviceIdentityResolver deviceIdentityResolver;
|
||||||
|
@Value("${mqtt.topics.publish-prefix:}")
|
||||||
|
private String mqttPublishPrefix;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pattern topicPattern() {
|
public Pattern topicPattern() {
|
||||||
@@ -44,22 +56,40 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(String deviceNo, String payload) {
|
public void handle(String deviceIdentity, String payload) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
||||||
AppDeviceBo device = buildRegisterDevice(deviceNo, dto);
|
AppDevice exists = deviceIdentityResolver.resolve(deviceIdentity);
|
||||||
|
String normalizedDeviceMac = resolveDeviceMac(deviceIdentity, dto);
|
||||||
|
if (exists == null && normalizedDeviceMac != null) {
|
||||||
|
exists = appDeviceMapper.selectByMac(normalizedDeviceMac);
|
||||||
|
}
|
||||||
|
if (exists == null) {
|
||||||
|
log.warn("[MQTT] 设备注册失败,设备标识未入库 时间={} 设备标识={} 消息体={}",
|
||||||
|
HandlerLogTime.now(), deviceIdentity, payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String deviceNo = exists.getDeviceNo();
|
||||||
|
if (normalizedDeviceMac == null) {
|
||||||
|
normalizedDeviceMac = deviceIdentityResolver.normalizeMacAddress(exists.getMacAddress());
|
||||||
|
}
|
||||||
|
AppDeviceBo device = buildRegisterDevice(deviceNo, normalizedDeviceMac, dto);
|
||||||
appDeviceService.registerByMqtt(device);
|
appDeviceService.registerByMqtt(device);
|
||||||
// 注册即在线
|
|
||||||
refreshDeviceOnline(deviceNo);
|
refreshDeviceOnline(deviceNo);
|
||||||
log.info("[MQTT] 设备注册 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
String commandTarget = normalizedDeviceMac == null ? deviceIdentity : normalizedDeviceMac;
|
||||||
|
sendDeviceNoToDevice(commandTarget, deviceNo);
|
||||||
|
log.info("[MQTT] 设备注册 时间={} MAC={} 设备编号={} 消息体={}",
|
||||||
|
HandlerLogTime.now(), normalizedDeviceMac, deviceNo, payload);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[MQTT] 设备注册失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
|
log.error("[MQTT] 设备注册失败 时间={} 设备标识={} 消息体={}", HandlerLogTime.now(), deviceIdentity, payload, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppDeviceBo buildRegisterDevice(String topicDeviceNo, Map<String, Object> dto) {
|
private AppDeviceBo buildRegisterDevice(String deviceNo, String deviceMac, Map<String, Object> dto) {
|
||||||
AppDeviceBo device = new AppDeviceBo();
|
AppDeviceBo device = new AppDeviceBo();
|
||||||
device.setDeviceNo(topicDeviceNo);
|
device.setDeviceNo(deviceNo);
|
||||||
|
device.setMacAddress(deviceMac);
|
||||||
device.setStatus("1"); // 注册即在线
|
device.setStatus("1"); // 注册即在线
|
||||||
if (dto == null) {
|
if (dto == null) {
|
||||||
return device;
|
return device;
|
||||||
@@ -68,11 +98,59 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
|
|||||||
device.setPowerLevel(valueAsString(dto.get("powerLevel")));
|
device.setPowerLevel(valueAsString(dto.get("powerLevel")));
|
||||||
device.setDeviceEm(valueAsString(dto.get("deviceEm")));
|
device.setDeviceEm(valueAsString(dto.get("deviceEm")));
|
||||||
device.setDeviceSn(valueAsString(dto.get("deviceSn")));
|
device.setDeviceSn(valueAsString(dto.get("deviceSn")));
|
||||||
device.setMacAddress(firstNotBlank(dto.get("deviceMac"), dto.get("macAddress")));
|
|
||||||
device.setFwVer(firstNotBlank(dto.get("version"), dto.get("fwVer")));
|
device.setFwVer(firstNotBlank(dto.get("version"), dto.get("fwVer")));
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveDeviceMac(String topicDeviceMac, Map<String, Object> dto) {
|
||||||
|
String payloadMac = dto == null ? null : firstNotBlank(dto.get("deviceMac"), dto.get("macAddress"));
|
||||||
|
return normalizeMacAddress(firstNotBlank(payloadMac, topicDeviceMac));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeMacAddress(String macAddress) {
|
||||||
|
if (macAddress == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String text = macAddress.trim();
|
||||||
|
return text.isBlank() ? null : text.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendDeviceNoToDevice(String deviceMac, String deviceNo) {
|
||||||
|
IDeviceCommandPublisher commandPublisher = commandPublisherProvider.getIfAvailable();
|
||||||
|
if (commandPublisher == null) {
|
||||||
|
log.warn("[MQTT] 设备编号下发失败,未找到命令发布器 时间={} MAC={} 设备编号={}",
|
||||||
|
HandlerLogTime.now(), deviceMac, deviceNo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DeviceCommand command = new DeviceCommand();
|
||||||
|
command.setDeviceNo(deviceNo);
|
||||||
|
command.setCommandType("registerDeviceNo");
|
||||||
|
command.setTopic(buildCommandTopic(deviceMac));
|
||||||
|
command.getPayload().put("deviceNo", deviceNo);
|
||||||
|
command.getPayload().put("deviceMac", deviceMac);
|
||||||
|
|
||||||
|
String commandId = commandPublisher.send(command);
|
||||||
|
log.info("[MQTT] 设备编号已下发 时间={} MAC={} 设备编号={} 命令编号={}",
|
||||||
|
HandlerLogTime.now(), deviceMac, deviceNo, commandId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MQTT] 设备编号下发失败 时间={} MAC={} 设备编号={}",
|
||||||
|
HandlerLogTime.now(), deviceMac, deviceNo, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildCommandTopic(String deviceMac) {
|
||||||
|
if (mqttPublishPrefix == null || mqttPublishPrefix.isBlank()) {
|
||||||
|
return "/" + deviceMac + "/subscriber/cmd";
|
||||||
|
}
|
||||||
|
String normalizedPrefix = mqttPublishPrefix.startsWith("/") ? mqttPublishPrefix : "/" + mqttPublishPrefix;
|
||||||
|
if (normalizedPrefix.endsWith("/")) {
|
||||||
|
normalizedPrefix = normalizedPrefix.substring(0, normalizedPrefix.length() - 1);
|
||||||
|
}
|
||||||
|
return normalizedPrefix + "/" + deviceMac + "/subscriber/cmd";
|
||||||
|
}
|
||||||
|
|
||||||
private String firstNotBlank(Object first, Object second) {
|
private String firstNotBlank(Object first, Object second) {
|
||||||
String firstText = valueAsString(first);
|
String firstText = valueAsString(first);
|
||||||
if (firstText != null && !firstText.isBlank()) {
|
if (firstText != null && !firstText.isBlank()) {
|
||||||
@@ -90,6 +168,11 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
|
|||||||
* 刷新设备在线状态到 Redis
|
* 刷新设备在线状态到 Redis
|
||||||
*/
|
*/
|
||||||
private void refreshDeviceOnline(String deviceNo) {
|
private void refreshDeviceOnline(String deviceNo) {
|
||||||
|
RLock lock = RedisUtils.getClient().getLock(DEVICE_STATUS_LOCK_PREFIX + deviceNo);
|
||||||
|
boolean locked = false;
|
||||||
|
try {
|
||||||
|
lock.lock(10, TimeUnit.SECONDS);
|
||||||
|
locked = true;
|
||||||
Map<String, Object> statusCache = new HashMap<>();
|
Map<String, Object> statusCache = new HashMap<>();
|
||||||
statusCache.put("deviceNo", deviceNo);
|
statusCache.put("deviceNo", deviceNo);
|
||||||
statusCache.put("status", "1");
|
statusCache.put("status", "1");
|
||||||
@@ -104,5 +187,10 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
|
|||||||
.set(AppDevice::getStatus, "1")
|
.set(AppDevice::getStatus, "1")
|
||||||
.eq(AppDevice::getDeviceNo, deviceNo)
|
.eq(AppDevice::getDeviceNo, deviceNo)
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
if (locked && lock.isHeldByCurrentThread()) {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import java.util.regex.Pattern;
|
|||||||
public class ErromesHandler implements MqttTopicHandler {
|
public class ErromesHandler implements MqttTopicHandler {
|
||||||
|
|
||||||
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/error$");
|
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/error$");
|
||||||
|
private final DeviceIdentityResolver deviceIdentityResolver;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pattern topicPattern() {
|
public Pattern topicPattern() {
|
||||||
@@ -25,12 +26,19 @@ public class ErromesHandler implements MqttTopicHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(String deviceNo, String payload) {
|
public void handle(String deviceIdentity, String payload) {
|
||||||
|
String deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
|
||||||
try {
|
try {
|
||||||
|
if (deviceNo == null) {
|
||||||
|
log.warn("[MQTT] 设备异常告警未找到设备 时间={} 设备标识={} 消息体={}",
|
||||||
|
HandlerLogTime.now(), deviceIdentity, payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
||||||
log.info("[MQTT] 设备异常告警 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, dto);
|
log.info("[MQTT] 设备异常告警 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, dto);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[MQTT] 设备异常处理失败 时间={} 设备编号={}", HandlerLogTime.now(), deviceNo, e);
|
log.error("[MQTT] 设备异常处理失败 时间={} 设备标识={} 设备编号={}",
|
||||||
|
HandlerLogTime.now(), deviceIdentity, deviceNo, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.dromara.app.handler;
|
package org.dromara.app.handler;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.app.domain.AppSchedulingDevice;
|
||||||
import org.dromara.app.domain.bo.AppWateringLogBo;
|
import org.dromara.app.domain.bo.AppWateringLogBo;
|
||||||
import org.dromara.app.domain.vo.AppDeviceVo;
|
import org.dromara.app.domain.vo.AppDeviceVo;
|
||||||
|
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
|
||||||
import org.dromara.app.mqtt.MqttTopicHandler;
|
import org.dromara.app.mqtt.MqttTopicHandler;
|
||||||
import org.dromara.app.service.IAppDeviceService;
|
import org.dromara.app.service.IAppDeviceService;
|
||||||
import org.dromara.app.service.IAppWateringLogService;
|
import org.dromara.app.service.IAppWateringLogService;
|
||||||
@@ -14,6 +17,7 @@ import org.springframework.stereotype.Component;
|
|||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -29,6 +33,8 @@ public class KeyFinishHandler implements MqttTopicHandler {
|
|||||||
|
|
||||||
private final IAppWateringLogService appWateringLogService;
|
private final IAppWateringLogService appWateringLogService;
|
||||||
private final IAppDeviceService appDeviceService;
|
private final IAppDeviceService appDeviceService;
|
||||||
|
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
|
||||||
|
private final DeviceIdentityResolver deviceIdentityResolver;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pattern topicPattern() {
|
public Pattern topicPattern() {
|
||||||
@@ -36,14 +42,26 @@ public class KeyFinishHandler implements MqttTopicHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(String deviceNo, String payload) {
|
public void handle(String deviceIdentity, String payload) {
|
||||||
|
String deviceNo = null;
|
||||||
try {
|
try {
|
||||||
|
deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
|
||||||
|
if (deviceNo == null) {
|
||||||
|
log.warn("[MQTT] 按键浇水完成上报未找到设备 时间={} 设备标识={} 消息体={}",
|
||||||
|
HandlerLogTime.now(), deviceIdentity, payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
||||||
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
|
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
|
||||||
|
if ("0".equals(logBo.getTriggerType())) {
|
||||||
|
appWateringLogService.confirmScheduleLog(logBo);
|
||||||
|
} else {
|
||||||
appWateringLogService.insertByBo(logBo);
|
appWateringLogService.insertByBo(logBo);
|
||||||
|
}
|
||||||
log.info("[MQTT] 按键浇水完成上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
log.info("[MQTT] 按键浇水完成上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[MQTT] 按键浇水完成上报处理失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
|
log.error("[MQTT] 按键浇水完成上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}",
|
||||||
|
HandlerLogTime.now(), deviceIdentity, deviceNo, payload, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +91,11 @@ public class KeyFinishHandler implements MqttTopicHandler {
|
|||||||
logBo.setTriggerType("1");
|
logBo.setTriggerType("1");
|
||||||
if ("schedule".equals(valueAsString(dto.get("triggerON")))){
|
if ("schedule".equals(valueAsString(dto.get("triggerON")))){
|
||||||
logBo.setTriggerType("0");
|
logBo.setTriggerType("0");
|
||||||
|
logBo.setScheduleId(queryScheduleId(topicDeviceNo));
|
||||||
}
|
}
|
||||||
logBo.setCreateTime(receivedAt);
|
logBo.setCreateTime(receivedAt);
|
||||||
logBo.setEndTime(endTime);
|
logBo.setEndTime(endTime);
|
||||||
|
logBo.setRemark("0".equals(logBo.getTriggerType()) ? "设备上报排程浇水完成" : "设备按键完成浇水");
|
||||||
return logBo;
|
return logBo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +104,21 @@ public class KeyFinishHandler implements MqttTopicHandler {
|
|||||||
return device == null ? null : device.getUserId();
|
return device == null ? null : device.getUserId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Long queryScheduleId(String deviceNo) {
|
||||||
|
List<AppSchedulingDevice> schedulingDevices = schedulingDeviceMapper.selectList(
|
||||||
|
Wrappers.<AppSchedulingDevice>lambdaQuery()
|
||||||
|
.eq(AppSchedulingDevice::getDeviceNo, deviceNo)
|
||||||
|
);
|
||||||
|
if (schedulingDevices == null || schedulingDevices.isEmpty()) {
|
||||||
|
log.warn("[MQTT] 按键浇水完成上报未找到设备绑定的排程 时间={} 设备编号={}", HandlerLogTime.now(), deviceNo);
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
if (schedulingDevices.size() > 1) {
|
||||||
|
log.warn("[MQTT] 按键浇水完成上报匹配到多个排程 时间={} 设备编号={} 数量={}", HandlerLogTime.now(), deviceNo, schedulingDevices.size());
|
||||||
|
}
|
||||||
|
return schedulingDevices.get(0).getScheduleId();
|
||||||
|
}
|
||||||
|
|
||||||
private Date parseStartTime(String startTime) {
|
private Date parseStartTime(String startTime) {
|
||||||
if (StringUtils.isBlank(startTime)) {
|
if (StringUtils.isBlank(startTime)) {
|
||||||
return new Date();
|
return new Date();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.dromara.app.service.IAppDeviceService;
|
|||||||
import org.dromara.app.service.IAppWateringLogService;
|
import org.dromara.app.service.IAppWateringLogService;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
import org.dromara.common.json.utils.JsonUtils;
|
import org.dromara.common.json.utils.JsonUtils;
|
||||||
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
@@ -34,6 +35,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
|
|||||||
private final IAppWateringLogService appWateringLogService;
|
private final IAppWateringLogService appWateringLogService;
|
||||||
private final IAppDeviceService appDeviceService;
|
private final IAppDeviceService appDeviceService;
|
||||||
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
|
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
|
||||||
|
private final DeviceIdentityResolver deviceIdentityResolver;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pattern topicPattern() {
|
public Pattern topicPattern() {
|
||||||
@@ -41,14 +43,23 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(String deviceNo, String payload) {
|
public void handle(String deviceIdentity, String payload) {
|
||||||
|
String deviceNo = null;
|
||||||
try {
|
try {
|
||||||
|
deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
|
||||||
|
if (deviceNo == null) {
|
||||||
|
log.warn("[MQTT] 浇水排程上报未找到设备 时间={} 设备标识={} 消息体={}",
|
||||||
|
HandlerLogTime.now(), deviceIdentity, payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
||||||
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
|
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
|
||||||
appWateringLogService.insertByBo(logBo);
|
logBo.setUserId(LoginHelper.getUserId());
|
||||||
|
appWateringLogService.confirmScheduleLog(logBo);
|
||||||
log.info("[MQTT] 浇水排程上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
log.info("[MQTT] 浇水排程上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[MQTT] 浇水排程上报处理失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
|
log.error("[MQTT] 浇水排程上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}",
|
||||||
|
HandlerLogTime.now(), deviceIdentity, deviceNo, payload, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +93,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
|
|||||||
}
|
}
|
||||||
logBo.setCreateTime(receivedAt);
|
logBo.setCreateTime(receivedAt);
|
||||||
logBo.setEndTime(endTime);
|
logBo.setEndTime(endTime);
|
||||||
|
logBo.setRemark("排程浇水完成");
|
||||||
return logBo;
|
return logBo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package org.dromara.app.mapper;
|
package org.dromara.app.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.apache.ibatis.annotations.Update;
|
import org.apache.ibatis.annotations.Update;
|
||||||
import org.dromara.app.domain.AppDevice;
|
import org.dromara.app.domain.AppDevice;
|
||||||
|
import org.dromara.app.domain.bo.AppDeviceBo;
|
||||||
import org.dromara.app.domain.vo.AppDeviceVo;
|
import org.dromara.app.domain.vo.AppDeviceVo;
|
||||||
|
import org.dromara.app.domain.vo.DashboardStatsVo;
|
||||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设备信息
|
* 设备信息
|
||||||
Mapper接口
|
Mapper接口
|
||||||
@@ -16,6 +21,154 @@ Mapper接口
|
|||||||
*/
|
*/
|
||||||
public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo> {
|
public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo> {
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
select
|
||||||
|
d.device_no,
|
||||||
|
d.user_id,
|
||||||
|
d.device_name,
|
||||||
|
d.device_img,
|
||||||
|
d.qrcode,
|
||||||
|
d.status,
|
||||||
|
d.work_status,
|
||||||
|
d.power_level,
|
||||||
|
d.power_level_updatatime,
|
||||||
|
d.wifi_name,
|
||||||
|
d.wifi_password,
|
||||||
|
d.device_em,
|
||||||
|
d.device_sn,
|
||||||
|
d.fw_ver,
|
||||||
|
d.mac_address,
|
||||||
|
d.expiration_time,
|
||||||
|
u.nick_name
|
||||||
|
from app_device d
|
||||||
|
left join sys_user u on u.user_id = d.user_id
|
||||||
|
<where>
|
||||||
|
<if test="bo.deviceNo != null and bo.deviceNo != ''">
|
||||||
|
and d.device_no = #{bo.deviceNo}
|
||||||
|
</if>
|
||||||
|
<if test="bo.userId != null">
|
||||||
|
and d.user_id = #{bo.userId}
|
||||||
|
</if>
|
||||||
|
<if test="bo.deviceName != null and bo.deviceName != ''">
|
||||||
|
and d.device_name like concat('%', #{bo.deviceName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bo.deviceImg != null and bo.deviceImg != ''">
|
||||||
|
and d.device_img = #{bo.deviceImg}
|
||||||
|
</if>
|
||||||
|
<if test="bo.qrcode != null and bo.qrcode != ''">
|
||||||
|
and d.qrcode = #{bo.qrcode}
|
||||||
|
</if>
|
||||||
|
<if test="bo.status != null and bo.status != ''">
|
||||||
|
and d.status = #{bo.status}
|
||||||
|
</if>
|
||||||
|
<if test="bo.powerLevel != null and bo.powerLevel != ''">
|
||||||
|
and d.power_level = #{bo.powerLevel}
|
||||||
|
</if>
|
||||||
|
<if test="bo.powerLevelUpdatatime != null">
|
||||||
|
and d.power_level_updatatime = #{bo.powerLevelUpdatatime}
|
||||||
|
</if>
|
||||||
|
<if test="bo.wifiName != null and bo.wifiName != ''">
|
||||||
|
and d.wifi_name like concat('%', #{bo.wifiName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bo.wifiPassword != null and bo.wifiPassword != ''">
|
||||||
|
and d.wifi_password = #{bo.wifiPassword}
|
||||||
|
</if>
|
||||||
|
<if test="bo.deviceEm != null and bo.deviceEm != ''">
|
||||||
|
and d.device_em = #{bo.deviceEm}
|
||||||
|
</if>
|
||||||
|
<if test="bo.deviceSn != null and bo.deviceSn != ''">
|
||||||
|
and d.device_sn = #{bo.deviceSn}
|
||||||
|
</if>
|
||||||
|
<if test="bo.fwVer != null and bo.fwVer != ''">
|
||||||
|
and d.fw_ver = #{bo.fwVer}
|
||||||
|
</if>
|
||||||
|
<if test="bo.macAddress != null and bo.macAddress != ''">
|
||||||
|
and d.mac_address = #{bo.macAddress}
|
||||||
|
</if>
|
||||||
|
<if test="bo.expirationTime != null">
|
||||||
|
and d.expiration_time = #{bo.expirationTime}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
order by d.device_no asc
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Page<AppDeviceVo> selectDeviceVoPage(Page<AppDeviceVo> page, @Param("bo") AppDeviceBo bo);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
select
|
||||||
|
d.device_no,
|
||||||
|
d.user_id,
|
||||||
|
d.device_name,
|
||||||
|
d.device_img,
|
||||||
|
d.qrcode,
|
||||||
|
d.status,
|
||||||
|
d.work_status,
|
||||||
|
d.power_level,
|
||||||
|
d.power_level_updatatime,
|
||||||
|
d.wifi_name,
|
||||||
|
d.wifi_password,
|
||||||
|
d.device_em,
|
||||||
|
d.device_sn,
|
||||||
|
d.fw_ver,
|
||||||
|
d.mac_address,
|
||||||
|
d.expiration_time,
|
||||||
|
u.nick_name
|
||||||
|
from app_device d
|
||||||
|
left join sys_user u on u.user_id = d.user_id
|
||||||
|
<where>
|
||||||
|
<if test="bo.deviceNo != null and bo.deviceNo != ''">
|
||||||
|
and d.device_no = #{bo.deviceNo}
|
||||||
|
</if>
|
||||||
|
<if test="bo.userId != null">
|
||||||
|
and d.user_id = #{bo.userId}
|
||||||
|
</if>
|
||||||
|
<if test="bo.deviceName != null and bo.deviceName != ''">
|
||||||
|
and d.device_name like concat('%', #{bo.deviceName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bo.deviceImg != null and bo.deviceImg != ''">
|
||||||
|
and d.device_img = #{bo.deviceImg}
|
||||||
|
</if>
|
||||||
|
<if test="bo.qrcode != null and bo.qrcode != ''">
|
||||||
|
and d.qrcode = #{bo.qrcode}
|
||||||
|
</if>
|
||||||
|
<if test="bo.status != null and bo.status != ''">
|
||||||
|
and d.status = #{bo.status}
|
||||||
|
</if>
|
||||||
|
<if test="bo.powerLevel != null and bo.powerLevel != ''">
|
||||||
|
and d.power_level = #{bo.powerLevel}
|
||||||
|
</if>
|
||||||
|
<if test="bo.powerLevelUpdatatime != null">
|
||||||
|
and d.power_level_updatatime = #{bo.powerLevelUpdatatime}
|
||||||
|
</if>
|
||||||
|
<if test="bo.wifiName != null and bo.wifiName != ''">
|
||||||
|
and d.wifi_name like concat('%', #{bo.wifiName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="bo.wifiPassword != null and bo.wifiPassword != ''">
|
||||||
|
and d.wifi_password = #{bo.wifiPassword}
|
||||||
|
</if>
|
||||||
|
<if test="bo.deviceEm != null and bo.deviceEm != ''">
|
||||||
|
and d.device_em = #{bo.deviceEm}
|
||||||
|
</if>
|
||||||
|
<if test="bo.deviceSn != null and bo.deviceSn != ''">
|
||||||
|
and d.device_sn = #{bo.deviceSn}
|
||||||
|
</if>
|
||||||
|
<if test="bo.fwVer != null and bo.fwVer != ''">
|
||||||
|
and d.fw_ver = #{bo.fwVer}
|
||||||
|
</if>
|
||||||
|
<if test="bo.macAddress != null and bo.macAddress != ''">
|
||||||
|
and d.mac_address = #{bo.macAddress}
|
||||||
|
</if>
|
||||||
|
<if test="bo.expirationTime != null">
|
||||||
|
and d.expiration_time = #{bo.expirationTime}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
order by d.device_no asc
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<AppDeviceVo> selectDeviceVoList(@Param("bo") AppDeviceBo bo);
|
||||||
|
|
||||||
@Update("""
|
@Update("""
|
||||||
<script>
|
<script>
|
||||||
update app_device
|
update app_device
|
||||||
@@ -63,8 +216,25 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
|
|||||||
@Select("""
|
@Select("""
|
||||||
select *
|
select *
|
||||||
from app_device
|
from app_device
|
||||||
where mac_address = #{macAddress}
|
where lower(mac_address) = lower(#{macAddress})
|
||||||
""")
|
""")
|
||||||
AppDevice selectByMac(@Param("macAddress") String macAddress);
|
AppDevice selectByMac(@Param("macAddress") String macAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪表盘统计 — 单条 SQL 条件聚合,一次查询返回全部 8 个指标。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
select
|
||||||
|
count(*) as totalDevices,
|
||||||
|
sum(case when status = '1' then 1 else 0 end) as onlineDevices,
|
||||||
|
sum(case when status = '2' then 1 else 0 end) as standbyDevices,
|
||||||
|
sum(case when status = '0' then 1 else 0 end) as offlineDevices,
|
||||||
|
sum(case when status = '3' then 1 else 0 end) as faultDevices,
|
||||||
|
sum(case when work_status = '1' then 1 else 0 end) as workingDevices,
|
||||||
|
sum(case when work_status = '0' then 1 else 0 end) as idleDevices,
|
||||||
|
sum(case when user_id is null then 1 else 0 end) as unboundDevices
|
||||||
|
from app_device
|
||||||
|
""")
|
||||||
|
DashboardStatsVo selectDashboardStats();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ public class MqttMessageDispatcher {
|
|||||||
for (MqttTopicHandler handler : handlers) {
|
for (MqttTopicHandler handler : handlers) {
|
||||||
Matcher matcher = handler.topicPattern().matcher(topic);
|
Matcher matcher = handler.topicPattern().matcher(topic);
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
String deviceNo = matcher.group(1);
|
String deviceIdentity = matcher.group(1);
|
||||||
handler.handle(deviceNo, payload);
|
handler.handle(deviceIdentity, payload);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ public interface MqttTopicHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回该处理器匹配的 Topic 正则。
|
* 返回该处理器匹配的 Topic 正则。
|
||||||
* 必须包含一个捕获组用于提取 deviceNo。
|
* 必须包含一个捕获组用于提取设备标识(设备编号或 MAC 地址)。
|
||||||
*/
|
*/
|
||||||
Pattern topicPattern();
|
Pattern topicPattern();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理匹配到的消息。
|
* 处理匹配到的消息。
|
||||||
*
|
*
|
||||||
* @param deviceNo 从 Topic 正则中提取的设备编号
|
* @param deviceIdentity 从 Topic 正则中提取的设备标识(设备编号或 MAC 地址)
|
||||||
* @param payload 消息体(JSON)
|
* @param payload 消息体(JSON)
|
||||||
*/
|
*/
|
||||||
void handle(String deviceNo, String payload);
|
void handle(String deviceIdentity, String payload);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.dromara.app.service;
|
|||||||
|
|
||||||
import org.dromara.app.domain.bo.AppDeviceBo;
|
import org.dromara.app.domain.bo.AppDeviceBo;
|
||||||
import org.dromara.app.domain.vo.AppDeviceVo;
|
import org.dromara.app.domain.vo.AppDeviceVo;
|
||||||
|
import org.dromara.app.domain.vo.DashboardStatsVo;
|
||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
|
|
||||||
@@ -87,6 +88,14 @@ public interface IAppDeviceService {
|
|||||||
|
|
||||||
Boolean switchDevice(String deviceNo, String workStatus, String startTime, Integer durationMin);
|
Boolean switchDevice(String deviceNo, String workStatus, String startTime, Integer durationMin);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量生成设备二维码并保存到设备二维码字段
|
||||||
|
*
|
||||||
|
* @param deviceNos 设备编号集合
|
||||||
|
* @return 是否生成成功
|
||||||
|
*/
|
||||||
|
Boolean generateQrCode(Collection<String> deviceNos);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验并批量删除设备信息
|
* 校验并批量删除设备信息
|
||||||
信息
|
信息
|
||||||
@@ -97,5 +106,21 @@ public interface IAppDeviceService {
|
|||||||
*/
|
*/
|
||||||
Boolean deleteWithValidByIds(Collection<String> deviceNos, Boolean isValid);
|
Boolean deleteWithValidByIds(Collection<String> deviceNos, Boolean isValid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑设备:清空设备用户、删除设备关联数据,并下发设备初始化指令。
|
||||||
|
*
|
||||||
|
* @param deviceNos 设备编号集合
|
||||||
|
* @param userId 当前用户ID
|
||||||
|
* @return 是否解绑成功
|
||||||
|
*/
|
||||||
|
Boolean unbindDevices(Collection<String> deviceNos, Long userId);
|
||||||
|
|
||||||
Map<String, Object> bindDeviceStatus(AppDeviceBo appDevice);
|
Map<String, Object> bindDeviceStatus(AppDeviceBo appDevice);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪表盘统计 — 返回首页 8 个统计卡片数据。
|
||||||
|
*
|
||||||
|
* @return 仪表盘统计 VO(设备总数/在线/待机/离线/故障/工作中/空闲/未绑定用户)
|
||||||
|
*/
|
||||||
|
DashboardStatsVo getDashboardStats();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package org.dromara.app.service;
|
package org.dromara.app.service;
|
||||||
|
|
||||||
import org.dromara.app.domain.vo.AppWateringLogVo;
|
|
||||||
import org.dromara.app.domain.bo.AppWateringLogBo;
|
import org.dromara.app.domain.bo.AppWateringLogBo;
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
import org.dromara.app.domain.vo.AppWateringLogVo;
|
||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -68,6 +68,43 @@ public interface IAppWateringLogService {
|
|||||||
*/
|
*/
|
||||||
Boolean finishManualLog(String deviceNo, Long userId, Date endTime);
|
Boolean finishManualLog(String deviceNo, Long userId, Date endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束当前手动浇水记录。
|
||||||
|
*
|
||||||
|
* @param deviceNo 设备编号
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @param remark 备注
|
||||||
|
* @return 是否更新成功
|
||||||
|
*/
|
||||||
|
Boolean finishManualLog(String deviceNo, Long userId, Date endTime, String remark);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束指定设备所有进行中的浇水记录。
|
||||||
|
*
|
||||||
|
* @param deviceNo 设备编号
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @param remark 备注
|
||||||
|
* @return 更新数量
|
||||||
|
*/
|
||||||
|
int finishRunningLogsByDevice(String deviceNo, Date endTime, String remark);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存设备离线期间按排程推算出的浇水记录,已存在则跳过。
|
||||||
|
*
|
||||||
|
* @param bo 浇水记录
|
||||||
|
* @return 是否新增成功
|
||||||
|
*/
|
||||||
|
Boolean saveEstimatedScheduleLog(AppWateringLogBo bo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存设备上报的排程浇水记录。若已有离线预估记录,则更新为设备确认记录。
|
||||||
|
*
|
||||||
|
* @param bo 浇水记录
|
||||||
|
* @return 是否保存成功
|
||||||
|
*/
|
||||||
|
Boolean confirmScheduleLog(AppWateringLogBo bo);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验并批量删除浇水记录信息
|
* 校验并批量删除浇水记录信息
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -42,4 +42,12 @@ public interface IDeviceCommandService {
|
|||||||
*/
|
*/
|
||||||
String sendCustomCommand(String deviceNo, String commandType, Map<String, Object> extra);
|
String sendCustomCommand(String deviceNo, String commandType, Map<String, Object> extra);
|
||||||
String sendBindDeviceCommand(String deviceNo);
|
String sendBindDeviceCommand(String deviceNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下发设备初始化指令。用于解绑设备,允许设备未绑定用户时下发。
|
||||||
|
*
|
||||||
|
* @param deviceNo 设备编号
|
||||||
|
* @return commandId
|
||||||
|
*/
|
||||||
|
String sendInitDeviceCommand(String deviceNo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.dromara.app.service.impl;
|
package org.dromara.app.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.extra.qrcode.QrCodeUtil;
|
||||||
import com.baomidou.lock.annotation.Lock4j;
|
import com.baomidou.lock.annotation.Lock4j;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
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.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -13,6 +16,7 @@ import org.dromara.app.domain.AppSchedulingDevice;
|
|||||||
import org.dromara.app.domain.AppWateringLog;
|
import org.dromara.app.domain.AppWateringLog;
|
||||||
import org.dromara.app.domain.bo.AppDeviceBo;
|
import org.dromara.app.domain.bo.AppDeviceBo;
|
||||||
import org.dromara.app.domain.vo.AppDeviceVo;
|
import org.dromara.app.domain.vo.AppDeviceVo;
|
||||||
|
import org.dromara.app.domain.vo.DashboardStatsVo;
|
||||||
import org.dromara.app.mapper.AppDeviceMapper;
|
import org.dromara.app.mapper.AppDeviceMapper;
|
||||||
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
|
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
|
||||||
import org.dromara.app.mapper.AppWateringLogMapper;
|
import org.dromara.app.mapper.AppWateringLogMapper;
|
||||||
@@ -22,8 +26,11 @@ import org.dromara.app.service.IDeviceCommandService;
|
|||||||
import org.dromara.common.core.exception.ServiceException;
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
import org.dromara.common.core.utils.MapstructUtils;
|
import org.dromara.common.core.utils.MapstructUtils;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.json.utils.JsonUtils;
|
||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
|
import org.dromara.common.oss.entity.UploadResult;
|
||||||
|
import org.dromara.common.oss.factory.OssFactory;
|
||||||
import org.dromara.common.satoken.utils.LoginHelper;
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
@@ -54,15 +61,24 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TableDataInfo<AppDeviceVo> queryPageList(AppDeviceBo bo, PageQuery pageQuery) {
|
public TableDataInfo<AppDeviceVo> queryPageList(AppDeviceBo bo, PageQuery pageQuery) {
|
||||||
LambdaQueryWrapper<AppDevice> lqw = buildQueryWrapper(bo);
|
Page<AppDeviceVo> page = pageQuery.build();
|
||||||
Page<AppDeviceVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
page.orders().replaceAll(this::qualifyDeviceOrderItem);
|
||||||
|
Page<AppDeviceVo> result = baseMapper.selectDeviceVoPage(page, bo);
|
||||||
return TableDataInfo.build(result);
|
return TableDataInfo.build(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AppDeviceVo> queryList(AppDeviceBo bo) {
|
public List<AppDeviceVo> queryList(AppDeviceBo bo) {
|
||||||
LambdaQueryWrapper<AppDevice> lqw = buildQueryWrapper(bo);
|
return baseMapper.selectDeviceVoList(bo);
|
||||||
return baseMapper.selectVoList(lqw);
|
}
|
||||||
|
|
||||||
|
private OrderItem qualifyDeviceOrderItem(OrderItem item) {
|
||||||
|
String column = item.getColumn();
|
||||||
|
if (StringUtils.isBlank(column) || column.contains(".")) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
item.setColumn("d." + column);
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LambdaQueryWrapper<AppDevice> buildQueryWrapper(AppDeviceBo bo) {
|
private LambdaQueryWrapper<AppDevice> buildQueryWrapper(AppDeviceBo bo) {
|
||||||
@@ -73,6 +89,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
|||||||
lqw.eq(bo.getUserId() != null, AppDevice::getUserId, bo.getUserId());
|
lqw.eq(bo.getUserId() != null, AppDevice::getUserId, bo.getUserId());
|
||||||
lqw.like(StringUtils.isNotBlank(bo.getDeviceName()), AppDevice::getDeviceName, bo.getDeviceName());
|
lqw.like(StringUtils.isNotBlank(bo.getDeviceName()), AppDevice::getDeviceName, bo.getDeviceName());
|
||||||
lqw.eq(StringUtils.isNotBlank(bo.getDeviceImg()), AppDevice::getDeviceImg, bo.getDeviceImg());
|
lqw.eq(StringUtils.isNotBlank(bo.getDeviceImg()), AppDevice::getDeviceImg, bo.getDeviceImg());
|
||||||
|
lqw.eq(StringUtils.isNotBlank(bo.getQrcode()), AppDevice::getQrcode, bo.getQrcode());
|
||||||
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), AppDevice::getStatus, bo.getStatus());
|
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), AppDevice::getStatus, bo.getStatus());
|
||||||
lqw.eq(StringUtils.isNotBlank(bo.getPowerLevel()), AppDevice::getPowerLevel, bo.getPowerLevel());
|
lqw.eq(StringUtils.isNotBlank(bo.getPowerLevel()), AppDevice::getPowerLevel, bo.getPowerLevel());
|
||||||
lqw.eq(bo.getPowerLevelUpdatatime() != null, AppDevice::getPowerLevelUpdatatime, bo.getPowerLevelUpdatatime());
|
lqw.eq(bo.getPowerLevelUpdatatime() != null, AppDevice::getPowerLevelUpdatatime, bo.getPowerLevelUpdatatime());
|
||||||
@@ -90,7 +107,11 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
|||||||
public Boolean insertByBo(AppDeviceBo bo) {
|
public Boolean insertByBo(AppDeviceBo bo) {
|
||||||
// fillBindTokenHash(bo);
|
// fillBindTokenHash(bo);
|
||||||
AppDevice add = MapstructUtils.convert(bo, AppDevice.class);
|
AppDevice add = MapstructUtils.convert(bo, AppDevice.class);
|
||||||
validEntityBeforeSave(add);
|
validEntityBeforeInsert(add);
|
||||||
|
normalizeMacAddress(add);
|
||||||
|
if (baseMapper.selectByMac(add.getMacAddress()) != null) {
|
||||||
|
throw new ServiceException("MAC地址已存在,不能重复新增设备");
|
||||||
|
}
|
||||||
return baseMapper.insert(add) > 0;
|
return baseMapper.insert(add) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +119,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
|||||||
public Boolean updateByBo(AppDeviceBo bo) {
|
public Boolean updateByBo(AppDeviceBo bo) {
|
||||||
// fillBindTokenHash(bo);
|
// fillBindTokenHash(bo);
|
||||||
AppDevice update = MapstructUtils.convert(bo, AppDevice.class);
|
AppDevice update = MapstructUtils.convert(bo, AppDevice.class);
|
||||||
validEntityBeforeSave(update);
|
validEntityBeforeUpdate(update);
|
||||||
return baseMapper.updateById(update) > 0;
|
return baseMapper.updateById(update) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +253,56 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean generateQrCode(Collection<String> deviceNos) {
|
||||||
|
if (deviceNos == null || deviceNos.isEmpty()) {
|
||||||
|
throw new ServiceException("设备编号不能为空");
|
||||||
|
}
|
||||||
|
List<AppDevice> devices = baseMapper.selectByIds(deviceNos);
|
||||||
|
if (devices.size() != new HashSet<>(deviceNos).size()) {
|
||||||
|
throw new ServiceException("存在无效的设备编号");
|
||||||
|
}
|
||||||
|
|
||||||
private void validEntityBeforeSave(AppDevice entity) {
|
for (AppDevice device : devices) {
|
||||||
|
byte[] imageBytes = QrCodeUtil.generatePng(buildQrCodeContent(device), 300, 300);
|
||||||
|
UploadResult uploadResult = OssFactory.instance().uploadSuffix(imageBytes, ".png", "image/png");
|
||||||
|
int rows = baseMapper.update(null, new LambdaUpdateWrapper<AppDevice>()
|
||||||
|
.set(AppDevice::getQrcode, uploadResult.getUrl())
|
||||||
|
.eq(AppDevice::getDeviceNo, device.getDeviceNo()));
|
||||||
|
if (rows <= 0) {
|
||||||
|
throw new ServiceException("设备二维码保存失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildQrCodeContent(AppDevice device) {
|
||||||
|
if (StringUtils.isBlank(device.getMacAddress())) {
|
||||||
|
throw new ServiceException("设备MAC地址不能为空,无法生成二维码");
|
||||||
|
}
|
||||||
|
Map<String, String> content = new LinkedHashMap<>();
|
||||||
|
content.put("deviceNo", device.getDeviceNo());
|
||||||
|
content.put("mac", device.getMacAddress());
|
||||||
|
content.put("transport", "ble");
|
||||||
|
content.put("name", device.getDeviceName());
|
||||||
|
return JsonUtils.toJsonString(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void validEntityBeforeInsert(AppDevice entity) {
|
||||||
|
if (StringUtils.isBlank(entity.getMacAddress())) {
|
||||||
|
throw new ServiceException("MAC地址不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void normalizeMacAddress(AppDevice entity) {
|
||||||
|
if (StringUtils.isNotBlank(entity.getMacAddress())) {
|
||||||
|
entity.setMacAddress(entity.getMacAddress().toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validEntityBeforeUpdate(AppDevice entity) {
|
||||||
if (StringUtils.isBlank(entity.getDeviceNo())) {
|
if (StringUtils.isBlank(entity.getDeviceNo())) {
|
||||||
throw new ServiceException("设备编号不能为空");
|
throw new ServiceException("设备编号不能为空");
|
||||||
}
|
}
|
||||||
@@ -251,4 +320,66 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
|||||||
}
|
}
|
||||||
return flag;
|
return flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean unbindDevices(Collection<String> deviceNos, Long userId) {
|
||||||
|
if (deviceNos == null || deviceNos.isEmpty()) {
|
||||||
|
throw new ServiceException("设备编号不能为空");
|
||||||
|
}
|
||||||
|
if (userId == null) {
|
||||||
|
throw new ServiceException("用户不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> distinctDeviceNos = deviceNos.stream()
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (distinctDeviceNos.isEmpty()) {
|
||||||
|
throw new ServiceException("设备编号不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AppDevice> devices = baseMapper.selectList(
|
||||||
|
Wrappers.<AppDevice>lambdaQuery()
|
||||||
|
.in(AppDevice::getDeviceNo, distinctDeviceNos)
|
||||||
|
.eq(AppDevice::getUserId, userId)
|
||||||
|
);
|
||||||
|
if (devices.size() != distinctDeviceNos.size()) {
|
||||||
|
throw new ServiceException("设备不存在或无权操作");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (AppDevice device : devices) {
|
||||||
|
deviceCommandService.sendInitDeviceCommand(device.getDeviceNo());
|
||||||
|
}
|
||||||
|
|
||||||
|
int rows = baseMapper.update(null,
|
||||||
|
Wrappers.<AppDevice>lambdaUpdate()
|
||||||
|
.set(AppDevice::getUserId, null)
|
||||||
|
.set(AppDevice::getWorkStatus, "0")
|
||||||
|
.set(AppDevice::getWifiName, null)
|
||||||
|
.set(AppDevice::getWifiPassword, null)
|
||||||
|
.set(AppDevice::getBindTokenHash, null)
|
||||||
|
.in(AppDevice::getDeviceNo, distinctDeviceNos)
|
||||||
|
.eq(AppDevice::getUserId, userId)
|
||||||
|
);
|
||||||
|
if (rows != distinctDeviceNos.size()) {
|
||||||
|
throw new ServiceException("设备解绑失败,请重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulingDeviceMapper.delete(
|
||||||
|
new QueryWrapper<AppSchedulingDevice>().in("device_no", distinctDeviceNos)
|
||||||
|
);
|
||||||
|
wateringLogMapper.delete(
|
||||||
|
new QueryWrapper<AppWateringLog>().in("device_no", distinctDeviceNos)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仪表盘统计 — 单条 SQL 聚合查询,避免 N+1 问题。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public DashboardStatsVo getDashboardStats() {
|
||||||
|
return baseMapper.selectDashboardStats();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
package org.dromara.app.service.impl;
|
package org.dromara.app.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import org.dromara.common.core.utils.MapstructUtils;
|
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
|
||||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.system.domain.SysConfig;
|
import org.dromara.app.domain.AppWateringLog;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.dromara.app.domain.bo.AppWateringLogBo;
|
import org.dromara.app.domain.bo.AppWateringLogBo;
|
||||||
import org.dromara.app.domain.vo.AppWateringLogVo;
|
import org.dromara.app.domain.vo.AppWateringLogVo;
|
||||||
import org.dromara.app.domain.AppWateringLog;
|
|
||||||
import org.dromara.app.mapper.AppWateringLogMapper;
|
import org.dromara.app.mapper.AppWateringLogMapper;
|
||||||
import org.dromara.app.service.IAppWateringLogService;
|
import org.dromara.app.service.IAppWateringLogService;
|
||||||
|
import org.dromara.common.core.utils.MapstructUtils;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 浇水记录Service业务层处理
|
* 浇水记录Service业务层处理
|
||||||
@@ -86,6 +86,7 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
|
|||||||
lqw.eq(StringUtils.isNotBlank(bo.getDurationMin()), AppWateringLog::getDurationMin, bo.getDurationMin());
|
lqw.eq(StringUtils.isNotBlank(bo.getDurationMin()), AppWateringLog::getDurationMin, bo.getDurationMin());
|
||||||
lqw.eq(StringUtils.isNotBlank(bo.getZones()), AppWateringLog::getZones, bo.getZones());
|
lqw.eq(StringUtils.isNotBlank(bo.getZones()), AppWateringLog::getZones, bo.getZones());
|
||||||
lqw.eq(StringUtils.isNotBlank(bo.getTriggerType()), AppWateringLog::getTriggerType, bo.getTriggerType());
|
lqw.eq(StringUtils.isNotBlank(bo.getTriggerType()), AppWateringLog::getTriggerType, bo.getTriggerType());
|
||||||
|
lqw.like(StringUtils.isNotBlank(bo.getRemark()), AppWateringLog::getRemark, bo.getRemark());
|
||||||
// lqw.eq(bo.getCreateTime() != null, AppWateringLog::getCreateTime, bo.getCreateTime());
|
// lqw.eq(bo.getCreateTime() != null, AppWateringLog::getCreateTime, bo.getCreateTime());
|
||||||
lqw.between(params.get("beginTime") != null && params.get("endTime") != null,
|
lqw.between(params.get("beginTime") != null && params.get("endTime") != null,
|
||||||
AppWateringLog::getCreateTime, params.get("beginTime"), params.get("endTime"));
|
AppWateringLog::getCreateTime, params.get("beginTime"), params.get("endTime"));
|
||||||
@@ -113,6 +114,49 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
|
|||||||
return flag;
|
return flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean saveEstimatedScheduleLog(AppWateringLogBo bo) {
|
||||||
|
if (bo == null || StringUtils.isBlank(bo.getDeviceNo()) || bo.getScheduleId() == null || bo.getStartTime() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (existsScheduleLog(bo.getDeviceNo(), bo.getScheduleId(), bo.getStartTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bo.setTriggerType("0");
|
||||||
|
if (StringUtils.isBlank(bo.getRemark())) {
|
||||||
|
bo.setRemark("设备离线,按排程自动补记");
|
||||||
|
}
|
||||||
|
return insertByBo(bo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean confirmScheduleLog(AppWateringLogBo bo) {
|
||||||
|
if (bo == null || StringUtils.isBlank(bo.getDeviceNo()) || bo.getScheduleId() == null || bo.getStartTime() == null) {
|
||||||
|
return insertByBo(bo);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppWateringLog estimatedLog = baseMapper.selectOne(
|
||||||
|
Wrappers.<AppWateringLog>lambdaQuery()
|
||||||
|
.eq(AppWateringLog::getDeviceNo, bo.getDeviceNo())
|
||||||
|
.eq(AppWateringLog::getScheduleId, bo.getScheduleId())
|
||||||
|
.eq(AppWateringLog::getStartTime, bo.getStartTime())
|
||||||
|
.eq(AppWateringLog::getTriggerType, "0")
|
||||||
|
.like(AppWateringLog::getRemark, "自动补记")
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (estimatedLog == null) {
|
||||||
|
if (existsScheduleLog(bo.getDeviceNo(), bo.getScheduleId(), bo.getStartTime())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return insertByBo(bo);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppWateringLog update = MapstructUtils.convert(bo, AppWateringLog.class);
|
||||||
|
update.setId(estimatedLog.getId());
|
||||||
|
update.setRemark("设备离线期间执行,联网后补传确认");
|
||||||
|
return baseMapper.updateById(update) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改浇水记录
|
* 修改浇水记录
|
||||||
*
|
*
|
||||||
@@ -126,8 +170,23 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
|
|||||||
return baseMapper.updateById(update) > 0;
|
return baseMapper.updateById(update) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean existsScheduleLog(String deviceNo, Long scheduleId, Date startTime) {
|
||||||
|
return baseMapper.exists(
|
||||||
|
Wrappers.<AppWateringLog>lambdaQuery()
|
||||||
|
.eq(AppWateringLog::getDeviceNo, deviceNo)
|
||||||
|
.eq(AppWateringLog::getScheduleId, scheduleId)
|
||||||
|
.eq(AppWateringLog::getStartTime, startTime)
|
||||||
|
.eq(AppWateringLog::getTriggerType, "0")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean finishManualLog(String deviceNo, Long userId, Date endTime) {
|
public Boolean finishManualLog(String deviceNo, Long userId, Date endTime) {
|
||||||
|
return finishManualLog(deviceNo, userId, endTime, "手动停止浇水");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean finishManualLog(String deviceNo, Long userId, Date endTime, String remark) {
|
||||||
if (StringUtils.isBlank(deviceNo) || userId == null || endTime == null) {
|
if (StringUtils.isBlank(deviceNo) || userId == null || endTime == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -147,13 +206,49 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeLog.setEndTime(endTime);
|
LambdaUpdateWrapper<AppWateringLog> updateWrapper = Wrappers.lambdaUpdate();
|
||||||
|
updateWrapper.eq(AppWateringLog::getId, activeLog.getId())
|
||||||
|
.isNull(AppWateringLog::getEndTime)
|
||||||
|
.set(AppWateringLog::getEndTime, endTime)
|
||||||
|
.set(StringUtils.isNotBlank(remark), AppWateringLog::getRemark, remark);
|
||||||
if (activeLog.getStartTime() != null) {
|
if (activeLog.getStartTime() != null) {
|
||||||
long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime();
|
long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime();
|
||||||
long durationMin = Math.max(durationMillis, 0L) / 60000L;
|
long durationMin = Math.max(durationMillis, 0L) / 60000L;
|
||||||
activeLog.setDurationMin(String.valueOf(durationMin));
|
updateWrapper.set(AppWateringLog::getDurationMin, String.valueOf(durationMin));
|
||||||
}
|
}
|
||||||
return baseMapper.updateById(activeLog) > 0;
|
return baseMapper.update(null, updateWrapper) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int finishRunningLogsByDevice(String deviceNo, Date endTime, String remark) {
|
||||||
|
if (StringUtils.isBlank(deviceNo) || endTime == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AppWateringLog> activeLogs = baseMapper.selectList(
|
||||||
|
Wrappers.<AppWateringLog>lambdaQuery()
|
||||||
|
.eq(AppWateringLog::getDeviceNo, deviceNo)
|
||||||
|
.isNull(AppWateringLog::getEndTime)
|
||||||
|
);
|
||||||
|
if (activeLogs == null || activeLogs.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int updated = 0;
|
||||||
|
for (AppWateringLog activeLog : activeLogs) {
|
||||||
|
LambdaUpdateWrapper<AppWateringLog> updateWrapper = Wrappers.lambdaUpdate();
|
||||||
|
updateWrapper.eq(AppWateringLog::getId, activeLog.getId())
|
||||||
|
.isNull(AppWateringLog::getEndTime)
|
||||||
|
.set(AppWateringLog::getEndTime, endTime)
|
||||||
|
.set(StringUtils.isNotBlank(remark), AppWateringLog::getRemark, remark);
|
||||||
|
if (activeLog.getStartTime() != null) {
|
||||||
|
long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime();
|
||||||
|
long durationMin = Math.max(durationMillis, 0L) / 60000L;
|
||||||
|
updateWrapper.set(AppWateringLog::getDurationMin, String.valueOf(durationMin));
|
||||||
|
}
|
||||||
|
updated += baseMapper.update(null, updateWrapper);
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
|||||||
AppWateringLogBo logBo = buildManualStartLog(deviceNo, userId, commandId, startTime, durationMin);
|
AppWateringLogBo logBo = buildManualStartLog(deviceNo, userId, commandId, startTime, durationMin);
|
||||||
wateringLogService.insertByBo(logBo);
|
wateringLogService.insertByBo(logBo);
|
||||||
} else {
|
} else {
|
||||||
boolean updated = wateringLogService.finishManualLog(deviceNo, userId, new Date());
|
boolean updated = wateringLogService.finishManualLog(deviceNo, userId, new Date(), "手动停止浇水");
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
log.warn("[命令] 未找到进行中的手动浇水记录 设备编号={} 用户ID={} 停止命令编号={}",
|
log.warn("[命令] 未找到进行中的手动浇水记录 设备编号={} 用户ID={} 停止命令编号={}",
|
||||||
deviceNo, userId, commandId);
|
deviceNo, userId, commandId);
|
||||||
@@ -124,6 +124,27 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
|||||||
command.getPayload().putAll(objectObjectHashMap);
|
command.getPayload().putAll(objectObjectHashMap);
|
||||||
return sendCommand(command);
|
return sendCommand(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String sendInitDeviceCommand(String deviceNo) {
|
||||||
|
AppDeviceVo device = deviceService.queryById(deviceNo);
|
||||||
|
if (ObjectUtil.isNull(device)) {
|
||||||
|
throw new ServiceException("设备不存在:" + deviceNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceCommand command = new DeviceCommand();
|
||||||
|
command.setDeviceNo(deviceNo);
|
||||||
|
command.setCommandType("initDevice");
|
||||||
|
Map<String, Object> payload = new HashMap<>();
|
||||||
|
payload.put("deviceNo", deviceNo);
|
||||||
|
payload.put("initStatus", true);
|
||||||
|
payload.put("cmd", "init");
|
||||||
|
command.getPayload().putAll(payload);
|
||||||
|
|
||||||
|
String commandId = commandPublisher.send(command);
|
||||||
|
log.info("[命令] 设备初始化命令已下发 设备编号={} 命令编号={}", deviceNo, commandId);
|
||||||
|
return commandId;
|
||||||
|
}
|
||||||
// ========================= 校验 =========================
|
// ========================= 校验 =========================
|
||||||
|
|
||||||
private void validate(DeviceCommand command) {
|
private void validate(DeviceCommand command) {
|
||||||
@@ -172,6 +193,7 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
|||||||
logBo.setScheduleId(0L);
|
logBo.setScheduleId(0L);
|
||||||
logBo.setDurationMin(String.valueOf(durationMin));
|
logBo.setDurationMin(String.valueOf(durationMin));
|
||||||
logBo.setTriggerType("1"); // 手动
|
logBo.setTriggerType("1"); // 手动
|
||||||
|
logBo.setRemark("手动开始浇水");
|
||||||
logBo.setCreateTime(new Date());
|
logBo.setCreateTime(new Date());
|
||||||
|
|
||||||
if (StringUtils.isNotEmpty(startTime)) {
|
if (StringUtils.isNotEmpty(startTime)) {
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.app.domain.AppDevice;
|
import org.dromara.app.domain.AppDevice;
|
||||||
import org.dromara.app.mapper.AppDeviceMapper;
|
import org.dromara.app.mapper.AppDeviceMapper;
|
||||||
|
import org.dromara.app.service.IAppWateringLogService;
|
||||||
import org.dromara.common.redis.utils.RedisUtils;
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.redisson.api.RLock;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,19 +36,20 @@ import java.util.stream.Collectors;
|
|||||||
public class DeviceOfflineCheckTask {
|
public class DeviceOfflineCheckTask {
|
||||||
|
|
||||||
private final AppDeviceMapper appDeviceMapper;
|
private final AppDeviceMapper appDeviceMapper;
|
||||||
|
private static final String DEVICE_STATUS_LOCK_PREFIX = "lock:mqtt:device:status:";
|
||||||
|
|
||||||
@Value("${mqtt.command-ack.device-status-cache-prefix}")
|
@Value("${mqtt.command-ack.device-status-cache-prefix}")
|
||||||
private String deviceStatusCachePrefix;
|
private String deviceStatusCachePrefix;
|
||||||
|
|
||||||
@Value("${mqtt.offline-check.enabled:true}")
|
@Value("${mqtt.offline-check.enabled:true}")
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
private final IAppWateringLogService wateringLogService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每 90 秒检查一次(Redis TTL 默认 300s,90s 间隔确保 1.5 个周期内同步)
|
* 每 90 秒检查一次(Redis TTL 默认 300s,90s 间隔确保 1.5 个周期内同步)
|
||||||
* 可通过 mqtt.offline-check.interval-ms 覆盖(单位 ms)
|
* 可通过 mqtt.offline-check.interval-ms 覆盖(单位 ms)
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedDelayString = "${mqtt.offline-check.interval-ms:90000}")
|
@Scheduled(fixedDelayString = "${mqtt.offline-check.interval-ms:90000}")
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void checkOfflineDevices() {
|
public void checkOfflineDevices() {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return;
|
return;
|
||||||
@@ -72,15 +77,55 @@ public class DeviceOfflineCheckTask {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量更新为离线
|
List<String> updatedDeviceNos = new ArrayList<>();
|
||||||
int updated = appDeviceMapper.update(null,
|
for (String deviceNo : offlineDeviceNos) {
|
||||||
|
if (markOfflineIfStillExpired(deviceNo)) {
|
||||||
|
updatedDeviceNos.add(deviceNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updatedDeviceNos.isEmpty()) {
|
||||||
|
log.info("[设备离线] 检测到 {} 台设备离线,已更新数据库状态,设备编号:{}",
|
||||||
|
updatedDeviceNos.size(), updatedDeviceNos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean markOfflineIfStillExpired(String deviceNo) {
|
||||||
|
RLock lock = RedisUtils.getClient().getLock(DEVICE_STATUS_LOCK_PREFIX + deviceNo);
|
||||||
|
boolean locked = false;
|
||||||
|
try {
|
||||||
|
locked = lock.tryLock(100, 10000, TimeUnit.MILLISECONDS);
|
||||||
|
if (!locked) {
|
||||||
|
log.debug("[设备离线] 获取设备状态锁失败,本轮跳过 设备编号={}", deviceNo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 获取锁后再次确认 Redis 在线 Key 仍不存在,避免设备刚注册上线时被旧扫描结果覆盖为离线
|
||||||
|
if (RedisUtils.hasKey(deviceStatusCachePrefix + deviceNo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return appDeviceMapper.update(null,
|
||||||
new LambdaUpdateWrapper<AppDevice>()
|
new LambdaUpdateWrapper<AppDevice>()
|
||||||
.set(AppDevice::getStatus, "0")
|
.set(AppDevice::getStatus, "0")
|
||||||
|
.set(AppDevice::getWorkStatus, "0")
|
||||||
.ne(AppDevice::getStatus, "0")
|
.ne(AppDevice::getStatus, "0")
|
||||||
.in(AppDevice::getDeviceNo, offlineDeviceNos)
|
.eq(AppDevice::getDeviceNo, deviceNo)
|
||||||
);
|
) > 0 && finishOfflineWateringLogs(deviceNo);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.warn("[设备离线] 获取设备状态锁被中断 设备编号={}", deviceNo);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (locked && lock.isHeldByCurrentThread()) {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.info("[设备离线] 检测到 {} 台设备离线,已更新数据库状态,设备编号:{}",
|
private boolean finishOfflineWateringLogs(String deviceNo) {
|
||||||
updated, offlineDeviceNos);
|
int count = wateringLogService.finishRunningLogsByDevice(deviceNo, new Date(), "设备掉线");
|
||||||
|
if (count > 0) {
|
||||||
|
log.info("[设备离线] 已结束设备进行中的浇水记录 设备编号={} 记录数={}", deviceNo, count);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package org.dromara.app.task;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.app.domain.AppDevice;
|
||||||
|
import org.dromara.app.domain.AppSchedule;
|
||||||
|
import org.dromara.app.domain.AppScheduleDetail;
|
||||||
|
import org.dromara.app.domain.AppSchedulingDevice;
|
||||||
|
import org.dromara.app.domain.bo.AppWateringLogBo;
|
||||||
|
import org.dromara.app.mapper.AppDeviceMapper;
|
||||||
|
import org.dromara.app.mapper.AppScheduleDetailMapper;
|
||||||
|
import org.dromara.app.mapper.AppScheduleMapper;
|
||||||
|
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
|
||||||
|
import org.dromara.app.service.IAppWateringLogService;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.json.utils.JsonUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.*;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备离线期间排程浇水记录补记任务。
|
||||||
|
* <p>
|
||||||
|
* 设备断网后可能仍按本地排程浇水,但服务端收不到完成上报。本任务按已开启排程生成预估历史记录,
|
||||||
|
* 设备恢复联网后上报同一记录时会在 {@link org.dromara.app.service.IAppWateringLogService#confirmScheduleLog}
|
||||||
|
* 中更新为设备确认记录。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OfflineScheduleWateringLogTask {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
||||||
|
|
||||||
|
private final AppDeviceMapper deviceMapper;
|
||||||
|
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
|
||||||
|
private final AppScheduleMapper scheduleMapper;
|
||||||
|
private final AppScheduleDetailMapper scheduleDetailMapper;
|
||||||
|
private final IAppWateringLogService wateringLogService;
|
||||||
|
|
||||||
|
@Value("${mqtt.offline-schedule-log.enabled:true}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Value("${mqtt.offline-schedule-log.lookback-days:7}")
|
||||||
|
private int lookbackDays;
|
||||||
|
|
||||||
|
@Scheduled(fixedDelayString = "${mqtt.offline-schedule-log.interval-ms:300000}")
|
||||||
|
public void fillOfflineScheduleLogs() {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AppDevice> offlineDevices = deviceMapper.selectList(
|
||||||
|
Wrappers.<AppDevice>lambdaQuery()
|
||||||
|
.eq(AppDevice::getStatus, "0")
|
||||||
|
.isNotNull(AppDevice::getUserId)
|
||||||
|
.select(AppDevice::getDeviceNo, AppDevice::getUserId)
|
||||||
|
);
|
||||||
|
if (offlineDevices.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int saved = 0;
|
||||||
|
for (AppDevice device : offlineDevices) {
|
||||||
|
saved += fillDeviceScheduleLogs(device);
|
||||||
|
}
|
||||||
|
if (saved > 0) {
|
||||||
|
log.info("[离线排程补记] 已补记离线排程浇水记录 数量={}", saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int fillDeviceScheduleLogs(AppDevice device) {
|
||||||
|
List<AppSchedulingDevice> bindings = schedulingDeviceMapper.selectList(
|
||||||
|
Wrappers.<AppSchedulingDevice>lambdaQuery()
|
||||||
|
.eq(AppSchedulingDevice::getDeviceNo, device.getDeviceNo())
|
||||||
|
);
|
||||||
|
if (bindings.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int saved = 0;
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
LocalDate startDate = LocalDate.now().minusDays(Math.max(lookbackDays, 1));
|
||||||
|
for (AppSchedulingDevice binding : bindings) {
|
||||||
|
AppSchedule schedule = scheduleMapper.selectById(binding.getScheduleId());
|
||||||
|
if (schedule == null || !"1".equals(schedule.getStatus())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<AppScheduleDetail> details = scheduleDetailMapper.selectList(
|
||||||
|
Wrappers.<AppScheduleDetail>lambdaQuery()
|
||||||
|
.eq(AppScheduleDetail::getScheduleId, schedule.getId())
|
||||||
|
);
|
||||||
|
for (AppScheduleDetail detail : details) {
|
||||||
|
saved += fillDetailLogs(device, schedule, detail, startDate, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int fillDetailLogs(
|
||||||
|
AppDevice device,
|
||||||
|
AppSchedule schedule,
|
||||||
|
AppScheduleDetail detail,
|
||||||
|
LocalDate startDate,
|
||||||
|
LocalDateTime now
|
||||||
|
) {
|
||||||
|
if (detail == null || "0".equals(detail.getStatus()) || detail.getWeekday() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TimeSlot> timeSlots = parseTimeSlots(detail.getTimeData());
|
||||||
|
if (timeSlots.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int saved = 0;
|
||||||
|
for (LocalDate date = startDate; !date.isAfter(LocalDate.now()); date = date.plusDays(1)) {
|
||||||
|
if (toScheduleWeekday(date.getDayOfWeek()) != detail.getWeekday()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (TimeSlot timeSlot : timeSlots) {
|
||||||
|
AppWateringLogBo logBo = buildLog(device, schedule, detail, date, timeSlot, now);
|
||||||
|
if (logBo != null && Boolean.TRUE.equals(wateringLogService.saveEstimatedScheduleLog(logBo))) {
|
||||||
|
saved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppWateringLogBo buildLog(
|
||||||
|
AppDevice device,
|
||||||
|
AppSchedule schedule,
|
||||||
|
AppScheduleDetail detail,
|
||||||
|
LocalDate date,
|
||||||
|
TimeSlot timeSlot,
|
||||||
|
LocalDateTime now
|
||||||
|
) {
|
||||||
|
if (StringUtils.isBlank(timeSlot.getStartTime()) || timeSlot.getDurationMin() == null || timeSlot.getDurationMin() <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalTime startLocalTime;
|
||||||
|
try {
|
||||||
|
startLocalTime = LocalTime.parse(timeSlot.getStartTime(), TIME_FORMATTER);
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
log.warn("[离线排程补记] 排程开始时间格式错误 设备编号={} 排程ID={} 开始时间={}",
|
||||||
|
device.getDeviceNo(), schedule.getId(), timeSlot.getStartTime());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime startTime = LocalDateTime.of(date, startLocalTime);
|
||||||
|
LocalDateTime endTime = startTime.plusMinutes(timeSlot.getDurationMin());
|
||||||
|
if (endTime.isAfter(now)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppWateringLogBo logBo = new AppWateringLogBo();
|
||||||
|
logBo.setDeviceNo(device.getDeviceNo());
|
||||||
|
logBo.setUserId(device.getUserId());
|
||||||
|
logBo.setScheduleId(schedule.getId());
|
||||||
|
logBo.setStartTime(toDate(startTime));
|
||||||
|
logBo.setEndTime(toDate(endTime));
|
||||||
|
logBo.setDurationMin(String.valueOf(timeSlot.getDurationMin()));
|
||||||
|
logBo.setZones(detail.getZones() == null ? null : String.valueOf(detail.getZones()));
|
||||||
|
logBo.setTriggerType("0");
|
||||||
|
logBo.setCreateTime(new Date());
|
||||||
|
logBo.setRemark("设备离线,按排程自动补记");
|
||||||
|
return logBo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TimeSlot> parseTimeSlots(String timeData) {
|
||||||
|
if (StringUtils.isBlank(timeData)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JsonUtils.parseArray(timeData, TimeSlot.class);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.warn("[离线排程补记] 排程时间数据解析失败 timeData={}", timeData);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int toScheduleWeekday(DayOfWeek dayOfWeek) {
|
||||||
|
return dayOfWeek.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date toDate(LocalDateTime dateTime) {
|
||||||
|
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TimeSlot {
|
||||||
|
private String startTime;
|
||||||
|
private Integer durationMin;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.dromara.system.service.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.baomidou.lock.annotation.Lock4j;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
@@ -152,10 +153,7 @@ public class SysOssConfigServiceImpl implements ISysOssConfigService {
|
|||||||
SysOssConfig info = baseMapper.selectOne(new LambdaQueryWrapper<SysOssConfig>()
|
SysOssConfig info = baseMapper.selectOne(new LambdaQueryWrapper<SysOssConfig>()
|
||||||
.select(SysOssConfig::getOssConfigId, SysOssConfig::getConfigKey)
|
.select(SysOssConfig::getOssConfigId, SysOssConfig::getConfigKey)
|
||||||
.eq(SysOssConfig::getConfigKey, sysOssConfig.getConfigKey()));
|
.eq(SysOssConfig::getConfigKey, sysOssConfig.getConfigKey()));
|
||||||
if (ObjectUtil.isNotNull(info) && info.getOssConfigId() != ossConfigId) {
|
return !ObjectUtil.isNotNull(info) || info.getOssConfigId() == ossConfigId;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,6 +161,7 @@ public class SysOssConfigServiceImpl implements ISysOssConfigService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Lock4j(keys = {"'oss:config:status'"}, acquireTimeout = 5000, expire = 30000)
|
||||||
public int updateOssConfigStatus(SysOssConfigBo bo) {
|
public int updateOssConfigStatus(SysOssConfigBo bo) {
|
||||||
SysOssConfig sysOssConfig = MapstructUtils.convert(bo, SysOssConfig.class);
|
SysOssConfig sysOssConfig = MapstructUtils.convert(bo, SysOssConfig.class);
|
||||||
int row = baseMapper.update(null, new LambdaUpdateWrapper<SysOssConfig>()
|
int row = baseMapper.update(null, new LambdaUpdateWrapper<SysOssConfig>()
|
||||||
|
|||||||
@@ -263,10 +263,12 @@ public class WorkflowGlobalListener implements GlobalListener {
|
|||||||
if (flwTaskService.isTaskEnd(instanceId)) {
|
if (flwTaskService.isTaskEnd(instanceId)) {
|
||||||
String status = BusinessStatusEnum.FINISH.getStatus();
|
String status = BusinessStatusEnum.FINISH.getStatus();
|
||||||
// 更新流程状态为已完成
|
// 更新流程状态为已完成
|
||||||
flwInstanceService.updateStatus(instanceId, status);
|
if (flwInstanceService.updateStatusIfNotFinal(instanceId, status)) {
|
||||||
log.info("流程已结束,状态更新为: {}", status);
|
log.info("流程已结束,状态更新为: {}", status);
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
log.debug("流程实例已被其他处理更新为最终状态,跳过重复完成事件,实例ID: {}", instanceId);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,15 @@ public interface IFlwInstanceService {
|
|||||||
*/
|
*/
|
||||||
void updateStatus(Long instanceId, String status);
|
void updateStatus(Long instanceId, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程未处于最终状态时更新状态
|
||||||
|
*
|
||||||
|
* @param instanceId 实例id
|
||||||
|
* @param status 状态
|
||||||
|
* @return 是否更新成功
|
||||||
|
*/
|
||||||
|
boolean updateStatusIfNotFinal(Long instanceId, String status);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取流程变量
|
* 获取流程变量
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -388,6 +388,15 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
|||||||
flowInstanceMapper.update(wrapper);
|
flowInstanceMapper.update(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateStatusIfNotFinal(Long instanceId, String status) {
|
||||||
|
LambdaUpdateWrapper<FlowInstance> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.set(FlowInstance::getFlowStatus, status);
|
||||||
|
wrapper.eq(FlowInstance::getId, instanceId);
|
||||||
|
wrapper.notIn(FlowInstance::getFlowStatus, BusinessStatusEnum.finishStatus());
|
||||||
|
return flowInstanceMapper.update(wrapper) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取流程变量
|
* 获取流程变量
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user