Przeglądaj źródła

Merge branch 'dev/lwh' of lwh/seo into develop

lwh 3 tygodni temu
rodzic
commit
24c2a790ee
100 zmienionych plików z 8801 dodań i 38 usunięć
  1. 869 0
      db/seo/pig.sql
  2. 9 4
      pig-auth/src/main/resources/application.yml
  3. 16 0
      pig-common/pig-common-bom/pom.xml
  4. 6 1
      pig-common/pig-common-core/pom.xml
  5. 10 0
      pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/constant/ServiceNameConstants.java
  6. 60 0
      pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/exception/BusinessException.java
  7. 248 0
      pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/ip/IPUtils.java
  8. BIN
      pig-common/pig-common-core/src/main/resources/ip/ip2region.xdb
  9. 156 4
      pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/sentinel/handle/GlobalBizExceptionHandler.java
  10. 0 26
      pig-gateway/src/main/java/com/pig4cloud/pig/gateway/config/CorsConfig.java
  11. 8 3
      pig-gateway/src/main/resources/application.yml
  12. 61 0
      pig-marketing/pig-marketing-api/pom.xml
  13. 17 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/MarketingConfigConstants.java
  14. 80 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/BatchUpdateMarketingAppsDTO.java
  15. 32 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/MarketingAppsDomainDTO.java
  16. 33 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/MarketingAppsIpDTO.java
  17. 38 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/ModMarketingAppsDomainDTO.java
  18. 38 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/ModMarketingAppsIpDTO.java
  19. 57 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/PageMarketingAppsDTO.java
  20. 50 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/PageStatAppMktDataDTO.java
  21. 55 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/SetMarketingAppsDTO.java
  22. 30 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/SetMarketingAppsStatusDTO.java
  23. 62 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/UpdateMarketingAppsDTO.java
  24. 39 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/common/BaseDomainDTO.java
  25. 49 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/common/BaseIpDTO.java
  26. 21 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/AddMarketingConfigDomainListDTO.java
  27. 22 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/AddMarketingConfigIpListDTO.java
  28. 39 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/AddMarketingGroupDTO.java
  29. 40 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/DelMarketingGroupDTO.java
  30. 45 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/MarketingGroupDomainDTO.java
  31. 56 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/MarketingGroupIPDTO.java
  32. 61 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/ModMarketingGroupDomainDTO.java
  33. 61 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/ModMarketingGroupIPDTO.java
  34. 121 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/data/MarketingDataReportDTO.java
  35. 44 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/data/PageFirstLevelDataDTO.java
  36. 40 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/data/PageSecondLevelDataDTO.java
  37. 141 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingApps.java
  38. 98 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingAppsDomain.java
  39. 117 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingAppsIp.java
  40. 87 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingConfigGroup.java
  41. 173 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingData.java
  42. 81 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingGroupDomain.java
  43. 93 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingGroupIp.java
  44. 102 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/DomainValidationUtil.java
  45. 71 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/FingerprintCombinerUtil.java
  46. 281 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/JA4GeneratorUtil.java
  47. 173 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/JA4HGeneratorUtil.java
  48. 26 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/DomainValidation.java
  49. 79 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/DomainValidator.java
  50. 26 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/IPValidation.java
  51. 120 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/IPValidator.java
  52. 23 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/ValueRange.java
  53. 31 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/ValueRangeValidator.java
  54. 53 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/MarketingAppsDomainVO.java
  55. 71 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/MarketingAppsIpVO.java
  56. 107 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/PageMarketingAppsVO.java
  57. 39 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/PageStatAppFirstMktDataVO.java
  58. 45 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/PageStatAppSecondMktDataVO.java
  59. 53 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingConfigDomainListVO.java
  60. 69 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingConfigIpListVO.java
  61. 31 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingDomainGroupVO.java
  62. 66 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingGlobalConfigVO.java
  63. 31 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingIpGroupVO.java
  64. 33 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/MarketingGroupDomainVO.java
  65. 45 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/MarketingGroupIpVO.java
  66. 64 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/data/MarketingDataReportVO.java
  67. 50 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/data/PageFirstLevelDataVO.java
  68. 62 0
      pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/data/PageSecondLevelDataVO.java
  69. 13 0
      pig-marketing/pig-marketing-biz/Dockerfile
  70. 149 0
      pig-marketing/pig-marketing-biz/pom.xml
  71. 27 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/PigMarketingApplication.java
  72. 162 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/controller/MarketingAppsController.java
  73. 198 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/controller/MarketingConfigController.java
  74. 55 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/controller/MarketingDataController.java
  75. 15 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingAppsDomainMapper.java
  76. 18 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingAppsIpMapper.java
  77. 37 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingAppsMapper.java
  78. 15 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingConfigGroupMapper.java
  79. 42 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingDataMapper.java
  80. 18 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingGroupDomainMapper.java
  81. 22 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingGroupIpMapper.java
  82. 102 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/MarketingAppsService.java
  83. 118 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/MarketingConfigService.java
  84. 39 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/MarketingDataService.java
  85. 801 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MarketingAppsServiceImpl.java
  86. 630 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MarketingConfigServiceImpl.java
  87. 492 0
      pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MarketingDataServiceImpl.java
  88. 30 0
      pig-marketing/pig-marketing-biz/src/main/resources/application.yml
  89. 69 0
      pig-marketing/pig-marketing-biz/src/main/resources/logback-spring.xml
  90. 25 0
      pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingAppsDomainMapper.xml
  91. 25 0
      pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingAppsIpMapper.xml
  92. 125 0
      pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingAppsMapper.xml
  93. 78 0
      pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingDataMapper.xml
  94. 58 0
      pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingGroupIpMapper.xml
  95. 36 0
      pig-marketing/pom.xml
  96. 61 0
      pig-statistics/pig-statistics-api/pom.xml
  97. 50 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/GetNewUserRetentionDTO.java
  98. 60 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/GetNewUserTrendDTO.java
  99. 45 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/GetVersionDistributionDTO.java
  100. 72 0
      pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/PageActiveDetailDTO.java

+ 869 - 0
db/seo/pig.sql

@@ -0,0 +1,869 @@
+/*
+ Navicat Premium Dump SQL
+
+ Source Server         : 192.168.10.101(mysql)
+ Source Server Type    : MySQL
+ Source Server Version : 80036 (8.0.36)
+ Source Host           : 192.168.10.101:3306
+ Source Schema         : pig
+
+ Target Server Type    : MySQL
+ Target Server Version : 80036 (8.0.36)
+ File Encoding         : 65001
+
+ Date: 12/08/2025 13:50:54
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for QRTZ_BLOB_TRIGGERS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_BLOB_TRIGGERS`;
+CREATE TABLE `QRTZ_BLOB_TRIGGERS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `BLOB_DATA` blob NULL,
+  PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+  CONSTRAINT `QRTZ_BLOB_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_CALENDARS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_CALENDARS`;
+CREATE TABLE `QRTZ_CALENDARS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `CALENDAR_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `CALENDAR` blob NOT NULL,
+  PRIMARY KEY (`SCHED_NAME`, `CALENDAR_NAME`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_CRON_TRIGGERS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_CRON_TRIGGERS`;
+CREATE TABLE `QRTZ_CRON_TRIGGERS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `CRON_EXPRESSION` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TIME_ZONE_ID` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+  CONSTRAINT `QRTZ_CRON_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_FIRED_TRIGGERS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_FIRED_TRIGGERS`;
+CREATE TABLE `QRTZ_FIRED_TRIGGERS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `ENTRY_ID` varchar(95) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `INSTANCE_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `FIRED_TIME` bigint NOT NULL,
+  `SCHED_TIME` bigint NOT NULL,
+  `PRIORITY` int NOT NULL,
+  `STATE` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `JOB_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `JOB_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  PRIMARY KEY (`SCHED_NAME`, `ENTRY_ID`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_JOB_DETAILS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`;
+CREATE TABLE `QRTZ_JOB_DETAILS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `JOB_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `JOB_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `DESCRIPTION` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `JOB_CLASS_NAME` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `IS_DURABLE` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `IS_UPDATE_DATA` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `JOB_DATA` blob NULL,
+  PRIMARY KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_LOCKS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_LOCKS`;
+CREATE TABLE `QRTZ_LOCKS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `LOCK_NAME` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  PRIMARY KEY (`SCHED_NAME`, `LOCK_NAME`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_PAUSED_TRIGGER_GRPS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_PAUSED_TRIGGER_GRPS`;
+CREATE TABLE `QRTZ_PAUSED_TRIGGER_GRPS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  PRIMARY KEY (`SCHED_NAME`, `TRIGGER_GROUP`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_SCHEDULER_STATE
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_SCHEDULER_STATE`;
+CREATE TABLE `QRTZ_SCHEDULER_STATE`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `INSTANCE_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `LAST_CHECKIN_TIME` bigint NOT NULL,
+  `CHECKIN_INTERVAL` bigint NOT NULL,
+  PRIMARY KEY (`SCHED_NAME`, `INSTANCE_NAME`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_SIMPLE_TRIGGERS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_SIMPLE_TRIGGERS`;
+CREATE TABLE `QRTZ_SIMPLE_TRIGGERS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `REPEAT_COUNT` bigint NOT NULL,
+  `REPEAT_INTERVAL` bigint NOT NULL,
+  `TIMES_TRIGGERED` bigint NOT NULL,
+  PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+  CONSTRAINT `QRTZ_SIMPLE_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_SIMPROP_TRIGGERS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_SIMPROP_TRIGGERS`;
+CREATE TABLE `QRTZ_SIMPROP_TRIGGERS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `STR_PROP_1` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `STR_PROP_2` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `STR_PROP_3` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `INT_PROP_1` int NULL DEFAULT NULL,
+  `INT_PROP_2` int NULL DEFAULT NULL,
+  `LONG_PROP_1` bigint NULL DEFAULT NULL,
+  `LONG_PROP_2` bigint NULL DEFAULT NULL,
+  `DEC_PROP_1` decimal(13, 4) NULL DEFAULT NULL,
+  `DEC_PROP_2` decimal(13, 4) NULL DEFAULT NULL,
+  `BOOL_PROP_1` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `BOOL_PROP_2` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+  CONSTRAINT `QRTZ_SIMPROP_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for QRTZ_TRIGGERS
+-- ----------------------------
+DROP TABLE IF EXISTS `QRTZ_TRIGGERS`;
+CREATE TABLE `QRTZ_TRIGGERS`  (
+  `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `JOB_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `JOB_GROUP` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `DESCRIPTION` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `NEXT_FIRE_TIME` bigint NULL DEFAULT NULL,
+  `PREV_FIRE_TIME` bigint NULL DEFAULT NULL,
+  `PRIORITY` int NULL DEFAULT NULL,
+  `TRIGGER_STATE` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `TRIGGER_TYPE` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `START_TIME` bigint NOT NULL,
+  `END_TIME` bigint NULL DEFAULT NULL,
+  `CALENDAR_NAME` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `MISFIRE_INSTR` smallint NULL DEFAULT NULL,
+  `JOB_DATA` blob NULL,
+  PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+  INDEX `SCHED_NAME`(`SCHED_NAME` ASC, `JOB_NAME` ASC, `JOB_GROUP` ASC) USING BTREE,
+  CONSTRAINT `QRTZ_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for gen_datasource_conf
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_datasource_conf`;
+CREATE TABLE `gen_datasource_conf`  (
+  `id` bigint NOT NULL COMMENT '主键',
+  `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '别名',
+  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'jdbcurl',
+  `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
+  `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
+  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标记',
+  `ds_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据库类型',
+  `conf_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '配置类型',
+  `ds_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据库名称',
+  `instance` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '实例',
+  `port` int NULL DEFAULT NULL COMMENT '端口',
+  `host` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '主机',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '数据源表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for gen_field_type
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_field_type`;
+CREATE TABLE `gen_field_type`  (
+  `id` bigint NOT NULL COMMENT '主键',
+  `column_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字段类型',
+  `attr_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性类型',
+  `package_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性包名',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改人',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标记',
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE INDEX `column_type`(`column_type` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '字段类型管理' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for gen_group
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_group`;
+CREATE TABLE `gen_group`  (
+  `id` bigint NOT NULL,
+  `group_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组名称',
+  `group_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组描述',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建人',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改人',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标记',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '模板分组' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for gen_table
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_table`;
+CREATE TABLE `gen_table`  (
+  `id` bigint NOT NULL,
+  `table_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '表名',
+  `class_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '类名',
+  `db_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据库类型',
+  `table_comment` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '说明',
+  `author` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '作者',
+  `email` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
+  `package_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目包名',
+  `version` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目版本号',
+  `i18n` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '是否生成带有i18n 0 不带有 1带有',
+  `style` bigint NULL DEFAULT NULL COMMENT '代码风格',
+  `child_table_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '子表名称',
+  `main_field` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '主表关联键',
+  `child_field` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '子表关联键',
+  `generator_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '生成方式  0:zip压缩包   1:自定义目录',
+  `backend_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '后端生成路径',
+  `frontend_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '前端生成路径',
+  `module_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模块名',
+  `function_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '功能名',
+  `form_layout` tinyint NULL DEFAULT NULL COMMENT '表单布局  1:一列   2:两列',
+  `ds_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据源ID',
+  `baseclass_id` bigint NULL DEFAULT NULL COMMENT '基类ID',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE INDEX `table_name`(`table_name` ASC, `ds_name` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '代码生成表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for gen_table_column
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_table_column`;
+CREATE TABLE `gen_table_column`  (
+  `id` bigint NOT NULL,
+  `ds_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据源名称',
+  `table_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '表名称',
+  `field_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字段名称',
+  `field_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字段类型',
+  `field_comment` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字段说明',
+  `attr_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性名',
+  `attr_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性类型',
+  `package_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性包名',
+  `sort` int NULL DEFAULT NULL COMMENT '排序',
+  `auto_fill` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自动填充  DEFAULT、INSERT、UPDATE、INSERT_UPDATE',
+  `primary_pk` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '主键 0:否  1:是',
+  `base_field` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '基类字段 0:否  1:是',
+  `form_item` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '表单项 0:否  1:是',
+  `form_required` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '表单必填 0:否  1:是',
+  `form_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '表单类型',
+  `form_validator` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '表单效验',
+  `grid_item` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '列表项 0:否  1:是',
+  `grid_sort` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '列表排序 0:否  1:是',
+  `query_item` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '查询项 0:否  1:是',
+  `query_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '查询方式',
+  `query_form_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '查询表单类型',
+  `field_dict` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字典类型',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '代码生成表字段' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for gen_template
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_template`;
+CREATE TABLE `gen_template`  (
+  `id` bigint NOT NULL COMMENT '主键',
+  `template_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板名称',
+  `generator_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板路径',
+  `template_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板描述',
+  `template_code` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板代码',
+  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '删除标记',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '模板' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for gen_template_group
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_template_group`;
+CREATE TABLE `gen_template_group`  (
+  `group_id` bigint NOT NULL COMMENT '分组id',
+  `template_id` bigint NOT NULL COMMENT '模板id',
+  PRIMARY KEY (`group_id`, `template_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '模板分组关联表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for mkt_mgmt_apps
+-- ----------------------------
+DROP TABLE IF EXISTS `mkt_mgmt_apps`;
+CREATE TABLE `mkt_mgmt_apps`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `app_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '应用ID',
+  `app_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '应用名',
+  `app_img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用图标',
+  `app_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '下载链接',
+  `backup_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备用下载链接',
+  `domain_type` int NULL DEFAULT NULL COMMENT '应用类型',
+  `domain_limit` tinyint(1) NULL DEFAULT NULL COMMENT '域名限制',
+  `launch` tinyint(1) NULL DEFAULT NULL COMMENT '营销投放',
+  `trigger_rule` int NULL DEFAULT NULL COMMENT '触发规则',
+  `trigger_num` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '触发频率',
+  `status` tinyint(1) NULL DEFAULT NULL COMMENT '状态-0 启用-1 删除',
+  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE INDEX `uk_app_id`(`app_id` ASC) USING BTREE COMMENT 'appId索引'
+) ENGINE = InnoDB AUTO_INCREMENT = 49 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Table structure for mkt_mgmt_apps_domain
+-- ----------------------------
+DROP TABLE IF EXISTS `mkt_mgmt_apps_domain`;
+CREATE TABLE `mkt_mgmt_apps_domain`  (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `app_id` bigint NULL DEFAULT NULL COMMENT '关联的应用ID',
+  `source_type` tinyint(1) NOT NULL COMMENT '来源类型,1-来自分组,2-具体域名',
+  `group_id` bigint NULL DEFAULT NULL COMMENT '所属分组ID(source_type=1时必填)',
+  `domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '域名',
+  `config` tinyint(1) NULL DEFAULT NULL COMMENT '是否属于全局配置',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NOT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_app_id`(`app_id` ASC) USING BTREE,
+  INDEX `idx_group_id`(`group_id` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 611 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Table structure for mkt_mgmt_apps_ip
+-- ----------------------------
+DROP TABLE IF EXISTS `mkt_mgmt_apps_ip`;
+CREATE TABLE `mkt_mgmt_apps_ip`  (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `app_id` bigint NULL DEFAULT NULL COMMENT '关联的应用ID',
+  `ip_type` tinyint(1) NOT NULL COMMENT 'IP类型,1-白名单,2-黑名单',
+  `source_type` tinyint(1) NOT NULL COMMENT '来源类型,1-来自分组,2-具体IP',
+  `group_id` bigint NULL DEFAULT NULL COMMENT '所属分组ID(source_type=1时必填)',
+  `ip_mode` tinyint(1) NULL DEFAULT NULL COMMENT 'IP模式:1-单IP,2-IP段',
+  `start_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '起始IP地址',
+  `end_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束IP地址(IP段模式下使用)',
+  `config` tinyint(1) NULL DEFAULT NULL COMMENT '是否属于全局配置',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NOT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_app_id`(`app_id` ASC) USING BTREE,
+  INDEX `idx_ip_type`(`ip_type` ASC) USING BTREE,
+  INDEX `idx_group_id`(`group_id` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 531 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Table structure for mkt_mgmt_data
+-- ----------------------------
+DROP TABLE IF EXISTS `mkt_mgmt_data`;
+CREATE TABLE `mkt_mgmt_data`  (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip',
+  `domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '域名',
+  `fingerprint` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户指纹',
+  `utm_source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量的原始来源',
+  `utm_medium` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '推广渠道类型',
+  `utm_campaign` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '活动名称',
+  `utm_term` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '关键词',
+  `utm_content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '活动内容',
+  `referrer` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '跳转来源',
+  `timestamp` datetime NULL DEFAULT NULL COMMENT '时间戳',
+  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '埋点服务url地址',
+  `is_mobile` tinyint(1) NULL DEFAULT NULL COMMENT '是否是移动端',
+  `browser` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '浏览器类型',
+  `browser_version` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '浏览器版本',
+  `user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户代理信息',
+  `os_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统类型',
+  `os_version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统版本',
+  `app_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用ID',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1139 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Table structure for mkt_mgmt_group
+-- ----------------------------
+DROP TABLE IF EXISTS `mkt_mgmt_group`;
+CREATE TABLE `mkt_mgmt_group`  (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `group_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '分组名称',
+  `group_type` tinyint(1) NOT NULL COMMENT '分组类型,1-ip,2-域名',
+  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NOT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_group_type`(`group_type` ASC) USING BTREE COMMENT '按类型查询分组'
+) ENGINE = InnoDB AUTO_INCREMENT = 162 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Table structure for mkt_mgmt_group_domain
+-- ----------------------------
+DROP TABLE IF EXISTS `mkt_mgmt_group_domain`;
+CREATE TABLE `mkt_mgmt_group_domain`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `group_id` bigint NOT NULL COMMENT '所属分组ID',
+  `domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '域名',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NOT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_group_id`(`group_id` ASC) USING BTREE COMMENT '分组ID索引'
+) ENGINE = InnoDB AUTO_INCREMENT = 49 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Table structure for mkt_mgmt_group_ip
+-- ----------------------------
+DROP TABLE IF EXISTS `mkt_mgmt_group_ip`;
+CREATE TABLE `mkt_mgmt_group_ip`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `group_id` bigint NOT NULL COMMENT '所属分组ID',
+  `ip_mode` tinyint(1) NOT NULL COMMENT 'IP模式:1-单IP,2-IP段',
+  `start_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '起始IP地址',
+  `end_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束IP地址(IP段模式下使用)',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NOT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_group_id`(`group_id` ASC) USING BTREE COMMENT '分组ID索引'
+) ENGINE = InnoDB AUTO_INCREMENT = 62 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Table structure for mkt_stat_user_analysis
+-- ----------------------------
+DROP TABLE IF EXISTS `mkt_stat_user_analysis`;
+CREATE TABLE `mkt_stat_user_analysis`  (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `channel` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '渠道',
+  `version` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '版本',
+  `app_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用ID',
+  `new_user` int NULL DEFAULT NULL COMMENT '新增用户数量',
+  `active_user` int NULL DEFAULT NULL COMMENT '活跃用户数量',
+  `launch` int NULL DEFAULT NULL COMMENT '启动次数',
+  `stat_date` datetime NULL DEFAULT NULL COMMENT '统计时间',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 959 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for mkt_stat_user_retention
+-- ----------------------------
+DROP TABLE IF EXISTS `mkt_stat_user_retention`;
+CREATE TABLE `mkt_stat_user_retention`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `channel` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '渠道',
+  `version` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '版本',
+  `app_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '应用ID',
+  `retention` decimal(5, 4) NOT NULL COMMENT '留存率',
+  `stat_date` datetime NOT NULL COMMENT '统计时间',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 408 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_dept
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_dept`;
+CREATE TABLE `sys_dept`  (
+  `dept_id` bigint NOT NULL COMMENT '部门ID',
+  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名称',
+  `sort_order` int NOT NULL DEFAULT 0 COMMENT '排序',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  `parent_id` bigint NULL DEFAULT NULL COMMENT '父级部门ID',
+  PRIMARY KEY (`dept_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门管理' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_dict
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_dict`;
+CREATE TABLE `sys_dict`  (
+  `id` bigint NOT NULL COMMENT '编号',
+  `dict_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字典类型',
+  `description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注信息',
+  `system_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '系统标志',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `sys_dict_del_flag`(`del_flag` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '字典表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_dict_item
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_dict_item`;
+CREATE TABLE `sys_dict_item`  (
+  `id` bigint NOT NULL COMMENT '编号',
+  `dict_id` bigint NOT NULL COMMENT '字典ID',
+  `item_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字典项值',
+  `label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字典项名称',
+  `dict_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字典类型',
+  `description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '字典项描述',
+  `sort_order` int NOT NULL DEFAULT 0 COMMENT '排序(升序)',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注信息',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `sys_dict_value`(`item_value` ASC) USING BTREE,
+  INDEX `sys_dict_label`(`label` ASC) USING BTREE,
+  INDEX `sys_dict_item_del_flag`(`del_flag` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '字典项' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_file
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_file`;
+CREATE TABLE `sys_file`  (
+  `id` bigint NOT NULL COMMENT '编号',
+  `file_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件名',
+  `bucket_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件存储桶名称',
+  `original` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '原始文件名',
+  `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件类型',
+  `file_size` bigint NULL DEFAULT NULL COMMENT '文件大小',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '上传时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '文件管理表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_job
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_job`;
+CREATE TABLE `sys_job`  (
+  `job_id` bigint NOT NULL COMMENT '任务id',
+  `job_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务名称',
+  `job_group` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务组名',
+  `job_order` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '组内执行顺利,值越大执行优先级越高,最大值9,最小值1',
+  `job_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1' COMMENT '1、java类;2、spring bean名称;3、rest调用;4、jar调用;9其他',
+  `execute_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'job_type=3时,rest调用地址,仅支持rest get协议,需要增加String返回值,0成功,1失败;job_type=4时,jar路径;其它值为空',
+  `class_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'job_type=1时,类完整路径;job_type=2时,spring bean名称;其它值为空',
+  `method_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务方法',
+  `method_params_value` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '参数值',
+  `cron_expression` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'cron执行表达式',
+  `misfire_policy` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '3' COMMENT '错失执行策略(1错失周期立即执行 2错失周期执行一次 3下周期执行)',
+  `job_tenant_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '1、多租户任务;2、非多租户任务',
+  `job_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '状态(1、未发布;2、运行中;3、暂停;4、删除;)',
+  `job_execute_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '状态(0正常 1异常)',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
+  `start_time` timestamp NULL DEFAULT NULL COMMENT '初次执行时间',
+  `previous_time` timestamp NULL DEFAULT NULL COMMENT '上次执行时间',
+  `next_time` timestamp NULL DEFAULT NULL COMMENT '下次执行时间',
+  `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '备注信息',
+  PRIMARY KEY (`job_id`) USING BTREE,
+  UNIQUE INDEX `job_name_group_idx`(`job_name` ASC, `job_group` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '定时任务调度表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_job_log
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_job_log`;
+CREATE TABLE `sys_job_log`  (
+  `job_log_id` bigint NOT NULL COMMENT '任务日志ID',
+  `job_id` bigint NOT NULL COMMENT '任务id',
+  `job_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务名称',
+  `job_group` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务组名',
+  `job_order` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组内执行顺利,值越大执行优先级越高,最大值9,最小值1',
+  `job_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1' COMMENT '1、java类;2、spring bean名称;3、rest调用;4、jar调用;9其他',
+  `execute_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'job_type=3时,rest调用地址,仅支持post协议;job_type=4时,jar路径;其它值为空',
+  `class_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'job_type=1时,类完整路径;job_type=2时,spring bean名称;其它值为空',
+  `method_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务方法',
+  `method_params_value` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '参数值',
+  `cron_expression` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'cron执行表达式',
+  `job_message` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '日志信息',
+  `job_log_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '执行状态(0正常 1失败)',
+  `execute_time` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行时间',
+  `exception_info` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '异常信息',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`job_log_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '定时任务执行日志表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_log
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_log`;
+CREATE TABLE `sys_log`  (
+  `id` bigint NOT NULL COMMENT '编号',
+  `log_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '日志类型',
+  `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '日志标题',
+  `service_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '服务ID',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `remote_addr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '远程地址',
+  `user_agent` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户代理',
+  `request_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求URI',
+  `method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求方法',
+  `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求参数',
+  `time` bigint NULL DEFAULT NULL COMMENT '执行时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志',
+  `exception` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '异常信息',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `sys_log_request_uri`(`request_uri` ASC) USING BTREE,
+  INDEX `sys_log_type`(`log_type` ASC) USING BTREE,
+  INDEX `sys_log_create_date`(`create_time` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '日志表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_menu`;
+CREATE TABLE `sys_menu`  (
+  `menu_id` bigint NOT NULL COMMENT '菜单ID',
+  `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单名称',
+  `en_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '英文名称',
+  `permission` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限标识',
+  `path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '路由路径',
+  `parent_id` bigint NULL DEFAULT NULL COMMENT '父菜单ID',
+  `icon` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单图标',
+  `visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '是否可见,0隐藏,1显示',
+  `sort_order` int NULL DEFAULT 1 COMMENT '排序值,越小越靠前',
+  `keep_alive` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '是否缓存,0否,1是',
+  `embedded` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '是否内嵌,0否,1是',
+  `menu_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '菜单类型,0目录,1菜单,2按钮',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志,0未删除,1已删除',
+  PRIMARY KEY (`menu_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_oauth_client_details
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_oauth_client_details`;
+CREATE TABLE `sys_oauth_client_details`  (
+  `id` bigint NOT NULL COMMENT 'ID',
+  `client_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客户端ID',
+  `resource_ids` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '资源ID集合',
+  `client_secret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端秘钥',
+  `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '授权范围',
+  `authorized_grant_types` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '授权类型',
+  `web_server_redirect_uri` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '回调地址',
+  `authorities` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限集合',
+  `access_token_validity` int NULL DEFAULT NULL COMMENT '访问令牌有效期(秒)',
+  `refresh_token_validity` int NULL DEFAULT NULL COMMENT '刷新令牌有效期(秒)',
+  `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '附加信息',
+  `autoapprove` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自动授权',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标记,0未删除,1已删除',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '终端信息表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_post
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_post`;
+CREATE TABLE `sys_post`  (
+  `post_id` bigint NOT NULL COMMENT '岗位ID',
+  `post_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '岗位编码',
+  `post_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '岗位名称',
+  `post_sort` int NOT NULL COMMENT '岗位排序',
+  `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '岗位描述',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '是否删除  -1:已删除  0:正常',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '创建人',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '更新人',
+  PRIMARY KEY (`post_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '岗位信息表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_public_param
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_public_param`;
+CREATE TABLE `sys_public_param`  (
+  `public_id` bigint NOT NULL COMMENT '编号',
+  `public_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
+  `public_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '键',
+  `public_value` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '值',
+  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '状态,0禁用,1启用',
+  `validate_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '校验码',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `public_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '类型,0未知,1系统,2业务',
+  `system_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '系统标识,0非系统,1系统',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标记,0未删除,1已删除',
+  PRIMARY KEY (`public_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公共参数配置表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_role
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role`;
+CREATE TABLE `sys_role`  (
+  `role_id` bigint NOT NULL COMMENT '角色ID',
+  `role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色名称',
+  `role_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色编码',
+  `role_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色描述',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标记,0未删除,1已删除',
+  PRIMARY KEY (`role_id`) USING BTREE,
+  INDEX `role_idx1_role_code`(`role_code` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统角色表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_role_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role_menu`;
+CREATE TABLE `sys_role_menu`  (
+  `role_id` bigint NOT NULL COMMENT '角色ID',
+  `menu_id` bigint NOT NULL COMMENT '菜单ID',
+  PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色菜单表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_user
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user`;
+CREATE TABLE `sys_user`  (
+  `user_id` bigint NOT NULL COMMENT '用户ID',
+  `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
+  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
+  `salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '盐值',
+  `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话号码',
+  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',
+  `nickname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称',
+  `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
+  `email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱地址',
+  `dept_id` bigint NULL DEFAULT NULL COMMENT '所属部门ID',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',
+  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+  `lock_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '锁定标记,0未锁定,9已锁定',
+  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标记,0未删除,1已删除',
+  `wx_openid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信登录openId',
+  `mini_openid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '小程序openId',
+  `qq_openid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'QQ openId',
+  `gitee_login` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '码云标识',
+  `osc_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开源中国标识',
+  PRIMARY KEY (`user_id`) USING BTREE,
+  INDEX `user_wx_openid`(`wx_openid` ASC) USING BTREE,
+  INDEX `user_qq_openid`(`qq_openid` ASC) USING BTREE,
+  INDEX `user_idx1_username`(`username` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for sys_user_post
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user_post`;
+CREATE TABLE `sys_user_post`  (
+  `user_id` bigint NOT NULL COMMENT '用户ID',
+  `post_id` bigint NOT NULL COMMENT '岗位ID',
+  PRIMARY KEY (`user_id`, `post_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户与岗位关联表' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Table structure for sys_user_role
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user_role`;
+CREATE TABLE `sys_user_role`  (
+  `user_id` bigint NOT NULL COMMENT '用户ID',
+  `role_id` bigint NOT NULL COMMENT '角色ID',
+  PRIMARY KEY (`user_id`, `role_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户角色表' ROW_FORMAT = Dynamic;
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 9 - 4
pig-auth/src/main/resources/application.yml

@@ -1,5 +1,5 @@
 server:
-  port: 3000
+  port: 13000
 
 spring:
   application:
@@ -9,13 +9,18 @@ spring:
       username: @nacos.username@
       password: @nacos.password@
       discovery:
-        server-addr: ${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}
+        namespace: @nacos.namespace@
+        server-addr: @nacos.address@
+        group: DEFAULT_GROUP
       config:
+        namespace: ${spring.cloud.nacos.discovery.namespace}
         server-addr: ${spring.cloud.nacos.discovery.server-addr}
+        group: DEFAULT_GROUP
+        refresh-enabled: true
   config:
     import:
-      - nacos:application[email protected]@.yml
-      - nacos:${spring.application.name}[email protected]@.yml
+      - nacos:application.yml
+      - nacos:${spring.application.name}.yml
 
 
 

+ 16 - 0
pig-common/pig-common-bom/pom.xml

@@ -38,6 +38,7 @@
         <common.io.version>2.18.0</common.io.version>
         <spring.checkstyle.plugin>0.0.43</spring.checkstyle.plugin>
         <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
+		<ip2region.version>2.6.6</ip2region.version>
     </properties>
 
     <!-- 定义全局jar版本,模块使用需要再次引入但不用写版本号-->
@@ -98,6 +99,16 @@
                 <artifactId>pig-upms-api</artifactId>
                 <version>${revision}</version>
             </dependency>
+			<dependency>
+				<groupId>com.pig4cloud</groupId>
+				<artifactId>pig-marketing-api</artifactId>
+				<version>${revision}</version>
+			</dependency>
+			<dependency>
+				<groupId>com.pig4cloud</groupId>
+				<artifactId>pig-statistics-api</artifactId>
+				<version>${revision}</version>
+			</dependency>
             <dependency>
                 <groupId>com.mysql</groupId>
                 <artifactId>mysql-connector-j</artifactId>
@@ -188,6 +199,11 @@
                     </exclusion>
                 </exclusions>
             </dependency>
+			<dependency>
+				<groupId>org.lionsoul</groupId>
+				<artifactId>ip2region</artifactId>
+				<version>${ip2region.version}</version>
+			</dependency>
         </dependencies>
     </dependencyManagement>
 

+ 6 - 1
pig-common/pig-common-core/pom.xml

@@ -65,5 +65,10 @@
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-commons</artifactId>
         </dependency>
-    </dependencies>
+		<!-- IP地区 -->
+		<dependency>
+			<groupId>org.lionsoul</groupId>
+			<artifactId>ip2region</artifactId>
+		</dependency>
+	</dependencies>
 </project>

+ 10 - 0
pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/constant/ServiceNameConstants.java

@@ -32,4 +32,14 @@ public interface ServiceNameConstants {
 	 */
 	String UPMS_SERVICE = "pig-upms-biz";
 
