Browse Source

update: 全局营销规则

lwh 1 week ago
parent
commit
6eafac455d
13 changed files with 572 additions and 238 deletions
  1. 166 12
      pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/ip/IPUtils.java
  2. 36 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/MarketingConfigConstants.java
  3. 79 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/SaveGlobalRuleDTO.java
  4. 4 3
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/rule/SaveMktRuleDTO.java
  5. 6 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MktMgmtRule.java
  6. 37 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/DomainValidationUtil.java
  7. 3 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/mongo/PageDeviceInfoVO.java
  8. 6 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/rule/PageMktRuleVO.java
  9. 20 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/controller/MarketingConfigController.java
  10. 14 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/MarketingConfigService.java
  11. 152 11
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MarketingConfigServiceImpl.java
  12. 19 212
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MktMgmtRuleServiceImpl.java
  13. 30 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/TcpDataServiceImpl.java

+ 166 - 12
pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/ip/IPUtils.java

@@ -1,6 +1,7 @@
 package com.pig4cloud.pig.common.core.util.ip;
 
 import cn.hutool.core.util.StrUtil;
+import com.pig4cloud.pig.common.core.exception.BusinessException;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.extern.log4j.Log4j2;
 import org.lionsoul.ip2region.xdb.Searcher;
@@ -13,7 +14,10 @@ import java.net.InetAddress;
 import java.net.URL;
 import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -134,18 +138,6 @@ public class IPUtils {
 		return ipToLong(startIp) < ipToLong(endIp);
 	}
 
