Compare commits
25 Commits
prod
...
07495f9270
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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::getUserId, aquUser.getId())
|
||||
);
|
||||
|
||||
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 (ObjectUtil.isNull(user)) {
|
||||
log.info("系统中不存在该手机号的用户,自动创建新用户: phone={}", 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,12 @@ aliyun:
|
||||
call-success-suppress-hours: 2
|
||||
# 设备离线告警延迟触发时间(分钟),设备离线超过此时间后才发送告警
|
||||
offline-warn-delay-minutes: 6
|
||||
# 是否启用报警电话通知(全局开关,关闭后所有类型的报警都不会触发电话通知)
|
||||
call-notice-enabled: false
|
||||
# AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret)
|
||||
amqp:
|
||||
# 是否启用
|
||||
enabled: true
|
||||
enabled: false
|
||||
# 数据同步 AppKey(与上面的 app-key 不同!)
|
||||
data-sync-app-key: 334224409
|
||||
# 数据同步 AppSecret(请在阿里云控制台查看)
|
||||
@@ -418,6 +420,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:
|
||||
|
||||
@@ -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),包含开关未挂塘口的控制器也能被找到
|
||||
@@ -719,7 +719,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 +946,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列表
|
||||
|
||||
@@ -101,6 +101,13 @@ public class AliyunIotProperties {
|
||||
*/
|
||||
private int saturabilityWarnHours = 8;
|
||||
|
||||
/**
|
||||
* 是否启用报警电话通知
|
||||
* 全局开关,关闭后所有类型的报警都不会触发电话通知
|
||||
* 默认开启
|
||||
*/
|
||||
private boolean callNoticeEnabled = true;
|
||||
|
||||
@Data
|
||||
public static class VmsMnsConfig {
|
||||
/**
|
||||
|
||||
@@ -188,8 +188,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 +229,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 +303,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
|
||||
@@ -570,7 +579,7 @@ public class DeviceDataHandler {
|
||||
}
|
||||
|
||||
// 仅对开启了电话通知开关的告警触发电话通知(MessageWarn 由通知方法内部插入)
|
||||
if (!callWarnList.isEmpty()) {
|
||||
if (!callWarnList.isEmpty() && aliyunIotProperties.isCallNoticeEnabled()) {
|
||||
triggerAlarmNotification(device, alarmMessage.toString(), sensorData, callWarnList);
|
||||
}
|
||||
}
|
||||
@@ -825,10 +834,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());
|
||||
@@ -903,6 +914,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 +1042,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,6 +1172,11 @@ 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>()
|
||||
@@ -1158,76 +1184,76 @@ public class DeviceDataHandler {
|
||||
.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;
|
||||
}
|
||||
|
||||
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 +1292,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1373,7 +1399,30 @@ public class DeviceDataHandler {
|
||||
String message = null;
|
||||
int switchIndex = 0;
|
||||
|
||||
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) {
|
||||
// 三相开关输出缺相
|
||||
int tempIndex = errorCode - DefineDeviceErrorCode.Three_Switch1LosePhaseA;
|
||||
@@ -1500,10 +1549,12 @@ public class DeviceDataHandler {
|
||||
if (userId != null) {
|
||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, title, WARN_TYPE_DISSOLVED_OXYGEN, message);
|
||||
messageWarnMapper.insert(warn);
|
||||
// 创建电话通知记录
|
||||
// 创建电话通知记录(全局开关控制)
|
||||
if (aliyunIotProperties.isCallNoticeEnabled()) {
|
||||
createCallNotices(device, pondName, warn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 写入 DeviceErrorCode 数据库记录
|
||||
DeviceErrorCode data = new DeviceErrorCode();
|
||||
@@ -1727,7 +1778,8 @@ public class DeviceDataHandler {
|
||||
try {
|
||||
Integer rawCode = params.getInt("sensorErrorCode");
|
||||
if (rawCode == null) {
|
||||
log.warn("[探头错误码] 解析 sensorErrorCode 失败: {}", deviceName);
|
||||
// 部分设备/数据包不携带此字段,属于正常情况
|
||||
log.debug("[探头错误码] 设备上报数据不包含 sensorErrorCode 字段: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
int sensorErrorCode = rawCode;
|
||||
@@ -1807,10 +1859,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 +1888,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 +2130,6 @@ public class DeviceDataHandler {
|
||||
// 设备标识(使用 deviceName 作为 serialNum)
|
||||
sensorData.setSerialNum(deviceName);
|
||||
sensorData.setDeviceName(deviceName);
|
||||
|
||||
// 解析传感器数据(根据实际字段映射)
|
||||
// 支持小写和驼峰命名两种格式
|
||||
if (params.containsKey("dissolvedoxygen") || params.containsKey("dissolvedOxygen")) {
|
||||
@@ -2077,22 +2137,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);
|
||||
}
|
||||
// 通过遍历 keySet 做精确字符串比较,彻底避免 Hutool ignoreCase 下 containsKey/getDouble 误匹配 salinitySet
|
||||
for (String k : params.keySet()) {
|
||||
if ("salinity".equals(k)) {
|
||||
sensorData.setSalinity(params.getDouble(k));
|
||||
break;
|
||||
}
|
||||
if (params.containsKey("salinity")) {
|
||||
sensorData.setSalinity(params.getDouble("salinity"));
|
||||
}
|
||||
if (params.containsKey("treference") || params.containsKey("Treference")) {
|
||||
Double value = params.containsKey("Treference") ?
|
||||
@@ -2104,8 +2170,11 @@ 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"));
|
||||
@@ -2194,7 +2263,7 @@ public class DeviceDataHandler {
|
||||
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)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (currentDevice == null || currentDevice.getId() == null) {
|
||||
@@ -2290,7 +2359,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 +2382,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 +2413,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 +2426,14 @@ public class DeviceDataHandler {
|
||||
|
||||
/**
|
||||
* 按联动控制开启/关闭一组开关(参考 C# SetDeviceSwitchLinkedOpen)
|
||||
* 下发成功后更新 DeviceSwitch.isOpen 和 lastTurnTime。
|
||||
* 下发成功后更新 DeviceSwitch.isOpen 和 lastTurnTime,并写入操作记录。
|
||||
*
|
||||
* @param device 设备信息(含 iotId)
|
||||
* @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 +2463,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,31 +149,6 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段名映射:将阿里云字段名映射为应用字段名
|
||||
*/
|
||||
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());
|
||||
|
||||
Reference in New Issue
Block a user