20260326:数据推送

This commit is contained in:
zhugongping 2026-03-28 10:38:52 +08:00
parent b7e0984654
commit c9620a344c
6 changed files with 327 additions and 244 deletions

View File

@ -8,9 +8,9 @@ package com.fastbee.common.enums;
*/ */
public enum DataLevelEnum { public enum DataLevelEnum {
GREEN(1, "✅ 正常", "#009900"), GREEN(1, "正常", "#009900"),
YELLOW(2, "⚠️ 告警", "#FF9900"), YELLOW(2, "告警", "#FF9900"),
RED(3, "🔴 警报", "#FF0000"); RED(3, "警报", "#FF0000");
private final int code; private final int code;
private final String label; private final String label;

View File

@ -1,9 +1,11 @@
package com.fastbee.iot.data.push; package com.fastbee.iot.data.push;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.common.core.notify.NotifySendResponse; import com.fastbee.common.core.notify.NotifySendResponse;
import com.fastbee.common.core.notify.config.WeChatConfigParams; 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.RedisCache;
import com.fastbee.common.core.redis.RedisKeyBuilder; import com.fastbee.common.core.redis.RedisKeyBuilder;
import com.fastbee.common.enums.DataLevelEnum; 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.utils.wechat.WechatUtils;
import com.fastbee.common.wechat.WeChatAppResult; import com.fastbee.common.wechat.WeChatAppResult;
import com.fastbee.iot.domain.PushModelItem; import com.fastbee.iot.domain.PushModelItem;
import com.fastbee.iot.domain.SocialUser;
import com.fastbee.iot.domain.ThingsModelPushConfig; import com.fastbee.iot.domain.ThingsModelPushConfig;
import com.fastbee.iot.model.ThingsModels.ValueItem; import com.fastbee.iot.model.ThingsModels.ValueItem;
import com.fastbee.iot.service.IThingsModelPushConfigService; 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.TemplateDataVo;
import com.fastbee.notify.core.wechat.vo.WeChatPublicAccountPushVO; 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.notify.service.INotifyChannelService;
import com.fastbee.iot.service.ISocialUserService; import com.fastbee.notify.service.INotifyTemplateService;
import com.fastbee.iot.domain.SocialUser;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
@ -28,28 +32,31 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.alibaba.fastjson2.JSONObject;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 物模型数据推送核心处理器 * 物模型数据推送核心处理器
* 读取 Redis 中设备最新上报值,按配置的阈值评估数据等级, * <p>
* 同等级数据合并为一条微信服务号模板消息推送。 * 复用"运维管理 - 通知模板"中已配置的微信公众号模板(notify_template),
* * 从中读取 templateId、content(变量)、redirectUrl 等信息。
* @author fastbee
*/ */
@Slf4j @Slf4j
@Component @Component
public class ThingsModelPushHandler { public class ThingsModelPushHandler {
private static final RestTemplate REST_TEMPLATE = new RestTemplate(); private static final RestTemplate REST_TEMPLATE = new RestTemplate();
private static final Pattern VAR_PATTERN = Pattern.compile("\\{\\{([^}]+)}}");
@Resource @Resource
private IThingsModelPushConfigService pushConfigService; private IThingsModelPushConfigService pushConfigService;
@Resource @Resource
private INotifyTemplateService notifyTemplateService;
@Resource
private INotifyChannelService notifyChannelService; private INotifyChannelService notifyChannelService;
@Resource @Resource
private ISocialUserService socialUserService; private ISocialUserService socialUserService;
@ -57,20 +64,35 @@ public class ThingsModelPushHandler {
private RedisCache redisCache; private RedisCache redisCache;
/** /**
* 按推送配置ID执行推送(读取该配置涉及设备的最新 Redis 值) * 按推送配置执行推送
*
* @param config 推送配置(含 items、userIds)
*/ */
public void doPush(ThingsModelPushConfig config) { public void doPush(ThingsModelPushConfig config) {
if (CollectionUtils.isEmpty(config.getItems())) { if (CollectionUtils.isEmpty(config.getItems())) {
log.warn("[Push] configId={} 没有配置物模型项,跳过", config.getId()); log.warn("[Push] configId={} 没有配置物模型项,跳过", config.getId());
return; 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) { if (channel == null) {
log.warn("[Push] configId={} 渠道不存在 channelId={}", config.getId(), config.getChannelId()); log.warn("[Push] configId={} 渠道不存在 channelId={}", config.getId(), notifyTemplate.getChannelId());
return; return;
} }
WeChatConfigParams wxConfig = JSONObject.parseObject(channel.getConfigContent(), WeChatConfigParams.class); WeChatConfigParams wxConfig = JSONObject.parseObject(channel.getConfigContent(), WeChatConfigParams.class);
@ -79,7 +101,7 @@ public class ThingsModelPushHandler {
return; return;
} }
// 获取 access_token // ---------- 获取 access_token ----------
WeChatAppResult tokenResult = WechatUtils.getAccessToken(wxConfig.getAppId(), wxConfig.getAppSecret()); WeChatAppResult tokenResult = WechatUtils.getAccessToken(wxConfig.getAppId(), wxConfig.getAppSecret());
if (tokenResult == null || StringUtils.isEmpty(tokenResult.getAccessToken())) { if (tokenResult == null || StringUtils.isEmpty(tokenResult.getAccessToken())) {
log.error("[Push] configId={} 获取微信 access_token 失败", config.getId()); 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(); String pushUrl = FastBeeConstant.URL.WX_PUBLIC_ACCOUNT_TEMPLATE_SEND_URL_PREFIX + tokenResult.getAccessToken();
// 确定要查询的设备编号列表 // ---------- 获取接收用户 openId ----------
List<String> serialNumbers = resolveSerialNumbers(config);
if (CollectionUtils.isEmpty(serialNumbers)) {
log.warn("[Push] configId={} 没有找到对应设备,跳过", config.getId());
return;
}
// 获取接收用户 openId 列表
List<Long> userIds = config.getUserIds(); List<Long> userIds = config.getUserIds();
if (CollectionUtils.isEmpty(userIds)) { if (CollectionUtils.isEmpty(userIds)) {
log.warn("[Push] configId={} 没有配置接收用户,跳过", config.getId()); log.warn("[Push] configId={} 没有配置接收用户,跳过", config.getId());
@ -103,28 +118,31 @@ public class ThingsModelPushHandler {
List<SocialUser> socialUsers = socialUserService.listWechatPublicAccountOpenId( List<SocialUser> socialUsers = socialUserService.listWechatPublicAccountOpenId(
userIds.stream().map(Object::toString).collect(Collectors.toList())); userIds.stream().map(Object::toString).collect(Collectors.toList()));
Map<Long, String> openIdMap = socialUsers.stream() Map<Long, String> openIdMap = socialUsers.stream()
.filter(u -> StringUtils.isNotEmpty(u.getOpenId()))
.collect(Collectors.toMap(SocialUser::getSysUserId, SocialUser::getOpenId, (a, b) -> b)); .collect(Collectors.toMap(SocialUser::getSysUserId, SocialUser::getOpenId, (a, b) -> b));
if (openIdMap.isEmpty()) {
// 对每台设备推送 log.warn("[Push] configId={} 所有用户均未绑定微信公众号,跳过", config.getId());
for (String serialNumber : serialNumbers) { return;
pushForDevice(config, serialNumber, pushUrl, openIdMap);
} }
// ---------- 执行推送 ----------
pushForDevice(config, msgParams, pushUrl, openIdMap);
// 更新最近推送时间 // 更新最近推送时间
pushConfigService.updateLastPushTime(config.getId()); pushConfigService.updateLastPushTime(config.getId());
} }
/** /**
* 对单台设备执行推送 * 对单台设备按等级分组推送
*/ */
private void pushForDevice(ThingsModelPushConfig config, String serialNumber, private void pushForDevice(ThingsModelPushConfig config, WechatMsgParams msgParams,
String pushUrl, Map<Long, String> openIdMap) { String pushUrl, Map<Long, String> openIdMap) {
String serialNumber = config.getSerialNumber().toUpperCase();
String redisKey = RedisKeyBuilder.buildTSLVCacheKey(config.getProductId(), serialNumber); String redisKey = RedisKeyBuilder.buildTSLVCacheKey(config.getProductId(), serialNumber);
// 按等级分组收集显示文本
Map<DataLevelEnum, List<String>> levelTextMap = new LinkedHashMap<>(); Map<DataLevelEnum, List<String>> levelTextMap = new LinkedHashMap<>();
for (DataLevelEnum level : DataLevelEnum.values()) { for (DataLevelEnum lvl : DataLevelEnum.values()) {
levelTextMap.put(level, new ArrayList<>()); levelTextMap.put(lvl, new ArrayList<>());
} }
for (PushModelItem item : config.getItems()) { for (PushModelItem item : config.getItems()) {
@ -134,8 +152,7 @@ public class ThingsModelPushHandler {
} }
String value; String value;
try { try {
ValueItem vi = JSON.parseObject(cacheValue, ValueItem.class); value = JSON.parseObject(cacheValue, ValueItem.class).getValue();
value = vi.getValue();
} catch (Exception e) { } catch (Exception e) {
value = cacheValue; value = cacheValue;
} }
@ -143,57 +160,69 @@ public class ThingsModelPushHandler {
value, item.getGreenMin(), item.getGreenMax(), value, item.getGreenMin(), item.getGreenMax(),
item.getYellowMin(), item.getYellowMax()); item.getYellowMin(), item.getYellowMax());
String displayName = StringUtils.isNotEmpty(item.getModelName()) ? item.getModelName() : item.getIdentifier(); String displayName = StringUtils.isNotEmpty(item.getModelName())
String text = displayName + "=" + value + (StringUtils.isNotEmpty(item.getUnit()) ? item.getUnit() : ""); ? item.getModelName() : item.getIdentifier();
String text = displayName + "=" + value
+ (StringUtils.isNotEmpty(item.getUnit()) ? item.getUnit() : "");
levelTextMap.get(level).add(text); levelTextMap.get(level).add(text);
} }
// 对每个非空等级发一条消息
for (Map.Entry<DataLevelEnum, List<String>> entry : levelTextMap.entrySet()) { for (Map.Entry<DataLevelEnum, List<String>> entry : levelTextMap.entrySet()) {
if (CollectionUtils.isEmpty(entry.getValue())) { if (CollectionUtils.isEmpty(entry.getValue())) {
continue; continue;
} }
DataLevelEnum level = entry.getKey(); DataLevelEnum level = entry.getKey();
String dataText = String.join(";", entry.getValue()); 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, private void sendTemplateMsg(ThingsModelPushConfig config, WechatMsgParams msgParams,
DataLevelEnum level, String dataText, String serialNumber, DataLevelEnum level, String dataText,
String pushUrl, Map<Long, String> openIdMap) { String pushUrl, Map<Long, String> openIdMap) {
// 微信模板格式(需在公众号后台与此对应): List<String> varNames = extractVarNames(msgParams.getContent());
// {{first.DATA}}
// 设备编号:{{keyword1.DATA}} LinkedHashMap<String, String> map = new LinkedHashMap<>();
// 数据等级:{{keyword2.DATA}} String deviceDisplay = StringUtils.isNotEmpty(config.getDeviceName())
// 监测数据:{{keyword3.DATA}} ? config.getDeviceName() : serialNumber;
// {{remark.DATA}} for (int i = 0; i < varNames.size(); i++) {
Map<String, Object> data = new LinkedHashMap<>(); String varName = varNames.get(i);
data.put("first", new TemplateDataVo("设备数据监测通知", "#333333")); if (i == 0) {
data.put("keyword1", new TemplateDataVo(serialNumber, "#333333")); map.put(varName, deviceDisplay);
data.put("keyword2", new TemplateDataVo(level.getLabel(), level.getColor())); } else if (i == 1) {
data.put("keyword3", new TemplateDataVo(dataText, level.getColor())); map.put(varName, level.getLabel());
data.put("remark", new TemplateDataVo("请及时关注设备状态", "#999999")); } else if (i == 2) {
map.put(varName, dataText);
} else {
map.put(varName, "");
}
}
// 与 WeChatPushServiceImpl 保持一致的 map 迭代方式
Map<String, Object> sendMap = new LinkedHashMap<>();
for (Map.Entry<String, String> m : map.entrySet()) {
sendMap.put(m.getKey(), new TemplateDataVo(m.getValue(), level.getColor()));
}
for (Map.Entry<Long, String> userEntry : openIdMap.entrySet()) { for (Map.Entry<Long, String> userEntry : openIdMap.entrySet()) {
String openId = userEntry.getValue(); String openId = userEntry.getValue();
if (StringUtils.isEmpty(openId)) {
continue;
}
WeChatPublicAccountPushVO vo = new WeChatPublicAccountPushVO(); WeChatPublicAccountPushVO vo = new WeChatPublicAccountPushVO();
vo.setTouser(openId); vo.setTouser(openId);
vo.setTemplateId(config.getWxTemplateId()); vo.setTemplateId(msgParams.getTemplateId());
vo.setData(data); vo.setData(sendMap);
if (StringUtils.isNotEmpty(config.getRedirectUrl())) { if (StringUtils.isNotEmpty(msgParams.getRedirectUrl())) {
vo.setUrl(config.getRedirectUrl()); vo.setUrl(msgParams.getRedirectUrl());
} }
try { try {
NotifySendResponse resp = doPost(JSON.toJSONString(vo), pushUrl); NotifySendResponse resp = doPost(JSON.toJSONString(vo), pushUrl);
log.info("[Push] configId={} sn={} userId={} level={} status={}", 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) { } catch (Exception e) {
log.error("[Push] configId={} sn={} userId={} 推送异常: {}", log.error("[Push] configId={} sn={} userId={} 推送异常: {}",
config.getId(), serialNumber, userEntry.getKey(), e.getMessage()); config.getId(), serialNumber, userEntry.getKey(), e.getMessage());
@ -201,13 +230,25 @@ public class ThingsModelPushHandler {
} }
} }
private List<String> extractVarNames(String content) {
List<String> 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) { private NotifySendResponse doPost(String json, String url) {
NotifySendResponse resp = new NotifySendResponse(); NotifySendResponse resp = new NotifySendResponse();
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/json; charset=UTF-8")); headers.setContentType(MediaType.parseMediaType("application/json; charset=UTF-8"));
headers.add("Accept", MediaType.APPLICATION_JSON.toString()); headers.add("Accept", MediaType.APPLICATION_JSON.toString());
ResponseEntity<String> result = REST_TEMPLATE.postForEntity(url, ResponseEntity<String> result = REST_TEMPLATE.postForEntity(
new HttpEntity<>(json, headers), String.class); url, new HttpEntity<>(json, headers), String.class);
String body = result.getBody(); String body = result.getBody();
resp.setResultContent(body); resp.setResultContent(body);
try { try {
@ -217,28 +258,4 @@ public class ThingsModelPushHandler {
} }
return resp; return resp;
} }
/**
* 确定推送涉及的设备编号列表
* device_id=0 → 产品级,需查产品下所有设备
* device_id>0 → 指定设备
*/
private List<String> 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<String> 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());
}
} }

View File

@ -31,26 +31,26 @@ public class ThingsModelPushConfig extends BaseEntity {
@ApiModelProperty("产品ID") @ApiModelProperty("产品ID")
private Long productId; private Long productId;
@ApiModelProperty("产品名称(非持久化字段)") @ApiModelProperty("产品名称(非持久化,JOIN查询)")
private String productName; private String productName;
@ApiModelProperty("设备ID,0=产品级") @ApiModelProperty("设备ID(必须指定具体设备)")
private Long deviceId; private Long deviceId;
@ApiModelProperty("设备名称(非持久化,JOIN查询)")
private String deviceName;
@ApiModelProperty("设备类型:1=直连 2=网关(非持久化,JOIN查询)")
private Integer deviceType;
@ApiModelProperty("设备编号") @ApiModelProperty("设备编号")
private String serialNumber; private String serialNumber;
@ApiModelProperty("通知渠道ID") @ApiModelProperty("通知模板ID(关联 notify_template)")
private Long channelId; private Long templateId;
@ApiModelProperty("通知渠道名称(非持久化字段)") @ApiModelProperty("通知模板名称(非持久化,JOIN查询)")
private String channelName; private String templateName;
@ApiModelProperty("微信公众号模板ID")
private String wxTemplateId;
@ApiModelProperty("消息点击跳转URL")
private String redirectUrl;
@ApiModelProperty("推送间隔(分钟),0=仅手动推送") @ApiModelProperty("推送间隔(分钟),0=仅手动推送")
private Integer pushInterval; private Integer pushInterval;

View File

@ -9,11 +9,11 @@
<result property="productId" column="product_id"/> <result property="productId" column="product_id"/>
<result property="productName" column="product_name"/> <result property="productName" column="product_name"/>
<result property="deviceId" column="device_id"/> <result property="deviceId" column="device_id"/>
<result property="deviceName" column="device_name"/>
<result property="deviceType" column="device_type"/>
<result property="serialNumber" column="serial_number"/> <result property="serialNumber" column="serial_number"/>
<result property="channelId" column="channel_id"/> <result property="templateId" column="template_id"/>
<result property="channelName" column="channel_name"/> <result property="templateName" column="template_name"/>
<result property="wxTemplateId" column="wx_template_id"/>
<result property="redirectUrl" column="redirect_url"/>
<result property="pushInterval" column="push_interval"/> <result property="pushInterval" column="push_interval"/>
<result property="lastPushTime" column="last_push_time"/> <result property="lastPushTime" column="last_push_time"/>
<result property="status" column="status"/> <result property="status" column="status"/>
@ -52,36 +52,38 @@
<sql id="baseColumns"> <sql id="baseColumns">
c.id, c.config_name, c.product_id, c.device_id, c.serial_number, 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.template_id, c.push_interval, c.last_push_time, c.status,
c.push_interval, c.last_push_time, c.status,
c.tenant_id, c.tenant_name, c.tenant_id, c.tenant_name,
c.create_by, c.create_time, c.update_by, c.update_time, 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
</sql>
<sql id="baseJoins">
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
</sql> </sql>
<select id="selectById" resultMap="ConfigResult"> <select id="selectById" resultMap="ConfigResult">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM iot_things_model_push_config c <include refid="baseJoins"/>
LEFT JOIN iot_product p ON c.product_id = p.product_id
LEFT JOIN notify_channel nc ON c.channel_id = nc.id
WHERE c.id = #{id} AND c.del_flag = '0' WHERE c.id = #{id} AND c.del_flag = '0'
</select> </select>
<select id="selectList" resultMap="ConfigResult"> <select id="selectList" resultMap="ConfigResult">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM iot_things_model_push_config c <include refid="baseJoins"/>
LEFT JOIN iot_product p ON c.product_id = p.product_id
LEFT JOIN notify_channel nc ON c.channel_id = nc.id
WHERE c.del_flag = '0' WHERE c.del_flag = '0'
<if test="configName != null and configName != ''"> <if test="configName != null and configName != ''">
AND c.config_name LIKE CONCAT('%', #{configName}, '%') AND c.config_name LIKE CONCAT('%', #{configName}, '%')
</if> </if>
<if test="productId != null">
AND c.product_id = #{productId}
</if>
<if test="deviceId != null"> <if test="deviceId != null">
AND c.device_id = #{deviceId} AND c.device_id = #{deviceId}
</if> </if>
<if test="serialNumber != null and serialNumber != ''">
AND c.serial_number LIKE CONCAT('%', #{serialNumber}, '%')
</if>
<if test="status != null"> <if test="status != null">
AND c.status = #{status} AND c.status = #{status}
</if> </if>
@ -93,30 +95,25 @@
<select id="selectAllEnabled" resultMap="ConfigResult"> <select id="selectAllEnabled" resultMap="ConfigResult">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM iot_things_model_push_config c <include refid="baseJoins"/>
LEFT JOIN iot_product p ON c.product_id = p.product_id
LEFT JOIN notify_channel nc ON c.channel_id = nc.id
WHERE c.del_flag = '0' AND c.status = 1 AND c.push_interval > 0 WHERE c.del_flag = '0' AND c.status = 1 AND c.push_interval > 0
</select> </select>
<select id="selectEnabledByProductAndDevice" resultMap="ConfigResult"> <select id="selectEnabledByProductAndDevice" resultMap="ConfigResult">
SELECT <include refid="baseColumns"/> SELECT <include refid="baseColumns"/>
FROM iot_things_model_push_config c <include refid="baseJoins"/>
LEFT JOIN iot_product p ON c.product_id = p.product_id
LEFT JOIN notify_channel nc ON c.channel_id = nc.id
WHERE c.del_flag = '0' AND c.status = 1 WHERE c.del_flag = '0' AND c.status = 1
AND c.product_id = #{productId} AND c.device_id = #{deviceId}
AND (c.device_id = 0 OR c.device_id = #{deviceId})
</select> </select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id"> <insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO iot_things_model_push_config INSERT INTO iot_things_model_push_config
(config_name, product_id, device_id, serial_number, channel_id, (config_name, product_id, device_id, serial_number, template_id,
wx_template_id, redirect_url, push_interval, status, push_interval, status,
tenant_id, tenant_name, create_by, create_time, del_flag) tenant_id, tenant_name, create_by, create_time, del_flag)
VALUES VALUES
(#{configName}, #{productId}, #{deviceId}, #{serialNumber}, #{channelId}, (#{configName}, #{productId}, #{deviceId}, #{serialNumber}, #{templateId},
#{wxTemplateId}, #{redirectUrl}, #{pushInterval}, #{status}, #{pushInterval}, #{status},
#{tenantId}, #{tenantName}, #{createBy}, #{createTime}, '0') #{tenantId}, #{tenantName}, #{createBy}, #{createTime}, '0')
</insert> </insert>
@ -127,9 +124,7 @@
<if test="productId != null">product_id = #{productId},</if> <if test="productId != null">product_id = #{productId},</if>
<if test="deviceId != null">device_id = #{deviceId},</if> <if test="deviceId != null">device_id = #{deviceId},</if>
<if test="serialNumber != null">serial_number = #{serialNumber},</if> <if test="serialNumber != null">serial_number = #{serialNumber},</if>
<if test="channelId != null">channel_id = #{channelId},</if> <if test="templateId != null">template_id = #{templateId},</if>
<if test="wxTemplateId != null">wx_template_id = #{wxTemplateId},</if>
<if test="redirectUrl != null">redirect_url = #{redirectUrl},</if>
<if test="pushInterval != null">push_interval = #{pushInterval},</if> <if test="pushInterval != null">push_interval = #{pushInterval},</if>
<if test="status != null">status = #{status},</if> <if test="status != null">status = #{status},</if>
update_by = #{updateBy}, update_by = #{updateBy},

View File

@ -7,11 +7,9 @@ CREATE TABLE IF NOT EXISTS `iot_things_model_push_config` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`config_name` VARCHAR(128) NOT NULL COMMENT '配置名称', `config_name` VARCHAR(128) NOT NULL COMMENT '配置名称',
`product_id` BIGINT NOT NULL COMMENT '产品ID', `product_id` BIGINT NOT NULL COMMENT '产品ID',
`device_id` BIGINT DEFAULT 0 COMMENT '设备ID,0=产品级(产品下所有设备)', `device_id` BIGINT DEFAULT NULL COMMENT '设备ID(必须指定具体设备)',
`serial_number` VARCHAR(64) DEFAULT NULL COMMENT '设备编号(device_id>0时有效)', `serial_number` VARCHAR(64) DEFAULT NULL COMMENT '设备编号',
`channel_id` BIGINT NOT NULL COMMENT '通知渠道ID (notify_channel.id,类型为 public_account)', `template_id` BIGINT NOT NULL COMMENT '通知模板ID(关联 notify_template.id)',
`wx_template_id` VARCHAR(128) NOT NULL COMMENT '微信公众号模板ID(在微信公众平台配置)',
`redirect_url` VARCHAR(512) DEFAULT NULL COMMENT '消息点击跳转URL',
`push_interval` INT DEFAULT 60 COMMENT '推送间隔(分钟),0=仅手动推送', `push_interval` INT DEFAULT 60 COMMENT '推送间隔(分钟),0=仅手动推送',
`last_push_time` DATETIME DEFAULT NULL COMMENT '最近一次推送时间', `last_push_time` DATETIME DEFAULT NULL COMMENT '最近一次推送时间',
`status` TINYINT DEFAULT 1 COMMENT '状态:0=停用 1=启用', `status` TINYINT DEFAULT 1 COMMENT '状态:0=停用 1=启用',
@ -23,8 +21,8 @@ CREATE TABLE IF NOT EXISTS `iot_things_model_push_config` (
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间', `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', `del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_product_id` (`product_id`), KEY `idx_device_id` (`device_id`),
KEY `idx_device_id` (`device_id`) KEY `idx_template_id` (`template_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物模型数据推送配置'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物模型数据推送配置';
@ -59,11 +57,9 @@ CREATE TABLE IF NOT EXISTS `iot_push_user` (
-- sys_menu 菜单权限(挂在"规则引擎"父菜单 parent_id=3051 下) -- sys_menu 菜单权限(挂在"规则引擎"父菜单 parent_id=3051 下)
-- ============================================================ -- ============================================================
-- 主菜单:数据推送配置
INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (3430, '数据推送配置', 3051, 9, 'pushConfig', 'iot/push/index', NULL, 1, 0, 'C', '0', '0', 'iot:push:list', 'notification', 'admin', now(), 'admin', now(), '微信服务号物模型数据推送'); VALUES (3430, '数据推送配置', 3051, 9, 'pushConfig', 'iot/push/index', NULL, 1, 0, 'C', '0', '0', 'iot:push:list', 'notification', 'admin', now(), 'admin', now(), '微信服务号物模型数据推送');
-- 按钮权限
INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (3431, '推送配置查询', 3430, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:push:query', '#', 'admin', now(), 'admin', now(), ''); VALUES (3431, '推送配置查询', 3430, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:push:query', '#', 'admin', now(), 'admin', now(), '');
@ -78,3 +74,17 @@ VALUES (3434, '推送配置删除', 3430, 4, '#', '', NULL, 1, 0, 'F', '0', '0',
INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) INSERT INTO `sys_menu` (menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES (3435, '手动立即推送', 3430, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:push:push', '#', 'admin', now(), 'admin', now(), ''); VALUES (3435, '手动立即推送', 3430, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:push:push', '#', 'admin', now(), 'admin', now(), '');
-- ============================================================
-- 升级脚本:如已执行过旧版本建表,执行以下语句迁移字段
-- ============================================================
ALTER TABLE `iot_things_model_push_config`
ADD COLUMN IF NOT EXISTS `template_id` BIGINT DEFAULT NULL COMMENT '通知模板ID(关联 notify_template.id)' AFTER `serial_number`,
ADD INDEX IF NOT EXISTS `idx_template_id` (`template_id`);
-- 迁移完成后可删除旧字段(按需执行):
-- ALTER TABLE `iot_things_model_push_config`
-- DROP COLUMN IF EXISTS `channel_id`,
-- DROP COLUMN IF EXISTS `wx_template_id`,
-- DROP COLUMN IF EXISTS `wx_msg_content`,
-- DROP COLUMN IF EXISTS `redirect_url`;

View File

@ -2,14 +2,15 @@
<div class="iot-push-config"> <div class="iot-push-config">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<el-card v-show="showSearch" style="margin-bottom: 10px"> <el-card v-show="showSearch" style="margin-bottom: 10px">
<el-form @submit.native.prevent :model="queryParams" ref="queryForm" :inline="true" size="mini" label-width="80px" style="margin-bottom: -18px"> <el-form @submit.native.prevent :model="queryParams" ref="queryForm" :inline="true"
size="mini" label-width="80px" style="margin-bottom: -18px">
<el-form-item label="配置名称" prop="configName"> <el-form-item label="配置名称" prop="configName">
<el-input v-model="queryParams.configName" placeholder="请输入配置名称" clearable @keyup.enter.native="handleQuery" /> <el-input v-model="queryParams.configName" placeholder="请输入配置名称"
clearable @keyup.enter.native="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="产品" prop="productId"> <el-form-item label="设备编号" prop="serialNumber">
<el-select v-model="queryParams.productId" placeholder="请选择产品" clearable filterable style="width:160px"> <el-input v-model="queryParams.serialNumber" placeholder="请输入设备编号"
<el-option v-for="p in productOptions" :key="p.productId" :label="p.productName" :value="p.productId" /> clearable @keyup.enter.native="handleQuery" style="width:160px" />
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width:100px"> <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width:100px">
@ -27,15 +28,21 @@
<el-card> <el-card>
<el-row :gutter="10" style="margin-bottom: 15px"> <el-row :gutter="10" style="margin-bottom: 15px">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['iot:push:add']">新增</el-button> <el-button type="primary" plain icon="el-icon-plus" size="mini"
@click="handleAdd" v-hasPermi="['iot:push:add']">新增</el-button>
</el-col> </el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
<el-table v-loading="loading" :data="configList" border> <el-table v-loading="loading" :data="configList" border>
<el-table-column label="配置名称" align="center" prop="configName" min-width="140" show-overflow-tooltip /> <el-table-column label="配置名称" align="center" prop="configName"
<el-table-column label="产品" align="center" prop="productName" min-width="120" show-overflow-tooltip /> min-width="140" show-overflow-tooltip />
<el-table-column label="通知渠道" align="center" prop="channelName" min-width="120" show-overflow-tooltip /> <el-table-column label="设备名称" align="center" prop="deviceName"
min-width="120" show-overflow-tooltip />
<el-table-column label="设备编号" align="center" prop="serialNumber"
min-width="120" show-overflow-tooltip />
<el-table-column label="通知模板" align="center" prop="templateName"
min-width="120" show-overflow-tooltip />
<el-table-column label="推送间隔" align="center" width="110"> <el-table-column label="推送间隔" align="center" width="110">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="scope.row.pushInterval > 0">每 {{ scope.row.pushInterval }} 分钟</span> <span v-if="scope.row.pushInterval > 0">每 {{ scope.row.pushInterval }} 分钟</span>
@ -50,12 +57,14 @@
<el-table-column label="状态" align="center" width="80"> <el-table-column label="状态" align="center" width="80">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" <el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0"
@change="handleStatusChange(scope.row)" v-hasPermi="['iot:push:edit']" /> @change="handleStatusChange(scope.row)"
v-hasPermi="['iot:push:edit']" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" width="200" fixed="right"> <el-table-column label="操作" align="center" width="200" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['iot:push:edit']">编辑</el-button> <el-button size="mini" type="text" icon="el-icon-edit"
@click="handleUpdate(scope.row)" v-hasPermi="['iot:push:edit']">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-video-play" style="color:#67C23A" <el-button size="mini" type="text" icon="el-icon-video-play" style="color:#67C23A"
@click="handleManualPush(scope.row)" v-hasPermi="['iot:push:push']">立即推送</el-button> @click="handleManualPush(scope.row)" v-hasPermi="['iot:push:push']">立即推送</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#F56C6C" <el-button size="mini" type="text" icon="el-icon-delete" style="color:#F56C6C"
@ -64,13 +73,16 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" /> <pagination v-show="total > 0" :total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList" />
</el-card> </el-card>
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="900px" append-to-body <el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="960px" append-to-body
:close-on-click-modal="false" :close-on-press-escape="false"> :close-on-click-modal="false" :close-on-press-escape="false">
<el-form ref="form" :model="form" :rules="rules" label-width="100px"> <el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="配置名称" prop="configName"> <el-form-item label="配置名称" prop="configName">
@ -78,34 +90,37 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="产品" prop="productId"> <el-form-item label="选择设备" prop="deviceId">
<el-select v-model="form.productId" placeholder="请选择产品" filterable style="width:100%" <el-select v-model="form.deviceId" placeholder="输入设备名称或编号搜索"
@change="onProductChange"> filterable remote :remote-method="remoteSearchDevice"
<el-option v-for="p in productOptions" :key="p.productId" :label="p.productName" :value="p.productId" /> :loading="deviceSearchLoading" style="width:100%"
@change="onDeviceChange">
<el-option v-for="d in deviceOptions" :key="d.deviceId"
:label="deviceLabel(d)" :value="d.deviceId">
<span>{{ d.deviceName }}</span>
<span style="color:#909399;font-size:12px;margin-left:6px">{{ d.serialNumber }}</span>
<el-tag size="mini" :type="d.deviceType === 2 ? 'warning' : 'success'"
style="margin-left:6px;zoom:0.85">
{{ d.deviceType === 2 ? '网关' : '直连' }}
</el-tag>
</el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="通知渠道" prop="channelId"> <el-form-item label="通知模板" prop="templateId">
<el-select v-model="form.channelId" placeholder="请选择公众号渠道" filterable style="width:100%"> <el-select v-model="form.templateId" placeholder="请选择通知模板(微信公众号类型)"
<el-option v-for="c in channelOptions" :key="c.id" :label="c.name" :value="c.id" /> filterable style="width:100%">
<el-option v-for="t in templateOptions" :key="t.id"
:label="t.name" :value="t.id" />
</el-select> </el-select>
</el-form-item> <div class="form-tip">请在「运维管理 - 通知模板」中配置微信公众号模板</div>
</el-col>
<el-col :span="12">
<el-form-item label="微信模板ID" prop="wxTemplateId">
<el-input v-model="form.wxTemplateId" placeholder="在微信公众号后台获取模板ID" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="推送间隔(分)" prop="pushInterval"> <el-form-item label="推送间隔(分)" prop="pushInterval">
<el-input-number v-model="form.pushInterval" :min="0" :max="1440" style="width:100%" <el-input-number v-model="form.pushInterval" :min="0" :max="1440"
placeholder="0=仅手动推送" /> style="width:100%" placeholder="0=仅手动推送" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="跳转URL">
<el-input v-model="form.redirectUrl" placeholder="消息点击跳转链接(选填)" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
@ -120,18 +135,21 @@
<!-- 物模型选择与阈值配置 --> <!-- 物模型选择与阈值配置 -->
<el-divider content-position="left">监测物模型及数据等级阈值</el-divider> <el-divider content-position="left">监测物模型及数据等级阈值</el-divider>
<div style="margin-bottom:10px;color:#606266;font-size:12px"> <div class="form-tip" style="margin-bottom:8px">
绿色(✅ 正常):值在 [绿下限, 绿上限] 范围内; 绿色(正常):值在 [绿下限, 绿上限] 范围内;
黄色(⚠️ 告警):值在 [黄下限, 黄上限] 范围内; 黄色(告警):值在 [黄下限, 黄上限] 范围内;
红色(🔴 警报):其余情况。留空表示不限。 红色(警报):其余情况。留空表示不限。
</div> </div>
<el-table :data="form.items" size="mini" border> <el-table :data="form.items" size="mini" border>
<el-table-column label="物模型" min-width="160"> <el-table-column label="物模型属性" min-width="170">
<template slot-scope="scope"> <template slot-scope="scope">
<el-select v-model="scope.row.identifier" placeholder="选择物模型属性" filterable <el-select v-model="scope.row.identifier" placeholder="选择属性"
size="mini" @change="(v) => onModelItemChange(v, scope.row)"> filterable size="mini"
:disabled="!form.productId"
@change="(v) => onModelItemChange(v, scope.row)">
<el-option v-for="m in thingsModelOptions" :key="m.identifier" <el-option v-for="m in thingsModelOptions" :key="m.identifier"
:label="m.modelName + ' (' + m.identifier + ')'" :value="m.identifier" /> :label="m.modelName + ' (' + m.identifier + ')'"
:value="m.identifier" />
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
@ -160,19 +178,22 @@
<el-input v-model="scope.row.yellowMax" size="mini" placeholder="不限" /> <el-input v-model="scope.row.yellowMax" size="mini" placeholder="不限" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="70" align="center"> <el-table-column label="操作" width="60" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" icon="el-icon-delete" style="color:#F56C6C" <el-button type="text" icon="el-icon-delete" style="color:#F56C6C"
size="mini" @click="removeModelItem(scope.$index)" /> size="mini" @click="removeModelItem(scope.$index)" />
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addModelItem">添加物模型</el-button> <el-button size="mini" icon="el-icon-plus" style="margin-top:8px"
:disabled="!form.productId"
@click="addModelItem">添加物模型</el-button>
<span v-if="!form.productId" class="form-tip" style="margin-left:8px">请先选择设备</span>
<!-- 接收用户 --> <!-- 接收用户 -->
<el-divider content-position="left">接收用户(需已绑定微信公众号)</el-divider> <el-divider content-position="left">接收用户(需已绑定微信公众号)</el-divider>
<el-select v-model="form.userIds" multiple filterable placeholder="请选择接收用户" <el-select v-model="form.userIds" multiple filterable
style="width:100%" size="small"> placeholder="请选择接收用户" style="width:100%" size="small">
<el-option v-for="u in userOptions" :key="u.userId" :value="u.userId" <el-option v-for="u in userOptions" :key="u.userId" :value="u.userId"
:label="u.nickName + ' (' + u.userName + ')'" /> :label="u.nickName + ' (' + u.userName + ')'" />
</el-select> </el-select>
@ -191,19 +212,18 @@ import {
listPushConfig, getPushConfig, addPushConfig, updatePushConfig, listPushConfig, getPushConfig, addPushConfig, updatePushConfig,
changePushConfigStatus, delPushConfig, manualPush changePushConfigStatus, delPushConfig, manualPush
} from '@/api/iot/pushConfig'; } from '@/api/iot/pushConfig';
import { listProduct } from '@/api/iot/product'; import { listDevice } from '@/api/iot/device';
import { listModel } from '@/api/iot/model'; import { listModel } from '@/api/iot/model';
import { listChannel } from '@/api/notify/channel'; import { listTemplate } from '@/api/notify/template';
import { listUser } from '@/api/system/user'; import { listUser } from '@/api/system/user';
const emptyForm = () => ({ const emptyForm = () => ({
id: null, id: null,
configName: '', configName: '',
productId: null, productId: null,
deviceId: 0, deviceId: null,
channelId: null, serialNumber: '',
wxTemplateId: '', templateId: null,
redirectUrl: '',
pushInterval: 60, pushInterval: 60,
status: 1, status: 1,
items: [], items: [],
@ -218,28 +238,27 @@ export default {
showSearch: true, showSearch: true,
total: 0, total: 0,
configList: [], configList: [],
queryParams: { pageNum: 1, pageSize: 10, configName: '', productId: null, status: null }, queryParams: { pageNum: 1, pageSize: 10, configName: '', serialNumber: '', status: null },
dialogOpen: false, dialogOpen: false,
dialogTitle: '', dialogTitle: '',
submitLoading: false, submitLoading: false,
form: emptyForm(), form: emptyForm(),
rules: { rules: {
configName: [{ required: true, message: '请输入配置名称', trigger: 'blur' }], configName: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
productId: [{ required: true, message: '请选择产品', trigger: 'change' }], deviceId: [{ required: true, message: '请选择设备', trigger: 'change' }],
channelId: [{ required: true, message: '请选择通知渠道', trigger: 'change' }], templateId: [{ required: true, message: '请选择通知模板', trigger: 'change' }],
wxTemplateId:[{ required: true, message: '请输入微信模板ID', trigger: 'blur' }],
pushInterval: [{ required: true, message: '请填写推送间隔', trigger: 'blur' }] pushInterval: [{ required: true, message: '请填写推送间隔', trigger: 'blur' }]
}, },
productOptions: [], deviceOptions: [],
channelOptions: [], deviceSearchLoading: false,
templateOptions: [],
thingsModelOptions: [], thingsModelOptions: [],
userOptions: [] userOptions: []
}; };
}, },
created() { created() {
this.getList(); this.getList();
this.loadProductOptions(); this.loadTemplateOptions();
this.loadChannelOptions();
this.loadUserOptions(); this.loadUserOptions();
}, },
methods: { methods: {
@ -253,21 +272,54 @@ export default {
handleQuery() { this.queryParams.pageNum = 1; this.getList(); }, handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
resetQuery() { resetQuery() {
this.resetForm('queryForm'); this.resetForm('queryForm');
this.queryParams = { pageNum: 1, pageSize: 10, configName: '', productId: null, status: null }; this.queryParams = { pageNum: 1, pageSize: 10, configName: '', serialNumber: '', status: null };
this.getList(); this.getList();
}, },
loadProductOptions() {
listProduct({ pageNum: 1, pageSize: 999 }).then(res => { remoteSearchDevice(keyword) {
this.productOptions = res.rows || []; if (!keyword) return;
this.deviceSearchLoading = true;
const baseQuery = { pageNum: 1, pageSize: 30 };
// 关键字同时匹配 deviceName 和 serialNumber
const byName = listDevice({ ...baseQuery, deviceType: null, deviceName: keyword });
const bySn = listDevice({ ...baseQuery, deviceType: null, serialNumber: keyword });
Promise.all([byName, bySn]).then(([r1, r2]) => {
const rows1 = r1.rows || [];
const rows2 = r2.rows || [];
// 合并去重(以 deviceId 为 key),只保留直连(1)和网关(2)设备
const map = new Map();
[...rows1, ...rows2].forEach(d => {
if ((d.deviceType === 1 || d.deviceType === 2) && !map.has(d.deviceId)) {
map.set(d.deviceId, d);
}
});
this.deviceOptions = Array.from(map.values());
}).finally(() => { this.deviceSearchLoading = false; });
},
deviceLabel(d) {
const typeStr = d.deviceType === 2 ? '[网关]' : '[直连]';
return typeStr + ' ' + d.deviceName + ' (' + d.serialNumber + ')';
},
onDeviceChange(deviceId) {
const device = this.deviceOptions.find(d => d.deviceId === deviceId);
if (!device) return;
this.form.productId = device.productId;
this.form.serialNumber = device.serialNumber;
this.form.items = [];
this.thingsModelOptions = [];
listModel({ productId: device.productId, type: 1, pageNum: 1, pageSize: 500 }).then(res => {
this.thingsModelOptions = res.rows || [];
}); });
}, },
loadChannelOptions() {
// 只加载微信公众号渠道 loadTemplateOptions() {
listChannel({ channelType: 'wechat', provider: 'public_account', pageNum: 1, pageSize: 100 }).then(res => { listTemplate({ provider: 'public_account', pageNum: 1, pageSize: 200 }).then(res => {
this.channelOptions = res.rows || []; this.templateOptions = res.rows || [];
}).catch(() => { }).catch(() => {
listChannel({ pageNum: 1, pageSize: 100 }).then(res => { listTemplate({ pageNum: 1, pageSize: 200 }).then(res => {
this.channelOptions = (res.rows || []).filter(c => c.provider === 'public_account'); this.templateOptions = (res.rows || []).filter(t => t.provider === 'public_account');
}); });
}); });
}, },
@ -276,14 +328,7 @@ export default {
this.userOptions = res.rows || []; this.userOptions = res.rows || [];
}); });
}, },
onProductChange(productId) {
this.thingsModelOptions = [];
this.form.items = [];
if (!productId) return;
listModel({ productId, type: 1, pageNum: 1, pageSize: 500 }).then(res => {
this.thingsModelOptions = res.rows || [];
});
},
onModelItemChange(identifier, row) { onModelItemChange(identifier, row) {
const model = this.thingsModelOptions.find(m => m.identifier === identifier); const model = this.thingsModelOptions.find(m => m.identifier === identifier);
if (model) { if (model) {
@ -304,8 +349,10 @@ export default {
removeModelItem(index) { removeModelItem(index) {
this.form.items.splice(index, 1); this.form.items.splice(index, 1);
}, },
handleAdd() { handleAdd() {
this.form = emptyForm(); this.form = emptyForm();
this.deviceOptions = [];
this.thingsModelOptions = []; this.thingsModelOptions = [];
this.dialogTitle = '新增推送配置'; this.dialogTitle = '新增推送配置';
this.dialogOpen = true; this.dialogOpen = true;
@ -319,6 +366,14 @@ export default {
userIds: data.userIds || [], userIds: data.userIds || [],
items: data.items || [] items: data.items || []
}; };
if (data.deviceId) {
this.deviceOptions = [{
deviceId: data.deviceId,
deviceName: data.deviceName || data.serialNumber,
serialNumber: data.serialNumber || '',
deviceType: data.deviceType || 1
}];
}
if (data.productId) { if (data.productId) {
listModel({ productId: data.productId, type: 1, pageNum: 1, pageSize: 500 }).then(r => { listModel({ productId: data.productId, type: 1, pageNum: 1, pageSize: 500 }).then(r => {
this.thingsModelOptions = r.rows || []; this.thingsModelOptions = r.rows || [];
@ -328,6 +383,7 @@ export default {
this.dialogOpen = true; this.dialogOpen = true;
}); });
}, },
handleStatusChange(row) { handleStatusChange(row) {
const text = row.status === 1 ? '启用' : '停用'; const text = row.status === 1 ? '启用' : '停用';
this.$confirm(`确认${text}【${row.configName}】?`, '提示', { type: 'warning' }).then(() => { this.$confirm(`确认${text}【${row.configName}】?`, '提示', { type: 'warning' }).then(() => {
@ -352,6 +408,7 @@ export default {
}); });
}); });
}, },
submitForm() { submitForm() {
this.$refs.form.validate(valid => { this.$refs.form.validate(valid => {
if (!valid) return; if (!valid) return;
@ -359,8 +416,7 @@ export default {
this.$message.warning('请至少添加一个监测物模型'); this.$message.warning('请至少添加一个监测物模型');
return; return;
} }
const hasEmptyIdentifier = this.form.items.some(i => !i.identifier); if (this.form.items.some(i => !i.identifier)) {
if (hasEmptyIdentifier) {
this.$message.warning('存在未选择物模型的行,请完善'); this.$message.warning('存在未选择物模型的行,请完善');
return; return;
} }
@ -381,4 +437,9 @@ export default {
.iot-push-config .el-divider { .iot-push-config .el-divider {
margin: 15px 0 10px; margin: 15px 0 10px;
} }
.form-tip {
color: #909399;
font-size: 12px;
line-height: 1.4;
}
</style> </style>