Compare commits

37 Commits

Author SHA1 Message Date
tianyongbao
a12cc4c905 fix: 测试服务,关闭vms回执。 2026-04-26 21:20:02 +08:00
tianyongbao
b36a92c981 fix: 电话报警,回执超时问题修复。 2026-04-26 21:19:25 +08:00
tianyongbao
303cd01a59 fix: 控制器定时控制删除时,实际未删除。 2026-04-26 11:19:10 +08:00
tianyongbao
467c05265f fix:太阳能网控,离线报警优化配置。 2026-04-26 07:25:03 +08:00
tianyongbao
65bcbf470c fix:太阳能网控,溶解氧下限未保存。 2026-04-26 06:27:00 +08:00
tianyongbao
7f1d1381ef fix:太阳能网控,后台代码修改完善。 2026-04-25 06:50:11 +08:00
tianyongbao
2bd71701a8 fix:子账号不充值后,回调后,更新设备有效期,问题修复。 2026-04-21 13:11:43 +08:00
tianyongbao
6b7b330934 fix:子账号不存在,提示信息修改。 2026-04-20 12:56:16 +08:00
tianyongbao
5c13c88c41 fix:设备电话通知和告警,以及设备历史数据,增加设备有效期判断。 2026-04-20 09:44:52 +08:00
tianyongbao
24a1e3e348 fix:给设备充值,去掉设备归属用户限制,允许给子账号充值。 2026-04-18 16:07:13 +08:00
tianyongbao
521c1ca344 fix:校准记录,去掉权限校验,登录用户都可以调用。 2026-04-15 16:33:37 +08:00
tianyongbao
b58f9c3b42 fix:告警类型错误,问题修复。 2026-04-14 22:45:05 +08:00
tianyongbao
07495f9270 fix:三相输入缺相,缺相无告警,bug修复。 2026-04-14 22:31:17 +08:00
tianyongbao
9eb2ec90c6 fix: 电话通知逻辑,功能修改,完善机制。 2026-04-14 22:31:14 +08:00
tianyongbao
45c55944a5 fix: 日志字符转义修复。 2026-04-12 20:49:56 +08:00
tianyongbao
2877fc9b7c fix: 控制器不报警,问题解决。 2026-04-12 20:43:35 +08:00
tianyongbao
8de7460d92 fix: 用户普通登录,接口新增几个字段。 2026-04-12 12:08:56 +08:00
tianyongbao
e1745c6130 fix: 修改微信用户和验证码登录,改为永不因不活跃过期,token时间1年有效。 2026-04-11 21:50:44 +08:00
tianyongbao
9b5f87342b fix: 修改微信用户登录默认token时间,改为不活跃7天,超时30天。 2026-04-11 20:25:43 +08:00
tianyongbao
cddaa2c63c fix: 测试服,取消接入amqp,数据库改用内网。 2026-04-11 17:01:47 +08:00
tianyongbao
222f74a7e1 fix: 测试服,取消电话通知。 2026-04-11 10:59:44 +08:00
tianyongbao
261486d235 fix: 回执时间过长,问题修复。 2026-04-10 22:10:38 +08:00
tianyongbao
11337ee89e fix: 盐度取值错误,问题彻底解决。 2026-04-10 14:03:47 +08:00
tianyongbao
74ec9caba8 fix: 盐度取值错误,问题再次解决。 2026-04-10 12:25:10 +08:00
tianyongbao
4fc7eb8006 fix: 盐度取值错误,问题解决。 2026-04-10 12:06:15 +08:00
tianyongbao
881ab2b4a6 fix: 相位差不上数,修复。 2026-04-10 11:21:46 +08:00
tianyongbao
2673eba9ce fix: 水温,饱和度等不上数,问题解决。 2026-04-10 10:58:12 +08:00
tianyongbao
38c038a950 fix: 离线报警逻辑问题修复。 2026-04-09 09:18:00 +08:00
tianyongbao
eeb9ca774e fix:测试问题修复,ph值。 2026-04-09 00:03:26 +08:00
tianyongbao
a50f9ee4c9 fix: 测试环境修改数据库配置。 2026-04-06 21:14:31 +08:00
tianyongbao
db7e0538aa fix: 塘口、设备按创建时间进行正序排列。 2026-04-05 13:29:06 +08:00
tianyongbao
b5c4bab4d4 fix: 微信支付回调接口加入白名单。 2026-04-05 13:05:22 +08:00
tianyongbao
83834ac4da fix: 用户登录逻辑判断修改。 2026-04-05 12:13:49 +08:00
tianyongbao
20cb24b0a1 fix: test分支,电话通知不开启。 2026-04-05 00:10:27 +08:00
tianyongbao
4d82c464ee fix: 配置文件加上电话通知报警是否开启。 2026-04-05 00:09:24 +08:00
tianyongbao
f67ae05413 fix: 新增电话告警统一配置,是否开启。 2026-04-05 00:05:45 +08:00
tianyongbao
9cdcd312cb fix: 测试环境配置文件修改。 2026-04-04 23:54:04 +08:00
43 changed files with 521 additions and 3875 deletions

View File

@@ -51,7 +51,7 @@ spring:
# username: root # username: root
# password: root # password: root
driverClassName: org.postgresql.Driver driverClassName: org.postgresql.Driver
url: jdbc:postgresql://172.16.42.181:15432/fishery_prod?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true url: jdbc:postgresql://172.16.42.181:15432/fishery_test?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
username: postgres username: postgres
password: htYkSuRn7Gb*pbFn password: htYkSuRn7Gb*pbFn
# 从库数据源 - TDengine # 从库数据源 - TDengine
@@ -110,7 +110,7 @@ spring.data:
# 端口默认为6379 # 端口默认为6379
port: 26379 port: 26379
# 数据库索引 # 数据库索引
database: 9 database: 6
# redis 密码必须配置 # redis 密码必须配置
password: bbd4b56e5d3f password: bbd4b56e5d3f
# 连接超时时间 # 连接超时时间
@@ -229,7 +229,8 @@ aliyun:
# VMS 语音通知配置 # VMS 语音通知配置
vms: vms:
# 是否启用 MNS 回扇消费true 时自动启动 MNS 消费线程接收 VMS 回扇) # 是否启用 MNS 回扇消费true 时自动启动 MNS 消费线程接收 VMS 回扇)
mns-enabled: true # 注意:测试服设为 false避免与正式服共抢同一个 MNS 队列导致回执随机丢失
mns-enabled: false
# VMS 语音服务对应的 AccessKey ID如果与上面 IoT 平台的 AK 相同可不填) # VMS 语音服务对应的 AccessKey ID如果与上面 IoT 平台的 AK 相同可不填)
access-key-id: LTAI5tKh6fEKJJESaTdnChkh access-key-id: LTAI5tKh6fEKJJESaTdnChkh
# VMS 语音服务对应的 AccessKey Secret # VMS 语音服务对应的 AccessKey Secret
@@ -248,11 +249,11 @@ aliyun:
# 太阳能网控离线告警延迟触发时间分钟工作1分钟休眠同分钟为正常行为需要更长延迟 # 太阳能网控离线告警延迟触发时间分钟工作1分钟休眠同分钟为正常行为需要更长延迟
solar-offline-warn-delay-minutes: 30 solar-offline-warn-delay-minutes: 30
# 是否启用报警电话通知(全局开关,关闭后所有类型的报警都不会触发电话通知) # 是否启用报警电话通知(全局开关,关闭后所有类型的报警都不会触发电话通知)
call-notice-enabled: true call-notice-enabled: false
# AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret # AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret
amqp: amqp:
# 是否启用 # 是否启用
enabled: true enabled: false
# 数据同步 AppKey与上面的 app-key 不同!) # 数据同步 AppKey与上面的 app-key 不同!)
data-sync-app-key: 334224409 data-sync-app-key: 334224409
# 数据同步 AppSecret请在阿里云控制台查看 # 数据同步 AppSecret请在阿里云控制台查看
@@ -262,7 +263,7 @@ aliyun:
# 消费组 ID按照官方文档应该与数据同步 AppKey 相同) # 消费组 ID按照官方文档应该与数据同步 AppKey 相同)
consumer-group-id: 334224409 consumer-group-id: 334224409
# 客户端 ID建议使用机器 UUID、MAC 地址等唯一标识) # 客户端 ID建议使用机器 UUID、MAC 地址等唯一标识)
client-id: fishery-backend-prod client-id: fishery-backend-001
# 连接超时时间(毫秒) # 连接超时时间(毫秒)
connection-timeout: 80000 connection-timeout: 80000
# 是否自动重连 # 是否自动重连
@@ -422,6 +423,6 @@ wx:
# 支付回调通知配置 # 支付回调通知配置
pay-notify: pay-notify:
# 支付回调通知URL需根据实际域名配置 # 支付回调通知URL需根据实际域名配置
notify-url: "https://api.yuceyun.cn/prod-api/weixin/pay_notify" notify-url: "https://api.yuceyun.cn/test-api/weixin/pay_notify"
# 微信商户号与上面wx.pay.mch-id保持一致 # 微信商户号与上面wx.pay.mch-id保持一致
mch-id: "1671289865" mch-id: "1671289865"

View File

@@ -138,7 +138,6 @@ tenant:
- sys_user_role - sys_user_role
- sys_client - sys_client
- sys_oss_config - sys_oss_config
- iot_device_status
- flow_spel - flow_spel
# MyBatisPlus配置 # MyBatisPlus配置
@@ -222,7 +221,6 @@ xss:
# 排除链接 # 排除链接
excludeUrls: excludeUrls:
- /system/notice - /system/notice
- /weixin/pay_notify
--- # 分布式锁 lock4j 全局配置 --- # 分布式锁 lock4j 全局配置
lock4j: lock4j:

View File