+	/**
+	 * 营销模块
+	 */
+	String MARKETING_SERVICE = "pig-marketing-biz";
+
+	/**
+	 * 统计模块
+	 */
+	String STATISTICS_SERVICE = "pig-statistics-biz";
+
 }

+ 60 - 0
pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/exception/BusinessException.java

@@ -0,0 +1,60 @@
+package com.pig4cloud.pig.common.core.exception;
+
+
+import com.pig4cloud.pig.common.core.util.MsgUtils;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-28
+ * @description: 业务异常类
+ */
+@NoArgsConstructor
+public class BusinessException extends CheckedException {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 通过错误码构建异常(支持i18n)
+	 * @param code 错误码(对应messages配置)
+	 */
+	public BusinessException(String code) {
+//		super(MsgUtils.getMessage(code));
+		super(code);
+	}
+
+	/**
+	 * 通过错误码和参数构建异常(支持i18n)
+	 * @param code 错误码
+	 * @param args 格式化参数
+	 */
+	public BusinessException(String code, Object... args) {
+		super(MsgUtils.getMessage(code, args));
+	}
+
+	/**
+	 * 直接传入错误信息
+	 * @param message 错误信息
+	 */
+	public BusinessException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * 带 cause 的异常
+	 * @param cause 原始异常
+	 */
+	public BusinessException(Throwable cause) {
+		super(cause);
+	}
+
+	/**
+	 * 完整构造器
+	 */
+	public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+}

+ 248 - 0
pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/ip/IPUtils.java

@@ -0,0 +1,248 @@
+package com.pig4cloud.pig.common.core.util.ip;
+
+import cn.hutool.core.util.StrUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.log4j.Log4j2;
+import org.lionsoul.ip2region.xdb.Searcher;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Log4j2
+public class IPUtils {
+
+	// 内网IP段正则
+	private static final String INTRANET_IP_REGEX = "(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|" +
+			"(172\\.((1[6-9])|(2\\d)|(3[01]))\\.\\d{1,3}\\.\\d{1,3})|" +
+			"(192\\.168\\.\\d{1,3}\\.\\d{1,3})|" +
+			"(127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})";
+	private static final Pattern INTRANET_PATTERN = Pattern.compile(INTRANET_IP_REGEX);
+
+	// IP地址合法性校验正则
+	private static final String IP_REGEX = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\." +
+			"(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\." +
+			"(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\." +
+			"(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
+	private static final Pattern IP_PATTERN = Pattern.compile(IP_REGEX);
+
+	private static Searcher searcher;
+
+	static {
+		initIpSearcher();
+	}
+
+	/**
+	 * 初始化IP地址索引
+	 */
+	private static void initIpSearcher() {
+		try {
+			// 基于内存查询-从资源文件加载xdb数据库
+			String dbPath = IPUtils.class.getClassLoader().getResource("ip/ip2region.xdb").getPath();
+			byte[] cBuff = Searcher.loadContentFromFile(dbPath);
+			searcher = Searcher.newWithBuffer(cBuff);
+			log.info("ip2region初始化成功");
+		} catch (Exception e) {
+			log.error("ip2region初始化失败", e);
+			searcher = null;
+		}
+	}
+
+	/**
+	 * 获取IP地址
+	 *
+	 * @param request request
+	 * @return ipAddress
+	 */
+	public static String getIpAddress(HttpServletRequest request) {
+		String ip = request.getHeader("x-forwarded-for");
+		if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+			ip = request.getHeader("Proxy-Client-IP");
+		}
+		if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+			ip = request.getHeader("WL-Proxy-Client-IP");
+		}
+		if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+			ip = request.getHeader("HTTP_CLIENT_IP");
+		}
+		if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+			ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+		}
+		if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+			ip = request.getRemoteAddr();
+		}
+		// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+		if (ip != null && ip.length() > 15) {
+			if (ip.indexOf(",") > 0) {
+				ip = ip.substring(0, ip.indexOf(","));
+			}
+		}
+		if ("127.0.0.1".equals(ip)) {
+			// 获取本机真正的ip地址
+			try {
+				ip = InetAddress.getLocalHost().getHostAddress();
+			} catch (UnknownHostException e) {
+				log.error(e.getMessage(), e);
+			}
+		}
+		return ip;
+	}
+
+	/**
+	 * 判断是否内网IP
+	 * @param ip ip
+	 * @return boolean
+	 */
+	public static boolean isIntranetIp(String ip) {
+		if (StrUtil.isBlank(ip)) {
+			return false;
+		}
+		Matcher matcher = INTRANET_PATTERN.matcher(ip);
+		return matcher.matches();
+	}
+
+	/**
+	 * 检查IP是否合法
+	 * @param ip ip
+	 * @return boolean
+	 */
+
+	public static boolean isValidIp(String ip) {
+		if (StrUtil.isBlank(ip)) {
+			return false;
+		}
+		Matcher matcher = IP_PATTERN.matcher(ip);
+		return matcher.matches();
+	}
+
+	/**
+	 * 比较两个IP地址大小(将IP转为Long比较)
+	 * @return true:endIp > startIp
+	 */
+	public static boolean isEndIpGreater(String startIp, String endIp) {
+		if (!isValidIp(startIp) || !isValidIp(endIp)) {
+			return false;
+		}
+		return ipToLong(startIp) < ipToLong(endIp);
+	}
+
+	/**
+	 * IP地址转Long(便于比较大小)
+	 */
+	private static long ipToLong(String ip) {
+		String[] parts = ip.split("\\.");
+		long result = 0;
+		for (int i = 0; i < 4; i++) {
+			result |= Long.parseLong(parts[i]) << (24 - i * 8);
+		}
+		return result;
+	}
+
+	/**
+	 * 获取IP归属地(国家-省份)
+	 * @param ip ip
+	 * @return 国家-省份
+	 */
+	public static String getIpRegion(String ip) {
+		// 处理多IP情况,取第一个
+		if (StrUtil.isNotBlank(ip) && ip.contains(",")) {
+			String[] split = ip.split(",");
+			ip = split[0].trim();
+		}
+
+		// 检查IP合法性
+		if (!isValidIp(ip)) {
+			return "未知";
+		}
+
+		// 检查是否为内网IP
+		if (isIntranetIp(ip)) {
+			return "内网";
+		}
+
+		// 如果搜索器未初始化成功,返回未知
+		if (searcher == null) {
+			log.warn("ip2region搜索器未初始化,无法查询IP归属地");
+			return "未知";
+		}
+
+		try {
+			String region = searcher.search(ip);
+			if (StrUtil.isNotBlank(region)) {
+				List<String> arr = StrUtil.split(region, '|');
+				// 确保数组有足够的元素
+				if (arr.size() >= 3) {
+					String country = "0".equals(arr.get(0)) ? "未知" : arr.get(0);
+					String province = "0".equals(arr.get(2)) ? "未知" : arr.get(2);
+
+					// 处理国外省份可能为0的情况
+					if ("未知".equals(province) && !"中国".equals(country)) {
+						return country;
+					}
+
+					return country + "-" + province;
+				}
+			}
+		} catch (Exception e) {
+			log.warn("查询IP[{}]归属地出错", ip, e);
+		}
+
+		return "未知";
+	}
+
+	/**
+	 * 获取外网IP
+	 * @return ip
+	 */
+	public static String getPublicIp() {
+		String ip = "";
+		StringBuilder inputLine = new StringBuilder();
+		String read;
+		BufferedReader in = null;
+		try {
+			URL url = new URL("http://ip.chinaz.com");
+			HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+			in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), StandardCharsets.UTF_8));
+			while ((read = in.readLine()) != null) {
+				inputLine.append(read).append("\r\n");
+			}
+		} catch (IOException e) {
+			log.error("获取公网IP失败:{}",e.getMessage());
+		} finally {
+			if (in != null) {
+				try {
+					in.close();
+				} catch (IOException e) {
+					log.error("获取公网IP失败:{}",e.getMessage());
+				}
+			}
+		}
+		Pattern p = Pattern.compile("<dd class=\"fz24\">(.*?)</dd>");
+		Matcher m = p.matcher(inputLine.toString());
+		if (m.find()) {
+			ip = m.group(1);
+		}
+		log.info("获取的公网ip为[{}]", ip);
+		return ip;
+	}
+
+	public static void main(String[] args) {
+		// 测试案例
+		System.out.println(getIpRegion("39.144.24.215"));   // 中国-北京
+		System.out.println(getIpRegion("103.26.94.206"));  // 中国-香港
+		System.out.println(getIpRegion("153.35.52.159"));  // 日本-未知
+		System.out.println(getIpRegion("8.8.8.8"));        // 美国-加利福尼亚州
+		System.out.println(getIpRegion("192.168.1.1"));    // 全球(内网)
+		System.out.println(getIpRegion("127.0.0.1"));      // 全球(内网)
+		System.out.println(getIpRegion("invalid.ip"));     // 未知(非法IP)
+		System.out.println(getIpRegion(""));               // 未知(空IP)
+	}
+}

BIN
pig-common/pig-common-core/src/main/resources/ip/ip2region.xdb


+ 156 - 4
pig-common/pig-common-feign/src/main/java/com/pig4cloud/pig/common/feign/sentinel/handle/GlobalBizExceptionHandler.java

@@ -17,23 +17,34 @@
 package com.pig4cloud.pig.common.feign.sentinel.handle;
 
 import com.alibaba.csp.sentinel.Tracer;
+import com.fasterxml.jackson.databind.JsonMappingException;
 import com.pig4cloud.pig.common.core.util.R;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.TypeMismatchException;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.context.MessageSourceResolvable;
 import org.springframework.core.annotation.Order;
 import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.core.SpringSecurityMessageSource;
 import org.springframework.util.Assert;
 import org.springframework.validation.BindException;
 import org.springframework.validation.FieldError;
+import org.springframework.validation.ObjectError;
 import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.HandlerMethodValidationException;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
 import org.springframework.web.servlet.resource.NoResourceFoundException;
 
 import java.util.List;
+import java.util.Set;
 
 /**
  * 全局业务异常处理器,结合Sentinel处理系统异常
@@ -50,6 +61,88 @@ import java.util.List;
 @ConditionalOnExpression("!'${security.oauth2.client.clientId}'.isEmpty()")
 public class GlobalBizExceptionHandler {
 
+
+	/**
+	 * 处理控制器方法参数校验异常(如@RequestParam/@PathVariable的校验失败)
+	 */
+	@ExceptionHandler(HandlerMethodValidationException.class)
+	@ResponseStatus(HttpStatus.BAD_REQUEST)
+	public R handleHandlerMethodValidationException(HandlerMethodValidationException e) {
+		log.error("控制器方法参数校验失败 ex={}", e.getMessage(), e);
+
+		List<? extends MessageSourceResolvable> errors = e.getAllErrors();
+		if (!errors.isEmpty()) {
+			MessageSourceResolvable firstError = errors.get(0);
+			String errorMsg = firstError.getDefaultMessage();
+			if (errorMsg == null && firstError.getCodes() != null && firstError.getCodes().length > 0) {
+				errorMsg = firstError.getCodes()[0];
+			}
+			return R.failed("参数校验失败:" + (errorMsg != null ? errorMsg : "未知错误,请联系管理员"));
+		}
+
+		// 兜底提示
+		return R.failed("参数校验失败,请检查请求参数");
+	}
+
+
+	/**
+	 * 处理HTTP消息解析异常(优先捕获,包括JSON类型不匹配)
+	 */
+	@ExceptionHandler(HttpMessageNotReadableException.class)
+	@ResponseStatus(HttpStatus.BAD_REQUEST)
+	public R handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
+		log.error("HTTP消息解析异常 ex={}", e.getMessage(), e);
+
+		// 检查根原因是否为JSON映射异常(包含字段信息)
+		Throwable rootCause = e.getRootCause();
+		if (rootCause instanceof JsonMappingException jsonMappingEx) {
+			// 提取出错的字段路径
+			String fieldPath = jsonMappingEx.getPath().stream()
+					.map(JsonMappingException.Reference::getFieldName)
+					.reduce((a, b) -> a + "." + b)
+					.orElse("");
+
+			return R.failed("参数类型错误:字段 [" + fieldPath + "] 类型不匹配,请检查");
+		}
+
+		// 其他解析错误(如JSON格式错误)
+		return R.failed("请求参数格式错误,请检查JSON格式是否正确");
+	}
+
+	/**
+	 * 处理类型转换相关的异常(表单/路径参数)
+	 * 包括:方法参数类型不匹配、绑定参数类型错误等
+	 */
+	@ExceptionHandler({
+			MethodArgumentTypeMismatchException.class,
+			TypeMismatchException.class
+	})
+	@ResponseStatus(HttpStatus.BAD_REQUEST)
+	public R handleTypeMismatchException(Exception e) {
+		log.error("参数类型错误 ex={}", e.getMessage(), e);
+
+		// 提取具体字段名
+		String fieldName = "";
+		if (e instanceof MethodArgumentTypeMismatchException methodEx) {
+			fieldName = methodEx.getName();
+		} else if (e instanceof TypeMismatchException typeEx && typeEx.getPropertyName() != null) {
+			fieldName = typeEx.getPropertyName();
+		}
+
+		return R.failed("参数类型错误:字段 [" + fieldName + "] 类型不匹配,请检查");
+	}
+
+	/**
+	 * 处理缺少参数的异常
+	 */
+	@ExceptionHandler(MissingServletRequestParameterException.class)
+	@ResponseStatus(HttpStatus.BAD_REQUEST)
+	public R handleMissingParamException(MissingServletRequestParameterException e) {
+		log.error("缺少请求参数 ex={}", e.getMessage(), e);
+		return R.failed("缺少必要参数:[" + e.getParameterName() + "],类型:" + e.getParameterType());
+	}
+
+
 	/**
 	 * 全局异常.
 	 * @param e the e
@@ -104,9 +197,36 @@ public class GlobalBizExceptionHandler {
 	@ExceptionHandler({ MethodArgumentNotValidException.class })
 	@ResponseStatus(HttpStatus.BAD_REQUEST)
 	public R handleBodyValidException(MethodArgumentNotValidException exception) {
+		// 优先处理类级别的校验错误(如@IPValidation)
+		List<ObjectError> globalErrors = exception.getBindingResult().getGlobalErrors();
+		if (!globalErrors.isEmpty()) {
+			String errorMsg = globalErrors.get(0).getDefaultMessage();
+			log.error("类级参数校验异常, ex = {}", errorMsg);
+			return R.failed(errorMsg);
+		}
+
+		// 处理字段级别的校验错误
 		List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
-		log.warn("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage());
-		return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
+		if (!fieldErrors.isEmpty()) {
+			FieldError firstError = fieldErrors.get(0);
+			String fieldName = firstError.getField(); // 错误字段名
+			String errorCode = firstError.getCode();  // 错误代码(关键:区分错误类型)
+			String defaultMsg = firstError.getDefaultMessage(); // 注解中定义的message
+
+			// 1. 如果是类型转换错误(code为"typeMismatch")
+			if ("typeMismatch".equals(errorCode)) {
+				log.error("字段[{}]类型转换异常: {}", fieldName, defaultMsg);
+				return R.failed("参数类型错误:字段 [" + fieldName + "] 类型不匹配,请检查");
+			}
+
+			// 2. 普通校验错误(如@NotNull、@Size等,使用注解中定义的message)
+			log.error("字段[{}]校验异常: {}", fieldName, defaultMsg);
+			return R.failed(defaultMsg);
+		}
+
+		// 兜底错误信息
+		log.error("参数校验异常,但未获取到具体错误信息");
+		return R.failed("参数校验失败");
 	}
 
 	/**
@@ -117,9 +237,26 @@ public class GlobalBizExceptionHandler {
 	@ExceptionHandler({ BindException.class })
 	@ResponseStatus(HttpStatus.BAD_REQUEST)
 	public R bindExceptionHandler(BindException exception) {
+		// 优先获取字段错误信息
 		List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
-		log.warn("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage());
-		return R.failed(fieldErrors.get(0).getDefaultMessage());
+		if (!fieldErrors.isEmpty()) {
+			String errorMsg = fieldErrors.get(0).getDefaultMessage();
+			log.warn("参数绑定异常,ex = {}", errorMsg);
+			return R.failed("参数类型错误:" + errorMsg); // 明确提示类型错误
+		}
+
+		// 处理全局错误
+		List<ObjectError> globalErrors = exception.getBindingResult().getGlobalErrors();
+		if (!globalErrors.isEmpty()) {
+			String errorMsg = globalErrors.get(0).getDefaultMessage();
+			log.warn("参数绑定全局异常,ex = {}", errorMsg);
+			return R.failed(errorMsg);
+		}
+		// 兜底提示
+		return R.failed("参数绑定错误,请检查参数类型");
+//		List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
+//		log.warn("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage());
+//		return R.failed(fieldErrors.get(0).getDefaultMessage());
 	}
 
 	/**
@@ -137,4 +274,19 @@ public class GlobalBizExceptionHandler {
 		return R.failed(exception.getMessage());
 	}
 
+	// 新增:处理自定义校验注解异常
+	@ExceptionHandler(ConstraintViolationException.class)
+	@ResponseStatus(HttpStatus.BAD_REQUEST)
+	public R handleConstraintViolationException(ConstraintViolationException exception) {
+		Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
+		// 避免空集合导致的类似错误
+		if (violations.isEmpty()) {
+			return R.failed("参数校验失败,但未获取到具体错误信息");
+		}
+		// 获取第一个错误信息(自定义校验器中设置的message)
+		String errorMsg = violations.iterator().next().getMessage();
+		log.warn("自定义校验异常: {}", errorMsg);
+		return R.failed(errorMsg);
+	}
+
 }

+ 0 - 26
pig-gateway/src/main/java/com/pig4cloud/pig/gateway/config/CorsConfig.java

@@ -1,26 +0,0 @@
-package com.pig4cloud.pig.gateway.config;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.cors.CorsConfiguration;
-import org.springframework.web.cors.reactive.CorsWebFilter;
-import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
-import org.springframework.web.util.pattern.PathPatternParser;
-
-@Configuration
-public class CorsConfig {
-
-    @Bean
-    public CorsWebFilter corsFilter() {
-        CorsConfiguration config = new CorsConfiguration();
-        config.addAllowedOriginPattern("*"); // 允许所有域名进行跨域调用
-        config.addAllowedHeader("*"); // 允许任何请求头
-        config.addAllowedMethod("*"); // 允许任何方法(POST、GET等)
-        config.setAllowCredentials(true); // 允许携带凭证
-
-        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
-        source.registerCorsConfiguration("/**", config); // 对所有接口都有效
-
-        return new CorsWebFilter(source);
-    }
-}

+ 8 - 3
pig-gateway/src/main/resources/application.yml

@@ -9,16 +9,21 @@ spring:
       username: @nacos.username@
       password: @nacos.password@
       discovery:
-        server-addr: ${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}
+        namespace: @nacos.namespace@
+        server-addr: @nacos.address@
+        group: DEFAULT_GROUP
         watch:
           enabled: true
         watch-delay: 1000
       config:
+        namespace: ${spring.cloud.nacos.discovery.namespace}
         server-addr: ${spring.cloud.nacos.discovery.server-addr}
+        group: DEFAULT_GROUP
+        refresh-enabled: true
   config:
     import:
-      - optional:nacos:application[email protected]@.yml
-      - optional:nacos:${spring.application.name}[email protected]@.yml
+      - optional:nacos:application.yml
+      - optional:nacos:${spring.application.name}.yml
 
 
 

+ 61 - 0
pig-marketing/pig-marketing-api/pom.xml

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>com.pig4cloud</groupId>
+		<artifactId>pig-marketing</artifactId>
+		<version>${revision}</version>
+	</parent>
+
+	<artifactId>pig-marketing-api</artifactId>
+	<packaging>jar</packaging>
+
+	<description>pig 营销管理公共api模块</description>
+
+
+	<dependencies>
+		<!--core 工具类-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-core</artifactId>
+		</dependency>
+		<!--feign 注解依赖-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-feign</artifactId>
+		</dependency>
+		<!--mybatis 依赖-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-mybatis</artifactId>
+		</dependency>
+		<!-- excel 导入导出 -->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-excel</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>javax.servlet-api</artifactId>
+			<version>4.0.1</version>
+			<scope>compile</scope>
+		</dependency>
+	</dependencies>
+</project>

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

@@ -0,0 +1,17 @@
+package com.pig4cloud.pig.marketing.api;
+
+
+/**
+ * @author: lwh
+ * @date: 2025-07-31
+ * @description: 营销配置常量类
+ */
+
+public interface MarketingConfigConstants {
+
+	String TRIGGER_MODE = "TRIGGER_MODE";
+	String TRIGGER_RULE = "TRIGGER_RULE";
+	String TRIGGER_NUM = "TRIGGER_NUM";
+	String PROMPT_MSG = "PROMPT_MSG";
+	String URL = "URL";
+}

+ 80 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/BatchUpdateMarketingAppsDTO.java

@@ -0,0 +1,80 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 批量修改应用列表入参
+ */
+@Data
+@Schema(description = "批量修改应用列表入参")
+public class BatchUpdateMarketingAppsDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@Schema(description = "id")
+	@NotNull(message = "id不能为空")
+	private Long id;
+
+	/**
+	 * 域名限制
+	 */
+	@Schema(description = "域名限制")
+	private Boolean domainLimit;
+
+	/**
+	 * 营销投放
+	 */
+	@Schema(description = "营销投放")
+	private Boolean launch;
+
+	/**
+	 * 触发规则
+	 */
+	@Schema(description = "触发规则")
+	private Integer triggerRule;
+
+	/**
+	 * 触发频率
+	 */
+	@Schema(description = "触发频率")
+	private String triggerNum;
+
+	/**
+	 * 备注
+	 */
+	@Schema(description = "备注")
+	@Size(max = 255, message = "备注长度不能超过255")
+	private String remark;
+
+	/**
+	 * 删除的分组IP id列表
+	 */
+	@Schema(description = "删除的分组IP id列表")
+	List<Long> delIps;
+
+	/**
+	 * 删除的分组域名id
+	 */
+	@Schema(description = "删除的分组域名id")
+	List<Long> delDomains;
+
+	@Valid
+	@Schema(description = "ip列表")
+	private List<MarketingAppsIpDTO> ips;
+
+	@Valid
+	@Schema(description = "域名列表")
+	private List<MarketingAppsDomainDTO> domains;
+}

+ 32 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/MarketingAppsDomainDTO.java

@@ -0,0 +1,32 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import com.pig4cloud.pig.marketing.api.dto.common.BaseDomainDTO;
+import com.pig4cloud.pig.marketing.api.valid.DomainValidation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-24
+ * @description: 应用域名列表
+ */
+@Data
+@DomainValidation
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "应用域名列表")
+public class MarketingAppsDomainDTO extends BaseDomainDTO {
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 是否修改
+	 */
+	@Schema(description = "是否修改")
+	private Boolean modify = false;
+}

+ 33 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/MarketingAppsIpDTO.java

@@ -0,0 +1,33 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import com.pig4cloud.pig.marketing.api.dto.common.BaseIpDTO;
+import com.pig4cloud.pig.marketing.api.valid.IPValidation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+
+/**
+ * @author: lwh
+ * @date: 2025-07-24
+ * @description: 应用ip列表入参
+ */
+@Data
+@IPValidation
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "应用ip列表入参")
+public class MarketingAppsIpDTO extends BaseIpDTO {
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 是否修改
+	 */
+	@Schema(description = "是否修改")
+	private Boolean modify = false;
+}

+ 38 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/ModMarketingAppsDomainDTO.java

@@ -0,0 +1,38 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-27
+ * @description: 修改应用域名列表入参
+ */
+@Data
+@Schema(description = "修改应用域名列表入参")
+public class ModMarketingAppsDomainDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@Schema(description = "id")
+	@NotNull(message = "id不能为空")
+	private Long id;
+
+	/**
+	 * 删除的分组域名id
+	 */
+	@Schema(description = "删除的分组域名id")
+	List<Long> delDomains;
+
+	@Valid
+	@Schema(description = "域名列表")
+	private List<MarketingAppsDomainDTO> domains;
+}

+ 38 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/ModMarketingAppsIpDTO.java

@@ -0,0 +1,38 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-27
+ * @description: 修改应用ip列表入参
+ */
+@Data
+@Schema(description = "修改应用ip列表入参")
+public class ModMarketingAppsIpDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@Schema(description = "id")
+	@NotNull(message = "id不能为空")
+	private Long id;
+
+	/**
+	 * 删除的分组IP id列表
+	 */
+	@Schema(description = "删除的分组IP id列表")
+	List<Long> delIps;
+
+	@Valid
+	@Schema(description = "ip列表")
+	private List<MarketingAppsIpDTO> ips;
+}

+ 57 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/PageMarketingAppsDTO.java

@@ -0,0 +1,57 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 分页查询应用列表DTO
+ */
+@Data
+@Schema(description = "分页查询应用列表入参")
+public class PageMarketingAppsDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 每页显示条数,默认 10
+	 */
+	@Schema(description = "每页显示条数")
+	private long size = 10;
+
+	/**
+	 * 当前页
+	 */
+	@Schema(description = "当前页")
+	private long current = 1;
+
+	/**
+	 * 应用ID
+	 */
+	@Schema(description = "应用ID")
+	private String appId;
+
+	/**
+	 * 应用名称
+	 */
+	@Schema(description = "应用名称")
+	private String appName;
+
+	/**
+	 * 应用类型
+	 */
+	@Schema(description = "应用类型")
+	private String domainType;
+
+	/**
+	 * 备注
+	 */
+	@Schema(description = "备注")
+	private String remark;
+}

+ 50 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/PageStatAppMktDataDTO.java

@@ -0,0 +1,50 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-30
+ * @description: 分页统计应用营销数据入参
+ */
+
+@Data
+@Schema(description = "分页统计应用营销数据入参")
+public class PageStatAppMktDataDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 每页显示条数,默认 10
+	 */
+	@Schema(description = "每页显示条数")
+	private long size = 10;
+
+	/**
+	 * 当前页
+	 */
+	@Schema(description = "当前页")
+	private long current = 1;
+
+	/**
+	 * 应用ID
+	 */
+	@Schema(description = "应用ID")
+	@NotBlank(message = "应用ID不能为空")
+	private String appId;
+
+	/**
+	 * 时间范围
+	 */
+	@Schema(description = "时间范围")
+	private LocalDateTime[] timeRange;
+
+}

+ 55 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/SetMarketingAppsDTO.java

@@ -0,0 +1,55 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 设置应用信息入参
+ */
+
+@Data
+@Schema(description = "设置应用信息入参")
+public class SetMarketingAppsDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	@NotNull(message = "不能为空")
+	private Long id;
+
+	/**
+	 * 应用类型
+	 */
+	@Schema(description = "应用类型,0-Android,1-IOS")
+	private Integer domainType;
+
+	/**
+	 * 域名限制
+	 */
+	@Schema(description = "域名限制")
+	Boolean domainLimit;
+
+	/**
+	 * 营销投放
+	 */
+	@Schema(description = "营销投放")
+	Boolean launch;
+
+	/**
+	 * 触发规则
+	 */
+	@Schema(description = "触发规则,1-仅一次,2-多次")
+	Integer triggerRule;
+
+}

+ 30 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/SetMarketingAppsStatusDTO.java

@@ -0,0 +1,30 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-24
+ * @description: 设置应用状态入参
+ */
+@Data
+@Schema(description = "设置应用状态入参")
+public class SetMarketingAppsStatusDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@Schema(description = "id")
+	@NotNull(message = "id不能为空")
+	private Long id;
+
+	@Schema(description = "状态,true-启用,false-拉黑")
+	@NotNull(message = "状态不能为空")
+	private Boolean status;
+}

+ 62 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/app/UpdateMarketingAppsDTO.java

@@ -0,0 +1,62 @@
+package com.pig4cloud.pig.marketing.api.dto.app;
+
+
+import com.pig4cloud.pig.marketing.api.vo.app.MarketingAppsDomainVO;
+import com.pig4cloud.pig.marketing.api.vo.app.MarketingAppsIpVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 修改应用IP、域名
+ */
+@Data
+@Schema(description = "修改应用IP、域名")
+public class UpdateMarketingAppsDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	@NotNull(message = "id不能为空")
+	Long id;
+
+	/**
+	 * 域名限制
+	 */
+	@Schema(description = "域名限制")
+	Boolean domainLimit;
+
+	/**
+	 * 营销投放
+	 */
+	@Schema(description = "营销投放")
+	Boolean launch;
+
+	/**
+	 * 备注
+	 */
+	@Schema(description = "备注")
+	String remark;
+
+	/**
+	 * 应用ip列表
+	 */
+	@Schema(description = "应用ip列表")
+	List<MarketingAppsIpVO> ips;
+
+	/**
+	 * 应用域名列表
+	 */
+	@Schema(description = "应用域名列表")
+	List<MarketingAppsDomainVO> domains;
+}

+ 39 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/common/BaseDomainDTO.java

@@ -0,0 +1,39 @@
+package com.pig4cloud.pig.marketing.api.dto.common;
+
+
+import com.pig4cloud.pig.marketing.api.valid.ValueRange;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-29
+ * @description: 域名相关DTO的公共父类
+ */
+@Data
+public class BaseDomainDTO implements Serializable {
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@NotNull(message = "来源类型不能为空")
+	@ValueRange(values = {1, 2}, message = "来源类型不合法(1-来自分组,2-具体域名)")
+	@Schema(description = "来源类型,1-来自分组,2-具体域名")
+	private Integer sourceType;
+
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	@Schema(description = "分组名称")
+	@Size(max = 255, message = "分组名称长度不能超过255")
+	private String groupName;
+
+	@Schema(description = "域名")
+	@Size(max = 255, message = "域名长度不能超过255")
+	private String domain;
+
+}

+ 49 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/common/BaseIpDTO.java

