Pārlūkot izejas kodu

Merge branch 'dev/lwh' into dev/lh

lh 1 nedēļu atpakaļ
vecāks
revīzija
d958424d23

+ 30 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/mongo/UserRegionStatisticsDTO.java

@@ -0,0 +1,30 @@
+package com.pig4cloud.pig.marketing.api.dto.mongo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Max;
+import java.io.Serializable;
+
+/**
+ * @author: wcl
+ * @date: 2025-09-07
+ * @description: 用户地区统计查询请求DTO
+ */
+@Data
+@Schema(description = "用户地区统计查询请求")
+public class UserRegionStatisticsDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @NotNull(message = "天数不能为空")
+    @Min(value = 1, message = "天数不能小于1")
+    @Max(value = 365, message = "天数不能大于365")
+    @Schema(description = "查询天数", example = "7", required = true)
+    private Integer days;
+
+    @Schema(description = "查询类型", example = "visitor", required = true)
+    private String queryType; // visitor, active, total
+}

+ 24 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/mongo/RegionStatisticsItem.java

@@ -0,0 +1,24 @@
+package com.pig4cloud.pig.marketing.api.vo.mongo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author: wcl
+ * @date: 2025-01-27
+ * @description: 地区统计项VO
+ */
+@Data
+@Schema(description = "地区统计项")
+public class RegionStatisticsItem implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "国家", example = "China")
+    private String country;
+
+    @Schema(description = "用户数量", example = "1500")
+    private Long userCount;
+}

+ 34 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/mongo/UserRegionStatisticsVO.java

@@ -0,0 +1,34 @@
+package com.pig4cloud.pig.marketing.api.vo.mongo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: wcl
+ * @date: 2025-09-07
+ * @description: 用户地区统计结果VO
+ */
+@Data
+@Schema(description = "用户地区统计结果")
+public class UserRegionStatisticsVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "统计时间", example = "2025-01-27 10:00:00")
+    private String statisticsTime;
+
+    @Schema(description = "查询天数", example = "7")
+    private Integer days;
+
+    @Schema(description = "查询类型", example = "visitor")
+    private String queryType;
+
+    @Schema(description = "国家统计列表")
+    private List<RegionStatisticsItem> countryStatistics;
+
+    @Schema(description = "总用户数", example = "5000")
+    private Long totalUsers;
+}

+ 33 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/rule/StatKeywordVO.java

@@ -0,0 +1,33 @@
+package com.pig4cloud.pig.marketing.api.vo.rule;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-09-07
+ * @description: 统计关键词频率出参
+ */
+@Data
+@Schema(description = "统计关键词频率出参")
+public class StatKeywordVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 关键词
+	 */
+	@Schema(description = "关键词")
+	private String keyword;
+
+	/**
+	 * 统计次数
+	 */
+	@Schema(description = "统计次数")
+	private Integer count;
+}

+ 14 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/controller/MktMgmtPushRecordController.java

@@ -5,17 +5,22 @@ import com.pig4cloud.pig.common.core.util.R;
 import com.pig4cloud.pig.marketing.api.dto.MktMgmtPushRecordQueryDTO;
 import com.pig4cloud.pig.marketing.api.dto.MktMgmtPushRecordSaveDTO;
 import com.pig4cloud.pig.marketing.api.entity.MktMgmtPushRecord;
