添加获取其他题库的比赛定时任务

This commit is contained in:
howie 2021-01-15 17:49:43 +08:00
parent 1aaf5968ac
commit f9f327de75
21 changed files with 313 additions and 82 deletions

View File

@ -140,6 +140,12 @@
</exclusion>
</exclusions>
</dependency>
<!-- jsoup 爬虫库 @ https://jsoup.org/ -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
</dependencies>
</project>

View File

@ -12,7 +12,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
* @Date: 2020/10/22 23:25
* @Description:
*/
@EnableScheduling
@EnableScheduling // 开启定时任务
@EnableDiscoveryClient // 开启注册发现
@SpringBootApplication
@EnableFeignClients // 开启feign

View File

@ -13,7 +13,11 @@ import top.hcode.hoj.pojo.vo.AnnouncementVo;
import top.hcode.hoj.pojo.vo.ConfigVo;
import top.hcode.hoj.pojo.vo.ContestVo;
import top.hcode.hoj.service.impl.AnnouncementServiceImpl;
import top.hcode.hoj.utils.Constants;
import top.hcode.hoj.utils.RedisUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
@ -33,48 +37,75 @@ public class HomeController {
@Autowired
private AnnouncementServiceImpl announcementDao;
@Autowired
private RedisUtils redisUtils;
/**
* @MethodName getRecentContest
* @Params * @param null
* @Params * @param null
* @Description 获取最近14天的比赛信息列表
* @Return CommonResult
* @Since 2020/12/29
*/
@GetMapping("/get-recent-contest")
public CommonResult getRecentContest(){
public CommonResult getRecentContest() {
List<ContestVo> contests = contestDao.getWithinNext14DaysContests();
return CommonResult.successResponse(contests);
}
/**
* @MethodName getRecentOtherContest
* @Params * @param null
* @Description 获取最近其他OJ的比赛信息列表
* @Return CommonResult
* @Since 2020/1/15
*/
@GetMapping("/get-recent-other-contest")
public CommonResult getRecentOtherContest() {
String redisKey = Constants.Schedule.RECENT_OTHER_CONTEST.getCode();
List<HashMap<String, Object>> contestsList;
// 从redis获取比赛列表
contestsList = (ArrayList<HashMap<String, Object>>) redisUtils.get(redisKey);
HashMap<String, Object> resp = new HashMap<>();
// 比赛列表为空传入空列表以免前端出错
if (contestsList == null) {
resp.put("data", "");
return CommonResult.successResponse(resp);
}
resp.put("data", contestsList);
return CommonResult.successResponse(resp);
}
/**
* @MethodName getCommonAnnouncement
* @Params * @param null
* @Params * @param null
* @Description 获取主页公告列表
* @Return CommonResult
* @Since 2020/12/29
*/
@GetMapping("/get-common-announcement")
public CommonResult getCommonAnnouncement(@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "currentPage", required = false) Integer currentPage){
@RequestParam(value = "currentPage", required = false) Integer currentPage) {
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 10;
IPage<AnnouncementVo> announcementList = announcementDao.getAnnouncementList(limit, currentPage,true);
IPage<AnnouncementVo> announcementList = announcementDao.getAnnouncementList(limit, currentPage, true);
return CommonResult.successResponse(announcementList);
}
/**
* @MethodName getWebConfig
* @Params * @param null
* @Params * @param null
* @Description 获取网站的基础配置例如名字缩写名字等等
* @Return CommonResult
* @Since 2020/12/29
*/
@GetMapping("/get-website-config")
public CommonResult getWebConfig() {

View File

@ -2,4 +2,6 @@ package top.hcode.hoj.service;
public interface ScheduleService {
void deleteAvatar();
void getOjContestsList();
}

View File

@ -1,5 +1,9 @@
package top.hcode.hoj.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
@ -9,10 +13,37 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import top.hcode.hoj.pojo.entity.File;
import top.hcode.hoj.service.ScheduleService;
import top.hcode.hoj.utils.Constants;
import top.hcode.hoj.utils.JsoupUtils;
import top.hcode.hoj.utils.RedisUtils;
import java.util.LinkedList;
import java.util.List;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 一个cron表达式有至少6个也可能7个有空格分隔的时间元素按顺序依次为
* <p>
* 字段 允许值 允许的特殊字符
* 0~59 , - * /
* 0~59 , - * /
* 小时 0~23 , - * /
* 日期 1-31 , - * ? / L W C
* 月份 1~12或者JAN~DEC , - * /
* 星期 1~7或者SUN~SAT , - * ? / L C #
* 可选 留空1970~2099 , - * /
* <p>
* * 字符代表所有可能的值
* - 字符代表数字范围 例如1-5
* / 字符用来指定数值的增量
* 字符仅被用于天和天星期两个子表达式表示不指定值
* 当2个子表达式其中之一被指定了值以后为了避免冲突需要将另一个子表达式的值设为
* L 字符仅被用于天和天星期两个子表达式它是单词last的缩写
* 如果在L前有具体的内容它就具有其他的含义了
* W 字符代表着平日(Mon-Fri)并且仅能用于日域中它用来指定离指定日的最近的一个平日
* 大部分的商业处理都是基于工作周的所以 W 字符可能是非常重要的
* "C" 代表Calendar的意思它的意思是计划所关联的日期如果日期没有被关联则相当于日历中所有日期
*/
@Service
@Async
@Slf4j
@ -20,8 +51,17 @@ public class ScheduleServiceImpl implements ScheduleService {
@Autowired
private FileServiceImpl fileService;
@Autowired
private RedisUtils redisUtils;
@Scheduled(cron = "0/5 * * * * *")
/**
* @MethodName deleteAvatar
* @Params * @param null
* @Description 每天4点定时查询数据库字段并删除未引用的头像
* @Return
* @Since 2021/1/13
*/
@Scheduled(cron = "0 0 4 * * *")
@Override
public void deleteAvatar() {
List<File> files = fileService.queryDeleteAvatarList();
@ -69,6 +109,48 @@ public class ScheduleServiceImpl implements ScheduleService {
} finally {
process.destroy();
}
// del
}
/**
* 获取其他OJ的比赛列表并保存在redis里
* 保存格式
* oj: "Codeforces",
* title: "Codeforces Round #680 (Div. 1, based on VK Cup 2020-2021 - Final)",
* beginTime: "2020-11-08T05:00:00Z",
* endTime: "2020-11-08T08:00:00Z",
*/
@Scheduled(cron = "0 0 0/2 * * *")
// @Scheduled(cron = "0/5 * * * * *")
@Override
public void getOjContestsList() {
String codeforcesContestAPI = "https://codeforces.com/api/contest.list";
List<Map<String, Object>> contestsList = new ArrayList<>();
try {
JSONObject resultObject = JsoupUtils.getJsonFromConnection(JsoupUtils.getConnectionFromUrl(codeforcesContestAPI, null, null));
JSONArray contestsArray = resultObject.getJSONArray("result");
for (int i = 0; i < contestsArray.size(); i++) {
JSONObject contest = contestsArray.getJSONObject(i);
// 如果比赛已经结束了则停止获取
if ("FINISHED".equals(contest.getStr("phase", "FINISHED"))) {
break;
}
// 把比赛列表信息添加在List里
contestsList.add(MapUtil.builder(new HashMap<String, Object>())
.put("oj", "Codeforces")
.put("title", contest.getStr("name"))
.put("beginTime", new Date(contest.getLong("startTimeSeconds") * 1000L))
.put("endTime", new Date((contest.getLong("startTimeSeconds") + contest.getLong("durationSeconds")) * 1000L)).map());
}
} catch (Exception e) {
log.error("爬虫爬取Codeforces比赛异常----------------------->{}", e.getMessage());
}
// 把所有比赛列表排序将来题库变多时用到
contestsList.sort((o1, o2) -> (int) (((Date) o1.get("beginTime")).getTime() - ((Date) o2.get("beginTime")).getTime()));
// System.out.println(contestsList);
String redisKey = Constants.Schedule.RECENT_OTHER_CONTEST.getCode();
// redis缓存比赛列表时间一天
redisUtils.set(redisKey, contestsList, 60 * 60 * 24);
}
}

View File

@ -10,25 +10,24 @@ public class Constants {
* @Description 提交评测结果的状态码
* @Since 2021/1/1
*/
public enum Judge{
STATUS_NOT_SUBMITTED (-10,"Not Submitted",null),
STATUS_PRESENTATION_ERROR (-3,"Presentation Error","pe"),
STATUS_COMPILE_ERROR (-2,"Compile Error","ce"),
STATUS_WRONG_ANSWER (-1,"Wrong Answer","wa"),
STATUS_ACCEPTED (0,"Accepted","ac"),
STATUS_CPU_TIME_LIMIT_EXCEEDED (1,"CPU Time Limit Exceeded","tle"),
STATUS_REAL_TIME_LIMIT_EXCEEDED (2,"Real Time Limit Exceeded","tle"),
STATUS_MEMORY_LIMIT_EXCEEDED (3,"Memory Limit Exceeded","mle"),
STATUS_RUNTIME_ERROR (4,"Runtime Error","re"),
STATUS_SYSTEM_ERROR (5,"System Error","se"),
STATUS_PENDING (6,"Pending",null),
STATUS_JUDGING (7,"Judging",null),
STATUS_PARTIAL_ACCEPTED (8,"Partial Accepted","pa"),
STATUS_SUBMITTING (9,"Submitting",null);
public enum Judge {
STATUS_NOT_SUBMITTED(-10, "Not Submitted", null),
STATUS_PRESENTATION_ERROR(-3, "Presentation Error", "pe"),
STATUS_COMPILE_ERROR(-2, "Compile Error", "ce"),
STATUS_WRONG_ANSWER(-1, "Wrong Answer", "wa"),
STATUS_ACCEPTED(0, "Accepted", "ac"),
STATUS_CPU_TIME_LIMIT_EXCEEDED(1, "CPU Time Limit Exceeded", "tle"),
STATUS_REAL_TIME_LIMIT_EXCEEDED(2, "Real Time Limit Exceeded", "tle"),
STATUS_MEMORY_LIMIT_EXCEEDED(3, "Memory Limit Exceeded", "mle"),
STATUS_RUNTIME_ERROR(4, "Runtime Error", "re"),
STATUS_SYSTEM_ERROR(5, "System Error", "se"),
STATUS_PENDING(6, "Pending", null),
STATUS_JUDGING(7, "Judging", null),
STATUS_PARTIAL_ACCEPTED(8, "Partial Accepted", "pa"),
STATUS_SUBMITTING(9, "Submitting", null);
private Judge(Integer status, String name,String columnName) {
private Judge(Integer status, String name, String columnName) {
this.status = status;
this.name = name;
this.columnName = columnName;
@ -50,9 +49,9 @@ public class Constants {
return columnName;
}
public static String getTableColumnNameByStatus(int status){
for(Judge judge:Judge.values()){
if (judge.getStatus()==status){
public static String getTableColumnNameByStatus(int status) {
for (Judge judge : Judge.values()) {
if (judge.getStatus() == status) {
return judge.getColumnName();
}
}
@ -64,13 +63,14 @@ public class Constants {
* @Description 比赛相关的常量
* @Since 2021/1/7
*/
public enum Contest{
TYPE_ACM(0,"ACM"),
TYPE_OI(1,"OI");
public enum Contest {
TYPE_ACM(0, "ACM"),
TYPE_OI(1, "OI");
private final Integer code;
private final String name;
Contest(Integer code,String name){
Contest(Integer code, String name) {
this.code = code;
this.name = name;
}
@ -83,13 +83,13 @@ public class Constants {
return name;
}
}
/**
* @Description 账户相关常量
* @Since 2021/1/8
*/
public enum Account{
public enum Account {
CODE_CHANGE_PASSWORD_FAIL("change-password-fail:"),
CODE_CHANGE_PASSWORD_LOCK("change-password-lock:"),
CODE_ACCOUNT_LOCK("account-lock:"),
@ -97,8 +97,9 @@ public class Constants {
CODE_CHANGE_EMAIL_LOCK("change-email-lock:");
private final String code;
Account(String code){
this.code =code;
Account(String code) {
this.code = code;
}
public String getCode() {
@ -111,7 +112,7 @@ public class Constants {
* @Description 文件操作的一些常量
* @Since 2021/1/10
*/
public enum File{
public enum File {
USER_FILE_HOST("http://localhost:9010"),
USER_AVATAR_FOLDER("D:\\avatar\\"),
@ -119,7 +120,7 @@ public class Constants {
private final String path;
File(String path){
File(String path) {
this.path = path;
}
@ -127,4 +128,18 @@ public class Constants {
return path;
}
}
public enum Schedule {
RECENT_OTHER_CONTEST("recent-other-contest");
private final String code;
Schedule(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
}

View File

@ -0,0 +1,58 @@
package top.hcode.hoj.utils;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import java.io.IOException;
import java.util.Map;
public class JsoupUtils {
/**
* 获取连接
*
* @param url api网址
* @param params 请求参数
* @param headers 用户头
* @return 返回一个object
* @throws IOException
*/
public static Connection getConnectionFromUrl(String url, Map<String, String> params, Map<String, String> headers) throws IOException {
Connection connection = Jsoup.connect(url);
// 设置用户代理
connection.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36");
// 设置超时时间10秒
connection.timeout(10000);
// 设置忽略请求类型
connection.ignoreContentType(true);
// 设置请求头
if (headers != null) {
connection.headers(headers);
}
// 给url添加参数
if (params != null) {
StringBuilder sb = new StringBuilder();
sb.append(url);
if (url.indexOf("?") <= 0) {
sb.append("?");
}
for (Map.Entry<String, String> entry : params.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
url = sb.toString();
}
return connection;
}
/**
* 通过jsoup连接返回json格式
* @param connection Jsoup的connection连接
* @return
* @throws IOException
*/
public static JSONObject getJsonFromConnection(Connection connection) throws IOException {
String body = connection.execute().body();
return new JSONObject(body);
}
}

View File

@ -1,11 +1,15 @@
package top.hcode.hoj;
import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mysql.cj.protocol.PacketReceivedTimeHolder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.dao.*;
import top.hcode.hoj.pojo.entity.Contest;
import top.hcode.hoj.pojo.entity.Role;
@ -20,10 +24,13 @@ import top.hcode.hoj.service.UserRoleService;
import top.hcode.hoj.service.impl.AnnouncementServiceImpl;
import top.hcode.hoj.service.impl.UserInfoServiceImpl;
import top.hcode.hoj.service.impl.UserRoleServiceImpl;
import top.hcode.hoj.utils.Constants;
import top.hcode.hoj.utils.IpUtils;
import top.hcode.hoj.utils.JsoupUtils;
import top.hcode.hoj.utils.RedisUtils;
import java.util.LinkedList;
import java.util.List;
import java.io.IOException;
import java.util.*;
/**
* @Author: Himit_ZH
@ -54,8 +61,9 @@ public class DataBackupApplicationTests {
@Autowired
private AnnouncementServiceImpl announcementService;
@Test
public void Test1(){
public void Test1() {
// UserRolesVo roles = userRoleMapper.getUserRoles("c5ddbe4b38d641bea7d87ae0e102260d",null);
// System.out.println(roles);
// IPage<UserRolesVo> admin = userRoleService.getUserList(10, 1, "admin");
@ -81,26 +89,54 @@ public class DataBackupApplicationTests {
}
@Test
public void Test2(){
public void Test2() {
RoleAuthsVo roleAuths = roleAuthMapper.getRoleAuths(1000L);
System.out.println(roleAuths);
}
@Test
public void Test3(){
public void Test3() {
String serviceIp = IpUtils.getServiceIp();
System.out.println(serviceIp);
}
@Test
public void Test4(){
public void Test4() {
//// int todayJudgeNum = judgeMapper.getTodayJudgeNum();
// List<ContestVo> withinNext14DaysContests = contestMapper.getWithinNext14DaysContests();
// System.out.println(withinNext14DaysContests);
}
@Test
public void Test5() {
System.out.println(System.getProperty("os.name"));
public void Test5() throws IOException {
String codeforcesContestAPI = "https://codeforces.com/api/contest.list";
JSONObject resultObject = JsoupUtils.getJsonFromConnection(JsoupUtils.getConnectionFromUrl(codeforcesContestAPI, null, null));
JSONArray contestsArray = resultObject.getJSONArray("result");
for (int i = 0; i < contestsArray.size(); i++) {
JSONObject contest = contestsArray.getJSONObject(i);
// 如果比赛已经结束了则停止获取
if ("FINISHED".equals(contest.getStr("phase", "FINISHED"))) {
break;
}
System.out.println(contest.getStr("name"));
System.out.println(new Date(contest.getLong("startTimeSeconds") * 1000));
System.out.println(new Date((contest.getLong("startTimeSeconds") + contest.getLong("durationSeconds")) * 1000));
}
}
@Autowired
RedisUtils redisUtils;
@Test
public void Test6() {
String redisKey = Constants.Schedule.RECENT_OTHER_CONTEST.getCode();
List<HashMap<String, Object>> contestsList;
contestsList = (ArrayList<HashMap<String, Object>>) redisUtils.get(redisKey);
System.out.println(contestsList);
}
}

View File

@ -24,13 +24,13 @@ if (process.env.NODE_ENV == 'development') {
axios.defaults.timeout = 15000;
axios.interceptors.request.use(
config => {
// NProgress.start();
// 每次发送请求之前判断vuex中是否存在token
// 每次发送请求之前判断vuex中是否存在token
// 如果存在则统一在http请求的header都加上token这样后台根据token判断你的登录情况
// 即使本地存在token也有可能token是过期的所以在响应拦截器中要对返回状态进行判断
// 即使本地存在token也有可能token是过期的所以在响应拦截器中要对返回状态进行判断
const token = localStorage.getItem('token')
token && (config.headers.Authorization = token);
let type = config.url.split("/")[1];
@ -39,7 +39,7 @@ axios.interceptors.request.use(
}else{
config.headers['Url-Type'] = 'general'
}
return config;
},
error => {
@ -54,26 +54,26 @@ axios.interceptors.response.use(
// NProgress.done();
if(response.headers['refresh-token']){ // token续约
store.commit('changeUserToken',response.headers['authorization'])
}
}
if (response.data.status === 200 || response.data.status==undefined) {
return Promise.resolve(response);
} else {
mMessage.error(response.data.msg);
return Promise.reject(response);
}
},
// 服务器状态码不是200的情况
// 服务器状态码不是200的情况
error => {
// NProgress.done();
if (error.response) {
if(error.response.headers['refresh-token']){ // token续约
store.commit('changeUserToken',error.response.headers['authorization'])
}
}
switch (error.response.status) {
// 401: 未登录 token过期
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
// 401: 未登录 token过期
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401:
mMessage.error(error.response.data.msg);
if(error.response.config.headers['Url-Type'] === 'admin'){
@ -83,16 +83,16 @@ axios.interceptors.response.use(
}
store.commit('clearUserInfoAndToken');
break;
// 403
// 无权限访问或操作的请求
// 403
// 无权限访问或操作的请求
case 403:
mMessage.error(error.response.data.msg);
break;
// 404请求不存在
// 404请求不存在
case 404:
mMessage.error('查询错误,找不到要请求的资源!');
break;
// 其他错误,直接抛出错误提示
// 其他错误,直接抛出错误提示
default:
mMessage.error( error.response.data.msg);
break;
@ -127,7 +127,8 @@ const ojApi = {
})
},
getRecentOtherContests(){
return ajax('/api/get-recent-other-contest', 'get', {
})
},
// 用户账户的相关请求
@ -257,7 +258,7 @@ const ojApi = {
params
})
},
submissionRejudge (submitId) {
return ajax('/admin/judge/rejudge', 'get', {
params: {
@ -542,7 +543,7 @@ const adminApi = {
data
})
},
admin_getContestProblemInfo(pid,cid) {
return ajax('/admin/contest/contest-problem', 'get', {
params: {
@ -598,7 +599,7 @@ const adminApi = {
data
})
},
exportProblems (data) {
return ajax('export_problem', 'post', {
data
@ -659,7 +660,7 @@ const adminApi = {
},
}
// 集中导出oj前台的api和admin管理端的api
// 集中导出oj前台的api和admin管理端的api
let api = Object.assign(ojApi,adminApi)
export default api
/**

View File

@ -95,18 +95,18 @@ export default {
},
mounted(){
this.getRecentContests()
// this.getRecentOtherContests()
this.getRecentOtherContests()
},
methods: {
getRecentContests(){
api.getRecentContests().then(res=>{
this.contests = res.data.data
this.contests = res.data.data;
})
},
getRecentOtherContests(){
api.getRecentOtherContests().then(res=>{
this.otherContests = res.data.data
this.otherContests = res.data.data;
})
},