@@ -0,0 +1,49 @@
+package com.pig4cloud.pig.marketing.api.dto.common;
+
+
+import com.pig4cloud.pig.marketing.api.valid.ValueRange;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-29
+ * @description: IP相关DTO的公共父类
+ */
+@Data
+public class BaseIpDTO implements Serializable {
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@NotNull(message = "ipType不能为空")
+	@ValueRange(values = {1, 2}, message = "IP类型值不合法(1-白名单,2-黑名单)")
+	@Schema(description = "ip类型,1-白名单,2-黑名单")
+	private Integer ipType;
+
+	@NotNull(message = "sourceType不能为空")
+	@Schema(description = "来源类型值不合法(1-来自分组,2-具体IP)")
+	@ValueRange(values = {1, 2}, message = "来源类型只能是1或2")
+	private Integer sourceType;
+
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	@Schema(description = "分组名称")
+	@Size(max = 255, message = "分组名称长度不能超过255")
+	private String groupName;
+
+	@Schema(description = "ip模式,1-单IP,2-IP段")
+	@ValueRange(values = {1, 2}, message = "IP模式值不合法(1-单IP,2-IP段)")
+	private Integer ipMode;
+
+	@Schema(description = "起始IP")
+	private String startIp;
+
+	@Schema(description = "结束IP")
+	private String endIp;
+}

+ 21 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/AddMarketingConfigDomainListDTO.java

@@ -0,0 +1,21 @@
+package com.pig4cloud.pig.marketing.api.dto.config;
+
+
+import com.pig4cloud.pig.marketing.api.dto.common.BaseDomainDTO;
+import com.pig4cloud.pig.marketing.api.valid.DomainValidation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-24
+ * @description: 添加域名入参
+ */
+@Data
+@DomainValidation
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "添加域名入参")
+public class AddMarketingConfigDomainListDTO extends BaseDomainDTO {
+
+}

+ 22 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/AddMarketingConfigIpListDTO.java

@@ -0,0 +1,22 @@
+package com.pig4cloud.pig.marketing.api.dto.config;
+
+
+import com.pig4cloud.pig.marketing.api.dto.common.BaseIpDTO;
+import com.pig4cloud.pig.marketing.api.valid.IPValidation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+
+/**
+ * @author: lwh
+ * @date: 2025-07-24
+ * @description: 添加IP集合入参
+ */
+@Data
+@IPValidation
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "添加IP集合入参")
+public class AddMarketingConfigIpListDTO extends BaseIpDTO {
+
+}

+ 39 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/AddMarketingGroupDTO.java

@@ -0,0 +1,39 @@
+package com.pig4cloud.pig.marketing.api.dto.config;
+
+
+import com.pig4cloud.pig.marketing.api.valid.ValueRange;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 添加营销分组入参
+ */
+@Data
+@Schema(description = "添加营销分组入参")
+public class AddMarketingGroupDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@Schema(description = "分组名称", example = "default")
+	@Size(max = 255, message = "分组名称长度不能超过255")
+	@NotEmpty(message = "分组名称不能为空")
+	private String groupName;
+
+	@Schema(description = "分组类型,1-ip分组,2-域名分组",example = "1")
+	@ValueRange(values = {1, 2}, message = "分组类型值不合法(1-ip分组,2-域名分组)")
+	@NotNull(message = "分组类型不能为空")
+	private Integer groupType;
+
+	@Schema(description = "备注")
+	@Size(max = 255, message = "备注长度不能超过255")
+	private String remark;
+}

+ 40 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/DelMarketingGroupDTO.java

@@ -0,0 +1,40 @@
+package com.pig4cloud.pig.marketing.api.dto.config;
+
+
+import com.pig4cloud.pig.marketing.api.valid.ValueRange;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 删除营销分组入参
+ */
+@Data
+@Schema(description = "删除营销分组入参")
+public class DelMarketingGroupDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 分组类型
+	 */
+	@NotNull(message = "分组类型不能为空")
+	@Schema(description = "分组类型,1-ip分组,2-域名分组",example = "1")
+	@ValueRange(values = {1, 2}, message = "分组类型值不合法(1-ip分组,2-域名分组)")
+	private Integer groupType;
+
+	/**
+	 * 分组id列表
+	 */
+	@Schema(description = "分组id列表")
+	@NotEmpty(message = "分组id列表不能为空")
+	private List<Long> ids;
+}

+ 45 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/MarketingGroupDomainDTO.java

@@ -0,0 +1,45 @@
+package com.pig4cloud.pig.marketing.api.dto.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 营销分组域名DTO
+ */
+@Data
+@Schema(description = "营销分组域名入参")
+public class MarketingGroupDomainDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名")
+	@NotBlank(message = "域名不能为空")
+	@Size(max = 255, message = "域名长度不能超过255")
+	private String domain;
+
+	/**
+	 * 是否修改
+	 */
+	@Schema(description = "是否修改")
+	private Boolean modify = false;
+}

+ 56 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/MarketingGroupIPDTO.java

@@ -0,0 +1,56 @@
+package com.pig4cloud.pig.marketing.api.dto.config;
+
+
+import com.pig4cloud.pig.marketing.api.valid.ValueRange;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销分组ipDTO
+ */
+@Data
+@Schema(description = "营销分组ip入参")
+public class MarketingGroupIPDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * ip模式,1-单IP,2-IP段
+	 */
+	@NotNull(message = "ip模式不能为空")
+	@Schema(description = "ip模式,1-单IP,2-IP段")
+	@ValueRange(values = {1, 2}, message = "IP模式值不合法(1-单IP,2-IP段)")
+	private Integer ipMode;
+
+	/**
+	 * 起始IP
+	 */
+	@Schema(description = "起始IP")
+	private String startIp;
+
+	/**
+	 * 结束IP
+	 */
+	@Schema(description = "结束IP")
+	private String endIp;
+
+	/**
+	 * 是否修改
+	 */
+	@Schema(description = "是否修改")
+	private Boolean modify = false;
+	
+}

+ 61 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/ModMarketingGroupDomainDTO.java

@@ -0,0 +1,61 @@
+package com.pig4cloud.pig.marketing.api.dto.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 修改营销分组域名入参
+ */
+@Data
+@Schema(description = "修改营销分组域名入参")
+public class ModMarketingGroupDomainDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 分组id
+	 */
+	@Schema(description = "分组id")
+	@NotNull(message = "分组id不能为空")
+	private Long id;
+
+	/**
+	 * 分组名称
+	 */
+	@Schema(description = "分组名称")
+	@Size(max = 255, message = "分组名称长度不能超过255")
+	@NotBlank(message = "分组名称不能为空")
+	private String groupName;
+
+	/**
+	 * 备注
+	 */
+	@Schema(description = "备注")
+	@Size(max = 255, message = "备注长度不能超过255")
+	private String remark;
+
+	/**
+	 * 删除的分组域名id
+	 */
+	@Schema(description = "删除的分组域名id")
+	List<Long> delList;
+
+	/**
+	 * 分组域名
+	 */
+	@Valid
+	@Schema(description = "分组域名")
+	private List<MarketingGroupDomainDTO> domains;
+}

+ 61 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/config/ModMarketingGroupIPDTO.java

@@ -0,0 +1,61 @@
+package com.pig4cloud.pig.marketing.api.dto.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 修改营销分组ip入参
+ */
+@Data
+@Schema(description = "修改营销分组ip入参")
+public class ModMarketingGroupIPDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 分组id
+	 */
+	@Schema(description = "分组id")
+	@NotNull(message = "分组id不能为空")
+	private Long id;
+
+	/**
+	 * 分组名称
+	 */
+	@Schema(description = "分组名称")
+	@Size(max = 255, message = "分组名称长度不能超过255")
+	@NotBlank(message = "分组名称不能为空")
+	private String groupName;
+
+	/**
+	 * 备注
+	 */
+	@Schema(description = "备注")
+	@Size(max = 255, message = "备注长度不能超过255")
+	private String remark;
+
+	/**
+	 * 删除的分组IP id列表
+	 */
+	@Schema(description = "删除的分组IP id列表")
+	List<Long> delList;
+
+	/**
+	 * 分组IP
+	 */
+	@Valid
+	@Schema(description = "分组IP")
+	private List<MarketingGroupIPDTO> ips;
+}

+ 121 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/data/MarketingDataReportDTO.java

@@ -0,0 +1,121 @@
+package com.pig4cloud.pig.marketing.api.dto.data;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * @author: lwh
+ * @date: 2025-07-16
+ * @description: 上报营销数据DTO
+ */
+@Data
+@Schema(description = "上报营销数据入参")
+public class MarketingDataReportDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+//
+//	/**
+//	 * id
+//	 */
+//	@Schema(description = "id")
+//	private Long id;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名")
+	private String domain;
+
+	/**
+	 * 浏览器指纹
+	 */
+	@Schema(description = "浏览器指纹")
+	private String fingerprint;
+
+	/**
+	 * 流量的原始来源
+	 */
+	@Schema(description = "流量的原始来源")
+	private String utmSource;
+
+	/**
+	 * 推广渠道类型
+	 */
+	@Schema(description = "推广渠道类型")
+	private String utmMedium;
+
+	/**
+	 * 活动名称
+	 */
+	@Schema(description = "活动名称")
+	private String utmCampaign;
+
+	/**
+	 * 关键词
+	 */
+	@Schema(description = "关键词")
+	private String utmTerm;
+
+	/**
+	 * 活动内容
+	 */
+	@Schema(description = "活动内容")
+	private String utmContent;
+
+	/**
+	 * 跳转来源
+	 */
+	@Schema(description = "跳转来源")
+	private String referrer;
+
+	/**
+	 * 时间戳
+	 */
+	@Schema(description = "时间戳")
+	private String timestamp;
+
+	/**
+	 * 埋点服务url地址
+	 */
+	@Schema(description = "埋点服务url地址")
+	private String url;
+
+	/**
+	 * 是否移动端
+	 */
+	@Schema(description = "是否移动端")
+	private Boolean isMobile;
+
+	/**
+	 * 浏览器
+	 */
+	@Schema(description = "浏览器")
+	private String browser;
+
+	@Schema(description = "浏览器版本")
+	private String browserVersion;
+
+	/**
+	 * 用户代理信息
+	 */
+	@Schema(description = "用户代理信息")
+	private String userAgent;
+
+	/**
+	 * 操作系统类型
+	 */
+	@Schema(description = "操作系统类型")
+	private String osType;
+
+	/**
+	 * 操作系统版本
+	 */
+	@Schema(description = "操作系统版本")
+	private String osVersion;
+}

+ 44 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/data/PageFirstLevelDataDTO.java

@@ -0,0 +1,44 @@
+package com.pig4cloud.pig.marketing.api.dto.data;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-16
+ * @description: 分页统计一级营销数据VO
+ */
+@Data
+public class PageFirstLevelDataDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 每页显示条数,默认 10
+	 */
+	@Schema(description = "每页显示条数", example = "10")
+	private long size = 10;
+
+	/**
+	 * 当前页
+	 */
+	@Schema(description = "当前页", example = "1")
+	private long current = 1;
+
+	/**
+	 * ip
+	 */
+	@Schema(description = "ip", example = "192.168.1.1")
+	private String ip;
+
+	/**
+	 * appId
+	 */
+	@Schema(description = "应用ID", example = "6ZUoEdzwnu6U1b5o5y7C0h5")
+	private String appId;
+}

+ 40 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/dto/data/PageSecondLevelDataDTO.java

@@ -0,0 +1,40 @@
+package com.pig4cloud.pig.marketing.api.dto.data;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-16
+ * @description: 分页统计一级营销数据VO
+ */
+@Data
+public class PageSecondLevelDataDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 每页显示条数,默认 10
+	 */
+	@Schema(description = "每页显示条数")
+	private long size = 10;
+
+	/**
+	 * 当前页
+	 */
+	@Schema(description = "当前页")
+	private long current = 1;
+
+	/**
+	 * ip
+	 */
+	@NotEmpty(message = "ip不能为空")
+	@Schema(description = "ip")
+	private String ip;
+}

+ 141 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingApps.java

@@ -0,0 +1,141 @@
+package com.pig4cloud.pig.marketing.api.entity;
+
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 营销应用
+ */
+
+@Data
+@TableName("mkt_mgmt_apps")
+@Schema(description = "营销应用")
+@EqualsAndHashCode(callSuper = true)
+public class MarketingApps extends Model<MarketingApps> {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@TableId(value = "id")
+	private Long id;
+
+	/**
+	 * 应用ID
+	 */
+	@Schema(description = "应用ID")
+	private String appId;
+
+	/**
+	 * 应用名称
+	 */
+	@Schema(description = "应用名称")
+	private String appName;
+
+	/**
+	 * 应用图标
+	 */
+	@Schema(description = "应用图标")
+	private String appImg;
+
+	/**
+	 * 应用链接
+	 */
+	@Schema(description = "应用链接")
+	private String appUrl;
+
+	/**
+	 * 备用链接
+	 */
+	@Schema(description = "备用链接")
+	private String backupUrl;
+
+	/**
+	 * 应用类型
+	 */
+	@Schema(description = "应用类型")
+	private Integer domainType;
+
+	/**
+	 * 域名限制
+	 */
+	@Schema(description = "域名限制")
+	private Boolean domainLimit;
+
+	/**
+	 * 营销投放
+	 */
+	@Schema(description = "营销投放")
+	private Boolean launch;
+
+	/**
+	 * 触发规则
+	 */
+	@Schema(description = "触发规则")
+	private Integer triggerRule;
+
+	/**
+	 * 触发频率
+	 */
+	@Schema(description = "触发频率")
+	private String triggerNum;
+
+	/**
+	 * 状态,false-拉黑,true-启用
+	 */
+	@Schema(description = "状态,false-拉黑,true-启用")
+	private Boolean status;
+
+	/**
+	 * 备注
+	 */
+	@Schema(description = "备注")
+	private String remark;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+}

+ 98 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingAppsDomain.java

@@ -0,0 +1,98 @@
+package com.pig4cloud.pig.marketing.api.entity;
+
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销应用域名
+ */
+@Data
+@TableName("mkt_mgmt_apps_domain")
+@Schema(description = "营销应用域名")
+@EqualsAndHashCode(callSuper = true)
+public class MarketingAppsDomain extends Model<MarketingAppsDomain> {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@TableId(value = "id")
+	private Long id;
+
+	/**
+	 * 应用ID
+	 */
+	@Schema(description = "应用ID")
+	private Long appId;
+
+	/**
+	 * 来源类型
+	 */
+	@Schema(description = "来源类型,1-来自分组,2-具体域名")
+	private Integer sourceType;
+
+	/**
+	 * 分组ID
+	 */
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名")
+	private String domain;
+
+	/**
+	 * 是否属于全局配置
+	 */
+	@Schema(description = "是否属于全局配置")
+	private Boolean config;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+}

+ 117 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingAppsIp.java

@@ -0,0 +1,117 @@
+package com.pig4cloud.pig.marketing.api.entity;
+
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销应用IP
+ */
+@Data
+@TableName("mkt_mgmt_apps_ip")
+@Schema(description = "营销应用IP")
+@EqualsAndHashCode(callSuper = true)
+public class MarketingAppsIp extends Model<MarketingAppsIp> {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@TableId(value = "id")
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 应用ID
+	 */
+	@Schema(description = "应用ID")
+	private Long appId;
+
+	/**
+	 * ip类型
+	 */
+	@Schema(description = "ip类型,1-白名单,2-黑名单")
+	private Integer ipType;
+
+	/**
+	 * 所属类型
+	 */
+	@Schema(description = "所属类型,1-来自分组,2-具体IP")
+	private Integer sourceType;
+
+	/**
+	 * 分组ID
+	 */
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	/**
+	 * ip模式
+	 */
+	@Schema(description = "ip模式,1-单IP,2-IP段")
+	private Integer ipMode;
+
+	/**
+	 * 起始IP
+	 */
+	@Schema(description = "起始IP")
+	private String startIp;
+
+	/**
+	 * 结束IP
+	 */
+	@Schema(description = "结束IP")
+	private String endIp;
+
+	/**
+	 * 是否属于全局配置
+	 */
+	@Schema(description = "是否属于全局配置")
+	private Boolean config;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+}

+ 87 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingConfigGroup.java

@@ -0,0 +1,87 @@
+package com.pig4cloud.pig.marketing.api.entity;
+
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销配置分组
+ */
+@Data
+@TableName("mkt_mgmt_group")
+@Schema(description = "营销配置分组")
+@EqualsAndHashCode(callSuper = true)
+public class MarketingConfigGroup extends Model<MarketingConfigGroup> {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@TableId(value = "id")
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 分组名称
+	 */
+	@Schema(description = "分组名称")
+	private String groupName;
+
+	/**
+	 * 分组类型,1-ip,2-域名
+	 */
+	@Schema(description = "分组类型,1-ip,2-域名")
+	private Integer groupType;
+
+	/**
+	 * 备注
+	 */
+	@Schema(description = "备注")
+	private String remark;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+}

+ 173 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingData.java

@@ -0,0 +1,173 @@
+package com.pig4cloud.pig.marketing.api.entity;
+
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-16
+ * @description: 营销数据统计
+ */
+@Data
+@TableName("mkt_mgmt_data")
+@Schema(description = "营销数据统计")
+@EqualsAndHashCode(callSuper = true)
+public class MarketingData extends Model<MarketingData> {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@TableId(value = "id")
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * ip
+	 */
+	@Schema(description = "ip")
+	private String ip;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名")
+	private String domain;
+
+	/**
+	 * 浏览器指纹
+	 */
+	@Schema(description = "浏览器指纹")
+	private String fingerprint;
+
+	/**
+	 * 流量的原始来源
+	 */
+	@Schema(description = "流量的原始来源")
+	private String utmSource;
+
+	/**
+	 * 推广渠道类型
+	 */
+	@Schema(description = "推广渠道类型")
+	private String utmMedium;
+
+	/**
+	 * 活动名称
+	 */
+	@Schema(description = "活动名称")
+	private String utmCampaign;
+
+	/**
+	 * 关键词
+	 */
+	@Schema(description = "关键词")
+	private String utmTerm;
+
+	/**
+	 * 活动内容
+	 */
+	@Schema(description = "活动内容")
+	private String utmContent;
+
+	/**
+	 * 跳转来源
+	 */
+	@Schema(description = "跳转来源")
+	private String referrer;
+
+	/**
+	 * 时间戳
+	 */
+	@Schema(description = "时间戳")
+	private LocalDateTime timestamp;
+
+	/**
+	 * 埋点服务url地址
+	 */
+	@Schema(description = "埋点服务url地址")
+	private String url;
+
+	/**
+	 * 是否移动端
+	 */
+	@Schema(description = "是否移动端")
+	private Boolean isMobile;
+
+	/**
+	 * 浏览器
+	 */
+	@Schema(description = "浏览器")
+	private String browser;
+
+	@Schema(description = "浏览器版本")
+	private String browserVersion;
+	/**
+	 * 用户代理信息
+	 */
+	@Schema(description = "用户代理信息")
+	private String userAgent;
+
+	/**
+	 * 操作系统类型
+	 */
+	@Schema(description = "操作系统类型")
+	private String osType;
+
+	/**
+	 * 操作系统版本
+	 */
+	@Schema(description = "操作系统版本")
+	private String osVersion;
+
+	/**
+	 * appId
+	 */
+	@Schema(description = "appId")
+	private String appId;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+}

+ 81 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingGroupDomain.java

@@ -0,0 +1,81 @@
+package com.pig4cloud.pig.marketing.api.entity;
+
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销分组域名
+ */
+@Data
+@TableName("mkt_mgmt_group_domain")
+@Schema(description = "营销分组域名")
+@EqualsAndHashCode(callSuper = true)
+public class MarketingGroupDomain extends Model<MarketingGroupDomain> {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@TableId(value = "id")
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 分组ID
+	 */
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名")
+	private String domain;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+}

+ 93 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/entity/MarketingGroupIp.java

@@ -0,0 +1,93 @@
+package com.pig4cloud.pig.marketing.api.entity;
+
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: TODO
+ */
+@Data
+@TableName("mkt_mgmt_group_ip")
+@Schema(description = "营销分组ip")
+@EqualsAndHashCode(callSuper = true)
+public class MarketingGroupIp extends Model<MarketingGroupIp> {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@TableId(value = "id")
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 分组ID
+	 */
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	/**
+	 * ip模式,1-单IP,2-IP段
+	 */
+	@Schema(description = "ip模式,1-单IP,2-IP段")
+	private Integer ipMode;
+
+	/**
+	 * 起始IP
+	 */
+	@Schema(description = "起始IP")
+	private String startIp;
+
+	/**
+	 * 结束IP
+	 */
+	@Schema(description = "结束IP")
+	private String endIp;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,102 @@
+package com.pig4cloud.pig.marketing.api.util;
+
+
+import java.util.regex.Pattern;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-29
+ * @description: 域名校验工具类
+ */
+
+public class DomainValidationUtil {
+	/**
+	 * 严格的域名正则表达式
+	 * 规则:
+	 * 1. 由字母、数字、连字符组成
+	 * 2. 不能以连字符开头或结尾
+	 * 3. 各部分长度不超过63个字符
+	 * 4. 总长度不超过255个字符
+	 * 5. 至少包含一个点(.)
+	 * 6. 顶级域名至少2个字符
+	 */
+	private static final Pattern DOMAIN_PATTERN = Pattern.compile(
+			"^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)\\." +  // 主域名部分
+					"+(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$"     // 顶级域名部分
+	);
+
+	/**
+	 * 验证域名格式是否合法
+	 * @param domain 待验证的域名
+	 * @return 合法返回true,否则返回false
+	 */
+	public static boolean isValidDomain(String domain) {
+		// 空值检查
+		if (domain == null || domain.trim().isEmpty()) {
+			return false;
+		}
+
+		String trimDomain = domain.trim().toLowerCase();
+
+		// 长度限制检查
+		if (trimDomain.length() > 255) {
+			return false;
+		}
+
+		// 正则匹配检查
+		if (!DOMAIN_PATTERN.matcher(trimDomain).matches()) {
+			return false;
+		}
+
+		// 检查每个部分的长度(通过点分割)
+		String[] parts = trimDomain.split("\\.");
+		for (String part : parts) {
+			if (part.length() < 1 || part.length() > 63) {
+				return false;
+			}
+		}
+
+		// 检查顶级域名至少有2个字符
+		String tld = parts[parts.length - 1];
+		if (tld.length() < 2) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * 验证域名并返回具体错误信息
+	 * @param domain 待验证的域名
+	 * @return 验证通过返回null,否则返回错误信息
+	 */
+	public static String getValidationMessage(String domain) {
+		if (domain == null || domain.trim().isEmpty()) {
+			return "域名不能为空";
+		}
+
+		String trimDomain = domain.trim().toLowerCase();
+
+		if (trimDomain.length() > 255) {
+			return "域名长度不能超过255个字符";
+		}
+
+		if (!DOMAIN_PATTERN.matcher(trimDomain).matches()) {
+			return "域名格式不正确,只能包含字母、数字和连字符,且不能以连字符开头或结尾";
+		}
+
+		String[] parts = trimDomain.split("\\.");
+		for (String part : parts) {
+			if (part.isEmpty() || part.length() > 63) {
+				return "域名各部分长度必须在1-63个字符之间";
+			}
+		}
+
+		String tld = parts[parts.length - 1];
+		if (tld.length() < 2) {
+			return "顶级域名长度不能少于2个字符";
+		}
+
+		return null;
+	}
+}

+ 71 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/FingerprintCombinerUtil.java

@@ -0,0 +1,71 @@
+package com.pig4cloud.pig.marketing.api.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * 指纹组合工具类
+ */
+public class FingerprintCombinerUtil {
+
+    /**
+     * 组合前端指纹和后端指纹
+     * @param frontFingerprint 前端传入的指纹(可为null)
+     * @param backendFingerprint 后端生成的指纹(可为null)
+     * @return 组合后的唯一指纹(32位MD5或64位SHA-256)
+     */
+    public static String combine(String frontFingerprint, String backendFingerprint) {
+        // 处理空值(空字符串替换为"null",避免拼接后差异丢失)
+        String front = (frontFingerprint == null || frontFingerprint.trim().isEmpty()) 
+            ? "null" : frontFingerprint.trim();
+        String backend = (backendFingerprint == null || backendFingerprint.trim().isEmpty()) 
+            ? "null" : backendFingerprint.trim();
+        
+        // 固定顺序拼接(前端+分隔符+后端)
+        String combined = front + "|" + backend;
+        
+        // 生成SHA-256哈希(推荐,比MD5更安全)
+        return generateSha256(combined);
+//		return generateMd5(combined);
+    }
+
+    /**
+     * 生成SHA-256哈希
+     */
+    private static String generateSha256(String input) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+            
+            // 转换为16进制字符串
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : hash) {
+                String hex = String.format("%02x", b);
+                hexString.append(hex);
+            }
+            return hexString.toString();
+        } catch (NoSuchAlgorithmException e) {
+            // 异常时降级为MD5(理论上SHA-256是JDK标配,此处为容错)
+            return generateMd5(input);
+        }
+    }
+
+    /**
+     * 生成MD5哈希(降级方案)
+     */
+    private static String generateMd5(String input) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("MD5");
+            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+            
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : hash) {
+                hexString.append(String.format("%02x", b));
+            }
+            return hexString.toString();
+        } catch (NoSuchAlgorithmException e) {
+			return input.length() >= 32 ? input.substring(0, 32) : input;
+        }
+    }
+}

+ 281 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/JA4GeneratorUtil.java

