Compare commits
37 Commits
prod
...
a12cc4c905
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a12cc4c905 | ||
|
|
b36a92c981 | ||
|
|
303cd01a59 | ||
|
|
467c05265f | ||
|
|
65bcbf470c | ||
|
|
7f1d1381ef | ||
|
|
2bd71701a8 | ||
|
|
6b7b330934 | ||
|
|
5c13c88c41 | ||
|
|
24a1e3e348 | ||
|
|
521c1ca344 | ||
|
|
b58f9c3b42 | ||
|
|
07495f9270 | ||
|
|
9eb2ec90c6 | ||
|
|
45c55944a5 | ||
|
|
2877fc9b7c | ||
|
|
8de7460d92 | ||
|
|
e1745c6130 | ||
|
|
9b5f87342b | ||
|
|
cddaa2c63c | ||
|
|
222f74a7e1 | ||
|
|
261486d235 | ||
|
|
11337ee89e | ||
|
|
74ec9caba8 | ||
|
|
4fc7eb8006 | ||
|
|
881ab2b4a6 | ||
|
|
2673eba9ce | ||
|
|
38c038a950 | ||
|
|
eeb9ca774e | ||
|
|
a50f9ee4c9 | ||
|
|
db7e0538aa | ||
|
|
b5c4bab4d4 | ||
|
|
83834ac4da | ||
|
|
20cb24b0a1 | ||
|
|
4d82c464ee | ||
|
|
f67ae05413 | ||
|
|
9cdcd312cb |
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 "设备断电";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
.map(device -> {
|
||||||
PublicDeviceDeadInfo vo = new PublicDeviceDeadInfo();
|
PublicDeviceDeadInfo vo = new PublicDeviceDeadInfo();
|
||||||
vo.setId(device.getId());
|
vo.setId(device.getId());
|
||||||
vo.setDeviceName(device.getDeviceName());
|
vo.setDeviceName(device.getDeviceName());
|
||||||
vo.setSerialNum(device.getSerialNum());
|
vo.setSerialNum(device.getSerialNum());
|
||||||
vo.setDeadTime(device.getDeadTime());
|
vo.setDeadTime(device.getDeadTime());
|
||||||
vo.setStatus(1);
|
return vo;
|
||||||
list.add(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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置测控一体机溶解氧启用/禁用
|
* 设置测控一体机溶解氧启用/禁用
|
||||||
* 启用时:同时启用溶解氧和温度告警及免打扰
|
* 启用时:同时启用溶解氧和温度告警及免打扰
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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个");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建定时控制记录
|
// 创建定时控制记录
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手机号
|
* 手机号
|
||||||
*/
|
*/
|
||||||
@@ -342,11 +330,6 @@ public class DeviceVo implements Serializable {
|
|||||||
@ExcelProperty(value = "塘口名称")
|
@ExcelProperty(value = "塘口名称")
|
||||||
private String pondName;
|
private String pondName;
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备在线状态(online/offline)
|
|
||||||
*/
|
|
||||||
private String onlineStatus;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否过期(0-未过期,1-已过期)
|
* 是否过期(0-未过期,1-已过期)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -109,11 +109,6 @@ public class PublicDeviceBaseData implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private Integer voltageWarnOpen;
|
private Integer voltageWarnOpen;
|
||||||
|
|
||||||
/**
|
|
||||||
* 控制器故障电话报警开关(断电不受此开关限制)
|
|
||||||
*/
|
|
||||||
private Integer controllerFaultCallOpen;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 低电量电话告警开关(太阳能网控专用)
|
* 低电量电话告警开关(太阳能网控专用)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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离线等待标记)
|
// 10. 最后删除设备
|
||||||
try {
|
return baseMapper.deleteByIds(ids) > 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 彻底删除设备及其所有关联数据(不重建设备记录)
|
|
||||||
* 与 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算设备列表的过期信息
|
* 计算设备列表的过期信息
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
// 处理额外的查询参数
|
// 处理额外的查询参数
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
|
||||||
TenantHelper.ignore(() -> {
|
|
||||||
deviceStatusMapper.insert(newStatus);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签名类型
|
* 签名类型
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,11 +244,9 @@ 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())
|
||||||
.ne(PayOrder::getOrderStatus, PayOrderStatus.SUCCESS)
|
|
||||||
.set(PayOrder::getTransactionId, transactionId)
|
.set(PayOrder::getTransactionId, transactionId)
|
||||||
.set(PayOrder::getPayerTotal, payerTotal)
|
.set(PayOrder::getPayerTotal, payerTotal)
|
||||||
.set(PayOrder::getPayerCurrency, payerCurrency)
|
.set(PayOrder::getPayerCurrency, payerCurrency)
|
||||||
@@ -313,40 +257,9 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
|||||||
.set(PayOrder::getOrderStatus, orderStatus)
|
.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,40 +328,32 @@ 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());
|
new LambdaQueryWrapper<Device>()
|
||||||
continue;
|
.eq(Device::getSerialNum, payDevice.getSerialNum())
|
||||||
}
|
)
|
||||||
|
);
|
||||||
|
|
||||||
Device device = queryDeviceByPayDevice(payDevice);
|
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
log.error("设备不存在,订单成功状态已保留,等待人工核对设备编号: orderId={}, payDeviceId={}, serialNum={}",
|
log.warn("设备不存在: serialNum={}", payDevice.getSerialNum());
|
||||||
orderId, payDevice.getId(), payDevice.getSerialNum());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算新的到期时间
|
// 计算新的到期时间
|
||||||
Date newDeadTime = calculateNewDeadTime(device.getDeadTime(), payDevice.getAddMonth());
|
Date newDeadTime = calculateNewDeadTime(device.getDeadTime(), payDevice.getAddMonth());
|
||||||
|
|
||||||
payDeviceMapper.update(null,
|
|
||||||
new LambdaUpdateWrapper<PayDevice>()
|
|
||||||
.eq(PayDevice::getId, payDevice.getId())
|
|
||||||
.set(PayDevice::getDeadTime, newDeadTime)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 清除 warnCode 中的「设备到期」标志位(续费后恢复正常状态)
|
// 清除 warnCode 中的「设备到期」标志位(续费后恢复正常状态)
|
||||||
Integer oldWarnCode = device.getWarnCode();
|
Integer oldWarnCode = device.getWarnCode();
|
||||||
Integer newWarnCode = oldWarnCode != null
|
Integer newWarnCode = oldWarnCode != null
|
||||||
@@ -456,61 +361,17 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
|||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// 更新设备到期时间,并同步清除到期告警位
|
// 更新设备到期时间,并同步清除到期告警位
|
||||||
int updateRows = TenantHelper.ignore(() -> deviceMapper.update(null,
|
deviceMapper.update(null,
|
||||||
new LambdaUpdateWrapper<Device>()
|
new LambdaUpdateWrapper<Device>()
|
||||||
.eq(Device::getId, device.getId())
|
.eq(Device::getId, device.getId())
|
||||||
.set(Device::getDeadTime, newDeadTime)
|
.set(Device::getDeadTime, newDeadTime)
|
||||||
.set(Device::getWarnCode, newWarnCode)
|
.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={}",
|
log.info("续费成功,更新设备到期时间并清除到期告警: deviceId={}, serialNum={}, newDeadTime={}, oldWarnCode={}, newWarnCode={}",
|
||||||
device.getId(), device.getSerialNum(), 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>()
|
|
||||||
.eq(Device::getSerialNum, payDevice.getSerialNum())
|
|
||||||
.eq(Device::getTenantId, payDevice.getTenantId())
|
|
||||||
.orderByDesc(Device::getUpdateTime)
|
|
||||||
.orderByDesc(Device::getCreateTime)
|
|
||||||
);
|
|
||||||
if (sameTenantDevices != null && !sameTenantDevices.isEmpty()) {
|
|
||||||
return sameTenantDevices.get(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Device> devices = deviceMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<Device>()
|
|
||||||
.eq(Device::getSerialNum, payDevice.getSerialNum())
|
|
||||||
.orderByDesc(Device::getUpdateTime)
|
|
||||||
.orderByDesc(Device::getCreateTime)
|
|
||||||
);
|
|
||||||
if (devices == null || devices.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user