关注

励志从零打造LeetCode平台之C端竞赛列表

目录

竞赛列表

业务分析

代码结构变阵

Redis优化竞赛

业务分析

代码优化

xxl-job   

引⼊项⽬

新增执行器

配置部署“执⾏器项⽬”

引入依赖

配置xxl-job

新增配置⽂件

增加“handler”⽅法

前端开发:

目前效果展示


竞赛列表

业务分析

C端与B端竞赛列表的差异主要体现在以下三个方面:

  1. 显示方式存在差异(可通过前端实现调整)

B端竞赛列表:

C端竞赛列表:

2.每个竞赛展示的数据不同

“C端只用显示竞赛标题、开始时间、结束时间”

而后端除此之外需要展示创建人,竞赛状态......

(可以通过修改sql语句进行修改)

3.C端支持游客使用,B端必须先完成登录

(使用网关白名单跳过即可)

通过业务分析可知,我们只需基于B端代码进行修改即可。

代码结构变阵

Controller

@RestController
@RequestMapping("/exam")
public class ExamController extends BaseController {

    @Autowired
    private IExamService examService;
    @GetMapping("/semiLogin/list")
    public List<ExamVO> list(ExamQueryDTO examQueryDTO) {
        return examService.list(examQueryDTO);
    }
}

Service

@Service
@Slf4j
public class ExamServiceImpl implements IExamService {
    @Autowired
    private ExamMapper examMapper;
    @Override
    public List<ExamVO> list(ExamQueryDTO examQueryDTO) {
        PageHelper.startPage(examQueryDTO.getPageNum(), examQueryDTO.getPageSize());
        return examMapper.selectExamList(examQueryDTO);
    }
}

Mapper

public interface ExamMapper extends BaseMapper<Exam> {
    List<ExamVO> selectExamList(ExamQueryDTO examQueryDTO);
}

DTO

这里相比之前B端竞赛列表DTO多了一个类型type,因为我们查找的时候可能选择未完赛和历史竞赛

@Getter
@Setter
public class ExamQueryDTO extends pageDomain {

    private String title;

    private String startTime;

    private String endTime;
    private Integer type; //0 未完善  1 历史竞赛
}

VO

由于我们前端只显示标题,开始时间,结束时间,所以我们这里减少非必要参数

@Getter
@Setter
public class ExamVO {

    @JsonSerialize(using = ToStringSerializer.class)
    private Long examId;

    private String title;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;

}

ExamMapper.xml