@@ -0,0 +1,281 @@
+package com.pig4cloud.pig.marketing.api.util;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.net.ssl.SSLSession;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * JA4指纹生成器(整合TLS会话获取功能)
+ * 负责HTTPS请求的JA4指纹生成,内部包含从请求中提取SSL会话的逻辑
+ */
+@Slf4j
+public class JA4GeneratorUtil {
+
+	// 密码套件映射表(可根据实际需求扩展)
+	private static final Map<String, String> CIPHER_SUITE_MAPPINGS = new HashMap<>() {{
+		put("TLS_AES_128_GCM_SHA256", "a128gcm");
+		put("TLS_AES_256_GCM_SHA384", "a256gcm");
+		put("TLS_CHACHA20_POLY1305_SHA256", "chacha20");
+		put("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "e128gcm");
+		put("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "r128gcm");
+		put("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "e256gcm");
+		put("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "r256gcm");
+		put("TLS_RSA_WITH_AES_128_GCM_SHA256", "rsa128gcm");
+		put("TLS_RSA_WITH_AES_256_GCM_SHA384", "rsa256gcm");
+	}};
+
+	// 扩展类型映射(简化版)
+	private static final Map<Integer, String> EXTENSION_MAPPINGS = new HashMap<>() {{
+		put(0, "sni");           // 服务器名称指示
+		put(10, "ec_points");    // 椭圆曲线点格式
+		put(16, "alpn");         // 应用层协议协商
+		put(18, "ec_points");    // 椭圆曲线
+		put(21, "status_req");   // OCSP状态请求
+		put(43, "ec_points");    // 加密套件
+		put(44, "ec_points");    // 压缩方法
+		put(51, "renegotiate");  // 重新协商
+		put(65281, "grease");    // GREASE
+	}};
+
+	/**
+	 * 从HttpServletRequest中获取SSLSession
+	 * 整合原TlsUtils的功能
+	 */
+	public SSLSession getSslSession(HttpServletRequest request) {
+		if (request == null) {
+			return null;
+		}
+
+		try {
+			// 尝试直接从请求属性获取(兼容Tomcat等容器)
+			Object sslSession = request.getAttribute("javax.servlet.request.ssl_session");
+			if (sslSession instanceof SSLSession) {
+				return (SSLSession) sslSession;
+			}
+
+			// 反射方式获取(兼容不同容器)
+			Method getSessionMethod = request.getClass().getMethod("getSession");
+			Object session = getSessionMethod.invoke(request);
+			if (session instanceof SSLSession) {
+				return (SSLSession) session;
+			}
+
+			// 尝试获取SSL会话的其他方法
+			Method getSslSessionMethod = request.getClass().getMethod("getSslSession");
+			Object result = getSslSessionMethod.invoke(request);
+			if (result instanceof SSLSession) {
+				return (SSLSession) result;
+			}
+		} catch (Exception e) {
+			// 获取失败,可能不是HTTPS请求或容器不支持
+			log.error("Failed to get SSL session", e);
+		}
+
+		return null;
+	}
+
+	/**
+	 * 生成JA4指纹
+	 * @param request HTTP请求对象(需为HTTPS请求)
+	 * @return JA4指纹字符串,非HTTPS请求或生成失败时返回null
+	 */
+	public String generateJA4(HttpServletRequest request) {
+		try {
+			SSLSession sslSession = getSslSession(request);
+			if (sslSession == null) {
+				return null; // 非HTTPS请求,无法生成指纹
+			}
+			return generateJA4(sslSession);
+		} catch (Exception e) {
+			log.error("Failed to generate JA4 fingerprint:{}",e.getMessage());
+			return null;
+		}
+	}
+
+	/**
+	 * 生成JA4指纹
+	 * @param sslSession SSL会话对象
+	 * @return JA4指纹字符串
+	 */
+	public String generateJA4(SSLSession sslSession) {
+		try {
+			// 1. 提取TLS版本
+			String tlsVersion = getNormalizedTlsVersion(sslSession.getProtocol());
+
+			// 2. 提取密码套件
+			String cipherSuite = getNormalizedCipherSuite(sslSession.getCipherSuite());
+
+			// 3. 提取扩展信息(简化实现,实际需解析握手包)
+			String extensions = getNormalizedExtensions(sslSession);
+
+			// 4. 组合生成JA4指纹
+			String rawJA4 = String.format("%s,%s,%s", tlsVersion, cipherSuite, extensions);
+
+			// 5. 计算哈希值作为最终指纹
+			return computeMD5(rawJA4);
+		} catch (Exception e) {
+			log.error("生成JA4指纹失败:{}", e.getMessage());
+			return null;
+		}
+	}
+
+	/**
+	 * 规范化TLS版本
+	 */
+	private String getNormalizedTlsVersion(String protocol) {
+		if (protocol == null) {
+			return "unknown";
+		}
+		return switch (protocol) {
+			case "TLSv1.0" -> "1.0";
+			case "TLSv1.1" -> "1.1";
+			case "TLSv1.2" -> "1.2";
+			case "TLSv1.3" -> "1.3";
+			default -> protocol.replace("TLSv", "").replace("SSLv", "");
+		};
+	}
+
+	/**
+	 * 规范化密码套件
+	 */
+	private String getNormalizedCipherSuite(String cipherSuite) {
+		if (cipherSuite == null) {
+			return "unknown";
+		}
+		return CIPHER_SUITE_MAPPINGS.getOrDefault(cipherSuite, hashUnknownValue(cipherSuite));
+	}
+
+	/**
+	 * 规范化TLS扩展信息(基于实际扩展类型提取)
+	 */
+	private String getNormalizedExtensions(SSLSession sslSession) {
+		List<String> extensions = new ArrayList<>();
+
+		try {
+			// 1. 尝试从SSLSession中提取扩展信息(不同JDK实现可能有差异)
+			// 反射获取底层TLS握手信息(以SunJDK的SSLSessionImpl为例)
+			Class<?> sslSessionClass = sslSession.getClass();
+
+			// 2. 提取SNI(服务器名称指示,扩展类型0)
+			if (hasSNIExtension(sslSession, sslSessionClass)) {
+				extensions.add("sni");
+			}
+
+			// 3. 提取ALPN(应用层协议协商,扩展类型16)
+			String alpnProtocol = getALPNProtocol(sslSession, sslSessionClass);
+			if (alpnProtocol != null && !alpnProtocol.isEmpty()) {
+				extensions.add("alpn");
+			}
+
+			// 4. 提取椭圆曲线扩展(EC Points,扩展类型10)
+			if (hasECExtension(sslSession, sslSessionClass)) {
+				extensions.add("ec_points");
+			}
+
+			// 5. 提取GREASE扩展(用于测试兼容性,扩展类型0x0a0a等)
+			if (hasGreaseExtension(sslSession, sslSessionClass)) {
+				extensions.add("grease");
+			}
+
+		} catch (Exception e) {
+			// 提取失败时返回默认扩展组合(不影响主流程)
+			return "sni,alpn,ec_points";
+		}
+
+		// 若未提取到任何扩展,返回默认值
+		return extensions.isEmpty() ? "sni,alpn,ec_points" : String.join(",", extensions);
+	}
+
+	/**
+	 * 判断是否存在SNI扩展(服务器名称指示)
+	 */
+	private boolean hasSNIExtension(SSLSession sslSession, Class<?> sslSessionClass) throws Exception {
+		// SunJDK中,SSLSessionImpl的handshakeSession可能包含扩展信息
+		Field handshakeSessionField = sslSessionClass.getDeclaredField("handshakeSession");
+		handshakeSessionField.setAccessible(true);
+		Object handshakeSession = handshakeSessionField.get(sslSession);
+
+		// 从握手信息中获取SNI主机名(若存在则说明有SNI扩展)
+		Method getPeerHostMethod = handshakeSession.getClass().getMethod("getPeerHost");
+		String peerHost = (String) getPeerHostMethod.invoke(handshakeSession);
+		return peerHost != null && !peerHost.isEmpty();
+	}
+
+	/**
+	 * 获取ALPN协商的协议(如http/1.1、h2等)
+	 */
+	private String getALPNProtocol(SSLSession sslSession, Class<?> sslSessionClass) throws Exception {
+		// 从SSLSession中提取ALPN结果(不同JDK实现方法不同)
+		try {
+			Method getApplicationProtocolMethod = sslSessionClass.getMethod("getApplicationProtocol");
+			return (String) getApplicationProtocolMethod.invoke(sslSession);
+		} catch (NoSuchMethodException e) {
+			// 旧版本JDK可能没有该方法,返回null
+			return null;
+		}
+	}
+
+	/**
+	 * 判断是否存在椭圆曲线扩展
+	 */
+	private boolean hasECExtension(SSLSession sslSession, Class<?> sslSessionClass) throws Exception {
+		// 反射获取TLS握手过程中的椭圆曲线参数
+		Field cipherSuiteField = sslSessionClass.getDeclaredField("cipherSuite");
+		cipherSuiteField.setAccessible(true);
+		String cipherSuite = (String) cipherSuiteField.get(sslSession);
+
+		// 包含ECDHE的密码套件通常依赖椭圆曲线扩展
+		return cipherSuite.contains("ECDHE");
+	}
+
+	/**
+	 * 判断是否存在GREASE扩展(用于兼容性测试的预留扩展)
+	 */
+	private boolean hasGreaseExtension(SSLSession sslSession, Class<?> sslSessionClass) throws Exception {
+		// GREASE扩展类型为0x0a0a、0x1a1a等特殊值,此处简化判断
+		// 实际需解析扩展类型列表,这里通过密码套件特征近似判断
+		String protocol = sslSession.getProtocol();
+		return protocol.startsWith("TLSv1.3") || protocol.startsWith("TLSv1.2");
+	}
+
+	/**
+	 * 对未知值进行哈希处理
+	 */
+	private String hashUnknownValue(String value) {
+		try {
+			MessageDigest md = MessageDigest.getInstance("MD5");
+			byte[] digest = md.digest(value.getBytes(StandardCharsets.UTF_8));
+			StringBuilder hexString = new StringBuilder();
+			for (byte b : digest) {
+				hexString.append(String.format("%02x", b));
+			}
+			return hexString.substring(0, 8); // 取前8位
+		} catch (NoSuchAlgorithmException e) {
+			return value.hashCode() + "";
+		}
+	}
+
+	/**
+	 * 计算MD5哈希
+	 */
+	private String computeMD5(String input) throws NoSuchAlgorithmException {
+		MessageDigest md = MessageDigest.getInstance("MD5");
+		byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
+
+		StringBuilder hexString = new StringBuilder();
+		for (byte b : digest) {
+			hexString.append(String.format("%02x", b));
+		}
+		return hexString.toString();
+	}
+}

+ 173 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/JA4HGeneratorUtil.java

@@ -0,0 +1,173 @@
+package com.pig4cloud.pig.marketing.api.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * JA4H指纹生成工具类
+ * 基于HTTP请求的关键特征生成指纹,遵循JA4规范
+ */
+public class JA4HGeneratorUtil {
+
+    // JA4H需要提取的关键HTTP头部(按规范定义的顺序)
+    private static final List<String> KEY_HEADERS = Arrays.asList(
+        "Accept",
+        "Accept-Language",
+        "Accept-Encoding",
+        "User-Agent",
+        "Connection",
+        "Upgrade",
+        "Content-Length",
+        "Content-Type"
+    );
+
+    /**
+     * 生成JA4H指纹
+     * @param method HTTP方法(如GET、POST)
+     * @param path 请求路径(不含查询参数)
+     * @param httpVersion HTTP版本(如HTTP/1.1)
+     * @param headers HTTP请求头部键值对
+     * @return JA4H指纹(MD5哈希值)
+     */
+    public String generateJA4H(String method, String path, String httpVersion, Map<String, String> headers) {
+        try {
+            // 1. 规范化输入参数
+            String normalizedMethod = normalizeMethod(method);
+            String normalizedPath = normalizePath(path);
+            String normalizedVersion = normalizeHttpVersion(httpVersion);
+            
+            // 2. 提取并规范化关键头部
+            List<String> headerValues = extractAndNormalizeHeaders(headers);
+            
+            // 3. 按JA4H规则拼接原始字符串
+            String rawJA4H = buildRawJA4HString(
+                normalizedMethod,
+                normalizedPath,
+                normalizedVersion,
+                headerValues
+            );
+            
+            // 4. 计算MD5哈希作为最终指纹
+            return computeMD5(rawJA4H);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to generate JA4H fingerprint", e);
+        }
+    }
+
+    /**
+     * 规范化HTTP方法(大写处理)
+     */
+    private String normalizeMethod(String method) {
+        return method == null ? "" : method.trim().toUpperCase();
+    }
+
+    /**
+     * 规范化请求路径(去除查询参数,保留基础路径)
+     */
+    private String normalizePath(String path) {
+        if (path == null || path.trim().isEmpty()) {
+            return "/";
+        }
+        // 移除查询参数(?后面的部分)
+        int queryIndex = path.indexOf('?');
+        return queryIndex != -1 ? path.substring(0, queryIndex).trim() : path.trim();
+    }
+
+    /**
+     * 规范化HTTP版本
+     */
+    private String normalizeHttpVersion(String version) {
+        if (version == null) {
+            return "";
+        }
+        // 提取主版本(如HTTP/1.1 -> 1.1)
+        String[] parts = version.trim().split("/");
+        return parts.length > 1 ? parts[1] : version.trim();
+    }
+
+    /**
+     * 提取并规范化关键头部
+     * 按KEY_HEADERS定义的顺序提取,不存在的头部用空字符串表示
+     */
+    private List<String> extractAndNormalizeHeaders(Map<String, String> headers) {
+        if (headers == null) {
+            headers = Collections.emptyMap();
+        }
+        
+        // 转换为小写键的映射,方便查找
+        Map<String, String> lowerCaseHeaders = headers.entrySet().stream()
+                .collect(Collectors.toMap(
+                    entry -> entry.getKey().toLowerCase(),
+                    entry -> normalizeHeaderValue(entry.getValue()),
+                    (v1, v2) -> v1 // 处理重复头部,取第一个值
+                ));
+        
+        // 按预设顺序提取头部值
+        return KEY_HEADERS.stream()
+                .map(header -> lowerCaseHeaders.getOrDefault(header.toLowerCase(), ""))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 规范化头部值(去除首尾空格,多个值用逗号拼接)
+     */
+    private String normalizeHeaderValue(String value) {
+        if (value == null) {
+            return "";
+        }
+        // 处理多个值的情况(如Accept: text/html, application/json)
+        return value.trim().replaceAll("\\s+,\\s+", ",").replaceAll("\\s+", " ");
+    }
+
+    /**
+     * 构建JA4H原始字符串
+     * 格式:method,path,version,header1,header2,...
+     */
+    private String buildRawJA4HString(String method, String path, String version, List<String> headers) {
+        List<String> parts = new ArrayList<>();
+        parts.add(method);
+        parts.add(path);
+        parts.add(version);
+        parts.addAll(headers);
+        // 用逗号分隔所有部分
+        return String.join(",", parts);
+    }
+
+    /**
+     * 计算字符串的MD5哈希
+     */
+    private String computeMD5(String input) throws NoSuchAlgorithmException {
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
+        
+        // 转换为16进制字符串
+        StringBuilder hexString = new StringBuilder();
+        for (byte b : digest) {
+            String hex = String.format("%02x", b);
+            hexString.append(hex);
+        }
+        return hexString.toString();
+    }
+
+    // 示例用法
+    public static void main(String[] args) {
+        // 模拟一个HTTP请求
+        String method = "GET";
+        String path = "/api/data?param=123"; // 路径会自动去除查询参数
+        String httpVersion = "HTTP/1.1";
+        
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Accept", "text/html, application/xhtml+xml");
+        headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/114.0.0.0");
+        headers.put("Accept-Encoding", "gzip, deflate, br");
+        headers.put("Connection", "keep-alive");
+        
+        JA4HGeneratorUtil generator = new JA4HGeneratorUtil();
+        String ja4h = generator.generateJA4H(method, path, httpVersion, headers);
+        System.out.println("Generated JA4H fingerprint: " + ja4h);
+    }
+}

+ 26 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/DomainValidation.java

@@ -0,0 +1,26 @@
+package com.pig4cloud.pig.marketing.api.valid;
+
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-29
+ * @description: 域名相关字段校验注解
+ */
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = {DomainValidator.class})
+@Documented
+public @interface DomainValidation {
+	String message() default "域名校验失败:不符合来源类型与其他字段的关联规则";
+
+	Class<?>[] groups() default {};
+
+	Class<? extends Payload>[] payload() default {};
+}
+

+ 79 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/DomainValidator.java

@@ -0,0 +1,79 @@
+package com.pig4cloud.pig.marketing.api.valid;
+
+
+import com.pig4cloud.pig.marketing.api.dto.app.MarketingAppsDomainDTO;
+import com.pig4cloud.pig.marketing.api.dto.common.BaseDomainDTO;
+import com.pig4cloud.pig.marketing.api.util.DomainValidationUtil;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-29
+ * @description: 域名校验器
+ */
+
+public class DomainValidator implements ConstraintValidator<DomainValidation, Object> {
+
+	@Override
+	public boolean isValid(Object value, ConstraintValidatorContext context) {
+		if (!(value instanceof BaseDomainDTO dto)) {
+			return true;
+		}
+
+		Integer sourceType = dto.getSourceType();
+		Long groupId = dto.getGroupId();
+		String domain = dto.getDomain();
+
+		if (sourceType == null){
+			setMessage(context, "来源类型不能为空");
+			return false;
+		}
+
+		// 来源类型为分组(1)时的校验
+		if (sourceType == 1) {
+			if (groupId == null) {
+				setMessage(context, "来源为分组时,分组ID不能为空");
+				return false;
+			}
+//			// 分组模式下不能有域名相关字段
+//			if (notEmpty(domain)) {
+//				setMessage(context, "来源为分组时,不能填写域名");
+//				return false;
+//			}
+			return true;
+		}
+
+		// 来源类型为具体域名(2)时的校验
+		if (sourceType == 2) {
+//			if (groupId != null) {
+//				setMessage(context, "来源为具体域名时,分组ID必须为空");
+//				return false;
+//			}
+			if (!notEmpty(domain)) {
+				setMessage(context, "来源为具体域名时,域名不能为空");
+				return false;
+			}
+			// 校验域名格式
+			if (!DomainValidationUtil.isValidDomain(domain)) {
+				setMessage(context, "域名格式不合法");
+				return false;
+			}
+			return true;
+		}
+
+		setMessage(context, "来源类型值不合法(1-来自分组,2-具体域名)");
+		return false;
+	}
+
+	// 工具方法:判断字符串非空
+	private boolean notEmpty(String str) {
+		return str != null && !str.trim().isEmpty();
+	}
+
+	// 自定义错误信息
+	private void setMessage(ConstraintValidatorContext context, String message) {
+		context.disableDefaultConstraintViolation();
+		context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
+	}
+}

+ 26 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/IPValidation.java

@@ -0,0 +1,26 @@
+package com.pig4cloud.pig.marketing.api.valid;
+
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-29
+ * @description: IP相关字段校验注解
+ */
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = {IPValidator.class})
+@Documented
+public @interface IPValidation {
+	String message() default "IP校验失败:不符合sourceType与其他字段的关联规则";
+
+	Class<?>[] groups() default {};
+
+	Class<? extends Payload>[] payload() default {};
+}
+

+ 120 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/IPValidator.java

@@ -0,0 +1,120 @@
+package com.pig4cloud.pig.marketing.api.valid;
+
+
+import com.pig4cloud.pig.common.core.util.ip.IPUtils;
+import com.pig4cloud.pig.marketing.api.dto.common.BaseIpDTO;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-29
+ * @description: IP校验器
+ */
+
+public class IPValidator implements ConstraintValidator<IPValidation, Object> {
+
+	@Override
+	public boolean isValid(Object value, ConstraintValidatorContext context) {
+		if (!(value instanceof BaseIpDTO dto)) {
+			return true;
+		}
+
+		Integer sourceType = dto.getSourceType();
+		Long groupId = dto.getGroupId();
+		Integer ipMode = dto.getIpMode();
+		String startIp = dto.getStartIp();
+		String endIp = dto.getEndIp();
+
+		if (sourceType == null){
+			setMessage(context, "来源类型不能为空");
+			return false;
+		}
+
+		// 来源类型为分组(1)时的校验
+		if (sourceType == 1) {
+			if (groupId == null) {
+				setMessage(context, "来源为分组时,分组ID不能为空");
+				return false;
+			}
+//			// 分组模式下不能有IP相关字段
+//			if (ipMode != null || notEmpty(startIp) || notEmpty(endIp)) {
+//				setMessage(context, "来源为分组时,不能填写IP模式、IP地址");
+//				return false;
+//			}
+			return true;
+		}
+
+		// 来源类型为具体IP(2)时的校验
+		if (sourceType == 2) {
+//			// 具体IP模式下不能有groupId
+//			if (groupId != null) {
+//				setMessage(context, "来源为具体IP时,分组ID必须为空");
+//				return false;
+//			}
+			// 必须指定IP模式
+			if (ipMode == null) {
+				setMessage(context, "来源为具体IP时,IP模式不能为空");
+				return false;
+			}
+
+			// 单IP模式(ipMode=1)
+			if (ipMode == 1) {
+				if (!notEmpty(startIp)) {
+					setMessage(context, "单IP模式下,startIp不能为空");
+					return false;
+				}
+//				if (notEmpty(endIp)) {
+//					setMessage(context, "单IP模式下,endIp必须为空");
+//					return false;
+//				}
+				// 校验IP格式
+				if (!IPUtils.isValidIp(startIp)) {
+					setMessage(context, "IP格式不合法(IPv4)");
+					return false;
+				}
+				return true;
+			}
+
+			// IP段模式(ipMode=2)
+			if (ipMode == 2) {
+				if (!notEmpty(startIp) || !notEmpty(endIp)) {
+					setMessage(context, "IP段模式下,startIp和endIp都不能为空");
+					return false;
+				}
+				// 校验IP格式
+				if (!IPUtils.isValidIp(startIp)) {
+					setMessage(context, "startIp格式不合法(IPv4)");
+					return false;
+				}
+				if (!IPUtils.isValidIp(endIp)) {
+					setMessage(context, "endIp格式不合法(IPv4)");
+					return false;
+				}
+				// 校验endIp > startIp
+				if (!IPUtils.isEndIpGreater(startIp, endIp)) {
+					setMessage(context, "IP段模式下,endIp必须大于startIp");
+					return false;
+				}
+				return true;
+			}
+
+			setMessage(context, "IP模式值不合法(1-单IP,2-IP段)");
+			return false;
+		}
+
+		setMessage(context, "来源类型值不合法(1-来自分组,2-具体IP)");
+		return false;
+	}
+
+	// 工具方法:判断字符串非空
+	private boolean notEmpty(String str) {
+		return str != null && !str.trim().isEmpty();
+	}
+
+	// 自定义错误信息
+	private void setMessage(ConstraintValidatorContext context, String message) {
+		context.disableDefaultConstraintViolation();
+		context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
+	}
+}

+ 23 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/ValueRange.java

@@ -0,0 +1,23 @@
+package com.pig4cloud.pig.marketing.api.valid;
+
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(validatedBy = ValueRangeValidator.class)
+public @interface ValueRange {
+    // 允许的值
+    int[] values();
+    
+    // 校验失败提示信息
+    String message() default "值不在允许的范围内";
+    
+    Class<?>[] groups() default {};
+    
+    Class<? extends Payload>[] payload() default {};
+}

+ 31 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/valid/ValueRangeValidator.java

@@ -0,0 +1,31 @@
+package com.pig4cloud.pig.marketing.api.valid;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class ValueRangeValidator implements ConstraintValidator<ValueRange, Integer> {
+    private Set<Integer> allowedValues;
+    
+    @Override
+    public void initialize(ValueRange constraintAnnotation) {
+        // 初始化允许的值集合
+        int[] values = constraintAnnotation.values();
+        allowedValues = new HashSet<>();
+        for (int value : values) {
+            allowedValues.add(value);
+        }
+    }
+    
+    @Override
+    public boolean isValid(Integer value, ConstraintValidatorContext context) {
+        // 如果值为null,由@NotNull注解来处理,这里不做判断
+        if (value == null) {
+            return true;
+        }
+        // 检查值是否在允许的范围内
+        return allowedValues.contains(value);
+    }
+}

+ 53 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/MarketingAppsDomainVO.java

@@ -0,0 +1,53 @@
+package com.pig4cloud.pig.marketing.api.vo.app;
+
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 应用域名列表
+ */
+@Data
+@Schema(description = "应用域名列表")
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+public class MarketingAppsDomainVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 来源类型
+	 */
+	@Schema(description = "来源类型,1-来自分组,2-具体域名")
+	private Integer sourceType;
+
+	/**
+	 * 分组ID
+	 */
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	/**
+	 * 分组名称
+	 */
+	@Schema(description = "分组名称")
+	private String groupName;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名")
+	private String domain;
+}

+ 71 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/MarketingAppsIpVO.java

@@ -0,0 +1,71 @@
+package com.pig4cloud.pig.marketing.api.vo.app;
+
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 应用 IP 列表
+ */
+@Data
+@Schema(description = "应用 IP 列表")
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+public class MarketingAppsIpVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * ip类型
+	 */
+	@Schema(description = "ip类型,1-白名单,2-黑名单")
+	private Integer ipType;
+
+	/**
+	 * 来源类型
+	 */
+	@Schema(description = "来源类型,1-来自分组,2-具体IP")
+	private Integer sourceType;
+
+	/**
+	 * 分组ID
+	 */
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	/**
+	 * 分组名称
+	 */
+	@Schema(description = "分组名称")
+	private String groupName;
+
+	/**
+	 * ip模式
+	 */
+	@Schema(description = "ip模式,1-单IP,2-IP段")
+	private Integer ipMode;
+
+	/**
+	 * 起始IP
+	 */
+	@Schema(description = "起始IP")
+	private String startIp;
+
+	/**
+	 * 结束IP
+	 */
+	@Schema(description = "结束IP")
+	private String endIp;
+}

+ 107 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/PageMarketingAppsVO.java

@@ -0,0 +1,107 @@
+package com.pig4cloud.pig.marketing.api.vo.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 分页查询应用列表出参
+ */
+@Data
+@Schema(description = "分页查询应用列表出参")
+public class PageMarketingAppsVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	Long id;
+
+	/**
+	 * 应用ID
+	 */
+	@Schema(description = "应用ID")
+	String appId;
+
+	/**
+	 * 应用名称
+	 */
+	@Schema(description = "应用名称")
+	String appName;
+
+	/**
+	 * 应用图标
+	 */
+	@Schema(description = "应用图标")
+	String appImg;
+
+	/**
+	 * 应用下载链接
+	 */
+	@Schema(description = "应用下载链接")
+	String appUrl;
+
+	/**
+	 * 应用下载链接
+	 */
+	@Schema(description = "应用备用下载链接")
+	String backupUrl;
+
+	/**
+	 * 应用类型
+	 */
+	@Schema(description = "应用类型")
+	Integer domainType;
+
+	/**
+	 * 域名限制
+	 */
+	@Schema(description = "域名限制")
+	Boolean domainLimit;
+
+	/**
+	 * 营销投放
+	 */
+	@Schema(description = "营销投放")
+	Boolean launch;
+
+	/**
+	 * 触发规则
+	 */
+	@Schema(description = "触发规则")
+	Integer triggerRule;
+
+	/**
+	 * 触发频率
+	 */
+	@Schema(description = "触发频率")
+	String triggerNum;
+
+	/**
+	 * 备注
+	 */
+	@Schema(description = "备注")
+	String remark;
+
+
+	/**
+	 * 应用ip列表
+	 */
+	@Schema(description = "应用ip列表")
+	List<MarketingAppsIpVO> ips;
+
+	/**
+	 * 应用域名列表
+	 */
+	@Schema(description = "应用域名列表")
+	List<MarketingAppsDomainVO> domains;
+}

+ 39 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/PageStatAppFirstMktDataVO.java

@@ -0,0 +1,39 @@
+package com.pig4cloud.pig.marketing.api.vo.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-30
+ * @description: 分页统计应用一级营销数据
+ */
+@Data
+@Schema(description = "分页统计应用一级营销数据")
+public class PageStatAppFirstMktDataVO implements Serializable {
+
+	@Schema(description = "ip")
+	private String ip;
+
+	@Schema(description = "首次访问时间")
+	private LocalDateTime firstTime;
+
+	@Schema(description = "最后访问时间")
+	private LocalDateTime lastTime;
+
+	@Schema(description = "访问来源")
+	private String utmSource;
+
+	@Schema(description = "最后受访页面")
+	private String url;
+
+	@Schema(description = "地区")
+	private String region;
+
+	@Schema(description = "访问量")
+	private Integer  count;
+}

+ 45 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/app/PageStatAppSecondMktDataVO.java

@@ -0,0 +1,45 @@
+package com.pig4cloud.pig.marketing.api.vo.app;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-30
+ * @description: 分页统计应用二级营销数据
+ */
+@Data
+@Schema(description = "分页统计应用二级营销数据")
+public class PageStatAppSecondMktDataVO {
+
+	@Schema(description = "浏览器指纹")
+	private String fingerprint;
+
+	@Schema(description = "系统类型")
+	private String osType;
+
+	@Schema(description = "系统版本")
+	private String osVersion;
+
+	@Schema(description = "浏览器类型")
+	private String browser;
+
+	@Schema(description = "浏览器版本")
+	private String browserVersion;
+
+	@Schema(description = "首次访问时间")
+	private LocalDateTime firstTime;
+
+	@Schema(description = "最后访问时间")
+	private LocalDateTime lastTime;
+
+	@Schema(description = "最后受访页面")
+	private String url;
+
+	@Schema(description = "访问量")
+	private Integer  count;
+
+}

+ 53 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingConfigDomainListVO.java

@@ -0,0 +1,53 @@
+package com.pig4cloud.pig.marketing.api.vo.config;
+
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-24
+ * @description: 获取域名列表出参
+ */
+@Data
+@Schema(description = "获取域名列表出参")
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+public class GetMarketingConfigDomainListVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 来源类型
+	 */
+	@Schema(description = "来源类型,1-来自分组,2-具体域名")
+	private Integer sourceType;
+
+	/**
+	 * 分组ID
+	 */
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	/**
+	 * 分组名称
+	 */
+	@Schema(description = "分组名称")
+	private String groupName;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名")
+	private String domain;
+}

+ 69 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingConfigIpListVO.java

@@ -0,0 +1,69 @@
+package com.pig4cloud.pig.marketing.api.vo.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-24
+ * @description: 获取IP集合出参
+ */
+@Data
+@Schema(description = "获取IP集合出参")
+public class GetMarketingConfigIpListVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * ip类型
+	 */
+	@Schema(description = "ip类型,1-白名单,2-黑名单")
+	private Integer ipType;
+
+	/**
+	 * 来源类型
+	 */
+	@Schema(description = "来源类型,1-来自分组,2-具体IP")
+	private Integer sourceType;
+
+	/**
+	 * 分组ID
+	 */
+	@Schema(description = "分组ID")
+	private Long groupId;
+
+	/**
+	 * 分组名称
+	 */
+	@Schema(description = "分组名称")
+	private String groupName;
+
+	/**
+	 * ip模式
+	 */
+	@Schema(description = "ip模式,1-单IP,2-IP段")
+	private Integer ipMode;
+
+	/**
+	 * 起始IP
+	 */
+	@Schema(description = "起始IP")
+	private String startIp;
+
+	/**
+	 * 结束IP
+	 */
+	@Schema(description = "结束IP")
+	private String endIp;
+}

+ 31 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingDomainGroupVO.java

@@ -0,0 +1,31 @@
+package com.pig4cloud.pig.marketing.api.vo.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 获取域名分组列表出参
+ */
+@Data
+@Schema(description = "获取域名分组列表出参")
+public class GetMarketingDomainGroupVO implements Serializable {
+
+	@Serial
+	private final static long serialVersionUID = 1L;
+
+	@Schema(description = "id")
+	private Long id;
+
+	@Schema(description = "分组名称")
+	private String groupName;
+
+	@Schema(description = "域名列表")
+	private List<MarketingGroupDomainVO> domains;
+}

+ 66 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingGlobalConfigVO.java

@@ -0,0 +1,66 @@
+package com.pig4cloud.pig.marketing.api.vo.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-25
+ * @description: 全局配置信息
+ */
+@Data
+@Schema(description = "全局配置信息")
+public class GetMarketingGlobalConfigVO implements Serializable {
+
+	@Serial
+	private final static long serialVersionUID = 1L;
+
+	/**
+	 * 触发方式
+	 */
+	@Schema(description = "触发方式")
+	@NotNull(message = "触发方式不能为空")
+	Integer triggerMode;
+
+	/**
+	 * 触发规则
+	 */
+	@Schema(description = "触发规则")
+	@NotNull(message = "触发规则不能为空")
+	Integer triggerRule;
+
+	/**
+	 * 触发频率
+	 */
+	@Schema(description = "触发频率,小于1为百分比,大于1为周期")
+	@NotBlank(message = "触发频率不能为空")
+	String triggerNum;
+
+	/**
+	 * 提示信息
+	 */
+	@Schema(description = "提示信息")
+	@NotBlank(message = "提示信息不能为空")
+	@Size(max = 128, message = "提示信息长度不能超过128")
+	String promptMsg;
+
+	/**
+	 * 跳转链接
+	 */
+	@Schema(description = "跳转链接")
+	@NotBlank(message = "跳转链接不能为空")
+	@Size(max = 128, message = "跳转链接长度不能超过128")
+	@Pattern(
+			regexp = "^(http|https|ftp)://([\\w-]+\\.)+[\\w-]+(:\\d+)?(/[\\w-./?%&=]*)?$",
+			message = "跳转链接格式不正确,请输入有效的URL"
+	)
+	String url;
+}

+ 31 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/GetMarketingIpGroupVO.java

@@ -0,0 +1,31 @@
+package com.pig4cloud.pig.marketing.api.vo.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 获取ip分组列表
+ */
+@Data
+@Schema(description = "获取ip分组列表出参")
+public class GetMarketingIpGroupVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@Schema(description = "id")
+	private Long id;
+
+	@Schema(description = "分组名称")
+	private String groupName;
+
+	@Schema(description = "ip列表")
+	private List<MarketingGroupIpVO> ips;
+}

+ 33 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/MarketingGroupDomainVO.java

@@ -0,0 +1,33 @@
+package com.pig4cloud.pig.marketing.api.vo.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 营销分组域名VO
+ */
+@Data
+@Schema(description = "营销分组域名出参")
+public class MarketingGroupDomainVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名")
+	private String domain;
+}

+ 45 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/config/MarketingGroupIpVO.java

@@ -0,0 +1,45 @@
+package com.pig4cloud.pig.marketing.api.vo.config;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 营销分组IP VO
+ */
+@Data
+@Schema(description = "营销分组IP出参")
+public class MarketingGroupIpVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * ip模式,1-单IP,2-IP段
+	 */
+	@Schema(description = "ip模式,1-单IP,2-IP段")
+	private Integer ipMode;
+
+	/**
+	 * 起始IP
+	 */
+	@Schema(description = "起始IP")
+	private String startIp;
+
+	/**
+	 * 结束IP
+	 */
+	@Schema(description = "结束IP")
+	private String endIp;
+}

+ 64 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/data/MarketingDataReportVO.java

@@ -0,0 +1,64 @@
+package com.pig4cloud.pig.marketing.api.vo.data;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 上报营销数据VO
+ */
+@Data
+@Schema(description = "上报营销数据出参")
+public class MarketingDataReportVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 应用ID
+	 */
+	@Schema(description = "应用ID")
+	private String appId;
+
+	/**
+	 * 跳转类型
+	 */
+	@Schema(description = "跳转类型")
+	private String type;
+
+	/**
+	 * 触发方式
+	 */
+	@Schema(description = "触发方式,全局获取")
+	private Integer triggerMode;
+
+	/**
+	 * 触发规则
+	 */
+	@Schema(description = "触发规则,具体应用获取")
+	private Integer triggerRule;
+
+	/**
+	 * 提示信息
+	 */
+	@Schema(description = "提示信息")
+	private String promptMsg;
+
+	/**
+	 * 跳转链接
+	 */
+	@Schema(description = "跳转链接")
+	private String url;
+
+	@Schema(description = "后台生成指纹")
+	private String fingerprint;
+
+	@Schema(description = "前后端组合指纹")
+	private String combine;
+
+}

+ 50 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/data/PageFirstLevelDataVO.java

@@ -0,0 +1,50 @@
+package com.pig4cloud.pig.marketing.api.vo.data;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 分页统计一级营销数据VO
+ */
+@Data
+public class PageFirstLevelDataVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * ip
+	 */
+	@Schema(description = "ip")
+	private String ip;
+
+	/**
+	 * 总访问量
+	 */
+	@Schema(description = "总访问量")
+	private Long total;
+
+	/**
+	 * 日活
+	 */
+	@Schema(description = "日活")
+	private Long daily;
+
+	/**
+	 * 时活
+	 */
+	@Schema(description = "时活")
+	private Long hourly;
+
+	/**
+	 * 15分钟在线人数
+	 */
+	@Schema(description = "15分钟在线人数")
+	private Long online;
+}

+ 62 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/vo/data/PageSecondLevelDataVO.java

@@ -0,0 +1,62 @@
+package com.pig4cloud.pig.marketing.api.vo.data;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-0722
+ * @description: 分页统计二级营销数据VO
+ */
+@Data
+public class PageSecondLevelDataVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 域名
+	 */
+	@Schema(description = "域名")
+	private String domain;
+
+	/**
+	 * 跳转来源
+	 */
+	@Schema(description = "跳转来源")
+	private String referrer;
+
+	/**
+	 * 浏览器指纹
+	 */
+	@Schema(description = "浏览器指纹")
+	private String fingerprint;
+
+	/**
+	 * 总访问量
+	 */
+	@Schema(description = "总访问量")
+	private Long total;
+
+	/**
+	 * 日活
+	 */
+	@Schema(description = "日活")
+	private Long daily;
+
+	/**
+	 * 时活
+	 */
+	@Schema(description = "时活")
+	private Long hourly;
+
+	/**
+	 * 15分钟在线人数
+	 */
+	@Schema(description = "15分钟在线人数")
+	private Long online;
+}

+ 13 - 0
pig-marketing/pig-marketing-biz/Dockerfile

@@ -0,0 +1,13 @@
+FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:21-anolis
+
+WORKDIR /pig-marketing
+
+ARG JAR_FILE=target/pig-marketing-biz.jar
+
+COPY ${JAR_FILE} app.jar
+
+EXPOSE 6000
+
+ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
+
+CMD sleep 60; java $JAVA_OPTS -jar app.jar

+ 149 - 0
pig-marketing/pig-marketing-biz/pom.xml

