mqtt对接嵌入式
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ public class AppSchedule extends BaseEntity {
|
||||
*/
|
||||
private String status;
|
||||
|
||||
|
||||
private Date createTime;
|
||||
// createTime 已由父类 BaseEntity 提供,此处不再重复定义
|
||||
// private Date createTime;
|
||||
|
||||
//private List<AppScheduleDetail> details;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -37,10 +37,13 @@ public class AppWateringLog extends BaseEntity {
|
||||
/**
|
||||
* 设备ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
private String deviceNo;
|
||||
|
||||
/**
|
||||
* 命令ID
|
||||
*/
|
||||
private String commandId;
|
||||
|
||||
/**
|
||||
* 关联排程
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -36,10 +36,13 @@ public class AppWateringLogBo extends BaseEntity {
|
||||
/**
|
||||
* 设备ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
private String deviceNo;
|
||||
|
||||
/**
|
||||
* 命令ID
|
||||
*/
|
||||
private String commandId;
|
||||
|
||||
/**
|
||||
* 关联排程
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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 开启
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 关联排程
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 校验并批量删除浇水记录信息
|
||||
*
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.dromara.app.service;
|
||||
|
||||
public interface IDeviceCommandAckHandler {
|
||||
|
||||
void handleAck(String deviceNo, String payload);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.dromara.app.service;
|
||||
|
||||
import org.dromara.app.domain.mqtt.DeviceCommand;
|
||||
|
||||
public interface IDeviceCommandPublisher {
|
||||
|
||||
String send(DeviceCommand command);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user