diff --git a/script/sql/update/update_5.5.3_5.6.0.sql b/script/sql/update/update_5.5.3_5.6.0.sql index 4de6538..811b7ba 100644 --- a/script/sql/update/update_5.5.3_5.6.0.sql +++ b/script/sql/update/update_5.5.3_5.6.0.sql @@ -20,7 +20,6 @@ INSERT INTO sys_menu VALUES ('11654', '流程变量查询', '11621', '2', '#', ' INSERT INTO sys_menu VALUES ('11655', '流程变量修改', '11621', '3', '#', '', '', 1, 0, 'F', '0', '0', 'workflow:instance:variable', '#', 103, 1, sysdate(), null, null, ''); -- Device binding and device_no primary key migration. -ALTER TABLE app_device ADD COLUMN bind_token_hash varchar(255) NULL COMMENT '设备绑定令牌哈希' AFTER device_no; ALTER TABLE app_device ADD COLUMN qrcode varchar(1000) NULL COMMENT '设备二维码' AFTER device_img; ALTER TABLE app_watering_log ADD COLUMN remark varchar(500) NULL COMMENT '备注' AFTER trigger_type; ALTER TABLE app_scheduling_device ADD COLUMN device_no varchar(64) NULL COMMENT '设备编号' AFTER schedule_id; diff --git a/water-admin/src/main/java/org/dromara/web/controller/AuthController.java b/water-admin/src/main/java/org/dromara/web/controller/AuthController.java index 26b2e9c..d524752 100644 --- a/water-admin/src/main/java/org/dromara/web/controller/AuthController.java +++ b/water-admin/src/main/java/org/dromara/web/controller/AuthController.java @@ -15,10 +15,7 @@ import me.zhyd.oauth.request.AuthRequest; import me.zhyd.oauth.utils.AuthStateUtils; import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.domain.R; -import org.dromara.common.core.domain.model.ForgotLoginBody; -import org.dromara.common.core.domain.model.LoginBody; -import org.dromara.common.core.domain.model.RegisterBody; -import org.dromara.common.core.domain.model.SocialLoginBody; +import org.dromara.common.core.domain.model.*; import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.utils.*; import org.dromara.common.encrypt.annotation.ApiEncrypt; @@ -42,10 +39,7 @@ import org.dromara.system.service.ISysTenantService; import org.dromara.web.domain.vo.LoginTenantVo; import org.dromara.web.domain.vo.LoginVo; import org.dromara.web.domain.vo.TenantListVo; -import org.dromara.web.service.ForgotPasswordService; -import org.dromara.web.service.IAuthStrategy; -import org.dromara.web.service.SysLoginService; -import org.dromara.web.service.SysRegisterService; +import org.dromara.web.service.*; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -79,6 +73,7 @@ public class AuthController { private final ISysClientService clientService; private final ScheduledExecutorService scheduledExecutorService; private final ForgotPasswordService forgotPasswordService; + private final AccountCancellationService accountCancellationService; /** * 登录方法 @@ -189,6 +184,17 @@ public class AuthController { return R.ok("退出成功"); } + /** + * 注销当前账号 + */ + @DeleteMapping("/account") + public R cancelAccount(@Validated @RequestBody AccountCancelBody body) { + StpUtil.checkLogin(); + accountCancellationService.cancelCurrentAccount(body.getCode()); + StpUtil.logout(); + return R.ok("注销成功"); + } + /** * 用户注册 */ diff --git a/water-admin/src/main/java/org/dromara/web/controller/CaptchaController.java b/water-admin/src/main/java/org/dromara/web/controller/CaptchaController.java index 11e0bab..37950ba 100644 --- a/water-admin/src/main/java/org/dromara/web/controller/CaptchaController.java +++ b/water-admin/src/main/java/org/dromara/web/controller/CaptchaController.java @@ -1,6 +1,7 @@ package org.dromara.web.controller; import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.stp.StpUtil; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.captcha.generator.MathGenerator; import cn.hutool.captcha.generator.RandomGenerator; @@ -21,11 +22,14 @@ import org.dromara.common.mail.utils.MailUtils; import org.dromara.common.ratelimiter.annotation.RateLimiter; import org.dromara.common.ratelimiter.enums.LimitType; import org.dromara.common.redis.utils.RedisUtils; -import org.dromara.common.web.core.WaveAndCircleCaptcha; +import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.web.config.properties.CaptchaProperties; +import org.dromara.common.web.core.WaveAndCircleCaptcha; import org.dromara.sms4j.api.SmsBlend; import org.dromara.sms4j.api.entity.SmsResponse; import org.dromara.sms4j.core.factory.SmsFactory; +import org.dromara.system.domain.vo.SysUserVo; +import org.dromara.system.service.ISysUserService; import org.dromara.web.domain.vo.CaptchaVo; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; @@ -52,6 +56,7 @@ public class CaptchaController { private final CaptchaProperties captchaProperties; private final MailProperties mailProperties; + private final ISysUserService userService; /** @@ -66,20 +71,20 @@ public class CaptchaController { String code = RandomUtil.randomNumbers(6); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); -// if (Validator.isMobile(username)){ -// // 验证码模板id 自行处理 (查数据库或写死均可) -// String templateId = "SMS_333877107"; -// LinkedHashMap map = new LinkedHashMap<>(1); -// map.put("code", code); -// SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); -// SmsResponse smsResponse = smsBlend.sendMessage(username, templateId, map); -// if (!smsResponse.isSuccess()) { -// log.error("验证码短信发送异常 => {}", smsResponse); -// return R.fail(smsResponse.getData().toString()); -// } -// }else { -// emailCodeImpl(username); -// } + if (Validator.isMobile(username)){ + // 验证码模板id 自行处理 (查数据库或写死均可) + String templateId = "SMS_333877107"; + LinkedHashMap map = new LinkedHashMap<>(1); + map.put("code", code); + SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); + SmsResponse smsResponse = smsBlend.sendMessage(username, templateId, map); + if (!smsResponse.isSuccess()) { + log.error("验证码短信发送异常 => {}", smsResponse); + return R.fail(smsResponse.getData().toString()); + } + }else { + emailCodeImpl(username); + } return R.ok(code); } @@ -95,7 +100,49 @@ public class CaptchaController { @RateLimiter(key = "#phonenumber", time = 60, count = 1) @GetMapping("/resource/sms/code") public R smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) { - String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber; + R result = sendSmsCode(phonenumber, phonenumber); + if (R.isError(result)) { + return result; + } + return R.ok(); + } + + /** + * 注销账号验证码 + */ + @GetMapping("/resource/account/cancel/code") + public R accountCancelCode() { + StpUtil.checkLogin(); + Long userId = LoginHelper.getUserId(); + String username = LoginHelper.getUsername(); + if (userId == null || StringUtils.isBlank(username)) { + return R.fail("用户未登录"); + } + SysUserVo user = userService.selectUserById(userId); + if (user == null) { + return R.fail("用户不存在"); + } + if (Validator.isMobile(user.getPhonenumber())) { + return sendSmsCode(user.getPhonenumber(), username); + } + if (StringUtils.isNotBlank(user.getEmail())) { + if (!mailProperties.getEnabled()) { + return R.fail("当前系统没有开启邮箱功能!"); + } + emailCodeImpl(user.getEmail(), username); + return R.ok("操作成功"); + } + return R.fail("当前账号未绑定手机号或邮箱"); + } + + /** + * 发送短信验证码 + * + * @param phonenumber 接收手机号 + * @param cacheKey 验证码缓存标识 + */ + public R sendSmsCode(String phonenumber, String cacheKey) { + String key = GlobalConstants.CAPTCHA_CODE_KEY + cacheKey; String code = RandomUtil.randomNumbers(6); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); // 验证码模板id 自行处理 (查数据库或写死均可) @@ -131,7 +178,15 @@ public class CaptchaController { */ @RateLimiter(key = "#email", time = 60, count = 1) public void emailCodeImpl(String email) { - String key = GlobalConstants.CAPTCHA_CODE_KEY + email; + emailCodeImpl(email, email); + } + + /** + * 邮箱验证码 + */ + @RateLimiter(key = "#email", time = 60, count = 1) + public void emailCodeImpl(String email, String cacheKey) { + String key = GlobalConstants.CAPTCHA_CODE_KEY + cacheKey; String code = RandomUtil.randomNumbers(6); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); try { diff --git a/water-admin/src/main/java/org/dromara/web/service/SysRegisterService.java b/water-admin/src/main/java/org/dromara/web/service/SysRegisterService.java index e69692a..ed8e791 100644 --- a/water-admin/src/main/java/org/dromara/web/service/SysRegisterService.java +++ b/water-admin/src/main/java/org/dromara/web/service/SysRegisterService.java @@ -3,7 +3,6 @@ package org.dromara.web.service; import cn.hutool.core.lang.Validator; import cn.hutool.crypto.digest.BCrypt; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.GlobalConstants; @@ -67,7 +66,7 @@ public class SysRegisterService { } SysUserBo sysUser = new SysUserBo(); - // sysUser.setUserName(username); + sysUser.setUserName(username); sysUser.setNickName("用户"+new Date().getTime()); @@ -76,6 +75,7 @@ public class SysRegisterService { boolean exist; if(Validator.isEmail(username)){ sysUser.setEmail(username); + exist = TenantHelper.dynamic(tenantId, () -> { return userMapper.exists(new LambdaQueryWrapper() .eq(SysUser::getEmail, sysUser.getEmail())); diff --git a/water-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java b/water-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java index 597a601..e6daa17 100644 --- a/water-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java +++ b/water-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java @@ -58,6 +58,7 @@ public class SmsAuthStrategy implements IAuthStrategy { }); loginUser.setClientKey(client.getClientKey()); loginUser.setDeviceType(client.getDeviceType()); + //loginUser.setUsername(phonenumber); SaLoginParameter model = new SaLoginParameter(); model.setDeviceType(client.getDeviceType()); // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 diff --git a/water-admin/src/main/resources/application-dev.yml b/water-admin/src/main/resources/application-dev.yml index 32a2d5e..91f8442 100644 --- a/water-admin/src/main/resources/application-dev.yml +++ b/water-admin/src/main/resources/application-dev.yml @@ -96,13 +96,13 @@ spring: spring.data: redis: # 地址 - host: localhost + host: 47.97.217.123 # 端口,默认为6379 port: 6379 # 数据库索引 database: 0 # redis 密码必须配置 - #password: + password: waterx123 # 连接超时时间 timeout: 10s # 是否开启ssl diff --git a/water-admin/src/main/resources/application.yml b/water-admin/src/main/resources/application.yml index b5c5881..613f655 100644 --- a/water-admin/src/main/resources/application.yml +++ b/water-admin/src/main/resources/application.yml @@ -22,9 +22,10 @@ server: --- # mqtt配置信息 mqtt: enabled: ${MQTT_ENABLED:true} - broker-url: ssl://service.reinkun.com:8883 + #broker-url: ssl://www.mmipco.cn:8883 + broker-url: tcp://47.97.217.123 username: admin - password: yukun_mqtt + password: 61e6062129e9 client-id: water-server-${server.port} qos: 1 keep-alive: 60 @@ -46,7 +47,7 @@ mqtt: command-ack: enabled: true max-retry-count: 3 - retry-interval-ms: 5000 + retry-interval-ms: 30000 scan-interval-ms: 5000 pending-ttl-seconds: 86400 ack-ttl-seconds: 86400 @@ -66,6 +67,7 @@ mqtt: - /+/publish/register #设备注册 - /+/publish/power #电量 - /+/publish/ack #应答 + - /+/publish/start #开始浇水上报 - /+/publish/finish/key #按键浇水完成 - /+/publish/error #硬件故障 # - /+/subscriber/cmd #下发命令 0关闭浇水 1开始浇水 @@ -123,9 +125,9 @@ spring: servlet: multipart: # 单个文件大小 - max-file-size: 10MB + max-file-size: 100MB # 设置总上传的文件大小 - max-request-size: 20MB + max-request-size: 100MB mvc: # 设置静态资源路径 防止所有请求都去查静态资源 static-path-pattern: /static/** diff --git a/water-common/water-common-mqtt/src/main/java/org/dromara/mqtt/MqttCommandAckService.java b/water-common/water-common-mqtt/src/main/java/org/dromara/mqtt/MqttCommandAckService.java index 7a9c227..64a9b1a 100644 --- a/water-common/water-common-mqtt/src/main/java/org/dromara/mqtt/MqttCommandAckService.java +++ b/water-common/water-common-mqtt/src/main/java/org/dromara/mqtt/MqttCommandAckService.java @@ -50,6 +50,24 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler { withCommandLock(commandId, () -> deletePending(commandId)); } + static boolean resolveMissingCommandId(DeviceCommandAck ack, List pendingCommands) { + if (ack == null || StringUtils.isNotBlank(ack.getCommandId())) { + return ack != null; + } + if (pendingCommands == null || pendingCommands.size() != 1) { + return false; + } + DeviceCommand command = pendingCommands.get(0); + if (command == null || StringUtils.isBlank(command.getCommandId())) { + return false; + } + ack.setCommandId(command.getCommandId()); + if (StringUtils.isBlank(ack.getStatus())) { + ack.setStatus("1"); + } + return true; + } + @Override public void handleAck(String deviceNo, String payload) { if (StringUtils.isBlank(payload)) { @@ -70,11 +88,19 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler { log.warn("[MQTT] ACK JSON 格式错误 设备编号={} 消息体={}", deviceNo, payload, e); return; } - if (ack == null || ack.getCommandId() == null) { + if (ack == null) { log.warn("[MQTT] ACK 缺少命令编号 设备编号={} 消息体={}", deviceNo, payload); return; } ack.setDeviceNo(deviceNo); + if (StringUtils.isBlank(ack.getCommandId())) { + List pendingCommands = findPendingCommandsByDeviceNo(deviceNo); + if (!resolveMissingCommandId(ack, pendingCommands)) { + log.warn("[MQTT] ACK 缺少命令编号且无法唯一匹配待确认命令 设备编号={} 待确认数量={} 消息体={}", + deviceNo, pendingCommands.size(), payload); + return; + } + } boolean ackSaved = withCommandLock(ack.getCommandId(), () -> { deletePending(ack.getCommandId()); RedisUtils.setCacheObject(ackKey(ack.getCommandId()), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds())); @@ -84,7 +110,7 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler { return; } refreshDeviceOnline(deviceNo); - log.info("[MQTT] 收到命令确认 设备编号={} 命令编号={} 状态={}", deviceNo, ack.getCommandId(), ack.getStatus()); + log.info("[MQTT] 收到命令确认 设备编号={} 命令编号={} message={}", deviceNo, ack.getCommandId(), ack.getMessage()); } private void handlePlainAck(String deviceNo, String payload) { diff --git a/water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java b/water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java index 9b84ad5..6d9f935 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java @@ -47,6 +47,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.text.SimpleDateFormat; import java.util.*; @@ -66,6 +67,7 @@ public class AppController extends BaseController { private final IAppWateringLogService appWateringLogService; private final IDeviceCommandService deviceCommandService; private final ISysOssService ossService; + private final IAppVersionService appVersionService; /** *查询设备 @@ -288,21 +290,12 @@ public class AppController extends BaseController { } private Map buildScheduleBindPayload(Long scheduleId, String deviceNo) { - // AppScheduleVo schedule = getOwnedSchedule(scheduleId); + List details = appScheduleDetailService.queryByScheduleIdByStatus(scheduleId); - -// Map schedulePayload = new HashMap<>(); -// schedulePayload.put("id", schedule.getId()); -// schedulePayload.put("name", schedule.getName()); -// schedulePayload.put("status", schedule.getStatus()); - Map payload = new HashMap<>(); - // payload.put("cmd", -1); payload.put("deviceNo", deviceNo); - // payload.put("schedule", schedulePayload); payload.put("details", toScheduleDetailPayload(details)); return payload; - // return toScheduleDetailPayload(details); } private List> toScheduleDetailPayload(List details) { @@ -317,13 +310,16 @@ public class AppController extends BaseController { timeSlots.add(timeSlot); } - Map item = new HashMap<>(); - item.put("id", detail.getId()); - item.put("weekday", detail.getWeekday()); - item.put("timeData", timeSlots); - item.put("triggerType", detail.getTriggerType()); - item.put("status", detail.getStatus()); - detailPayload.add(item); + if (detail.getStatus().equals("1")){ + Map item = new HashMap<>(); + // item.put("id", detail.getId()); + item.put("weekday", detail.getWeekday()); + item.put("timeData", timeSlots); + item.put("triggerType", detail.getTriggerType()); + item.put("status", detail.getStatus()); + detailPayload.add(item); + } + } return detailPayload; } @@ -470,7 +466,7 @@ public class AppController extends BaseController { Boolean updated = appScheduleService.updateStatusByBo(bo); if (updated) { // 向所有绑定设备下发排程状态变更 - notifyBoundDevicesScheduleUpdate(bo.getId()); + notifyBoundDevicesScheduleStatusChange(bo.getId(), bo.getStatus()); } return toAjax(updated); } catch (Exception e) { @@ -560,6 +556,39 @@ public class AppController extends BaseController { return appWateringLogVoTableDataInfo; } + /** + * 查询设备最新进行中的浇水记录 + */ + @GetMapping("/latestWaterLog") + public R> latestWaterLog(@RequestParam String deviceNo) { + try { + assertDeviceOwned(deviceNo); + + AppWateringLogBo bo = new AppWateringLogBo(); + bo.setUserId(LoginHelper.getUserId()); + bo.setDeviceNo(deviceNo); + bo.setStatus("1"); + List logs = appWateringLogService.queryList(bo); + if (logs == null || logs.isEmpty()) { + return R.ok(null); + } + + AppWateringLogVo latest = logs.stream() + .max(Comparator.comparing(AppWateringLogVo::getId, Comparator.nullsFirst(Long::compareTo))) + .orElse(null); + if (latest == null) { + return R.ok(null); + } + + Map result = new HashMap<>(); + result.put("startTime", formatSecondTime(latest.getStartTime())); + result.put("endTime", formatSecondTime(latest.getEndTime())); + return R.ok(result); + } catch (Exception e) { + return fail(e); + } + } + /** * 删除浇水记录 @@ -666,6 +695,41 @@ public class AppController extends BaseController { } } + /** + * 检查APP版本更新 + */ + @GetMapping("/checkVersion") + public R checkVersion(@RequestParam(required = false, defaultValue = "android") String platform, + @RequestParam(required = false) String currentVersion) { + try { + if (StringUtils.isBlank(currentVersion)) { + throw new ServiceException("当前版本不能为空"); + } + String normalizedPlatform = StringUtils.lowerCase(StringUtils.trim(platform)); + if (!StringUtils.equalsAny(normalizedPlatform, "android", "ios")) { + throw new ServiceException("平台类型仅支持 android 或 ios"); + } + + AppVersionVo version = appVersionService.queryLatestByPlatform(normalizedPlatform); + if (version == null || StringUtils.isBlank(version.getLatestVersion())) { + throw new ServiceException("版本配置不存在"); + } + + AppVersionCheckVo result = new AppVersionCheckVo(); + result.setPlatform(normalizedPlatform); + result.setCurrentVersion(currentVersion); + result.setLatestVersion(version.getLatestVersion()); + result.setVersionCode(version.getVersionCode()); + result.setUpdateAvailable(!StringUtils.equals(currentVersion, version.getLatestVersion())); + result.setForceUpdate(StringUtils.equals(version.getForceUpdate(), "1")); + result.setDownloadUrl(version.getDownloadUrl()); + result.setReleaseNotes(version.getReleaseNotes()); + return R.ok(result); + } catch (Exception e) { + return fail(e); + } + } + /** * 修改用户 */ @@ -763,6 +827,10 @@ public class AppController extends BaseController { return message.equals(i18nMessage) ? message : i18nMessage; } + private String formatSecondTime(Date time) { + return time == null ? null : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time); + } + private AppDeviceVo getOwnedDevice(String deviceNo) { if (StringUtils.isBlank(deviceNo)) { throw new ServiceException("app.device.no.not.blank"); @@ -833,6 +901,25 @@ public class AppController extends BaseController { } } + /** + * 按排程开关状态下发不同主题:关闭走取消主题,开启重新下发排程。 + */ + private void notifyBoundDevicesScheduleStatusChange(Long scheduleId, String status) { + List deviceNos = findBoundDeviceNos(scheduleId); + for (String deviceNo : deviceNos) { + try { + if ("0".equals(status)) { + deviceCommandService.sendScheduleCanceledCommand(deviceNo, scheduleId); + } else { + deviceCommandService.sendScheduleBindCommand(deviceNo, buildScheduleBindPayload(scheduleId, deviceNo)); + } + } catch (Exception e) { + log.warn("[排程通知] 下发排程状态变更失败 排程ID={} 状态={} 设备编号={} 原因={}", + scheduleId, status, deviceNo, e.getMessage()); + } + } + } + /** * 向排程关联的所有设备下发排程解绑通知 */ diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/AppDevice.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/AppDevice.java index d835fdf..503958e 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/domain/AppDevice.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/AppDevice.java @@ -20,11 +20,10 @@ public class AppDevice extends BaseEntity { @TableId(value = "device_no") private String deviceNo; - private String bindTokenHash; - private Long userId; private String deviceName; + private String deviceInitName; private String deviceImg; diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/AppWateringLog.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/AppWateringLog.java index 303d33c..bb2e328 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/domain/AppWateringLog.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/AppWateringLog.java @@ -78,6 +78,7 @@ public class AppWateringLog extends BaseEntity { * 备注 */ private String remark; + private String status; /** * 创建时间 diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppDeviceBo.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppDeviceBo.java index 262a115..865356f 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppDeviceBo.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppDeviceBo.java @@ -19,9 +19,6 @@ public class AppDeviceBo extends BaseEntity { @NotBlank(message = "设备编号不能为空", groups = { EditGroup.class }) private String deviceNo; - private String bindToken; - - private String bindTokenHash; private Long userId; @@ -29,6 +26,7 @@ public class AppDeviceBo extends BaseEntity { * 设备名称 */ private String deviceName; + private String deviceInitName; /** * 设备图片 diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppWateringLogBo.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppWateringLogBo.java index c825607..6ab0dc7 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppWateringLogBo.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppWateringLogBo.java @@ -76,6 +76,7 @@ public class AppWateringLogBo extends BaseEntity { * 备注 */ private String remark; + private String status; /** * 创建时间 diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppDeviceVo.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppDeviceVo.java index 4eb2742..219334c 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppDeviceVo.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppDeviceVo.java @@ -26,6 +26,7 @@ public class AppDeviceVo implements Serializable { @ExcelProperty(value = "设备名称") private String deviceName; + private String deviceInitName; @ExcelProperty(value = "设备图片") private String deviceImg; diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppWateringLogVo.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppWateringLogVo.java index 0701537..8dc6244 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppWateringLogVo.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppWateringLogVo.java @@ -92,6 +92,7 @@ public class AppWateringLogVo implements Serializable { */ @ExcelProperty(value = "备注") private String remark; + private String status; /** * 创建时间 diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceRegisterHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceRegisterHandler.java index 0794fbd..2cbaf7f 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceRegisterHandler.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceRegisterHandler.java @@ -12,6 +12,7 @@ import org.dromara.app.service.IAppDeviceService; import org.dromara.app.service.IDeviceCommandPublisher; import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.redis.utils.RedisUtils; +import org.jspecify.annotations.Nullable; import org.redisson.api.RLock; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; @@ -92,6 +93,7 @@ DeviceRegisterHandler implements MqttTopicHandler { return device; } device.setDeviceName(valueAsString(dto.get("deviceName"))); + device.setDeviceInitName(valueAsString(dto.get("deviceName"))); device.setPowerLevel(valueAsString(dto.get("powerLevel"))); device.setDeviceEm(valueAsString(dto.get("deviceEm"))); device.setDeviceSn(valueAsString(dto.get("deviceSn"))); @@ -143,7 +145,7 @@ DeviceRegisterHandler implements MqttTopicHandler { } } - private String firstNotBlank(Object first, Object second) { + private @Nullable String firstNotBlank(Object first, Object second) { String firstText = valueAsString(first); if (firstText != null && !firstText.isBlank()) { return firstText; diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/KeyFinishHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/KeyFinishHandler.java index e4421b7..83e188a 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/handler/KeyFinishHandler.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/KeyFinishHandler.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.app.domain.AppSchedulingDevice; +import org.dromara.app.domain.bo.AppDeviceBo; import org.dromara.app.domain.bo.AppWateringLogBo; import org.dromara.app.domain.vo.AppDeviceVo; import org.dromara.app.mapper.AppSchedulingDeviceMapper; @@ -53,11 +54,8 @@ public class KeyFinishHandler implements MqttTopicHandler { } Map dto = JsonUtils.parseObject(payload, Map.class); AppWateringLogBo logBo = buildWateringLog(deviceNo, dto); - if ("0".equals(logBo.getTriggerType())) { - appWateringLogService.confirmScheduleLog(logBo); - } else { - appWateringLogService.insertByBo(logBo); - } + appWateringLogService.confirmScheduleLog(logBo); + updateDeviceWorkStatusIdle(deviceNo); log.info("[MQTT] 按键浇水完成上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload); } catch (Exception e) { log.error("[MQTT] 按键浇水完成上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}", @@ -89,6 +87,7 @@ public class KeyFinishHandler implements MqttTopicHandler { logBo.setStartTime(startTime); logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin)); logBo.setTriggerType("1"); + logBo.setStatus("0"); if ("schedule".equals(valueAsString(dto.get("triggerON")))){ logBo.setTriggerType("0"); logBo.setScheduleId(queryScheduleId(topicDeviceNo)); @@ -99,6 +98,13 @@ public class KeyFinishHandler implements MqttTopicHandler { return logBo; } + private void updateDeviceWorkStatusIdle(String deviceNo) { + AppDeviceBo deviceBo = new AppDeviceBo(); + deviceBo.setDeviceNo(deviceNo); + deviceBo.setWorkStatus("0"); + appDeviceService.updateByBo(deviceBo); + } + private Long queryUserId(String deviceNo) { AppDeviceVo device = appDeviceService.queryById(deviceNo); return device == null ? null : device.getUserId(); diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/ScheduleFinishHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/ScheduleFinishHandler.java index e09e6fd..b37c8a8 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/handler/ScheduleFinishHandler.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/ScheduleFinishHandler.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.app.domain.AppSchedulingDevice; +import org.dromara.app.domain.bo.AppDeviceBo; import org.dromara.app.domain.bo.AppWateringLogBo; import org.dromara.app.domain.vo.AppDeviceVo; import org.dromara.app.mapper.AppSchedulingDeviceMapper; @@ -54,6 +55,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler { Map dto = JsonUtils.parseObject(payload, Map.class); AppWateringLogBo logBo = buildWateringLog(deviceNo, dto); appWateringLogService.confirmScheduleLog(logBo); + updateDeviceWorkStatusIdle(deviceNo); log.info("[MQTT] 浇水排程上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload); } catch (Exception e) { log.error("[MQTT] 浇水排程上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}", @@ -86,6 +88,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler { logBo.setStartTime(startTime); logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin)); logBo.setTriggerType("0"); + logBo.setStatus("0"); if ("mqtt on".equals(valueAsString(dto.get("triggerON")))){ logBo.setTriggerType("1"); } @@ -95,6 +98,13 @@ public class ScheduleFinishHandler implements MqttTopicHandler { return logBo; } + private void updateDeviceWorkStatusIdle(String deviceNo) { + AppDeviceBo deviceBo = new AppDeviceBo(); + deviceBo.setDeviceNo(deviceNo); + deviceBo.setWorkStatus("0"); + appDeviceService.updateByBo(deviceBo); + } + private Long queryUserId(String deviceNo) { AppDeviceVo device = appDeviceService.queryById(deviceNo); return device == null ? null : device.getUserId(); @@ -128,6 +138,16 @@ public class ScheduleFinishHandler implements MqttTopicHandler { // Try the next supported format. } } + String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + for (String pattern : new String[]{"yyyy-MM-dd H:mm:ss", "yyyy-MM-dd H:mm"}) { + try { + SimpleDateFormat format = new SimpleDateFormat(pattern); + format.setLenient(false); + return format.parse(today + " " + startTime); + } catch (ParseException ignored) { + // Try the next supported format. + } + } throw new IllegalArgumentException("Unsupported startTime format: " + startTime); } diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/StartWaterHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/StartWaterHandler.java new file mode 100644 index 0000000..0d9abc7 --- /dev/null +++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/StartWaterHandler.java @@ -0,0 +1,237 @@ +package org.dromara.app.handler; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.app.domain.AppSchedulingDevice; +import org.dromara.app.domain.bo.AppDeviceBo; +import org.dromara.app.domain.bo.AppWateringLogBo; +import org.dromara.app.domain.vo.AppDeviceVo; +import org.dromara.app.domain.vo.AppWateringLogVo; +import org.dromara.app.mapper.AppSchedulingDeviceMapper; +import org.dromara.app.mqtt.MqttTopicHandler; +import org.dromara.app.service.IAppDeviceService; +import org.dromara.app.service.IAppWateringLogService; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.json.utils.JsonUtils; +import org.springframework.stereotype.Component; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.regex.Pattern; + +/** + * 开始浇水上报处理器,匹配 /{deviceNo}/publish/startWater。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class StartWaterHandler implements MqttTopicHandler { + + private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/start$"); + + private final IAppWateringLogService appWateringLogService; + private final IAppDeviceService appDeviceService; + private final AppSchedulingDeviceMapper schedulingDeviceMapper; + private final DeviceIdentityResolver deviceIdentityResolver; + + @Override + public Pattern topicPattern() { + return PATTERN; + } + + @Override + public void handle(String deviceIdentity, String payload) { + String deviceNo = null; + try { + deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity); + if (deviceNo == null) { + log.warn("[MQTT] 开始浇水上报未找到设备 时间={} 设备标识={} 消息体={}", + HandlerLogTime.now(), deviceIdentity, payload); + return; + } + Map dto = JsonUtils.parseObject(payload, Map.class); + AppWateringLogBo logBo = buildWateringLog(deviceNo, dto); + if (hasDuplicateRunningLog(logBo)) { + updateDeviceWorkStatusWorking(deviceNo); + log.info("[MQTT] 开始浇水上报已存在进行中记录,跳过保存 时间={} 设备编号={} 开始时间={} 浇水方式={}", + HandlerLogTime.now(), deviceNo, logBo.getStartTime(), logBo.getTriggerType()); + return; + } + appWateringLogService.insertByBo(logBo); + updateDeviceWorkStatusWorking(deviceNo); + log.info("[MQTT] 开始浇水上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload); + } catch (Exception e) { + log.error("[MQTT] 开始浇水上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}", + HandlerLogTime.now(), deviceIdentity, deviceNo, payload, e); + } + } + + private void updateDeviceWorkStatusWorking(String deviceNo) { + AppDeviceBo deviceBo = new AppDeviceBo(); + deviceBo.setDeviceNo(deviceNo); + deviceBo.setWorkStatus("1"); + appDeviceService.updateByBo(deviceBo); + } + + private AppWateringLogBo buildWateringLog(String topicDeviceNo, Map dto) { + String payloadDeviceNo = valueAsString(dto.get("deviceNo")); + if (StringUtils.isNotBlank(payloadDeviceNo) && !topicDeviceNo.equals(payloadDeviceNo)) { + log.warn("[MQTT] 开始浇水上报设备编号不一致 时间={} 主题设备编号={} 消息体设备编号={}", + HandlerLogTime.now(), topicDeviceNo, payloadDeviceNo); + } + + String triggerType = resolveTriggerType(dto); + Date startTime = parseStartTime(valueAsString(dto.get("startTime"))); + Integer durationMin = valueAsInteger(dto.get("durationMin")); + + AppWateringLogBo logBo = new AppWateringLogBo(); + logBo.setDeviceNo(topicDeviceNo); + logBo.setCommandId(valueAsString(dto.get("commandId"))); + logBo.setUserId(queryUserId(topicDeviceNo)); + logBo.setScheduleId("0".equals(triggerType) ? queryScheduleId(topicDeviceNo) : 0L); + logBo.setStartTime(startTime); + logBo.setEndTime(resolveEndTime(startTime, durationMin)); + logBo.setDurationMin(durationMin == null ? null : String.valueOf(durationMin)); + logBo.setTriggerType(triggerType); + logBo.setStatus("1"); + logBo.setCreateTime(new Date()); + logBo.setRemark("0".equals(triggerType) ? "设备上报排程开始浇水" : "设备上报手动开始浇水"); + return logBo; + } + + private boolean hasDuplicateRunningLog(AppWateringLogBo logBo) { + AppWateringLogBo query = new AppWateringLogBo(); + query.setDeviceNo(logBo.getDeviceNo()); + query.setScheduleId(logBo.getScheduleId()); + query.setTriggerType(logBo.getTriggerType()); + List exists = appWateringLogService.queryList(query); + if (exists == null || exists.isEmpty()) { + return false; + } + return exists.stream().anyMatch(item -> { + boolean sameCommand = StringUtils.isNotBlank(logBo.getCommandId()) + && Objects.equals(item.getCommandId(), logBo.getCommandId()); + boolean sameWateringWindow = sameMinute(item.getStartTime(), logBo.getStartTime()); + boolean running = "1".equals(item.getStatus()) + || (StringUtils.isBlank(item.getStatus()) && item.getEndTime() == null); + return Objects.equals(item.getDeviceNo(), logBo.getDeviceNo()) + && scheduleIdMatches(item.getScheduleId(), logBo) + && Objects.equals(item.getTriggerType(), logBo.getTriggerType()) + && running + && (sameCommand || sameWateringWindow); + }); + } + + private boolean scheduleIdMatches(Long existsScheduleId, AppWateringLogBo logBo) { + if ("1".equals(logBo.getTriggerType())) { + return existsScheduleId == null || existsScheduleId == 0L; + } + return Objects.equals(existsScheduleId, logBo.getScheduleId()); + } + + private boolean sameMinute(Date existsStartTime, Date reportStartTime) { + if (existsStartTime == null || reportStartTime == null) { + return Objects.equals(existsStartTime, reportStartTime); + } + Calendar existsCalendar = Calendar.getInstance(); + existsCalendar.setTime(existsStartTime); + Calendar reportCalendar = Calendar.getInstance(); + reportCalendar.setTime(reportStartTime); + return existsCalendar.get(Calendar.YEAR) == reportCalendar.get(Calendar.YEAR) + && existsCalendar.get(Calendar.DAY_OF_YEAR) == reportCalendar.get(Calendar.DAY_OF_YEAR) + && existsCalendar.get(Calendar.HOUR_OF_DAY) == reportCalendar.get(Calendar.HOUR_OF_DAY) + && existsCalendar.get(Calendar.MINUTE) == reportCalendar.get(Calendar.MINUTE); + } + + private Date resolveEndTime(Date startTime, Integer durationMin) { + if (startTime == null || durationMin == null || durationMin <= 0) { + return null; + } + return new Date(startTime.getTime() + durationMin * 60000L); + } + + private String resolveTriggerType(Map dto) { + String triggerType = valueAsString(dto.get("triggerType")); + if ("0".equals(triggerType) || "1".equals(triggerType)) { + return triggerType; + } + + String trigger = valueAsString(dto.get("triggerON")); + if (StringUtils.isBlank(trigger)) { + trigger = valueAsString(dto.get("wateringType")); + } + if (StringUtils.isBlank(trigger)) { + trigger = valueAsString(dto.get("type")); + } + if (StringUtils.isBlank(trigger)) { + return "1"; + } + + String normalized = trigger.trim().toLowerCase(Locale.ROOT); + if ("schedule".equals(normalized) || "0".equals(normalized)) { + return "0"; + } + return "1"; + } + + private Long queryUserId(String deviceNo) { + AppDeviceVo device = appDeviceService.queryById(deviceNo); + return device == null ? null : device.getUserId(); + } + + private Long queryScheduleId(String deviceNo) { + List schedulingDevices = schedulingDeviceMapper.selectList( + Wrappers.lambdaQuery() + .eq(AppSchedulingDevice::getDeviceNo, deviceNo) + ); + if (schedulingDevices == null || schedulingDevices.isEmpty()) { + log.warn("[MQTT] 开始浇水上报未找到设备绑定的排程 时间={} 设备编号={}", HandlerLogTime.now(), deviceNo); + return 0L; + } + if (schedulingDevices.size() > 1) { + log.warn("[MQTT] 开始浇水上报匹配到多个排程 时间={} 设备编号={} 数量={}", HandlerLogTime.now(), deviceNo, schedulingDevices.size()); + } + return schedulingDevices.get(0).getScheduleId(); + } + + private Date parseStartTime(String startTime) { + if (StringUtils.isBlank(startTime)) { + return new Date(); + } + for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"}) { + try { + SimpleDateFormat format = new SimpleDateFormat(pattern); + format.setLenient(false); + return format.parse(startTime); + } catch (ParseException ignored) { + // Try the next supported format. + } + } + try { + String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd H:mm"); + format.setLenient(false); + return format.parse(today + " " + startTime); + } catch (ParseException ignored) { + // Fall through and report the unsupported format. + } + throw new IllegalArgumentException("Unsupported startTime format: " + startTime); + } + + private String valueAsString(Object value) { + return value == null ? null : String.valueOf(value); + } + + private Integer valueAsInteger(Object value) { + if (value == null) { + return null; + } + if (value instanceof Number number) { + return number.intValue(); + } + String text = String.valueOf(value); + return StringUtils.isBlank(text) ? null : Integer.valueOf(text); + } +} diff --git a/water-modules/water-app/src/main/java/org/dromara/app/mapper/AppDeviceMapper.java b/water-modules/water-app/src/main/java/org/dromara/app/mapper/AppDeviceMapper.java index b254563..a741d8a 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/mapper/AppDeviceMapper.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/mapper/AppDeviceMapper.java @@ -27,6 +27,7 @@ public interface AppDeviceMapper extends BaseMapperPlus d.device_no, d.user_id, d.device_name, + d.device_init_name, d.device_img, d.qrcode, d.status, @@ -53,6 +54,9 @@ public interface AppDeviceMapper extends BaseMapperPlus and d.device_name like concat('%', #{bo.deviceName}, '%') + + and d.device_init_name = #{bo.deviceInitName} + and d.device_img = #{bo.deviceImg} @@ -101,6 +105,7 @@ public interface AppDeviceMapper extends BaseMapperPlus d.device_no, d.user_id, d.device_name, + d.device_init_name, d.device_img, d.qrcode, d.status, @@ -127,6 +132,9 @@ public interface AppDeviceMapper extends BaseMapperPlus and d.device_name like concat('%', #{bo.deviceName}, '%') + + and d.device_init_name = #{bo.deviceInitName} + and d.device_img = #{bo.deviceImg} @@ -174,7 +182,6 @@ public interface AppDeviceMapper extends BaseMapperPlus update app_device user_id = #{userId}, - bind_token_hash = null, device_name = #{deviceName}, @@ -220,6 +227,13 @@ public interface AppDeviceMapper extends BaseMapperPlus """) AppDevice selectByMac(@Param("macAddress") String macAddress); + @Select(""" + select * + from app_device + where device_init_name = #{deviceInitName} + """) + AppDevice selectByDeviceInitName(@Param("deviceInitName") String deviceInitName); + /** * 仪表盘统计 — 单条 SQL 条件聚合,一次查询返回全部 8 个指标。 */ diff --git a/water-modules/water-app/src/main/java/org/dromara/app/service/IDeviceCommandService.java b/water-modules/water-app/src/main/java/org/dromara/app/service/IDeviceCommandService.java index 9180432..b1a24af 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/service/IDeviceCommandService.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/service/IDeviceCommandService.java @@ -61,6 +61,15 @@ public interface IDeviceCommandService { */ String sendScheduleUnbindCommand(String deviceNo, Long scheduleId); + /** + * 下发排程取消命令,通知设备暂停指定排程。 + * + * @param deviceNo 设备编号 + * @param scheduleId 排程ID + * @return commandId + */ + String sendScheduleCanceledCommand(String deviceNo, Long scheduleId); + /** * 下发设备初始化指令。用于解绑设备,允许设备未绑定用户时下发。 * diff --git a/water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppDeviceServiceImpl.java b/water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppDeviceServiceImpl.java index 1e5d904..51f2b4a 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppDeviceServiceImpl.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppDeviceServiceImpl.java @@ -37,13 +37,26 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.*; +import java.util.List; @Slf4j @RequiredArgsConstructor @Service public class AppDeviceServiceImpl implements IAppDeviceService { + private static final int QR_CODE_SIZE = 300; + private static final int QR_CODE_LABEL_HEIGHT = 20; + private static final int QR_CODE_LABEL_PADDING = 16; + private static final int QR_CODE_LABEL_FONT_SIZE = 20; + private static final int QR_CODE_LABEL_OFFSET_Y = -10; + private final AppDeviceMapper baseMapper; private final AppSchedulingDeviceMapper schedulingDeviceMapper; private final AppWateringLogMapper wateringLogMapper; @@ -150,43 +163,53 @@ public class AppDeviceServiceImpl implements IAppDeviceService { return baseMapper.updateById(device) > 0; } - @Override - public Map bindDeviceStatus(AppDeviceBo bo) { - Map params = new HashMap<>(); - params.put("bindDeviceStatus", 200); - params.put("bindDeviceStatusName", ""); - if (bo == null) { - params.put("bindDeviceStatus", 300); - params.put("bindDeviceStatusName", "设备信息不能为空"); - return params; + static byte[] renderQrCodeImage(String content, String deviceNo) { + try { + byte[] qrCodeBytes = QrCodeUtil.generatePng(content, QR_CODE_SIZE, QR_CODE_SIZE); + BufferedImage qrCodeImage = ImageIO.read(new ByteArrayInputStream(qrCodeBytes)); + BufferedImage image = new BufferedImage( + QR_CODE_SIZE, + QR_CODE_SIZE + QR_CODE_LABEL_HEIGHT, + BufferedImage.TYPE_INT_RGB + ); + + Graphics2D graphics = image.createGraphics(); + try { + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, image.getWidth(), image.getHeight()); + graphics.drawImage(qrCodeImage, 0, 0, null); + drawCenteredDeviceNo(graphics, deviceNo); + } finally { + graphics.dispose(); + } + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(image, "png", outputStream); + return outputStream.toByteArray(); + } catch (IOException e) { + throw new ServiceException("设备二维码生成失败"); + } + } + + private static void drawCenteredDeviceNo(Graphics2D graphics, String deviceNo) { + String text = StringUtils.blankToDefault(deviceNo, ""); + graphics.setColor(Color.BLACK); + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + Font font = new Font(Font.SANS_SERIF, Font.BOLD, QR_CODE_LABEL_FONT_SIZE); + graphics.setFont(font); + + FontMetrics metrics = graphics.getFontMetrics(); + int maxTextWidth = QR_CODE_SIZE - QR_CODE_LABEL_PADDING * 2; + while (metrics.stringWidth(text) > maxTextWidth && font.getSize() > 12) { + font = font.deriveFont((float) font.getSize() - 1); + graphics.setFont(font); + metrics = graphics.getFontMetrics(); } - if (StringUtils.isBlank(bo.getMacAddress())) { - params.put("bindDeviceStatus", 301); - params.put("bindDeviceStatusName", "MAC地址不能为空"); - return params; - } - params.put("bindDevice", null); - Long userId = LoginHelper.getUserId(); - AppDevice exists = baseMapper.selectByMac(bo.getMacAddress()); - if (exists == null) { - params.put("bindDeviceStatus", 302); - params.put("bindDeviceStatusName", "设备未上线注册,请先完成配网"); - return params; - } - params.put("bindDevice", exists); - if (exists.getUserId() != null && !exists.getUserId().equals(userId)) { - params.put("bindDeviceStatus", 303); - params.put("bindDeviceStatusName", "设备已被其他用户绑定"); - return params; - } - if (exists.getUserId() == null ) { - params.put("bindDeviceStatus", 304); - params.put("bindDeviceStatusName", "设备未绑定,请先绑定用户"); - return params; - } - - return params; + int x = (QR_CODE_SIZE - metrics.stringWidth(text)) / 2; + int y = QR_CODE_SIZE + (QR_CODE_LABEL_HEIGHT - metrics.getHeight()) / 2 + metrics.getAscent() + + QR_CODE_LABEL_OFFSET_Y; + graphics.drawString(text, Math.max(QR_CODE_LABEL_PADDING, x), y); } @Override @@ -254,6 +277,70 @@ public class AppDeviceServiceImpl implements IAppDeviceService { return true; } + @Override + public Map bindDeviceStatus(AppDeviceBo bo) { + Map params = new HashMap<>(); + params.put("bindDeviceStatus", 200); + params.put("bindDeviceStatusName", ""); + if (bo == null) { + params.put("bindDeviceStatus", 300); + params.put("bindDeviceStatusName", "设备信息不能为空"); + return params; + } + + if (StringUtils.isAllBlank(bo.getDeviceNo(), bo.getMacAddress(), bo.getDeviceInitName())) { + params.put("bindDeviceStatus", 301); + params.put("bindDeviceStatusName", "设备编号、MAC地址或设备初始名称不能为空"); + return params; + } + params.put("bindDevice", null); + Long userId = LoginHelper.getUserId(); + AppDevice exists = findDeviceForBindStatus(bo); + if (exists == null) { + params.put("bindDeviceStatus", 302); + params.put("bindDeviceStatusName", "设备未入库注册,请先进行入库"); + return params; + } + params.put("bindDevice", exists); + if (exists.getUserId() != null && !exists.getUserId().equals(userId)) { + params.put("bindDeviceStatus", 303); + params.put("bindDeviceStatusName", "设备已被其他用户绑定"); + return params; + } + if (exists.getUserId() == null ) { + params.put("bindDeviceStatus", 304); + params.put("bindDeviceStatusName", "设备未绑定,请先绑定用户"); + return params; + } + if (exists.getUserId() == null && exists.getWorkStatus().equals("2") && exists.getStatus().equals("0") ) { + params.put("bindDeviceStatus", 305); + params.put("bindDeviceStatusName", "设备未配网,请先去配置网络"); + return params; + } + + return params; + } + + private AppDevice findDeviceForBindStatus(AppDeviceBo bo) { + AppDevice exists = null; + if (StringUtils.isNotBlank(bo.getDeviceNo())) { + exists = baseMapper.selectById(bo.getDeviceNo()); + if (exists != null) { + return exists; + } + } + if (StringUtils.isNotBlank(bo.getMacAddress())) { + exists = baseMapper.selectByMac(bo.getMacAddress()); + if (exists != null) { + return exists; + } + } + if (StringUtils.isNotBlank(bo.getDeviceInitName())) { + return baseMapper.selectByDeviceInitName(bo.getDeviceInitName()); + } + return null; + } + @Override @Transactional(rollbackFor = Exception.class) public Boolean generateQrCode(Collection deviceNos) { @@ -266,7 +353,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService { } for (AppDevice device : devices) { - byte[] imageBytes = QrCodeUtil.generatePng(buildQrCodeContent(device), 300, 300); + byte[] imageBytes = renderQrCodeImage(buildQrCodeContent(device), device.getDeviceNo()); UploadResult uploadResult = OssFactory.instance().uploadSuffix(imageBytes, ".png", "image/png"); int rows = baseMapper.update(null, new LambdaUpdateWrapper() .set(AppDevice::getQrcode, uploadResult.getUrl()) @@ -286,7 +373,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService { content.put("deviceNo", device.getDeviceNo()); content.put("mac", device.getMacAddress()); content.put("transport", "ble"); - content.put("name", device.getDeviceName()); + content.put("name", device.getDeviceInitName()); return JsonUtils.toJsonString(content); } @@ -334,24 +421,27 @@ public class AppDeviceServiceImpl implements IAppDeviceService { throw new ServiceException("设备编号不能为空"); } - Long userId = LoginHelper.getUserId(); - if (userId == null) { - throw new ServiceException("用户未登录"); - } + boolean platformAdmin = LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin(); + Long userId = platformAdmin ? null : LoginHelper.getUserId(); + if (!platformAdmin) { + if (userId == null) { + throw new ServiceException("用户未登录"); + } - Long ownedCount = baseMapper.selectCount( - Wrappers.lambdaQuery() - .in(AppDevice::getDeviceNo, distinctDeviceNos) - .eq(AppDevice::getUserId, userId) - ); - if (ownedCount == null || ownedCount.intValue() != distinctDeviceNos.size()) { - throw new ServiceException("设备不存在或无权操作"); + Long ownedCount = baseMapper.selectCount( + Wrappers.lambdaQuery() + .in(AppDevice::getDeviceNo, distinctDeviceNos) + .eq(AppDevice::getUserId, userId) + ); + if (ownedCount == null || ownedCount.intValue() != distinctDeviceNos.size()) { + throw new ServiceException("设备不存在或无权操作"); + } } boolean flag = baseMapper.delete( Wrappers.lambdaUpdate() .in(AppDevice::getDeviceNo, distinctDeviceNos) - .eq(AppDevice::getUserId, userId) + .eq(!platformAdmin, AppDevice::getUserId, userId) ) > 0; if (flag) { schedulingDeviceMapper.delete( @@ -399,7 +489,6 @@ public class AppDeviceServiceImpl implements IAppDeviceService { .set(AppDevice::getWorkStatus, "0") .set(AppDevice::getWifiName, null) .set(AppDevice::getWifiPassword, null) - .set(AppDevice::getBindTokenHash, null) .in(AppDevice::getDeviceNo, distinctDeviceNos) .eq(AppDevice::getUserId, userId) ); diff --git a/water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppWateringLogServiceImpl.java b/water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppWateringLogServiceImpl.java index f150513..edd0d2b 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppWateringLogServiceImpl.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/service/impl/AppWateringLogServiceImpl.java @@ -18,10 +18,7 @@ import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.springframework.stereotype.Service; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; +import java.util.*; /** * 浇水记录Service业务层处理 @@ -123,6 +120,7 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService { return false; } bo.setTriggerType("0"); + bo.setStatus("0"); if (StringUtils.isBlank(bo.getRemark())) { bo.setRemark("设备离线,按排程自动补记"); } @@ -145,6 +143,18 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService { .last("limit 1") ); if (estimatedLog == null) { + AppWateringLog runningLog = findMatchingRunningScheduleLog(bo); + if (runningLog != null) { + AppWateringLog update = MapstructUtils.convert(bo, AppWateringLog.class); + update.setId(runningLog.getId()); + return baseMapper.updateById(update) > 0; + } + AppWateringLog finishedLog = findMatchingFinishedLog(bo); + if (finishedLog != null) { + AppWateringLog update = MapstructUtils.convert(bo, AppWateringLog.class); + update.setId(finishedLog.getId()); + return baseMapper.updateById(update) > 0; + } if (existsScheduleLog(bo.getDeviceNo(), bo.getScheduleId(), bo.getStartTime())) { return true; } @@ -157,6 +167,54 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService { return baseMapper.updateById(update) > 0; } + private AppWateringLog findMatchingRunningScheduleLog(AppWateringLogBo bo) { + if (bo.getStartTime() == null || StringUtils.isBlank(bo.getDurationMin()) || bo.getEndTime() == null) { + return null; + } + return baseMapper.selectOne( + Wrappers.lambdaQuery() + .eq(AppWateringLog::getDeviceNo, bo.getDeviceNo()) + .between(AppWateringLog::getStartTime, minuteStart(bo.getStartTime()), minuteEnd(bo.getStartTime())) + .eq(StringUtils.isNotBlank(bo.getTriggerType()), AppWateringLog::getTriggerType, bo.getTriggerType()) + // .between(AppWateringLog::getEndTime, minuteStart(bo.getEndTime()), minuteEnd(bo.getEndTime())) + .eq(AppWateringLog::getStatus, "1") + .last("limit 1") + ); + } + + private AppWateringLog findMatchingFinishedLog(AppWateringLogBo bo) { + if (bo.getStartTime() == null) { + return null; + } + return baseMapper.selectOne( + Wrappers.lambdaQuery() + .eq(AppWateringLog::getDeviceNo, bo.getDeviceNo()) + .between(AppWateringLog::getStartTime, minuteStart(bo.getStartTime()), minuteEnd(bo.getStartTime())) + .eq(StringUtils.isNotBlank(bo.getTriggerType()), AppWateringLog::getTriggerType, bo.getTriggerType()) + .eq(AppWateringLog::getStatus, "0") + .orderByDesc(AppWateringLog::getEndTime) + .orderByDesc(AppWateringLog::getCreateTime) + .orderByDesc(AppWateringLog::getId) + .last("limit 1") + ); + } + + private Date minuteStart(Date time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(time); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + private Date minuteEnd(Date time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(time); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 999); + return calendar.getTime(); + } + /** * 修改浇水记录 * @@ -225,30 +283,30 @@ public class AppWateringLogServiceImpl implements IAppWateringLogService { return 0; } - List activeLogs = baseMapper.selectList( + AppWateringLog activeLog = baseMapper.selectOne( Wrappers.lambdaQuery() .eq(AppWateringLog::getDeviceNo, deviceNo) - .isNull(AppWateringLog::getEndTime) + .eq(AppWateringLog::getStatus, "1") + .orderByDesc(AppWateringLog::getStartTime) + .orderByDesc(AppWateringLog::getCreateTime) + .orderByDesc(AppWateringLog::getId) + .last("limit 1") ); - if (activeLogs == null || activeLogs.isEmpty()) { + if (activeLog == null) { return 0; } - int updated = 0; - for (AppWateringLog activeLog : activeLogs) { - LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); - updateWrapper.eq(AppWateringLog::getId, activeLog.getId()) - .isNull(AppWateringLog::getEndTime) - .set(AppWateringLog::getEndTime, endTime) - .set(StringUtils.isNotBlank(remark), AppWateringLog::getRemark, remark); - if (activeLog.getStartTime() != null) { - long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime(); - long durationMin = Math.max(durationMillis, 0L) / 60000L; - updateWrapper.set(AppWateringLog::getDurationMin, String.valueOf(durationMin)); - } - updated += baseMapper.update(null, updateWrapper); + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(AppWateringLog::getId, activeLog.getId()) + .set(AppWateringLog::getEndTime, endTime) + .set(AppWateringLog::getStatus, "0") + .set(StringUtils.isNotBlank(remark), AppWateringLog::getRemark, remark); + if (activeLog.getStartTime() != null) { + long durationMillis = endTime.getTime() - activeLog.getStartTime().getTime(); + long durationMin = Math.max(durationMillis, 0L) / 60000L; + updateWrapper.set(AppWateringLog::getDurationMin, String.valueOf(durationMin)); } - return updated; + return baseMapper.update(null, updateWrapper); } /** diff --git a/water-modules/water-app/src/main/java/org/dromara/app/service/impl/DeviceCommandServiceImpl.java b/water-modules/water-app/src/main/java/org/dromara/app/service/impl/DeviceCommandServiceImpl.java index d64f3e6..c1cef66 100644 --- a/water-modules/water-app/src/main/java/org/dromara/app/service/impl/DeviceCommandServiceImpl.java +++ b/water-modules/water-app/src/main/java/org/dromara/app/service/impl/DeviceCommandServiceImpl.java @@ -69,9 +69,10 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService { command.setDeviceNo(deviceNo); command.setDeviceMac(requireDeviceMac(deviceNo)); command.setCommandType("switchDevice"); + String commandStartTime = normalizeCommandStartTime(startTime); command.getPayload().put("deviceNo", deviceNo); command.getPayload().put("cmd", workStatus); - command.getPayload().put("startTime", startTime); + command.getPayload().put("startTime", commandStartTime); if (durationMin != null) { command.getPayload().put("durationMin", durationMin); } @@ -82,12 +83,12 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService { Long userId = LoginHelper.getUserId(); if ("1".equals(workStatus)) { - AppWateringLogBo logBo = buildManualStartLog(deviceNo, userId, commandId, startTime, durationMin); + AppWateringLogBo logBo = buildManualStartLog(deviceNo, userId, commandId, commandStartTime, durationMin); wateringLogService.insertByBo(logBo); } else { - boolean updated = wateringLogService.finishManualLog(deviceNo, userId, new Date(), "手动停止浇水"); - if (!updated) { - log.warn("[命令] 未找到进行中的手动浇水记录 设备编号={} 用户ID={} 停止命令编号={}", + int updated = wateringLogService.finishRunningLogsByDevice(deviceNo, new Date(), "手动停止浇水"); + if (updated <= 0) { + log.warn("[命令] 未找到进行中的浇水记录 设备编号={} 用户ID={} 停止命令编号={}", deviceNo, userId, commandId); } } @@ -168,6 +169,24 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService { return sendCommand(command); } + @Override + public String sendScheduleCanceledCommand(String deviceNo, Long scheduleId) { + AppDeviceVo device = requireCommandDevice(deviceNo); + if (StringUtils.isBlank(device.getMacAddress())) { + throw new ServiceException("设备未登记 MAC,无法下发排程取消命令:" + deviceNo); + } + + DeviceCommand command = new DeviceCommand(); + command.setDeviceNo(deviceNo); + command.setDeviceMac(device.getMacAddress()); + command.setCommandType("cancelSchedule"); + command.setTopic(buildScheduleCanceledTopic(deviceNo)); + command.getPayload().put("deviceNo", deviceNo); + command.getPayload().put("scheduleId", scheduleId); + command.getPayload().put("canceled", true); + return sendCommand(command); + } + @Override public String sendInitDeviceCommand(String deviceNo) { AppDeviceVo device = deviceService.queryById(deviceNo); @@ -237,6 +256,10 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService { return "/" + deviceNo.trim().toLowerCase(Locale.ROOT) + "/subscriber/schedule"; } + private String buildScheduleCanceledTopic(String deviceNo) { + return "/" + deviceNo.trim().toLowerCase(Locale.ROOT) + "/schedule/canceled"; + } + private void assertSwitchStatus(String workStatus) { if (!"0".equals(workStatus) && !"1".equals(workStatus)) { throw new ServiceException("设备工作状态只能为0或1"); @@ -256,6 +279,7 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService { String startTime, Integer durationMin ) { + Date starttime = parseStartTime(valueAsString(startTime)); AppWateringLogBo logBo = new AppWateringLogBo(); logBo.setDeviceNo(deviceNo); logBo.setCommandId(commandId); @@ -265,6 +289,8 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService { logBo.setTriggerType("1"); // 手动 logBo.setRemark("手动开始浇水"); logBo.setCreateTime(new Date()); + logBo.setStatus("1"); + logBo.setEndTime(resolveEndTime(starttime, durationMin)); if (StringUtils.isNotEmpty(startTime)) { logBo.setStartTime(parseStartTime(startTime)); @@ -274,7 +300,43 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService { return logBo; } + private String valueAsString(Object value) { + return value == null ? null : String.valueOf(value); + } + + private String normalizeCommandStartTime(String startTime) { + if (StringUtils.isBlank(startTime)) { + return startTime; + } + if (startTime.matches("^\\d{2}:\\d{2}$")) { + return startTime + ":00"; + } + return startTime; + } + + private Date resolveEndTime(Date startTime, Integer durationMin) { + if (startTime == null || durationMin == null || durationMin <= 0) { + return null; + } + return new Date(startTime.getTime() + durationMin * 60000L); + } private Date parseStartTime(String startTime) { + if (startTime.matches("^\\d{2}:\\d{2}:\\d{2}$")) { + try { + Date parsedTime = new SimpleDateFormat("HH:mm:ss").parse(startTime); + Calendar parsed = Calendar.getInstance(); + parsed.setTime(parsedTime); + + Calendar today = Calendar.getInstance(); + today.set(Calendar.HOUR_OF_DAY, parsed.get(Calendar.HOUR_OF_DAY)); + today.set(Calendar.MINUTE, parsed.get(Calendar.MINUTE)); + today.set(Calendar.SECOND, parsed.get(Calendar.SECOND)); + today.set(Calendar.MILLISECOND, 0); + return today.getTime(); + } catch (ParseException e) { + throw new ServiceException("开始时间格式错误,期望 HH:mm、HH:mm:ss 或 yyyy-MM-dd HH:mm:ss"); + } + } if (startTime.matches("^\\d{2}:\\d{2}$")) { try { Date parsedTime = new SimpleDateFormat("HH:mm").parse(startTime); @@ -288,13 +350,13 @@ public class DeviceCommandServiceImpl implements IDeviceCommandService { today.set(Calendar.MILLISECOND, 0); return today.getTime(); } catch (ParseException e) { - throw new ServiceException("开始时间格式错误,期望 HH:mm 或 yyyy-MM-dd HH:mm:ss"); + throw new ServiceException("开始时间格式错误,期望 HH:mm、HH:mm:ss 或 yyyy-MM-dd HH:mm:ss"); } } try { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(startTime); } catch (ParseException e) { - throw new ServiceException("开始时间格式错误,期望 HH:mm 或 yyyy-MM-dd HH:mm:ss"); + throw new ServiceException("开始时间格式错误,期望 HH:mm、HH:mm:ss 或 yyyy-MM-dd HH:mm:ss"); } } } diff --git a/water-modules/water-system/src/main/java/org/dromara/system/domain/SysUser.java b/water-modules/water-system/src/main/java/org/dromara/system/domain/SysUser.java index a06172f..63593c8 100644 --- a/water-modules/water-system/src/main/java/org/dromara/system/domain/SysUser.java +++ b/water-modules/water-system/src/main/java/org/dromara/system/domain/SysUser.java @@ -65,7 +65,7 @@ public class SysUser extends TenantEntity { /** * 用户头像 */ - private Long avatar; + private String avatar; /** * 密码 diff --git a/water-modules/water-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java b/water-modules/water-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java index 576ffc9..45d4a9c 100644 --- a/water-modules/water-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java +++ b/water-modules/water-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java @@ -72,6 +72,8 @@ public class SysUserBo extends BaseEntity { */ private String sex; + private String avatar; + /** * 密码 */ diff --git a/water-modules/water-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java b/water-modules/water-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java index 457a0fe..7a84578 100644 --- a/water-modules/water-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java +++ b/water-modules/water-system/src/main/java/org/dromara/system/domain/vo/SysUserVo.java @@ -1,14 +1,12 @@ package org.dromara.system.domain.vo; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; import org.dromara.common.sensitive.annotation.Sensitive; import org.dromara.common.sensitive.core.SensitiveStrategy; import org.dromara.common.translation.annotation.Translation; import org.dromara.common.translation.constant.TransConstant; import org.dromara.system.domain.SysUser; -import io.github.linpeilie.annotations.AutoMapper; -import lombok.Data; import java.io.Serial; import java.io.Serializable; @@ -78,8 +76,8 @@ public class SysUserVo implements Serializable { /** * 头像地址 */ - @Translation(type = TransConstant.OSS_ID_TO_URL) - private Long avatar; + //@Translation(type = TransConstant.OSS_ID_TO_URL) + private String avatar; /** * 密码