@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>com.pig4cloud</groupId>
+		<artifactId>pig-marketing</artifactId>
+		<version>${revision}</version>
+	</parent>
+
+	<artifactId>pig-marketing-biz</artifactId>
+	<packaging>jar</packaging>
+
+	<description>pig 营销管理业务处理模块</description>
+
+	<dependencies>
+		<!--marketing api、model 模块-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-marketing-api</artifactId>
+		</dependency>
+		<!--UPMS接口模块-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-upms-api</artifactId>
+		</dependency>
+		<!--文件管理-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-oss</artifactId>
+		</dependency>
+		<!--feign 调用-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-feign</artifactId>
+		</dependency>
+		<!--安全模块-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-security</artifactId>
+		</dependency>
+		<!--日志处理-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-log</artifactId>
+		</dependency>
+		<!--接口文档-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-swagger</artifactId>
+		</dependency>
+		<!-- orm 模块-->
+		<dependency>
+			<groupId>com.baomidou</groupId>
+			<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.mysql</groupId>
+			<artifactId>mysql-connector-j</artifactId>
+		</dependency>
+		<!--注册中心客户端-->
+		<dependency>
+			<groupId>com.alibaba.cloud</groupId>
+			<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+		</dependency>
+		<!--配置中心客户端-->
+		<dependency>
+			<groupId>com.alibaba.cloud</groupId>
+			<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+		</dependency>
+		<!-- 短信下发 -->
+		<dependency>
+			<groupId>org.dromara.sms4j</groupId>
+			<artifactId>sms4j-spring-boot-starter</artifactId>
+		</dependency>
+		<!--xss 过滤-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-xss</artifactId>
+		</dependency>
+		<!--undertow容器-->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-undertow</artifactId>
+		</dependency>
+	</dependencies>
+
+	<profiles>
+		<profile>
+			<id>boot</id>
+		</profile>
+		<profile>
+			<id>cloud</id>
+			<activation>
+				<!-- 默认环境 -->
+				<activeByDefault>true</activeByDefault>
+			</activation>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.springframework.boot</groupId>
+						<artifactId>spring-boot-maven-plugin</artifactId>
+					</plugin>
+					<plugin>
+						<groupId>io.fabric8</groupId>
+						<artifactId>docker-maven-plugin</artifactId>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+	</profiles>
+
+	<build>
+		<resources>
+			<resource>
+				<directory>src/main/resources</directory>
+				<filtering>true</filtering>
+				<excludes>
+					<exclude>**/*.xlsx</exclude>
+					<exclude>**/*.xls</exclude>
+				</excludes>
+			</resource>
+			<resource>
+				<directory>src/main/resources</directory>
+				<filtering>false</filtering>
+				<includes>
+					<include>**/*.xlsx</include>
+					<include>**/*.xls</include>
+				</includes>
+			</resource>
+		</resources>
+	</build>
+</project>

+ 27 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/PigMarketingApplication.java

@@ -0,0 +1,27 @@
+package com.pig4cloud.pig.marketing;
+
+
+import com.pig4cloud.pig.common.feign.annotation.EnablePigFeignClients;
+import com.pig4cloud.pig.common.security.annotation.EnablePigResourceServer;
+import com.pig4cloud.pig.common.swagger.annotation.EnablePigDoc;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-21
+ * @description: 营销模块
+ */
+@EnablePigDoc(value = "marketing")
+@EnablePigFeignClients
+@EnablePigResourceServer
+@EnableDiscoveryClient
+@SpringBootApplication
+public class PigMarketingApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(PigMarketingApplication.class, args);
+	}
+
+}

+ 162 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/controller/MarketingAppsController.java

@@ -0,0 +1,162 @@
+package com.pig4cloud.pig.marketing.controller;
+
+
+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.marketing.api.dto.app.*;
+import com.pig4cloud.pig.marketing.api.vo.app.PageMarketingAppsVO;
+import com.pig4cloud.pig.marketing.service.MarketingAppsService;
+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.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+
+/**
+ * @author: lwh
+ * @date: 2025-07-21
+ * @description: 营销应用列表
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/app")
+@Tag(description = "marketingApp", name = "营销应用管理")
+@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
+public class MarketingAppsController {
+
+	private final MarketingAppsService marketingAppsService;
+
+
+	/**
+	 * 分页查询应用列表
+	 * @param reqDto 分页参数对象
+	 * @return 分页查询结果
+	 */
+	@GetMapping("/page")
+	@Operation(summary = "分页查询应用列表")
+	public R pageMarketingApps(@ParameterObject PageMarketingAppsDTO reqDto) {
+		Page page = marketingAppsService.pageMarketingApps(reqDto, true);
+		return R.ok(page);
+	}
+
+	@GetMapping("/detail")
+	@Operation(summary = "查询应用详情")
+	public R getMarketingAppById(@ParameterObject Long id) {
+		if (id == null){
+			throw new BusinessException("应用ID不能为空");
+		}
+		PageMarketingAppsVO res = marketingAppsService.getMarketingAppById(id);
+		return R.ok(res);
+	}
+
+	/**
+	 * 分页查询拉黑应用列表
+	 * @param reqDto 分页参数对象
+	 * @return 分页查询结果
+	 */
+	@GetMapping("/pageDel")
+	@Operation(summary = "分页查询拉黑应用列表")
+	public R pageDelMarketingApps(@ParameterObject PageMarketingAppsDTO reqDto) {
+		Page page = marketingAppsService.pageMarketingApps(reqDto, false);
+		return R.ok(page);
+	}
+
+	/**
+	 * 批量更新应用信息
+	 * @param reqDtoList 批量更新参数
+	 * @return 保存结果
+	 */
+	@PostMapping("/update")
+	@Operation(summary = "批量更新应用信息")
+	public R batchUpdateMarketingApps(@Valid @RequestBody List<BatchUpdateMarketingAppsDTO> reqDtoList) {
+		return marketingAppsService.batchUpdateMarketingApps(reqDtoList);
+	}
+
+	/**
+	 * 设置应用信息
+	 * @param reqDto 应用信息
+	 * @return 保存结果
+	 */
+	@PostMapping("/setInfo")
+	@Operation(summary = "设置应用信息")
+	public R setMarketingAppsInfo(@Valid @RequestBody SetMarketingAppsDTO reqDto) {
+		// 先查询域名是否存在
+		Boolean result = marketingAppsService.setMarketingAppsInfo(reqDto);
+		return result?R.ok():R.failed();
+	}
+
+	/**
+	 * 设置应用状态
+	 * @param reqDto 应用信息
+	 * @return R
+	 */
+	@PostMapping("/setStatus")
+	@Operation(summary = "设置应用状态")
+	public R setAppStatusById(@Valid @RequestBody SetMarketingAppsStatusDTO reqDto)  {
+		Boolean res = marketingAppsService.setAppStatusById(reqDto);
+		return res?R.ok():R.failed();
+	}
+
+	@PostMapping("/synced")
+	@Operation(summary = "同步应用列表")
+	public R syncedAppList() {
+		return R.ok(marketingAppsService.syncedAppList());
+	}
+
+	/**
+	 * 修改应用IP列表
+	 * @param reqDto IP参数
+	 * @return 保存结果
+	 */
+	@PostMapping("/modIp")
+	@Operation(summary = "修改应用IP集合")
+	public R modMarketingAppsIp(@Valid @RequestBody ModMarketingAppsIpDTO reqDto) {
+		return marketingAppsService.modMarketingAppsIps(reqDto);
+	}
+
+	/**
+	 * 修改应用域名集合
+	 * @param reqDto 域名参数
+	 * @return 保存结果
+	 */
+	@PostMapping("/modDomain")
+	@Operation(summary = "修改应用域名集合")
+	public R modMarketingAppsIp(@Valid @RequestBody ModMarketingAppsDomainDTO reqDto) {
+		return marketingAppsService.modMarketingAppsDomains(reqDto);
+	}
+
+	/**
+	 * 分页统计应用一级营销数据
+	 * @param reqDto 查询参数
+	 * @return 一级营销数据
+	 */
+	@GetMapping("/stat/page")
+	@Operation(summary = "分页统计应用一级营销数据")
+	public R pageStatAppFirstMktData(@Valid @ParameterObject PageStatAppMktDataDTO reqDto) {
+		return R.ok(marketingAppsService.pageStatAppFirstMktData(reqDto));
+	}
+
+	/**
+	 * 分页统计应用二级营销数据
+	 * @param reqDto 统计参数
+	 * @return 二级营销数据
+	 */
+	@GetMapping("/stat/second/page")
+	@Operation(summary = "分页统计应用二级营销数据")
+	public R pageStatAppSecondMktData(@Valid @ParameterObject PageStatAppMktDataDTO reqDto,
+									  @Valid
+									  @ParameterObject
+									  @NotBlank(message = "ip不能为空")
+									  @Schema(description = "ip") String ip) {
+		return R.ok(marketingAppsService.pageStatAppSecondMktData(reqDto, ip));
+	}
+}

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

@@ -0,0 +1,198 @@
+package com.pig4cloud.pig.marketing.controller;
+
+
+import com.pig4cloud.pig.common.core.exception.BusinessException;
+import com.pig4cloud.pig.common.core.util.R;
+import com.pig4cloud.pig.marketing.api.dto.config.*;
+import com.pig4cloud.pig.marketing.api.vo.config.*;
+import com.pig4cloud.pig.marketing.service.MarketingConfigService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+
+/**
+ * @author: lwh
+ * @date: 2025-07-15
+ * @description: 营销配置管理
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/config")
+@Tag(description = "marketingConfig", name = "营销配置管理")
+@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
+public class MarketingConfigController {
+
+	private final MarketingConfigService marketingConfigService;
+
+	/**
+	 * 添加分组
+	 * @return 添加结果
+	 */
+	@PostMapping("/addGroup")
+	@Operation(summary = "添加分组")
+	public R addMarketingConfigGroup(@Valid @RequestBody AddMarketingGroupDTO reqDto) {
+		return marketingConfigService.addMarketingConfigGroup(reqDto);
+	}
+
+	/**
+	 * 删除分组
+	 * @return 删除结果
+	 */
+	@PostMapping("/delGroup")
+	@Operation(summary = "删除分组")
+	public R delMarketingConfigGroup(@Valid @RequestBody DelMarketingGroupDTO reqDto) {
+		return R.ok(marketingConfigService.delMarketingConfigGroup(reqDto));
+	}
+
+	/**
+	 * 根据ID查询分组详情
+	 * @return R
+	 */
+	@GetMapping("/group/detail")
+	@Operation(summary = "根据ID查询分组详情")
+	public R getMarketingGroupDetail(@ParameterObject  Long id) {
+		if (id == null){
+			throw new BusinessException("分组ID不能为空");
+		}
+		return marketingConfigService.getMarketingGroupDetail(id);
+	}
+
+	/**
+	 * 修改域名分组
+	 * @return 域名分组列表
+	 */
+	@PostMapping("/modDomainGroup")
+	@Operation(summary = "修改域名分组")
+	public R modMarketingGroupDomain(@Valid @RequestBody ModMarketingGroupDomainDTO reqDto) {
+		return marketingConfigService.modMarketingGroupDomain(reqDto);
+	}
+
+	/**
+	 * 获取域名分组列表
+	 * @return 域名分组列表
+	 */
+	@GetMapping("/getDomainGroup")
+	@Operation(summary = "获取域名分组列表")
+	public R getMarketingDomainGroupList() {
+		List<GetMarketingDomainGroupVO> res = marketingConfigService.getMarketingDomainGroupList();
+		return R.ok(res);
+	}
+
+	/**
+	 * 修改分组IP
+	 * @return IP分组列表
+	 */
+	@PostMapping("/modIpGroup")
+	@Operation(summary = "修改IP分组")
+	public R modMarketingGroupIp(@Valid @RequestBody ModMarketingGroupIPDTO reqDto) {
+		return marketingConfigService.modMarketingGroupIp(reqDto);
+	}
+
+	/**
+	 * 获取ip分组列表
+	 * @return ip分组列表
+	 */
+	@GetMapping("/getIpGroup")
+	@Operation(summary = "获取IP分组列表")
+	public R getMarketingIpGroupList() {
+		List<GetMarketingIpGroupVO> res = marketingConfigService.getMarketingIpGroupList();
+		return R.ok(res);
+	}
+
+	/**
+	 * 获取ip集合
+	 * @return ip分组列表
+	 */
+	@GetMapping("/ipList")
+	@Operation(summary = "获取IP集合")
+	public R getMarketingConfigIpList() {
+		List<GetMarketingConfigIpListVO> res = marketingConfigService.getMarketingConfigIpList();
+		return R.ok(res);
+	}
+
+	/**
+	 * 添加IP集合
+	 * @return R
+	 */
+	@PostMapping("/addIp")
+	@Operation(summary = "IP集合添加IP")
+	public R addMarketingConfigIpList(@Valid @RequestBody AddMarketingConfigIpListDTO reqDto) {
+		return R.ok(marketingConfigService.addMarketingConfigIpList(reqDto));
+	}
+
+	/**
+	 * 删除IP集合
+	 * @return R
+	 */
+	@GetMapping("/delIp/{id}")
+	@Operation(summary = "IP集合删除IP")
+	public R delMarketingConfigIpList(@PathVariable Long id) {
+		if (id == null) {
+			throw new BusinessException("id不能为空");
+		}
+		return R.ok(marketingConfigService.delMarketingConfigIpList(id));
+	}
+
+	/**
+	 * 获取域名集合
+	 * @return 域名分组列表
+	 */
+	@GetMapping("/domainList")
+	@Operation(summary = "获取域名集合")
+	public R getMarketingConfigDomainList() {
+		List<GetMarketingConfigDomainListVO> res = marketingConfigService.getMarketingConfigDomainList();
+		return R.ok(res);
+	}
+
+	/**
+	 * 添加域名集合
+	 * @return R
+	 */
+	@PostMapping("/addDomain")
+	@Operation(summary = "域名集合添加域名")
+	public R addMarketingConfigDomainList(@Valid @RequestBody AddMarketingConfigDomainListDTO reqDto) {
+		return R.ok(marketingConfigService.addMarketingConfigDomainList(reqDto));
+	}
+
+	/**
+	 * 删除IP集合
+	 * @return R
+	 */
+	@GetMapping("/delDomain/{id}")
+	@Operation(summary = "域名集合删除域名")
+	public R delMarketingConfigDomainList(@PathVariable Long id) {
+		if (id == null){
+			throw new BusinessException("id不能为空");
+		}
+		return R.ok(marketingConfigService.delMarketingConfigDomainList(id));
+	}
+
+	/**
+	 * 查询全局配置信息
+	 * @return R
+	 */
+	@GetMapping("/getConfig")
+	@Operation(summary = "查询全局配置信息")
+	public R getMarketingGlobalConfig() {
+		return R.ok(marketingConfigService.getMarketingGlobalConfig());
+	}
+
+	/**
+	 * 查询全局配置信息
+	 * @return R
+	 */
+	@PostMapping("/setConfig")
+	@Operation(summary = "设置全局配置信息")
+	public R setMarketingGlobalConfig(@Valid @RequestBody GetMarketingGlobalConfigVO reDto) {
+		return R.ok(marketingConfigService.setMarketingGlobalConfig(reDto));
+	}
+
+}

+ 55 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/controller/MarketingDataController.java

@@ -0,0 +1,55 @@
+package com.pig4cloud.pig.marketing.controller;
+
+
+import com.pig4cloud.pig.common.core.util.R;
+import com.pig4cloud.pig.common.security.annotation.Inner;
+import com.pig4cloud.pig.marketing.api.dto.data.MarketingDataReportDTO;
+import com.pig4cloud.pig.marketing.api.dto.data.PageSecondLevelDataDTO;
+import com.pig4cloud.pig.marketing.api.vo.data.MarketingDataReportVO;
+import com.pig4cloud.pig.marketing.api.dto.data.PageFirstLevelDataDTO;
+import com.pig4cloud.pig.marketing.service.MarketingDataService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.AllArgsConstructor;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.annotation.*;
+
+
+/**
+ * @author: lwh
+ * @date: 2025-07-21
+ * @description: 营销数据统计
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/data")
+@Tag(description = "marketingData", name = "营销数据统计")
+@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
+public class MarketingDataController {
+
+	private final MarketingDataService dataService;
+
+	@Inner(value = false)
+	@PostMapping("/report")
+	@Operation(summary = "上报营销数据")
+	public R reportMarketingData(@RequestBody MarketingDataReportDTO reqDto, HttpServletRequest request) {
+		MarketingDataReportVO result= dataService.reportMarketingData(reqDto, request);
+		return R.ok(result);
+	}
+
+	@GetMapping("/page")
+	@Operation(summary = "分页统计一级营销数据")
+	public R pageFirstLevelData(@ParameterObject PageFirstLevelDataDTO reqDto) {
+		return R.ok(dataService.pageFirstLevelData(reqDto));
+	}
+
+	@GetMapping("/detail")
+	@Operation(summary = "分页统计二级营销数据")
+	public R pageSecondLevelData(@ParameterObject PageSecondLevelDataDTO reqDto) {
+		return R.ok(dataService.pageSecondLevelData(reqDto));
+	}
+
+}

+ 15 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingAppsDomainMapper.java

@@ -0,0 +1,15 @@
+package com.pig4cloud.pig.marketing.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.pig4cloud.pig.marketing.api.entity.MarketingAppsDomain;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销应用域名Mapper
+ */
+@Mapper
+public interface MarketingAppsDomainMapper extends BaseMapper<MarketingAppsDomain> {
+}

+ 18 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingAppsIpMapper.java

@@ -0,0 +1,18 @@
+package com.pig4cloud.pig.marketing.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.pig4cloud.pig.marketing.api.entity.MarketingAppsIp;
+import jakarta.validation.constraints.NotNull;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销应用IP Mapper
+ */
+@Mapper
+public interface MarketingAppsIpMapper extends BaseMapper<MarketingAppsIp> {
+
+}

+ 37 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingAppsMapper.java

@@ -0,0 +1,37 @@
+package com.pig4cloud.pig.marketing.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.pig4cloud.pig.marketing.api.entity.MarketingApps;
+import com.pig4cloud.pig.marketing.api.vo.app.PageStatAppFirstMktDataVO;
+import com.pig4cloud.pig.marketing.api.vo.app.PageStatAppSecondMktDataVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 营销应用Mapper
+ */
+@Mapper
+public interface MarketingAppsMapper extends BaseMapper<MarketingApps> {
+
+	List<PageStatAppFirstMktDataVO> statAppFirstMktData(
+			@Param("page") Page<PageStatAppFirstMktDataVO> page,
+			@Param("appId") String appId,
+			@Param("startTime") LocalDateTime startTime,
+			@Param("endTime") LocalDateTime endTime
+	);
+
+	List<PageStatAppSecondMktDataVO> statAppSecondMktData(
+			@Param("page") Page<PageStatAppSecondMktDataVO> page,
+			@Param("appId") String appId,
+			@Param("ip") String ip,
+			@Param("startTime") LocalDateTime startTime,
+			@Param("endTime") LocalDateTime endTime
+	);
+}

+ 15 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingConfigGroupMapper.java

@@ -0,0 +1,15 @@
+package com.pig4cloud.pig.marketing.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.pig4cloud.pig.marketing.api.entity.MarketingConfigGroup;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销配置-分组Mapper
+ */
+@Mapper
+public interface MarketingConfigGroupMapper extends BaseMapper<MarketingConfigGroup> {
+}

+ 42 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingDataMapper.java

@@ -0,0 +1,42 @@
+package com.pig4cloud.pig.marketing.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.pig4cloud.pig.marketing.api.entity.MarketingData;
+import com.pig4cloud.pig.marketing.api.vo.data.PageFirstLevelDataVO;
+import com.pig4cloud.pig.marketing.api.vo.data.PageSecondLevelDataVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 营销数据统计Mapper
+ */
+@Mapper
+public interface MarketingDataMapper extends BaseMapper<MarketingData> {
+
+	List<PageFirstLevelDataVO> countFirstLevelMarketingData(
+			@Param("ip") String ip,
+			@Param("appId") String appId,
+			@Param("lastHourTime") String lastHourTime,
+			@Param("todayStartTime") String todayStartTime,
+			@Param("offset") Long offset,
+			@Param("pageSize") Long pageSize);
+
+
+	Long countFirstLevelDataTotal(@Param("ip") String ip, @Param("appId") String appId);
+
+
+	List<PageSecondLevelDataVO> countSecondLevelMarketingData(
+			@Param("ip") String ip,
+			@Param("lastHourTime") String lastHourTime,
+			@Param("todayStartTime") String todayStartTime,
+			@Param("offset") Long offset,
+			@Param("pageSize") Long pageSize);
+
+	Long countSecondLevelDataTotal(@Param("ip") String ip);
+
+}

+ 18 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingGroupDomainMapper.java

@@ -0,0 +1,18 @@
+package com.pig4cloud.pig.marketing.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.pig4cloud.pig.marketing.api.entity.MarketingGroupDomain;
+import jakarta.validation.constraints.NotNull;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销分组域名Mapper
+ */
+@Mapper
+public interface MarketingGroupDomainMapper extends BaseMapper<MarketingGroupDomain> {
+
+}

+ 22 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/mapper/MarketingGroupIpMapper.java

@@ -0,0 +1,22 @@
+package com.pig4cloud.pig.marketing.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.pig4cloud.pig.marketing.api.entity.MarketingGroupIp;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-23
+ * @description: 营销分组IP
+ */
+@Mapper
+public interface MarketingGroupIpMapper extends BaseMapper<MarketingGroupIp> {
+
+	Boolean checkIpConflict(@Param("ipMode") Integer ipMode,
+							@Param("startIp") String startIp,
+							@Param("endIp") String endIp,
+							@Param("id") Long id,
+							@Param("groupId") Long groupId);
+}

+ 102 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/MarketingAppsService.java

@@ -0,0 +1,102 @@
+package com.pig4cloud.pig.marketing.service;
+
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.pig4cloud.pig.common.core.util.R;
+import com.pig4cloud.pig.marketing.api.dto.app.*;
+import com.pig4cloud.pig.marketing.api.vo.app.PageMarketingAppsVO;
+import jakarta.validation.Valid;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 应用列表Service
+ */
+
+public interface MarketingAppsService {
+
+	/**
+	 * 分页查询应用列表
+	 * @param reqDto 查询参数
+	 * @return Page
+	 */
+	Page pageMarketingApps(PageMarketingAppsDTO reqDto, Boolean status);
+
+	/**
+	 * 根据ID获取应用详情
+	 * @param id 应用ID
+	 * @return 应用详情
+	 */
+	PageMarketingAppsVO getMarketingAppById(Long id);
+
+	/**
+	 * 批量更新应用信息
+	 * @param reqDtoList 批量更新参数
+	 * @return List<Long>
+	 */
+	R batchUpdateMarketingApps(List<BatchUpdateMarketingAppsDTO> reqDtoList);
+
+	/**
+	 * 设置应用信息
+	 * @param reqDto 应用信息
+	 * @return Boolean
+	 */
+	Boolean setMarketingAppsInfo(SetMarketingAppsDTO reqDto);
+
+	/**
+	 * 设置应用状态
+	 * @param reqDto 设置参数
+	 * @return  Boolean
+	 */
+	Boolean setAppStatusById(SetMarketingAppsStatusDTO reqDto);
+
+	/**
+	 * 同步应用列表
+	 * @return Map<String, Integer>
+	 */
+	Map<String, Integer> syncedAppList();
+
+	/**
+	 * 修改应用ip列表
+	 * @param reqDto 修改参数
+	 * @return R
+	 */
+	R modMarketingAppsIps(@Valid ModMarketingAppsIpDTO reqDto);
+
+	/**
+	 * 修改应用域名列表
+	 * @param reqDto 修改参数
+	 * @return R
+	 */
+	R modMarketingAppsDomains(@Valid ModMarketingAppsDomainDTO reqDto);
+
+	/**
+	 * 分页统计应用一级营销数据
+	 * @param reqDto 查询参数
+	 * @return 一级营销数据
+	 */
+	Page pageStatAppFirstMktData(PageStatAppMktDataDTO reqDto);
+
+	/**
+	 * 分页统计应用二级营销数据
+	 * @param reqDto 统计参数
+	 * @param ip ip
+	 * @return 二级营销数据
+	 */
+	Page pageStatAppSecondMktData(PageStatAppMktDataDTO reqDto, String ip);
+
+	/**
+	 * 校验IP冲突
+	 * @return Boolean
+	 */
+	Boolean checkIp(MarketingAppsIpDTO ipDto, Long appId, Boolean isConfig);
+
+	/**
+	 * 校验域名冲突
+	 * @return Boolean
+	 */
+	Boolean checkDomain(MarketingAppsDomainDTO domainDTO, Long appId, Boolean isConfig);
+}

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

@@ -0,0 +1,118 @@
+package com.pig4cloud.pig.marketing.service;
+
+
+import com.pig4cloud.pig.common.core.util.R;
+import com.pig4cloud.pig.marketing.api.dto.config.*;
+import com.pig4cloud.pig.marketing.api.vo.config.*;
+import jakarta.validation.Valid;
+
+import java.util.List;
+
+
+/**
+ * @author: lwh
+ * @date: 2025-07-16
+ * @description: TODO
+ */
+
+public interface MarketingConfigService {
+	/**
+	 * 添加分组
+	 * @param reqDto 添加分组入参
+	 * @return Boolean
+	 */
+	R addMarketingConfigGroup(AddMarketingGroupDTO reqDto);
+
+	/**
+	 * 删除分组
+	 * @param reqDto 入参
+	 * @return Boolean
+	 */
+	Boolean delMarketingConfigGroup(DelMarketingGroupDTO reqDto);
+
+	/**
+	 * 根据ID查询分组详情
+	 * @param id ID
+	 * @return 分组详情
+	 */
+	R getMarketingGroupDetail(Long id);
+
+	/**
+	 * 修改域名分组
+	 * @param reqDto 修改域名分组入参
+	 * @return Boolean
+	 */
+	R modMarketingGroupDomain(ModMarketingGroupDomainDTO reqDto);
+
+	/**
+	 * 获取域名分组列表
+	 * @return 域名分组列表
+	 */
+	List<GetMarketingDomainGroupVO> getMarketingDomainGroupList();
+
+	/**
+	 * 修改分组IP
+	 * @param reqDto 修改IP分组入参
+	 * @return R
+	 */
+	R modMarketingGroupIp(ModMarketingGroupIPDTO reqDto);
+
+	/**
+	 * 获取ip分组列表
+	 * @return ip分组列表
+	 */
+	List<GetMarketingIpGroupVO> getMarketingIpGroupList();
+
+	/**
+	 * 获取IP集合
+	 * @return IP集合
+	 */
+    List<GetMarketingConfigIpListVO> getMarketingConfigIpList();
+
+	/**
+	 * 添加IP集合
+	 * @param reqDto 添加IP集合入参
+	 * @return ID
+	 */
+	Boolean addMarketingConfigIpList(@Valid AddMarketingConfigIpListDTO reqDto);
+
+	/**
+	 * 删除IP集合
+	 * @param id id
+	 * @return Boolean
+	 */
+	Boolean delMarketingConfigIpList(Long id);
+
+	/**
+	 * 获取域名集合
+	 * @return 域名集合
+	 */
+	List<GetMarketingConfigDomainListVO> getMarketingConfigDomainList();
+
+	/**
+	 * 添加域名集合
+	 * @param reqDto 添加域名集合入参
+	 * @return Boolean
+	 */
+	Boolean addMarketingConfigDomainList(@Valid AddMarketingConfigDomainListDTO reqDto);
+
+	/**
+	 * 删除域名集合
+	 * @param id id
+	 * @return Boolean
+	 */
+	Boolean delMarketingConfigDomainList(Long id);
+
+	/**
+	 * 查询全局配置信息
+	 * @return 全局配置信息
+	 */
+	GetMarketingGlobalConfigVO getMarketingGlobalConfig();
+
+	/**
+	 * 设置全局配置信息
+	 * @param reDto 全局配置信息
+	 * @return 全局配置信息
+	 */
+	Boolean setMarketingGlobalConfig(GetMarketingGlobalConfigVO reDto);
+}

+ 39 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/MarketingDataService.java

@@ -0,0 +1,39 @@
+package com.pig4cloud.pig.marketing.service;
+
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.pig4cloud.pig.marketing.api.dto.data.MarketingDataReportDTO;
+import com.pig4cloud.pig.marketing.api.dto.data.PageFirstLevelDataDTO;
+import com.pig4cloud.pig.marketing.api.dto.data.PageSecondLevelDataDTO;
+import com.pig4cloud.pig.marketing.api.vo.data.MarketingDataReportVO;
+import jakarta.servlet.http.HttpServletRequest;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 营销数据service
+ */
+
+public interface MarketingDataService {
+	/**
+	 * 上报营销数据
+	 * @param reqDto 营销数据
+	 * @param request request
+	 * @return MarketingDataReportVO
+	 */
+	MarketingDataReportVO reportMarketingData(MarketingDataReportDTO reqDto, HttpServletRequest request);
+
+	/**
+	 * 分页统计一级营销数据
+	 * @param reqDto 分页参数
+	 * @return Page
+	 */
+	Page pageFirstLevelData(PageFirstLevelDataDTO reqDto);
+
+	/**
+	 * 分页统计二级营销数据
+	 * @param reqDto 分页参数
+	 * @return Page
+	 */
+	Page pageSecondLevelData(PageSecondLevelDataDTO reqDto);
+}

+ 801 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MarketingAppsServiceImpl.java

