|
@@ -1,321 +0,0 @@
|
|
|
-package com.xuekairui.activity.service;
|
|
|
|
|
-
|
|
|
|
|
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
|
-import com.xuekairui.activity.dto.ActivityCheckinCycleConfigRequest;
|
|
|
|
|
-import com.xuekairui.activity.dto.ActivityCheckinCycleStatusResponse;
|
|
|
|
|
-import com.xuekairui.activity.dto.ActivityRewardRecordResponse;
|
|
|
|
|
-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. 活动时间窗可配置,所有用户可参加;
|
|
|
|
|
- * 2. 周期模式可配置:MON_SUN(周一至周日)或 FRI_THU(周五至下周四);
|
|
|
|
|
- * 3. 周期内累计签到达标天数即获得奖励;
|
|
|
|
|
- * 4. 周期结束后的第一天发放奖励;
|
|
|
|
|
- * 5. 每用户最多领取指定次数。
|
|
|
|
|
- * <p>
|
|
|
|
|
- * 数据复用现有 t_checkin_record 签到记录统计周期内签到天数,不改动每日签到奖励逻辑。
|
|
|
|
|
- */
|
|
|
|
|
-@Slf4j
|
|
|
|
|
-@Service
|
|
|
|
|
-@RequiredArgsConstructor
|
|
|
|
|
-public class ActivityCheckinCycleService {
|
|
|
|
|
-
|
|
|
|
|
- private static final Long CONFIG_ID = 1L;
|
|
|
|
|
- /** 周期模式:周一至周日 */
|
|
|
|
|
- private static final String CYCLE_MODE_MON_SUN = "MON_SUN";
|
|
|
|
|
- /** 周期模式:周五至下周四 */
|
|
|
|
|
- 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;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- String cycleMode = config.getCycleMode();
|
|
|
|
|
- LocalDate today = LocalDate.now();
|
|
|
|
|
- LocalDate currentCycleStart = cycleStartOf(today, cycleMode);
|
|
|
|
|
- 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();
|
|
|
|
|
- String cycleMode = config.getCycleMode();
|
|
|
|
|
- LocalDate currentCycleStart = cycleStartOf(today, cycleMode);
|
|
|
|
|
- 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))
|
|
|
|
|
- .startTime(config.getStartTime())
|
|
|
|
|
- .endTime(config.getEndTime())
|
|
|
|
|
- .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));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ==========================================
|
|
|
|
|
- // 运营端:分页查询签到周期奖励记录
|
|
|
|
|
- // ==========================================
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 分页查询签到周期奖励记录(运营端),支持按用户ID筛选。
|
|
|
|
|
- * 只返回已发放奖励的周期记录,按发放时间倒序,转换为统一 DTO。
|
|
|
|
|
- */
|
|
|
|
|
- public Page<ActivityRewardRecordResponse> listCycleRewards(
|
|
|
|
|
- Long userId, int pageNum, int pageSize) {
|
|
|
|
|
- Page<ActivityCheckinCycleRecord> page = new Page<>(pageNum, pageSize);
|
|
|
|
|
- LambdaQueryWrapper<ActivityCheckinCycleRecord> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
- if (userId != null) {
|
|
|
|
|
- wrapper.eq(ActivityCheckinCycleRecord::getUserId, userId);
|
|
|
|
|
- }
|
|
|
|
|
- wrapper.eq(ActivityCheckinCycleRecord::getRewardGranted, 1)
|
|
|
|
|
- .orderByDesc(ActivityCheckinCycleRecord::getGrantTime);
|
|
|
|
|
- Page<ActivityCheckinCycleRecord> recordPage = cycleRecordMapper.selectPage(page, wrapper);
|
|
|
|
|
-
|
|
|
|
|
- // 读取奖励天数(来自配置)
|
|
|
|
|
- ActivityCheckinCycleConfig config = getConfig();
|
|
|
|
|
- int rewardDays = config.getRewardDays() != null ? config.getRewardDays() : 7;
|
|
|
|
|
-
|
|
|
|
|
- Page<ActivityRewardRecordResponse> resultPage = new Page<>(pageNum, pageSize, recordPage.getTotal());
|
|
|
|
|
- List<ActivityRewardRecordResponse> list = recordPage.getRecords().stream()
|
|
|
|
|
- .map(r -> ActivityRewardRecordResponse.builder()
|
|
|
|
|
- .id(r.getId())
|
|
|
|
|
- .userId(r.getUserId())
|
|
|
|
|
- .activityType("CHECKIN")
|
|
|
|
|
- .rewardName("签到有礼奖励")
|
|
|
|
|
- .rewardDays(rewardDays)
|
|
|
|
|
- .rewardLevel("PRO")
|
|
|
|
|
- .rewardType("MEMBERSHIP")
|
|
|
|
|
- .checkinCount(r.getCheckinCount())
|
|
|
|
|
- .cycleDesc(r.getCycleStart() + "~" + r.getCycleEnd())
|
|
|
|
|
- .createTime(r.getGrantTime())
|
|
|
|
|
- .build())
|
|
|
|
|
- .toList();
|
|
|
|
|
- resultPage.setRecords(list);
|
|
|
|
|
- return resultPage;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ==========================================
|
|
|
|
|
- // 私有辅助
|
|
|
|
|
- // ==========================================
|
|
|
|
|
-
|
|
|
|
|
- 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());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 计算日期所属周期的起始日。
|
|
|
|
|
- * 根据配置的周期模式计算:
|
|
|
|
|
- * - MON_SUN:周一至周日,以周一为起点
|
|
|
|
|
- * - FRI_THU:周五至下周四,以周五为起点
|
|
|
|
|
- *
|
|
|
|
|
- * @param date 目标日期
|
|
|
|
|
- * @param cycleMode 周期模式(MON_SUN 或 FRI_THU)
|
|
|
|
|
- * @return 周期起始日
|
|
|
|
|
- */
|
|
|
|
|
- private LocalDate cycleStartOf(LocalDate date, String cycleMode) {
|
|
|
|
|
- int dow = date.getDayOfWeek().getValue(); // MON=1 ... SUN=7
|
|
|
|
|
- int offset;
|
|
|
|
|
- if (CYCLE_MODE_MON_SUN.equalsIgnoreCase(cycleMode)) {
|
|
|
|
|
- // 周一为起点:offset = (dow - 1 + 7) % 7
|
|
|
|
|
- offset = (dow - 1 + 7) % 7;
|
|
|
|
|
- } else {
|
|
|
|
|
- // 默认周五为起点:offset = (dow - 5 + 7) % 7
|
|
|
|
|
- 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;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|