20260326:数据推送

This commit is contained in:
zhugongping 2026-03-26 22:59:16 +08:00
parent d5cdfa9ba4
commit b7e0984654
17 changed files with 1582 additions and 5 deletions

View File

@ -0,0 +1,84 @@
package com.fastbee.common.enums;
/**
* 物模型数据等级枚举
* 绿色=正常,黄色=告警,红色=警报
*
* @author fastbee
*/
public enum DataLevelEnum {
GREEN(1, "✅ 正常", "#009900"),
YELLOW(2, "⚠️ 告警", "#FF9900"),
RED(3, "🔴 警报", "#FF0000");
private final int code;
private final String label;
/** 微信模板消息字体颜色 */
private final String color;
DataLevelEnum(int code, String label, String color) {
this.code = code;
this.label = label;
this.color = color;
}
public int getCode() {
return code;
}
public String getLabel() {
return label;
}
public String getColor() {
return color;
}
/**
* 根据值和阈值配置计算数据等级
*
* @param value 物模型上报值(字符串)
* @param greenMin 绿色下限(null=不限)
* @param greenMax 绿色上限(null=不限)
* @param yellowMin 黄色下限(null=不限)
* @param yellowMax 黄色上限(null=不限)
* @return 数据等级
*/
public static DataLevelEnum evaluate(String value,
String greenMin, String greenMax,
String yellowMin, String yellowMax) {
if (value == null) {
return RED;
}
// 优先尝试数值比较
try {
double v = Double.parseDouble(value.trim());
if (inNumericRange(v, greenMin, greenMax)) {
return GREEN;
}
if (inNumericRange(v, yellowMin, yellowMax)) {
return YELLOW;
}
return RED;
} catch (NumberFormatException e) {
// 字符串等值比较
if (value.equals(greenMin)) {
return GREEN;
}
if (value.equals(yellowMin)) {
return YELLOW;
}
return RED;
}
}
private static boolean inNumericRange(double v, String min, String max) {
if (min == null && max == null) {
return false;
}
boolean lowerOk = (min == null) || v >= Double.parseDouble(min);
boolean upperOk = (max == null) || v <= Double.parseDouble(max);
return lowerOk && upperOk;
}
}

View File

@ -1,6 +1,7 @@
package com.fastbee.framework.config; package com.fastbee.framework.config;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@ -12,6 +13,7 @@ import java.util.concurrent.ScheduledExecutorService;
* - 使用项目自定义的 scheduledExecutorService(已做 MDC 透传与异常打印) * - 使用项目自定义的 scheduledExecutorService(已做 MDC 透传与异常打印)
*/ */
@Configuration @Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer { public class SchedulingConfig implements SchedulingConfigurer {
@Resource(name = "scheduledExecutorService") @Resource(name = "scheduledExecutorService")

View File

@ -0,0 +1,244 @@
package com.fastbee.iot.data.push;
import com.alibaba.fastjson2.JSON;
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.redis.RedisCache;
import com.fastbee.common.core.redis.RedisKeyBuilder;
import com.fastbee.common.enums.DataLevelEnum;
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.ThingsModelPushConfig;
import com.fastbee.iot.model.ThingsModels.ValueItem;
import com.fastbee.iot.service.IThingsModelPushConfigService;
import com.fastbee.notify.domain.NotifyChannel;
import com.fastbee.notify.core.wechat.vo.TemplateDataVo;
import com.fastbee.notify.core.wechat.vo.WeChatPublicAccountPushVO;
import com.fastbee.notify.service.INotifyChannelService;
import com.fastbee.iot.service.ISocialUserService;
import com.fastbee.iot.domain.SocialUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
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.stream.Collectors;
/**
* 物模型数据推送核心处理器
* 读取 Redis 中设备最新上报值,按配置的阈值评估数据等级,
* 同等级数据合并为一条微信服务号模板消息推送。
*
* @author fastbee
*/
@Slf4j
@Component
public class ThingsModelPushHandler {
private static final RestTemplate REST_TEMPLATE = new RestTemplate();
@Resource
private IThingsModelPushConfigService pushConfigService;
@Resource
private INotifyChannelService notifyChannelService;
@Resource
private ISocialUserService socialUserService;
@Resource
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;
}
// 获取通知渠道(公众号)
NotifyChannel channel = notifyChannelService.selectNotifyChannelById(config.getChannelId());
if (channel == null) {
log.warn("[Push] configId={} 渠道不存在 channelId={}", config.getId(), config.getChannelId());
return;
}
WeChatConfigParams wxConfig = JSONObject.parseObject(channel.getConfigContent(), WeChatConfigParams.class);
if (StringUtils.isEmpty(wxConfig.getAppId()) || StringUtils.isEmpty(wxConfig.getAppSecret())) {
log.warn("[Push] configId={} 渠道微信配置不完整", config.getId());
return;
}
// 获取 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());
return;
}
String pushUrl = FastBeeConstant.URL.WX_PUBLIC_ACCOUNT_TEMPLATE_SEND_URL_PREFIX + tokenResult.getAccessToken();
// 确定要查询的设备编号列表
List<String> serialNumbers = resolveSerialNumbers(config);
if (CollectionUtils.isEmpty(serialNumbers)) {
log.warn("[Push] configId={} 没有找到对应设备,跳过", config.getId());
return;
}
// 获取接收用户 openId 列表
List<Long> userIds = config.getUserIds();
if (CollectionUtils.isEmpty(userIds)) {
log.warn("[Push] configId={} 没有配置接收用户,跳过", config.getId());
return;
}
List<SocialUser> socialUsers = socialUserService.listWechatPublicAccountOpenId(
userIds.stream().map(Object::toString).collect(Collectors.toList()));
Map<Long, String> openIdMap = socialUsers.stream()
.collect(Collectors.toMap(SocialUser::getSysUserId, SocialUser::getOpenId, (a, b) -> b));
// 对每台设备推送
for (String serialNumber : serialNumbers) {
pushForDevice(config, serialNumber, pushUrl, openIdMap);
}
// 更新最近推送时间
pushConfigService.updateLastPushTime(config.getId());
}
/**
* 对单台设备执行推送
*/
private void pushForDevice(ThingsModelPushConfig config, String serialNumber,
String pushUrl, Map<Long, String> openIdMap) {
String redisKey = RedisKeyBuilder.buildTSLVCacheKey(config.getProductId(), serialNumber);
// 按等级分组收集显示文本
Map<DataLevelEnum, List<String>> levelTextMap = new LinkedHashMap<>();
for (DataLevelEnum level : DataLevelEnum.values()) {
levelTextMap.put(level, new ArrayList<>());
}
for (PushModelItem item : config.getItems()) {
String cacheValue = redisCache.getCacheMapValue(redisKey, item.getIdentifier());
if (StringUtils.isEmpty(cacheValue)) {
continue;
}
String value;
try {
ValueItem vi = JSON.parseObject(cacheValue, ValueItem.class);
value = vi.getValue();
} catch (Exception e) {
value = cacheValue;
}
DataLevelEnum level = DataLevelEnum.evaluate(
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() : "");
levelTextMap.get(level).add(text);
}
// 对每个非空等级发一条消息
for (Map.Entry<DataLevelEnum, List<String>> 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);
}
}
/**
* 发送单条微信模板消息(带颜色标记)
*/
private void sendTemplateMsg(ThingsModelPushConfig config, String serialNumber,
DataLevelEnum level, String dataText,
String pushUrl, Map<Long, String> openIdMap) {
// 微信模板格式(需在公众号后台与此对应):
// {{first.DATA}}
// 设备编号:{{keyword1.DATA}}
// 数据等级:{{keyword2.DATA}}
// 监测数据:{{keyword3.DATA}}
// {{remark.DATA}}
Map<String, Object> 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"));
for (Map.Entry<Long, String> 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());
}
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());
} catch (Exception e) {
log.error("[Push] configId={} sn={} userId={} 推送异常: {}",
config.getId(), serialNumber, userEntry.getKey(), e.getMessage());
}
}
}
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<String> result = REST_TEMPLATE.postForEntity(url,
new HttpEntity<>(json, headers), String.class);
String body = result.getBody();
resp.setResultContent(body);
try {
resp.setStatus("0".equals(JSONObject.parseObject(body).get("errcode").toString()) ? 1 : 0);
} catch (Exception e) {
resp.setStatus(0);
}
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