@@ -0,0 +1,801 @@
+package com.pig4cloud.pig.marketing.service.impl;
+
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.pig4cloud.pig.admin.api.dto.SysPublicParamDTO;
+import com.pig4cloud.pig.admin.api.feign.RemoteParamService;
+import com.pig4cloud.pig.common.core.exception.BusinessException;
+import com.pig4cloud.pig.common.core.util.R;
+import com.pig4cloud.pig.common.core.util.ip.IPUtils;
+import com.pig4cloud.pig.marketing.api.dto.app.*;
+import com.pig4cloud.pig.marketing.api.entity.*;
+import com.pig4cloud.pig.marketing.api.vo.app.*;
+import com.pig4cloud.pig.marketing.mapper.*;
+import com.pig4cloud.pig.marketing.service.MarketingAppsService;
+import com.pig4cloud.pig.marketing.service.MarketingConfigService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import static com.pig4cloud.pig.marketing.api.MarketingConfigConstants.*;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 应用列表Service实现类
+ */
+@Slf4j
+@Service
+@AllArgsConstructor
+public class MarketingAppsServiceImpl implements MarketingAppsService {
+
+
+	// 从配置文件读取URL
+//	@Value("${marketing.app.url}")
+	private static final String getAppListUrl = "http://192.168.3.17:2888/ipa/getApps";
+
+	// 从配置文件读取accessKey
+//	@Value("${marketing.app.access-key}")
+	private static final String accessKey = "4ea5ba93-d222-45dc-862a-1c7fc7789d11";
+
+	private final MarketingAppsMapper appsMapper;
+
+	private final MarketingAppsIpMapper appsIpMapper;
+
+	private final MarketingAppsDomainMapper appsDomainMapper;
+
+	private final MarketingGroupDomainMapper groupDomainMapper;
+
+	private final MarketingConfigGroupMapper groupMapper;
+
+	private final MarketingGroupIpMapper groupIpMapper;
+
+	private final RemoteParamService remoteParamService;
+
+
+
+
+	/**
+	 * 分页查询应用列表
+	 * @param reqDto 查询参数
+	 * @return Page
+	 */
+	@Override
+	public Page pageMarketingApps(PageMarketingAppsDTO reqDto, Boolean status){
+		Page page = appsMapper.selectPage(new Page(reqDto.getCurrent(), reqDto.getSize()),
+				Wrappers.<MarketingApps>lambdaQuery()
+						.eq(MarketingApps::getStatus, status)
+						.like(StrUtil.isNotBlank(reqDto.getAppId()), MarketingApps::getAppId, reqDto.getAppId())
+						.like(StrUtil.isNotBlank(reqDto.getAppName()), MarketingApps::getAppName, reqDto.getAppName())
+						.like(StrUtil.isNotBlank(reqDto.getDomainType()), MarketingApps::getDomainType, reqDto.getDomainType())
+						.like(StrUtil.isNotBlank(reqDto.getRemark()), MarketingApps::getRemark, reqDto.getRemark())
+		);
+
+		List<MarketingApps> records = page.getRecords();
+		List<PageMarketingAppsVO> appsVOList = new ArrayList<>();
+		for (MarketingApps app : records) {
+			PageMarketingAppsVO appsVO = new PageMarketingAppsVO();
+			BeanUtils.copyProperties(app, appsVO);
+			// 组装IP
+			List<MarketingAppsIp> ips = appsIpMapper.selectList(new QueryWrapper<MarketingAppsIp>().eq("app_id", app.getId()));
+			List<MarketingAppsIpVO> ipVOList = new ArrayList<>();
+			for (MarketingAppsIp ip : ips) {
+				MarketingAppsIpVO ipVO = new MarketingAppsIpVO();
+				BeanUtils.copyProperties(ip, ipVO);
+				if (ip.getGroupId() != null){
+					MarketingConfigGroup group = groupMapper.selectById(ip.getGroupId());
+					if (group != null)
+						ipVO.setGroupName(group.getGroupName());
+				}
+				ipVOList.add(ipVO);
+			}
+			appsVO.setIps(ipVOList);
+			// 组装域名
+
+			List<MarketingAppsDomain> domains = appsDomainMapper.selectList(new QueryWrapper<MarketingAppsDomain>().eq("app_id", app.getId()));
+			ArrayList<MarketingAppsDomainVO> domainVOList = new ArrayList<>();
+			for (MarketingAppsDomain domain : domains) {
+				MarketingAppsDomainVO domainVO = new MarketingAppsDomainVO();
+				BeanUtils.copyProperties(domain, domainVO);
+				if (domain.getGroupId() != null){
+					MarketingConfigGroup group = groupMapper.selectById(domain.getGroupId());
+					if (group != null)
+						domainVO.setGroupName(group.getGroupName());
+				}
+				domainVOList.add(domainVO);
+			}
+			appsVO.setDomains(domainVOList);
+			appsVOList.add(appsVO);
+		}
+
+		page.setRecords(appsVOList);
+		return page;
+	}
+
+	/**
+	 * 根据ID获取应用详情
+	 * @param id 应用ID
+	 * @return 应用详情
+	 */
+	@Override
+	public PageMarketingAppsVO getMarketingAppById(Long id) {
+		MarketingApps app = appsMapper.selectById(id);
+		if (app == null){
+			throw new BusinessException("应用不存在");
+		}
+		PageMarketingAppsVO appsVO = new PageMarketingAppsVO();
+		BeanUtils.copyProperties(app, appsVO);
+		// 组装IP
+		List<MarketingAppsIp> ips = appsIpMapper.selectList(new QueryWrapper<MarketingAppsIp>().eq("app_id", app.getId()));
+		List<MarketingAppsIpVO> ipVOList = new ArrayList<>();
+		for (MarketingAppsIp ip : ips) {
+			MarketingAppsIpVO ipVO = new MarketingAppsIpVO();
+			BeanUtils.copyProperties(ip, ipVO);
+			if (ip.getGroupId() != null){
+				MarketingConfigGroup group = groupMapper.selectById(ip.getGroupId());
+				if (group != null)
+					ipVO.setGroupName(group.getGroupName());
+			}
+			ipVOList.add(ipVO);
+		}
+		appsVO.setIps(ipVOList);
+		// 组装域名
+
+		List<MarketingAppsDomain> domains = appsDomainMapper.selectList(new QueryWrapper<MarketingAppsDomain>().eq("app_id", app.getId()));
+		ArrayList<MarketingAppsDomainVO> domainVOList = new ArrayList<>();
+		for (MarketingAppsDomain domain : domains) {
+			MarketingAppsDomainVO domainVO = new MarketingAppsDomainVO();
+			BeanUtils.copyProperties(domain, domainVO);
+			if (domain.getGroupId() != null){
+				MarketingConfigGroup group = groupMapper.selectById(domain.getGroupId());
+				if (group != null)
+					domainVO.setGroupName(group.getGroupName());
+			}
+			domainVOList.add(domainVO);
+		}
+		appsVO.setDomains(domainVOList);
+		return appsVO;
+	}
+
+	/**
+	 * 批量更新应用信息
+	 * @param reqDtoList 批量更新参数
+	 * @return Long
+	 */
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public R batchUpdateMarketingApps(List<BatchUpdateMarketingAppsDTO> reqDtoList) {
+		for (BatchUpdateMarketingAppsDTO reqDto : reqDtoList) {
+			Long id = reqDto.getId();
+			if (reqDto.getId() == null){
+				throw new BusinessException("应用ID不能为空");
+			}
+
+			MarketingApps apps = appsMapper.selectById(id);
+			if (apps == null){
+				throw new BusinessException("应用ID:"+id+"不存在");
+			}
+			if (!apps.getStatus()){
+				throw new BusinessException("应用ID:"+id+"已拉黑,请还原后操作");
+			}
+
+			// 1、更新基本信息
+			MarketingApps marketingApps = new MarketingApps();
+			BeanUtils.copyProperties(reqDto, marketingApps);
+			appsMapper.updateById(marketingApps);
+
+
+			// 2. 处理IP信息(包含分组检查)
+			handleIpOperations(reqDto, id);
+
+			// 3. 处理域名信息(包含分组检查)
+			handleDomainOperations(reqDto, id);
+		}
+		return R.ok();
+	}
+
+	/**
+	 * 设置应用信息
+	 * @param reqDto 更新参数
+	 * @return Long
+	 */
+	@Override
+	public Boolean setMarketingAppsInfo(SetMarketingAppsDTO reqDto) {
+		MarketingApps app = appsMapper.selectById(reqDto.getId());
+		if (app == null){
+			throw new BusinessException("应用不存在");
+		}
+		if (!app.getStatus()){
+			throw new BusinessException("应用ID:"+app.getId()+"已拉黑,请还原后操作");
+		}
+		MarketingApps apps = new MarketingApps();
+		BeanUtils.copyProperties(reqDto, apps);
+		int i = appsMapper.updateById(apps);
+		return i>0 ;
+	}
+
+	/**
+	 * 设置应用状态
+	 * @param reqDto 设置参数
+	 * @return  Boolean
+	 */
+	@Override
+	public Boolean setAppStatusById(SetMarketingAppsStatusDTO reqDto) {
+		MarketingApps apps = appsMapper.selectById(reqDto.getId());
+		if (apps == null) {
+			throw new BusinessException("应用不存在");
+		}
+		apps.setStatus(reqDto.getStatus());
+		return appsMapper.updateById(apps) > 0;
+	}
+
+	/**
+	 * 同步应用列表
+	 *
+	 * @return 应用列表大小
+	 */
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public Map<String, Integer> syncedAppList() {
+		List<MarketingApps> appList = getAppsFromLafaService();
+
+
+		// 初始化统计结果
+		Map<String, Integer> result = new HashMap<>();
+		int addCount = 0;
+		int updateCount = 0;
+		int deleteCount = 0;
+
+		// 1. 获取lafa服务应用的appId集合
+		Set<String> lafaAppIds = appList.stream()
+				.map(MarketingApps::getAppId)
+				.collect(Collectors.toSet());
+
+		// 2. 查询本地数据库中所有未删除的应用
+		List<MarketingApps> localApps = appsMapper.selectList(new QueryWrapper<>());
+
+		// 3. 将本地应用按appId分组,便于快速查询
+		Map<String, MarketingApps> localAppMap = localApps.stream()
+				.collect(Collectors.toMap(MarketingApps::getAppId, app -> app));
+
+		// 4. 获取全局配置
+		AtomicReference<Integer> triggerRule = new AtomicReference<>();
+		AtomicReference<String> triggerNum = new AtomicReference<>("");
+		R<ArrayList<SysPublicParamDTO>> ParamDTO = remoteParamService.getParamList(Arrays.asList(TRIGGER_RULE, TRIGGER_NUM));
+
+		ParamDTO.getData().forEach(param -> {
+			if (TRIGGER_RULE.equals(param.getPublicKey())) {
+				triggerRule.set(Integer.valueOf(param.getPublicValue()));
+			} else if (TRIGGER_NUM.equals(param.getPublicKey())) {
+				triggerNum.set(param.getPublicValue());
+			}
+		});
+
+		// 5. 处理新增和更新
+		for (MarketingApps app : appList) {
+			MarketingApps localApp = localAppMap.get(app.getAppId());
+
+			if (localApp == null) {
+				// 4.1 本地不存在,新增
+				app.setStatus( true);
+				app.setDomainLimit(false);
+				app.setLaunch( true);
+				app.setTriggerRule(triggerRule.get());
+				app.setTriggerNum(triggerNum.get());
+				appsMapper.insert(app);
+				addCount++;
+			} else {
+				// 4.2 本地存在,判断是否需要更新
+				// 复制ID(本地自增ID)和创建时间(保留原始创建时间)
+				app.setId(localApp.getId());
+
+				// 对比是否有变化
+				if (!isSameApp(app, localApp)) {
+					appsMapper.updateById(app);
+					updateCount++;
+				}
+			}
+		}
+
+		// 5. 处理删除:本地存在但A服务不存在的应用,标记为删除
+		for (MarketingApps localApp : localApps) {
+			if (!lafaAppIds.contains(localApp.getAppId())) {
+				appsMapper.deleteById(localApp);
+				// 删除应用关联的IP、域名
+				appsIpMapper.delete(Wrappers.<MarketingAppsIp>lambdaQuery().eq(MarketingAppsIp::getAppId, localApp.getId()));
+				appsDomainMapper.delete(Wrappers.<MarketingAppsDomain>lambdaQuery().eq(MarketingAppsDomain::getAppId, localApp.getId()));
+				deleteCount++;
+			}
+		}
+
+		// 6. 组装结果
+		result.put("add", addCount);
+		result.put("update", updateCount);
+		result.put("delete", deleteCount);
+		return result;
+	}
+
+	/**
+	 * 修改应用IP列表
+	 * @param reqDto 修改参数
+	 * @return R
+	 */
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public R modMarketingAppsIps(ModMarketingAppsIpDTO reqDto) {
+		Long appId = reqDto.getId();
+		if (reqDto.getId() == null) {
+			throw new BusinessException("应用ID不能为空");
+		}
+		MarketingApps app = appsMapper.selectById(appId);
+		if (app == null){
+			throw new BusinessException("应用ID:"+appId+"不存在");
+		}
+		if (!app.getStatus()){
+			throw new BusinessException("应用ID:"+app.getId()+"已拉黑,请还原后操作");
+		}
+		BatchUpdateMarketingAppsDTO updateIp = new BatchUpdateMarketingAppsDTO();
+		updateIp.setId(appId);
+		updateIp.setIps(reqDto.getIps());
+		updateIp.setDelIps(reqDto.getDelIps());
+		// 处理IP信息
+		handleIpOperations(updateIp, appId);
+		return  R.ok();
+	}
+
+	/**
+	 * 修改应用域名列表
+	 * @param reqDto 修改参数
+	 * @return R
+	 */
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public R modMarketingAppsDomains(ModMarketingAppsDomainDTO reqDto) {
+		Long appId = reqDto.getId();
+		if (reqDto.getId() == null){
+			throw new BusinessException("应用ID不能为空");
+		}
+		MarketingApps app = appsMapper.selectById(appId);
+		if (app == null){
+			throw new BusinessException("应用ID:"+appId+"不存在");
+		}
+		if (!app.getStatus()){
+			throw new BusinessException("应用ID:"+app.getId()+"已拉黑,请还原后操作");
+		}
+		BatchUpdateMarketingAppsDTO updateDomain = new BatchUpdateMarketingAppsDTO();
+		updateDomain.setId(appId);
+		updateDomain.setDomains(reqDto.getDomains());
+		updateDomain.setDelDomains(reqDto.getDelDomains());
+		// 处理域名信息
+		handleDomainOperations(updateDomain, appId);
+		return R.ok();
+	}
+
+	/**
+	 * 分页统计应用一级营销数据
+	 * @param reqDto 查询参数
+	 * @return 一级营销数据
+	 */
+	@Override
+	public Page pageStatAppFirstMktData(PageStatAppMktDataDTO reqDto) {
+		Page<PageStatAppFirstMktDataVO> page = new Page<>(reqDto.getCurrent(), reqDto.getSize());
+
+		LocalDateTime[] timeRange = reqDto.getTimeRange();
+		LocalDateTime startTime = timeRange != null && timeRange.length > 0 ? timeRange[0] : null;
+		LocalDateTime endTime = timeRange != null && timeRange.length > 1 ? timeRange[1] : null;
+
+		List<PageStatAppFirstMktDataVO> records = appsMapper.statAppFirstMktData(page, reqDto.getAppId(), startTime, endTime);
+
+		for (PageStatAppFirstMktDataVO record : records) {
+			String ip = record.getIp();
+			String region = IPUtils.getIpRegion(ip);
+			record.setRegion(region);
+		}
+		page.setRecords(records);
+		return page;
+	}
+
+	/**
+	 * 分页统计应用二级营销数据
+	 * @param reqDto 统计参数
+	 * @return 二级营销数据
+	 */
+	@Override
+	public Page pageStatAppSecondMktData(PageStatAppMktDataDTO reqDto, String ip) {
+		Page<PageStatAppSecondMktDataVO> page = new Page<>(reqDto.getCurrent(), reqDto.getSize());
+
+		LocalDateTime[] timeRange = reqDto.getTimeRange();
+		LocalDateTime startTime = timeRange != null && timeRange.length > 0 ? timeRange[0] : null;
+		LocalDateTime endTime = timeRange != null && timeRange.length > 1 ? timeRange[1] : null;
+
+		// 调用mapper查询,自动填充分页信息
+		List<PageStatAppSecondMktDataVO> records = appsMapper.statAppSecondMktData(page, reqDto.getAppId(), ip, startTime, endTime);
+		page.setRecords(records);
+		return page;
+	}
+
+	/**
+	 * 从lafa服务获取应用列表
+	 * @return 应用列表
+	 */
+	public List<MarketingApps> getAppsFromLafaService(){
+
+		// 构建请求参数
+		JSONObject requestParam = new JSONObject();
+		requestParam.put("accessKey", accessKey);
+		// 创建OkHttp客户端
+		OkHttpClient client = new OkHttpClient();
+
+		// 构建POST请求体
+		MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
+		RequestBody body = RequestBody.create(mediaType, requestParam.toJSONString());
+
+		// 构建请求
+		Request request = new Request.Builder()
+				.url(getAppListUrl)
+				.post(body)
+				.build();
+
+		try {
+			// 发送请求并获取响应
+			Response response = client.newCall(request).execute();
+			ArrayList<MarketingApps> appList = new ArrayList<>();
+			if (response.isSuccessful() && response.body() != null) {
+				JSONObject responseBody = JSONObject.parseObject(response.body().string());
+				JSONArray apps = responseBody.getJSONArray("apps");
+				apps.forEach(item -> {
+					JSONObject app = (JSONObject) item;
+					MarketingApps marketingApps = new MarketingApps();
+					marketingApps.setAppId(app.getString("id"));
+					marketingApps.setAppName(app.getString("name"));
+					marketingApps.setAppImg(app.getString("img"));
+					marketingApps.setAppUrl("https://"+app.getString("download_url")+"/"+marketingApps.getAppId());
+					marketingApps.setBackupUrl("https://"+app.getString("backup_url")+"/"+marketingApps.getAppId());
+					marketingApps.setDomainType(app.getInteger("type"));
+					appList.add(marketingApps);
+				});
+				return appList;
+			} else {
+				// 响应失败
+				log.error("请求失败,响应码: {}", response.code());
+			}
+			return null;
+		} catch (IOException e) {
+			// 网络请求异常
+			log.error("网络请求异常: {}", e.getLocalizedMessage());
+			return null;
+		}
+	}
+
+	/**
+	 * 对比两个应用是否相同
+	 * @param newApp 新数据
+	 * @param oldApp 旧数据
+	 * @return 是否相同
+	 */
+	private boolean isSameApp(MarketingApps newApp, MarketingApps oldApp) {
+		// 比较核心业务字段
+		return Objects.equals(newApp.getAppId(), oldApp.getAppId()) &&
+				Objects.equals(newApp.getAppName(), oldApp.getAppName()) &&
+				Objects.equals(newApp.getAppImg(), oldApp.getAppImg()) &&
+				Objects.equals(newApp.getAppUrl(), oldApp.getAppUrl()) &&
+				Objects.equals(newApp.getBackupUrl(), oldApp.getBackupUrl()) &&
+				Objects.equals(newApp.getDomainType(), oldApp.getDomainType());
+	}
+
+	/**
+	 * 处理IP的新增/修改/删除,包含分组存在性检查
+	 */
+	private void handleIpOperations(BatchUpdateMarketingAppsDTO reqDto, Long appId) throws BusinessException {
+		// 2.1 删除指定IP
+		if (reqDto.getDelIps() != null && !reqDto.getDelIps().isEmpty()) {
+			appsIpMapper.deleteByIds(reqDto.getDelIps());
+		}
+
+		// 2.2 新增或修改IP
+		if (reqDto.getIps() != null && !reqDto.getIps().isEmpty()) {
+			for (MarketingAppsIpDTO ipDto : reqDto.getIps()) {
+				if (ipDto.getId() != null && (ipDto.getModify()==null || !ipDto.getModify())){
+					continue;
+				}
+				// 去除无用数据
+				if (ipDto.getSourceType() == 1){
+					ipDto.setIpMode(null);
+					ipDto.setStartIp(null);
+					ipDto.setEndIp( null);
+
+					// 查询分组是否存在
+					MarketingConfigGroup group = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+							.eq(MarketingConfigGroup::getId, ipDto.getGroupId())
+							.eq(MarketingConfigGroup::getGroupType, 1)
+					);
+					if (group == null){
+						throw new BusinessException("分组ID:"+ipDto.getGroupId()+"不存在");
+					}
+				}else {
+					ipDto.setGroupId(null);
+					ipDto.setGroupName(null);
+					if (ipDto.getIpMode() == 1){
+						ipDto.setEndIp(null);
+					}
+				}
+
+				// 查询ip表和ip分组表中是否存在
+				if (checkIp(ipDto, appId, false)){
+					if (ipDto.getSourceType() == 1){
+						throw new BusinessException("分组ID:"+ipDto.getGroupId()+"已存在");
+					}
+					if (ipDto.getIpMode() == 1){
+						throw new BusinessException("ip:"+ipDto.getStartIp()+"已存在");
+					}
+					String[] end = ipDto.getEndIp().split("\\.");
+					throw new BusinessException("ip:"+ipDto.getStartIp()+"/"+end[3]+"已存在、或有重叠");
+				}
+
+				// 执行新增或修改
+				MarketingAppsIp appsIp = new MarketingAppsIp();
+				BeanUtils.copyProperties(ipDto, appsIp);
+				appsIp.setAppId(appId);
+
+				if (ipDto.getId() == null) {
+					appsIp.setConfig(false);
+					appsIpMapper.insert(appsIp);
+				} else if (ipDto.getModify() != null && ipDto.getModify()) {
+					appsIpMapper.updateById(appsIp);
+				}
+			}
+		}
+	}
+
+	/**
+	 * 处理域名的新增/修改/删除,包含完整校验逻辑
+	 */
+	private void handleDomainOperations(BatchUpdateMarketingAppsDTO reqDto, Long appId) throws BusinessException {
+		// 3.1 删除指定域名
+		if (reqDto.getDelDomains() != null && !reqDto.getDelDomains().isEmpty()) {
+			appsDomainMapper.deleteBatchIds(reqDto.getDelDomains());
+		}
+
+		// 3.2 新增或修改域名
+		if (reqDto.getDomains() != null && !reqDto.getDomains().isEmpty()) {
+			for (MarketingAppsDomainDTO domainDto : reqDto.getDomains()) {
+				if (domainDto.getId() != null && (domainDto.getModify()==null || !domainDto.getModify())){
+					continue;
+				}
+
+				// 去除无用数据
+				if (domainDto.getSourceType() == 1){
+					domainDto.setDomain(null);
+
+					// 查询分组是否存在
+					MarketingConfigGroup group = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+							.eq(MarketingConfigGroup::getId, domainDto.getGroupId())
+							.eq(MarketingConfigGroup::getGroupType, 2)
+					);
+					if (group == null){
+						throw new BusinessException("分组ID:"+domainDto.getGroupId()+"不存在");
+					}
+				}else {
+					domainDto.setGroupId(null);
+					domainDto.setGroupName(null);
+				}
+
+				if (checkDomain(domainDto, appId, false)){
+					if (domainDto.getSourceType() == 1){
+						throw new BusinessException("分组ID:"+domainDto.getGroupId()+"已存在");
+					}
+					throw new BusinessException("域名:"+domainDto.getDomain()+"已存在");
+				}
+
+				// 执行新增或修改
+				MarketingAppsDomain appsDomain = new MarketingAppsDomain();
+				BeanUtils.copyProperties(domainDto, appsDomain);
+				appsDomain.setAppId(appId);
+
+				if (domainDto.getId() == null) {
+					appsDomain.setConfig(false);
+					appsDomainMapper.insert(appsDomain);
+				} else if (domainDto.getModify() != null && domainDto.getModify()) {
+					appsDomainMapper.updateById(appsDomain);
+				}
+			}
+		}
+	}
+
+	/**
+	 * 判断IP是否已存在(单IP或IP段)
+	 * @param appId 应用数据ID
+	 * @param ipDto IP
+	 * @return 是否存在
+	 */
+	@Override
+	public Boolean checkIp(MarketingAppsIpDTO ipDto, Long appId, Boolean isConfig) {
+		List<Long> groupIds = getAppRelatedGroupIds(appId, true, isConfig);
+		if (ipDto.getSourceType().equals(1)){
+			return groupIds.contains(ipDto.getGroupId());
+		}
+		// 1. 检查应用IP表
+		if (checkAppsIpExists(ipDto, appId, isConfig)) {
+			return true;
+		}
+
+		// 2. 检查分组IP表(需要关联应用关联的分组)
+		return checkGroupIpExists(ipDto, groupIds);
+	}
+
+	/**
+	 * 判断域名是否已存在
+	 * @param domainDTO 域名信息DTO
+	 * @param appId 应用ID
+	 * @return 是否存在
+	 */
+	public Boolean checkDomain(MarketingAppsDomainDTO domainDTO, Long appId, Boolean isConfig) {
+		List<Long> groupIds = getAppRelatedGroupIds(appId, false, isConfig);  // 需要实现获取应用关联的分组ID列表
+		if (domainDTO.getSourceType().equals(1)){
+			// 来自分组
+			return groupIds.contains(domainDTO.getGroupId());
+		}
+
+		// 1. 检查应用域名表
+		LambdaQueryWrapper<MarketingAppsDomain> appsQuery = new LambdaQueryWrapper<>();
+		appsQuery.eq(appId != null, MarketingAppsDomain::getAppId, appId)
+				.eq(MarketingAppsDomain::getSourceType, 2)
+				.eq(MarketingAppsDomain::getDomain, domainDTO.getDomain())
+				.eq(MarketingAppsDomain::getConfig, isConfig);
+		if (appsDomainMapper.selectCount(appsQuery) > 0) {
+			return true;
+		}
+
+		// 2. 检查分组域名表(需要关联应用关联的分组)
+		if (!groupIds.isEmpty()) {
+			LambdaQueryWrapper<MarketingGroupDomain> groupQuery = new LambdaQueryWrapper<>();
+			groupQuery.in(MarketingGroupDomain::getGroupId, groupIds)
+					.eq(MarketingGroupDomain::getDomain, domainDTO.getDomain());
+			return groupDomainMapper.selectCount(groupQuery) > 0;
+		}
+		return false;
+	}
+
+	/**
+	 * 检查应用IP表中是否存在冲突
+	 */
+	private Boolean checkAppsIpExists(MarketingAppsIpDTO ipDto,Long appId, Boolean isConfig){
+		// 1、校验该appId应用下ip表,及ip分组表中,是否存在该ip
+		// 1.1、校验marketing_apps_ip表,appId=appId,是否包含
+		// 1.2、校验marketing_group_ip表,是否包含ip,如果包含查出分组id,在查看appId下是否包含这个分组
+		QueryWrapper<MarketingAppsIp> query = new QueryWrapper<>();
+		query.eq("source_type",2).eq("config", isConfig?1:0);
+		if (appId != null){
+			query.eq("app_id", appId);
+		}
+
+		query.and(wrapper -> {
+			// 单IP模式检查
+			if (ipDto.getIpMode() == 1) {
+				String targetIp = ipDto.getStartIp();
+				// 检查是否有相同单IP
+				wrapper.and(w -> w
+						.eq("ip_mode", 1)
+						.eq("start_ip", targetIp));
+
+				// 检查是否被IP段包含
+				wrapper.or(w -> w
+						.eq("ip_mode", 2)
+						.apply("INET_ATON({0}) BETWEEN INET_ATON(start_ip) AND INET_ATON(end_ip)", targetIp));
+
+			}
+
+			// IP段模式检查
+			if (ipDto.getIpMode() == 2) {
+				String startIp = ipDto.getStartIp();
+				String endIp = ipDto.getEndIp();
+
+				// 检查是否与单IP重叠:单IP在输入IP段范围内
+				wrapper.and(w -> w
+						.eq("ip_mode", 1)
+						.apply("INET_ATON(start_ip) BETWEEN INET_ATON({0}) AND INET_ATON({1})", startIp, endIp));
+				// 检查是否与IP段重叠:两个IP段有交集
+				wrapper.or(w -> w
+						.eq("ip_mode", 2)
+						.and(w2 -> w2
+								.apply("INET_ATON(start_ip) BETWEEN INET_ATON({0}) AND INET_ATON({1})", startIp, endIp)
+								.or().apply("INET_ATON(end_ip) BETWEEN INET_ATON({0}) AND INET_ATON({1})", startIp, endIp)
+								.or().apply("INET_ATON({0}) BETWEEN INET_ATON(start_ip) AND INET_ATON(end_ip)", startIp)
+						)
+				);
+			}
+		});
+		return appsIpMapper.selectCount(query) > 0;
+	}
+
+	/**
+	 * 检查分组IP表中是否存在冲突
+	 */
+	private boolean checkGroupIpExists(MarketingAppsIpDTO ipDTO, List<Long> groupIds) {
+		if (groupIds.isEmpty()) {
+			return false;
+		}
+
+		QueryWrapper<MarketingGroupIp> query = new QueryWrapper<>();
+		query.in("group_id", groupIds);
+
+		// 单IP模式
+		if (ipDTO.getIpMode() == 1) {
+			String targetIp = ipDTO.getStartIp();
+			// 检查是否有相同单IP
+			query.and(wrapper -> wrapper
+					.eq("ip_mode", 1)
+					.eq("start_ip", targetIp));
+
+			// 检查是否被IP段包含
+			query.or(wrapper -> wrapper
+					.eq("ip_mode", 2)
+					.apply("INET_ATON({0}) BETWEEN INET_ATON(start_ip) AND INET_ATON(end_ip)", targetIp));
+		}
+
+		// IP段模式检查
+		if (ipDTO.getIpMode() == 2) {
+			String startIp = ipDTO.getStartIp();
+			String endIp = ipDTO.getEndIp();
+
+			query.and(wrapper -> wrapper
+					.eq("ip_mode", 1)
+					.apply("INET_ATON(start_ip) BETWEEN INET_ATON({0}) AND INET_ATON({1})", startIp, endIp));
+
+			query.or(wrapper -> wrapper
+					.eq("ip_mode", 2)
+					.and(w2 -> w2
+							.apply("INET_ATON(start_ip) BETWEEN INET_ATON({0}) AND INET_ATON({1})", startIp, endIp)
+							.or().apply("INET_ATON(end_ip) BETWEEN INET_ATON({0}) AND INET_ATON({1})", startIp, endIp)
+							.or().apply("INET_ATON({0}) BETWEEN INET_ATON(start_ip) AND INET_ATON(end_ip)", startIp)
+					)
+			);
+		}
+		return groupIpMapper.selectCount(query) > 0;
+	}
+
+	/**
+	 * 获取应用关联的所有分组ID
+	 */
+	private List<Long> getAppRelatedGroupIds(Long appId, Boolean isIp, Boolean isConfig) {
+		// 示例实现,实际需根据业务调整
+		if (isIp) {
+			LambdaQueryWrapper<MarketingAppsIp> query = new LambdaQueryWrapper<>();
+			query.eq(appId!=null ,MarketingAppsIp::getAppId, appId)
+					.eq(MarketingAppsIp::getConfig, isConfig)
+					.eq(MarketingAppsIp::getSourceType, 1)  // 来源类型为分组
+					.select(MarketingAppsIp::getGroupId)
+					.groupBy(MarketingAppsIp::getGroupId);
+			return appsIpMapper.selectObjs(query).stream()
+					.map(obj -> (Long) obj)
+					.toList();
+		}else {
+			LambdaQueryWrapper<MarketingAppsDomain> query = new LambdaQueryWrapper<>();
+			query.eq(appId!=null, MarketingAppsDomain::getAppId, appId)
+					.eq(MarketingAppsDomain::getConfig, isConfig)
+					.eq(MarketingAppsDomain::getSourceType, 1)  // 来源类型为分组
+					.select(MarketingAppsDomain::getGroupId)
+					.groupBy(MarketingAppsDomain::getGroupId);
+			return appsDomainMapper.selectObjs(query).stream()
+					.map(obj -> (Long) obj)
+					.toList();
+		}
+	}
+
+
+}

+ 630 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MarketingConfigServiceImpl.java

