Przeglądaj źródła

抽奖活动,邀请有礼活动,签到活动这几种更新

liuchengsen 1 miesiąc temu
rodzic
commit
54bf9c60c0

+ 10 - 0
zhijiayun-activity/src/main/java/com/xuekairui/activity/controller/ActivityController.java

@@ -4,6 +4,7 @@ import com.xuekairui.activity.dto.ActivityCheckinCycleStatusResponse;
 import com.xuekairui.activity.dto.ActivityInviteStatusResponse;
 import com.xuekairui.activity.dto.ActivityInviteStatusResponse;
 import com.xuekairui.activity.dto.ActivityLotteryResultResponse;
 import com.xuekairui.activity.dto.ActivityLotteryResultResponse;
 import com.xuekairui.activity.dto.ActivityLotteryStatusResponse;
 import com.xuekairui.activity.dto.ActivityLotteryStatusResponse;
+import com.xuekairui.activity.entity.ActivityLotteryPrize;
 import com.xuekairui.activity.entity.ActivityLotteryRecord;
 import com.xuekairui.activity.entity.ActivityLotteryRecord;
 import com.xuekairui.activity.service.ActivityCheckinCycleService;
 import com.xuekairui.activity.service.ActivityCheckinCycleService;
 import com.xuekairui.activity.service.ActivityInviteService;
 import com.xuekairui.activity.service.ActivityInviteService;
@@ -65,6 +66,15 @@ public class ActivityController {
         return Result.success(activityLotteryService.getStatus(userId));
         return Result.success(activityLotteryService.getStatus(userId));
     }
     }
 
 
+    /**
+     * 获取可抽奖品列表(用户可见,不含概率权重等敏感信息)
+     * GET /api/activity/lottery/prizes
+     */
+    @GetMapping("/lottery/prizes")
+    public Result<List<ActivityLotteryPrize>> lotteryPrizes() {
+        return Result.success(activityLotteryService.listEnabledPrizes());
+    }
+
     /**
     /**
      * 执行抽奖
      * 执行抽奖
      * POST /api/activity/lottery/draw
      * POST /api/activity/lottery/draw

+ 3 - 0
zhijiayun-activity/src/main/java/com/xuekairui/activity/dto/ActivityCheckinCycleStatusResponse.java

@@ -42,4 +42,7 @@ public class ActivityCheckinCycleStatusResponse {
 
 
     /** 剩余可领取次数 */
     /** 剩余可领取次数 */
     private Long remainingCount;
     private Long remainingCount;
+
+    /** 达标可获会员天数 */
+    private Integer rewardDays;
 }
 }

+ 1 - 5
zhijiayun-activity/src/main/java/com/xuekairui/activity/dto/AdminCheckInRecordResponse.java