@ -0,0 +1,67 @@
package com.fastbee.iot.data.push;
import com.fastbee.iot.domain.ThingsModelPushConfig;
import com.fastbee.iot.service.IThingsModelPushConfigService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* 物模型数据定时推送调度器
* 每分钟检查所有启用的推送配置,按照 push_interval 决定是否触发本次推送。
*
* @author fastbee
*/
@Slf4j
@Component
public class ThingsModelPushJob {
@Resource
private IThingsModelPushConfigService pushConfigService;
@Resource
private ThingsModelPushHandler pushHandler;
/**
* 每分钟执行一次,检查并触发到期的推送任务
*/
@Scheduled(fixedDelay = 60_000)
public void execute() {
List<ThingsModelPushConfig> configs = pushConfigService.selectAllEnabled();
if (CollectionUtils.isEmpty(configs)) {
return;
}
long now = System.currentTimeMillis();
for (ThingsModelPushConfig config : configs) {
try {
if (isDue(config, now)) {
log.info("[PushJob] 触发推送 configId={} configName={}", config.getId(), config.getConfigName());
pushHandler.doPush(config);
}
} catch (Exception e) {
log.error("[PushJob] 推送异常 configId={}: {}", config.getId(), e.getMessage(), e);
}
}
}
/**
* 判断配置是否到达推送时间
* push_interval=0 时不自动推送(仅手动)
*/
private boolean isDue(ThingsModelPushConfig config, long nowMs) {
if (config.getPushInterval() == null || config.getPushInterval() <= 0) {
return false;
}
Date lastPush = config.getLastPushTime();
if (lastPush == null) {
// 从未推送过,立即触发
return true;
}
long intervalMs = (long) config.getPushInterval() * 60 * 1000;
return (nowMs - lastPush.getTime()) >= intervalMs;
}
}

View File

@ -71,6 +71,7 @@ public class WeChatPushServiceImpl implements WeChatPushService {
break; break;
case WECHAT_PUBLIC_ACCOUNT: case WECHAT_PUBLIC_ACCOUNT:
notifySendResponse = this.weChatPublicAccountSend(notifyVO); notifySendResponse = this.weChatPublicAccountSend(notifyVO);
break;
default: default:
break; break;
} }

View File

@ -1,19 +1,27 @@
package com.fastbee.notify.core.wechat.vo; package com.fastbee.notify.core.wechat.vo;
import com.alibaba.fastjson.annotation.JSONField; import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor
public class TemplateDataVo { public class TemplateDataVo {
/*微信文档中要求的格式 "data": { "name01": {"value": "某某"},"thing01": {"value": "广州至北京" /*微信文档中要求的格式 "data": { "name01": {"value": "某某","color":"#173177"},"thing01": {"value": "广州至北京"} }*/
} ,"date01": {"value": "2018-01-01"}
}*/
@JSONField(name = "value") @JSONField(name = "value")
private String value; private String value;
/** 字体颜色,格式 #RRGGBB,为 null 时不序列化(微信默认色) */
@JSONField(name = "color")
private String color;
public TemplateDataVo(String value) {
this.value = value;
}
public TemplateDataVo(String value, String color) {
this.value = value;
this.color = color;
}
} }

