Bladeren bron

Merge remote-tracking branch 'refs/remotes/origin/dev/lwh' into dev/wcl

wangcl 3 weken geleden
bovenliggende
commit
34e506044d
24 gewijzigde bestanden met toevoegingen van 1829 en 569 verwijderingen
  1. 12 7
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/GetVersionDistributionDTO.java
  2. 8 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/PageAllVersionDetailDTO.java
  3. 62 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/PageUserAnalyseDTO.java
  4. 12 9
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/PageVersionDistributionDetailDTO.java
  5. 62 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/UserAnalyseQueryBaseDTO.java
  6. 18 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageActiveDetailVO.java
  7. 40 21
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageAllVersionDetailVO.java
  8. 8 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageLaunchDetailVO.java
  9. 8 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageNewUserTrendDetailVO.java
  10. 6 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageRetentionDetailVO.java
  11. 21 8
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageSingleVersionDetailVO.java
  12. 12 5
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageVersionDistributionDetailVO.java
  13. 7 1
      pig-statistics/pig-statistics-biz/pom.xml
  14. 341 23
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/controller/UserAnalyseController.java
  15. 1 1
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/UserAnalyseService.java
  16. 581 493
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/impl/UserAnalyseServiceImpl.java
  17. 57 0
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/ClientHandler.java
  18. 22 0
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/ClientHandlerFactory.java
  19. 91 0
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/DataProcessor.java
  20. 23 0
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/Packet.java
  21. 127 0
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/ProtocolHandler.java
  22. 221 0
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/TcpClient.java
  23. 82 0
      pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/TcpServer.java
  24. 7 1
      pig-statistics/pig-statistics-biz/src/main/resources/application.yml

+ 12 - 7
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/GetVersionDistributionDTO.java

@@ -2,6 +2,7 @@ package com.pig4cloud.pig.statistics.api.dto.user;
 
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
 import lombok.Data;
 
 import java.io.Serial;
@@ -20,26 +21,30 @@ public class GetVersionDistributionDTO implements Serializable {
 	private static final long serialVersionUID = 1L;
 
 	/**
-	 * 查询条件名称
+	 * 应用ID
 	 */
-	@Schema(description = "查询条件名称")
-	private String conditionName;
+	@NotBlank(message = "应用ID不能为空")
+	@Schema(description = "应用ID", example = "Fqs2CL9CUn7U1AqilSFXgb")
+	private String appId;
 
 	/**
-	 * 查询条件值
+	 * 版本
 	 */
-	@Schema(description = "查询条件值")
-	private String conditionValue;
+	@NotBlank(message = "版本不能为空")
+	@Schema(description = "版本", example = "1.0.0")
+	private String version;
 
 	/**
 	 * 时间单位
 	 */
-	@Schema(description = "时间单位")
+	@NotBlank(message = "时间单位不能为空")
+	@Schema(description = "时间单位,昨天-day,过去7天-week,过去30天-month", example = "day")
 	private String timeUnit;
 
 	/**
 	 * 类型
 	 */
+	@NotBlank(message = "类型不能为空")
 	@Schema(description = "类型,upgradeChannel-升级用户渠道",example = "upgradeChannel")
 	private String type;
 }

+ 8 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/PageAllVersionDetailDTO.java

@@ -2,6 +2,7 @@ package com.pig4cloud.pig.statistics.api.dto.user;
 
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
 import lombok.Data;
 
 import java.io.Serial;
@@ -19,6 +20,13 @@ public class PageAllVersionDetailDTO implements Serializable {
 	@Serial
 	private static final long serialVersionUID = 1L;
 
+	/**
+	 * 应用ID
+	 */
+	@NotBlank(message = "应用ID不能为空")
+	@Schema(description = "应用ID", example = "Fqs2CL9CUn7U1AqilSFXgb")
+	private String appId;
+
 	/**
 	 * 每页显示条数,默认 10
 	 */

+ 62 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/PageUserAnalyseDTO.java

@@ -2,6 +2,7 @@ package com.pig4cloud.pig.statistics.api.dto.user;
 
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
@@ -9,6 +10,7 @@ import lombok.Data;
 import java.io.Serial;
 import java.io.Serializable;
 import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
 import java.util.List;
 
 /**
@@ -74,4 +76,64 @@ public class PageUserAnalyseDTO implements Serializable {
 	 */
 	@Schema(description = "当前页",example = "1")
 	private long current = 1;
+
+	/**
+	 * 校验规则1:fromDate不能大于toDate,只能小于等于
+	 */
+	@Schema(hidden = true)
+	@AssertTrue(message = "开始时间不能晚于结束时间")
+	public boolean isFromDateLessThanOrEqualToToDate() {
+		if (fromDate == null || toDate == null) {
+			return true;
+		}
+		return !fromDate.isAfter(toDate);
+	}
+
+	/**
+	 * 校验规则2:如果timeUnit为hour,则fromDate~toDate的天数不能大于一周
+	 */
+	@Schema(hidden = true)
+	@AssertTrue(message = "小时维度查询时间跨度不能超过7天")
+	public boolean isHourTimeUnitWithinOneWeek() {
+		if (fromDate == null || toDate == null || timeUnit == null) {
+			return true;
+		}
+		if ("hour".equals(timeUnit)) {
+			long days = ChronoUnit.DAYS.between(fromDate, toDate);
+			return days < 7;
+		}
+		return true;
+	}
+
+	/**
+	 * 校验规则3:如果timeUnit为week,则跨度不能小于一周
+	 */
+	@Schema(hidden = true)
+	@AssertTrue(message = "周维度查询时间跨度不能小于7天")
+	public boolean isWeekTimeUnitAtLeastOneWeek() {
+		if (fromDate == null || toDate == null || timeUnit == null) {
+			return true;
+		}
+		if ("week".equals(timeUnit)) {
+			long days = ChronoUnit.DAYS.between(fromDate, toDate);
+			return days >= 7;
+		}
+		return true;
+	}
+
+	/**
+	 * 校验规则4:月维度查询时间跨度不能小于30天
+	 */
+	@Schema(hidden = true)
+	@AssertTrue(message = "月维度查询时间跨度不能小于30天")
+	public boolean isMonthTimeUnitAtLeastMonth() {
+		if (fromDate == null || toDate == null || timeUnit == null) {
+			return true;
+		}
+		if ("month".equals(timeUnit)) {
+			long days = ChronoUnit.DAYS.between(fromDate, toDate);
+			return days >= 30;
+		}
+		return true;
+	}
 }

+ 12 - 9
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/PageVersionDistributionDetailDTO.java

@@ -2,6 +2,7 @@ package com.pig4cloud.pig.statistics.api.dto.user;
 
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
 import lombok.Data;
 
 import java.io.Serial;
