Compare commits
23 Commits
prod
...
45c55944a5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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);
|
||||
}
|
||||
}
|
||||
@@ -903,6 +912,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 +1040,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 +1170,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 +1182,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 +1290,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1500,10 +1524,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 +1753,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 +1834,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 +1863,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 +2105,6 @@ public class DeviceDataHandler {
|
||||
// 设备标识(使用 deviceName 作为 serialNum)
|
||||
sensorData.setSerialNum(deviceName);
|
||||
sensorData.setDeviceName(deviceName);
|
||||
|
||||
// 解析传感器数据(根据实际字段映射)
|
||||
// 支持小写和驼峰命名两种格式
|
||||
if (params.containsKey("dissolvedoxygen") || params.containsKey("dissolvedOxygen")) {
|
||||
@@ -2077,22 +2112,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 +2145,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"));
|
||||
|
||||
@@ -115,4 +115,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,9 +56,10 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
|
||||
/**
|
||||
* 回执超时时间(分钟)
|
||||
* VMS 回执延迟较长,改为 5 分钟,避免回执未到就被标记为超时
|
||||
* VMS 回执延迟较长,设为 15 分钟,给 VMS 平台充足的回调缓冲时间
|
||||
* 注意:超时判断基于 callTime(预计呼叫时间),实际呼叫发起后回调可能延迟数分钟到达
|
||||
*/
|
||||
private static final int CALLBACK_TIMEOUT_MINUTES = 5;
|
||||
private static final int CALLBACK_TIMEOUT_MINUTES = 15;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -104,7 +105,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 +258,18 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
int sentCount = 0;
|
||||
for (AquWarnCallNotice notice : pendingList) {
|
||||
try {
|
||||
// 查询关联的告警消息内容,用于构建 TTS 参数
|
||||
String rawMessage = warnCallNoticeMapper.selectWarnMessageByCallNoticeId(notice.getId());
|
||||
String warnMessageShort = parseWarnMessageShort(rawMessage);
|
||||
// 查询关联的告警标题(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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user