设备心跳调整 app新增增设备时绑定联网设备

This commit is contained in:
yuhaiming
2026-06-13 10:51:45 +08:00
parent c7f9df980a
commit c74363afa4
65 changed files with 1622 additions and 592 deletions

View File

@@ -10,15 +10,19 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.bo.*;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.bo.AppScheduleBo;
import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.mqtt.DeviceCommand;
import org.dromara.app.domain.vo.*;
import org.dromara.app.service.*;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.app.service.impl.AppScheduleServiceImpl;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
@@ -77,16 +81,15 @@ public class AppController extends BaseController {
*
*/
@ApiEncrypt
@PostMapping("/addDevice")
public R<Void> addDevice(@RequestBody String body) {
AppDeviceBo appDevic = JsonUtils.parseObject(body, AppDeviceBo.class);
appDevic.setUserId(LoginHelper.getUserId());
Boolean b = appDeviceService.insertByBo(appDevic);
if (b){
return R.ok();
}else {
return R.fail();
@PostMapping("/bindDeviceStatus")
public R<Map> bindDeviceStatus(@RequestBody String body) {
try {
AppDeviceBo appDevice = JsonUtils.parseObject(body, AppDeviceBo.class);
appDevice.setUserId(LoginHelper.getUserId());
Map<String, Object> map = appDeviceService.bindDeviceStatus(appDevice);
return R.ok(map);
} catch (Exception e) {
return fail(e);
}
}
@@ -94,11 +97,15 @@ public class AppController extends BaseController {
* 绑定已通过MQTT注册上线的设备
*/
@ApiEncrypt
@PostMapping("/bindDevice")
@PostMapping("/addDevice")
public R<AppDeviceVo> bindDevice(@RequestBody String body) {
AppDeviceBo appDevice = JsonUtils.parseObject(body, AppDeviceBo.class);
appDevice.setUserId(LoginHelper.getUserId());
return R.ok(appDeviceService.bindRegisteredDevice(appDevice));
try {
AppDeviceBo appDevice = JsonUtils.parseObject(body, AppDeviceBo.class);
appDevice.setUserId(LoginHelper.getUserId());
return R.ok(appDeviceService.bindRegisteredDevice(appDevice));
} catch (Exception e) {
return fail(e);
}
}
/**
@@ -110,7 +117,11 @@ public class AppController extends BaseController {
@GetMapping("/device/{deviceNo}")
public R<AppDeviceVo> getDeviceInfo(@NotNull(message = "主键不能为空")
@PathVariable String deviceNo) {
return R.ok(appDeviceService.queryById(deviceNo));
try {
return R.ok(getOwnedDevice(deviceNo));
} catch (Exception e) {
return fail(e);
}
}
@@ -123,9 +134,11 @@ public class AppController extends BaseController {
@PutMapping("/updateDeviceInfo")
public R<Void> updateDeviceInfo(@Validated @RequestBody AppDeviceBo bo) {
try {
assertDeviceOwned(bo.getDeviceNo());
bo.setUserId(LoginHelper.getUserId());
return toAjax(appDeviceService.updateByBo(bo));
} catch (Exception e) {
throw new RuntimeException(e);
return fail(e);
}
}
@@ -137,7 +150,14 @@ public class AppController extends BaseController {
@DeleteMapping("/deleteDevice/{deviceNos}")
public R<Void> removeDevice(@NotEmpty(message = "主键不能为空")
@PathVariable String[] deviceNos) {
return toAjax(appDeviceService.deleteWithValidByIds(List.of(deviceNos), true));
try {
for (String deviceNo : deviceNos) {
assertDeviceOwned(deviceNo);
}
return toAjax(appDeviceService.deleteWithValidByIds(List.of(deviceNos), true));
} catch (Exception e) {
return fail(e);
}
}
@@ -149,11 +169,11 @@ public class AppController extends BaseController {
@RepeatSubmit()
@PutMapping("/switchDevice")
public R<Void> switchDevice(@RequestBody String body) {
Map<String,String> map = JsonUtils.parseObject(body, Map.class);
try {
Map<String,String> map = JsonUtils.parseObject(body, Map.class);
String deviceNo = map.get("deviceNo");
String status = map.get("workStatus");
Date date = new Date();
String startTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
String durationMinValue = map.get("durationMin");
Integer durationMin = StringUtils.isBlank(durationMinValue) ? null : Integer.valueOf(durationMinValue);
//更新设备工作状态
@@ -161,7 +181,7 @@ public class AppController extends BaseController {
// 记录信息到历史表
return toAjax(flag);
} catch (Exception e) {
return R.fail(e.getMessage());
return fail(e);
}
}
@@ -173,28 +193,32 @@ public class AppController extends BaseController {
*/
@GetMapping("/scheduleDeviceList/{scheduleId}")
public R<Map<String,Object>> scheduleDeviceList( @PathVariable Long scheduleId) {
try {
Map retMap = new HashMap();
//所有设备列表
AppDeviceBo appDeviceBo = new AppDeviceBo();
appDeviceBo.setUserId(LoginHelper.getUserId());
List<AppDeviceVo> appDeviceVos = appDeviceService.queryList(appDeviceBo);
Map retMap = new HashMap();
//所有设备列表
AppDeviceBo appDeviceBo = new AppDeviceBo();
appDeviceBo.setUserId(LoginHelper.getUserId());
List<AppDeviceVo> appDeviceVos = appDeviceService.queryList(appDeviceBo);
List<AppDeviceVo> appDeviceVoList = new ArrayList();
AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
schedulingDeviceBo.setScheduleId(scheduleId);
List<AppDeviceVo> appDeviceVoList = new ArrayList();
AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
schedulingDeviceBo.setScheduleId(scheduleId);
for (AppDeviceVo appDeviceVo:appDeviceVos){
List<AppSchedulingDeviceVo> appSchedulingDeviceVos = appSchedulingDeviceService.findByDeviceNo(appDeviceVo.getDeviceNo());
if(appSchedulingDeviceVos.size() == 0){
appDeviceVoList.add(appDeviceVo);
for (AppDeviceVo appDeviceVo:appDeviceVos){
List<AppSchedulingDeviceVo> appSchedulingDeviceVos = appSchedulingDeviceService.findByDeviceNo(appDeviceVo.getDeviceNo());
if(appSchedulingDeviceVos.size() == 0){
appDeviceVoList.add(appDeviceVo);
}
}
retMap.put("deviceList", appDeviceVoList);
return R.ok(retMap);
} catch (Exception e) {
return fail(e);
}
retMap.put("deviceList", appDeviceVoList);
return R.ok(retMap);
}
/**
*绑定排程跟设备
@@ -202,26 +226,32 @@ public class AppController extends BaseController {
@RepeatSubmit()
@PostMapping("/addScheduleDevice")
public R<Void> addScheduleDevice( @RequestBody String body) {
Map<String, Object> map = JsonUtils.parseObject(body, Map.class);
if (map == null || map.get("scheduleId") == null) {
throw new ServiceException("scheduleId不能为空");
}
Long scheduleId = Long.valueOf(map.get("scheduleId").toString());
List<String> deviceNos = parseDeviceNos(map);
if (deviceNos.isEmpty()) {
throw new ServiceException("deviceNos不能为空");
}
for (String deviceNo:deviceNos){
if (StringUtils.isBlank(deviceNo)) {
continue;
try {
Map<String, Object> map = JsonUtils.parseObject(body, Map.class);
if (map == null || map.get("scheduleId") == null) {
throw new ServiceException("app.schedule.id.not.blank.request");
}
AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
schedulingDeviceBo.setDeviceNo(deviceNo);
schedulingDeviceBo.setScheduleId(scheduleId);
appSchedulingDeviceService.insertByBo(schedulingDeviceBo);
Long scheduleId = Long.valueOf(map.get("scheduleId").toString());
assertScheduleOwned(scheduleId);
List<String> deviceNos = parseDeviceNos(map);
if (deviceNos.isEmpty()) {
throw new ServiceException("app.schedule.devices.not.blank");
}
for (String deviceNo:deviceNos){
if (StringUtils.isBlank(deviceNo)) {
continue;
}
assertDeviceOwned(deviceNo);
AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
schedulingDeviceBo.setDeviceNo(deviceNo);
schedulingDeviceBo.setScheduleId(scheduleId);
appSchedulingDeviceService.insertByBo(schedulingDeviceBo);
}
return R.ok();
} catch (Exception e) {
return fail(e);
}
return R.ok();
}
private List<String> parseDeviceNos(Map<String, Object> map) {
@@ -255,9 +285,22 @@ public class AppController extends BaseController {
@Log(title = "排程与设备关联", businessType = BusinessType.DELETE)
@DeleteMapping("/deleteScheduleDevice")
public R<Void> deleteScheduleDevice(@NotEmpty(message = "主键不能为空")
@RequestParam String scheduleId, @RequestParam String deviceNo) {
return toAjax(appSchedulingDeviceService.deleteWithValidByScheduleIdAndDeviceNo(Long.valueOf(scheduleId), deviceNo));
public R<Void> deleteScheduleDevice(@RequestParam(required = false) String scheduleId,
@RequestParam(required = false) String deviceNo) {
try {
if (StringUtils.isBlank(scheduleId)) {
throw new ServiceException("app.schedule.id.not.blank.request");
}
if (StringUtils.isBlank(deviceNo)) {
throw new ServiceException("app.device.no.not.blank");
}
Long scheduleIdValue = Long.valueOf(scheduleId);
assertScheduleOwned(scheduleIdValue);
assertDeviceOwned(deviceNo);
return toAjax(appSchedulingDeviceService.deleteWithValidByScheduleIdAndDeviceNo(scheduleIdValue, deviceNo));
} catch (Exception e) {
return fail(e);
}
}
@@ -289,8 +332,8 @@ public class AppController extends BaseController {
}
return R.fail();
// return toAjax(appScheduleService.insertByBo(bo));
} catch (RuntimeException e) {
throw new RuntimeException(e);
} catch (Exception e) {
return fail(e);
}
}
@@ -303,48 +346,52 @@ public class AppController extends BaseController {
@GetMapping("/schedule/{id}")
public R<AppScheduleVo> getScheduleInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
AppScheduleVo appScheduleVo = appScheduleService.queryById(id);
if (ObjectUtil.isNotNull(appScheduleVo)){
List<AppScheduleDetailVo> list = appScheduleDetailService.queryByScheduleIdByStatus(appScheduleVo.getId());
try {
AppScheduleVo appScheduleVo = getOwnedSchedule(id);
if (ObjectUtil.isNotNull(appScheduleVo)){
List<AppScheduleDetailVo> list = appScheduleDetailService.queryByScheduleIdByStatus(appScheduleVo.getId());
AppSchedulingDeviceBo appSchedulingDeviceBo = new AppSchedulingDeviceBo();
appSchedulingDeviceBo.setScheduleId(appScheduleVo.getId());
List<AppSchedulingDeviceVo> appSchedulingDeviceVos = appSchedulingDeviceService.queryList(appSchedulingDeviceBo);
List deviceList = new ArrayList();
for (AppSchedulingDeviceVo appSchedulingDeviceVo:appSchedulingDeviceVos){
AppDeviceVo appDeviceVo = appDeviceService.queryById(appSchedulingDeviceVo.getDeviceNo());
if (ObjectUtil.isNotNull(appDeviceVo)){
deviceList.add(appDeviceVo);
AppSchedulingDeviceBo appSchedulingDeviceBo = new AppSchedulingDeviceBo();
appSchedulingDeviceBo.setScheduleId(appScheduleVo.getId());
List<AppSchedulingDeviceVo> appSchedulingDeviceVos = appSchedulingDeviceService.queryList(appSchedulingDeviceBo);
List deviceList = new ArrayList();
for (AppSchedulingDeviceVo appSchedulingDeviceVo:appSchedulingDeviceVos){
AppDeviceVo appDeviceVo = appDeviceService.queryById(appSchedulingDeviceVo.getDeviceNo());
if (ObjectUtil.isNotNull(appDeviceVo)){
deviceList.add(appDeviceVo);
}
}
List detailList = new ArrayList();
for (AppScheduleDetailVo detail:list){
List timeSlots = new ArrayList();
JSONArray mapArray = JSONUtil.parseArray(detail.getTimeData());
for (Object timeSlot:mapArray){
timeSlots.add(timeSlot);
}
Map map = new HashMap();
map.put("id",detail.getId() );
map.put("weekday",detail.getWeekday() );
map.put("timeData",timeSlots );
map.put("triggerType",detail.getTriggerType() );
map.put("status",detail.getStatus() );
detailList.add(map);
}
}
List detailList = new ArrayList();
for (AppScheduleDetailVo detail:list){
List timeSlots = new ArrayList();
JSONArray mapArray = JSONUtil.parseArray(detail.getTimeData());
for (Object timeSlot:mapArray){
timeSlots.add(timeSlot);
}
Map map = new HashMap();
map.put("id",detail.getId() );
map.put("weekday",detail.getWeekday() );
map.put("timeData",timeSlots );
map.put("triggerType",detail.getTriggerType() );
map.put("status",detail.getStatus() );
detailList.add(map);
appScheduleVo.setDetails(detailList);
appScheduleVo.setDeviceNos(deviceList);
return R.ok(appScheduleVo);
}
appScheduleVo.setDetails(detailList);
appScheduleVo.setDeviceNos(deviceList);
return R.ok(appScheduleVo);
return R.fail(appScheduleVo);
} catch (Exception e) {
return fail(e);
}
return R.fail(appScheduleVo);
}
@@ -356,8 +403,13 @@ public class AppController extends BaseController {
@PutMapping("/editScheduleStatus")
public R<Void> editScheduleStatus(@NotNull(message = "主键不能为空")
@RequestBody AppScheduleBo bo) {
return toAjax(appScheduleService.updateStatusByBo(bo));
try {
assertScheduleOwned(bo.getId());
bo.setUserId(LoginHelper.getUserId());
return toAjax(appScheduleService.updateStatusByBo(bo));
} catch (Exception e) {
return fail(e);
}
}
/**
@@ -367,11 +419,17 @@ public class AppController extends BaseController {
@RepeatSubmit()
@PutMapping("/updataschedule")
public R<Object> editSchedule(@Validated(EditGroup.class) @RequestBody AppScheduleBo bo) {
Map<String, Object> map = appScheduleService.updateByBo(bo);
if (Boolean.valueOf(map.get("flag").toString())){
return R.ok(map.get("appSchedule"));
}else {
return R.fail();
try {
assertScheduleOwned(bo.getId());
bo.setUserId(LoginHelper.getUserId());
Map<String, Object> map = appScheduleService.updateByBo(bo);
if (Boolean.valueOf(map.get("flag").toString())){
return R.ok(map.get("appSchedule"));
}else {
return R.fail();
}
} catch (Exception e) {
return fail(e);
}
}
@@ -386,7 +444,14 @@ public class AppController extends BaseController {
@DeleteMapping("/deleteschedule/{ids}")
public R<Void> removeSchedule(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(appScheduleService.deleteWithValidByIds(List.of(ids), true));
try {
for (Long id : ids) {
assertScheduleOwned(id);
}
return toAjax(appScheduleService.deleteWithValidByIds(List.of(ids), true));
} catch (Exception e) {
return fail(e);
}
}
/**
@@ -427,7 +492,14 @@ public class AppController extends BaseController {
@DeleteMapping("/waterLog/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(appWateringLogService.deleteWithValidByIds(List.of(ids), true));
try {
for (Long id : ids) {
assertWateringLogOwned(id);
}
return toAjax(appWateringLogService.deleteWithValidByIds(List.of(ids), true));
} catch (Exception e) {
return fail(e);
}
}
@@ -463,7 +535,7 @@ public class AppController extends BaseController {
map.put("averageTime",averageTime);
return R.ok(map);
} catch (Exception e) {
throw new RuntimeException(e);
return fail(e);
}
}
@@ -475,16 +547,20 @@ public class AppController extends BaseController {
*/
@GetMapping("/getUserInfo")
public R<UserInfoVo> getUserInfo() {
UserInfoVo userInfoVo = new UserInfoVo();
LoginUser loginUser = LoginHelper.getLoginUser();
SysUserVo user = DataPermissionHelper.ignore(() -> userService.selectUserById(loginUser.getUserId()));
if (ObjectUtil.isNull(user)) {
return R.fail("没有权限访问用户数据!");
try {
UserInfoVo userInfoVo = new UserInfoVo();
LoginUser loginUser = LoginHelper.getLoginUser();
SysUserVo user = DataPermissionHelper.ignore(() -> userService.selectUserById(loginUser.getUserId()));
if (ObjectUtil.isNull(user)) {
return R.fail(MessageUtils.message("app.user.data.denied"));
}
userInfoVo.setUser(user);
userInfoVo.setPermissions(loginUser.getMenuPermission());
userInfoVo.setRoles(loginUser.getRolePermission());
return R.ok(userInfoVo);
} catch (Exception e) {
return fail(e);
}
userInfoVo.setUser(user);
userInfoVo.setPermissions(loginUser.getMenuPermission());
userInfoVo.setRoles(loginUser.getRolePermission());
return R.ok(userInfoVo);
}
/**
@@ -495,13 +571,17 @@ public class AppController extends BaseController {
@RepeatSubmit()
@PutMapping("/updataUser")
public R<Void> editUser( @RequestBody SysUserBo user) {
userService.checkUserAllowed(user.getUserId());
userService.checkUserDataScope(user.getUserId());
if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
return R.fail("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
}
try {
userService.checkUserAllowed(user.getUserId());
userService.checkUserDataScope(user.getUserId());
if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
return R.fail(MessageUtils.message("app.user.phone.exists", user.getUserName()));
}
return toAjax(userService.updateAppUser(user));
return toAjax(userService.updateAppUser(user));
} catch (Exception e) {
return fail(e);
}
}
/**
@@ -510,15 +590,19 @@ public class AppController extends BaseController {
@RepeatSubmit()
@PutMapping("/updataPassword")
public R<Void> updataPassword(@RequestBody SysUserVo vo) {
Long userId = LoginHelper.getUserId();
SysUserVo sysUserVo = userService.selectUserById(userId);
if (!BCrypt.checkpw(vo.getOldPassword(), sysUserVo.getPassword())){
throw new UserException("user.password.not.match");
try {
Long userId = LoginHelper.getUserId();
SysUserVo sysUserVo = userService.selectUserById(userId);
if (!BCrypt.checkpw(vo.getOldPassword(), sysUserVo.getPassword())){
throw new UserException("user.password.not.match");
}
SysUserBo userBo = new SysUserBo();
userBo.setUserId(userId);
userBo.setPassword(vo.getPassword());
return toAjax(userService.updateUserPas(userBo));
} catch (Exception e) {
return fail(e);
}
SysUserBo userBo = new SysUserBo();
userBo.setUserId(userId);
userBo.setPassword(vo.getPassword());
return toAjax(userService.updateUserPas(userBo));
}
/**
@@ -527,35 +611,99 @@ public class AppController extends BaseController {
@ApiEncrypt
@PostMapping("/retrievePassword")
public R<Void> forgot(@RequestBody String body) {
SysUserBo bo = JsonUtils.parseObject(body, SysUserBo.class);
bo.setUserId(LoginHelper.getUserId());
try {
SysUserBo bo = JsonUtils.parseObject(body, SysUserBo.class);
bo.setUserId(LoginHelper.getUserId());
if(Validator.isEmail(bo.getUserName())){
boolean checkPhoneFlag = userService.checkEmailUnique(bo);
if (!checkPhoneFlag){
throw new UserException("user.email.not.username");
}
}else if (Validator.isMobile(bo.getUserName())){
boolean checkPhoneFlag = userService.checkPhoneUnique(bo);
if (!checkPhoneFlag){
throw new UserException("user.mobile.phone.number.not.username");
if(Validator.isEmail(bo.getUserName())){
boolean checkPhoneFlag = userService.checkEmailUnique(bo);
if (!checkPhoneFlag){
throw new UserException("user.email.not.username");
}
}else if (Validator.isMobile(bo.getUserName())){
boolean checkPhoneFlag = userService.checkPhoneUnique(bo);
if (!checkPhoneFlag){
throw new UserException("user.mobile.phone.number.not.username");
}
}
return toAjax(userService.updateUserPas(bo));
} catch (Exception e) {
return fail(e);
}
return toAjax(userService.updateUserPas(bo));
}
@GetMapping("/test")
public R<Void> test (){
try {
DeviceCommand deviceCommand = new DeviceCommand();
DeviceCommand deviceCommand = new DeviceCommand();
String bindDevice = deviceCommandService.sendBindDeviceCommand("01");
String bindDevice = deviceCommandService.sendBindDeviceCommand("01");
// String s = deviceCommandService.sendSwitchCommand("01", "1", "2026-06-10 14:35:31", 20);
return R.ok(bindDevice);
// String s = deviceCommandService.sendSwitchCommand("01", "1", "2026-06-10 14:35:31", 20);
return R.ok(bindDevice);
} catch (Exception e) {
return fail(e);
}
}
private <T> R<T> fail(Exception e) {
return R.fail(toI18nErrorMessage(e));
}
private String toI18nErrorMessage(Exception e) {
String message = e.getMessage();
if (StringUtils.isBlank(message)) {
return MessageUtils.message("operation.fail");
}
String i18nMessage = MessageUtils.message(message);
return message.equals(i18nMessage) ? message : i18nMessage;
}
private AppDeviceVo getOwnedDevice(String deviceNo) {
if (StringUtils.isBlank(deviceNo)) {
throw new ServiceException("app.device.no.not.blank");
}
AppDeviceVo device = appDeviceService.queryById(deviceNo);
Long userId = LoginHelper.getUserId();
if (device == null || !Objects.equals(device.getUserId(), userId)) {
throw new ServiceException("app.device.not.exists.or.denied");
}
return device;
}
private void assertDeviceOwned(String deviceNo) {
getOwnedDevice(deviceNo);
}
private AppScheduleVo getOwnedSchedule(Long scheduleId) {
if (scheduleId == null) {
throw new ServiceException("app.schedule.id.not.blank");
}
AppScheduleVo schedule = appScheduleService.queryById(scheduleId);
Long userId = LoginHelper.getUserId();
if (schedule == null || !Objects.equals(schedule.getUserId(), userId)) {
throw new ServiceException("app.schedule.not.exists.or.denied");
}
return schedule;
}
private void assertScheduleOwned(Long scheduleId) {
getOwnedSchedule(scheduleId);
}
private void assertWateringLogOwned(Long id) {
if (id == null) {
throw new ServiceException("app.watering.log.id.not.blank");
}
AppWateringLogVo log = appWateringLogService.queryById(id);
Long userId = LoginHelper.getUserId();
if (log == null || !Objects.equals(log.getUserId(), userId)) {
throw new ServiceException("app.watering.log.not.exists.or.denied");
}
}
}

View File

@@ -1,14 +1,22 @@
package org.dromara.app.handler;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.mapper.AppDeviceMapper;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@@ -22,6 +30,13 @@ public class DeviceDataHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/power$");
private final IAppDeviceService appDeviceService;
private final AppDeviceMapper appDeviceMapper;
@Value("${mqtt.command-ack.device-status-cache-prefix}")
private String deviceStatusCachePrefix;
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds}")
private int deviceStatusCacheTtlSeconds;
@Override
public Pattern topicPattern() {
@@ -32,14 +47,44 @@ public class DeviceDataHandler implements MqttTopicHandler {
public void handle(String deviceNo, String payload) {
try {
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
if (dto == null || dto.get("powerLevel") == null) {
log.warn("[MQTT] 设备电量上报缺少电量字段 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
refreshDeviceOnline(deviceNo);
return;
}
// 更新设备电量 + 同步在线状态到数据库
AppDeviceBo appDeviceBo = new AppDeviceBo();
appDeviceBo.setDeviceNo(deviceNo);
appDeviceBo.setPowerLevel(dto.get("powerLevel").toString());
appDeviceBo.setPowerLevelUpdatatime(new Date());
appDeviceBo.setStatus("1"); // 收到数据 = 在线
appDeviceService.updateByBo(appDeviceBo);
log.info("[MQTT] 设备电量更新 deviceNo={} payload={}", deviceNo, payload);
// 同步刷新 Redis 在线缓存(定时任务离线检测依赖此 Key
refreshDeviceOnline(deviceNo);
log.info("[MQTT] 设备电量更新 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
} catch (Exception e) {
log.error("[MQTT] 设备电量更新失败 deviceNo={} payload={}", deviceNo, payload, e);
log.error("[MQTT] 设备电量更新失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
}
}
/**
* 刷新设备在线状态到 Redis与 MqttCommandAckService 逻辑一致)
* 写入 Redis 后 TTL 到期自动过期 = 离线
*/
private void refreshDeviceOnline(String deviceNo) {
Map<String, Object> statusCache = new HashMap<>();
statusCache.put("deviceNo", deviceNo);
statusCache.put("status", "1");
statusCache.put("lastReportTime", Instant.now().toString());
RedisUtils.setCacheObject(
deviceStatusCachePrefix + deviceNo,
statusCache,
Duration.ofSeconds(deviceStatusCacheTtlSeconds)
);
appDeviceMapper.update(null,
new LambdaUpdateWrapper<AppDevice>()
.set(AppDevice::getStatus, "1")
.eq(AppDevice::getDeviceNo, deviceNo)
);
}
}

View File

@@ -1,13 +1,21 @@
package org.dromara.app.handler;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.mapper.AppDeviceMapper;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@@ -22,6 +30,13 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/register$");
private final IAppDeviceService appDeviceService;
private final AppDeviceMapper appDeviceMapper;
@Value("${mqtt.command-ack.device-status-cache-prefix}")
private String deviceStatusCachePrefix;
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds}")
private int deviceStatusCacheTtlSeconds;
@Override
public Pattern topicPattern() {
@@ -34,15 +49,18 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
AppDeviceBo device = buildRegisterDevice(deviceNo, dto);
appDeviceService.registerByMqtt(device);
log.info("[MQTT] 设备注册 deviceNo={} payload={}", deviceNo, payload);
// 注册即在线
refreshDeviceOnline(deviceNo);
log.info("[MQTT] 设备注册 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
} catch (Exception e) {
log.error("[MQTT] 设备注册失败 deviceNo={} payload={}", deviceNo, payload, e);
log.error("[MQTT] 设备注册失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
}
}
private AppDeviceBo buildRegisterDevice(String topicDeviceNo, Map<String, Object> dto) {
AppDeviceBo device = new AppDeviceBo();
device.setDeviceNo(topicDeviceNo);
device.setStatus("1"); // 注册即在线
if (dto == null) {
return device;
}
@@ -67,4 +85,24 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
private String valueAsString(Object value) {
return value == null ? null : String.valueOf(value);
}
/**
* 刷新设备在线状态到 Redis
*/
private void refreshDeviceOnline(String deviceNo) {
Map<String, Object> statusCache = new HashMap<>();
statusCache.put("deviceNo", deviceNo);
statusCache.put("status", "1");
statusCache.put("lastReportTime", Instant.now().toString());
RedisUtils.setCacheObject(
deviceStatusCachePrefix + deviceNo,
statusCache,
Duration.ofSeconds(deviceStatusCacheTtlSeconds)
);
appDeviceMapper.update(null,
new LambdaUpdateWrapper<AppDevice>()
.set(AppDevice::getStatus, "1")
.eq(AppDevice::getDeviceNo, deviceNo)
);
}
}

View File

@@ -28,9 +28,9 @@ public class ErromesHandler implements MqttTopicHandler {
public void handle(String deviceNo, String payload) {
try {
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
log.info("[MQTT] 设备异常告警 deviceNo={} payload={}", deviceNo, dto);
log.info("[MQTT] 设备异常告警 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, dto);
} catch (Exception e) {
log.error("[MQTT] 设备异常处理失败 deviceNo={}", deviceNo, e);
log.error("[MQTT] 设备异常处理失败 时间={} 设备编号={}", HandlerLogTime.now(), deviceNo, e);
}
}
}

View File

@@ -0,0 +1,13 @@
package org.dromara.app.handler;
import org.dromara.common.core.utils.DateUtils;
final class HandlerLogTime {
private HandlerLogTime() {
}
static String now() {
return DateUtils.getTime();
}
}

View File

@@ -41,16 +41,16 @@ public class KeyFinishHandler implements MqttTopicHandler {
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
appWateringLogService.insertByBo(logBo);
log.info("[MQTT] 按键浇水完成上报已保存 deviceNo={} payload={}", deviceNo, payload);
log.info("[MQTT] 按键浇水完成上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
} catch (Exception e) {
log.error("[MQTT] 按键浇水完成上报处理失败 deviceNo={} payload={}", deviceNo, payload, e);
log.error("[MQTT] 按键浇水完成上报处理失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
}
}
private AppWateringLogBo buildWateringLog(String topicDeviceNo, Map<String, Object> dto) {
String payloadDeviceNo = valueAsString(dto.get("deviceNo"));
if (StringUtils.isNotBlank(payloadDeviceNo) && !topicDeviceNo.equals(payloadDeviceNo)) {
log.warn("[MQTT] 按键浇水完成上报设备编号不一致 topicDeviceNo={} payloadDeviceNo={}", topicDeviceNo, payloadDeviceNo);
log.warn("[MQTT] 按键浇水完成上报设备编号不一致 时间={} 主题设备编号={} 消息体设备编号={}", HandlerLogTime.now(), topicDeviceNo, payloadDeviceNo);
}
Date receivedAt = new Date();
@@ -127,7 +127,7 @@ public class KeyFinishHandler implements MqttTopicHandler {
}
long diffMillis = endTime.getTime() - startTime.getTime();
if (diffMillis < 0) {
log.warn("[MQTT] 按键浇水完成上报结束时间早于开始时间 startTime={} endTime={}", startTime, endTime);
log.warn("[MQTT] 按键浇水完成上报结束时间早于开始时间 时间={} 开始时间={} 结束时间={}", HandlerLogTime.now(), startTime, endTime);
return 0;
}
return (int) (diffMillis / 60000L);

View File

@@ -1,8 +1,8 @@
package org.dromara.app.handler;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.vo.AppDeviceVo;
@@ -46,16 +46,16 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
Map<String, Object> dto = JsonUtils.parseObject(payload, Map.class);
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
appWateringLogService.insertByBo(logBo);
log.info("[MQTT] 浇水排程上报 saved deviceNo={} payload={}", deviceNo, payload);
log.info("[MQTT] 浇水排程上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
} catch (Exception e) {
log.error("[MQTT] 浇水排程上报 failed deviceNo={} payload={}", deviceNo, payload, e);
log.error("[MQTT] 浇水排程上报处理失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
}
}
private AppWateringLogBo buildWateringLog(String topicDeviceNo, Map<String, Object> dto) {
String payloadDeviceNo = valueAsString(dto.get("deviceNo"));
if (StringUtils.isNotBlank(payloadDeviceNo) && !topicDeviceNo.equals(payloadDeviceNo)) {
log.warn("[MQTT] 排程完成上报设备编号不一致 topicDeviceNo={} payloadDeviceNo={}", topicDeviceNo, payloadDeviceNo);
log.warn("[MQTT] 排程完成上报设备编号不一致 时间={} 主题设备编号={} 消息体设备编号={}", HandlerLogTime.now(), topicDeviceNo, payloadDeviceNo);
}
@@ -96,11 +96,11 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
.eq(AppSchedulingDevice::getDeviceNo, deviceNo)
);
if (schedulingDevices == null || schedulingDevices.isEmpty()) {
log.warn("[MQTT] 排程完成上报未找到设备绑定的排程 deviceNo={}", deviceNo);
log.warn("[MQTT] 排程完成上报未找到设备绑定的排程 时间={} 设备编号={}", HandlerLogTime.now(), deviceNo);
return 0L;
}
if (schedulingDevices.size() > 1) {
log.warn("[MQTT] 排程完成上报匹配到多个排程 deviceNo={} count={}", deviceNo, schedulingDevices.size());
log.warn("[MQTT] 排程完成上报匹配到多个排程 时间={} 设备编号={} 数量={}", HandlerLogTime.now(), deviceNo, schedulingDevices.size());
}
return schedulingDevices.get(0).getScheduleId();
}
@@ -148,7 +148,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
}
long diffMillis = endTime.getTime() - startTime.getTime();
if (diffMillis < 0) {
log.warn("[MQTT] 排程完成上报结束时间早于开始时间 startTime={} endTime={}", startTime, endTime);
log.warn("[MQTT] 排程完成上报结束时间早于开始时间 时间={} 开始时间={} 结束时间={}", HandlerLogTime.now(), startTime, endTime);
return 0;
}
return (int) (diffMillis / 60000L);

View File

@@ -1,12 +1,11 @@
package org.dromara.app.mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 设备信息
@@ -18,26 +17,38 @@ Mapper接口
public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo> {
@Update("""
<script>
update app_device
<set>
user_id = #{userId},
bind_token_hash = null,
<if test="deviceName != null and deviceName != ''">
device_name = #{deviceName},
</if>
<if test="status != null and status != ''">
status = #{status},
</if>
</set>
where device_no = #{deviceNo}
and (user_id is null or user_id = #{userId})
</script>
""")
<script>
update app_device
<set>
user_id = #{userId},
bind_token_hash = null,
<if test="deviceName != null and deviceName != ''">
device_name = #{deviceName},
</if>
<if test="status != null and status != ''">
status = #{status},
</if>
<if test="workStatus != null and workStatus != ''">
work_status = #{workStatus},
</if>
<if test="wifiName != null and wifiName != ''">
wifi_name = #{wifiName},
</if>
<if test="wifiPassword != null and wifiPassword != ''">
wifi_password = #{wifiPassword},
</if>
</set>
where device_no = #{deviceNo}
and (user_id is null or user_id = #{userId})
</script>
""")
int bindIfAvailable(@Param("deviceNo") String deviceNo,
@Param("userId") Long userId,
@Param("deviceName") String deviceName,
@Param("status") String status);
@Param("status") String status,
@Param("workStatus") String workStatus,
@Param("wifiName") String wifiName,
@Param("wifiPassword") String wifiPassword);
@Update("""
update app_device
@@ -49,4 +60,11 @@ public interface AppDeviceMapper extends BaseMapperPlus<AppDevice, AppDeviceVo>
@Param("userId") Long userId,
@Param("workStatus") String workStatus);
@Select("""
select *
from app_device
where mac_address = #{macAddress}
""")
AppDevice selectByMac(@Param("macAddress") String macAddress);
}

View File

@@ -15,13 +15,13 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
public interface AppWateringLogMapper extends BaseMapperPlus<AppWateringLog, AppWateringLogVo> {
@Select("SELECT count(id) FROM app_watering_log\n" +
"WHERE user_id=${userId} AND YEAR(create_time) = YEAR(CURDATE()) AND MONTH(create_time) = MONTH(CURDATE());")
"WHERE user_id=#{userId} AND YEAR(create_time) = YEAR(CURDATE()) AND MONTH(create_time) = MONTH(CURDATE());")
Long selectByMonCout(@Param("userId") Long userId);
@Select("SELECT COUNT(id) \n" +
"FROM app_watering_log \n" +
"WHERE user_id=${userId} AND WEEK(create_time) = WEEK(CURDATE());")
"WHERE user_id=#{userId} AND WEEK(create_time) = WEEK(CURDATE());")
Long selectByWeekCount(@Param("userId") Long userId);
@@ -29,11 +29,11 @@ public interface AppWateringLogMapper extends BaseMapperPlus<AppWateringLog, App
@Select("SELECT COUNT(id) \n" +
"FROM app_watering_log \n" +
"WHERE user_id=${userId} AND trigger_type =${triggerType}")
Long selectByTriggerTypeCount(Long userId, String triggerType);
"WHERE user_id=#{userId} AND trigger_type =#{triggerType}")
Long selectByTriggerTypeCount(@Param("userId") Long userId, @Param("triggerType") String triggerType);
@Select("SELECT SUM(duration_min)\n" +
"FROM app_watering_log \n" +
"WHERE user_id=${userId}")
Long selectByTotalWateringTime(Long userId);
"WHERE user_id=#{userId}")
Long selectByTotalWateringTime(@Param("userId") Long userId);
}

View File

@@ -20,9 +20,9 @@ public class MqttMessageDispatcher {
public MqttMessageDispatcher(List<MqttTopicHandler> handlers) {
this.handlers = handlers;
log.info("[MQTT] dispatcher initialized with {} handlers", handlers.size());
log.info("[MQTT] 消息分发器初始化完成,处理器数量={}", handlers.size());
for (MqttTopicHandler handler : handlers) {
log.info("[MQTT] └ registered: {} -> {}", handler.getClass().getSimpleName(), handler.topicPattern());
log.info("[MQTT] 已注册处理器:{} -> {}", handler.getClass().getSimpleName(), handler.topicPattern());
}
}
@@ -33,7 +33,7 @@ public class MqttMessageDispatcher {
* @param payload 消息体
*/
public void dispatch(String topic, String payload) {
log.debug("[MQTT] received topic={} payload={}", topic, payload);
log.debug("[MQTT] 收到消息 主题={} 消息体={}", topic, payload);
for (MqttTopicHandler handler : handlers) {
Matcher matcher = handler.topicPattern().matcher(topic);
@@ -44,6 +44,6 @@ public class MqttMessageDispatcher {
}
}
log.warn("[MQTT] no handler for topic: {} payload={}", topic, payload);
log.warn("[MQTT] 未找到匹配的处理器 主题={} 消息体={}", topic, payload);
}
}

View File

@@ -1,14 +1,13 @@
package org.dromara.app.service;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 设备信息
@@ -97,4 +96,6 @@ public interface IAppDeviceService {
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<String> deviceNos, Boolean isValid);
Map<String, Object> bindDeviceStatus(AppDeviceBo appDevice);
}

View File

@@ -25,15 +25,12 @@ import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.*;
@Slf4j
@RequiredArgsConstructor
@@ -116,7 +113,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
device.setPowerLevelUpdatatime(new Date());
// applyRegisterBindToken(device, bo, exists);
if (StringUtils.isBlank(device.getStatus())) {
device.setStatus("2");
device.setStatus("1"); // 注册默认在线
device.setWorkStatus("2");
}
@@ -131,6 +128,45 @@ 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;
}
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;
}
@Override
public AppDeviceVo bindRegisteredDevice(AppDeviceBo bo) {
if (StringUtils.isBlank(bo.getDeviceNo())) {
@@ -147,8 +183,10 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
// validBindToken(bo.getBindToken(), exists);
String status = StringUtils.isBlank(bo.getStatus()) ? exists.getStatus() : bo.getStatus();
String wifiName = StringUtils.isBlank(bo.getWifiName()) ? exists.getWifiName() : bo.getWifiName();
String wifiPassword = StringUtils.isBlank(bo.getWifiPassword()) ? exists.getWifiPassword() : bo.getWifiPassword();
String deviceName = StringUtils.isBlank(bo.getDeviceName()) ? exists.getDeviceName() : bo.getDeviceName();
int rows = baseMapper.bindIfAvailable(exists.getDeviceNo(), userId, deviceName, status);
int rows = baseMapper.bindIfAvailable(exists.getDeviceNo(), userId, deviceName, status,"0",wifiName,wifiPassword);
if (rows <= 0) {
AppDevice current = baseMapper.selectById(bo.getDeviceNo());
if (current == null) {
@@ -188,7 +226,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
if (ObjectUtil.isNotNull(device) && StringUtils.isNotBlank(device.getDeviceNo())) {
// 通过业务层统一下发命令(含浇水日志记录)
String commandId = deviceCommandService.sendSwitchCommand(deviceNo, workStatus, startTime, durationMin);
log.info("[DEVICE] switch command sent deviceNo={} commandId={}", deviceNo, commandId);
log.info("[设备] 开关命令已下发 设备编号={} 命令编号={}", deviceNo, commandId);
}
return true;

View File

@@ -25,11 +25,7 @@ import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
@Slf4j
@RequiredArgsConstructor
@@ -88,7 +84,13 @@ public class AppScheduleServiceImpl implements IAppScheduleService {
@Override
public Boolean updateStatusByBo(AppScheduleBo bo) {
AppSchedule update = MapstructUtils.convert(bo, AppSchedule.class);
return baseMapper.updateById(update) > 0;
Long userId = bo.getUserId() == null ? LoginHelper.getUserId() : bo.getUserId();
return baseMapper.update(
update,
Wrappers.<AppSchedule>lambdaQuery()
.eq(AppSchedule::getId, bo.getId())
.eq(AppSchedule::getUserId, userId)
) > 0;
}
@Override
@@ -98,7 +100,13 @@ public class AppScheduleServiceImpl implements IAppScheduleService {
validEntityBeforeSave(update);
Map<String, Object> repMap = new HashMap<>();
Boolean flag = baseMapper.updateById(update) > 0;
Long userId = bo.getUserId() == null ? LoginHelper.getUserId() : bo.getUserId();
Boolean flag = baseMapper.update(
update,
Wrappers.<AppSchedule>lambdaQuery()
.eq(AppSchedule::getId, bo.getId())
.eq(AppSchedule::getUserId, userId)
) > 0;
repMap.put("flag", flag);
repMap.put("list", new ArrayList<>());
repMap.put("appSchedule", "");

View File

@@ -3,13 +3,13 @@ package org.dromara.app.service.impl;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.mqtt.DeviceCommand;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.app.service.IDeviceCommandPublisher;
import org.dromara.app.service.IDeviceCommandService;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
@@ -50,7 +50,7 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
public String sendCommand(DeviceCommand command) {
validate(command);
String commandId = commandPublisher.send(command);
log.info("[CMD] command sent deviceNo={} commandType={} commandId={}",
log.info("[命令] 命令已下发 设备编号={} 命令类型={} 命令编号={}",
command.getDeviceNo(), command.getCommandType(), commandId);
return commandId;
}
@@ -76,7 +76,7 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
}
String commandId = commandPublisher.send(command);
log.info("[CMD] switch command sent deviceNo={} workStatus={} durationMin={} commandId={}",
log.info("[命令] 开关命令已下发 设备编号={} 工作状态={} 持续分钟数={} 命令编号={}",
deviceNo, workStatus, durationMin, commandId);
Long userId = LoginHelper.getUserId();
@@ -86,7 +86,7 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
} else {
boolean updated = wateringLogService.finishManualLog(deviceNo, userId, new Date());
if (!updated) {
log.warn("[CMD] no active manual watering log found deviceNo={} userId={} stopCommandId={}",
log.warn("[命令] 未找到进行中的手动浇水记录 设备编号={} 用户ID={} 停止命令编号={}",
deviceNo, userId, commandId);
}
}
@@ -119,7 +119,7 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService {
command.setCommandType("bindDevice");
Map<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("bindStatus", true);
objectObjectHashMap.put("deviceNo", "01");
objectObjectHashMap.put("deviceNo", deviceNo);
objectObjectHashMap.put("cmd", -1);
command.getPayload().putAll(objectObjectHashMap);
return sendCommand(command);

View File

@@ -0,0 +1,86 @@
package org.dromara.app.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.mapper.AppDeviceMapper;
import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 设备离线检测定时任务
* <p>
* 逻辑Redis 中 mqtt:device:status:{deviceNo} Key 的 TTL 由 device-status-cache-ttl-seconds 控制。
* 设备断电/断网后不再上报数据TTL 到期 Key 自动消失。
* 本任务定期扫描数据库中非离线状态的设备,如果对应 Redis Key 已不存在,
* 则将数据库 status 更新为 0离线
* </p>
*
* <p>扫描间隔通过 {@code mqtt.offline-check.interval-ms} 配置,建议小于设备在线 Key 的 TTL。</p>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceOfflineCheckTask {
private final AppDeviceMapper appDeviceMapper;
@Value("${mqtt.command-ack.device-status-cache-prefix}")
private String deviceStatusCachePrefix;
@Value("${mqtt.offline-check.enabled:true}")
private boolean enabled;
/**
* 每 90 秒检查一次Redis TTL 默认 300s90s 间隔确保 1.5 个周期内同步)
* 可通过 mqtt.offline-check.interval-ms 覆盖(单位 ms
*/
@Scheduled(fixedDelayString = "${mqtt.offline-check.interval-ms:90000}")
@Transactional(rollbackFor = Exception.class)
public void checkOfflineDevices() {
if (!enabled) {
return;
}
// 查询数据库中所有非离线状态的设备,避免状态不是 1 但仍需离线修正的设备被漏掉
List<AppDevice> onlineDevices = appDeviceMapper.selectList(
new LambdaQueryWrapper<AppDevice>()
.ne(AppDevice::getStatus, "0")
.select(AppDevice::getDeviceNo)
);
if (onlineDevices.isEmpty()) {
return;
}
// 过滤出 Redis Key 已过期(不存在)的设备
List<String> offlineDeviceNos = onlineDevices.stream()
.map(AppDevice::getDeviceNo)
.filter(deviceNo -> !RedisUtils.hasKey(deviceStatusCachePrefix + deviceNo))
.collect(Collectors.toList());
if (offlineDeviceNos.isEmpty()) {
log.debug("[设备离线] 本次检测未发现离线设备,扫描设备数:{}", onlineDevices.size());
return;
}
// 批量更新为离线
int updated = appDeviceMapper.update(null,
new LambdaUpdateWrapper<AppDevice>()
.set(AppDevice::getStatus, "0")
.ne(AppDevice::getStatus, "0")
.in(AppDevice::getDeviceNo, offlineDeviceNos)
);
log.info("[设备离线] 检测到 {} 台设备离线,已更新数据库状态,设备编号:{}",
updated, offlineDeviceNos);
}
}