@@ -20,21 +21,23 @@ public class PageVersionDistributionDetailDTO implements Serializable {
 	private static final long serialVersionUID = 1L;
 
 	/**
-	 * 查询条件名称
+	 * 应用ID
 	 */
-	@Schema(description = "查询条件名称")
-	private String conditionName;
+	@NotBlank(message = "应用ID不能为空")
+	@Schema(description = "应用ID", example = "Fqs2CL9CUn7U1AqilSFXgb")
+	private String appId;
 
 	/**
-	 * 查询条件值
+	 * 版本
 	 */
-	@Schema(description = "查询条件值")
-	private String conditionValue;
+	@NotBlank(message = "版本不能为空")
+	@Schema(description = "版本", example = "1.0.0")
+	private String version;
 
 	/**
 	 * 时间单位
 	 */
-	@Schema(description = "时间单位")
+	@Schema(description = "时间单位,昨天-day,过去7天-week,过去30天-month", example = "day")
 	private String timeUnit;
 
 	/**
@@ -46,12 +49,12 @@ public class PageVersionDistributionDetailDTO implements Serializable {
 	/**
 	 * 每页显示条数,默认 10
 	 */
-	@Schema(description = "每页显示条数")
+	@Schema(description = "每页显示条数", example = "10")
 	private long size = 10;
 
 	/**
 	 * 当前页
 	 */
-	@Schema(description = "当前页")
+	@Schema(description = "当前页", example = "1")
 	private long current = 1;
 }

+ 62 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/UserAnalyseQueryBaseDTO.java

@@ -2,6 +2,7 @@ package com.pig4cloud.pig.statistics.api.dto.user;
 
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
@@ -9,6 +10,7 @@ import lombok.Data;
 import java.io.Serial;
 import java.io.Serializable;
 import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
 import java.util.List;
 
 /**
@@ -61,4 +63,64 @@ public class UserAnalyseQueryBaseDTO implements Serializable {
 	 */
 	@Schema(description = "版本", example = "[\"1.0.0\"]")
 	private List<String> version;
+
+	/**
+	 * 校验规则1:fromDate不能大于toDate,只能小于等于
+	 */
+	@Schema(hidden = true)
+	@AssertTrue(message = "开始时间不能晚于结束时间")
+	public boolean isFromDateLessThanOrEqualToToDate() {
+		if (fromDate == null || toDate == null) {
+			return true;
+		}
+		return !fromDate.isAfter(toDate);
+	}
+
+	/**
+	 * 校验规则2:如果timeUnit为hour,则fromDate~toDate的天数不能大于一周
+	 */
+	@Schema(hidden = true)
+	@AssertTrue(message = "小时维度查询时间跨度不能超过7天")
+	public boolean isHourTimeUnitWithinOneWeek() {
+		if (fromDate == null || toDate == null || timeUnit == null) {
+			return true;
+		}
+		if ("hour".equals(timeUnit)) {
+			long days = ChronoUnit.DAYS.between(fromDate, toDate);
+			return days < 7;
+		}
+		return true;
+	}
+
+	/**
+	 * 校验规则3:如果timeUnit为week,则跨度不能小于一周
+	 */
+	@Schema(hidden = true)
+	@AssertTrue(message = "周维度查询时间跨度不能小于7天")
+	public boolean isWeekTimeUnitAtLeastOneWeek() {
+		if (fromDate == null || toDate == null || timeUnit == null) {
+			return true;
+		}
+		if ("week".equals(timeUnit)) {
+			long days = ChronoUnit.DAYS.between(fromDate, toDate);
+			return days >= 7;
+		}
+		return true;
+	}
+
+	/**
+	 * 校验规则4:月维度查询时间跨度不能小于30天
+	 */
+	@Schema(hidden = true)
+	@AssertTrue(message = "月维度查询时间跨度不能小于30天")
+	public boolean isMonthTimeUnitAtLeastMonth() {
+		if (fromDate == null || toDate == null || timeUnit == null) {
+			return true;
+		}
+		if ("month".equals(timeUnit)) {
+			long days = ChronoUnit.DAYS.between(fromDate, toDate);
+			return days >= 30;
+		}
+		return true;
+	}
 }

+ 18 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageActiveDetailVO.java

@@ -1,6 +1,10 @@
 package com.pig4cloud.pig.statistics.api.vo.user;
 
 