修改sql语句,以满足前端返回值的需求以及搜索要求的改变

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bite.friend.mapper.exam.ExamMapper">

    <select id="selectExamList" resultType="com.bite.friend.model.exam.vo.ExamVO">
        SELECT
        te.exam_id,
        te.title,
        te.start_time,
        te.end_time
        FROM
        tb_exam te
        where
        status = 1
        <if test="title !=null and title !='' ">
            AND te.title LIKE CONCAT('%',#{title},'%')
        </if>
        <if test="startTime != null and startTime != '' ">
            AND te.start_time >= #{startTime}
        </if>
        <if test="endTime != null and endTime != ''">
            AND te.end_time &lt;= #{endTime}
        </if>
        <if test="type == 0">
            And te.end_time > NOW()
        </if>
        <if test="type == 1">
            And te.end_time &lt;= NOW()
        </if>
        ORDER BY
        te.create_time DESC
    </select>
</mapper>

postman测试

数据库里面搜索历史竞赛

select * from tb_exam where status = 1

Redis优化竞赛

业务分析

因为我们致力于打造一个leetCode级别的竞赛系统,那我们就得注重我们竞赛平台的效率。但是现在每一个用户查询竞赛列表的时候,我们都得从数据库里面进行查找,这样一方面速度上可能受到影响,另一方法数据库服务器也可能会因为各种原因导致崩溃,所以我们就引入Redis作为缓存去优化我们得查找功能

什么时候将 C 端竞赛数据存储到 redis 当中?

  • 在发布竞赛的时候应该将 C 端的竞赛数据存储到 redis 里。
  • 取消发布时候还需要将 C 端的竞赛数据从 redis 当中移除。

选择怎样的数据结构来存储 C 端的竞赛数据?

选择:list 选择两个 list 结构,分别存储未完赛的竞赛列表 和 历史竞赛列表。

key: e:t:l        e:h:l

value:  examid

此外,我们还需要再将竞赛信息单独储存一份

key :   examid

value :  json{竞赛信息}

代码优化

为了简化我们后续操作,我们将Redis相关的操作封装为一个类

@Component
public class ExamCacheManager {

    @Autowired
    private RedisService redisService;

    public void addCache(Exam exam) {
        redisService.leftPushForList(getExamListKey(), exam.getExamId());
        redisService.setCacheObject(getDetailKey(exam.getExamId()), exam);
    }

    public void deleteCache(Long examId) {
        redisService.removeForList(getExamListKey(), examId);
        redisService.deleteObject(getDetailKey(examId));
    }

    private String getExamListKey() {
        //得到竞赛列表key
        return CacheConstants.EXAM_UNFINISHED_LIST;
    }

    private String getDetailKey(Long examId) {
        //得到详细信息key
        return CacheConstants.EXAM_DETAIL + examId;
    }
}

add:将未完赛/历史竞赛对应的examid以及竞赛信息存储到redis

delete: 将未完赛/历史竞赛对应的examid以及竞赛相关信息从redis中删除

第一步:在我们发布竞赛得时候,将竞赛储存到redis中

@Override
    public int publish(Long examId) {
        Exam exam = getExam(examId);

        if (exam.getEndTime().isBefore(LocalDateTime.now())) {
            throw new ServiceException(ResultCode.EXAM_IS_FINISH);
        }

        Long count = examQuestionMapper
                .selectCount(new LambdaQueryWrapper<ExamQuestion>()
                        .eq(ExamQuestion::getExamId, examId));
        if (count == null || count <= 0) {
            throw new ServiceException(ResultCode.EXAM_NOT_HAS_QUESTION);
        }
        exam.setStatus(Contants.TRUE);
        examCacheManager.addCache(exam);
        return examMapper.updateById(exam);
    }

第二步:在撤销竞赛的时候,将竞赛从redis中删除

 @Override
    public int cancelPublish(Long examId) {
        Exam exam = getExam(examId);
        checkExam(exam);
        if (exam.getEndTime().isBefore(LocalDateTime.now())) {
            throw new ServiceException(ResultCode.EXAM_IS_FINISH);
        }
        exam.setStatus(Contants.FALSE);
        examCacheManager.deleteCache(examId);
        return examMapper.updateById(exam);
    }

第三步:使用接口调用redis查询竞赛列表

首先再引入一个管理类:将竞赛相关操作封装为一个类,接下来我们来详细剖析一下代码作用

@Component
public class ExamCacheManager {

    @Autowired
    private ExamMapper examMapper;
    @Autowired
    private RedisService redisService;

    public Long getListSize(Integer examListType) {
        String examListKey = getExamListKey(examListType);
        return redisService.getListSize(examListKey);
    }
    public List<ExamVO> getExamVOList(ExamQueryDTO examQueryDTO) {
        int start = (examQueryDTO.getPageNum() - 1) * examQueryDTO.getPageSize();
        int end = start + examQueryDTO.getPageSize() - 1; //下标需要 -1
        String examListKey = getExamListKey(examQueryDTO.getType());
        List<Long> examIdList = redisService.getCacheListByRange(examListKey, start, end, Long.class);
        List<ExamVO> examVOList = assembleExamVOList(examIdList);
        if (CollectionUtil.isEmpty(examVOList)) {
            //说明redis中数据可能有问题 从数据库中查数据并且重新刷新缓存
            examVOList = getExamListByDB(examQueryDTO); //从数据库中获取数据
            refreshCache(examQueryDTO.getType());
        }
        return examVOList;
    }
    //刷新缓存逻辑
    public void refreshCache(Integer examListType) {
        List<Exam> examList = new ArrayList<>();
        if (ExamListType.EXAM_UN_FINISH_LIST.getValue().equals(examListType)) {
            //查询未完赛的竞赛列表
            examList = examMapper.selectList(new LambdaQueryWrapper<Exam>()
                    .select(Exam::getExamId, Exam::getTitle, Exam::getStartTime, Exam::getEndTime)
                    .gt(Exam::getEndTime, LocalDateTime.now())
                    .eq(Exam::getStatus, Contants.TRUE)
                    .orderByDesc(Exam::getCreateTime));
        } else if (ExamListType.EXAM_HISTORY_LIST.getValue().equals(examListType)) {
            //查询历史竞赛
            examList = examMapper.selectList(new LambdaQueryWrapper<Exam>()
                    .select(Exam::getExamId, Exam::getTitle, Exam::getStartTime, Exam::getEndTime)
                    .le(Exam::getEndTime, LocalDateTime.now())
                    .eq(Exam::getStatus, Contants.TRUE)
                    .orderByDesc(Exam::getCreateTime));
        }
        if (CollectionUtil.isEmpty(examList)) {
            return;
        }

        Map<String, Exam> examMap = new HashMap<>();
        List<Long> examIdList = new ArrayList<>();
        for (Exam exam : examList) {
            examMap.put(getDetailKey(exam.getExamId()), exam);
            examIdList.add(exam.getExamId());
        }
        redisService.multiSet(examMap);  //批量将竞赛信息存入
        redisService.deleteObject(getExamListKey(examListType));//因为下面是rightPushAll,所以我们需要先清楚,而上面是set可以直接覆盖,所以不用
        redisService.rightPushAll(getExamListKey(examListType), examIdList);      //刷新列表缓存
    }

    private List<ExamVO> getExamListByDB(ExamQueryDTO examQueryDTO) {
        PageHelper.startPage(examQueryDTO.getPageNum(), examQueryDTO.getPageSize());
            //查询C端的竞赛列表
            return examMapper.selectExamList(examQueryDTO);
    }

    private List<ExamVO> assembleExamVOList(List<Long> examIdList) {
        if (CollectionUtil.isEmpty(examIdList)) {
            //说明redis当中没数据 从数据库中查数据并且重新刷新缓存
            return null;
        }
        //拼接redis当中key的方法 并且将拼接好的key存储到一个list中
        List<String> detailKeyList = new ArrayList<>();
        for (Long examId : examIdList) {
            detailKeyList.add(getDetailKey(examId));
        }
        List<ExamVO> examVOList = redisService.multiGet(detailKeyList, ExamVO.class);
        CollUtil.removeNull(examVOList);
        if (CollectionUtil.isEmpty(examVOList) || examVOList.size() != examIdList.size()) {
            //说明redis中数据有问题 从数据库中查数据并且重新刷新缓存
            return null;
        }
        return examVOList;
    }

    private String getExamListKey(Integer examListType) {
        if (ExamListType.EXAM_UN_FINISH_LIST.getValue().equals(examListType)) {
            return CacheConstants.EXAM_UNFINISHED_LIST;
        } else {
            return CacheConstants.EXAM_HISTORY_LIST;
        }
    }
    private String getDetailKey(Long examId) {
        return CacheConstants.EXAM_DETAIL + examId;
    }
}

getExamListKey:通过传入的type类型返回是哪个列表(未完赛列表还是历史列表)
getDetailKey :通过传入的竞赛id,或者竞赛id的修饰key

它的作用是刷新缓存

分类讨论,如果查询未完赛的话就去数据库里面查找未完赛

如果数据库中也为空,则返回,否则将查找到的数据存储到map里面以便后续使用

首先批量将我们数据库中查到的竞赛信息存入redis,然后将未完赛/历史竞赛对应的examid以及竞赛相关信息从redis中删除(为了后面的重新pushAll)

从数据库中获得竞赛数据

去redis中获取数据,如果不包含数据或者数据的数量不符合我们预期的数量,则返回空。否则返回查询到的数据列表

查询竞赛列表

首先获取要查询的页数和个数(分页功能),然后判断assemble方法获取的返回值是否为空如果为空,则直接去数据库中查找,然后刷新缓存,否则直接返回查询到的examVoList

Controller

 @GetMapping("/semiLogin/redis/list")
    public TableDataInfo redisList(ExamQueryDTO examQueryDTO) {
        return examService.redisList(examQueryDTO);
    }

Service

 @Override
    public TableDataInfo redisList(ExamQueryDTO examQueryDTO) {
        Long total = examCacheManager.getListSize(examQueryDTO.getType());
        List<ExamVO> examVOList;
        if (total == null || total <= 0) {
            examVOList=list(examQueryDTO);
            examCacheManager.refreshCache(examQueryDTO.getType());
            total=new PageInfo<>(examVOList).getTotal();
        }else {
            examVOList=examCacheManager.getExamVOList(examQueryDTO);
            total=examCacheManager.getListSize(examQueryDTO.getType());
        }
        if(CollectionUtil.isEmpty(examVOList)){
            return TableDataInfo.empty();
        }
        return TableDataInfo.success(examVOList,total);
    }

测试代码

虽然效率有所提升,但目前我们遇到一个棘手的问题:

Redis中存储的竞赛信息并非实时更新,随着时间的推移,这些数据可能会出现偏差。

解决方法:

我们可以协商一个固定的缓存刷新时间点,及时清理过时的竞赛信息。

我们采用xxl-job组件来实现任务调度功能。

xxl-job   

引⼊项⽬

1.通过Docker 镜像⽅式搭建调度中⼼:

docker pull xuxueli/xxl-job-admin:2.4.0

2.初始化数据库

#
# XXL-JOB
# Copyright (c) 2015-present, xuxueli.

CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `xxl_job`;

SET NAMES utf8mb4;

## —————————————————————— job group and registry ——————————————————

CREATE TABLE `xxl_job_group`
(
    `id`           int(11)     NOT NULL AUTO_INCREMENT,
    `app_name`     varchar(64) NOT NULL COMMENT '执行器AppName',
    `title`        varchar(12) NOT NULL COMMENT '执行器名称',
    `address_type` tinyint(4)  NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
    `address_list` text COMMENT '执行器地址列表,多地址逗号分隔',
    `update_time`  datetime             DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE `xxl_job_registry`
(
    `id`             int(11)      NOT NULL AUTO_INCREMENT,
    `registry_group` varchar(50)  NOT NULL,
    `registry_key`   varchar(255) NOT NULL,
    `registry_value` varchar(255) NOT NULL,
    `update_time`    datetime DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `i_g_k_v` (`registry_group`, `registry_key`, `registry_value`) USING BTREE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

## —————————————————————— job info ——————————————————

CREATE TABLE `xxl_job_info`
(
    `id`                        int(11)      NOT NULL AUTO_INCREMENT,
    `job_group`                 int(11)      NOT NULL COMMENT '执行器主键ID',
    `job_desc`                  varchar(255) NOT NULL,
    `add_time`                  datetime              DEFAULT NULL,
    `update_time`               datetime              DEFAULT NULL,
    `author`                    varchar(64)           DEFAULT NULL COMMENT '作者',
    `alarm_email`               varchar(255)          DEFAULT NULL COMMENT '报警邮件',
    `schedule_type`             varchar(50)  NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
    `schedule_conf`             varchar(128)          DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
    `misfire_strategy`          varchar(50)  NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
    `executor_route_strategy`   varchar(50)           DEFAULT NULL COMMENT '执行器路由策略',
    `executor_handler`          varchar(255)          DEFAULT NULL COMMENT '执行器任务handler',
    `executor_param`            varchar(512)          DEFAULT NULL COMMENT '执行器任务参数',
    `executor_block_strategy`   varchar(50)           DEFAULT NULL COMMENT '阻塞处理策略',
    `executor_timeout`          int(11)      NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
    `executor_fail_retry_count` int(11)      NOT NULL DEFAULT '0' COMMENT '失败重试次数',
    `glue_type`                 varchar(50)  NOT NULL COMMENT 'GLUE类型',
    `glue_source`               mediumtext COMMENT 'GLUE源代码',
    `glue_remark`               varchar(128)          DEFAULT NULL COMMENT 'GLUE备注',
    `glue_updatetime`           datetime              DEFAULT NULL COMMENT 'GLUE更新时间',
    `child_jobid`               varchar(255)          DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
    `trigger_status`            tinyint(4)   NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
    `trigger_last_time`         bigint(13)   NOT NULL DEFAULT '0' COMMENT '上次调度时间',
    `trigger_next_time`         bigint(13)   NOT NULL DEFAULT '0' COMMENT '下次调度时间',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE `xxl_job_logglue`
(
    `id`          int(11)      NOT NULL AUTO_INCREMENT,
    `job_id`      int(11)      NOT NULL COMMENT '任务,主键ID',
    `glue_type`   varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
    `glue_source` mediumtext COMMENT 'GLUE源代码',
    `glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
    `add_time`    datetime    DEFAULT NULL,
    `update_time` datetime    DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

## —————————————————————— job log and report ——————————————————

CREATE TABLE `xxl_job_log`
(
    `id`                        bigint(20) NOT NULL AUTO_INCREMENT,
    `job_group`                 int(11)    NOT NULL COMMENT '执行器主键ID',
    `job_id`                    int(11)    NOT NULL COMMENT '任务,主键ID',
    `executor_address`          varchar(255)        DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
    `executor_handler`          varchar(255)        DEFAULT NULL COMMENT '执行器任务handler',
    `executor_param`            varchar(512)        DEFAULT NULL COMMENT '执行器任务参数',
    `executor_sharding_param`   varchar(20)         DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
    `executor_fail_retry_count` int(11)    NOT NULL DEFAULT '0' COMMENT '失败重试次数',
    `trigger_time`              datetime            DEFAULT NULL COMMENT '调度-时间',
    `trigger_code`              int(11)    NOT NULL COMMENT '调度-结果',
    `trigger_msg`               text COMMENT '调度-日志',
    `handle_time`               datetime            DEFAULT NULL COMMENT '执行-时间',
    `handle_code`               int(11)    NOT NULL COMMENT '执行-状态',
    `handle_msg`                text COMMENT '执行-日志',
    `alarm_status`              tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
    PRIMARY KEY (`id`),
    KEY `I_trigger_time` (`trigger_time`),
    KEY `I_handle_code` (`handle_code`),
    KEY `I_jobid_jobgroup` (`job_id`,`job_group`),
    KEY `I_job_id` (`job_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE `xxl_job_log_report`
(
    `id`            int(11) NOT NULL AUTO_INCREMENT,
    `trigger_day`   datetime         DEFAULT NULL COMMENT '调度-时间',
    `running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
    `suc_count`     int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
    `fail_count`    int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
    `update_time`   datetime         DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

## —————————————————————— lock ——————————————————

CREATE TABLE `xxl_job_lock`
(
    `lock_name` varchar(50) NOT NULL COMMENT '锁名称',
    PRIMARY KEY (`lock_name`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

## —————————————————————— user ——————————————————

CREATE TABLE `xxl_job_user`
(
    `id`         int(11)     NOT NULL AUTO_INCREMENT,
    `username`   varchar(50) NOT NULL COMMENT '账号',
    `password`   varchar(100) NOT NULL COMMENT '密码加密信息',
    `token`      varchar(100) DEFAULT NULL COMMENT '登录token',
    `role`       tinyint(4)  NOT NULL COMMENT '角色:0-普通用户、1-管理员',
    `permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
    PRIMARY KEY (`id`),
    UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;


## —————————————————————— for default data ——————————————————

INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`)
    VALUES (1, 'xxl-job-executor-sample', '通用执行器Sample', 0, NULL, now()),
           (2, 'xxl-job-executor-sample-ai', 'AI执行器Sample', 0, NULL, now());

INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`,
                           `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`,
                           `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`,
                           `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`,
                           `child_jobid`)
VALUES (1, 1, '示例任务01', now(), now(), 'XXL', '', 'CRON', '0 0 0 * * ? *',
        'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化',
        now(), ''),
       (2, 2, 'Ollama示例任务01', now(), now(), 'XXL', '', 'NONE', '',
        'DO_NOTHING', 'FIRST', 'ollamaJobHandler', '{
    "input": "慢SQL问题分析思路",
    "prompt": "你是一个研发工程师,擅长解决技术类问题。",
    "model": "qwen3:0.6b"
}', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化',
        now(), ''),
       (3, 2, 'Dify示例任务', now(), now(), 'XXL', '', 'NONE', '',
        'DO_NOTHING', 'FIRST', 'difyWorkflowJobHandler', '{
    "inputs":{
        "input":"查询班级各学科前三名"
    },
    "user": "xxl-job",
    "baseUrl": "http://localhost/v1",
    "apiKey": "app-OUVgNUOQRIMokfmuJvBJoUTN"
}', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化',
        now(), '');

INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`)
VALUES (1, 'admin', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 1, NULL);

INSERT INTO `xxl_job_lock` (`lock_name`)
VALUES ('schedule_lock');

commit;

赋予权限

以拥有足够权限的用户登录数据库

mysql -u root

授予 ojtest 用户对 xxl_job 数据库的所有权限(包括 SELECTINSERTUPDATEDELETE 等),使用以下 SQL 语句:

GRANT ALL PRIVILEGES ON xxl_job.* TO 'ojtest'@'%';

刷新权限

FLUSH PRIVILEGES;
启动docker容器
注意: 将数据源配置为上⼀步所创建的。
docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://172.17.0.2:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=ojtest --spring.datasource.password=123456" -p 8080:8080 --name xxl-job-admin -d xuxueli/xxl-job-admin:2.4.0
进⼊调度中⼼

调度中⼼访问地址:http://localhost:8080/xxl-job-admin

默认账号密码:admin/123456

新增执行器

新增任务

配置部署“执⾏器项⽬”

引入依赖

 <dependency>
            <groupId>com.bite</groupId>
            <artifactId>oj-common-core</artifactId>
            <version>${oj-common-core.version}</version>
        </dependency>

        <dependency>
            <groupId>com.bite</groupId>
            <artifactId>oj-common-redis</artifactId>
            <version>${oj-common-redis.version}</version>
        </dependency>

        <!-- xxl-job-core -->
        <dependency>
            <groupId>com.xuxueli</groupId>
            <artifactId>xxl-job-core</artifactId>
            <version>${xxl-job.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

配置xxl-job

nacos上增加配置:

server:
  port: 9203

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/bitoj_dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
    username: ojtest
    password: 123456
  
  data:
    redis:
      host: localhost
      port: 6379
      password: 123456

xxl:
  job:
    admin:
      addresses: http://localhost:8080/xxl-job-admin
    accessToken: default_token
    executor:
      appname: ${spring.application.name}-executor

新增配置⽂件

package com.bite.job.config;

import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class XxlJobConfig {

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.appname}")
    private String appname;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        log.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        return xxlJobSpringExecutor;
    }
}

通过Bean来创建一个执行器对象,作为任务执行器的核心组件。

增加“handler”⽅法

分别将未完赛竞赛和历史竞赛从数据库中取出,然后刷新缓存


@Component
public class ExamXxlJob {
    @Autowired
    private ExamMapper examMapper;
    @Autowired
    private RedisService redisService;
    @XxlJob("examListOrganizeHandler")
    public void examListOrganizeHandler() {
        // 未完赛竞赛列表
        List<Exam> unFinishList = examMapper.selectList(new LambdaQueryWrapper<Exam>()
                .select(Exam::getExamId, Exam::getTitle, Exam::getStartTime, Exam::getEndTime)
                .gt(Exam::getEndTime, LocalDateTime.now())
                .eq(Exam::getStatus, Contants.TRUE)
                .orderByDesc(Exam::getCreateTime));
        refreshCache(CacheConstants.EXAM_UNFINISHED_LIST, unFinishList);

        // 历史竞赛列表
        List<Exam> historyExamList = examMapper.selectList(new LambdaQueryWrapper<Exam>()
                .select(Exam::getExamId, Exam::getTitle, Exam::getStartTime, Exam::getEndTime)
                .le(Exam::getEndTime, LocalDateTime.now())
                .eq(Exam::getStatus, Contants.TRUE)
                .orderByDesc(Exam::getCreateTime));
        refreshCache(CacheConstants.EXAM_HISTORY_LIST, historyExamList);
    }

    public void refreshCache(String examListKey, List<Exam> examList) {
        if (CollectionUtil.isEmpty(examList)) {
            return;
        }

        Map<String, Exam> examMap = new HashMap<>();
        List<Long> examIdList = new ArrayList<>();
        for (Exam exam : examList) {
            examMap.put(getDetailKey(exam.getExamId()), exam);
            examIdList.add(exam.getExamId());
        }
        redisService.multiSet(examMap); // 刷新详情缓存
        redisService.deleteObject(examListKey);
        redisService.rightPushAll(examListKey, examIdList); // 刷新列表缓存
    }
    private String getDetailKey(Long examId) {
        return CacheConstants.EXAM_DETAIL + examId;
    }
}

这俩个是相互对应的

启动任务

前端开发:

配置路由

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path:"/",
      redirect:'/c-oj/home/question',
    },
    {
      path: "/c-oj/home", 
      name: "home",
      component: () => import("@/views/Home.vue"),
      children: [
        {
          path: 'question',
          name: 'question',
          component: () => import('@/views/Question.vue'),
          meta: {showBanner: true}
        },
        {
          path: 'exam',
          name: 'exam',
          component: () => import('@/views/Exam.vue'),
          meta: {showBanner: true}
        },      ]
    },
    {
      path: "/c-oj/login",
      name: "login",
      component: () => import("@/views/Login.vue"),
    },
  ]
})


export default router

前后端交互请求

import service from "@/utils/request";

export function getExamListService(params) {
  return service({
    url: "/exam/semiLogin/redis/list",
    method: "get",
    params,
  });
}
export function getDbExamListService(params) {
  return service({
    url: "/exam/semiLogin/list",
    method: "get",
    params,
  });
}
}

Exam.vue(竞赛)

<template>
  <div class="exam-page flex-col">
    <div class="exam-selected-section flex-col">
      <div class="exam-option-group flex-row justify-between">
        <div class="exam-option" v-for="option in options" :key="option.value"
             @click="selectOption(option.value)"
             :class="{ selected: selectedOption === option.value }">
          {{ option.label }}
        </div>
      </div>
      <div class="center-box">
        <span class="exam-list-title">推荐竞赛</span>
        <el-form inline="true" class="exam-navigation flex-row justify-between">
          <el-form-item label="竞赛时间" prop="datetimerange" class="exam-navigation-box">
             <el-date-picker v-model="datetimeRange" style="width: 240px" type="datetimerange" range-separator="至"
        start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
    </el-form-item>
          <el-form-item>
            <el-button @click="onSearch" plain type="primary">搜索</el-button>
            <el-button @click="onReset" plain type="info">重置</el-button>
          </el-form-item>
        </el-form>
        <div class="exam-list-group flex-row">
          <div class="exam-list-item flex-col" v-for="(exam, index) in examList" :key="index">
            <div>
              <img src="@/assets/images/exam.png">
            </div>
            <div class="right-info">
              <span class="exam-title">{{ exam.title }}</span>
              <div class="exam-content flex-col justify-between">
                <span>开赛时间:{{ exam.startTime }}</span>
                <span>结束时间:{{ exam.endTime }}</span>
              </div>
            </div>
          </div>
        </div>
        <div class="exam-page-pagination flex-row">
          <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"
                         v-model:current-page="params.pageNum" v-model:page-size="params.pageSize" :page-sizes="[3, 6, 9, 12]"
                         @size-change="handleSizeChange" @current-change="handleCurrentChange" />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { reactive, ref } from 'vue'
import { getExamListService ,getDbExamListService} from '@/api/exam'

//参数定义
const examList = ref([]) //题目列表
const total = ref(0)
const selectedOption = ref(0); // 初始化选中的文本
const options = ref([
  { label: '未完赛', value: 0 },
  { label: '历史竞赛', value: 1 },
])
const params = reactive({
  pageNum: 1,
  pageSize: 9,
  startTime:'',
  endTime:'',
  title: '',
  type: 0,
})
const datetimeRange = ref([])
//竞赛列表
async function getExamList() {
    if (datetimeRange.value[0] instanceof Date) {
        console.log("startTime"+datetimeRange.value[0])
    params.startTime = datetimeRange.value[0].toISOString()
    console.log("startTime"+params.startTime)
  }
  if (datetimeRange.value[1] instanceof Date) {
    params.endTime = datetimeRange.value[1].toISOString()
  }
  console.log("最终参数"+params)
  params.type = selectedOption.value
  const result = await getExamListService(params)
  examList.value = result.rows
  total.value = result.total
}
getExamList()

async function selectOption(type) {
  selectedOption.value = type
  params.pageNum = 1
  params.type = type
  getExamList()
}
async function getDbExamList(){
     if (datetimeRange.value[0] instanceof Date) {
        console.log("startTime"+datetimeRange.value[0])
    params.startTime = datetimeRange.value[0].toISOString()
    console.log("startTime"+params.startTime)
  }
  if (datetimeRange.value[1] instanceof Date) {
    params.endTime = datetimeRange.value[1].toISOString()
  }
  params.type = selectedOption.value
  const result = await getDbExamListService(params)
  examList.value = result.rows
  total.value = result.total
}
function onSearch() {
  params.pageNum = 1
   params.pageSize = 3
  console.log(params)
  getDbExamList()
}

function onReset() {
  params.pageNum = 1
  params.pageSize = 3
  params.title = ''
  params.startTime = ''
  params.endTime = ''
  datetimeRange.value.length = 0
  getExamList()
}
// 分页
function handleSizeChange(newSize) {
  params.pageNum = 1
  getExamList()
}

function handleCurrentChange(newPage) {
  getExamList()
}
</script>

<style lang="scss" scoped>
.exam-page {
  background-color: rgba(247, 247, 247, 1);
  position: relative;
  overflow: hidden;
  display: flex;

  .center-box {
    max-width: 1520px;
    margin: 0 auto;
    width: 100%;
    min-height: 368px;
    background: #FFFFFF;
    border-radius: 16px;
    padding: 0 20px;
    padding-top: 30px;
    margin-top: 10px;
  }

  .exam-selected-section {
    margin: 0 auto;
    margin-bottom: 20px;
    position: relative;
    padding-top: 50px;
    max-width: 1520px;
    width: 100%;

    .exam-option-group {
      width: fit-content;
      height: 50px;
      position: absolute;
      top: 0;
      left: 0;

      .exam-option {
        cursor: pointer;
        padding: 10px;
        border-bottom: 2px solid transparent;
        transition: all 0.3s ease;
        font-family: PingFangSC, PingFang SC;
        font-weight: 600;
        font-size: 18px;
        color: #222;
        height: 50px;
        width: fit-content;
        display: flex;
        justify-content: center;
        align-items: center;
        margin-right: 20px;
      }

      .exam-option.selected {
        color: #32C5FF;
        border-bottom: 2px solid #32C5FF;
      }
    }

    .exam-list-title {
      height: 24px;
      font-family: PingFangSC, PingFang SC;
      font-weight: 600;
      font-size: 18px;
      color: #222222;
      line-height: 25px;
      text-align: left;
      margin-bottom: 30px;
      display: block;
    }

    :deep(.exam-navigation) {
      width: 554px;
      height: 40px;
      font-size: 100px;
      margin-bottom: 30px;

      .el-form-item {
        margin-right: 20px;
      }

      .el-form-item__label {
        background: #fff;
      }

      .exam-navigation-box {
        background-color: rgba(242, 243, 244, 1);
        border-radius: 6px;
        height: 30px;
        font-weight: 700;
      }
    }

    .exam-list-group {
      flex-wrap: wrap;

      @media screen and (min-width: 1420px) {
        .exam-list-item {
          width: 32%;

          &:nth-of-type(3n) {
            margin-right: 0;
          }
        }

      }

      @media screen and (max-width: 1419px) {
        .exam-list-item {
          width: 48%;
          margin-right: 2%;

          &:nth-of-type(2n) {
            margin-right: 0;
          }
        }
      }

      @media screen and (max-width: 970px) {
        .exam-list-item {
          width: 100%;
          margin-right: 0;
        }
      }

      .exam-list-item {
        height: 220px;
        background: #F9F9F9;
        border-radius: 10px;
        margin-right: 2%;
        margin-bottom: 20px;
        padding: 20px;
        box-sizing: border-box;
        display: flex;
        align-items: center;
        flex-direction: row;
        justify-content: space-between;
        cursor: pointer;

        .right-info {
          width: calc(100% - 146px);
        }

        .exam-title {
          height: 26px;
          font-family: PingFangSC, PingFang SC;
          font-weight: 600;
          font-size: 18px;
          color: #222222;
          line-height: 26px;
          text-align: left;
          max-width: 90%;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          margin-bottom: 16px;
          display: block;
        }

        .exam-content {
          margin-bottom: 26px;

          span {
            height: 22px;
            font-family: PingFangSC, PingFang SC;
            font-weight: 400;
            font-size: 16px;
            color: #666666;
            line-height: 22px;
            display: block;
            margin-bottom: 12px;
          }

        }

        img {
          width: 126px;
          height: 180px;
          border-radius: 6px;
        }

        &:hover {
          background: #fff;
          box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1);

          .exam-title {
            color: #32C5FF;
          }

          .el-button {
            background: #f7f7f7;
          }
        }

        .exam-hash-entry {
          float: right;
          font-size: 18px;
          font-family: PingFangSC-Regular, PingFang SC;
          font-weight: 400;
          color: #999;
        }

        .exam-button-container {
          display: flex;
          justify-content: space-between;
          align-items: center;

          .el-button {
            width: 120px;
            height: 44px;
            background: #F7F7F7;
            border-radius: 4px;
            border: 1px solid #32C5FF;
            font-family: PingFangSC, PingFang SC;
            font-weight: 400;
            font-size: 18px;
            color: #32C5FF;
            line-height: 44px;
            text-align: center;
          }
        }
      }
    }

    .exam-page-pagination {
      width: 594px;
      height: 40px;
      margin: 30px 0 73px 800px;
    }
  }
}
</style>

由于Redis难以对查询条件进行有效约束,在按时间范围查询时存在局限性,因此我们仍沿用原有的数据库查询方案。

但是为了提高效率,我们的竞赛列表初始展示以及重置功能我们还是使用Redis缓存进行查询

这样我们就解决了查询全部和按时间查询了

目前效果展示

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
 

在这里插入图片描述

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/mr_yuanshen/article/details/152884726

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--