CheckInService.java 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. package com.xuekairui.activity.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.core.metadata.IPage;
  4. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  5. import com.xuekairui.activity.dto.AdminCheckInRecordResponse;
  6. import com.xuekairui.activity.dto.CheckInCalendarResponse;
  7. import com.xuekairui.activity.dto.CheckInPeriodStatsResponse;
  8. import com.xuekairui.activity.dto.CheckInResultResponse;
  9. import com.xuekairui.activity.dto.CheckInStatsResponse;
  10. import com.xuekairui.activity.dto.CheckInStatusResponse;
  11. import com.xuekairui.activity.dto.ActivityCheckinCycleStatusResponse;
  12. import com.xuekairui.activity.dto.ActivityRewardRecordResponse;
  13. import com.xuekairui.activity.entity.CheckInConfig;
  14. import com.xuekairui.activity.entity.CheckInPeriod;
  15. import com.xuekairui.activity.entity.CheckInRecord;
  16. import com.xuekairui.activity.entity.CheckInRewardRecord;
  17. import com.xuekairui.activity.mapper.CheckInPeriodMapper;
  18. import com.xuekairui.activity.mapper.CheckInRecordMapper;
  19. import com.xuekairui.activity.mapper.CheckInRewardRecordMapper;
  20. import com.xuekairui.common.BusinessException;
  21. import com.xuekairui.common.ErrorCode;
  22. import com.xuekairui.user.enums.MembershipLevel;
  23. import com.xuekairui.user.service.MembershipService;
  24. import lombok.RequiredArgsConstructor;
  25. import lombok.extern.slf4j.Slf4j;
  26. import org.springframework.dao.DuplicateKeyException;
  27. import org.springframework.stereotype.Service;
  28. import org.springframework.transaction.annotation.Transactional;
  29. import java.time.LocalDate;
  30. import java.time.LocalDateTime;
  31. import java.time.YearMonth;
  32. import java.util.List;
  33. import java.util.stream.Collectors;
  34. /**
  35. * 签到核心服务(用户端)—— 统一签到 + 周期奖励
  36. * <p>
  37. * 业务规则:
  38. * 1. 每个用户每天仅可签到一次(DB 唯一索引兜底),签到记录归属到当前进行中的周期
  39. * 2. 签到本身不立即发放奖励,仅记录签到行为
  40. * 3. 周期达标奖励:周期结束后,检查该周期累计签到是否>=所需天数,达标则发放会员奖励
  41. * 4. 奖励发放触发时机:用户签到后 / 查询状态时 自动检查上一周期
  42. * 5. 每人活动期间最多领取 maxRewardCount 次奖励
  43. */
  44. @Slf4j
  45. @Service
  46. @RequiredArgsConstructor
  47. public class CheckInService {
  48. private final CheckInConfigService checkinConfigService;
  49. private final CheckInRecordMapper checkinRecordMapper;
  50. private final CheckInPeriodMapper periodMapper;
  51. private final CheckInRewardRecordMapper rewardRecordMapper;
  52. private final MembershipService membershipService;
  53. /**
  54. * 查询今日签到状态 + 当前周期进度
  55. */
  56. public CheckInStatusResponse getCheckInStatus(Long userId) {
  57. CheckInConfig config = checkinConfigService.getConfig();
  58. LocalDate today = LocalDate.now();
  59. if (checkinConfigService.isActive(config)) {
  60. try {
  61. tryGrantCycleReward(userId);
  62. } catch (Exception e) {
  63. log.warn("查询状态时尝试发放周期奖励失败: userId={}", userId, e);
  64. }
  65. }
  66. CheckInRecord todayRecord = findRecord(userId, today);
  67. boolean checkedToday = todayRecord != null;
  68. int continuousDays = calcContinuousDays(userId, today);
  69. CheckInPeriod currentPeriod = periodMapper.findActiveByDate(today);
  70. int periodCheckinCount = 0;
  71. boolean periodReached = false;
  72. Long currentPeriodId = null;
  73. String cycleMode = null;
  74. LocalDate periodStart = null;
  75. LocalDate periodEnd = null;
  76. Integer periodRequiredDays = null;
  77. Integer periodRewardDays = null;
  78. String periodRewardMode = null;
  79. if (currentPeriod != null) {
  80. currentPeriodId = currentPeriod.getId();
  81. cycleMode = currentPeriod.getCycleMode();
  82. periodStart = currentPeriod.getStartDate();
  83. periodEnd = currentPeriod.getEndDate();
  84. periodRequiredDays = currentPeriod.getRequiredDays();
  85. periodRewardDays = currentPeriod.getRewardDays();
  86. periodRewardMode = currentPeriod.getRewardMode() != null ? currentPeriod.getRewardMode() : "CUMULATIVE";
  87. periodCheckinCount = countCheckinsInRange(userId, periodStart, periodEnd);
  88. periodReached = isPeriodTargetReached(userId, currentPeriod);
  89. }
  90. long rewardedCount = rewardRecordMapper.countRewardedByUser(userId);
  91. int maxReward = config.getMaxRewardCount() != null ? config.getMaxRewardCount() : 3;
  92. long remaining = Math.max(0, maxReward - rewardedCount);
  93. return CheckInStatusResponse.builder()
  94. .checkedToday(checkedToday)
  95. .continuousDays(continuousDays)
  96. .nextCheckinDate(checkedToday ? today.plusDays(1) : today)
  97. .enabled(checkinConfigService.isActive(config))
  98. .currentPeriodId(currentPeriodId)
  99. .cycleMode(cycleMode)
  100. .periodStartDate(periodStart)
  101. .periodEndDate(periodEnd)
  102. .periodRequiredDays(periodRequiredDays)
  103. .rewardMode(periodRewardMode)
  104. .periodCheckinCount(periodCheckinCount)
  105. .periodReachedTarget(periodReached)
  106. .periodRewardDays(periodRewardDays)
  107. .rewardedCount(rewardedCount)
  108. .maxRewardCount(maxReward)
  109. .remainingCount(remaining)
  110. .build();
  111. }
  112. /**
  113. * 执行签到(不立即发放奖励)
  114. */
  115. @Transactional
  116. public CheckInResultResponse doCheckIn(Long userId, String clientSource) {
  117. checkinConfigService.ensureEnabled();
  118. LocalDate today = LocalDate.now();
  119. CheckInRecord exist = findRecord(userId, today);
  120. if (exist != null) {
  121. throw new BusinessException(ErrorCode.BUSINESS_ERROR, "今日已签到");
  122. }
  123. CheckInPeriod currentPeriod = periodMapper.findActiveByDate(today);
  124. if (currentPeriod == null) {
  125. throw new BusinessException(ErrorCode.BUSINESS_ERROR, "当前没有进行中的签到周期,暂无法签到");
  126. }
  127. int continuousDays = calcContinuousDays(userId, today) + 1;
  128. CheckInRecord record = CheckInRecord.builder()
  129. .userId(userId)
  130. .checkinDate(today)
  131. .periodId(currentPeriod.getId())
  132. .continuousDays(continuousDays)
  133. .rewardType("MEMBERSHIP")
  134. .rewardAmount(0)
  135. .clientSource(clientSource)
  136. .build();
  137. checkinRecordMapper.insert(record);
  138. log.info("用户签到成功: userId={}, date={}, periodId={}, continuousDays={}",
  139. userId, today, currentPeriod.getId(), continuousDays);
  140. try {
  141. tryGrantCycleReward(userId);
  142. } catch (Exception e) {
  143. log.warn("签到后尝试发周期奖励失败: userId={}", userId, e);
  144. }
  145. return CheckInResultResponse.builder()
  146. .id(record.getId())
  147. .checkinDate(record.getCheckinDate())
  148. .continuousDays(record.getContinuousDays())
  149. .rewardAmount(0)
  150. .rewardType("MEMBERSHIP")
  151. .createTime(record.getCreateTime())
  152. .build();
  153. }
  154. public CheckInCalendarResponse getMonthlyCalendar(Long userId, Integer year, Integer month) {
  155. YearMonth ym = (year != null && month != null) ? YearMonth.of(year, month) : YearMonth.now();
  156. LocalDate start = ym.atDay(1);
  157. LocalDate end = ym.atEndOfMonth();
  158. List<CheckInRecord> records = checkinRecordMapper.selectMonthlyRecords(userId, start, end);
  159. List<LocalDate> checkedDates = records.stream()
  160. .map(CheckInRecord::getCheckinDate)
  161. .collect(Collectors.toList());
  162. int continuousDays = calcContinuousDays(userId, LocalDate.now());
  163. boolean checkedToday = checkedDates.contains(LocalDate.now());
  164. return CheckInCalendarResponse.builder()
  165. .month(ym.toString())
  166. .checkedDays(checkedDates.size())
  167. .continuousDays(continuousDays)
  168. .checkedDates(checkedDates)
  169. .checkedToday(checkedToday)
  170. .build();
  171. }
  172. public Page<CheckInRecord> listRecords(Long userId, int pageNum, int pageSize) {
  173. Page<CheckInRecord> page = new Page<>(pageNum, pageSize);
  174. LambdaQueryWrapper<CheckInRecord> wrapper = new LambdaQueryWrapper<CheckInRecord>()
  175. .eq(CheckInRecord::getUserId, userId)
  176. .orderByDesc(CheckInRecord::getCheckinDate);
  177. return checkinRecordMapper.selectPage(page, wrapper);
  178. }
  179. // ==========================================
  180. // 运营端管理方法
  181. // ==========================================
  182. public IPage<AdminCheckInRecordResponse> adminListRecords(Page<AdminCheckInRecordResponse> page,
  183. Long userId, LocalDate startDate,
  184. LocalDate endDate, String clientSource) {
  185. return checkinRecordMapper.selectAdminRecordsPage(page, userId, startDate, endDate, clientSource);
  186. }
  187. public CheckInStatsResponse adminGetStats() {
  188. LocalDate today = LocalDate.now();
  189. CheckInConfig config = checkinConfigService.getConfig();
  190. return CheckInStatsResponse.builder()
  191. .todayCheckinCount(nvl(checkinRecordMapper.countByDate(today)))
  192. .yesterdayCheckinCount(nvl(checkinRecordMapper.countByDate(today.minusDays(1))))
  193. .last7DaysCount(nvl(checkinRecordMapper.countByDateRange(today.minusDays(6), today)))
  194. .last30DaysCount(nvl(checkinRecordMapper.countByDateRange(today.minusDays(29), today)))
  195. .totalCheckinCount(nvl(checkinRecordMapper.countTotalRecords()))
  196. .totalCheckinUsers(nvl(checkinRecordMapper.countTotalUsers()))
  197. .totalRewardAmount(nvl(checkinRecordMapper.sumTotalReward()))
  198. .statDate(today)
  199. .enabled(checkinConfigService.isActive(config))
  200. .build();
  201. }
  202. /**
  203. * 运营端:按周期维度统计签到数据
  204. */
  205. public List<CheckInPeriodStatsResponse> adminGetPeriodStats() {
  206. List<CheckInPeriod> periods = checkinConfigService.listPeriods();
  207. List<CheckInPeriodStatsResponse> statsList = new java.util.ArrayList<>();
  208. for (CheckInPeriod period : periods) {
  209. Long totalUsers = checkinRecordMapper.countUsersByPeriod(period.getStartDate(), period.getEndDate());
  210. Long reachedUsers = checkinRecordMapper.countReachedUsersByPeriod(
  211. period.getStartDate(), period.getEndDate(),
  212. period.getRequiredDays(), period.getRewardMode());
  213. Long rewardedUsers = rewardRecordMapper.selectCount(
  214. new LambdaQueryWrapper<CheckInRewardRecord>()
  215. .eq(CheckInRewardRecord::getPeriodId, period.getId()));
  216. double reachRate = totalUsers > 0 ? (reachedUsers * 100.0 / totalUsers) : 0.0;
  217. statsList.add(CheckInPeriodStatsResponse.builder()
  218. .periodId(period.getId())
  219. .periodName(period.getPeriodName())
  220. .startDate(period.getStartDate())
  221. .endDate(period.getEndDate())
  222. .status(period.getStatus())
  223. .cycleMode(period.getCycleMode())
  224. .requiredDays(period.getRequiredDays())
  225. .rewardMode(period.getRewardMode())
  226. .rewardDays(period.getRewardDays())
  227. .totalCheckinUsers(totalUsers)
  228. .reachedUsers(reachedUsers)
  229. .reachRate(Math.round(reachRate * 100.0) / 100.0)
  230. .rewardedUsers(rewardedUsers != null ? rewardedUsers : 0L)
  231. .build());
  232. }
  233. return statsList;
  234. }
  235. public List<AdminCheckInRecordResponse> adminListUserRecords(Long userId) {
  236. if (userId == null) {
  237. throw new BusinessException(ErrorCode.PARAM_ERROR, "用户ID不能为空");
  238. }
  239. Page<AdminCheckInRecordResponse> page = new Page<>(1, 500);
  240. return checkinRecordMapper.selectAdminRecordsPage(page, userId, null, null, null).getRecords();
  241. }
  242. private Long nvl(Long v) { return v == null ? 0L : v; }
  243. // ==========================================
  244. // 周期奖励发放
  245. // ==========================================
  246. /**
  247. * 检查并发放最近一个已结束周期的奖励。幂等:DB 唯一索引兜底。
  248. */
  249. @Transactional
  250. public void tryGrantCycleReward(Long userId) {
  251. CheckInConfig config = checkinConfigService.getConfig();
  252. if (!checkinConfigService.isActive(config)) {
  253. return;
  254. }
  255. LocalDate today = LocalDate.now();
  256. CheckInPeriod lastClosed = periodMapper.findLastClosedByDate(today);
  257. if (lastClosed == null) {
  258. return;
  259. }
  260. int checkinCount = countCheckinsInRange(userId, lastClosed.getStartDate(), lastClosed.getEndDate());
  261. int requiredDays = lastClosed.getRequiredDays() != null ? lastClosed.getRequiredDays() : 4;
  262. // 根据 rewardMode 判断是否达标
  263. boolean reached = isPeriodTargetReached(userId, lastClosed);
  264. if (!reached) {
  265. return;
  266. }
  267. long rewarded = rewardRecordMapper.countRewardedByUser(userId);
  268. int maxReward = config.getMaxRewardCount() != null ? config.getMaxRewardCount() : 3;
  269. if (rewarded >= maxReward) {
  270. return;
  271. }
  272. if (isPeriodRewardGranted(userId, lastClosed)) {
  273. return;
  274. }
  275. int rewardDays = lastClosed.getRewardDays() != null ? lastClosed.getRewardDays() : 7;
  276. String rewardLevelStr = lastClosed.getRewardLevel() != null ? lastClosed.getRewardLevel() : "PRO";
  277. MembershipLevel level = MembershipLevel.fromCode(rewardLevelStr);
  278. if (level == null) {
  279. level = MembershipLevel.PRO;
  280. }
  281. membershipService.grantOrExtendMembership(userId, level, rewardDays,
  282. "CHECKIN_PERIOD", "签到周期奖励:" + lastClosed.getStartDate() + "~" + lastClosed.getEndDate()
  283. + " 累计签到" + checkinCount + "天 获" + rewardDays + "天" + rewardLevelStr + "会员");
  284. try {
  285. CheckInRewardRecord rewardRecord = CheckInRewardRecord.builder()
  286. .userId(userId)
  287. .periodId(lastClosed.getId())
  288. .checkinCount(checkinCount)
  289. .grantTime(LocalDateTime.now())
  290. .build();
  291. rewardRecordMapper.insert(rewardRecord);
  292. } catch (DuplicateKeyException e) {
  293. log.info("周期奖励并发处理: userId={}, period={}", userId, lastClosed.getStartDate());
  294. }
  295. log.info("周期奖励已发放: userId={}, period={}~{}={}, days={}",
  296. userId, lastClosed.getStartDate(), lastClosed.getEndDate(), checkinCount, rewardDays);
  297. }
  298. // ==========================================
  299. // 私有辅助
  300. // ==========================================
  301. private CheckInRecord findRecord(Long userId, LocalDate date) {
  302. return checkinRecordMapper.selectOne(
  303. new LambdaQueryWrapper<CheckInRecord>()
  304. .eq(CheckInRecord::getUserId, userId)
  305. .eq(CheckInRecord::getCheckinDate, date));
  306. }
  307. private int calcContinuousDays(Long userId, LocalDate today) {
  308. int days = 0;
  309. LocalDate d = today.minusDays(1);
  310. while (findRecord(userId, d) != null) {
  311. days++;
  312. d = d.minusDays(1);
  313. }
  314. return days;
  315. }
  316. private int countCheckinsInRange(Long userId, LocalDate start, LocalDate end) {
  317. Long count = checkinRecordMapper.selectCount(
  318. new LambdaQueryWrapper<CheckInRecord>()
  319. .eq(CheckInRecord::getUserId, userId)
  320. .ge(CheckInRecord::getCheckinDate, start)
  321. .le(CheckInRecord::getCheckinDate, end));
  322. return count != null ? count.intValue() : 0;
  323. }
  324. /**
  325. * 判断用户是否达到周期签到目标。
  326. * CUMULATIVE:累计签到天数 >= requiredDays
  327. * CONSECUTIVE:周期内最长连续签到天数 >= requiredDays
  328. */
  329. private boolean isPeriodTargetReached(Long userId, CheckInPeriod period) {
  330. int requiredDays = period.getRequiredDays() != null ? period.getRequiredDays() : 4;
  331. String mode = period.getRewardMode() != null ? period.getRewardMode() : "CUMULATIVE";
  332. if ("CONSECUTIVE".equalsIgnoreCase(mode)) {
  333. int maxConsecutive = countMaxConsecutiveInRange(userId, period.getStartDate(), period.getEndDate());
  334. return maxConsecutive >= requiredDays;
  335. }
  336. // 默认累计模式
  337. int total = countCheckinsInRange(userId, period.getStartDate(), period.getEndDate());
  338. return total >= requiredDays;
  339. }
  340. /**
  341. * 统计周期内最长连续签到天数
  342. */
  343. private int countMaxConsecutiveInRange(Long userId, LocalDate start, LocalDate end) {
  344. List<CheckInRecord> records = checkinRecordMapper.selectList(
  345. new LambdaQueryWrapper<CheckInRecord>()
  346. .eq(CheckInRecord::getUserId, userId)
  347. .ge(CheckInRecord::getCheckinDate, start)
  348. .le(CheckInRecord::getCheckinDate, end)
  349. .orderByAsc(CheckInRecord::getCheckinDate));
  350. if (records.isEmpty()) {
  351. return 0;
  352. }
  353. int maxLen = 1;
  354. int curLen = 1;
  355. for (int i = 1; i < records.size(); i++) {
  356. if (records.get(i).getCheckinDate().minusDays(1).equals(records.get(i - 1).getCheckinDate())) {
  357. curLen++;
  358. maxLen = Math.max(maxLen, curLen);
  359. } else {
  360. curLen = 1;
  361. }
  362. }
  363. return maxLen;
  364. }
  365. private boolean isPeriodRewardGranted(Long userId, CheckInPeriod period) {
  366. return rewardRecordMapper.selectCount(
  367. new LambdaQueryWrapper<CheckInRewardRecord>()
  368. .eq(CheckInRewardRecord::getUserId, userId)
  369. .eq(CheckInRewardRecord::getPeriodId, period.getId())) > 0;
  370. }
  371. // ==========================================
  372. // 活动中心:签到送会员周期状态(供 ActivityController 使用)
  373. // ==========================================
  374. /**
  375. * 签到送会员周期状态(当前周期签到天数、是否达标、累计领取次数、剩余次数)
  376. */
  377. public ActivityCheckinCycleStatusResponse getCheckinCycleStatus(Long userId) {
  378. CheckInConfig config = checkinConfigService.getConfig();
  379. // 查询时顺带尝试发放上一周期奖励
  380. if (userId != null && checkinConfigService.isActive(config)) {
  381. try {
  382. tryGrantCycleReward(userId);
  383. } catch (Exception e) {
  384. log.warn("查询签到周期状态时尝试发奖失败,不影响查询: userId={}", userId, e);
  385. }
  386. }
  387. LocalDate today = LocalDate.now();
  388. CheckInPeriod currentPeriod = periodMapper.findActiveByDate(today);
  389. LocalDate currentCycleStart;
  390. LocalDate currentCycleEnd;
  391. int requiredDays;
  392. int rewardDays;
  393. if (currentPeriod != null) {
  394. currentCycleStart = currentPeriod.getStartDate();
  395. currentCycleEnd = currentPeriod.getEndDate();
  396. requiredDays = currentPeriod.getRequiredDays() != null ? currentPeriod.getRequiredDays() : 4;
  397. rewardDays = currentPeriod.getRewardDays() != null ? currentPeriod.getRewardDays() : 7;
  398. } else {
  399. String cycleMode = config.getCycleMode();
  400. currentCycleStart = cycleStartOf(today, cycleMode);
  401. currentCycleEnd = currentCycleStart.plusDays(6);
  402. requiredDays = config.getRequiredDays() != null ? config.getRequiredDays() : 4;
  403. rewardDays = config.getRewardDays() != null ? config.getRewardDays() : 7;
  404. }
  405. int currentCount = userId != null ? countCheckinsInRange(userId, currentCycleStart, currentCycleEnd) : 0;
  406. long rewarded = userId != null ? rewardRecordMapper.countRewardedByUser(userId) : 0L;
  407. int maxReward = config.getMaxRewardCount() != null ? config.getMaxRewardCount() : 3;
  408. long remaining = Math.max(0, maxReward - rewarded);
  409. return ActivityCheckinCycleStatusResponse.builder()
  410. .enabled(checkinConfigService.isActive(config))
  411. .inWindow(checkinConfigService.isActive(config))
  412. .startTime(config.getStartTime())
  413. .endTime(config.getEndTime())
  414. .currentCycleStart(currentCycleStart)
  415. .currentCycleEnd(currentCycleEnd)
  416. .currentCycleCheckinCount(currentCount)
  417. .requiredDays(requiredDays)
  418. .reachedTarget(currentCount >= requiredDays)
  419. .rewardedCount(rewarded)
  420. .maxRewardCount(maxReward)
  421. .remainingCount(remaining)
  422. .rewardDays(rewardDays)
  423. .build();
  424. }
  425. // ==========================================
  426. // 签到周期奖励记录查询(用户端 + 运营端)
  427. // ==========================================
  428. /**
  429. * 用户端:分页查询我的签到周期奖励记录
  430. */
  431. public Page<ActivityRewardRecordResponse> listMyCycleRewards(Long userId, int pageNum, int pageSize) {
  432. return listCycleRewards(userId, pageNum, pageSize);
  433. }
  434. /**
  435. * 运营端:分页查询签到周期奖励记录,支持按用户ID筛选
  436. */
  437. public Page<ActivityRewardRecordResponse> listCycleRewards(Long userId, int pageNum, int pageSize) {
  438. Page<CheckInRewardRecord> page = new Page<>(pageNum, pageSize);
  439. LambdaQueryWrapper<CheckInRewardRecord> wrapper = new LambdaQueryWrapper<>();
  440. if (userId != null) {
  441. wrapper.eq(CheckInRewardRecord::getUserId, userId);
  442. }
  443. wrapper.orderByDesc(CheckInRewardRecord::getGrantTime);
  444. Page<CheckInRewardRecord> recordPage = rewardRecordMapper.selectPage(page, wrapper);
  445. CheckInConfig config = checkinConfigService.getConfig();
  446. int rewardDays = config.getRewardDays() != null ? config.getRewardDays() : 7;
  447. Page<ActivityRewardRecordResponse> resultPage = new Page<>(pageNum, pageSize, recordPage.getTotal());
  448. List<ActivityRewardRecordResponse> list = recordPage.getRecords().stream()
  449. .map(r -> ActivityRewardRecordResponse.builder()
  450. .id(r.getId())
  451. .userId(r.getUserId())
  452. .activityType("CHECKIN")
  453. .rewardName("签到有礼奖励")
  454. .rewardDays(rewardDays)
  455. .rewardLevel("PRO")
  456. .rewardType("MEMBERSHIP")
  457. .checkinCount(r.getCheckinCount())
  458. .createTime(r.getGrantTime())
  459. .build())
  460. .collect(Collectors.toList());
  461. resultPage.setRecords(list);
  462. return resultPage;
  463. }
  464. // ==========================================
  465. // 私有辅助
  466. // ==========================================
  467. /**
  468. * 计算日期所属周期的起始日(FRI_THU 或 MON_SUN)
  469. */
  470. private LocalDate cycleStartOf(LocalDate date, String cycleMode) {
  471. int dow = date.getDayOfWeek().getValue(); // MON=1 ... SUN=7
  472. int offset;
  473. if ("MON_SUN".equalsIgnoreCase(cycleMode)) {
  474. offset = (dow - 1 + 7) % 7;
  475. } else {
  476. offset = (dow - 5 + 7) % 7;
  477. }
  478. return date.minusDays(offset);
  479. }
  480. }