+import cn.idev.excel.annotation.ExcelIgnore;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.write.style.ColumnWidth;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
@@ -26,18 +30,24 @@ public class PageActiveDetailVO implements Serializable {
 	/**
 	 * 日期
 	 */
+	@ColumnWidth(20)
+	@ExcelProperty(value = "日期")
 	@Schema(description = "日期")
 	private String date;
 
 	/**
 	 * 活跃用户数
 	 */
+	@ColumnWidth(10)
+	 @ExcelProperty(value = "活跃用户数")
 	@Schema(description = "活跃用户数")
 	private Integer activeUser;
 
 	/**
 	 * 新用户占比
 	 */
+	@ColumnWidth(10)
+	@ExcelProperty(value = "新用户占比")
 	@Schema(description = "新用户占比")
 	private BigDecimal newUserRate;
 
@@ -45,18 +55,24 @@ public class PageActiveDetailVO implements Serializable {
 	/***********************************单位为天时
 	 * DAU/过去7日活跃用户
 	 */
+	@ColumnWidth(15)
+	@ExcelProperty(value = "DAU/过去7日活跃用户")
 	@Schema(description = "DAU/过去7日活跃用户")
 	private BigDecimal wauRate;
 
 	/***********************************单位为天时
 	 * DAU/过去30日活跃用户
 	 */
+	@ColumnWidth(15)
+	@ExcelProperty(value = "DAU/过去30日活跃用户")
 	@Schema(description = "DAU/过去30日活跃用户")
 	private BigDecimal mauRate;
 
 	/***********************************单位为周时
 	 * 用户周活跃率
 	 */
+	@ColumnWidth(10)
+	@ExcelProperty(value = "用户周活跃率")
 	@Schema(description = "用户周活跃率")
 	private BigDecimal weekActiveUserRate;
 
@@ -64,6 +80,8 @@ public class PageActiveDetailVO implements Serializable {
 	/***********************************单位为月时
 	 * 用户月活跃率
 	 */
+	@ColumnWidth(10)
+	@ExcelProperty(value = "用户月活跃率")
 	@Schema(description = "用户月活跃率")
 	private BigDecimal monthActiveUserRate;
 }

+ 40 - 21
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageAllVersionDetailVO.java

@@ -1,11 +1,14 @@
 package com.pig4cloud.pig.statistics.api.vo.user;
 
 
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.write.style.ColumnWidth;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 /**
  * @author: lwh
@@ -20,51 +23,67 @@ public class PageAllVersionDetailVO implements Serializable {
 	private static final long serialVersionUID = 1L;
 
 	/**
-	 * 活跃用户占比
-	 */
-	@Schema(description = "活跃用户占比")
-	private Double activeUserRate;
-
-	/**
-	 * 活跃用户
+	 * 版本
 	 */
-	@Schema(description = "活跃用户")
-	private Integer activeUser;
+	@ColumnWidth(8)
+	@ExcelProperty(value = "版本")
+	@Schema(description = "版本")
+	private String version;
 
 	/**
-	 * 新增用户
+	 * 累计用户占比
 	 */
-	@Schema(description = "新增用户")
-	private Integer newUser;
+	@ColumnWidth(16)
+	@ExcelProperty(value = "截至今日版本累计用户占比")
+	@Schema(description = "累计用户占比")
+	private BigDecimal totalUserRate;
 
 	/**
 	 * 累计用户
 	 */
+	@ColumnWidth(11)
+	@ExcelProperty(value = "累计用户")
 	@Schema(description = "累计用户")
 	private Integer totalUser;
 
 	/**
-	 * 累计用户占比
+	 * 新增用户
 	 */
-	@Schema(description = "累计用户占比")
-	private Double totalUserRate;
+	@ColumnWidth(11)
+	@ExcelProperty(value = "新增用户")
+	@Schema(description = "新增用户")
+	private Integer newUser;
 
 	/**
-	 * 升级用户
+	 * 活跃用户
 	 */
-	@Schema(description = "升级用户")
-	private Integer upgradeUser;
+	@ColumnWidth(11)
+	@ExcelProperty(value = "活跃用户")
+	@Schema(description = "活跃用户")
+	private Integer activeUser;
+
+	/**
+	 * 活跃用户占比
+	 */
+	@ColumnWidth(10)
+	@ExcelProperty(value = "活跃用户占比")
+	@Schema(description = "活跃用户占比")
+	private BigDecimal activeUserRate;
 
 	/**
 	 * 启动次数
 	 */
+	@ColumnWidth(11)
+	@ExcelProperty(value = "启动次数")
 	@Schema(description = "启动次数")
 	private Integer launch;
 
 	/**
-	 * 版本
+	 * 升级用户
 	 */
-	@Schema(description = "版本")
-	private String version;
+	@ColumnWidth(11)
+	@ExcelProperty(value = "升级用户")
+	@Schema(description = "升级用户")
+	private Integer upgradeUser;
 
 }

+ 8 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageLaunchDetailVO.java

@@ -1,6 +1,8 @@
 package com.pig4cloud.pig.statistics.api.vo.user;
 
 
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.write.style.ColumnWidth;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -23,18 +25,24 @@ public class PageLaunchDetailVO implements Serializable {
 	/**
 	 * 日期
 	 */
+	@ColumnWidth(20)
+	@ExcelProperty(value = "日期")
 	@Schema(description = "日期")
 	private String date;
 
 	/**
 	 * 启动次数占比
 	 */
+	@ColumnWidth(16)
+	@ExcelProperty(value = "启动次数占比")
 	@Schema(description = "启动次数占比")
 	private BigDecimal launchRate;
 
 	/**
 	 * 启动次数
 	 */
+	@ColumnWidth(13)
+	@ExcelProperty(value = "启动次数")
 	@Schema(description = "启动次数")
 	private Integer launch;
 }

+ 8 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageNewUserTrendDetailVO.java

@@ -1,6 +1,8 @@
 package com.pig4cloud.pig.statistics.api.vo.user;
 
 
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.write.style.ColumnWidth;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -23,18 +25,24 @@ public class PageNewUserTrendDetailVO implements Serializable {
 	/**
 	 * 日期
 	 */
+	@ColumnWidth(20)
+	@ExcelProperty("日期")
 	@Schema(description = "日期")
 	private String date;
 
 	/**
 	 * 新增用户数
 	 */
+	@ColumnWidth(20)
+	@ExcelProperty("新增用户数")
 	@Schema(description = "新增用户数")
 	private Integer newUser;
 
 	/**
 	 * 新增用户占比
 	 */
+	@ColumnWidth(20)
+	@ExcelProperty("新增用户占比")
 	@Schema(description = "新增用户占比")
 	private BigDecimal newUserRate;
 }

+ 6 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageRetentionDetailVO.java

@@ -1,6 +1,8 @@
 package com.pig4cloud.pig.statistics.api.vo.user;
 
 
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.write.style.ColumnWidth;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -23,12 +25,16 @@ public class PageRetentionDetailVO implements Serializable {
 	/**
 	 * 日期
 	 */
+	@ColumnWidth(12)
+	@ExcelProperty(value = "日期")
 	@Schema(description = "日期")
 	private String date;
 
 	/**
 	 * 留存率
 	 */
+	@ColumnWidth(10)
+	@ExcelProperty(value = "留存率")
 	@Schema(description = "留存率")
 	private BigDecimal retention;
 }

+ 21 - 8
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageSingleVersionDetailVO.java

@@ -1,6 +1,8 @@
 package com.pig4cloud.pig.statistics.api.vo.user;
 
 
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.write.style.ColumnWidth;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -22,30 +24,41 @@ public class PageSingleVersionDetailVO implements Serializable {
 	/**
 	 * 日期
 	 */
+	@ColumnWidth(12)
+	@ExcelProperty(value = "日期")
 	@Schema(description = "日期")
 	private String date;
 
+	/**
+	 * 新增用户
+	 */
+	@ColumnWidth(11)
+	@ExcelProperty(value = "新增用户")
+	@Schema(description = "新增用户")
+	private Integer newUser;
+
 	/**
 	 * 活跃用户
 	 */
+	@ColumnWidth(11)
+	@ExcelProperty(value = "活跃用户")
 	@Schema(description = "活跃用户")
 	private Integer activeUser;
 
 	/**
-	 * 新增用户
+	 * 启动次数
 	 */
-	@Schema(description = "新增用户")
-	private Integer newUser;
+	@ColumnWidth(11)
+	@ExcelProperty(value = "启动次数")
+	@Schema(description = "启动次数")
+	private Integer launch;
 
 	/**
 	 * 升级用户
 	 */
+	@ColumnWidth(11)
+	@ExcelProperty(value = "升级用户")
 	@Schema(description = "升级用户")
 	private Integer upgradeUser;
 
-	/**
-	 * 启动次数
-	 */
-	@Schema(description = "启动次数")
-	private Integer launch;
 }

+ 12 - 5
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/vo/user/PageVersionDistributionDetailVO.java

@@ -1,11 +1,13 @@
 package com.pig4cloud.pig.statistics.api.vo.user;
 
 
+import cn.idev.excel.annotation.write.style.ColumnWidth;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 /**
  * @author: lwh
@@ -19,21 +21,26 @@ public class PageVersionDistributionDetailVO implements Serializable {
 	@Serial
 	private static final long serialVersionUID = 1L;
 
-	/**
-	 * 占比率
-	 */
-	@Schema(description = "占比率")
-	private Double rate;
 
 	/**
 	 * 名称
 	 */
+	@ColumnWidth(12)
 	@Schema(description = "名称")
 	private String name;
 
+
 	/**
 	 * 数量
 	 */
+	@ColumnWidth(11)
 	@Schema(description = "数量")
 	private Integer value;
+
+	/**
+	 * 占比率
+	 */
+	@ColumnWidth(11)
+	@Schema(description = "占比率")
+	private BigDecimal rate;
 }

+ 7 - 1
pig-statistics/pig-statistics-biz/pom.xml

@@ -100,7 +100,13 @@
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-undertow</artifactId>
 		</dependency>
-	</dependencies>
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
+            <version>1.3.2</version>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
 
 	<profiles>
 		<profile>

+ 341 - 23
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/controller/UserAnalyseController.java

@@ -1,20 +1,35 @@
 package com.pig4cloud.pig.statistics.controller;
 
 
+import cn.idev.excel.EasyExcel;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.pig4cloud.pig.common.core.exception.BusinessException;
 import com.pig4cloud.pig.common.core.util.R;
 import com.pig4cloud.pig.statistics.api.dto.user.*;
 import com.pig4cloud.pig.statistics.api.vo.user.*;
 import com.pig4cloud.pig.statistics.service.UserAnalyseService;
+import com.pig4cloud.plugin.excel.annotation.ResponseExcel;
+import com.pig4cloud.plugin.excel.annotation.Sheet;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.Valid;
 import lombok.AllArgsConstructor;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.beans.BeanUtils;
 import org.springframework.http.HttpHeaders;