+import com.pig4cloud.pig.marketing.api.vo.rule.StatKeywordVO;
 import com.pig4cloud.pig.marketing.service.MktMgmtPushRecordService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springdoc.core.annotations.ParameterObject;
 import org.springframework.http.HttpHeaders;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
 /**
  * @author: wcl
  * @date: 2025-08-31
@@ -74,4 +79,13 @@ public class MktMgmtPushRecordController {
 			return R.failed("新增失败:" + e.getMessage());
 		}
 	}
+
+
+
+	@GetMapping("/stat/keyword")
+	@Operation(summary = "统计关键词频率")
+	public R<List<StatKeywordVO>> statKeyword(@NotNull(message = "统计天数不能为空") @ParameterObject Integer  days) {
+		List<StatKeywordVO> result = mktMgmtPushRecordService.statKeyword(days);
+		return R.ok(result);
+	}
 }

+ 15 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/controller/TcpDataController.java

@@ -12,6 +12,8 @@ import com.pig4cloud.pig.marketing.api.entity.mongo.Message;
 import com.pig4cloud.pig.marketing.api.vo.mongo.OnlineUserVO;
 import com.pig4cloud.pig.marketing.api.vo.mongo.PageDeviceInfoVO;
 import com.pig4cloud.pig.marketing.api.vo.mongo.UserStatisticsVO;
+import com.pig4cloud.pig.marketing.api.dto.mongo.UserRegionStatisticsDTO;
+import com.pig4cloud.pig.marketing.api.vo.mongo.UserRegionStatisticsVO;
 import com.pig4cloud.pig.marketing.service.TcpDataService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -108,4 +110,17 @@ public class TcpDataController {
 			return R.failed("查询失败:" + e.getMessage());
 		}
 	}
+
+	@PostMapping("/statistics/users/region")
+	@Operation(summary = "用户地区统计", description = "根据条件统计各国家用户数量")
+	public R<UserRegionStatisticsVO> getUserRegionStatistics(@Valid @RequestBody UserRegionStatisticsDTO reqDto) {
+		log.info("开始统计用户地区分布,查询条件:{}", reqDto);
+		try {
+			UserRegionStatisticsVO statistics = tcpDataService.getUserRegionStatistics(reqDto);
+			return R.ok(statistics);
+		} catch (Exception e) {
+			log.error("统计用户地区分布异常", e);
+			return R.failed("统计失败:" + e.getMessage());
+		}
+	}
 }

+ 11 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/MktMgmtPushRecordService.java

@@ -4,6 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.pig4cloud.pig.marketing.api.dto.MktMgmtPushRecordQueryDTO;
 import com.pig4cloud.pig.marketing.api.dto.MktMgmtPushRecordSaveDTO;
 import com.pig4cloud.pig.marketing.api.entity.MktMgmtPushRecord;
+import com.pig4cloud.pig.marketing.api.vo.rule.StatKeywordVO;
+import jakarta.validation.constraints.NotNull;
+
+import java.util.List;
 
 /**
  * @author: wcl
@@ -25,4 +29,11 @@ public interface MktMgmtPushRecordService {
 	 * @return 成功时返回记录ID,失败时返回详细错误信息
 	 */
 	String saveRecord(MktMgmtPushRecordSaveDTO saveDTO);
+
+	/**
+	 * 统计关键词
+	 * @param days 统计天数
+	 * @return 统计结果
+	 */
+	List<StatKeywordVO> statKeyword(@NotNull(message = "统计天数不能为空") Integer days);
 }

+ 9 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/TcpDataService.java

@@ -6,10 +6,12 @@ import com.pig4cloud.pig.marketing.api.dto.mongo.PageDeviceInfoDTO;
 import com.pig4cloud.pig.marketing.api.dto.mongo.PageMessageDTO;
 import com.pig4cloud.pig.marketing.api.dto.mongo.SaveDeviceInfoDTO;
 import com.pig4cloud.pig.marketing.api.dto.mongo.SaveTcpMessageDTO;
+import com.pig4cloud.pig.marketing.api.dto.mongo.UserRegionStatisticsDTO;
 import com.pig4cloud.pig.marketing.api.entity.mongo.Device;
 import com.pig4cloud.pig.marketing.api.entity.mongo.Message;
 import com.pig4cloud.pig.marketing.api.vo.mongo.PageDeviceInfoVO;
 import com.pig4cloud.pig.marketing.api.vo.mongo.UserStatisticsVO;
