【项目】【在线判题系统】c端:竞赛报名

目录

一、功能分析

比赛:报名的用户需要记录

竞赛排名:比赛结束,统计已报名该竞赛的用户的分数

我的竞赛:当前用户已经报名的竞赛

竞赛列表:需要记录报名的用户

  • 先登录,登录成功之后在竞赛列表中找到要报名的竞赛,点击报名参赛按钮,前端携带着竞赛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);
    }
}
相关推荐
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 个人资料管理的需求分析与接口设计
java·开发语言·交友
1024奇点1 小时前
【仓颉语言入门 · 第二篇 第 8 课】
开发语言·数据库·ide·mysql·开源
gopher95112 小时前
RUST:序列化与反序列化 serde + derive 详解
开发语言·后端·rust
H.莓飛2 小时前
【Linux】自主 Shell
linux·运维·服务器·c语言·开发语言·visual studio
熊猫钓鱼>_>2 小时前
Kotlin Multiplatform for OpenHarmony 实战:kotlinx-coroutines 结构化并发适配
开发语言·kotlin·agent·harmonyos·openharmony·适配·华为云码道
沫璃染墨2 小时前
《Qt从零入门系列(十四):QThread多线程详解——从线程创建到客户端阻塞处理》
开发语言·c++·笔记·qt·ui·交互·线程
hixiong1233 小时前
C# 使用 ZXingCpp.Angle 进行条码、二维码识别
开发语言·c#·扫码
云和数据.ChenGuang3 小时前
AI 工程师相关就业岗位(细分,偏工程落地,由底层到上层)
服务器·开发语言·人工智能·python
爱读书的小胖3 小时前
Codewars:Last digit of a large number(C)解题分析
c语言·开发语言·r语言