@@ -107,14 +107,6 @@
<artifactId>intc-system</artifactId> <artifactId>intc-system</artifactId>
</dependency> </dependency>
<!-- TDengine 模块(用于清理设备时序数据) -->
<dependency>
<groupId>com.intc</groupId>
<artifactId>intc-tdengine</artifactId>
<version>5.5.0</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -66,13 +66,6 @@ public class DefineDeviceWarnCode {
*/ */
public static final int SaturaHigh = 1 << 10; public static final int SaturaHigh = 1 << 10;
/**
* 设备休眠(太阳能网控专用) 1 << 11 = 2048
* 太阳能网控设备正常工作周期为 在线2分钟+休眠8分钟
* 休眠期间 IoT 平台上报离线,但属于正常行为,不等同于真正离线。
*/
public static final int DeviceSleeping = 1 << 11;
/** /**
* 设备断电(控制器) 1 << 15 = 32768 * 设备断电(控制器) 1 << 15 = 32768
*/ */
@@ -98,8 +91,7 @@ public class DefineDeviceWarnCode {
/** /**
* 转换告警码为告警描述(与 C# ToWarnDescription 保持一致) * 转换告警码为告警描述(与 C# ToWarnDescription 保持一致)
* 优先级:设备过期 > 设备离线 > 设备休眠 > 设备断电 > 溶解氧离线 > 溶解氧告警 > 温度告警 > 饱和度过高 * 优先级:设备过期 > 设备离线 > 设备断电 > 溶解氧离线 > 溶解氧告警 > 温度告警 > 饱和度过高
* 注PH/盐度/浊度/水位/氨氮探头离线不需要前端展示,故不在此处理
*/ */
public static String toWarnDescription(int warnCode) { public static String toWarnDescription(int warnCode) {
if (warnCode == None) { if (warnCode == None) {
@@ -111,9 +103,6 @@ public class DefineDeviceWarnCode {
if ((warnCode & DeviceOffline) != 0) { if ((warnCode & DeviceOffline) != 0) {
return "设备离线"; return "设备离线";
} }
if ((warnCode & DeviceSleeping) != 0) {
return "设备休眠";
}
if ((warnCode & ErrorPowerOff) != 0) { if ((warnCode & ErrorPowerOff) != 0) {
return "设备断电"; return "设备断电";
} }

View File

@@ -295,13 +295,30 @@ public class AquUserController extends BaseController {
return R.fail("用户未登录"); return R.fail("用户未登录");
} }
// 查询用户信息,确认当前用户存在 // 查询用户信息,获取当前用户的手机号
AquUserVo user = aquUserService.queryById(userId); AquUserVo user = aquUserService.queryById(userId);
if (user == null) { if (user == null) {
return R.fail("用户不存在"); return R.fail("用户不存在");
} }
String oldMobilePhone = user.getMobilePhone();
if (StringUtils.isEmpty(oldMobilePhone)) {
return R.fail("用户手机号不存在,无权操作");
}
// 验证报警电话列表是否包含用户自己的手机号
List<String> listPhone = request.getListPhone(); List<String> listPhone = request.getListPhone();
boolean containUser = false;
for (String phone : listPhone) {
if (phone.equals(oldMobilePhone)) {
containUser = true;
break;
}
}
if (!containUser) {
return R.fail("报警电话必须包含自己手机号");
}
// 序列化为JSON字符串 // 序列化为JSON字符串
String warnPhoneJson = JsonUtils.toJsonString(listPhone); String warnPhoneJson = JsonUtils.toJsonString(listPhone);
@@ -438,7 +455,6 @@ public class AquUserController extends BaseController {
// 同步更新 SysUser 中的 phonenumber // 同步更新 SysUser 中的 phonenumber
SysUserBo sysUserBo = new SysUserBo(); SysUserBo sysUserBo = new SysUserBo();
sysUserBo.setUserId(userId); sysUserBo.setUserId(userId);
sysUserBo.setUserName(request.getNewMobilePhone());
sysUserBo.setPhonenumber(request.getNewMobilePhone()); sysUserBo.setPhonenumber(request.getNewMobilePhone());
sysUserService.updateUserProfile(sysUserBo); sysUserService.updateUserProfile(sysUserBo);

View File

@@ -1,7 +1,6 @@
package com.intc.fishery.controller; package com.intc.fishery.controller;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import com.intc.fishery.domain.bo.*; import com.intc.fishery.domain.bo.*;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -30,25 +29,18 @@ import com.intc.fishery.domain.vo.PublicDeviceLinkedCtrl;
import com.intc.fishery.domain.vo.PublicDeviceDeadInfo; import com.intc.fishery.domain.vo.PublicDeviceDeadInfo;
import com.intc.fishery.domain.vo.PublicDeviceTypeAndSerialNum; import com.intc.fishery.domain.vo.PublicDeviceTypeAndSerialNum;
import com.intc.fishery.domain.Device; import com.intc.fishery.domain.Device;
import com.intc.fishery.domain.DeviceBindRecord;
import com.intc.fishery.domain.DeviceSwitch; import com.intc.fishery.domain.DeviceSwitch;
import com.intc.fishery.domain.DeviceErrorCode;
import com.intc.fishery.domain.Pond; import com.intc.fishery.domain.Pond;
import com.intc.fishery.domain.LinkedCtrl; import com.intc.fishery.domain.LinkedCtrl;
import com.intc.fishery.domain.DeviceCorrectRecord; import com.intc.fishery.domain.DeviceCorrectRecord;
import com.intc.fishery.mapper.DeviceMapper; import com.intc.fishery.mapper.DeviceMapper;
import com.intc.fishery.mapper.DeviceSwitchMapper; import com.intc.fishery.mapper.DeviceSwitchMapper;
import com.intc.fishery.mapper.DeviceBindRecordMapper;
import com.intc.fishery.mapper.DeviceErrorCodeMapper;
import com.intc.fishery.mapper.PondMapper; import com.intc.fishery.mapper.PondMapper;
import com.intc.fishery.mapper.LinkedCtrlMapper; import com.intc.fishery.mapper.LinkedCtrlMapper;
import com.intc.fishery.mapper.DeviceCorrectRecordMapper; import com.intc.fishery.mapper.DeviceCorrectRecordMapper;
import com.intc.fishery.constant.DefineDeviceWarnCode; import com.intc.fishery.constant.DefineDeviceWarnCode;
import com.intc.fishery.utils.MessageOpRecordUtil; import com.intc.fishery.utils.MessageOpRecordUtil;
import com.intc.common.core.config.properties.DeviceTypeProperties; import com.intc.common.core.config.properties.DeviceTypeProperties;
import com.intc.common.redis.utils.RedisUtils;
import com.intc.tdengine.service.IDeviceSensorDataService;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -76,13 +68,8 @@ public class DeviceController extends BaseController {
private final PondMapper pondMapper; private final PondMapper pondMapper;
private final LinkedCtrlMapper linkedCtrlMapper; private final LinkedCtrlMapper linkedCtrlMapper;
private final DeviceCorrectRecordMapper deviceCorrectRecordMapper; private final DeviceCorrectRecordMapper deviceCorrectRecordMapper;
private final DeviceBindRecordMapper deviceBindRecordMapper;
private final DeviceErrorCodeMapper deviceErrorCodeMapper;
private final DeviceTypeProperties deviceTypeProperties; private final DeviceTypeProperties deviceTypeProperties;
@Autowired(required = false)
private IDeviceSensorDataService deviceSensorDataService;
/** /**
* 查询设备管理列表 * 查询设备管理列表
*/ */
@@ -92,16 +79,6 @@ public class DeviceController extends BaseController {
return deviceService.queryPageList(bo, pageQuery); return deviceService.queryPageList(bo, pageQuery);
} }
/**
* 查询太阳能网控设备列表
*/
@SaCheckPermission("fishery:solarControl:list")
@GetMapping("/solar/list")
public TableDataInfo<DeviceVo> solarList(DeviceBo bo, PageQuery pageQuery) {
bo.setDeviceType(3);
return deviceService.queryPageList(bo, pageQuery);
}
/** /**
* 导出设备管理列表 * 导出设备管理列表
*/ */
@@ -148,7 +125,7 @@ public class DeviceController extends BaseController {
} }
/** /**
* 删除设备管理解绑模式删除后重建设备记录保留IoT信息 * 删除设备管理
* *
* @param ids 主键串 * @param ids 主键串
*/ */
@@ -160,140 +137,6 @@ public class DeviceController extends BaseController {
return toAjax(deviceService.deleteWithValidByIds(List.of(ids), true)); return toAjax(deviceService.deleteWithValidByIds(List.of(ids), true));
} }
/**
* 彻底删除设备及其所有关联数据(物理删除,不重建设备记录)
* <p>删除范围:设备记录、开关、定时控制、联动控制、故障码记录、校准记录、绑定/解绑记录、TDengine时序数据、Redis告警标记
* <p>与普通删除的区别:不重建设备记录,设备彻底从系统中移除</p>
*
* @param ids 主键串
* @return 操作结果
*/
@SaCheckPermission("fishery:device:remove")
@Log(title = "设备管理", businessType = BusinessType.DELETE)
@DeleteMapping("/hard_delete/{ids}")
public R<Void> hardDelete(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
// 1. 删除TDengine时序数据需在设备记录删除前通过serialNum清理
if (deviceSensorDataService != null) {
for (Long id : ids) {
Device device = deviceMapper.selectById(id);
if (device != null && device.getSerialNum() != null && !device.getSerialNum().isBlank()) {
try {
deviceSensorDataService.dropSubTable(device.getSerialNum());
} catch (Exception e) {
org.slf4j.LoggerFactory.getLogger(DeviceController.class)
.warn("彻底删除-清理TDengine数据失败: deviceId={}, serialNum={}, error={}",
id, device.getSerialNum(), e.getMessage());
}
}
}
}
// 2. 执行彻底删除设备记录、关联数据、Redis标记
return toAjax(deviceService.hardDeleteByIds(List.of(ids)));
}
/**
* 清零设备测试数据(出厂测试用)
* <p>清理范围:
* <ul>
* <li>用户/塘口绑定关系、有效期(deadTime)、告警状态(warnCode)、实时传感器值</li>
* <li>TDengine时序数据删除设备子表清除所有历史传感器数据</li>
* <li>设备绑定/解绑记录</li>
* <li>设备校准记录</li>
* <li>设备故障码记录</li>
* <li>Redis告警标记</li>
* </ul>
* deadTime 置空作为出厂清零标记,下一次绑定时重新计算 365 天有效期;其他必填数值字段清零为 0不置空。</p>
*
* @param id 设备ID
* @return 操作结果
*/
@SaCheckPermission("fishery:device:edit")
@Log(title = "设备管理", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/resetTestData/{id}")
public R<Void> resetTestData(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
Device device = deviceMapper.selectById(id);
if (device == null) {
return R.fail("设备不存在");
}
String serialNum = device.getSerialNum();
// 1. 清零设备表:解除绑定、清空有效期作为重新计费标记,必填数值字段清零为 0
deviceMapper.update(null,
new LambdaUpdateWrapper<Device>()
.eq(Device::getId, id)
.set(Device::getUserId, null)
.set(Device::getPondId, null)
.set(Device::getDeadTime, null)
.set(Device::getWarnCode, DefineDeviceWarnCode.None)
.set(Device::getValueDissolvedOxygen, 0.0)
.set(Device::getValueTemperature, 0.0)
.set(Device::getValueSaturability, 0.0)
.set(Device::getValuePh, 0.0)
.set(Device::getValueSalinity, 0.0)
.set(Device::getValueBattery, 0)
.set(Device::getIsOxygenWarnExist, 0)
.set(Device::getIsTempWarnExist, 0)
);
// 2. 删除TDengine时序数据设备子表含所有历史传感器数据
if (deviceSensorDataService != null && serialNum != null && !serialNum.isBlank()) {
try {
deviceSensorDataService.dropSubTable(serialNum);
} catch (Exception e) {
// TDengine清理失败不阻断主流程
org.slf4j.LoggerFactory.getLogger(DeviceController.class)
.warn("清理TDengine数据失败: deviceId={}, serialNum={}, error={}", id, serialNum, e.getMessage());
}
}
// 3. 删除设备绑定/解绑记录
deviceBindRecordMapper.delete(
new LambdaQueryWrapper<DeviceBindRecord>()
.eq(DeviceBindRecord::getSerialNum, serialNum)
);
// 4. 删除设备校准记录
deviceCorrectRecordMapper.delete(
new LambdaQueryWrapper<DeviceCorrectRecord>()
.eq(DeviceCorrectRecord::getDeviceId, id)
);
// 5. 删除设备故障码记录
deviceErrorCodeMapper.delete(
new LambdaQueryWrapper<DeviceErrorCode>()
.eq(DeviceErrorCode::getDeviceId, id)
);
// 6. 清除Redis告警标记
if (serialNum != null && !serialNum.isBlank()) {
try {
RedisUtils.deleteObject("device:offline:wait:" + serialNum);
RedisUtils.deleteObject("device:detector:oxy:offline:wait:" + serialNum);
} catch (Exception e) {
// Redis清理失败不影响主流程
}
}
// 7. 记录操作日志
if (device.getUserId() != null) {
MessageOpRecordUtil.addMessageOpRecordUser(
device.getUserId(),
"设备管理",
String.format("%s(%s),清零设备测试数据。", device.getDeviceName(), serialNum)
);
} else {
org.slf4j.LoggerFactory.getLogger(DeviceController.class)
.info("清零设备测试数据(设备未绑定用户): deviceId={}, serialNum={}", id, serialNum);
}
return R.ok();
}
/** /**
* 查询用户所有设备简化列表 * 查询用户所有设备简化列表
* type=1: 返回完整信息(包含塘口信息和开关列表) * type=1: 返回完整信息(包含塘口信息和开关列表)
@@ -474,7 +317,6 @@ public class DeviceController extends BaseController {
data.setSalinityCompensation(device.getSalinityCompensation()); data.setSalinityCompensation(device.getSalinityCompensation());
data.setInputVoltage(device.getInputVoltage()); data.setInputVoltage(device.getInputVoltage());
data.setVoltageWarnOpen(device.getVoltageWarnOpen()); data.setVoltageWarnOpen(device.getVoltageWarnOpen());
data.setControllerFaultCallOpen(device.getControllerFaultCallOpen() == null ? 1 : device.getControllerFaultCallOpen());
data.setBatteryWarnCallOpen(device.getBatteryWarnCallOpen()); data.setBatteryWarnCallOpen(device.getBatteryWarnCallOpen());
data.setBatteryWarnCallNoDis(device.getBatteryWarnCallNoDis()); data.setBatteryWarnCallNoDis(device.getBatteryWarnCallNoDis());
data.setBatteryWarnLower(device.getBatteryWarnLower()); data.setBatteryWarnLower(device.getBatteryWarnLower());
@@ -982,65 +824,45 @@ public class DeviceController extends BaseController {
} }
/** /**
* 查询设备过期列表 * 查询即将到期设备列表
* 返回已过期和即将过期未最1个月内的设备 * 返回当前时间到未来1个月内将要到期的设备
* *
* @param rootUserId 用户ID * @param rootUserId 用户ID
* @return 设备过期列表(包含已过期和即将过期) * @return 即将到期的设备列表
*/ */
@GetMapping("/list_device_dead") @GetMapping("/list_device_dead")
public R<List<PublicDeviceDeadInfo>> queryListDeviceDead( public R<List<PublicDeviceDeadInfo>> queryListDeviceDead(
@RequestParam("rootUserId") Long rootUserId) { @RequestParam("rootUserId") Long rootUserId) {
// 计算时间范围:当前时间到未1个月 // 计算时间范围:当前时间到未1个月
Date now = new Date(); Date now = new Date();
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
calendar.setTime(now); calendar.setTime(now);
calendar.add(Calendar.MONTH, 1); calendar.add(Calendar.MONTH, 1);
Date oneMonthLater = calendar.getTime(); Date oneMonthLater = calendar.getTime();
List<PublicDeviceDeadInfo> list = new ArrayList<>(); // 查询即将到期的设备
List<Device> devices = deviceMapper.selectList(
// 查询已过期的设备deadTime <= 当前时间)
List<Device> expiredDevices = deviceMapper.selectList(
new LambdaQueryWrapper<Device>()
.eq(Device::getUserId, rootUserId)
.le(Device::getDeadTime, now)
.select(Device::getId, Device::getDeviceName, Device::getSerialNum, Device::getDeadTime)
.orderByAsc(Device::getDeadTime)
);
// 查询即将过期的设备(当前时间 < deadTime <= 未最1个月
List<Device> soonExpireDevices = deviceMapper.selectList(
new LambdaQueryWrapper<Device>() new LambdaQueryWrapper<Device>()
.eq(Device::getUserId, rootUserId) .eq(Device::getUserId, rootUserId)
.gt(Device::getDeadTime, now) .gt(Device::getDeadTime, now)
.le(Device::getDeadTime, oneMonthLater) .le(Device::getDeadTime, oneMonthLater)
.ge(Device::getWarnCode, 0)
.select(Device::getId, Device::getDeviceName, Device::getSerialNum, Device::getDeadTime) .select(Device::getId, Device::getDeviceName, Device::getSerialNum, Device::getDeadTime)
.orderByAsc(Device::getDeadTime) .orderByAsc(Device::getDeadTime)
); );
// 转换已过期设备status=1 // 转换为VO列表
for (Device device : expiredDevices) { List<PublicDeviceDeadInfo> list = devices.stream()
PublicDeviceDeadInfo vo = new PublicDeviceDeadInfo(); .map(device -> {
vo.setId(device.getId()); PublicDeviceDeadInfo vo = new PublicDeviceDeadInfo();
vo.setDeviceName(device.getDeviceName()); vo.setId(device.getId());
vo.setSerialNum(device.getSerialNum()); vo.setDeviceName(device.getDeviceName());
vo.setDeadTime(device.getDeadTime()); vo.setSerialNum(device.getSerialNum());
vo.setStatus(1); vo.setDeadTime(device.getDeadTime());
list.add(vo); return vo;
} })
.collect(Collectors.toList());
// 转换即将过期设备status=2
for (Device device : soonExpireDevices) {
PublicDeviceDeadInfo vo = new PublicDeviceDeadInfo();
vo.setId(device.getId());
vo.setDeviceName(device.getDeviceName());
vo.setSerialNum(device.getSerialNum());
vo.setDeadTime(device.getDeadTime());
vo.setStatus(2);
list.add(vo);
}
return R.ok(list); return R.ok(list);
} }
@@ -1197,62 +1019,6 @@ public class DeviceController extends BaseController {
return R.ok(); return R.ok();
} }
/**
* 设置控制器故障电话报警开关。
* 仅控制缺相、过压、欠压、开关空载/欠流/过流等故障的电话通知;断电电话通知不受此开关限制。
*
* @param rootUserId 用户ID
* @param request 请求对象包含设备ID和开关状态
* @return 操作结果
*/
@PutMapping("/controller_fault_call_open")
public R<Void> updateControllerFaultCallOpen(
@RequestParam("rootUserId") Long rootUserId,
@Validated @RequestBody ReqTurnOpen request) {
Device device = deviceMapper.selectOne(
new LambdaQueryWrapper<Device>()
.eq(Device::getId, request.getId())
.select(Device::getId, Device::getUserId, Device::getSerialNum,
Device::getDeviceName, Device::getDeviceType,
Device::getControllerFaultCallOpen, Device::getDeadTime)
);
if (device == null || device.getUserId() == null || !device.getUserId().equals(rootUserId)
|| device.getDeviceType() == null || device.getDeviceType() != 2) {
return R.fail("设备不存在或无权限操作");
}
if (device.getDeadTime() != null && device.getDeadTime().before(new Date())) {
return R.fail("设备服务已过期");
}
Integer currentOpen = device.getControllerFaultCallOpen() == null ? 1 : device.getControllerFaultCallOpen();
if (currentOpen.equals(request.getIsOpen())) {
return R.ok();
}
boolean updated = deviceMapper.update(null,
new LambdaUpdateWrapper<Device>()
.eq(Device::getId, request.getId())
.set(Device::getControllerFaultCallOpen, request.getIsOpen())
) > 0;
if (!updated) {
return R.fail("更新失败");
}
String op = request.getIsOpen() == 1 ? "开启" : "关闭";
MessageOpRecordUtil.addMessageOpRecordUser(
rootUserId,
"设备告警设置",
String.format("%s(%s) 设置控制器故障电话报警开关:%s",
device.getDeviceName(), device.getSerialNum(), op)
);
return R.ok();
}
/** /**
* 设置测控一体机溶解氧启用/禁用 * 设置测控一体机溶解氧启用/禁用
* 启用时:同时启用溶解氧和温度告警及免打扰 * 启用时:同时启用溶解氧和温度告警及免打扰

View File

@@ -306,8 +306,7 @@ public class PondController extends BaseController {
PondMode1WarnCodeInfo detectorWarnCodeInfo = new PondMode1WarnCodeInfo(); PondMode1WarnCodeInfo detectorWarnCodeInfo = new PondMode1WarnCodeInfo();
if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) { if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) {
detectorWarnCodeInfo.setWarnCode(device.getWarnCode()); detectorWarnCodeInfo.setWarnCode(device.getWarnCode());
String desc = DefineDeviceWarnCode.toWarnDescription(device.getWarnCode()); detectorWarnCodeInfo.setWarnDescription(DefineDeviceWarnCode.toWarnDescription(device.getWarnCode()));
detectorWarnCodeInfo.setWarnDescription(desc != null ? desc : "");
} else { } else {
detectorWarnCodeInfo.setWarnCode(DefineDeviceWarnCode.None); detectorWarnCodeInfo.setWarnCode(DefineDeviceWarnCode.None);
detectorWarnCodeInfo.setWarnDescription(""); detectorWarnCodeInfo.setWarnDescription("");
@@ -358,8 +357,7 @@ public class PondController extends BaseController {
PondMode1WarnCodeInfo warnCodeInfo = new PondMode1WarnCodeInfo(); PondMode1WarnCodeInfo warnCodeInfo = new PondMode1WarnCodeInfo();
if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) { if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) {
warnCodeInfo.setWarnCode(device.getWarnCode()); warnCodeInfo.setWarnCode(device.getWarnCode());
String desc = DefineDeviceWarnCode.toWarnDescription(device.getWarnCode()); warnCodeInfo.setWarnDescription(DefineDeviceWarnCode.toWarnDescription(device.getWarnCode()));
warnCodeInfo.setWarnDescription(desc != null ? desc : "");
} else { } else {
warnCodeInfo.setWarnCode(DefineDeviceWarnCode.None); warnCodeInfo.setWarnCode(DefineDeviceWarnCode.None);
warnCodeInfo.setWarnDescription(""); warnCodeInfo.setWarnDescription("");
@@ -811,7 +809,7 @@ public class PondController extends BaseController {
warnCode |= device.getWarnCode(); warnCode |= device.getWarnCode();
} }
// 判断设备是否在线DeviceSleeping 不算离线,太阳能网控休眠属于正常工作) // 判断设备是否在线(简化判断,实际应根据业务逻辑)
if (device.getWarnCode() == null if (device.getWarnCode() == null
|| ((device.getWarnCode() & DefineDeviceWarnCode.DeviceTimeDead) == 0 || ((device.getWarnCode() & DefineDeviceWarnCode.DeviceTimeDead) == 0
&& (device.getWarnCode() & DefineDeviceWarnCode.DeviceOffline) == 0)) { && (device.getWarnCode() & DefineDeviceWarnCode.DeviceOffline) == 0)) {
@@ -925,8 +923,7 @@ public class PondController extends BaseController {
data.setDeviceCount(hashSetDeviceId.size()); data.setDeviceCount(hashSetDeviceId.size());
data.setSwitchCount(pondSwitches.size()); data.setSwitchCount(pondSwitches.size());
data.getWarnCodeInfo().setWarnCode(warnCode); data.getWarnCodeInfo().setWarnCode(warnCode);
String warnDesc = DefineDeviceWarnCode.toWarnDescription(warnCode); data.getWarnCodeInfo().setWarnDescription(DefineDeviceWarnCode.toWarnDescription(warnCode));
data.getWarnCodeInfo().setWarnDescription(warnDesc != null ? warnDesc : "");
listData.add(data); listData.add(data);
} }

View File

@@ -234,14 +234,14 @@ public class TimingCtrlController extends BaseController {
return R.fail("开关未绑定塘口"); return R.fail("开关未绑定塘口");
} }
// 检查定时控制数量是否超限5 // 检查定时控制数量是否超限4
long count = timingCtrlMapper.selectCount( long count = timingCtrlMapper.selectCount(
new LambdaQueryWrapper<TimingCtrl>() new LambdaQueryWrapper<TimingCtrl>()
.eq(TimingCtrl::getSwitchId, request.getId()) .eq(TimingCtrl::getSwitchId, request.getId())
); );
if (count >= 5) { if (count >= 4) {
return R.fail("定时控制数量不得超过5"); return R.fail("定时控制数量不得超过4");
} }
// 创建定时控制记录 // 创建定时控制记录

View File

@@ -208,11 +208,6 @@ public class Device extends TenantEntity {
*/ */
private Integer voltageWarnOpen; private Integer voltageWarnOpen;
/**
* 控制器故障电话报警开关(断电不受此开关限制)
*/
private Integer controllerFaultCallOpen;
/** /**
* 备注 * 备注
*/ */
@@ -248,10 +243,5 @@ public class Device extends TenantEntity {
*/ */
private Integer valueBattery; private Integer valueBattery;
/**
* 最后上数时间
*/
private Date lastReportTime;
} }

View File

@@ -240,11 +240,6 @@ public class DeviceBo extends BaseEntity {
// @NotNull(message = "电压告警开关不能为空", groups = { AddGroup.class, EditGroup.class }) // @NotNull(message = "电压告警开关不能为空", groups = { AddGroup.class, EditGroup.class })
private Integer voltageWarnOpen; private Integer voltageWarnOpen;
/**
* 控制器故障电话报警开关(断电不受此开关限制)
*/
private Integer controllerFaultCallOpen;
/** /**
* 备注 * 备注
*/ */
@@ -275,10 +270,5 @@ public class DeviceBo extends BaseEntity {
*/ */
private Long batteryWarnLower; private Long batteryWarnLower;
/**
* 最后上数时间
*/
private Date lastReportTime;
} }

View File

@@ -270,12 +270,6 @@ public class DeviceVo implements Serializable {
@ExcelProperty(value = "电压告警开关") @ExcelProperty(value = "电压告警开关")
private Integer voltageWarnOpen; private Integer voltageWarnOpen;
/**
* 控制器故障电话报警开关(断电不受此开关限制)
*/
@ExcelProperty(value = "控制器故障电话报警开关")
private Integer controllerFaultCallOpen;
/** /**
* 备注 * 备注
*/ */
@@ -318,12 +312,6 @@ public class DeviceVo implements Serializable {
@ExcelProperty(value = "电量值") @ExcelProperty(value = "电量值")
private Integer valueBattery; private Integer valueBattery;
/**
* 最后上数时间
*/
@ExcelProperty(value = "最后上数时间")
private Date lastReportTime;
/** /**
* 手机号 * 手机号
*/ */
@@ -336,17 +324,12 @@ public class DeviceVo implements Serializable {
@ExcelProperty(value = "用户名") @ExcelProperty(value = "用户名")
private String userName; private String userName;
/** /**
* 塘口名称 * 塘口名称
*/ */
@ExcelProperty(value = "塘口名称") @ExcelProperty(value = "塘口名称")
private String pondName; private String pondName;
/**
* 设备在线状态online/offline
*/
private String onlineStatus;
/** /**
* 是否过期0-未过期1-已过期) * 是否过期0-未过期1-已过期)
*/ */

View File

@@ -109,11 +109,6 @@ public class PublicDeviceBaseData implements Serializable {
*/ */
private Integer voltageWarnOpen; private Integer voltageWarnOpen;
/**
* 控制器故障电话报警开关(断电不受此开关限制)
*/
private Integer controllerFaultCallOpen;
/** /**
* 低电量电话告警开关(太阳能网控专用) * 低电量电话告警开关(太阳能网控专用)
*/ */

View File

@@ -45,10 +45,4 @@ public class PublicDeviceDeadInfo implements Serializable {
@Schema(description = "服务到期时间") @Schema(description = "服务到期时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date deadTime; private Date deadTime;
/**
* 过期状态1-已过期2-即将过期
*/
@Schema(description = "过期状态1-已过期2-即将过期")
private Integer status;
} }

View File

@@ -65,13 +65,4 @@ public interface IDeviceService {
* @return 是否删除成功 * @return 是否删除成功
*/ */
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid); Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 彻底删除设备及其所有关联数据(不重建设备记录)
* <p>删除范围设备记录、开关、定时控制、联动控制、故障码记录、校准记录、绑定记录、Redis告警标记</p>
*
* @param ids 待删除的主键集合
* @return 是否删除成功
*/
Boolean hardDeleteByIds(Collection<Long> ids);
} }

View File

@@ -171,12 +171,6 @@ public class CallNoticeServiceImpl implements ICallNoticeService {
wrapper.apply("mw.warn_type = {0}", warnType); wrapper.apply("mw.warn_type = {0}", warnType);
} }
// 处理告警标题模糊查询
String warnTitle = (String) params.get("warnTitle");
if (StringUtils.isNotBlank(warnTitle)) {
wrapper.like("mw.title", warnTitle);
}
// 处理告警ID精确查询 // 处理告警ID精确查询
Object messageWarnIdObj = params.get("messageWarnId"); Object messageWarnIdObj = params.get("messageWarnId");
if (messageWarnIdObj != null) { if (messageWarnIdObj != null) {

View File

@@ -95,7 +95,7 @@ public class DeviceBindRecordServiceImpl implements IDeviceBindRecordService {
// 基础查询条件 // 基础查询条件
wrapper.like(StringUtils.isNotBlank(bo.getIotId()), DeviceBindRecord::getIotId, bo.getIotId()); wrapper.like(StringUtils.isNotBlank(bo.getIotId()), DeviceBindRecord::getIotId, bo.getIotId());
wrapper.eq(bo.getDeviceType() != null, DeviceBindRecord::getDeviceType, bo.getDeviceType()); wrapper.eq(bo.getDeviceType() != null, DeviceBindRecord::getDeviceType, bo.getDeviceType());
wrapper.like(StringUtils.isNotBlank(bo.getSerialNum()), DeviceBindRecord::getSerialNum, bo.getSerialNum()); wrapper.eq(StringUtils.isNotBlank(bo.getSerialNum()), DeviceBindRecord::getSerialNum, bo.getSerialNum());
wrapper.eq(bo.getUserId() != null, DeviceBindRecord::getUserId, bo.getUserId()); wrapper.eq(bo.getUserId() != null, DeviceBindRecord::getUserId, bo.getUserId());
wrapper.eq(bo.getIsBind() != null, DeviceBindRecord::getIsBind, bo.getIsBind()); wrapper.eq(bo.getIsBind() != null, DeviceBindRecord::getIsBind, bo.getIsBind());

View File

@@ -94,7 +94,7 @@ public class DeviceCorrectRecordServiceImpl implements IDeviceCorrectRecordServi
// 查询条件 // 查询条件
wrapper.eq(bo.getDeviceId() != null, DeviceCorrectRecord::getDeviceId, bo.getDeviceId()) wrapper.eq(bo.getDeviceId() != null, DeviceCorrectRecord::getDeviceId, bo.getDeviceId())
.eq(bo.getUserId() != null, DeviceCorrectRecord::getUserId, bo.getUserId()) .eq(bo.getUserId() != null, DeviceCorrectRecord::getUserId, bo.getUserId())
.like(StringUtils.isNotBlank(bo.getSerialNum()), DeviceCorrectRecord::getSerialNum, bo.getSerialNum()) .eq(StringUtils.isNotBlank(bo.getSerialNum()), DeviceCorrectRecord::getSerialNum, bo.getSerialNum())
.eq(bo.getDeviceType() != null, DeviceCorrectRecord::getDeviceType, bo.getDeviceType()) .eq(bo.getDeviceType() != null, DeviceCorrectRecord::getDeviceType, bo.getDeviceType())
.eq(bo.getValueDissolvedOxygen() != null, DeviceCorrectRecord::getValueDissolvedOxygen, bo.getValueDissolvedOxygen()) .eq(bo.getValueDissolvedOxygen() != null, DeviceCorrectRecord::getValueDissolvedOxygen, bo.getValueDissolvedOxygen())
.eq(bo.getValueTemperature() != null, DeviceCorrectRecord::getValueTemperature, bo.getValueTemperature()) .eq(bo.getValueTemperature() != null, DeviceCorrectRecord::getValueTemperature, bo.getValueTemperature())

View File

@@ -5,12 +5,10 @@ import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.intc.common.core.config.properties.DeviceTypeProperties;
import com.intc.common.core.utils.MapstructUtils; import com.intc.common.core.utils.MapstructUtils;
import com.intc.common.core.utils.StringUtils; import com.intc.common.core.utils.StringUtils;
import com.intc.common.mybatis.core.page.PageQuery; import com.intc.common.mybatis.core.page.PageQuery;
import com.intc.common.mybatis.core.page.TableDataInfo; import com.intc.common.mybatis.core.page.TableDataInfo;
import com.intc.common.redis.utils.RedisUtils;
import com.intc.fishery.domain.AquUser; import com.intc.fishery.domain.AquUser;
import com.intc.fishery.domain.Device; import com.intc.fishery.domain.Device;
import com.intc.fishery.domain.DeviceBindRecord; import com.intc.fishery.domain.DeviceBindRecord;
@@ -22,8 +20,6 @@ import com.intc.fishery.domain.Pond;
import com.intc.fishery.domain.TimingCtrl; import com.intc.fishery.domain.TimingCtrl;
import com.intc.fishery.domain.bo.DeviceBo; import com.intc.fishery.domain.bo.DeviceBo;
import com.intc.fishery.domain.vo.DeviceVo; import com.intc.fishery.domain.vo.DeviceVo;
import com.intc.fishery.constant.DefineDeviceWarnCode;
import com.intc.fishery.mapper.AquUserMapper;
import com.intc.fishery.mapper.DeviceBindRecordMapper; import com.intc.fishery.mapper.DeviceBindRecordMapper;
import com.intc.fishery.mapper.DeviceCorrectRecordMapper; import com.intc.fishery.mapper.DeviceCorrectRecordMapper;
import com.intc.fishery.mapper.DeviceErrorCodeMapper; import com.intc.fishery.mapper.DeviceErrorCodeMapper;
@@ -32,7 +28,6 @@ import com.intc.fishery.mapper.DeviceSwitchMapper;
import com.intc.fishery.mapper.LinkedCtrlMapper; import com.intc.fishery.mapper.LinkedCtrlMapper;
import com.intc.fishery.mapper.TimingCtrlMapper; import com.intc.fishery.mapper.TimingCtrlMapper;
import com.intc.fishery.service.IDeviceService; import com.intc.fishery.service.IDeviceService;
import com.intc.fishery.utils.MessageOpRecordUtil;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -54,29 +49,12 @@ import lombok.extern.slf4j.Slf4j;
public class DeviceServiceImpl implements IDeviceService { public class DeviceServiceImpl implements IDeviceService {
private final DeviceMapper baseMapper; private final DeviceMapper baseMapper;
private final AquUserMapper aquUserMapper;
private final DeviceBindRecordMapper deviceBindRecordMapper; private final DeviceBindRecordMapper deviceBindRecordMapper;
private final DeviceCorrectRecordMapper deviceCorrectRecordMapper; private final DeviceCorrectRecordMapper deviceCorrectRecordMapper;
private final DeviceErrorCodeMapper deviceErrorCodeMapper; private final DeviceErrorCodeMapper deviceErrorCodeMapper;
private final DeviceSwitchMapper deviceSwitchMapper; private final DeviceSwitchMapper deviceSwitchMapper;
private final LinkedCtrlMapper linkedCtrlMapper; private final LinkedCtrlMapper linkedCtrlMapper;
private final TimingCtrlMapper timingCtrlMapper; private final TimingCtrlMapper timingCtrlMapper;
private final DeviceTypeProperties deviceTypeProperties;
/** Redis 离线等待标记前缀(与 DeviceDataHandler 保持一致) */
private static final String DEVICE_OFFLINE_WAIT_KEY_PREFIX = "device:offline:wait:";
/** Redis 溶解氧探头离线等待标记前缀 */
private static final String DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX = "device:detector:oxy:offline:wait:";
/** 太阳能网控设备类型 */
private static final Integer DEVICE_TYPE_SOLAR_CONTROLLER = 3;
/** 设备在线状态表别名 */
private static final String DEVICE_STATUS_ALIAS = "ids";
/** 设备在线状态表状态字段 */
private static final String DEVICE_STATUS_COLUMN = DEVICE_STATUS_ALIAS + ".status";
/** 设备在线状态展示字段 */
private static final String DEVICE_STATUS_DISPLAY_COLUMN = "LOWER(COALESCE(" + DEVICE_STATUS_COLUMN + ", 'offline'))";
/** 设备在线状态表状态字段(小写) */
private static final String DEVICE_STATUS_LOWER_COLUMN = "LOWER(" + DEVICE_STATUS_COLUMN + ")";
/** /**
* 查询设备管理 * 查询设备管理
@@ -127,7 +105,6 @@ public class DeviceServiceImpl implements IDeviceService {
private MPJLambdaWrapper<Device> buildQueryWrapper(DeviceBo bo) { private MPJLambdaWrapper<Device> buildQueryWrapper(DeviceBo bo) {
Map<String, Object> params = bo.getParams(); Map<String, Object> params = bo.getParams();
MPJLambdaWrapper<Device> wrapper = new MPJLambdaWrapper<>(); MPJLambdaWrapper<Device> wrapper = new MPJLambdaWrapper<>();
boolean solarQuery = DEVICE_TYPE_SOLAR_CONTROLLER.equals(bo.getDeviceType());
// Select main table fields // Select main table fields
wrapper.selectAll(Device.class); wrapper.selectAll(Device.class);
@@ -140,12 +117,6 @@ public class DeviceServiceImpl implements IDeviceService {
wrapper.leftJoin(AquUser.class, AquUser::getId, Device::getUserId); wrapper.leftJoin(AquUser.class, AquUser::getId, Device::getUserId);
// Left join with pond table // Left join with pond table
wrapper.leftJoin(Pond.class, Pond::getId, Device::getPondId); wrapper.leftJoin(Pond.class, Pond::getId, Device::getPondId);
if (solarQuery) {
wrapper.selectAs(DEVICE_STATUS_DISPLAY_COLUMN, DeviceVo::getOnlineStatus)
.leftJoin(buildSolarStatusJoinSql());
applySolarOnlineStatusFilter(wrapper, params);
wrapper.orderByAsc("CASE WHEN " + DEVICE_STATUS_LOWER_COLUMN + " = 'online' THEN 0 ELSE 1 END");
}
// 处理排序逼辑如果expiredFlag为1按过期时间倒序否则按创建时间倒序 // 处理排序逼辑如果expiredFlag为1按过期时间倒序否则按创建时间倒序
String expiredFlag = params != null ? (String) params.get("expiredFlag") : null; String expiredFlag = params != null ? (String) params.get("expiredFlag") : null;
@@ -157,7 +128,7 @@ public class DeviceServiceImpl implements IDeviceService {
} }
wrapper.eq(bo.getUserId() != null, Device::getUserId, bo.getUserId()); wrapper.eq(bo.getUserId() != null, Device::getUserId, bo.getUserId());
wrapper.like(StringUtils.isNotBlank(bo.getSerialNum()), Device::getSerialNum, bo.getSerialNum()); wrapper.eq(StringUtils.isNotBlank(bo.getSerialNum()), Device::getSerialNum, bo.getSerialNum());
wrapper.like(StringUtils.isNotBlank(bo.getDeviceName()), Device::getDeviceName, bo.getDeviceName()); wrapper.like(StringUtils.isNotBlank(bo.getDeviceName()), Device::getDeviceName, bo.getDeviceName());
wrapper.eq(bo.getDeviceType() != null, Device::getDeviceType, bo.getDeviceType()); wrapper.eq(bo.getDeviceType() != null, Device::getDeviceType, bo.getDeviceType());
wrapper.eq(bo.getBindTime() != null, Device::getBindTime, bo.getBindTime()); wrapper.eq(bo.getBindTime() != null, Device::getBindTime, bo.getBindTime());
@@ -173,33 +144,6 @@ public class DeviceServiceImpl implements IDeviceService {
return wrapper; return wrapper;
} }
private String buildSolarStatusJoinSql() {
String sql = "iot_device_status " + DEVICE_STATUS_ALIAS + " ON "
+ DEVICE_STATUS_ALIAS + ".device_name = t.serial_num";
String productKey = deviceTypeProperties.getSolarController();
if (StringUtils.isNotBlank(productKey)) {
sql += " AND " + DEVICE_STATUS_ALIAS + ".product_key = '" + productKey.replace("'", "''") + "'";
}
return sql;
}
/**
* 太阳能网控在线状态查询。
*/
private void applySolarOnlineStatusFilter(MPJLambdaWrapper<Device> wrapper, Map<String, Object> params) {
if (params == null || params.isEmpty()) {
return;
}
String onlineStatus = params.get("onlineStatus") != null ? params.get("onlineStatus").toString() : null;
if ("online".equalsIgnoreCase(onlineStatus)) {
wrapper.apply(DEVICE_STATUS_LOWER_COLUMN + " = {0}", "online");
} else if ("offline".equalsIgnoreCase(onlineStatus)) {
wrapper.and(w -> w.isNull(DEVICE_STATUS_COLUMN)
.or()
.apply(DEVICE_STATUS_LOWER_COLUMN + " <> {0}", "online"));
}
}
/** /**
* 处理额外的查询参数 * 处理额外的查询参数
* *
@@ -280,18 +224,8 @@ public class DeviceServiceImpl implements IDeviceService {
} }
/** /**
* 校验并批量删除设备管理信息(参考 C# DeleteDevice 逻辑) * 校验并批量删除设备管理信息
* <p> * 删除顺序:故障码 -> 校准记录 -> 定时控制 -> 开关(先解除联动控制引用) -> 联动控制 -> 设备
* 处理流程:
* 1. 查询完整设备信息与用户信息
* 2. 新增解绑记录
* 3. 删除联动控制
* 4. 删除关联数据(故障码、校准记录、定时控制、开关)
* 5. 删除设备
* 6. 非管理员用户创建新的无绑定设备保留IoT信息
* 7. 清除告警数据warnCode + Redis离线等待标记
* 8. 记录操作日志
* </p>
* *
* @param ids 待删除的主键集合 * @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验 * @param isValid 是否进行有效性校验
@@ -300,23 +234,85 @@ public class DeviceServiceImpl implements IDeviceService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) { public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) { if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验 //TODO 做一些业务上的校验,判断是否需要校验
} }
// 1. 先查询完整设备信息(删除前需要用到各字段) // 1. 删除设备关联的故障码记录
deviceErrorCodeMapper.delete(
new LambdaQueryWrapper<DeviceErrorCode>()
.in(DeviceErrorCode::getDeviceId, ids)
);
// 2. 删除设备关联的校准记录
deviceCorrectRecordMapper.delete(
new LambdaQueryWrapper<DeviceCorrectRecord>()
.in(DeviceCorrectRecord::getDeviceId, ids)
);
// 3. 查询设备关联的开关
List<DeviceSwitch> switches = deviceSwitchMapper.selectList(
new LambdaQueryWrapper<DeviceSwitch>()
.in(DeviceSwitch::getDeviceId, ids)
);
if (!switches.isEmpty()) {
// 收集开关ID
List<Long> switchIds = switches.stream()
.map(DeviceSwitch::getId)
.collect(Collectors.toList());
// 4. 删除开关关联的定时控制
timingCtrlMapper.delete(
new LambdaQueryWrapper<TimingCtrl>()
.in(TimingCtrl::getSwitchId, switchIds)
);
// 收集开关关联的联动控制ID
List<Long> linkedCtrlIds = switches.stream()
.map(DeviceSwitch::getLinkedCtrlId)
.filter(id -> id != null && id > 0)
.distinct()
.collect(Collectors.toList());
// 5. 解除开关与联动控制的绑定
deviceSwitchMapper.update(null,
new LambdaUpdateWrapper<DeviceSwitch>()
.set(DeviceSwitch::getLinkedCtrlId, null)
.in(DeviceSwitch::getDeviceId, ids)
);
// 6. 删除联动控制(如果有)
if (!linkedCtrlIds.isEmpty()) {
linkedCtrlMapper.delete(
new LambdaQueryWrapper<LinkedCtrl>()
.in(LinkedCtrl::getId, linkedCtrlIds)
);
}
// 7. 删除开关
deviceSwitchMapper.delete(
new LambdaQueryWrapper<DeviceSwitch>()
.in(DeviceSwitch::getDeviceId, ids)
);
}
// 8. 删除设备本身关联的联动控制
linkedCtrlMapper.delete(
new LambdaQueryWrapper<LinkedCtrl>()
.in(LinkedCtrl::getDeviceId, ids)
);
// 9. 查询要删除的设备信息,添加解绑记录
List<Device> devicesToDelete = baseMapper.selectList( List<Device> devicesToDelete = baseMapper.selectList(
new LambdaQueryWrapper<Device>() new LambdaQueryWrapper<Device>()
.in(Device::getId, ids) .in(Device::getId, ids)
.select(Device::getId, Device::getUserId, Device::getIotId,
Device::getDeviceType, Device::getSerialNum)
); );
if (devicesToDelete.isEmpty()) {
return false;
}
// 为每个设备添加解绑记录
for (Device device : devicesToDelete) { for (Device device : devicesToDelete) {
Long deviceId = device.getId();
// 2. 新增解绑记录
try { try {
DeviceBindRecord unbindRecord = new DeviceBindRecord(); DeviceBindRecord unbindRecord = new DeviceBindRecord();
unbindRecord.setUserId(device.getUserId()); unbindRecord.setUserId(device.getUserId());
@@ -326,268 +322,18 @@ public class DeviceServiceImpl implements IDeviceService {
unbindRecord.setSerialNum(device.getSerialNum()); unbindRecord.setSerialNum(device.getSerialNum());
deviceBindRecordMapper.insert(unbindRecord); deviceBindRecordMapper.insert(unbindRecord);
log.info("添加设备解绑记录: deviceId={}, userId={}, serialNum={}", log.info("添加设备解绑记录: deviceId={}, userId={}, serialNum={}",
deviceId, device.getUserId(), device.getSerialNum()); device.getId(), device.getUserId(), device.getSerialNum());
} catch (Exception e) { } catch (Exception e) {
log.error("添加设备解绑记录失败: deviceId={}, error={}", deviceId, e.getMessage(), e); log.error("添加设备解绑记录失败: deviceId={}, error={}", device.getId(), e.getMessage(), e);
} // 不影响删除流程,继续执行
// 3. 删除设备关联的联动控制
linkedCtrlMapper.delete(
new LambdaQueryWrapper<LinkedCtrl>()
.eq(LinkedCtrl::getDeviceId, deviceId)
);
// 4. 删除设备关联的故障码记录
deviceErrorCodeMapper.delete(
new LambdaQueryWrapper<DeviceErrorCode>()
.eq(DeviceErrorCode::getDeviceId, deviceId)
);
// 5. 删除设备关联的校准记录
deviceCorrectRecordMapper.delete(
new LambdaQueryWrapper<DeviceCorrectRecord>()
.eq(DeviceCorrectRecord::getDeviceId, deviceId)
);
// 6. 查询设备关联的开关,删除定时控制、解除联动绑定后删除开关
List<DeviceSwitch> switches = deviceSwitchMapper.selectList(
new LambdaQueryWrapper<DeviceSwitch>()
.eq(DeviceSwitch::getDeviceId, deviceId)
);
if (!switches.isEmpty()) {
List<Long> switchIds = switches.stream()
.map(DeviceSwitch::getId)
.collect(Collectors.toList());
// 删除开关关联的定时控制
timingCtrlMapper.delete(
new LambdaQueryWrapper<TimingCtrl>()
.in(TimingCtrl::getSwitchId, switchIds)
);
// 收集开关关联的联动控制ID
List<Long> linkedCtrlIds = switches.stream()
.map(DeviceSwitch::getLinkedCtrlId)
.filter(id -> id != null && id > 0)
.distinct()
.collect(Collectors.toList());
// 解除开关与联动控制的绑定
deviceSwitchMapper.update(null,
new LambdaUpdateWrapper<DeviceSwitch>()
.set(DeviceSwitch::getLinkedCtrlId, null)
.eq(DeviceSwitch::getDeviceId, deviceId)
);
// 删除联动控制(如果有)
if (!linkedCtrlIds.isEmpty()) {
linkedCtrlMapper.delete(
new LambdaQueryWrapper<LinkedCtrl>()
.in(LinkedCtrl::getId, linkedCtrlIds)
);
}
// 删除开关
deviceSwitchMapper.delete(
new LambdaQueryWrapper<DeviceSwitch>()
.eq(DeviceSwitch::getDeviceId, deviceId)
);
}
// 7. 删除设备记录
baseMapper.deleteById(deviceId);
// 8. 用户创建新的无绑定设备保留IoT信息清除用户和塘口关联
if (device.getUserId() != null) {
try {
AquUser user = aquUserMapper.selectById(device.getUserId());
if (user != null) {
// 管理员(出厂测试人员)解绑时清空有效期,普通用户解绑时保留有效期(防止解绑续命)
boolean isManager = user.getIsManager() != null && user.getIsManager() == 1L;
Device newDevice = new Device();
newDevice.setUserId(null);
newDevice.setIsOxygenUsed(device.getIsOxygenUsed());
newDevice.setIotId(device.getIotId());
newDevice.setSerialNum(device.getSerialNum());
newDevice.setDeviceName(device.getDeviceName());
newDevice.setDeviceType(device.getDeviceType());
newDevice.setIccId(device.getIccId());
newDevice.setBindTime(device.getBindTime());
// 管理员解绑时清空有效期(出厂测试),普通用户解绑时保留有效期
newDevice.setDeadTime(isManager ? null : device.getDeadTime());
newDevice.setPondId(null);
newDevice.setSalinityCompensation(0.0);
newDevice.setOxyWarnLower(device.getOxyWarnLower());
newDevice.setOxyWarnCallOpen(0);
newDevice.setOxyWarnCallNoDis(0);
newDevice.setTempWarnCallOpen(0);
newDevice.setTempWarnCallNoDis(0);
newDevice.setControllerFaultCallOpen(1);
newDevice.setWarnCode(DefineDeviceWarnCode.None);
newDevice.setCategory(device.getCategory());
baseMapper.insert(newDevice);
log.info("已为用户重建无绑定设备: oldDeviceId={}, newDeviceId={}, serialNum={}, isManager={}, deadTime={}",
deviceId, newDevice.getId(), device.getSerialNum(), isManager, newDevice.getDeadTime());
}
} catch (Exception e) {
log.error("重建无绑定设备失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
}
}
// 9. 清除告警数据Redis离线等待标记
try {
String serialNum = device.getSerialNum();
if (StringUtils.isNotBlank(serialNum)) {
RedisUtils.deleteObject(DEVICE_OFFLINE_WAIT_KEY_PREFIX + serialNum);
RedisUtils.deleteObject(DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX + serialNum);
log.info("已清除设备Redis告警标记: serialNum={}", serialNum);
}
} catch (Exception e) {
log.error("清除Redis告警标记失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
}
// 10. 记录操作日志
try {
MessageOpRecordUtil.addMessageOpRecordUser(
device.getUserId(), device.getUserId(),
"设备操作",
String.format("%s(%s),解除绑定。", device.getDeviceName(), device.getSerialNum())
);
} catch (Exception e) {
log.error("记录操作日志失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
} }
} }
return true; // 10. 最后删除设备
return baseMapper.deleteByIds(ids) > 0;
} }
/**
* 彻底删除设备及其所有关联数据(不重建设备记录)
* 与 deleteWithValidByIds 的区别:
* - 不插入解绑记录
* - 不重建无绑定设备
* - 额外删除该设备的所有绑定/解绑记录
* 设备关联数据全部清除包括联动控制、故障码、校准记录、开关、定时控制、绑定记录、Redis告警标记
*
* @param ids 待删除的主键集合
* @return 是否删除成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean hardDeleteByIds(Collection<Long> ids) {
// 1. 先查询完整设备信息(删除前需要用到各字段)
List<Device> devicesToDelete = baseMapper.selectList(
new LambdaQueryWrapper<Device>()
.in(Device::getId, ids)
);
if (devicesToDelete.isEmpty()) {
return false;
}
for (Device device : devicesToDelete) {
Long deviceId = device.getId();
// 2. 删除设备关联的联动控制
linkedCtrlMapper.delete(
new LambdaQueryWrapper<LinkedCtrl>()
.eq(LinkedCtrl::getDeviceId, deviceId)
);
// 3. 删除设备关联的故障码记录
deviceErrorCodeMapper.delete(
new LambdaQueryWrapper<DeviceErrorCode>()
.eq(DeviceErrorCode::getDeviceId, deviceId)
);
// 4. 删除设备关联的校准记录
deviceCorrectRecordMapper.delete(
new LambdaQueryWrapper<DeviceCorrectRecord>()
.eq(DeviceCorrectRecord::getDeviceId, deviceId)
);
// 5. 删除设备关联的开关、定时控制、联动绑定
List<DeviceSwitch> switches = deviceSwitchMapper.selectList(
new LambdaQueryWrapper<DeviceSwitch>()
.eq(DeviceSwitch::getDeviceId, deviceId)
);
if (!switches.isEmpty()) {
List<Long> switchIds = switches.stream()
.map(DeviceSwitch::getId)
.collect(Collectors.toList());
// 删除开关关联的定时控制
timingCtrlMapper.delete(
new LambdaQueryWrapper<TimingCtrl>()
.in(TimingCtrl::getSwitchId, switchIds)
);
// 收集开关关联的联动控制ID
List<Long> linkedCtrlIds = switches.stream()
.map(DeviceSwitch::getLinkedCtrlId)
.filter(id -> id != null && id > 0)
.distinct()
.collect(Collectors.toList());
// 解除开关与联动控制的绑定
deviceSwitchMapper.update(null,
new LambdaUpdateWrapper<DeviceSwitch>()
.set(DeviceSwitch::getLinkedCtrlId, null)
.eq(DeviceSwitch::getDeviceId, deviceId)
);
// 删除联动控制(如果有)
if (!linkedCtrlIds.isEmpty()) {
linkedCtrlMapper.delete(
new LambdaQueryWrapper<LinkedCtrl>()
.in(LinkedCtrl::getId, linkedCtrlIds)
);
}
// 删除开关
deviceSwitchMapper.delete(
new LambdaQueryWrapper<DeviceSwitch>()
.eq(DeviceSwitch::getDeviceId, deviceId)
);
}
// 6. 删除该设备的所有绑定/解绑记录
if (StringUtils.isNotBlank(device.getSerialNum())) {
deviceBindRecordMapper.delete(
new LambdaQueryWrapper<DeviceBindRecord>()
.eq(DeviceBindRecord::getSerialNum, device.getSerialNum())
);
}
// 7. 物理删除设备记录
baseMapper.deleteById(deviceId);
// 8. 清除Redis告警标记
try {
String serialNum = device.getSerialNum();
if (StringUtils.isNotBlank(serialNum)) {
RedisUtils.deleteObject(DEVICE_OFFLINE_WAIT_KEY_PREFIX + serialNum);
RedisUtils.deleteObject(DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX + serialNum);
log.info("[彻底删除] 已清除设备Redis告警标记: serialNum={}", serialNum);
}
} catch (Exception e) {
log.error("[彻底删除] 清除Redis告警标记失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
}
// 9. 记录操作日志
try {
MessageOpRecordUtil.addMessageOpRecordUser(
device.getUserId(), device.getUserId(),
"设备操作",
String.format("%s(%s),彻底删除。", device.getDeviceName(), device.getSerialNum())
);
} catch (Exception e) {
log.error("[彻底删除] 记录操作日志失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
}
}
return true;
}
/** /**
* 计算设备列表的过期信息 * 计算设备列表的过期信息

View File

@@ -81,7 +81,7 @@ public class DeviceWarnCombineServiceImpl implements IDeviceWarnCombineService {
.selectAll(DeviceWarnCombine.class) .selectAll(DeviceWarnCombine.class)
.selectCount(DeviceWarnOne::getId, DeviceWarnCombineVo::getTotalCount) .selectCount(DeviceWarnOne::getId, DeviceWarnCombineVo::getTotalCount)
.leftJoin(DeviceWarnOne.class, DeviceWarnOne::getWarnCombineId, DeviceWarnCombine::getId) .leftJoin(DeviceWarnOne.class, DeviceWarnOne::getWarnCombineId, DeviceWarnCombine::getId)
.like(StringUtils.isNotBlank(bo.getDeviceSerialNum()), DeviceWarnCombine::getDeviceSerialNum, bo.getDeviceSerialNum()) .eq(StringUtils.isNotBlank(bo.getDeviceSerialNum()), DeviceWarnCombine::getDeviceSerialNum, bo.getDeviceSerialNum())
.eq(bo.getDeviceType() != null, DeviceWarnCombine::getDeviceType, bo.getDeviceType()) .eq(bo.getDeviceType() != null, DeviceWarnCombine::getDeviceType, bo.getDeviceType())
.eq(bo.getThresholdType() != null, DeviceWarnCombine::getThresholdType, bo.getThresholdType()) .eq(bo.getThresholdType() != null, DeviceWarnCombine::getThresholdType, bo.getThresholdType())
.eq(bo.getProcessStatus() != null, DeviceWarnCombine::getProcessStatus, bo.getProcessStatus()) .eq(bo.getProcessStatus() != null, DeviceWarnCombine::getProcessStatus, bo.getProcessStatus())

View File

@@ -154,10 +154,6 @@ public class LinkedCtrlServiceImpl implements ILinkedCtrlService {
.eq(bo.getOxyUpperOpen() != null, LinkedCtrl::getOxyUpperOpen, bo.getOxyUpperOpen()) .eq(bo.getOxyUpperOpen() != null, LinkedCtrl::getOxyUpperOpen, bo.getOxyUpperOpen())
.eq(bo.getOxyLowerOpen() != null, LinkedCtrl::getOxyLowerOpen, bo.getOxyLowerOpen()) .eq(bo.getOxyLowerOpen() != null, LinkedCtrl::getOxyLowerOpen, bo.getOxyLowerOpen())
.eq(bo.getIsOxyUpperTrigger() != null, LinkedCtrl::getIsOxyUpperTrigger, bo.getIsOxyUpperTrigger()); .eq(bo.getIsOxyUpperTrigger() != null, LinkedCtrl::getIsOxyUpperTrigger, bo.getIsOxyUpperTrigger());
// 处理额外的查询参数
if (params != null && !params.isEmpty()) {
handleExtraParams(wrapper, params);
}
return wrapper; return wrapper;
} }

View File

@@ -8,12 +8,15 @@ import com.intc.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.intc.fishery.domain.*;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.intc.fishery.domain.bo.MessageWarnBo; import com.intc.fishery.domain.bo.MessageWarnBo;
import com.intc.fishery.domain.vo.MessageWarnVo; import com.intc.fishery.domain.vo.MessageWarnVo;
import com.intc.fishery.domain.MessageWarn;
import com.intc.fishery.domain.AquUser;
import com.intc.fishery.domain.Device;
import com.intc.fishery.domain.Pond;
import com.intc.fishery.mapper.MessageWarnMapper; import com.intc.fishery.mapper.MessageWarnMapper;
import com.intc.fishery.service.IMessageWarnService; import com.intc.fishery.service.IMessageWarnService;
@@ -106,11 +109,9 @@ public class MessageWarnServiceImpl implements IMessageWarnService {
.eq(bo.getDeviceId() != null, MessageWarn::getDeviceId, bo.getDeviceId()) .eq(bo.getDeviceId() != null, MessageWarn::getDeviceId, bo.getDeviceId())
.eq(StringUtils.isNotBlank(bo.getTitle()), MessageWarn::getTitle, bo.getTitle()) .eq(StringUtils.isNotBlank(bo.getTitle()), MessageWarn::getTitle, bo.getTitle())
.eq(StringUtils.isNotBlank(bo.getMessage()), MessageWarn::getMessage, bo.getMessage()) .eq(StringUtils.isNotBlank(bo.getMessage()), MessageWarn::getMessage, bo.getMessage())
.like(StringUtils.isNotBlank(bo.getTitle()), MessageWarn::getTitle, bo.getTitle())
.eq(bo.getIsRead() != null, MessageWarn::getIsRead, bo.getIsRead()) .eq(bo.getIsRead() != null, MessageWarn::getIsRead, bo.getIsRead())
.eq(bo.getWarnType() != null, MessageWarn::getWarnType, bo.getWarnType()) .eq(bo.getWarnType() != null, MessageWarn::getWarnType, bo.getWarnType())
.like(StringUtils.isNotBlank(bo.getPondName()), MessageWarn::getPondName, bo.getPondName()) .like(StringUtils.isNotBlank(bo.getPondName()), MessageWarn::getPondName, bo.getPondName())
.orderByAsc(bo.getUserId() != null, MessageWarn::getIsRead)
.orderByDesc(MessageWarn::getCreateTime); .orderByDesc(MessageWarn::getCreateTime);
// 处理额外的查询参数 // 处理额外的查询参数

View File

@@ -1,43 +0,0 @@
package com.intc.iot.domain.bo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
/**
* 太阳能网控属性下发请求对象
*
* @author intc
*/
@Data
@Schema(description = "太阳能网控属性下发请求对象")
public class DeviceSolarPropertyCommandBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 设备ID
*/
@Schema(description = "设备ID")
@NotNull(message = "设备ID不能为空")
private Long id;
/**
* 待下发属性key 为物模型属性标识符
*/
@Schema(description = "待下发属性")
@NotEmpty(message = "下发属性不能为空")
private Map<String, Object> properties;
/**
* 是否缓存下发。true=写入缓存设备上线后自动补发false=立即下发
*/
@Schema(description = "是否缓存下发")
private Boolean cache;
}

View File

@@ -1,34 +0,0 @@
package com.intc.iot.service;
import lombok.Data;
/**
* 开关执行结果
*/
@Data
public class SwitchTurnResult {
/**
* 是否成功
*/
private boolean success;
/**
* 结果描述
*/
private String message;
public static SwitchTurnResult ok(String message) {
SwitchTurnResult result = new SwitchTurnResult();
result.setSuccess(true);
result.setMessage(message);
return result;
}
public static SwitchTurnResult fail(String message) {
SwitchTurnResult result = new SwitchTurnResult();
result.setSuccess(false);
result.setMessage(message);
return result;
}
}

View File

@@ -1,38 +0,0 @@
package com.intc.iot.service;
import com.intc.fishery.domain.TimingCtrl;
import java.util.List;
/**
* 定时控制执行服务
*/
public interface TimingCtrlExecuteService {
/**
* 按开关ID执行开/关控制
*
* @param switchId 开关ID
* @param targetStatus 目标状态1=开启0=关闭
* @param opTypeLabel 操作类型描述
* @param opMessagePrefix 操作记录前缀
* @param timingOp 是否按定时任务记录
* @return 执行结果
*/
SwitchTurnResult executeSwitch(Long switchId, Integer targetStatus, String opTypeLabel,
String opMessagePrefix, boolean timingOp);
/**
* 扫描并执行当天到点的循环定时任务
*
* @return 执行数量
*/
int scanAndExecuteDailyTimingCtrl();
/**
* 查询启用中的每天循环定时控制
*
* @return 定时控制列表
*/
List<TimingCtrl> listEnabledDailyTimingCtrls();
}

View File

@@ -5,15 +5,11 @@ import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.intc.iot.handler.DeviceDataHandler; import com.intc.iot.handler.DeviceDataHandler;
import com.intc.iot.service.AmqpMessageHandler; import com.intc.iot.service.AmqpMessageHandler;
import com.intc.iot.service.DeviceStatusService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/** /**
* AMQP 消息处理器实现类 * AMQP 消息处理器实现类
* *
@@ -33,9 +29,6 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
@Autowired(required = false) @Autowired(required = false)
private DeviceDataHandler deviceDataHandler; private DeviceDataHandler deviceDataHandler;
@Autowired(required = false)
private DeviceStatusService deviceStatusService;
@Override @Override
public void handleMessage(String message) { public void handleMessage(String message) {
try { try {
@@ -57,9 +50,6 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
String topic = json.getStr("topic"); String topic = json.getStr("topic");
String method = json.getStr("method"); String method = json.getStr("method");
String status = json.getStr("status"); String status = json.getStr("status");
if (status != null) {
status = status.trim().toLowerCase();
}
// 检查是否是设备上线/下线消息 // 检查是否是设备上线/下线消息
if (status != null && ("online".equals(status) || "offline".equals(status))) { if (status != null && ("online".equals(status) || "offline".equals(status))) {
@@ -225,11 +215,14 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
private void handleStatusMessage(JSONObject json) { private void handleStatusMessage(JSONObject json) {
String deviceName = json.getStr("deviceName"); String deviceName = json.getStr("deviceName");
String status = json.getStr("status"); String status = json.getStr("status");
status = normalizeStatus(status);
log.debug("设备状态: {} - {}", deviceName, status); log.debug("设备状态: {} - {}", deviceName, status);
handleDeviceStatusChange(json, deviceName, status); if ("offline".equals(status) && deviceDataHandler != null && deviceName != null) {
deviceDataHandler.handleDeviceOffline(deviceName);
} else if ("online".equals(status) && deviceDataHandler != null && deviceName != null) {
deviceDataHandler.handleDeviceOnline(deviceName);
}
} }
/** /**
@@ -242,52 +235,14 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
if (action == null) { if (action == null) {
action = json.getStr("status"); action = json.getStr("status");
} }
action = normalizeStatus(action);
log.debug("设备{}: {}", "online".equals(action) ? "上线" : "下线", deviceName); log.debug("设备{}: {}", "online".equals(action) ? "上线" : "下线", deviceName);
handleDeviceStatusChange(json, deviceName, action); if ("offline".equals(action) && deviceDataHandler != null && deviceName != null) {
}
private void handleDeviceStatusChange(JSONObject json, String deviceName, String status) {
if (deviceName == null || status == null) {
log.warn("设备状态消息缺少设备名称或状态: {}", json);
return;
}
String productKey = json.getStr("productKey");
if (deviceStatusService != null && productKey != null && !productKey.trim().isEmpty()) {
try {
Map<String, Object> statusData = new HashMap<>();
statusData.put("productKey", productKey);
statusData.put("deviceName", deviceName);
statusData.put("status", status);
statusData.put("statusTime", json.get("statusTime"));
statusData.put("lastTime", json.get("lastTime"));
statusData.put("clientIp", json.getStr("clientIp"));
statusData.put("iotId", json.getStr("iotId"));
statusData.put("userId", json.get("userId"));
deviceStatusService.handleStatusChange(statusData);
return;
} catch (Exception e) {
log.error("写入设备状态失败,降级执行上下线处理: deviceName={}, status={}, err={}",
deviceName, status, e.getMessage(), e);
}
}
if ("offline".equals(status) && deviceDataHandler != null) {
deviceDataHandler.handleDeviceOffline(deviceName); deviceDataHandler.handleDeviceOffline(deviceName);
} else if ("online".equals(status) && deviceDataHandler != null) { } else if ("online".equals(action) && deviceDataHandler != null && deviceName != null) {
deviceDataHandler.handleDeviceOnline(deviceName); deviceDataHandler.handleDeviceOnline(deviceName);
} }
} }
private String normalizeStatus(String status) {
if (status == null) {
return null;
}
status = status.trim().toLowerCase();
return ("online".equals(status) || "offline".equals(status)) ? status : null;
}
} }

View File

@@ -2,20 +2,16 @@ package com.intc.iot.service.impl;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.intc.common.tenant.helper.TenantHelper;
import com.intc.iot.handler.DeviceDataHandler;
import com.intc.iot.domain.IotDeviceStatus; import com.intc.iot.domain.IotDeviceStatus;
import com.intc.iot.mapper.IotDeviceStatusMapper; import com.intc.iot.mapper.IotDeviceStatusMapper;
import com.intc.iot.service.DeviceStatusService; import com.intc.iot.service.DeviceStatusService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@@ -26,45 +22,37 @@ import java.util.Map;
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ConditionalOnBean(IotDeviceStatusMapper.class)
@Slf4j @Slf4j
public class DeviceStatusServiceImpl implements DeviceStatusService { public class DeviceStatusServiceImpl implements DeviceStatusService {
private final IotDeviceStatusMapper deviceStatusMapper; private final IotDeviceStatusMapper deviceStatusMapper;
@Autowired(required = false)
private DeviceDataHandler deviceDataHandler;
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void handleStatusChange(Map<String, Object> statusData) { public void handleStatusChange(Map<String, Object> statusData) {
try { try {
if (statusData == null || statusData.isEmpty()) { String productKey = (String) statusData.get("productKey");
log.warn("[设备状态] 状态数据为空,跳过处理"); String deviceName = (String) statusData.get("deviceName");
return; String status = (String) statusData.get("status");
}
String productKey = trimToNull(statusData.get("productKey"));
String deviceName = trimToNull(statusData.get("deviceName"));
String status = normalizeStatus(statusData.get("status"));
Long statusTime = getLongValue(statusData, "statusTime"); Long statusTime = getLongValue(statusData, "statusTime");
Long lastTime = getLongValue(statusData, "lastTime"); Long lastTime = getLongValue(statusData, "lastTime");
String clientIp = trimToNull(statusData.get("clientIp")); String clientIp = (String) statusData.get("clientIp");
String iotId = trimToNull(statusData.get("iotId"));
Long userId = getLongValue(statusData, "userId");
if (productKey == null || deviceName == null || status == null) { if (productKey == null || deviceName == null || status == null) {
log.warn("[设备状态] 状态数据不完整,跳过处理: {}", JSONUtil.toJsonStr(statusData)); log.warn("[设备状态] 状态数据不完整,跳过处理: {}", JSONUtil.toJsonStr(statusData));
return; return;
} }
if (statusTime == null) {
statusTime = System.currentTimeMillis() / 1000;
}
if (lastTime == null) {
lastTime = statusTime;
}
log.info("[设备状态] 设备状态变更 - ProductKey: {}, DeviceName: {}, Status: {}", log.info("[设备状态] 设备状态变更 - ProductKey: {}, DeviceName: {}, Status: {}",
productKey, deviceName, status); productKey, deviceName, status);
IotDeviceStatus existingStatus = findExistingStatus(productKey, deviceName); // 查询是否已存在记录
IotDeviceStatus existingStatus = deviceStatusMapper.selectOne(
new LambdaQueryWrapper<IotDeviceStatus>()
.eq(IotDeviceStatus::getProductKey, productKey)
.eq(IotDeviceStatus::getDeviceName, deviceName)
);
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
@@ -74,12 +62,8 @@ public class DeviceStatusServiceImpl implements DeviceStatusService {
existingStatus.setStatusTime(statusTime); existingStatus.setStatusTime(statusTime);
existingStatus.setLastTime(lastTime); existingStatus.setLastTime(lastTime);
existingStatus.setClientIp(clientIp); existingStatus.setClientIp(clientIp);
existingStatus.setIotId(iotId);
existingStatus.setUserId(userId);
existingStatus.setUpdatedTime(now); existingStatus.setUpdatedTime(now);
TenantHelper.ignore(() -> { deviceStatusMapper.updateById(existingStatus);
deviceStatusMapper.updateById(existingStatus);
});
log.debug("[设备状态] 更新设备状态记录 - ID: {}, Status: {}", existingStatus.getId(), status); log.debug("[设备状态] 更新设备状态记录 - ID: {}, Status: {}", existingStatus.getId(), status);
} else { } else {
@@ -87,34 +71,18 @@ public class DeviceStatusServiceImpl implements DeviceStatusService {
IotDeviceStatus newStatus = new IotDeviceStatus(); IotDeviceStatus newStatus = new IotDeviceStatus();
newStatus.setProductKey(productKey); newStatus.setProductKey(productKey);
newStatus.setDeviceName(deviceName); newStatus.setDeviceName(deviceName);
newStatus.setIotId(iotId); newStatus.setIotId((String) statusData.get("iotId"));
newStatus.setStatus(status); newStatus.setStatus(status);
newStatus.setStatusTime(statusTime); newStatus.setStatusTime(statusTime);
newStatus.setLastTime(lastTime); newStatus.setLastTime(lastTime);
newStatus.setClientIp(clientIp); newStatus.setClientIp(clientIp);
newStatus.setUserId(userId);
newStatus.setCreatedTime(now); newStatus.setCreatedTime(now);
newStatus.setUpdatedTime(now); newStatus.setUpdatedTime(now);
try { deviceStatusMapper.insert(newStatus);
TenantHelper.ignore(() -> {
deviceStatusMapper.insert(newStatus);
});
} catch (DuplicateKeyException e) {
log.warn("[设备状态] 状态记录并发插入冲突,改为更新: productKey={}, deviceName={}", productKey, deviceName);
updateStatusByDevice(productKey, deviceName, newStatus);
}
log.info("[设备状态] 新增设备状态记录 - ProductKey: {}, DeviceName: {}, Status: {}", log.info("[设备状态] 新增设备状态记录 - ProductKey: {}, DeviceName: {}, Status: {}",
productKey, deviceName, status); productKey, deviceName, status);
} }
if (deviceDataHandler != null) {
if ("online".equals(status)) {
deviceDataHandler.handleDeviceOnline(deviceName);
} else if ("offline".equals(status)) {
deviceDataHandler.handleDeviceOffline(deviceName);
}
}
} catch (Exception e) { } catch (Exception e) {
log.error("[设备状态] 处理设备状态变更异常: {}", e.getMessage(), e); log.error("[设备状态] 处理设备状态变更异常: {}", e.getMessage(), e);
throw e; throw e;
@@ -123,11 +91,11 @@ public class DeviceStatusServiceImpl implements DeviceStatusService {
@Override @Override
public IotDeviceStatus queryDeviceStatus(String productKey, String deviceName) { public IotDeviceStatus queryDeviceStatus(String productKey, String deviceName) {
return TenantHelper.ignore(() -> deviceStatusMapper.selectOne( return deviceStatusMapper.selectOne(
new LambdaQueryWrapper<IotDeviceStatus>() new LambdaQueryWrapper<IotDeviceStatus>()
.eq(IotDeviceStatus::getProductKey, productKey) .eq(IotDeviceStatus::getProductKey, productKey)
.eq(IotDeviceStatus::getDeviceName, deviceName) .eq(IotDeviceStatus::getDeviceName, deviceName)
)); );
} }
@Override @Override
@@ -153,53 +121,4 @@ public class DeviceStatusServiceImpl implements DeviceStatusService {
return null; return null;
} }
} }
private IotDeviceStatus findExistingStatus(String productKey, String deviceName) {
return TenantHelper.ignore(() -> {
List<IotDeviceStatus> list = deviceStatusMapper.selectList(
new LambdaQueryWrapper<IotDeviceStatus>()
.eq(IotDeviceStatus::getProductKey, productKey)
.eq(IotDeviceStatus::getDeviceName, deviceName)
);
if (list == null || list.isEmpty()) {
return null;
}
if (list.size() > 1) {
log.warn("[设备状态] 存在重复状态记录: productKey={}, deviceName={}, count={}",
productKey, deviceName, list.size());
}
return list.get(0);
});
}
private void updateStatusByDevice(String productKey, String deviceName, IotDeviceStatus status) {
TenantHelper.ignore(() -> {
deviceStatusMapper.update(status,
new LambdaUpdateWrapper<IotDeviceStatus>()
.eq(IotDeviceStatus::getProductKey, productKey)
.eq(IotDeviceStatus::getDeviceName, deviceName)
);
});
}
private String normalizeStatus(Object status) {
String text = trimToNull(status);
if (text == null) {
return null;
}
text = text.toLowerCase();
if ("online".equals(text) || "offline".equals(text)) {
return text;
}
log.warn("[设备状态] 未知状态值,跳过处理: {}", text);
return null;
}
private String trimToNull(Object value) {
if (value == null) {
return null;
}
String text = value.toString().trim();
return text.isEmpty() ? null : text;
}
} }

View File

@@ -1,363 +0,0 @@
package com.intc.iot.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.intc.common.redis.utils.RedisUtils;
import com.intc.common.tenant.helper.TenantHelper;
import com.intc.fishery.constant.DefineDeviceWarnCode;
import com.intc.fishery.domain.Device;
import com.intc.fishery.domain.DeviceSwitch;
import com.intc.fishery.domain.TimingCtrl;
import com.intc.fishery.mapper.DeviceMapper;
import com.intc.fishery.mapper.DeviceSwitchMapper;
import com.intc.fishery.mapper.TimingCtrlMapper;
import com.intc.fishery.utils.MessageOpRecordUtil;
import com.intc.iot.constant.IOTPropertyName;
import com.intc.iot.service.IotCloudService;
import com.intc.iot.service.SwitchTurnResult;
import com.intc.iot.service.TimingCtrlExecuteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 定时控制执行服务实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnBean(IotCloudService.class)
public class TimingCtrlExecuteServiceImpl implements TimingCtrlExecuteService {
private static final String DAILY_EXEC_LOCK_PREFIX = "fishery:timingCtrl:daily:exec:";
private static final DateTimeFormatter DATE_KEY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final long SCAN_WINDOW_MINUTES = 4L;
/**
* 同一控制器多个开关同时到期时,每条指令之间的下发间隔(秒)
*/
private static final long SAME_DEVICE_DELAY_SECONDS = 20L;
/**
* 启动时打印日志,用于确认 Bean 是否成功创建
*/
@jakarta.annotation.PostConstruct
public void init() {
log.info("[TimingCtrl] TimingCtrlExecuteService 已初始化,扫描窗口={}分钟Redis锁前缀={}",
SCAN_WINDOW_MINUTES, DAILY_EXEC_LOCK_PREFIX);
}
@jakarta.annotation.PreDestroy
public void destroy() {
log.info("[TimingCtrl] 关闭定时控制指令调度线程池...");
commandScheduler.shutdown();
try {
if (!commandScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
commandScheduler.shutdownNow();
}
} catch (InterruptedException e) {
commandScheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
private final DeviceMapper deviceMapper;
private final DeviceSwitchMapper deviceSwitchMapper;
private final TimingCtrlMapper timingCtrlMapper;
private final IotCloudService iotCloudService;
/**
* 延迟下发指令的调度线程池(守护线程,不阻塞应用关闭)
*/
private final ScheduledExecutorService commandScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "timing-ctrl-cmd-scheduler");
t.setDaemon(true);
return t;
});
@Override
public SwitchTurnResult executeSwitch(Long switchId, Integer targetStatus, String opTypeLabel,
String opMessagePrefix, boolean timingOp) {
try {
if (switchId == null || targetStatus == null) {
return SwitchTurnResult.fail("参数不能为空");
}
DeviceSwitch deviceSwitch = deviceSwitchMapper.selectOne(
new LambdaQueryWrapper<DeviceSwitch>()
.eq(DeviceSwitch::getId, switchId)
.select(DeviceSwitch::getId, DeviceSwitch::getDeviceId, DeviceSwitch::getIndex,
DeviceSwitch::getSwitchName, DeviceSwitch::getIsOpen, DeviceSwitch::getLastTurnTime)
);
if (deviceSwitch == null) {
return SwitchTurnResult.fail("开关不存在");
}
if (deviceSwitch.getLastTurnTime() != null) {
long timeDiff = System.currentTimeMillis() - deviceSwitch.getLastTurnTime().getTime();
if (timeDiff < 3000) {
return SwitchTurnResult.fail("操作过于频繁,请稍后再试");
}
}
Device device = deviceMapper.selectOne(
new LambdaQueryWrapper<Device>()
.eq(Device::getId, deviceSwitch.getDeviceId())
.select(Device::getId, Device::getUserId, Device::getIotId, Device::getSerialNum,
Device::getDeviceName, Device::getWarnCode, Device::getDeadTime)
);
if (device == null) {
return SwitchTurnResult.fail("开关不存在或无权限访问");
}
if (device.getDeadTime() != null && new Date().after(device.getDeadTime())) {
return SwitchTurnResult.fail("设备服务已过期");
}
if (device.getWarnCode() == null || device.getWarnCode() == 99
|| (device.getWarnCode() & DefineDeviceWarnCode.DeviceOffline) != 0) {
return SwitchTurnResult.fail("设备离线或已关机");
}
if (iotCloudService == null) {
return SwitchTurnResult.fail("物联网服务未启用");
}
Map<String, Object> properties = new HashMap<>();
properties.put(IOTPropertyName.SWITCH_INDEX + deviceSwitch.getIndex(), targetStatus);
boolean success = iotCloudService.setProperty(device.getIotId(), properties, true, 2);
if (!success) {
return SwitchTurnResult.fail("设置开关失败");
}
Date now = new Date();
deviceSwitchMapper.update(null,
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<DeviceSwitch>()
.eq(DeviceSwitch::getId, switchId)
.set(DeviceSwitch::getIsOpen, targetStatus)
.set(DeviceSwitch::getLastTurnTime, now)
);
if (device.getUserId() != null && opTypeLabel != null && timingOp) {
String deviceLabel = device.getDeviceName() != null ? device.getDeviceName() : device.getSerialNum();
String switchLabel = deviceSwitch.getSwitchName() != null
? deviceSwitch.getSwitchName()
: ("Switch" + deviceSwitch.getIndex());
String operation = targetStatus == 1 ? "开启" : "关闭";
String message = String.format("%s(%s)的开关%s%s%s。",
deviceLabel, device.getSerialNum(), switchLabel, operation,
opMessagePrefix != null ? "" + opMessagePrefix + "" : "");
MessageOpRecordUtil.addMessageOpRecordTimingLoop(device.getUserId(), opTypeLabel, message);
}
log.info("[SwitchTurn] 设备={} 开关{} {}成功",
device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(),
deviceSwitch.getSwitchName(),
targetStatus == 1 ? "开启" : "关闭");
return SwitchTurnResult.ok("操作成功");
} catch (Exception e) {
log.error("[SwitchTurn] 开关执行失败: {}", e.getMessage(), e);
return SwitchTurnResult.fail("开关控制失败: " + e.getMessage());
}
}
@Override
public int scanAndExecuteDailyTimingCtrl() {
// 后台定时任务没有租户上下文必须忽略租户过滤否则SQL会拼接 WHERE tenant_id = NULL 导致查不到数据
return TenantHelper.ignore(() -> doScanAndExecuteDailyTimingCtrl());
}
private int doScanAndExecuteDailyTimingCtrl() {
List<TimingCtrl> list = listEnabledDailyTimingCtrls();
if (list.isEmpty()) {
log.info("[TimingCtrl] 数据库中没有启用的每天循环定时控制记录(isOpen=1, loopType=1)");
return 0;
}
LocalDateTime now = LocalDateTime.now();
log.info("[TimingCtrl] 扫描开始,当前时间={},查到 {} 条启用的定时控制", now, list.size());
// 第一轮收集所有到期任务按设备ID分组
Map<Long, List<DueTask>> tasksByDevice = new LinkedHashMap<>();
for (TimingCtrl timingCtrl : list) {
try {
if (timingCtrl.getSwitchId() == null || timingCtrl.getOpenTime() == null || timingCtrl.getCloseTime() == null) {
log.debug("[TimingCtrl] 跳过: 字段为空id={}, switchId={}, openTime={}, closeTime={}",
timingCtrl.getId(), timingCtrl.getSwitchId(), timingCtrl.getOpenTime(), timingCtrl.getCloseTime());
continue;
}
LocalTime openTime = toLocalTime(timingCtrl.getOpenTime());
LocalTime closeTime = toLocalTime(timingCtrl.getCloseTime());
boolean openDue = isDueInLookbackWindow(now, openTime);
boolean closeDue = isDueInLookbackWindow(now, closeTime);
if (!openDue && !closeDue) {
log.debug("[TimingCtrl] 跳过: 未到执行时间id={}, openTime={}, closeTime={}, now={}",
timingCtrl.getId(), openTime, closeTime, now.toLocalTime());
continue;
}
// 查询开关所属设备ID用于按设备分组错峰下发
Long deviceId = getDeviceIdBySwitchId(timingCtrl.getSwitchId());
if (deviceId == null) {
log.warn("[TimingCtrl] 开关不存在跳过switchId={}", timingCtrl.getSwitchId());
continue;
}
if (openDue) {
if (tryLock(timingCtrl.getId(), now.toLocalDate(), "open")) {
tasksByDevice.computeIfAbsent(deviceId, k -> new ArrayList<>())
.add(new DueTask(timingCtrl.getId(), timingCtrl.getSwitchId(), 1));
} else {
log.debug("[TimingCtrl] 开启: 今天已执行过跳过id={}", timingCtrl.getId());
}
}
if (closeDue) {
if (tryLock(timingCtrl.getId(), now.toLocalDate(), "close")) {
tasksByDevice.computeIfAbsent(deviceId, k -> new ArrayList<>())
.add(new DueTask(timingCtrl.getId(), timingCtrl.getSwitchId(), 0));
} else {
log.debug("[TimingCtrl] 关闭: 今天已执行过跳过id={}", timingCtrl.getId());
}
}
} catch (Exception e) {
log.error("[TimingCtrl] 处理定时控制失败id={}: {}", timingCtrl.getId(), e.getMessage(), e);
}
}
if (tasksByDevice.isEmpty()) {
log.info("[TimingCtrl] 本次扫描没有需要执行的定时控制");
return 0;
}
// 第二轮:按设备分组下发指令,同一设备的多个开关每隔 SAME_DEVICE_DELAY_SECONDS 秒下发一条
int executedCount = 0;
for (Map.Entry<Long, List<DueTask>> entry : tasksByDevice.entrySet()) {
Long deviceId = entry.getKey();
List<DueTask> tasks = entry.getValue();
log.info("[TimingCtrl] 设备{} 共有 {} 条定时控制待执行", deviceId, tasks.size());
for (int i = 0; i < tasks.size(); i++) {
DueTask task = tasks.get(i);
long delaySeconds = i * SAME_DEVICE_DELAY_SECONDS;
if (delaySeconds == 0) {
// 第一条立即下发
SwitchTurnResult result = executeSwitch(task.switchId, task.targetStatus,
"定时控制管理", "定时任务到达" + (task.targetStatus == 1 ? "开启" : "关闭") + "时间", true);
if (result.isSuccess()) {
executedCount++;
} else {
log.warn("[TimingCtrl] 执行失败timingCtrlId={}, msg={}", task.timingCtrlId, result.getMessage());
}
} else {
// 同一设备后续开关延迟下发,避免短时间内集中下发多条指令导致硬件无法及时处理
final Long tcId = task.timingCtrlId;
final Long switchId = task.switchId;
final int targetStatus = task.targetStatus;
final long delay = delaySeconds;
log.info("[TimingCtrl] 定时控制{} 将延迟{}秒后执行(设备{}排队)", tcId, delay, deviceId);
commandScheduler.schedule(() -> {
try {
TenantHelper.ignore(() -> {
log.info("[TimingCtrl] 开始延迟执行定时控制timingCtrlId={}", tcId);
SwitchTurnResult result = executeSwitch(switchId, targetStatus,
"定时控制管理", "定时任务到达" + (targetStatus == 1 ? "开启" : "关闭") + "时间", true);
if (result.isSuccess()) {
log.info("[TimingCtrl] 延迟执行成功timingCtrlId={}", tcId);
} else {
log.warn("[TimingCtrl] 延迟执行失败timingCtrlId={}, msg={}", tcId, result.getMessage());
}
});
} catch (Exception e) {
log.error("[TimingCtrl] 延迟执行异常timingCtrlId={}: {}", tcId, e.getMessage(), e);
}
}, delay, TimeUnit.SECONDS);
executedCount++;
}
}
}
return executedCount;
}
/**
* 根据开关ID查询所属设备ID
*/
private Long getDeviceIdBySwitchId(Long switchId) {
DeviceSwitch ds = deviceSwitchMapper.selectOne(
new LambdaQueryWrapper<DeviceSwitch>()
.eq(DeviceSwitch::getId, switchId)
.select(DeviceSwitch::getDeviceId)
);
return ds != null ? ds.getDeviceId() : null;
}
/**
* 到期的定时控制任务
*/
private static class DueTask {
final Long timingCtrlId;
final Long switchId;
final int targetStatus;
DueTask(Long timingCtrlId, Long switchId, int targetStatus) {
this.timingCtrlId = timingCtrlId;
this.switchId = switchId;
this.targetStatus = targetStatus;
}
}
@Override
public List<TimingCtrl> listEnabledDailyTimingCtrls() {
return timingCtrlMapper.selectList(
new LambdaQueryWrapper<TimingCtrl>()
.eq(TimingCtrl::getIsOpen, 1L)
.eq(TimingCtrl::getLoopType, 2L)
.select(TimingCtrl::getId, TimingCtrl::getSwitchId, TimingCtrl::getOpenTime,
TimingCtrl::getCloseTime, TimingCtrl::getLoopType, TimingCtrl::getIsOpen)
);
}
private static LocalTime toLocalTime(Date date) {
return LocalDateTime.ofInstant(date.toInstant(), java.time.ZoneId.systemDefault()).toLocalTime();
}
private static boolean isDueInLookbackWindow(LocalDateTime now, LocalTime scheduleTime) {
LocalDateTime scheduleDateTime = LocalDateTime.of(now.toLocalDate(), scheduleTime);
if (scheduleDateTime.isAfter(now)) {
scheduleDateTime = scheduleDateTime.minusDays(1);
}
LocalDateTime lowerBound = now.minusMinutes(SCAN_WINDOW_MINUTES);
return !scheduleDateTime.isBefore(lowerBound) && !scheduleDateTime.isAfter(now);
}
private boolean tryLock(Long timingCtrlId, LocalDate date, String action) {
String redisKey = DAILY_EXEC_LOCK_PREFIX + timingCtrlId + ":" + date.format(DATE_KEY_FORMATTER) + ":" + action;
boolean locked = RedisUtils.setObjectIfAbsent(redisKey, "1", Duration.ofHours(25));
if (locked) {
log.info("[TimingCtrl] 获取执行锁成功timingCtrlId={}, date={}, action={}", timingCtrlId, date, action);
}
return locked;
}
}

View File

@@ -1,83 +0,0 @@
package com.intc.iot.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.intc.fishery.domain.Device;
import com.intc.fishery.mapper.DeviceMapper;
import com.intc.iot.handler.DeviceDataHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.Date;
import java.util.List;
/**
* 设备即将到期告警定时任务
* 每小时扫描一次设备表对到期时间距离当前不到24小时的设备触发电话通知提醒用户续费。
*
* @author intc-iot
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnBean(DeviceDataHandler.class)
public class DeviceExpirationWarnTask {
private final DeviceDataHandler deviceDataHandler;
private final DeviceMapper deviceMapper;
/**
* 每2小时执行一次扫描即将到期24小时内的设备并触发电话通知
*/
@Scheduled(fixedDelay = 7200000)
public void checkDeviceExpirationAndNotify() {
try {
// 只在 8:00 ~ 21:00 之间拨打电话通知,避免夜间打扰用户
LocalTime currentTime = LocalTime.now();
if (currentTime.isBefore(LocalTime.of(8, 0)) || !currentTime.isBefore(LocalTime.of(21, 0))) {
log.debug("[到期告警] 当前时间 {} 不在电话通知时段(08:00~21:00),跳过本次扫描", currentTime);
return;
}
Date now = new Date();
// 24小时后的时间点
Date deadline = new Date(now.getTime() + 24L * 60 * 60 * 1000);
// 查询所有满足条件的设备:
// 1. deadTime 不为空
// 2. deadTime > 当前时间(尚未到期)
// 3. deadTime <= 当前时间 + 24小时即将到期
// 4. userId 不为空(已绑定用户)
List<Device> expiringDevices = deviceMapper.selectList(
new LambdaQueryWrapper<Device>()
.isNotNull(Device::getDeadTime)
.gt(Device::getDeadTime, now)
.le(Device::getDeadTime, deadline)
.isNotNull(Device::getUserId)
.select(Device::getId, Device::getUserId, Device::getSerialNum,
Device::getDeviceName, Device::getDeviceType, Device::getPondId,
Device::getDeadTime)
);
if (expiringDevices == null || expiringDevices.isEmpty()) {
return;
}
log.info("[到期告警] 扫描到 {} 台即将到期的设备,开始逐一通知", expiringDevices.size());
for (Device device : expiringDevices) {
try {
deviceDataHandler.triggerDeviceExpirationAlarm(device);
} catch (Exception e) {
log.error("[到期告警] 处理单台设备异常deviceId={}: {}", device.getId(), e.getMessage(), e);
}
}
} catch (Exception e) {
log.error("[到期告警] 扫描设备到期告警异常: {}", e.getMessage(), e);
}
}
}

View File

@@ -1,7 +1,6 @@
package com.intc.iot.task; package com.intc.iot.task;
import com.intc.common.redis.utils.RedisUtils; import com.intc.common.redis.utils.RedisUtils;
import com.intc.fishery.constant.DefineDeviceWarnCode;
import com.intc.iot.config.AliyunIotProperties; import com.intc.iot.config.AliyunIotProperties;
import com.intc.iot.handler.DeviceDataHandler; import com.intc.iot.handler.DeviceDataHandler;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -29,12 +28,6 @@ public class DeviceOfflineWarnTask {
private static final String OFFLINE_WAIT_KEY_PREFIX = "device:offline:wait:"; private static final String OFFLINE_WAIT_KEY_PREFIX = "device:offline:wait:";
/** Redis value 分隔符,格式:{offlineTime}|{deviceType} */
private static final String VALUE_SEPARATOR = "|";
/** 太阳能网控设备类型值 */
private static final int SOLAR_CONTROLLER_TYPE = 3;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final DeviceDataHandler deviceDataHandler; private final DeviceDataHandler deviceDataHandler;
@@ -43,8 +36,7 @@ public class DeviceOfflineWarnTask {
/** /**
* 每分钟扫描一次离线等待标记 * 每分钟扫描一次离线等待标记
* 对超过配置的延迟时长的设备触发告警 * 对超过配置的 offline-warn-delay-minutes 分钟的设备触发告警
* Redis value 格式:{offlineTime}|{deviceType},根据设备类型使用对应延迟时长
*/ */
@Scheduled(fixedDelay = 60000) @Scheduled(fixedDelay = 60000)
public void checkOfflineWaitAndTrigger() { public void checkOfflineWaitAndTrigger() {
@@ -57,48 +49,22 @@ public class DeviceOfflineWarnTask {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
for (String key : keys) { for (String key : keys) {
try { try {
String value = RedisUtils.getCacheObject(key); String offlineTimeStr = RedisUtils.getCacheObject(key);
if (value == null) { if (offlineTimeStr == null) {
continue; continue;
} }
// 解析 value兼容旧格式纯时间字符串和新格式时间|设备类型)
String offlineTimeStr;
int deviceType = 0;
if (value.contains(VALUE_SEPARATOR)) {
String[] parts = value.split("\\" + VALUE_SEPARATOR, 2);
offlineTimeStr = parts[0];
try {
deviceType = Integer.parseInt(parts[1]);
} catch (NumberFormatException ignore) {
}
} else {
// 旧格式兼容:纯时间字符串,设备类型未知,使用默认延迟
offlineTimeStr = value;
}
LocalDateTime offlineTime = LocalDateTime.parse(offlineTimeStr, FORMATTER); LocalDateTime offlineTime = LocalDateTime.parse(offlineTimeStr, FORMATTER);
long elapsedMinutes = java.time.Duration.between(offlineTime, now).toMinutes(); long elapsedMinutes = java.time.Duration.between(offlineTime, now).toMinutes();
// 根据设备类型选择对应的延迟时长 if (elapsedMinutes >= aliyunIotProperties.getOfflineWarnDelayMinutes()) {
int delayMinutes = (deviceType == SOLAR_CONTROLLER_TYPE)
? aliyunIotProperties.getSolarOfflineWarnDelayMinutes()
: aliyunIotProperties.getOfflineWarnDelayMinutes();
if (elapsedMinutes >= delayMinutes) {
// 提取 deviceName去掉前缀 // 提取 deviceName去掉前缀
String deviceName = key.substring(OFFLINE_WAIT_KEY_PREFIX.length()); String deviceName = key.substring(OFFLINE_WAIT_KEY_PREFIX.length());
// 触发告警前先删除标记,防止重复触发 // 触发告警前先删除标记,防止重复触发
RedisUtils.deleteObject(key); RedisUtils.deleteObject(key);
log.info("[离线告警] 设备离线已超过 {} 分钟(阈值{}分钟),触发告警: {}", elapsedMinutes, delayMinutes, deviceName); log.info("[离线告警] 设备离线已超过 {} 分钟,触发告警: {}", elapsedMinutes, deviceName);
// 太阳能网控设备超时后,从休眠状态转为真正离线:置 DeviceOffline 位,清 DeviceSleeping 位
if (deviceType == SOLAR_CONTROLLER_TYPE) {
deviceDataHandler.setDeviceOfflineAndClearSleeping(deviceName);
}
deviceDataHandler.triggerOfflineAlarm(deviceName); deviceDataHandler.triggerOfflineAlarm(deviceName);
} }
} catch (Exception e) { } catch (Exception e) {
@@ -109,50 +75,4 @@ public class DeviceOfflineWarnTask {
log.error("[离线告警] 扫描离线标记异常: {}", e.getMessage(), e); log.error("[离线告警] 扫描离线标记异常: {}", e.getMessage(), e);
} }
} }
/**
* 每分钟扫描一次溶解氧探头离线等待标记
* 对超过配置的延迟时长的设备触发探头离线告警
* Redis value 格式:{offlineTime}yyyy-MM-dd HH:mm:ss
*/
@Scheduled(fixedDelay = 60000)
public void checkDetectorOxyOfflineWaitAndTrigger() {
try {
Collection<String> keys = RedisUtils.keys(DeviceDataHandler.DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX + "*");
if (keys == null || keys.isEmpty()) {
return;
}
LocalDateTime now = LocalDateTime.now();
int delayMinutes = aliyunIotProperties.getOfflineWarnDelayMinutes();
for (String key : keys) {
try {
String offlineTimeStr = RedisUtils.getCacheObject(key);
if (offlineTimeStr == null) {
continue;
}
LocalDateTime offlineTime = LocalDateTime.parse(offlineTimeStr, FORMATTER);
long elapsedMinutes = java.time.Duration.between(offlineTime, now).toMinutes();
if (elapsedMinutes >= delayMinutes) {
// 提取 deviceName去掉前缀
String deviceName = key.substring(DeviceDataHandler.DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX.length());
// 触发告警前先删除标记,防止重复触发
RedisUtils.deleteObject(key);
log.info("[探头离线告警] 溶解氧探头离线已超过 {} 分钟(阈值{}分钟),触发告警: {}", elapsedMinutes, delayMinutes, deviceName);
deviceDataHandler.triggerDetectorOxyOfflineAlarm(deviceName);
}
} catch (Exception e) {
log.error("[探头离线告警] 处理单条探头离线标记异常key={}: {}", key, e.getMessage(), e);
}
}
} catch (Exception e) {
log.error("[探头离线告警] 扫描探头离线标记异常: {}", e.getMessage(), e);
}
}
} }

View File

@@ -1,38 +0,0 @@
package com.intc.iot.task;
import com.intc.iot.service.TimingCtrlExecuteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 开关定时任务执行扫描
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnBean(TimingCtrlExecuteService.class)
public class TimingCtrlExecuteTask {
private static final long SCAN_DELAY_MS = 120000L;
private static final long INITIAL_DELAY_MS = 10000L;
private final TimingCtrlExecuteService timingCtrlExecuteService;
/**
* 每5分钟扫描一次启用中的每天循环定时控制
* 启动后10秒执行第一次扫描
*/
@Scheduled(fixedDelay = SCAN_DELAY_MS, initialDelay = INITIAL_DELAY_MS)
public void scanDailyTimingCtrl() {
try {
log.info("[TimingCtrlTask] 开始扫描定时控制...");
int count = timingCtrlExecuteService.scanAndExecuteDailyTimingCtrl();
log.info("[TimingCtrlTask] 本次扫描完成,执行了 {} 条定时控制", count);
} catch (Exception e) {
log.error("[TimingCtrlTask] 扫描定时控制失败: {}", e.getMessage(), e);
}
}
}

View File

@@ -77,23 +77,4 @@ public interface DeviceSensorDataMapper {
@Param("endTime") String endTime, @Param("endTime") String endTime,
@Param("intervalType") Integer intervalType); @Param("intervalType") Integer intervalType);
/**
* 查询设备最新上数时间
*
* @param serialNum 设备序列号
* @return 最新传感器数据
*/
@DS("taos")
@InterceptorIgnore(tenantLine = "true")
DeviceSensorData getLatestReportTime(@Param("serialNum") String serialNum);
/**
* 删除设备子表(清除该设备的所有时序数据)
*
* @param serialNum 设备序列号
*/
@DS("taos")
@InterceptorIgnore(tenantLine = "true")
void dropSubTable(@Param("serialNum") String serialNum);
} }

View File

@@ -38,19 +38,4 @@ public interface IDeviceSensorDataService {
* @return 设备传感器数据列表 * @return 设备传感器数据列表
*/ */
public List<DeviceSensorData> getHistoryDataList(String serialNum, String startTime, String endTime, Integer intervalType); public List<DeviceSensorData> getHistoryDataList(String serialNum, String startTime, String endTime, Integer intervalType);
/**
* 查询设备最新上数时间
*
* @param serialNum 设备序列号
* @return 最新上数时间字符串,格式 yyyy-MM-dd HH:mm:ss无数据时返回 null
*/
String getLatestReportTime(String serialNum);
/**
* 删除设备子表(清除该设备的所有时序数据)
*
* @param serialNum 设备序列号
*/
void dropSubTable(String serialNum);
} }

View File

@@ -257,31 +257,6 @@ public class DeviceSensorDataService implements IDeviceSensorDataService {
return list; return list;
} }
/**
* 查询设备最新上数时间
*/
@Override
public String getLatestReportTime(String serialNum) {
if (serialNum == null || serialNum.isBlank()) {
return null;
}
try {
DeviceSensorData latest = deviceSensorDataMapper.getLatestReportTime(serialNum);
if (latest == null) {
return null;
}
return latest.getCreateTime() != null ? latest.getCreateTime() : latest.getTime();
} catch (Exception e) {
String errorMsg = e.getMessage();
if (errorMsg != null && errorMsg.toLowerCase().contains("not exist")) {
log.debug("设备TDengine子表不存在或无数据: serialNum={}", serialNum);
} else {
log.warn("查询设备最新上数时间失败: serialNum={}, error={}", serialNum, errorMsg);
}
return null;
}
}
/** /**
* 获取间隔类型描述 * 获取间隔类型描述
*/ */
@@ -296,27 +271,4 @@ public class DeviceSensorDataService implements IDeviceSensorDataService {
default: return "未知"; default: return "未知";
} }
} }
/**
* 删除设备子表(清除该设备的所有时序数据)
*/
@Override
public void dropSubTable(String serialNum) {
if (serialNum == null || serialNum.isBlank()) {
return;
}
try {
deviceSensorDataMapper.dropSubTable(serialNum);
// 清除建表缓存,下次插入数据时会重新建表
CREATED_TABLES.remove(serialNum);
log.info("已删除设备TDengine子表: serialNum={}", serialNum);
} catch (Exception e) {
String errorMsg = e.getMessage();
if (errorMsg != null && errorMsg.toLowerCase().contains("not exist")) {
log.debug("设备TDengine子表不存在无需删除: serialNum={}", serialNum);
} else {
log.warn("删除设备TDengine子表失败: serialNum={}, error={}", serialNum, errorMsg);
}
}
}
} }

