mqtt对接嵌入式

This commit is contained in:
yuhaiming
2026-06-11 10:07:03 +08:00
parent 1ab8c28c36
commit c7f9df980a
56 changed files with 2698 additions and 905 deletions

View File

@@ -10,10 +10,11 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.domain.bo.*;
import org.dromara.app.domain.mqtt.DeviceCommand;
import org.dromara.app.domain.vo.*;
import org.dromara.app.service.*;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.app.service.impl.AppScheduleServiceImpl;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.model.LoginUser;
@@ -38,7 +39,6 @@ import org.dromara.system.service.ISysUserService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.text.SimpleDateFormat;
import java.util.*;
@@ -55,8 +55,8 @@ public class AppController extends BaseController {
private final AppScheduleServiceImpl appScheduleServiceimpl;
private final IAppSchedulingDeviceService appSchedulingDeviceService;
private final ISysUserService userService;
private final IAppWateringLogService wateringLogService;
private final IAppWateringLogService appWateringLogService;
private final IDeviceCommandService deviceCommandService;
/**
*查询设备
@@ -76,7 +76,6 @@ public class AppController extends BaseController {
* 添加设备
*
*/
//@SaCheckRole("appadmin")
@ApiEncrypt
@PostMapping("/addDevice")
public R<Void> addDevice(@RequestBody String body) {
@@ -91,16 +90,27 @@ public class AppController extends BaseController {
}
}
/**
* 绑定已通过MQTT注册上线的设备
*/
@ApiEncrypt
@PostMapping("/bindDevice")
public R<AppDeviceVo> bindDevice(@RequestBody String body) {
AppDeviceBo appDevice = JsonUtils.parseObject(body, AppDeviceBo.class);
appDevice.setUserId(LoginHelper.getUserId());
return R.ok(appDeviceService.bindRegisteredDevice(appDevice));
}
/**
* 获取设备信息
详细信息
*
* @param id 主键
* @param deviceNo 主键
*/
@GetMapping("/device/{id}")
@GetMapping("/device/{deviceNo}")
public R<AppDeviceVo> getDeviceInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(appDeviceService.queryById(id));
@PathVariable String deviceNo) {
return R.ok(appDeviceService.queryById(deviceNo));
}
@@ -124,10 +134,10 @@ public class AppController extends BaseController {
* 删除设备
*/
@Log(title = "设备信息", businessType = BusinessType.DELETE)
@DeleteMapping("/deleteDevice/{ids}")
@DeleteMapping("/deleteDevice/{deviceNos}")
public R<Void> removeDevice(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(appDeviceService.deleteWithValidByIds(List.of(ids), true));
@PathVariable String[] deviceNos) {
return toAjax(appDeviceService.deleteWithValidByIds(List.of(deviceNos), true));
}
@@ -141,40 +151,17 @@ public class AppController extends BaseController {
public R<Void> switchDevice(@RequestBody String body) {
Map<String,String> map = JsonUtils.parseObject(body, Map.class);
try {
Long id = Long.valueOf(map.get("id"));
String deviceNo = map.get("deviceNo");
String status = map.get("workStatus");
String startTime = map.get("startTime");
int durationMin = Integer.valueOf(map.get("durationMin"));
Date date = new Date();
String durationMinValue = map.get("durationMin");
Integer durationMin = StringUtils.isBlank(durationMinValue) ? null : Integer.valueOf(durationMinValue);
//更新设备工作状态
AppDeviceBo appDeviceBo = new AppDeviceBo();
appDeviceBo.setId(id);
appDeviceBo.setWorkStatus(status);
Boolean flag = appDeviceService.updateByBo(appDeviceBo);
if (flag){
//todo 给设备发送开始浇水信息
Boolean flag = appDeviceService.switchDevice(deviceNo, status, startTime, durationMin);
// 记录信息到历史表
AppWateringLogBo appWateringLog = new AppWateringLogBo();
appWateringLog.setDeviceId(id);
appWateringLog.setUserId(LoginHelper.getUserId());
appWateringLog.setDurationMin(durationMin+"");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
if (StringUtils.isNotEmpty(startTime)){
Date date = sdf.parse(startTime);
appWateringLog.setStartTime(date);
long time = date.getTime()+durationMin*60000L;
Date endTime = new Date(time);
appWateringLog.setEndTime(endTime);
}
appWateringLog.setTriggerType("1");
appWateringLog.setCreateTime(new Date());
appWateringLog.setScheduleId(1L);
wateringLogService.insertByBo(appWateringLog);
}
return toAjax(flag);
} catch (Exception e) {
throw new RuntimeException(e);
return R.fail(e.getMessage());
}
}
@@ -199,42 +186,15 @@ public class AppController extends BaseController {
for (AppDeviceVo appDeviceVo:appDeviceVos){
List<AppSchedulingDeviceVo> appSchedulingDeviceVos = appSchedulingDeviceService.queryByDeviveId(appDeviceVo.getId());
List<AppSchedulingDeviceVo> appSchedulingDeviceVos = appSchedulingDeviceService.findByDeviceNo(appDeviceVo.getDeviceNo());
if(appSchedulingDeviceVos.size() == 0){
appDeviceVoList.add(appDeviceVo);
}
}
retMap.put("deviceList", appDeviceVoList);
// if (appDeviceVoList.size()==0){
// retMap.put("deviceList", appDeviceVos);
// }
return R.ok(retMap);
/** //排程信息
AppScheduleVo appScheduleVo = appScheduleService.queryById(scheduleId);
if (ObjectUtil.isNotNull(appScheduleVo)&&"1".equals(appScheduleVo.getStatus())){
for (AppDeviceVo appDeviceVo:appDeviceVos){
//查询设备日程冲突
List list = appScheduleServiceimpl.checkConflict(appDeviceVo.getId(), appScheduleVo);
if (list.size()==0){
AppDeviceVoList.add(appDeviceVo);
}
// retMap.put("conflict", list);
}
retMap.put("devicelist", AppDeviceVoList);
}else {
retMap.put("devicelist", appDeviceVos);
// retMap.put("conflict", new ArrayList<>());
return R.ok(retMap);
}
return R.ok(retMap); **/
}
/**
*绑定排程跟设备
@@ -242,33 +202,62 @@ public class AppController extends BaseController {
@RepeatSubmit()
@PostMapping("/addScheduleDevice")
public R<Void> addScheduleDevice( @RequestBody String body) {
try {
Map<String, Object> map = JsonUtils.parseObject(body, Map.class);
Long scheduleId = Long.valueOf(map.get("scheduleId").toString());
List<String> deviceIds= (List<String>) map.get("deviceIds");
for (String deviceId:deviceIds){
AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
schedulingDeviceBo.setDeviceId(Long.valueOf(deviceId));
schedulingDeviceBo.setScheduleId(scheduleId);
appSchedulingDeviceService.insertByBo(schedulingDeviceBo);
}
return R.ok();
} catch (RuntimeException e) {
throw new RuntimeException(e);
Map<String, Object> map = JsonUtils.parseObject(body, Map.class);
if (map == null || map.get("scheduleId") == null) {
throw new ServiceException("scheduleId不能为空");
}
Long scheduleId = Long.valueOf(map.get("scheduleId").toString());
List<String> deviceNos = parseDeviceNos(map);
if (deviceNos.isEmpty()) {
throw new ServiceException("deviceNos不能为空");
}
for (String deviceNo:deviceNos){
if (StringUtils.isBlank(deviceNo)) {
continue;
}
AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
schedulingDeviceBo.setDeviceNo(deviceNo);
schedulingDeviceBo.setScheduleId(scheduleId);
appSchedulingDeviceService.insertByBo(schedulingDeviceBo);
}
return R.ok();
}
private List<String> parseDeviceNos(Map<String, Object> map) {
Object value = Optional.ofNullable(map.get("deviceNos"))
.orElseGet(() -> Optional.ofNullable(map.get("deviceNo")).orElse(map.get("deviceIds")));
if (value == null) {
return Collections.emptyList();
}
if (value instanceof Collection<?> collection) {
return collection.stream()
.filter(Objects::nonNull)
.map(Object::toString)
.filter(StringUtils::isNotBlank)
.toList();
}
String text = value.toString();
if (StringUtils.isBlank(text)) {
return Collections.emptyList();
}
return Arrays.stream(text.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.toList();
}
/**
* 删除排程与设备绑定
* @param scheduleId,deviceId
* @param scheduleId,deviceNo
* @return
*/
@Log(title = "排程与设备关联", businessType = BusinessType.DELETE)
@DeleteMapping("/deleteScheduleDevice")
public R<Void> deleteScheduleDevice(@NotEmpty(message = "主键不能为空")
@RequestParam String scheduleId, @RequestParam String deviceId) {
return toAjax(appSchedulingDeviceService.deleteWithValidByschedulingIds2Ids(Long.valueOf(scheduleId),Long.valueOf(deviceId)));
@RequestParam String scheduleId, @RequestParam String deviceNo) {
return toAjax(appSchedulingDeviceService.deleteWithValidByScheduleIdAndDeviceNo(Long.valueOf(scheduleId), deviceNo));
}
@@ -323,8 +312,11 @@ public class AppController extends BaseController {
List<AppSchedulingDeviceVo> appSchedulingDeviceVos = appSchedulingDeviceService.queryList(appSchedulingDeviceBo);
List deviceList = new ArrayList();
for (AppSchedulingDeviceVo appSchedulingDeviceVo:appSchedulingDeviceVos){
AppDeviceVo appDeviceVo = appDeviceService.queryById(appSchedulingDeviceVo.getDeviceId());
deviceList.add(appDeviceVo);
AppDeviceVo appDeviceVo = appDeviceService.queryById(appSchedulingDeviceVo.getDeviceNo());
if (ObjectUtil.isNotNull(appDeviceVo)){
deviceList.add(appDeviceVo);
}
}
List detailList = new ArrayList();
@@ -347,7 +339,7 @@ public class AppController extends BaseController {
appScheduleVo.setDetails(detailList);
appScheduleVo.setDeviceIds(deviceList);
appScheduleVo.setDeviceNos(deviceList);
return R.ok(appScheduleVo);
}
@@ -413,12 +405,11 @@ public class AppController extends BaseController {
List l = new ArrayList<>();
for (AppWateringLogVo vo : appWateringLogVoTableDataInfo.getRows()){
for(AppDeviceVo appDeviceVo:appDeviceVos){
if (appDeviceVo.getId().equals(vo.getDeviceId())){
if (appDeviceVo.getDeviceNo().equals(vo.getDeviceNo())){
vo.setDeviceName(appDeviceVo.getDeviceName());
vo.setDeviceNo(appDeviceVo.getDeviceNo());
}
}
// AppDeviceVo appDeviceVo = appDeviceService.queryById(vo.getDeviceId());
vo.setTriggerType(vo.getTriggerType().equals("0")?"排程":"手动");
l.add(vo);
}
@@ -459,9 +450,9 @@ public class AppController extends BaseController {
//手动浇水次数
Long manualNum = appWateringLogService.queryByTriggerTypeCount(userId, "1");
//总浇水时间
Long allTime = appWateringLogService.queryByTotalWateringTime(userId);
Long allTime = Optional.ofNullable(appWateringLogService.queryByTotalWateringTime(userId)).orElse(0L);
//平均浇水时间
Long averageTime = allTime/count;
Long averageTime = count == null || count == 0 ? 0L : allTime / count;
map.put("count",count);
map.put("monCount",monCount);
@@ -554,4 +545,17 @@ public class AppController extends BaseController {
return toAjax(userService.updateUserPas(bo));
}
@GetMapping("/test")
public R<Void> test (){
DeviceCommand deviceCommand = new DeviceCommand();
String bindDevice = deviceCommandService.sendBindDeviceCommand("01");
// String s = deviceCommandService.sendSwitchCommand("01", "1", "2026-06-10 14:35:31", 20);
return R.ok(bindDevice);
}
}

View File

@@ -66,10 +66,10 @@ public class AppDeviceController extends BaseController {
* @param id 主键
*/
@SaCheckPermission("app:device:query")
@GetMapping("/{id}")
@GetMapping("/{deviceNo}")
public R<AppDeviceVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(appDeviceService.queryById(id));
@PathVariable String deviceNo) {
return R.ok(appDeviceService.queryById(deviceNo));
}
/**
@@ -104,9 +104,9 @@ public class AppDeviceController extends BaseController {
*/
@SaCheckPermission("app:device:remove")
@Log(title = "设备信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
@DeleteMapping("/{deviceNos}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(appDeviceService.deleteWithValidByIds(List.of(ids), true));
@PathVariable String[] deviceNos) {
return toAjax(appDeviceService.deleteWithValidByIds(List.of(deviceNos), true));
}
}

View File

@@ -1,21 +1,15 @@
package org.dromara.app.domain;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
import java.util.Date;
/**
* 设备信息
对象 app_device
*
* @author Lion Li
* @date 2026-04-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("app_device")
@@ -24,92 +18,36 @@ public class AppDevice extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
*
*/
@TableId(value = "id")
private Long id;
/**
* 设备编号
*/
@TableId(value = "device_no", type = IdType.INPUT)
private String deviceNo;
/**
* 用户id
*/
private String bindTokenHash;
private Long userId;
/**
* 设备名称
*/
private String deviceName;
/**
* 设备图片
*/
private String deviceImg;
/**
* 状态 1-工作中 0-离线 2-待机 3-故障
*/
private String status;
/**
* 工作状态 1-工作中 0-休息
*/
private String workStatus;
/**
* 电量
*/
private String powerLevel;
/**
* 电量更新时间
*/
private Date powerLevelUpdatatime;
/**
* WiFi名称
*/
private String wifiName;
/**
* WiFi密码
*/
private String wifiPassword;
/**
* 设备型号
*/
private String deviceEm;
/**
* 设备序列号
*/
private String deviceSn;
/**
* 固件版本
*/
private String fwVer;
/**
* mac地址
*/
private String macAddress;
/**
* 删除标志0代表存在 1代表删除
*/
//@TableLogic
//private String delFlag;
/**
* 到期时间
*/
private Date expirationTime;
}

View File

@@ -46,8 +46,8 @@ public class AppSchedule extends BaseEntity {
*/
private String status;
private Date createTime;
// createTime 已由父类 BaseEntity 提供,此处不再重复定义
// private Date createTime;
//private List<AppScheduleDetail> details;

View File

@@ -1,18 +1,12 @@
package org.dromara.app.domain;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
/**
* 排程与设备关联对象 app_scheduling_device
*
* @author Lion Li
* @date 2026-04-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("app_scheduling_device")
@@ -21,15 +15,7 @@ public class AppSchedulingDevice extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 排程id
*/
private Long scheduleId;
/**
* 设备id
*/
private Long deviceId;
private String deviceNo;
}

View File

@@ -37,10 +37,13 @@ public class AppWateringLog extends BaseEntity {
/**
* 设备ID
*/
private Long deviceId;
private String deviceNo;
/**
* 命令ID
*/
private String commandId;
/**
* 关联排程
*/

View File

@@ -1,42 +1,29 @@
package org.dromara.app.domain.bo;
import org.dromara.app.domain.AppDevice;
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 lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.dromara.app.domain.AppDevice;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Date;
/**
* 设备信息
业务对象 app_device
*
* @author Lion Li
* @date 2026-04-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = AppDevice.class, reverseConvertGenerate = false)
public class AppDeviceBo extends BaseEntity {
/**
*
*/
@NotNull(message = "不能为空", groups = { EditGroup.class })
private Long id;
/**
* 设备编号
*/
@NotBlank(message = "设备编号不能为空", groups = { AddGroup.class, EditGroup.class })
private String deviceNo;
/**
* 用户id
*/
private String bindToken;
private String bindTokenHash;
@NotNull(message = "用户id不能为空", groups = { AddGroup.class, EditGroup.class })
private Long userId;
@@ -51,11 +38,12 @@ public class AppDeviceBo extends BaseEntity {
private String deviceImg;
/**
* 状态 1-工作中 0-离线 2-待机 3-故障
* 状态 1-在线 0-离线 2-到期 3-故障
*/
private String status;
/**
* 工作状态 1-工作中 0-休息
* 工作状态 0-休息 1-工作
*/
private String workStatus;
@@ -103,6 +91,4 @@ public class AppDeviceBo extends BaseEntity {
* 到期时间
*/
private Date expirationTime;
}

View File

@@ -1,22 +1,13 @@
package org.dromara.app.domain.bo;
import cn.hutool.json.JSON;
import org.dromara.app.domain.AppSchedule;
import org.dromara.app.domain.AppScheduleDetail;
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 lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import java.time.LocalTime;
import java.util.Date;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 日程排列业务对象 app_schedule
@@ -45,7 +36,7 @@ public class AppScheduleBo extends BaseEntity {
*/
private String name;
private List<Long> deviceIds;
private List<String> deviceNos;
/**

View File

@@ -1,36 +1,24 @@
package org.dromara.app.domain.bo;
import org.dromara.app.domain.AppSchedulingDevice;
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 lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 排程与设备关联业务对象 app_scheduling_device
*
* @author Lion Li
* @date 2026-04-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = AppSchedulingDevice.class, reverseConvertGenerate = false)
public class AppSchedulingDeviceBo extends BaseEntity {
/**
* 排程id
*/
@NotNull(message = "排程id不能为空", groups = { AddGroup.class, EditGroup.class })
private Long scheduleId;
/**
* 设备id
*/
@NotNull(message = "设备id不能为空", groups = { AddGroup.class, EditGroup.class })
private Long deviceId;
@NotBlank(message = "设备编号不能为空", groups = { AddGroup.class, EditGroup.class })
private String deviceNo;
}

View File

@@ -36,10 +36,13 @@ public class AppWateringLogBo extends BaseEntity {
/**
* 设备ID
*/
private Long deviceId;
private String deviceNo;
/**
* 命令ID
*/
private String commandId;
/**
* 关联排程
*/

View File

@@ -0,0 +1,25 @@
package org.dromara.app.domain.mqtt;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Data
public class DeviceCommand implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String commandId;
private String deviceNo;
private String commandType;
private String topic;
private Map<String, Object> payload = new HashMap<>();
private int retryCount;
private long createdAt;
private long lastSentAt;
private long nextRetryAt;
}

View File

@@ -0,0 +1,18 @@
package org.dromara.app.domain.mqtt;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class DeviceCommandAck implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String commandId;
private String deviceNo;
private String status;
private String message;
}

View File

@@ -1,28 +1,15 @@
package org.dromara.app.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.dromara.app.domain.AppDevice;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
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 lombok.Data;
import org.dromara.app.domain.AppDevice;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 设备信息
视图对象 app_device
*
* @author Lion Li
* @date 2026-04-25
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = AppDevice.class)
@@ -31,98 +18,47 @@ public class AppDeviceVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
*
*/
@ExcelProperty(value = "")
private Long id;
/**
* 设备编号
*/
@ExcelProperty(value = "设备编号")
private String deviceNo;
/**
* 用户id
*/
@ExcelProperty(value = "用户id")
private Long userId;
/**
* 设备名称
*/
@ExcelProperty(value = "设备名称")
private String deviceName;
/**
* 设备图片
*/
@ExcelProperty(value = "设备图片")
private String deviceImg;
/**
* 状态 1-工作中 0-离线 2-待机 3-故障
*/
@ExcelProperty(value = "状态 1-工作中 0-离线 2-待机 3-故障")
@ExcelProperty(value = "状态")
private String status;
/**
* 工作状态 1-工作中 0-休息
*/
private String workStatus;
/**
* 电量
*/
@ExcelProperty(value = "电量")
private String powerLevel;
/**
* 电量更新时间
*/
@ExcelProperty(value = "电量更新时间")
private Date powerLevelUpdatatime;
/**
* WiFi名称
*/
@ExcelProperty(value = "WiFi名称")
private String wifiName;
/**
* WiFi密码
*/
@ExcelProperty(value = "WiFi密码")
private String wifiPassword;
/**
* 设备型号
*/
@ExcelProperty(value = "设备型号")
private String deviceEm;
/**
* 设备序列号
*/
@ExcelProperty(value = "设备序列号")
private String deviceSn;
/**
* 固件版本
*/
@ExcelProperty(value = "固件版本")
private String fwVer;
/**
* mac地址
*/
@ExcelProperty(value = "mac地址")
private String macAddress;
/**
* 到期时间
*/
@ExcelProperty(value = "到期时间")
private Date expirationTime;
}

View File

@@ -1,20 +1,14 @@
package org.dromara.app.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotEmpty;
import org.dromara.app.domain.AppSchedule;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import org.dromara.app.domain.bo.AppScheduleBo;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@@ -53,7 +47,7 @@ public class AppScheduleVo implements Serializable {
/**
* 设备ids
*/
private List deviceIds;
private List deviceNos;
/**
* 状态 0-关闭 1 开启
*/

View File

@@ -1,26 +1,14 @@
package org.dromara.app.domain.vo;
import org.dromara.app.domain.AppSchedule;
import org.dromara.app.domain.AppSchedulingDevice;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
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 lombok.Data;
import org.dromara.app.domain.AppSchedulingDevice;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 排程与设备关联视图对象 app_scheduling_device
*
* @author Lion Li
* @date 2026-04-25
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = AppSchedulingDevice.class)
@@ -29,18 +17,11 @@ public class AppSchedulingDeviceVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 排程id
*/
@ExcelProperty(value = "排程id")
private Long scheduleId;
/**
* 设备id
*/
@ExcelProperty(value = "设备id")
private Long deviceId;
@ExcelProperty(value = "设备编号")
private String deviceNo;
private AppScheduleVo schedule;
}

View File

@@ -46,9 +46,14 @@ public class AppWateringLogVo implements Serializable {
* 设备ID
*/
@ExcelProperty(value = "设备ID")
private Long deviceId;
private String deviceNo;
/**
* 命令ID
*/
@ExcelProperty(value = "命令ID")
private String commandId;
/**
* 关联排程
*/

View File

@@ -0,0 +1,35 @@
package org.dromara.app.handler;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IDeviceCommandAckHandler;
import org.springframework.stereotype.Component;
import java.util.regex.Pattern;
/**
* 设备命令 ACK 处理器 — 匹配 /{deviceNo}/publish/ack。
* 委托给 {@link IDeviceCommandAckHandler}(实际由 water-common-mqtt 模块的 MqttCommandAckService 实现)。
*/
@Slf4j
@Component
public class DeviceCommandAckHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/ack$");
private final IDeviceCommandAckHandler delegate;
public DeviceCommandAckHandler(IDeviceCommandAckHandler delegate) {
this.delegate = delegate;
}
@Override
public Pattern topicPattern() {
return PATTERN;
}
@Override
public void handle(String deviceNo, String payload) {
delegate.handleAck(deviceNo, payload);
}
}

View File

@@ -2,25 +2,44 @@ package org.dromara.app.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 设备数据上报处理器 — 匹配 /{deviceNo}/publish/power
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceDataHandler {
public class DeviceDataHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/power$");
private final IAppDeviceService appDeviceService;
public void handle(String deviceId, String payload) {
@Override
public Pattern topicPattern() {
return PATTERN;
}
@Override
public void handle(String deviceNo, String payload) {
try {
// DeviceDataDTO dto = JsonUtils.parseObject(payload, DeviceDataDTO.class);
// dto.setDeviceId(deviceId);
// dto.setReportTime(LocalDateTime.now());
// deviceDataService.saveDeviceData(dto);
log.info("[MQTT] 设备数据入库 deviceId={}", deviceId);
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
AppDeviceBo appDeviceBo = new AppDeviceBo();
appDeviceBo.setDeviceNo(deviceNo);
appDeviceBo.setPowerLevel(dto.get("powerLevel").toString());
appDeviceBo.setPowerLevelUpdatatime(new Date());
appDeviceService.updateByBo(appDeviceBo);
log.info("[MQTT] 设备电量更新 deviceNo={} payload={}", deviceNo, payload);
} catch (Exception e) {
log.error("[MQTT] 设备数据解析失败 deviceId={} payload={}", deviceId, payload, e);
log.error("[MQTT] 设备电量更新失败 deviceNo={} payload={}", deviceNo, payload, e);
}
}
}

View File

@@ -0,0 +1,70 @@
package org.dromara.app.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Device registration handler, matches /{deviceNo}/publish/register.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceRegisterHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/register$");
private final IAppDeviceService appDeviceService;
@Override
public Pattern topicPattern() {
return PATTERN;
}
@Override
public void handle(String deviceNo, String payload) {
try {
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
AppDeviceBo device = buildRegisterDevice(deviceNo, dto);
appDeviceService.registerByMqtt(device);
log.info("[MQTT] 设备注册 deviceNo={} payload={}", deviceNo, payload);
} catch (Exception e) {
log.error("[MQTT] 设备注册失败 deviceNo={} payload={}", deviceNo, payload, e);
}
}
private AppDeviceBo buildRegisterDevice(String topicDeviceNo, Map<String, Object> dto) {
AppDeviceBo device = new AppDeviceBo();
device.setDeviceNo(topicDeviceNo);
if (dto == null) {
return device;
}
device.setDeviceName(valueAsString(dto.get("deviceName")));
device.setPowerLevel(valueAsString(dto.get("powerLevel")));
device.setDeviceEm(valueAsString(dto.get("deviceEm")));
device.setDeviceSn(valueAsString(dto.get("deviceSn")));
device.setMacAddress(firstNotBlank(dto.get("deviceMac"), dto.get("macAddress")));
device.setFwVer(firstNotBlank(dto.get("version"), dto.get("fwVer")));
return device;
}
private String firstNotBlank(Object first, Object second) {
String firstText = valueAsString(first);
if (firstText != null && !firstText.isBlank()) {
return firstText;
}
String secondText = valueAsString(second);
return secondText != null && !secondText.isBlank() ? secondText : null;
}
private String valueAsString(Object value) {
return value == null ? null : String.valueOf(value);
}
}

View File

@@ -1,29 +0,0 @@
package org.dromara.app.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.stereotype.Component;
import java.util.Map;
//设备在线状态
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceStatusHandler {
public void handle(String deviceNo, String payload) {
try {
Map<String,Object> dto = JsonUtils.parseObject(payload, Map.class);
// 更新设备在线状态
// log.info("[MQTT] 设备状态更新 deviceId={} status={}", deviceId, dto.getStatus());
} catch (Exception e) {
log.error("[MQTT] 设备状态处理失败 deviceId={}", deviceNo, e);
}
}
}

View File

@@ -2,25 +2,35 @@ package org.dromara.app.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.regex.Pattern;
//设备推送异常告警入库
/**
* 设备异常告警处理器 — 匹配 /{deviceNo}/publish/error
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ErromesHandler {
public class ErromesHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/error$");
@Override
public Pattern topicPattern() {
return PATTERN;
}
@Override
public void handle(String deviceNo, String payload) {
try {
Map<String,Object> dto = JsonUtils.parseObject(payload, Map.class);
log.info("[MQTT] 设备状态发送异常 deviceNo={}", deviceNo);
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
log.info("[MQTT] 设备异常告警 deviceNo={} payload={}", deviceNo, dto);
} catch (Exception e) {
log.error("[MQTT] 设备状态处理失败 deviceNo={}", deviceNo, e);
log.error("[MQTT] 设备异常处理失败 deviceNo={}", deviceNo, e);
}
}
}

View File

@@ -0,0 +1,150 @@
package org.dromara.app.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 按键浇水完成处理器,匹配 /{deviceNo}/publish/finish/key。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class KeyFinishHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/finish/key$");
private final IAppWateringLogService appWateringLogService;
private final IAppDeviceService appDeviceService;
@Override
public Pattern topicPattern() {
return PATTERN;
}
@Override
public void handle(String deviceNo, String payload) {
try {
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
appWateringLogService.insertByBo(logBo);
log.info("[MQTT] 按键浇水完成上报已保存 deviceNo={} payload={}", deviceNo, payload);
} catch (Exception e) {
log.error("[MQTT] 按键浇水完成上报处理失败 deviceNo={} payload={}", deviceNo, payload, e);
}
}
private AppWateringLogBo buildWateringLog(String topicDeviceNo, Map<String, Object> dto) {
String payloadDeviceNo = valueAsString(dto.get("deviceNo"));
if (StringUtils.isNotBlank(payloadDeviceNo) && !topicDeviceNo.equals(payloadDeviceNo)) {
log.warn("[MQTT] 按键浇水完成上报设备编号不一致 topicDeviceNo={} payloadDeviceNo={}", topicDeviceNo, payloadDeviceNo);
}
Date receivedAt = new Date();
Date startTime = parseStartTime(valueAsString(dto.get("startTime")));
Date plannedEndTime = resolvePlannedEndTime(
startTime,
parseOptionalTime(valueAsString(dto.get("endTime"))),
valueAsInteger(dto.get("durationMin"))
);
Date endTime = resolveActualEndTime(receivedAt, plannedEndTime);
Integer durationMin = calculateDurationMin(startTime, endTime);
AppWateringLogBo logBo = new AppWateringLogBo();
logBo.setDeviceNo(topicDeviceNo);
logBo.setCommandId(valueAsString(dto.get("commandId")));
logBo.setUserId(queryUserId(topicDeviceNo));
logBo.setScheduleId(0L);
logBo.setStartTime(startTime);
logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin));
logBo.setTriggerType("1");
if ("schedule".equals(valueAsString(dto.get("triggerON")))){
logBo.setTriggerType("0");
}
logBo.setCreateTime(receivedAt);
logBo.setEndTime(endTime);
return logBo;
}
private Long queryUserId(String deviceNo) {
AppDeviceVo device = appDeviceService.queryById(deviceNo);
return device == null ? null : device.getUserId();
}
private Date parseStartTime(String startTime) {
if (StringUtils.isBlank(startTime)) {
return new Date();
}
for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"}) {
try {
SimpleDateFormat format = new SimpleDateFormat(pattern);
format.setLenient(false);
return format.parse(startTime);
} catch (ParseException ignored) {
// Try the next supported format.
}
}
throw new IllegalArgumentException("Unsupported startTime format: " + startTime);
}
private Date parseOptionalTime(String time) {
if (StringUtils.isBlank(time)) {
return null;
}
return parseStartTime(time);
}
private Date resolvePlannedEndTime(Date startTime, Date payloadEndTime, Integer durationMin) {
if (startTime != null && durationMin != null && durationMin > 0) {
return new Date(startTime.getTime() + durationMin * 60000L);
}
return payloadEndTime;
}
private Date resolveActualEndTime(Date receivedAt, Date plannedEndTime) {
if (plannedEndTime == null) {
return receivedAt;
}
return receivedAt.before(plannedEndTime) ? receivedAt : plannedEndTime;
}
private Integer calculateDurationMin(Date startTime, Date endTime) {
if (startTime == null || endTime == null) {
return null;
}
long diffMillis = endTime.getTime() - startTime.getTime();
if (diffMillis < 0) {
log.warn("[MQTT] 按键浇水完成上报结束时间早于开始时间 startTime={} endTime={}", startTime, endTime);
return 0;
}
return (int) (diffMillis / 60000L);
}
private String valueAsString(Object value) {
return value == null ? null : String.valueOf(value);
}
private Integer valueAsInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.intValue();
}
String text = String.valueOf(value);
return StringUtils.isBlank(text) ? null : Integer.valueOf(text);
}
}

View File

@@ -0,0 +1,172 @@
package org.dromara.app.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 排程浇水完成处理器,匹配 /{deviceNo}/publish/finish/schedule。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ScheduleFinishHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/finish/schedule$");
private final IAppWateringLogService appWateringLogService;
private final IAppDeviceService appDeviceService;
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
@Override
public Pattern topicPattern() {
return PATTERN;
}
@Override
public void handle(String deviceNo, String payload) {
try {
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
appWateringLogService.insertByBo(logBo);
log.info("[MQTT] 浇水排程上报 saved deviceNo={} payload={}", deviceNo, payload);
} catch (Exception e) {
log.error("[MQTT] 浇水排程上报 failed deviceNo={} payload={}", deviceNo, payload, e);
}
}
private AppWateringLogBo buildWateringLog(String topicDeviceNo, Map<String, Object> dto) {
String payloadDeviceNo = valueAsString(dto.get("deviceNo"));
if (StringUtils.isNotBlank(payloadDeviceNo) && !topicDeviceNo.equals(payloadDeviceNo)) {
log.warn("[MQTT] 排程完成上报设备编号不一致 topicDeviceNo={} payloadDeviceNo={}", topicDeviceNo, payloadDeviceNo);
}
Date receivedAt = new Date();
Date startTime = parseStartTime(valueAsString(dto.get("startTime")));
Date plannedEndTime = resolvePlannedEndTime(
startTime,
parseOptionalTime(valueAsString(dto.get("endTime"))),
valueAsInteger(dto.get("durationMin"))
);
Date endTime = resolveActualEndTime(receivedAt, plannedEndTime);
Integer durationMin = calculateDurationMin(startTime, endTime);
AppWateringLogBo logBo = new AppWateringLogBo();
logBo.setDeviceNo(topicDeviceNo);
logBo.setCommandId(valueAsString(dto.get("commandId")));
logBo.setUserId(queryUserId(topicDeviceNo));
logBo.setScheduleId(queryScheduleId(topicDeviceNo));
logBo.setStartTime(startTime);
logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin));
logBo.setTriggerType("0");
if ("mqtt on".equals(valueAsString(dto.get("triggerON")))){
logBo.setTriggerType("1");
}
logBo.setCreateTime(receivedAt);
logBo.setEndTime(endTime);
return logBo;
}
private Long queryUserId(String deviceNo) {
AppDeviceVo device = appDeviceService.queryById(deviceNo);
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] 排程完成上报未找到设备绑定的排程 deviceNo={}", deviceNo);
return 0L;
}
if (schedulingDevices.size() > 1) {
log.warn("[MQTT] 排程完成上报匹配到多个排程 deviceNo={} count={}", deviceNo, schedulingDevices.size());
}
return schedulingDevices.get(0).getScheduleId();
}
private Date parseStartTime(String startTime) {
if (StringUtils.isBlank(startTime)) {
return new Date();
}
for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"}) {
try {
SimpleDateFormat format = new SimpleDateFormat(pattern);
format.setLenient(false);
return format.parse(startTime);
} catch (ParseException ignored) {
// Try the next supported format.
}
}
throw new IllegalArgumentException("Unsupported startTime format: " + startTime);
}
private Date parseOptionalTime(String time) {
if (StringUtils.isBlank(time)) {
return null;
}
return parseStartTime(time);
}
private Date resolvePlannedEndTime(Date startTime, Date payloadEndTime, Integer durationMin) {
if (startTime != null && durationMin != null && durationMin > 0) {
return new Date(startTime.getTime() + durationMin * 60000L);
}
return payloadEndTime;
}
private Date resolveActualEndTime(Date receivedAt, Date plannedEndTime) {
if (plannedEndTime == null) {
return receivedAt;
}
return receivedAt.before(plannedEndTime) ? receivedAt : plannedEndTime;
}
private Integer calculateDurationMin(Date startTime, Date endTime) {
if (startTime == null || endTime == null) {
return null;
}
long diffMillis = endTime.getTime() - startTime.getTime();
if (diffMillis < 0) {
log.warn("[MQTT] 排程完成上报结束时间早于开始时间 startTime={} endTime={}", startTime, endTime);
return 0;
}
return (int) (diffMillis / 60000L);
}
private String valueAsString(Object value) {
return value == null ? null : String.valueOf(value);
}
private Integer valueAsInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.intValue();
}
String text = String.valueOf(value);
return StringUtils.isBlank(text) ? null : Integer.valueOf(text);
}
}

View File

@@ -5,6 +5,8 @@ import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 设备信息
@@ -15,4 +17,36 @@ Mapper接口
*/
public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo> {
@Update("""
<script>
update app_device
<set>
user_id = #{userId},
bind_token_hash = null,
<if test="deviceName != null and deviceName != ''">
device_name = #{deviceName},
</if>
<if test="status != null and status != ''">
status = #{status},
</if>
</set>
where device_no = #{deviceNo}
and (user_id is null or user_id = #{userId})
</script>
""")
int bindIfAvailable(@Param("deviceNo") String deviceNo,
@Param("userId") Long userId,
@Param("deviceName") String deviceName,
@Param("status") String status);
@Update("""
update app_device
set work_status = #{workStatus}
where device_no = #{deviceNo}
and user_id = #{userId}
""")
int updateWorkStatusByUser(@Param("deviceNo") String deviceNo,
@Param("userId") Long userId,
@Param("workStatus") String workStatus);
}

View File

@@ -1,42 +1,49 @@
package org.dromara.app.mqtt;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.handler.DeviceDataHandler;
import org.dromara.app.handler.DeviceStatusHandler;
import org.dromara.app.handler.ErromesHandler;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.regex.Matcher;
/**
* MQTT 上行消息分发器(策略模式)。
* <p>
* 启动时自动收集所有 {@link MqttTopicHandler} Bean按 Topic 正则匹配分发。
* 新增 Topic 只需新增 Handler 实现类,零改动此处。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MqttMessageDispatcher {
private final DeviceDataHandler dataHandler;
private final DeviceStatusHandler statusHandler;
private final ErromesHandler erroHandler;
// 路由规则:匹配 topic 分发到对应 Handler
private static final String DATA_PATTERN = "/water/([^/]+)/data";
private static final String STATUS_PATTERN = "/water/([^/]+)/status";
private static final String ERROMES_PATTERN = "/water/([^/]+)/erromes";
private final List<MqttTopicHandler> handlers;
public void dispatch(String topic, String payload) {
log.debug("[MQTT] 收到消息 topic={} 内容: {}", topic,payload);
if (topic.matches(DATA_PATTERN)) {
String deviceNo = extract(topic, DATA_PATTERN);
dataHandler.handle(deviceNo, payload);
} else if (topic.matches(STATUS_PATTERN)) {
String deviceNo = extract(topic, STATUS_PATTERN);
statusHandler.handle(deviceNo, payload);
}else if (topic.matches(ERROMES_PATTERN)) {
String deviceNo = extract(topic, STATUS_PATTERN);
erroHandler.handle(deviceNo, payload);
} else {
log.warn("[MQTT] 未知 topic: {} 内容: {}", topic,payload);
public MqttMessageDispatcher(List<MqttTopicHandler> handlers) {
this.handlers = handlers;
log.info("[MQTT] dispatcher initialized with {} handlers", handlers.size());
for (MqttTopicHandler handler : handlers) {
log.info("[MQTT] └ registered: {} -> {}", handler.getClass().getSimpleName(), handler.topicPattern());
}
}
private String extract(String topic, String pattern) {
return topic.replaceAll(pattern, "$1");
/**
* 分发消息到匹配的处理器。
*
* @param topic MQTT Topic
* @param payload 消息体
*/
public void dispatch(String topic, String payload) {
log.debug("[MQTT] received topic={} payload={}", topic, payload);
for (MqttTopicHandler handler : handlers) {
Matcher matcher = handler.topicPattern().matcher(topic);
if (matcher.matches()) {
String deviceNo = matcher.group(1);
handler.handle(deviceNo, payload);
return;
}
}
log.warn("[MQTT] no handler for topic: {} payload={}", topic, payload);
}
}

View File

@@ -0,0 +1,28 @@
package org.dromara.app.mqtt;
import java.util.regex.Pattern;
/**
* MQTT 消息 Topic 处理器策略接口。
* <p>
* 所有上行消息处理器实现此接口,由 {@link MqttMessageDispatcher} 自动发现并路由。
* 新增 Topic 只需新增实现类并注册为 Spring Bean无需修改 Dispatcher。
*
* @author water team
*/
public interface MqttTopicHandler {
/**
* 返回该处理器匹配的 Topic 正则。
* 必须包含一个捕获组用于提取 deviceNo。
*/
Pattern topicPattern();
/**
* 处理匹配到的消息。
*
* @param deviceNo 从 Topic 正则中提取的设备编号
* @param payload 消息体JSON
*/
void handle(String deviceNo, String payload);
}

View File

@@ -27,7 +27,7 @@ public interface IAppDeviceService {
* @return 设备信息
*/
AppDeviceVo queryById(Long id);
AppDeviceVo queryById(String deviceNo);
/**
* 分页查询设备信息
@@ -70,13 +70,31 @@ public interface IAppDeviceService {
*/
Boolean updateByBo(AppDeviceBo bo);
/**
* MQTT设备注册或刷新设备基础信息
*
* @param bo 设备信息
* @return 是否保存成功
*/
Boolean registerByMqtt(AppDeviceBo bo);
/**
* 将已注册设备绑定到当前登录用户
*
* @param bo 设备信息
* @return 设备信息
*/
AppDeviceVo bindRegisteredDevice(AppDeviceBo bo);
Boolean switchDevice(String deviceNo, String workStatus, String startTime, Integer durationMin);
/**
* 校验并批量删除设备信息
信息
*
* @param ids 待删除的主键集合
* @param deviceNos 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
Boolean deleteWithValidByIds(Collection<String> deviceNos, Boolean isValid);
}

View File

@@ -1,82 +1,31 @@
package org.dromara.app.service;
import jakarta.validation.constraints.NotEmpty;
import org.dromara.app.domain.vo.AppSchedulingDeviceVo;
import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.app.domain.vo.AppSchedulingDeviceVo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import java.util.Collection;
import java.util.List;
/**
* 排程与设备关联Service接口
*
* @author Lion Li
* @date 2026-04-25
*/
public interface IAppSchedulingDeviceService {
/**
* 查询排程与设备关联
*
* @param schedulingId 主键
* @return 排程与设备关联
*/
AppSchedulingDeviceVo queryById(Long schedulingId);
/**
* 分页查询排程与设备关联列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 排程与设备关联分页列表
*/
TableDataInfo<AppSchedulingDeviceVo> queryPageList(AppSchedulingDeviceBo bo, PageQuery pageQuery);
/**
* 查询符合条件的排程与设备关联列表
*
* @param bo 查询条件
* @return 排程与设备关联列表
*/
List<AppSchedulingDeviceVo> queryList(AppSchedulingDeviceBo bo);
/**
* 新增排程与设备关联
*
* @param bo 排程与设备关联
* @return 是否新增成功
*/
Boolean insertByBo(AppSchedulingDeviceBo bo);
/**
* 修改排程与设备关联
*
* @param bo 排程与设备关联
* @return 是否修改成功
*/
Boolean updateByBo(AppSchedulingDeviceBo bo);
/**
* 校验并批量删除排程与设备关联信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 查找某一设备的所有日程
* @param deviceId
* @return
*/
List<AppSchedulingDeviceVo> findByDeviceId(Long deviceId);
List<AppSchedulingDeviceVo> findByDeviceNo(String deviceNo);
Boolean deleteWithValidByschedulingIds2Ids(@NotEmpty(message = "主键不能为空") Long schedulingId, Long deviceId);
Boolean deleteWithValidByScheduleIdAndDeviceNo(@NotEmpty(message = "主键不能为空") Long scheduleId, String deviceNo);
AppSchedulingDeviceVo queryById2DeviveId(Long scheduleId, Long id);
List<AppSchedulingDeviceVo> queryByDeviveId(Long id);
AppSchedulingDeviceVo queryByScheduleIdAndDeviceNo(Long scheduleId, String deviceNo);
}

View File

@@ -6,6 +6,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
@@ -57,6 +58,16 @@ public interface IAppWateringLogService {
*/
Boolean updateByBo(AppWateringLogBo bo);
/**
* 结束当前手动浇水记录。
*
* @param deviceNo 设备编号
* @param userId 用户ID
* @param endTime 结束时间
* @return 是否更新成功
*/
Boolean finishManualLog(String deviceNo, Long userId, Date endTime);
/**
* 校验并批量删除浇水记录信息
*

View File

@@ -0,0 +1,6 @@
package org.dromara.app.service;
public interface IDeviceCommandAckHandler {
void handleAck(String deviceNo, String payload);
}

View File

@@ -0,0 +1,8 @@
package org.dromara.app.service;
import org.dromara.app.domain.mqtt.DeviceCommand;
public interface IDeviceCommandPublisher {
String send(DeviceCommand command);
}

View File

@@ -0,0 +1,45 @@
package org.dromara.app.service;
import org.dromara.app.domain.mqtt.DeviceCommand;
import java.util.Map;
/**
* 设备命令下发业务层接口。
* <p>
* 封装命令构造、设备校验、下发与日志记录,避免各业务方直接拼装 {@link DeviceCommand}。
*
* @author water team
*/
public interface IDeviceCommandService {
/**
* 通用命令下发。
*
* @param command 命令对象commandId 为空时自动生成)
* @return commandId
*/
String sendCommand(DeviceCommand command);
/**
* 下发设备开关/浇水命令。
*
* @param deviceNo 设备编号
* @param workStatus 目标工作状态
* @param startTime 开始时间(可为 null
* @param durationMin 持续时间(分钟)
* @return commandId
*/
String sendSwitchCommand(String deviceNo, String workStatus, String startTime, Integer durationMin);
/**
* 下发自定义命令。
*
* @param deviceNo 设备编号
* @param commandType 命令类型
* @param extra 额外参数
* @return commandId
*/
String sendCustomCommand(String deviceNo, String commandType, Map<String, Object> extra);
String sendBindDeviceCommand(String deviceNo);
}

View File

@@ -1,39 +1,40 @@
package org.dromara.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
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 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.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Service;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.AppWateringLog;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.mapper.AppDeviceMapper;
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
import org.dromara.app.mapper.AppWateringLogMapper;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.app.service.IDeviceCommandService;
import org.dromara.common.core.exception.ServiceException;
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.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Collection;
/**
* 设备信息
Service业务层处理
*
* @author Lion Li
* @date 2026-04-25
*/
@Slf4j
@RequiredArgsConstructor
@Service
@@ -41,29 +42,19 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
private final AppDeviceMapper baseMapper;
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
private final AppWateringLogMapper wateringLogMapper;
/**
* 查询设备信息
// @Lazy 打破 AppDeviceServiceImpl ↔ DeviceCommandServiceImpl 双向构造器循环
@Lazy
@Autowired
private IDeviceCommandService deviceCommandService;
private final IAppWateringLogService wateringLogService;
*
* @param id 主键
* @return 设备信息
*/
@Override
public AppDeviceVo queryById(Long id){
return baseMapper.selectVoById(id);
public AppDeviceVo queryById(String deviceNo) {
return baseMapper.selectVoById(deviceNo);
}
/**
* 分页查询设备信息
列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 设备信息
分页列表
*/
@Override
public TableDataInfo<AppDeviceVo> queryPageList(AppDeviceBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<AppDevice> lqw = buildQueryWrapper(bo);
@@ -71,14 +62,6 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
return TableDataInfo.build(result);
}
/**
* 查询符合条件的设备信息
列表
*
* @param bo 查询条件
* @return 设备信息
列表
*/
@Override
public List<AppDeviceVo> queryList(AppDeviceBo bo) {
LambdaQueryWrapper<AppDevice> lqw = buildQueryWrapper(bo);
@@ -88,7 +71,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
private LambdaQueryWrapper<AppDevice> buildQueryWrapper(AppDeviceBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<AppDevice> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(AppDevice::getId);
lqw.orderByAsc(AppDevice::getDeviceNo);
lqw.eq(StringUtils.isNotBlank(bo.getDeviceNo()), AppDevice::getDeviceNo, bo.getDeviceNo());
lqw.eq(bo.getUserId() != null, AppDevice::getUserId, bo.getUserId());
lqw.like(StringUtils.isNotBlank(bo.getDeviceName()), AppDevice::getDeviceName, bo.getDeviceName());
@@ -106,67 +89,127 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
return lqw;
}
/**
* 新增设备信息
*
* @param bo 设备信息
* @return 是否新增成功
*/
@Override
public Boolean insertByBo(AppDeviceBo bo) {
// fillBindTokenHash(bo);
AppDevice add = MapstructUtils.convert(bo, AppDevice.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
return baseMapper.insert(add) > 0;
}
/**
* 修改设备信息
*
* @param bo 设备信息
* @return 是否修改成功
*/
@Override
public Boolean updateByBo(AppDeviceBo bo) {
// fillBindTokenHash(bo);
AppDevice update = MapstructUtils.convert(bo, AppDevice.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(AppDevice entity){
//TODO 做一些数据校验,如唯一约束
@Override
public Boolean registerByMqtt(AppDeviceBo bo) {
if (StringUtils.isBlank(bo.getDeviceNo())) {
throw new ServiceException("设备编号不能为空");
}
AppDevice exists = baseMapper.selectById(bo.getDeviceNo());
AppDevice device = MapstructUtils.convert(bo, AppDevice.class);
device.setPowerLevelUpdatatime(new Date());
// applyRegisterBindToken(device, bo, exists);
if (StringUtils.isBlank(device.getStatus())) {
device.setStatus("2");
device.setWorkStatus("2");
}
if (exists == null) {
try {
return baseMapper.insert(device) > 0;
} catch (org.springframework.dao.DuplicateKeyException ignored) {
return baseMapper.updateById(device) > 0;
}
}
return baseMapper.updateById(device) > 0;
}
@Override
public AppDeviceVo bindRegisteredDevice(AppDeviceBo bo) {
if (StringUtils.isBlank(bo.getDeviceNo())) {
throw new ServiceException("设备编号不能为空");
}
Long userId = LoginHelper.getUserId();
AppDevice exists = baseMapper.selectById(bo.getDeviceNo());
if (exists == null) {
throw new ServiceException("设备未上线注册,请先完成配网");
}
if (exists.getUserId() != null && !exists.getUserId().equals(userId)) {
throw new ServiceException("设备已被其他用户绑定");
}
// validBindToken(bo.getBindToken(), exists);
String status = StringUtils.isBlank(bo.getStatus()) ? exists.getStatus() : bo.getStatus();
String deviceName = StringUtils.isBlank(bo.getDeviceName()) ? exists.getDeviceName() : bo.getDeviceName();
int rows = baseMapper.bindIfAvailable(exists.getDeviceNo(), userId, deviceName, status);
if (rows <= 0) {
AppDevice current = baseMapper.selectById(bo.getDeviceNo());
if (current == null) {
throw new ServiceException("设备未上线注册,请先完成配网");
}
if (current.getUserId() != null && !current.getUserId().equals(userId)) {
throw new ServiceException("设备已被其他用户绑定");
}
throw new ServiceException("设备绑定失败,请重试");
}
//下发设备已绑定状态
deviceCommandService.sendBindDeviceCommand(exists.getDeviceNo());
return baseMapper.selectVoById(exists.getDeviceNo());
}
@Override
@Transactional(rollbackFor = Exception.class)
@Lock4j(keys = {"'device:command:' + #deviceNo"}, acquireTimeout = 5000, expire = 30000)
public Boolean switchDevice(String deviceNo, String workStatus, String startTime, Integer durationMin) {
if (StringUtils.isBlank(deviceNo)) {
throw new ServiceException("设备编号不能为空");
}
if (StringUtils.isBlank(workStatus)) {
throw new ServiceException("设备工作状态不能为空");
}
if ("1".equals(workStatus) && (durationMin == null || durationMin <= 0)) {
throw new ServiceException("持续时间必须大于0");
}
Long userId = LoginHelper.getUserId();
int rows = baseMapper.updateWorkStatusByUser(deviceNo, userId, workStatus);
if (rows <= 0) {
throw new ServiceException("设备不存在或无权操作");
}
AppDeviceVo device = baseMapper.selectVoById(deviceNo);
if (ObjectUtil.isNotNull(device) && StringUtils.isNotBlank(device.getDeviceNo())) {
// 通过业务层统一下发命令(含浇水日志记录)
String commandId = deviceCommandService.sendSwitchCommand(deviceNo, workStatus, startTime, durationMin);
log.info("[DEVICE] switch command sent deviceNo={} commandId={}", deviceNo, commandId);
}
return true;
}
private void validEntityBeforeSave(AppDevice entity) {
if (StringUtils.isBlank(entity.getDeviceNo())) {
throw new ServiceException("设备编号不能为空");
}
}
/**
* 校验并批量删除设备信息
信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
@Override
@Transactional
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
boolean flag = baseMapper.deleteByIds(ids) > 0;
if (flag){
for (Long id :ids){
schedulingDeviceMapper.delete(new QueryWrapper<AppSchedulingDevice>().eq("device_id",id));
}
public Boolean deleteWithValidByIds(Collection<String> deviceNos, Boolean isValid) {
boolean flag = baseMapper.deleteByIds(deviceNos) > 0;
if (flag) {
schedulingDeviceMapper.delete(
new QueryWrapper<AppSchedulingDevice>().in("device_no", deviceNos)
);
wateringLogMapper.delete(new QueryWrapper<AppWateringLog>().in("device_no", deviceNos));
}
return flag;
}

View File

@@ -1,43 +1,36 @@
package org.dromara.app.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppSchedule;
import org.dromara.app.domain.AppScheduleDetail;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.domain.vo.AppScheduleDetailVo;
import org.dromara.app.domain.vo.AppSchedulingDeviceVo;
import org.dromara.app.domain.bo.AppScheduleBo;
import org.dromara.app.domain.vo.AppScheduleVo;
import org.dromara.app.mapper.AppScheduleDetailMapper;
import org.dromara.app.mapper.AppScheduleMapper;
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
import org.dromara.app.service.IAppScheduleService;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
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.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Service;
import org.dromara.app.domain.bo.AppScheduleBo;
import org.dromara.app.domain.vo.AppScheduleVo;
import org.dromara.app.domain.AppScheduleDetail;
import org.dromara.app.domain.AppSchedule;
import org.dromara.app.mapper.AppScheduleMapper;
import org.dromara.app.service.IAppScheduleService;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 日程排列Service业务层处理
*
* @author Lion Li
* @date 2026-04-25
*/
@Slf4j
@RequiredArgsConstructor
@Service
@@ -47,24 +40,11 @@ public class AppScheduleServiceImpl implements IAppScheduleService {
private final AppScheduleDetailMapper scheduleDetailMapper;
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
/**
* 查询日程排列
*
* @param id 主键
* @return 日程排列
*/
@Override
public AppScheduleVo queryById(Long id){
public AppScheduleVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
/**
* 分页查询日程排列列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 日程排列分页列表
*/
@Override
public TableDataInfo<AppScheduleVo> queryPageList(AppScheduleBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<AppSchedule> lqw = buildQueryWrapper(bo);
@@ -72,12 +52,6 @@ public class AppScheduleServiceImpl implements IAppScheduleService {
return TableDataInfo.build(result);
}
/**
* 查询符合条件的日程排列列表
*
* @param bo 查询条件
* @return 日程排列列表
*/
@Override
public List<AppScheduleVo> queryList(AppScheduleBo bo) {
LambdaQueryWrapper<AppSchedule> lqw = buildQueryWrapper(bo);
@@ -94,197 +68,143 @@ public class AppScheduleServiceImpl implements IAppScheduleService {
return lqw;
}
/**
* 新增日程排列
*
* @param bo 日程排列
* @return 是否新增成功
*/
@Override
@Transactional
public AppScheduleVo insertByBo(AppScheduleBo bo) {
AppSchedule add = MapstructUtils.convert(bo, AppSchedule.class);
add.setUserId(LoginHelper.getUserId());
validEntityBeforeSave(add);
//创建排程
boolean flag = baseMapper.insert(add) > 0;
if (flag){
AppScheduleVo vo = MapstructUtils.convert(add, AppScheduleVo.class);
List list = new ArrayList<>();
//创建详情
for (AppScheduleBo.AppScheduleDetail item:bo.getDetails()){
AppScheduleDetail detail = new AppScheduleDetail();
detail.setScheduleId(add.getId());
detail.setWeekday(item.getWeekday());
if (ObjectUtil.isNotNull(item.getTimeData())){
detail.setTimeData(JsonUtils.toJsonString(item.getTimeData()));
// detail.setEndTime(item.getStartTime().plusMinutes(item.getDurationMin()));
}
detail.setZones(item.getZones());
detail.setTriggerType(item.getTriggerType());
detail.setStatus(item.getStatus());
scheduleDetailMapper.insert(detail);
List timeSlots = new ArrayList();
for (AppScheduleBo.TimeSlot timeSlot:item.getTimeData()){
timeSlots.add(timeSlot);
}
Map map = new HashMap();
map.put("id",detail.getId() );
map.put("weekday",detail.getWeekday() );
map.put("timeData",timeSlots );
map.put("triggerType",detail.getTriggerType() );
map.put("status",detail.getStatus() );
map.put("createTime",detail.getCreateTime() );
list.add(map);
}
vo.setDetails(list);
return vo;
boolean flag = baseMapper.insert(add) > 0;
if (!flag) {
return new AppScheduleVo();
}
return new AppScheduleVo();
AppScheduleVo vo = MapstructUtils.convert(add, AppScheduleVo.class);
vo.setDetails(saveDetails(add.getId(), bo.getDetails()));
return vo;
}
@Override
public Boolean updateStatusByBo(AppScheduleBo bo) {
AppSchedule update = MapstructUtils.convert(bo, AppSchedule.class);
return baseMapper.updateById(update)>0;
return baseMapper.updateById(update) > 0;
}
/**
* 修改日程排列
*
* @param bo 日程排列
* @return 是否修改成功
*/
@Override
@Transactional
public Map<String,Object> updateByBo(AppScheduleBo bo) {
public Map<String, Object> updateByBo(AppScheduleBo bo) {
AppSchedule update = MapstructUtils.convert(bo, AppSchedule.class);
validEntityBeforeSave(update);
Map<String,Object> repMap = new HashMap();
Boolean flag = baseMapper.updateById(update)>0;
Map<String, Object> repMap = new HashMap<>();
Boolean flag = baseMapper.updateById(update) > 0;
repMap.put("flag", flag);
repMap.put("list", new ArrayList<>());
repMap.put("appSchedule", "");
if (flag){
AppScheduleVo appScheduleVo = baseMapper.selectVoById(update.getId());
//修改详情
List detailList = new ArrayList<>();
for (AppScheduleBo.AppScheduleDetail item:bo.getDetails()){
AppScheduleDetail detail = new AppScheduleDetail();
detail.setWeekday(item.getWeekday());
detail.setId(item.getId());
detail.setTimeData(JsonUtils.toJsonString(item.getTimeData()));
detail.setZones(item.getZones());
detail.setTriggerType(item.getTriggerType());
detail.setStatus(item.getStatus());
scheduleDetailMapper.updateById(detail);
List timeSlots = new ArrayList();
for (AppScheduleBo.TimeSlot timeSlot:item.getTimeData()){
timeSlots.add(timeSlot);
}
Map map = new HashMap();
map.put("id",detail.getId() );
map.put("weekday",detail.getWeekday() );
map.put("timeData",timeSlots );
map.put("triggerType",detail.getTriggerType() );
map.put("status",detail.getStatus() );
map.put("createTime",detail.getCreateTime() );
detailList.add(map);
}
appScheduleVo.setDetails(detailList);
repMap.put("appSchedule", appScheduleVo);
if (!flag) {
return repMap;
}
AppScheduleVo appScheduleVo = baseMapper.selectVoById(update.getId());
appScheduleVo.setDetails(updateDetails(bo.getDetails()));
repMap.put("appSchedule", appScheduleVo);
return repMap;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(AppSchedule entity){
//TODO 做一些数据校验,如唯一约束
private void validEntityBeforeSave(AppSchedule entity) {
// TODO business validation
}
/**
* 校验并批量删除日程排列信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
@Override
@Transactional
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
boolean flag = false;
try {
flag = baseMapper.deleteByIds(ids) > 0;
if (flag){
for (Long id :ids){
schedulingDeviceMapper.delete(new QueryWrapper<AppSchedulingDevice>().eq("schedule_id",id ));
scheduleDetailMapper.delete(new QueryWrapper<AppScheduleDetail>().eq("schedule_id", id));
}
boolean flag = baseMapper.deleteByIds(ids) > 0;
if (flag) {
for (Long id : ids) {
schedulingDeviceMapper.delete(new QueryWrapper<AppSchedulingDevice>().eq("schedule_id", id));
scheduleDetailMapper.delete(new QueryWrapper<AppScheduleDetail>().eq("schedule_id", id));
}
return flag;
} catch (Exception e) {
throw new RuntimeException(e);
}
return flag;
}
/**
* 检查新日程与设备已绑定日程是否存在时间冲突
*/
public List checkConflict(Long deviceId, AppScheduleVo newSchedule) {
// 1. 获取该设备所有已绑定的日程
List<AppSchedulingDevice> existingBindings = schedulingDeviceMapper.selectList(new QueryWrapper<AppSchedulingDevice>().eq("device_id", deviceId));
public List checkConflict(String deviceNo, AppScheduleVo newSchedule) {
List<AppSchedulingDevice> existingBindings = schedulingDeviceMapper.selectList(
new QueryWrapper<AppSchedulingDevice>().eq("device_no", deviceNo)
);
List conflicts = new ArrayList<>();
//新日程的详情列
List<AppScheduleDetail> scheduleDetails = scheduleDetailMapper.selectList(new QueryWrapper<AppScheduleDetail>().eq("schedule_id", newSchedule.getId()));
// 2. 对新日程的每一天,与已存在的每一天逐一比较
List<AppScheduleDetail> scheduleDetails = scheduleDetailMapper.selectList(
new QueryWrapper<AppScheduleDetail>().eq("schedule_id", newSchedule.getId())
);
for (AppScheduleDetail newDay : scheduleDetails) {
//此设备日程
for (AppSchedulingDevice existing : existingBindings){
for (AppSchedulingDevice existing : existingBindings) {
AppScheduleVo appScheduleVo = baseMapper.selectVoById(existing.getScheduleId());
if (ObjectUtil.isNotNull(appScheduleVo) && !"0".equals(appScheduleVo.getStatus())){
List<AppScheduleDetail> details = scheduleDetailMapper.selectList(new QueryWrapper<AppScheduleDetail>().eq("schedule_id", existing.getScheduleId()));
if (ObjectUtil.isNotNull(appScheduleVo) && !"0".equals(appScheduleVo.getStatus())) {
List<AppScheduleDetail> details = scheduleDetailMapper.selectList(
new QueryWrapper<AppScheduleDetail>().eq("schedule_id", existing.getScheduleId())
);
for (AppScheduleDetail existingDay:details ){
// 3. 同一天才检查时间重叠
if (newDay.getWeekday() == existingDay.getWeekday()) {
// if (timeOverlap(newDay, existingDay)) {
//
// Map map = new HashMap();
// map.put("scheduleName", appScheduleVo.getName());
// map.put("newDay", newDay);
// map.put("existingDay", existingDay);
// conflicts.add(map);
// }
for (AppScheduleDetail existingDay : details) {
if (newDay.getWeekday().equals(existingDay.getWeekday())) {
// TODO time overlap validation
}
}
}
}
}
return conflicts; // 空列表 = 无冲突
return conflicts;
}
/**
* 判断两段时间是否重叠
* 重叠条件start1 < end2 && start2 < end1
*/
// public boolean timeOverlap(AppScheduleDetail a, AppScheduleDetail b) {
// LocalTime bendTime = b.getStartTime().plusMinutes(b.getDurationMin());
// LocalTime aendTime = a.getStartTime().plusMinutes(a.getDurationMin());
//
// return a.getStartTime().isBefore(bendTime)
// && b.getStartTime().isBefore(aendTime);
// }
private List saveDetails(Long scheduleId, List<AppScheduleBo.AppScheduleDetail> details) {
List list = new ArrayList<>();
if (details == null) {
return list;
}
for (AppScheduleBo.AppScheduleDetail item : details) {
AppScheduleDetail detail = new AppScheduleDetail();
detail.setScheduleId(scheduleId);
detail.setWeekday(item.getWeekday());
if (ObjectUtil.isNotNull(item.getTimeData())) {
detail.setTimeData(JsonUtils.toJsonString(item.getTimeData()));
}
detail.setZones(item.getZones());
detail.setTriggerType(item.getTriggerType());
detail.setStatus(item.getStatus());
scheduleDetailMapper.insert(detail);
list.add(toDetailMap(detail, item.getTimeData()));
}
return list;
}
private List updateDetails(List<AppScheduleBo.AppScheduleDetail> details) {
List detailList = new ArrayList<>();
if (details == null) {
return detailList;
}
for (AppScheduleBo.AppScheduleDetail item : details) {
AppScheduleDetail detail = new AppScheduleDetail();
detail.setId(item.getId());
detail.setWeekday(item.getWeekday());
detail.setTimeData(JsonUtils.toJsonString(item.getTimeData()));
detail.setZones(item.getZones());
detail.setTriggerType(item.getTriggerType());
detail.setStatus(item.getStatus());
scheduleDetailMapper.updateById(detail);
detailList.add(toDetailMap(detail, item.getTimeData()));
}
return detailList;
}
private Map<String, Object> toDetailMap(AppScheduleDetail detail, List<AppScheduleBo.TimeSlot> timeData) {
Map<String, Object> map = new HashMap<>();
map.put("id", detail.getId());
map.put("weekday", detail.getWeekday());
map.put("timeData", timeData == null ? new ArrayList<>() : new ArrayList<>(timeData));
map.put("triggerType", detail.getTriggerType());
map.put("status", detail.getStatus());
map.put("createTime", detail.getCreateTime());
return map;
}
}

View File

@@ -1,31 +1,27 @@
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.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.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.app.domain.vo.AppSchedulingDeviceVo;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
import org.dromara.app.service.IAppSchedulingDeviceService;
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.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Collection;
/**
* 排程与设备关联Service业务层处理
*
* @author Lion Li
* @date 2026-04-25
*/
@Slf4j
@RequiredArgsConstructor
@Service
@@ -33,24 +29,11 @@ public class AppSchedulingDeviceServiceImpl implements IAppSchedulingDeviceServi
private final AppSchedulingDeviceMapper baseMapper;
/**
* 查询排程与设备关联
*
* @param schedulingId 主键
* @return 排程与设备关联
*/
@Override
public AppSchedulingDeviceVo queryById(Long schedulingId){
public AppSchedulingDeviceVo queryById(Long schedulingId) {
return baseMapper.selectVoById(schedulingId);
}
/**
* 分页查询排程与设备关联列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 排程与设备关联分页列表
*/
@Override
public TableDataInfo<AppSchedulingDeviceVo> queryPageList(AppSchedulingDeviceBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<AppSchedulingDevice> lqw = buildQueryWrapper(bo);
@@ -58,12 +41,6 @@ public class AppSchedulingDeviceServiceImpl implements IAppSchedulingDeviceServi
return TableDataInfo.build(result);
}
/**
* 查询符合条件的排程与设备关联列表
*
* @param bo 查询条件
* @return 排程与设备关联列表
*/
@Override
public List<AppSchedulingDeviceVo> queryList(AppSchedulingDeviceBo bo) {
LambdaQueryWrapper<AppSchedulingDevice> lqw = buildQueryWrapper(bo);
@@ -74,33 +51,24 @@ public class AppSchedulingDeviceServiceImpl implements IAppSchedulingDeviceServi
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<AppSchedulingDevice> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getScheduleId() != null, AppSchedulingDevice::getScheduleId, bo.getScheduleId());
lqw.eq(bo.getDeviceId() != null, AppSchedulingDevice::getDeviceId, bo.getDeviceId());
lqw.eq(StringUtils.isNotBlank(bo.getDeviceNo()), AppSchedulingDevice::getDeviceNo, bo.getDeviceNo());
return lqw;
}
/**
* 新增排程与设备关联
*
* @param bo 排程与设备关联
* @return 是否新增成功
*/
@Override
public Boolean insertByBo(AppSchedulingDeviceBo bo) {
if (queryByScheduleIdAndDeviceNo(bo.getScheduleId(), bo.getDeviceNo()) != null) {
return true;
}
AppSchedulingDevice add = MapstructUtils.convert(bo, AppSchedulingDevice.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setScheduleId(add.getScheduleId());
try {
return baseMapper.insert(add) > 0;
} catch (DuplicateKeyException ignored) {
return true;
}
return flag;
}
/**
* 修改排程与设备关联
*
* @param bo 排程与设备关联
* @return 是否修改成功
*/
@Override
public Boolean updateByBo(AppSchedulingDeviceBo bo) {
AppSchedulingDevice update = MapstructUtils.convert(bo, AppSchedulingDevice.class);
@@ -108,46 +76,34 @@ public class AppSchedulingDeviceServiceImpl implements IAppSchedulingDeviceServi
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(AppSchedulingDevice entity){
//TODO 做一些数据校验,如唯一约束
private void validEntityBeforeSave(AppSchedulingDevice entity) {
// TODO 业务校验
}
/**
* 校验并批量删除排程与设备关联信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
if (isValid) {
// TODO 业务校验
}
return baseMapper.deleteByIds(ids) > 0;
}
@Override
public AppSchedulingDeviceVo queryById2DeviveId(Long scheduleId, Long id) {
return baseMapper.selectVoOne(new QueryWrapper<AppSchedulingDevice>().eq("schedule_id",scheduleId).eq("device_id",id));
public AppSchedulingDeviceVo queryByScheduleIdAndDeviceNo(Long scheduleId, String deviceNo) {
return baseMapper.selectVoOne(new QueryWrapper<AppSchedulingDevice>()
.eq("schedule_id", scheduleId)
.eq("device_no", deviceNo));
}
@Override
public List<AppSchedulingDeviceVo> queryByDeviveId(Long id) {
return baseMapper.selectVoList(new QueryWrapper<AppSchedulingDevice>().eq("device_id",id));
public Boolean deleteWithValidByScheduleIdAndDeviceNo(Long scheduleId, String deviceNo) {
return baseMapper.delete(new QueryWrapper<AppSchedulingDevice>()
.eq("schedule_id", scheduleId)
.eq("device_no", deviceNo)) > 0;
}
@Override
public Boolean deleteWithValidByschedulingIds2Ids(Long schedulingId, Long deviceId) {
return baseMapper.delete(new QueryWrapper<AppSchedulingDevice>().eq("schedule_id",schedulingId).eq("device_id",deviceId)) > 0;
}
@Override
public List<AppSchedulingDeviceVo> findByDeviceId(Long deviceId) {
return baseMapper.selectVoList(new QueryWrapper<AppSchedulingDevice>().eq("device_id",deviceId));
public List<AppSchedulingDeviceVo> findByDeviceNo(String deviceNo) {
return baseMapper.selectVoList(new QueryWrapper<AppSchedulingDevice>().eq("device_no", deviceNo));
}
}

View File

@@ -18,6 +18,7 @@ import org.dromara.app.domain.AppWateringLog;
import org.dromara.app.mapper.AppWateringLogMapper;
import org.dromara.app.service.IAppWateringLogService;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Collection;
@@ -77,7 +78,8 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
LambdaQueryWrapper<AppWateringLog> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(AppWateringLog::getId);
lqw.eq(bo.getUserId() != null, AppWateringLog::getUserId, bo.getUserId());
lqw.eq(bo.getDeviceId() != null, AppWateringLog::getDeviceId, bo.getDeviceId());
lqw.eq(StringUtils.isNotBlank(bo.getDeviceNo()), AppWateringLog::getDeviceNo, bo.getDeviceNo());
lqw.eq(StringUtils.isNotBlank(bo.getCommandId()), AppWateringLog::getCommandId, bo.getCommandId());
lqw.eq(bo.getScheduleId() != null, AppWateringLog::getScheduleId, bo.getScheduleId());
lqw.eq(bo.getStartTime() != null, AppWateringLog::getStartTime, bo.getStartTime());
lqw.eq(bo.getEndTime() != null, AppWateringLog::getEndTime, bo.getEndTime());
@@ -100,6 +102,9 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
@Override
public Boolean insertByBo(AppWateringLogBo bo) {
AppWateringLog add = MapstructUtils.convert(bo, AppWateringLog.class);
if (add.getScheduleId() == null) {
add.setScheduleId(0L);
}
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
@@ -121,6 +126,36 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
return baseMapper.updateById(update) > 0;
}
@Override
public Boolean finishManualLog(String deviceNo, Long userId, Date endTime) {
if (StringUtils.isBlank(deviceNo) || userId == null || endTime == null) {
return false;
}
AppWateringLog activeLog = baseMapper.selectOne(
Wrappers.<AppWateringLog>lambdaQuery()
.eq(AppWateringLog::getDeviceNo, deviceNo)
.eq(AppWateringLog::getUserId, userId)
.eq(AppWateringLog::getTriggerType, "1")
.isNull(AppWateringLog::getEndTime)
.orderByDesc(AppWateringLog::getStartTime)
.orderByDesc(AppWateringLog::getCreateTime)
.orderByDesc(AppWateringLog::getId)
.last("limit 1")
);
if (activeLog == null) {
return false;
}
activeLog.setEndTime(endTime);
if (activeLog.getStartTime() != null) {
long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime();
long durationMin = Math.max(durationMillis, 0L) / 60000L;
activeLog.setDurationMin(String.valueOf(durationMin));
}
return baseMapper.updateById(activeLog) > 0;
}
/**
* 保存前的数据校验
*/

View File

@@ -0,0 +1,208 @@
package org.dromara.app.service.impl;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.mqtt.DeviceCommand;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.app.service.IDeviceCommandPublisher;
import org.dromara.app.service.IDeviceCommandService;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 设备命令下发业务实现。
* <p>
* 负责命令构造、设备校验、下发生效,以及手动操作的浇水日志记录。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceCommandServiceImpl implements IDeviceCommandService {
// 字段注入 + @Lazy 打破DeviceMqttCommandPublisher → MqttClientManager → MqttMessageDispatcher
// → DeviceRegisterHandler → AppDeviceServiceImpl → DeviceCommandServiceImpl → IDeviceCommandPublisher 循环
// commandPublisher 仅在 sendCommand/sendSwitchCommand 运行时调用,构造阶段无需立即解析
@Lazy
@Autowired
private IDeviceCommandPublisher commandPublisher;
private final IAppDeviceService deviceService;
private final IAppWateringLogService wateringLogService;
// ========================= 通用下发 =========================
@Override
public String sendCommand(DeviceCommand command) {
validate(command);
String commandId = commandPublisher.send(command);
log.info("[CMD] command sent deviceNo={} commandType={} commandId={}",
command.getDeviceNo(), command.getCommandType(), commandId);
return commandId;
}
// ========================= 类型化下发 =========================
@Override
public String sendSwitchCommand(String deviceNo, String workStatus, String startTime, Integer durationMin) {
assertDeviceExists(deviceNo);
assertSwitchStatus(workStatus);
if ("1".equals(workStatus)) {
assertDurationPositive(durationMin);
}
DeviceCommand command = new DeviceCommand();
command.setDeviceNo(deviceNo);
command.setCommandType("switchDevice");
command.getPayload().put("deviceNo", deviceNo);
command.getPayload().put("cmd", workStatus);
command.getPayload().put("startTime", startTime);
if (durationMin != null) {
command.getPayload().put("durationMin", durationMin);
}
String commandId = commandPublisher.send(command);
log.info("[CMD] switch command sent deviceNo={} workStatus={} durationMin={} commandId={}",
deviceNo, workStatus, durationMin, commandId);
Long userId = LoginHelper.getUserId();
if ("1".equals(workStatus)) {
AppWateringLogBo logBo = buildManualStartLog(deviceNo, userId, commandId, startTime, durationMin);
wateringLogService.insertByBo(logBo);
} else {
boolean updated = wateringLogService.finishManualLog(deviceNo, userId, new Date());
if (!updated) {
log.warn("[CMD] no active manual watering log found deviceNo={} userId={} stopCommandId={}",
deviceNo, userId, commandId);
}
}
return commandId;
}
@Override
public String sendCustomCommand(String deviceNo, String commandType, Map<String, Object> extra) {
assertDeviceExists(deviceNo);
DeviceCommand command = new DeviceCommand();
command.setDeviceNo(deviceNo);
command.setCommandType(commandType);
if (extra != null) {
command.getPayload().putAll(extra);
}
return sendCommand(command);
}
@Override
public String sendBindDeviceCommand(String deviceNo) {
AppDeviceVo device = deviceService.queryById(deviceNo);
if (ObjectUtil.isNull(device)) {
throw new ServiceException("设备不存在:" + deviceNo);
}
DeviceCommand command = new DeviceCommand();
command.setDeviceNo(deviceNo);
command.setCommandType("bindDevice");
Map<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("bindStatus", true);
objectObjectHashMap.put("deviceNo", "01");
objectObjectHashMap.put("cmd", -1);
command.getPayload().putAll(objectObjectHashMap);
return sendCommand(command);
}
// ========================= 校验 =========================
private void validate(DeviceCommand command) {
if (StringUtils.isBlank(command.getDeviceNo())) {
throw new ServiceException("设备编号不能为空");
}
if (StringUtils.isBlank(command.getCommandType())) {
throw new ServiceException("命令类型不能为空");
}
assertDeviceExists(command.getDeviceNo());
}
private void assertDeviceExists(String deviceNo) {
AppDeviceVo device = deviceService.queryById(deviceNo);
if (ObjectUtil.isNull(device)) {
throw new ServiceException("设备不存在:" + deviceNo);
}
if (device.getUserId() == null) {
throw new ServiceException("设备未绑定用户,无法下发命令:" + deviceNo);
}
}
private void assertSwitchStatus(String workStatus) {
if (!"0".equals(workStatus) && !"1".equals(workStatus)) {
throw new ServiceException("设备工作状态只能为0或1");
}
}
private void assertDurationPositive(Integer durationMin) {
if (durationMin == null || durationMin <= 0) {
throw new ServiceException("持续时间必须大于0");
}
}
private AppWateringLogBo buildManualStartLog(
String deviceNo,
Long userId,
String commandId,
String startTime,
Integer durationMin
) {
AppWateringLogBo logBo = new AppWateringLogBo();
logBo.setDeviceNo(deviceNo);
logBo.setCommandId(commandId);
logBo.setUserId(userId);
logBo.setScheduleId(0L);
logBo.setDurationMin(String.valueOf(durationMin));
logBo.setTriggerType("1"); // 手动
logBo.setCreateTime(new Date());
if (StringUtils.isNotEmpty(startTime)) {
logBo.setStartTime(parseStartTime(startTime));
} else {
logBo.setStartTime(new Date());
}
return logBo;
}
private Date parseStartTime(String startTime) {
if (startTime.matches("^\\d{2}:\\d{2}$")) {
try {
Date parsedTime = new SimpleDateFormat("HH:mm").parse(startTime);
Calendar parsed = Calendar.getInstance();
parsed.setTime(parsedTime);
Calendar today = Calendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, parsed.get(Calendar.HOUR_OF_DAY));
today.set(Calendar.MINUTE, parsed.get(Calendar.MINUTE));
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);
return today.getTime();
} catch (ParseException e) {
throw new ServiceException("开始时间格式错误,期望 HH:mm 或 yyyy-MM-dd HH:mm:ss");
}
}
try {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(startTime);
} catch (ParseException e) {
throw new ServiceException("开始时间格式错误,期望 HH:mm 或 yyyy-MM-dd HH:mm:ss");
}
}
}