+import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.*;
 
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 
 /**
@@ -47,10 +62,39 @@ public class UserAnalyseController {
 		return R.ok(result);
 	}
 
-	@GetMapping("/new/trend/export")
-	@Operation(summary = "导出新增趋势")
-	public R exportNewUserTrend() {
-		return R.ok();
+	@ResponseExcel(name = "新增趋势详情")
+	@PostMapping("/new/trend/export")
+	@Operation(summary = "导出新增趋势详情")
+	public List<PageNewUserTrendDetailVO> exportNewUserTrend(@Valid @RequestBody UserAnalyseQueryBaseDTO reqDto) {
+		// 初始化结果集
+		List<PageNewUserTrendDetailVO> allRecords = new ArrayList<>();
+		// 分页查询参数
+		PageUserAnalyseDTO pageQuery = new PageUserAnalyseDTO();
+		BeanUtils.copyProperties(reqDto, pageQuery);
+		// 每次查询的批次大小,根据实际内存情况调整
+		long batchSize = 1000;
+		pageQuery.setSize(batchSize);
+		pageQuery.setCurrent(1);
+
+		while (true) {
+			// 分批查询数据
+			Page<PageNewUserTrendDetailVO> pageResult = userAnalyseService.pageNewUserTrendDetail(pageQuery);
+			List<PageNewUserTrendDetailVO> records = pageResult.getRecords();
+			if (CollectionUtils.isEmpty(records)) {
+				// 没有更多数据,退出循环
+				break;
+			}
+			// 添加到结果集
+			allRecords.addAll(records);
+			// 如果当前页记录数小于批次大小,说明是最后一页
+			if (records.size() < batchSize) {
+				break;
+			}
+			// 准备查询下一页
+			pageQuery.setCurrent(pageQuery.getCurrent() + 1);
+		}
+
+		return allRecords;
 	}
 
 	@PostMapping("/new/retention")
@@ -66,10 +110,39 @@ public class UserAnalyseController {
 		return R.ok(result);
 	}
 
-	@GetMapping("/new/retention/export")
-	@Operation(summary = "导出次日留存率")
-	public R exportNewUserRetention() {
-		return R.ok();
+	@ResponseExcel(name = "次日留存率详情")
+	@PostMapping("/new/retention/export")
+	@Operation(summary = "导出次日留存率详情")
+	public List<PageRetentionDetailVO> exportNewUserRetention(@Valid @RequestBody UserAnalyseQueryBaseDTO reqDto) {
+		// 初始化结果集
+		List<PageRetentionDetailVO> allRecords = new ArrayList<>();
+		// 分页查询参数
+		PageUserAnalyseDTO pageQuery = new PageUserAnalyseDTO();
+		BeanUtils.copyProperties(reqDto, pageQuery);
+		// 每次查询的批次大小,根据实际内存情况调整
+		long batchSize = 1000;
+		pageQuery.setSize(batchSize);
+		pageQuery.setCurrent(1);
+
+		while (true) {
+			// 分批查询数据
+			Page<PageRetentionDetailVO> pageResult = userAnalyseService.pageNewUserRetentionDetail(pageQuery);
+			List<PageRetentionDetailVO> records = pageResult.getRecords();
+			if (CollectionUtils.isEmpty(records)) {
+				// 没有更多数据,退出循环
+				break;
+			}
+			// 添加到结果集
+			allRecords.addAll(records);
+			// 如果当前页记录数小于批次大小,说明是最后一页
+			if (records.size() < batchSize) {
+				break;
+			}
+			// 准备查询下一页
+			pageQuery.setCurrent(pageQuery.getCurrent() + 1);
+		}
+
+		return allRecords;
 	}
 
 /***************************************** 活跃用户 *****************************************/
@@ -116,10 +189,64 @@ public class UserAnalyseController {
 		return R.ok(result);
 	}
 
-	@GetMapping("/active/export")
+	@PostMapping("/active/export")
 	@Operation(summary = "导出活跃详情")
