|
@@ -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 "未知";
|
|
|
+ }
|
|
|
}
|