| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- package com.xuekairui.activity.service;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.xuekairui.activity.dto.ActivityCheckinCycleConfigRequest;
- import com.xuekairui.activity.dto.ActivityCheckinCycleStatusResponse;
- import com.xuekairui.activity.entity.ActivityCheckinCycleConfig;
- import com.xuekairui.activity.entity.ActivityCheckinCycleRecord;
- import com.xuekairui.activity.entity.CheckInRecord;
- import com.xuekairui.activity.mapper.ActivityCheckinCycleConfigMapper;
- import com.xuekairui.activity.mapper.ActivityCheckinCycleRecordMapper;
- import com.xuekairui.activity.mapper.CheckInRecordMapper;
- import com.xuekairui.user.enums.MembershipLevel;
- import com.xuekairui.user.service.MembershipService;
- import lombok.RequiredArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.dao.DuplicateKeyException;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.time.LocalDate;
- import java.time.LocalDateTime;
- import java.util.List;
- /**
- * 签到送高级会员活动服务(周期制)。
- * <p>
- * 规则:
- * 1. 活动时间窗 2026-07-10 ~ 2026-07-31,所有用户可参加;
- * 2. 每周五至下周四为一个周期,累计 4 天签到即获得 7 天高级会员;
- * 3. 周期结束后的第一天(即下一周期首日周五)发放奖励;
- * 4. 每用户最多领取 3 次。
- * <p>
- * 数据复用现有 t_checkin_record 签到记录统计周期内签到天数,不改动每日签到奖励逻辑。
- */
- @Slf4j
- @Service
- @RequiredArgsConstructor
- public class ActivityCheckinCycleService {
- private static final Long CONFIG_ID = 1L;
- /** 周期模式:周五至下周四 */
- private static final String CYCLE_MODE_FRI_THU = "FRI_THU";
- private final ActivityCheckinCycleConfigMapper configMapper;
- private final ActivityCheckinCycleRecordMapper cycleRecordMapper;
- private final CheckInRecordMapper checkInRecordMapper;
- private final MembershipService membershipService;
- // ==========================================
- // 配置管理
- // ==========================================
- public ActivityCheckinCycleConfig getConfig() {
- ActivityCheckinCycleConfig config = configMapper.selectById(CONFIG_ID);
- if (config == null) {
- return ActivityCheckinCycleConfig.builder()
- .id(CONFIG_ID).enabled(0)
- .startTime(LocalDateTime.now()).endTime(LocalDateTime.now())
- .cycleMode(CYCLE_MODE_FRI_THU).requiredDays(4).rewardDays(7).maxRewardCount(3)
- .build();
- }
- return config;
- }
- @Transactional
- public ActivityCheckinCycleConfig updateConfig(ActivityCheckinCycleConfigRequest request) {
- ActivityCheckinCycleConfig config = getConfig();
- config.setEnabled(request.getEnabled());
- config.setStartTime(request.getStartTime());
- config.setEndTime(request.getEndTime());
- if (request.getCycleMode() != null) config.setCycleMode(request.getCycleMode());
- if (request.getRequiredDays() != null) config.setRequiredDays(request.getRequiredDays());
- if (request.getRewardDays() != null) config.setRewardDays(request.getRewardDays());
- if (request.getMaxRewardCount() != null) config.setMaxRewardCount(request.getMaxRewardCount());
- if (request.getRemark() != null) config.setRemark(request.getRemark());
- if (config.getId() == null) {
- config.setId(CONFIG_ID);
- configMapper.insert(config);
- } else {
- configMapper.updateById(config);
- }
- log.info("签到送会员活动配置已更新: enabled={}, required={}, reward={}, max={}",
- config.getEnabled(), config.getRequiredDays(), config.getRewardDays(), config.getMaxRewardCount());
- return config;
- }
- // ==========================================
- // 周期奖励发放(签到或查询状态时触发)
- // ==========================================
- /**
- * 尝试发放最近一个已结束周期的奖励。
- * <p>
- * 在用户每日签到成功后、或查询周期状态时调用。
- * 仅当活动开启、该周期累计签到达标、未发放过、且用户未达领取上限时发放。
- */
- @Transactional
- public boolean tryGrantCycleReward(Long userId) {
- ActivityCheckinCycleConfig config = getConfig();
- if (!isActive(config)) {
- return false;
- }
- LocalDate today = LocalDate.now();
- LocalDate currentCycleStart = cycleStartOf(today);
- LocalDate prevCycleStart = currentCycleStart.minusDays(7);
- LocalDate prevCycleEnd = prevCycleStart.plusDays(6);
- // 仅处理已完整结束的周期(周期截止日 < 今天)
- if (!prevCycleEnd.isBefore(today)) {
- return false;
- }
- int checkinCount = countCheckinInCycle(userId, prevCycleStart, prevCycleEnd);
- int requiredDays = config.getRequiredDays() != null ? config.getRequiredDays() : 4;
- if (checkinCount < requiredDays) {
- return false;
- }
- // 校验领取上限
- long rewarded = cycleRecordMapper.countRewardedByUser(userId);
- int maxReward = config.getMaxRewardCount() != null ? config.getMaxRewardCount() : 3;
- if (rewarded >= maxReward) {
- log.info("用户{}已达签到周期奖励上限{}次", userId, maxReward);
- return false;
- }
- // 幂等:插入周期记录(uk_user_cycle 兜底),已存在则更新为已发放
- ActivityCheckinCycleRecord exist = cycleRecordMapper.selectOne(
- new LambdaQueryWrapper<ActivityCheckinCycleRecord>()
- .eq(ActivityCheckinCycleRecord::getUserId, userId)
- .eq(ActivityCheckinCycleRecord::getCycleStart, prevCycleStart));
- if (exist != null && exist.getRewardGranted() == 1) {
- return false;
- }
- int rewardDays = config.getRewardDays() != null ? config.getRewardDays() : 7;
- membershipService.grantOrExtendMembership(userId, MembershipLevel.PRO, rewardDays,
- "ACTIVITY_CHECKIN_CYCLE", "签到送会员活动:周期" + prevCycleStart + "~" + prevCycleEnd + "累计签到" + checkinCount + "天奖励" + rewardDays + "天高级会员");
- if (exist == null) {
- ActivityCheckinCycleRecord record = ActivityCheckinCycleRecord.builder()
- .userId(userId)
- .cycleStart(prevCycleStart)
- .cycleEnd(prevCycleEnd)
- .checkinCount(checkinCount)
- .rewardGranted(1)
- .grantTime(LocalDateTime.now())
- .build();
- try {
- cycleRecordMapper.insert(record);
- } catch (DuplicateKeyException e) {
- // 并发兜底:已被其他线程处理
- log.info("签到周期奖励并发处理,已跳过: userId={}, cycle={}", userId, prevCycleStart);
- return false;
- }
- } else {
- exist.setCheckinCount(checkinCount);
- exist.setRewardGranted(1);
- exist.setGrantTime(LocalDateTime.now());
- cycleRecordMapper.updateById(exist);
- }
- log.info("签到周期奖励已发放: userId={}, cycle={}~{}, checkinCount={}, days={}",
- userId, prevCycleStart, prevCycleEnd, checkinCount, rewardDays);
- return true;
- }
- // ==========================================
- // 用户端状态查询
- // ==========================================
- public ActivityCheckinCycleStatusResponse getStatus(Long userId) {
- ActivityCheckinCycleConfig config = getConfig();
- // 查询时顺带尝试发放上一周期奖励(用户若未在发放日签到,查询时补发)
- if (userId != null && isActive(config)) {
- try {
- tryGrantCycleReward(userId);
- } catch (Exception e) {
- log.warn("查询签到周期状态时尝试发奖失败,不影响查询: userId={}", userId, e);
- }
- }
- LocalDate today = LocalDate.now();
- LocalDate currentCycleStart = cycleStartOf(today);
- LocalDate currentCycleEnd = currentCycleStart.plusDays(6);
- int currentCount = userId != null ? countCheckinInCycle(userId, currentCycleStart, currentCycleEnd) : 0;
- int requiredDays = config.getRequiredDays() != null ? config.getRequiredDays() : 4;
- int rewardDays = config.getRewardDays() != null ? config.getRewardDays() : 7;
- long rewarded = userId != null ? cycleRecordMapper.countRewardedByUser(userId) : 0L;
- int maxReward = config.getMaxRewardCount() != null ? config.getMaxRewardCount() : 3;
- long remaining = Math.max(0, maxReward - rewarded);
- return ActivityCheckinCycleStatusResponse.builder()
- .enabled(isActive(config))
- .inWindow(inWindow(config))
- .currentCycleStart(currentCycleStart)
- .currentCycleEnd(currentCycleEnd)
- .currentCycleCheckinCount(currentCount)
- .requiredDays(requiredDays)
- .reachedTarget(currentCount >= requiredDays)
- .rewardedCount(rewarded)
- .maxRewardCount(maxReward)
- .remainingCount(remaining)
- .rewardDays(rewardDays)
- .build();
- }
- /**
- * 用户端:查询我的签到周期奖励记录
- * 返回已发放的周期奖励列表,按发放时间倒序
- */
- public List<ActivityCheckinCycleRecord> listMyCycleRewards(Long userId) {
- return cycleRecordMapper.selectList(
- new LambdaQueryWrapper<ActivityCheckinCycleRecord>()
- .eq(ActivityCheckinCycleRecord::getUserId, userId)
- .eq(ActivityCheckinCycleRecord::getRewardGranted, 1)
- .orderByDesc(ActivityCheckinCycleRecord::getGrantTime));
- }
- // ==========================================
- // 私有辅助
- // ==========================================
- private boolean isActive(ActivityCheckinCycleConfig config) {
- return config.getEnabled() != null && config.getEnabled() == 1 && inWindow(config);
- }
- private boolean inWindow(ActivityCheckinCycleConfig config) {
- LocalDateTime now = LocalDateTime.now();
- return !now.isBefore(config.getStartTime()) && !now.isAfter(config.getEndTime());
- }
- /**
- * 计算日期所属周期的起始日(周五)。
- * 周期为周五~下周四:以周五为起点,offset = (dayOfWeek - 5 + 7) % 7。
- */
- private LocalDate cycleStartOf(LocalDate date) {
- int dow = date.getDayOfWeek().getValue(); // MON=1 ... SUN=7
- int offset = (dow - 5 + 7) % 7;
- return date.minusDays(offset);
- }
- private int countCheckinInCycle(Long userId, LocalDate start, LocalDate end) {
- if (userId == null) {
- return 0;
- }
- Long count = checkInRecordMapper.selectCount(
- new LambdaQueryWrapper<CheckInRecord>()
- .eq(CheckInRecord::getUserId, userId)
- .ge(CheckInRecord::getCheckinDate, start)
- .le(CheckInRecord::getCheckinDate, end));
- return count != null ? count.intValue() : 0;
- }
- }
|