文章目录
-
- 概要
- 整体架构流程
- 技术细节
- 小结
概要
需求分析以及接口设计
由KEY的结构可知,要签到,就必须知道是谁在哪一天签到,也就是两个信息:
-
当前用户
-
当前时间
这两个信息我们都可以自己获取,因此签到时,前端无需传递任何参数。
那么签到以后是否需要返回数据呢?
需求中说连续签到会有积分奖励,那么为了提升用户体验,在用户签到成功以后是不是应该返回连续签到天数和获取的积分奖励呢。
综上,最终签到的接口信息如下:
另外,为了便于统计,我们计划每个月为每个用户生成一个独立的KEY,因此KEY中必须包含用户信息、月份信息,长这样:
sign:uid:xxx:202401
技术细节
package com.tianji.learning.domain.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "签到结果")
public class SignResultVO {
@ApiModelProperty("连续签到天数")
private Integer signDays;
@ApiModelProperty("签到得分")
private Integer signPoints = 1;
@ApiModelProperty("连续签到奖励积分,连续签到超过7天以上才有奖励")
private Integer rewardPoints;
@JsonIgnore
public int totalPoints(){
return signPoints + rewardPoints;
}
}
1.Controller层
package com.tianji.learning.controller;
import com.tianji.learning.domain.vo.SignResultVO;
import com.tianji.learning.service.ISignRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "签到相关接口")
@RestController
@RequestMapping("sign-records")
@RequiredArgsConstructor
public class SignRecordController {
private final ISignRecordService recordService;
@PostMapping
@ApiOperation("签到功能接口")
public SignResultVO addSignRecords(){
return recordService.addSignRecords();
}
}
2.Service层:
package com.tianji.learning.service.impl;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BooleanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.DateUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.constants.RedisConstants;
import com.tianji.learning.domain.vo.SignResultVO;
import com.tianji.learning.mq.message.SignInMessage;
import com.tianji.learning.service.ISignRecordService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
@Service
@RequiredArgsConstructor
public class SignRecordServiceImpl implements ISignRecordService {
private final StringRedisTemplate redisTemplate;
@Override
public SignResultVO addSignRecords() {
// 1.签到
// 1.1.获取登录用户
Long userId = UserContext.getUser();
// 1.2.获取日期
LocalDate now = LocalDate.now();
// 1.3.拼接key
String key = RedisConstants.SIGN_RECORD_KEY_PREFIX
+ userId
+ now.format(DateUtils.SIGN_DATE_SUFFIX_FORMATTER);
// 1.4.计算offset
int offset = now.getDayOfMonth() - 1;
// 1.5.保存签到信息
Boolean exists = redisTemplate.opsForValue().setBit(key, offset, true);
if (BooleanUtils.isTrue(exists)) {
throw new BizIllegalException("不允许重复签到!");
}
// 2.计算连续签到天数
int signDays = countSignDays(key, now.getDayOfMonth());
// 3.计算签到得分
int rewardPoints = 0;
switch (signDays) {
case 7:
rewardPoints = 10;
break;
case 14:
rewardPoints = 20;
break;
case 28:
rewardPoints = 40;
break;
}
// TODO 4.保存积分明细记录
// 5.封装返回
SignResultVO vo = new SignResultVO();
vo.setSignDays(signDays);
vo.setRewardPoints(rewardPoints);
return vo;
}
private int countSignDays(String key, int len) {
// 1.获取本月从第一天开始,到今天为止的所有签到记录
List<Long> result = redisTemplate.opsForValue()
.bitField(key, BitFieldSubCommands.create().get(
BitFieldSubCommands.BitFieldType.unsigned(len)).valueAt(0));
if (CollUtils.isEmpty(result)) {
return 0;
}
int num = result.get(0).intValue();
// 2.定义一个计数器
int count = 0;
// 3.循环,与1做与运算,得到最后一个bit,判断是否为0,为0则终止,为1则继续
while ((num & 1) == 1) {
// 4.计数器+1
count++;
// 5.把数字右移一位,最后一位被舍弃,倒数第二位成了最后一位
num >>>= 1;
}
return count;
}
}