View File

@ -0,0 +1,114 @@
package com.fastbee.controller.push;
import com.fastbee.common.annotation.Log;
import com.fastbee.common.core.controller.BaseController;
import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.common.core.page.TableDataInfo;
import com.fastbee.common.enums.BusinessType;
import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.iot.data.push.ThingsModelPushHandler;
import com.fastbee.iot.domain.ThingsModelPushConfig;
import com.fastbee.iot.service.IThingsModelPushConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 物模型数据推送配置 Controller
*
* @author fastbee
*/
@Api(tags = "物模型数据推送配置")
@RestController
@RequestMapping("/iot/push/config")
public class ThingsModelPushController extends BaseController {
@Resource
private IThingsModelPushConfigService pushConfigService;
@Resource
private ThingsModelPushHandler pushHandler;
/** 查询列表 */
@ApiOperation("查询推送配置列表")
@PreAuthorize("@ss.hasPermi('iot:push:list')")
@GetMapping("/list")
public TableDataInfo list(ThingsModelPushConfig query) {
startPage();
List<ThingsModelPushConfig> list = pushConfigService.selectList(query);
return getDataTable(list);
}
/** 获取详情 */
@ApiOperation("获取推送配置详情")
@PreAuthorize("@ss.hasPermi('iot:push:query')")
@GetMapping("/{id}")
public AjaxResult getById(@PathVariable Long id) {
return AjaxResult.success(pushConfigService.selectById(id));
}
/** 新增 */
@ApiOperation("新增推送配置")
@PreAuthorize("@ss.hasPermi('iot:push:add')")
@Log(title = "推送配置", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody @Validated ThingsModelPushConfig config) {
config.setCreateBy(SecurityUtils.getUsername());
return toAjax(pushConfigService.insertConfig(config));
}
/** 修改 */
@ApiOperation("修改推送配置")
@PreAuthorize("@ss.hasPermi('iot:push:edit')")
@Log(title = "推送配置", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody ThingsModelPushConfig config) {
config.setUpdateBy(SecurityUtils.getUsername());
return toAjax(pushConfigService.updateConfig(config));
}
/** 启用/停用 */
@ApiOperation("修改推送配置状态")
@PreAuthorize("@ss.hasPermi('iot:push:edit')")
@Log(title = "推送配置状态", businessType = BusinessType.UPDATE)
@PutMapping("/status")
public AjaxResult changeStatus(@RequestBody ThingsModelPushConfig config) {
ThingsModelPushConfig update = new ThingsModelPushConfig();
update.setId(config.getId());
update.setStatus(config.getStatus());
update.setUpdateBy(SecurityUtils.getUsername());
return toAjax(pushConfigService.updateConfig(update));
}
/** 删除 */
@ApiOperation("删除推送配置")
@PreAuthorize("@ss.hasPermi('iot:push:remove')")
@Log(title = "推送配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(pushConfigService.deleteByIds(ids));
}
/**
* 手动立即推送最新数据
*/
@ApiOperation("手动立即推送最新设备数据")
@PreAuthorize("@ss.hasPermi('iot:push:push')")
@Log(title = "手动推送", businessType = BusinessType.OTHER)
@PostMapping("/manual/{id}")
public AjaxResult manualPush(@PathVariable Long id) {
ThingsModelPushConfig config = pushConfigService.selectById(id);
if (config == null) {
return AjaxResult.error("推送配置不存在");
}
if (config.getStatus() == null || config.getStatus() == 0) {
return AjaxResult.error("推送配置已停用,请先启用");
}
pushHandler.doPush(config);
return AjaxResult.success("推送已触发");
}
}

View File

@ -0,0 +1,48 @@
package com.fastbee.iot.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 推送物模型项及等级阈值配置 iot_push_model_item
*
* @author sumuxi
*/
@Data
@ApiModel(value = "PushModelItem", description = "推送物模型项及等级阈值配置")
public class PushModelItem {
@ApiModelProperty("主键")
private Long id;
@ApiModelProperty("推送配置ID")
private Long configId;
@ApiModelProperty("物模型ID")
private Long modelId;
@ApiModelProperty("物模型标识符")
private String identifier;
@ApiModelProperty("物模型名称")
private String modelName;
@ApiModelProperty("单位")
private String unit;
@ApiModelProperty("绿色(正常)下限,null=不限")
private String greenMin;
@ApiModelProperty("绿色(正常)上限,null=不限")
private String greenMax;
@ApiModelProperty("黄色(告警)下限,null=不限")
private String yellowMin;
@ApiModelProperty("黄色(告警)上限,null=不限")
private String yellowMax;
@ApiModelProperty("排列顺序")
private Integer sortOrder;
}

View File

@ -0,0 +1,27 @@
package com.fastbee.iot.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 推送接收用户 iot_push_user
*
* @author sumuxi
*/
@Data
@ApiModel(value = "PushUser", description = "推送接收用户")
public class PushUser {
@ApiModelProperty("主键")
private Long id;
@ApiModelProperty("推送配置ID")
private Long configId;
@ApiModelProperty("用户ID")
private Long userId;
@ApiModelProperty("用户名")
private String userName;
}

View File

