目录
- 一、功能分析
- 二、controller
- 三、service
- 四、mapper
- 五、缓存更新方法
- 六、获取当前登陆用户
-
- [6.1 引入依赖](#6.1 引入依赖)
- [6.2 工具类](#6.2 工具类)
- [6.3 修改已有代码](#6.3 修改已有代码)
-
- [6.3.1 TokenService](#6.3.1 TokenService)
- [6.3.2 拦截器TokenInterceptor](#6.3.2 拦截器TokenInterceptor)
- [6.3.3 mybatis自动更新MyMetaObjectHandler](#6.3.3 mybatis自动更新MyMetaObjectHandler)

一、功能分析
比赛:报名的用户需要记录
竞赛排名:比赛结束,统计已报名该竞赛的用户的分数
我的竞赛:当前用户已经报名的竞赛
竞赛列表:需要记录报名的用户
- 先登录,登录成功之后在竞赛列表中找到要报名的竞赛,点击报名参赛按钮,前端携带着竞赛id和token向后端发请求。
- 后端接收到请求之后,先判断是否符合报名条件。如果不满足,直接返回用户报名失败+报名失败原因。如果满足:将用户的报名信息存储到数据库和redis。
- 前端接收到后端的响应之后,根据返回的结果 提示用户报名成功或者报名失败。如果成功竟赛列表中该竟赛展示已报名标签,并且在我的竞赛列表中要能找到报名的个竞赛。
- 报名条件:1.用户必须处于登录状态、2、不能报名不存在的比赛3、不能重复报名、4、已经开赛的竞赛不能再进行
我的竞赛列表缓存:key: u:e:l:userId(user:exam:list:userId),value:list(存储examId)
建表语句:
sql
-- 竞赛报名:用户-竞赛关联表
create table tb_user_exam(
user_exam_id bigint unsigned NOT NULL COMMENT '用户竞赛关系id(主键)',
user_id bigint unsigned NOT NULL COMMENT '用户id',
exam_id bigint unsigned NOT NULL COMMENT '竞赛id',
score int unsigned COMMENT '得分',
exam_rank int unsigned COMMENT '排名',
create_by bigint unsigned not null comment '创建人',
create_time datetime not null comment '创建时间',
update_by bigint unsigned comment '更新人',
update_time datetime comment '更新时间',
primary key(user_exam_id),
-- 联合唯一索引(防止同一用户重复报名同一竞赛)
unique key uk_user_exam (user_id, exam_id),
-- 普通索引(提升查询效率)
key idx_exam_id (exam_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户竞赛报名表';
二、controller
oj-modules/oj-friend/src/main/java/com/yj/friend/controller/user/UserExamController.java
java
package com.yj.friend.controller.user;
import com.yj.common.core.constants.HttpConstants;
import com.yj.common.core.controller.BaseController;
import com.yj.common.core.pojo.R;
import com.yj.friend.domain.user.dto.ExamDTO;
import com.yj.friend.service.user.IUserExamService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user/exam")
public class UserExamController extends BaseController {
@Resource
private IUserExamService userExamService;
@PostMapping("/enter")
public R<Void> enter(@RequestBody@Validated ExamDTO examDTO, @RequestHeader(HttpConstants.AUTHENTICATION) String token) {
userExamService.enter(examDTO,token);
return R.success();
}
}
接收参数:
oj-modules/oj-friend/src/main/java/com/yj/friend/domain/user/dto/ExamDTO.java
java
package com.yj.friend.domain.user.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ExamDTO {
@NotBlank(message = "竞赛ID不能为空")
private Long examId;
}
三、service
oj-modules/oj-friend/src/main/java/com/yj/friend/service/user/IUserExamService.java,接口类
java
package com.yj.friend.service.user;
import com.yj.friend.domain.user.dto.ExamDTO;
public interface IUserExamService {
void enter(ExamDTO examDTO, String token);
}
oj-modules/oj-friend/src/main/java/com/yj/friend/service/user/impl/UserExamServiceImpl.java,对报名的条件进行一一筛选,然后存放进redis和mysql
java
package com.yj.friend.service.user.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yj.common.core.constants.Constants;
import com.yj.common.core.enums.ResultCodeEnum;
import com.yj.common.core.utils.ThreadLocalUtil;
import com.yj.common.security.exception.ServiceException;
import com.yj.common.security.service.TokenService;
import com.yj.friend.domain.exam.ExamDO;
import com.yj.friend.domain.user.UserExamDO;
import com.yj.friend.domain.user.dto.ExamDTO;
import com.yj.friend.manage.ExamCacheManager;
import com.yj.friend.mapper.exam.ExamMapper;
import com.yj.friend.mapper.user.UserExamMapper;
import com.yj.friend.service.user.IUserExamService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
public class UserExamServiceImpl implements IUserExamService {
@Autowired
private UserExamMapper userExamMapper;
@Autowired
private ExamMapper examMapper;
@Autowired
private ExamCacheManager examCacheManager;
@Override
public void enter(ExamDTO examDTO, String token) {
//竞赛是否存在
Long examId = examDTO.getExamId();
ExamDO examDO = examMapper.selectById(examId);
if(null == examDO){
throw new ServiceException(ResultCodeEnum.EXAM_NOT_EXISTS);
}
//竞赛是否已经开赛
if(examDO.getStartTime().isBefore(LocalDateTime.now())){
throw new ServiceException(ResultCodeEnum.EXAM_STARTED);
}
//重复报名
Long userId = ThreadLocalUtil.get(Constants.USER_ID, Long.class);
UserExamDO userExamDO = userExamMapper.selectOne(new LambdaQueryWrapper<UserExamDO>()
.eq(UserExamDO::getExamId, examId)
.eq(UserExamDO::getUserId, userId));
if(null != userExamDO) {
throw new ServiceException(ResultCodeEnum.USER_EXAM_HAS_ENTER);
}
//存储redis mysql 数据
examCacheManager.addUserExamCache(userId, examId);
userExamDO = new UserExamDO();
userExamDO.setUserId(userId);
userExamDO.setExamId(examId);
userExamMapper.insert(userExamDO);
}
}
四、mapper
oj-modules/oj-friend/src/main/java/com/yj/friend/mapper/user/UserExamMapper.java
java
package com.yj.friend.mapper.user;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yj.friend.domain.user.UserExamDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserExamMapper extends BaseMapper<UserExamDO> {
}
实体类:
java
package com.yj.friend.domain.user;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.yj.common.core.domain.BaseDO;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/**
* 用户竞赛报名表 实体类
* 对应数据库表:tb_user_exam
*/
@Getter
@Setter
@TableName("tb_user_exam")
public class UserExamDO extends BaseDO {
/**
* 用户竞赛关系id(主键)
*/
@JsonSerialize(using = ToStringSerializer.class)
@TableId(value = "user_exam_id", type = IdType.ASSIGN_ID)
private Long userExamId;
/**
* 用户id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 竞赛id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long examId;
/**
* 得分
*/
private Integer score;
/**
* 排名
*/
private Integer examRank;
}
五、缓存更新方法
oj-modules/oj-friend/src/main/java/com/yj/friend/manage/ExamCacheManager.java,添加方法
java
//添加用户报名竞赛缓存
public void addUserExamCache(Long userId, Long examId) {
String userExamListKey = getUserExamListKey(userId);
redisService.leftPushForList(userExamListKey, examId);
}
private String getUserExamListKey(Long userId) {
return CacheConstants.USER_EXAM_LIST + userId;
}
oj-common/oj-common-core/src/main/java/com/yj/common/core/constants/CacheConstants.java,缓存头
java
public final static String USER_EXAM_LIST = "u:e:l:"; //用户竞赛列表
六、获取当前登陆用户
使用ThreadLocal,当前线程变量存储用户userId即可,存储在拦截器中,每一个模块都需要进行。
6.1 引入依赖
oj-common/oj-common-core/pom.xml
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.4</version>
</dependency>
6.2 工具类
oj-common/oj-common-core/src/main/java/com/yj/common/core/utils/ThreadLocalUtil.java,提供设置、获取、删除方法。
java
package com.yj.common.core.utils;
import cn.hutool.core.util.StrUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ThreadLocalUtil {
private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();
public static void set(String key, Object value) {
Map<String, Object> map = getLocalMap();
map.put(key, value == null ? StrUtil.EMPTY : value);
}
public static <T> T get(String key, Class<T> clazz) {
Map<String, Object> map = getLocalMap();
return (T) map.getOrDefault(key, null);
}
public static Map<String, Object> getLocalMap() {
Map<String, Object> map = THREAD_LOCAL.get();
if (map == null) {
map = new ConcurrentHashMap<String, Object>();
THREAD_LOCAL.set(map);
}
return map;
}
public static void remove() {
THREAD_LOCAL.remove();
}
}
6.3 修改已有代码
6.3.1 TokenService
oj-common/oj-common-security/src/main/java/com/yj/common/security/service/TokenService.java提供根据token获取userId:
java
public Claims getClaims(String token, String secret) {
Claims claims;
try {
claims = JwtUtils.parseToken(token, secret); //获取令牌中信息 解析payload中信息
if (claims == null) {
log.error("解析Token: {},出现异常,", token);
return null;
}
} catch (Exception e) {
log.error("解析Token: {},出现异常,", token,e);
return null;
}
return claims;
}
public Long getUserId(Claims claims) {
if (claims == null) return null;
return Long.valueOf(JwtUtils.getUserId(claims)); //获取jwt中的userId
}
6.3.2 拦截器TokenInterceptor
oj-common/oj-common-security/src/main/java/com/yj/common/security/interceptor/TokenInterceptor.java,将userId存入当前线程,完成后删除。
java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = getToken(request);
Claims claims = tokenService.getClaims(token, jwtSecret);
Long userId = tokenService.getUserId(claims);
ThreadLocalUtil.set(Constants.USER_ID, userId);
tokenService.extendToken(token, jwtSecret);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ThreadLocalUtil.remove();
}
oj-common/oj-common-core/src/main/java/com/yj/common/core/constants/Constants.java
java
public static final String USER_ID = "user_id";
6.3.3 mybatis自动更新MyMetaObjectHandler
oj-common/oj-common-mybatis/src/main/java/com/yj/common/mybatis/MyMetaObjectHandler.java
java
package com.yj.common.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.yj.common.core.utils.ThreadLocalUtil;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import com.yj.common.core.constants.Constants;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
//创建人 获取当前用户用户id
this.strictInsertFill(metaObject, "createBy", Long.class, ThreadLocalUtil.get(Constants.USER_ID, Long.class));
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
this.setFieldValByName("updateBy", ThreadLocalUtil.get(Constants.USER_ID, Long.class), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
this.setFieldValByName("updateBy", ThreadLocalUtil.get(Constants.USER_ID, Long.class), metaObject);
}
}