@@ -0,0 +1,630 @@
+package com.pig4cloud.pig.marketing.service.impl;
+
+
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.pig4cloud.pig.admin.api.dto.SysPublicParamDTO;
+import com.pig4cloud.pig.admin.api.feign.RemoteParamService;
+import com.pig4cloud.pig.common.core.exception.BusinessException;
+import com.pig4cloud.pig.common.core.util.R;
+import com.pig4cloud.pig.common.core.util.ip.IPUtils;
+import com.pig4cloud.pig.marketing.api.dto.app.MarketingAppsDomainDTO;
+import com.pig4cloud.pig.marketing.api.dto.app.MarketingAppsIpDTO;
+import com.pig4cloud.pig.marketing.api.dto.config.*;
+import com.pig4cloud.pig.marketing.api.entity.*;
+import com.pig4cloud.pig.marketing.api.util.DomainValidationUtil;
+import com.pig4cloud.pig.marketing.api.vo.config.*;
+import com.pig4cloud.pig.marketing.mapper.*;
+import com.pig4cloud.pig.marketing.service.MarketingAppsService;
+import com.pig4cloud.pig.marketing.service.MarketingConfigService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.*;
+
+import static com.pig4cloud.pig.marketing.api.MarketingConfigConstants.*;
+
+
+/**
+ * @author: lwh
+ * @date: 2025-07-16
+ * @description: 营销全局配置服务
+ */
+
+@Service
+@Slf4j
+@AllArgsConstructor
+public class MarketingConfigServiceImpl implements MarketingConfigService {
+
+	private final MarketingConfigGroupMapper groupMapper;
+
+	private final MarketingGroupDomainMapper groupDomainMapper;
+
+	private final MarketingGroupIpMapper groupIpMapper;
+
+	private final MarketingAppsIpMapper ipMapper;
+
+	private final MarketingAppsDomainMapper domainMapper;
+
+	private  final MarketingAppsService appsService;
+
+	private final RemoteParamService remoteParamService;
+
+	/**
+	 * 添加分组
+	 * @param reqDto 分组信息
+	 * @return Boolean
+	 */
+	@Override
+	public R addMarketingConfigGroup(AddMarketingGroupDTO reqDto) {
+		// 先查询是否已存在
+		MarketingConfigGroup group = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+				.eq(MarketingConfigGroup::getGroupName, reqDto.getGroupName())
+				.eq(MarketingConfigGroup::getGroupType, reqDto.getGroupType())
+		);
+		if (group != null){
+			return R.failed("分组名已存在");
+		}
+		MarketingConfigGroup marketingConfigGroup = new MarketingConfigGroup();
+		BeanUtils.copyProperties(reqDto, marketingConfigGroup);
+		return groupMapper.insert(marketingConfigGroup) > 0?R.ok():R.failed();
+	}
+
+	/**
+	 * 删除分组
+	 * @param reqDto 入参
+	 * @return Boolean
+	 */
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public Boolean delMarketingConfigGroup(DelMarketingGroupDTO reqDto) {
+		List<Long> ids = reqDto.getIds();
+		for (Long id : reqDto.getIds()) {
+			MarketingConfigGroup group = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+					.eq(MarketingConfigGroup::getId, id)
+					.eq(MarketingConfigGroup::getGroupType, reqDto.getGroupType())
+			);
+			if (group == null){
+				if (reqDto.getGroupType() == 1)
+					throw new BusinessException("分组id:"+id+",IP分组不存在");
+				else
+					throw new BusinessException("分组id:"+id+",域名分组不存在");
+			}
+		}
+
+		// 删除分组
+		int delGroup = groupMapper.deleteByIds(ids);
+		// 删除分组下域名或ip
+		if (reqDto.getGroupType() == 1){
+			// 删除IP
+			groupIpMapper.delete(Wrappers.<MarketingGroupIp>lambdaQuery().in(MarketingGroupIp::getGroupId, ids));
+			// 删除应用、全局配置关联的IP分组
+			ipMapper.delete(Wrappers.<MarketingAppsIp>lambdaQuery().in(MarketingAppsIp::getGroupId, ids));
+		}else if (reqDto.getGroupType() == 2){
+			// 删除域名
+			groupDomainMapper.delete(Wrappers.<MarketingGroupDomain>lambdaQuery().in(MarketingGroupDomain::getGroupId, ids));
+			// 删除应用、全局配置关联的域名分组
+			domainMapper.delete(Wrappers.<MarketingAppsDomain>lambdaQuery().in(MarketingAppsDomain::getGroupId, ids));
+		}
+		return delGroup > 0;
+	}
+
+	/**
+	 * 根据ID查询分组详情
+	 * @param id ID
+	 * @return 分组详情
+	 */
+	@Override
+	public R getMarketingGroupDetail(Long id) {
+		MarketingConfigGroup group = groupMapper.selectById(id);
+		if (group == null){
+			throw new BusinessException("id:"+id+",分组不存在");
+		}
+		if (group.getGroupType().equals(1)){
+			// IP类型
+			GetMarketingIpGroupVO rspVO = new GetMarketingIpGroupVO();
+			rspVO.setId(group.getId());
+			rspVO.setGroupName(group.getGroupName());
+
+			// 查询分组下IP列表
+			List<MarketingGroupIp> ipList = groupIpMapper.selectList(Wrappers.<MarketingGroupIp>lambdaQuery().eq(MarketingGroupIp::getGroupId, group.getId()));
+			ArrayList<MarketingGroupIpVO> ips = new ArrayList<>();
+			for (MarketingGroupIp ip : ipList) {
+				MarketingGroupIpVO ipVO = new MarketingGroupIpVO();
+				BeanUtils.copyProperties(ip, ipVO);
+				ips.add(ipVO);
+			}
+			rspVO.setIps(ips);
+			return R.ok(rspVO);
+		}else if (group.getGroupType().equals(2)){
+			// 域名类型
+			GetMarketingDomainGroupVO rspVO = new GetMarketingDomainGroupVO();
+			rspVO.setId(group.getId());
+			rspVO.setGroupName(group.getGroupName());
+
+			// 查询分组下域名列表
+			List<MarketingGroupDomain> domains = groupDomainMapper.selectList(Wrappers.<MarketingGroupDomain>lambdaQuery()
+					.eq(MarketingGroupDomain::getGroupId, group.getId()));
+
+			ArrayList<MarketingGroupDomainVO> domainVOS = new ArrayList<>();
+			for (MarketingGroupDomain domain : domains) {
+				MarketingGroupDomainVO domainVO = new MarketingGroupDomainVO();
+				BeanUtils.copyProperties(domain, domainVO);
+				domainVOS.add(domainVO);
+			}
+			rspVO.setDomains(domainVOS);
+			return R.ok(rspVO);
+		}
+		return R.failed();
+	}
+
+	/**
+	 * 修改分组域名
+	 * @param reqDto 分组域名信息
+	 * @return Boolean
+	 */
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public R modMarketingGroupDomain(ModMarketingGroupDomainDTO reqDto) {
+		//先查询分组是否存在
+		MarketingConfigGroup group = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+				.eq(MarketingConfigGroup::getId, reqDto.getId())
+				.eq(MarketingConfigGroup::getGroupType, 2)
+		);
+
+		if (group == null){
+			throw new BusinessException("分组id:"+reqDto.getId()+",分组不存在");
+		}
+
+		// 在查询域名是否重复
+		MarketingConfigGroup groupExist = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+				.eq(MarketingConfigGroup::getGroupType, "2")
+				.eq(MarketingConfigGroup::getGroupName, reqDto.getGroupName()));
+		if (groupExist != null && !groupExist.getId().equals(reqDto.getId())){
+			return R.failed("分组名已存在");
+		}
+		// 更新分组信息
+		group.setGroupName(reqDto.getGroupName());
+		group.setRemark(reqDto.getRemark());
+		groupMapper.updateById(group);
+
+		// 删除域名
+		groupDomainMapper.deleteByIds(reqDto.getDelList());
+
+		// 更新域名信息
+		List<MarketingGroupDomainDTO> domains = reqDto.getDomains();
+		for (MarketingGroupDomainDTO domainDto : domains) {
+			if (domainDto.getId() != null && (domainDto.getModify()==null || !domainDto.getModify())){
+				continue;
+			}
+
+
+			// 域名格式校验
+			if(!DomainValidationUtil.isValidDomain(domainDto.getDomain())){
+				throw new BusinessException("域名格式不合法(支持字母、数字、下划线、横线、点)");
+			}
+
+			// 校验域名是否重复
+			MarketingGroupDomain domainExist = groupDomainMapper.selectOne(Wrappers.<MarketingGroupDomain>lambdaQuery()
+					.eq(MarketingGroupDomain::getDomain, domainDto.getDomain())
+					.eq(MarketingGroupDomain::getGroupId, reqDto.getId())
+			);
+			if (domainExist != null && !domainExist.getId().equals(domainDto.getId())){
+				return R.failed("域名:"+domainDto.getDomain()+"已存在");
+			}
+
+			// 新增、修改
+			MarketingGroupDomain domain = new MarketingGroupDomain();
+			BeanUtils.copyProperties(domainDto, domain);
+			domain.setGroupId(reqDto.getId());
+			if (domainDto.getId() == null){
+				// 新增
+				groupDomainMapper.insert(domain);
+			} else if (domainDto.getModify() != null && domainDto.getModify()){
+				// 修改
+				groupDomainMapper.updateById(domain);
+			}
+		}
+		return R.ok();
+	}
+
+	/**
+	 * 获取域名分组列表
+	 * @return 域名分组列表
+	 */
+	@Override
+	public List<GetMarketingDomainGroupVO> getMarketingDomainGroupList() {
+		List<MarketingConfigGroup> domainGroups = groupMapper.selectList(Wrappers.<MarketingConfigGroup>lambdaQuery().eq(MarketingConfigGroup::getGroupType, 2));
+
+		ArrayList<GetMarketingDomainGroupVO> result = new ArrayList<>();
+		for (MarketingConfigGroup domainGroup : domainGroups) {
+			GetMarketingDomainGroupVO rspVO = new GetMarketingDomainGroupVO();
+			rspVO.setId(domainGroup.getId());
+			rspVO.setGroupName(domainGroup.getGroupName());
+
+			// 查询分组下域名列表
+			List<MarketingGroupDomain> domains = groupDomainMapper.selectList(Wrappers.<MarketingGroupDomain>lambdaQuery().eq(MarketingGroupDomain::getGroupId, domainGroup.getId()));
+			ArrayList<MarketingGroupDomainVO> domainVOS = new ArrayList<>();
+			for (MarketingGroupDomain domain : domains) {
+				MarketingGroupDomainVO domainVO = new MarketingGroupDomainVO();
+				BeanUtils.copyProperties(domain, domainVO);
+				domainVOS.add(domainVO);
+			}
+			rspVO.setDomains(domainVOS);
+			result.add(rspVO);
+		}
+		return result;
+	}
+
+	/**
+	 * 修改分组IP
+	 * @param reqDto 修改IP分组入参
+	 * @return R
+	 */
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public R modMarketingGroupIp(ModMarketingGroupIPDTO reqDto) {
+		//先查询分组是否存在
+		MarketingConfigGroup group = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+				.eq(MarketingConfigGroup::getId, reqDto.getId())
+				.eq(MarketingConfigGroup::getGroupType, 1)
+		);
+
+		if (group == null){
+			throw new BusinessException("分组id:"+reqDto.getId()+",分组不存在");
+		}
+
+		MarketingConfigGroup groupExist = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+				.eq(MarketingConfigGroup::getGroupType, "1")
+				.eq(MarketingConfigGroup::getGroupName, reqDto.getGroupName())
+		);
+		if (groupExist != null && !groupExist.getId().equals(reqDto.getId())){
+			return R.failed("分组名已存在");
+		}
+
+		// 更新分组信息
+		 group.setGroupName(reqDto.getGroupName());
+		 group.setRemark(reqDto.getRemark());
+		 groupMapper.updateById(group);
+
+		// 删除分组ip
+		 groupIpMapper.deleteByIds(reqDto.getDelList());
+
+		 // 更新IP信息
+		List<MarketingGroupIPDTO> ips = reqDto.getIps();
+		for (MarketingGroupIPDTO ipDto : ips) {
+			if (ipDto.getId() != null && (ipDto.getModify()==null || !ipDto.getModify())){
+				continue;
+			}
+			// 去除无用数据
+			if (ipDto.getIpMode() == 1){
+				ipDto.setEndIp(null);
+			}
+
+			// ip格式校验
+			if (!IPUtils.isValidIp(ipDto.getStartIp())){
+				throw new BusinessException("IP格式不合法(IPv4)");
+			}
+			if (ipDto.getIpMode() == 2){
+				if (!IPUtils.isValidIp(ipDto.getEndIp())){
+					throw new BusinessException("结束IP格式不合法(IPv4)");
+				}
+				if (!IPUtils.isEndIpGreater(ipDto.getStartIp(), ipDto.getEndIp())){
+					throw new BusinessException("结束IP不能小于开始IP");
+				}
+			}
+
+			// 校验是否重复
+			Boolean isExist = groupIpMapper.checkIpConflict(ipDto.getIpMode(),ipDto.getStartIp(), ipDto.getEndIp(), ipDto.getId(), reqDto.getId());
+			if (isExist){
+				if (ipDto.getIpMode() == 1){
+					throw new BusinessException("ip:"+ipDto.getStartIp()+"已存在");
+				}
+				String[] end = ipDto.getEndIp().split("\\.");
+				throw new BusinessException("ip:"+ipDto.getStartIp()+"/"+end[3]+"已存在、或有重叠");
+			}
+
+			// 新增、修改
+			MarketingGroupIp groupIp = new MarketingGroupIp();
+			BeanUtils.copyProperties(ipDto, groupIp);
+			groupIp.setGroupId(reqDto.getId());
+			 if (ipDto.getId() == null){
+				 // 新增
+				 groupIpMapper.insert(groupIp);
+			 }else if (ipDto.getModify() != null && ipDto.getModify()){
+				 // 修改
+				 groupIpMapper.updateById(groupIp);
+			 }
+		}
+		return R.ok();
+	}
+
+	/**
+	 * 获取ip分组列表
+	 * @return ip分组列表
+	 */
+	@Override
+	public List<GetMarketingIpGroupVO> getMarketingIpGroupList() {
+		List<MarketingConfigGroup> ipGroups = groupMapper.selectList(Wrappers.<MarketingConfigGroup>lambdaQuery().eq(MarketingConfigGroup::getGroupType, 1));
+
+		ArrayList<GetMarketingIpGroupVO> result = new ArrayList<>();
+		for (MarketingConfigGroup ipGroup : ipGroups) {
+			GetMarketingIpGroupVO rspVO = new GetMarketingIpGroupVO();
+			rspVO.setId(ipGroup.getId());
+			rspVO.setGroupName(ipGroup.getGroupName());
+
+			// 查询分组下IP列表
+			List<MarketingGroupIp> ipList = groupIpMapper.selectList(Wrappers.<MarketingGroupIp>lambdaQuery().eq(MarketingGroupIp::getGroupId, ipGroup.getId()));
+			ArrayList<MarketingGroupIpVO> ips = new ArrayList<>();
+			for (MarketingGroupIp ip : ipList) {
+				MarketingGroupIpVO ipVO = new MarketingGroupIpVO();
+				BeanUtils.copyProperties(ip, ipVO);
+				ips.add(ipVO);
+			}
+			rspVO.setIps(ips);
+			result.add(rspVO);
+		}
+		return result;
+	}
+
+	/**
+	 * 获取IP集合
+	 * @return IP集合
+	 */
+	@Override
+	public List<GetMarketingConfigIpListVO> getMarketingConfigIpList() {
+		List<GetMarketingConfigIpListVO> result = new ArrayList<>();
+		List<MarketingAppsIp> ips = ipMapper.selectList(Wrappers.<MarketingAppsIp>lambdaQuery().eq(MarketingAppsIp::getConfig, true));
+		ips.forEach(ip -> {
+			GetMarketingConfigIpListVO ipVO = new GetMarketingConfigIpListVO();
+			BeanUtils.copyProperties(ip, ipVO);
+
+			if (ip.getGroupId() != null){
+				MarketingConfigGroup group = groupMapper.selectById(ip.getGroupId());
+				if (group != null)
+					ipVO.setGroupName(group.getGroupName());
+			}
+			result.add(ipVO);
+		});
+		return result;
+	}
+
+	/**
+	 * 添加IP集合
+	 * @param reqDto 添加IP集合入参
+	 * @return Boolean
+	 */
+	@Override
+	public Boolean addMarketingConfigIpList(AddMarketingConfigIpListDTO reqDto) {
+
+		// 去除无用数据
+		if (reqDto.getSourceType() == 1){
+			reqDto.setIpMode(null);
+			reqDto.setStartIp(null);
+			reqDto.setEndIp( null);
+
+			// 查询分组是否存在
+			MarketingConfigGroup group = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+					.eq(MarketingConfigGroup::getId, reqDto.getGroupId())
+					.eq(MarketingConfigGroup::getGroupType, 1)
+			);
+			if (group == null){
+				throw new BusinessException("分组ID:"+reqDto.getGroupId()+"不存在");
+			}
+		}else {
+			reqDto.setGroupId(null);
+			reqDto.setGroupName(null);
+			if (reqDto.getIpMode() == 1){
+				reqDto.setEndIp(null);
+			}
+		}
+		// 校验IP是否存在
+		MarketingAppsIpDTO ipDTO = new MarketingAppsIpDTO();
+		BeanUtils.copyProperties(reqDto, ipDTO);
+		if (appsService.checkIp(ipDTO,null, true)){
+			if (reqDto.getSourceType() ==1){
+				throw new BusinessException("分组ID:"+reqDto.getGroupId()+"已存在");
+			}
+			if (ipDTO.getIpMode() == 1) {
+				throw new BusinessException("ip:" + ipDTO.getStartIp() + "已存在");
+			}
+			String[] end = ipDTO.getEndIp().split("\\.");
+			throw new BusinessException("ip:" + ipDTO.getStartIp() + "/" + end[3] + "已存在");
+		}
+		MarketingAppsIp marketingAppsIp = new MarketingAppsIp();
+		BeanUtils.copyProperties(reqDto, marketingAppsIp);
+		marketingAppsIp.setConfig(true);
+		int insert = ipMapper.insert(marketingAppsIp);
+		return insert > 0;
+	}
+
+	/**
+	 * 删除IP集合
+	 * @param id id
+	 * @return Boolean
+	 */
+	@Override
+	public Boolean delMarketingConfigIpList(Long id) {
+		MarketingAppsIp ip = ipMapper.selectOne(Wrappers.<MarketingAppsIp>lambdaQuery()
+				.eq(MarketingAppsIp::getId, id)
+				.eq(MarketingAppsIp::getConfig, true));
+		if (ip == null) {
+			throw new BusinessException("id: "+id+" ,IP不存在");
+		}
+		int delete = ipMapper.deleteById(id);
+		return delete > 0;
+	}
+
+	/**
+	 * 获取域名集合
+	 * @return 域名集合
+	 */
+	@Override
+	public List<GetMarketingConfigDomainListVO> getMarketingConfigDomainList() {
+		List<GetMarketingConfigDomainListVO> result = new ArrayList<>();
+
+		List<MarketingAppsDomain> domains = domainMapper.selectList(Wrappers.<MarketingAppsDomain>lambdaQuery().eq(MarketingAppsDomain::getConfig, true));
+		domains.forEach(domain -> {
+			GetMarketingConfigDomainListVO domainVO = new GetMarketingConfigDomainListVO();
+			BeanUtils.copyProperties(domain, domainVO);
+			if (domain.getGroupId() != null){
+				MarketingConfigGroup group = groupMapper.selectById(domain.getGroupId());
+				if (group != null)
+					domainVO.setGroupName(group.getGroupName());
+			}
+			result.add(domainVO);
+		});
+		return result;
+	}
+
+	/**
+	 * 添加域名集合
+	 * @param reqDto 添加域名集合入参
+	 * @return Boolean
+	 */
+	@Override
+	public Boolean addMarketingConfigDomainList(AddMarketingConfigDomainListDTO reqDto) {
+
+		if (reqDto.getSourceType() == 1){
+			reqDto.setDomain(null);
+			// 查询分组是否存在
+			MarketingConfigGroup group = groupMapper.selectOne(Wrappers.<MarketingConfigGroup>lambdaQuery()
+					.eq(MarketingConfigGroup::getId, reqDto.getGroupId())
+					.eq(MarketingConfigGroup::getGroupType, 2)
+			);
+			if (group == null){
+				throw new BusinessException("分组ID:"+reqDto.getGroupId()+"不存在");
+			}
+		}else {
+			reqDto.setGroupId(null);
+			reqDto.setGroupName(null);
+		}
+		// 校验域名是否存在
+		MarketingAppsDomainDTO domainDTO = new MarketingAppsDomainDTO();
+		BeanUtils.copyProperties(reqDto, domainDTO);
+		if (appsService.checkDomain(domainDTO, null, true)){
+			throw new BusinessException("域名:"+reqDto.getDomain()+"已存在");
+		}
+
+		MarketingAppsDomain domain = new MarketingAppsDomain();
+		BeanUtils.copyProperties(reqDto, domain);
+		domain.setConfig( true);
+		int insert = domainMapper.insert(domain);
+		return insert > 0;
+	}
+
+	/**
+	 * 删除域名集合
+	 * @param id id
+	 * @return Boolean
+	 */
+	@Override
+	public Boolean delMarketingConfigDomainList(Long id) {
+		// 判断是否存在
+		MarketingAppsDomain domain = domainMapper.selectOne(Wrappers.<MarketingAppsDomain>lambdaQuery()
+				.eq(MarketingAppsDomain::getId, id)
+				.eq(MarketingAppsDomain::getConfig, true));
+		if (domain == null){
+			throw new BusinessException("id:"+id+",域名不存在");
+		}
+		int delete = domainMapper.deleteById(id);
+		return delete > 0;
+	}
+
+	/**
+	 * 查询全局配置信息
+	 * @return 全局配置信息
+	 */
+	@Override
+	public GetMarketingGlobalConfigVO getMarketingGlobalConfig() {
+		GetMarketingGlobalConfigVO res = new GetMarketingGlobalConfigVO();
+		R<ArrayList<SysPublicParamDTO>> ParamDTO = remoteParamService.getParamList(Arrays.asList(TRIGGER_MODE, TRIGGER_RULE, TRIGGER_NUM, PROMPT_MSG, URL));
+		ParamDTO.getData().forEach(item -> {
+			switch (item.getPublicKey()) {
+				case TRIGGER_MODE:
+					res.setTriggerMode(Integer.valueOf(item.getPublicValue()));
+					break;
+				case TRIGGER_RULE:
+					res.setTriggerRule(Integer.valueOf(item.getPublicValue()));
+					break;
+				case TRIGGER_NUM:
+					res.setTriggerNum(item.getPublicValue());
+					break;
+				case PROMPT_MSG:
+					res.setPromptMsg(item.getPublicValue());
+					break;
+				case URL:
+					res.setUrl(item.getPublicValue());
+					break;
+			}
+		});
+		return res;
+	}
+
+	/**
+	 * 修改全局配置
+	 * @param reDto 全局配置信息
+	 * @return Boolean
+	 */
+	@Override
+	public Boolean setMarketingGlobalConfig(GetMarketingGlobalConfigVO reDto) {
+		List<SysPublicParamDTO> sysParamList = new ArrayList<>();
+		SysPublicParamDTO sysParam = new SysPublicParamDTO();
+		if (reDto.getTriggerMode() != null){
+			sysParam.setPublicKey(TRIGGER_MODE);
+			sysParam.setPublicValue(reDto.getTriggerMode().toString());
+			sysParamList.add(sysParam);
+		}
+		if (reDto.getTriggerRule() != null){
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(TRIGGER_RULE);
+			sysParam.setPublicValue(reDto.getTriggerRule().toString());
+			sysParamList.add(sysParam);
+		}
+		if (StringUtils.isNotBlank(reDto.getTriggerNum())){
+			BigDecimal bigDecimal = new BigDecimal(reDto.getTriggerNum());
+			if (bigDecimal.compareTo(BigDecimal.ZERO) < 0){
+				throw new BusinessException("触发频率不能小于0");
+			}
+			if (bigDecimal.compareTo(new BigDecimal("10000")) > 0) {
+				throw new BusinessException("触发频率不能大于10000");
+			}
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(TRIGGER_NUM);
+			sysParam.setPublicValue(reDto.getTriggerNum());
+			sysParamList.add(sysParam);
+		}
+		if (StringUtils.isNotBlank(reDto.getPromptMsg())){
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(PROMPT_MSG);
+			sysParam.setPublicValue(reDto.getPromptMsg());
+			sysParamList.add(sysParam);
+		}
+		if (StringUtils.isNotBlank(reDto.getUrl())){
+			sysParam = new SysPublicParamDTO();
+			sysParam.setPublicKey(URL);
+			sysParam.setPublicValue(reDto.getUrl());
+			sysParamList.add(sysParam);
+		}
+		remoteParamService.setByKey(sysParamList);
+		return null;
+	}
+
+//	@Override
+//	public Boolean getMarketingConfig(AddMarketingConfigDomainListDTO reqDto) {
+//		// 判断域名表中是否重复
+//		// 判断分组中是否重复
+//
+//		MarketingAppsDomain domain = new MarketingAppsDomain();
+//		BeanUtils.copyProperties(reqDto, domain);
+//		domain.setConfig( true);
+//		int insert = domainMapper.insert(domain);
+//		return insert > 0;
+//	}
+}

+ 492 - 0
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MarketingDataServiceImpl.java