-	/**
-	 * IP地址转Long(便于比较大小)
-	 */
-	private static long ipToLong(String ip) {
-		String[] parts = ip.split("\\.");
-		long result = 0;
-		for (int i = 0; i < 4; i++) {
-			result |= Long.parseLong(parts[i]) << (24 - i * 8);
-		}
-		return result;
-	}
-
 	/**
 	 * 获取IP归属地(国家-省份)
 	 * @param ip ip
@@ -245,4 +237,166 @@ public class IPUtils {
 		System.out.println(getIpRegion("invalid.ip"));     // 未知(非法IP)
 		System.out.println(getIpRegion(""));               // 未知(空IP)
 	}
+
+	/**
+	 * 验证IP列表,检查是否存在冲突
+	 * @param ipList IP列表
+	 */
+	public static void validateIpList(List<String> ipList) {
+		if (ipList == null || ipList.isEmpty()) {
+			return;
+		}
+
+		// 过滤空值和去重
+		List<String> validIps = ipList.stream()
+				.filter(org.springframework.util.StringUtils::hasText)
+				.map(String::trim)
+				.toList();
+
+		// 检查是否有重复的单IP
+		Set<String> singleIps = new HashSet<>();
+		Set<String> ipRanges = new HashSet<>();
+
+		for (String ip : validIps) {
+			if (ip.contains("/")) {
+				// IP段格式
+				ipRanges.add(ip);
+			} else {
+				// 单IP格式
+				if (!singleIps.add(ip)) {
+					throw new BusinessException("IP列表中存在重复的单IP: " + ip);
+				}
+			}
+		}
+
+		// 检查是否有重复的IP段
+		List<String> rangeList = validIps.stream()
+				.filter(ip -> ip.contains("/"))
+				.toList();
+		Set<String> uniqueRanges = new HashSet<>(rangeList);
+		if (rangeList.size() != uniqueRanges.size()) {
+			throw new BusinessException("IP列表中存在重复的IP段");
+		}
+
+		// 检查单IP是否在IP段中
+		for (String singleIp : singleIps) {
+			for (String ipRange : ipRanges) {
+				if (isIpInRange(singleIp, ipRange)) {
+					throw new BusinessException("单IP '" + singleIp + "' 在IP段 '" + ipRange + "' 中,存在冲突");
+				}
+			}
+		}
+
+		// 检查IP段之间是否重叠
+		List<String> uniqueRangeList = new ArrayList<>(ipRanges);
+		for (int i = 0; i < uniqueRangeList.size(); i++) {
+			for (int j = i + 1; j < uniqueRangeList.size(); j++) {
+				if (isIpRangesOverlap(uniqueRangeList.get(i), uniqueRangeList.get(j))) {
+					throw new BusinessException("IP段 '" + uniqueRangeList.get(i) + "' 和 '" + uniqueRangeList.get(j) + "' 存在重叠");
+				}
+			}
+		}
+	}
+
+	/**
+	 * 检查单IP是否在IP段范围内
+	 * @param singleIp 单IP
+	 * @param ipRange IP段 (格式: 192.168.10.200/220 表示 192.168.10.200~192.168.10.220)
+	 * @return 是否在范围内
+	 */
+	public static boolean isIpInRange(String singleIp, String ipRange) {
+		if (!IPUtils.isValidIp(singleIp)) {
+			return false;
+		}
+
+		String[] parts = ipRange.split("/");
+		if (parts.length != 2) {
+			return false;
+		}
+
+		String startIp = parts[0];
+		String endPart = parts[1];
+
+		if (!IPUtils.isValidIp(startIp)) {
+			return false;
+		}
+
+		// 构造完整的结束IP
+		String[] startParts = startIp.split("\\.");
+		String endIp = String.format("%s.%s.%s.%s",
+				startParts[0], startParts[1], startParts[2], endPart);
+
+		if (!IPUtils.isValidIp(endIp)) {
+			return false;
+		}
+
+		long singleIpLong = ipToLong(singleIp);
+		long startIpLong = ipToLong(startIp);
+		long endIpLong = ipToLong(endIp);
+
+		// 确保开始IP小于等于结束IP
+		if (startIpLong > endIpLong) {
+			long temp = startIpLong;
+			startIpLong = endIpLong;
+			endIpLong = temp;
+		}
+
+		return singleIpLong >= startIpLong && singleIpLong <= endIpLong;
+	}
+
+	/**
+	 * 检查两个IP段是否重叠
+	 * @param range1 第一个IP段
+	 * @param range2 第二个IP段
+	 * @return 是否重叠
+	 */
+	public static boolean isIpRangesOverlap(String range1, String range2) {
+		String[] parts1 = range1.split("/");
+		String[] parts2 = range2.split("/");
+
+		if (parts1.length != 2 || parts2.length != 2) {
+			return false;
+		}
+
+		String network1 = parts1[0];
+		String network2 = parts2[0];
+		int prefix1 = Integer.parseInt(parts1[1]);
+		int prefix2 = Integer.parseInt(parts2[1]);
+
+		if (!IPUtils.isValidIp(network1) || !IPUtils.isValidIp(network2)) {
+			return false;
+		}
+
+		long network1Long = ipToLong(network1);
+		long network2Long = ipToLong(network2);
+
+		// 计算网络掩码
+		long mask1 = (0xFFFFFFFFL << (32 - prefix1)) & 0xFFFFFFFFL;
+		long mask2 = (0xFFFFFFFFL << (32 - prefix2)) & 0xFFFFFFFFL;
+
+		// 计算网络地址
+		long net1 = network1Long & mask1;
+		long net2 = network2Long & mask2;
+
+		// 计算广播地址
+		long broadcast1 = net1 | (~mask1 & 0xFFFFFFFFL);
+		long broadcast2 = net2 | (~mask2 & 0xFFFFFFFFL);
+
+		// 检查是否重叠:一个网络的开始地址小于等于另一个网络的结束地址,且一个网络的结束地址大于等于另一个网络的开始地址
+		return net1 <= broadcast2 && broadcast1 >= net2;
+	}
+
+	/**
+	 * 将IP地址转换为长整型
+	 * @param ip IP地址
+	 * @return 长整型值
+	 */
+	public static long ipToLong(String ip) {
+		String[] parts = ip.split("\\.");
+		long result = 0;
+		for (int i = 0; i < 4; i++) {
+			result |= Long.parseLong(parts[i]) << (24 - i * 8);
+		}
+		return result;
+	}
 }

+ 36 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/MarketingConfigConstants.java

