Compare commits
35 Commits
prod
...
303cd01a59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -50,8 +50,11 @@ public class EmailAuthStrategy implements IAuthStrategy {
|
||||
String tenantId = loginBody.getTenantId();
|
||||
String email = loginBody.getEmail();
|
||||
String emailCode = loginBody.getEmailCode();
|
||||
// 用于保存用户信息
|
||||
SysUserVo[] userHolder = new SysUserVo[1];
|
||||
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
|
||||
SysUserVo user = loadUserByEmail(email);
|
||||
userHolder[0] = user;
|
||||
loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
return loginService.buildLoginUser(user);
|
||||
@@ -72,6 +75,10 @@ public class EmailAuthStrategy implements IAuthStrategy {
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
loginVo.setUserId(loginUser.getUserId());
|
||||
loginVo.setUserName(loginUser.getUsername());
|
||||
loginVo.setNickName(loginUser.getNickname());
|
||||
loginVo.setPhonenumber(userHolder[0].getPhonenumber());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,11 @@ public class PasswordAuthStrategy implements IAuthStrategy {
|
||||
if (captchaEnabled) {
|
||||
validateCaptcha(tenantId, username, code, uuid);
|
||||
}
|
||||
// 用于保存用户信息(包括手机号)
|
||||
SysUserVo[] userHolder = new SysUserVo[1];
|
||||
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
|
||||
SysUserVo user = loadUserByUsername(username);
|
||||
userHolder[0] = user; // 保存用户信息
|
||||
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
return loginService.buildLoginUser(user);
|
||||
@@ -84,6 +87,10 @@ public class PasswordAuthStrategy implements IAuthStrategy {
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
loginVo.setUserId(loginUser.getUserId());
|
||||
loginVo.setUserName(loginUser.getUsername());
|
||||
loginVo.setNickName(loginUser.getNickname());
|
||||
loginVo.setPhonenumber(userHolder[0].getPhonenumber());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,12 +71,16 @@ public class SmsAuthStrategy implements IAuthStrategy {
|
||||
});
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
|
||||
// 短信登录用户:token 有效期1年,允许多设备同时在线(与微信小程序一致)
|
||||
// - timeout: 1年 = 31536000秒
|
||||
// - activeTimeout: -1 表示永不因不活跃而过期
|
||||
// - isConcurrent: true 允许多设备同时在线
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setTimeout(31536000L); // 1年 = 365天 * 24小时 * 60分钟 * 60秒
|
||||
model.setActiveTimeout(-1); // 永不因不活跃过期
|
||||
model.setIsConcurrent(true); // 允许多设备同时在线
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
@@ -80,8 +80,11 @@ public class SocialAuthStrategy implements IAuthStrategy {
|
||||
} else {
|
||||
social = list.get(0);
|
||||
}
|
||||
// 用于保存用户信息
|
||||
SysUserVo[] userHolder = new SysUserVo[1];
|
||||
LoginUser loginUser = TenantHelper.dynamic(social.getTenantId(), () -> {
|
||||
SysUserVo user = loadUser(social.getUserId());
|
||||
userHolder[0] = user;
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
return loginService.buildLoginUser(user);
|
||||
});
|
||||
@@ -101,6 +104,10 @@ public class SocialAuthStrategy implements IAuthStrategy {
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
loginVo.setUserId(loginUser.getUserId());
|
||||
loginVo.setUserName(loginUser.getUsername());
|
||||
loginVo.setNickName(loginUser.getNickname());
|
||||
loginVo.setPhonenumber(userHolder[0].getPhonenumber());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
|
||||
@@ -93,12 +93,24 @@ public class WechatAuthStrategy implements IAuthStrategy {
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
|
||||
// 4. 配置登录参数
|
||||
// 微信小程序用户:token 有效期1年,允许多设备同时在线
|
||||
// - timeout: 1年 = 31536000秒
|
||||
// - activeTimeout: -1 表示永不因不活跃而过期(用户1年内随时可用)
|
||||
// - isConcurrent: true 允许多设备同时在线
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
// 微信小程序:token有效期1年,永不因不活跃过期
|
||||
model.setTimeout(31536000L); // 1年 = 365天 * 24小时 * 60分钟 * 60秒
|
||||
model.setActiveTimeout(-1); // 永不因不活跃过期
|
||||
// isConcurrent: 是否允许同一账号多地同时登录
|
||||
// true = 允许多设备同时在线(手机+平板)
|
||||
// false = 同一设备类型只能一个在线,新登录踢掉旧的(换手机场景)
|
||||
model.setIsConcurrent(true); // 允许多设备同时在线
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
|
||||
log.info("微信登录token配置: clientId={}, timeout=1年, activeTimeout=永不过期, isConcurrent={}",
|
||||
client.getClientId(), true);
|
||||
|
||||
// 5. 执行登录,生成 token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
@@ -124,15 +136,58 @@ public class WechatAuthStrategy implements IAuthStrategy {
|
||||
* @return 系统用户信息
|
||||
*/
|
||||
private SysUserVo loadUserByPhone(String phone, AquUser aquUser) {
|
||||
// 1. 优先按 AquUser ID 查询 SysUser,保证 ID 一致性
|
||||
SysUserVo user = userMapper.selectVoOne(
|
||||
new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getPhonenumber, phone)
|
||||
.eq(SysUser::getUserId, aquUser.getId())
|
||||
);
|
||||
|
||||
// 如果用户不存在,创建新用户
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("系统中不存在该手机号的用户,自动创建新用户: phone={}", phone);
|
||||
user = createNewUser(phone, aquUser);
|
||||
if (user != null) {
|
||||
log.info("通过AquUser ID找到系统用户: aquUserId={}, sysUserId={}, phone={}",
|
||||
aquUser.getId(), user.getUserId(), phone);
|
||||
// 检查手机号是否一致
|
||||
if (!phone.equals(user.getPhonenumber())) {
|
||||
log.warn("系统用户手机号与AquUser不一致,更新手机号: sysUserId={}, oldPhone={}, newPhone={}",
|
||||
user.getUserId(), user.getPhonenumber(), phone);
|
||||
// 更新手机号
|
||||
SysUser updateUser = new SysUser();
|
||||
updateUser.setUserId(user.getUserId());
|
||||
updateUser.setPhonenumber(phone);
|
||||
updateUser.setUserName(phone); // 用户名也更新为手机号
|
||||
userMapper.updateById(updateUser);
|
||||
// 重新查询获取更新后的数据
|
||||
user = userMapper.selectVoOne(
|
||||
new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getUserId, aquUser.getId())
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 2. 按手机号查询 SysUser
|
||||
user = userMapper.selectVoOne(
|
||||
new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getPhonenumber, phone)
|
||||
);
|
||||
|
||||
if (user != null) {
|
||||
// 手机号匹配但 ID 不一致,说明是历史数据,需要更新 ID 以保持一致
|
||||
log.info("通过手机号找到系统用户,但ID不一致: sysUserId={}, aquUserId={}, phone={}",
|
||||
user.getUserId(), aquUser.getId(), phone);
|
||||
log.warn("将更新系统用户ID以与AquUser保持一致: oldSysUserId={}, newSysUserId={}",
|
||||
user.getUserId(), aquUser.getId());
|
||||
|
||||
// 更新 SysUser 的 ID 以与 AquUser 一致
|
||||
// 注意:需要先删除旧记录,再插入新记录(因为 ID 是主键)
|
||||
Long oldUserId = user.getUserId();
|
||||
userMapper.deleteById(oldUserId);
|
||||
|
||||
// 创建新的 SysUser 记录,使用 AquUser 的 ID
|
||||
user = createNewUser(phone, aquUser);
|
||||
log.info("已将系统用户ID从 {} 更新为 {},与AquUser保持一致", oldUserId, aquUser.getId());
|
||||
} else {
|
||||
// 3. 用户不存在,创建新用户
|
||||
log.info("系统中不存在该手机号的用户,自动创建新用户: phone={}, aquUserId={}", phone, aquUser.getId());
|
||||
user = createNewUser(phone, aquUser);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
|
||||
@@ -51,16 +51,16 @@ spring:
|
||||
# username: root
|
||||
# password: root
|
||||
driverClassName: org.postgresql.Driver
|
||||
url: jdbc:postgresql://81.70.89.108:15432/fishery_dev?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
|
||||
password: intc@123987
|
||||
password: htYkSuRn7Gb*pbFn
|
||||
# 从库数据源 - TDengine
|
||||
taos:
|
||||
lazy: false
|
||||
type: ${spring.datasource.type}
|
||||
driverClassName: com.taosdata.jdbc.rs.RestfulDriver
|
||||
# 不指定数据库名,在 SQL 中使用完整路径 fishery.table_name
|
||||
url: jdbc:TAOS-RS://81.70.89.108:6041?timezone=Shanghai&charset=UTF-8&locale=en_US.UTF-8
|
||||
url: jdbc:TAOS-RS://172.16.42.181:6041?timezone=Shanghai&charset=UTF-8&locale=en_US.UTF-8
|
||||
username: root
|
||||
password: intc@123456
|
||||
hikari:
|
||||
@@ -106,11 +106,11 @@ spring:
|
||||
spring.data:
|
||||
redis:
|
||||
# 地址
|
||||
host: 81.70.89.108
|
||||
host: 172.16.42.181
|
||||
# 端口,默认为6379
|
||||
port: 26379
|
||||
# 数据库索引
|
||||
database: 9
|
||||
database: 6
|
||||
# redis 密码必须配置
|
||||
password: bbd4b56e5d3f
|
||||
# 连接超时时间
|
||||
@@ -245,10 +245,14 @@ aliyun:
|
||||
call-success-suppress-hours: 2
|
||||
# 设备离线告警延迟触发时间(分钟),设备离线超过此时间后才发送告警
|
||||
offline-warn-delay-minutes: 6
|
||||
# 太阳能网控离线告警延迟触发时间(分钟),工作1分钟休眠同分钟为正常行为,需要更长延迟
|
||||
solar-offline-warn-delay-minutes: 30
|
||||
# 是否启用报警电话通知(全局开关,关闭后所有类型的报警都不会触发电话通知)
|
||||
call-notice-enabled: false
|
||||
# AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret)
|
||||
amqp:
|
||||
# 是否启用
|
||||
enabled: true
|
||||
enabled: false
|
||||
# 数据同步 AppKey(与上面的 app-key 不同!)
|
||||
data-sync-app-key: 334224409
|
||||
# 数据同步 AppSecret(请在阿里云控制台查看)
|
||||
@@ -418,6 +422,6 @@ wx:
|
||||
# 支付回调通知配置
|
||||
pay-notify:
|
||||
# 支付回调通知URL(需根据实际域名配置)
|
||||
notify-url: "https://api.yuceyun.cn/fishery-api/weixin/pay_notify"
|
||||
notify-url: "https://api.yuceyun.cn/test-api/weixin/pay_notify"
|
||||
# 微信商户号(与上面wx.pay.mch-id保持一致)
|
||||
mch-id: "1671289865"
|
||||
|
||||
@@ -121,6 +121,7 @@ security:
|
||||
- /iot/test
|
||||
- /iot/device/**
|
||||
- /iot/amqp/**
|
||||
- /weixin/pay_notify
|
||||
|
||||
# 多租户配置
|
||||
tenant:
|
||||
|
||||
@@ -136,18 +136,18 @@ public class AquUserController extends BaseController {
|
||||
if (userId == null || userId < 0) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
|
||||
|
||||
// 查询用户的报警电话 JSON字段
|
||||
AquUserVo user = aquUserService.queryById(userId);
|
||||
if (user == null) {
|
||||
return R.fail("用户不存在");
|
||||
}
|
||||
|
||||
|
||||
String warnPhoneJson = user.getWarnPhoneJson();
|
||||
if (StringUtils.isEmpty(warnPhoneJson)) {
|
||||
return R.fail("不存在报警电话列表");
|
||||
}
|
||||
|
||||
|
||||
// 反序列化JSON为List<String>
|
||||
try {
|
||||
List<String> warnPhones = JsonUtils.parseArray(warnPhoneJson, String.class);
|
||||
@@ -156,7 +156,7 @@ public class AquUserController extends BaseController {
|
||||
return R.fail("报警电话数据格式错误");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取子用户列表
|
||||
*
|
||||
@@ -169,10 +169,10 @@ public class AquUserController extends BaseController {
|
||||
if (userId == null || userId < 0) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
|
||||
|
||||
// 查询该用户的所有子账号
|
||||
List<UserChildVo> list = userRelationMapper.selectChildUsersByParentId(userId);
|
||||
|
||||
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@@ -188,10 +188,10 @@ public class AquUserController extends BaseController {
|
||||
if (userId == null || userId < 0) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
|
||||
|
||||
// 查询该用户的所有父账号
|
||||
List<UserChildVo> list = userRelationMapper.selectParentUsersByChildId(userId);
|
||||
|
||||
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@@ -215,11 +215,11 @@ public class AquUserController extends BaseController {
|
||||
.eq(AquUser::getMobilePhone, request.getMobilePhone())
|
||||
.select(AquUser::getId)
|
||||
);
|
||||
|
||||
|
||||
if (childUser == null) {
|
||||
return R.fail("该手机号用户不存在");
|
||||
return R.fail("该手机号还未注册小程序,请先注册");
|
||||
}
|
||||
|
||||
|
||||
Long childUserId = childUser.getId();
|
||||
|
||||
// 检查是否已存在该子账号关系
|
||||
@@ -228,7 +228,7 @@ public class AquUserController extends BaseController {
|
||||
.eq(UserRelation::getParentUserId, userId)
|
||||
.eq(UserRelation::getChildUserId, childUserId)
|
||||
);
|
||||
|
||||
|
||||
if (count > 0) {
|
||||
return R.fail("子账号已添加,无需重复添加");
|
||||
}
|
||||
|
||||
@@ -317,6 +317,10 @@ public class DeviceController extends BaseController {
|
||||
data.setSalinityCompensation(device.getSalinityCompensation());
|
||||
data.setInputVoltage(device.getInputVoltage());
|
||||
data.setVoltageWarnOpen(device.getVoltageWarnOpen());
|
||||
data.setBatteryWarnCallOpen(device.getBatteryWarnCallOpen());
|
||||
data.setBatteryWarnCallNoDis(device.getBatteryWarnCallNoDis());
|
||||
data.setBatteryWarnLower(device.getBatteryWarnLower());
|
||||
|
||||
|
||||
// 设置塘口信息
|
||||
if (device.getPondId() != null && device.getPondId() > 0) {
|
||||
|
||||
@@ -186,7 +186,7 @@ public class PondController extends BaseController {
|
||||
/**
|
||||
* 根据塘口ID查询设备列表
|
||||
* 返回数据按设备类型分类:
|
||||
* - listDetector: 水质检测仪列表(deviceType=1)+ 开启溶氧检测的测控一体机(deviceType=2 && isOxygenUsed=1)
|
||||
* - listDetector: 水质检测仪列表(deviceType=1)+ 开启溢氧检测的测控一体机(deviceType=2 && isOxygenUsed=1)+ 太阳能网控(deviceType=3)
|
||||
* - listController: 测控一体机列表(deviceType=2),包含开关列表
|
||||
*
|
||||
* @param pondId 塘口ID
|
||||
@@ -217,7 +217,7 @@ public class PondController extends BaseController {
|
||||
new LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getPondId, pondId)
|
||||
.orderByAsc(Device::getDeviceType)
|
||||
.orderByDesc(Device::getCreateTime)
|
||||
.orderByAsc(Device::getCreateTime)
|
||||
);
|
||||
|
||||
// 3. 查询塘口下的所有开关(switch.pondId == pondId),包含开关未挂塘口的控制器也能被找到
|
||||
@@ -294,10 +294,11 @@ public class PondController extends BaseController {
|
||||
// 9. 处理探测器列表(严格从 pondDevices 取,与 C# pond.ListDevice 保持一致,不包含仅开关挂塘口的设备)
|
||||
List<DeviceVo> detectorList = new ArrayList<>();
|
||||
for (Device device : pondDevices) {
|
||||
// 水质检测仪 或 开启溶氧检测的测控一体机
|
||||
// 水质检测仪 或 开启溢氧检测的测控一体机 或 太阳能网控
|
||||
if ((device.getDeviceType() != null && device.getDeviceType() == 1)
|
||||
|| (device.getDeviceType() != null && device.getDeviceType() == 2
|
||||
&& device.getIsOxygenUsed() != null && device.getIsOxygenUsed() == 1)) {
|
||||
&& device.getIsOxygenUsed() != null && device.getIsOxygenUsed() == 1)
|
||||
|| (device.getDeviceType() != null && device.getDeviceType() == 3)) {
|
||||
|
||||
DeviceVo deviceVo = MapstructUtils.convert(device, DeviceVo.class);
|
||||
|
||||
@@ -601,12 +602,12 @@ public class PondController extends BaseController {
|
||||
com.intc.fishery.domain.bo.MessageWarnBo query = new com.intc.fishery.domain.bo.MessageWarnBo();
|
||||
query.setDeviceId(deviceId);
|
||||
List<com.intc.fishery.domain.vo.MessageWarnVo> messageWarns = messageWarnService.queryList(query);
|
||||
|
||||
|
||||
if (!messageWarns.isEmpty()) {
|
||||
List<Long> messageWarnIds = messageWarns.stream()
|
||||
.map(com.intc.fishery.domain.vo.MessageWarnVo::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
// 删除告警消息
|
||||
messageWarnService.deleteWithValidByIds(messageWarnIds, false);
|
||||
log.info("清除设备 {} 的 {} 条告警消息", deviceId, messageWarnIds.size());
|
||||
@@ -719,7 +720,7 @@ public class PondController extends BaseController {
|
||||
List<Pond> ponds = pondMapper.selectList(
|
||||
new LambdaQueryWrapper<Pond>()
|
||||
.eq(Pond::getUserId, rootUserId)
|
||||
.orderByDesc(Pond::getCreateTime)
|
||||
.orderByAsc(Pond::getCreateTime)
|
||||
);
|
||||
|
||||
List<PublicPondMode1Vo> listData = new ArrayList<>();
|
||||
@@ -946,7 +947,7 @@ public class PondController extends BaseController {
|
||||
new LambdaQueryWrapper<Pond>()
|
||||
.eq(Pond::getUserId, rootUserId)
|
||||
.select(Pond::getId, Pond::getPondName)
|
||||
.orderByDesc(Pond::getCreateTime)
|
||||
.orderByAsc(Pond::getCreateTime)
|
||||
);
|
||||
|
||||
// 转换为VO列表
|
||||
|
||||
@@ -238,5 +238,10 @@ public class Device extends TenantEntity {
|
||||
*/
|
||||
private Long batteryWarnLower;
|
||||
|
||||
/**
|
||||
* 电量值(太阳能网控专用,0-100)
|
||||
*/
|
||||
private Integer valueBattery;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -306,6 +306,12 @@ public class DeviceVo implements Serializable {
|
||||
@ExcelProperty(value = "电量电话告警下限")
|
||||
private Long batteryWarnLower;
|
||||
|
||||
/**
|
||||
* 电量值(太阳能网控专用,0-100)
|
||||
*/
|
||||
@ExcelProperty(value = "电量值")
|
||||
private Integer valueBattery;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
|
||||
@@ -109,6 +109,21 @@ public class PublicDeviceBaseData implements Serializable {
|
||||
*/
|
||||
private Integer voltageWarnOpen;
|
||||
|
||||
/**
|
||||
* 低电量电话告警开关(太阳能网控专用)
|
||||
*/
|
||||
private Long batteryWarnCallOpen;
|
||||
|
||||
/**
|
||||
* 低电量告警免打扰(太阳能网控专用)
|
||||
*/
|
||||
private Long batteryWarnCallNoDis;
|
||||
|
||||
/**
|
||||
* 低电量告警下限(太阳能网控专用,0-100)
|
||||
*/
|
||||
private Long batteryWarnLower;
|
||||
|
||||
/**
|
||||
* 开关列表(仅测控一体机)
|
||||
*/
|
||||
|
||||
@@ -89,6 +89,13 @@ public class AliyunIotProperties {
|
||||
*/
|
||||
private int offlineWarnDelayMinutes = 6;
|
||||
|
||||
/**
|
||||
* 太阳能网控离线告警延迟触发时间(分钟)
|
||||
* 太阳能网控工作1分钟、休眠10分钟属于正常行为,需要更长的延迟才能判定为真正离线
|
||||
* 默认 30 分钟
|
||||
*/
|
||||
private int solarOfflineWarnDelayMinutes = 30;
|
||||
|
||||
/**
|
||||
* 夜间溶解氧饱和度告警阈值(%),超过此值时开始计时
|
||||
* 对应 C# DeviceSaturabilityWarnValue = 200
|
||||
@@ -101,6 +108,13 @@ public class AliyunIotProperties {
|
||||
*/
|
||||
private int saturabilityWarnHours = 8;
|
||||
|
||||
/**
|
||||
* 是否启用报警电话通知
|
||||
* 全局开关,关闭后所有类型的报警都不会触发电话通知
|
||||
* 默认开启
|
||||
*/
|
||||
private boolean callNoticeEnabled = true;
|
||||
|
||||
@Data
|
||||
public static class VmsMnsConfig {
|
||||
/**
|
||||
|
||||
@@ -1662,6 +1662,11 @@ public class IotController extends BaseController {
|
||||
device.setDeviceName("太阳能网控" + (deviceCount + 1));
|
||||
device.setBindTime(now);
|
||||
device.setPondId(bo.getPondId() != null && bo.getPondId() > 0 ? bo.getPondId() : null);
|
||||
device.setOxyWarnLower(bo.getOxyWarnLower());
|
||||
device.setOxyWarnCallOpen(1);
|
||||
device.setOxyWarnCallNoDis(1);
|
||||
device.setTempWarnCallOpen(0);
|
||||
device.setTempWarnCallNoDis(0);
|
||||
device.setBatteryWarnCallOpen(bo.getBatteryWarnCallOpen());
|
||||
device.setBatteryWarnCallNoDis(bo.getBatteryWarnCallNoDis());
|
||||
device.setBatteryWarnLower(bo.getBatteryWarnLower());
|
||||
@@ -1704,15 +1709,18 @@ public class IotController extends BaseController {
|
||||
// 初始化警告码
|
||||
int warnCode = 0;
|
||||
|
||||
// 判断是否离线
|
||||
boolean isOffline = status != null && "OFFLINE".equalsIgnoreCase(status);
|
||||
|
||||
// 如果设备离线,添加离线警告
|
||||
if (status != null && "OFFLINE".equalsIgnoreCase(status)) {
|
||||
if (isOffline) {
|
||||
warnCode |= 0x0080; // 设备离线
|
||||
}
|
||||
|
||||
device.setWarnCode(warnCode);
|
||||
|
||||
// 验证 ICCID 是否存在
|
||||
if (device.getIccId() == null || device.getIccId().isEmpty()) {
|
||||
// 验证 ICCID 是否存在:设备离线时属性无法读取,允许跳过,上线后会通过数据上报补全
|
||||
if (!isOffline && (device.getIccId() == null || device.getIccId().isEmpty())) {
|
||||
return R.fail("设备缺少物联网卡号(ICCID)");
|
||||
}
|
||||
|
||||
@@ -1760,8 +1768,8 @@ public class IotController extends BaseController {
|
||||
Device::getValueDissolvedOxygen, Device::getValueTemperature)
|
||||
);
|
||||
|
||||
if (device == null || device.getUserId() == null || !device.getUserId().equals(userId)) {
|
||||
return R.fail("设备不存在或无权限访问");
|
||||
if (device == null) {
|
||||
return R.fail("设备不存在");
|
||||
}
|
||||
|
||||
// 如果是控制器且未启用溶解氧功能
|
||||
@@ -2626,6 +2634,77 @@ public class IotController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制同步开关定时控制到设备(运维补救接口)
|
||||
* 用于修复历史上因错误删除逻辑导致设备端槽位残留的问题:
|
||||
* 将数据库中该开关当前全量定时列表推送给设备,设备端多余的历史槽位将被覆盖为 isValid=0
|
||||
*
|
||||
* @param switchId 开关ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@Operation(summary = "强制同步定时控制到设备")
|
||||
@PostMapping("/timingCtrl/syncToDevice")
|
||||
public R<Void> syncTimingCtrlToDevice(@RequestParam Long switchId) {
|
||||
try {
|
||||
// 查询开关信息
|
||||
DeviceSwitch deviceSwitch = deviceSwitchMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DeviceSwitch>()
|
||||
.eq(DeviceSwitch::getId, switchId)
|
||||
.select(DeviceSwitch::getId,
|
||||
DeviceSwitch::getDeviceId,
|
||||
DeviceSwitch::getIndex,
|
||||
DeviceSwitch::getSwitchName)
|
||||
);
|
||||
if (deviceSwitch == null) {
|
||||
return R.fail("开关不存在");
|
||||
}
|
||||
|
||||
// 查询设备信息
|
||||
Device device = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getId, deviceSwitch.getDeviceId())
|
||||
.select(Device::getId, Device::getUserId, Device::getDeviceName,
|
||||
Device::getIotId, Device::getSerialNum, Device::getWarnCode, Device::getDeadTime)
|
||||
);
|
||||
if (device == null) {
|
||||
return R.fail("设备不存在");
|
||||
}
|
||||
|
||||
// 查询该开关在数据库中的全量定时控制
|
||||
java.util.List<TimingCtrl> allTimingCtrls = timingCtrlMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TimingCtrl>()
|
||||
.eq(TimingCtrl::getSwitchId, switchId)
|
||||
);
|
||||
|
||||
// 直接构建属性并推送到设备,传 timeCtrlId=-1(无匹配项,跳过特殊时间重算)
|
||||
// 数据库中不存在的历史槽位不会出现在 listTimingCtrl 中,设备收到后会用 isValid=0 的空槽覆盖
|
||||
Map<String, Object> properties = new java.util.HashMap<>();
|
||||
java.util.List<Object> listTimingCtrl = new java.util.ArrayList<>();
|
||||
for (TimingCtrl tc : allTimingCtrls) {
|
||||
Map<String, Object> timeCtrl = new java.util.HashMap<>();
|
||||
timeCtrl.put("timerMode", tc.getLoopType() != null ? tc.getLoopType() - 1 : 0);
|
||||
timeCtrl.put("isValid", tc.getIsOpen() != null && tc.getIsOpen() == 1 ? 1 : 0);
|
||||
timeCtrl.put("onTime", String.valueOf(tc.getOpenTime().getTime() / 1000));
|
||||
timeCtrl.put("offTime", String.valueOf(tc.getCloseTime().getTime() / 1000));
|
||||
listTimingCtrl.add(timeCtrl);
|
||||
}
|
||||
String propertyKey = IOTPropertyName.LOCAL_TIMER_SWITCH + deviceSwitch.getIndex();
|
||||
properties.put(propertyKey, listTimingCtrl);
|
||||
|
||||
boolean setOk = iotCloudService.setProperty(device.getIotId(), properties, false, 0);
|
||||
if (!setOk) {
|
||||
return R.fail("推送设备失败");
|
||||
}
|
||||
|
||||
log.info("[SyncTimingCtrl] 强制同步定时控制完成: switchId={}, device={}, 共{}条",
|
||||
switchId, device.getDeviceName(), listTimingCtrl.size());
|
||||
return R.ok();
|
||||
} catch (Exception e) {
|
||||
log.error("强制同步定时控制失败: {}", e.getMessage(), e);
|
||||
return R.fail("同步失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置定时控制开关
|
||||
*
|
||||
@@ -3035,10 +3114,15 @@ public class IotController extends BaseController {
|
||||
.eq(TimingCtrl::getSwitchId, dbTimeCtrl.getSwitchId())
|
||||
);
|
||||
|
||||
// 从列表中移除要删除的定时控制(删除场景直接移出列表,回查时 targetIdx < 0 直接返回成功)
|
||||
allTimingCtrls.removeIf(tc -> tc.getId().equals(request.getId()));
|
||||
// 删除场景:保持槽位数组长度不变,将目标项的 isOpen 置为 0(isValid=0)
|
||||
// 不能直接移除,否则设备端槽位会错位,被删项数据残留
|
||||
allTimingCtrls.forEach(tc -> {
|
||||
if (tc.getId().equals(request.getId())) {
|
||||
tc.setIsOpen(0L);
|
||||
}
|
||||
});
|
||||
|
||||
// 调用设置属性方法,将更新后的定时控制列表同步到设备
|
||||
// 调用设置属性方法,将完整槽位列表(含已置无效的目标项)同步到设备
|
||||
boolean success = setPropertyTimeCtrl(deviceSwitch, device, allTimingCtrls, request.getId());
|
||||
if (!success) {
|
||||
return R.fail("设置定时控制失败");
|
||||
|
||||
@@ -53,4 +53,18 @@ public class AddDeviceSolarControllerBo implements Serializable {
|
||||
@Schema(description = "电量电话告警下限(百分比)")
|
||||
@NotNull(message = "电量电话告警下限不能为空")
|
||||
private Long batteryWarnLower;
|
||||
|
||||
/**
|
||||
* 盐度补偿值
|
||||
*/
|
||||
@Schema(description = "盐度补偿值")
|
||||
@NotNull(message = "盐度补偿值不能为空")
|
||||
private Double salinityCompensation;
|
||||
|
||||
/**
|
||||
* 溶解氧报警下限
|
||||
*/
|
||||
@Schema(description = "溶解氧报警下限")
|
||||
@NotNull(message = "溶解氧报警下限不能为空")
|
||||
private Double oxyWarnLower;
|
||||
}
|
||||
|
||||
@@ -145,6 +145,8 @@ public class DeviceDataHandler {
|
||||
private static final int WARN_TYPE_TEMPERATURE = 2; // 温度
|
||||
private static final int WARN_TYPE_BATTERY = 2; // 电池电量
|
||||
private static final int WARN_TYPE_OFFLINE = 2; // 设备离线
|
||||
private static final int WARN_TYPE_CONTROLLER = 3; // 控制器告警
|
||||
private static final int WARN_TYPE_SWITCH = 4; // 开关告警
|
||||
|
||||
/**
|
||||
* 告警类型名称
|
||||
@@ -188,8 +190,12 @@ public class DeviceDataHandler {
|
||||
try {
|
||||
JSONObject data = new JSONObject(payload, JSONConfig.create().setIgnoreCase(false));
|
||||
|
||||
// 解析飞燕平台消息格式
|
||||
JSONObject params = data.getJSONObject("params");
|
||||
// 解析飞燕平台消息格式(保持ignoreCase=false,避免子对象丢失大小写配置)
|
||||
JSONObject params = null;
|
||||
Object paramsObj = data.get("params");
|
||||
if (paramsObj != null) {
|
||||
params = new JSONObject(paramsObj.toString(), JSONConfig.create().setIgnoreCase(false));
|
||||
}
|
||||
|
||||
// 从Topic中解析产品Key和设备名称
|
||||
// Topic格式: /sys/{ProductKey}/{DeviceName}/thing/event/property/post
|
||||
@@ -225,14 +231,14 @@ public class DeviceDataHandler {
|
||||
try {
|
||||
// 如果是控制器,需要区分处理
|
||||
if (isController) {
|
||||
// 控制器故障码处理(独立于开关数据判断,确保只有故障码时也能处理)
|
||||
if (params.containsKey("errorCode")) {
|
||||
handleControllerErrorCode(deviceName, params);
|
||||
}
|
||||
// 判断数据类型(开关数据和传感器数据不会同时出现)
|
||||
if (hasSwitchData(params)) {
|
||||
// 开关数据:更新开关状态和电压电流
|
||||
handleSwitchData(deviceName, params);
|
||||
// 控制器故障码处理(C# itemController.errorCode)
|
||||
if (params.containsKey("errorCode")) {
|
||||
handleControllerErrorCode(deviceName, params);
|
||||
}
|
||||
} else if (hasSensorData(params)) {
|
||||
// 传感器数据:写入TDengine
|
||||
sensorData = convertToSensorData(productKey, deviceName, params);
|
||||
@@ -299,7 +305,12 @@ public class DeviceDataHandler {
|
||||
try {
|
||||
JSONObject data = new JSONObject(payload, JSONConfig.create().setIgnoreCase(false));
|
||||
|
||||
JSONObject params = data.getJSONObject("params");
|
||||
// 保持ignoreCase=false,避免子对象丢失大小写配置
|
||||
JSONObject params = null;
|
||||
Object paramsObj = data.get("params");
|
||||
if (paramsObj != null) {
|
||||
params = new JSONObject(paramsObj.toString(), JSONConfig.create().setIgnoreCase(false));
|
||||
}
|
||||
|
||||
// 从Topic中解析产品Key、设备名称和事件标识符
|
||||
// Topic格式: /sys/{ProductKey}/{DeviceName}/thing/event/{EventIdentifier}/post
|
||||
@@ -401,6 +412,12 @@ public class DeviceDataHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设备已过期,不触发告警
|
||||
if (device.getDeadTime() != null && device.getDeadTime().before(new java.util.Date())) {
|
||||
log.debug("[告警] 设备已过期,跳过告警检查: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
Long deviceId = device.getId();
|
||||
Long userId = device.getUserId();
|
||||
|
||||
@@ -570,7 +587,7 @@ public class DeviceDataHandler {
|
||||
}
|
||||
|
||||
// 仅对开启了电话通知开关的告警触发电话通知(MessageWarn 由通知方法内部插入)
|
||||
if (!callWarnList.isEmpty()) {
|
||||
if (!callWarnList.isEmpty() && aliyunIotProperties.isCallNoticeEnabled()) {
|
||||
triggerAlarmNotification(device, alarmMessage.toString(), sensorData, callWarnList);
|
||||
}
|
||||
}
|
||||
@@ -825,10 +842,12 @@ public class DeviceDataHandler {
|
||||
);
|
||||
|
||||
if (response != null && response.isSuccess()) {
|
||||
// 更新通知记录状态
|
||||
// 更新通知记录状态,同时将 callTime 更新为实际发出时间
|
||||
// 超时判断基于 callTime,必须使用实际发出时间而非预计呼叫时间
|
||||
callNotice.setCallStatus(1); // 呼叫成功,等待结果反馈
|
||||
callNotice.setCallId(response.getCallId());
|
||||
callNotice.setOutId(outId);
|
||||
callNotice.setCallTime(LocalDateTime.now()); // 记录实际发出时间,用于超时判断
|
||||
warnCallNoticeMapper.updateById(callNotice);
|
||||
|
||||
log.info("[告警] 呼叫成功: {}", callNotice.getMobilePhone());
|
||||
@@ -869,8 +888,25 @@ public class DeviceDataHandler {
|
||||
log.debug("[离线告警] 已有等待标记,跳过重复写入: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
String offlineTime = LocalDateTime.now().format(DATETIME_FORMATTER);
|
||||
|
||||
// 查询设备类型,太阳能网控(type=3)使用独立的延迟时长
|
||||
int offlineDelayMinutes = aliyunIotProperties.getOfflineWarnDelayMinutes();
|
||||
try {
|
||||
Device deviceForType = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getSerialNum, deviceName)
|
||||
.select(Device::getDeviceType)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (deviceForType != null && Integer.valueOf(3).equals(deviceForType.getDeviceType())) {
|
||||
offlineDelayMinutes = aliyunIotProperties.getSolarOfflineWarnDelayMinutes();
|
||||
log.debug("[离线告警] 太阳能网控设备,使用延迟 {} 分钟: {}", offlineDelayMinutes, deviceName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[离线告警] 查询设备类型失败,使用默认延迟: {}", e.getMessage());
|
||||
}
|
||||
|
||||
String offlineTime = LocalDateTime.now().format(DATETIME_FORMATTER);
|
||||
int ttlMinutes = offlineDelayMinutes * OFFLINE_WAIT_TTL_MULTIPLIER;
|
||||
RedisUtils.setCacheObject(redisKey, offlineTime, Duration.ofMinutes(ttlMinutes));
|
||||
log.info("[离线告警] 设备离线,已记录等待标记,将在 {} 分钟后触发告警: {}", offlineDelayMinutes, deviceName);
|
||||
@@ -903,6 +939,11 @@ public class DeviceDataHandler {
|
||||
* @param deviceName 设备名称(serialNum)
|
||||
*/
|
||||
public void triggerOfflineAlarm(String deviceName) {
|
||||
// 全局电话通知开关检查
|
||||
if (!aliyunIotProperties.isCallNoticeEnabled()) {
|
||||
log.debug("[离线告警] 电话通知已全局关闭,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Device device = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
@@ -1026,6 +1067,11 @@ public class DeviceDataHandler {
|
||||
* @param deviceName 设备名称(serialNum)
|
||||
*/
|
||||
public void triggerSaturaHighAlarm(String deviceName) {
|
||||
// 全局电话通知开关检查
|
||||
if (!aliyunIotProperties.isCallNoticeEnabled()) {
|
||||
log.debug("[饱和度告警] 电话通知已全局关闭,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Device device = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
@@ -1151,83 +1197,96 @@ public class DeviceDataHandler {
|
||||
* @param params 事件参数
|
||||
*/
|
||||
private void triggerDeviceErrorAlarm(String deviceName, JSONObject params) {
|
||||
// 全局电话通知开关检查
|
||||
if (!aliyunIotProperties.isCallNoticeEnabled()) {
|
||||
log.debug("[设备故障] 电话通知已全局关闭,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Device device = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getSerialNum, deviceName)
|
||||
.select(Device::getId, Device::getSerialNum, Device::getDeviceName,
|
||||
Device::getPondId, Device::getUserId, Device::getDeadTime)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (device == null) {
|
||||
log.debug("[\u8bbe\u5907\u6545\u969c] \u8bbe\u5907\u4e0d\u5b58\u5728\uff0c\u8df3\u8fc7: {}", deviceName);
|
||||
log.debug("[设备故障] 设备不存在,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设备已过期,不触发故障告警
|
||||
if (device.getDeadTime() != null && device.getDeadTime().before(new java.util.Date())) {
|
||||
log.debug("[设备故障] 设备已过期,跳过故障告警: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
Long deviceId = device.getId();
|
||||
Long userId = device.getUserId();
|
||||
if (userId == null) {
|
||||
log.debug("[\u8bbe\u5907\u6545\u969c] \u8bbe\u5907\u672a\u7ed1\u5b9a\u7528\u6237\uff0c\u8df3\u8fc7: {}", deviceName);
|
||||
log.debug("[设备故障] 设备未绑定用户,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.getPondId() == null) {
|
||||
log.debug("[\u8bbe\u5907\u6545\u969c] \u8bbe\u5907\u672a\u7ed1\u5b9a\u5858\u53e3\uff0c\u8df3\u8fc7: {}", deviceName);
|
||||
log.debug("[设备故障] 设备未绑定塘口,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
Pond pond = pondMapper.selectById(device.getPondId());
|
||||
if (pond == null) {
|
||||
log.debug("[\u8bbe\u5907\u6545\u969c] \u5858\u53e3\u4e0d\u5b58\u5728\uff0c\u8df3\u8fc7: {}", deviceName);
|
||||
log.debug("[设备故障] 塘口不存在,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
String pondName = pond.getPondName();
|
||||
String displayName = device.getDeviceName() != null ? device.getDeviceName() : deviceName;
|
||||
|
||||
// \u89e3\u6790\u4e8b\u4ef6\u53c2\u6570\u83b7\u53d6\u6545\u969c\u7801
|
||||
// 解析事件参数获取故障码
|
||||
int errorCode = params != null && params.containsKey("errorCode")
|
||||
? params.getInt("errorCode", 0) : 0;
|
||||
String errorDesc = DefineDeviceErrorCode.getErrorMessage(errorCode);
|
||||
if (StrUtil.isBlank(errorDesc)) {
|
||||
errorDesc = "\u8bbe\u5907\u6545\u969c(code=" + errorCode + ")";
|
||||
errorDesc = "设备故障(code=" + errorCode + ")";
|
||||
}
|
||||
|
||||
String warnMsg = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u53d1\u751f\u6545\u969c\uff1a%s",
|
||||
String warnMsg = String.format("%s塘口中%s(%s)发生故障:%s",
|
||||
pondName, displayName, deviceName, errorDesc);
|
||||
log.error("[\u8bbe\u5907\u6545\u969c] \u8bbe\u5907: {}, \u6545\u969c\u4fe1\u606f: {}", deviceName, warnMsg);
|
||||
log.error("[设备故障] 设备: {}, 故障信息: {}", deviceName, warnMsg);
|
||||
|
||||
// \u83b7\u53d6\u544a\u8b66\u624b\u673a\u53f7
|
||||
// 获取告警手机号
|
||||
AquUser aquUser = aquUserMapper.selectById(userId);
|
||||
if (aquUser == null) {
|
||||
log.warn("[\u8bbe\u5907\u6545\u969c] \u7528\u6237\u4e0d\u5b58\u5728\uff0c\u8df3\u8fc7: {}", deviceName);
|
||||
log.warn("[设备故障] 用户不存在,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
java.util.List<String> phoneList = parseWarnPhoneList(aquUser.getWarnPhoneJson());
|
||||
if (phoneList == null || phoneList.isEmpty()) {
|
||||
log.warn("[\u8bbe\u5907\u6545\u969c] \u7528\u6237\u672a\u914d\u7f6e\u544a\u8b66\u624b\u673a\u53f7\uff0c\u8df3\u8fc7: {}", deviceName);
|
||||
log.warn("[设备故障] 用户未配置告警手机号,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
// \u53cc\u91cd\u6291\u5236
|
||||
String errorTitle = "\u63a7\u5236\u5668\u6545\u969c";
|
||||
// 双重抑制
|
||||
String errorTitle = "控制器故障";
|
||||
int maxBatchMinutesErr = CALL_NOTICE_RETRY_COUNT * CALL_NOTICE_MINUTE_INTERVAL * 2;
|
||||
LocalDateTime activeBatchStartErr = LocalDateTime.now().minusMinutes(maxBatchMinutesErr);
|
||||
long activeCountErr = warnCallNoticeMapper.countActiveByDeviceAndTitleAfter(deviceId, errorTitle, activeBatchStartErr);
|
||||
if (activeCountErr > 0) {
|
||||
log.debug("[\u8bbe\u5907\u6545\u969c] \u5f53\u524d\u6709\u6d3b\u8dc3\u901a\u77e5\u6279\u6b21\u5728\u8fdb\u884c\uff0c\u8df3\u8fc7: {}", deviceName);
|
||||
log.debug("[设备故障] 当前有活跃通知批次在进行,跳过: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
LocalDateTime suppressTimeErr = LocalDateTime.now().minusHours(aliyunIotProperties.getCallSuccessSuppressHours());
|
||||
long recentCountErr = warnCallNoticeMapper.countSuccessByDeviceAndTitleAfter(deviceId, errorTitle, suppressTimeErr);
|
||||
if (recentCountErr > 0) {
|
||||
log.debug("[\u8bbe\u5907\u6545\u969c] {}h\u5185\u5df2\u6709\u56de\u6267\u6210\u529f\u7684\u901a\u77e5\uff0c\u8df3\u8fc7: {}", aliyunIotProperties.getCallSuccessSuppressHours(), deviceName);
|
||||
log.debug("[设备故障] {}h内已有回执成功的通知,跳过: {}", aliyunIotProperties.getCallSuccessSuppressHours(), deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
// \u5199\u5165\u544a\u8b66\u6d88\u606f
|
||||
// 写入告警消息
|
||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, errorTitle, WARN_TYPE_DISSOLVED_OXYGEN, warnMsg);
|
||||
messageWarnMapper.insert(warn);
|
||||
|
||||
// \u521b\u5efa\u7535\u8bdd\u901a\u77e5\u8bb0\u5f55
|
||||
// 创建电话通知记录
|
||||
LocalDateTime baseTime = LocalDateTime.now();
|
||||
int index = 0;
|
||||
AquWarnCallNotice firstCallNotice = null;
|
||||
@@ -1266,11 +1325,11 @@ public class DeviceDataHandler {
|
||||
sendVoiceNotification(firstCallNotice, displayName, errorTitle);
|
||||
}
|
||||
|
||||
log.info("[\u8bbe\u5907\u6545\u969c] \u5df2\u89e6\u53d1\u901a\u77e5 - \u8bbe\u5907\uff1a{}, \u5858\u53e3\uff1a{}, \u624b\u673a\u53f7\u6570\uff1a{}",
|
||||
log.info("[设备故障] 已触发通知 - 设备:{}, 塘口:{}, 手机号数:{}",
|
||||
deviceName, pondName, phoneList.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[\u8bbe\u5907\u6545\u969c] \u89e6\u53d1\u6545\u969c\u544a\u8b66\u5931\u8d25: {}", e.getMessage(), e);
|
||||
log.error("[设备故障] 触发故障告警失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1298,12 +1357,20 @@ public class DeviceDataHandler {
|
||||
Device device = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getSerialNum, deviceName)
|
||||
.select(Device::getId, Device::getSerialNum, Device::getDeviceName, Device::getWarnCode,
|
||||
Device::getPondId, Device::getUserId, Device::getDeadTime)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设备已过期,不处理故障码
|
||||
if (device.getDeadTime() != null && device.getDeadTime().before(new java.util.Date())) {
|
||||
log.debug("[故障码] 设备已过期,跳过处理: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorCode == 0) {
|
||||
// 清除所有故障码
|
||||
clearErrorCodeRecords(device);
|
||||
@@ -1372,10 +1439,36 @@ public class DeviceDataHandler {
|
||||
String title = null;
|
||||
String message = null;
|
||||
int switchIndex = 0;
|
||||
// 告警类型:默认为控制器告警(3),开关相关故障使用开关告警(4)
|
||||
int warnType = WARN_TYPE_CONTROLLER;
|
||||
|
||||
if (errorCode >= DefineDeviceErrorCode.Three_Switch1LosePhaseA
|
||||
if (errorCode >= DefineDeviceErrorCode.Three_InputLosePhaseA
|
||||
&& errorCode <= DefineDeviceErrorCode.Three_InputLosePhaseC) {
|
||||
// 三相输入缺相(1001-1003):强制关闭所有4路开关
|
||||
String phaseName = DefineDeviceErrorCode.getPhaseName(errorCode - DefineDeviceErrorCode.Three_InputLosePhaseA);
|
||||
for (int i = 1; i <= 4; i++) {
|
||||
forceSwitchOff(device, i);
|
||||
}
|
||||
if (warnTrigger) {
|
||||
title = "控制器输入缺相";
|
||||
message = String.format("%s塘口中%s(%s)输入缺%s相",
|
||||
pondName, displayName, device.getSerialNum(), phaseName);
|
||||
}
|
||||
} else if (errorCode >= DefineDeviceErrorCode.Three_PhaseA_OverVoltage
|
||||
&& errorCode <= DefineDeviceErrorCode.Three_PhaseC_UnderVoltage) {
|
||||
// 三相过压欠压(1004-1009):不强制关闭开关
|
||||
boolean isOver = (errorCode - DefineDeviceErrorCode.Three_PhaseA_OverVoltage) % 2 == 0;
|
||||
int phaseIndex = (errorCode - DefineDeviceErrorCode.Three_PhaseA_OverVoltage) / 2;
|
||||
String phaseName = DefineDeviceErrorCode.getPhaseName(phaseIndex);
|
||||
if (warnTrigger) {
|
||||
title = isOver ? "控制器输入过压" : "控制器输入欠压";
|
||||
message = String.format("%s塘口中%s(%s)%s相%s",
|
||||
pondName, displayName, device.getSerialNum(), phaseName, isOver ? "过压" : "欠压");
|
||||
}
|
||||
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1LosePhaseA
|
||||
&& errorCode <= DefineDeviceErrorCode.Three_Switch4LosePhaseC) {
|
||||
// 三相开关输出缺相
|
||||
// 三相开关输出缺相 - 开关告警
|
||||
warnType = WARN_TYPE_SWITCH;
|
||||
int tempIndex = errorCode - DefineDeviceErrorCode.Three_Switch1LosePhaseA;
|
||||
String phaseName = DefineDeviceErrorCode.getPhaseName(tempIndex % 3);
|
||||
switchIndex = tempIndex / 3 + 1;
|
||||
@@ -1392,7 +1485,8 @@ public class DeviceDataHandler {
|
||||
}
|
||||
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1OverElectricA
|
||||
&& errorCode <= DefineDeviceErrorCode.Three_Switch4OverElectricC) {
|
||||
// 三相过流
|
||||
// 三相过流 - 开关告警
|
||||
warnType = WARN_TYPE_SWITCH;
|
||||
int tempIndex = errorCode - DefineDeviceErrorCode.Three_Switch1OverElectricA;
|
||||
String phaseName = DefineDeviceErrorCode.getPhaseName(tempIndex % 3);
|
||||
switchIndex = tempIndex / 3 + 1;
|
||||
@@ -1408,7 +1502,8 @@ public class DeviceDataHandler {
|
||||
}
|
||||
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1UnderElectricA
|
||||
&& errorCode <= DefineDeviceErrorCode.Three_Switch4UnderElectricC) {
|
||||
// 三相欠流
|
||||
// 三相欠流 - 开关告警
|
||||
warnType = WARN_TYPE_SWITCH;
|
||||
int tempIndex = errorCode - DefineDeviceErrorCode.Three_Switch1UnderElectricA;
|
||||
String phaseName = DefineDeviceErrorCode.getPhaseName(tempIndex % 3);
|
||||
switchIndex = tempIndex / 3 + 1;
|
||||
@@ -1424,7 +1519,8 @@ public class DeviceDataHandler {
|
||||
}
|
||||
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1ElectricEmpty
|
||||
&& errorCode <= DefineDeviceErrorCode.Three_Switch4ElectricEmpty) {
|
||||
// 三相空载
|
||||
// 三相空载 - 开关告警
|
||||
warnType = WARN_TYPE_SWITCH;
|
||||
switchIndex = errorCode - DefineDeviceErrorCode.Three_Switch1ElectricEmpty + 1;
|
||||
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
|
||||
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
|
||||
@@ -1438,7 +1534,8 @@ public class DeviceDataHandler {
|
||||
}
|
||||
} else if (errorCode >= DefineDeviceErrorCode.One_Switch1OverElectric
|
||||
&& errorCode <= DefineDeviceErrorCode.One_Switch4OverElectric) {
|
||||
// 单相过流
|
||||
// 单相过流 - 开关告警
|
||||
warnType = WARN_TYPE_SWITCH;
|
||||
switchIndex = errorCode - DefineDeviceErrorCode.One_Switch1OverElectric + 1;
|
||||
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
|
||||
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
|
||||
@@ -1452,7 +1549,8 @@ public class DeviceDataHandler {
|
||||
}
|
||||
} else if (errorCode >= DefineDeviceErrorCode.One_Switch1UnderElectric
|
||||
&& errorCode <= DefineDeviceErrorCode.One_Switch4UnderElectric) {
|
||||
// 单相欠流
|
||||
// 单相欠流 - 开关告警
|
||||
warnType = WARN_TYPE_SWITCH;
|
||||
switchIndex = errorCode - DefineDeviceErrorCode.One_Switch1UnderElectric + 1;
|
||||
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
|
||||
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
|
||||
@@ -1466,7 +1564,8 @@ public class DeviceDataHandler {
|
||||
}
|
||||
} else if (errorCode >= DefineDeviceErrorCode.One_Switch1ElectricEmpty
|
||||
&& errorCode <= DefineDeviceErrorCode.One_Switch4ElectricEmpty) {
|
||||
// 单相空载
|
||||
// 单相空载 - 开关告警
|
||||
warnType = WARN_TYPE_SWITCH;
|
||||
switchIndex = errorCode - DefineDeviceErrorCode.One_Switch1ElectricEmpty + 1;
|
||||
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
|
||||
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
|
||||
@@ -1498,10 +1597,12 @@ public class DeviceDataHandler {
|
||||
if (warnTrigger && title != null && message != null) {
|
||||
Long userId = device.getUserId();
|
||||
if (userId != null) {
|
||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, title, WARN_TYPE_DISSOLVED_OXYGEN, message);
|
||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, title, warnType, message);
|
||||
messageWarnMapper.insert(warn);
|
||||
// 创建电话通知记录
|
||||
createCallNotices(device, pondName, warn);
|
||||
// 创建电话通知记录(全局开关控制)
|
||||
if (aliyunIotProperties.isCallNoticeEnabled()) {
|
||||
createCallNotices(device, pondName, warn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1727,22 +1828,29 @@ public class DeviceDataHandler {
|
||||
try {
|
||||
Integer rawCode = params.getInt("sensorErrorCode");
|
||||
if (rawCode == null) {
|
||||
log.warn("[探头错误码] 解析 sensorErrorCode 失败: {}", deviceName);
|
||||
// 部分设备/数据包不携带此字段,属于正常情况
|
||||
log.debug("[探头错误码] 设备上报数据不包含 sensorErrorCode 字段: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
int sensorErrorCode = rawCode;
|
||||
|
||||
// 查询设备,需要 id / warnCode / isOxygenUsed / pondId
|
||||
// 查询设备,需要 id / warnCode / isOxygenUsed / pondId / deadTime
|
||||
Device device = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getSerialNum, deviceName)
|
||||
.select(Device::getId, Device::getWarnCode, Device::getIsOxygenUsed, Device::getPondId)
|
||||
.select(Device::getId, Device::getWarnCode, Device::getIsOxygenUsed, Device::getPondId, Device::getDeadTime)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设备已过期,不处理探头错误码
|
||||
if (device.getDeadTime() != null && device.getDeadTime().before(new java.util.Date())) {
|
||||
log.debug("[探头错误码] 设备已过期,跳过处理: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
int currentCode = device.getWarnCode() != null ? device.getWarnCode() : 0;
|
||||
int newCode = currentCode;
|
||||
|
||||
@@ -1807,10 +1915,19 @@ public class DeviceDataHandler {
|
||||
|
||||
/**
|
||||
* 清除设备离线标志(收到属性上报时调用)
|
||||
* 同时清除数据库 warnCode 位和 Redis 离线等待标记
|
||||
*
|
||||
* @param deviceName 设备名称(serialNum)
|
||||
*/
|
||||
private void clearDeviceOfflineFlag(String deviceName) {
|
||||
// 1. 清除 Redis 离线等待标记(防止定时任务误触发告警)
|
||||
String redisKey = DEVICE_OFFLINE_WAIT_KEY_PREFIX + deviceName;
|
||||
boolean redisRemoved = RedisUtils.deleteObject(redisKey);
|
||||
if (redisRemoved) {
|
||||
log.info("[在线] 收到属性上报,已清除 Redis 离线等待标记: {}", deviceName);
|
||||
}
|
||||
|
||||
// 2. 清除数据库 warnCode 离线标志位
|
||||
try {
|
||||
Device device = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
@@ -1827,7 +1944,7 @@ public class DeviceDataHandler {
|
||||
update.setId(device.getId());
|
||||
update.setWarnCode(device.getWarnCode() & ~DefineDeviceWarnCode.DeviceOffline);
|
||||
deviceMapper.updateById(update);
|
||||
log.debug("[在线] 收到属性上报,已清除设备离线标志: {}", deviceName);
|
||||
log.debug("[在线] 已清除设备数据库离线标志: {}", deviceName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[在线] 清除设备离线标志失败[{}]: {}", deviceName, e.getMessage());
|
||||
@@ -2069,7 +2186,6 @@ public class DeviceDataHandler {
|
||||
// 设备标识(使用 deviceName 作为 serialNum)
|
||||
sensorData.setSerialNum(deviceName);
|
||||
sensorData.setDeviceName(deviceName);
|
||||
|
||||
// 解析传感器数据(根据实际字段映射)
|
||||
// 支持小写和驼峰命名两种格式
|
||||
if (params.containsKey("dissolvedoxygen") || params.containsKey("dissolvedOxygen")) {
|
||||
@@ -2077,22 +2193,28 @@ public class DeviceDataHandler {
|
||||
params.getDouble("dissolvedOxygen") : params.getDouble("dissolvedoxygen");
|
||||
sensorData.setDissolvedOxygen(value);
|
||||
}
|
||||
if (params.containsKey("temperature")) {
|
||||
sensorData.setTemperature(params.getDouble("temperature"));
|
||||
}
|
||||
if (params.containsKey("currentTemperature")) {
|
||||
sensorData.setTemperature(params.getDouble("currentTemperature"));
|
||||
} else if (params.containsKey("temperature")) {
|
||||
sensorData.setTemperature(params.getDouble("temperature"));
|
||||
}
|
||||
if (params.containsKey("saturability") || params.containsKey("dosat")) {
|
||||
Double value = params.containsKey("dosat") ?
|
||||
params.getDouble("dosat") : params.getDouble("saturability");
|
||||
sensorData.setSaturability(value);
|
||||
}
|
||||
if (params.containsKey("ph")) {
|
||||
sensorData.setPh(params.getDouble("ph"));
|
||||
if (params.containsKey("PH") || params.containsKey("ph") || params.containsKey("Ph")) {
|
||||
Double value = params.containsKey("PH") ? params.getDouble("PH")
|
||||
: params.containsKey("Ph") ? params.getDouble("Ph")
|
||||
: params.getDouble("ph");
|
||||
sensorData.setPh(value);
|
||||
}
|
||||
if (params.containsKey("salinity")) {
|
||||
sensorData.setSalinity(params.getDouble("salinity"));
|
||||
// 通过遍历 keySet 做精确字符串比较,彻底避免 Hutool ignoreCase 下 containsKey/getDouble 误匹配 salinitySet
|
||||
for (String k : params.keySet()) {
|
||||
if ("salinity".equals(k)) {
|
||||
sensorData.setSalinity(params.getDouble(k));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (params.containsKey("treference") || params.containsKey("Treference")) {
|
||||
Double value = params.containsKey("Treference") ?
|
||||
@@ -2104,11 +2226,14 @@ public class DeviceDataHandler {
|
||||
params.getDouble("Tfluorescence") : params.getDouble("tfluorescence");
|
||||
sensorData.setTfluorescence(value);
|
||||
}
|
||||
if (params.containsKey("phasedifference")) {
|
||||
sensorData.setPhaseDifference(params.getDouble("phasedifference"));
|
||||
if (params.containsKey("Phasedifference") || params.containsKey("phaseDifference") || params.containsKey("phasedifference")) {
|
||||
Double value = params.containsKey("Phasedifference") ? params.getDouble("Phasedifference")
|
||||
: params.containsKey("phaseDifference") ? params.getDouble("phaseDifference")
|
||||
: params.getDouble("phasedifference");
|
||||
sensorData.setPhaseDifference(value);
|
||||
}
|
||||
if (params.containsKey("battery")) {
|
||||
sensorData.setBattery(params.getDouble("battery"));
|
||||
if (params.containsKey("ChargeFlag")) {
|
||||
sensorData.setBattery(params.getDouble("ChargeFlag"));
|
||||
}
|
||||
|
||||
// TODO: 如果需要设置 deviceId、userId 等标签字段,需要从设备管理系统查询
|
||||
@@ -2163,6 +2288,9 @@ public class DeviceDataHandler {
|
||||
if (sensorData.getSalinity() != null) {
|
||||
updateDevice.setValueSalinity(sensorData.getSalinity());
|
||||
}
|
||||
if (sensorData.getBattery() != null) {
|
||||
updateDevice.setValueBattery(sensorData.getBattery().intValue());
|
||||
}
|
||||
int updated = deviceMapper.updateById(updateDevice);
|
||||
if (updated > 0) {
|
||||
log.debug("设备实时数据已更新: {}", deviceName);
|
||||
@@ -2189,18 +2317,25 @@ public class DeviceDataHandler {
|
||||
*/
|
||||
private void handleLinkedCtrl(String deviceName, double dissolvedOxygen) {
|
||||
try {
|
||||
// 查询当前设备(需要 id、iotId、pondId)
|
||||
// 查询当前设备(需要 id、iotId、pondId、deadTime)
|
||||
Device currentDevice = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getSerialNum, deviceName)
|
||||
.select(Device::getId, Device::getIotId, Device::getSerialNum, Device::getDeviceName,
|
||||
Device::getPondId, Device::getValueDissolvedOxygen)
|
||||
Device::getPondId, Device::getValueDissolvedOxygen, Device::getUserId,
|
||||
Device::getDeadTime)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (currentDevice == null || currentDevice.getId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设备已过期,不执行联动控制
|
||||
if (currentDevice.getDeadTime() != null && currentDevice.getDeadTime().before(new java.util.Date())) {
|
||||
log.debug("[联动控制] 设备已过期,跳过处理: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 先更新当前设备的实时溶解氧(确保后续取最低值时已包含本次上报值)
|
||||
// 注意:updateDeviceRealTimeData 在 handleLinkedCtrl 调用前已执行,此处直接使用传入值
|
||||
|
||||
@@ -2290,7 +2425,8 @@ public class DeviceDataHandler {
|
||||
deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getId, id)
|
||||
.select(Device::getId, Device::getIotId, Device::getSerialNum, Device::getDeviceName)
|
||||
.select(Device::getId, Device::getIotId, Device::getSerialNum, Device::getDeviceName,
|
||||
Device::getUserId)
|
||||
.last("LIMIT 1")
|
||||
)
|
||||
);
|
||||
@@ -2312,7 +2448,9 @@ public class DeviceDataHandler {
|
||||
&& effectiveDissolvedOxygen <= linkedCtrl.getOxyLowerValue()) {
|
||||
log.info("[联动控制] 触发设备={} 塘口最低溶解氧={} ≤ 下限={},开启开关(本机上报={})",
|
||||
deviceName, effectiveDissolvedOxygen, linkedCtrl.getOxyLowerValue(), dissolvedOxygen);
|
||||
setDeviceSwitchLinkedOpen(device, switches, true);
|
||||
setDeviceSwitchLinkedOpen(device, switches, true,
|
||||
String.format("溶解氧%.2f ≤ 下限%.2f",
|
||||
effectiveDissolvedOxygen, linkedCtrl.getOxyLowerValue()));
|
||||
}
|
||||
|
||||
// 溶解氧上限联动(带滞回防止重复触发)
|
||||
@@ -2341,7 +2479,9 @@ public class DeviceDataHandler {
|
||||
linkedCtrlMapper.updateById(updateCtrl);
|
||||
log.info("[联动控制] 触发设备={} 塘口最低溶解氧={} ≥ 上限={},关闭开关",
|
||||
deviceName, effectiveDissolvedOxygen, linkedCtrl.getOxyUpperValue());
|
||||
setDeviceSwitchLinkedOpen(device, switches, false);
|
||||
setDeviceSwitchLinkedOpen(device, switches, false,
|
||||
String.format("溶解氧%.2f ≥ 上限%.2f",
|
||||
effectiveDissolvedOxygen, linkedCtrl.getOxyUpperValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2352,13 +2492,14 @@ public class DeviceDataHandler {
|
||||
|
||||
/**
|
||||
* 按联动控制开启/关闭一组开关(参考 C# SetDeviceSwitchLinkedOpen)
|
||||
* 下发成功后更新 DeviceSwitch.isOpen 和 lastTurnTime。
|
||||
* 下发成功后更新 DeviceSwitch.isOpen 和 lastTurnTime,并写入操作记录。
|
||||
*
|
||||
* @param device 设备信息(含 iotId)
|
||||
* @param switches 待操作的开关列表
|
||||
* @param open true=开启, false=关闭
|
||||
* @param device 设备信息(含 iotId、userId)
|
||||
* @param switches 待操作的开关列表
|
||||
* @param open true=开启, false=关闭
|
||||
* @param opReasonDesc 操作原因描述(用于操作记录 message 前缀)
|
||||
*/
|
||||
private void setDeviceSwitchLinkedOpen(Device device, List<com.intc.fishery.domain.DeviceSwitch> switches, boolean open) {
|
||||
private void setDeviceSwitchLinkedOpen(Device device, List<com.intc.fishery.domain.DeviceSwitch> switches, boolean open, String opReasonDesc) {
|
||||
if (device == null || device.getIotId() == null || switches == null || switches.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -2388,16 +2529,32 @@ public class DeviceDataHandler {
|
||||
boolean success = iotCloudService.setProperty(device.getIotId(), properties, false, 1);
|
||||
if (success) {
|
||||
java.util.Date now = new java.util.Date();
|
||||
String deviceLabel = device.getSerialNum() != null ? device.getSerialNum() : device.getIotId();
|
||||
for (com.intc.fishery.domain.DeviceSwitch sw : toChange) {
|
||||
com.intc.fishery.domain.DeviceSwitch updateSw = new com.intc.fishery.domain.DeviceSwitch();
|
||||
updateSw.setId(sw.getId());
|
||||
updateSw.setIsOpen(targetState);
|
||||
updateSw.setLastTurnTime(now);
|
||||
deviceSwitchMapper.updateById(updateSw);
|
||||
// 写入操作记录
|
||||
if (device.getUserId() != null) {
|
||||
String dName = device.getDeviceName() != null ? device.getDeviceName() : deviceLabel;
|
||||
String switchName = sw.getSwitchName() != null ? sw.getSwitchName() : ("Switch" + sw.getIndex());
|
||||
String reasonPart = opReasonDesc != null ? "(" + opReasonDesc + ")" : "";
|
||||
com.intc.fishery.utils.MessageOpRecordUtil.addMessageOpRecordLinked(
|
||||
device.getUserId(),
|
||||
"开关操作",
|
||||
String.format("%s(%s)的开关%s%s:%s%s。",
|
||||
dName, deviceLabel,
|
||||
sw.getIndex() != null ? sw.getIndex() + " " : "",
|
||||
switchName,
|
||||
open ? "开启" : "关闭",
|
||||
reasonPart)
|
||||
);
|
||||
}
|
||||
}
|
||||
log.info("[联动控制] 设备={} 开关下发成功:{} -> {}",
|
||||
device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(),
|
||||
properties.keySet(), open ? "开启" : "关闭");
|
||||
deviceLabel, properties.keySet(), open ? "开启" : "关闭");
|
||||
} else {
|
||||
log.warn("[联动控制] 设备={} 开关下发失败:{}",
|
||||
device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(),
|
||||
|
||||
@@ -104,6 +104,38 @@ public interface AquWarnCallNoticeMapper extends BaseMapper<AquWarnCallNotice> {
|
||||
@Select("SELECT * FROM aqu_call_notice WHERE call_status = 0 AND call_time <= #{now} ORDER BY call_time ASC")
|
||||
List<AquWarnCallNotice> selectDuePendingNotices(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 查询指定设备在指定时间之后是否已有「等待回执中(callStatus=1)」或「回执成功(callStatus=3)」的通知记录
|
||||
* 用于定时任务发出待发通知前的拦截判断:
|
||||
* - callStatus=1:前一条已发出但回执未到,不应重复拨打,等其超时后再发
|
||||
* - callStatus=3:已成功,直接取消后续记录
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param afterTime 批次窗口起始时间
|
||||
* @return 满足条件的通知数量
|
||||
*/
|
||||
@Select("SELECT COUNT(1) FROM aqu_call_notice " +
|
||||
"WHERE device_id = #{deviceId} " +
|
||||
"AND call_status IN (1, 3) " +
|
||||
"AND call_time >= #{afterTime}")
|
||||
long countWaitingOrSuccessByDeviceAfter(@Param("deviceId") Long deviceId,
|
||||
@Param("afterTime") LocalDateTime afterTime);
|
||||
|
||||
/**
|
||||
* 查询指定设备在指定时间之后是否已有「回执成功」的通知记录(不区分 title)
|
||||
* 用于定时任务发出待发通知前,判断同批次是否已有成功回执,避免重复拨打
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param afterTime 起始时间(通常取批次第一条记录的 callTime)
|
||||
* @return 满足条件的通知数量
|
||||
*/
|
||||
@Select("SELECT COUNT(1) FROM aqu_call_notice " +
|
||||
"WHERE device_id = #{deviceId} " +
|
||||
"AND call_status = 3 " +
|
||||
"AND call_time >= #{afterTime}")
|
||||
long countSuccessByDeviceAfter(@Param("deviceId") Long deviceId,
|
||||
@Param("afterTime") LocalDateTime afterTime);
|
||||
|
||||
/**
|
||||
* 通过 callNoticeId 查询关联的第一条告警消息内容
|
||||
* 用于定时任务重发时构建 TTS 参数
|
||||
@@ -115,4 +147,16 @@ public interface AquWarnCallNoticeMapper extends BaseMapper<AquWarnCallNotice> {
|
||||
"INNER JOIN aqu_map_message_warn_call_notice m ON m.message_warn_id = w.id " +
|
||||
"WHERE m.call_notice_id = #{callNoticeId} LIMIT 1")
|
||||
String selectWarnMessageByCallNoticeId(@Param("callNoticeId") Long callNoticeId);
|
||||
|
||||
/**
|
||||
* 通过 callNoticeId 查询关联的第一条告警消息标题(title)
|
||||
* 用于定时任务重发时作为 TTS warnMessage 参数,避免将完整消息内容传入导致 TTS 播报异常
|
||||
*
|
||||
* @param callNoticeId 通知记录ID
|
||||
* @return 告警标题,不存在时返回 null
|
||||
*/
|
||||
@Select("SELECT w.title FROM aqu_message_warn w " +
|
||||
"INNER JOIN aqu_map_message_warn_call_notice m ON m.message_warn_id = w.id " +
|
||||
"WHERE m.call_notice_id = #{callNoticeId} LIMIT 1")
|
||||
String selectWarnTitleByCallNoticeId(@Param("callNoticeId") Long callNoticeId);
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
|
||||
// 使用 DeviceDataHandler 处理数据(包括存储和报警)
|
||||
try {
|
||||
// 转换 items 格式:{"dissolvedOxygen": {"value": 7.82, "time": xxx}} -> {"dissolvedOxygen": 7.82}
|
||||
// 保留原始字段名不做任何映射,convertToSensorData 已兼容多种字段名格式
|
||||
JSONObject params = new JSONObject(JSONConfig.create().setIgnoreCase(false));
|
||||
if (items != null) {
|
||||
for (String key : items.keySet()) {
|
||||
@@ -126,14 +127,11 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
|
||||
JSONObject itemObj = (JSONObject) itemValue;
|
||||
// 提取 value 字段
|
||||
if (itemObj.containsKey("value")) {
|
||||
// 字段名映射:currentTemperature -> temperature, dosat -> saturability
|
||||
String mappedKey = mapFieldName(key);
|
||||
params.set(mappedKey, itemObj.get("value"));
|
||||
params.set(key, itemObj.get("value"));
|
||||
}
|
||||
} else {
|
||||
// 如果不是嵌套格式,直接使用
|
||||
String mappedKey = mapFieldName(key);
|
||||
params.set(mappedKey, itemValue);
|
||||
params.set(key, itemValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,31 +148,6 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
|
||||
log.error("使用 DeviceDataHandler 处理属性数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段名映射:将阿里云字段名映射为应用字段名
|
||||
*/
|
||||
private String mapFieldName(String fieldName) {
|
||||
switch (fieldName) {
|
||||
case "currentTemperature":
|
||||
return "temperature";
|
||||
case "dosat":
|
||||
return "saturability";
|
||||
case "Treference":
|
||||
return "treference";
|
||||
case "Tfluorescence":
|
||||
return "tfluorescence";
|
||||
case "Tcorrect":
|
||||
return "tcorrect";
|
||||
case "Tsignal":
|
||||
return "tsignal";
|
||||
case "salinitySet":
|
||||
return "salinity";
|
||||
default:
|
||||
// 其他字段名保持原始大小写,不做转换
|
||||
return fieldName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理从 Topic 判断的属性消息(飞燕平台原始格式)
|
||||
|
||||
@@ -56,7 +56,9 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
|
||||
/**
|
||||
* 回执超时时间(分钟)
|
||||
* VMS 回执延迟较长,改为 5 分钟,避免回执未到就被标记为超时
|
||||
* 从呼叫实际发出(callTime 已修正为真实发出时间)到收到 VMS 回执的最长等待时间。
|
||||
* 正常链路:通话约 30s + VMS 生成回执约 1~2min,3 分钟内完成。
|
||||
* 设为 5 分钟,兼顾回执延迟容忍与批次重试及时性(批次间隔 3 分钟)。
|
||||
*/
|
||||
private static final int CALLBACK_TIMEOUT_MINUTES = 5;
|
||||
|
||||
@@ -104,7 +106,10 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
}
|
||||
|
||||
if (callNotice == null) {
|
||||
log.warn("[告警通知] 未找到对应的通知记录 - 呼叫ID: {}, outId: {}", callback.getCallId(), outId);
|
||||
log.warn("[告警通知] 未找到对应的通知记录,标记为已处理跳过 - 呼叫ID: {}, outId: {}", callback.getCallId(), outId);
|
||||
// 通知记录不存在(可能已被清除),直接标记回执为已处理,避免无限重复扫描
|
||||
callback.setProcessed(1);
|
||||
vmsCallbackMapper.updateById(callback);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -254,16 +259,32 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
int sentCount = 0;
|
||||
for (AquWarnCallNotice notice : pendingList) {
|
||||
try {
|
||||
// 查询关联的告警消息内容,用于构建 TTS 参数
|
||||
String rawMessage = warnCallNoticeMapper.selectWarnMessageByCallNoticeId(notice.getId());
|
||||
String warnMessageShort = parseWarnMessageShort(rawMessage);
|
||||
// 发出前先检查同设备同批次窗口内的状态
|
||||
// 批次窗口 = 当前记录的 callTime 往前 CALLBACK_TIMEOUT_MINUTES 分钟(足够覆盖整个批次)
|
||||
// - status=1(等待回执中):前一条已发出但回执未到,不重复拨打,等其超时后再发
|
||||
// - status=3(已成功):已有成功回执,跳过该条记录
|
||||
if (notice.getDeviceId() != null) {
|
||||
LocalDateTime batchWindowStart = notice.getCallTime().minusMinutes(CALLBACK_TIMEOUT_MINUTES);
|
||||
long blockingCount = warnCallNoticeMapper.countWaitingOrSuccessByDeviceAfter(
|
||||
notice.getDeviceId(), batchWindowStart);
|
||||
if (blockingCount > 0) {
|
||||
log.info("[待发通知] 设备 {} 同批次内已有等待回执或成功的记录,跳过记录ID: {}", notice.getDeviceId(), notice.getId());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询关联的告警标题(title),作为 TTS warnMessage 参数
|
||||
// 使用 title(如"设备离线"、"溶解氧")而非完整 message,避免长字符串被 TTS 逐字播报
|
||||
String warnTitle = warnCallNoticeMapper.selectWarnTitleByCallNoticeId(notice.getId());
|
||||
String warnMessageShort = (warnTitle != null && !warnTitle.isEmpty()) ? warnTitle : "异常";
|
||||
|
||||
// 通过 deviceId 查询设备名称
|
||||
// 注意:不使用 serialNum 作为 fallback,避免数字序列号被 TTS 逐位播报
|
||||
String deviceName = null;
|
||||
if (notice.getDeviceId() != null) {
|
||||
Device device = deviceMapper.selectById(notice.getDeviceId());
|
||||
if (device != null) {
|
||||
deviceName = device.getDeviceName() != null ? device.getDeviceName() : device.getSerialNum();
|
||||
if (device != null && device.getDeviceName() != null && !device.getDeviceName().isEmpty()) {
|
||||
deviceName = device.getDeviceName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +306,7 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
notice.setCallStatus(CALL_STATUS_CALL_SUCCESS);
|
||||
notice.setCallId(response.getCallId());
|
||||
notice.setOutId(outId);
|
||||
notice.setCallTime(LocalDateTime.now()); // 记录实际发出时间,用于超时判断
|
||||
warnCallNoticeMapper.updateById(notice);
|
||||
sentCount++;
|
||||
log.info("[待发通知] 呼叫成功: {} - 记录ID: {}", notice.getMobilePhone(), notice.getId());
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.intc.tdengine.mapper;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 设备到期信息Mapper(查询MySQL主库)
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Repository
|
||||
@Mapper
|
||||
public interface DeviceExpireMapper {
|
||||
|
||||
/**
|
||||
* 根据设备序列号查询服务到期时间
|
||||
*
|
||||
* @param serialNum 设备序列号
|
||||
* @return 服务到期时间,设备不存在时返回null
|
||||
*/
|
||||
@DS("master")
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
Date getDeadTimeBySerialNum(@Param("serialNum") String serialNum);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.intc.tdengine.service.impl;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.intc.tdengine.domain.DeviceSensorData;
|
||||
import com.intc.tdengine.mapper.DeviceExpireMapper;
|
||||
import com.intc.tdengine.mapper.DeviceSensorDataMapper;
|
||||
import com.intc.tdengine.service.IDeviceSensorDataService;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -9,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@@ -17,6 +19,9 @@ import java.util.List;
|
||||
public class DeviceSensorDataService implements IDeviceSensorDataService {
|
||||
@Resource
|
||||
private DeviceSensorDataMapper deviceSensorDataMapper;
|
||||
|
||||
@Resource
|
||||
private DeviceExpireMapper deviceExpireMapper;
|
||||
|
||||
/**
|
||||
* 已创建的子表缓存(设备序列号)
|
||||
@@ -134,33 +139,36 @@ public class DeviceSensorDataService implements IDeviceSensorDataService {
|
||||
}
|
||||
|
||||
try {
|
||||
// 打印第一条数据的详细信息
|
||||
if (!dataList.isEmpty()) {
|
||||
DeviceSensorData firstData = dataList.get(0);
|
||||
// 检查子表是否存在(使用缓存优化)
|
||||
String serialNum = firstData.getSerialNum();
|
||||
// 按 serialNum 分组,对每个设备单独检查并确保子表存在
|
||||
java.util.Map<String, DeviceSensorData> firstBySerial = new java.util.LinkedHashMap<>();
|
||||
for (DeviceSensorData data : dataList) {
|
||||
String sn = data.getSerialNum();
|
||||
if (sn != null && !sn.isEmpty() && !firstBySerial.containsKey(sn)) {
|
||||
firstBySerial.put(sn, data);
|
||||
}
|
||||
}
|
||||
|
||||
for (java.util.Map.Entry<String, DeviceSensorData> entry : firstBySerial.entrySet()) {
|
||||
String serialNum = entry.getKey();
|
||||
String tableName = "t_" + serialNum;
|
||||
|
||||
// 先检查缓存,避免重复查询数据库
|
||||
|
||||
if (!CREATED_TABLES.containsKey(serialNum)) {
|
||||
try {
|
||||
Integer exists = deviceSensorDataMapper.checkTableExists(serialNum);
|
||||
if (exists == null || exists == 0) {
|
||||
deviceSensorDataMapper.createTable(firstData);
|
||||
deviceSensorDataMapper.createTable(entry.getValue());
|
||||
log.debug("子表 {} 已创建", tableName);
|
||||
}
|
||||
// 加入缓存
|
||||
CREATED_TABLES.put(serialNum, true);
|
||||
} catch (Exception e) {
|
||||
// 检查表不存在是正常情况,使用 USING 语法会自动创建
|
||||
String errorMsg = e.getMessage();
|
||||
if (errorMsg != null && errorMsg.contains("Table does not exist")) {
|
||||
log.debug("子表 {} 不存在,将自动创建", tableName);
|
||||
} else {
|
||||
log.warn("检查子表失败: {}", errorMsg != null && errorMsg.length() > 100 ? errorMsg.substring(0, 100) : errorMsg);
|
||||
log.warn("检查子表失败[{}]: {}", tableName,
|
||||
errorMsg != null && errorMsg.length() > 100 ? errorMsg.substring(0, 100) : errorMsg);
|
||||
}
|
||||
// 即使失败也加入缓存,避免重复尝试
|
||||
CREATED_TABLES.put(serialNum, true);
|
||||
// 建表异常时不加入缓存,下次重试
|
||||
}
|
||||
} else {
|
||||
log.debug("子表 {} 已在缓存中", tableName);
|
||||
@@ -203,6 +211,13 @@ public class DeviceSensorDataService implements IDeviceSensorDataService {
|
||||
public List<DeviceSensorData> getHistoryDataList(String serialNum, String startTime, String endTime, Integer intervalType) {
|
||||
List<DeviceSensorData> list = new ArrayList<>();
|
||||
try {
|
||||
// 检查设备是否已过期
|
||||
Date deadTime = deviceExpireMapper.getDeadTimeBySerialNum(serialNum);
|
||||
if (deadTime != null && deadTime.before(new Date())) {
|
||||
log.info("设备已过期,不返回历史数据: serialNum={}, deadTime={}", serialNum, deadTime);
|
||||
return list;
|
||||
}
|
||||
|
||||
// 默认为原始数据
|
||||
if (intervalType == null) {
|
||||
intervalType = 1;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.intc.tdengine.mapper.DeviceExpireMapper">
|
||||
|
||||
<select id="getDeadTimeBySerialNum" resultType="java.util.Date">
|
||||
SELECT dead_time FROM aqu_device WHERE serial_num = #{serialNum} LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.intc.common.core.exception.ServiceException;
|
||||
import com.intc.common.tenant.helper.TenantHelper;
|
||||
import com.intc.fishery.constant.DefineDeviceWarnCode;
|
||||
import com.intc.fishery.domain.Device;
|
||||
import com.intc.fishery.domain.PayDevice;
|
||||
@@ -24,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 支付订单业务服务实现
|
||||
@@ -54,14 +56,50 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
||||
throw new ServiceException("支付选项不存在");
|
||||
}
|
||||
|
||||
// 2. 验证设备归属
|
||||
List<Device> devices = deviceMapper.selectList(
|
||||
new LambdaQueryWrapper<Device>()
|
||||
.in(Device::getId, deviceIds)
|
||||
.eq(Device::getUserId, userId)
|
||||
// 2. 去重设备ID(防止前端传入重复ID导致 size 不匹配)
|
||||
List<Long> uniqueDeviceIds = deviceIds.stream().distinct().collect(Collectors.toList());
|
||||
if (uniqueDeviceIds.size() != deviceIds.size()) {
|
||||
log.warn("设备ID列表存在重复,原始数量={},去重后数量={}", deviceIds.size(), uniqueDeviceIds.size());
|
||||
}
|
||||
|
||||
// 3. 验证设备归属(忽略租户过滤,因为设备可能由IOT模块写入,租户ID可能不一致)
|
||||
List<Device> devices = TenantHelper.ignore(() ->
|
||||
deviceMapper.selectList(
|
||||
new LambdaQueryWrapper<Device>()
|
||||
.in(Device::getId, uniqueDeviceIds)
|
||||
// .eq(Device::getUserId, userId)
|
||||
)
|
||||
);
|
||||
|
||||
if (devices == null || devices.size() != deviceIds.size()) {
|
||||
if (devices == null || devices.size() != uniqueDeviceIds.size()) {
|
||||
// 诊断日志:定位具体缺失的设备
|
||||
Set<Long> foundIds = devices != null
|
||||
? devices.stream().map(Device::getId).collect(Collectors.toSet())
|
||||
: Collections.emptySet();
|
||||
List<Long> missingIds = uniqueDeviceIds.stream()
|
||||
.filter(id -> !foundIds.contains(id))
|
||||
.collect(Collectors.toList());
|
||||
log.error("设备归属验证失败: userId={}, 请求设备数={}, 找到设备数={}, 缺失设备ID={}",
|
||||
userId, uniqueDeviceIds.size(), devices != null ? devices.size() : 0, missingIds);
|
||||
|
||||
// 进一步诊断:检查缺失设备是否存在于数据库(不限制userId)
|
||||
if (!missingIds.isEmpty()) {
|
||||
List<Device> existDevices = TenantHelper.ignore(() ->
|
||||
deviceMapper.selectList(
|
||||
new LambdaQueryWrapper<Device>()
|
||||
.in(Device::getId, missingIds)
|
||||
)
|
||||
);
|
||||
if (existDevices != null && !existDevices.isEmpty()) {
|
||||
for (Device d : existDevices) {
|
||||
log.error("设备存在但不属于当前用户: deviceId={}, deviceUserId={}, currentUserId={}, tenantId={}",
|
||||
d.getId(), d.getUserId(), userId, d.getTenantId());
|
||||
}
|
||||
} else {
|
||||
log.error("设备在数据库中不存在: missingIds={}", missingIds);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ServiceException("部分设备不存在或无权限操作");
|
||||
}
|
||||
|
||||
@@ -298,16 +336,18 @@ public class PayOrderBusinessServiceImpl implements PayOrderBusinessService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量更新设备到期时间
|
||||
// 批量更新设备到期时间(忽略租户过滤,因为设备可能由IOT模块写入,租户ID可能不一致)
|
||||
for (PayDevice payDevice : payDevices) {
|
||||
Device device = deviceMapper.selectOne(
|
||||
new LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getSerialNum, payDevice.getSerialNum())
|
||||
.eq(Device::getUserId, payDevice.getUserId())
|
||||
// 仅通过 serialNum 查找设备,不限制 userId,因为允许代充(支付者可能与设备所有者不同)
|
||||
Device device = TenantHelper.ignore(() ->
|
||||
deviceMapper.selectOne(
|
||||
new LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getSerialNum, payDevice.getSerialNum())
|
||||
)
|
||||
);
|
||||
|
||||
if (device == null) {
|
||||
log.warn("设备不存在: serialNum={}, userId={}", payDevice.getSerialNum(), payDevice.getUserId());
|
||||
log.warn("设备不存在: serialNum={}", payDevice.getSerialNum());
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user