@@ -0,0 +1,492 @@
+package com.pig4cloud.pig.marketing.service.impl;
+
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.pig4cloud.pig.common.core.util.ip.IPUtils;
+import com.pig4cloud.pig.marketing.api.dto.data.MarketingDataReportDTO;
+import com.pig4cloud.pig.marketing.api.dto.data.PageFirstLevelDataDTO;
+import com.pig4cloud.pig.marketing.api.dto.data.PageSecondLevelDataDTO;
+import com.pig4cloud.pig.marketing.api.entity.*;
+import com.pig4cloud.pig.marketing.api.util.FingerprintCombinerUtil;
+import com.pig4cloud.pig.marketing.api.util.JA4GeneratorUtil;
+import com.pig4cloud.pig.marketing.api.util.JA4HGeneratorUtil;
+import com.pig4cloud.pig.marketing.api.vo.config.GetMarketingGlobalConfigVO;
+import com.pig4cloud.pig.marketing.api.vo.data.MarketingDataReportVO;
+import com.pig4cloud.pig.marketing.api.vo.data.PageFirstLevelDataVO;
+import com.pig4cloud.pig.marketing.api.vo.data.PageSecondLevelDataVO;
+import com.pig4cloud.pig.marketing.mapper.*;
+import com.pig4cloud.pig.marketing.service.MarketingConfigService;
+import com.pig4cloud.pig.marketing.service.MarketingDataService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * @author: lwh
+ * @date: 2025-07-22
+ * @description: 营销数据service服务类
+ */
+@Slf4j
+@Service
+@AllArgsConstructor
+public class MarketingDataServiceImpl implements MarketingDataService {
+
+	private final MarketingDataMapper marketingDataMapper;
+
+	private final MarketingAppsMapper marketingAppsMapper;
+
+	private final MarketingAppsIpMapper marketingAppsIpMapper;
+
+	private final MarketingGroupIpMapper marketingGroupIpMapper;
+
+	private final MarketingAppsDomainMapper marketingAppsDomainMapper;
+
+	private final MarketingGroupDomainMapper marketingGroupDomainMapper;
+
+	private final MarketingConfigService marketingConfigService;
+
+	private final RedisTemplate redisTemplate;
+
+
+	/**
+	 * 上报营销数据
+	 * @param reqDto 营销数据
+	 * @param request request
+	 * @return MarketingDataReportVO
+	 */
+	@Override
+	public MarketingDataReportVO reportMarketingData(MarketingDataReportDTO reqDto, HttpServletRequest request) {
+
+		// 获取全局配置
+		GetMarketingGlobalConfigVO config = marketingConfigService.getMarketingGlobalConfig();
+
+		String domain = getDomainFromRequest(request);
+
+		MarketingDataReportVO result = new MarketingDataReportVO();
+		BeanUtils.copyProperties(config,  result);
+		result.setType("link");
+
+		// 获取客户端IP地址
+		String ip = IPUtils.getIpAddress(request);
+		// 获取域名
+//		String domain = reqDto.getDomain();
+		// 应用匹配
+		MarketingApps marketingApps = matchedApp(ip, domain);
+
+		// 生成指纹
+		String backend = generateFingerprint(request);
+		String fingerprint = FingerprintCombinerUtil.combine(reqDto.getFingerprint(), backend);
+
+		result.setFingerprint(backend);
+		result.setCombine(fingerprint);
+
+		log.info("ip:{}---domain:{}", ip, domain);
+		log.info("前端指纹:{}", reqDto.getFingerprint());
+		log.info("后端指纹:{}", backend);
+		log.info("组合指纹:{}", fingerprint);
+		// 未匹配到应用,返回默认信息
+		if (marketingApps == null){
+			result.setUrl(getTriggeredUrl(ip, fingerprint,config.getUrl(), config.getTriggerNum()));
+			return result;
+		}
+
+
+		// 入库
+		MarketingData marketingData = new MarketingData();
+		BeanUtils.copyProperties(reqDto, marketingData);
+		marketingData.setFingerprint(fingerprint);
+		marketingData.setIp( ip);
+		marketingData.setAppId(marketingApps.getAppId());
+		marketingDataMapper.insert(marketingData);
+
+		// 计算是否达到频率
+		result.setAppId(marketingApps.getAppId());
+		result.setUrl(getTriggeredUrl(ip, fingerprint,marketingApps.getAppUrl(), marketingApps.getTriggerNum()));
+
+		// 返回结果
+
+		return result;
+	}
+
+	/**
+	 * 从HttpServletRequest中提取域名信息
+	 * @param request 请求对象
+	 * @return 域名(如example.com),异常时返回null
+	 */
+	private String getDomainFromRequest(HttpServletRequest request) {
+		try {
+			// 1. 优先从请求的服务器名称获取(包含端口时需处理)
+			String serverName = request.getServerName();
+			if (serverName != null && !serverName.trim().isEmpty()) {
+				return serverName.trim();
+			}
+
+			// 2. 若serverName为空,尝试从Host头获取(可能包含端口,如example.com:8080)
+			String hostHeader = request.getHeader("Host");
+			if (hostHeader != null && !hostHeader.trim().isEmpty()) {
+				// 去除端口部分(若有)
+				int colonIndex = hostHeader.indexOf(':');
+				return colonIndex != -1 ? hostHeader.substring(0, colonIndex).trim() : hostHeader.trim();
+			}
+
+			// 3. 若Host头也为空,尝试从请求URL中解析
+			String requestUrl = request.getRequestURL().toString();
+			if (requestUrl != null && !requestUrl.trim().isEmpty()) {
+				// 解析URL(如https://example.com/path → 提取example.com)
+				java.net.URL url = new java.net.URL(requestUrl);
+				return url.getHost();
+			}
+		} catch (Exception e) {
+			// 记录异常,但不影响主流程
+			log.error("获取域名信息失败", e);
+		}
+
+		// 所有方式失败时,返回默认值或null
+		return null;
+	}
+
+	/**
+	 * 根据请求协议类型生成对应指纹
+	 * @param request HTTP请求对象
+	 * @return 包含指纹和指纹类型的Map,key分别为"fingerprint"和"fingerprintType"
+	 */
+	private String generateFingerprint(HttpServletRequest request) {
+		String fingerprint = null;
+		String fingerprintType = null;
+		String scheme = request.getScheme().toLowerCase();
+
+		try {
+			if ("https".equals(scheme)) {
+				// HTTPS请求 - 生成JA4指纹
+				JA4GeneratorUtil ja4GeneratorUtil = new JA4GeneratorUtil();
+				fingerprint = ja4GeneratorUtil.generateJA4(request);
+				fingerprintType = "ja4";
+			} else if ("http".equals(scheme)) {
+				// HTTP请求 - 生成JA4H指纹
+				// 提取HTTP请求信息
+				String method = request.getMethod();
+				String path = request.getRequestURI();
+				String httpVersion = request.getProtocol();
+
+				// 提取请求头
+				Map<String, String> headers = new HashMap<>();
+				Enumeration<String> headerNames = request.getHeaderNames();
+				if (headerNames != null) {
+					while (headerNames.hasMoreElements()) {
+						String headerName = headerNames.nextElement();
+						headers.put(headerName, request.getHeader(headerName));
+					}
+				}
+				headers.remove("Origin");
+				headers.remove("Referer");
+				headers.remove("REQUEST-START-TIME");
+				headers.remove("Content-Length");
+
+				// 生成JA4H指纹
+				JA4HGeneratorUtil ja4HGeneratorUtil = new JA4HGeneratorUtil();
+				fingerprint = ja4HGeneratorUtil.generateJA4H(method, path, httpVersion, headers);
+				fingerprintType = "ja4h";
+			}
+		} catch (Exception e) {
+			// 指纹生成失败时记录日志,但不影响主流程
+			log.error("生成指纹失败:{}", e.getMessage());
+		}
+
+		return fingerprint;
+	}
+
+	/**
+	 * 分页统计一级营销数据
+	 * @param reqDto 分页参数
+	 * @return Page
+	 */
+	@Override
+	public Page pageFirstLevelData(PageFirstLevelDataDTO reqDto) {
+		String ip = reqDto.getIp();
+		// 计算时间条件
+		LocalDateTime now = LocalDateTime.now();
+		LocalDateTime lastHourTime = now.minusHours(1);
+		LocalDateTime todayStartTime = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
+
+		// 格式化时间
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+		String lastHourTimeStr = lastHourTime.format(formatter);
+		String todayStartTimeStr = todayStartTime.format(formatter);
+
+		// 计算偏移量
+		long offset = (reqDto.getCurrent() - 1) * reqDto.getSize();
+
+		// 查询数据
+		Page<PageFirstLevelDataVO> page = new Page<>(reqDto.getCurrent(), reqDto.getSize());
+
+		List<PageFirstLevelDataVO> rs = marketingDataMapper.countFirstLevelMarketingData(ip, reqDto.getAppId(), lastHourTimeStr, todayStartTimeStr, offset, reqDto.getSize());
+		page.setRecords(rs);
+		page.setTotal(marketingDataMapper.countFirstLevelDataTotal(ip, reqDto.getAppId()));
+
+		return page;
+	}
+
+	/**
+	 * 分页统计二级营销数据
+	 * @param reqDto 分页参数
+	 * @return Page
+	 */
+	@Override
+	public Page pageSecondLevelData(PageSecondLevelDataDTO reqDto) {
+		String ip = reqDto.getIp();
+
+		// 计算时间条件
+		LocalDateTime now = LocalDateTime.now();
+		LocalDateTime lastHourTime = now.minusHours(1);
+		LocalDateTime todayStartTime = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
+
+		// 格式化时间
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+		String lastHourTimeStr = lastHourTime.format(formatter);
+		String todayStartTimeStr = todayStartTime.format(formatter);
+
+		// 计算偏移量
+		long offset = (reqDto.getCurrent() - 1) * reqDto.getSize();
+
+		// 查询数据
+		Page<PageSecondLevelDataVO> page = new Page<>(reqDto.getCurrent(), reqDto.getSize());
+		page.setRecords(marketingDataMapper.countSecondLevelMarketingData(ip, lastHourTimeStr, todayStartTimeStr, offset, reqDto.getSize()));
+		page.setTotal(marketingDataMapper.countSecondLevelDataTotal(ip));
+
+		return page;
+	}
+
+
+	/**
+	 * 获取匹配的app
+	 * @param ip ip
+	 * @param domain 域名
+	 * @return MarketingApps
+	 */
+	private MarketingApps matchedApp(String ip, String domain){
+		// 根据IP和域名查询跳转链接
+		// 1、筛选营销投放开启、域名限制开启的应用
+		// 1.1、 查询ip表和ip分组表中,包含该ip的应用id列表,条件为应用的营销投放开启、域名限制开启
+		List<Long> ipMatchedAppIds = getIpMatchedAppIds(ip, true);
+
+		if (!ipMatchedAppIds.isEmpty()) {
+			// 1.2 查询域名匹配的应用ID列表
+			List<Long> domainMatchedAppIds = getDomainMatchedAppIds(domain, ipMatchedAppIds);
+			if (!domainMatchedAppIds.isEmpty()) {
+				// 1.3 查找最新创建的应用
+				return getLatestMarketingApp(domainMatchedAppIds);
+			}
+		}
+		// 2、筛选营销投放开启、域名限制关闭的应用
+		List<Long> ipMatchedAppIdsWithoutDomainLimit = getIpMatchedAppIds(ip, false);
+		if (!ipMatchedAppIdsWithoutDomainLimit.isEmpty()) {
+			return getLatestMarketingApp(ipMatchedAppIdsWithoutDomainLimit);
+		}
+		return null;
+	}
+
+	/**
+	 * 根据triggerNum和Redis计数决定是否返回URL
+	 */
+	private String getTriggeredUrl(String ip, String fingerprint, String url, String num) {
+
+		if (fingerprint == null || ip == null) {
+			return "";
+		}
+
+		BigDecimal triggerNum = new BigDecimal(num)
+				.setScale(2, RoundingMode.HALF_UP);
+		String countKey = String.format("marketing:trigger:count:%s:%s", fingerprint, ip);
+
+		// 1. 概率模式(triggerNum < 1)
+		if (triggerNum.compareTo(BigDecimal.ONE) < 0) {
+			// 生成0-1之间的随机数与triggerNum比较
+			double random = Math.random();
+			return random <= triggerNum.doubleValue() ? url : "";
+		}
+
+		// 2. 每次返回(triggerNum = 1)
+		if (triggerNum.compareTo(BigDecimal.ONE) == 0) {
+			return url;
+		}
+
+		// 3. 周期模式(triggerNum > 1)
+		// 向上取整转换为整数周期(如2.3 → 3,3.9 → 4)
+		int cycle = triggerNum.setScale(0, RoundingMode.CEILING).intValue();
+		if (cycle < 1) {
+			return "";
+		}
+
+		// Redis原子自增计数
+		Long currentCount = redisTemplate.opsForValue().increment(countKey);
+		if (currentCount == null) {
+			return "";
+		}
+
+		// 首次访问设置过期时间(24小时)
+		if (currentCount == 1) {
+			redisTemplate.expire(countKey, 24, TimeUnit.HOURS);
+		}
+
+		// 达到周期次数时返回URL并重置计数
+		if (currentCount % cycle == 0) {
+			redisTemplate.delete(countKey);
+			return url;
+		}
+		return "";
+	}
+
+	/**
+	 * 根据IP获取匹配的应用ID列表
+	 * @param ip IP地址
+	 * @param domainLimit 是否开启域名限制
+	 * @return 应用ID列表
+	 */
+	private List<Long> getIpMatchedAppIds(String ip, boolean domainLimit) {
+		// 查询营销投放开启、域名限制符合条件的应用ID
+		List<MarketingApps> qualifiedApps = marketingAppsMapper.selectList(Wrappers.<MarketingApps>lambdaQuery()
+				.eq(MarketingApps::getLaunch, true)
+				.eq(MarketingApps::getDomainLimit, domainLimit)
+		);
+		if (qualifiedApps.isEmpty()) {
+			return Collections.emptyList();
+		}
+		List<Long> qualifiedAppIds = qualifiedApps.stream()
+				.map(MarketingApps::getId)
+				.collect(Collectors.toList());
+
+		// 转换IP为长整型用于比较
+		long ipLong = ipToLong(ip);
+
+		// 1. 从marketing_apps_ip表查询直接关联的IP匹配
+		QueryWrapper<MarketingAppsIp> directIpQuery = new QueryWrapper<>();
+		directIpQuery.eq("ip_type",1)
+				.in("app_id", qualifiedAppIds)
+				.and(wrapper -> wrapper
+						// 单IP匹配
+						.eq("ip_mode", 1).eq("start_ip", ip)
+						// IP段匹配:将数据库字符串IP转换为长整型后比较
+						.or().eq("ip_mode", 2)
+						.le("INET_ATON(start_ip)", ipLong)
+						.ge("INET_ATON(end_ip)", ipLong)
+				);
+		List<MarketingAppsIp> directIpMatches = marketingAppsIpMapper.selectList(directIpQuery);
+		Set<Long> appIdSet = directIpMatches.stream()
+				.map(MarketingAppsIp::getAppId)
+				.collect(Collectors.toSet());
+
+		// 2. 从分组查询IP匹配的应用
+		// 2.1 查询包含该IP的分组ID
+		QueryWrapper<MarketingGroupIp> groupIpQuery = new QueryWrapper<>();
+		groupIpQuery.and(qw -> qw
+						// 单IP匹配
+						.eq("ip_mode", 1).eq("start_ip", ip)
+						// IP段匹配:转换为长整型后比较
+						.or().eq("ip_mode", 2)
+						.le("INET_ATON(start_ip)", ipLong)
+						.ge("INET_ATON(end_ip)", ipLong)
+				);
+		List<MarketingGroupIp> groupIpMatches = marketingGroupIpMapper.selectList(groupIpQuery);
+		if (!groupIpMatches.isEmpty()) {
+			List<Long> groupIds = groupIpMatches.stream()
+					.map(MarketingGroupIp::getGroupId)
+					.collect(Collectors.toList());
+
+			// 2.2 查询关联这些分组的应用
+			List<MarketingAppsIp> groupAppMatches = marketingAppsIpMapper.selectList(Wrappers.<MarketingAppsIp>lambdaQuery()
+					.in(MarketingAppsIp::getGroupId, groupIds)
+					.eq(MarketingAppsIp::getSourceType, 1) // 来源类型为分组
+					.eq(MarketingAppsIp::getIpType, 1)	// 白名单
+					.in(MarketingAppsIp::getAppId, qualifiedAppIds)
+			);
+			groupAppMatches.forEach(appIp -> appIdSet.add(appIp.getAppId()));
+		}
+
+		return new ArrayList<>(appIdSet);
+	}
+
+	/**
+	 * 根据域名获取匹配的应用ID列表
+	 * @param domain 域名
+	 * @param appIdList 待筛选的应用ID列表
+	 * @return 匹配的应用ID列表
+	 */
+	private List<Long> getDomainMatchedAppIds(String domain, List<Long> appIdList) {
+		Set<Long> matchedAppIds = new HashSet<>();
+
+		// 1. 从marketing_apps_domain表查询直接关联的域名匹配
+		List<MarketingAppsDomain> directDomainMatches = marketingAppsDomainMapper.selectList(Wrappers.<MarketingAppsDomain>lambdaQuery()
+				.in(MarketingAppsDomain::getAppId, appIdList)
+				.eq(MarketingAppsDomain::getSourceType, 2)
+				.eq(MarketingAppsDomain::getDomain, domain)
+		);
+		directDomainMatches.forEach(domainApp -> matchedAppIds.add(domainApp.getAppId()));
+
+		// 2. 从分组查询域名匹配的应用
+		// 2.1 查询包含该域名的分组ID
+		List<MarketingGroupDomain> groupDomainMatches = marketingGroupDomainMapper.selectList(Wrappers.<MarketingGroupDomain>lambdaQuery()
+				.eq(MarketingGroupDomain::getDomain, domain)
+		);
+		if (!groupDomainMatches.isEmpty()) {
+			List<Long> groupIds = groupDomainMatches.stream()
+					.map(MarketingGroupDomain::getGroupId)
+					.collect(Collectors.toList());
+
+			// 2.2 查询关联这些分组的应用
+			List<MarketingAppsDomain> groupAppMatches = marketingAppsDomainMapper.selectList(Wrappers.<MarketingAppsDomain>lambdaQuery()
+					.in(MarketingAppsDomain::getGroupId, groupIds)
+					.eq(MarketingAppsDomain::getSourceType, 1)
+					.in(MarketingAppsDomain::getAppId, appIdList)
+			);
+			groupAppMatches.forEach(domainApp -> matchedAppIds.add(domainApp.getAppId()));
+		}
+		return new ArrayList<>(matchedAppIds);
+	}
+
+	/**
+	 * 获取最新创建的应用
+	 * @param appIds 应用ID列表
+	 * @return 最新创建的应用
+	 */
+	private MarketingApps getLatestMarketingApp(List<Long> appIds) {
+		if (appIds.isEmpty()) {
+			return null;
+		}
+
+		MarketingApps marketingApps = marketingAppsMapper.selectOne(Wrappers.<MarketingApps>lambdaQuery()
+				.in(MarketingApps::getId, appIds)
+				.orderByDesc(MarketingApps::getCreateTime)
+				.last("LIMIT 1")
+		);
+		return marketingApps;
+	}
+
+
+	/**
+	 * IP地址转长整型
+	 * @param ip IP地址
+	 * @return 长整型IP
+	 */
+	private long ipToLong(String ip) {
+		String[] parts = ip.split("\\.");
+		long result = 0;
+		for (int i = 0; i < 4; i++) {
+			result |= Long.parseLong(parts[i]) << (24 - i * 8);
+		}
+		return result & 0xFFFFFFFFL;
+	}
+
+}

+ 30 - 0
pig-marketing/pig-marketing-biz/src/main/resources/application.yml

@@ -0,0 +1,30 @@
+server:
+  port: 16000
+
+spring:
+  application:
+    name: @artifactId@
+  cloud:
+    nacos:
+      username: @nacos.username@
+      password: @nacos.password@
+      discovery:
+        namespace: @nacos.namespace@
+        server-addr: @nacos.address@
+        group: DEFAULT_GROUP
+      config:
+        namespace: ${spring.cloud.nacos.discovery.namespace}
+        server-addr: ${spring.cloud.nacos.discovery.server-addr}
+        group: DEFAULT_GROUP
+        refresh-enabled: true
+  config:
+    import:
+      - nacos:application.yml
+      - nacos:${spring.application.name}.yml
+
+marketing:
+  app:
+    url: http://192.168.3.17:2888/ipa/getApps
+    access-key: 4ea5ba93-d222-45dc-862a-1c7fc7789d11
+
+

+ 69 - 0
pig-marketing/pig-marketing-biz/src/main/resources/logback-spring.xml

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    小技巧: 在根pom里面设置统一存放路径,统一管理方便维护
+    <properties>
+        <log-path>/Users/lengleng</log-path>
+    </properties>
+    1. 其他模块加日志输出,直接copy本文件放在resources 目录即可
+    2. 注意修改 <property name="${log-path}/log.path" value=""/> 的value模块
+-->
+<configuration debug="false" scan="false">
+    <property name="log.path" value="logs/${project.artifactId}"/>
+    <!-- 彩色日志格式 -->
+    <property name="CONSOLE_LOG_PATTERN"
+              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+	<property name="FILE_LOG_PATTERN" value=":%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %t %c{1}: %m%n"/>
+	<!-- 彩色日志依赖的渲染类 -->
+    <conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter"/>
+    <conversionRule conversionWord="wex"
+                    class="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
+    <conversionRule conversionWord="wEx"
+                    class="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
+    <!-- Console log output -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
+        </encoder>
+    </appender>
+
+    <!-- Log file debug output -->
+    <appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/debug.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+        </encoder>
+    </appender>
+
+    <!-- Log file error output -->
+    <appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/error.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>ERROR</level>
+        </filter>
+    </appender>
+
+    <!--nacos 心跳 INFO 屏蔽-->
+    <logger name="com.alibaba.nacos" level="OFF">
+        <appender-ref ref="error"/>
+    </logger>
+
+    <!-- Level: FATAL 0  ERROR 3  WARN 4  INFO 6  DEBUG 7 -->
+    <root level="debug">
+        <appender-ref ref="console"/>
+        <appender-ref ref="debug"/>
+        <appender-ref ref="error"/>
+    </root>
+</configuration>

+ 25 - 0
pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingAppsDomainMapper.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~
+  ~      Copyright (c) 2018-2025, lengleng All rights reserved.
+  ~
+  ~  Redistribution and use in source and binary forms, with or without
+  ~  modification, are permitted provided that the following conditions are met:
+  ~
+  ~ Redistributions of source code must retain the above copyright notice,
+  ~  this list of conditions and the following disclaimer.
+  ~  Redistributions in binary form must reproduce the above copyright
+  ~  notice, this list of conditions and the following disclaimer in the
+  ~  documentation and/or other materials provided with the distribution.
+  ~  Neither the name of the pig4cloud.com developer nor the names of its
+  ~  contributors may be used to endorse or promote products derived from
+  ~  this software without specific prior written permission.
+  ~  Author: lengleng ([email protected])
+  ~
+  -->
+
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.pig4cloud.pig.marketing.mapper.MarketingAppsDomainMapper">
+
+
+</mapper>

+ 25 - 0
pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingAppsIpMapper.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~
+  ~      Copyright (c) 2018-2025, lengleng All rights reserved.
+  ~
+  ~  Redistribution and use in source and binary forms, with or without
+  ~  modification, are permitted provided that the following conditions are met:
+  ~
+  ~ Redistributions of source code must retain the above copyright notice,
+  ~  this list of conditions and the following disclaimer.
+  ~  Redistributions in binary form must reproduce the above copyright
+  ~  notice, this list of conditions and the following disclaimer in the
+  ~  documentation and/or other materials provided with the distribution.
+  ~  Neither the name of the pig4cloud.com developer nor the names of its
+  ~  contributors may be used to endorse or promote products derived from
+  ~  this software without specific prior written permission.
+  ~  Author: lengleng ([email protected])
+  ~
+  -->
+
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.pig4cloud.pig.marketing.mapper.MarketingAppsIpMapper">
+
+
+</mapper>

+ 125 - 0
pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingAppsMapper.xml

@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~
+  ~      Copyright (c) 2018-2025, lengleng All rights reserved.
+  ~
+  ~  Redistribution and use in source and binary forms, with or without
+  ~  modification, are permitted provided that the following conditions are met:
+  ~
+  ~ Redistributions of source code must retain the above copyright notice,
+  ~  this list of conditions and the following disclaimer.
+  ~  Redistributions in binary form must reproduce the above copyright
+  ~  notice, this list of conditions and the following disclaimer in the
+  ~  documentation and/or other materials provided with the distribution.
+  ~  Neither the name of the pig4cloud.com developer nor the names of its
+  ~  contributors may be used to endorse or promote products derived from
+  ~  this software without specific prior written permission.
+  ~  Author: lengleng ([email protected])
+  ~
+  -->
+
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.pig4cloud.pig.marketing.mapper.MarketingAppsMapper">
+
+
+	<select id="statAppFirstMktData" resultType="com.pig4cloud.pig.marketing.api.vo.app.PageStatAppFirstMktDataVO">
+		SELECT
+			ip,
+			MIN(create_time) AS firstTime,
+			MAX(create_time) AS lastTime,
+			COUNT(*) AS count,
+			(SELECT utm_source FROM marketing_data
+			WHERE ip = md.ip
+			AND app_id = #{appId}
+			<if test="startTime != null">AND create_time &gt;= #{startTime}</if>
+			<if test="endTime != null">AND create_time &lt;= #{endTime}</if>
+			AND del_flag = '0'
+			ORDER BY create_time DESC LIMIT 1) AS utmSource,
+			(SELECT url FROM marketing_data
+			WHERE ip = md.ip
+			AND app_id = #{appId}
+			<if test="startTime != null">AND create_time &gt;= #{startTime}</if>
+			<if test="endTime != null">AND create_time &lt;= #{endTime}</if>
+			AND del_flag = '0'
+			AND url IS NOT NULL
+			ORDER BY create_time DESC, id DESC LIMIT 1) AS url
+		FROM marketing_data md
+		WHERE
+			md.app_id = #{appId}
+			AND md.del_flag = '0'
+			<if test="startTime != null">
+				AND md.create_time &gt;= #{startTime}
+			</if>
+			<if test="endTime != null">
+				AND md.create_time &lt;= #{endTime}
+			</if>
+		GROUP BY md.ip
+		ORDER BY lastTime DESC
+	</select>
+	<select id="statAppSecondMktData" resultType="com.pig4cloud.pig.marketing.api.vo.app.PageStatAppSecondMktDataVO">
+		SELECT
+			fingerprint,
+			MIN(create_time) AS firstTime,
+			MAX(create_time) AS lastTime,
+			COUNT(*) AS count,
+
+			(SELECT url FROM marketing_data
+			WHERE fingerprint = md.fingerprint
+			AND ip = #{ip}
+			AND app_id = #{appId}
+			<if test="startTime != null">AND create_time &gt;= #{startTime}</if>
+			<if test="endTime != null">AND create_time &lt;= #{endTime}</if>
+			AND del_flag = '0'
+			ORDER BY create_time DESC, id DESC LIMIT 1) AS url,
+
+			(SELECT os_type FROM marketing_data
+			WHERE fingerprint = md.fingerprint
+			AND ip = #{ip}
+			AND app_id = #{appId}
+			<if test="startTime != null">AND create_time &gt;= #{startTime}</if>
+		    <if test="endTime != null">AND create_time &lt;= #{endTime}</if>
+			AND del_flag = '0'
+			ORDER BY create_time DESC, id DESC LIMIT 1) AS osType,
+
+			(SELECT os_version FROM marketing_data
+			WHERE fingerprint = md.fingerprint
+			AND ip = #{ip}
+			AND app_id = #{appId}
+			<if test="startTime != null">AND create_time &gt;= #{startTime}</if>
+			<if test="endTime != null">AND create_time &lt;= #{endTime}</if>
+			AND del_flag = '0'
+			ORDER BY create_time DESC, id DESC LIMIT 1) AS osVersion,
+
+			(SELECT browser FROM marketing_data
+			WHERE fingerprint = md.fingerprint
+			AND ip = #{ip}
+			AND app_id = #{appId}
+			<if test="startTime != null">AND create_time &gt;= #{startTime}</if>
+			<if test="endTime != null">AND create_time &lt;= #{endTime}</if>
+			AND del_flag = '0'
+			ORDER BY create_time DESC, id DESC LIMIT 1) AS browser,
+
+		(SELECT browser_version FROM marketing_data
+		WHERE fingerprint = md.fingerprint
+		AND ip = #{ip}
+		AND app_id = #{appId}
+		<if test="startTime != null">AND create_time &gt;= #{startTime}</if>
+		<if test="endTime != null">AND create_time &lt;= #{endTime}</if>
+		AND del_flag = '0'
+		ORDER BY create_time DESC, id DESC LIMIT 1) AS browserVersion
+		FROM marketing_data md
+		WHERE
+			md.app_id = #{appId}
+			AND md.ip = #{ip}
+			AND md.del_flag = '0'
+			<if test="startTime != null">
+				AND md.create_time &gt;= #{startTime}
+			</if>
+			<if test="endTime != null">
+				AND md.create_time &lt;= #{endTime}
+			</if>
+			AND md.fingerprint IS NOT NULL
+		GROUP BY md.fingerprint
+		ORDER BY lastTime DESC
+	</select>
+</mapper>

+ 78 - 0
pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingDataMapper.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~
+  ~      Copyright (c) 2018-2025, lengleng All rights reserved.
+  ~
+  ~  Redistribution and use in source and binary forms, with or without
+  ~  modification, are permitted provided that the following conditions are met:
+  ~
+  ~ Redistributions of source code must retain the above copyright notice,
+  ~  this list of conditions and the following disclaimer.
+  ~  Redistributions in binary form must reproduce the above copyright
+  ~  notice, this list of conditions and the following disclaimer in the
+  ~  documentation and/or other materials provided with the distribution.
+  ~  Neither the name of the pig4cloud.com developer nor the names of its
+  ~  contributors may be used to endorse or promote products derived from
+  ~  this software without specific prior written permission.
+  ~  Author: lengleng ([email protected])
+  ~
+  -->
+
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.pig4cloud.pig.marketing.mapper.MarketingDataMapper">
+
+	<select id="countFirstLevelMarketingData" resultType="com.pig4cloud.pig.marketing.api.vo.data.PageFirstLevelDataVO">
+        SELECT
+        ip,
+        COUNT(*) AS total,
+        SUM(CASE WHEN create_time >= #{lastHourTime} THEN 1 ELSE 0 END) AS hourly,
+        SUM(CASE WHEN create_time >= #{todayStartTime} THEN 1 ELSE 0 END) AS daily
+        FROM
+        marketing_data
+        <where>
+            <if test="ip != null and ip != ''">
+                AND ip LIKE CONCAT('%', #{ip}, '%')
+            </if>
+			<if test="appId != null and appId != ''">
+				AND app_id LIKE CONCAT('%', #{appId}, '%')
+			</if>
+        </where>
+        GROUP BY ip
+        LIMIT #{offset}, #{pageSize}
+    </select>
+
+	<select id="countFirstLevelDataTotal" resultType="java.lang.Long">
+		SELECT
+		COUNT(DISTINCT ip)
+		FROM
+		marketing_data
+		<where>
+			<if test="ip != null and ip != ''">
+				AND ip LIKE CONCAT('%', #{ip}, '%')
+			</if>
+			<if test="appId != null and appId != ''">
+				AND app_id LIKE CONCAT('%', #{appId}, '%')
+			</if>
+		</where>
+	</select>
+
+	<select id="countSecondLevelMarketingData"
+			resultType="com.pig4cloud.pig.marketing.api.vo.data.PageSecondLevelDataVO">
+        SELECT
+            domain, referrer, fingerprint, COUNT(*) AS total, SUM(CASE WHEN create_time >= #{lastHourTime} THEN 1 ELSE 0 END) AS hourly, SUM(CASE WHEN create_time >=
+                                                                                             #{todayStartTime} THEN 1 ELSE 0 END) AS daily
+        FROM
+            marketing_data
+        WHERE
+            ip = #{ip}
+        GROUP BY
+            domain, referrer, fingerprint
+            LIMIT #{offset}, #{pageSize}
+    </select>
+
+	<select id="countSecondLevelDataTotal" resultType="java.lang.Long">
+		SELECT COUNT(DISTINCT domain, referrer, fingerprint)
+		FROM marketing_data
+		WHERE ip = #{ip}
+	</select>
+</mapper>

+ 58 - 0
pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingGroupIpMapper.xml

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~
+  ~      Copyright (c) 2018-2025, lengleng All rights reserved.
+  ~
+  ~  Redistribution and use in source and binary forms, with or without
+  ~  modification, are permitted provided that the following conditions are met:
+  ~
+  ~ Redistributions of source code must retain the above copyright notice,
+  ~  this list of conditions and the following disclaimer.
+  ~  Redistributions in binary form must reproduce the above copyright
+  ~  notice, this list of conditions and the following disclaimer in the
+  ~  documentation and/or other materials provided with the distribution.
+  ~  Neither the name of the pig4cloud.com developer nor the names of its
+  ~  contributors may be used to endorse or promote products derived from
+  ~  this software without specific prior written permission.
+  ~  Author: lengleng ([email protected])
+  ~
+  -->
+
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.pig4cloud.pig.marketing.mapper.MarketingGroupIpMapper">
+
+	<select id="checkIpConflict" resultType="java.lang.Boolean">
+        SELECT
+        COUNT(*) > 0 AS exists_flag
+        FROM
+        marketing_group_ip
+        WHERE del_flag = '0'
+        <if test="groupId != null">
+            AND group_id = #{groupId}
+        </if>
+        <if test="id != null">
+            AND id != #{id}
+        </if>
+        AND (
+        <if test="ipMode == 1">
+            (
+            (ip_mode = 1 AND start_ip = #{startIp}) OR
+            (ip_mode = 2 AND INET_ATON(#{startIp}) BETWEEN INET_ATON(start_ip) AND INET_ATON(end_ip))
+            )
+        </if>
+
+        <if test="ipMode == 2">
+            (
+            <!-- 与单IP重叠:单IP在输入IP段范围内 -->
+            (ip_mode = 1 AND INET_ATON(start_ip) BETWEEN INET_ATON(#{startIp}) AND INET_ATON(#{endIp})) OR
+            <!-- 与IP段重叠:两个IP段有交集 -->
+            (ip_mode = 2 AND
+            (INET_ATON(start_ip) BETWEEN INET_ATON(#{startIp}) AND INET_ATON(#{endIp})) OR
+            (INET_ATON(end_ip) BETWEEN INET_ATON(#{startIp}) AND INET_ATON(#{endIp})) OR
+            (INET_ATON(#{startIp}) BETWEEN INET_ATON(start_ip) AND INET_ATON(end_ip))
+            )
+            )
+        </if>
+        )
+    </select>
+</mapper>

+ 36 - 0
pig-marketing/pom.xml

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>com.pig4cloud</groupId>
+		<artifactId>pig</artifactId>
+		<version>${revision}</version>
+	</parent>
+
+	<artifactId>pig-marketing</artifactId>
+
+	<description>pig 营销模块</description>
+	<packaging>pom</packaging>
+
+	<modules>
+		<module>pig-marketing-api</module>
+		<module>pig-marketing-biz</module>
+	</modules>
+</project>

+ 61 - 0
pig-statistics/pig-statistics-api/pom.xml

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>com.pig4cloud</groupId>
+		<artifactId>pig-statistics</artifactId>
+		<version>${revision}</version>
+	</parent>
+
+	<artifactId>pig-statistics-api</artifactId>
+
+	<packaging>jar</packaging>
+
+	<description>pig 统计公共api模块</description>
+
+	<dependencies>
+		<!--core 工具类-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-core</artifactId>
+		</dependency>
+		<!--feign 注解依赖-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-feign</artifactId>
+		</dependency>
+		<!--mybatis 依赖-->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-mybatis</artifactId>
+		</dependency>
+		<!-- excel 导入导出 -->
+		<dependency>
+			<groupId>com.pig4cloud</groupId>
+			<artifactId>pig-common-excel</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>javax.servlet-api</artifactId>
+			<version>4.0.1</version>
+			<scope>compile</scope>
+		</dependency>
+	</dependencies>
+</project>

+ 50 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/GetNewUserRetentionDTO.java

@@ -0,0 +1,50 @@
+package com.pig4cloud.pig.statistics.api.dto.user;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-06
+ * @description: 查询次日留存率入参
+ */
+@Data
+@Schema(description = "查询次日留存率入参")
+public class GetNewUserRetentionDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 渠道
+	 */
+	@Schema(description = "渠道", example = "[\"App Store\"]")
+	private List<String> channel;
+
+	/**
+	 * 开始时间
+	 */
+	@NotNull(message = "开始时间不能为空")
+	@Schema(description = "开始时间", example = "2025-07-29")
+	private LocalDate fromDate;
+
+	/**
+	 * 结束时间
+	 */
+	@NotNull(message = "结束时间不能为空")
+	@Schema(description = "结束时间", example = "2025-08-05")
+	private LocalDate toDate;
+
+	/**
+	 * 版本
+	 */
+	@Schema(description = "版本", example = "[\"1.0.0\"]")
+	private List<String> version;
+}

+ 60 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/GetNewUserTrendDTO.java

@@ -0,0 +1,60 @@
+package com.pig4cloud.pig.statistics.api.dto.user;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-06
+ * @description: 获取新增趋势入参
+ */
+
+@Data
+@Schema(description = "获取新增趋势入参")
+public class GetNewUserTrendDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 渠道
+	 */
+	@Schema(description = "渠道", example = "[\"App Store\"]")
+	private List<String> channel;
+
+	/**
+	 * 开始时间
+	 */
+	@NotNull(message = "开始时间不能为空")
+	@Schema(description = "开始时间", example = "2025-07-29")
+	private LocalDate fromDate;
+
+	/**
+	 * 结束时间
+	 */
+	@NotNull(message = "结束时间不能为空")
+	@Schema(description = "结束时间", example = "2025-08-05")
+	private LocalDate toDate;
+
+	/**
+	 * 时间单位
+	 */
+	@NotBlank(message = "时间单位不能为空")
+	@Schema(description = "时间单位,hour-小时,day-天,week-周,month-月", example = "day")
+	private String timeUnit;
+
+	/**
+	 * 版本
+	 */
+	@Schema(description = "版本", example = "[\"1.0.0\"]")
+	private List<String> version;
+
+}

+ 45 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/GetVersionDistributionDTO.java

@@ -0,0 +1,45 @@
+package com.pig4cloud.pig.statistics.api.dto.user;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-07
+ * @description: 获取版本用户来源入参
+ */
+@Data
+@Schema(description = "获取版本用户来源入参")
+public class GetVersionDistributionDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 查询条件名称
+	 */
+	@Schema(description = "查询条件名称")
+	private String conditionName;
+
+	/**
+	 * 查询条件值
+	 */
+	@Schema(description = "查询条件值")
+	private String conditionValue;
+
+	/**
+	 * 时间单位
+	 */
+	@Schema(description = "时间单位")
+	private String timeUnit;
+
+	/**
+	 * 类型
+	 */
+	@Schema(description = "类型,upgradeChannel-升级用户渠道",example = "upgradeChannel")
+	private String type;
+}

+ 72 - 0
pig-statistics/pig-statistics-api/src/main/java/com/pig4cloud/pig/statistics/api/dto/user/PageActiveDetailDTO.java

@@ -0,0 +1,72 @@
+package com.pig4cloud.pig.statistics.api.dto.user;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * @author: lwh
+ * @date: 2025-08-06
+ * @description: 分页查询活跃详情入参
+ */
+
+@Data
+@Schema(description = "分页查询活跃详情入参")
+public class PageActiveDetailDTO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 渠道
+	 */
+	@Schema(description = "渠道", example = "[\"App Store\"]")
+	private List<String> channel;
+
+	/**
+	 * 开始时间
+	 */
+	@NotNull(message = "开始时间不能为空")
+	@Schema(description = "开始时间", example = "2025-07-29")
+	private LocalDate fromDate;
+
+	/**
+	 * 结束时间
+	 */
+	@NotNull(message = "结束时间不能为空")
+	@Schema(description = "结束时间", example = "2025-08-05")
+	private LocalDate toDate;
+
+	/**
+	 * 时间单位
+	 */
+	@NotBlank(message = "时间单位不能为空")
+	@Schema(description = "时间单位,hour-小时,day-天,week-周,month-月", example = "day")
+	private String timeUnit;
+
+	/**
+	 * 版本
+	 */
+	@Schema(description = "版本", example = "[\"1.0.0\"]")
+	private List<String> version;
+
+	/**
+	 * 每页显示条数,默认 10
+	 */
+	@Schema(description = "每页显示条数")
+	private long size = 10;
+
+	/**
+	 * 当前页
+	 */
+	@Schema(description = "当前页")
+	private long current = 1;
+
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików