| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- 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;
- /**
- * 签到核心服务(用户端)
- * <p>
- * 业务规则:
- * 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<CheckInRecord> records = checkinRecordMapper.selectMonthlyRecords(userId, start, end);
- List<LocalDate> checkedDates = records.stream()
- .map(CheckInRecord::getCheckinDate)
- .collect(Collectors.toList());
- int continuousDays = calcContinuousDays(userId, LocalDate.now());
- boolean checkedToday = checkedDates.contains(LocalDate.now());
- return CheckInCalendarResponse.builder()
- .month(ym.toString())
- .checkedDays(checkedDates.size())
- .continuousDays(continuousDays)
- .checkedDates(checkedDates)
- .checkedToday(checkedToday)
- .build();
- }
- /**
- * 获取签到记录列表
- */
- public List<CheckInRecord> listRecords(Long userId, Integer limit) {
- LambdaQueryWrapper<CheckInRecord> wrapper = new LambdaQueryWrapper<CheckInRecord>()
- .eq(CheckInRecord::getUserId, userId)
- .orderByDesc(CheckInRecord::getCheckinDate);
- if (limit != null && limit > 0) {
- wrapper.last("LIMIT " + limit);
- }
- return checkinRecordMapper.selectList(wrapper);
- }
- // ==========================================
- // 运营端管理方法
- // ==========================================
- /**
- * 运营端:分页查询签到记录(关联用户信息)
- *
- * @param page 分页对象
- * @param userId 用户ID(可选)
- * @param startDate 起始日期(可选)
- * @param endDate 截止日期(可选)
- * @param clientSource 签到端(可选)
- */
- public IPage<AdminCheckInRecordResponse> adminListRecords(Page<AdminCheckInRecordResponse> page,
- Long userId,
- LocalDate startDate,
- LocalDate endDate,
- String clientSource) {
- return checkinRecordMapper.selectAdminRecordsPage(page, userId, startDate, endDate, clientSource);
- }
- /**
- * 运营端:签到统计概览
- */
- public CheckInStatsResponse adminGetStats() {
- LocalDate today = LocalDate.now();
- CheckInConfig config = checkinConfigService.getConfig();
- return CheckInStatsResponse.builder()
- .todayCheckinCount(nvl(checkinRecordMapper.countByDate(today)))
- .yesterdayCheckinCount(nvl(checkinRecordMapper.countByDate(today.minusDays(1))))
- .last7DaysCount(nvl(checkinRecordMapper.countByDateRange(today.minusDays(6), today)))
- .last30DaysCount(nvl(checkinRecordMapper.countByDateRange(today.minusDays(29), today)))
- .totalCheckinCount(nvl(checkinRecordMapper.countTotalRecords()))
- .totalCheckinUsers(nvl(checkinRecordMapper.countTotalUsers()))
- .totalRewardAmount(nvl(checkinRecordMapper.sumTotalReward()))
- .statDate(today)
- .enabled(config.getEnabled() != null && config.getEnabled() == 1)
- .build();
- }
- /**
- * 运营端:查询指定用户的签到记录(含用户基础信息,最多500条)
- */
- public List<AdminCheckInRecordResponse> adminListUserRecords(Long userId) {
- if (userId == null) {
- throw new BusinessException(ErrorCode.PARAM_ERROR, "用户ID不能为空");
- }
- Page<AdminCheckInRecordResponse> page = new Page<>(1, 500);
- return checkinRecordMapper.selectAdminRecordsPage(page, userId, null, null, null).getRecords();
- }
- private Long nvl(Long v) {
- return v == null ? 0L : v;
- }
- // ==========================================
- // 私有辅助方法
- // ==========================================
- private CheckInRecord findRecord(Long userId, LocalDate date) {
- return checkinRecordMapper.selectOne(
- new LambdaQueryWrapper<CheckInRecord>()
- .eq(CheckInRecord::getUserId, userId)
- .eq(CheckInRecord::getCheckinDate, date));
- }
- /**
- * 计算截至 today 的连续签到天数(不含 today)
- * 若昨天签到则为昨天连续天数+1,否则为 0
- */
- private int calcContinuousDays(Long userId, LocalDate today) {
- CheckInRecord yesterday = findRecord(userId, today.minusDays(1));
- if (yesterday == null) {
- return 0;
- }
- return yesterday.getContinuousDays() + 1;
- }
- /**
- * 计算签到奖励:基础 + 连续奖励(达阈值后叠加,受上限约束)
- */
- private int calcReward(CheckInConfig config, int continuousDays) {
- int reward = config.getBaseReward();
- if (continuousDays >= config.getContinuousThreshold()) {
- int extra = continuousDays - config.getContinuousThreshold() + 1;
- extra = Math.min(extra, config.getContinuousCap());
- reward += config.getContinuousReward() * Math.max(0, extra);
- }
- return reward;
- }
- }
|