@ -0,0 +1,76 @@
package com.fastbee.iot.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fastbee.common.core.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.List;
/**
* 物模型数据推送配置 iot_things_model_push_config
*
* @author fastbee
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "ThingsModelPushConfig", description = "物模型数据推送配置")
public class ThingsModelPushConfig extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键")
private Long id;
@ApiModelProperty("配置名称")
private String configName;
@ApiModelProperty("产品ID")
private Long productId;
@ApiModelProperty("产品名称(非持久化字段)")
private String productName;
@ApiModelProperty("设备ID,0=产品级")
private Long deviceId;
@ApiModelProperty("设备编号")
private String serialNumber;
@ApiModelProperty("通知渠道ID")
private Long channelId;
@ApiModelProperty("通知渠道名称(非持久化字段)")
private String channelName;
@ApiModelProperty("微信公众号模板ID")
private String wxTemplateId;
@ApiModelProperty("消息点击跳转URL")
private String redirectUrl;
@ApiModelProperty("推送间隔(分钟),0=仅手动推送")
private Integer pushInterval;
@ApiModelProperty("最近推送时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastPushTime;
@ApiModelProperty("状态:0=停用 1=启用")
private Integer status;
@ApiModelProperty("租户ID")
private Long tenantId;
@ApiModelProperty("租户名称")
private String tenantName;
/** 物模型项列表(非持久化,前端用) */
private List<PushModelItem> items;
/** 接收用户列表(非持久化,前端用) */
private List<Long> userIds;
}

View File

@ -0,0 +1,57 @@
package com.fastbee.iot.mapper;
import com.fastbee.iot.domain.PushModelItem;
import com.fastbee.iot.domain.PushUser;
import com.fastbee.iot.domain.ThingsModelPushConfig;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.Date;
import java.util.List;
/**
* 物模型数据推送配置 Mapper
*
* @author fastbee
*/
@Repository
public interface ThingsModelPushConfigMapper {
ThingsModelPushConfig selectById(Long id);
List<ThingsModelPushConfig> selectList(ThingsModelPushConfig query);
/** 查询所有启用的推送配置(含物模型项和接收用户) */
List<ThingsModelPushConfig> selectAllEnabled();
/** 按产品ID+设备ID查询启用配置 */
List<ThingsModelPushConfig> selectEnabledByProductAndDevice(@Param("productId") Long productId,
@Param("deviceId") Long deviceId);
int insert(ThingsModelPushConfig config);
int update(ThingsModelPushConfig config);
int deleteById(Long id);
int deleteByIds(Long[] ids);
/** 更新最近推送时间 */
int updateLastPushTime(@Param("id") Long id, @Param("lastPushTime") Date lastPushTime);
// ---- 物模型项 ----
List<PushModelItem> selectItemsByConfigId(Long configId);
int insertItems(List<PushModelItem> items);
int deleteItemsByConfigId(Long configId);
// ---- 接收用户 ----
List<PushUser> selectUsersByConfigId(Long configId);
int insertUsers(List<PushUser> users);
int deleteUsersByConfigId(Long configId);
}

View File

@ -0,0 +1,32 @@
package com.fastbee.iot.service;
import com.fastbee.iot.domain.ThingsModelPushConfig;
import java.util.List;
/**
* 物模型数据推送配置 Service 接口
*
* @author sumuxi
*/
public interface IThingsModelPushConfigService {
ThingsModelPushConfig selectById(Long id);
List<ThingsModelPushConfig> selectList(ThingsModelPushConfig query);
/** 查询所有启用且设置了推送间隔的配置(用于定时任务) */
List<ThingsModelPushConfig> selectAllEnabled();
/** 按产品/设备查询启用配置 */
List<ThingsModelPushConfig> selectEnabledByProductAndDevice(Long productId, Long deviceId);
int insertConfig(ThingsModelPushConfig config);
int updateConfig(ThingsModelPushConfig config);
int deleteByIds(Long[] ids);
/** 更新最近推送时间 */
void updateLastPushTime(Long id);
}

View File

