浇水bug修改

This commit is contained in:
yuhaiming
2026-07-09 08:26:37 +08:00
parent 319ebd19ad
commit 7deda9bdc0
27 changed files with 826 additions and 152 deletions

View File

@@ -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());
}
}
}
/**
* 向排程关联的所有设备下发排程解绑通知
*/

View File

@@ -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;

View File

@@ -78,6 +78,7 @@ public class AppWateringLog extends BaseEntity {
* 备注
*/
private String remark;
private String status;
/**
* 创建时间

View File

@@ -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;
/**
* 设备图片

View File

@@ -76,6 +76,7 @@ public class AppWateringLogBo extends BaseEntity {
* 备注
*/
private String remark;
private String status;
/**
* 创建时间

View File

@@ -26,6 +26,7 @@ public class AppDeviceVo implements Serializable {
@ExcelProperty(value = "设备名称")
private String deviceName;
private String deviceInitName;
@ExcelProperty(value = "设备图片")
private String deviceImg;

View File

@@ -92,6 +92,7 @@ public class AppWateringLogVo implements Serializable {
*/
@ExcelProperty(value = "备注")
private String remark;
private String status;
/**
* 创建时间

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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 个指标。
*/

View File

@@ -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);
/**
* 下发设备初始化指令。用于解绑设备,允许设备未绑定用户时下发。
*

View File

@@ -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)
);

View File

@@ -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);
}
/**

View File

@@ -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");
}
}
}