Compare commits

23 Commits

Author SHA1 Message Date
tianyongbao
45c55944a5 fix: 日志字符转义修复。 2026-04-12 20:49:56 +08:00
tianyongbao
2877fc9b7c fix: 控制器不报警,问题解决。 2026-04-12 20:43:35 +08:00
tianyongbao
8de7460d92 fix: 用户普通登录,接口新增几个字段。 2026-04-12 12:08:56 +08:00
tianyongbao
e1745c6130 fix: 修改微信用户和验证码登录,改为永不因不活跃过期,token时间1年有效。 2026-04-11 21:50:44 +08:00
tianyongbao
9b5f87342b fix: 修改微信用户登录默认token时间,改为不活跃7天,超时30天。 2026-04-11 20:25:43 +08:00
tianyongbao
cddaa2c63c fix: 测试服,取消接入amqp,数据库改用内网。 2026-04-11 17:01:47 +08:00
tianyongbao
222f74a7e1 fix: 测试服,取消电话通知。 2026-04-11 10:59:44 +08:00
tianyongbao
261486d235 fix: 回执时间过长,问题修复。 2026-04-10 22:10:38 +08:00
tianyongbao
11337ee89e fix: 盐度取值错误,问题彻底解决。 2026-04-10 14:03:47 +08:00
tianyongbao
74ec9caba8 fix: 盐度取值错误,问题再次解决。 2026-04-10 12:25:10 +08:00
tianyongbao
4fc7eb8006 fix: 盐度取值错误,问题解决。 2026-04-10 12:06:15 +08:00
tianyongbao
881ab2b4a6 fix: 相位差不上数,修复。 2026-04-10 11:21:46 +08:00
tianyongbao
2673eba9ce fix: 水温,饱和度等不上数,问题解决。 2026-04-10 10:58:12 +08:00
tianyongbao
38c038a950 fix: 离线报警逻辑问题修复。 2026-04-09 09:18:00 +08:00
tianyongbao
eeb9ca774e fix:测试问题修复,ph值。 2026-04-09 00:03:26 +08:00
tianyongbao
a50f9ee4c9 fix: 测试环境修改数据库配置。 2026-04-06 21:14:31 +08:00
tianyongbao
db7e0538aa fix: 塘口、设备按创建时间进行正序排列。 2026-04-05 13:29:06 +08:00
tianyongbao
b5c4bab4d4 fix: 微信支付回调接口加入白名单。 2026-04-05 13:05:22 +08:00
tianyongbao
83834ac4da fix: 用户登录逻辑判断修改。 2026-04-05 12:13:49 +08:00
tianyongbao
20cb24b0a1 fix: test分支,电话通知不开启。 2026-04-05 00:10:27 +08:00
tianyongbao
4d82c464ee fix: 配置文件加上电话通知报警是否开启。 2026-04-05 00:09:24 +08:00
tianyongbao
f67ae05413 fix: 新增电话告警统一配置,是否开启。 2026-04-05 00:05:45 +08:00
tianyongbao
9cdcd312cb fix: 测试环境配置文件修改。 2026-04-04 23:54:04 +08:00
13 changed files with 227 additions and 102 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);
}
}
// 检查用户状态

View File

@@ -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"

View File

@@ -121,6 +121,7 @@ security:
- /iot/test
- /iot/device/**
- /iot/amqp/**
- /weixin/pay_notify
# 多租户配置
tenant:

View File

@@ -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包含开关未挂塘口的控制器也能被找到
@@ -601,12 +601,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 +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列表

View File

@@ -101,6 +101,13 @@ public class AliyunIotProperties {
*/
private int saturabilityWarnHours = 8;
/**
* 是否启用报警电话通知
* 全局开关,关闭后所有类型的报警都不会触发电话通知
* 默认开启
*/
private boolean callNoticeEnabled = true;
@Data
public static class VmsMnsConfig {
/**

View File

@@ -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,8 +1524,10 @@ public class DeviceDataHandler {
if (userId != null) {
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, title, WARN_TYPE_DISSOLVED_OXYGEN, message);
messageWarnMapper.insert(warn);
// 创建电话通知记录
createCallNotices(device, pondName, warn);
// 创建电话通知记录(全局开关控制)
if (aliyunIotProperties.isCallNoticeEnabled()) {
createCallNotices(device, pondName, warn);
}
}
}
@@ -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);
}
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,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"));

View File

@@ -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);
}

View File

@@ -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 判断的属性消息(飞燕平台原始格式)

View File

@@ -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();
}
}