View File

@@ -111,17 +111,4 @@
</choose> </choose>
</if> </if>
</select> </select>
<select id="getLatestReportTime" resultType="com.intc.tdengine.domain.DeviceSensorData">
SELECT
`time`,
createTime
FROM `fishery`.`t_${serialNum}`
ORDER BY `time` DESC
LIMIT 1
</select>
<update id="dropSubTable">
DROP TABLE IF EXISTS `fishery`.`t_${serialNum}`
</update>
</mapper> </mapper>

View File

@@ -262,7 +262,6 @@ public class WeixinController extends BaseController {
// 9. 构建返回结果 // 9. 构建返回结果
WxPayOrderVo result = new WxPayOrderVo(); WxPayOrderVo result = new WxPayOrderVo();
result.setPrepayId(prepayId); result.setPrepayId(prepayId);
result.setOutTradeNumber(order.getOutTradeNumber());
result.setTotalAmount(order.getTotalAmount()); result.setTotalAmount(order.getTotalAmount());
result.setTimestamp(payParams.get("timeStamp")); result.setTimestamp(payParams.get("timeStamp"));
result.setNonceStr(payParams.get("nonceStr")); result.setNonceStr(payParams.get("nonceStr"));
@@ -279,36 +278,6 @@ public class WeixinController extends BaseController {
} }
} }
@Operation(summary = "主动确认微信支付订单")
@PostMapping("/pay/confirm_order")
public R<Boolean> confirmOrder(@RequestBody Map<String, Object> request) {
try {
if (payOrderBusinessService == null) {
return R.fail("支付服务未启用");
}
String outTradeNumber = request != null && request.get("outTradeNumber") != null
? String.valueOf(request.get("outTradeNumber"))
: null;
if ((outTradeNumber == null || outTradeNumber.isEmpty()) && request != null && request.get("orderId") != null) {
Long orderId = Long.valueOf(String.valueOf(request.get("orderId")));
com.intc.fishery.domain.PayOrder order = payOrderBusinessService.queryById(orderId);
if (order != null) {
outTradeNumber = order.getOutTradeNumber();
}
}
if (outTradeNumber == null || outTradeNumber.isEmpty()) {
return R.fail("商户订单号不能为空");
}
boolean success = payOrderBusinessService.confirmPaidOrder(outTradeNumber);
return success ? R.ok(true) : R.fail("订单未支付成功或确认失败");
} catch (Exception e) {
log.error("确认支付订单失败", e);
return R.fail("确认支付订单失败: " + e.getMessage());
}
}
@Operation(summary = "微信支付回调通知") @Operation(summary = "微信支付回调通知")
@PostMapping("/pay_notify") @PostMapping("/pay_notify")
public Map<String, String> payNotify( public Map<String, String> payNotify(

View File

@@ -24,12 +24,6 @@ public class WxPayOrderVo implements Serializable {
@Schema(description = "预支付ID") @Schema(description = "预支付ID")
private String prepayId; private String prepayId;
/**
* 商户系统内部订单号
*/
@Schema(description = "商户订单号")
private String outTradeNumber;
/** /**
* 签名类型 * 签名类型
*/ */

View File

@@ -82,28 +82,4 @@ public interface PayOrderBusinessService {
* @return 是否更新成功 * @return 是否更新成功
*/ */
boolean updatePrepayId(Long orderId, String prepayId); boolean updatePrepayId(Long orderId, String prepayId);
/**
* 补偿未处理的支付订单
* 主动向微信支付查询状态为“未支付/支付中/支付失败/已关闭”且有商户订单号的订单,
* 如果微信侧已支付成功则执行回调补偿
*
* @return 补偿成功的订单数量
*/
int compensateUnpaidOrders();
/**
* 主动确认单笔订单支付状态。
*
* @param outTradeNumber 商户订单号
* @return 是否已确认并处理支付成功
*/
boolean confirmPaidOrder(String outTradeNumber);
/**
* 修复已支付订单中设备有效期未落库的异常数据。
*
* @return 修复的设备数量
*/
int repairPaidOrderDeviceDeadTime();
} }

View File

@@ -61,13 +61,4 @@ public interface WxPayService {
* @return 解密后的明文 * @return 解密后的明文
*/ */
String decryptCallbackData(String associatedData, String nonce, String ciphertext); String decryptCallbackData(String associatedData, String nonce, String ciphertext);
/**
* 主动查询微信支付订单状态
* 通过商户订单号向微信支付平台查询订单的实际支付状态
*
* @param outTradeNumber 商户订单号
* @return 微信返回的订单JSON字符串查询失败返回null
*/
String queryOrderByOutTradeNo(String outTradeNumber);
} }

View File

@@ -1,8 +1,8 @@
package com.intc.weixin.service.impl; package com.intc.weixin.service.impl;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.intc.common.core.exception.ServiceException; import com.intc.common.core.exception.ServiceException;
import com.intc.common.tenant.helper.TenantHelper; import com.intc.common.tenant.helper.TenantHelper;
import com.intc.fishery.constant.DefineDeviceWarnCode; import com.intc.fishery.constant.DefineDeviceWarnCode;
@@ -15,21 +15,16 @@ import com.intc.fishery.mapper.PayOrderMapper;
import com.intc.weixin.config.WxPayItemProperties; import com.intc.weixin.config.WxPayItemProperties;
import com.intc.weixin.constant.PayOrderStatus; import com.intc.weixin.constant.PayOrderStatus;
import com.intc.weixin.service.PayOrderBusinessService; import com.intc.weixin.service.PayOrderBusinessService;
import com.intc.weixin.service.WxPayService;
import com.intc.weixin.utils.OrderNumberGenerator; import com.intc.weixin.utils.OrderNumberGenerator;
import com.intc.weixin.utils.PayOrderStatusUtil; import com.intc.weixin.utils.PayOrderStatusUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -46,20 +41,11 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
private final PayDeviceMapper payDeviceMapper; private final PayDeviceMapper payDeviceMapper;
private final DeviceMapper deviceMapper; private final DeviceMapper deviceMapper;
private final WxPayItemProperties wxPayItemProperties; private final WxPayItemProperties wxPayItemProperties;
private final WxPayService wxPayService;
private final TransactionTemplate transactionTemplate;
/** /**
* 订单处理锁,防止并发处理同一订单 * 订单处理锁,防止并发处理同一订单
*/ */
private static final ConcurrentHashMap<Long, Long> ORDER_PROCESSING_LOCK = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<Long, Long> ORDER_PROCESSING_LOCK = new ConcurrentHashMap<>();
private static final AtomicLong COMPENSATION_HISTORY_CURSOR_ID = new AtomicLong(0L);
private static final AtomicLong REPAIR_HISTORY_CURSOR_ID = new AtomicLong(0L);
private static final int RECENT_SCAN_SIZE = 200;
private static final int HISTORY_SCAN_SIZE = 200;
private static final int ORDER_UPDATE_FAILED = -1;
private static final int ORDER_ALREADY_SUCCESS = 0;
private static final int ORDER_UPDATED = 1;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@@ -186,6 +172,7 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void closeUnpaidOrders(Long userId, String openId) { public void closeUnpaidOrders(Long userId, String openId) {
// 查询用户所有未完成的订单 // 查询用户所有未完成的订单
List<PayOrder> unpaidOrders = payOrderMapper.selectList( List<PayOrder> unpaidOrders = payOrderMapper.selectList(
@@ -203,50 +190,8 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
return; return;
} }
// 批量处理订单:先查微信确认状态,避免错误关闭已支付订单 // 批量关闭订单
for (PayOrder order : unpaidOrders) { for (PayOrder order : unpaidOrders) {
try {
String queryResult = wxPayService.queryOrderByOutTradeNo(order.getOutTradeNumber());
if (queryResult != null) {
JSONObject resultJson = JSONUtil.parseObj(queryResult);
String tradeState = resultJson.getStr("trade_state");
if ("SUCCESS".equalsIgnoreCase(tradeState)) {
// 微信侧已支付成功,执行补偿而不是关闭
log.info("关闭订单前查询发现已支付: orderId={}, outTradeNumber={}", order.getId(), order.getOutTradeNumber());
compensateSingleOrder(order, resultJson);
continue;
}
if ("CLOSED".equalsIgnoreCase(tradeState)) {
payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, order.getId())
.set(PayOrder::getOrderStatus, PayOrderStatus.CLOSED)
.set(PayOrder::getTradeState, "CLOSED")
.set(PayOrder::getTradeStateDescription, "微信侧已关闭")
);
log.info("旧订单微信侧已关闭,同步本地状态: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
continue;
}
if ("USERPAYING".equalsIgnoreCase(tradeState)) {
log.info("旧订单仍在支付中,跳过关闭: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
continue;
}
}
} catch (Exception e) {
log.warn("关闭订单前查询微信失败,跳过关闭避免误关已支付订单: orderId={}, error={}", order.getId(), e.getMessage());
continue;
}
boolean closed = wxPayService.closeOrder(order.getOutTradeNumber());
if (!closed) {
log.warn("微信关单失败,跳过本地关闭: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
continue;
}
// 微信侧确认未支付,安全关闭
payOrderMapper.update(null, payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>() new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, order.getId()) .eq(PayOrder::getId, order.getId())
@@ -260,6 +205,7 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public boolean handlePaymentSuccess(String outTradeNumber, String transactionId, Integer payerTotal, public boolean handlePaymentSuccess(String outTradeNumber, String transactionId, Integer payerTotal,
String payerCurrency, String successTime, String tradeState, String payerCurrency, String successTime, String tradeState,
String tradeStateDescription, String bankType) { String tradeStateDescription, String bankType) {
@@ -272,20 +218,20 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
// 2. 检查订单状态,如果已经是成功状态,直接返回 // 2. 检查订单状态,如果已经是成功状态,直接返回
if (PayOrderStatus.SUCCESS.equals(order.getOrderStatus())) { if (PayOrderStatus.SUCCESS.equals(order.getOrderStatus())) {
int repairCount = repairOrderDeviceDeadTime(order); log.info("订单已处理过: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
log.info("订单已处理过: orderId={}, outTradeNumber={}, repairDeviceCount={}",
order.getId(), outTradeNumber, repairCount);
return true; return true;
} }
// 3. 并发控制:检查订单是否正在处理 // 3. 并发控制:检查订单是否正在处理
Long previousLockTime = ORDER_PROCESSING_LOCK.putIfAbsent(order.getId(), System.currentTimeMillis()); if (ORDER_PROCESSING_LOCK.containsKey(order.getId())) {
if (previousLockTime != null) { log.warn("订单正在处理中,跳过: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
log.warn("订单正在处理中,等待微信下次通知或补偿任务处理: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber); return false;
return true;
} }
try { try {
// 4. 加锁
ORDER_PROCESSING_LOCK.put(order.getId(), System.currentTimeMillis());
// 5. 检查金额是否一致 // 5. 检查金额是否一致
if (!order.getTotalAmount().equals(payerTotal)) { if (!order.getTotalAmount().equals(payerTotal)) {
log.error("订单金额不匹配: orderId={}, orderAmount={}, payAmount={}", log.error("订单金额不匹配: orderId={}, orderAmount={}, payAmount={}",
@@ -298,55 +244,22 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
// 7. 更新订单状态 // 7. 更新订单状态
Integer orderStatus = PayOrderStatusUtil.convertTradeStateToOrderStatus(tradeState); Integer orderStatus = PayOrderStatusUtil.convertTradeStateToOrderStatus(tradeState);
Integer updateResult = transactionTemplate.execute(status -> { payOrderMapper.update(null,
int updateRows = payOrderMapper.update(null, new LambdaUpdateWrapper<PayOrder>()
new LambdaUpdateWrapper<PayOrder>() .eq(PayOrder::getId, order.getId())
.eq(PayOrder::getId, order.getId()) .set(PayOrder::getTransactionId, transactionId)
.ne(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS) .set(PayOrder::getPayerTotal, payerTotal)
.set(PayOrder::getTransactionId, transactionId) .set(PayOrder::getPayerCurrency, payerCurrency)
.set(PayOrder::getPayerTotal, payerTotal) .set(PayOrder::getSuccessTime, successDate)
.set(PayOrder::getPayerCurrency, payerCurrency) .set(PayOrder::getTradeState, tradeState)
.set(PayOrder::getSuccessTime, successDate) .set(PayOrder::getTradeStateDescription, tradeStateDescription)
.set(PayOrder::getTradeState, tradeState) .set(PayOrder::getBankType, bankType)
.set(PayOrder::getTradeStateDescription, tradeStateDescription) .set(PayOrder::getOrderStatus, orderStatus)
.set(PayOrder::getBankType, bankType) );
.set(PayOrder::getOrderStatus, orderStatus)
);
if (updateRows <= 0) {
PayOrder latestOrder = payOrderMapper.selectById(order.getId());
if (latestOrder != null && PayOrderStatus.SUCCESS.equals(latestOrder.getOrderStatus())) {
log.info("订单已由其他回调处理成功: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
return ORDER_ALREADY_SUCCESS;
}
log.warn("订单状态更新失败: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
return ORDER_UPDATE_FAILED;
}
return ORDER_UPDATED;
});
if (updateResult == null || ORDER_UPDATE_FAILED == updateResult) {
return false;
}
// 8. 如果支付成功,更新设备到期时间 // 8. 如果支付成功,更新设备到期时间
if (PayOrderStatus.SUCCESS.equals(orderStatus)) { if (PayOrderStatus.SUCCESS.equals(orderStatus)) {
if (ORDER_UPDATED == updateResult) { updateDeviceDeadTime(order.getId());
try {
int updatedDevices = updateDeviceDeadTime(order.getId());
if (updatedDevices <= 0) {
log.warn("订单已支付成功,但本次未更新任何设备有效期,等待修复任务兜底: orderId={}, outTradeNumber={}",
order.getId(), outTradeNumber);
}
} catch (Exception e) {
log.error("订单已支付成功,但更新设备有效期异常,等待修复任务兜底: orderId={}, outTradeNumber={}, error={}",
order.getId(), outTradeNumber, e.getMessage(), e);
}
} else {
int repairCount = repairOrderDeviceDeadTime(order);
log.info("订单已由其他实例处理成功,本次只做设备有效期修复: orderId={}, outTradeNumber={}, repairDeviceCount={}",
order.getId(), outTradeNumber, repairCount);
}
} }
log.info("订单支付处理完成: orderId={}, outTradeNumber={}, status={}", log.info("订单支付处理完成: orderId={}, outTradeNumber={}, status={}",
@@ -415,101 +328,49 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
* *
* @param orderId 订单ID * @param orderId 订单ID
*/ */
private int updateDeviceDeadTime(Long orderId) { private void updateDeviceDeadTime(Long orderId) {
// 查询订单关联的设备充值记录 // 查询订单关联的设备充值记录
List<PayDevice> payDevices = queryPayDevicesByOrderId(orderId); List<PayDevice> payDevices = queryPayDevicesByOrderId(orderId);
if (payDevices == null || payDevices.isEmpty()) { if (payDevices == null || payDevices.isEmpty()) {
log.warn("订单没有关联的设备充值记录: orderId={}", orderId); log.warn("订单没有关联的设备充值记录: orderId={}", orderId);
return 0; return;
} }
int updatedCount = 0;
// 批量更新设备到期时间忽略租户过滤因为设备可能由IOT模块写入租户ID可能不一致 // 批量更新设备到期时间忽略租户过滤因为设备可能由IOT模块写入租户ID可能不一致
for (PayDevice payDevice : payDevices) { for (PayDevice payDevice : payDevices) {
try { // 仅通过 serialNum 查找设备,不限制 userId因为允许代充支付者可能与设备所有者不同
if (payDevice.getAddMonth() == null) { Device device = TenantHelper.ignore(() ->
log.error("设备充值记录缺少续费月数,跳过设备有效期更新: payDeviceId={}, orderId={}, serialNum={}", deviceMapper.selectOne(
payDevice.getId(), orderId, payDevice.getSerialNum());
continue;
}
Device device = queryDeviceByPayDevice(payDevice);
if (device == null) {
log.error("设备不存在,订单成功状态已保留,等待人工核对设备编号: orderId={}, payDeviceId={}, serialNum={}",
orderId, payDevice.getId(), payDevice.getSerialNum());
continue;
}
// 计算新的到期时间
Date newDeadTime = calculateNewDeadTime(device.getDeadTime(), payDevice.getAddMonth());
payDeviceMapper.update(null,
new LambdaUpdateWrapper<PayDevice>()
.eq(PayDevice::getId, payDevice.getId())
.set(PayDevice::getDeadTime, newDeadTime)
);
// 清除 warnCode 中的「设备到期」标志位(续费后恢复正常状态)
Integer oldWarnCode = device.getWarnCode();
Integer newWarnCode = oldWarnCode != null
? (oldWarnCode & ~DefineDeviceWarnCode.DeviceTimeDead)
: 0;
// 更新设备到期时间,并同步清除到期告警位
int updateRows = TenantHelper.ignore(() -> deviceMapper.update(null,
new LambdaUpdateWrapper<Device>()
.eq(Device::getId, device.getId())
.set(Device::getDeadTime, newDeadTime)
.set(Device::getWarnCode, newWarnCode)
));
if (updateRows <= 0) {
log.error("更新设备有效期失败,订单成功状态已保留,等待修复任务兜底: orderId={}, payDeviceId={}, serialNum={}, deviceId={}",
orderId, payDevice.getId(), payDevice.getSerialNum(), device.getId());
continue;
}
updatedCount++;
log.info("续费成功,更新设备到期时间并清除到期告警: deviceId={}, serialNum={}, newDeadTime={}, oldWarnCode={}, newWarnCode={}",
device.getId(), device.getSerialNum(), newDeadTime, oldWarnCode, newWarnCode);
} catch (Exception e) {
log.error("更新单台设备有效期异常,订单成功状态已保留: orderId={}, payDeviceId={}, serialNum={}, error={}",
orderId, payDevice.getId(), payDevice.getSerialNum(), e.getMessage(), e);
}
}
return updatedCount;
}
private Device queryDeviceByPayDevice(PayDevice payDevice) {
return TenantHelper.ignore(() -> {
if (payDevice.getTenantId() != null && !payDevice.getTenantId().isEmpty()) {
List<Device> sameTenantDevices = deviceMapper.selectList(
new LambdaQueryWrapper<Device>() new LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, payDevice.getSerialNum()) .eq(Device::getSerialNum, payDevice.getSerialNum())
.eq(Device::getTenantId, payDevice.getTenantId()) )
.orderByDesc(Device::getUpdateTime) );
.orderByDesc(Device::getCreateTime)
); if (device == null) {
if (sameTenantDevices != null && !sameTenantDevices.isEmpty()) { log.warn("设备不存在: serialNum={}", payDevice.getSerialNum());
return sameTenantDevices.get(0); continue;
}
} }
List<Device> devices = deviceMapper.selectList( // 计算新的到期时间
new LambdaQueryWrapper<Device>() Date newDeadTime = calculateNewDeadTime(device.getDeadTime(), payDevice.getAddMonth());
.eq(Device::getSerialNum, payDevice.getSerialNum())
.orderByDesc(Device::getUpdateTime) // 清除 warnCode 中的「设备到期」标志位(续费后恢复正常状态)
.orderByDesc(Device::getCreateTime) Integer oldWarnCode = device.getWarnCode();
Integer newWarnCode = oldWarnCode != null
? (oldWarnCode & ~DefineDeviceWarnCode.DeviceTimeDead)
: 0;
// 更新设备到期时间,并同步清除到期告警位
deviceMapper.update(null,
new LambdaUpdateWrapper<Device>()
.eq(Device::getId, device.getId())
.set(Device::getDeadTime, newDeadTime)
.set(Device::getWarnCode, newWarnCode)
); );
if (devices == null || devices.isEmpty()) {
return null; log.info("续费成功,更新设备到期时间并清除到期告警: deviceId={}, serialNum={}, newDeadTime={}, oldWarnCode={}, newWarnCode={}",
} device.getId(), device.getSerialNum(), newDeadTime, oldWarnCode, newWarnCode);
if (devices.size() > 1) { }
log.warn("根据设备编号查到多台设备,已取最近更新的一台: serialNum={}, selectedDeviceId={}, matchedCount={}",
payDevice.getSerialNum(), devices.get(0).getId(), devices.size());
}
return devices.get(0);
});
} }
/** /**
@@ -533,338 +394,4 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
return new Date(); return new Date();
} }
} }
@Override
public int compensateUnpaidOrders() {
// 最新订单优先处理,历史订单用游标慢慢补扫,避免老异常数据被固定时间窗口或分页上限漏掉。
Date now = new Date();
Date endTime = new Date(now.getTime() - 1L * 60 * 1000); // 1分钟前
Map<Long, PayOrder> candidateMap = new LinkedHashMap<>();
scanRecentCompensationOrders(endTime).forEach(order -> candidateMap.put(order.getId(), order));
scanHistoryCompensationOrders(endTime).forEach(order -> candidateMap.putIfAbsent(order.getId(), order));
if (candidateMap.isEmpty()) {
return 0;
}
int compensatedCount = 0;
for (PayOrder order : candidateMap.values()) {
if (compensateOrderFromWechat(order)) {
compensatedCount++;
}
}
log.info("[支付补偿] 本次扫描 {} 笔待补偿订单", candidateMap.size());
if (compensatedCount > 0) {
log.info("[支付补偿] 本次补偿完成 {} 笔订单", compensatedCount);
}
return compensatedCount;
}
private List<PayOrder> scanRecentCompensationOrders(Date endTime) {
Page<PayOrder> page = payOrderMapper.selectPage(
new Page<>(1, RECENT_SCAN_SIZE, false),
buildCompensationOrderQuery(endTime)
.orderByDesc(PayOrder::getCreateTime)
.orderByDesc(PayOrder::getId)
);
return page.getRecords() != null ? page.getRecords() : Collections.emptyList();
}
private List<PayOrder> scanHistoryCompensationOrders(Date endTime) {
long cursorId = COMPENSATION_HISTORY_CURSOR_ID.get();
LambdaQueryWrapper<PayOrder> queryWrapper = buildCompensationOrderQuery(endTime);
if (cursorId > 0) {
queryWrapper.gt(PayOrder::getId, cursorId);
}
Page<PayOrder> page = payOrderMapper.selectPage(
new Page<>(1, HISTORY_SCAN_SIZE, false),
queryWrapper.orderByAsc(PayOrder::getId)
);
List<PayOrder> records = page.getRecords();
if (records == null || records.isEmpty()) {
if (cursorId > 0) {
COMPENSATION_HISTORY_CURSOR_ID.set(0L);
log.info("[支付补偿] 历史扫描游标已到末尾,下次从最早订单重新扫描");
}
return Collections.emptyList();
}
COMPENSATION_HISTORY_CURSOR_ID.set(records.get(records.size() - 1).getId());
return records;
}
private LambdaQueryWrapper<PayOrder> buildCompensationOrderQuery(Date endTime) {
return new LambdaQueryWrapper<PayOrder>()
.and(wrapper -> wrapper
.in(PayOrder::getOrderStatus, Arrays.asList(
PayOrderStatus.NOTPAY,
PayOrderStatus.USERPAYING,
PayOrderStatus.PAYERROR
))
.or()
.and(closed -> closed
.eq(PayOrder::getOrderStatus, PayOrderStatus.CLOSED)
.and(unconfirmedClosed -> unconfirmedClosed
.isNull(PayOrder::getTradeStateDescription)
.or()
.ne(PayOrder::getTradeStateDescription, "微信侧已关闭")
)
)
.or()
.isNull(PayOrder::getOrderStatus)
)
.isNotNull(PayOrder::getOutTradeNumber)
.ne(PayOrder::getOutTradeNumber, "")
.and(time -> time
.isNull(PayOrder::getCreateTime)
.or()
.le(PayOrder::getCreateTime, endTime)
);
}
private boolean compensateOrderFromWechat(PayOrder order) {
try {
String queryResult = wxPayService.queryOrderByOutTradeNo(order.getOutTradeNumber());
if (queryResult == null) {
log.warn("[支付补偿] 查询微信订单失败: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
return false;
}
JSONObject resultJson = JSONUtil.parseObj(queryResult);
String tradeState = resultJson.getStr("trade_state");
log.info("[支付补偿] 微信查单结果: orderId={}, outTradeNumber={}, localStatus={}, tradeState={}, prepayId={}, createTime={}",
order.getId(), order.getOutTradeNumber(), order.getOrderStatus(), tradeState, order.getPrepayId(), order.getCreateTime());
if ("SUCCESS".equalsIgnoreCase(tradeState)) {
boolean success = compensateSingleOrder(order, resultJson);
if (success) {
log.info("[支付补偿] 补偿成功: orderId={}, outTradeNumber={}", order.getId(), order.getOutTradeNumber());
} else {
log.warn("[支付补偿] 微信已支付但本地补偿失败: orderId={}, outTradeNumber={}",
order.getId(), order.getOutTradeNumber());
}
return success;
}
if ("CLOSED".equalsIgnoreCase(tradeState)) {
syncLocalClosedOrder(order.getId(), "微信侧已关闭");
}
return false;
} catch (Exception e) {
log.error("[支付补偿] 处理订单异常: orderId={}, outTradeNumber={}, error={}",
order.getId(), order.getOutTradeNumber(), e.getMessage(), e);
return false;
}
}
private void syncLocalClosedOrder(Long orderId, String description) {
payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, orderId)
.ne(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS)
.set(PayOrder::getOrderStatus, PayOrderStatus.CLOSED)
.set(PayOrder::getTradeState, "CLOSED")
.set(PayOrder::getTradeStateDescription, description)
);
log.info("[支付补偿] 订单微信侧已关闭,同步关闭: orderId={}", orderId);
}
/**
* 对单笔订单执行支付补偿
*
* @param order 本地订单
* @param wxResult 微信查询结果
* @return 是否补偿成功
*/
private boolean compensateSingleOrder(PayOrder order, JSONObject wxResult) {
String transactionId = wxResult.getStr("transaction_id");
String tradeState = wxResult.getStr("trade_state");
String tradeStateDesc = wxResult.getStr("trade_state_desc");
String successTime = wxResult.getStr("success_time");
String bankType = wxResult.getStr("bank_type");
JSONObject amountObj = wxResult.getJSONObject("amount");
Integer payerTotal = amountObj != null ? amountObj.getInt("payer_total") : null;
String payerCurrency = amountObj != null ? amountObj.getStr("payer_currency") : "CNY";
if (payerTotal == null) {
log.error("[支付补偿] 微信返回金额为空: orderId={}", order.getId());
return false;
}
return handlePaymentSuccess(
order.getOutTradeNumber(),
transactionId,
payerTotal,
payerCurrency,
successTime,
tradeState,
tradeStateDesc,
bankType
);
}
@Override
public boolean confirmPaidOrder(String outTradeNumber) {
if (outTradeNumber == null || outTradeNumber.isEmpty()) {
return false;
}
PayOrder order = queryByOutTradeNumber(outTradeNumber);
if (order == null) {
log.warn("[支付确认] 本地订单不存在: outTradeNumber={}", outTradeNumber);
return false;
}
if (PayOrderStatus.SUCCESS.equals(order.getOrderStatus())) {
int repairCount = repairOrderDeviceDeadTime(order);
log.info("[支付确认] 订单已成功: orderId={}, outTradeNumber={}, repairDeviceCount={}",
order.getId(), outTradeNumber, repairCount);
return true;
}
String queryResult = wxPayService.queryOrderByOutTradeNo(outTradeNumber);
if (queryResult == null) {
log.warn("[支付确认] 查询微信订单失败: orderId={}, outTradeNumber={}", order.getId(), outTradeNumber);
return false;
}
JSONObject resultJson = JSONUtil.parseObj(queryResult);
String tradeState = resultJson.getStr("trade_state");
if ("SUCCESS".equalsIgnoreCase(tradeState)) {
return compensateSingleOrder(order, resultJson);
}
if ("CLOSED".equalsIgnoreCase(tradeState)) {
payOrderMapper.update(null,
new LambdaUpdateWrapper<PayOrder>()
.eq(PayOrder::getId, order.getId())
.ne(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS)
.set(PayOrder::getOrderStatus, PayOrderStatus.CLOSED)
.set(PayOrder::getTradeState, "CLOSED")
.set(PayOrder::getTradeStateDescription, "微信侧已关闭")
);
}
log.info("[支付确认] 订单未支付成功: orderId={}, outTradeNumber={}, tradeState={}",
order.getId(), outTradeNumber, tradeState);
return false;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int repairPaidOrderDeviceDeadTime() {
Map<Long, PayOrder> paidOrderMap = new LinkedHashMap<>();
scanRecentPaidOrdersForRepair().forEach(order -> paidOrderMap.put(order.getId(), order));
scanHistoryPaidOrdersForRepair().forEach(order -> paidOrderMap.putIfAbsent(order.getId(), order));
if (paidOrderMap.isEmpty()) {
return 0;
}
int repairCount = 0;
for (PayOrder order : paidOrderMap.values()) {
repairCount += repairOrderDeviceDeadTime(order);
}
if (repairCount > 0) {
log.warn("[支付有效期修复] 本次修复 {} 台设备", repairCount);
} else {
log.info("[支付有效期修复] 本次扫描 {} 笔支付成功订单,无需修复", paidOrderMap.size());
}
return repairCount;
}
private List<PayOrder> scanRecentPaidOrdersForRepair() {
Page<PayOrder> page = payOrderMapper.selectPage(
new Page<>(1, RECENT_SCAN_SIZE, false),
buildPaidOrderRepairQuery()
.orderByDesc(PayOrder::getCreateTime)
.orderByDesc(PayOrder::getId)
);
return page.getRecords() != null ? page.getRecords() : Collections.emptyList();
}
private List<PayOrder> scanHistoryPaidOrdersForRepair() {
long cursorId = REPAIR_HISTORY_CURSOR_ID.get();
LambdaQueryWrapper<PayOrder> queryWrapper = buildPaidOrderRepairQuery();
if (cursorId > 0) {
queryWrapper.gt(PayOrder::getId, cursorId);
}
Page<PayOrder> page = payOrderMapper.selectPage(
new Page<>(1, HISTORY_SCAN_SIZE, false),
queryWrapper.orderByAsc(PayOrder::getId)
);
List<PayOrder> records = page.getRecords();
if (records == null || records.isEmpty()) {
if (cursorId > 0) {
REPAIR_HISTORY_CURSOR_ID.set(0L);
log.info("[支付有效期修复] 历史扫描游标已到末尾,下次从最早成功订单重新扫描");
}
return Collections.emptyList();
}
REPAIR_HISTORY_CURSOR_ID.set(records.get(records.size() - 1).getId());
return records;
}
private LambdaQueryWrapper<PayOrder> buildPaidOrderRepairQuery() {
return new LambdaQueryWrapper<PayOrder>()
.eq(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS);
}
private int repairOrderDeviceDeadTime(PayOrder order) {
List<PayDevice> payDevices = queryPayDevicesByOrderId(order.getId());
if (payDevices == null || payDevices.isEmpty()) {
log.warn("[支付有效期修复] 支付成功订单没有设备充值记录: orderId={}", order.getId());
return 0;
}
int repairCount = 0;
for (PayDevice payDevice : payDevices) {
try {
if (payDevice.getDeadTime() == null) {
log.warn("[支付有效期修复] 设备充值记录没有目标到期时间: payDeviceId={}, orderId={}",
payDevice.getId(), order.getId());
continue;
}
Device device = queryDeviceByPayDevice(payDevice);
if (device == null) {
log.warn("[支付有效期修复] 设备不存在: orderId={}, serialNum={}",
order.getId(), payDevice.getSerialNum());
continue;
}
if (device.getDeadTime() != null && !device.getDeadTime().before(payDevice.getDeadTime())) {
continue;
}
Integer oldWarnCode = device.getWarnCode();
Integer newWarnCode = oldWarnCode != null
? (oldWarnCode & ~DefineDeviceWarnCode.DeviceTimeDead)
: 0;
int updateRows = TenantHelper.ignore(() -> deviceMapper.update(null,
new LambdaUpdateWrapper<Device>()
.eq(Device::getId, device.getId())
.set(Device::getDeadTime, payDevice.getDeadTime())
.set(Device::getWarnCode, newWarnCode)
));
if (updateRows > 0) {
repairCount++;
log.warn("[支付有效期修复] 已修复设备有效期: orderId={}, deviceId={}, serialNum={}, oldDeadTime={}, targetDeadTime={}",
order.getId(), device.getId(), device.getSerialNum(), device.getDeadTime(), payDevice.getDeadTime());
}
} catch (Exception e) {
log.error("[支付有效期修复] 修复单台设备异常: orderId={}, payDeviceId={}, serialNum={}, error={}",
order.getId(), payDevice.getId(), payDevice.getSerialNum(), e.getMessage(), e);
}
}
return repairCount;
}
} }

View File

@@ -36,7 +36,6 @@ public class WxPayServiceImpl implements WxPayService {
private static final String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"; private static final String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
private static final String CLOSE_URL_TEMPLATE = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close"; private static final String CLOSE_URL_TEMPLATE = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close";
private static final String QUERY_ORDER_URL_TEMPLATE = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s?mchid=%s";
private final WxPayProperties wxPayProperties; private final WxPayProperties wxPayProperties;
private final WxMaProperties wxMaProperties; private final WxMaProperties wxMaProperties;
@@ -127,31 +126,6 @@ public class WxPayServiceImpl implements WxPayService {
return response.body(); return response.body();
} }
// ----------------------------------------------------------------
// 发送 HTTP GET
// ----------------------------------------------------------------
private String doGet(String url) throws Exception {
URI uri = URI.create(url);
String urlPath = uri.getPath();
if (uri.getQuery() != null) {
urlPath = urlPath + "?" + uri.getQuery();
}
String authorization = buildAuthorization("GET", urlPath, "");
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.header("Accept", "application/json")
.header("Authorization", authorization)
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
log.info("微信支付查询响应: status={}, body={}", response.statusCode(), response.body());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new RuntimeException("微信支付查询失败: " + response.statusCode() + " " + response.body());
}
return response.body();
}
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// 发送 HTTP POST无响应体如关闭订单 // 发送 HTTP POST无响应体如关闭订单
// ---------------------------------------------------------------- // ----------------------------------------------------------------
@@ -181,7 +155,7 @@ public class WxPayServiceImpl implements WxPayService {
@Override @Override
public String createJsapiOrder(String openId, String outTradeNumber, Integer totalAmount, public String createJsapiOrder(String openId, String outTradeNumber, Integer totalAmount,
String description, String notifyUrl) { String description, String notifyUrl) {
log.info("创建JSAPI支付订单: openId={}, outTradeNumber={}, amount={}, notifyUrl={}", openId, outTradeNumber, totalAmount, notifyUrl); log.info("创建JSAPI支付订单: openId={}, outTradeNumber={}, amount={}", openId, outTradeNumber, totalAmount);
try { try {
String appId = wxMaProperties.getAppId(); String appId = wxMaProperties.getAppId();
String mchId = wxPayProperties.getMchId(); String mchId = wxPayProperties.getMchId();
@@ -309,16 +283,4 @@ public class WxPayServiceImpl implements WxPayService {
if (value == null) return ""; if (value == null) return "";
return value.replace("\\", "\\\\").replace("\"", "\\\""); return value.replace("\\", "\\\\").replace("\"", "\\\"");
} }
@Override
public String queryOrderByOutTradeNo(String outTradeNumber) {
log.info("主动查询订单状态: outTradeNumber={}", outTradeNumber);
try {
String url = String.format(QUERY_ORDER_URL_TEMPLATE, outTradeNumber, wxPayProperties.getMchId());
return doGet(url);
} catch (Exception e) {
log.error("查询订单状态失败: outTradeNumber={}, error={}", outTradeNumber, e.getMessage());
return null;
}
}
} }

View File

@@ -1,59 +0,0 @@
package com.intc.weixin.task;
import com.intc.weixin.service.PayOrderBusinessService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 支付订单补偿定时任务
* 定期扫描状态为"未支付/支付中/支付失败/已关闭"且有商户订单号的订单,
* 主动向微信支付查询实际状态,对已支付成功但回调丢失的订单进行补偿处理。
*
* 解决问题:用户实际已付款成功,但因网络/服务器等原因未收到微信支付回调通知,
* 导致订单一直停留在未支付状态。
*
* @author intc
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnBean(PayOrderBusinessService.class)
public class PayOrderCompensationTask {
private final PayOrderBusinessService payOrderBusinessService;
/**
* 每1分钟执行一次订单补偿扫描
* 优先扫描最近订单,同时通过历史游标持续补扫老异常订单。
*/
// @Scheduled(fixedDelay = 60000, initialDelay = 60000)
public void compensateOrders() {
try {
int count = payOrderBusinessService.compensateUnpaidOrders();
if (count > 0) {
log.info("[支付补偿定时任务] 本次补偿了 {} 笔订单", count);
}
} catch (Exception e) {
log.error("[支付补偿定时任务] 执行异常: {}", e.getMessage(), e);
}
}
/**
* 每10分钟修复一次已支付订单的设备有效期。
* 优先扫描最近成功订单,同时通过历史游标持续补扫老成功订单。
*/
// @Scheduled(fixedDelay = 600000, initialDelay = 120000)
public void repairDeviceDeadTime() {
try {
int count = payOrderBusinessService.repairPaidOrderDeviceDeadTime();
if (count > 0) {
log.warn("[支付有效期修复任务] 本次修复了 {} 台设备", count);
}
} catch (Exception e) {
log.error("[支付有效期修复任务] 执行异常: {}", e.getMessage(), e);
}
}
}