add private training

This commit is contained in:
Himit_ZH 2021-12-25 22:40:52 +08:00
parent c42c6ade08
commit f13330ee11
38 changed files with 462 additions and 492 deletions

View File

@ -46,4 +46,5 @@ Hcode Online Judge (HOJ) : 基于前后端分离,分布式架构的在线测
## 联系我们
QQ: [372347736](https://wpa.qq.com/msgrd?v=3&uin=372347736&site=qq&menu=yes)
HOJ交流群: 598587305
HOJ交流群: [598587305](https://qm.qq.com/cgi-bin/qm/qr?k=WWGBZ5gfDiBZOcpNvM8xnZTfUq7BT4Rs&jump_from=webapi)

View File

@ -37,20 +37,20 @@
# redis的配置
REDIS_HOST=172.20.0.2
REDIS_PORT=6379
REDIS_PASSWORD=hoj123456 # 正式部署请修改
REDIS_PASSWORD=hoj123456
# mysql的配置
MYSQL_HOST=172.20.0.3
# 如果判题服务是分布式请提供当前mysql所在服务器的公网ip
MYSQL_PUBLIC_HOST=172.20.0.3
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=hoj123456 # 正式部署请修改
MYSQL_ROOT_PASSWORD=hoj123456
# nacos的配置
NACOS_HOST=172.20.0.4
NACOS_PORT=8848
NACOS_USERNAME=root
NACOS_PASSWORD=hoj123456 # 正式部署请修改
NACOS_PASSWORD=hoj123456
# backend后端服务的配置
BACKEND_HOST=172.20.0.5
@ -82,9 +82,17 @@
JUDGE_SERVER_IP=172.20.0.7
JUDGE_SERVER_PORT=8088
JUDGE_SERVER_NAME=judger-alone
# -1表示可接收最大判题任务数为cpu核心数+1
MAX_TASK_NUM=-1
# 当前判题服务器是否开启远程虚拟判题功能
REMOTE_JUDGE_OPEN=true
# -1表示可接收最大远程判题任务数为cpu核心数*2+1
REMOTE_JUDGE_MAX_TASK_NUM=-1
# 默认沙盒并行判题程序数为cpu核心数
PARALLEL_TASK=default
# docker network的配置
SUBNET=172.20.0.0/16
# docker network的配置
SUBNET=172.20.0.0/16
```
@ -137,8 +145,8 @@ docker ps -a
- 判题并发数默认cpu核心数+1
- 默认开启vj判题需要手动修改添加账号与密码如果不添加不能vj判题
- vj判题并发数默认cpu核心数*2+1
:::
:::
**登录root账号到后台查看服务状态以及到`http://ip/admin/conf`修改服务配置!**
@ -210,20 +218,20 @@ Password: 开启SMTP服务后生成的随机授权码
# redis的配置
REDIS_HOST=172.20.0.2
REDIS_PORT=6379
REDIS_PASSWORD=hoj123456 # 正式部署请修改
REDIS_PASSWORD=hoj123456
# mysql的配置
MYSQL_HOST=172.20.0.3
# 请提供当前mysql所在服务器的公网ip
MYSQL_PUBLIC_HOST=172.20.0.3
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=hoj123456 # 正式部署请修改
MYSQL_ROOT_PASSWORD=hoj123456
# nacos的配置
NACOS_HOST=172.20.0.4
NACOS_PORT=8848
NACOS_USERNAME=root
NACOS_PASSWORD=hoj123456 # 正式部署请修改
NACOS_PASSWORD=hoj123456
# backend后端服务的配置
BACKEND_HOST=172.20.0.5
@ -253,30 +261,30 @@ Password: 开启SMTP服务后生成的随机授权码
# 评测数据同步的配置
# 请修改数据同步密码
RSYNC_PASSWORD=hoj123456 # 正式部署请修改
RSYNC_PASSWORD=hoj123456
# docker network的配置
SUBNET=172.20.0.0/16
```
配置修改保存后,当前路径下启动该服务
```shell
docker-compose up -d
```
根据网速情况,大约十分钟即可安装完毕,全程无需人工干预。
等待命令执行完毕后,查看容器状态
```shell
docker ps -a
```
当看到所有的容器的状态status都为`UP`或`healthy`就代表 OJ 已经启动成功。
当看到所有的容器的状态status都为`UP`或`healthy`就代表 OJ 已经启动成功。
4. 接着在另一台服务器上依旧git clone该文件夹下来然后进入`judgeserver`文件夹,修改`.env`的配置
```properties
@ -298,12 +306,14 @@ Password: 开启SMTP服务后生成的随机授权码
JUDGE_SERVER_IP=172.20.0.7
JUDGE_SERVER_PORT=8088
JUDGE_SERVER_NAME=judger-1
# -1表示最大并行任务数为cpu核心数+1
# -1表示可接收最大判题任务数为cpu核心数+1
MAX_TASK_NUM=-1
# 当前判题服务器是否开启远程虚拟判题功能
REMOTE_JUDGE_OPEN=true
# -1表示最大并行任务数为cpu核心数*2+1
# -1表示可接收最大远程判题任务数为cpu核心数*2+1
REMOTE_JUDGE_MAX_TASK_NUM=-1
# 默认沙盒并行判题程序数为cpu核心数
PARALLEL_TASK=default
# rsync评测数据同步的配置
# 写入主服务器ip
@ -317,7 +327,7 @@ Password: 开启SMTP服务后生成的随机授权码
```shell
docker-compose up -d
```
:::tip
提示需要开启多台判题机就如当前第4步的操作一样在每台服务器上执行以上的操作即可。
:::
:::tip
提示需要开启多台判题机就如当前第4步的操作一样在每台服务器上执行以上的操作即可。
:::
5. 两个服务都启动完成在浏览器输入主服务ip或域名进行访问登录root账号到后台查看服务状态。

View File

@ -76,35 +76,37 @@
JUDGE_SERVER_IP=172.20.0.7
JUDGE_SERVER_PORT=8088
JUDGE_SERVER_NAME=judger-1
# -1表示最大并行任务数为cpu核心数*2
# -1表示可接收最大判题任务数为cpu核心数+1
MAX_TASK_NUM=-1
# 当前判题服务器是否开启远程虚拟判题功能
REMOTE_JUDGE_OPEN=true
# -1表示最大并行任务数为(cpu核心数*2)*2
# -1表示可接收最大远程判题任务数为cpu核心数*2+1
REMOTE_JUDGE_MAX_TASK_NUM=-1
# 默认沙盒并行判题程序数为cpu核心数
PARALLEL_TASK=default
# rsync评测数据同步的配置
# 写入主服务器ip
RSYNC_MASTER_ADDR=127.0.0.1
# 与主服务器的rsync密码一致
RSYNC_PASSWORD=hoj123456
RSYNC_PASSWORD=hoj123456
```
3. 启动即可
```shell
docker-compose up -d
```
4. 验证:
```
访问 http://ip:8088/version
如果返回信息正常即启动成功!
如果返回信息正常即启动成功!
```
@ -135,12 +137,14 @@
JUDGE_SERVER_IP=172.20.0.7
JUDGE_SERVER_PORT=8088
JUDGE_SERVER_NAME=judger-1
# -1表示最大并行任务数为cpu核心数*2
# -1表示可接收最大判题任务数为cpu核心数+1
MAX_TASK_NUM=-1
# 当前判题服务器是否开启远程虚拟判题功能
REMOTE_JUDGE_OPEN=true
# -1表示最大并行任务数为(cpu核心数*2)*2
# -1表示可接收最大远程判题任务数为cpu核心数*2+1
REMOTE_JUDGE_MAX_TASK_NUM=-1
# 默认沙盒并行判题程序数为cpu核心数
PARALLEL_TASK=default
# rsync评测数据同步的配置
# 写入主服务器ip

View File

@ -196,9 +196,10 @@ services:
- NACOS_URL=172.20.0.4:8848 # nacos的url
- NACOS_USERNAME=nacos # nacos的管理员账号
- NACOS_PASSWORD=nacos # naocs的管理员账号密码
- MAX_TASK_NUM=-1 # -1表示最大并行任务数为cpu核心数+1
- MAX_TASK_NUM=-1 # -1表示最大可接收判题任务数为cpu核心数+1
- REMOTE_JUDGE_OPEN=true # 当前判题服务器是否开启远程虚拟判题功能
- REMOTE_JUDGE_MAX_TASK_NUM=-1 # -1表示最大并行任务数为cpu核心数*2+1
- REMOTE_JUDGE_MAX_TASK_NUM=-1 # -1表示最大可接收远程判题任务数为cpu核心数*2+1
- PARALLEL_TASK=default # 默认沙盒并行判题程序数为cpu核心数
ports:
- "0.0.0.0:8088:8088"
# - "0.0.0.0:5050:5050" # 一般不开放安全沙盒端口
@ -245,11 +246,26 @@ bash ./run.sh
启动judgesever的springboot jar包 和SandBox判题安全沙盒
```shell
ulimit -s unlimited
chmod +777 SandBox
nohup ./SandBox -release=true &
if test -z "$PARALLEL_TASK";then
nohup ./SandBox --silent=true --file-timeout=10m &
echo -e "\033[42;34m ./SandBox --silent=true --file-timeout=10m \033[0m"
elif [ -z "$(echo $PARALLEL_TASK | sed 's#[0-9]##g')" ]; then
nohup ./SandBox --silent=true --file-timeout=10m --parallelism=$PARALLEL_TASK &
echo -e "\033[42;34m ./SandBox --silent=true --file-timeout=10m --parallelism=$PARALLEL_TASK \033[0m"
else
nohup ./SandBox --silent=true --file-timeout=10m &
echo -e "\033[42;34m ./SandBox --silent=true --file-timeout=10m \033[0m"
fi
java -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom -jar ./app.jar
if test -z "$JAVA_OPTS";then
java -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom -jar ./app.jar
else
java -XX:+UseG1GC $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar ./app.jar
fi
```
### 4. Dockerfile

View File

@ -24,6 +24,7 @@ import top.hcode.hoj.pojo.dto.ProblemDto;
import top.hcode.hoj.pojo.entity.contest.Contest;
import top.hcode.hoj.pojo.entity.contest.ContestAnnouncement;
import top.hcode.hoj.pojo.entity.contest.ContestProblem;
import top.hcode.hoj.pojo.entity.contest.ContestRegister;
import top.hcode.hoj.pojo.entity.judge.Judge;
import top.hcode.hoj.pojo.entity.problem.Problem;
import top.hcode.hoj.pojo.vo.AdminContestVo;
@ -32,6 +33,7 @@ import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.common.impl.AnnouncementServiceImpl;
import top.hcode.hoj.service.contest.impl.ContestAnnouncementServiceImpl;
import top.hcode.hoj.service.contest.impl.ContestProblemServiceImpl;
import top.hcode.hoj.service.contest.impl.ContestRegisterServiceImpl;
import top.hcode.hoj.service.contest.impl.ContestServiceImpl;
import top.hcode.hoj.service.judge.impl.JudgeServiceImpl;
import top.hcode.hoj.service.problem.impl.ProblemServiceImpl;
@ -41,10 +43,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.*;
/**
@ -59,6 +58,9 @@ public class AdminContestController {
@Autowired
private ContestServiceImpl contestService;
@Autowired
private ContestRegisterServiceImpl contestRegisterService;
@Autowired
private ProblemServiceImpl problemService;
@ -125,9 +127,9 @@ public class AdminContestController {
return CommonResult.errorResponse("对不起,你无权限操作!", CommonResult.STATUS_FORBIDDEN);
}
AdminContestVo adminContestVo = BeanUtil.copyProperties(contest, AdminContestVo.class, "starAccount");
if (StringUtils.isEmpty(contest.getStarAccount())){
if (StringUtils.isEmpty(contest.getStarAccount())) {
adminContestVo.setStarAccount(new ArrayList<>());
}else {
} else {
JSONObject jsonObject = JSONUtil.parseObj(contest.getStarAccount());
List<String> starAccount = jsonObject.get("star_account", List.class);
adminContestVo.setStarAccount(starAccount);
@ -169,6 +171,7 @@ public class AdminContestController {
@PutMapping("")
@RequiresAuthentication
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
@Transactional(rollbackFor = Exception.class)
public CommonResult updateContest(@RequestBody AdminContestVo adminContestVo, HttpServletRequest request) {
// 获取当前登录的用户
@ -184,8 +187,16 @@ public class AdminContestController {
JSONObject accountJson = new JSONObject();
accountJson.set("star_account", adminContestVo.getStarAccount());
contest.setStarAccount(accountJson.toString());
Contest oldContest = contestService.getById(contest.getId());
boolean result = contestService.saveOrUpdate(contest);
if (result) {
if (!contest.getAuth().equals(Constants.Contest.AUTH_PUBLIC.getCode())) {
if (!Objects.equals(oldContest.getPwd(), contest.getPwd())) { // 改了比赛密码则需要删掉已有的注册比赛用户
UpdateWrapper<ContestRegister> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("cid", contest.getId());
contestRegisterService.remove(updateWrapper);
}
}
return CommonResult.successResponse(null, "修改成功!");
} else {
return CommonResult.errorResponse("修改失败", CommonResult.STATUS_FAIL);

View File

@ -71,7 +71,8 @@ public class AdminProblemController {
IPage<Problem> problemList;
QueryWrapper<Problem> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("gmt_create");
queryWrapper.orderByDesc("gmt_create")
.orderByDesc("id");
// 根据oj筛选过滤
if (oj != null && !"All".equals(oj)) {

View File

@ -140,7 +140,6 @@ public class AdminTrainingController {
if (!isRoot && !userRolesVo.getUsername().equals(trainingDto.getTraining().getAuthor())) {
return CommonResult.errorResponse("对不起,你无权限操作!", CommonResult.STATUS_FORBIDDEN);
}
boolean result = trainingService.updateTraining(trainingDto);
if (result) {
trainingRecordService.checkSyncRecord(trainingDto.getTraining());

View File

@ -315,10 +315,23 @@ public class AccountController {
return CommonResult.errorResponse("用户名长度不能超过20位!");
}
String userIpAddr = IpUtils.getUserIpAddr(request);
String key = Constants.Account.TRY_LOGIN_NUM.getCode() + loginDto.getUsername() + "_" + userIpAddr;
Integer tryLoginCount = (Integer) redisUtils.get(key);
if (tryLoginCount != null && tryLoginCount >= 20) {
return CommonResult.errorResponse("对不起!登录失败次数过多!您的账号有风险,半个小时内暂时无法登录!", CommonResult.STATUS_FORBIDDEN);
}
UserRolesVo userRoles = userRoleDao.getUserRoles(null, loginDto.getUsername());
Assert.notNull(userRoles, "用户名不存在,请注意大小写!");
Assert.notNull(userRoles, "用户名或密码错误!请注意大小写!");
if (!userRoles.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {
return CommonResult.errorResponse("密码不正确");
if (tryLoginCount == null) {
redisUtils.set(key, 1, 60 * 30); // 三十分钟不尝试该限制会自动清空消失
} else {
redisUtils.set(key, tryLoginCount + 1, 60 * 30);
}
return CommonResult.errorResponse("用户名或密码错误!请注意大小写!");
}
if (userRoles.getStatus() != 0) {
@ -334,6 +347,12 @@ public class AccountController {
.setUid(userRoles.getUid())
.setIp(IpUtils.getUserIpAddr(request))
.setUserAgent(request.getHeader("User-Agent")));
// 登录成功清除锁定限制
if (tryLoginCount != null) {
redisUtils.del(key);
}
// 异步检查是否异地登录
sessionService.checkRemoteLogin(userRoles.getUid());

View File

@ -262,8 +262,7 @@ public class ContestController {
List<ContestProblemVo> contestProblemList;
boolean isAdmin = isRoot || contest.getAuthor().equals(userRolesVo.getUsername());
// 如果比赛开启封榜
if (contest.getSealRank() && contest.getStatus().intValue() == Constants.Contest.STATUS_RUNNING.getCode() &&
contest.getSealRankTime().before(new Date())) {
if (contestService.isSealRank(userRolesVo.getUid(), contest, false, isRoot)) {
contestProblemList = contestProblemService.getContestProblemList(cid, contest.getStartTime(), contest.getEndTime(),
contest.getSealRankTime(), isAdmin, contest.getAuthor());
} else {
@ -355,14 +354,9 @@ public class ContestController {
tmpMap.put(language.getId(), language.getName());
});
Date now = new Date();
Date sealRankTime = null;
//封榜时间除超级管理员和比赛管理员外 其它人不可看到最新数据
if (contest.getSealRank() &&
!isRoot &&
!userRolesVo.getUid().equals(contest.getUid()) &&
contest.getStatus().intValue() == Constants.Contest.STATUS_RUNNING.getCode() &&
contest.getSealRankTime().before(now)) {
if (contestService.isSealRank(userRolesVo.getUid(), contest, false, isRoot)) {
sealRankTime = contest.getSealRankTime();
}
@ -440,15 +434,11 @@ public class ContestController {
}
Date sealRankTime = null;
// 不是比赛管理员和超级管理又有开启封榜需要判断是否处于封榜期间
if (contest.getSealRank() && !isRoot && !userRolesVo.getUid().equals(contest.getUid())) {
// 当前是比赛期间 同时处于封榜时间
if (contest.getStatus().intValue() == Constants.Contest.STATUS_RUNNING.getCode()
&& contest.getSealRankTime().before(new Date())) {
sealRankTime = contest.getSealRankTime();
}
// 需要判断是否需要封榜
if (contestService.isSealRank(userRolesVo.getUid(), contest, false, isRoot)) {
sealRankTime = contest.getSealRankTime();
}
// OI比赛封榜期间不更新ACM比赛封榜期间可看到自己的提交但是其它人的不更新
// OI比赛封榜期间不更新ACM比赛封榜期间可看到自己的提交但是其它人的不可见
IPage<JudgeVo> commonJudgeList = judgeService.getContestJudgeList(limit, currentPage, displayId, searchCid,
searchStatus, searchUsername, uid, beforeContestSubmit, rule, contest.getStartTime(),
sealRankTime, userRolesVo.getUid(), completeProblemID);

View File

@ -268,20 +268,9 @@ public class JudgeController {
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
boolean root = SecurityUtils.getSubject().hasRole("root"); // 是否为超级管理员
boolean isRoot = SecurityUtils.getSubject().hasRole("root"); // 是否为超级管理员
HashMap<String, Object> result = new HashMap<>();
Contest contest = null;
if (judge.getCid() != 0 && !root) {
contest = contestService.getById(judge.getCid());
if (userRolesVo != null && !userRolesVo.getUid().equals(contest.getUid()) && contest.getSealRank()
&& contest.getType().intValue() == Constants.Contest.TYPE_OI.getCode()
&& contest.getStatus().intValue() == Constants.Contest.STATUS_RUNNING.getCode()
&& contest.getSealRankTime().before(new Date())) {
result.put("submission", new Judge().setStatus(Constants.Judge.STATUS_SUBMITTED_UNKNOWN_RESULT.getStatus()));
return CommonResult.successResponse(result, "获取提交数据成功!");
}
}
// 清空vj信息
judge.setVjudgeUsername(null);
judge.setVjudgeSubmitId(null);
@ -291,16 +280,16 @@ public class JudgeController {
// 如果不是本人或者并未分享代码则不可查看
// 当此次提交代码不共享
// 比赛提交只有比赛创建者和root账号可看代码
HashMap<String, Object> result = new HashMap<>();
if (judge.getCid() != 0) {
if (contest == null) {
contest = contestService.getById(judge.getCid());
if (userRolesVo == null) {
return CommonResult.errorResponse("请先登录!", CommonResult.STATUS_ACCESS_DENIED);
}
if (userRolesVo != null && !root && !userRolesVo.getUid().equals(contest.getUid())) {
Contest contest = contestService.getById(judge.getCid());
if (!isRoot && !userRolesVo.getUid().equals(contest.getUid())) {
// 如果是比赛,那么还需要判断是否为封榜,比赛管理员和超级管理员可以有权限查看(ACM题目除外)
if (contest.getSealRank()
&& contest.getType().intValue() == Constants.Contest.TYPE_OI.getCode()
&& contest.getStatus().intValue() == Constants.Contest.STATUS_RUNNING.getCode()
&& contest.getSealRankTime().before(new Date())) {
if (contest.getType().intValue() == Constants.Contest.TYPE_OI.getCode()
&& contestService.isSealRank(userRolesVo.getUid(), contest, false, false)) {
result.put("submission", new Judge().setStatus(Constants.Judge.STATUS_SUBMITTED_UNKNOWN_RESULT.getStatus()));
return CommonResult.successResponse(result, "获取提交数据成功!");
}
@ -318,7 +307,7 @@ public class JudgeController {
}
} else {
boolean admin = SecurityUtils.getSubject().hasRole("problem_admin");// 是否为题目管理员
if (!judge.getShare() && !root && !admin) {
if (!judge.getShare() && !isRoot && !admin) {
if (userRolesVo != null) { // 当前是登陆状态
// 需要判断是否为当前登陆用户自己的提交代码
if (!judge.getUid().equals(userRolesVo.getUid())) {

View File

@ -136,10 +136,18 @@ public class ProblemController {
HashMap<Long, Object> result = new HashMap<>();
// 先查询判断该用户对于这些题是否已经通过若已通过则无论后续再提交结果如何该题都标记为通过
QueryWrapper<Judge> queryWrapper = new QueryWrapper<>();
queryWrapper.select("distinct pid,status,submit_time,score").in("pid", pidListDto.getPidList())
// 如果是比赛的提交记录需要判断cid
.eq(pidListDto.getIsContestProblemList(), "cid", pidListDto.getCid())
.eq("uid", userRolesVo.getUid()).orderByDesc("submit_time");
queryWrapper.select("distinct pid,status,submit_time,score")
.in("pid", pidListDto.getPidList())
.eq("uid", userRolesVo.getUid())
.orderByDesc("submit_time");
if (pidListDto.getIsContestProblemList()) {
// 如果是比赛的提交记录需要判断cid
queryWrapper.eq("cid", pidListDto.getCid());
} else {
queryWrapper.eq("cid", 0);
}
List<Judge> judges = judgeService.list(queryWrapper);
boolean isACMContest = true;
@ -162,11 +170,8 @@ public class ProblemController {
if (!result.containsKey(judge.getPid())) { // IO比赛的如果还未写入则使用最新一次提交的结果
// 判断该提交是否为封榜之后的提交,OI赛制封榜后的提交看不到提交结果
// 只有比赛结束可以看到,比赛管理员与超级管理员的提交除外
if (contest.getSealRank() &&
!contest.getUid().equals(userRolesVo.getUid()) &&
!SecurityUtils.getSubject().hasRole("root") &&
contest.getStatus().intValue() == Constants.Contest.STATUS_RUNNING.getCode() &&
judge.getSubmitTime().after(contest.getSealRankTime())) {
if (contestService.isSealRank(userRolesVo.getUid(), contest, false,
SecurityUtils.getSubject().hasRole("root"))) {
temp.put("status", Constants.Judge.STATUS_SUBMITTED_UNKNOWN_RESULT.getStatus());
temp.put("score", null);
} else {

View File

@ -7,6 +7,7 @@ import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.pojo.entity.contest.Contest;
import top.hcode.hoj.pojo.entity.training.Training;
import top.hcode.hoj.pojo.entity.training.TrainingCategory;
import top.hcode.hoj.pojo.entity.training.TrainingRegister;
@ -179,8 +180,16 @@ public class TrainingController {
QueryWrapper<TrainingRegister> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("tid", tid).eq("uid", userRolesVo.getUid());
TrainingRegister trainingRegister = trainingRegisterService.getOne(queryWrapper, false);
boolean access = false;
if (trainingRegister != null) {
access = true;
Training training = trainingService.getById(tid);
if (training == null || !training.getStatus()) {
return CommonResult.errorResponse("对不起,该训练不存在!");
}
}
HashMap<String, Object> result = new HashMap<>();
result.put("access", trainingRegister != null);
result.put("access", access);
return CommonResult.successResponse(result);
}
@ -215,7 +224,7 @@ public class TrainingController {
// 页数每页数若为空设置默认值
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 30;
IPage<TrainingRankVo> trainingRankPager = trainingRecordService.getTrainingRank(tid, currentPage, limit);
IPage<TrainingRankVo> trainingRankPager = trainingRecordService.getTrainingRank(tid, training.getAuthor(), currentPage, limit);
return CommonResult.successResponse(trainingRankPager, "success");
}

View File

@ -2,7 +2,7 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.hcode.hoj.dao.TrainingRecordMapper">
<select id="getTrainingRecord" resultType="top.hcode.hoj.pojo.vo.TrainingRecordVo">
SELECT tr.tid,tr.uid,tr.pid,tr.tpid,tr.submit_id,j.status,j.score,j.use_time,
SELECT tr.tid,tr.uid,tr.pid,tr.tpid,tr.submit_id,j.status,j.score,j.time as use_time,
u.gender,u.realname as realname,u.username,u.avatar,u.school,u.nickname
FROM training_record tr,user_info u,judge j
WHERE tr.uid = u.uuid

View File

@ -36,9 +36,6 @@ public class TrainingRankVo {
@ApiModelProperty(value = "头像")
private String avatar;
@ApiModelProperty(value = "总提交数")
private Integer total;
@ApiModelProperty(value = "ac题目数")
private Integer ac;

View File

@ -97,11 +97,12 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, Contest> impl
return false;
} else if (contest.getSealRank() && contest.getSealRankTime() != null) { // 该比赛开启封榜模式
Date now = new Date();
// 如果现在时间处于封榜开始到比赛结束之间或者没有开启自动解除封榜不可刷新榜单
if ((now.after(contest.getSealRankTime()) && now.before(contest.getEndTime()))
|| !contest.getAutoRealRank()) {
// 如果现在时间处于封榜开始到比赛结束之间
if (now.after(contest.getSealRankTime()) && now.before(contest.getEndTime())) {
return true;
}
// 或者没有开启赛后自动解除封榜不可刷新榜单
return !contest.getAutoRealRank() && now.after(contest.getEndTime());
}
return false;
}

View File

@ -21,7 +21,7 @@ public interface TrainingRecordService extends IService<TrainingRecord> {
public CommonResult submitTrainingProblem(ToJudgeDto judgeDto, UserRolesVo userRolesVo, Judge judge);
public IPage<TrainingRankVo> getTrainingRank(Long tid, int currentPage, int limit);
public IPage<TrainingRankVo> getTrainingRank(Long tid,String username, int currentPage, int limit);
public void syncUserSubmissionToRecordByTid(Long tid, String uid);

View File

@ -5,12 +5,14 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.tomcat.util.bcel.Const;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.dao.TrainingRecordMapper;
import top.hcode.hoj.dao.UserInfoMapper;
import top.hcode.hoj.pojo.dto.ToJudgeDto;
import top.hcode.hoj.pojo.entity.contest.Contest;
import top.hcode.hoj.pojo.entity.contest.ContestProblem;
@ -19,6 +21,7 @@ import top.hcode.hoj.pojo.entity.problem.Problem;
import top.hcode.hoj.pojo.entity.training.Training;
import top.hcode.hoj.pojo.entity.training.TrainingProblem;
import top.hcode.hoj.pojo.entity.training.TrainingRecord;
import top.hcode.hoj.pojo.entity.user.UserInfo;
import top.hcode.hoj.pojo.vo.TrainingRankVo;
import top.hcode.hoj.pojo.vo.TrainingRecordVo;
import top.hcode.hoj.pojo.vo.UserRolesVo;
@ -44,8 +47,8 @@ public class TrainingRecordServiceImpl extends ServiceImpl<TrainingRecordMapper,
@Resource
private TrainingProblemServiceImpl trainingProblemService;
@Resource
private RedisUtils redisUtils;
@Autowired
private UserInfoMapper userInfoMapper;
@Resource
private TrainingRecordMapper trainingRecordMapper;
@ -109,16 +112,27 @@ public class TrainingRecordServiceImpl extends ServiceImpl<TrainingRecordMapper,
}
@Override
public IPage<TrainingRankVo> getTrainingRank(Long tid, int currentPage, int limit) {
public IPage<TrainingRankVo> getTrainingRank(Long tid, String username, int currentPage, int limit) {
Map<Long, String> tpIdMapDisplayId = getTPIdMapDisplayId(tid);
List<TrainingRecordVo> trainingRecordVoList = trainingRecordMapper.getTrainingRecord(tid);
List<UserInfo> superAdminList = userInfoMapper.getSuperAdminList();
List<String> superAdminUidList = superAdminList.stream().map(UserInfo::getUuid).collect(Collectors.toList());
List<TrainingRankVo> result = new ArrayList<>();
HashMap<String, Integer> uidMapIndex = new HashMap<>();
int pos = 0;
for (TrainingRecordVo trainingRecordVo : trainingRecordVoList) {
// 超级管理员和训练创建者的提交不入排行榜
if (username.equals(trainingRecordVo.getUsername())
|| superAdminUidList.contains(trainingRecordVo.getUid())) {
continue;
}
TrainingRankVo trainingRankVo;
Integer index = uidMapIndex.get(trainingRecordVo.getUid());
if (index == null) {
@ -131,8 +145,7 @@ public class TrainingRecordServiceImpl extends ServiceImpl<TrainingRecordMapper,
.setUsername(trainingRecordVo.getUsername())
.setNickname(trainingRecordVo.getNickname())
.setAc(0)
.setTotalRunTime(0)
.setTotal(0);
.setTotalRunTime(0);
HashMap<String, HashMap<String, Object>> submissionInfo = new HashMap<>();
trainingRankVo.setSubmissionInfo(submissionInfo);
@ -146,7 +159,6 @@ public class TrainingRecordServiceImpl extends ServiceImpl<TrainingRecordMapper,
HashMap<String, Object> problemSubmissionInfo = trainingRankVo
.getSubmissionInfo()
.getOrDefault(displayId, new HashMap<>());
trainingRankVo.setTotal(trainingRankVo.getTotal() + 1);
// 如果该题目已经AC过了只比较运行时间取最小
if ((Boolean) problemSubmissionInfo.getOrDefault("isAC", false)) {
@ -294,18 +306,11 @@ public class TrainingRecordServiceImpl extends ServiceImpl<TrainingRecordMapper,
saveBatchNewRecordByJudgeList(judgeList, tid, null, pidMapTPid);
}
@SuppressWarnings("unchecked")
private Map<Long, String> getTPIdMapDisplayId(Long tid) {
String key = Constants.Training.MAP_REDIS_KEY_PRE.getValue() + tid;
Map<Long, String> res = (Map<Long, String>) redisUtils.get(key);
if (res == null) {
QueryWrapper<TrainingProblem> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("tid", tid);
List<TrainingProblem> trainingProblemList = trainingProblemService.list(queryWrapper);
res = trainingProblemList.stream().collect(Collectors.toMap(TrainingProblem::getId, TrainingProblem::getDisplayId));
redisUtils.set(key, res, 12 * 3600);
}
return res;
QueryWrapper<TrainingProblem> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("tid", tid);
List<TrainingProblem> trainingProblemList = trainingProblemService.list(queryWrapper);
return trainingProblemList.stream().collect(Collectors.toMap(TrainingProblem::getId, TrainingProblem::getDisplayId));
}
}

View File

@ -3,6 +3,7 @@ package top.hcode.hoj.service.training.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import top.hcode.hoj.common.result.CommonResult;
@ -35,6 +36,7 @@ public class TrainingRegisterServiceImpl extends ServiceImpl<TrainingRegisterMap
private TrainingServiceImpl trainingService;
@Resource
@Lazy
private TrainingRecordServiceImpl trainingRecordService;
@Override

View File

@ -11,19 +11,23 @@ import org.springframework.transaction.annotation.Transactional;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.dao.MappingTrainingCategoryMapper;
import top.hcode.hoj.dao.TrainingMapper;
import top.hcode.hoj.dao.TrainingRegisterMapper;
import top.hcode.hoj.pojo.dto.TrainingDto;
import top.hcode.hoj.pojo.entity.training.MappingTrainingCategory;
import top.hcode.hoj.pojo.entity.training.Training;
import top.hcode.hoj.pojo.entity.training.TrainingCategory;
import top.hcode.hoj.pojo.entity.training.TrainingRegister;
import top.hcode.hoj.pojo.vo.TrainingVo;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.training.TrainingService;
import top.hcode.hoj.utils.Constants;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* @Author: Himit_ZH
@ -42,6 +46,9 @@ public class TrainingServiceImpl extends ServiceImpl<TrainingMapper, Training> i
@Resource
private MappingTrainingCategoryMapper mappingTrainingCategoryMapper;
@Resource
private TrainingRegisterMapper trainingRegisterMapper;
@Override
public IPage<TrainingVo> getTrainingList(int limit, int currentPage, Long categoryId, String auth, String keyword) {
List<TrainingVo> trainingList = trainingMapper.getTrainingList(categoryId, auth, keyword);
@ -87,7 +94,19 @@ public class TrainingServiceImpl extends ServiceImpl<TrainingMapper, Training> i
public boolean updateTraining(TrainingDto trainingDto) {
Training training = trainingDto.getTraining();
Training oldTraining = trainingMapper.selectById(training.getId());
trainingMapper.updateById(training);
// 私有训练 修改密码 需要清空之前注册训练的记录
if (training.getAuth().equals(Constants.Training.AUTH_PRIVATE.getValue())) {
if (!Objects.equals(training.getPrivatePwd(), oldTraining.getPrivatePwd())) {
UpdateWrapper<TrainingRegister> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("tid", training.getId());
trainingRegisterMapper.delete(updateWrapper);
}
}
TrainingCategory trainingCategory = trainingDto.getTrainingCategory();
if (trainingCategory.getId() == null) {
try {

View File

@ -265,9 +265,7 @@ public class Constants {
public enum Training {
AUTH_PRIVATE("Private"),
AUTH_PUBLIC("Public"),
MAP_REDIS_KEY_PRE("training_TPId_map_DisplayId:");
AUTH_PUBLIC("Public");
private final String value;

View File

@ -368,13 +368,6 @@ const ojApi = {
params: {displayId,cid}
})
},
// 获取训练提交列表
getTrainingSubmissionList (limit, params) {
params.limit = limit
return ajax('/api/submissions', 'get', {
params
})
},
// 获取训练记录榜单
getTrainingRank(params){
return ajax('/api/get-training-rank', 'get', {

View File

@ -98,7 +98,7 @@ const adminRoutes= [
},
{
path: 'training/create',
name: 'admin-training-contest',
name: 'admin-create-training',
component: Training,
meta: { title:'Create Training'},
},

View File

@ -31,6 +31,7 @@ import SysMsg from "@/views/oj/message/SysMsg.vue"
import TrainingList from "@/views/oj/training/TrainingList.vue"
import TrainingDetails from "@/views/oj/training/TrainingDetails.vue"
import TrainingProblemList from "@/views/oj/training/TrainingProblemList.vue"
import TrainingRank from "@/views/oj/training/TrainingRank.vue"
import NotFound from "@/views/404.vue"
const ojRoutes = [
@ -82,6 +83,12 @@ const ojRoutes = [
component: Problem,
meta: { title: 'Training Problem Details' }
},
{
name: 'TrainingRank',
path: 'rank',
component: TrainingRank,
meta: { title: 'Training Rank' }
}
]
},
{

View File

@ -200,7 +200,7 @@ export default {
return;
}
if (this.training.auth != 'Public' && !this.training.pwd) {
if (this.training.auth != 'Public' && !this.training.privatePwd) {
myMessage.error(
this.$i18n.t('m.Training_Password') +
' ' +

View File

@ -14,6 +14,15 @@
@keyup.enter.native="filterByKeyword"
></vxe-input>
</span>
<span>
<el-button
type="primary"
size="small"
@click="goCreateTraining"
icon="el-icon-plus"
>{{ $t('m.Create') }}
</el-button>
</span>
</div>
</div>
<vxe-table
@ -209,6 +218,9 @@ export default {
filterByKeyword() {
this.currentChange(1);
},
goCreateTraining() {
this.$router.push({ name: 'admin-create-training' });
},
},
};
</script>

View File

@ -108,7 +108,7 @@
max-height="500px"
:loading="loading.recent7ACRankLoading"
>
<vxe-table-column type="seq" min-width="30">
<vxe-table-column type="seq" min-width="50">
<template v-slot="{ rowIndex }">
<span :class="getRankTagClass(rowIndex)"
>{{ rowIndex + 1 }}
@ -119,7 +119,7 @@
<vxe-table-column
field="username"
:title="$t('m.Username')"
min-width="130"
min-width="100"
align="left"
>
<template v-slot="{ row }">
@ -141,7 +141,7 @@
<vxe-table-column
field="ac"
:title="$t('m.AC')"
min-width="30"
min-width="50"
align="left"
>
</vxe-table-column>

View File

@ -488,8 +488,7 @@ export default {
}
}
},
applyToTable(data) {
let dataRank = JSON.parse(JSON.stringify(data));
applyToTable(dataRank) {
dataRank.forEach((rank, i) => {
let info = rank.submissionInfo;
let cellClass = {};

View File

@ -14,32 +14,32 @@
field="status"
title=""
width="50"
v-if="
isAuthenticated && isGetStatusOk && contestRuleType == RULE_TYPE.OI
"
v-if="isAuthenticated && contestRuleType == RULE_TYPE.OI"
>
<template v-slot="{ row }">
<span :class="getScoreColor(row.score)" v-if="row.score != null">{{
row.score
}}</span>
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
v-else-if="row.myStatus == -5"
>
<i class="fa fa-question" :style="getIconColor(row.myStatus)"></i>
</el-tooltip>
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
v-else
>
<i
class="el-icon-minus"
:style="getIconColor(row.myStatus)"
v-if="row.myStatus != -10"
></i>
</el-tooltip>
<template v-if="isGetStatusOk">
<span :class="getScoreColor(row.score)" v-if="row.score != null">{{
row.score
}}</span>
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
v-else-if="row.myStatus == -5"
>
<i class="fa fa-question" :style="getIconColor(row.myStatus)"></i>
</el-tooltip>
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
v-else
>
<i
class="el-icon-minus"
:style="getIconColor(row.myStatus)"
v-if="row.myStatus != -10"
></i>
</el-tooltip>
</template>
</template>
</vxe-table-column>
@ -48,26 +48,26 @@
field="status"
title=""
width="50"
v-if="
isAuthenticated && isGetStatusOk && contestRuleType == RULE_TYPE.ACM
"
v-if="isAuthenticated && contestRuleType == RULE_TYPE.ACM"
>
<template v-slot="{ row }">
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
>
<i
class="el-icon-check"
:style="getIconColor(row.myStatus)"
v-if="row.myStatus == 0"
></i>
<i
class="el-icon-minus"
:style="getIconColor(row.myStatus)"
v-else-if="row.myStatus != -10"
></i>
</el-tooltip>
<template v-if="isGetStatusOk">
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
>
<i
class="el-icon-check"
:style="getIconColor(row.myStatus)"
v-if="row.myStatus == 0"
></i>
<i
class="el-icon-minus"
:style="getIconColor(row.myStatus)"
v-else-if="row.myStatus != -10"
></i>
</el-tooltip>
</template>
</template>
</vxe-table-column>
<vxe-table-column field="displayId" width="80" title="#">

View File

@ -455,8 +455,7 @@ export default {
this.options.xAxis[0].data = user;
this.options.series[0].data = scores;
},
applyToTable(data) {
let dataRank = JSON.parse(JSON.stringify(data));
applyToTable(dataRank) {
dataRank.forEach((rank, i) => {
let info = rank.submissionInfo;
let cellClass = {};

View File

@ -387,8 +387,7 @@ export default {
}
}
},
applyToTable(data) {
let dataRank = JSON.parse(JSON.stringify(data));
applyToTable(dataRank) {
let acCountMap = {};
let errorCountMap = {};
dataRank.forEach((rank, i) => {

View File

@ -349,8 +349,7 @@ export default {
query: { username: username, uid: uid },
});
},
applyToTable(data) {
let dataRank = JSON.parse(JSON.stringify(data));
applyToTable(dataRank) {
let acCountMap = {};
let errorCountMap = {};
dataRank.forEach((rank, i) => {

View File

@ -117,27 +117,28 @@
@cell-mouseenter="cellHover"
:data="problemList"
>
<vxe-table-column
title=""
width="30"
v-if="isAuthenticated && isGetStatusOk"
>
<vxe-table-column title="" width="30" v-if="isAuthenticated">
<template v-slot="{ row }">
<el-tooltip
:content="JUDGE_STATUS[row.myStatus].name"
placement="top"
>
<i
class="el-icon-check"
:style="getIconColor(row.myStatus)"
v-if="row.myStatus == 0"
></i>
<i
class="el-icon-minus"
:style="getIconColor(row.myStatus)"
v-else-if="row.myStatus != -10"
></i>
</el-tooltip>
<template v-if="isGetStatusOk">
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
>
<template v-if="row.myStatus == 0">
<i
class="el-icon-check"
:style="getIconColor(row.myStatus)"
></i>
</template>
<template v-else-if="row.myStatus != -10">
<i
class="el-icon-minus"
:style="getIconColor(row.myStatus)"
></i>
</template>
</el-tooltip>
</template>
</template>
</vxe-table-column>
<vxe-table-column

View File

@ -84,7 +84,9 @@
align="left"
>
<template v-slot="{ row }">
<span v-katex class="rank-signature-body">{{ row.signature }}</span>
<span v-katex class="rank-signature-body" v-if="row.signature">{{
row.signature
}}</span>
</template>
</vxe-table-column>
</vxe-table>

View File

@ -90,7 +90,9 @@
align="left"
>
<template v-slot="{ row }">
<span v-katex class="rank-signature-body">{{ row.signature }}</span>
<span v-katex class="rank-signature-body" v-if="row.signature">{{
row.signature
}}</span>
</template>
</vxe-table-column>
</vxe-table>

View File

@ -79,7 +79,7 @@
<span>
<span>{{ $t('m.Training_Auth') }}</span>
</span>
<span>
<span v-if="training.auth">
<el-tag
:type="TRAINING_TYPE[training.auth]['color']"
effect="dark"
@ -263,6 +263,9 @@ export default {
}
},
},
beforeDestroy() {
this.$store.commit('clearTraining');
},
};
</script>

View File

@ -13,6 +13,28 @@
@search-click="filterByChange"
></vxe-input>
</section>
<section>
<b class="training-category">{{ $t('m.Training_Auth') }}</b>
<div>
<el-tag
size="medium"
class="category-item"
:effect="query.auth ? 'plain' : 'dark'"
@click="filterByAuthType(null)"
>{{ $t('m.All') }}</el-tag
>
<el-tag
size="medium"
class="category-item"
v-for="(key, index) in TRAINING_TYPE"
:type="key.color"
:effect="query.auth == key.name ? 'dark' : 'plain'"
:key="index"
@click="filterByAuthType(key.name)"
>{{ key.name }}</el-tag
>
</div>
</section>
<section>
<b class="training-category">{{ $t('m.Training_Category') }}</b>
<div>
@ -159,6 +181,7 @@ export default {
query: {
keyword: '',
categoryId: null,
auth: null,
},
total: 0,
currentPage: 1,
@ -182,6 +205,7 @@ export default {
this.query.keyword = route.keyword || '';
this.currentPage = parseInt(route.currentPage) || 1;
this.categoryId = route.categoryId || null;
this.query.auth = route.auth || null;
this.getTrainingList(1);
},
filterByCategory(categoryId) {
@ -189,6 +213,11 @@ export default {
this.filterByChange();
},
filterByAuthType(auth) {
this.query.auth = auth;
this.filterByChange();
},
filterByChange() {
let query = Object.assign({}, this.query);
query.currentPage = this.currentPage;

View File

@ -13,24 +13,29 @@
field="status"
title=""
width="50"
v-if="isAuthenticated && isGetStatusOk"
v-if="isAuthenticated"
>
<template v-slot="{ row }">
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
>
<i
class="el-icon-check"
:style="getIconColor(row.myStatus)"
v-if="row.myStatus == 0"
></i>
<i
class="el-icon-minus"
:style="getIconColor(row.myStatus)"
v-else-if="row.myStatus != -10"
></i>
</el-tooltip>
<template v-if="isGetStatusOk">
<el-tooltip
:content="JUDGE_STATUS[row.myStatus]['name']"
placement="top"
>
<template v-if="row.myStatus == 0">
<i
class="el-icon-check"
:style="getIconColor(row.myStatus)"
></i>
</template>
<template v-else-if="row.myStatus != -10">
<i
class="el-icon-minus"
:style="getIconColor(row.myStatus)"
></i>
</template>
</el-tooltip>
</template>
</template>
</vxe-table-column>
<vxe-table-column

View File

@ -1,40 +1,9 @@
<template>
<el-card shadow>
<div slot="header">
<span class="panel-title">{{ $t('m.Training_Rank') }}</span>
<span style="float:right;font-size: 20px;">
<el-popover trigger="hover" placement="left-start">
<i class="el-icon-s-tools" slot="reference"></i>
<div id="switches">
<p>
<span>{{ $t('m.Chart') }}</span>
<el-switch v-model="showChart"></el-switch>
</p>
<p>
<span>{{ $t('m.Table') }}</span>
<el-switch v-model="showTable"></el-switch>
</p>
<p>
<span>{{ $t('m.Auto_Refresh') }}(10s)</span>
<el-switch
:disabled="refreshDisabled"
v-model="autoRefresh"
@change="handleAutoRefresh"
></el-switch>
</p>
<template>
<el-button type="primary" size="small" @click="downloadRankCSV">{{
$t('m.Download_as_CSV')
}}</el-button>
</template>
</div>
</el-popover>
</span>
<div slot="header" class="rank-title">
<span class="panel-title">{{ $t('m.Record_List') }}</span>
</div>
<div v-show="showChart" class="echarts">
<ECharts :options="options" ref="chart" :autoresize="true"></ECharts>
</div>
<div v-show="showTable">
<div>
<vxe-table
round
border
@ -62,22 +31,22 @@
>
<template v-slot="{ row }">
<avatar
:username="row[training.rankShowName]"
:username="row.username"
:inline="true"
:size="37"
color="#FFF"
:src="row.avatar"
:title="row[training.rankShowName]"
:title="row.username"
></avatar>
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="training-username"
><span class="female-flag" v-if="row.gender == 'female'"
<span class="contest-username"
><span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>{{ row[training.rankShowName] }}</span
>{{ row.username }}</span
>
<span class="training-school" v-if="row.school">{{
<span class="contest-school" v-if="row.school">{{
row.school
}}</span>
</a>
@ -94,22 +63,22 @@
>
<template v-slot="{ row }">
<avatar
:username="row[training.rankShowName]"
:username="row.username"
:inline="true"
:size="37"
color="#FFF"
:src="row.avatar"
:title="row[training.rankShowName]"
:title="row.username"
></avatar>
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="training-username"
><span class="female-flag" v-if="row.gender == 'female'"
<span class="contest-username"
><span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>{{ row[training.rankShowName] }}</span
>{{ row.username }}</span
>
<span class="training-school" v-if="row.school">{{
<span class="contest-school" v-if="row.school">{{
row.school
}}</span>
</a>
@ -125,49 +94,59 @@
</vxe-table-column>
<vxe-table-column
field="rating"
:title="$t('m.AC') + ' / ' + $t('m.Total')"
min-width="80"
:title="$t('m.Total_Score')"
min-width="90"
>
<template v-slot="{ row }">
<span
>{{ row.ac }} /
<a
@click="getUserTotalSubmit(row.username)"
style="color:rgb(87, 163, 243);"
>{{ row.total }}</a
><a
@click="getUserACSubmit(row.username)"
style="color:rgb(87, 163, 243);font-size:16px"
>{{ row.ac }}</a
>
<br />
<span class="judge-time">({{ row.totalRunTime }}ms)</span>
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="totalTime"
:title="$t('m.TotalTime')"
min-width="100"
>
<template v-slot="{ row }">
<span>{{ parseTotalTime(row.totalTime) }}</span>
</template>
</vxe-table-column>
<vxe-table-column
min-width="120"
min-width="70"
v-for="problem in trainingProblemList"
:key="problem.displayId"
:key="problem.problemId"
>
<template v-slot:header>
<span
><a
@click="getTrainingProblemById(problem.displayId)"
@click="getTrainingProblemById(problem.problemId)"
class="emphasis"
style="color:#495060;"
>{{ problem.displayId }}</a
>{{ problem.problemId }}</a
></span
>
</template>
<template v-slot="{ row }">
<span v-if="row.submissionInfo[problem.displayId]">
<span v-if="row.submissionInfo[problem.displayId].isAC"
>{{ row.submissionInfo[problem.displayId].ACTime }}<br
/></span>
<span v-if="row.submissionInfo[problem.problemId]">
<span
class="judge-status"
:style="
'color:' +
JUDGE_STATUS[row.submissionInfo[problem.problemId].status]
.color
"
>
{{
JUDGE_STATUS[row.submissionInfo[problem.problemId].status]
.short
}}
</span>
<br />
<span class="judge-time">
({{
row.submissionInfo[problem.problemId].runTime
? row.submissionInfo[problem.problemId].runTime
: 0
}}ms)
</span>
</span>
</template>
</vxe-table-column>
@ -185,11 +164,12 @@
</template>
<script>
import Avatar from 'vue-avatar';
import moment from 'moment';
import { mapActions } from 'vuex';
import { mapActions, mapGetters } from 'vuex';
import { JUDGE_STATUS } from '@/common/constants';
const Pagination = () => import('@/components/oj/common/Pagination');
import api from '@/common/api';
import { mapState } from 'vuex';
import time from '@/common/time';
import utils from '@/common/utils';
export default {
name: 'TrainingRank',
@ -202,91 +182,43 @@ export default {
total: 0,
page: 1,
limit: 30,
autoRefresh: false,
trainingID: '',
dataRank: [],
options: {
title: {
text: this.$i18n.t('m.Top_10_Teams'),
left: 'center',
top: 0,
},
dataZoom: [
{
type: 'inside',
filterMode: 'none',
xAxisIndex: [0],
start: 0,
end: 100,
},
],
toolbox: {
show: true,
feature: {
saveAsImage: { show: true, title: this.$i18n.t('m.save_as_image') },
},
right: '0',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
axis: 'x',
},
},
legend: {
orient: 'horizontal',
x: 'center',
top: '8%',
right: 0,
data: [],
formatter: (value) => {
return utils.breakLongWords(value, 16);
},
textStyle: {
fontSize: 12,
},
},
grid: {
x: 80,
x2: 100,
left: '5%', //canvas
top: '25%',
right: '5%',
bottom: '10%',
},
xAxis: [
{
type: 'time',
splitLine: false,
axisPointer: {
show: true,
snap: true,
},
},
],
yAxis: [
{
type: 'category',
boundaryGap: false,
data: [0],
},
],
series: [],
},
JUDGE_STATUS: {},
};
},
created() {
this.JUDGE_STATUS = Object.assign({}, JUDGE_STATUS);
if (!this.trainingProblemList.length) {
this.getTrainingProblemList();
}
},
mounted() {
this.trainingID = this.$route.params.trainingID;
this.getTrainingRankData(1);
this.addChartCategory(this.trainingProblemList);
this.getTrainingRankData();
},
methods: {
...mapActions(['getTrainingProblems']),
getUserTotalSubmit(username) {
...mapActions(['getTrainingProblemList']),
getTrainingRankData() {
let data = {
tid: this.trainingID,
limit: this.limit,
currentPage: this.page,
};
api.getTrainingRank(data).then(
(res) => {
this.total = res.data.data.total;
this.applyToTable(res.data.data.records);
},
(err) => {}
);
},
getUserACSubmit(username) {
this.$router.push({
name: 'TrainingSubmissionList',
query: { username: username },
name: 'SubmissionList',
query: { username: username, status: 0 },
});
},
getUserHomeByUsername(uid, username) {
@ -308,113 +240,24 @@ export default {
if (column.property === 'username' && row.userCellClassName) {
return row.userCellClassName;
}
if (
column.property !== 'id' &&
column.property !== 'rating' &&
column.property !== 'totalTime' &&
column.property !== 'username' &&
column.property !== 'realname'
) {
if (this.isTrainingAdmin) {
return row.cellClassName[
[this.trainingProblemList[columnIndex - 5].displayId]
];
} else {
return row.cellClassName[
[this.trainingProblemList[columnIndex - 4].displayId]
];
}
}
},
applyToTable(data) {
let dataRank = JSON.parse(JSON.stringify(data));
applyToTable(dataRank) {
dataRank.forEach((rank, i) => {
let info = rank.submissionInfo;
let cellClass = {};
Object.keys(info).forEach((problemID) => {
dataRank[i][problemID] = info[problemID];
dataRank[i][problemID].ACTime = time.secondFormat(
dataRank[i][problemID].ACTime
);
let status = info[problemID];
if (status.isFirstAC) {
cellClass[problemID] = 'first-ac';
} else if (status.isAC) {
cellClass[problemID] = 'ac';
} else if (status.tryNum != null && status.tryNum > 0) {
cellClass[problemID] = 'try';
} else if (status.errorNum != 0) {
cellClass[problemID] = 'wa';
}
});
dataRank[i].cellClassName = cellClass;
if (dataRank[i].gender == 'female') {
dataRank[i].userCellClassName = 'bg-female';
}
});
this.dataRank = dataRank;
},
addChartCategory(trainingProblemList) {
let category = [];
for (let i = 0; i <= trainingProblemList.length; ++i) {
category.push(i);
}
this.options.yAxis[0].data = category;
},
applyToChart(rankData) {
let [users, seriesData] = [[], []];
rankData.forEach((rank) => {
users.push(rank[this.training.rankShowName]);
let info = rank.submissionInfo;
// AC
let timeData = [];
Object.keys(info).forEach((problemID) => {
if (info[problemID].isAC) {
timeData.push(info[problemID].ACTime);
}
});
timeData.sort((a, b) => {
return a - b;
});
let data = [];
data.push([this.training.startTime, 0]);
for (let [index, value] of timeData.entries()) {
let realTime = moment(this.training.startTime)
.add(value, 'seconds')
.format();
data.push([realTime, index + 1]);
}
seriesData.push({
name: rank[this.training.rankShowName],
type: 'line',
data,
});
});
this.options.legend.data = users;
this.options.series = seriesData;
},
parseTotalTime(totalTime) {
return time.secondFormat(totalTime);
},
downloadRankCSV() {
utils.downloadFile(
`/api/file/download-training-rank?cid=${
this.$route.params.trainingID
}&forceRefresh=${this.forceUpdate ? true : false}`
);
},
},
watch: {
trainingProblemList(newVal, OldVal) {
if (newVal.length != 0) {
this.addChartCategory(this.trainingProblemList);
}
},
},
computed: {
...mapState({
trainingProblemList: (state) => state.training.trainingProblemList,
}),
...mapGetters(['isTrainingAdmin']),
training() {
return this.$store.state.training.training;
},
@ -425,39 +268,20 @@ export default {
};
</script>
<style scoped>
.echarts {
margin: 20px auto;
height: 400px;
width: 100%;
.rank-title {
margin-bottom: 18px;
text-align: center;
}
/deep/.el-card__body {
padding: 20px !important;
padding-top: 0px !important;
}
.screen-full {
margin-right: 8px;
}
#switches p {
margin-top: 5px;
}
#switches p:first-child {
margin-top: 0;
}
#switches p span {
margin-left: 8px;
margin-right: 4px;
}
.vxe-cell p,
.vxe-cell span {
margin: 0;
padding: 0;
}
/deep/.vxe-table .vxe-body--column {
line-height: 20px !important;
padding: 0 !important;
}
@media screen and (max-width: 768px) {
/deep/.el-card__body {
padding: 0 !important;
@ -469,6 +293,18 @@ a.emphasis {
a.emphasis:hover {
color: #2d8cf0 !important;
}
/deep/.vxe-table .vxe-header--column:not(.col--ellipsis) {
padding: 4px 0 !important;
}
/deep/.vxe-table .vxe-body--column {
padding: 4px 0 !important;
line-height: 20px !important;
}
/deep/.vxe-table .vxe-body--column:not(.col--ellipsis) {
line-height: 20px !important;
padding: 0 !important;
}
/deep/.vxe-body--column {
min-width: 0;
height: 48px;
@ -481,4 +317,12 @@ a.emphasis:hover {
padding-left: 5px !important;
padding-right: 5px !important;
}
.judge-status {
font-size: 16px;
font-weight: bold;
}
.judge-time {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
}
</style>