+import com.pig4cloud.pig.marketing.api.vo.mongo.UserRegionStatisticsVO;
 import com.pig4cloud.pig.marketing.api.vo.mongo.OnlineUserVO;
 
 /**
@@ -75,4 +77,11 @@ public interface TcpDataService {
 	 * @return 在线用户列表
 	 */
 	java.util.List<OnlineUserVO> getOnlineUsers();
+
+	/**
+	 * 用户地区统计
+	 * @param reqDto 查询条件
+	 * @return 用户地区统计信息
+	 */
+	UserRegionStatisticsVO getUserRegionStatistics(UserRegionStatisticsDTO reqDto);
 }

+ 68 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MktMgmtPushRecordServiceImpl.java

@@ -1,10 +1,15 @@
 package com.pig4cloud.pig.marketing.service.impl;
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.pig4cloud.pig.marketing.api.dto.MktMgmtPushRecordQueryDTO;
 import com.pig4cloud.pig.marketing.api.dto.MktMgmtPushRecordSaveDTO;
 import com.pig4cloud.pig.marketing.api.entity.*;
+import com.pig4cloud.pig.marketing.api.vo.rule.StatKeywordVO;
 import com.pig4cloud.pig.marketing.mapper.*;
 import com.pig4cloud.pig.marketing.service.MktMgmtPushRecordService;
 import com.pig4cloud.pig.marketing.util.PushFrequencyUtil;
@@ -15,6 +20,7 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
+import java.time.LocalDateTime;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -152,6 +158,68 @@ public class MktMgmtPushRecordServiceImpl implements MktMgmtPushRecordService {
 		}
 	}
 
+	/**
+	 * 统计关键词
+	 * @param days 统计天数
+	 * @return 统计结果
+	 */
+	@Override
+	public List<StatKeywordVO> statKeyword(Integer days) {
+		log.info("开始统计关键词频率,统计天数:{}", days);
+
+		// 计算开始时间(days天前)
+		LocalDateTime startTime = LocalDateTime.now().minusDays(days);
+
+		// 查询指定天数内的推送记录
+		List<MktMgmtPushRecord> mktMgmtPushRecords = mktMgmtPushRecordMapper.selectList(
+				Wrappers.<MktMgmtPushRecord>lambdaQuery()
+						.ge(MktMgmtPushRecord::getCreateTime, startTime)
+						.select(MktMgmtPushRecord::getTriggerCondition)
+		);
+
+		log.info("查询到{}条推送记录", mktMgmtPushRecords.size());
+
+		// 统计关键字出现次数
+		Map<String, Integer> keywordCountMap = new HashMap<>();
+
+		for (MktMgmtPushRecord record : mktMgmtPushRecords) {
+			String triggerCondition = record.getTriggerCondition();
+			if (StringUtils.hasText(triggerCondition)) {
+				try {
+					// 解析JSON格式的triggerCondition
+					JSONObject jsonObject = JSON.parseObject(triggerCondition);
+					JSONArray keywordsArray = jsonObject.getJSONArray("keywords");
+
+					if (keywordsArray != null) {
+						for (int i = 0; i < keywordsArray.size(); i++) {
+							String keyword = keywordsArray.getString(i);
+							if (StringUtils.hasText(keyword)) {
+								keywordCountMap.put(keyword, keywordCountMap.getOrDefault(keyword, 0) + 1);
+							}
+						}
+					}
+				} catch (Exception e) {
+					log.warn("解析triggerCondition失败,记录ID:{},内容:{},错误:{}",
+							record.getId(), triggerCondition, e.getMessage());
+				}
+			}
+		}
+
+		// 转换为StatKeywordVO列表并按出现次数降序排列
+		List<StatKeywordVO> result = keywordCountMap.entrySet().stream()
+				.sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue()))
+				.map(entry -> {
+					StatKeywordVO vo = new StatKeywordVO();
+					vo.setKeyword(entry.getKey());
+					vo.setCount(entry.getValue());
+					return vo;
+				})
+				.collect(Collectors.toList());
+
+		log.info("统计完成,共统计到{}个不同的关键字", result.size());
+		return result;
+	}
+
 	@NotNull
 	private static MktMgmtPushRecord getMktMgmtPushRecord(MktMgmtPushRecordSaveDTO saveDTO, String triggerCondition, MktMgmtRule selectedRule) {
 		// 构建推送详情的JSON格式

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

@@ -8,12 +8,15 @@ import com.pig4cloud.pig.marketing.api.dto.mongo.PageDeviceInfoDTO;
 import com.pig4cloud.pig.marketing.api.dto.mongo.PageMessageDTO;
 import com.pig4cloud.pig.marketing.api.dto.mongo.SaveDeviceInfoDTO;
 import com.pig4cloud.pig.marketing.api.dto.mongo.SaveTcpMessageDTO;
+import com.pig4cloud.pig.marketing.api.dto.mongo.UserRegionStatisticsDTO;
 import com.pig4cloud.pig.marketing.api.entity.mongo.Device;
 import com.pig4cloud.pig.marketing.api.entity.mongo.Message;
 import com.pig4cloud.pig.marketing.api.service.MktMgmtHandPushService;
 import com.pig4cloud.pig.marketing.api.vo.mongo.OnlineUserVO;
 import com.pig4cloud.pig.marketing.api.vo.mongo.PageDeviceInfoVO;
 import com.pig4cloud.pig.marketing.api.vo.mongo.UserStatisticsVO;
+import com.pig4cloud.pig.marketing.api.vo.mongo.UserRegionStatisticsVO;
+import com.pig4cloud.pig.marketing.api.vo.mongo.RegionStatisticsItem;
 import com.pig4cloud.pig.marketing.config.UserStatisticsConfig;
 import com.pig4cloud.pig.marketing.repository.MessageRepository;
 import com.pig4cloud.pig.marketing.service.MktMgmtPushRecordService;
@@ -34,6 +37,7 @@ import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.stereotype.Service;
+import com.pig4cloud.pig.common.core.util.ip.IPUtils;
 
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -634,4 +638,151 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 		
 		return onlineUserVO;
 	}