-	public R exportActiveDetail() {
-		return R.ok();
+	public void exportActiveDetail(@Valid @RequestBody UserAnalyseQueryBaseDTO reqDto, HttpServletResponse response) throws Exception {
+		// 1. 根据时间单位确定需要排除的字段
+		List<String> excludeFields = new ArrayList<>();
+		String timeUnit = reqDto.getTimeUnit();
+		switch (timeUnit) {
+			case "day":
+				excludeFields.add("monthActiveUserRate");
+				excludeFields.add("weekActiveUserRate");
+				break;
+			case "week":
+				excludeFields.add("monthActiveUserRate");
+				excludeFields.add("mauRate");
+				excludeFields.add("wauRate");
+				break;
+			case "month":
+				excludeFields.add("weekActiveUserRate");
+				excludeFields.add("mauRate");
+				excludeFields.add("wauRate");
+				break;
+			default:
+				// 可根据需要处理默认情况
+				break;
+		}
+
+		// 2. 分页查询数据(保持原有逻辑)
+		List<PageActiveDetailVO> allRecords = new ArrayList<>();
+		PageUserAnalyseDTO pageQuery = new PageUserAnalyseDTO();
+		BeanUtils.copyProperties(reqDto, pageQuery);
+		long batchSize = 1000;
+		pageQuery.setSize(batchSize);
+		pageQuery.setCurrent(1);
+
+		while (true) {
+			Page<PageActiveDetailVO> pageResult = userAnalyseService.pageActiveDetail(pageQuery);
+			List<PageActiveDetailVO> records = pageResult.getRecords();
+			if (CollectionUtils.isEmpty(records)) {
+				break;
+			}
+			allRecords.addAll(records);
+			if (records.size() < batchSize) {
+				break;
+			}
+			pageQuery.setCurrent(pageQuery.getCurrent() + 1);
+		}
+
+		// 3. 动态设置Excel响应和导出字段
+		response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+		response.setCharacterEncoding("utf-8");
+		String fileName = URLEncoder.encode("活跃详情", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
+		response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+
+		// 使用EasyExcel编程式导出,指定排除字段
+		EasyExcel.write(response.getOutputStream(), PageActiveDetailVO.class)
+				.excludeColumnFiledNames(excludeFields)
+				.sheet("活跃详情")
+				.doWrite(allRecords);
 	}
 
 /***************************************** 启动次数 *****************************************/
@@ -137,10 +264,39 @@ public class UserAnalyseController {
 		return R.ok(result);
 	}
 
-	@GetMapping("/launch/export")
+	@ResponseExcel(name = "启动次数详情")
+	@PostMapping("/launch/export")
 	@Operation(summary = "导出启动次数详情")
-	public R exportLaunchDetail() {
-		return R.ok();
+	public List<PageLaunchDetailVO> exportLaunchDetail(@Valid @RequestBody UserAnalyseQueryBaseDTO reqDto) {
+		// 初始化结果集
+		List<PageLaunchDetailVO> allRecords = new ArrayList<>();
+		// 分页查询参数
+		PageUserAnalyseDTO pageQuery = new PageUserAnalyseDTO();
+		BeanUtils.copyProperties(reqDto, pageQuery);
+		// 每次查询的批次大小,根据实际内存情况调整
+		long batchSize = 1000;
+		pageQuery.setSize(batchSize);
+		pageQuery.setCurrent(1);
+
+		while (true) {
+			// 分批查询数据
+			Page<PageLaunchDetailVO> pageResult = userAnalyseService.pageLaunchDetail(pageQuery);
+			List<PageLaunchDetailVO> records = pageResult.getRecords();
+			if (CollectionUtils.isEmpty(records)) {
+				// 没有更多数据,退出循环
+				break;
+			}
+			// 添加到结果集
+			allRecords.addAll(records);
+			// 如果当前页记录数小于批次大小,说明是最后一页
+			if (records.size() < batchSize) {
+				break;
+			}
+			// 准备查询下一页
+			pageQuery.setCurrent(pageQuery.getCurrent() + 1);
+		}
+
+		return allRecords;
 	}
 
 /***************************************** 版本分布 *****************************************/
@@ -152,23 +308,110 @@ public class UserAnalyseController {
 		return R.ok(result);
 	}
 
+	@ResponseExcel(name = "全部版本详情", sheets = {
+			@Sheet(sheetName = "今日"),
+			@Sheet(sheetName = "昨日")
+	})
 	@GetMapping("/version/export")
 	@Operation(summary = "导出全部版本详情")
-	public R exportAllVersionDetail() {
-		return R.ok();
+	public List<List<PageAllVersionDetailVO>> exportAllVersionDetail(
+			@Schema(description = "应用ID",example = "Fqs2CL9CUn7U1AqilSFXgb")
+			@ParameterObject String appId) {
+		if (appId == null || appId.isEmpty()){
+			throw new BusinessException("appId不能为空");
+		}
+
+		List<List<PageAllVersionDetailVO>> result = new ArrayList<>();
+		for (int i = 0; i < 2; i++) {
+			// 初始化结果集
+			List<PageAllVersionDetailVO> allRecords = new ArrayList<>();
+			// 分页查询参数
+			PageAllVersionDetailDTO pageQuery = new PageAllVersionDetailDTO();
+			pageQuery.setAppId(appId);
+			pageQuery.setQueryDate(i);
+			// 每次查询的批次大小,根据实际内存情况调整
+			long batchSize = 1000;
+			pageQuery.setSize(batchSize);
+			pageQuery.setCurrent(1);
+
+			while (true) {
+				// 分批查询数据
+				Page<PageAllVersionDetailVO> pageResult = userAnalyseService.pageAllVersionDetail(pageQuery);
+				List<PageAllVersionDetailVO> records = pageResult.getRecords();
+				if (CollectionUtils.isEmpty(records)) {
+					// 没有更多数据,退出循环
+					break;
+				}
+				// 添加到结果集
+				allRecords.addAll(records);
+				// 如果当前页记录数小于批次大小,说明是最后一页
+				if (records.size() < batchSize) {
+					break;
+				}
+				// 准备查询下一页
+				pageQuery.setCurrent(pageQuery.getCurrent() + 1);
+			}
+			result.add(allRecords);
+		}
+
+		return result;
 	}
 
 	@PostMapping("/version/single/detail")
 	@Operation(summary = "分页查询单版本详情")
-	public R<Page<PageSingleVersionDetailVO>> pageSingleVersionDetail(@Valid @RequestBody PageSingleVersionDetailDTO reqDto) {
+	public R<Page<PageSingleVersionDetailVO>> pageSingleVersionDetail(@Valid @RequestBody PageUserAnalyseDTO reqDto) {
 		Page<PageSingleVersionDetailVO> result = userAnalyseService.pageSingleVersionDetail(reqDto);
 		return R.ok(result);
 	}
 
-	@GetMapping("/version/single/export")
+	@PostMapping("/version/single/export")
 	@Operation(summary = "导出单版本详情")
-	public R exportSingleVersionDetail() {
-		return R.ok();
+	public void exportSingleVersionDetail(
+			@Valid @RequestBody UserAnalyseQueryBaseDTO reqDto,
+			HttpServletResponse response) throws Exception {
+
+		// 获取版本并验证
+		if (reqDto.getVersion() == null || reqDto.getVersion().isEmpty()){
+			throw new BusinessException("版本不能为空");
+		}
+		String version = reqDto.getVersion().get(0);
+		String sheetName = version + "版本详情"; // 动态生成表名
+
+		// 初始化结果集
+		List<PageSingleVersionDetailVO> allRecords = new ArrayList<>();
+		// 分页查询参数
+		PageUserAnalyseDTO pageQuery = new PageUserAnalyseDTO();
+		BeanUtils.copyProperties(reqDto, pageQuery);
+		long batchSize = 1000;
+		pageQuery.setSize(batchSize);
+		pageQuery.setCurrent(1);
+
+		// 分批查询数据
+		while (true) {
+			Page<PageSingleVersionDetailVO> pageResult = userAnalyseService.pageSingleVersionDetail(pageQuery);
+			List<PageSingleVersionDetailVO> records = pageResult.getRecords();
+			if (CollectionUtils.isEmpty(records)) {
+				break;
+			}
+			allRecords.addAll(records);
+			if (records.size() < batchSize) {
+				break;
+			}
+			pageQuery.setCurrent(pageQuery.getCurrent() + 1);
+		}
+
+		// 设置响应头,动态生成文件名
+		String fileName = URLEncoder.encode(sheetName, StandardCharsets.UTF_8)
+				.replaceAll("\\+", "%20");
+		response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+		response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+		response.setHeader("Content-disposition",
+				"attachment;filename*=utf-8''" + fileName + ".xlsx");
+
+		// 使用EasyExcel手动写入,指定动态表名
+		EasyExcel.write(response.getOutputStream(), PageSingleVersionDetailVO.class)
+				.sheet(sheetName) // 设置工作表名称
+				.doWrite(allRecords);
 	}
 
 	@PostMapping("/version/distribution")
@@ -186,10 +429,85 @@ public class UserAnalyseController {
 	}
 
 
-	@GetMapping("/version/distribution/export")
+	@PostMapping("/version/distribution/export")
 	@Operation(summary = "导出版本用户来源详情")
-	public R exportVersionDistributionDetail() {
-		return R.ok();
+	public void exportVersionDistributionDetail(
+			@Valid @RequestBody GetVersionDistributionDTO reqDto,
+			HttpServletResponse response) throws Exception {
+
+		// 1. 参数校验与获取
+		String version = reqDto.getVersion();
+		String type = reqDto.getType();
+		if (version == null || version.isEmpty()) {
+			throw new BusinessException("版本不能为空");
+		}
+		if (type == null || type.isEmpty()) {
+			throw new BusinessException("类型不能为空");
+		}
+
+		// 2. 动态生成文件名和表头
+		String fileName;
+		List<List<String>> head = new ArrayList<>();
+		String sheetName = version + "版本用户来源详情"; // 固定sheet名
+
+		switch (type) {
+			case "newUserChannel":
+				fileName = version + "版本新增用户渠道详情";
+				head.add(Collections.singletonList("渠道"));
+				head.add(Collections.singletonList("新增用户"));
+				head.add(Collections.singletonList("新增用户占比"));
+				break;
+			case "upgradeChannel":
+				fileName = version + "版本升级用户渠道详情";
+				head.add(Collections.singletonList("渠道"));
+				head.add(Collections.singletonList("升级用户"));
+				head.add(Collections.singletonList("升级用户占比"));
+				break;
+			case "upgradeVersion":
+				fileName = version + "版本升级用户版本详情";
+				head.add(Collections.singletonList("版本"));
+				head.add(Collections.singletonList("升级用户"));
+				head.add(Collections.singletonList("升级用户占比"));
+				break;
+			default:
+				throw new BusinessException("不支持的导出类型: " + type);
+		}
+
+		// 3. 设置Excel响应头(确保文件类型正确)
+		response.reset();
+		response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+		response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+		String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
+		response.setHeader("Content-Disposition",
+				"attachment;filename=\"" + encodedFileName + ".xlsx\";" +
+						"filename*=utf-8''" + encodedFileName + ".xlsx");
+
+		// 4. 分页查询数据
+		List<PageVersionDistributionDetailVO> allRecords = new ArrayList<>();
+		PageVersionDistributionDetailDTO pageQuery = new PageVersionDistributionDetailDTO();
+		BeanUtils.copyProperties(reqDto, pageQuery);
+		long batchSize = 1000;
+		pageQuery.setSize(batchSize);
+		pageQuery.setCurrent(1);
+
+		while (true) {
+			Page<PageVersionDistributionDetailVO> pageResult = userAnalyseService.pageVersionDistributionDetail(pageQuery);
+			List<PageVersionDistributionDetailVO> records = pageResult.getRecords();
+			if (records == null || records.isEmpty()) {
+				break;
+			}
+			allRecords.addAll(records);
+			if (records.size() < batchSize) {
+				break;
+			}
+			pageQuery.setCurrent(pageQuery.getCurrent() + 1);
+		}
+
+		// 使用EasyExcel手动写入,指定动态表名
+		EasyExcel.write(response.getOutputStream())
+				.head(head)
+				.sheet(sheetName)
+				.doWrite(allRecords);
 	}
 
 }

+ 1 - 1
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/UserAnalyseService.java

@@ -114,7 +114,7 @@ public interface UserAnalyseService {
 	 * @param reqDto 入参
 	 * @return Page
 	 */
-	Page<PageSingleVersionDetailVO> pageSingleVersionDetail(@Valid PageSingleVersionDetailDTO reqDto);
+	Page<PageSingleVersionDetailVO> pageSingleVersionDetail(@Valid PageUserAnalyseDTO reqDto);
 
 	/**
 	 * 查询版本用户来源

File diff suppressed because it is too large
+ 581 - 493
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/service/impl/UserAnalyseServiceImpl.java


+ 57 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/ClientHandler.java

@@ -0,0 +1,57 @@
+package com.pig4cloud.pig.statistics.socket;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+/**
+ * 客户端处理器,负责与单个客户端的所有数据交互
+ */
+public class ClientHandler implements Runnable {
+    private final Socket clientSocket;
+    private final ProtocolHandler protocolHandler;
+    private final DataProcessor dataProcessor;
+
+    public ClientHandler(Socket clientSocket, ProtocolHandler protocolHandler, DataProcessor dataProcessor) {
+        this.clientSocket = clientSocket;
+        this.protocolHandler = protocolHandler;
+        this.dataProcessor = dataProcessor;
+    }
+
+    @Override
+    public void run() {
+        try (InputStream in = clientSocket.getInputStream();
+             OutputStream out = clientSocket.getOutputStream()) {
+
+            System.out.println("开始处理客户端数据: " + clientSocket.getInetAddress());
+            
+            // 循环处理客户端发送的数据包
+            while (!clientSocket.isClosed()) {
+                // 使用协议处理器解析数据包
+                Packet packet = protocolHandler.readPacket(in);
+                if (packet == null) {
+                    break; // 连接关闭
+                }
+                
+                // 处理数据包并获取响应
+                byte[] responseData = dataProcessor.processData(packet.getType(), packet.getContent());
+                
+                // 发送响应
+                protocolHandler.sendPacket(out, packet.getType(), responseData);
+            }
+        } catch (IOException e) {
+            if (!clientSocket.isClosed()) {
+                System.err.println("客户端处理错误: " + e.getMessage());
+            }
+        } finally {
+            try {
+                clientSocket.close();
+                System.out.println("客户端连接已关闭: " + clientSocket.getInetAddress());
+            } catch (IOException e) {
+                System.err.println("关闭客户端连接错误: " + e.getMessage());
+            }
+        }
+    }
+}
+    

+ 22 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/ClientHandlerFactory.java

@@ -0,0 +1,22 @@
+package com.pig4cloud.pig.statistics.socket;
+
+import java.net.Socket;
+
+/**
+ * 客户端处理器工厂,负责创建ClientHandler实例
+ * 解耦TcpServer与ClientHandler的依赖关系
+ */
+public class ClientHandlerFactory {
+    /**
+     * 创建客户端处理器实例
+     */
+    public ClientHandler createClientHandler(Socket clientSocket) {
+        // 创建所需的依赖组件
+        ProtocolHandler protocolHandler = new ProtocolHandler();
+        DataProcessor dataProcessor = new DataProcessor();
+        
+        // 创建并返回ClientHandler实例
+        return new ClientHandler(clientSocket, protocolHandler, dataProcessor);
+    }
+}
+    

+ 91 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/DataProcessor.java

@@ -0,0 +1,91 @@
+package com.pig4cloud.pig.statistics.socket;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.UUID;
+
+/**
+ * 数据处理器,负责实际的业务逻辑处理
+ */
+public class DataProcessor {
+    // 图片保存路径
+    private static final String IMAGE_SAVE_PATH = "images";
+
+    public DataProcessor() {
+        // 确保图片目录存在
+        createImageDirectory();
+    }
+
+    /**
+     * 处理接收到的数据
+     * @param dataType 数据类型
+     * @param content 数据内容
+     * @return 响应数据
+     */
+    public byte[] processData(int dataType, byte[] content) {
+        try {
+            if (dataType == ProtocolHandler.TYPE_TEXT) {
+                // 文本数据,原路返回
+                String text = new String(content, "UTF-8");
+                System.out.println("收到文本: " + text);
+				System.out.println("文本长度: " + text.length());
+                return content;
+                
+            } else if (dataType == ProtocolHandler.TYPE_IMAGE) {
+                // 图片数据,保存并返回路径
+                String fileName = generateImageFileName();
+                String filePath = IMAGE_SAVE_PATH + File.separator + fileName;
+
+                // 保存图片
+                Files.write(Paths.get(filePath), content);
+                System.out.println("图片已保存: " + filePath);
+				System.out.println("图片大小: " + formatFileSize(content.length));
+                // 返回保存路径
+                return filePath.getBytes("UTF-8");
+                
+            } else {
+                // 未知数据类型
+                String errorMsg = "未知的数据类型: " + dataType;
+                System.err.println(errorMsg);
+                return errorMsg.getBytes("UTF-8");
+            }
+        } catch (IOException e) {
+            String errorMsg = "处理数据错误: " + e.getMessage();
+            System.err.println(errorMsg);
+            return errorMsg.getBytes();
+        }
+    }
+	// 辅助方法:将字节数格式化为更易读的单位(KB, MB等)
+	private String formatFileSize(long bytes) {
+		if (bytes < 1024) {
+			return bytes + " B";
+		} else if (bytes < 1024 * 1024) {
+			return String.format("%.2f KB", bytes / 1024.0);
+		} else if (bytes < 1024 * 1024 * 1024) {
+			return String.format("%.2f MB", bytes / (1024.0 * 1024));
+		} else {
+			return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
+		}
+	}
+
+    /**
+     * 创建图片保存目录
+     */
+    private void createImageDirectory() {
+        File directory = new File(IMAGE_SAVE_PATH);
+        if (!directory.exists()) {
+            directory.mkdirs();
+        }
+    }
+
+    /**
+     * 生成唯一的图片文件名
+     */
+    private String generateImageFileName() {
+        return "img_" + System.currentTimeMillis() + "_" + 
+               UUID.randomUUID().toString().substring(0, 8) + ".jpg";
+    }
+}
+    

+ 23 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/Packet.java

@@ -0,0 +1,23 @@
+package com.pig4cloud.pig.statistics.socket;
+
+/**
+ * 数据包模型类,用于封装解析后的数据包
+ */
+public class Packet {
+    private final int type;
+    private final byte[] content;
+
+    public Packet(int type, byte[] content) {
+        this.type = type;
+        this.content = content;
+    }
+
+    public int getType() {
+        return type;
+    }
+
+    public byte[] getContent() {
+        return content;
+    }
+}
+    

+ 127 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/ProtocolHandler.java

@@ -0,0 +1,127 @@
+package com.pig4cloud.pig.statistics.socket;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * 协议处理器,负责数据包的解析和构建
+ */
+public class ProtocolHandler {
+    // 协议定义
+    public static final byte PACKET_HEADER = 0x2;
+    public static final byte PACKET_TAIL = 0x3;
+    public static final byte TYPE_TEXT = 0x01;
+    public static final byte TYPE_IMAGE = 0x02;
+
+    /**
+     * 从输入流读取一个完整的数据包
+     */
+    public Packet readPacket(InputStream in) throws IOException {
+        ByteArrayOutputStream packetBuffer = new ByteArrayOutputStream();
+        boolean inPacket = false;
+        int dataType = -1;
+        byte[] lengthBytes = new byte[4];
+        int lengthIndex = 0;
+        int contentLength = -1;
+        ByteArrayOutputStream contentBuffer = new ByteArrayOutputStream();
+
+        int b;
+        while ((b = in.read()) != -1) {
+            byte currentByte = (byte) b;
+
+            // 处理包头
+            if (!inPacket && currentByte == PACKET_HEADER) {
+                inPacket = true;
+                packetBuffer.reset();
+                packetBuffer.write(currentByte);
+                lengthIndex = 0;
+                contentLength = -1;
+                contentBuffer.reset();
+                continue;
+            }
+
+            // 处理包内数据
+            if (inPacket) {
+                packetBuffer.write(currentByte);
+
+                // 读取数据类型
+                if (dataType == -1) {
+                    dataType = currentByte & 0xFF;
+                    continue;
+                }
+
+                // 读取数据长度(4字节)
+                if (contentLength == -1) {
+                    lengthBytes[lengthIndex++] = currentByte;
+                    if (lengthIndex == 4) {
+                        // 转换4字节为整数(大端模式)
+                        contentLength = ((lengthBytes[0] & 0xFF) << 24) |
+                                       ((lengthBytes[1] & 0xFF) << 16) |
+                                       ((lengthBytes[2] & 0xFF) << 8) |
+                                       (lengthBytes[3] & 0xFF);
+                    }
+                    continue;
+                }
+
+                // 读取数据内容
+                if (contentBuffer.size() < contentLength) {
+                    contentBuffer.write(currentByte);
+
+                    // 检查是否已读取所有内容
+                    if (contentBuffer.size() == contentLength) {
+                        // 接下来应该是包尾
+                        continue;
+                    }
+                } else {
+                    // 检查包尾
+                    if (currentByte == PACKET_TAIL) {
+                        // 完整包接收完成,返回数据包
+                        return new Packet(dataType, contentBuffer.toByteArray());
+                    } else {
+                        // 包尾不正确,视为无效包
+                        System.err.println("无效的包尾,丢弃数据包");
+                        inPacket = false;
+                        dataType = -1;
+                        contentLength = -1;
+                    }
+                }
+            }
+        }
+
+        // 到达流末尾,返回null
+        return null;
+    }
+
+    /**
+     * 发送一个数据包到输出流
+     */
+    public void sendPacket(OutputStream out, int dataType, byte[] data) throws IOException {
+        // 构造响应包
+        ByteArrayOutputStream responseBuffer = new ByteArrayOutputStream();
+
+        // 写入包头
+        responseBuffer.write(PACKET_HEADER);
+
+        // 写入数据类型
+        responseBuffer.write(dataType);
+
+        // 写入数据长度(4字节,大端模式)
+        responseBuffer.write((data.length >> 24) & 0xFF);
+        responseBuffer.write((data.length >> 16) & 0xFF);
+        responseBuffer.write((data.length >> 8) & 0xFF);
+        responseBuffer.write(data.length & 0xFF);
+
+        // 写入数据内容
+        responseBuffer.write(data);
+
+        // 写入包尾
+        responseBuffer.write(PACKET_TAIL);
+
+        // 发送响应
+        out.write(responseBuffer.toByteArray());
+        out.flush();
+    }
+}
+    

+ 221 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/TcpClient.java

@@ -0,0 +1,221 @@
+package com.pig4cloud.pig.statistics.socket;
+
+import java.io.*;
+import java.net.*;
+import java.nio.file.*;
+
+/**
+ * TCP客户端测试类,用于测试服务器功能
+ * 支持发送文本消息和图片文件
+ */
+public class TcpClient {
+    // 协议定义,与服务器保持一致
+    private static final byte PACKET_HEADER = 0x2;
+    private static final byte PACKET_TAIL = 0x3;
+    private static final byte TYPE_TEXT = 0x01;
+    private static final byte TYPE_IMAGE = 0x02;
+    
+    private String serverHost;
+    private int serverPort;
+    private Socket socket;
+    private InputStream in;
+    private OutputStream out;
+    
+    public TcpClient(String serverHost, int serverPort) {
+        this.serverHost = serverHost;
+        this.serverPort = serverPort;
+    }
+    
+    /**
+     * 连接到服务器
+     */
+    public void connect() throws IOException {
+        socket = new Socket(serverHost, serverPort);
+        in = socket.getInputStream();
+        out = socket.getOutputStream();
+        System.out.println("已连接到服务器: " + serverHost + ":" + serverPort);
+    }
+    
+    /**
+     * 发送文本消息到服务器
+     */
+    public String sendText(String text) throws IOException {
+        if (socket == null || socket.isClosed()) {
+            throw new IOException("未连接到服务器,请先调用connect()方法");
+        }
+        
+        byte[] data = text.getBytes("UTF-8");
+        sendPacket(TYPE_TEXT, data);
+        return receiveResponse();
+    }
+    
+    /**
+     * 发送图片文件到服务器
+     */
+    public String sendImage(String imageFilePath) throws IOException {
+        if (socket == null || socket.isClosed()) {
+            throw new IOException("未连接到服务器,请先调用connect()方法");
+        }
+        
+        // 读取图片文件
+        byte[] imageData = Files.readAllBytes(Paths.get(imageFilePath));
+        sendPacket(TYPE_IMAGE, imageData);
+        return receiveResponse();
+    }
+    
+    /**
+     * 按照协议格式发送数据包
+     */
+    private void sendPacket(int type, byte[] data) throws IOException {
+        ByteArrayOutputStream packetBuffer = new ByteArrayOutputStream();
+        
+        // 写入包头
+        packetBuffer.write(PACKET_HEADER);
+        
+        // 写入数据类型
+        packetBuffer.write(type);
+        
+        // 写入数据长度(4字节,大端模式)
+        packetBuffer.write((data.length >> 24) & 0xFF);
+        packetBuffer.write((data.length >> 16) & 0xFF);
+        packetBuffer.write((data.length >> 8) & 0xFF);
+        packetBuffer.write(data.length & 0xFF);
+        
+        // 写入数据内容
+        packetBuffer.write(data);
+        
+        // 写入包尾
+        packetBuffer.write(PACKET_TAIL);
+        
+        // 发送数据包
+        out.write(packetBuffer.toByteArray());
+        out.flush();
+    }
+    
+    /**
+     * 接收服务器响应
+     */
+    private String receiveResponse() throws IOException {
+        // 解析服务器响应的数据包
+        ByteArrayOutputStream packetBuffer = new ByteArrayOutputStream();
+        boolean inPacket = false;
+        int dataType = -1;
+        byte[] lengthBytes = new byte[4];
+        int lengthIndex = 0;
+        int contentLength = -1;
+        ByteArrayOutputStream contentBuffer = new ByteArrayOutputStream();
+        
+        int b;
+        while ((b = in.read()) != -1) {
+            byte currentByte = (byte) b;
+            
+            // 处理包头
+            if (!inPacket && currentByte == PACKET_HEADER) {
+                inPacket = true;
+                packetBuffer.reset();
+                packetBuffer.write(currentByte);
+                lengthIndex = 0;
+                contentLength = -1;
+                contentBuffer.reset();
+                continue;
+            }
+            
+            // 处理包内数据
+            if (inPacket) {
+                packetBuffer.write(currentByte);
+                
+                // 读取数据类型
+                if (dataType == -1) {
+                    dataType = currentByte & 0xFF;
+                    continue;
+                }
+                
+                // 读取数据长度(4字节)
+                if (contentLength == -1) {
+                    lengthBytes[lengthIndex++] = currentByte;
+                    if (lengthIndex == 4) {
+                        // 转换4字节为整数(大端模式)
+                        contentLength = ((lengthBytes[0] & 0xFF) << 24) |
+                                       ((lengthBytes[1] & 0xFF) << 16) |
+                                       ((lengthBytes[2] & 0xFF) << 8) |
+                                       (lengthBytes[3] & 0xFF);
+                    }
+                    continue;
+                }
+                
+                // 读取数据内容
+                if (contentBuffer.size() < contentLength) {
+                    contentBuffer.write(currentByte);
+                    
+                    // 检查是否已读取所有内容
+                    if (contentBuffer.size() == contentLength) {
+                        // 接下来应该是包尾
+                        continue;
+                    }
+                } else {
+                    // 检查包尾
+                    if (currentByte == PACKET_TAIL) {
+                        // 完整包接收完成,返回内容
+                        return new String(contentBuffer.toByteArray(), "UTF-8");
+                    } else {
+                        // 包尾不正确,视为无效包
+                        throw new IOException("收到无效的数据包,包尾不正确");
+                    }
+                }
+            }
+        }
+        
+        throw new IOException("与服务器的连接已关闭");
+    }
+    
+    /**
+     * 关闭与服务器的连接
+     */
+    public void disconnect() {
+        try {
+            if (in != null) in.close();
+            if (out != null) out.close();
+            if (socket != null && !socket.isClosed()) {
+                socket.close();
+                System.out.println("已断开与服务器的连接");
+            }
+        } catch (IOException e) {
+            System.err.println("关闭连接时发生错误: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 测试方法
+     */
+    public static void main(String[] args) {
+        // 服务器地址和端口
+        String host = "localhost";
+        int port = 8888;
+        
+        TcpClient client = new TcpClient(host, port);
+        
+        try {
+            // 连接服务器
+            client.connect();
+            
+            // 测试发送文本
+            String text = "Hello, TCP Server!";
+            System.out.println("发送文本: " + text);
+            String textResponse = client.sendText(text);
+            System.out.println("服务器响应: " + textResponse);
+            
+            // 测试发送图片(请替换为实际的图片路径)
+            String imagePath = "C:\\Users\\L\\Desktop\\素材\\image16.png"; // 测试图片路径
+            System.out.println("发送图片: " + imagePath);
+            String imageResponse = client.sendImage(imagePath);
+            System.out.println("服务器响应: " + imageResponse);
+            
+        } catch (IOException e) {
+            System.err.println("客户端错误: " + e.getMessage());
+            e.printStackTrace();
+        } finally {
+            // 断开连接
+            client.disconnect();
+        }
+    }
+}

+ 82 - 0
pig-statistics/pig-statistics-biz/src/main/java/com/pig4cloud/pig/statistics/socket/TcpServer.java

@@ -0,0 +1,82 @@
+package com.pig4cloud.pig.statistics.socket;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+/**
+ * TCP服务器主类,负责监听端口和管理客户端连接
+ */
+@Slf4j
+@Component
+public class TcpServer {
+	@Value("${tcp.server.port}")
+	private int port;
+
+	private ServerSocket serverSocket;
+	private boolean isRunning;
+	private final ClientHandlerFactory clientHandlerFactory;
+
+	public TcpServer() {
+		this.clientHandlerFactory = new ClientHandlerFactory();
+	}
+
+	/**
+	 * 启动服务器,Spring初始化后自动调用
+	 */
+	@PostConstruct
+	public void start() throws IOException {
+		serverSocket = new ServerSocket(port);
+		isRunning = true;
+		log.info("TCP服务器已启动,监听端口: {}", port);
+
+		// 启动接受客户端连接的线程
+		new Thread(this::acceptConnections, "ConnectionAcceptor").start();
+	}
+
+	/**
+	 * 接受客户端连接的循环
+	 */
+	private void acceptConnections() {
+		while (isRunning) {
+			try {
+				// 接受客户端连接
+				Socket clientSocket = serverSocket.accept();
+				log.info("新客户端连接: {}", clientSocket.getInetAddress().getHostAddress());
+
+				// 创建并启动客户端处理器
+				ClientHandler clientHandler = clientHandlerFactory.createClientHandler(clientSocket);
+				new Thread(clientHandler, "ClientHandler-" + clientSocket.getInetAddress()).start();
+			} catch (IOException e) {
+				if (isRunning) {
+					log.error("接受客户端连接错误: {}", e.getMessage(), e);
+				} else {
+					log.info("服务器已停止,不再接受新连接");
+				}
+			}
+		}
+	}
+
+	/**
+	 * 停止服务器,Spring销毁前自动调用
+	 */
+	@PreDestroy
+	public void stop() {
+		isRunning = false;
+		try {
+			if (serverSocket != null) {
+				serverSocket.close();
+			}
+			log.info("TCP服务器已停止");
+		} catch (IOException e) {
+			log.error("停止服务器错误: {}", e.getMessage(), e);
+		}
+	}
+}
+    

+ 7 - 1
pig-statistics/pig-statistics-biz/src/main/resources/application.yml

@@ -20,4 +20,10 @@ spring:
   config:
     import:
       - nacos:application.yml
-      - nacos:${spring.application.name}.yml
+      - nacos:${spring.application.name}.yml
+
+
+# TCP服务器配置
+tcp:
+  server:
+    port: 8888

Some files were not shown because too many files changed in this diff