diff --git a/fastbee-common/src/main/java/com/fastbee/common/enums/DataLevelEnum.java b/fastbee-common/src/main/java/com/fastbee/common/enums/DataLevelEnum.java
index 2274cd88..7be892b8 100644
--- a/fastbee-common/src/main/java/com/fastbee/common/enums/DataLevelEnum.java
+++ b/fastbee-common/src/main/java/com/fastbee/common/enums/DataLevelEnum.java
@@ -8,9 +8,9 @@ package com.fastbee.common.enums;
*/
public enum DataLevelEnum {
- GREEN(1, "✅ 正常", "#009900"),
- YELLOW(2, "⚠️ 告警", "#FF9900"),
- RED(3, "🔴 警报", "#FF0000");
+ GREEN(1, "正常", "#009900"),
+ YELLOW(2, "告警", "#FF9900"),
+ RED(3, "警报", "#FF0000");
private final int code;
private final String label;
diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushHandler.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushHandler.java
index 9e66b46c..7b083e00 100644
--- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushHandler.java
+++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/push/ThingsModelPushHandler.java
@@ -1,9 +1,11 @@
package com.fastbee.iot.data.push;
import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.common.core.notify.NotifySendResponse;
import com.fastbee.common.core.notify.config.WeChatConfigParams;
+import com.fastbee.common.core.notify.msg.WechatMsgParams;
import com.fastbee.common.core.redis.RedisCache;
import com.fastbee.common.core.redis.RedisKeyBuilder;
import com.fastbee.common.enums.DataLevelEnum;
@@ -11,15 +13,17 @@ import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.wechat.WechatUtils;
import com.fastbee.common.wechat.WeChatAppResult;
import com.fastbee.iot.domain.PushModelItem;
+import com.fastbee.iot.domain.SocialUser;
import com.fastbee.iot.domain.ThingsModelPushConfig;
import com.fastbee.iot.model.ThingsModels.ValueItem;
import com.fastbee.iot.service.IThingsModelPushConfigService;
-import com.fastbee.notify.domain.NotifyChannel;
+import com.fastbee.iot.service.ISocialUserService;
import com.fastbee.notify.core.wechat.vo.TemplateDataVo;
import com.fastbee.notify.core.wechat.vo.WeChatPublicAccountPushVO;
+import com.fastbee.notify.domain.NotifyChannel;
+import com.fastbee.notify.domain.NotifyTemplate;
import com.fastbee.notify.service.INotifyChannelService;
-import com.fastbee.iot.service.ISocialUserService;
-import com.fastbee.iot.domain.SocialUser;
+import com.fastbee.notify.service.INotifyTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.http.HttpEntity;
@@ -28,28 +32,31 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
-import com.alibaba.fastjson2.JSONObject;
import javax.annotation.Resource;
import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 物模型数据推送核心处理器
- * 读取 Redis 中设备最新上报值,按配置的阈值评估数据等级,
- * 同等级数据合并为一条微信服务号模板消息推送。
- *
- * @author fastbee
+ *
+ * 复用"运维管理 - 通知模板"中已配置的微信公众号模板(notify_template),
+ * 从中读取 templateId、content(变量)、redirectUrl 等信息。
*/
@Slf4j
@Component
public class ThingsModelPushHandler {
private static final RestTemplate REST_TEMPLATE = new RestTemplate();
+ private static final Pattern VAR_PATTERN = Pattern.compile("\\{\\{([^}]+)}}");
@Resource
private IThingsModelPushConfigService pushConfigService;
@Resource
+ private INotifyTemplateService notifyTemplateService;
+ @Resource
private INotifyChannelService notifyChannelService;
@Resource
private ISocialUserService socialUserService;
@@ -57,20 +64,35 @@ public class ThingsModelPushHandler {
private RedisCache redisCache;
/**
- * 按推送配置ID执行推送(读取该配置涉及设备的最新 Redis 值)
- *
- * @param config 推送配置(含 items、userIds)
+ * 按推送配置执行推送
*/
public void doPush(ThingsModelPushConfig config) {
if (CollectionUtils.isEmpty(config.getItems())) {
log.warn("[Push] configId={} 没有配置物模型项,跳过", config.getId());
return;
}
+ if (config.getDeviceId() == null || config.getDeviceId() <= 0
+ || StringUtils.isEmpty(config.getSerialNumber())) {
+ log.warn("[Push] configId={} 未指定具体设备,跳过", config.getId());
+ return;
+ }
- // 获取通知渠道(公众号)
- NotifyChannel channel = notifyChannelService.selectNotifyChannelById(config.getChannelId());
+ // ---------- 从通知模板获取微信配置 ----------
+ NotifyTemplate notifyTemplate = notifyTemplateService.selectNotifyTemplateById(config.getTemplateId());
+ if (notifyTemplate == null) {
+ log.warn("[Push] configId={} 通知模板不存在 templateId={}", config.getId(), config.getTemplateId());
+ return;
+ }
+ WechatMsgParams msgParams = JSONObject.parseObject(notifyTemplate.getMsgParams(), WechatMsgParams.class);
+ if (msgParams == null || StringUtils.isEmpty(msgParams.getTemplateId())) {
+ log.warn("[Push] configId={} 通知模板 msgParams 不完整", config.getId());
+ return;
+ }
+
+ // ---------- 从通知渠道获取 appId/appSecret ----------
+ NotifyChannel channel = notifyChannelService.selectNotifyChannelById(notifyTemplate.getChannelId());
if (channel == null) {
- log.warn("[Push] configId={} 渠道不存在 channelId={}", config.getId(), config.getChannelId());
+ log.warn("[Push] configId={} 渠道不存在 channelId={}", config.getId(), notifyTemplate.getChannelId());
return;
}
WeChatConfigParams wxConfig = JSONObject.parseObject(channel.getConfigContent(), WeChatConfigParams.class);
@@ -79,7 +101,7 @@ public class ThingsModelPushHandler {
return;
}
- // 获取 access_token
+ // ---------- 获取 access_token ----------
WeChatAppResult tokenResult = WechatUtils.getAccessToken(wxConfig.getAppId(), wxConfig.getAppSecret());
if (tokenResult == null || StringUtils.isEmpty(tokenResult.getAccessToken())) {
log.error("[Push] configId={} 获取微信 access_token 失败", config.getId());
@@ -87,14 +109,7 @@ public class ThingsModelPushHandler {
}
String pushUrl = FastBeeConstant.URL.WX_PUBLIC_ACCOUNT_TEMPLATE_SEND_URL_PREFIX + tokenResult.getAccessToken();
- // 确定要查询的设备编号列表
- List serialNumbers = resolveSerialNumbers(config);
- if (CollectionUtils.isEmpty(serialNumbers)) {
- log.warn("[Push] configId={} 没有找到对应设备,跳过", config.getId());
- return;
- }
-
- // 获取接收用户 openId 列表
+ // ---------- 获取接收用户 openId ----------
List userIds = config.getUserIds();
if (CollectionUtils.isEmpty(userIds)) {
log.warn("[Push] configId={} 没有配置接收用户,跳过", config.getId());
@@ -103,28 +118,31 @@ public class ThingsModelPushHandler {
List socialUsers = socialUserService.listWechatPublicAccountOpenId(
userIds.stream().map(Object::toString).collect(Collectors.toList()));
Map openIdMap = socialUsers.stream()
+ .filter(u -> StringUtils.isNotEmpty(u.getOpenId()))
.collect(Collectors.toMap(SocialUser::getSysUserId, SocialUser::getOpenId, (a, b) -> b));
-
- // 对每台设备推送
- for (String serialNumber : serialNumbers) {
- pushForDevice(config, serialNumber, pushUrl, openIdMap);
+ if (openIdMap.isEmpty()) {
+ log.warn("[Push] configId={} 所有用户均未绑定微信公众号,跳过", config.getId());
+ return;
}
+ // ---------- 执行推送 ----------
+ pushForDevice(config, msgParams, pushUrl, openIdMap);
+
// 更新最近推送时间
pushConfigService.updateLastPushTime(config.getId());
}
/**
- * 对单台设备执行推送
+ * 对单台设备按等级分组推送
*/
- private void pushForDevice(ThingsModelPushConfig config, String serialNumber,
+ private void pushForDevice(ThingsModelPushConfig config, WechatMsgParams msgParams,
String pushUrl, Map openIdMap) {
+ String serialNumber = config.getSerialNumber().toUpperCase();
String redisKey = RedisKeyBuilder.buildTSLVCacheKey(config.getProductId(), serialNumber);
- // 按等级分组收集显示文本
Map> levelTextMap = new LinkedHashMap<>();
- for (DataLevelEnum level : DataLevelEnum.values()) {
- levelTextMap.put(level, new ArrayList<>());
+ for (DataLevelEnum lvl : DataLevelEnum.values()) {
+ levelTextMap.put(lvl, new ArrayList<>());
}
for (PushModelItem item : config.getItems()) {
@@ -134,8 +152,7 @@ public class ThingsModelPushHandler {
}
String value;
try {
- ValueItem vi = JSON.parseObject(cacheValue, ValueItem.class);
- value = vi.getValue();
+ value = JSON.parseObject(cacheValue, ValueItem.class).getValue();
} catch (Exception e) {
value = cacheValue;
}
@@ -143,57 +160,69 @@ public class ThingsModelPushHandler {
value, item.getGreenMin(), item.getGreenMax(),
item.getYellowMin(), item.getYellowMax());
- String displayName = StringUtils.isNotEmpty(item.getModelName()) ? item.getModelName() : item.getIdentifier();
- String text = displayName + "=" + value + (StringUtils.isNotEmpty(item.getUnit()) ? item.getUnit() : "");
+ String displayName = StringUtils.isNotEmpty(item.getModelName())
+ ? item.getModelName() : item.getIdentifier();
+ String text = displayName + "=" + value
+ + (StringUtils.isNotEmpty(item.getUnit()) ? item.getUnit() : "");
levelTextMap.get(level).add(text);
}
- // 对每个非空等级发一条消息
for (Map.Entry> entry : levelTextMap.entrySet()) {
if (CollectionUtils.isEmpty(entry.getValue())) {
continue;
}
DataLevelEnum level = entry.getKey();
String dataText = String.join(";", entry.getValue());
- sendTemplateMsg(config, serialNumber, level, dataText, pushUrl, openIdMap);
+ sendTemplateMsg(config, msgParams, serialNumber, level, dataText, pushUrl, openIdMap);
}
}
/**
- * 发送单条微信模板消息(带颜色标记)
+ * 发送单条微信模板消息。
+ * 从通知模板的 content 字段提取 {{变量.DATA}} 变量名顺序,
+ * 按顺序映射:第1个→设备名称,第2个→数据等级,第3个→监测数据。
*/
- private void sendTemplateMsg(ThingsModelPushConfig config, String serialNumber,
- DataLevelEnum level, String dataText,
+ private void sendTemplateMsg(ThingsModelPushConfig config, WechatMsgParams msgParams,
+ String serialNumber, DataLevelEnum level, String dataText,
String pushUrl, Map openIdMap) {
- // 微信模板格式(需在公众号后台与此对应):
- // {{first.DATA}}
- // 设备编号:{{keyword1.DATA}}
- // 数据等级:{{keyword2.DATA}}
- // 监测数据:{{keyword3.DATA}}
- // {{remark.DATA}}
- Map data = new LinkedHashMap<>();
- data.put("first", new TemplateDataVo("设备数据监测通知", "#333333"));
- data.put("keyword1", new TemplateDataVo(serialNumber, "#333333"));
- data.put("keyword2", new TemplateDataVo(level.getLabel(), level.getColor()));
- data.put("keyword3", new TemplateDataVo(dataText, level.getColor()));
- data.put("remark", new TemplateDataVo("请及时关注设备状态", "#999999"));
+ List varNames = extractVarNames(msgParams.getContent());
+
+ LinkedHashMap map = new LinkedHashMap<>();
+ String deviceDisplay = StringUtils.isNotEmpty(config.getDeviceName())
+ ? config.getDeviceName() : serialNumber;
+ for (int i = 0; i < varNames.size(); i++) {
+ String varName = varNames.get(i);
+ if (i == 0) {
+ map.put(varName, deviceDisplay);
+ } else if (i == 1) {
+ map.put(varName, level.getLabel());
+ } else if (i == 2) {
+ map.put(varName, dataText);
+ } else {
+ map.put(varName, "");
+ }
+ }
+
+ // 与 WeChatPushServiceImpl 保持一致的 map 迭代方式
+ Map sendMap = new LinkedHashMap<>();
+ for (Map.Entry m : map.entrySet()) {
+ sendMap.put(m.getKey(), new TemplateDataVo(m.getValue(), level.getColor()));
+ }
for (Map.Entry userEntry : openIdMap.entrySet()) {
String openId = userEntry.getValue();
- if (StringUtils.isEmpty(openId)) {
- continue;
- }
WeChatPublicAccountPushVO vo = new WeChatPublicAccountPushVO();
vo.setTouser(openId);
- vo.setTemplateId(config.getWxTemplateId());
- vo.setData(data);
- if (StringUtils.isNotEmpty(config.getRedirectUrl())) {
- vo.setUrl(config.getRedirectUrl());
+ vo.setTemplateId(msgParams.getTemplateId());
+ vo.setData(sendMap);
+ if (StringUtils.isNotEmpty(msgParams.getRedirectUrl())) {
+ vo.setUrl(msgParams.getRedirectUrl());
}
try {
NotifySendResponse resp = doPost(JSON.toJSONString(vo), pushUrl);
log.info("[Push] configId={} sn={} userId={} level={} status={}",
- config.getId(), serialNumber, userEntry.getKey(), level.getLabel(), resp.getStatus());
+ config.getId(), serialNumber, userEntry.getKey(),
+ level.getLabel(), resp.getStatus());
} catch (Exception e) {
log.error("[Push] configId={} sn={} userId={} 推送异常: {}",
config.getId(), serialNumber, userEntry.getKey(), e.getMessage());
@@ -201,13 +230,25 @@ public class ThingsModelPushHandler {
}
}
+ private List extractVarNames(String content) {
+ List names = new ArrayList<>();
+ if (StringUtils.isEmpty(content)) {
+ return names;
+ }
+ Matcher matcher = VAR_PATTERN.matcher(content);
+ while (matcher.find()) {
+ names.add(matcher.group(1).replace(".DATA", ""));
+ }
+ return names;
+ }
+
private NotifySendResponse doPost(String json, String url) {
NotifySendResponse resp = new NotifySendResponse();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/json; charset=UTF-8"));
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
- ResponseEntity result = REST_TEMPLATE.postForEntity(url,
- new HttpEntity<>(json, headers), String.class);
+ ResponseEntity result = REST_TEMPLATE.postForEntity(
+ url, new HttpEntity<>(json, headers), String.class);
String body = result.getBody();
resp.setResultContent(body);
try {
@@ -217,28 +258,4 @@ public class ThingsModelPushHandler {
}
return resp;
}
-
- /**
- * 确定推送涉及的设备编号列表
- * device_id=0 → 产品级,需查产品下所有设备
- * device_id>0 → 指定设备
- */
- private List resolveSerialNumbers(ThingsModelPushConfig config) {
- if (config.getDeviceId() != null && config.getDeviceId() > 0) {
- if (StringUtils.isNotEmpty(config.getSerialNumber())) {
- return Collections.singletonList(config.getSerialNumber().toUpperCase());
- }
- return Collections.emptyList();
- }
- // 产品级:从 Redis 中找该产品所有已有 TSLV 缓存的设备(通过前缀扫描)
- String pattern = FastBeeConstant.REDIS.DEVICE_PRE_KEY + config.getProductId() + "_*";
- Collection keys = redisCache.keys(pattern);
- if (keys == null || keys.isEmpty()) {
- return Collections.emptyList();
- }
- String prefix = FastBeeConstant.REDIS.DEVICE_PRE_KEY + config.getProductId() + "_";
- return keys.stream()
- .map(k -> k.substring(prefix.length()))
- .collect(Collectors.toList());
- }
}
diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ThingsModelPushConfig.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ThingsModelPushConfig.java
index cff47bf0..8d5fbb84 100644
--- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ThingsModelPushConfig.java
+++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ThingsModelPushConfig.java
@@ -31,26 +31,26 @@ public class ThingsModelPushConfig extends BaseEntity {
@ApiModelProperty("产品ID")
private Long productId;
- @ApiModelProperty("产品名称(非持久化字段)")
+ @ApiModelProperty("产品名称(非持久化,JOIN查询)")
private String productName;
- @ApiModelProperty("设备ID,0=产品级")
+ @ApiModelProperty("设备ID(必须指定具体设备)")
private Long deviceId;
+ @ApiModelProperty("设备名称(非持久化,JOIN查询)")
+ private String deviceName;
+
+ @ApiModelProperty("设备类型:1=直连 2=网关(非持久化,JOIN查询)")
+ private Integer deviceType;
+
@ApiModelProperty("设备编号")
private String serialNumber;
- @ApiModelProperty("通知渠道ID")
- private Long channelId;
+ @ApiModelProperty("通知模板ID(关联 notify_template)")
+ private Long templateId;
- @ApiModelProperty("通知渠道名称(非持久化字段)")
- private String channelName;
-
- @ApiModelProperty("微信公众号模板ID")
- private String wxTemplateId;
-
- @ApiModelProperty("消息点击跳转URL")
- private String redirectUrl;
+ @ApiModelProperty("通知模板名称(非持久化,JOIN查询)")
+ private String templateName;
@ApiModelProperty("推送间隔(分钟),0=仅手动推送")
private Integer pushInterval;
diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelPushConfigMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelPushConfigMapper.xml
index 963be1e7..84d84876 100644
--- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelPushConfigMapper.xml
+++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelPushConfigMapper.xml
@@ -4,28 +4,28 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
@@ -52,36 +52,38 @@
c.id, c.config_name, c.product_id, c.device_id, c.serial_number,
- c.channel_id, c.wx_template_id, c.redirect_url,
- c.push_interval, c.last_push_time, c.status,
+ c.template_id, c.push_interval, c.last_push_time, c.status,
c.tenant_id, c.tenant_name,
c.create_by, c.create_time, c.update_by, c.update_time,
- p.product_name, nc.name AS channel_name
+ p.product_name, p.device_type, d.device_name, nt.name AS template_name
+
+
+
+ FROM iot_things_model_push_config c
+ LEFT JOIN iot_device d ON c.device_id = d.device_id
+ LEFT JOIN iot_product p ON c.product_id = p.product_id
+ LEFT JOIN notify_template nt ON c.template_id = nt.id