+
+	/**
+	 * 用户地区统计
+	 * @param reqDto 查询条件
+	 * @return 用户地区统计信息
+	 */
+	@Override
+	public UserRegionStatisticsVO getUserRegionStatistics(UserRegionStatisticsDTO reqDto) {
+		log.info("开始统计用户地区分布,查询条件:{}", reqDto);
+		
+		try {
+			// 1. 构建时间范围
+			LocalDateTime endTime = LocalDateTime.now();
+			LocalDateTime startTime = endTime.minusDays(reqDto.getDays());
+			
+			// 2. 根据查询类型构建不同的查询条件
+			Query query = buildQueryByType(reqDto.getQueryType(), startTime, endTime);
+			
+			// 3. 查询设备数据
+			List<Device> devices = mongoTemplate.find(query, Device.class);
+			
+			// 4. 按IP解析国家并统计
+			Map<String, Long> countryCountMap = new HashMap<>();
+			
+			for (Device device : devices) {
+				if (StringUtils.isNotBlank(device.getClientIp())) {
+					String country = getCountryFromIp(device.getClientIp());
+					if (StringUtils.isNotBlank(country) && !"未知".equals(country) && !"内网".equals(country)) {
+						countryCountMap.merge(country, 1L, Long::sum);
+					}
+				}
+			}
+			
+			// 5. 构建返回结果
+			UserRegionStatisticsVO result = new UserRegionStatisticsVO();
+			result.setStatisticsTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
+			result.setDays(reqDto.getDays());
+			result.setQueryType(reqDto.getQueryType());
+			
+			List<RegionStatisticsItem> countryStatistics = new ArrayList<>();
+			long totalUsers = countryCountMap.values().stream().mapToLong(Long::longValue).sum();
+			
+			// 按用户数量降序排列
+			countryCountMap.entrySet().stream()
+				.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
+				.forEach(entry -> {
+					RegionStatisticsItem item = new RegionStatisticsItem();
+					item.setCountry(entry.getKey());
+					item.setUserCount(entry.getValue());
+					countryStatistics.add(item);
+				});
+			
+			result.setCountryStatistics(countryStatistics);
+			result.setTotalUsers(totalUsers);
+			
+			log.info("用户地区统计完成,总用户数:{},国家数量:{}", totalUsers, countryStatistics.size());
+			return result;
+			
+		} catch (Exception e) {
+			log.error("统计用户地区分布异常", e);
+			// 返回默认值
+			UserRegionStatisticsVO result = new UserRegionStatisticsVO();
+			result.setStatisticsTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
+			result.setDays(reqDto.getDays());
+			result.setQueryType(reqDto.getQueryType());
+			result.setCountryStatistics(new ArrayList<>());
+			result.setTotalUsers(0L);
+			return result;
+		}
+	}
+	
+	/**
+	 * 根据查询类型构建查询条件
+	 * @param queryType 查询类型
+	 * @param startTime 开始时间
+	 * @param endTime 结束时间
+	 * @return 查询条件
+	 */
+	private Query buildQueryByType(String queryType, LocalDateTime startTime, LocalDateTime endTime) {
+		Query query = new Query();
+		
+		switch (queryType.toLowerCase()) {
+			case "visitor":
+				// 访客数:统计指定时间范围内有设备记录的用户
+				query.addCriteria(Criteria.where("createTime").gte(startTime.format(DATE_TIME_FORMATTER))
+										  .lte(endTime.format(DATE_TIME_FORMATTER)));
+				break;
+			case "active":
+				// 活跃用户数:统计指定时间范围内有消息记录的用户
+				// 这里需要查询Message表,然后通过clientID关联Device表
+				Query messageQuery = new Query();
+				messageQuery.addCriteria(Criteria.where("reportTime").gte(startTime.format(DATE_TIME_FORMATTER))
+												  .lte(endTime.format(DATE_TIME_FORMATTER)));
+				
+				List<String> activeClientIDs = mongoTemplate.findDistinct(
+					messageQuery, 
+					"clientID", 
+					Message.class, 
+					String.class
+				);
+				
+				// 过滤掉null和空字符串
+				List<String> validClientIDs = activeClientIDs.stream()
+					.filter(clientID -> clientID != null && !clientID.trim().isEmpty())
+					.collect(Collectors.toList());
+				
+				if (!validClientIDs.isEmpty()) {
+					query.addCriteria(Criteria.where("clientID").in(validClientIDs));
+				} else {
+					// 如果没有活跃用户,返回空结果
+					query.addCriteria(Criteria.where("clientID").in(Collections.emptyList()));
+				}
+				break;
+			case "total":
+				// 总用户数:统计所有用户(不限制时间)
+				// 不需要添加时间条件
+				break;
+			default:
+				log.warn("未知的查询类型:{},使用默认的visitor类型", queryType);
+				query.addCriteria(Criteria.where("createTime").gte(startTime.format(DATE_TIME_FORMATTER))
+										  .lte(endTime.format(DATE_TIME_FORMATTER)));
+				break;
+		}
+		
+		return query;
+	}
+	
+	/**
+	 * 从IP地址获取国家信息
+	 * @param ip IP地址
+	 * @return 国家名称
+	 */
+	private String getCountryFromIp(String ip) {
+		try {
+			String region = IPUtils.getIpRegion(ip);
+			if (StringUtils.isNotBlank(region) && !"未知".equals(region) && !"内网".equals(region)) {
+				// 解析地区信息,格式通常是"国家-省份"
+				String[] parts = region.split("-");
+				if (parts.length >= 1) {
+					return parts[0].trim();
+				}
+			}
+		} catch (Exception e) {
+			log.warn("解析IP[{}]归属地失败", ip, e);
+		}
+		return "未知";
+	}
 }