浇水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

@@ -20,7 +20,6 @@ INSERT INTO sys_menu VALUES ('11654', '流程变量查询', '11621', '2', '#', '
INSERT INTO sys_menu VALUES ('11655', '流程变量修改', '11621', '3', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:instance:variable', '#', 103, 1, sysdate(), null, null, ''); INSERT INTO sys_menu VALUES ('11655', '流程变量修改', '11621', '3', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:instance:variable', '#', 103, 1, sysdate(), null, null, '');
-- Device binding and device_no primary key migration. -- Device binding and device_no primary key migration.
ALTER TABLE app_device ADD COLUMN bind_token_hash varchar(255) NULL COMMENT '设备绑定令牌哈希' AFTER device_no;
ALTER TABLE app_device ADD COLUMN qrcode varchar(1000) NULL COMMENT '设备二维码' AFTER device_img; ALTER TABLE app_device ADD COLUMN qrcode varchar(1000) NULL COMMENT '设备二维码' AFTER device_img;
ALTER TABLE app_watering_log ADD COLUMN remark varchar(500) NULL COMMENT '备注' AFTER trigger_type; ALTER TABLE app_watering_log ADD COLUMN remark varchar(500) NULL COMMENT '备注' AFTER trigger_type;
ALTER TABLE app_scheduling_device ADD COLUMN device_no varchar(64) NULL COMMENT '设备编号' AFTER schedule_id; ALTER TABLE app_scheduling_device ADD COLUMN device_no varchar(64) NULL COMMENT '设备编号' AFTER schedule_id;

View File

@@ -15,10 +15,7 @@ import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils; import me.zhyd.oauth.utils.AuthStateUtils;
import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.model.ForgotLoginBody; import org.dromara.common.core.domain.model.*;
import org.dromara.common.core.domain.model.LoginBody;
import org.dromara.common.core.domain.model.RegisterBody;
import org.dromara.common.core.domain.model.SocialLoginBody;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.*; import org.dromara.common.core.utils.*;
import org.dromara.common.encrypt.annotation.ApiEncrypt; import org.dromara.common.encrypt.annotation.ApiEncrypt;
@@ -42,10 +39,7 @@ import org.dromara.system.service.ISysTenantService;
import org.dromara.web.domain.vo.LoginTenantVo; import org.dromara.web.domain.vo.LoginTenantVo;
import org.dromara.web.domain.vo.LoginVo; import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.domain.vo.TenantListVo; import org.dromara.web.domain.vo.TenantListVo;
import org.dromara.web.service.ForgotPasswordService; import org.dromara.web.service.*;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.dromara.web.service.SysRegisterService;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -79,6 +73,7 @@ public class AuthController {
private final ISysClientService clientService; private final ISysClientService clientService;
private final ScheduledExecutorService scheduledExecutorService; private final ScheduledExecutorService scheduledExecutorService;
private final ForgotPasswordService forgotPasswordService; private final ForgotPasswordService forgotPasswordService;
private final AccountCancellationService accountCancellationService;
/** /**
* 登录方法 * 登录方法
@@ -189,6 +184,17 @@ public class AuthController {
return R.ok("退出成功"); return R.ok("退出成功");
} }
/**
* 注销当前账号
*/
@DeleteMapping("/account")
public R<Void> cancelAccount(@Validated @RequestBody AccountCancelBody body) {
StpUtil.checkLogin();
accountCancellationService.cancelCurrentAccount(body.getCode());
StpUtil.logout();
return R.ok("注销成功");
}
/** /**
* 用户注册 * 用户注册
*/ */

View File

@@ -1,6 +1,7 @@
package org.dromara.web.controller; package org.dromara.web.controller;
import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator; import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator; import cn.hutool.captcha.generator.RandomGenerator;
@@ -21,11 +22,14 @@ import org.dromara.common.mail.utils.MailUtils;
import org.dromara.common.ratelimiter.annotation.RateLimiter; import org.dromara.common.ratelimiter.annotation.RateLimiter;
import org.dromara.common.ratelimiter.enums.LimitType; import org.dromara.common.ratelimiter.enums.LimitType;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.web.core.WaveAndCircleCaptcha; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.config.properties.CaptchaProperties; import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.common.web.core.WaveAndCircleCaptcha;
import org.dromara.sms4j.api.SmsBlend; import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse; import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory; import org.dromara.sms4j.core.factory.SmsFactory;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.service.ISysUserService;
import org.dromara.web.domain.vo.CaptchaVo; import org.dromara.web.domain.vo.CaptchaVo;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.ExpressionParser;
@@ -52,6 +56,7 @@ public class CaptchaController {
private final CaptchaProperties captchaProperties; private final CaptchaProperties captchaProperties;
private final MailProperties mailProperties; private final MailProperties mailProperties;
private final ISysUserService userService;
/** /**
@@ -66,20 +71,20 @@ public class CaptchaController {
String code = RandomUtil.randomNumbers(6); String code = RandomUtil.randomNumbers(6);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// if (Validator.isMobile(username)){ if (Validator.isMobile(username)){
// // 验证码模板id 自行处理 (查数据库或写死均可) // 验证码模板id 自行处理 (查数据库或写死均可)
// String templateId = "SMS_333877107"; String templateId = "SMS_333877107";
// LinkedHashMap<String, String> map = new LinkedHashMap<>(1); LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
// map.put("code", code); map.put("code", code);
// SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
// SmsResponse smsResponse = smsBlend.sendMessage(username, templateId, map); SmsResponse smsResponse = smsBlend.sendMessage(username, templateId, map);
// if (!smsResponse.isSuccess()) { if (!smsResponse.isSuccess()) {
// log.error("验证码短信发送异常 => {}", smsResponse); log.error("验证码短信发送异常 => {}", smsResponse);
// return R.fail(smsResponse.getData().toString()); return R.fail(smsResponse.getData().toString());
// } }
// }else { }else {
// emailCodeImpl(username); emailCodeImpl(username);
// } }
return R.ok(code); return R.ok(code);
} }
@@ -95,7 +100,49 @@ public class CaptchaController {
@RateLimiter(key = "#phonenumber", time = 60, count = 1) @RateLimiter(key = "#phonenumber", time = 60, count = 1)
@GetMapping("/resource/sms/code") @GetMapping("/resource/sms/code")
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) { public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber; R<Void> result = sendSmsCode(phonenumber, phonenumber);
if (R.isError(result)) {
return result;
}
return R.ok();
}
/**
* 注销账号验证码
*/
@GetMapping("/resource/account/cancel/code")
public R<Void> accountCancelCode() {
StpUtil.checkLogin();
Long userId = LoginHelper.getUserId();
String username = LoginHelper.getUsername();
if (userId == null || StringUtils.isBlank(username)) {
return R.fail("用户未登录");
}
SysUserVo user = userService.selectUserById(userId);
if (user == null) {
return R.fail("用户不存在");
}
if (Validator.isMobile(user.getPhonenumber())) {
return sendSmsCode(user.getPhonenumber(), username);
}
if (StringUtils.isNotBlank(user.getEmail())) {
if (!mailProperties.getEnabled()) {
return R.fail("当前系统没有开启邮箱功能!");
}
emailCodeImpl(user.getEmail(), username);
return R.ok("操作成功");
}
return R.fail("当前账号未绑定手机号或邮箱");
}
/**
* 发送短信验证码
*
* @param phonenumber 接收手机号
* @param cacheKey 验证码缓存标识
*/
public R<Void> sendSmsCode(String phonenumber, String cacheKey) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + cacheKey;
String code = RandomUtil.randomNumbers(6); String code = RandomUtil.randomNumbers(6);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// 验证码模板id 自行处理 (查数据库或写死均可) // 验证码模板id 自行处理 (查数据库或写死均可)
@@ -131,7 +178,15 @@ public class CaptchaController {
*/ */
@RateLimiter(key = "#email", time = 60, count = 1) @RateLimiter(key = "#email", time = 60, count = 1)
public void emailCodeImpl(String email) { public void emailCodeImpl(String email) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + email; emailCodeImpl(email, email);
}
/**
* 邮箱验证码
*/
@RateLimiter(key = "#email", time = 60, count = 1)
public void emailCodeImpl(String email, String cacheKey) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + cacheKey;
String code = RandomUtil.randomNumbers(6); String code = RandomUtil.randomNumbers(6);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
try { try {

View File

@@ -3,7 +3,6 @@ package org.dromara.web.service;
import cn.hutool.core.lang.Validator; import cn.hutool.core.lang.Validator;
import cn.hutool.crypto.digest.BCrypt; import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.constant.GlobalConstants;
@@ -67,7 +66,7 @@ public class SysRegisterService {
} }
SysUserBo sysUser = new SysUserBo(); SysUserBo sysUser = new SysUserBo();
// sysUser.setUserName(username); sysUser.setUserName(username);
sysUser.setNickName("用户"+new Date().getTime()); sysUser.setNickName("用户"+new Date().getTime());
@@ -76,6 +75,7 @@ public class SysRegisterService {
boolean exist; boolean exist;
if(Validator.isEmail(username)){ if(Validator.isEmail(username)){
sysUser.setEmail(username); sysUser.setEmail(username);
exist = TenantHelper.dynamic(tenantId, () -> { exist = TenantHelper.dynamic(tenantId, () -> {
return userMapper.exists(new LambdaQueryWrapper<SysUser>() return userMapper.exists(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getEmail, sysUser.getEmail())); .eq(SysUser::getEmail, sysUser.getEmail()));

View File

@@ -58,6 +58,7 @@ public class SmsAuthStrategy implements IAuthStrategy {
}); });
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
//loginUser.setUsername(phonenumber);
SaLoginParameter model = new SaLoginParameter(); SaLoginParameter model = new SaLoginParameter();
model.setDeviceType(client.getDeviceType()); model.setDeviceType(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置

View File

@@ -96,13 +96,13 @@ spring:
spring.data: spring.data:
redis: redis:
# 地址 # 地址
host: localhost host: 47.97.217.123
# 端口默认为6379 # 端口默认为6379
port: 6379 port: 6379
# 数据库索引 # 数据库索引
database: 0 database: 0
# redis 密码必须配置 # redis 密码必须配置
#password: password: waterx123
# 连接超时时间 # 连接超时时间
timeout: 10s timeout: 10s
# 是否开启ssl # 是否开启ssl

View File

@@ -22,9 +22,10 @@ server:
--- # mqtt配置信息 --- # mqtt配置信息
mqtt: mqtt:
enabled: ${MQTT_ENABLED:true} enabled: ${MQTT_ENABLED:true}
broker-url: ssl://service.reinkun.com:8883 #broker-url: ssl://www.mmipco.cn:8883
broker-url: tcp://47.97.217.123
username: admin username: admin
password: yukun_mqtt password: 61e6062129e9
client-id: water-server-${server.port} client-id: water-server-${server.port}
qos: 1 qos: 1
keep-alive: 60 keep-alive: 60
@@ -46,7 +47,7 @@ mqtt:
command-ack: command-ack:
enabled: true enabled: true
max-retry-count: 3 max-retry-count: 3
retry-interval-ms: 5000 retry-interval-ms: 30000
scan-interval-ms: 5000 scan-interval-ms: 5000
pending-ttl-seconds: 86400 pending-ttl-seconds: 86400
ack-ttl-seconds: 86400 ack-ttl-seconds: 86400
@@ -66,6 +67,7 @@ mqtt:
- /+/publish/register #设备注册 - /+/publish/register #设备注册
- /+/publish/power #电量 - /+/publish/power #电量
- /+/publish/ack #应答 - /+/publish/ack #应答
- /+/publish/start #开始浇水上报
- /+/publish/finish/key #按键浇水完成 - /+/publish/finish/key #按键浇水完成
- /+/publish/error #硬件故障 - /+/publish/error #硬件故障
# - /+/subscriber/cmd #下发命令 0关闭浇水 1开始浇水 # - /+/subscriber/cmd #下发命令 0关闭浇水 1开始浇水
@@ -123,9 +125,9 @@ spring:
servlet: servlet:
multipart: multipart:
# 单个文件大小 # 单个文件大小
max-file-size: 10MB max-file-size: 100MB
# 设置总上传的文件大小 # 设置总上传的文件大小
max-request-size: 20MB max-request-size: 100MB
mvc: mvc:
# 设置静态资源路径 防止所有请求都去查静态资源 # 设置静态资源路径 防止所有请求都去查静态资源
static-path-pattern: /static/** static-path-pattern: /static/**

View File

@@ -50,6 +50,24 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
withCommandLock(commandId, () -> deletePending(commandId)); withCommandLock(commandId, () -> deletePending(commandId));
} }
static boolean resolveMissingCommandId(DeviceCommandAck ack, List<DeviceCommand> pendingCommands) {
if (ack == null || StringUtils.isNotBlank(ack.getCommandId())) {
return ack != null;
}
if (pendingCommands == null || pendingCommands.size() != 1) {
return false;
}
DeviceCommand command = pendingCommands.get(0);
if (command == null || StringUtils.isBlank(command.getCommandId())) {
return false;
}
ack.setCommandId(command.getCommandId());
if (StringUtils.isBlank(ack.getStatus())) {
ack.setStatus("1");
}
return true;
}
@Override @Override
public void handleAck(String deviceNo, String payload) { public void handleAck(String deviceNo, String payload) {
if (StringUtils.isBlank(payload)) { if (StringUtils.isBlank(payload)) {
@@ -70,11 +88,19 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
log.warn("[MQTT] ACK JSON 格式错误 设备编号={} 消息体={}", deviceNo, payload, e); log.warn("[MQTT] ACK JSON 格式错误 设备编号={} 消息体={}", deviceNo, payload, e);
return; return;
} }
if (ack == null || ack.getCommandId() == null) { if (ack == null) {
log.warn("[MQTT] ACK 缺少命令编号 设备编号={} 消息体={}", deviceNo, payload); log.warn("[MQTT] ACK 缺少命令编号 设备编号={} 消息体={}", deviceNo, payload);
return; return;
} }
ack.setDeviceNo(deviceNo); ack.setDeviceNo(deviceNo);
if (StringUtils.isBlank(ack.getCommandId())) {
List<DeviceCommand> pendingCommands = findPendingCommandsByDeviceNo(deviceNo);
if (!resolveMissingCommandId(ack, pendingCommands)) {
log.warn("[MQTT] ACK 缺少命令编号且无法唯一匹配待确认命令 设备编号={} 待确认数量={} 消息体={}",
deviceNo, pendingCommands.size(), payload);
return;
}
}
boolean ackSaved = withCommandLock(ack.getCommandId(), () -> { boolean ackSaved = withCommandLock(ack.getCommandId(), () -> {
deletePending(ack.getCommandId()); deletePending(ack.getCommandId());
RedisUtils.setCacheObject(ackKey(ack.getCommandId()), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds())); RedisUtils.setCacheObject(ackKey(ack.getCommandId()), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds()));
@@ -84,7 +110,7 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
return; return;
} }
refreshDeviceOnline(deviceNo); refreshDeviceOnline(deviceNo);
log.info("[MQTT] 收到命令确认 设备编号={} 命令编号={} 状态={}", deviceNo, ack.getCommandId(), ack.getStatus()); log.info("[MQTT] 收到命令确认 设备编号={} 命令编号={} message={}", deviceNo, ack.getCommandId(), ack.getMessage());
} }
private void handlePlainAck(String deviceNo, String payload) { private void handlePlainAck(String deviceNo, String payload) {

View File

@@ -47,6 +47,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
@@ -66,6 +67,7 @@ public class AppController extends BaseController {
private final IAppWateringLogService appWateringLogService; private final IAppWateringLogService appWateringLogService;
private final IDeviceCommandService deviceCommandService; private final IDeviceCommandService deviceCommandService;
private final ISysOssService ossService; 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) { private Map<String, Object> buildScheduleBindPayload(Long scheduleId, String deviceNo) {
// AppScheduleVo schedule = getOwnedSchedule(scheduleId);
List<AppScheduleDetailVo> details = appScheduleDetailService.queryByScheduleIdByStatus(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<>(); Map<String, Object> payload = new HashMap<>();
// payload.put("cmd", -1);
payload.put("deviceNo", deviceNo); payload.put("deviceNo", deviceNo);
// payload.put("schedule", schedulePayload);
payload.put("details", toScheduleDetailPayload(details)); payload.put("details", toScheduleDetailPayload(details));
return payload; return payload;
// return toScheduleDetailPayload(details);
} }
private List<Map<String, Object>> toScheduleDetailPayload(List<AppScheduleDetailVo> details) { private List<Map<String, Object>> toScheduleDetailPayload(List<AppScheduleDetailVo> details) {
@@ -317,14 +310,17 @@ public class AppController extends BaseController {
timeSlots.add(timeSlot); timeSlots.add(timeSlot);
} }
if (detail.getStatus().equals("1")){
Map<String, Object> item = new HashMap<>(); Map<String, Object> item = new HashMap<>();
item.put("id", detail.getId()); // item.put("id", detail.getId());
item.put("weekday", detail.getWeekday()); item.put("weekday", detail.getWeekday());
item.put("timeData", timeSlots); item.put("timeData", timeSlots);
item.put("triggerType", detail.getTriggerType()); item.put("triggerType", detail.getTriggerType());
item.put("status", detail.getStatus()); item.put("status", detail.getStatus());
detailPayload.add(item); detailPayload.add(item);
} }
}
return detailPayload; return detailPayload;
} }
@@ -470,7 +466,7 @@ public class AppController extends BaseController {
Boolean updated = appScheduleService.updateStatusByBo(bo); Boolean updated = appScheduleService.updateStatusByBo(bo);
if (updated) { if (updated) {
// 向所有绑定设备下发排程状态变更 // 向所有绑定设备下发排程状态变更
notifyBoundDevicesScheduleUpdate(bo.getId()); notifyBoundDevicesScheduleStatusChange(bo.getId(), bo.getStatus());
} }
return toAjax(updated); return toAjax(updated);
} catch (Exception e) { } catch (Exception e) {
@@ -560,6 +556,39 @@ public class AppController extends BaseController {
return appWateringLogVoTableDataInfo; 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; 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) { private AppDeviceVo getOwnedDevice(String deviceNo) {
if (StringUtils.isBlank(deviceNo)) { if (StringUtils.isBlank(deviceNo)) {
throw new ServiceException("app.device.no.not.blank"); 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") @TableId(value = "device_no")
private String deviceNo; private String deviceNo;
private String bindTokenHash;
private Long userId; private Long userId;
private String deviceName; private String deviceName;
private String deviceInitName;
private String deviceImg; private String deviceImg;

View File

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

View File

@@ -19,9 +19,6 @@ public class AppDeviceBo extends BaseEntity {
@NotBlank(message = "设备编号不能为空", groups = { EditGroup.class }) @NotBlank(message = "设备编号不能为空", groups = { EditGroup.class })
private String deviceNo; private String deviceNo;
private String bindToken;
private String bindTokenHash;
private Long userId; private Long userId;
@@ -29,6 +26,7 @@ public class AppDeviceBo extends BaseEntity {
* 设备名称 * 设备名称
*/ */
private String deviceName; private String deviceName;
private String deviceInitName;
/** /**
* 设备图片 * 设备图片

View File

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

View File

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

View File

@@ -92,6 +92,7 @@ public class AppWateringLogVo implements Serializable {
*/ */
@ExcelProperty(value = "备注") @ExcelProperty(value = "备注")
private String remark; 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.app.service.IDeviceCommandPublisher;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.jspecify.annotations.Nullable;
import org.redisson.api.RLock; import org.redisson.api.RLock;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -92,6 +93,7 @@ DeviceRegisterHandler implements MqttTopicHandler {
return device; return device;
} }
device.setDeviceName(valueAsString(dto.get("deviceName"))); device.setDeviceName(valueAsString(dto.get("deviceName")));
device.setDeviceInitName(valueAsString(dto.get("deviceName")));
device.setPowerLevel(valueAsString(dto.get("powerLevel"))); device.setPowerLevel(valueAsString(dto.get("powerLevel")));
device.setDeviceEm(valueAsString(dto.get("deviceEm"))); device.setDeviceEm(valueAsString(dto.get("deviceEm")));
device.setDeviceSn(valueAsString(dto.get("deviceSn"))); device.setDeviceSn(valueAsString(dto.get("deviceSn")));
@@ -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); String firstText = valueAsString(first);
if (firstText != null && !firstText.isBlank()) { if (firstText != null && !firstText.isBlank()) {
return firstText; return firstText;

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppSchedulingDevice; import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.bo.AppWateringLogBo; import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.vo.AppDeviceVo; import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.mapper.AppSchedulingDeviceMapper; import org.dromara.app.mapper.AppSchedulingDeviceMapper;
@@ -53,11 +54,8 @@ public class KeyFinishHandler implements MqttTopicHandler {
} }
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class); Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto); AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
if ("0".equals(logBo.getTriggerType())) {
appWateringLogService.confirmScheduleLog(logBo); appWateringLogService.confirmScheduleLog(logBo);
} else { updateDeviceWorkStatusIdle(deviceNo);
appWateringLogService.insertByBo(logBo);
}
log.info("[MQTT] 按键浇水完成上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload); log.info("[MQTT] 按键浇水完成上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
} catch (Exception e) { } catch (Exception e) {
log.error("[MQTT] 按键浇水完成上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}", log.error("[MQTT] 按键浇水完成上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}",
@@ -89,6 +87,7 @@ public class KeyFinishHandler implements MqttTopicHandler {
logBo.setStartTime(startTime); logBo.setStartTime(startTime);
logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin)); logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin));
logBo.setTriggerType("1"); logBo.setTriggerType("1");
logBo.setStatus("0");
if ("schedule".equals(valueAsString(dto.get("triggerON")))){ if ("schedule".equals(valueAsString(dto.get("triggerON")))){
logBo.setTriggerType("0"); logBo.setTriggerType("0");
logBo.setScheduleId(queryScheduleId(topicDeviceNo)); logBo.setScheduleId(queryScheduleId(topicDeviceNo));
@@ -99,6 +98,13 @@ public class KeyFinishHandler implements MqttTopicHandler {
return logBo; 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) { private Long queryUserId(String deviceNo) {
AppDeviceVo device = appDeviceService.queryById(deviceNo); AppDeviceVo device = appDeviceService.queryById(deviceNo);
return device == null ? null : device.getUserId(); return device == null ? null : device.getUserId();

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppSchedulingDevice; import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.bo.AppWateringLogBo; import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.vo.AppDeviceVo; import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.mapper.AppSchedulingDeviceMapper; import org.dromara.app.mapper.AppSchedulingDeviceMapper;
@@ -54,6 +55,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class); Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto); AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
appWateringLogService.confirmScheduleLog(logBo); appWateringLogService.confirmScheduleLog(logBo);
updateDeviceWorkStatusIdle(deviceNo);
log.info("[MQTT] 浇水排程上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload); log.info("[MQTT] 浇水排程上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
} catch (Exception e) { } catch (Exception e) {
log.error("[MQTT] 浇水排程上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}", log.error("[MQTT] 浇水排程上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}",
@@ -86,6 +88,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
logBo.setStartTime(startTime); logBo.setStartTime(startTime);
logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin)); logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin));
logBo.setTriggerType("0"); logBo.setTriggerType("0");
logBo.setStatus("0");
if ("mqtt on".equals(valueAsString(dto.get("triggerON")))){ if ("mqtt on".equals(valueAsString(dto.get("triggerON")))){
logBo.setTriggerType("1"); logBo.setTriggerType("1");
} }
@@ -95,6 +98,13 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
return logBo; 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) { private Long queryUserId(String deviceNo) {
AppDeviceVo device = appDeviceService.queryById(deviceNo); AppDeviceVo device = appDeviceService.queryById(deviceNo);
return device == null ? null : device.getUserId(); return device == null ? null : device.getUserId();
@@ -128,6 +138,16 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
// Try the next supported format. // 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); 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.device_no,
d.user_id, d.user_id,
d.device_name, d.device_name,
d.device_init_name,
d.device_img, d.device_img,
d.qrcode, d.qrcode,
d.status, d.status,
@@ -53,6 +54,9 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
<if test="bo.deviceName != null and bo.deviceName != ''"> <if test="bo.deviceName != null and bo.deviceName != ''">
and d.device_name like concat('%', #{bo.deviceName}, '%') and d.device_name like concat('%', #{bo.deviceName}, '%')
</if> </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 != ''"> <if test="bo.deviceImg != null and bo.deviceImg != ''">
and d.device_img = #{bo.deviceImg} and d.device_img = #{bo.deviceImg}
</if> </if>
@@ -101,6 +105,7 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
d.device_no, d.device_no,
d.user_id, d.user_id,
d.device_name, d.device_name,
d.device_init_name,
d.device_img, d.device_img,
d.qrcode, d.qrcode,
d.status, d.status,
@@ -127,6 +132,9 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
<if test="bo.deviceName != null and bo.deviceName != ''"> <if test="bo.deviceName != null and bo.deviceName != ''">
and d.device_name like concat('%', #{bo.deviceName}, '%') and d.device_name like concat('%', #{bo.deviceName}, '%')
</if> </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 != ''"> <if test="bo.deviceImg != null and bo.deviceImg != ''">
and d.device_img = #{bo.deviceImg} and d.device_img = #{bo.deviceImg}
</if> </if>
@@ -174,7 +182,6 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
update app_device update app_device
<set> <set>
user_id = #{userId}, user_id = #{userId},
bind_token_hash = null,
<if test="deviceName != null and deviceName != ''"> <if test="deviceName != null and deviceName != ''">
device_name = #{deviceName}, device_name = #{deviceName},
</if> </if>
@@ -220,6 +227,13 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
""") """)
AppDevice selectByMac(@Param("macAddress") String macAddress); AppDevice selectByMac(@Param("macAddress") String macAddress);
@Select("""
select *
from app_device
where device_init_name = #{deviceInitName}
""")
AppDevice selectByDeviceInitName(@Param("deviceInitName") String deviceInitName);
/** /**
* 仪表盘统计 — 单条 SQL 条件聚合,一次查询返回全部 8 个指标。 * 仪表盘统计 — 单条 SQL 条件聚合,一次查询返回全部 8 个指标。
*/ */

View File

@@ -61,6 +61,15 @@ public interface IDeviceCommandService {
*/ */
String sendScheduleUnbindCommand(String deviceNo, Long scheduleId); 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; 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.*;
import java.util.List;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@Service @Service
public class AppDeviceServiceImpl implements IAppDeviceService { 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 AppDeviceMapper baseMapper;
private final AppSchedulingDeviceMapper schedulingDeviceMapper; private final AppSchedulingDeviceMapper schedulingDeviceMapper;
private final AppWateringLogMapper wateringLogMapper; private final AppWateringLogMapper wateringLogMapper;
@@ -150,43 +163,53 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
return baseMapper.updateById(device) > 0; return baseMapper.updateById(device) > 0;
} }
@Override static byte[] renderQrCodeImage(String content, String deviceNo) {
public Map<String, Object> bindDeviceStatus(AppDeviceBo bo) { try {
Map<String, Object> params = new HashMap<>(); byte[] qrCodeBytes = QrCodeUtil.generatePng(content, QR_CODE_SIZE, QR_CODE_SIZE);
params.put("bindDeviceStatus", 200); BufferedImage qrCodeImage = ImageIO.read(new ByteArrayInputStream(qrCodeBytes));
params.put("bindDeviceStatusName", ""); BufferedImage image = new BufferedImage(
if (bo == null) { QR_CODE_SIZE,
params.put("bindDeviceStatus", 300); QR_CODE_SIZE + QR_CODE_LABEL_HEIGHT,
params.put("bindDeviceStatusName", "设备信息不能为空"); BufferedImage.TYPE_INT_RGB
return params; );
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();
} }
if (StringUtils.isBlank(bo.getMacAddress())) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
params.put("bindDeviceStatus", 301); ImageIO.write(image, "png", outputStream);
params.put("bindDeviceStatusName", "MAC地址不能为空"); return outputStream.toByteArray();
return params; } catch (IOException e) {
throw new ServiceException("设备二维码生成失败");
} }
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; 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();
}
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 @Override
@@ -254,6 +277,70 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
return true; 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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Boolean generateQrCode(Collection<String> deviceNos) { public Boolean generateQrCode(Collection<String> deviceNos) {
@@ -266,7 +353,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
} }
for (AppDevice device : devices) { 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"); UploadResult uploadResult = OssFactory.instance().uploadSuffix(imageBytes, ".png", "image/png");
int rows = baseMapper.update(null, new LambdaUpdateWrapper<AppDevice>() int rows = baseMapper.update(null, new LambdaUpdateWrapper<AppDevice>()
.set(AppDevice::getQrcode, uploadResult.getUrl()) .set(AppDevice::getQrcode, uploadResult.getUrl())
@@ -286,7 +373,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
content.put("deviceNo", device.getDeviceNo()); content.put("deviceNo", device.getDeviceNo());
content.put("mac", device.getMacAddress()); content.put("mac", device.getMacAddress());
content.put("transport", "ble"); content.put("transport", "ble");
content.put("name", device.getDeviceName()); content.put("name", device.getDeviceInitName());
return JsonUtils.toJsonString(content); return JsonUtils.toJsonString(content);
} }
@@ -334,7 +421,9 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
throw new ServiceException("设备编号不能为空"); throw new ServiceException("设备编号不能为空");
} }
Long userId = LoginHelper.getUserId(); boolean platformAdmin = LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin();
Long userId = platformAdmin ? null : LoginHelper.getUserId();
if (!platformAdmin) {
if (userId == null) { if (userId == null) {
throw new ServiceException("用户未登录"); throw new ServiceException("用户未登录");
} }
@@ -347,11 +436,12 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
if (ownedCount == null || ownedCount.intValue() != distinctDeviceNos.size()) { if (ownedCount == null || ownedCount.intValue() != distinctDeviceNos.size()) {
throw new ServiceException("设备不存在或无权操作"); throw new ServiceException("设备不存在或无权操作");
} }
}
boolean flag = baseMapper.delete( boolean flag = baseMapper.delete(
Wrappers.<AppDevice>lambdaUpdate() Wrappers.<AppDevice>lambdaUpdate()
.in(AppDevice::getDeviceNo, distinctDeviceNos) .in(AppDevice::getDeviceNo, distinctDeviceNos)
.eq(AppDevice::getUserId, userId) .eq(!platformAdmin, AppDevice::getUserId, userId)
) > 0; ) > 0;
if (flag) { if (flag) {
schedulingDeviceMapper.delete( schedulingDeviceMapper.delete(
@@ -399,7 +489,6 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
.set(AppDevice::getWorkStatus, "0") .set(AppDevice::getWorkStatus, "0")
.set(AppDevice::getWifiName, null) .set(AppDevice::getWifiName, null)
.set(AppDevice::getWifiPassword, null) .set(AppDevice::getWifiPassword, null)
.set(AppDevice::getBindTokenHash, null)
.in(AppDevice::getDeviceNo, distinctDeviceNos) .in(AppDevice::getDeviceNo, distinctDeviceNos)
.eq(AppDevice::getUserId, userId) .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.dromara.common.mybatis.core.page.TableDataInfo;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Collection; import java.util.*;
import java.util.Date;
import java.util.List;
import java.util.Map;
/** /**
* 浇水记录Service业务层处理 * 浇水记录Service业务层处理
@@ -123,6 +120,7 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
return false; return false;
} }
bo.setTriggerType("0"); bo.setTriggerType("0");
bo.setStatus("0");
if (StringUtils.isBlank(bo.getRemark())) { if (StringUtils.isBlank(bo.getRemark())) {
bo.setRemark("设备离线,按排程自动补记"); bo.setRemark("设备离线,按排程自动补记");
} }
@@ -145,6 +143,18 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
.last("limit 1") .last("limit 1")
); );
if (estimatedLog == null) { 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())) { if (existsScheduleLog(bo.getDeviceNo(), bo.getScheduleId(), bo.getStartTime())) {
return true; return true;
} }
@@ -157,6 +167,54 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService {
return baseMapper.updateById(update) > 0; 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; return 0;
} }
List<AppWateringLog> activeLogs = baseMapper.selectList( AppWateringLog activeLog = baseMapper.selectOne(
Wrappers.<AppWateringLog>lambdaQuery() Wrappers.<AppWateringLog>lambdaQuery()
.eq(AppWateringLog::getDeviceNo, deviceNo) .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; return 0;
} }
int updated = 0;
for (AppWateringLog activeLog : activeLogs) {
LambdaUpdateWrapper<AppWateringLog> updateWrapper = Wrappers.lambdaUpdate(); LambdaUpdateWrapper<AppWateringLog> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(AppWateringLog::getId, activeLog.getId()) updateWrapper.eq(AppWateringLog::getId, activeLog.getId())
.isNull(AppWateringLog::getEndTime)
.set(AppWateringLog::getEndTime, endTime) .set(AppWateringLog::getEndTime, endTime)
.set(AppWateringLog::getStatus, "0")
.set(StringUtils.isNotBlank(remark), AppWateringLog::getRemark, remark); .set(StringUtils.isNotBlank(remark), AppWateringLog::getRemark, remark);
if (activeLog.getStartTime() != null) { if (activeLog.getStartTime() != null) {
long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime(); long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime();
long durationMin = Math.max(durationMillis, 0L) / 60000L; long durationMin = Math.max(durationMillis, 0L) / 60000L;
updateWrapper.set(AppWateringLog::getDurationMin, String.valueOf(durationMin)); updateWrapper.set(AppWateringLog::getDurationMin, String.valueOf(durationMin));
} }
updated += baseMapper.update(null, updateWrapper); return baseMapper.update(null, updateWrapper);
}
return updated;
} }
/** /**

View File

@@ -69,9 +69,10 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
command.setDeviceNo(deviceNo); command.setDeviceNo(deviceNo);
command.setDeviceMac(requireDeviceMac(deviceNo)); command.setDeviceMac(requireDeviceMac(deviceNo));
command.setCommandType("switchDevice"); command.setCommandType("switchDevice");
String commandStartTime = normalizeCommandStartTime(startTime);
command.getPayload().put("deviceNo", deviceNo); command.getPayload().put("deviceNo", deviceNo);
command.getPayload().put("cmd", workStatus); command.getPayload().put("cmd", workStatus);
command.getPayload().put("startTime", startTime); command.getPayload().put("startTime", commandStartTime);
if (durationMin != null) { if (durationMin != null) {
command.getPayload().put("durationMin", durationMin); command.getPayload().put("durationMin", durationMin);
} }
@@ -82,12 +83,12 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
Long userId = LoginHelper.getUserId(); Long userId = LoginHelper.getUserId();
if ("1".equals(workStatus)) { if ("1".equals(workStatus)) {
AppWateringLogBo logBo = buildManualStartLog(deviceNo, userId, commandId, startTime, durationMin); AppWateringLogBo logBo = buildManualStartLog(deviceNo, userId, commandId, commandStartTime, durationMin);
wateringLogService.insertByBo(logBo); wateringLogService.insertByBo(logBo);
} else { } else {
boolean updated = wateringLogService.finishManualLog(deviceNo, userId, new Date(), "手动停止浇水"); int updated = wateringLogService.finishRunningLogsByDevice(deviceNo, new Date(), "手动停止浇水");
if (!updated) { if (updated <= 0) {
log.warn("[命令] 未找到进行中的手动浇水记录 设备编号={} 用户ID={} 停止命令编号={}", log.warn("[命令] 未找到进行中的浇水记录 设备编号={} 用户ID={} 停止命令编号={}",
deviceNo, userId, commandId); deviceNo, userId, commandId);
} }
} }
@@ -168,6 +169,24 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
return sendCommand(command); 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 @Override
public String sendInitDeviceCommand(String deviceNo) { public String sendInitDeviceCommand(String deviceNo) {
AppDeviceVo device = deviceService.queryById(deviceNo); AppDeviceVo device = deviceService.queryById(deviceNo);
@@ -237,6 +256,10 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
return "/" + deviceNo.trim().toLowerCase(Locale.ROOT) + "/subscriber/schedule"; 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) { private void assertSwitchStatus(String workStatus) {
if (!"0".equals(workStatus) && !"1".equals(workStatus)) { if (!"0".equals(workStatus) && !"1".equals(workStatus)) {
throw new ServiceException("设备工作状态只能为0或1"); throw new ServiceException("设备工作状态只能为0或1");
@@ -256,6 +279,7 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
String startTime, String startTime,
Integer durationMin Integer durationMin
) { ) {
Date starttime = parseStartTime(valueAsString(startTime));
AppWateringLogBo logBo = new AppWateringLogBo(); AppWateringLogBo logBo = new AppWateringLogBo();
logBo.setDeviceNo(deviceNo); logBo.setDeviceNo(deviceNo);
logBo.setCommandId(commandId); logBo.setCommandId(commandId);
@@ -265,6 +289,8 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
logBo.setTriggerType("1"); // 手动 logBo.setTriggerType("1"); // 手动
logBo.setRemark("手动开始浇水"); logBo.setRemark("手动开始浇水");
logBo.setCreateTime(new Date()); logBo.setCreateTime(new Date());
logBo.setStatus("1");
logBo.setEndTime(resolveEndTime(starttime, durationMin));
if (StringUtils.isNotEmpty(startTime)) { if (StringUtils.isNotEmpty(startTime)) {
logBo.setStartTime(parseStartTime(startTime)); logBo.setStartTime(parseStartTime(startTime));
@@ -274,7 +300,43 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
return logBo; 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) { 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}$")) { if (startTime.matches("^\\d{2}:\\d{2}$")) {
try { try {
Date parsedTime = new SimpleDateFormat("HH:mm").parse(startTime); Date parsedTime = new SimpleDateFormat("HH:mm").parse(startTime);
@@ -288,13 +350,13 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
today.set(Calendar.MILLISECOND, 0); today.set(Calendar.MILLISECOND, 0);
return today.getTime(); return today.getTime();
} catch (ParseException e) { } 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 { try {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(startTime); return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(startTime);
} catch (ParseException e) { } 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");
} }
} }
} }

View File

@@ -65,7 +65,7 @@ public class SysUser extends TenantEntity {
/** /**
* 用户头像 * 用户头像
*/ */
private Long avatar; private String avatar;
/** /**
* 密码 * 密码

View File

@@ -72,6 +72,8 @@ public class SysUserBo extends BaseEntity {
*/ */
private String sex; private String sex;
private String avatar;
/** /**
* 密码 * 密码
*/ */

View File

@@ -1,14 +1,12 @@
package org.dromara.system.domain.vo; package org.dromara.system.domain.vo;
import com.fasterxml.jackson.annotation.JsonIgnore; import io.github.linpeilie.annotations.AutoMapper;
import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data;
import org.dromara.common.sensitive.annotation.Sensitive; import org.dromara.common.sensitive.annotation.Sensitive;
import org.dromara.common.sensitive.core.SensitiveStrategy; import org.dromara.common.sensitive.core.SensitiveStrategy;
import org.dromara.common.translation.annotation.Translation; import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.constant.TransConstant; import org.dromara.common.translation.constant.TransConstant;
import org.dromara.system.domain.SysUser; import org.dromara.system.domain.SysUser;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
@@ -78,8 +76,8 @@ public class SysUserVo implements Serializable {
/** /**
* 头像地址 * 头像地址
*/ */
@Translation(type = TransConstant.OSS_ID_TO_URL) //@Translation(type = TransConstant.OSS_ID_TO_URL)
private Long avatar; private String avatar;
/** /**
* 密码 * 密码