From fe0f3e04323ed2d8acf8014887109500abd8e4f0 Mon Sep 17 00:00:00 2001 From: tianyongbao Date: Thu, 15 Jan 2026 11:35:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BE=AE=E4=BF=A1=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E6=8E=A5=E5=8F=A3=E5=AF=B9=E6=8E=A5=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=EF=BC=8C=E8=81=94=E8=B0=83=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/CaptchaController.java | 28 + .../java/com/intc/web/domain/vo/LoginVo.java | 10 + .../web/service/impl/SmsAuthStrategy.java | 2 + .../src/main/resources/application-dev.yml | 13 +- .../properties/DeviceTypeProperties.java | 62 ++ .../fishery/controller/AquUserController.java | 358 +++++++++++ .../fishery/controller/DeviceController.java | 61 +- .../fishery/controller/PondController.java | 91 ++- .../controller/TimingCtrlController.java | 163 +++++ .../fishery/domain/bo/ReqAddTimeCtrl.java | 48 ++ .../fishery/domain/bo/ReqAddUserChild.java | 32 + .../fishery/domain/bo/ReqUpdatePhone.java | 47 ++ .../fishery/domain/bo/ReqUpdateWarnPhone.java | 32 + .../fishery/domain/bo/ReqVerifySmsCode.java | 39 ++ .../fishery/domain/vo/PondDeviceListVo.java | 15 + .../vo/PublicDeviceTypeAndSerialNum.java | 29 + .../fishery/domain/vo/PublicTimeCtrl.java | 53 ++ .../intc/fishery/domain/vo/UserChildVo.java | 54 ++ .../fishery/mapper/UserRelationMapper.java | 31 + .../service/impl/DeviceServiceImpl.java | 84 ++- .../intc/iot/config/AliyunIotProperties.java | 50 -- .../intc/iot/constant/IOTPropertyName.java | 105 ++++ .../intc/iot/controller/IotController.java | 569 ++++++++++++++---- 23 files changed, 1797 insertions(+), 179 deletions(-) create mode 100644 intc-common/intc-common-core/src/main/java/com/intc/common/core/config/properties/DeviceTypeProperties.java create mode 100644 intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqAddTimeCtrl.java create mode 100644 intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqAddUserChild.java create mode 100644 intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqUpdatePhone.java create mode 100644 intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqUpdateWarnPhone.java create mode 100644 intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqVerifySmsCode.java create mode 100644 intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceTypeAndSerialNum.java create mode 100644 intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicTimeCtrl.java create mode 100644 intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/UserChildVo.java create mode 100644 intc-modules/intc-iot/src/main/java/com/intc/iot/constant/IOTPropertyName.java diff --git a/intc-admin/src/main/java/com/intc/web/controller/CaptchaController.java b/intc-admin/src/main/java/com/intc/web/controller/CaptchaController.java index e9df936..ec62ef7 100644 --- a/intc-admin/src/main/java/com/intc/web/controller/CaptchaController.java +++ b/intc-admin/src/main/java/com/intc/web/controller/CaptchaController.java @@ -21,6 +21,7 @@ import com.intc.common.sms.config.properties.SmsProperties; import com.intc.common.web.config.properties.CaptchaProperties; import com.intc.common.web.enums.CaptchaType; import com.intc.web.domain.vo.CaptchaVo; +import com.intc.fishery.domain.bo.ReqVerifySmsCode; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -32,6 +33,8 @@ import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.time.Duration; @@ -105,6 +108,31 @@ public class CaptchaController { return R.ok(); } + /** + * 验证短信验证码 + * + * @param request 手机号和验证码数据 + * @return 验证结果 + */ + @PostMapping("/resource/sms/verify") + public R verifySmsCode(@Validated @RequestBody ReqVerifySmsCode request) { + String key = GlobalConstants.CAPTCHA_CODE_KEY + request.getMobilePhone(); + String cachedCode = RedisUtils.getCacheObject(key); + + if (StringUtils.isBlank(cachedCode)) { + return R.fail("短信验证码已过期"); + } + + if (!cachedCode.equals(request.getSmsCode())) { + return R.fail("短信验证码错误"); + } + + // 验证成功后删除验证码(根据 C# 代码中的 true 参数) + RedisUtils.deleteObject(key); + + return R.ok(); + } + /** * 邮箱验证码 * diff --git a/intc-admin/src/main/java/com/intc/web/domain/vo/LoginVo.java b/intc-admin/src/main/java/com/intc/web/domain/vo/LoginVo.java index aabd21b..decfa84 100644 --- a/intc-admin/src/main/java/com/intc/web/domain/vo/LoginVo.java +++ b/intc-admin/src/main/java/com/intc/web/domain/vo/LoginVo.java @@ -57,4 +57,14 @@ public class LoginVo { */ private Long userId; + /** + * 用户名 + */ + private String userName; + + /** + * 昵称 + */ + private String nickName; + } diff --git a/intc-admin/src/main/java/com/intc/web/service/impl/SmsAuthStrategy.java b/intc-admin/src/main/java/com/intc/web/service/impl/SmsAuthStrategy.java index 75cef79..6829826 100644 --- a/intc-admin/src/main/java/com/intc/web/service/impl/SmsAuthStrategy.java +++ b/intc-admin/src/main/java/com/intc/web/service/impl/SmsAuthStrategy.java @@ -73,6 +73,8 @@ public class SmsAuthStrategy implements IAuthStrategy { loginVo.setExpireIn(StpUtil.getTokenTimeout()); loginVo.setClientId(client.getClientId()); loginVo.setUserId(loginUser.getUserId()); + loginVo.setNickName(loginUser.getNickname()); + loginVo.setUserName(loginUser.getUsername()); return loginVo; } diff --git a/intc-admin/src/main/resources/application-dev.yml b/intc-admin/src/main/resources/application-dev.yml index ccfdf22..173ba0e 100644 --- a/intc-admin/src/main/resources/application-dev.yml +++ b/intc-admin/src/main/resources/application-dev.yml @@ -203,7 +203,12 @@ sms: # 模板内容需包含 ${code} 参数 code-template-id: 'SMS_465720430' # TODO: 请填写您的阿里云短信模板CODE - + # 设备类型到 ProductKey 的映射配置 +device-type: + # 1-水质检测仪 ProductKey(请填写实际的 ProductKey) + water-quality-monitor: a15hA3oBPmB # TODO: 请替换为实际的水质检测仪 ProductKey + # 2-控制一体机 ProductKey(请填写实际的 ProductKey) + control-integrated: a1Xj9dagTIx # TODO: 请替换为实际的控制一体机 ProductKey --- # 阿里云生活物联网平台(飞燕平台)配置 aliyun: living-iot: @@ -219,12 +224,6 @@ aliyun: app-key: 334224397 # App Secret app-secret: 70de3018ec39423e9ca1e1b6a6a84ad6 - # 设备类型到 ProductKey 的映射配置 - device-type: - # 1-水质检测仪 ProductKey(请填写实际的 ProductKey) - water-quality-monitor: a15hA3oBPmB # TODO: 请替换为实际的水质检测仪 ProductKey - # 2-控制一体机 ProductKey(请填写实际的 ProductKey) - control-integrated: a1Xj9dagTIx # TODO: 请替换为实际的控制一体机 ProductKey # AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret) amqp: # 是否启用 diff --git a/intc-common/intc-common-core/src/main/java/com/intc/common/core/config/properties/DeviceTypeProperties.java b/intc-common/intc-common-core/src/main/java/com/intc/common/core/config/properties/DeviceTypeProperties.java new file mode 100644 index 0000000..8a0abc6 --- /dev/null +++ b/intc-common/intc-common-core/src/main/java/com/intc/common/core/config/properties/DeviceTypeProperties.java @@ -0,0 +1,62 @@ +package com.intc.common.core.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 设备类型配置 + * + * @author intc + */ +@Data +@Component +@ConfigurationProperties(prefix = "device-type") +public class DeviceTypeProperties { + + /** + * 水质检测仪 ProductKey + */ + private String waterQualityMonitor; + + /** + * 控制一体机 ProductKey + */ + private String controlIntegrated; + + /** + * 根据 ProductKey 判断设备类型 + * @param productKey ProductKey + * @return 设备类型: 1-水质检测仪, 2-控制一体机, null-未知类型 + */ + public Integer getDeviceTypeByProductKey(String productKey) { + if (productKey == null) { + return null; + } + if (productKey.equals(waterQualityMonitor)) { + return 1; + } else if (productKey.equals(controlIntegrated)) { + return 2; + } + return null; + } + + /** + * 根据设备类型获取 ProductKey + * @param deviceType 1-水质检测仪, 2-控制一体机 + * @return ProductKey + */ + public String getProductKeyByType(Integer deviceType) { + if (deviceType == null) { + return null; + } + switch (deviceType) { + case 1: + return waterQualityMonitor; + case 2: + return controlIntegrated; + default: + return null; + } + } +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/AquUserController.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/AquUserController.java index 8bf4cc0..498f404 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/AquUserController.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/AquUserController.java @@ -18,9 +18,27 @@ import com.intc.common.core.validate.EditGroup; import com.intc.common.log.enums.BusinessType; import com.intc.common.excel.utils.ExcelUtil; import com.intc.fishery.domain.vo.AquUserVo; +import com.intc.fishery.domain.vo.UserChildVo; import com.intc.fishery.domain.bo.AquUserBo; +import com.intc.fishery.domain.bo.ReqUpdateWarnPhone; +import com.intc.fishery.domain.bo.ReqChangeName; +import com.intc.fishery.domain.bo.ReqUpdatePhone; +import com.intc.fishery.domain.bo.ReqAddUserChild; +import com.intc.fishery.domain.bo.ReqId; +import com.intc.fishery.domain.AquUser; +import com.intc.fishery.domain.UserRelation; import com.intc.fishery.service.IAquUserService; +import com.intc.fishery.mapper.AquUserMapper; +import com.intc.fishery.mapper.UserRelationMapper; +import com.intc.system.service.ISysUserService; +import com.intc.system.domain.bo.SysUserBo; import com.intc.common.mybatis.core.page.TableDataInfo; +import com.intc.common.satoken.utils.LoginHelper; +import com.intc.common.core.utils.StringUtils; +import com.intc.common.json.utils.JsonUtils; +import com.intc.common.core.constant.GlobalConstants; +import com.intc.common.redis.utils.RedisUtils; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; /** * 养殖用户管理 @@ -35,6 +53,9 @@ import com.intc.common.mybatis.core.page.TableDataInfo; public class AquUserController extends BaseController { private final IAquUserService aquUserService; + private final AquUserMapper aquUserMapper; + private final UserRelationMapper userRelationMapper; + private final ISysUserService sysUserService; /** * 查询养殖用户管理列表 @@ -102,4 +123,341 @@ public class AquUserController extends BaseController { @PathVariable Long[] ids) { return toAjax(aquUserService.deleteWithValidByIds(List.of(ids), true)); } + + /** + * 获取报警电话列表 + * + * @return 报警电话列表 + */ + @GetMapping("/list_warn_phone") + public R> getWarnPhone() { + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + if (userId == null || userId < 0) { + return R.fail("用户未登录"); + } + + // 查询用户的报警电话 JSON字段 + AquUserVo user = aquUserService.queryById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + + String warnPhoneJson = user.getWarnPhoneJson(); + if (StringUtils.isEmpty(warnPhoneJson)) { + return R.fail("不存在报警电话列表"); + } + + // 反序列化JSON为List + try { + List warnPhones = JsonUtils.parseArray(warnPhoneJson, String.class); + return R.ok(warnPhones); + } catch (Exception e) { + return R.fail("报警电话数据格式错误"); + } + } + + /** + * 获取子用户列表 + * + * @return 子用户列表 + */ + @GetMapping("/list_user_child") + public R> getListUserChild() { + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + if (userId == null || userId < 0) { + return R.fail("用户未登录"); + } + + // 查询该用户的所有子账号 + List list = userRelationMapper.selectChildUsersByParentId(userId); + + return R.ok(list); + } + + /** + * 获取父用户列表 + * + * @return 父用户列表 + */ + @GetMapping("/list_user_parent") + public R> getListUserParent() { + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + if (userId == null || userId < 0) { + return R.fail("用户未登录"); + } + + // 查询该用户的所有父账号 + List list = userRelationMapper.selectParentUsersByChildId(userId); + + return R.ok(list); + } + + /** + * 添加子用户 + * + * @param request 子用户手机号 + * @return 操作结果 + */ + @PostMapping("/add_user_child") + public R addUserChild(@Validated @RequestBody ReqAddUserChild request) { + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + if (userId == null || userId < 0) { + return R.fail("用户未登录"); + } + + // 根据手机号查询子用户 + AquUser childUser = aquUserMapper.selectOne( + new LambdaQueryWrapper() + .eq(AquUser::getMobilePhone, request.getMobilePhone()) + .select(AquUser::getId) + ); + + if (childUser == null) { + return R.fail("该手机号用户不存在"); + } + + Long childUserId = childUser.getId(); + + // 检查是否已存在该子账号关系 + long count = userRelationMapper.selectCount( + new LambdaQueryWrapper() + .eq(UserRelation::getParentUserId, userId) + .eq(UserRelation::getChildUserId, childUserId) + ); + + if (count > 0) { + return R.fail("子账号已添加,无需重复添加"); + } + + // 创建子账号关系 + UserRelation relation = new UserRelation(); + relation.setParentUserId(userId); + relation.setChildUserId(childUserId); + boolean success = userRelationMapper.insert(relation) > 0; + + return toAjax(success); + } + + /** + * 删除子用户 + * + * @param request 关系ID + * @return 操作结果 + */ + @DeleteMapping("/delete_user_child") + public R deleteUserChild(@Validated @RequestBody ReqId request) { + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + if (userId == null || userId < 0) { + return R.fail("用户未登录"); + } + + // 查询子账号关系,验证权限 + UserRelation userRelation = userRelationMapper.selectOne( + new LambdaQueryWrapper() + .eq(UserRelation::getId, request.getId()) + .select(UserRelation::getId, UserRelation::getParentUserId, UserRelation::getChildUserId) + ); + + if (userRelation == null) { + return R.fail("子账号关系不存在"); + } + + // 验证是否是当前用户的子账号 + if (!userRelation.getParentUserId().equals(userId)) { + return R.fail("无权删除该子账号"); + } + + // 删除子账号关系 + int count = userRelationMapper.deleteById(request.getId()); + if (count == 0) { + return R.fail("删除失败"); + } + + return R.ok(); + } + + /** + * 更新报警电话列表 + * + * @param request 报警电话列表 + * @return 操作结果 + */ + @PutMapping("/update_warn_phone") + public R updateWarnPhone(@Validated @RequestBody ReqUpdateWarnPhone request) { + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + if (userId == null || userId < 0) { + return R.fail("用户未登录"); + } + + // 查询用户信息,获取当前用户的手机号 + AquUserVo user = aquUserService.queryById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + + String oldMobilePhone = user.getMobilePhone(); + if (StringUtils.isEmpty(oldMobilePhone)) { + return R.fail("用户手机号不存在,无权操作"); + } + + // 验证报警电话列表是否包含用户自己的手机号 + List listPhone = request.getListPhone(); + boolean containUser = false; + for (String phone : listPhone) { + if (phone.equals(oldMobilePhone)) { + containUser = true; + break; + } + } + + if (!containUser) { + return R.fail("报警电话必须包含自己手机号"); + } + + // 序列化为JSON字符串 + String warnPhoneJson = JsonUtils.toJsonString(listPhone); + + // 更新数据库 + AquUserBo updateBo = new AquUserBo(); + updateBo.setId(userId); + updateBo.setWarnPhoneJson(warnPhoneJson); + boolean success = aquUserService.updateByBo(updateBo); + + return toAjax(success); + } + + /** + * 更新用户名 + * + * @param request 更新用户名请求 + * @return 操作结果 + */ + @PutMapping("/update_user_name") + public R updateUserName(@Validated @RequestBody ReqChangeName request) { + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + if (userId == null || userId < 0) { + return R.fail("用户未登录"); + } + + // 更新 AquUserBo 的用户名(长度验证已在 ReqChangeName 中处理) + AquUserBo updateBo = new AquUserBo(); + updateBo.setId(userId); + updateBo.setUserName(request.getNewName()); + boolean success = aquUserService.updateByBo(updateBo); + + // 同时更新 SysUserBo 的昵称 + if (success) { + SysUserBo sysUserBo = new SysUserBo(); + sysUserBo.setUserId(userId); + sysUserBo.setNickName(request.getNewName()); + sysUserService.updateUser(sysUserBo); + } + + return toAjax(success); + } + + /** + * 更新手机号 + * + * @param request 更新手机号请求 + * @return 操作结果 + */ + @PutMapping("/update_mobile_phone") + public R updateMobilePhone(@Validated @RequestBody ReqUpdatePhone request) { + // 获取当前登录用户ID + Long userId = LoginHelper.getUserId(); + if (userId == null || userId < 0) { + return R.fail("用户未登录"); + } + + // 查询用户信息 + AquUserVo user = aquUserService.queryById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + + String oldMobilePhone = user.getMobilePhone(); + if (StringUtils.isEmpty(oldMobilePhone)) { + return R.fail("用户手机号不存在,无权操作"); + } + + // 验证旧手机号是否匹配 + if (!request.getOldMobilePhone().equals(oldMobilePhone)) { + return R.fail("旧手机号不匹配"); + } + + // 如果新旧手机号相同,直接返回成功 + if (request.getOldMobilePhone().equals(request.getNewMobilePhone())) { + return R.ok(); + } + + // 检查新手机号是否已被注册 + long count = aquUserMapper.selectCount( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(AquUser::getMobilePhone, request.getNewMobilePhone()) + ); + if (count > 0) { + return R.fail("新手机号已被注册"); + } + + // 验证短信验证码 + String codeKey = GlobalConstants.CAPTCHA_CODE_KEY + request.getNewMobilePhone(); + String cachedCode = RedisUtils.getCacheObject(codeKey); + if (StringUtils.isBlank(cachedCode)) { + return R.fail("短信验证码已过期"); + } + if (!cachedCode.equals(request.getSmsCode())) { + return R.fail("短信验证码错误"); + } + + // 处理报警电话列表:替换旧手机号为新手机号,并移除重复的新手机号 + String warnPhoneJson = user.getWarnPhoneJson(); + List listWarnPhone = new java.util.ArrayList<>(); + if (StringUtils.isNotEmpty(warnPhoneJson)) { + try { + listWarnPhone = JsonUtils.parseArray(warnPhoneJson, String.class); + if (listWarnPhone == null) { + listWarnPhone = new java.util.ArrayList<>(); + } + } catch (Exception e) { + listWarnPhone = new java.util.ArrayList<>(); + } + } + + // 移除列表中所有的新手机号(避免重复) + listWarnPhone.removeIf(phone -> phone.equals(request.getNewMobilePhone())); + + // 将旧手机号替换为新手机号 + for (int i = 0; i < listWarnPhone.size(); i++) { + if (listWarnPhone.get(i).equals(oldMobilePhone)) { + listWarnPhone.set(i, request.getNewMobilePhone()); + } + } + + // 序列化报警电话列表 + String newWarnPhoneJson = JsonUtils.toJsonString(listWarnPhone); + + // 更新数据库:手机号和报警电话列表 + AquUserBo updateBo = new AquUserBo(); + updateBo.setId(userId); + updateBo.setMobilePhone(request.getNewMobilePhone()); + updateBo.setWarnPhoneJson(newWarnPhoneJson); + boolean success = aquUserService.updateByBo(updateBo); + + if (success) { + // 删除已使用的验证码 + RedisUtils.deleteObject(codeKey); + // 也删除旧手机号的验证码(如果存在) + RedisUtils.deleteObject(GlobalConstants.CAPTCHA_CODE_KEY + request.getOldMobilePhone()); + } + + return toAjax(success); + } } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/DeviceController.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/DeviceController.java index e1a9964..fbf9c87 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/DeviceController.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/DeviceController.java @@ -27,6 +27,7 @@ import com.intc.fishery.domain.vo.PublicDeviceSwitchSimpleVo; import com.intc.fishery.domain.vo.PublicDeviceBaseData; import com.intc.fishery.domain.vo.PublicDeviceLinkedCtrl; import com.intc.fishery.domain.vo.PublicDeviceDeadInfo; +import com.intc.fishery.domain.vo.PublicDeviceTypeAndSerialNum; import com.intc.fishery.domain.bo.ReqId; import com.intc.fishery.domain.bo.ReqSetOxyTempWarnCall; import com.intc.fishery.domain.bo.ReqSetOxyWarnValue; @@ -46,6 +47,7 @@ import com.intc.fishery.mapper.PondMapper; import com.intc.fishery.mapper.LinkedCtrlMapper; import com.intc.fishery.mapper.DeviceCorrectRecordMapper; import com.intc.fishery.constant.DefineDeviceWarnCode; +import com.intc.common.core.config.properties.DeviceTypeProperties; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.springframework.transaction.annotation.Transactional; @@ -73,6 +75,7 @@ public class DeviceController extends BaseController { private final PondMapper pondMapper; private final LinkedCtrlMapper linkedCtrlMapper; private final DeviceCorrectRecordMapper deviceCorrectRecordMapper; + private final DeviceTypeProperties deviceTypeProperties; /** * 查询设备管理列表 @@ -133,7 +136,7 @@ public class DeviceController extends BaseController { * * @param ids 主键串 */ - @SaCheckPermission("fishery:device:remove") +// @SaCheckPermission("fishery:device:remove") @Log(title = "设备管理", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") public R remove(@NotEmpty(message = "主键不能为空") @@ -976,4 +979,60 @@ public class DeviceController extends BaseController { return R.ok(deviceBaseData); } + + /** + * 检查设备二维码,解析设备类型和序列号 + * 二维码格式:pk=xxxxx&dn=xxxxx + * + * @param rootUserId 用户ID(预留参数) + * @param qrCode 设备二维码内容 + * @return 设备类型和序列号 + */ + @GetMapping("/check_device_qrcode") + public R checkDeviceQrcode( + @RequestParam("rootUserId") Long rootUserId, + @RequestParam("qrCode") String qrCode) { + + // 验证二维码不能为空 + if (qrCode == null || qrCode.trim().isEmpty()) { + return R.fail("二维码格式错误"); + } + + // 解析二维码,格式:pk=xxxxx&dn=xxxxx + String[] parts = qrCode.trim().split("&"); + if (parts.length != 2) { + return R.fail("二维码格式错误"); + } + + String pkPart = parts[0].trim(); + String dnPart = parts[1].trim(); + + // 验证格式前缀 + if (!pkPart.startsWith("pk=") || !dnPart.startsWith("dn=")) { + return R.fail("二维码格式错误"); + } + + // 提取实际值 + String pk = pkPart.substring(3); + String dn = dnPart.substring(3); + + // 验证长度 + if (pk.length() < 10 || dn.length() < 10) { + return R.fail("二维码格式错误"); + } + + // 根据ProductKey判断设备类型 + Integer deviceType = deviceTypeProperties.getDeviceTypeByProductKey(pk); + + if (deviceType == null) { + return R.fail("二维码格式错误"); + } + + // 构建返回结果 + PublicDeviceTypeAndSerialNum result = new PublicDeviceTypeAndSerialNum(); + result.setDeviceType(deviceType); + result.setSerialNum(dn); + + return R.ok(result); + } } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java index 58e645c..0c25dfd 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java @@ -20,6 +20,7 @@ import com.intc.common.log.enums.BusinessType; import com.intc.common.excel.utils.ExcelUtil; import com.intc.fishery.domain.bo.PondBo; import com.intc.fishery.domain.bo.ReqChangePond; +import com.intc.fishery.domain.bo.ReqTurnOpen; import com.intc.fishery.service.IPondService; import com.intc.common.mybatis.core.page.TableDataInfo; import com.intc.common.satoken.utils.LoginHelper; @@ -184,7 +185,23 @@ public class PondController extends BaseController { @PathVariable Long pondId) { PondDeviceListVo result = new PondDeviceListVo(); - // 1. 查询塘口下的所有设备 + // 1. 查询塘口信息 + Pond pond = pondMapper.selectOne( + new LambdaQueryWrapper() + .eq(Pond::getId, pondId) + .select(Pond::getId, Pond::getPondName, Pond::getKeepNightOpen) + ); + + if (pond == null) { + return R.fail("塘口不存在"); + } + + // 设置塘口信息 + result.setPondId(pond.getId()); + result.setPondName(pond.getPondName()); + result.setKeepNightOpen(pond.getKeepNightOpen()); + + // 2. 查询塘口下的所有设备 List devices = deviceMapper.selectList( new LambdaQueryWrapper() .eq(Device::getPondId, pondId) @@ -198,12 +215,12 @@ public class PondController extends BaseController { return R.ok(result); } - // 2. 收集所有设备ID,用于查询联动控制和故障码 + // 3. 收集所有设备ID,用于查询联动控制和故障码 List deviceIds = devices.stream() .map(Device::getId) .collect(Collectors.toList()); - // 3. 批量查询所有设备的联动控制 + // 4. 批量查询所有设备的联动控制 List allLinkedCtrls = linkedCtrlMapper.selectList( new LambdaQueryWrapper() .in(LinkedCtrl::getDeviceId, deviceIds) @@ -211,14 +228,14 @@ public class PondController extends BaseController { java.util.Map> linkedCtrlsByDevice = allLinkedCtrls.stream() .collect(Collectors.groupingBy(LinkedCtrl::getDeviceId)); - // 4. 批量查询塘口的所有开关 + // 5. 批量查询塘口的所有开关 List allSwitches = deviceSwitchMapper.selectList( new LambdaQueryWrapper() .eq(DeviceSwitch::getPondId, pondId) .orderByAsc(DeviceSwitch::getIndex) ); - // 5. 收集开关ID,查询定时控制 + // 6. 收集开关ID,查询定时控制 List switchIds = allSwitches.stream() .map(DeviceSwitch::getId) .collect(Collectors.toList()); @@ -232,11 +249,11 @@ public class PondController extends BaseController { .collect(Collectors.groupingBy(TimingCtrl::getSwitchId)); } - // 6. 按设备ID分组开关 + // 7. 按设备ID分组开关 java.util.Map> switchesByDevice = allSwitches.stream() .collect(Collectors.groupingBy(DeviceSwitch::getDeviceId)); - // 7. 批量查询故障码 + // 8. 批量查询故障码 Set controllerIds = devices.stream() .filter(d -> d.getDeviceType() != null && d.getDeviceType() == 2) .map(Device::getId) @@ -249,7 +266,7 @@ public class PondController extends BaseController { ); } - // 8. 处理探测器列表 (包含: deviceType=1 + deviceType=2且isOxygenUsed=1) + // 9. 处理探测器列表 (包含: deviceType=1 + deviceType=2且isOxygenUsed=1) List detectorList = new ArrayList<>(); for (Device device : devices) { // 水质检测仪 或 开启溶氧检测的测控一体机 @@ -283,7 +300,7 @@ public class PondController extends BaseController { } } - // 9. 处理控制器列表 (仅测控一体机 deviceType=2) + // 10. 处理控制器列表 (仅测控一体机 deviceType=2) List controllerList = new ArrayList<>(); for (Device device : devices) { if (device.getDeviceType() != null && device.getDeviceType() == 2) { @@ -865,8 +882,8 @@ public class PondController extends BaseController { // 如果塘口没有变化,直接返回 Long oldPondId = device.getPondId(); Long newPondId = (request.getPondId() != null && request.getPondId() > 0) ? request.getPondId() : null; - - if ((oldPondId == null && newPondId == null) || + + if ((oldPondId == null && newPondId == null) || (oldPondId != null && oldPondId.equals(newPondId))) { return R.ok(); } @@ -944,4 +961,56 @@ public class PondController extends BaseController { return R.ok(); } + + /** + * 设置塘口夜间防误关开关 + * 开启后,夜间时段(21:00-次日6:00)内不会因为故障而关闭设备 + * + * @param request 请求对象(包含塘口ID和开关状态) + * @return 操作结果 + */ + @PutMapping("/keep_night_open") + public R setKeepNightOpen( + @Validated @RequestBody ReqTurnOpen request) { + + // 验证塘口ID + if (request.getId() == null || request.getId() <= 0) { + return R.fail("塘口ID不能为空"); + } + + // 查询塘口信息 + Pond pond = pondMapper.selectOne( + new LambdaQueryWrapper() + .eq(Pond::getId, request.getId()) + .select(Pond::getId, Pond::getUserId, Pond::getPondName, Pond::getKeepNightOpen) + ); + + // 验证塘口存在性和权限 + if (pond == null) { + return R.fail("塘口不存在或无权限访问"); + } + + // 如果开关状态没有变化,直接返回 + if (pond.getKeepNightOpen() != null && pond.getKeepNightOpen().equals(request.getIsOpen())) { + return R.ok(); + } + + // 更新夜间防误关开关 + boolean updated = pondMapper.update(null, + new LambdaUpdateWrapper() + .eq(Pond::getId, request.getId()) + .set(Pond::getKeepNightOpen, request.getIsOpen()) + ) > 0; + + if (!updated) { + return R.fail("更新失败"); + } + + // TODO: 记录操作日志 + // String op = request.getIsOpen() == 1 ? "开启" : "关闭"; + // CacheData.AddMessageOpRecordUser(rootUserId, userId, "塘口操作", + // String.format("塘口(%s)夜间防误关:%s", pond.getPondName(), op)); + + return R.ok(); + } } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/TimingCtrlController.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/TimingCtrlController.java index bd05380..12076d6 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/TimingCtrlController.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/TimingCtrlController.java @@ -17,9 +17,22 @@ import com.intc.common.core.validate.AddGroup; import com.intc.common.core.validate.EditGroup; import com.intc.common.log.enums.BusinessType; import com.intc.fishery.domain.vo.TimingCtrlVo; +import com.intc.fishery.domain.vo.PublicTimeCtrl; import com.intc.fishery.domain.bo.TimingCtrlBo; +import com.intc.fishery.domain.bo.ReqId; +import com.intc.fishery.domain.bo.ReqAddTimeCtrl; import com.intc.fishery.service.ITimingCtrlService; import com.intc.common.mybatis.core.page.TableDataInfo; +import com.intc.fishery.mapper.TimingCtrlMapper; +import com.intc.fishery.mapper.DeviceSwitchMapper; +import com.intc.fishery.mapper.DeviceMapper; +import com.intc.fishery.domain.TimingCtrl; +import com.intc.fishery.domain.DeviceSwitch; +import com.intc.fishery.domain.Device; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import java.util.Calendar; +import java.util.stream.Collectors; +import java.util.Date; /** * 开关定时控制 @@ -34,6 +47,9 @@ import com.intc.common.mybatis.core.page.TableDataInfo; public class TimingCtrlController extends BaseController { private final ITimingCtrlService timingCtrlService; + private final TimingCtrlMapper timingCtrlMapper; + private final DeviceSwitchMapper deviceSwitchMapper; + private final DeviceMapper deviceMapper; /** * 查询开关定时控制列表 @@ -101,4 +117,151 @@ public class TimingCtrlController extends BaseController { @PathVariable Long[] ids) { return toAjax(timingCtrlService.deleteWithValidByIds(List.of(ids), true)); } + + /** + * 获取指定开关的定时控制列表 + * + * @param request 请求对象(包含开关ID) + * @return 定时控制列表 + */ + @PutMapping("/list") + public R> getTimeCtrlList( + @Validated @RequestBody ReqId request) { + + // 查询指定开关的所有定时控制,按创建时间降序排列 + List timingCtrls = timingCtrlMapper.selectList( + new LambdaQueryWrapper() + .eq(TimingCtrl::getSwitchId, request.getId()) + .orderByDesc(TimingCtrl::getCreateTime) + ); + + // 转换为VO列表 + List list = timingCtrls.stream() + .map(tc -> { + PublicTimeCtrl vo = new PublicTimeCtrl(); + vo.setId(tc.getId()); + vo.setSwitchId(tc.getSwitchId()); + vo.setOpenTime(tc.getOpenTime()); + vo.setCloseTime(tc.getCloseTime()); + vo.setLoopType(tc.getLoopType()); + vo.setIsOpen(tc.getIsOpen()); + return vo; + }) + .collect(Collectors.toList()); + + return R.ok(list); + } + + /** + * 添加定时控制 + * + * @param request 请求对象 + * @return 操作结果 + */ + @PostMapping("/add") + public R addTimeCtrl( + @Validated @RequestBody ReqAddTimeCtrl request) { + + // 验证开启和关闭时间不能相同 + if (request.getOpenTime().equals(request.getCloseTime())) { + return R.fail("开启和关闭时间不能相同"); + } + + // 将时间转换为今天的具体时刻 + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + Date dateToday = calendar.getTime(); + + // 设置开启时间 + Calendar openCal = Calendar.getInstance(); + openCal.setTime(dateToday); + Calendar reqOpenCal = Calendar.getInstance(); + reqOpenCal.setTime(request.getOpenTime()); + openCal.set(Calendar.HOUR_OF_DAY, reqOpenCal.get(Calendar.HOUR_OF_DAY)); + openCal.set(Calendar.MINUTE, reqOpenCal.get(Calendar.MINUTE)); + Date openTime = openCal.getTime(); + + // 设置关闭时间 + Calendar closeCal = Calendar.getInstance(); + closeCal.setTime(dateToday); + Calendar reqCloseCal = Calendar.getInstance(); + reqCloseCal.setTime(request.getCloseTime()); + closeCal.set(Calendar.HOUR_OF_DAY, reqCloseCal.get(Calendar.HOUR_OF_DAY)); + closeCal.set(Calendar.MINUTE, reqCloseCal.get(Calendar.MINUTE)); + Date closeTime = closeCal.getTime(); + + // 如果开启时间大于关闭时间,则关闭时间加1天 + if (openTime.after(closeTime)) { + closeCal.add(Calendar.DAY_OF_MONTH, 1); + closeTime = closeCal.getTime(); + } + + // 查询开关信息,包括关联的设备信息 + DeviceSwitch deviceSwitch = deviceSwitchMapper.selectOne( + new LambdaQueryWrapper() + .eq(DeviceSwitch::getId, request.getId()) + .select(DeviceSwitch::getId, DeviceSwitch::getSwitchName, + DeviceSwitch::getPondId, DeviceSwitch::getDeviceId, DeviceSwitch::getIsOpen) + ); + + if (deviceSwitch == null) { + return R.fail("开关不存在"); + } + + // 查询设备信息 + Device device = deviceMapper.selectOne( + new LambdaQueryWrapper() + .eq(Device::getId, deviceSwitch.getDeviceId()) + .select(Device::getId, Device::getUserId, Device::getDeviceName, + Device::getWarnCode, Device::getDeadTime) + ); + + if (device == null) { + return R.fail("开关不存在"); + } + + // 检查设备是否过期 + if (device.getDeadTime() != null && device.getDeadTime().before(new Date())) { + return R.fail("设备服务已过期"); + } + + // 检查开关是否绑定了塘口 + if (deviceSwitch.getPondId() == null || deviceSwitch.getPondId() <= 0) { + return R.fail("开关未绑定塘口"); + } + + // 检查定时控制数量是否超限4个 + long count = timingCtrlMapper.selectCount( + new LambdaQueryWrapper() + .eq(TimingCtrl::getSwitchId, request.getId()) + ); + + if (count >= 4) { + return R.fail("定时控制数量不得超过4个"); + } + + // 创建定时控制记录 + TimingCtrl timingCtrl = new TimingCtrl(); + timingCtrl.setSwitchId(request.getId()); + timingCtrl.setOpenTime(openTime); + timingCtrl.setCloseTime(closeTime); + timingCtrl.setLoopType(request.getLoopType().longValue()); + timingCtrl.setIsOpen(0L); // 默认未启用 + + // 插入数据库 + int result = timingCtrlMapper.insert(timingCtrl); + + if (result > 0) { + // TODO: 记录操作日志 + // CacheData.AddMessageOpRecordUser(rootUserId, userId, "开关定时控制", + // String.format("%s(%s)新增定时控制。", + // device.getDeviceName(), deviceSwitch.getSwitchName())); + return R.ok(); + } else { + return R.fail("添加失败"); + } + } } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqAddTimeCtrl.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqAddTimeCtrl.java new file mode 100644 index 0000000..11c0bd5 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqAddTimeCtrl.java @@ -0,0 +1,48 @@ +package com.intc.fishery.domain.bo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 添加定时控制请求对象 + * + * @author intc + * @date 2026-01-14 + */ +@Data +public class ReqAddTimeCtrl implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 开关ID + */ + @NotNull(message = "开关ID不能为空") + private Long id; + + /** + * 开启时间(只包含时分) + */ + @NotNull(message = "开启时间不能为空") + @JsonFormat(pattern = "HH:mm") + private Date openTime; + + /** + * 关闭时间(只包含时分) + */ + @NotNull(message = "关闭时间不能为空") + @JsonFormat(pattern = "HH:mm") + private Date closeTime; + + /** + * 循环类型 + */ + @NotNull(message = "循环类型不能为空") + private Integer loopType; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqAddUserChild.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqAddUserChild.java new file mode 100644 index 0000000..f34c357 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqAddUserChild.java @@ -0,0 +1,32 @@ +package com.intc.fishery.domain.bo; + +import com.intc.common.core.constant.RegexConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 添加子用户请求对象 + * + * @author intc + * @date 2026-01-14 + */ +@Data +@Schema(description = "添加子用户请求对象") +public class ReqAddUserChild implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 子用户手机号 + */ + @Schema(description = "子用户手机号") + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式不正确") + private String mobilePhone; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqUpdatePhone.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqUpdatePhone.java new file mode 100644 index 0000000..3435f80 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqUpdatePhone.java @@ -0,0 +1,47 @@ +package com.intc.fishery.domain.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import com.intc.common.core.constant.RegexConstants; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 更新手机号请求对象 + * + * @author intc + * @date 2026-01-14 + */ +@Data +@Schema(description = "更新手机号请求对象") +public class ReqUpdatePhone implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 旧手机号 + */ + @Schema(description = "旧手机号") + @NotBlank(message = "旧手机号不能为空") + @Pattern(regexp = RegexConstants.MOBILE, message = "旧手机号格式不正确") + private String oldMobilePhone; + + /** + * 新手机号 + */ + @Schema(description = "新手机号") + @NotBlank(message = "新手机号不能为空") + @Pattern(regexp = RegexConstants.MOBILE, message = "新手机号格式不正确") + private String newMobilePhone; + + /** + * 短信验证码 + */ + @Schema(description = "短信验证码") + @NotBlank(message = "短信验证码不能为空") + private String smsCode; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqUpdateWarnPhone.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqUpdateWarnPhone.java new file mode 100644 index 0000000..0dac797 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqUpdateWarnPhone.java @@ -0,0 +1,32 @@ +package com.intc.fishery.domain.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import com.intc.common.core.constant.RegexConstants; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 更新报警电话请求对象 + * + * @author intc + * @date 2026-01-14 + */ +@Data +@Schema(description = "更新报警电话请求对象") +public class ReqUpdateWarnPhone implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 报警电话列表 + */ + @Schema(description = "报警电话列表") + @NotEmpty(message = "报警电话列表不能为空") + private List<@Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式不正确") String> listPhone; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqVerifySmsCode.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqVerifySmsCode.java new file mode 100644 index 0000000..8086891 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/bo/ReqVerifySmsCode.java @@ -0,0 +1,39 @@ +package com.intc.fishery.domain.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import com.intc.common.core.constant.RegexConstants; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 验证短信验证码请求对象 + * + * @author intc + * @date 2026-01-14 + */ +@Data +@Schema(description = "验证短信验证码请求对象") +public class ReqVerifySmsCode implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 手机号 + */ + @Schema(description = "手机号") + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式不正确") + private String mobilePhone; + + /** + * 短信验证码 + */ + @Schema(description = "短信验证码") + @NotBlank(message = "短信验证码不能为空") + private String smsCode; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondDeviceListVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondDeviceListVo.java index 414404f..3637ca7 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondDeviceListVo.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PondDeviceListVo.java @@ -18,6 +18,21 @@ public class PondDeviceListVo implements Serializable { @Serial private static final long serialVersionUID = 1L; + /** + * 塘口ID + */ + private Long pondId; + + /** + * 塘口名称 + */ + private String pondName; + + /** + * 夜间防误关(0-关闭,1-开启) + */ + private Integer keepNightOpen; + /** * 探测器列表(水质检测仪) */ diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceTypeAndSerialNum.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceTypeAndSerialNum.java new file mode 100644 index 0000000..4c5ef6d --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicDeviceTypeAndSerialNum.java @@ -0,0 +1,29 @@ +package com.intc.fishery.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 设备类型和序列号视图对象 + * + * @author intc + * @date 2026-01-14 + */ +@Data +public class PublicDeviceTypeAndSerialNum implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 设备类型(1-水质检测仪,2-控制一体机) + */ + private Integer deviceType; + + /** + * 设备序列号 + */ + private String serialNum; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicTimeCtrl.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicTimeCtrl.java new file mode 100644 index 0000000..eecc261 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/PublicTimeCtrl.java @@ -0,0 +1,53 @@ +package com.intc.fishery.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 定时控制视图对象 + * + * @author intc + * @date 2026-01-14 + */ +@Data +public class PublicTimeCtrl implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + private Long id; + + /** + * 开关ID + */ + private Long switchId; + + /** + * 开启时间 + */ + @JsonFormat(pattern = "HH:mm:ss") + private Date openTime; + + /** + * 关闭时间 + */ + @JsonFormat(pattern = "HH:mm:ss") + private Date closeTime; + + /** + * 循环类型 + */ + private Long loopType; + + /** + * 是否启用 + */ + private Long isOpen; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/UserChildVo.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/UserChildVo.java new file mode 100644 index 0000000..54ce0c8 --- /dev/null +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/domain/vo/UserChildVo.java @@ -0,0 +1,54 @@ + package com.intc.fishery.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 子用户关系视图对象 + * + * @author intc + * @date 2026-01-14 + */ +@Data +@Schema(description = "子用户关系视图对象") +public class UserChildVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 关系表主键id + */ + @Schema(description = "关系表主键id") + private Long id; + + /** + * 子账号用户id + */ + @Schema(description = "子账号用户id") + private Long userId; + + /** + * 子账号用户名 + */ + @Schema(description = "子账号用户名") + private String userName; + + /** + * 子账号手机号 + */ + @Schema(description = "子账号手机号") + private String mobilePhone; + + /** + * 关系创建时间 + */ + @Schema(description = "关系创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdTime; +} diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/mapper/UserRelationMapper.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/mapper/UserRelationMapper.java index 6f6387f..ec1a3fc 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/mapper/UserRelationMapper.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/mapper/UserRelationMapper.java @@ -2,7 +2,12 @@ package com.intc.fishery.mapper; import com.intc.fishery.domain.UserRelation; import com.intc.fishery.domain.vo.UserRelationVo; +import com.intc.fishery.domain.vo.UserChildVo; import com.intc.common.mybatis.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; /** * 养殖用户子账号Mapper接口 @@ -12,4 +17,30 @@ import com.intc.common.mybatis.core.mapper.BaseMapperPlus; */ public interface UserRelationMapper extends BaseMapperPlus { + /** + * 查询用户的子账号列表 + * + * @param parentUserId 父级用户ID + * @return 子账号列表 + */ + @Select("SELECT r.id, r.child_user_id as userId, u.user_name as userName, " + + "u.mobile_phone as mobilePhone, r.create_time as createdTime " + + "FROM aqu_user_relation r " + + "LEFT JOIN aqu_user u ON r.child_user_id = u.id " + + "WHERE r.parent_user_id = #{parentUserId}") + List selectChildUsersByParentId(@Param("parentUserId") Long parentUserId); + + /** + * 查询用户的父账号列表 + * + * @param childUserId 子用户ID + * @return 父账号列表 + */ + @Select("SELECT r.id, r.parent_user_id as userId, u.user_name as userName, " + + "u.mobile_phone as mobilePhone, r.create_time as createdTime " + + "FROM aqu_user_relation r " + + "LEFT JOIN aqu_user u ON r.parent_user_id = u.id " + + "WHERE r.child_user_id = #{childUserId}") + List selectParentUsersByChildId(@Param("childUserId") Long childUserId); + } diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/service/impl/DeviceServiceImpl.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/service/impl/DeviceServiceImpl.java index 89314f3..8f1e816 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/service/impl/DeviceServiceImpl.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/service/impl/DeviceServiceImpl.java @@ -1,22 +1,31 @@ package com.intc.fishery.service.impl; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.intc.common.core.utils.MapstructUtils; import com.intc.common.core.utils.StringUtils; import com.intc.common.mybatis.core.page.PageQuery; import com.intc.common.mybatis.core.page.TableDataInfo; import com.intc.fishery.domain.AquUser; import com.intc.fishery.domain.Device; +import com.intc.fishery.domain.DeviceCorrectRecord; +import com.intc.fishery.domain.DeviceSwitch; +import com.intc.fishery.domain.LinkedCtrl; import com.intc.fishery.domain.Pond; +import com.intc.fishery.domain.TimingCtrl; import com.intc.fishery.domain.bo.DeviceBo; import com.intc.fishery.domain.vo.DeviceVo; +import com.intc.fishery.mapper.DeviceCorrectRecordMapper; import com.intc.fishery.mapper.DeviceMapper; +import com.intc.fishery.mapper.DeviceSwitchMapper; +import com.intc.fishery.mapper.LinkedCtrlMapper; +import com.intc.fishery.mapper.TimingCtrlMapper; import com.intc.fishery.service.IDeviceService; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.yulichang.wrapper.MPJLambdaWrapper; @@ -36,6 +45,10 @@ import lombok.extern.slf4j.Slf4j; public class DeviceServiceImpl implements IDeviceService { private final DeviceMapper baseMapper; + private final DeviceCorrectRecordMapper deviceCorrectRecordMapper; + private final DeviceSwitchMapper deviceSwitchMapper; + private final LinkedCtrlMapper linkedCtrlMapper; + private final TimingCtrlMapper timingCtrlMapper; /** * 查询设备管理 @@ -206,16 +219,79 @@ public class DeviceServiceImpl implements IDeviceService { /** * 校验并批量删除设备管理信息 + * 删除顺序:校准记录 -> 定时控制 -> 开关(先解除联动控制引用)-> 联动控制 -> 设备 * * @param ids 待删除的主键集合 * @param isValid 是否进行有效性校验 * @return 是否删除成功 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { if(isValid){ //TODO 做一些业务上的校验,判断是否需要校验 } + + // 1. 删除设备关联的校准记录 + deviceCorrectRecordMapper.delete( + new LambdaQueryWrapper() + .in(DeviceCorrectRecord::getDeviceId, ids) + ); + + // 2. 查询设备关联的开关 + List switches = deviceSwitchMapper.selectList( + new LambdaQueryWrapper() + .in(DeviceSwitch::getDeviceId, ids) + ); + + if (!switches.isEmpty()) { + // 收集开关ID + List switchIds = switches.stream() + .map(DeviceSwitch::getId) + .collect(Collectors.toList()); + + // 3. 删除开关关联的定时控制 + timingCtrlMapper.delete( + new LambdaQueryWrapper() + .in(TimingCtrl::getSwitchId, switchIds) + ); + + // 收集开关关联的联动控制ID + List linkedCtrlIds = switches.stream() + .map(DeviceSwitch::getLinkedCtrlId) + .filter(id -> id != null && id > 0) + .distinct() + .collect(Collectors.toList()); + + // 4. 解除开关与联动控制的绑定 + deviceSwitchMapper.update(null, + new LambdaUpdateWrapper() + .set(DeviceSwitch::getLinkedCtrlId, null) + .in(DeviceSwitch::getDeviceId, ids) + ); + + // 5. 删除联动控制(如果有) + if (!linkedCtrlIds.isEmpty()) { + linkedCtrlMapper.delete( + new LambdaQueryWrapper() + .in(LinkedCtrl::getId, linkedCtrlIds) + ); + } + + // 6. 删除开关 + deviceSwitchMapper.delete( + new LambdaQueryWrapper() + .in(DeviceSwitch::getDeviceId, ids) + ); + } + + // 7. 删除设备本身关联的联动控制 + linkedCtrlMapper.delete( + new LambdaQueryWrapper() + .in(LinkedCtrl::getDeviceId, ids) + ); + + // 8. 最后删除设备 return baseMapper.deleteByIds(ids) > 0; } diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java index 7ed61d4..a1b4888 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java @@ -47,13 +47,6 @@ public class AliyunIotProperties { */ private String categoryKey; - /** - * 设备类型到产品Key的映射 - * 1: 水质检测仪 - * 2: 控制一体机 - */ - private DeviceTypeMapping deviceType = new DeviceTypeMapping(); - /** * MQTT 配置(可选,用于设备直连) */ @@ -165,47 +158,4 @@ public class AliyunIotProperties { private Integer reconnectDelay = 30000; } - /** - * 设备类型映射配置 - */ - @Data - public static class DeviceTypeMapping { - /** - * 水质检测仪 ProductKey - */ - private String waterQualityMonitor; - - /** - * 控制一体机 ProductKey - */ - private String controlIntegrated; - - /** - * 控制器 ProductKey (别名,指向 controlIntegrated) - * @return ProductKey - */ - public String getController() { - return controlIntegrated; - } - - /** - * 根据设备类型获取 ProductKey - * @param deviceType 1-水质检测仪, 2-控制一体机 - * @return ProductKey - */ - public String getProductKeyByType(Integer deviceType) { - if (deviceType == null) { - return null; - } - switch (deviceType) { - case 1: - return waterQualityMonitor; - case 2: - return controlIntegrated; - default: - return null; - } - } - } - } diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/constant/IOTPropertyName.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/constant/IOTPropertyName.java new file mode 100644 index 0000000..7a4194e --- /dev/null +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/constant/IOTPropertyName.java @@ -0,0 +1,105 @@ +package com.intc.iot.constant; + +/** + * 物联网平台设备属性名称常量 + * + * @author intc + * @date 2026-01-14 + */ +public class IOTPropertyName { + + private IOTPropertyName() { + } + + // ========== 水质检测仪属性 ========== + /** + * 物联网卡号 + */ + public static final String ICCID = "ICCID"; + + /** + * 溶解氧 + */ + public static final String DISSOLVED_OXYGEN = "dissolvedOxygen"; + + /** + * PH值 + */ + public static final String PH = "PH"; + + /** + * 水温 + */ + public static final String CURRENT_TEMPERATURE = "currentTemperature"; + + /** + * 饱和度 + */ + public static final String DOSAT = "dosat"; + + /** + * 故障码 + */ + public static final String ERROR_CODE = "errorCode"; + + /** + * 校准检测 + */ + public static final String T_CORRECT = "Tcorrect"; + + /** + * 盐度 + */ + public static final String SALINITY = "salinity"; + + /** + * 盐度设置 + */ + public static final String SALINITY_SET = "salinitySet"; + + /** + * 参比值 + */ + public static final String T_REFERENCE = "Treference"; + + /** + * 荧光值 + */ + public static final String T_FLUORESCENCE = "Tfluorescence"; + + /** + * 相位差 + */ + public static final String PHASE_DIFFERENCE = "Phasedifference"; + + /** + * 设备校准 + */ + public static final String CORRECT = "correct"; + + // ========== 控制器属性 ========== + /** + * 控制器额定电压 + */ + public static final String RATED_VOLTAGE = "rated_voltage"; + + /** + * 控制器开关定时控制(前缀) + */ + public static final String LOCAL_TIMER_SWITCH = "localTimer_switch"; + + /** + * 控制器开关(前缀) + */ + public static final String SWITCH_INDEX = "Switch"; + + /** + * 控制器开关额定电流(前缀) + */ + public static final String RATING_SWITCH_INDEX = "rating_switch"; + + /** + * 控制器传感器故障码 + */ + public static final String SENSOR_ERROR_CODE = "sensorErrorCode"; +} diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java index ab2dc06..587c13d 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java @@ -5,10 +5,21 @@ import com.intc.common.mybatis.core.page.PageQuery; import com.intc.common.mybatis.core.page.TableDataInfo; import com.intc.common.satoken.utils.LoginHelper; import com.intc.common.web.core.BaseController; +import com.intc.common.core.config.properties.DeviceTypeProperties; import com.intc.fishery.domain.Device; +import com.intc.fishery.domain.DeviceSwitch; +import com.intc.fishery.domain.TimingCtrl; +import com.intc.fishery.domain.Pond; +import com.intc.fishery.domain.LinkedCtrl; +import com.intc.fishery.domain.DeviceCorrectRecord; +import com.intc.fishery.domain.AquUser; +import com.intc.fishery.domain.bo.*; +import com.intc.fishery.domain.vo.DeviceSwitchVo; import com.intc.fishery.mapper.DeviceMapper; import com.intc.fishery.mapper.PondMapper; -import com.intc.iot.config.AliyunIotProperties; +import com.intc.fishery.mapper.DeviceSwitchMapper; +import com.intc.fishery.mapper.DeviceCorrectRecordMapper; +import com.intc.fishery.mapper.TimingCtrlMapper; import com.intc.iot.domain.bo.AddDeviceControllerBo; import com.intc.iot.domain.bo.AddDeviceDetectorBo; import com.intc.iot.domain.bo.DeviceCalibrateBo; @@ -27,12 +38,15 @@ import com.intc.iot.service.WarnCallNoticeService; import com.intc.iot.utils.AliyunAmqpSignUtil; import com.intc.iot.utils.ControllerHelper; import com.intc.iot.service.IotCloudService; +import com.intc.iot.constant.IOTPropertyName; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.Date; @@ -50,7 +64,7 @@ import java.util.Map; @Tag(name = "生活物联网平台管理", description = "阿里云飞燕平台对接接口") public class IotController extends BaseController { - private final AliyunIotProperties aliyunIotProperties; + private final DeviceTypeProperties deviceTypeProperties; @Autowired(required = false) private IotDeviceService iotDeviceService; @@ -86,13 +100,13 @@ public class IotController extends BaseController { private PondMapper pondMapper; @Autowired(required = false) - private com.intc.fishery.mapper.DeviceSwitchMapper deviceSwitchMapper; + private DeviceSwitchMapper deviceSwitchMapper; @Autowired(required = false) - private com.intc.fishery.mapper.DeviceCorrectRecordMapper deviceCorrectRecordMapper; + private DeviceCorrectRecordMapper deviceCorrectRecordMapper; @Autowired(required = false) - private com.intc.fishery.mapper.TimingCtrlMapper timingCtrlMapper; + private TimingCtrlMapper timingCtrlMapper; @Autowired(required = false) private IotCloudService iotCloudService; @@ -305,7 +319,7 @@ public class IotController extends BaseController { } // 根据设备类型获取 ProductKey - String productKey = aliyunIotProperties.getDeviceType().getProductKeyByType(devicetype); + String productKey = deviceTypeProperties.getProductKeyByType(devicetype); if (productKey == null || productKey.isEmpty()) { return R.fail("未配置该设备类型的 ProductKey,请检查配置文件(deviceType: " + devicetype + ")"); } @@ -722,9 +736,9 @@ public class IotController extends BaseController { // 验证塘口是否存在且属于当前用户 if (bo.getPondId() != null && bo.getPondId() > 0 && pondMapper != null) { long count = pondMapper.selectCount( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .eq(com.intc.fishery.domain.Pond::getUserId, userId) - .eq(com.intc.fishery.domain.Pond::getId, bo.getPondId()) + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(Pond::getUserId, userId) + .eq(Pond::getId, bo.getPondId()) ); if (count == 0) { return R.fail("塘口不存在或无权限访问"); @@ -742,7 +756,7 @@ public class IotController extends BaseController { } // 获取控制器的 ProductKey - String productKey = aliyunIotProperties.getDeviceType().getControlIntegrated(); + String productKey = deviceTypeProperties.getControlIntegrated(); if (productKey == null || productKey.isEmpty()) { return R.fail("未配置测控一体机的 ProductKey"); } @@ -944,9 +958,9 @@ public class IotController extends BaseController { device.setTempWarnCallNoDis(0); // 创建开关列表 - java.util.List switches = new java.util.ArrayList<>(); + java.util.List switches = new java.util.ArrayList<>(); for (int i = 1; i <= 4; i++) { - com.intc.fishery.domain.DeviceSwitch deviceSwitch = new com.intc.fishery.domain.DeviceSwitch(); + DeviceSwitch deviceSwitch = new DeviceSwitch(); deviceSwitch.setIndex(i); deviceSwitch.setSwitchName(device.getDeviceName() + "_开关_" + i); deviceSwitch.setConnectVoltageType(bo.getInputVoltage()); @@ -1089,7 +1103,7 @@ public class IotController extends BaseController { } // 保存开关信息 - for (com.intc.fishery.domain.DeviceSwitch deviceSwitch : switches) { + for (DeviceSwitch deviceSwitch : switches) { deviceSwitch.setDeviceId(device.getId()); deviceSwitchMapper.insert(deviceSwitch); } @@ -1119,7 +1133,7 @@ public class IotController extends BaseController { /** * 解析开关电压电流数据 */ - private void parseSwitchVoltCur(String json, com.intc.fishery.domain.DeviceSwitch deviceSwitch) { + private void parseSwitchVoltCur(String json, DeviceSwitch deviceSwitch) { try { // 清理JSON字符串 json = json.replace("\"{", "{").replace("}\"", "}").replace("\\", ""); @@ -1156,9 +1170,9 @@ public class IotController extends BaseController { // 验证塘口是否存在且属于当前用户 if (bo.getPondId() != null && bo.getPondId() > 0 && pondMapper != null) { long count = pondMapper.selectCount( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .eq(com.intc.fishery.domain.Pond::getUserId, userId) - .eq(com.intc.fishery.domain.Pond::getId, bo.getPondId()) + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(Pond::getUserId, userId) + .eq(Pond::getId, bo.getPondId()) ); if (count == 0) { return R.fail("塘口不存在或无权限访问"); @@ -1176,7 +1190,7 @@ public class IotController extends BaseController { } // 获取水质检测仪的 ProductKey - String productKey = aliyunIotProperties.getDeviceType().getWaterQualityMonitor(); + String productKey = deviceTypeProperties.getWaterQualityMonitor(); if (productKey == null || productKey.isEmpty()) { return R.fail("未配置水质检测仪的 ProductKey"); } @@ -1478,7 +1492,7 @@ public class IotController extends BaseController { // 创建校准记录 if (deviceCorrectRecordMapper != null) { - com.intc.fishery.domain.DeviceCorrectRecord correctRecord = new com.intc.fishery.domain.DeviceCorrectRecord(); + DeviceCorrectRecord correctRecord = new DeviceCorrectRecord(); correctRecord.setDeviceId(device.getId()); correctRecord.setUserId(userId); correctRecord.setSerialNum(device.getSerialNum()); @@ -1669,9 +1683,9 @@ public class IotController extends BaseController { // 更新关联开关的接线方式 deviceSwitchMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() - .eq(com.intc.fishery.domain.DeviceSwitch::getDeviceId, request.getId()) - .set(com.intc.fishery.domain.DeviceSwitch::getConnectVoltageType, request.getVoltageType()) + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(DeviceSwitch::getDeviceId, request.getId()) + .set(DeviceSwitch::getConnectVoltageType, request.getVoltageType()) ); String voltageDesc = ControllerHelper.getVoltageDescription(request.getVoltageType()); @@ -1693,7 +1707,7 @@ public class IotController extends BaseController { */ @Operation(summary = "开关开启/关闭控制") @PutMapping("/switch/turn_switch") - public R turnSwitch(@RequestBody com.intc.fishery.domain.bo.ReqTurnOpen request) { + public R turnSwitch(@RequestBody ReqTurnOpen request) { try { if (deviceSwitchMapper == null || deviceMapper == null) { return R.fail("系统配置未完成"); @@ -1704,7 +1718,7 @@ public class IotController extends BaseController { Integer targetStatus = request.getIsOpen(); // 查询开关信息 - com.intc.fishery.domain.DeviceSwitch deviceSwitch = deviceSwitchMapper.selectById(switchId); + DeviceSwitch deviceSwitch = deviceSwitchMapper.selectById(switchId); if (deviceSwitch == null) { return R.fail("开关不存在"); } @@ -1751,7 +1765,7 @@ public class IotController extends BaseController { // 构造IoT属性 Map properties = new java.util.HashMap<>(); - properties.put("switchIndex" + deviceSwitch.getIndex(), targetStatus); + properties.put(IOTPropertyName.SWITCH_INDEX + deviceSwitch.getIndex(), targetStatus); // 调用物联网服务设置属性 boolean success = iotCloudService.setProperty(device.getIotId(), properties, true, 2); @@ -1761,15 +1775,15 @@ public class IotController extends BaseController { // 更新数据库中的开关状态和最后操作时间 deviceSwitchMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() - .eq(com.intc.fishery.domain.DeviceSwitch::getId, switchId) - .set(com.intc.fishery.domain.DeviceSwitch::getIsOpen, targetStatus) - .set(com.intc.fishery.domain.DeviceSwitch::getLastTurnTime, new Date()) + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(DeviceSwitch::getId, switchId) + .set(DeviceSwitch::getIsOpen, targetStatus) + .set(DeviceSwitch::getLastTurnTime, new Date()) ); // 记录操作日志 String operation = targetStatus == 1 ? "开启" : "关闭"; - log.info("开关操作:{}({})的开关{}:{}", + log.info("开关操作:{}({})的开关{}:{}", device.getDeviceName(), device.getSerialNum(), deviceSwitch.getSwitchName(), operation); return R.ok(); @@ -1787,7 +1801,7 @@ public class IotController extends BaseController { */ @Operation(summary = "设置塘口所有开关状态") @PutMapping("/switch/turn_pond_switch") - public R turnPondSwitch(@RequestBody com.intc.fishery.domain.bo.ReqTurnOpen request) { + public R turnPondSwitch(@RequestBody ReqTurnOpen request) { try { if (deviceSwitchMapper == null || deviceMapper == null) { return R.fail("系统配置未完成"); @@ -1798,30 +1812,30 @@ public class IotController extends BaseController { Integer targetStatus = request.getIsOpen(); // 查询塘口下所有开关信息(包括关联的设备信息) - com.github.yulichang.wrapper.MPJLambdaWrapper wrapper = - new com.github.yulichang.wrapper.MPJLambdaWrapper() - .selectAll(com.intc.fishery.domain.DeviceSwitch.class) + com.github.yulichang.wrapper.MPJLambdaWrapper wrapper = + new com.github.yulichang.wrapper.MPJLambdaWrapper() + .selectAll(DeviceSwitch.class) .selectAs(Device::getUserId, "userId") .selectAs(Device::getIotId, "iotId") .selectAs(Device::getSerialNum, "serialNum") .selectAs(Device::getDeviceName, "deviceName") .selectAs(Device::getWarnCode, "warnCode") .selectAs(Device::getDeadTime, "deadTime") - .leftJoin(Device.class, Device::getId, com.intc.fishery.domain.DeviceSwitch::getDeviceId) - .eq(com.intc.fishery.domain.DeviceSwitch::getPondId, pondId); + .leftJoin(Device.class, Device::getId, DeviceSwitch::getDeviceId) + .eq(DeviceSwitch::getPondId, pondId); - java.util.List listSwitch = - deviceSwitchMapper.selectJoinList(com.intc.fishery.domain.vo.DeviceSwitchVo.class, wrapper); + java.util.List listSwitch = + deviceSwitchMapper.selectJoinList(DeviceSwitchVo.class, wrapper); if (listSwitch == null || listSwitch.isEmpty()) { return R.fail("该塘口下没有开关"); } // 按设备IotId分组,并过滤不符合条件的开关 - Map> iotIdSwitchMap = + Map> iotIdSwitchMap = new java.util.HashMap<>(); - - for (com.intc.fishery.domain.vo.DeviceSwitchVo switchVo : listSwitch) { + + for (DeviceSwitchVo switchVo : listSwitch) { // 权限验证 if (switchVo.getUserId() == null || !switchVo.getUserId().equals(userId)) { return R.fail("开关不存在或无权限访问"); @@ -1866,23 +1880,23 @@ public class IotController extends BaseController { // 按设备批量设置开关状态 java.util.Set successSwitchIds = new java.util.HashSet<>(); - java.util.List successSwitches = new java.util.ArrayList<>(); + java.util.List successSwitches = new java.util.ArrayList<>(); - for (Map.Entry> entry : iotIdSwitchMap.entrySet()) { + for (Map.Entry> entry : iotIdSwitchMap.entrySet()) { String iotId = entry.getKey(); - java.util.List switches = entry.getValue(); + java.util.List switches = entry.getValue(); // 构造该设备的所有开关属性 Map properties = new java.util.HashMap<>(); - for (com.intc.fishery.domain.vo.DeviceSwitchVo switchVo : switches) { - properties.put("switchIndex" + switchVo.getIndex(), targetStatus); + for (DeviceSwitchVo switchVo : switches) { + properties.put(IOTPropertyName.SWITCH_INDEX + switchVo.getIndex(), targetStatus); } // 调用物联网服务设置属性 boolean success = iotCloudService.setProperty(iotId, properties, false, 0); if (success) { // 记录成功的开关ID - for (com.intc.fishery.domain.vo.DeviceSwitchVo switchVo : switches) { + for (DeviceSwitchVo switchVo : switches) { successSwitchIds.add(switchVo.getId()); successSwitches.add(switchVo); } @@ -1892,15 +1906,15 @@ public class IotController extends BaseController { // 批量更新数据库中成功设置的开关状态 if (!successSwitchIds.isEmpty()) { deviceSwitchMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() - .in(com.intc.fishery.domain.DeviceSwitch::getId, successSwitchIds) - .set(com.intc.fishery.domain.DeviceSwitch::getIsOpen, targetStatus) - .set(com.intc.fishery.domain.DeviceSwitch::getLastTurnTime, new Date()) + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .in(DeviceSwitch::getId, successSwitchIds) + .set(DeviceSwitch::getIsOpen, targetStatus) + .set(DeviceSwitch::getLastTurnTime, new Date()) ); // 记录操作日志 String operation = targetStatus == 1 ? "开启" : "关闭"; - for (com.intc.fishery.domain.vo.DeviceSwitchVo switchVo : successSwitches) { + for (DeviceSwitchVo switchVo : successSwitches) { log.info("开关操作:{}({})的开关{}:{}", switchVo.getDeviceName(), switchVo.getSerialNum(), switchVo.getSwitchName(), operation); } @@ -1921,7 +1935,7 @@ public class IotController extends BaseController { */ @Operation(summary = "设置开关额定电流") @PutMapping("/switch/electric_set") - public R updateElectricSet(@RequestBody com.intc.fishery.domain.bo.ReqSwitchElectricSet request) { + public R updateElectricSet(@RequestBody ReqSwitchElectricSet request) { try { if (deviceSwitchMapper == null || deviceMapper == null) { return R.fail("系统配置未完成"); @@ -1932,7 +1946,7 @@ public class IotController extends BaseController { Double electric = request.getElectric(); // 查询开关信息 - com.intc.fishery.domain.DeviceSwitch deviceSwitch = deviceSwitchMapper.selectById(switchId); + DeviceSwitch deviceSwitch = deviceSwitchMapper.selectById(switchId); if (deviceSwitch == null) { return R.fail("开关不存在"); } @@ -1956,7 +1970,7 @@ public class IotController extends BaseController { } // 检查额定电流值是否已改变(精度0.001) - if (deviceSwitch.getRateElectricValue() != null + if (deviceSwitch.getRateElectricValue() != null && Math.abs(deviceSwitch.getRateElectricValue() - electric) < 0.001) { return R.ok(); } @@ -1983,9 +1997,9 @@ public class IotController extends BaseController { // 更新数据库中的额定电流值 deviceSwitchMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() - .eq(com.intc.fishery.domain.DeviceSwitch::getId, switchId) - .set(com.intc.fishery.domain.DeviceSwitch::getRateElectricValue, electric) + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(DeviceSwitch::getId, switchId) + .set(DeviceSwitch::getRateElectricValue, electric) ); // 记录操作日志 @@ -2007,7 +2021,7 @@ public class IotController extends BaseController { */ @Operation(summary = "修改开关输出电压类型") @PutMapping("/switch/update_voltage_type") - public R updateConnectVoltageType(@RequestBody com.intc.fishery.domain.bo.ReqSetVoltageType request) { + public R updateConnectVoltageType(@RequestBody ReqSetVoltageType request) { try { if (deviceSwitchMapper == null || deviceMapper == null) { return R.fail("系统配置未完成"); @@ -2018,9 +2032,9 @@ public class IotController extends BaseController { Integer voltageType = request.getVoltageType(); // 查询开关信息(包括关联的设备信息) - com.github.yulichang.wrapper.MPJLambdaWrapper wrapper = - new com.github.yulichang.wrapper.MPJLambdaWrapper() - .selectAll(com.intc.fishery.domain.DeviceSwitch.class) + com.github.yulichang.wrapper.MPJLambdaWrapper wrapper = + new com.github.yulichang.wrapper.MPJLambdaWrapper() + .selectAll(DeviceSwitch.class) .selectAs(Device::getUserId, "userId") .selectAs(Device::getIotId, "iotId") .selectAs(Device::getSerialNum, "serialNum") @@ -2029,11 +2043,11 @@ public class IotController extends BaseController { .selectAs(Device::getInputVoltage, "inputVoltage") .selectAs(Device::getWarnCode, "warnCode") .selectAs(Device::getDeadTime, "deadTime") - .leftJoin(Device.class, Device::getId, com.intc.fishery.domain.DeviceSwitch::getDeviceId) - .eq(com.intc.fishery.domain.DeviceSwitch::getId, switchId); + .leftJoin(Device.class, Device::getId, DeviceSwitch::getDeviceId) + .eq(DeviceSwitch::getId, switchId); - com.intc.fishery.domain.vo.DeviceSwitchVo switchVo = - deviceSwitchMapper.selectJoinOne(com.intc.fishery.domain.vo.DeviceSwitchVo.class, wrapper); + DeviceSwitchVo switchVo = + deviceSwitchMapper.selectJoinOne(DeviceSwitchVo.class, wrapper); if (switchVo == null) { return R.fail("开关不存在"); @@ -2092,9 +2106,9 @@ public class IotController extends BaseController { // 更新数据库中的电压类型 deviceSwitchMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() - .eq(com.intc.fishery.domain.DeviceSwitch::getId, switchId) - .set(com.intc.fishery.domain.DeviceSwitch::getConnectVoltageType, voltageType) + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(DeviceSwitch::getId, switchId) + .set(DeviceSwitch::getConnectVoltageType, voltageType) ); // 记录操作日志 @@ -2141,7 +2155,7 @@ public class IotController extends BaseController { */ @Operation(summary = "修改开关所属塘口") @PutMapping("/switch/update_pond") - public R updateSwitchPond(@RequestBody com.intc.fishery.domain.bo.ReqChangePond request) { + public R updateSwitchPond(@RequestBody ReqChangePond request) { try { if (deviceSwitchMapper == null || pondMapper == null || deviceMapper == null) { return R.fail("系统配置未完成"); @@ -2152,21 +2166,21 @@ public class IotController extends BaseController { Long pondId = request.getPondId(); // 查询开关信息,包括关联的设备和塘口信息 - com.github.yulichang.wrapper.MPJLambdaWrapper wrapper = - new com.github.yulichang.wrapper.MPJLambdaWrapper() - .selectAll(com.intc.fishery.domain.DeviceSwitch.class) + com.github.yulichang.wrapper.MPJLambdaWrapper wrapper = + new com.github.yulichang.wrapper.MPJLambdaWrapper() + .selectAll(DeviceSwitch.class) .selectAs(Device::getDeviceName, "deviceName") .selectAs(Device::getSerialNum, "serialNum") .selectAs(Device::getIotId, "iotId") .selectAs(Device::getWarnCode, "warnCode") - .selectAs(com.intc.fishery.domain.Pond::getPondName, "pondName") - .leftJoin(Device.class, Device::getId, com.intc.fishery.domain.DeviceSwitch::getDeviceId) - .leftJoin(com.intc.fishery.domain.Pond.class, com.intc.fishery.domain.Pond::getId, com.intc.fishery.domain.DeviceSwitch::getPondId) - .eq(com.intc.fishery.domain.DeviceSwitch::getId, switchId); + .selectAs(Pond::getPondName, "pondName") + .leftJoin(Device.class, Device::getId, DeviceSwitch::getDeviceId) + .leftJoin(Pond.class, Pond::getId, DeviceSwitch::getPondId) + .eq(DeviceSwitch::getId, switchId); + + DeviceSwitchVo vo = deviceSwitchMapper.selectJoinOne( + DeviceSwitchVo.class, wrapper); - com.intc.fishery.domain.vo.DeviceSwitchVo vo = deviceSwitchMapper.selectJoinOne( - com.intc.fishery.domain.vo.DeviceSwitchVo.class, wrapper); - if (vo == null) { return R.fail("开关不存在"); } @@ -2186,30 +2200,30 @@ public class IotController extends BaseController { if (device != null && com.intc.common.core.utils.StringUtils.isNotBlank(device.getIotId())) { // 检查设备是否在线(通过判断warnCode判断设备状态) boolean isOnline = device.getWarnCode() != null && device.getWarnCode() != 99; - + if (isOnline && iotCloudService != null) { try { // 构造清空定时控制的属性 Map properties = new java.util.HashMap<>(); // 清空定时器:localTimer_switch{Index} properties.put("localTimer_switch" + vo.getIndex(), java.util.Collections.emptyList()); - + // 如果开关是打开状态,需要关闭它 if (vo.getIsOpen() != null && vo.getIsOpen() == 1) { - // 关闭开关:switchIndex{Index} = 0 - properties.put("switchIndex" + vo.getIndex(), 0); - + // 关闭开关:Switch{Index} = 0 + properties.put(IOTPropertyName.SWITCH_INDEX + vo.getIndex(), 0); + // 更新数据库中的开关状态 deviceSwitchMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() - .eq(com.intc.fishery.domain.DeviceSwitch::getId, switchId) - .set(com.intc.fishery.domain.DeviceSwitch::getIsOpen, 0) + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(DeviceSwitch::getId, switchId) + .set(DeviceSwitch::getIsOpen, 0) ); } - + // 调用物联网服务设置属性 boolean success = iotCloudService.setProperty(device.getIotId(), properties, false, 0); - + if (success) { log.info("成功通过物联网服务清空开关定时控制和关闭开关, switchId={}", switchId); } else { @@ -2220,53 +2234,53 @@ public class IotController extends BaseController { } } } - + // 删除该开关的所有定时控制记录 if (timingCtrlMapper != null) { timingCtrlMapper.delete( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .eq(com.intc.fishery.domain.TimingCtrl::getSwitchId, switchId) + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(TimingCtrl::getSwitchId, switchId) ); } - + log.info("已清除开关ID={}的定时控制数据", switchId); } // 处理新塘口分配 if (pondId != null && pondId > 0) { // 验证塘口是否存在 - com.intc.fishery.domain.Pond pond = pondMapper.selectById(pondId); + Pond pond = pondMapper.selectById(pondId); if (pond == null) { return R.fail("塘口不存在"); } // 更新开关的塘口ID,并清空联动控制ID deviceSwitchMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() - .eq(com.intc.fishery.domain.DeviceSwitch::getId, switchId) - .set(com.intc.fishery.domain.DeviceSwitch::getPondId, pondId) - .set(com.intc.fishery.domain.DeviceSwitch::getLinkedCtrlId, null) + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(DeviceSwitch::getId, switchId) + .set(DeviceSwitch::getPondId, pondId) + .set(DeviceSwitch::getLinkedCtrlId, null) ); // 记录操作日志 if (oldPondId != null) { - log.info("开关操作:{}({})的开关{},转移到塘口:{}", + log.info("开关操作:{}({})的开关{},转移到塘口:{}", vo.getDeviceName(), vo.getSerialNum(), vo.getSwitchName(), pond.getPondName()); } else { - log.info("开关操作:{}({})的开关{},分配到塘口:{}", + log.info("开关操作:{}({})的开关{},分配到塘口:{}", vo.getDeviceName(), vo.getSerialNum(), vo.getSwitchName(), pond.getPondName()); } } else if (oldPondId != null) { // 移除塘口分配 deviceSwitchMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() - .eq(com.intc.fishery.domain.DeviceSwitch::getId, switchId) - .set(com.intc.fishery.domain.DeviceSwitch::getPondId, null) - .set(com.intc.fishery.domain.DeviceSwitch::getLinkedCtrlId, null) + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(DeviceSwitch::getId, switchId) + .set(DeviceSwitch::getPondId, null) + .set(DeviceSwitch::getLinkedCtrlId, null) ); // 记录操作日志 - log.info("开关操作:{}({})的开关{} 从 {} 移除", + log.info("开关操作:{}({})的开关{} 从 {} 移除", vo.getDeviceName(), vo.getSerialNum(), vo.getSwitchName(), oldPondName); } @@ -2276,4 +2290,347 @@ public class IotController extends BaseController { return R.fail("修改失败: " + e.getMessage()); } } + + /** + * 设置定时控制开关 + * + * @param request 请求对象 + * @return 操作结果 + */ + @Operation(summary = "设置定时控制开关") + @PostMapping("/timingCtrl/update") + public R setTimeCtrlOpen( + @Validated @org.springframework.web.bind.annotation.RequestBody ReqTurnOpen request) { + + try { + // 查询定时控制信息 + TimingCtrl timingCtrl = timingCtrlMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(TimingCtrl::getId, request.getId()) + .select(TimingCtrl::getId, + TimingCtrl::getIsOpen, + TimingCtrl::getLoopType, + TimingCtrl::getSwitchId, + TimingCtrl::getOpenTime, + TimingCtrl::getCloseTime) + ); + + if (timingCtrl == null) { + return R.fail("定时控制不存在"); + } + + // 如果状态相同,无需更新 + if (timingCtrl.getIsOpen().intValue() == request.getIsOpen()) { + return R.ok(); + } + + // 如果是启用且循环类型为1(每天循环),验证当前时间不在定时范围内 + if (request.getIsOpen() == 1 && timingCtrl.getLoopType() != null && timingCtrl.getLoopType() == 1) { + Date now = new Date(); + java.util.Calendar calendar = java.util.Calendar.getInstance(); + calendar.setTime(now); + int currentHour = calendar.get(java.util.Calendar.HOUR_OF_DAY); + int currentMinute = calendar.get(java.util.Calendar.MINUTE); + + java.util.Calendar openCal = java.util.Calendar.getInstance(); + openCal.setTime(timingCtrl.getOpenTime()); + int openHour = openCal.get(java.util.Calendar.HOUR_OF_DAY); + int openMinute = openCal.get(java.util.Calendar.MINUTE); + + java.util.Calendar closeCal = java.util.Calendar.getInstance(); + closeCal.setTime(timingCtrl.getCloseTime()); + int closeHour = closeCal.get(java.util.Calendar.HOUR_OF_DAY); + int closeMinute = closeCal.get(java.util.Calendar.MINUTE); + + int currentTimeInMinutes = currentHour * 60 + currentMinute; + int openTimeInMinutes = openHour * 60 + openMinute; + int closeTimeInMinutes = closeHour * 60 + closeMinute; + + if (currentTimeInMinutes >= openTimeInMinutes && currentTimeInMinutes <= closeTimeInMinutes) { + return R.fail("开启失败"); + } + } + + // 查询开关和设备信息 + DeviceSwitch deviceSwitch = deviceSwitchMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(DeviceSwitch::getId, timingCtrl.getSwitchId()) + .select(DeviceSwitch::getId, + DeviceSwitch::getDeviceId, + DeviceSwitch::getIndex, + DeviceSwitch::getSwitchName) + ); + + if (deviceSwitch == null) { + return R.fail("定时控制不存在"); + } + + // 查询设备信息 + Device device = deviceMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(Device::getId, deviceSwitch.getDeviceId()) + .select(Device::getId, Device::getUserId, Device::getDeviceName, + Device::getIotId, Device::getSerialNum, Device::getWarnCode, Device::getDeadTime) + ); + + if (device == null) { + return R.fail("定时控制不存在"); + } + + // 检查设备是否过期 + if (device.getDeadTime() != null && device.getDeadTime().before(new Date())) { + return R.fail("设备服务已过期"); + } + +// // 检查设备是否离线(warnCode & 0x0080) +// if (device.getWarnCode() != null && (device.getWarnCode() & 0x0080) != 0) { +// return R.fail("设备离线或关机"); +// } + + // 查询该开关的所有定时控制 + java.util.List allTimingCtrls = timingCtrlMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(TimingCtrl::getSwitchId, timingCtrl.getSwitchId()) + ); + + // 更新当前定时控制的状态 + for (TimingCtrl tc : allTimingCtrls) { + if (tc.getId().equals(request.getId())) { + tc.setIsOpen(request.getIsOpen().longValue()); + break; + } + } + + // 调用设置属性方法 + boolean success = setPropertyTimeCtrl(deviceSwitch, device, allTimingCtrls, request.getId()); + if (!success) { + return R.fail("设置定时控制失败"); + } + + // 更新数据库 + timingCtrlMapper.update(null, + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(TimingCtrl::getId, request.getId()) + .set(TimingCtrl::getIsOpen, request.getIsOpen().longValue()) + .set(TimingCtrl::getOpenTime, timingCtrl.getOpenTime()) + .set(TimingCtrl::getCloseTime, timingCtrl.getCloseTime()) + ); + + // TODO: 记录操作日志 + String operation = request.getIsOpen() == 1 ? "启用" : "停用"; + log.info("开关定时控制:{}({}){}d定时控制。", + device.getDeviceName(), deviceSwitch.getSwitchName(), operation); + + return R.ok(); + } catch (Exception e) { + log.error("设置定时控制开关失败: {}", e.getMessage(), e); + return R.fail("设置失败: " + e.getMessage()); + } + } + + /** + * 设置设备定时控制属性 + * + * @param deviceSwitch 开关信息 + * @param device 设备信息 + * @param allTimingCtrls 该开关的所有定时控制 + * @param timeCtrlId 当前操作的定时控制ID + * @return 是否成功 + */ + private boolean setPropertyTimeCtrl( + DeviceSwitch deviceSwitch, + Device device, + java.util.List allTimingCtrls, + Long timeCtrlId) { + + try { + java.util.List listTimingCtrl = new java.util.ArrayList<>(); + Map properties = new java.util.HashMap<>(); + + Date dateToday = new Date(); + java.util.Calendar calendar = java.util.Calendar.getInstance(); + calendar.setTime(dateToday); + calendar.set(java.util.Calendar.HOUR_OF_DAY, 0); + calendar.set(java.util.Calendar.MINUTE, 0); + calendar.set(java.util.Calendar.SECOND, 0); + calendar.set(java.util.Calendar.MILLISECOND, 0); + Date todayStart = calendar.getTime(); + + Date now = new Date(); + + for (TimingCtrl timingCtrl : allTimingCtrls) { + Date onTime = timingCtrl.getOpenTime(); + Date offTime = timingCtrl.getCloseTime(); + + // 如果是当前启用的定时控制,重新计算时间 + if (timeCtrlId.equals(timingCtrl.getId()) && timingCtrl.getIsOpen() != null && timingCtrl.getIsOpen() == 1) { + java.util.Calendar onCal = java.util.Calendar.getInstance(); + onCal.setTime(todayStart); + java.util.Calendar tempCal = java.util.Calendar.getInstance(); + tempCal.setTime(onTime); + onCal.set(java.util.Calendar.HOUR_OF_DAY, tempCal.get(java.util.Calendar.HOUR_OF_DAY)); + onCal.set(java.util.Calendar.MINUTE, tempCal.get(java.util.Calendar.MINUTE)); + onTime = onCal.getTime(); + + java.util.Calendar offCal = java.util.Calendar.getInstance(); + offCal.setTime(todayStart); + tempCal.setTime(offTime); + offCal.set(java.util.Calendar.HOUR_OF_DAY, tempCal.get(java.util.Calendar.HOUR_OF_DAY)); + offCal.set(java.util.Calendar.MINUTE, tempCal.get(java.util.Calendar.MINUTE)); + offTime = offCal.getTime(); + + // 如果开启时间小于当前时间,加1天 + if (onTime.before(now)) { + onCal.add(java.util.Calendar.DAY_OF_MONTH, 1); + onTime = onCal.getTime(); + } + + // 如果关闭时间小于当前时间,加1天 + if (offTime.before(now)) { + offCal.add(java.util.Calendar.DAY_OF_MONTH, 1); + offTime = offCal.getTime(); + } + + // 如果开启时间大于关闭时间,关闭时间加1天 + if (onTime.after(offTime)) { + offCal.add(java.util.Calendar.DAY_OF_MONTH, 1); + offTime = offCal.getTime(); + } + + // 如果当前时间在定时范围内,返回失败 + if (!now.before(onTime) && !now.after(offTime)) { + return false; + } + + // 更新定时控制的时间 + timingCtrl.setOpenTime(onTime); + timingCtrl.setCloseTime(offTime); + } + + // 转换为UTC Unix时间戳(秒) + long onUnixStamp = onTime.getTime() / 1000; + long offUnixStamp = offTime.getTime() / 1000; + + // 构建定时控制对象 + Map timeCtrl = new java.util.HashMap<>(); + timeCtrl.put("timerMode", timingCtrl.getLoopType() != null ? timingCtrl.getLoopType() - 1 : 0); + timeCtrl.put("isValid", timingCtrl.getIsOpen() != null && timingCtrl.getIsOpen() == 1 ? 1 : 0); + timeCtrl.put("onTime", String.valueOf(onUnixStamp)); + timeCtrl.put("offTime", String.valueOf(offUnixStamp)); + + listTimingCtrl.add(timeCtrl); + } + + // 设置属性 + properties.put(IOTPropertyName.LOCAL_TIMER_SWITCH + deviceSwitch.getIndex(), listTimingCtrl); + + // 调用IoT服务设置属性0 + return iotCloudService.setProperty(device.getIotId(), properties, false, 0); + } catch (Exception e) { + log.error("设置定时控制属性失败: {}", e.getMessage(), e); + return false; + } + } + + /** + * 删除定时控制 + * + * @param request 请求对象(包含定时控制ID) + * @return 操作结果 + */ + @Operation(summary = "删除定时控制") + @DeleteMapping("/timingCtrl/delete") + @Transactional(rollbackFor = Exception.class) + public R deleteTimeCtrl( + @Validated @RequestBody ReqId request) { + + try { + Long userId = LoginHelper.getUserId(); + + // 查询定时控制基本信息 + TimingCtrl dbTimeCtrl = timingCtrlMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(TimingCtrl::getId, request.getId()) + .select(TimingCtrl::getId, + TimingCtrl::getIsOpen, + TimingCtrl::getSwitchId) + ); + + if (dbTimeCtrl == null) { + return R.fail("定时控制不存在"); + } + + // 查询开关信息及关联的设备信息 + DeviceSwitch deviceSwitch = deviceSwitchMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(DeviceSwitch::getId, dbTimeCtrl.getSwitchId()) + .select(DeviceSwitch::getId, + DeviceSwitch::getDeviceId, + DeviceSwitch::getIndex, + DeviceSwitch::getSwitchName) + ); + + if (deviceSwitch == null) { + return R.fail("定时控制不存在"); + } + + // 查询设备信息 + Device device = deviceMapper.selectOne( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(Device::getId, deviceSwitch.getDeviceId()) + .select(Device::getId, Device::getUserId, Device::getDeviceName, + Device::getIotId, Device::getSerialNum, Device::getWarnCode, Device::getDeadTime) + ); + + // 权限验证 + if (device == null || device.getUserId() == null || !device.getUserId().equals(userId)) { + return R.fail("定时控制不存在"); + } + + // 检查设备是否过期 + if (device.getDeadTime() != null && device.getDeadTime().before(new Date())) { + return R.fail("设备服务已过期"); + } + +// // 检查设备是否离线(warnCode & 0x0080) +// if (device.getWarnCode() != null && (device.getWarnCode() & 0x0080) != 0) { +// return R.fail("设备离线或关机"); +// } + + // 查询该开关的所有定时控制 + java.util.List allTimingCtrls = timingCtrlMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(TimingCtrl::getSwitchId, dbTimeCtrl.getSwitchId()) + ); + + // 从列表中移除要删除的定时控制,并将其 isOpen 设置为 0 + for (TimingCtrl tc : allTimingCtrls) { + if (tc.getId().equals(request.getId())) { + tc.setIsOpen(0L); + break; + } + } + + // 调用设置属性方法,将更新后的定时控制列表同步到设备 + boolean success = setPropertyTimeCtrl(deviceSwitch, device, allTimingCtrls, request.getId()); + if (!success) { + return R.fail("设置定时控制失败"); + } + + // 从数据库中删除定时控制记录 + int deleteCount = timingCtrlMapper.deleteById(request.getId()); + if (deleteCount == 0) { + return R.fail("删除失败"); + } + + // 记录操作日志 + log.info("开关定时控制:{}({})删除定时控制。", + device.getDeviceName(), deviceSwitch.getSwitchName()); + + return R.ok(); + } catch (Exception e) { + log.error("删除定时控制失败: {}", e.getMessage(), e); + return R.fail("删除失败: " + e.getMessage()); + } + } }