1.基础签到实现
1.1代码如下
@Service
@Slf4j
public class SignInService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String SIGN_KEY_PREFIX = "sign:";
public boolean signIn(Long userId, LocalDate date) {
try {
String key = buildSignKey(userId, date);
int dayOfMonth = date.getDayOfMonth();
return Boolean.TRUE.equals(redisTemplate.opsForValue()
.setBit(key, dayOfMonth - 1, true));
} catch (Exception e) {
log.error("签到失败", e);
throw new RuntimeException("签到失败", e);
}
}
public boolean isSignedIn(Long userId, LocalDate date) {
try {
String key = buildSignKey(userId, date);
int dayOfMonth = date.getDayOfMonth();
return Boolean.TRUE.equals(redisTemplate.opsForValue()
.getBit(key, dayOfMonth - 1));
} catch (Exception e) {
log.error("检查签到状态失败", e);
throw new RuntimeException("检查签到状态失败", e);
}
}
public long getMonthSignCount(Long userId, LocalDate date) {
try {
String key = buildSignKey(userId, date);
return redisTemplate.execute((RedisCallback<Long>) con ->
con.bitCount(key.getBytes()));
} catch (Exception e) {
log.error("获取签到次数失败", e);
throw new RuntimeException("获取签到次数失败", e);
}
}
private String buildSignKey(Long userId, LocalDate date) {
return String.format("%s%d:%s",
SIGN_KEY_PREFIX, userId, date.format(DateTimeFormatter.ofPattern("yyyyMM")));
}
}
2. 进阶功能实现
2.1代码如下
@Service
@Slf4j
public class AdvancedSignInService {
@Autowired
private StringRedisTemplate redisTemplate;
public int getContinuousSignCount(Long userId, LocalDate date) {
try {
String key = buildSignKey(userId, date);
int dayOfMonth = date.getDayOfMonth();
int count = 0;
for (int i = dayOfMonth - 1; i >= 0; i--) {
if (Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, i))) {
count++;
} else {
break;
}
}
return count;
} catch (Exception e) {
log.error("获取连续签到天数失败", e);
throw new RuntimeException("获取连续签到天数失败", e);
}
}
public List<Boolean> getMonthSignList(Long userId, LocalDate date) {
try {
String key = buildSignKey(userId, date);
int dayCount = date.lengthOfMonth();
List<Boolean> result = new ArrayList<>(dayCount);
for (int i = 0; i < dayCount; i++) {
result.add(Boolean.TRUE.equals(
redisTemplate.opsForValue().getBit(key, i)));
}
return result;
} catch (Exception e) {
log.error("获取签到日历失败", e);
throw new RuntimeException("获取签到日历失败", e);
}
}
public boolean retroactiveSign(Long userId, LocalDate date) {
try {
if (isSignedIn(userId, date) || date.isAfter(LocalDate.now())) {
return false;
}
String key = buildSignKey(userId, date);
int dayOfMonth = date.getDayOfMonth();
return Boolean.TRUE.equals(redisTemplate.opsForValue()
.setBit(key, dayOfMonth - 1, true));
} catch (Exception e) {
log.error("补签失败", e);
throw new RuntimeException("补签失败", e);
}
}
public LocalDate getFirstSignDate(Long userId, LocalDate date) {
try {
String key = buildSignKey(userId, date);
int dayCount = date.lengthOfMonth();
for (int i = 0; i < dayCount; i++) {
if (Boolean.TRUE.equals(
redisTemplate.opsForValue().getBit(key, i))) {
return date.withDayOfMonth(i + 1);
}
}
return null;
} catch (Exception e) {
log.error("获取首次签到日期失败", e);
throw new RuntimeException("获取首次签到日期失败", e);
}
}
}
3. 签到统计和奖励系统
3.1代码如下
@Service
@Slf4j
public class SignInRewardService {
@Autowired
private AdvancedSignInService signInService;
public SignInReward calculateReward(Long userId, LocalDate date) {
int continuousDays = signInService.getContinuousSignCount(userId, date);
long monthTotal = signInService.getMonthSignCount(userId, date);
SignInReward reward = new SignInReward();
reward.setPoints(calculatePoints(continuousDays));
reward.setBonus(calculateBonus(monthTotal));
return reward;
}
private int calculatePoints(int continuousDays) {
if (continuousDays >= 30) return 100;
if (continuousDays >= 20) return 50;
if (continuousDays >= 10) return 30;
if (continuousDays >= 7) return 20;
if (continuousDays >= 3) return 10;
return 5;
}
private BigDecimal calculateBonus(long monthTotal) {
if (monthTotal >= 28) return new BigDecimal("100");
if (monthTotal >= 20) return new BigDecimal("50");
if (monthTotal >= 15) return new BigDecimal("30");
if (monthTotal >= 10) return new BigDecimal("20");
return BigDecimal.ZERO;
}
}
@Data
public class SignInReward {
private int points;
private BigDecimal bonus;
}
4. Controller层实现
4.1.代码如下
@RestController
@RequestMapping("/api/sign")
@Slf4j
public class SignInController {
@Autowired
private SignInService signInService;
@Autowired
private AdvancedSignInService advancedSignInService;
@Autowired
private SignInRewardService rewardService;
@PostMapping("/in")
public ResponseEntity<?> signIn(@RequestParam Long userId) {
try {
LocalDate today = LocalDate.now();
boolean success = signInService.signIn(userId, today);
if (success) {
SignInReward reward = rewardService.calculateReward(userId, today);
return ResponseEntity.ok(reward);
}
return ResponseEntity.badRequest().body("签到失败");
} catch (Exception e) {
log.error("签到异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("系统错误");
}
}
@GetMapping("/calendar")
public ResponseEntity<?> getSignCalendar(
@RequestParam Long userId,
@RequestParam(required = false) String monthStr) {
try {
LocalDate date = monthStr != null ?
YearMonth.parse(monthStr).atDay(1) : LocalDate.now();
List<Boolean> calendar = advancedSignInService.getMonthSignList(userId, date);
return ResponseEntity.ok(calendar);
} catch (Exception e) {
log.error("获取签到日历异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("系统错误");
}
}
@PostMapping("/retroactive")
public ResponseEntity<?> retroactiveSign(
@RequestParam Long userId,
@RequestParam String dateStr) {
try {
LocalDate date = LocalDate.parse(dateStr);
boolean success = advancedSignInService.retroactiveSign(userId, date);
if (success) {
return ResponseEntity.ok().build();
}
return ResponseEntity.badRequest().body("补签失败");
} catch (Exception e) {
log.error("补签异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("系统错误");
}
}
}
5. 性能优化建议
5.1 使用Pipeline批量操作:
public List<Boolean> getMonthSignListWithPipeline(Long userId, LocalDate date) {
String key = buildSignKey(userId, date);
int dayCount = date.lengthOfMonth();
List<Boolean> result = new ArrayList<>(dayCount);
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (int i = 0; i < dayCount; i++) {
connection.getBit(key.getBytes(), i);
}
return null;
}).forEach(b -> result.add((Boolean) b));
return result;
}
5.2.使用缓存减少Redis访问:
@Cacheable(value = "sign_calendar", key = "#userId + ':' + #date.format(T(java.time.format.DateTimeFormatter).ofPattern('yyyyMM'))")
public List<Boolean> getMonthSignList(Long userId, LocalDate date) {
}