@@ -14,4 +14,40 @@ public interface MarketingConfigConstants {
 	String TRIGGER_NUM = "TRIGGER_NUM";
 	String PROMPT_MSG = "PROMPT_MSG";
 	String URL = "URL";
+
+
+	/**
+	 * 推送类型: 0-图片,1-链接
+	 */
+	String PUSH_TYPE = "PUSH_TYPE";
+	/**
+	 * 推送频率
+	 */
+	String PUSH_FREQUENCY = "PUSH_FREQUENCY";
+	/**
+	 * 延时推送,单位秒
+	 */
+	String DELAY_PUSH = "DELAY_PUSH";
+	/**
+	 * 推送方式,1-弹出提示,2-打开网址,3-内置浏览器打开网址,4-使用webview打开网址
+	 */
+	String ACTION = "ACTION";
+	/**
+	 * 推送内容
+	 */
+	String PUSH_CONTENT = "PUSH_CONTENT";
+	/**
+	 * 自动推送
+	 */
+	String AUTO_PUSH = "AUTO_PUSH";
+
+	/**
+	 * 推送IP
+	 */
+	String PUSH_IP = "PUSH_IP";
+
+	/**
+	 * 推送域名
+	 */
+	String PUSH_DOMAIN = "PUSH_DOMAIN";
 }

+ 79 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/SaveGlobalRuleDTO.java

@@ -0,0 +1,79 @@
+package com.pig4cloud.pig.marketing.api.dto.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-09-03
+ * @description: 保存全局规则入参
+ */
+@Data
+@Schema(description = "保存全局规则入参")
+public class SaveGlobalRuleDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * ip
+	 */
+	@Schema(description = "ip列表")
+	private List<String> ip;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名列表")
+	private List<String> domain;
+
+	/**
+	 * 推送内容
+	 */
+	@Schema(description = "推送内容")
+	@NotBlank(message = "推送内容不能为空")
+	private String pushContent;
+
+	/**
+	 * 推送类型
+	 */
+	@Schema(description = "推送类型,true-图片,false-链接")
+	@NotNull(message = "推送类型不能为空")
+	private Boolean pushType;
+
+	/**
+	 * 推送频率
+	 */
+	@Schema(description = "推送频率")
+	@NotBlank(message = "推送频率不能为空")
+	private String pushFrequency;
+
+	/**
+	 * 延迟推送
+	 */
+	@Schema(description = "延迟推送,单位秒")
+	@NotNull(message = "延迟推送不能为空")
+	private String delayPush;
+
+	/**
+	 * 推送方式,1-弹出提示,2-打开网址,3-内置浏览器打开网址,4-使用webview打开网址
+	 */
+	@NotBlank(message = "推送方式不能为空")
+	@Schema(description = "推送方式,1-弹出提示,2-打开网址,3-内置浏览器打开网址,4-使用webview打开网址")
+	private String action;
+
+	/**
+	 * 自动推送
+	 */
+	@NotNull(message = "自动推送不能为空")
+	@Schema(description = "自动推送")
+	private Boolean autoPush;
+
+}

+ 4 - 3
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/rule/SaveMktRuleDTO.java

