Browse Source

Merge branch 'dev/wcl' of https://s-20coaj910c.zht2.com/lwh/seo into dev/lwh

lwh 1 week ago
parent
commit
6594a99892

+ 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;
+}

+ 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());
+		}
+	}
 }

+ 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);
 }

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

@@ -7,12 +7,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;
@@ -30,6 +33,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;
@@ -514,4 +518,151 @@ import java.util.stream.Collectors;
 		
 		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 "未知";
+	}
 }