package com.xuekairui.activity.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.xuekairui.activity.dto.AdminCheckInRecordResponse; import com.xuekairui.activity.dto.CheckInCalendarResponse; import com.xuekairui.activity.dto.CheckInResultResponse; import com.xuekairui.activity.dto.CheckInStatsResponse; import com.xuekairui.activity.dto.CheckInStatusResponse; import com.xuekairui.activity.entity.CheckInConfig; import com.xuekairui.activity.entity.CheckInRecord; import com.xuekairui.activity.mapper.CheckInRecordMapper; import com.xuekairui.common.BusinessException; import com.xuekairui.common.ErrorCode; import com.xuekairui.user.entity.CrawlerQuotaGrant; import com.xuekairui.user.service.CrawlerQuotaGrantService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; import java.util.List; import java.util.stream.Collectors; /** * 签到核心服务(用户端) *
* 业务规则:
* 1. 每个用户每个自然日仅可签到一次(DB 唯一索引兜底)
* 2. 连续签到:若昨日已签到,则连续天数 +1;否则重置为 1
* 3. 奖励计算:基础奖励 + (连续天数 >= 阈值 ? 连续奖励 : 0),且累计连续奖励不超过 continuousCap
* 4. 奖励以爬虫次数发放,复用 t_crawler_quota_grant(grant_type=CHECKIN)
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CheckInService {
/** 签到发放爬虫配额类型 */
public static final String GRANT_TYPE_CHECKIN = "CHECKIN";
private final CheckInConfigService checkinConfigService;
private final CheckInRecordMapper checkinRecordMapper;
private final CrawlerQuotaGrantService crawlerQuotaGrantService;
private final ActivityCheckinCycleService activityCheckinCycleService;
/**
* 查询今日签到状态
*/
public CheckInStatusResponse getCheckInStatus(Long userId) {
CheckInConfig config = checkinConfigService.getConfig();
LocalDate today = LocalDate.now();
CheckInRecord todayRecord = findRecord(userId, today);
boolean checkedToday = todayRecord != null;
int continuousDays;
if (checkedToday) {
continuousDays = todayRecord.getContinuousDays();
} else {
continuousDays = calcContinuousDays(userId, today);
}
int todayReward = checkedToday ? todayRecord.getRewardAmount()
: calcReward(config, continuousDays + 1);
return CheckInStatusResponse.builder()
.checkedToday(checkedToday)
.continuousDays(continuousDays)
.todayReward(todayReward)
.nextCheckinDate(checkedToday ? today.plusDays(1) : today)
.enabled(config.getEnabled() != null && config.getEnabled() == 1)
.build();
}
/**
* 执行签到
*/
@Transactional
public CheckInResultResponse doCheckIn(Long userId, String clientSource) {
checkinConfigService.ensureEnabled();
CheckInConfig config = checkinConfigService.getConfig();
LocalDate today = LocalDate.now();
CheckInRecord exist = findRecord(userId, today);
if (exist != null) {
throw new BusinessException(ErrorCode.BUSINESS_ERROR, "今日已签到");
}
int continuousDays = calcContinuousDays(userId, today) + 1;
int rewardAmount = calcReward(config, continuousDays);
LocalDateTime expireTime = config.getRewardExpireDays() != null
? LocalDateTime.now().plusDays(config.getRewardExpireDays())
: null;
CrawlerQuotaGrant grant = crawlerQuotaGrantService.grantAdminQuota(
userId, rewardAmount, expireTime,
"签到奖励 连续第" + continuousDays + "天");
CheckInRecord record = CheckInRecord.builder()
.userId(userId)
.checkinDate(today)
.continuousDays(continuousDays)
.rewardType(config.getRewardType())
.rewardAmount(rewardAmount)
.grantId(grant != null ? grant.getId() : null)
.clientSource(clientSource)
.build();
checkinRecordMapper.insert(record);
log.info("用户签到成功: userId={}, date={}, continuous={}, reward={}",
userId, today, continuousDays, rewardAmount);
// 签到送会员活动:尝试发放上一周期奖励(活动关闭时内部直接返回,不影响原逻辑)
try {
activityCheckinCycleService.tryGrantCycleReward(userId);
} catch (Exception e) {
log.warn("签到后尝试发放周期奖励失败,不影响签到主流程: userId={}", userId, e);
}
return CheckInResultResponse.builder()
.id(record.getId())
.checkinDate(record.getCheckinDate())
.continuousDays(record.getContinuousDays())
.rewardAmount(record.getRewardAmount())
.rewardType(record.getRewardType())
.createTime(record.getCreateTime())
.build();
}
/**
* 获取当月签到日历
*/
public CheckInCalendarResponse getMonthlyCalendar(Long userId, Integer year, Integer month) {
YearMonth ym = (year != null && month != null)
? YearMonth.of(year, month)
: YearMonth.now();
LocalDate start = ym.atDay(1);
LocalDate end = ym.atEndOfMonth();
List