浇水bug修改
This commit is contained in:
@@ -47,6 +47,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
@@ -66,6 +67,7 @@ public class AppController extends BaseController {
|
||||
private final IAppWateringLogService appWateringLogService;
|
||||
private final IDeviceCommandService deviceCommandService;
|
||||
private final ISysOssService ossService;
|
||||
private final IAppVersionService appVersionService;
|
||||
|
||||
/**
|
||||
*查询设备
|
||||
@@ -288,21 +290,12 @@ public class AppController extends BaseController {
|
||||
}
|
||||
|
||||
private Map<String, Object> buildScheduleBindPayload(Long scheduleId, String deviceNo) {
|
||||
// AppScheduleVo schedule = getOwnedSchedule(scheduleId);
|
||||
|
||||
List<AppScheduleDetailVo> details = appScheduleDetailService.queryByScheduleIdByStatus(scheduleId);
|
||||
|
||||
// Map<String, Object> schedulePayload = new HashMap<>();
|
||||
// schedulePayload.put("id", schedule.getId());
|
||||
// schedulePayload.put("name", schedule.getName());
|
||||
// schedulePayload.put("status", schedule.getStatus());
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
// payload.put("cmd", -1);
|
||||
payload.put("deviceNo", deviceNo);
|
||||
// payload.put("schedule", schedulePayload);
|
||||
payload.put("details", toScheduleDetailPayload(details));
|
||||
return payload;
|
||||
// return toScheduleDetailPayload(details);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> toScheduleDetailPayload(List<AppScheduleDetailVo> details) {
|
||||
@@ -317,13 +310,16 @@ public class AppController extends BaseController {
|
||||
timeSlots.add(timeSlot);
|
||||
}
|
||||
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("id", detail.getId());
|
||||
item.put("weekday", detail.getWeekday());
|
||||
item.put("timeData", timeSlots);
|
||||
item.put("triggerType", detail.getTriggerType());
|
||||
item.put("status", detail.getStatus());
|
||||
detailPayload.add(item);
|
||||
if (detail.getStatus().equals("1")){
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
// item.put("id", detail.getId());
|
||||
item.put("weekday", detail.getWeekday());
|
||||
item.put("timeData", timeSlots);
|
||||
item.put("triggerType", detail.getTriggerType());
|
||||
item.put("status", detail.getStatus());
|
||||
detailPayload.add(item);
|
||||
}
|
||||
|
||||
}
|
||||
return detailPayload;
|
||||
}
|
||||
@@ -470,7 +466,7 @@ public class AppController extends BaseController {
|
||||
Boolean updated = appScheduleService.updateStatusByBo(bo);
|
||||
if (updated) {
|
||||
// 向所有绑定设备下发排程状态变更
|
||||
notifyBoundDevicesScheduleUpdate(bo.getId());
|
||||
notifyBoundDevicesScheduleStatusChange(bo.getId(), bo.getStatus());
|
||||
}
|
||||
return toAjax(updated);
|
||||
} catch (Exception e) {
|
||||
@@ -560,6 +556,39 @@ public class AppController extends BaseController {
|
||||
return appWateringLogVoTableDataInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询设备最新进行中的浇水记录
|
||||
*/
|
||||
@GetMapping("/latestWaterLog")
|
||||
public R<Map<String, Object>> latestWaterLog(@RequestParam String deviceNo) {
|
||||
try {
|
||||
assertDeviceOwned(deviceNo);
|
||||
|
||||
AppWateringLogBo bo = new AppWateringLogBo();
|
||||
bo.setUserId(LoginHelper.getUserId());
|
||||
bo.setDeviceNo(deviceNo);
|
||||
bo.setStatus("1");
|
||||
List<AppWateringLogVo> logs = appWateringLogService.queryList(bo);
|
||||
if (logs == null || logs.isEmpty()) {
|
||||
return R.ok(null);
|
||||
}
|
||||
|
||||
AppWateringLogVo latest = logs.stream()
|
||||
.max(Comparator.comparing(AppWateringLogVo::getId, Comparator.nullsFirst(Long::compareTo)))
|
||||
.orElse(null);
|
||||
if (latest == null) {
|
||||
return R.ok(null);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("startTime", formatSecondTime(latest.getStartTime()));
|
||||
result.put("endTime", formatSecondTime(latest.getEndTime()));
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
return fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除浇水记录
|
||||
@@ -666,6 +695,41 @@ public class AppController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查APP版本更新
|
||||
*/
|
||||
@GetMapping("/checkVersion")
|
||||
public R<AppVersionCheckVo> checkVersion(@RequestParam(required = false, defaultValue = "android") String platform,
|
||||
@RequestParam(required = false) String currentVersion) {
|
||||
try {
|
||||
if (StringUtils.isBlank(currentVersion)) {
|
||||
throw new ServiceException("当前版本不能为空");
|
||||
}
|
||||
String normalizedPlatform = StringUtils.lowerCase(StringUtils.trim(platform));
|
||||
if (!StringUtils.equalsAny(normalizedPlatform, "android", "ios")) {
|
||||
throw new ServiceException("平台类型仅支持 android 或 ios");
|
||||
}
|
||||
|
||||
AppVersionVo version = appVersionService.queryLatestByPlatform(normalizedPlatform);
|
||||
if (version == null || StringUtils.isBlank(version.getLatestVersion())) {
|
||||
throw new ServiceException("版本配置不存在");
|
||||
}
|
||||
|
||||
AppVersionCheckVo result = new AppVersionCheckVo();
|
||||
result.setPlatform(normalizedPlatform);
|
||||
result.setCurrentVersion(currentVersion);
|
||||
result.setLatestVersion(version.getLatestVersion());
|
||||
result.setVersionCode(version.getVersionCode());
|
||||
result.setUpdateAvailable(!StringUtils.equals(currentVersion, version.getLatestVersion()));
|
||||
result.setForceUpdate(StringUtils.equals(version.getForceUpdate(), "1"));
|
||||
result.setDownloadUrl(version.getDownloadUrl());
|
||||
result.setReleaseNotes(version.getReleaseNotes());
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
return fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户
|
||||
*/
|
||||
@@ -763,6 +827,10 @@ public class AppController extends BaseController {
|
||||
return message.equals(i18nMessage) ? message : i18nMessage;
|
||||
}
|
||||
|
||||
private String formatSecondTime(Date time) {
|
||||
return time == null ? null : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time);
|
||||
}
|
||||
|
||||
private AppDeviceVo getOwnedDevice(String deviceNo) {
|
||||
if (StringUtils.isBlank(deviceNo)) {
|
||||
throw new ServiceException("app.device.no.not.blank");
|
||||
@@ -833,6 +901,25 @@ public class AppController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按排程开关状态下发不同主题:关闭走取消主题,开启重新下发排程。
|
||||
*/
|
||||
private void notifyBoundDevicesScheduleStatusChange(Long scheduleId, String status) {
|
||||
List<String> deviceNos = findBoundDeviceNos(scheduleId);
|
||||
for (String deviceNo : deviceNos) {
|
||||
try {
|
||||
if ("0".equals(status)) {
|
||||
deviceCommandService.sendScheduleCanceledCommand(deviceNo, scheduleId);
|
||||
} else {
|
||||
deviceCommandService.sendScheduleBindCommand(deviceNo, buildScheduleBindPayload(scheduleId, deviceNo));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[排程通知] 下发排程状态变更失败 排程ID={} 状态={} 设备编号={} 原因={}",
|
||||
scheduleId, status, deviceNo, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向排程关联的所有设备下发排程解绑通知
|
||||
*/
|
||||
|
||||
@@ -20,11 +20,10 @@ public class AppDevice extends BaseEntity {
|
||||
@TableId(value = "device_no")
|
||||
private String deviceNo;
|
||||
|
||||
private String bindTokenHash;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String deviceName;
|
||||
private String deviceInitName;
|
||||
|
||||
private String deviceImg;
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ public class AppWateringLog extends BaseEntity {
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
|
||||
@@ -19,9 +19,6 @@ public class AppDeviceBo extends BaseEntity {
|
||||
@NotBlank(message = "设备编号不能为空", groups = { EditGroup.class })
|
||||
private String deviceNo;
|
||||
|
||||
private String bindToken;
|
||||
|
||||
private String bindTokenHash;
|
||||
|
||||
private Long userId;
|
||||
|
||||
@@ -29,6 +26,7 @@ public class AppDeviceBo extends BaseEntity {
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
private String deviceInitName;
|
||||
|
||||
/**
|
||||
* 设备图片
|
||||
|
||||
@@ -76,6 +76,7 @@ public class AppWateringLogBo extends BaseEntity {
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
|
||||
@@ -26,6 +26,7 @@ public class AppDeviceVo implements Serializable {
|
||||
|
||||
@ExcelProperty(value = "设备名称")
|
||||
private String deviceName;
|
||||
private String deviceInitName;
|
||||
|
||||
@ExcelProperty(value = "设备图片")
|
||||
private String deviceImg;
|
||||
|
||||
@@ -92,6 +92,7 @@ public class AppWateringLogVo implements Serializable {
|
||||
*/
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.dromara.app.service.IAppDeviceService;
|
||||
import org.dromara.app.service.IDeviceCommandPublisher;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.redisson.api.RLock;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -92,6 +93,7 @@ DeviceRegisterHandler implements MqttTopicHandler {
|
||||
return device;
|
||||
}
|
||||
device.setDeviceName(valueAsString(dto.get("deviceName")));
|
||||
device.setDeviceInitName(valueAsString(dto.get("deviceName")));
|
||||
device.setPowerLevel(valueAsString(dto.get("powerLevel")));
|
||||
device.setDeviceEm(valueAsString(dto.get("deviceEm")));
|
||||
device.setDeviceSn(valueAsString(dto.get("deviceSn")));
|
||||
@@ -143,7 +145,7 @@ DeviceRegisterHandler implements MqttTopicHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private String firstNotBlank(Object first, Object second) {
|
||||
private @Nullable String firstNotBlank(Object first, Object second) {
|
||||
String firstText = valueAsString(first);
|
||||
if (firstText != null && !firstText.isBlank()) {
|
||||
return firstText;
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppSchedulingDevice;
|
||||
import org.dromara.app.domain.bo.AppDeviceBo;
|
||||
import org.dromara.app.domain.bo.AppWateringLogBo;
|
||||
import org.dromara.app.domain.vo.AppDeviceVo;
|
||||
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
|
||||
@@ -53,11 +54,8 @@ public class KeyFinishHandler implements MqttTopicHandler {
|
||||
}
|
||||
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
||||
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
|
||||
if ("0".equals(logBo.getTriggerType())) {
|
||||
appWateringLogService.confirmScheduleLog(logBo);
|
||||
} else {
|
||||
appWateringLogService.insertByBo(logBo);
|
||||
}
|
||||
appWateringLogService.confirmScheduleLog(logBo);
|
||||
updateDeviceWorkStatusIdle(deviceNo);
|
||||
log.info("[MQTT] 按键浇水完成上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("[MQTT] 按键浇水完成上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}",
|
||||
@@ -89,6 +87,7 @@ public class KeyFinishHandler implements MqttTopicHandler {
|
||||
logBo.setStartTime(startTime);
|
||||
logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin));
|
||||
logBo.setTriggerType("1");
|
||||
logBo.setStatus("0");
|
||||
if ("schedule".equals(valueAsString(dto.get("triggerON")))){
|
||||
logBo.setTriggerType("0");
|
||||
logBo.setScheduleId(queryScheduleId(topicDeviceNo));
|
||||
@@ -99,6 +98,13 @@ public class KeyFinishHandler implements MqttTopicHandler {
|
||||
return logBo;
|
||||
}
|
||||
|
||||
private void updateDeviceWorkStatusIdle(String deviceNo) {
|
||||
AppDeviceBo deviceBo = new AppDeviceBo();
|
||||
deviceBo.setDeviceNo(deviceNo);
|
||||
deviceBo.setWorkStatus("0");
|
||||
appDeviceService.updateByBo(deviceBo);
|
||||
}
|
||||
|
||||
private Long queryUserId(String deviceNo) {
|
||||
AppDeviceVo device = appDeviceService.queryById(deviceNo);
|
||||
return device == null ? null : device.getUserId();
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppSchedulingDevice;
|
||||
import org.dromara.app.domain.bo.AppDeviceBo;
|
||||
import org.dromara.app.domain.bo.AppWateringLogBo;
|
||||
import org.dromara.app.domain.vo.AppDeviceVo;
|
||||
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
|
||||
@@ -54,6 +55,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
|
||||
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
||||
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
|
||||
appWateringLogService.confirmScheduleLog(logBo);
|
||||
updateDeviceWorkStatusIdle(deviceNo);
|
||||
log.info("[MQTT] 浇水排程上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("[MQTT] 浇水排程上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}",
|
||||
@@ -86,6 +88,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
|
||||
logBo.setStartTime(startTime);
|
||||
logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin));
|
||||
logBo.setTriggerType("0");
|
||||
logBo.setStatus("0");
|
||||
if ("mqtt on".equals(valueAsString(dto.get("triggerON")))){
|
||||
logBo.setTriggerType("1");
|
||||
}
|
||||
@@ -95,6 +98,13 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
|
||||
return logBo;
|
||||
}
|
||||
|
||||
private void updateDeviceWorkStatusIdle(String deviceNo) {
|
||||
AppDeviceBo deviceBo = new AppDeviceBo();
|
||||
deviceBo.setDeviceNo(deviceNo);
|
||||
deviceBo.setWorkStatus("0");
|
||||
appDeviceService.updateByBo(deviceBo);
|
||||
}
|
||||
|
||||
private Long queryUserId(String deviceNo) {
|
||||
AppDeviceVo device = appDeviceService.queryById(deviceNo);
|
||||
return device == null ? null : device.getUserId();
|
||||
@@ -128,6 +138,16 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
|
||||
// Try the next supported format.
|
||||
}
|
||||
}
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
||||
for (String pattern : new String[]{"yyyy-MM-dd H:mm:ss", "yyyy-MM-dd H:mm"}) {
|
||||
try {
|
||||
SimpleDateFormat format = new SimpleDateFormat(pattern);
|
||||
format.setLenient(false);
|
||||
return format.parse(today + " " + startTime);
|
||||
} catch (ParseException ignored) {
|
||||
// Try the next supported format.
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported startTime format: " + startTime);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package org.dromara.app.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppSchedulingDevice;
|
||||
import org.dromara.app.domain.bo.AppDeviceBo;
|
||||
import org.dromara.app.domain.bo.AppWateringLogBo;
|
||||
import org.dromara.app.domain.vo.AppDeviceVo;
|
||||
import org.dromara.app.domain.vo.AppWateringLogVo;
|
||||
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.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 开始浇水上报处理器,匹配 /{deviceNo}/publish/startWater。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class StartWaterHandler implements MqttTopicHandler {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/start$");
|
||||
|
||||
private final IAppWateringLogService appWateringLogService;
|
||||
private final IAppDeviceService appDeviceService;
|
||||
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
|
||||
private final DeviceIdentityResolver deviceIdentityResolver;
|
||||
|
||||
@Override
|
||||
public Pattern topicPattern() {
|
||||
return PATTERN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(String deviceIdentity, String payload) {
|
||||
String deviceNo = null;
|
||||
try {
|
||||
deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
|
||||
if (deviceNo == null) {
|
||||
log.warn("[MQTT] 开始浇水上报未找到设备 时间={} 设备标识={} 消息体={}",
|
||||
HandlerLogTime.now(), deviceIdentity, payload);
|
||||
return;
|
||||
}
|
||||
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
|
||||
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
|
||||
if (hasDuplicateRunningLog(logBo)) {
|
||||
updateDeviceWorkStatusWorking(deviceNo);
|
||||
log.info("[MQTT] 开始浇水上报已存在进行中记录,跳过保存 时间={} 设备编号={} 开始时间={} 浇水方式={}",
|
||||
HandlerLogTime.now(), deviceNo, logBo.getStartTime(), logBo.getTriggerType());
|
||||
return;
|
||||
}
|
||||
appWateringLogService.insertByBo(logBo);
|
||||
updateDeviceWorkStatusWorking(deviceNo);
|
||||
log.info("[MQTT] 开始浇水上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("[MQTT] 开始浇水上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}",
|
||||
HandlerLogTime.now(), deviceIdentity, deviceNo, payload, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDeviceWorkStatusWorking(String deviceNo) {
|
||||
AppDeviceBo deviceBo = new AppDeviceBo();
|
||||
deviceBo.setDeviceNo(deviceNo);
|
||||
deviceBo.setWorkStatus("1");
|
||||
appDeviceService.updateByBo(deviceBo);
|
||||
}
|
||||
|
||||
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] 开始浇水上报设备编号不一致 时间={} 主题设备编号={} 消息体设备编号={}",
|
||||
HandlerLogTime.now(), topicDeviceNo, payloadDeviceNo);
|
||||
}
|
||||
|
||||
String triggerType = resolveTriggerType(dto);
|
||||
Date startTime = parseStartTime(valueAsString(dto.get("startTime")));
|
||||
Integer durationMin = valueAsInteger(dto.get("durationMin"));
|
||||
|
||||
AppWateringLogBo logBo = new AppWateringLogBo();
|
||||
logBo.setDeviceNo(topicDeviceNo);
|
||||
logBo.setCommandId(valueAsString(dto.get("commandId")));
|
||||
logBo.setUserId(queryUserId(topicDeviceNo));
|
||||
logBo.setScheduleId("0".equals(triggerType) ? queryScheduleId(topicDeviceNo) : 0L);
|
||||
logBo.setStartTime(startTime);
|
||||
logBo.setEndTime(resolveEndTime(startTime, durationMin));
|
||||
logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin));
|
||||
logBo.setTriggerType(triggerType);
|
||||
logBo.setStatus("1");
|
||||
logBo.setCreateTime(new Date());
|
||||
logBo.setRemark("0".equals(triggerType) ? "设备上报排程开始浇水" : "设备上报手动开始浇水");
|
||||
return logBo;
|
||||
}
|
||||
|
||||
private boolean hasDuplicateRunningLog(AppWateringLogBo logBo) {
|
||||
AppWateringLogBo query = new AppWateringLogBo();
|
||||
query.setDeviceNo(logBo.getDeviceNo());
|
||||
query.setScheduleId(logBo.getScheduleId());
|
||||
query.setTriggerType(logBo.getTriggerType());
|
||||
List<AppWateringLogVo> exists = appWateringLogService.queryList(query);
|
||||
if (exists == null || exists.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return exists.stream().anyMatch(item -> {
|
||||
boolean sameCommand = StringUtils.isNotBlank(logBo.getCommandId())
|
||||
&& Objects.equals(item.getCommandId(), logBo.getCommandId());
|
||||
boolean sameWateringWindow = sameMinute(item.getStartTime(), logBo.getStartTime());
|
||||
boolean running = "1".equals(item.getStatus())
|
||||
|| (StringUtils.isBlank(item.getStatus()) && item.getEndTime() == null);
|
||||
return Objects.equals(item.getDeviceNo(), logBo.getDeviceNo())
|
||||
&& scheduleIdMatches(item.getScheduleId(), logBo)
|
||||
&& Objects.equals(item.getTriggerType(), logBo.getTriggerType())
|
||||
&& running
|
||||
&& (sameCommand || sameWateringWindow);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean scheduleIdMatches(Long existsScheduleId, AppWateringLogBo logBo) {
|
||||
if ("1".equals(logBo.getTriggerType())) {
|
||||
return existsScheduleId == null || existsScheduleId == 0L;
|
||||
}
|
||||
return Objects.equals(existsScheduleId, logBo.getScheduleId());
|
||||
}
|
||||
|
||||
private boolean sameMinute(Date existsStartTime, Date reportStartTime) {
|
||||
if (existsStartTime == null || reportStartTime == null) {
|
||||
return Objects.equals(existsStartTime, reportStartTime);
|
||||
}
|
||||
Calendar existsCalendar = Calendar.getInstance();
|
||||
existsCalendar.setTime(existsStartTime);
|
||||
Calendar reportCalendar = Calendar.getInstance();
|
||||
reportCalendar.setTime(reportStartTime);
|
||||
return existsCalendar.get(Calendar.YEAR) == reportCalendar.get(Calendar.YEAR)
|
||||
&& existsCalendar.get(Calendar.DAY_OF_YEAR) == reportCalendar.get(Calendar.DAY_OF_YEAR)
|
||||
&& existsCalendar.get(Calendar.HOUR_OF_DAY) == reportCalendar.get(Calendar.HOUR_OF_DAY)
|
||||
&& existsCalendar.get(Calendar.MINUTE) == reportCalendar.get(Calendar.MINUTE);
|
||||
}
|
||||
|
||||
private Date resolveEndTime(Date startTime, Integer durationMin) {
|
||||
if (startTime == null || durationMin == null || durationMin <= 0) {
|
||||
return null;
|
||||
}
|
||||
return new Date(startTime.getTime() + durationMin * 60000L);
|
||||
}
|
||||
|
||||
private String resolveTriggerType(Map<String, Object> dto) {
|
||||
String triggerType = valueAsString(dto.get("triggerType"));
|
||||
if ("0".equals(triggerType) || "1".equals(triggerType)) {
|
||||
return triggerType;
|
||||
}
|
||||
|
||||
String trigger = valueAsString(dto.get("triggerON"));
|
||||
if (StringUtils.isBlank(trigger)) {
|
||||
trigger = valueAsString(dto.get("wateringType"));
|
||||
}
|
||||
if (StringUtils.isBlank(trigger)) {
|
||||
trigger = valueAsString(dto.get("type"));
|
||||
}
|
||||
if (StringUtils.isBlank(trigger)) {
|
||||
return "1";
|
||||
}
|
||||
|
||||
String normalized = trigger.trim().toLowerCase(Locale.ROOT);
|
||||
if ("schedule".equals(normalized) || "0".equals(normalized)) {
|
||||
return "0";
|
||||
}
|
||||
return "1";
|
||||
}
|
||||
|
||||
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] 开始浇水上报未找到设备绑定的排程 时间={} 设备编号={}", HandlerLogTime.now(), deviceNo);
|
||||
return 0L;
|
||||
}
|
||||
if (schedulingDevices.size() > 1) {
|
||||
log.warn("[MQTT] 开始浇水上报匹配到多个排程 时间={} 设备编号={} 数量={}", HandlerLogTime.now(), deviceNo, schedulingDevices.size());
|
||||
}
|
||||
return schedulingDevices.get(0).getScheduleId();
|
||||
}
|
||||
|
||||
private Date parseStartTime(String startTime) {
|
||||
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.
|
||||
}
|
||||
}
|
||||
try {
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd H:mm");
|
||||
format.setLenient(false);
|
||||
return format.parse(today + " " + startTime);
|
||||
} catch (ParseException ignored) {
|
||||
// Fall through and report the unsupported format.
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported startTime format: " + startTime);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
|
||||
d.device_no,
|
||||
d.user_id,
|
||||
d.device_name,
|
||||
d.device_init_name,
|
||||
d.device_img,
|
||||
d.qrcode,
|
||||
d.status,
|
||||
@@ -53,6 +54,9 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
|
||||
<if test="bo.deviceName != null and bo.deviceName != ''">
|
||||
and d.device_name like concat('%', #{bo.deviceName}, '%')
|
||||
</if>
|
||||
<if test="bo.deviceInitName != null and bo.deviceInitName != ''">
|
||||
and d.device_init_name = #{bo.deviceInitName}
|
||||
</if>
|
||||
<if test="bo.deviceImg != null and bo.deviceImg != ''">
|
||||
and d.device_img = #{bo.deviceImg}
|
||||
</if>
|
||||
@@ -101,6 +105,7 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
|
||||
d.device_no,
|
||||
d.user_id,
|
||||
d.device_name,
|
||||
d.device_init_name,
|
||||
d.device_img,
|
||||
d.qrcode,
|
||||
d.status,
|
||||
@@ -127,6 +132,9 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
|
||||
<if test="bo.deviceName != null and bo.deviceName != ''">
|
||||
and d.device_name like concat('%', #{bo.deviceName}, '%')
|
||||
</if>
|
||||
<if test="bo.deviceInitName != null and bo.deviceInitName != ''">
|
||||
and d.device_init_name = #{bo.deviceInitName}
|
||||
</if>
|
||||
<if test="bo.deviceImg != null and bo.deviceImg != ''">
|
||||
and d.device_img = #{bo.deviceImg}
|
||||
</if>
|
||||
@@ -174,7 +182,6 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
|
||||
update app_device
|
||||
<set>
|
||||
user_id = #{userId},
|
||||
bind_token_hash = null,
|
||||
<if test="deviceName != null and deviceName != ''">
|
||||
device_name = #{deviceName},
|
||||
</if>
|
||||
@@ -220,6 +227,13 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
|
||||
""")
|
||||
AppDevice selectByMac(@Param("macAddress") String macAddress);
|
||||
|
||||
@Select("""
|
||||
select *
|
||||
from app_device
|
||||
where device_init_name = #{deviceInitName}
|
||||
""")
|
||||
AppDevice selectByDeviceInitName(@Param("deviceInitName") String deviceInitName);
|
||||
|
||||
/**
|
||||
* 仪表盘统计 — 单条 SQL 条件聚合,一次查询返回全部 8 个指标。
|
||||
*/
|
||||
|
||||
@@ -61,6 +61,15 @@ public interface IDeviceCommandService {
|
||||
*/
|
||||
String sendScheduleUnbindCommand(String deviceNo, Long scheduleId);
|
||||
|
||||
/**
|
||||
* 下发排程取消命令,通知设备暂停指定排程。
|
||||
*
|
||||
* @param deviceNo 设备编号
|
||||
* @param scheduleId 排程ID
|
||||
* @return commandId
|
||||
*/
|
||||
String sendScheduleCanceledCommand(String deviceNo, Long scheduleId);
|
||||
|
||||
/**
|
||||
* 下发设备初始化指令。用于解绑设备,允许设备未绑定用户时下发。
|
||||
*
|
||||
|
||||
@@ -37,13 +37,26 @@ import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class AppDeviceServiceImpl implements IAppDeviceService {
|
||||
|
||||
private static final int QR_CODE_SIZE = 300;
|
||||
private static final int QR_CODE_LABEL_HEIGHT = 20;
|
||||
private static final int QR_CODE_LABEL_PADDING = 16;
|
||||
private static final int QR_CODE_LABEL_FONT_SIZE = 20;
|
||||
private static final int QR_CODE_LABEL_OFFSET_Y = -10;
|
||||
|
||||
private final AppDeviceMapper baseMapper;
|
||||
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
|
||||
private final AppWateringLogMapper wateringLogMapper;
|
||||
@@ -150,43 +163,53 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
||||
return baseMapper.updateById(device) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> bindDeviceStatus(AppDeviceBo bo) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("bindDeviceStatus", 200);
|
||||
params.put("bindDeviceStatusName", "");
|
||||
if (bo == null) {
|
||||
params.put("bindDeviceStatus", 300);
|
||||
params.put("bindDeviceStatusName", "设备信息不能为空");
|
||||
return params;
|
||||
static byte[] renderQrCodeImage(String content, String deviceNo) {
|
||||
try {
|
||||
byte[] qrCodeBytes = QrCodeUtil.generatePng(content, QR_CODE_SIZE, QR_CODE_SIZE);
|
||||
BufferedImage qrCodeImage = ImageIO.read(new ByteArrayInputStream(qrCodeBytes));
|
||||
BufferedImage image = new BufferedImage(
|
||||
QR_CODE_SIZE,
|
||||
QR_CODE_SIZE + QR_CODE_LABEL_HEIGHT,
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
);
|
||||
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
try {
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
graphics.drawImage(qrCodeImage, 0, 0, null);
|
||||
drawCenteredDeviceNo(graphics, deviceNo);
|
||||
} finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "png", outputStream);
|
||||
return outputStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("设备二维码生成失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawCenteredDeviceNo(Graphics2D graphics, String deviceNo) {
|
||||
String text = StringUtils.blankToDefault(deviceNo, "");
|
||||
graphics.setColor(Color.BLACK);
|
||||
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
Font font = new Font(Font.SANS_SERIF, Font.BOLD, QR_CODE_LABEL_FONT_SIZE);
|
||||
graphics.setFont(font);
|
||||
|
||||
FontMetrics metrics = graphics.getFontMetrics();
|
||||
int maxTextWidth = QR_CODE_SIZE - QR_CODE_LABEL_PADDING * 2;
|
||||
while (metrics.stringWidth(text) > maxTextWidth && font.getSize() > 12) {
|
||||
font = font.deriveFont((float) font.getSize() - 1);
|
||||
graphics.setFont(font);
|
||||
metrics = graphics.getFontMetrics();
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(bo.getMacAddress())) {
|
||||
params.put("bindDeviceStatus", 301);
|
||||
params.put("bindDeviceStatusName", "MAC地址不能为空");
|
||||
return params;
|
||||
}
|
||||
params.put("bindDevice", null);
|
||||
Long userId = LoginHelper.getUserId();
|
||||
AppDevice exists = baseMapper.selectByMac(bo.getMacAddress());
|
||||
if (exists == null) {
|
||||
params.put("bindDeviceStatus", 302);
|
||||
params.put("bindDeviceStatusName", "设备未上线注册,请先完成配网");
|
||||
return params;
|
||||
}
|
||||
params.put("bindDevice", exists);
|
||||
if (exists.getUserId() != null && !exists.getUserId().equals(userId)) {
|
||||
params.put("bindDeviceStatus", 303);
|
||||
params.put("bindDeviceStatusName", "设备已被其他用户绑定");
|
||||
return params;
|
||||
}
|
||||
if (exists.getUserId() == null ) {
|
||||
params.put("bindDeviceStatus", 304);
|
||||
params.put("bindDeviceStatusName", "设备未绑定,请先绑定用户");
|
||||
return params;
|
||||
}
|
||||
|
||||
return params;
|
||||
int x = (QR_CODE_SIZE - metrics.stringWidth(text)) / 2;
|
||||
int y = QR_CODE_SIZE + (QR_CODE_LABEL_HEIGHT - metrics.getHeight()) / 2 + metrics.getAscent()
|
||||
+ QR_CODE_LABEL_OFFSET_Y;
|
||||
graphics.drawString(text, Math.max(QR_CODE_LABEL_PADDING, x), y);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -254,6 +277,70 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> bindDeviceStatus(AppDeviceBo bo) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("bindDeviceStatus", 200);
|
||||
params.put("bindDeviceStatusName", "");
|
||||
if (bo == null) {
|
||||
params.put("bindDeviceStatus", 300);
|
||||
params.put("bindDeviceStatusName", "设备信息不能为空");
|
||||
return params;
|
||||
}
|
||||
|
||||
if (StringUtils.isAllBlank(bo.getDeviceNo(), bo.getMacAddress(), bo.getDeviceInitName())) {
|
||||
params.put("bindDeviceStatus", 301);
|
||||
params.put("bindDeviceStatusName", "设备编号、MAC地址或设备初始名称不能为空");
|
||||
return params;
|
||||
}
|
||||
params.put("bindDevice", null);
|
||||
Long userId = LoginHelper.getUserId();
|
||||
AppDevice exists = findDeviceForBindStatus(bo);
|
||||
if (exists == null) {
|
||||
params.put("bindDeviceStatus", 302);
|
||||
params.put("bindDeviceStatusName", "设备未入库注册,请先进行入库");
|
||||
return params;
|
||||
}
|
||||
params.put("bindDevice", exists);
|
||||
if (exists.getUserId() != null && !exists.getUserId().equals(userId)) {
|
||||
params.put("bindDeviceStatus", 303);
|
||||
params.put("bindDeviceStatusName", "设备已被其他用户绑定");
|
||||
return params;
|
||||
}
|
||||
if (exists.getUserId() == null ) {
|
||||
params.put("bindDeviceStatus", 304);
|
||||
params.put("bindDeviceStatusName", "设备未绑定,请先绑定用户");
|
||||
return params;
|
||||
}
|
||||
if (exists.getUserId() == null && exists.getWorkStatus().equals("2") && exists.getStatus().equals("0") ) {
|
||||
params.put("bindDeviceStatus", 305);
|
||||
params.put("bindDeviceStatusName", "设备未配网,请先去配置网络");
|
||||
return params;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
private AppDevice findDeviceForBindStatus(AppDeviceBo bo) {
|
||||
AppDevice exists = null;
|
||||
if (StringUtils.isNotBlank(bo.getDeviceNo())) {
|
||||
exists = baseMapper.selectById(bo.getDeviceNo());
|
||||
if (exists != null) {
|
||||
return exists;
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(bo.getMacAddress())) {
|
||||
exists = baseMapper.selectByMac(bo.getMacAddress());
|
||||
if (exists != null) {
|
||||
return exists;
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(bo.getDeviceInitName())) {
|
||||
return baseMapper.selectByDeviceInitName(bo.getDeviceInitName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean generateQrCode(Collection<String> deviceNos) {
|
||||
@@ -266,7 +353,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
||||
}
|
||||
|
||||
for (AppDevice device : devices) {
|
||||
byte[] imageBytes = QrCodeUtil.generatePng(buildQrCodeContent(device), 300, 300);
|
||||
byte[] imageBytes = renderQrCodeImage(buildQrCodeContent(device), device.getDeviceNo());
|
||||
UploadResult uploadResult = OssFactory.instance().uploadSuffix(imageBytes, ".png", "image/png");
|
||||
int rows = baseMapper.update(null, new LambdaUpdateWrapper<AppDevice>()
|
||||
.set(AppDevice::getQrcode, uploadResult.getUrl())
|
||||
@@ -286,7 +373,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
||||
content.put("deviceNo", device.getDeviceNo());
|
||||
content.put("mac", device.getMacAddress());
|
||||
content.put("transport", "ble");
|
||||
content.put("name", device.getDeviceName());
|
||||
content.put("name", device.getDeviceInitName());
|
||||
return JsonUtils.toJsonString(content);
|
||||
}
|
||||
|
||||
@@ -334,24 +421,27 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
||||
throw new ServiceException("设备编号不能为空");
|
||||
}
|
||||
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (userId == null) {
|
||||
throw new ServiceException("用户未登录");
|
||||
}
|
||||
boolean platformAdmin = LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin();
|
||||
Long userId = platformAdmin ? null : LoginHelper.getUserId();
|
||||
if (!platformAdmin) {
|
||||
if (userId == null) {
|
||||
throw new ServiceException("用户未登录");
|
||||
}
|
||||
|
||||
Long ownedCount = baseMapper.selectCount(
|
||||
Wrappers.<AppDevice>lambdaQuery()
|
||||
.in(AppDevice::getDeviceNo, distinctDeviceNos)
|
||||
.eq(AppDevice::getUserId, userId)
|
||||
);
|
||||
if (ownedCount == null || ownedCount.intValue() != distinctDeviceNos.size()) {
|
||||
throw new ServiceException("设备不存在或无权操作");
|
||||
Long ownedCount = baseMapper.selectCount(
|
||||
Wrappers.<AppDevice>lambdaQuery()
|
||||
.in(AppDevice::getDeviceNo, distinctDeviceNos)
|
||||
.eq(AppDevice::getUserId, userId)
|
||||
);
|
||||
if (ownedCount == null || ownedCount.intValue() != distinctDeviceNos.size()) {
|
||||
throw new ServiceException("设备不存在或无权操作");
|
||||
}
|
||||
}
|
||||
|
||||
boolean flag = baseMapper.delete(
|
||||
Wrappers.<AppDevice>lambdaUpdate()
|
||||
.in(AppDevice::getDeviceNo, distinctDeviceNos)
|
||||
.eq(AppDevice::getUserId, userId)
|
||||
.eq(!platformAdmin, AppDevice::getUserId, userId)
|
||||
) > 0;
|
||||
if (flag) {
|
||||
schedulingDeviceMapper.delete(
|
||||
@@ -399,7 +489,6 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
||||
.set(AppDevice::getWorkStatus, "0")
|
||||
.set(AppDevice::getWifiName, null)
|
||||
.set(AppDevice::getWifiPassword, null)
|
||||
.set(AppDevice::getBindTokenHash, null)
|
||||
.in(AppDevice::getDeviceNo, distinctDeviceNos)
|
||||
.eq(AppDevice::getUserId, userId)
|
||||
);
|
||||
|
||||
@@ -18,10 +18,7 @@ import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 浇水记录Service业务层处理
|
||||
@@ -123,6 +120,7 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
|
||||
return false;
|
||||
}
|
||||
bo.setTriggerType("0");
|
||||
bo.setStatus("0");
|
||||
if (StringUtils.isBlank(bo.getRemark())) {
|
||||
bo.setRemark("设备离线,按排程自动补记");
|
||||
}
|
||||
@@ -145,6 +143,18 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
|
||||
.last("limit 1")
|
||||
);
|
||||
if (estimatedLog == null) {
|
||||
AppWateringLog runningLog = findMatchingRunningScheduleLog(bo);
|
||||
if (runningLog != null) {
|
||||
AppWateringLog update = MapstructUtils.convert(bo, AppWateringLog.class);
|
||||
update.setId(runningLog.getId());
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
AppWateringLog finishedLog = findMatchingFinishedLog(bo);
|
||||
if (finishedLog != null) {
|
||||
AppWateringLog update = MapstructUtils.convert(bo, AppWateringLog.class);
|
||||
update.setId(finishedLog.getId());
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
if (existsScheduleLog(bo.getDeviceNo(), bo.getScheduleId(), bo.getStartTime())) {
|
||||
return true;
|
||||
}
|
||||
@@ -157,6 +167,54 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
private AppWateringLog findMatchingRunningScheduleLog(AppWateringLogBo bo) {
|
||||
if (bo.getStartTime() == null || StringUtils.isBlank(bo.getDurationMin()) || bo.getEndTime() == null) {
|
||||
return null;
|
||||
}
|
||||
return baseMapper.selectOne(
|
||||
Wrappers.<AppWateringLog>lambdaQuery()
|
||||
.eq(AppWateringLog::getDeviceNo, bo.getDeviceNo())
|
||||
.between(AppWateringLog::getStartTime, minuteStart(bo.getStartTime()), minuteEnd(bo.getStartTime()))
|
||||
.eq(StringUtils.isNotBlank(bo.getTriggerType()), AppWateringLog::getTriggerType, bo.getTriggerType())
|
||||
// .between(AppWateringLog::getEndTime, minuteStart(bo.getEndTime()), minuteEnd(bo.getEndTime()))
|
||||
.eq(AppWateringLog::getStatus, "1")
|
||||
.last("limit 1")
|
||||
);
|
||||
}
|
||||
|
||||
private AppWateringLog findMatchingFinishedLog(AppWateringLogBo bo) {
|
||||
if (bo.getStartTime() == null) {
|
||||
return null;
|
||||
}
|
||||
return baseMapper.selectOne(
|
||||
Wrappers.<AppWateringLog>lambdaQuery()
|
||||
.eq(AppWateringLog::getDeviceNo, bo.getDeviceNo())
|
||||
.between(AppWateringLog::getStartTime, minuteStart(bo.getStartTime()), minuteEnd(bo.getStartTime()))
|
||||
.eq(StringUtils.isNotBlank(bo.getTriggerType()), AppWateringLog::getTriggerType, bo.getTriggerType())
|
||||
.eq(AppWateringLog::getStatus, "0")
|
||||
.orderByDesc(AppWateringLog::getEndTime)
|
||||
.orderByDesc(AppWateringLog::getCreateTime)
|
||||
.orderByDesc(AppWateringLog::getId)
|
||||
.last("limit 1")
|
||||
);
|
||||
}
|
||||
|
||||
private Date minuteStart(Date time) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(time);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
return calendar.getTime();
|
||||
}
|
||||
|
||||
private Date minuteEnd(Date time) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(time);
|
||||
calendar.set(Calendar.SECOND, 59);
|
||||
calendar.set(Calendar.MILLISECOND, 999);
|
||||
return calendar.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改浇水记录
|
||||
*
|
||||
@@ -225,30 +283,30 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<AppWateringLog> activeLogs = baseMapper.selectList(
|
||||
AppWateringLog activeLog = baseMapper.selectOne(
|
||||
Wrappers.<AppWateringLog>lambdaQuery()
|
||||
.eq(AppWateringLog::getDeviceNo, deviceNo)
|
||||
.isNull(AppWateringLog::getEndTime)
|
||||
.eq(AppWateringLog::getStatus, "1")
|
||||
.orderByDesc(AppWateringLog::getStartTime)
|
||||
.orderByDesc(AppWateringLog::getCreateTime)
|
||||
.orderByDesc(AppWateringLog::getId)
|
||||
.last("limit 1")
|
||||
);
|
||||
if (activeLogs == null || activeLogs.isEmpty()) {
|
||||
if (activeLog == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int updated = 0;
|
||||
for (AppWateringLog activeLog : activeLogs) {
|
||||
LambdaUpdateWrapper<AppWateringLog> updateWrapper = Wrappers.lambdaUpdate();
|
||||
updateWrapper.eq(AppWateringLog::getId, activeLog.getId())
|
||||
.isNull(AppWateringLog::getEndTime)
|
||||
.set(AppWateringLog::getEndTime, endTime)
|
||||
.set(StringUtils.isNotBlank(remark), AppWateringLog::getRemark, remark);
|
||||
if (activeLog.getStartTime() != null) {
|
||||
long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime();
|
||||
long durationMin = Math.max(durationMillis, 0L) / 60000L;
|
||||
updateWrapper.set(AppWateringLog::getDurationMin, String.valueOf(durationMin));
|
||||
}
|
||||
updated += baseMapper.update(null, updateWrapper);
|
||||
LambdaUpdateWrapper<AppWateringLog> updateWrapper = Wrappers.lambdaUpdate();
|
||||
updateWrapper.eq(AppWateringLog::getId, activeLog.getId())
|
||||
.set(AppWateringLog::getEndTime, endTime)
|
||||
.set(AppWateringLog::getStatus, "0")
|
||||
.set(StringUtils.isNotBlank(remark), AppWateringLog::getRemark, remark);
|
||||
if (activeLog.getStartTime() != null) {
|
||||
long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime();
|
||||
long durationMin = Math.max(durationMillis, 0L) / 60000L;
|
||||
updateWrapper.set(AppWateringLog::getDurationMin, String.valueOf(durationMin));
|
||||
}
|
||||
return updated;
|
||||
return baseMapper.update(null, updateWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -69,9 +69,10 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
||||
command.setDeviceNo(deviceNo);
|
||||
command.setDeviceMac(requireDeviceMac(deviceNo));
|
||||
command.setCommandType("switchDevice");
|
||||
String commandStartTime = normalizeCommandStartTime(startTime);
|
||||
command.getPayload().put("deviceNo", deviceNo);
|
||||
command.getPayload().put("cmd", workStatus);
|
||||
command.getPayload().put("startTime", startTime);
|
||||
command.getPayload().put("startTime", commandStartTime);
|
||||
if (durationMin != null) {
|
||||
command.getPayload().put("durationMin", durationMin);
|
||||
}
|
||||
@@ -82,12 +83,12 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
||||
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if ("1".equals(workStatus)) {
|
||||
AppWateringLogBo logBo = buildManualStartLog(deviceNo, userId, commandId, startTime, durationMin);
|
||||
AppWateringLogBo logBo = buildManualStartLog(deviceNo, userId, commandId, commandStartTime, durationMin);
|
||||
wateringLogService.insertByBo(logBo);
|
||||
} else {
|
||||
boolean updated = wateringLogService.finishManualLog(deviceNo, userId, new Date(), "手动停止浇水");
|
||||
if (!updated) {
|
||||
log.warn("[命令] 未找到进行中的手动浇水记录 设备编号={} 用户ID={} 停止命令编号={}",
|
||||
int updated = wateringLogService.finishRunningLogsByDevice(deviceNo, new Date(), "手动停止浇水");
|
||||
if (updated <= 0) {
|
||||
log.warn("[命令] 未找到进行中的浇水记录 设备编号={} 用户ID={} 停止命令编号={}",
|
||||
deviceNo, userId, commandId);
|
||||
}
|
||||
}
|
||||
@@ -168,6 +169,24 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
||||
return sendCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sendScheduleCanceledCommand(String deviceNo, Long scheduleId) {
|
||||
AppDeviceVo device = requireCommandDevice(deviceNo);
|
||||
if (StringUtils.isBlank(device.getMacAddress())) {
|
||||
throw new ServiceException("设备未登记 MAC,无法下发排程取消命令:" + deviceNo);
|
||||
}
|
||||
|
||||
DeviceCommand command = new DeviceCommand();
|
||||
command.setDeviceNo(deviceNo);
|
||||
command.setDeviceMac(device.getMacAddress());
|
||||
command.setCommandType("cancelSchedule");
|
||||
command.setTopic(buildScheduleCanceledTopic(deviceNo));
|
||||
command.getPayload().put("deviceNo", deviceNo);
|
||||
command.getPayload().put("scheduleId", scheduleId);
|
||||
command.getPayload().put("canceled", true);
|
||||
return sendCommand(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sendInitDeviceCommand(String deviceNo) {
|
||||
AppDeviceVo device = deviceService.queryById(deviceNo);
|
||||
@@ -237,6 +256,10 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
||||
return "/" + deviceNo.trim().toLowerCase(Locale.ROOT) + "/subscriber/schedule";
|
||||
}
|
||||
|
||||
private String buildScheduleCanceledTopic(String deviceNo) {
|
||||
return "/" + deviceNo.trim().toLowerCase(Locale.ROOT) + "/schedule/canceled";
|
||||
}
|
||||
|
||||
private void assertSwitchStatus(String workStatus) {
|
||||
if (!"0".equals(workStatus) && !"1".equals(workStatus)) {
|
||||
throw new ServiceException("设备工作状态只能为0或1");
|
||||
@@ -256,6 +279,7 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
||||
String startTime,
|
||||
Integer durationMin
|
||||
) {
|
||||
Date starttime = parseStartTime(valueAsString(startTime));
|
||||
AppWateringLogBo logBo = new AppWateringLogBo();
|
||||
logBo.setDeviceNo(deviceNo);
|
||||
logBo.setCommandId(commandId);
|
||||
@@ -265,6 +289,8 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
||||
logBo.setTriggerType("1"); // 手动
|
||||
logBo.setRemark("手动开始浇水");
|
||||
logBo.setCreateTime(new Date());
|
||||
logBo.setStatus("1");
|
||||
logBo.setEndTime(resolveEndTime(starttime, durationMin));
|
||||
|
||||
if (StringUtils.isNotEmpty(startTime)) {
|
||||
logBo.setStartTime(parseStartTime(startTime));
|
||||
@@ -274,7 +300,43 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
||||
return logBo;
|
||||
}
|
||||
|
||||
private String valueAsString(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private String normalizeCommandStartTime(String startTime) {
|
||||
if (StringUtils.isBlank(startTime)) {
|
||||
return startTime;
|
||||
}
|
||||
if (startTime.matches("^\\d{2}:\\d{2}$")) {
|
||||
return startTime + ":00";
|
||||
}
|
||||
return startTime;
|
||||
}
|
||||
|
||||
private Date resolveEndTime(Date startTime, Integer durationMin) {
|
||||
if (startTime == null || durationMin == null || durationMin <= 0) {
|
||||
return null;
|
||||
}
|
||||
return new Date(startTime.getTime() + durationMin * 60000L);
|
||||
}
|
||||
private Date parseStartTime(String startTime) {
|
||||
if (startTime.matches("^\\d{2}:\\d{2}:\\d{2}$")) {
|
||||
try {
|
||||
Date parsedTime = new SimpleDateFormat("HH:mm:ss").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, parsed.get(Calendar.SECOND));
|
||||
today.set(Calendar.MILLISECOND, 0);
|
||||
return today.getTime();
|
||||
} catch (ParseException e) {
|
||||
throw new ServiceException("开始时间格式错误,期望 HH:mm、HH:mm:ss 或 yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
}
|
||||
if (startTime.matches("^\\d{2}:\\d{2}$")) {
|
||||
try {
|
||||
Date parsedTime = new SimpleDateFormat("HH:mm").parse(startTime);
|
||||
@@ -288,13 +350,13 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
|
||||
today.set(Calendar.MILLISECOND, 0);
|
||||
return today.getTime();
|
||||
} catch (ParseException e) {
|
||||
throw new ServiceException("开始时间格式错误,期望 HH:mm 或 yyyy-MM-dd HH:mm:ss");
|
||||
throw new ServiceException("开始时间格式错误,期望 HH:mm、HH:mm:ss 或 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");
|
||||
throw new ServiceException("开始时间格式错误,期望 HH:mm、HH:mm:ss 或 yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user