@@ -41,13 +41,9 @@ public class AdminCheckInRecordResponse {
     /** 奖励类型 */
     /** 奖励类型 */
     private String rewardType;
     private String rewardType;
 
 
-    /** 本次奖励数量 */
+    /** 本次奖励数量(会员天数) */
     private Integer rewardAmount;
     private Integer rewardAmount;
 
 
-    /** 关联配额发放记录ID */
-    @JsonSerialize(using = ToStringSerializer.class)
-    private Long grantId;
-
     /** 签到端 */
     /** 签到端 */
     private String clientSource;
     private String clientSource;
 
 

+ 7 - 10
zhijiayun-activity/src/main/java/com/xuekairui/activity/dto/CheckInConfigRequest.java

@@ -6,6 +6,9 @@ import lombok.Data;
 
 
 /**
 /**
  * 签到配置更新请求(运营端)
  * 签到配置更新请求(运营端)
+ * <p>
+ * 奖励为高级会员天数:baseReward=基础天数,连续签到满 continuousThreshold 天后每天额外叠加 continuousReward 天,
+ * 叠加累计不超过 continuousCap 天。
  */
  */
 @Data
 @Data
 public class CheckInConfigRequest {
 public class CheckInConfigRequest {
@@ -13,15 +16,12 @@ public class CheckInConfigRequest {
     @NotNull(message = "启用状态不能为空")
     @NotNull(message = "启用状态不能为空")
     private Integer enabled;
     private Integer enabled;
 
 
-    /** 奖励类型,目前固定 CRAWLER */
-    private String rewardType;
-
-    @NotNull(message = "基础奖励不能为空")
-    @Min(value = 0, message = "基础奖励不能小于0")
+    @NotNull(message = "基础会员天数不能为空")
+    @Min(value = 0, message = "基础会员天数不能小于0")
     private Integer baseReward;
     private Integer baseReward;
 
 
-    @NotNull(message = "连续奖励不能为空")
-    @Min(value = 0, message = "连续奖励不能小于0")
+    @NotNull(message = "连续额外天数不能为空")
+    @Min(value = 0, message = "连续额外天数不能小于0")
     private Integer continuousReward;
     private Integer continuousReward;
 
 
     @NotNull(message = "连续奖励阈值不能为空")
     @NotNull(message = "连续奖励阈值不能为空")
@@ -32,8 +32,5 @@ public class CheckInConfigRequest {
     @Min(value = 1, message = "连续奖励上限至少为1")
     @Min(value = 1, message = "连续奖励上限至少为1")
     private Integer continuousCap;
     private Integer continuousCap;
 
 
-    /** 奖励有效天数(NULL=永不过期) */
-    private Integer rewardExpireDays;
-
     private String remark;
     private String remark;
 }
 }

+ 3 - 6
zhijiayun-activity/src/main/java/com/xuekairui/activity/entity/CheckInConfig.java

@@ -25,9 +25,9 @@ public class CheckInConfig {
     @Builder.Default
     @Builder.Default
     private Integer enabled = 1;
     private Integer enabled = 1;
 
 
-    /** 奖励类型:CRAWLER-爬虫次数 */
+    /** 奖励类型:MEMBERSHIP-会员天数 */
     @Builder.Default
     @Builder.Default
-    private String rewardType = "CRAWLER";
+    private String rewardType = "MEMBERSHIP";
 
 
     /** 基础签到奖励(每日) */
     /** 基础签到奖励(每日) */
     @Builder.Default
     @Builder.Default
@@ -41,13 +41,10 @@ public class CheckInConfig {
     @Builder.Default
     @Builder.Default
     private Integer continuousThreshold = 7;
     private Integer continuousThreshold = 7;
 
 
-    /** 连续签到最大累计奖励次数上限 */
+    /** 连续签到最大累计额外天数上限 */
     @Builder.Default
     @Builder.Default
     private Integer continuousCap = 30;
     private Integer continuousCap = 30;
 
 
-    /** 奖励爬虫次数有效天数(NULL=永不过期) */
-    private Integer rewardExpireDays;
-
     /** 备注 */
     /** 备注 */
     private String remark;
     private String remark;
 
 

+ 3 - 5
zhijiayun-activity/src/main/java/com/xuekairui/activity/entity/CheckInRecord.java

@@ -33,15 +33,13 @@ public class CheckInRecord {
 
 
     /** 奖励类型 */
     /** 奖励类型 */
     @Builder.Default
     @Builder.Default
-    private String rewardType = "CRAWLER";
 
 
-    /** 本次奖励数量 */
+    private String rewardType = "MEMBERSHIP";
+
+    /** 本次奖励数量(会员天数) */
     @Builder.Default
     @Builder.Default
     private Integer rewardAmount = 0;
     private Integer rewardAmount = 0;
 
 
-    /** 关联 t_crawler_quota_grant 发放记录ID */
-    private Long grantId;
-
     /** 签到端:WINDOWS/ANDROID/MINIAPP */
     /** 签到端:WINDOWS/ANDROID/MINIAPP */
     private String clientSource;
     private String clientSource;
 
 

+ 2 - 0
zhijiayun-activity/src/main/java/com/xuekairui/activity/service/ActivityCheckinCycleService.java

@@ -187,6 +187,7 @@ public class ActivityCheckinCycleService {
 
 
         int currentCount = userId != null ? countCheckinInCycle(userId, currentCycleStart, currentCycleEnd) : 0;
         int currentCount = userId != null ? countCheckinInCycle(userId, currentCycleStart, currentCycleEnd) : 0;
         int requiredDays = config.getRequiredDays() != null ? config.getRequiredDays() : 4;
         int requiredDays = config.getRequiredDays() != null ? config.getRequiredDays() : 4;
+        int rewardDays = config.getRewardDays() != null ? config.getRewardDays() : 7;
         long rewarded = userId != null ? cycleRecordMapper.countRewardedByUser(userId) : 0L;
         long rewarded = userId != null ? cycleRecordMapper.countRewardedByUser(userId) : 0L;
         int maxReward = config.getMaxRewardCount() != null ? config.getMaxRewardCount() : 3;
         int maxReward = config.getMaxRewardCount() != null ? config.getMaxRewardCount() : 3;
         long remaining = Math.max(0, maxReward - rewarded);
         long remaining = Math.max(0, maxReward - rewarded);
@@ -202,6 +203,7 @@ public class ActivityCheckinCycleService {
                 .rewardedCount(rewarded)
                 .rewardedCount(rewarded)
                 .maxRewardCount(maxReward)
                 .maxRewardCount(maxReward)
                 .remainingCount(remaining)
                 .remainingCount(remaining)
+                .rewardDays(rewardDays)
                 .build();
                 .build();
     }
     }
 
 

+ 15 - 17
zhijiayun-activity/src/main/java/com/xuekairui/activity/service/ActivityLotteryService.java

@@ -16,11 +16,9 @@ import com.xuekairui.activity.mapper.ActivityLotteryPrizeMapper;
 import com.xuekairui.activity.mapper.ActivityLotteryRecordMapper;
 import com.xuekairui.activity.mapper.ActivityLotteryRecordMapper;
 import com.xuekairui.common.BusinessException;
 import com.xuekairui.common.BusinessException;
 import com.xuekairui.common.ErrorCode;
 import com.xuekairui.common.ErrorCode;
-import com.xuekairui.user.entity.CrawlerQuotaGrant;
 import com.xuekairui.user.entity.User;
 import com.xuekairui.user.entity.User;
 import com.xuekairui.user.enums.MembershipLevel;
 import com.xuekairui.user.enums.MembershipLevel;
 import com.xuekairui.user.mapper.UserMapper;
 import com.xuekairui.user.mapper.UserMapper;
-import com.xuekairui.user.service.CrawlerQuotaGrantService;
 import com.xuekairui.user.service.MembershipService;
 import com.xuekairui.user.service.MembershipService;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -49,7 +47,6 @@ public class ActivityLotteryService {
     private static final Long CONFIG_ID = 1L;
     private static final Long CONFIG_ID = 1L;
     private static final String SOURCE_COMPARE = "COMPARE";
     private static final String SOURCE_COMPARE = "COMPARE";
     private static final String REWARD_TYPE_MEMBERSHIP = "MEMBERSHIP";
     private static final String REWARD_TYPE_MEMBERSHIP = "MEMBERSHIP";
-    private static final String REWARD_TYPE_CRAWLER = "CRAWLER";
     private static final String REWARD_TYPE_EMPTY = "EMPTY";
     private static final String REWARD_TYPE_EMPTY = "EMPTY";
 
 
     private final ActivityLotteryConfigMapper configMapper;
     private final ActivityLotteryConfigMapper configMapper;
@@ -58,7 +55,6 @@ public class ActivityLotteryService {
     private final ActivityLotteryRecordMapper recordMapper;
     private final ActivityLotteryRecordMapper recordMapper;
     private final UserMapper userMapper;
     private final UserMapper userMapper;
     private final MembershipService membershipService;
     private final MembershipService membershipService;
-    private final CrawlerQuotaGrantService crawlerQuotaGrantService;
 
 
     // ==========================================
     // ==========================================
     // 配置管理
     // 配置管理
@@ -266,6 +262,20 @@ public class ActivityLotteryService {
                 new LambdaQueryWrapper<ActivityLotteryPrize>().orderByAsc(ActivityLotteryPrize::getSort));
                 new LambdaQueryWrapper<ActivityLotteryPrize>().orderByAsc(ActivityLotteryPrize::getSort));
     }
     }
 
 
+    /**
+     * 用户端:获取启用的奖品列表(不含概率权重)
+     */
+    public List<ActivityLotteryPrize> listEnabledPrizes() {
+        List<ActivityLotteryPrize> prizes = prizeMapper.selectList(
+                new LambdaQueryWrapper<ActivityLotteryPrize>()
+                        .eq(ActivityLotteryPrize::getEnabled, 1)
+                        .orderByAsc(ActivityLotteryPrize::getSort));
+        for (ActivityLotteryPrize p : prizes) {
+            p.setProbabilityWeight(null);
+        }
+        return prizes;
+    }
+
     @Transactional
     @Transactional
     public ActivityLotteryPrize createPrize(ActivityLotteryPrizeRequest request) {
     public ActivityLotteryPrize createPrize(ActivityLotteryPrizeRequest request) {
         ActivityLotteryPrize prize = ActivityLotteryPrize.builder()
         ActivityLotteryPrize prize = ActivityLotteryPrize.builder()
@@ -365,7 +375,7 @@ public class ActivityLotteryService {
     }
     }
 
 
     /**
     /**
-     * 根据奖品类型发放奖励
+     * 根据奖品类型发放奖励(MEMBERSHIP 或 EMPTY)
      */
      */
     private void applyReward(Long userId, ActivityLotteryPrize prize) {
     private void applyReward(Long userId, ActivityLotteryPrize prize) {
         if (REWARD_TYPE_EMPTY.equalsIgnoreCase(prize.getRewardType())) {
         if (REWARD_TYPE_EMPTY.equalsIgnoreCase(prize.getRewardType())) {
@@ -383,18 +393,6 @@ public class ActivityLotteryService {
             }
             }
             membershipService.grantOrExtendMembership(userId, level, days,
             membershipService.grantOrExtendMembership(userId, level, days,
                     "ACTIVITY_LOTTERY", "比价抽奖中奖:" + prize.getName());
                     "ACTIVITY_LOTTERY", "比价抽奖中奖:" + prize.getName());
-            return;
-        }
-        if (REWARD_TYPE_CRAWLER.equalsIgnoreCase(prize.getRewardType())) {
-            int amount = prize.getRewardAmount() != null ? prize.getRewardAmount() : 0;
-            if (amount <= 0) {
-                return;
-            }
-            LocalDateTime expireTime = LocalDateTime.now().plusDays(30);
-            CrawlerQuotaGrant grant = crawlerQuotaGrantService.grantAdminQuota(
-                    userId, amount, expireTime, "比价抽奖中奖:" + prize.getName());
-            log.info("抽奖爬虫次数奖励已发放: userId={}, amount={}, grantId={}", userId, amount,
-                    grant != null ? grant.getId() : null);
         }
         }
     }
     }
 }
 }

+ 7 - 8
zhijiayun-activity/src/main/java/com/xuekairui/activity/service/CheckInConfigService.java

@@ -12,6 +12,9 @@ import org.springframework.transaction.annotation.Transactional;
 
 
 /**
 /**
  * 签到配置服务(运营端)
  * 签到配置服务(运营端)
+ * <p>
+ * 奖励体系为高级会员天数:
+ * baseReward = 每日基础会员天数,continuousReward = 连续满阈值后每日额外会员天数。
  */
  */
 @Slf4j
 @Slf4j
 @Service
 @Service
@@ -31,13 +34,12 @@ public class CheckInConfigService {
             config = CheckInConfig.builder()
             config = CheckInConfig.builder()
                     .id(CONFIG_ID)
                     .id(CONFIG_ID)
                     .enabled(1)
                     .enabled(1)
-                    .rewardType("CRAWLER")
+                    .rewardType("MEMBERSHIP")
                     .baseReward(1)
                     .baseReward(1)
                     .continuousReward(1)
                     .continuousReward(1)
                     .continuousThreshold(7)
                     .continuousThreshold(7)
                     .continuousCap(30)
                     .continuousCap(30)
-                    .rewardExpireDays(30)
-                    .remark("默认签到配置")
+                    .remark("默认签到配置:每日1天高级会员,连续7天起每日额外+1天")
                     .build();
                     .build();
             checkinConfigMapper.insert(config);
             checkinConfigMapper.insert(config);
         }
         }
@@ -51,17 +53,14 @@ public class CheckInConfigService {
     public CheckInConfig updateConfig(CheckInConfigRequest request) {
     public CheckInConfig updateConfig(CheckInConfigRequest request) {
         CheckInConfig config = getConfig();
         CheckInConfig config = getConfig();
         config.setEnabled(request.getEnabled());
         config.setEnabled(request.getEnabled());
-        if (request.getRewardType() != null && !request.getRewardType().isBlank()) {
-            config.setRewardType(request.getRewardType());
-        }
+        config.setRewardType("MEMBERSHIP");
         config.setBaseReward(request.getBaseReward());
         config.setBaseReward(request.getBaseReward());
         config.setContinuousReward(request.getContinuousReward());
         config.setContinuousReward(request.getContinuousReward());
         config.setContinuousThreshold(request.getContinuousThreshold());
         config.setContinuousThreshold(request.getContinuousThreshold());
         config.setContinuousCap(request.getContinuousCap());
         config.setContinuousCap(request.getContinuousCap());
-        config.setRewardExpireDays(request.getRewardExpireDays());
         config.setRemark(request.getRemark());
         config.setRemark(request.getRemark());
         checkinConfigMapper.updateById(config);
         checkinConfigMapper.updateById(config);
-        log.info("签到配置已更新: enabled={}, baseReward={}, continuousReward={}, threshold={}",
+        log.info("签到配置已更新: enabled={}, baseReward={}, continuousReward={}, threshold={}",
                 config.getEnabled(), config.getBaseReward(),
                 config.getEnabled(), config.getBaseReward(),
                 config.getContinuousReward(), config.getContinuousThreshold());
                 config.getContinuousReward(), config.getContinuousThreshold());
         return config;
         return config;

+ 11 - 29
zhijiayun-activity/src/main/java/com/xuekairui/activity/service/CheckInService.java

@@ -13,15 +13,14 @@ import com.xuekairui.activity.entity.CheckInRecord;
 import com.xuekairui.activity.mapper.CheckInRecordMapper;
 import com.xuekairui.activity.mapper.CheckInRecordMapper;
 import com.xuekairui.common.BusinessException;
 import com.xuekairui.common.BusinessException;
 import com.xuekairui.common.ErrorCode;
 import com.xuekairui.common.ErrorCode;
-import com.xuekairui.user.entity.CrawlerQuotaGrant;
-import com.xuekairui.user.service.CrawlerQuotaGrantService;
+import com.xuekairui.user.enums.MembershipLevel;
+import com.xuekairui.user.service.MembershipService;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 
 
 import java.time.LocalDate;
 import java.time.LocalDate;
-import java.time.LocalDateTime;
 import java.time.YearMonth;
 import java.time.YearMonth;
 import java.util.List;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
@@ -32,20 +31,17 @@ import java.util.stream.Collectors;
  * 业务规则:
  * 业务规则:
  * 1. 每个用户每个自然日仅可签到一次(DB 唯一索引兜底)
  * 1. 每个用户每个自然日仅可签到一次(DB 唯一索引兜底)
  * 2. 连续签到:若昨日已签到,则连续天数 +1;否则重置为 1
  * 2. 连续签到:若昨日已签到,则连续天数 +1;否则重置为 1
- * 3. 奖励计算:基础奖励 + (连续天数 >= 阈值 ? 连续奖励 : 0),且累计连续奖励不超过 continuousCap
- * 4. 奖励以爬虫次数发放,复用 t_crawler_quota_grant(grant_type=CHECKIN)
+ * 3. 奖励计算:基础会员天数 + (连续天数 >= 阈值 ? 额外会员天数 : 0),累计额外不超过 continuousCap
+ * 4. 奖励以高级会员天数发放,通过 MembershipService 续期
  */
  */
 @Slf4j
 @Slf4j
 @Service
 @Service
 @RequiredArgsConstructor
 @RequiredArgsConstructor
 public class CheckInService {
 public class CheckInService {
 
 
-    /** 签到发放爬虫配额类型 */
-    public static final String GRANT_TYPE_CHECKIN = "CHECKIN";
-
     private final CheckInConfigService checkinConfigService;
     private final CheckInConfigService checkinConfigService;
     private final CheckInRecordMapper checkinRecordMapper;
     private final CheckInRecordMapper checkinRecordMapper;
-    private final CrawlerQuotaGrantService crawlerQuotaGrantService;
+    private final MembershipService membershipService;
     private final ActivityCheckinCycleService activityCheckinCycleService;
     private final ActivityCheckinCycleService activityCheckinCycleService;
 
 
     /**
     /**
@@ -78,7 +74,7 @@ public class CheckInService {
     }
     }
 
 
     /**
     /**
-     * 执行签到
+     * 执行签到,发放高级会员天数
      */
      */
     @Transactional
     @Transactional
     public CheckInResultResponse doCheckIn(Long userId, String clientSource) {
     public CheckInResultResponse doCheckIn(Long userId, String clientSource) {
@@ -94,13 +90,8 @@ public class CheckInService {
         int continuousDays = calcContinuousDays(userId, today) + 1;
         int continuousDays = calcContinuousDays(userId, today) + 1;
         int rewardAmount = calcReward(config, continuousDays);
         int rewardAmount = calcReward(config, continuousDays);
 
 
-        LocalDateTime expireTime = config.getRewardExpireDays() != null
-                ? LocalDateTime.now().plusDays(config.getRewardExpireDays())
-                : null;
-
-        CrawlerQuotaGrant grant = crawlerQuotaGrantService.grantAdminQuota(
-                userId, rewardAmount, expireTime,
-                "签到奖励 连续第" + continuousDays + "天");
+        membershipService.grantOrExtendMembership(userId, MembershipLevel.PRO, rewardAmount,
+                "CHECKIN", "签到奖励 连续第" + continuousDays + "天 获" + rewardAmount + "天高级会员");
 
 
         CheckInRecord record = CheckInRecord.builder()
         CheckInRecord record = CheckInRecord.builder()
                 .userId(userId)
                 .userId(userId)
@@ -108,15 +99,13 @@ public class CheckInService {
                 .continuousDays(continuousDays)
                 .continuousDays(continuousDays)
                 .rewardType(config.getRewardType())
                 .rewardType(config.getRewardType())
                 .rewardAmount(rewardAmount)
                 .rewardAmount(rewardAmount)
-                .grantId(grant != null ? grant.getId() : null)
                 .clientSource(clientSource)
                 .clientSource(clientSource)
                 .build();
                 .build();
         checkinRecordMapper.insert(record);
         checkinRecordMapper.insert(record);
 
 
-        log.info("用户签到成功: userId={}, date={}, continuous={}, reward={}",
+        log.info("用户签到成功: userId={}, date={}, continuous={}, reward={}天高级会员",
                 userId, today, continuousDays, rewardAmount);
                 userId, today, continuousDays, rewardAmount);
 
 
-        // 签到送会员活动:尝试发放上一周期奖励(活动关闭时内部直接返回,不影响原逻辑)
         try {
         try {
             activityCheckinCycleService.tryGrantCycleReward(userId);
             activityCheckinCycleService.tryGrantCycleReward(userId);
         } catch (Exception e) {
         } catch (Exception e) {
@@ -179,12 +168,6 @@ public class CheckInService {
 
 
     /**
     /**
      * 运营端:分页查询签到记录(关联用户信息)
      * 运营端:分页查询签到记录(关联用户信息)
-     *
-     * @param page         分页对象
-     * @param userId       用户ID(可选)
-     * @param startDate    起始日期(可选)
-     * @param endDate      截止日期(可选)
-     * @param clientSource 签到端(可选)
      */
      */
     public IPage<AdminCheckInRecordResponse> adminListRecords(Page<AdminCheckInRecordResponse> page,
     public IPage<AdminCheckInRecordResponse> adminListRecords(Page<AdminCheckInRecordResponse> page,
                                                               Long userId,
                                                               Long userId,
@@ -215,7 +198,7 @@ public class CheckInService {
     }
     }
 
 
     /**
     /**
-     * 运营端:查询指定用户的签到记录(含用户基础信息,最多500条)
+     * 运营端:查询指定用户的签到记录
      */
      */
     public List<AdminCheckInRecordResponse> adminListUserRecords(Long userId) {
     public List<AdminCheckInRecordResponse> adminListUserRecords(Long userId) {
         if (userId == null) {
         if (userId == null) {
@@ -242,7 +225,6 @@ public class CheckInService {
 
 
     /**
     /**
      * 计算截至 today 的连续签到天数(不含 today)
      * 计算截至 today 的连续签到天数(不含 today)
-     * 若昨天签到则为昨天连续天数+1,否则为 0
      */
      */
     private int calcContinuousDays(Long userId, LocalDate today) {
     private int calcContinuousDays(Long userId, LocalDate today) {
         CheckInRecord yesterday = findRecord(userId, today.minusDays(1));
         CheckInRecord yesterday = findRecord(userId, today.minusDays(1));
@@ -253,7 +235,7 @@ public class CheckInService {
     }
     }
 
 
     /**
     /**
-     * 计算签到奖励:基础 + 连续奖励(达阈值后叠加,受上限约束)
+     * 计算签到奖励(会员天数):基础天数 + 连续额外天数(达阈值后叠加,受上限约束)
      */
      */
     private int calcReward(CheckInConfig config, int continuousDays) {
     private int calcReward(CheckInConfig config, int continuousDays) {
         int reward = config.getBaseReward();
         int reward = config.getBaseReward();

+ 0 - 1
zhijiayun-activity/src/main/resources/mapper/CheckInRecordMapper.xml

@@ -15,7 +15,6 @@
             r.continuous_days AS continuousDays,
             r.continuous_days AS continuousDays,
             r.reward_type     AS rewardType,
             r.reward_type     AS rewardType,
             r.reward_amount   AS rewardAmount,
             r.reward_amount   AS rewardAmount,
-            r.grant_id        AS grantId,
             r.client_source   AS clientSource,
             r.client_source   AS clientSource,
             r.create_time     AS createTime
             r.create_time     AS createTime
         FROM t_checkin_record r
         FROM t_checkin_record r

+ 10 - 12
zhijiayun-gateway/src/main/resources/db/migration-v10.sql

@@ -1,18 +1,17 @@
 -- ============================================
 -- ============================================
 -- 迁移 v10:签到有礼(活动模块)
 -- 迁移 v10:签到有礼(活动模块)
--- 说明:签到奖励以爬虫次数形式发放,复用 t_crawler_quota_grant 表(grant_type=CHECKIN)
+-- 说明:签到奖励以高级会员天数形式发放,通过 MembershipService 续期
 -- ============================================
 -- ============================================
 
 
 -- 1. 签到配置表(全局唯一一条记录,运营后台可编辑)
 -- 1. 签到配置表(全局唯一一条记录,运营后台可编辑)
 CREATE TABLE IF NOT EXISTS `t_checkin_config` (
 CREATE TABLE IF NOT EXISTS `t_checkin_config` (
     `id`                   BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
     `id`                   BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
     `enabled`              TINYINT       NOT NULL DEFAULT 1 COMMENT '是否启用签到:0-停用 1-启用',
     `enabled`              TINYINT       NOT NULL DEFAULT 1 COMMENT '是否启用签到:0-停用 1-启用',
-    `reward_type`          VARCHAR(20)   NOT NULL DEFAULT 'CRAWLER' COMMENT '奖励类型:CRAWLER-爬虫次数(预留 POINTS/COUPON)',
-    `base_reward`          INT           NOT NULL DEFAULT 1 COMMENT '基础签到奖励(每日)',
-    `continuous_reward`    INT           NOT NULL DEFAULT 1 COMMENT '连续签到额外奖励(满足连续天数后每日叠加)',
-    `continuous_threshold` INT           NOT NULL DEFAULT 7 COMMENT '连续签到奖励触发阈值(天)',
-    `continuous_cap`       INT           NOT NULL DEFAULT 30 COMMENT '连续签到最大累计奖励次数上限',
-    `reward_expire_days`   INT           DEFAULT NULL COMMENT '奖励爬虫次数有效天数(NULL=永不过期)',
+    `reward_type`          VARCHAR(20)   NOT NULL DEFAULT 'MEMBERSHIP' COMMENT '奖励类型:MEMBERSHIP-会员天数',
+    `base_reward`          INT           NOT NULL DEFAULT 1 COMMENT '基础签到奖励会员天数(每日)',
+    `continuous_reward`    INT           NOT NULL DEFAULT 1 COMMENT '连续签到额外会员天数(满足连续天数后每日叠加)',
+    `continuous_threshold` INT           NOT NULL DEFAULT 7 COMMENT '连续签到额外奖励触发阈值(天)',
+    `continuous_cap`       INT           NOT NULL DEFAULT 30 COMMENT '连续签到最大累计额外天数上限',
     `remark`               VARCHAR(500)  DEFAULT NULL COMMENT '备注说明',
     `remark`               VARCHAR(500)  DEFAULT NULL COMMENT '备注说明',
     `create_time`          DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     `create_time`          DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     `update_time`          DATETIME      DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     `update_time`          DATETIME      DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
@@ -21,8 +20,8 @@ CREATE TABLE IF NOT EXISTS `t_checkin_config` (
 
 
 -- 初始化默认配置(id=1)
 -- 初始化默认配置(id=1)
 INSERT INTO `t_checkin_config` (`id`, `enabled`, `reward_type`, `base_reward`, `continuous_reward`,
 INSERT INTO `t_checkin_config` (`id`, `enabled`, `reward_type`, `base_reward`, `continuous_reward`,
-                                `continuous_threshold`, `continuous_cap`, `reward_expire_days`, `remark`)
-VALUES (1, 1, 'CRAWLER', 1, 1, 7, 30, 30, '默认签到配置:每日1次,连续7天起每日额外+1,单次奖励30天有效')
+                                `continuous_threshold`, `continuous_cap`, `remark`)
+VALUES (1, 1, 'MEMBERSHIP', 1, 1, 7, 30, '默认签到配置:每日1天高级会员,连续7天起每日额外+1天')
 ON DUPLICATE KEY UPDATE id = id;
 ON DUPLICATE KEY UPDATE id = id;
 
 
 -- 2. 用户签到记录表(每日一条,唯一索引保证一人一天一次)
 -- 2. 用户签到记录表(每日一条,唯一索引保证一人一天一次)
@@ -31,9 +30,8 @@ CREATE TABLE IF NOT EXISTS `t_checkin_record` (
     `user_id`          BIGINT        NOT NULL COMMENT '用户ID',
     `user_id`          BIGINT        NOT NULL COMMENT '用户ID',
     `checkin_date`     DATE          NOT NULL COMMENT '签到日期(自然日)',
     `checkin_date`     DATE          NOT NULL COMMENT '签到日期(自然日)',
     `continuous_days`  INT           NOT NULL DEFAULT 1 COMMENT '签到时连续天数',
     `continuous_days`  INT           NOT NULL DEFAULT 1 COMMENT '签到时连续天数',
-    `reward_type`      VARCHAR(20)   NOT NULL DEFAULT 'CRAWLER' COMMENT '奖励类型',
-    `reward_amount`    INT           NOT NULL DEFAULT 0 COMMENT '本次奖励数量',
-    `grant_id`         BIGINT        DEFAULT NULL COMMENT '关联 t_crawler_quota_grant 发放记录ID',
+    `reward_type`      VARCHAR(20)   NOT NULL DEFAULT 'MEMBERSHIP' COMMENT '奖励类型',
+    `reward_amount`    INT           NOT NULL DEFAULT 0 COMMENT '本次奖励会员天数',
     `client_source`   VARCHAR(20)   DEFAULT NULL COMMENT '签到端:WINDOWS/ANDROID/MINIAPP',
     `client_source`   VARCHAR(20)   DEFAULT NULL COMMENT '签到端:WINDOWS/ANDROID/MINIAPP',
     `create_time`      DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间',
     `create_time`      DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间',
     PRIMARY KEY (`id`),
     PRIMARY KEY (`id`),

+ 3 - 4
zhijiayun-gateway/src/main/resources/db/migration-v11.sql

@@ -119,7 +119,7 @@ CREATE TABLE IF NOT EXISTS `t_activity_lottery_chance` (
 CREATE TABLE IF NOT EXISTS `t_activity_lottery_prize` (
 CREATE TABLE IF NOT EXISTS `t_activity_lottery_prize` (
     `id`                 BIGINT       NOT NULL AUTO_INCREMENT,
     `id`                 BIGINT       NOT NULL AUTO_INCREMENT,
     `name`               VARCHAR(50)  NOT NULL COMMENT '奖品名称',
     `name`               VARCHAR(50)  NOT NULL COMMENT '奖品名称',
-    `reward_type`        VARCHAR(20)  NOT NULL COMMENT '奖励类型:MEMBERSHIP-会员 CRAWLER-爬虫次数 EMPTY-未中奖',
+    `reward_type`        VARCHAR(20)  NOT NULL COMMENT '奖励类型:MEMBERSHIP-会员 EMPTY-未中奖',
     `reward_amount`      INT          NOT NULL DEFAULT 0 COMMENT '奖励数量(爬虫次数等)',
     `reward_amount`      INT          NOT NULL DEFAULT 0 COMMENT '奖励数量(爬虫次数等)',
     `reward_days`        INT          DEFAULT NULL COMMENT '奖励会员天数(MEMBERSHIP类型)',
     `reward_days`        INT          DEFAULT NULL COMMENT '奖励会员天数(MEMBERSHIP类型)',
     `reward_level`       VARCHAR(20)  DEFAULT NULL COMMENT '奖励会员等级(MEMBERSHIP类型)',
     `reward_level`       VARCHAR(20)  DEFAULT NULL COMMENT '奖励会员等级(MEMBERSHIP类型)',
@@ -136,9 +136,8 @@ INSERT INTO `t_activity_lottery_prize` (`name`, `reward_type`, `reward_amount`,
 VALUES
 VALUES
 ('高级会员7天', 'MEMBERSHIP', 0, 7, 'PRO', 10, 1, 1),
 ('高级会员7天', 'MEMBERSHIP', 0, 7, 'PRO', 10, 1, 1),
 ('高级会员3天', 'MEMBERSHIP', 0, 3, 'PRO', 20, 1, 2),
 ('高级会员3天', 'MEMBERSHIP', 0, 3, 'PRO', 20, 1, 2),
-('爬虫次数5次', 'CRAWLER', 5, NULL, NULL, 30, 1, 3),
-('爬虫次数2次', 'CRAWLER', 2, NULL, NULL, 40, 1, 4),
-('谢谢参与', 'EMPTY', 0, NULL, NULL, 100, 1, 5);
+('高级会员1天', 'MEMBERSHIP', 0, 1, 'PRO', 40, 1, 3),
+('谢谢参与', 'EMPTY', 0, NULL, NULL, 100, 1, 4);
 
 
 -- 抽奖记录表
 -- 抽奖记录表
 CREATE TABLE IF NOT EXISTS `t_activity_lottery_record` (
 CREATE TABLE IF NOT EXISTS `t_activity_lottery_record` (

+ 173 - 43
zhijiayun-test/src/main/resources/static/activity-test.html

@@ -73,6 +73,11 @@
         .field-desc-table td { padding: 4px 8px; border: 1px solid #ddd; color: #555; }
         .field-desc-table td { padding: 4px 8px; border: 1px solid #ddd; color: #555; }
         .field-desc-table td code { background: #e8e8e8; padding: 1px 4px; border-radius: 2px; font-size: 11px; }
         .field-desc-table td code { background: #e8e8e8; padding: 1px 4px; border-radius: 2px; font-size: 11px; }
 
 
+        .field-desc-details { margin-bottom: 12px; background: #f5f7fa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px 12px; }
+        .field-desc-summary { cursor: pointer; font-size: 13px; font-weight: 600; color: #555; user-select: none; padding: 2px 0; }
+        .field-desc-summary:hover { color: #1976d2; }
+        .curl-example { background: #1e1e1e; color: #dcdcaa; padding: 12px 14px; border-radius: 4px; font-family: Consolas, Monaco, 'Courier New', monospace; font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; margin: 6px 0 0 0; line-height: 1.6; }
+
         .summary-bar { display: flex; gap: 16px; padding: 12px 16px; background: white; border-radius: 8px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); align-items: center; flex-wrap: wrap; }
         .summary-bar { display: flex; gap: 16px; padding: 12px 16px; background: white; border-radius: 8px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); align-items: center; flex-wrap: wrap; }
         .summary-item { display: flex; align-items: center; gap: 6px; font-size: 14px; }
         .summary-item { display: flex; align-items: center; gap: 6px; font-size: 14px; }
         .summary-count { font-weight: 700; font-size: 20px; }
         .summary-count { font-weight: 700; font-size: 20px; }
@@ -460,7 +465,7 @@ const FIELD_DESC = {
     'checkinStatus': [
     'checkinStatus': [
         ['data.checkedToday', 'Boolean', '今日是否已签到'],
         ['data.checkedToday', 'Boolean', '今日是否已签到'],
         ['data.continuousDays', 'Integer', '当前连续签到天数(今日未签到时为若签到将达成的天数-1)'],
         ['data.continuousDays', 'Integer', '当前连续签到天数(今日未签到时为若签到将达成的天数-1)'],
-        ['data.todayReward', 'Integer', '今日签到可获奖励数量'],
+        ['data.todayReward', 'Integer', '今日签到可获会员天数'],
         ['data.nextCheckinDate', 'Date', '下一次可签到日期(今日未签到=今天)'],
         ['data.nextCheckinDate', 'Date', '下一次可签到日期(今日未签到=今天)'],
         ['data.enabled', 'Boolean', '签到功能是否启用']
         ['data.enabled', 'Boolean', '签到功能是否启用']
     ],
     ],
@@ -468,8 +473,8 @@ const FIELD_DESC = {
         ['data.id', 'Long', '签到记录ID'],
         ['data.id', 'Long', '签到记录ID'],
         ['data.checkinDate', 'Date', '签到日期'],
         ['data.checkinDate', 'Date', '签到日期'],
         ['data.continuousDays', 'Integer', '签到时连续天数'],
         ['data.continuousDays', 'Integer', '签到时连续天数'],
-        ['data.rewardAmount', 'Integer', '本次奖励数量(爬虫次数)'],
-        ['data.rewardType', 'String', '奖励类型(CRAWLER)'],
+        ['data.rewardAmount', 'Integer', '本次奖励会员天数'],
+        ['data.rewardType', 'String', '奖励类型(MEMBERSHIP)'],
         ['data.createTime', 'DateTime', '签到时间']
         ['data.createTime', 'DateTime', '签到时间']
     ],
     ],
     'checkinCalendar': [
     'checkinCalendar': [
@@ -482,12 +487,11 @@ const FIELD_DESC = {
     'checkinConfig': [
     'checkinConfig': [
         ['data.id', 'Long', '配置ID(固定1)'],
         ['data.id', 'Long', '配置ID(固定1)'],
         ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
         ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
-        ['data.rewardType', 'String', '奖励类型(CRAWLER)'],
-        ['data.baseReward', 'Integer', '基础签到奖励(每日)'],
-        ['data.continuousReward', 'Integer', '连续签到额外奖励'],
-        ['data.continuousThreshold', 'Integer', '连续奖励触发阈值(天)'],
-        ['data.continuousCap', 'Integer', '连续奖励累计上限'],
-        ['data.rewardExpireDays', 'Integer', '奖励有效天数(null=永久)'],
+        ['data.rewardType', 'String', '奖励类型(MEMBERSHIP-会员天数)'],
+        ['data.baseReward', 'Integer', '基础会员天数(每日)'],
+        ['data.continuousReward', 'Integer', '连续签到额外会员天数'],
+        ['data.continuousThreshold', 'Integer', '连续额外触发阈值(天)'],
+        ['data.continuousCap', 'Integer', '连续额外累计上限(天)'],
         ['data.remark', 'String', '备注'],
         ['data.remark', 'String', '备注'],
         ['data.createTime', 'DateTime', '创建时间'],
         ['data.createTime', 'DateTime', '创建时间'],
         ['data.updateTime', 'DateTime', '更新时间']
         ['data.updateTime', 'DateTime', '更新时间']
@@ -499,7 +503,7 @@ const FIELD_DESC = {
         ['data.last30DaysCount', 'Long', '近30天签到人数(去重)'],
         ['data.last30DaysCount', 'Long', '近30天签到人数(去重)'],
         ['data.totalCheckinCount', 'Long', '累计签到总次数'],
         ['data.totalCheckinCount', 'Long', '累计签到总次数'],
         ['data.totalCheckinUsers', 'Long', '累计签到用户数(去重)'],
         ['data.totalCheckinUsers', 'Long', '累计签到用户数(去重)'],
-        ['data.totalRewardAmount', 'Long', '累计发放奖励次数总和'],
+        ['data.totalRewardAmount', 'Long', '累计发放会员天数总和'],
         ['data.statDate', 'Date', '统计日期'],
         ['data.statDate', 'Date', '统计日期'],
         ['data.enabled', 'Boolean', '签到功能是否启用']
         ['data.enabled', 'Boolean', '签到功能是否启用']
     ],
     ],
@@ -512,14 +516,113 @@ const FIELD_DESC = {
         ['data.records[].checkinDate', 'Date', '签到日期'],
         ['data.records[].checkinDate', 'Date', '签到日期'],
         ['data.records[].continuousDays', 'Integer', '签到时连续天数'],
         ['data.records[].continuousDays', 'Integer', '签到时连续天数'],
         ['data.records[].rewardType', 'String', '奖励类型'],
         ['data.records[].rewardType', 'String', '奖励类型'],
-        ['data.records[].rewardAmount', 'Integer', '本次奖励数量'],
-        ['data.records[].grantId', 'Long', '关联配额发放记录ID'],
+        ['data.records[].rewardAmount', 'Integer', '本次奖励会员天数'],
         ['data.records[].clientSource', 'String', '签到端'],
         ['data.records[].clientSource', 'String', '签到端'],
         ['data.records[].createTime', 'DateTime', '签到时间'],
         ['data.records[].createTime', 'DateTime', '签到时间'],
         ['data.total', 'Long', '总记录数'],
         ['data.total', 'Long', '总记录数'],
         ['data.current', 'Long', '当前页'],
         ['data.current', 'Long', '当前页'],
         ['data.size', 'Long', '每页条数'],
         ['data.size', 'Long', '每页条数'],
         ['data.pages', 'Long', '总页数']
         ['data.pages', 'Long', '总页数']
+    ],
+    'activityInviteStatus': [
+        ['data.enabled', 'Boolean', '活动是否启用'],
+        ['data.inWindow', 'Boolean', '是否在活动时间窗内'],
+        ['data.rewardedCount', 'Long', '已获奖励次数'],
+        ['data.maxInviterReward', 'Integer', '邀请人最多奖励次数'],
+        ['data.remainingCount', 'Long', '剩余可获奖励次数']
+    ],
+    'activityCheckinStatus': [
+        ['data.enabled', 'Boolean', '活动是否启用'],
+        ['data.inWindow', 'Boolean', '是否在活动时间窗内'],
+        ['data.currentCycleStart', 'Date', '当前周期起始日(周五)'],
+        ['data.currentCycleEnd', 'Date', '当前周期截止日(下周四)'],
+        ['data.currentCycleCheckinCount', 'Integer', '当前周期内已签到天数'],
+        ['data.requiredDays', 'Integer', '达标所需签到天数'],
+        ['data.reachedTarget', 'Boolean', '当前周期是否已达标'],
+        ['data.rewardDays', 'Integer', '达标后可获会员天数'],
+        ['data.rewardedCount', 'Long', '已领取奖励次数'],
+        ['data.maxRewardCount', 'Integer', '最多可领取奖励次数'],
+        ['data.remainingCount', 'Long', '剩余可领取奖励次数']
+    ],
+    'activityLotteryStatus': [
+        ['data.enabled', 'Boolean', '活动是否启用'],
+        ['data.inWindow', 'Boolean', '是否在活动时间窗内'],
+        ['data.eligible', 'Boolean', '是否满足参与资格(会员等级)'],
+        ['data.membershipLevel', 'String', '当前用户会员等级'],
+        ['data.requireMembershipLevel', 'String', '所需会员等级'],
+        ['data.availableChances', 'Integer', '今日可用抽奖机会数'],
+        ['data.usedChances', 'Integer', '今日已用抽奖机会数']
+    ],
+    'activityLotteryPrizes': [
+        ['data[].id', 'Long', '奖品ID'],
+        ['data[].name', 'String', '奖品名称'],
+        ['data[].rewardType', 'String', '奖励类型(MEMBERSHIP会员/CRAWLER爬虫次数/EMPTY谢谢参与)'],
+        ['data[].rewardDays', 'Integer', '会员奖励天数'],
+        ['data[].rewardLevel', 'String', '会员奖励等级(PRO/ULTRA)'],
+        ['data[].rewardAmount', 'Integer', '爬虫奖励次数(CRAWLER类型时有效,暂未实现)'],
+        ['data[].sort', 'Integer', '排序']
+    ],
+    'activityLotteryDraw': [
+        ['data.recordId', 'Long', '抽奖记录ID'],
+        ['data.prizeName', 'String', '奖品名称'],
+        ['data.rewardType', 'String', '奖励类型(MEMBERSHIP会员/CRAWLER爬虫次数/EMPTY谢谢参与)'],
+        ['data.rewardAmount', 'Integer', '奖励数量(预留)'],
+        ['data.rewardDays', 'Integer', '会员奖励天数'],
+        ['data.rewardLevel', 'String', '会员奖励等级(PRO/ULTRA)'],
+        ['data.win', 'Boolean', '是否中奖(EMPTY=未中奖)'],
+        ['data.createTime', 'DateTime', '抽奖时间']
+    ],
+    'adminActivityInviteConfig': [
+        ['data.id', 'Long', '配置ID(固定1)'],
+        ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
+        ['data.startTime', 'DateTime', '活动开始时间'],
+        ['data.endTime', 'DateTime', '活动结束时间'],
+        ['data.inviterRewardDays', 'Integer', '邀请人奖励天数'],
+        ['data.inviteeRewardDays', 'Integer', '被邀请人奖励天数'],
+        ['data.maxInviterReward', 'Integer', '邀请人最多奖励次数'],
+        ['data.rewardTrigger', 'String', '奖励触发模式(REGISTRATION/LICENSE_APPROVED)'],
+        ['data.requireMiniappInviter', 'Integer', '是否要求邀请人为小程序用户:0-否 1-是'],
+        ['data.remark', 'String', '备注'],
+        ['data.createTime', 'DateTime', '创建时间'],
+        ['data.updateTime', 'DateTime', '更新时间']
+    ],
+    'adminActivityInviteRevoke': [
+        ['data', 'Integer', '撤销的奖励记录数']
+    ],
+    'adminActivityCheckinConfig': [
+        ['data.id', 'Long', '配置ID(固定1)'],
+        ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
+        ['data.startTime', 'DateTime', '活动开始时间'],
+        ['data.endTime', 'DateTime', '活动结束时间'],
+        ['data.cycleMode', 'String', '周期模式(FRI_THU=周五至下周四)'],
+        ['data.requiredDays', 'Integer', '达标所需签到天数'],
+        ['data.rewardDays', 'Integer', '达标奖励会员天数'],
+        ['data.maxRewardCount', 'Integer', '每人最多可领取奖励次数'],
+        ['data.remark', 'String', '备注'],
+        ['data.createTime', 'DateTime', '创建时间'],
+        ['data.updateTime', 'DateTime', '更新时间']
+    ],
+    'adminActivityLotteryConfig': [
+        ['data.id', 'Long', '配置ID(固定1)'],
+        ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
+        ['data.startTime', 'DateTime', '活动开始时间'],
+        ['data.endTime', 'DateTime', '活动结束时间'],
+        ['data.dailyMaxChance', 'Integer', '每个自然日最多获得抽奖机会数'],
+        ['data.requireMembershipLevel', 'String', '参与所需会员等级(PRO/ULTRA/PLUS)'],
+        ['data.remark', 'String', '备注'],
+        ['data.createTime', 'DateTime', '创建时间'],
+        ['data.updateTime', 'DateTime', '更新时间']
+    ],
+    'adminActivityLotteryPrize': [
+        ['data.id', 'Long', '奖品ID'],
+        ['data.name', 'String', '奖品名称'],
+        ['data.rewardType', 'String', '奖励类型(MEMBERSHIP会员/CRAWLER爬虫次数/EMPTY谢谢参与)'],
+        ['data.rewardAmount', 'Integer', '奖励数量(预留)'],
+        ['data.rewardDays', 'Integer', '会员奖励天数'],
+        ['data.rewardLevel', 'String', '会员奖励等级'],
+        ['data.probabilityWeight', 'Integer', '中奖概率权重(越大越容易中)'],
+        ['data.enabled', 'Integer', '是否启用:0-停用 1-启用'],
+        ['data.sort', 'Integer', '排序(数字越小越靠前)']
     ]
     ]
 };
 };
 
 
@@ -527,10 +630,35 @@ function renderFieldDesc(descKey) {
     const fields = FIELD_DESC[descKey];
     const fields = FIELD_DESC[descKey];
     if (!fields) return '';
     if (!fields) return '';
     let rows = fields.map(f => `<tr><td><code>${f[0]}</code></td><td>${f[1]}</td><td>${f[2]}</td></tr>`).join('');
     let rows = fields.map(f => `<tr><td><code>${f[0]}</code></td><td>${f[1]}</td><td>${f[2]}</td></tr>`).join('');
-    return `<div class="field-desc-box">
-        <div class="field-desc-title">返回字段说明</div>
+    return `<details open class="field-desc-details">
+        <summary class="field-desc-summary">📋 返回字段说明</summary>
         <table class="field-desc-table"><tr><th>字段名</th><th>类型</th><th>说明</th></tr>${rows}</table>
         <table class="field-desc-table"><tr><th>字段名</th><th>类型</th><th>说明</th></tr>${rows}</table>
-    </div>`;
+    </details>`;
+}
+
+// ==================== 调用示例生成 ====================
+function renderCurlExample(id, method, path, fields) {
+    const methodUpper = method.toUpperCase();
+    const baseUrl = document.getElementById('baseUrl').value;
+    let curl = `curl -X ${methodUpper} "${baseUrl}${path}"`;
+    curl += ` \\\n  -H "Authorization: Bearer \${TOKEN}"`;
+    if (methodUpper === 'POST' || methodUpper === 'PUT') {
+        curl += ` \\\n  -H "Content-Type: application/json"`;
+    }
+    if (fields && fields.length > 0) {
+        let bodyParts = [];
+        fields.forEach(f => {
+            let val = f.default || f.placeholder || '';
+            bodyParts.push(`"${f.name}": "${val}"`);
+        });
+        if (bodyParts.length > 0) {
+            curl += ` \\\n  -d '{${bodyParts.join(', ')}}'`;
+        }
+    }
+    return `<details class="field-desc-details">
+        <summary class="field-desc-summary">📞 cURL 调用示例</summary>
+        <pre class="curl-example">${curl}</pre>
+    </details>`;
 }
 }
 
 
 // ==================== 测试用例区块渲染 ====================
 // ==================== 测试用例区块渲染 ====================
@@ -553,6 +681,7 @@ function testSection(id, title, method, methodClass, path, authTag, authClass, f
     }
     }
     const descHtml = desc ? `<div class="section-desc">${desc}</div>` : '';
     const descHtml = desc ? `<div class="section-desc">${desc}</div>` : '';
     const fieldDescHtml = descKey ? renderFieldDesc(descKey) : '';
     const fieldDescHtml = descKey ? renderFieldDesc(descKey) : '';
+    const curlHtml = renderCurlExample(id, method, path, fields);
     return `
     return `
     <div class="test-section">
     <div class="test-section">
         <div class="section-title">
         <div class="section-title">
@@ -562,6 +691,8 @@ function testSection(id, title, method, methodClass, path, authTag, authClass, f
             <span class="${ac} auth-tag">${authLabel}</span>
             <span class="${ac} auth-tag">${authLabel}</span>
         </div>
         </div>
         ${descHtml}
         ${descHtml}
+        ${curlHtml}
+        ${fieldDescHtml}
         ${fieldsHtml}
         ${fieldsHtml}
         <button class="btn btn-primary" onclick="runTest('${id}', '${method}', '${path}', ${buildBody(id, fields)}, ${authTag === '公开' ? 'false' : 'true'}, ${hasValidator ? `'${toValidatorName(id)}'` : 'null'})">执行测试</button>
         <button class="btn btn-primary" onclick="runTest('${id}', '${method}', '${path}', ${buildBody(id, fields)}, ${authTag === '公开' ? 'false' : 'true'}, ${hasValidator ? `'${toValidatorName(id)}'` : 'null'})">执行测试</button>
         <div class="response-box" id="${id}-box" style="display:none;">
         <div class="response-box" id="${id}-box" style="display:none;">
@@ -570,7 +701,6 @@ function testSection(id, title, method, methodClass, path, authTag, authClass, f
                 <span class="status-badge" id="${id}-status"></span>
                 <span class="status-badge" id="${id}-status"></span>
             </div>
             </div>
             <div class="response-content" id="${id}-raw"></div>
             <div class="response-content" id="${id}-raw"></div>
-            ${fieldDescHtml}
         </div>
         </div>
     </div>`;
     </div>`;
 }
 }
@@ -618,9 +748,9 @@ function renderCheckinModule() {
     <div class="batch-buttons">
     <div class="batch-buttons">
         <button class="btn btn-warning btn-sm" onclick="runCheckinTests()">运行全部签到测试</button>
         <button class="btn btn-warning btn-sm" onclick="runCheckinTests()">运行全部签到测试</button>
     </div>
     </div>
-    ${testSection('checkin-status', '今日签到状态', 'GET', 'get', '/api/checkin/status', '需认证', 'auth-required', null, true, 'checkinStatus', '获取当前用户今日签到状态、连续天数、可获奖励')}
+    ${testSection('checkin-status', '今日签到状态', 'GET', 'get', '/api/checkin/status', '需认证', 'auth-required', null, true, 'checkinStatus', '获取当前用户今日签到状态、连续天数、可获会员天数')}
     ${testSection('checkin-do', '执行签到', 'POST', 'post', '/api/checkin', '需认证', 'auth-required',
     ${testSection('checkin-do', '执行签到', 'POST', 'post', '/api/checkin', '需认证', 'auth-required',
-        [{label:'签到端(clientSource,可选)', name:'clientSource', placeholder:'WINDOWS/ANDROID/MINIAPP', default:'WINDOWS'}], true, 'checkinResult', '执行当日签到,发放爬虫次数奖励。每个自然日仅可签到一次')}
+        [{label:'签到端(clientSource,可选)', name:'clientSource', placeholder:'WINDOWS/ANDROID/MINIAPP', default:'WINDOWS'}], true, 'checkinResult', '执行当日签到,发放高级会员天数奖励。每个自然日仅可签到一次')}
     ${testSection('checkin-calendar', '当月签到日历', 'GET', 'get', '/api/checkin/calendar?year=2026&month=7', '需认证', 'auth-required',
     ${testSection('checkin-calendar', '当月签到日历', 'GET', 'get', '/api/checkin/calendar?year=2026&month=7', '需认证', 'auth-required',
         [{label:'年份(year,可选)', name:'year', placeholder:'2026', default:'2026'},
         [{label:'年份(year,可选)', name:'year', placeholder:'2026', default:'2026'},
          {label:'月份(month,可选)', name:'month', placeholder:'7', default:'7'}], true, 'checkinCalendar', '获取指定月份的签到日历,不传默认当月')}
          {label:'月份(month,可选)', name:'month', placeholder:'7', default:'7'}], true, 'checkinCalendar', '获取指定月份的签到日历,不传默认当月')}
@@ -652,6 +782,7 @@ function renderActivityLotteryModule() {
     <div class="batch-buttons">
     <div class="batch-buttons">
         <button class="btn btn-warning btn-sm" onclick="runActivityLotteryTests()">运行全部比价抽奖测试</button>
         <button class="btn btn-warning btn-sm" onclick="runActivityLotteryTests()">运行全部比价抽奖测试</button>
     </div>
     </div>
+    ${testSection('activity-lottery-prizes', '奖励信息(可抽奖品列表)', 'GET', 'get', '/api/activity/lottery/prizes', '需认证', 'auth-required', null, true, 'activityLotteryPrizes', '查看当前可抽奖品列表(仅启用奖品,不显示概率权重等敏感信息)')}
     ${testSection('activity-lottery-status', '比价抽奖活动状态', 'GET', 'get', '/api/activity/lottery/status', '需认证', 'auth-required', null, true, 'activityLotteryStatus', '查询抽奖活动状态:是否开启、是否满足资格、今日可用机会')}
     ${testSection('activity-lottery-status', '比价抽奖活动状态', 'GET', 'get', '/api/activity/lottery/status', '需认证', 'auth-required', null, true, 'activityLotteryStatus', '查询抽奖活动状态:是否开启、是否满足资格、今日可用机会')}
     ${testSection('activity-lottery-draw', '执行抽奖', 'POST', 'post', '/api/activity/lottery/draw', '需认证', 'auth-required', null, true, 'activityLotteryDraw', '消耗一次抽奖机会,随机抽取奖品(会员/爬虫次数/谢谢参与)')}
     ${testSection('activity-lottery-draw', '执行抽奖', 'POST', 'post', '/api/activity/lottery/draw', '需认证', 'auth-required', null, true, 'activityLotteryDraw', '消耗一次抽奖机会,随机抽取奖品(会员/爬虫次数/谢谢参与)')}
     ${testSection('activity-lottery-records', '我的抽奖记录', 'GET', 'get', '/api/activity/lottery/records?limit=20', '需认证', 'auth-required',
     ${testSection('activity-lottery-records', '我的抽奖记录', 'GET', 'get', '/api/activity/lottery/records?limit=20', '需认证', 'auth-required',
@@ -668,14 +799,12 @@ function renderAdminCheckinModule() {
     ${testSection('admin-checkin-config-update', '更新签到配置', 'PUT', 'put', '/api/admin/checkin/config', '需认证', 'auth-required',
     ${testSection('admin-checkin-config-update', '更新签到配置', 'PUT', 'put', '/api/admin/checkin/config', '需认证', 'auth-required',
         [
         [
          {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
          {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
-         {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'CRAWLER', default:'CRAWLER'},
-         {label:'基础奖励(baseReward)', name:'baseReward', placeholder:'1', default:'1'},
-         {label:'连续奖励(continuousReward)', name:'continuousReward', placeholder:'1', default:'1'},
+         {label:'基础会员天数(baseReward)', name:'baseReward', placeholder:'1', default:'1'},
+         {label:'连续额外天数(continuousReward)', name:'continuousReward', placeholder:'1', default:'1'},
          {label:'连续阈值(continuousThreshold)', name:'continuousThreshold', placeholder:'7', default:'7'},
          {label:'连续阈值(continuousThreshold)', name:'continuousThreshold', placeholder:'7', default:'7'},
-         {label:'连续上限(continuousCap)', name:'continuousCap', placeholder:'30', default:'30'},
-         {label:'有效天数(rewardExpireDays)', name:'rewardExpireDays', placeholder:'30', default:'30'},
-         {label:'备注(remark)', name:'remark', placeholder:'默认签到配置', default:'默认签到配置:每日1次,连续7天起每日额外+1'}
-        ], true, 'checkinConfig', '更新签到规则配置。连续签到满阈值后每日额外发放continuousReward,累计不超过continuousCap')}
+         {label:'连续额外上限(continuousCap)', name:'continuousCap', placeholder:'30', default:'30'},
+         {label:'备注(remark)', name:'remark', placeholder:'默认签到配置', default:'默认签到配置:每日1天高级会员,连续7天起每日额外+1天'}
+        ], true, 'checkinConfig', '更新签到规则配置。连续签到满阈值后每日额外发放 continuousReward 天,累计不超过 continuousCap 天')}
     ${testSection('admin-checkin-stats', '签到统计概览', 'GET', 'get', '/api/admin/checkin/stats', '需认证', 'auth-required', null, true, 'checkinStats', '签到数据统计:今日/昨日/近7天/近30天签到人数、累计签到次数与用户数、累计发放奖励')}
     ${testSection('admin-checkin-stats', '签到统计概览', 'GET', 'get', '/api/admin/checkin/stats', '需认证', 'auth-required', null, true, 'checkinStats', '签到数据统计:今日/昨日/近7天/近30天签到人数、累计签到次数与用户数、累计发放奖励')}
     ${testSection('admin-checkin-records', '签到记录分页查询', 'GET', 'get', '/api/admin/checkin/records?page=1&size=10&userId=1&startDate=2026-07-01&endDate=2026-07-07&clientSource=WINDOWS', '需认证', 'auth-required',
     ${testSection('admin-checkin-records', '签到记录分页查询', 'GET', 'get', '/api/admin/checkin/records?page=1&size=10&userId=1&startDate=2026-07-01&endDate=2026-07-07&clientSource=WINDOWS', '需认证', 'auth-required',
         [{label:'页码(page)', name:'page', placeholder:'1', default:'1'},
         [{label:'页码(page)', name:'page', placeholder:'1', default:'1'},
@@ -753,26 +882,26 @@ function renderAdminActivityLotteryModule() {
     ${testSection('admin-activity-lottery-prizes', '奖品池列表', 'GET', 'get', '/api/admin/activity/lottery/prizes', '需认证', 'auth-required', null, true, null, '获取所有奖品及其权重配置')}
     ${testSection('admin-activity-lottery-prizes', '奖品池列表', 'GET', 'get', '/api/admin/activity/lottery/prizes', '需认证', 'auth-required', null, true, null, '获取所有奖品及其权重配置')}
     ${testSection('admin-activity-lottery-prize-create', '新增奖品', 'POST', 'post', '/api/admin/activity/lottery/prizes', '需认证', 'auth-required',
     ${testSection('admin-activity-lottery-prize-create', '新增奖品', 'POST', 'post', '/api/admin/activity/lottery/prizes', '需认证', 'auth-required',
         [
         [
-         {label:'奖品名称(name)', name:'name', placeholder:'高级会员3天', default:'爬虫次数+10'},
-         {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'CRAWLER'},
-         {label:'奖励数量(rewardAmount)', name:'rewardAmount', placeholder:'爬虫次数填数量', default:'10'},
-         {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'MEMBERSHIP类型填', default:''},
-         {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:''},
-         {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'5'},
+         {label:'奖品名称(name)', name:'name', placeholder:'高级会员3天', default:'高级会员3天'},
+         {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'MEMBERSHIP'},
+         {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'3', default:'3'},
+         {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:'PRO'},
+         {label:'爬虫次数(rewardAmount)', name:'rewardAmount', placeholder:'CRAWLER类型时填写', default:'0'},
+         {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'20'},
          {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
          {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
-         {label:'排序(sort)', name:'sort', placeholder:'0', default:'6'}
-        ], true, 'adminActivityLotteryPrize', '新增奖品到奖品池。rewardType: MEMBERSHIP会员/CRAWLER爬虫/EMPTY谢谢参与')}
+         {label:'排序(sort)', name:'sort', placeholder:'0', default:'3'}
+        ], true, 'adminActivityLotteryPrize', '新增奖品到奖品池。rewardType: MEMBERSHIP会员/CRAWLER爬虫次数(暂未实现)/EMPTY谢谢参与')}
     ${testSection('admin-activity-lottery-prize-update', '更新奖品', 'PUT', 'put', '/api/admin/activity/lottery/prizes/{id}', '需认证', 'auth-required',
     ${testSection('admin-activity-lottery-prize-update', '更新奖品', 'PUT', 'put', '/api/admin/activity/lottery/prizes/{id}', '需认证', 'auth-required',
         [
         [
          {label:'奖品ID(id)', name:'id', placeholder:'1', default:''},
          {label:'奖品ID(id)', name:'id', placeholder:'1', default:''},
-         {label:'奖品名称(name)', name:'name', placeholder:'高级会员3天', default:'爬虫次数+10'},
-         {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'CRAWLER'},
-         {label:'奖励数量(rewardAmount)', name:'rewardAmount', placeholder:'爬虫次数填数量', default:'10'},
-         {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'MEMBERSHIP类型填', default:''},
-         {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:''},
-         {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'5'},
+         {label:'奖品名称(name)', name:'name', placeholder:'高级会员3天', default:'高级会员3天'},
+         {label:'奖励类型(rewardType)', name:'rewardType', placeholder:'MEMBERSHIP/CRAWLER/EMPTY', default:'MEMBERSHIP'},
+         {label:'会员天数(rewardDays)', name:'rewardDays', placeholder:'3', default:'3'},
+         {label:'会员等级(rewardLevel)', name:'rewardLevel', placeholder:'PRO/ULTRA', default:'PRO'},
+         {label:'爬虫次数(rewardAmount)', name:'rewardAmount', placeholder:'CRAWLER类型时填写', default:'0'},
+         {label:'概率权重(probabilityWeight)', name:'probabilityWeight', placeholder:'权重越大越容易中', default:'20'},
          {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
          {label:'启用(enabled)', name:'enabled', placeholder:'0停用/1启用', default:'1'},
-         {label:'排序(sort)', name:'sort', placeholder:'0', default:'6'}
+         {label:'排序(sort)', name:'sort', placeholder:'0', default:'3'}
         ], true, 'adminActivityLotteryPrize', '更新奖品池中的奖品配置')}
         ], true, 'adminActivityLotteryPrize', '更新奖品池中的奖品配置')}
     ${testSection('admin-activity-lottery-prize-delete', '删除奖品', 'DELETE', 'delete', '/api/admin/activity/lottery/prizes/{id}', '需认证', 'auth-required',
     ${testSection('admin-activity-lottery-prize-delete', '删除奖品', 'DELETE', 'delete', '/api/admin/activity/lottery/prizes/{id}', '需认证', 'auth-required',
         [{label:'奖品ID(id)', name:'id', placeholder:'需删除的奖品ID', default:''}], true, null, '从奖品池中删除指定奖品')}`;
         [{label:'奖品ID(id)', name:'id', placeholder:'需删除的奖品ID', default:''}], true, null, '从奖品池中删除指定奖品')}`;
@@ -896,9 +1025,10 @@ function validate_adminCheckinStats(result) { return result.data?.code === 200 &
 function validate_adminCheckinRecords(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
 function validate_adminCheckinRecords(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
 function validate_adminCheckinUserRecords(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
 function validate_adminCheckinUserRecords(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
 
 
-function validate_activityInviteStatus(result) { return result.data?.code === 200 && result.data?.data?.active !== undefined; }
-function validate_activityCheckinStatus(result) { return result.data?.code === 200 && result.data?.data?.active !== undefined; }
-function validate_activityLotteryStatus(result) { return result.data?.code === 200 && result.data?.data?.active !== undefined; }
+function validate_activityInviteStatus(result) { return result.data?.code === 200 && result.data?.data?.enabled !== undefined; }
+function validate_activityCheckinStatus(result) { return result.data?.code === 200 && result.data?.data?.enabled !== undefined; }
+function validate_activityLotteryStatus(result) { return result.data?.code === 200 && result.data?.data?.enabled !== undefined; }
+function validate_activityLotteryPrizes(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
 function validate_activityLotteryDraw(result) { return result.data?.code === 200 && result.data?.data?.prizeName !== undefined; }
 function validate_activityLotteryDraw(result) { return result.data?.code === 200 && result.data?.data?.prizeName !== undefined; }
 function validate_adminActivityInviteConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
 function validate_adminActivityInviteConfig(result) { return result.data?.code === 200 && result.data?.data?.id; }
 function validate_adminActivityInviteRevoke(result) { return result.data?.code === 200 && result.data?.data !== undefined; }
 function validate_adminActivityInviteRevoke(result) { return result.data?.code === 200 && result.data?.data !== undefined; }