@ -0,0 +1,110 @@
package com.fastbee.iot.service.impl;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.iot.domain.PushModelItem;
import com.fastbee.iot.domain.PushUser;
import com.fastbee.iot.domain.ThingsModelPushConfig;
import com.fastbee.iot.mapper.ThingsModelPushConfigMapper;
import com.fastbee.iot.service.IThingsModelPushConfigService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 物模型数据推送配置 Service 实现
*
* @author fastbee
*/
@Slf4j
@Service
public class ThingsModelPushConfigServiceImpl implements IThingsModelPushConfigService {
@Resource
private ThingsModelPushConfigMapper mapper;
@Override
public ThingsModelPushConfig selectById(Long id) {
return mapper.selectById(id);
}
@Override
public List<ThingsModelPushConfig> selectList(ThingsModelPushConfig query) {
return mapper.selectList(query);
}
@Override
public List<ThingsModelPushConfig> selectAllEnabled() {
return mapper.selectAllEnabled();
}
@Override
public List<ThingsModelPushConfig> selectEnabledByProductAndDevice(Long productId, Long deviceId) {
return mapper.selectEnabledByProductAndDevice(productId, deviceId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int insertConfig(ThingsModelPushConfig config) {
config.setCreateTime(DateUtils.getNowDate());
config.setStatus(config.getStatus() == null ? 1 : config.getStatus());
config.setDeviceId(config.getDeviceId() == null ? 0L : config.getDeviceId());
config.setPushInterval(config.getPushInterval() == null ? 60 : config.getPushInterval());
int rows = mapper.insert(config);
saveItemsAndUsers(config);
return rows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int updateConfig(ThingsModelPushConfig config) {
config.setUpdateTime(DateUtils.getNowDate());
int rows = mapper.update(config);
mapper.deleteItemsByConfigId(config.getId());
mapper.deleteUsersByConfigId(config.getId());
saveItemsAndUsers(config);
return rows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteByIds(Long[] ids) {
for (Long id : ids) {
mapper.deleteItemsByConfigId(id);
mapper.deleteUsersByConfigId(id);
}
return mapper.deleteByIds(ids);
}
@Override
public void updateLastPushTime(Long id) {
mapper.updateLastPushTime(id, new Date());
}
private void saveItemsAndUsers(ThingsModelPushConfig config) {
if (CollectionUtils.isNotEmpty(config.getItems())) {
List<PushModelItem> items = config.getItems();
items.forEach(item -> {
item.setConfigId(config.getId());
if (item.getSortOrder() == null) {
item.setSortOrder(0);
}
});
mapper.insertItems(items);
}
if (CollectionUtils.isNotEmpty(config.getUserIds())) {
List<PushUser> users = config.getUserIds().stream().map(uid -> {
PushUser u = new PushUser();
u.setConfigId(config.getId());
u.setUserId(uid);
return u;
}).collect(Collectors.toList());
mapper.insertUsers(users);
}
}
}

View File

@ -0,0 +1,205 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fastbee.iot.mapper.ThingsModelPushConfigMapper">
<resultMap id="ConfigResult" type="com.fastbee.iot.domain.ThingsModelPushConfig">
<id property="id" column="id"/>
<result property="configName" column="config_name"/>
<result property="productId" column="product_id"/>
<result property="productName" column="product_name"/>
<result property="deviceId" column="device_id"/>
<result property="serialNumber" column="serial_number"/>
<result property="channelId" column="channel_id"/>
<result property="channelName" column="channel_name"/>
<result property="wxTemplateId" column="wx_template_id"/>
<result property="redirectUrl" column="redirect_url"/>
<result property="pushInterval" column="push_interval"/>
<result property="lastPushTime" column="last_push_time"/>
<result property="status" column="status"/>
<result property="tenantId" column="tenant_id"/>
<result property="tenantName" column="tenant_name"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<collection property="items" ofType="com.fastbee.iot.domain.PushModelItem"
select="selectItemsByConfigId" column="id"/>
<collection property="userIds" ofType="java.lang.Long"
select="selectUserIdsByConfigId" column="id"/>
</resultMap>
<resultMap id="ItemResult" type="com.fastbee.iot.domain.PushModelItem">
<id property="id" column="id"/>
<result property="configId" column="config_id"/>
<result property="modelId" column="model_id"/>
<result property="identifier" column="identifier"/>
<result property="modelName" column="model_name"/>
<result property="unit" column="unit"/>
<result property="greenMin" column="green_min"/>
<result property="greenMax" column="green_max"/>
<result property="yellowMin" column="yellow_min"/>
<result property="yellowMax" column="yellow_max"/>
<result property="sortOrder" column="sort_order"/>
</resultMap>
<resultMap id="UserResult" type="com.fastbee.iot.domain.PushUser">
<id property="id" column="id"/>
<result property="configId" column="config_id"/>
<result property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
</resultMap>
<sql id="baseColumns">
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.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
</sql>
<select id="selectById" resultMap="ConfigResult">
SELECT <include refid="baseColumns"/>
FROM iot_things_model_push_config c
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'
</select>
<select id="selectList" resultMap="ConfigResult">
SELECT <include refid="baseColumns"/>
FROM iot_things_model_push_config c
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'
<if test="configName != null and configName != ''">
AND c.config_name LIKE CONCAT('%', #{configName}, '%')
</if>
<if test="productId != null">
AND c.product_id = #{productId}
</if>
<if test="deviceId != null">
AND c.device_id = #{deviceId}
</if>
<if test="status != null">
AND c.status = #{status}
</if>
<if test="tenantId != null">
AND c.tenant_id = #{tenantId}
</if>
ORDER BY c.create_time DESC
</select>
<select id="selectAllEnabled" resultMap="ConfigResult">
SELECT <include refid="baseColumns"/>
FROM iot_things_model_push_config c
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
</select>
<select id="selectEnabledByProductAndDevice" resultMap="ConfigResult">
SELECT <include refid="baseColumns"/>
FROM iot_things_model_push_config c
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.product_id = #{productId}
AND (c.device_id = 0 OR c.device_id = #{deviceId})
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO iot_things_model_push_config
(config_name, product_id, device_id, serial_number, channel_id,
wx_template_id, redirect_url, push_interval, status,
tenant_id, tenant_name, create_by, create_time, del_flag)
VALUES
(#{configName}, #{productId}, #{deviceId}, #{serialNumber}, #{channelId},
#{wxTemplateId}, #{redirectUrl}, #{pushInterval}, #{status},
#{tenantId}, #{tenantName}, #{createBy}, #{createTime}, '0')
</insert>
<update id="update">
UPDATE iot_things_model_push_config
<set>
<if test="configName != null">config_name = #{configName},</if>
<if test="productId != null">product_id = #{productId},</if>
<if test="deviceId != null">device_id = #{deviceId},</if>
<if test="serialNumber != null">serial_number = #{serialNumber},</if>
<if test="channelId != null">channel_id = #{channelId},</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="status != null">status = #{status},</if>
update_by = #{updateBy},
update_time = #{updateTime}
</set>
WHERE id = #{id}
</update>
<update id="deleteById">
UPDATE iot_things_model_push_config SET del_flag = '2' WHERE id = #{id}
</update>
<update id="deleteByIds">
UPDATE iot_things_model_push_config SET del_flag = '2'
WHERE id IN
<foreach collection="array" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<update id="updateLastPushTime">
UPDATE iot_things_model_push_config SET last_push_time = #{lastPushTime} WHERE id = #{id}
</update>
<!-- 物模型项 -->
<select id="selectItemsByConfigId" resultMap="ItemResult">
SELECT id, config_id, model_id, identifier, model_name, unit,
green_min, green_max, yellow_min, yellow_max, sort_order
FROM iot_push_model_item
WHERE config_id = #{configId}
ORDER BY sort_order
</select>
<insert id="insertItems">
INSERT INTO iot_push_model_item
(config_id, model_id, identifier, model_name, unit,
green_min, green_max, yellow_min, yellow_max, sort_order)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.configId}, #{item.modelId}, #{item.identifier}, #{item.modelName}, #{item.unit},
#{item.greenMin}, #{item.greenMax}, #{item.yellowMin}, #{item.yellowMax}, #{item.sortOrder})
</foreach>
</insert>
<delete id="deleteItemsByConfigId">
DELETE FROM iot_push_model_item WHERE config_id = #{configId}
</delete>
<!-- 接收用户 -->
<select id="selectUsersByConfigId" resultMap="UserResult">
SELECT u.id, u.config_id, u.user_id, su.user_name
FROM iot_push_user u
LEFT JOIN sys_user su ON u.user_id = su.user_id
WHERE u.config_id = #{configId}
</select>
<select id="selectUserIdsByConfigId" resultType="java.lang.Long">
SELECT user_id FROM iot_push_user WHERE config_id = #{configId}
</select>
<insert id="insertUsers">
INSERT INTO iot_push_user (config_id, user_id, user_name)
VALUES
<foreach collection="list" item="u" separator=",">
(#{u.configId}, #{u.userId}, #{u.userName})
</foreach>
</insert>
<delete id="deleteUsersByConfigId">
DELETE FROM iot_push_user WHERE config_id = #{configId}
</delete>
</mapper>

View File

@ -0,0 +1,80 @@
-- ============================================================
-- 物模型数据推送配置 (微信服务号模板消息)
-- 执行顺序:在主库 fastbee 中执行
-- ============================================================
CREATE TABLE IF NOT EXISTS `iot_things_model_push_config` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`config_name` VARCHAR(128) NOT NULL COMMENT '配置名称',
`product_id` BIGINT NOT NULL COMMENT '产品ID',
`device_id` BIGINT DEFAULT 0 COMMENT '设备ID,0=产品级(产品下所有设备)',
`serial_number` VARCHAR(64) DEFAULT NULL COMMENT '设备编号(device_id>0时有效)',
`channel_id` BIGINT NOT NULL COMMENT '通知渠道ID (notify_channel.id,类型为 public_account)',
`wx_template_id` VARCHAR(128) NOT NULL COMMENT '微信公众号模板ID(在微信公众平台配置)',
`redirect_url` VARCHAR(512) DEFAULT NULL COMMENT '消息点击跳转URL',
`push_interval` INT DEFAULT 60 COMMENT '推送间隔(分钟),0=仅手动推送',
`last_push_time` DATETIME DEFAULT NULL COMMENT '最近一次推送时间',
`status` TINYINT DEFAULT 1 COMMENT '状态:0=停用 1=启用',
`tenant_id` BIGINT DEFAULT NULL COMMENT '租户ID',
`tenant_name` VARCHAR(64) DEFAULT NULL COMMENT '租户名称',
`create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
PRIMARY KEY (`id`),
KEY `idx_product_id` (`product_id`),
KEY `idx_device_id` (`device_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物模型数据推送配置';
CREATE TABLE IF NOT EXISTS `iot_push_model_item` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`config_id` BIGINT NOT NULL COMMENT '推送配置ID',
`model_id` BIGINT DEFAULT NULL COMMENT '物模型ID(iot_things_model.model_id)',
`identifier` VARCHAR(128) NOT NULL COMMENT '物模型标识符',
`model_name` VARCHAR(128) DEFAULT NULL COMMENT '物模型名称(冗余展示用)',
`unit` VARCHAR(32) DEFAULT NULL COMMENT '单位(冗余展示用)',
`green_min` VARCHAR(64) DEFAULT NULL COMMENT '绿色(正常)下限,NULL表示不限',
`green_max` VARCHAR(64) DEFAULT NULL COMMENT '绿色(正常)上限,NULL表示不限',
`yellow_min` VARCHAR(64) DEFAULT NULL COMMENT '黄色(告警)下限,NULL表示不限',
`yellow_max` VARCHAR(64) DEFAULT NULL COMMENT '黄色(告警)上限,NULL表示不限',
`sort_order` INT DEFAULT 0 COMMENT '排列顺序',
PRIMARY KEY (`id`),
KEY `idx_config_id` (`config_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推送物模型项及等级阈值配置';
CREATE TABLE IF NOT EXISTS `iot_push_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`config_id` BIGINT NOT NULL COMMENT '推送配置ID',
`user_id` BIGINT NOT NULL COMMENT '系统用户ID(需已绑定微信公众号)',
`user_name` VARCHAR(64) DEFAULT NULL COMMENT '用户名(冗余展示用)',
PRIMARY KEY (`id`),
KEY `idx_config_id` (`config_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推送接收用户';
-- ============================================================
-- 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)
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)
VALUES (3431, '推送配置查询', 3430, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:push:query', '#', '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)
VALUES (3432, '推送配置新增', 3430, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:push:add', '#', '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)
VALUES (3433, '推送配置修改', 3430, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:push:edit', '#', '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)
VALUES (3434, '推送配置删除', 3430, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:push:remove', '#', '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)
VALUES (3435, '手动立即推送', 3430, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:push:push', '#', 'admin', now(), 'admin', now(), '');

View File

@ -0,0 +1,38 @@
import request from '@/utils/request';
const BASE = '/iot/push/config';
// 查询推送配置列表
export function listPushConfig(query) {
return request({ url: `${BASE}/list`, method: 'get', params: query });
}
// 获取推送配置详情
export function getPushConfig(id) {
return request({ url: `${BASE}/${id}`, method: 'get' });
}
// 新增推送配置
export function addPushConfig(data) {
return request({ url: BASE, method: 'post', data });
}
// 修改推送配置
export function updatePushConfig(data) {
return request({ url: BASE, method: 'put', data });
}
// 修改状态
export function changePushConfigStatus(id, status) {
return request({ url: `${BASE}/status`, method: 'put', data: { id, status } });
}
// 删除推送配置
export function delPushConfig(ids) {
return request({ url: `${BASE}/${ids}`, method: 'delete' });
}
// 手动立即推送最新数据
export function manualPush(id) {
return request({ url: `${BASE}/manual/${id}`, method: 'post' });
}

View File

@ -0,0 +1,384 @@
<template>
<div class="iot-push-config">
<!-- 搜索栏 -->
<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-item label="配置名称" prop="configName">
<el-input v-model="queryParams.configName" placeholder="请输入配置名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="产品" prop="productId">
<el-select v-model="queryParams.productId" placeholder="请选择产品" clearable filterable style="width:160px">
<el-option v-for="p in productOptions" :key="p.productId" :label="p.productName" :value="p.productId" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width:100px">
<el-option label="启用" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<el-form-item style="float: right">
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-row :gutter="10" style="margin-bottom: 15px">
<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-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<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="productName" min-width="120" show-overflow-tooltip />
<el-table-column label="通知渠道" align="center" prop="channelName" min-width="120" show-overflow-tooltip />
<el-table-column label="推送间隔" align="center" width="110">
<template slot-scope="scope">
<span v-if="scope.row.pushInterval > 0">每 {{ scope.row.pushInterval }} 分钟</span>
<el-tag v-else size="small" type="info">仅手动</el-tag>
</template>
</el-table-column>
<el-table-column label="最近推送" align="center" prop="lastPushTime" width="155">
<template slot-scope="scope">
<span>{{ scope.row.lastPushTime || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" width="80">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0"
@change="handleStatusChange(scope.row)" v-hasPermi="['iot:push:edit']" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" fixed="right">
<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-video-play" style="color:#67C23A"
@click="handleManualPush(scope.row)" v-hasPermi="['iot:push:push']">立即推送</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#F56C6C"
@click="handleDelete(scope.row)" v-hasPermi="['iot:push:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="900px" append-to-body
:close-on-click-modal="false" :close-on-press-escape="false">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="配置名称" prop="configName">
<el-input v-model="form.configName" placeholder="请输入配置名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品" prop="productId">
<el-select v-model="form.productId" placeholder="请选择产品" filterable style="width:100%"
@change="onProductChange">
<el-option v-for="p in productOptions" :key="p.productId" :label="p.productName" :value="p.productId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="通知渠道" prop="channelId">
<el-select v-model="form.channelId" placeholder="请选择公众号渠道" filterable style="width:100%">
<el-option v-for="c in channelOptions" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
</el-form-item>
</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-col>
<el-col :span="12">
<el-form-item label="推送间隔(分)" prop="pushInterval">
<el-input-number v-model="form.pushInterval" :min="0" :max="1440" 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-col>
<el-col :span="24">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<!-- 物模型选择与阈值配置 -->
<el-divider content-position="left">监测物模型及数据等级阈值</el-divider>
<div style="margin-bottom:10px;color:#606266;font-size:12px">
绿色(✅ 正常):值在 [绿下限, 绿上限] 范围内;
黄色(⚠️ 告警):值在 [黄下限, 黄上限] 范围内;
红色(🔴 警报):其余情况。留空表示不限。
</div>
<el-table :data="form.items" size="mini" border>
<el-table-column label="物模型" min-width="160">
<template slot-scope="scope">
<el-select v-model="scope.row.identifier" placeholder="选择物模型属性" filterable
size="mini" @change="(v) => onModelItemChange(v, scope.row)">
<el-option v-for="m in thingsModelOptions" :key="m.identifier"
:label="m.modelName + ' (' + m.identifier + ')'" :value="m.identifier" />
</el-select>
</template>
</el-table-column>
<el-table-column label="单位" width="70">
<template slot-scope="scope">
<el-input v-model="scope.row.unit" size="mini" />
</template>
</el-table-column>
<el-table-column label="绿色下限" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.greenMin" size="mini" placeholder="不限" />
</template>
</el-table-column>
<el-table-column label="绿色上限" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.greenMax" size="mini" placeholder="不限" />
</template>
</el-table-column>
<el-table-column label="黄色下限" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.yellowMin" size="mini" placeholder="不限" />
</template>
</el-table-column>
<el-table-column label="黄色上限" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.yellowMax" size="mini" placeholder="不限" />
</template>
</el-table-column>
<el-table-column label="操作" width="70" align="center">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-delete" style="color:#F56C6C"
size="mini" @click="removeModelItem(scope.$index)" />
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addModelItem">添加物模型</el-button>
<!-- 接收用户 -->
<el-divider content-position="left">接收用户(需已绑定微信公众号)</el-divider>
<el-select v-model="form.userIds" multiple filterable placeholder="请选择接收用户"
style="width:100%" size="small">
<el-option v-for="u in userOptions" :key="u.userId" :value="u.userId"
:label="u.nickName + ' (' + u.userName + ')'" />
</el-select>
</el-form>
<div slot="footer">
<el-button @click="dialogOpen = false">取 消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listPushConfig, getPushConfig, addPushConfig, updatePushConfig,
changePushConfigStatus, delPushConfig, manualPush
} from '@/api/iot/pushConfig';
import { listProduct } from '@/api/iot/product';
import { listModel } from '@/api/iot/model';
import { listChannel } from '@/api/notify/channel';
import { listUser } from '@/api/system/user';
const emptyForm = () => ({
id: null,
configName: '',
productId: null,
deviceId: 0,
channelId: null,
wxTemplateId: '',
redirectUrl: '',
pushInterval: 60,
status: 1,
items: [],
userIds: []
});
export default {
name: 'PushConfig',
data() {
return {
loading: false,
showSearch: true,
total: 0,
configList: [],
queryParams: { pageNum: 1, pageSize: 10, configName: '', productId: null, status: null },
dialogOpen: false,
dialogTitle: '',
submitLoading: false,
form: emptyForm(),
rules: {
configName: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
productId: [{ required: true, message: '请选择产品', trigger: 'change' }],
channelId: [{ required: true, message: '请选择通知渠道', trigger: 'change' }],
wxTemplateId:[{ required: true, message: '请输入微信模板ID', trigger: 'blur' }],
pushInterval:[{ required: true, message: '请填写推送间隔', trigger: 'blur' }]
},
productOptions: [],
channelOptions: [],
thingsModelOptions: [],
userOptions: []
};
},
created() {
this.getList();
this.loadProductOptions();
this.loadChannelOptions();
this.loadUserOptions();
},
methods: {
getList() {
this.loading = true;
listPushConfig(this.queryParams).then(res => {
this.configList = res.rows;
this.total = res.total;
}).finally(() => { this.loading = false; });
},
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
resetQuery() {
this.resetForm('queryForm');
this.queryParams = { pageNum: 1, pageSize: 10, configName: '', productId: null, status: null };
this.getList();
},
loadProductOptions() {
listProduct({ pageNum: 1, pageSize: 999 }).then(res => {
this.productOptions = res.rows || [];
});
},
loadChannelOptions() {
// 只加载微信公众号渠道
listChannel({ channelType: 'wechat', provider: 'public_account', pageNum: 1, pageSize: 100 }).then(res => {
this.channelOptions = res.rows || [];
}).catch(() => {
listChannel({ pageNum: 1, pageSize: 100 }).then(res => {
this.channelOptions = (res.rows || []).filter(c => c.provider === 'public_account');
});
});
},
loadUserOptions() {
listUser({ pageNum: 1, pageSize: 500 }).then(res => {
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) {
const model = this.thingsModelOptions.find(m => m.identifier === identifier);
if (model) {
row.modelId = model.modelId;
row.modelName = model.modelName;
row.unit = model.unit || '';
}
},
addModelItem() {
this.form.items.push({
id: null, configId: null, modelId: null,
identifier: '', modelName: '', unit: '',
greenMin: null, greenMax: null,
yellowMin: null, yellowMax: null,
sortOrder: this.form.items.length
});
},
removeModelItem(index) {
this.form.items.splice(index, 1);
},
handleAdd() {
this.form = emptyForm();
this.thingsModelOptions = [];
this.dialogTitle = '新增推送配置';
this.dialogOpen = true;
this.$nextTick(() => this.resetForm('form'));
},
handleUpdate(row) {
getPushConfig(row.id).then(res => {
const data = res.data;
this.form = {
...data,
userIds: data.userIds || [],
items: data.items || []
};
if (data.productId) {
listModel({ productId: data.productId, type: 1, pageNum: 1, pageSize: 500 }).then(r => {
this.thingsModelOptions = r.rows || [];
});
}
this.dialogTitle = '编辑推送配置';
this.dialogOpen = true;
});
},
handleStatusChange(row) {
const text = row.status === 1 ? '启用' : '停用';
this.$confirm(`确认${text}【${row.configName}】?`, '提示', { type: 'warning' }).then(() => {
changePushConfigStatus(row.id, row.status).then(() => {
this.$message.success(`${text}成功`);
});
}).catch(() => { row.status = row.status === 1 ? 0 : 1; });
},
handleManualPush(row) {
this.$confirm(`确认立即推送【${row.configName}】的最新设备数据?`, '手动推送', { type: 'info' }).then(() => {
manualPush(row.id).then(res => {
this.$message.success(res.msg || '推送已触发');
this.getList();
});
});
},
handleDelete(row) {
this.$confirm(`确认删除推送配置【${row.configName}】?`, '警告', { type: 'warning' }).then(() => {
delPushConfig(row.id).then(() => {
this.$message.success('删除成功');
this.getList();
});
});
},
submitForm() {
this.$refs.form.validate(valid => {
if (!valid) return;
if (this.form.items.length === 0) {
this.$message.warning('请至少添加一个监测物模型');
return;
}
const hasEmptyIdentifier = this.form.items.some(i => !i.identifier);
if (hasEmptyIdentifier) {
this.$message.warning('存在未选择物模型的行,请完善');
return;
}
this.submitLoading = true;
const api = this.form.id ? updatePushConfig : addPushConfig;
api(this.form).then(() => {
this.$message.success(this.form.id ? '修改成功' : '新增成功');
this.dialogOpen = false;
this.getList();
}).finally(() => { this.submitLoading = false; });
});
}
}
};
</script>
<style scoped>
.iot-push-config .el-divider {
margin: 15px 0 10px;
}
</style>