@@ -44,9 +44,6 @@ public class SaveMktRuleDTO implements Serializable {
 	@NotBlank(message = "推送内容不能为空")
 	private String pushContent;
 
-	/**
-	 * 推送类型
-	 */
 	@Schema(description = "推送类型,true-图片,false-链接")
 	@NotNull(message = "推送类型不能为空")
 	private Boolean pushType;
@@ -55,6 +52,10 @@ public class SaveMktRuleDTO implements Serializable {
 	@NotBlank(message = "推送频率不能为空")
 	private String pushFrequency;
 
+	@Schema(description = "延迟推送,单位整数秒")
+	@NotNull(message = "延迟推送不能为空")
+	private Integer delayPush;
+
 	@Schema(description = "推送方式,1-弹出提示,2-打开网址,3-内置浏览器打开网址,4-使用webview打开网址",example = "1")
 	@NotNull(message = "推送方式不能为空")
 	private String action;

+ 6 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MktMgmtRule.java

@@ -55,6 +55,12 @@ public class MktMgmtRule extends Model<MktMgmtRule> {
 	@Schema(description = "推送频率")
 	private String pushFrequency;
 
+	/**
+	 * 延迟推送
+	 */
+	@Schema(description = "延迟推送,单位秒")
+	private Integer delayPush;
+
 	/**
 	 * 推送方式
 	 */

+ 37 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/DomainValidationUtil.java

@@ -1,6 +1,12 @@
 package com.pig4cloud.pig.marketing.api.util;
 
 
+import com.pig4cloud.pig.common.core.exception.BusinessException;
+import org.springframework.util.StringUtils;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 import java.util.regex.Pattern;
 
 /**
@@ -161,4 +167,35 @@ public class DomainValidationUtil {
 
 		return null;
 	}
+
+	/**
+	 * 验证域名列表,检查格式和重复
+	 * @param domainList 域名列表
+	 */
+	public static void validateDomainList(List<String> domainList) {
+		if (domainList == null || domainList.isEmpty()) {
+			return;
+		}
+
+		// 过滤空值,但不去重(保留重复项用于检测)
+		List<String> validDomains = domainList.stream()
+				.filter(StringUtils::hasText)
+				.map(String::trim)
+				.toList();
+
+		// 检查是否有重复的域名
+		Set<String> uniqueDomains = new HashSet<>();
+		for (String domain : validDomains) {
+			if (!uniqueDomains.add(domain)) {
+				throw new BusinessException("域名列表中存在重复的域名: " + domain);
+			}
+		}
+
+		// 校验每个域名的格式
+		for (String domain : validDomains) {
+			if (!DomainValidationUtil.isValidDomain2(domain)) {
+				throw new BusinessException("域名格式错误: " + domain);
+			}
+		}
+	}
 }

+ 3 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/mongo/PageDeviceInfoVO.java

@@ -34,6 +34,9 @@ public class PageDeviceInfoVO implements Serializable {
 	@Schema(description = "创建时间")
 	private String createTime;
 
+	@Schema(description = "最后推送时间")
+	private String latestPushTime;
+
 	@Schema(description = "更新时间")
 	private String updateTime;
 

+ 6 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/rule/PageMktRuleVO.java

@@ -65,6 +65,12 @@ public class PageMktRuleVO implements Serializable {
 	@Schema(description = "推送频率")
 	private String pushFrequency;
 
+	/**
+	 * 延迟推送
+	 */
+	@Schema(description = "延迟推送,单位秒")
+	private Integer delayPush;
+
 	/**
 	 * 推送方式,1-弹出提示,2-打开网址,3-内置浏览器打开网址,4-使用webview打开网址
 	 */

+ 20 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/controller/MarketingConfigController.java

@@ -4,6 +4,7 @@ package com.pig4cloud.pig.marketing.controller;
 import com.pig4cloud.pig.common.core.exception.BusinessException;
 import com.pig4cloud.pig.common.core.util.R;
 import com.pig4cloud.pig.marketing.api.dto.config.*;
+import com.pig4cloud.pig.marketing.api.dto.rule.SaveMktRuleDTO;
 import com.pig4cloud.pig.marketing.api.vo.config.*;
 import com.pig4cloud.pig.marketing.service.MarketingConfigService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -195,4 +196,23 @@ public class MarketingConfigController {
 		return R.ok(marketingConfigService.setMarketingGlobalConfig(reDto));
 	}
 
+	/**
+	 * 查询全局推送规则
+	 * @return R
+	 */
+	@GetMapping("/getRule")
+	@Operation(summary = "查询全局推送规则")
+	public R<SaveGlobalRuleDTO> getGlobalRule() {
+		return R.ok(marketingConfigService.getGlobalRule());
+	}
+
+	/**
+	 * 设置全局推送规则
+	 * @return R
+	 */
+	@PostMapping("/setRule")
+	@Operation(summary = "设置全局推送规则")
+	public R<Boolean> saveGlobalRule(@Valid @RequestBody SaveGlobalRuleDTO reqDto) {
+		return R.ok(marketingConfigService.saveGlobalRule(reqDto));
+	}
 }

+ 14 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/MarketingConfigService.java

@@ -115,4 +115,18 @@ public interface MarketingConfigService {
 	 * @return 全局配置信息
 	 */
 	Boolean setMarketingGlobalConfig(GetMarketingGlobalConfigVO reDto);
+
+	/**
+	 * 获取全局规则
+	 * @return 全局规则
+	 */
+	SaveGlobalRuleDTO getGlobalRule();
+
+	/**
+	 * 保存全局规则
+	 * @param reqDto 保存全局规则入参
+	 * @return Boolean
+	 */
+	Boolean saveGlobalRule(@Valid SaveGlobalRuleDTO reqDto);
+
 }

+ 152 - 11
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MarketingConfigServiceImpl.java

@@ -616,15 +616,156 @@ public class MarketingConfigServiceImpl implements MarketingConfigService {
 		return null;
 	}
 
-//	@Override
-//	public Boolean getMarketingConfig(AddMarketingConfigDomainListDTO reqDto) {
-//		// 判断域名表中是否重复
-//		// 判断分组中是否重复
-//
-//		MarketingAppsDomain domain = new MarketingAppsDomain();
-//		BeanUtils.copyProperties(reqDto, domain);
-//		domain.setConfig( true);
-//		int insert = domainMapper.insert(domain);
-//		return insert > 0;
-//	}
+	@Override
+	public SaveGlobalRuleDTO getGlobalRule() {
+		SaveGlobalRuleDTO res = new SaveGlobalRuleDTO();
+		R<ArrayList<SysPublicParamDTO>> ParamDTO = remoteParamService.getParamList(Arrays.asList(
+				PUSH_TYPE,
+				PUSH_FREQUENCY,
+				DELAY_PUSH,
+				ACTION,
+				PUSH_CONTENT,
+				AUTO_PUSH,
+				PUSH_IP,
+				PUSH_DOMAIN
+		));
+		ParamDTO.getData().forEach(item -> {
+			switch (item.getPublicKey()) {
+				case PUSH_TYPE:
+					res.setPushType("0".equals(item.getPublicValue()));
+					break;
+				case PUSH_FREQUENCY:
+					res.setPushFrequency(item.getPublicValue());
+					break;
+				case DELAY_PUSH:
+					res.setDelayPush(item.getPublicValue());
+					break;
+				case ACTION:
+					res.setAction(item.getPublicValue());
+					break;
+				case PUSH_CONTENT:
+					res.setPushContent(item.getPublicValue());
+					break;
+				case AUTO_PUSH:
+					res.setAutoPush("0".equals(item.getPublicValue()));
+					break;
+				case PUSH_IP:
+					if (StringUtils.isNotBlank(item.getPublicValue())) {
+						String[] ip = item.getPublicValue().split("&&");
+						res.setIp(List.of(ip));
+					} else {
+						res.setIp(new ArrayList<>());
+					}
+					break;
+				case PUSH_DOMAIN:
+					if (StringUtils.isNotBlank(item.getPublicValue())) {
+						String[] domain = item.getPublicValue().split("&&");
+						res.setDomain(List.of(domain));
+					} else {
+						res.setDomain(new ArrayList<>());
+					}
+					break;
+			}
+		});
+		return res;
+	}
+
+	/**
+	 * 保存全局规则
+	 * @param reqDto 保存全局规则入参
+	 * @return Boolean
+	 */
+	@Override
+	public Boolean saveGlobalRule(SaveGlobalRuleDTO reqDto) {
+		List<SysPublicParamDTO> sysParamList = new ArrayList<>();
+		SysPublicParamDTO sysParam;
+		// 处理IP列表
+		StringBuilder ipBuilder = new StringBuilder();
+		if (reqDto.getIp() != null && !reqDto.getIp().isEmpty()){
+			IPUtils.validateIpList(reqDto.getIp());
+			for (String ip : reqDto.getIp()){
+				ipBuilder.append( ip);
+				ipBuilder.append("&&");
+			}
+			ipBuilder.delete(ipBuilder.length()-2, ipBuilder.length());
+		}
+		// 处理域名列表
+		StringBuilder domainBuilder = new StringBuilder();
+		if (reqDto.getDomain() != null && !reqDto.getDomain().isEmpty()){
+			DomainValidationUtil.validateDomainList(reqDto.getDomain());
+			for (String domain : reqDto.getDomain()){
+				domainBuilder.append( domain);
+				domainBuilder.append("&&");
+			}
+			domainBuilder.delete(domainBuilder.length()-2, domainBuilder.length());
+		}
+		// 添加IP
+		sysParam = new SysPublicParamDTO();
+		sysParam.setPublicKey(PUSH_IP);
+		sysParam.setPublicValue(ipBuilder.toString());
+		sysParamList.add(sysParam);
+
+		// 添加域名
+		sysParam = new SysPublicParamDTO();
+		sysParam.setPublicKey(PUSH_DOMAIN);
+		sysParam.setPublicValue(domainBuilder.toString());
+		sysParamList.add(sysParam);
+
+		// 推送类型
+		if (reqDto.getPushType() != null){
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(PUSH_TYPE);
+			sysParam.setPublicValue(reqDto.getPushType() ? "0" : "1");
+			sysParamList.add(sysParam);
+		}
+
+		// 处理推送频率
+		if (StringUtils.isNotBlank(reqDto.getPushFrequency())){
+			BigDecimal bigDecimal = new BigDecimal(reqDto.getPushFrequency());
+			if (bigDecimal.compareTo(BigDecimal.ZERO) < 0){
+				throw new BusinessException("推送频率不能小于0");
+			}
+			if (bigDecimal.compareTo(new BigDecimal("10000")) > 0) {
+				throw new BusinessException("推送频率不能大于10000");
+			}
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(PUSH_FREQUENCY);
+			sysParam.setPublicValue(reqDto.getPushFrequency());
+			sysParamList.add(sysParam);
+		}
+
+		// 处理延时推送
+		if (StringUtils.isNotBlank(reqDto.getDelayPush())){
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(DELAY_PUSH);
+			sysParam.setPublicValue(reqDto.getDelayPush());
+			sysParamList.add(sysParam);
+		}
+
+		// 处理推送方式
+		if (StringUtils.isNotBlank(reqDto.getAction())){
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(ACTION);
+			sysParam.setPublicValue(reqDto.getAction());
+			sysParamList.add(sysParam);
+		}
+
+		// 处理推送内容
+		if (StringUtils.isNotBlank(reqDto.getPushContent())){
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(PUSH_CONTENT);
+			sysParam.setPublicValue(reqDto.getPushContent());
+			sysParamList.add(sysParam);
+		}
+
+		// 处理推送方式
+		if (reqDto.getAutoPush() != null){
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(AUTO_PUSH);
+			sysParam.setPublicValue(reqDto.getAutoPush() ? "0" : "1");
+			sysParamList.add(sysParam);
+		}
+		remoteParamService.setByKey(sysParamList);
+		return null;
+	}
 }

+ 19 - 212
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MktMgmtRuleServiceImpl.java

@@ -65,10 +65,10 @@ public class MktMgmtRuleServiceImpl implements MktMgmtRuleService {
 
 		// 统一验证输入数据
 		if (ipList != null && !ipList.isEmpty()) {
-			validateIpList(ipList);
+			IPUtils.validateIpList(ipList);
 		}
 		if (domainList != null && !domainList.isEmpty()) {
-			validateDomainList(domainList);
+			DomainValidationUtil.validateDomainList(domainList);
 		}
 		if (keywords != null && !keywords.isEmpty()) {
 			validateKeywordList(keywords);
@@ -118,17 +118,25 @@ public class MktMgmtRuleServiceImpl implements MktMgmtRuleService {
 			}
 		}
 		else {
-			// 修改规则
-			// 1. 更新主规则信息
-			MktMgmtRule mktMgmtRule = mktMgmtRuleMapper.selectById(reqDto.getId());
-			if (mktMgmtRule == null) {
-				throw new BusinessException("规则不存在");
-			}
+			Long ruleId = reqDto.getId();
 			MktMgmtRule rule = new MktMgmtRule();
 			BeanUtils.copyProperties(reqDto, rule);
-			mktMgmtRuleMapper.updateById(rule);
-
-			Long ruleId = reqDto.getId();
+			// 是否是全局配置
+			if (ruleId == 0L){
+				MktMgmtRule globalRule = mktMgmtRuleMapper.selectById(reqDto.getId());
+				if (globalRule == null) {
+					int insert = mktMgmtRuleMapper.insert(rule);
+				}else {
+					mktMgmtRuleMapper.updateById(rule);
+				}
+			}else {
+				// 1. 更新主规则信息
+				MktMgmtRule mktMgmtRule = mktMgmtRuleMapper.selectById(reqDto.getId());
+				if (mktMgmtRule == null) {
+					throw new BusinessException("规则不存在");
+				}
+				mktMgmtRuleMapper.updateById(rule);
+			}
 
 			// 2. 处理IP列表
 			updateIpList(ipList, ruleId);
@@ -393,15 +401,6 @@ public class MktMgmtRuleServiceImpl implements MktMgmtRuleService {
 		}
 	}
 
-	/**
-	 * 校验域名格式是否合法
-	 * @param domain 域名
-	 */
-	private void validateDomain(String domain){
-		if (!DomainValidationUtil.isValidDomain2(domain)) {
-			throw new BusinessException("域名格式错误: " + domain);
-		}
-	}
 
 	/**
 	 * 验证规则名是否重复
@@ -427,95 +426,6 @@ public class MktMgmtRuleServiceImpl implements MktMgmtRuleService {
 		}
 	}
 
-	/**
-	 * 验证IP列表,检查是否存在冲突
-	 * @param ipList IP列表
-	 */
-	private void validateIpList(List<String> ipList) {
-		if (ipList == null || ipList.isEmpty()) {
-			return;
-		}
-
-		// 过滤空值和去重
-		List<String> validIps = ipList.stream()
-				.filter(StringUtils::hasText)
-				.map(String::trim)
-				.toList();
-
-		// 检查是否有重复的单IP
-		Set<String> singleIps = new HashSet<>();
-		Set<String> ipRanges = new HashSet<>();
-
-		for (String ip : validIps) {
-			if (ip.contains("/")) {
-				// IP段格式
-				ipRanges.add(ip);
-			} else {
-				// 单IP格式
-				if (!singleIps.add(ip)) {
-					throw new BusinessException("IP列表中存在重复的单IP: " + ip);
-				}
-			}
-		}
-
-		// 检查是否有重复的IP段
-		List<String> rangeList = validIps.stream()
-				.filter(ip -> ip.contains("/"))
-				.toList();
-		Set<String> uniqueRanges = new HashSet<>(rangeList);
-		if (rangeList.size() != uniqueRanges.size()) {
-			throw new BusinessException("IP列表中存在重复的IP段");
-		}
-
-		// 检查单IP是否在IP段中
-		for (String singleIp : singleIps) {
-			for (String ipRange : ipRanges) {
-				if (isIpInRange(singleIp, ipRange)) {
-					throw new BusinessException("单IP '" + singleIp + "' 在IP段 '" + ipRange + "' 中,存在冲突");
-				}
-			}
-		}
-
-		// 检查IP段之间是否重叠
-		List<String> uniqueRangeList = new ArrayList<>(ipRanges);
-		for (int i = 0; i < uniqueRangeList.size(); i++) {
-			for (int j = i + 1; j < uniqueRangeList.size(); j++) {
-				if (isIpRangesOverlap(uniqueRangeList.get(i), uniqueRangeList.get(j))) {
-					throw new BusinessException("IP段 '" + uniqueRangeList.get(i) + "' 和 '" + uniqueRangeList.get(j) + "' 存在重叠");
-				}
-			}
-		}
-	}
-
-	/**
-	 * 验证域名列表,检查格式和重复
-	 * @param domainList 域名列表
-	 */
-	private void validateDomainList(List<String> domainList) {
-		if (domainList == null || domainList.isEmpty()) {
-			return;
-		}
-
-		// 过滤空值,但不去重(保留重复项用于检测)
-		List<String> validDomains = domainList.stream()
-				.filter(StringUtils::hasText)
-				.map(String::trim)
-				.toList();
-
-		// 检查是否有重复的域名
-		Set<String> uniqueDomains = new HashSet<>();
-		for (String domain : validDomains) {
-			if (!uniqueDomains.add(domain)) {
-				throw new BusinessException("域名列表中存在重复的域名: " + domain);
-			}
-		}
-
-		// 校验每个域名的格式
-		for (String domain : validDomains) {
-			validateDomain(domain);
-		}
-	}
-
 	/**
 	 * 验证关键字列表,检查重复
 	 * @param keywordList 关键字列表
@@ -540,109 +450,6 @@ public class MktMgmtRuleServiceImpl implements MktMgmtRuleService {
 		}
 	}
 
-	/**
-	 * 检查单IP是否在IP段范围内
-	 * @param singleIp 单IP
-	 * @param ipRange IP段 (格式: 192.168.10.200/220 表示 192.168.10.200~192.168.10.220)
-	 * @return 是否在范围内
-	 */
-	private boolean isIpInRange(String singleIp, String ipRange) {
-		if (!IPUtils.isValidIp(singleIp)) {
-			return false;
-		}
-
-		String[] parts = ipRange.split("/");
-		if (parts.length != 2) {
-			return false;
-		}
-
-		String startIp = parts[0];
-		String endPart = parts[1];
-
-		if (!IPUtils.isValidIp(startIp)) {
-			return false;
-		}
-
-		// 构造完整的结束IP
-		String[] startParts = startIp.split("\\.");
-		String endIp = String.format("%s.%s.%s.%s",
-				startParts[0], startParts[1], startParts[2], endPart);
-
-		if (!IPUtils.isValidIp(endIp)) {
-			return false;
-		}
-
-		long singleIpLong = ipToLong(singleIp);
-		long startIpLong = ipToLong(startIp);
-		long endIpLong = ipToLong(endIp);
-
-		// 确保开始IP小于等于结束IP
-		if (startIpLong > endIpLong) {
-			long temp = startIpLong;
-			startIpLong = endIpLong;
-			endIpLong = temp;
-		}
-
-		return singleIpLong >= startIpLong && singleIpLong <= endIpLong;
-	}
-
-	/**
-	 * 检查两个IP段是否重叠
-	 * @param range1 第一个IP段
-	 * @param range2 第二个IP段
-	 * @return 是否重叠
-	 */
-	private boolean isIpRangesOverlap(String range1, String range2) {
-		String[] parts1 = range1.split("/");
-		String[] parts2 = range2.split("/");
-
-		if (parts1.length != 2 || parts2.length != 2) {
-			return false;
-		}
-
-		String network1 = parts1[0];
-		String network2 = parts2[0];
-		int prefix1 = Integer.parseInt(parts1[1]);
-		int prefix2 = Integer.parseInt(parts2[1]);
-
-		if (!IPUtils.isValidIp(network1) || !IPUtils.isValidIp(network2)) {
-			return false;
-		}
-
-		long network1Long = ipToLong(network1);
-		long network2Long = ipToLong(network2);
-
-		// 计算网络掩码
-		long mask1 = (0xFFFFFFFFL << (32 - prefix1)) & 0xFFFFFFFFL;
-		long mask2 = (0xFFFFFFFFL << (32 - prefix2)) & 0xFFFFFFFFL;
-
-		// 计算网络地址
-		long net1 = network1Long & mask1;
-		long net2 = network2Long & mask2;
-
-		// 计算广播地址
-		long broadcast1 = net1 | (~mask1 & 0xFFFFFFFFL);
-		long broadcast2 = net2 | (~mask2 & 0xFFFFFFFFL);
-
-		// 检查是否重叠:一个网络的开始地址小于等于另一个网络的结束地址,且一个网络的结束地址大于等于另一个网络的开始地址
-		return net1 <= broadcast2 && broadcast1 >= net2;
-	}
-
-	/**
-	 * 将IP地址转换为长整型
-	 * @param ip IP地址
-	 * @return 长整型值
-	 */
-	private long ipToLong(String ip) {
-		String[] parts = ip.split("\\.");
-		long result = 0;
-		for (int i = 0; i < 4; i++) {
-			result |= Long.parseLong(parts[i]) << (24 - i * 8);
-		}
-		return result;
-	}
-
-
 	/**
 	 * 构建IP规则对象
 	 * @param ip IP字符串

+ 30 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/TcpDataServiceImpl.java

@@ -179,6 +179,11 @@ import java.util.Objects;
 			// 查询总条数
 			Long dataTotal = getMsgDataTotal(device.getClientID());
 			info.setTotal(dataTotal);
+
+			// 查询最新推送时间
+			String latestPushTime = getLatestPushTime(device.getClientID());
+			info.setLatestPushTime(latestPushTime);
+
 			result.add(info);
 		}
 
@@ -285,6 +290,11 @@ import java.util.Objects;
 	}
 
 
+	/**
+	 * 获取设备总数据条数
+	 * @param clientId 客户端ID
+	 * @return 设备总数据条数
+	 */
 	private Long getMsgDataTotal(String clientId){
 		// 创建查询条件
 		Criteria criteria = new Criteria();
@@ -300,6 +310,26 @@ import java.util.Objects;
 
 	}
 
+	/**
+	 * 获取指定客户端的最新推送时间
+	 * @param clientId 客户端ID
+	 * @return 最新推送时间,如果没有数据则返回null
+	 */
+	private String getLatestPushTime(String clientId) {
+		// 创建查询条件
+		Criteria criteria = Criteria.where("clientID").is(clientId);
+		Query query = new Query(criteria);
+
+		// 按reportTime降序排序,只取第一条记录
+		query.with(Sort.by(Sort.Direction.DESC, "reportTime")).limit(1);
+
+		// 执行查询
+		Message latestMessage = mongoTemplate.findOne(query, Message.class);
+
+		// 返回最新推送时间,如果没有数据则返回null
+		return latestMessage != null ? latestMessage.getReportTime() : null;
+	}
+
 	/**
 	 * 统计用户数
 	 * @return 用户统计信息