美化比赛排行榜,增加外榜、打星队伍的支持

This commit is contained in:
Himit_ZH 2021-12-09 16:58:23 +08:00
parent e3a7be0382
commit fe3be8dc1a
46 changed files with 2052 additions and 291 deletions

View File

@ -96,6 +96,7 @@ docker ps # 查看当前运行的容器状态
| 2021-09-21 | 增加比赛打印功能、账号限制功能 | Himit_ZH |
| 2021-10-05 | 增加站内消息系统——评论、回复、点赞、系统通知的消息,优化前端。 | Himit_ZH |
| 2021-10-06 | 美化比赛排行榜增加对FPS题目导入的支持 | Himit_ZH |
| 2021-12-09 | 美化比赛排行榜,增加外榜、打星队伍的支持 | Himit_ZH |
## 五、部分截图

View File

@ -32,6 +32,6 @@
然后压缩测试用例到一个zip中
注意:不要在这些文件外面套多一层文件夹,请直接压缩!!!
**注意:即使没有输入或者没有输出,也请提供对应的空输入(输出)文件,不要在这些文件外面套多一层文件夹,请直接压缩!!!**
同时建议:尽量合并测试用例到一个文件中,减少测试用例组数,这会一定程度上提高判题性能。

View File

@ -1,7 +1,10 @@
package top.hcode.hoj.controller.admin;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@ -23,6 +26,7 @@ import top.hcode.hoj.pojo.entity.contest.ContestAnnouncement;
import top.hcode.hoj.pojo.entity.contest.ContestProblem;
import top.hcode.hoj.pojo.entity.judge.Judge;
import top.hcode.hoj.pojo.entity.problem.Problem;
import top.hcode.hoj.pojo.vo.AdminContestVo;
import top.hcode.hoj.pojo.vo.AnnouncementVo;
import top.hcode.hoj.pojo.vo.UserRolesVo;
import top.hcode.hoj.service.common.impl.AnnouncementServiceImpl;
@ -37,6 +41,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;
@ -119,8 +124,15 @@ public class AdminContestController {
if (!isRoot && !userRolesVo.getUid().equals(contest.getUid())) {
return CommonResult.errorResponse("对不起,你无权限操作!", CommonResult.STATUS_FORBIDDEN);
}
return CommonResult.successResponse(contest, "查询成功!");
AdminContestVo adminContestVo = BeanUtil.copyProperties(contest, AdminContestVo.class, "starAccount");
if (StringUtils.isEmpty(contest.getStarAccount())){
adminContestVo.setStarAccount(new ArrayList<>());
}else {
JSONObject jsonObject = JSONUtil.parseObj(contest.getStarAccount());
List<String> starAccount = jsonObject.get("star_account", List.class);
adminContestVo.setStarAccount(starAccount);
}
return CommonResult.successResponse(adminContestVo, "查询成功!");
}
@DeleteMapping("")
@ -141,7 +153,11 @@ public class AdminContestController {
@PostMapping("")
@RequiresAuthentication
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
public CommonResult addContest(@RequestBody Contest contest) {
public CommonResult addContest(@RequestBody AdminContestVo adminContestVo) {
Contest contest = BeanUtil.copyProperties(adminContestVo, Contest.class, "starAccount");
JSONObject accountJson = new JSONObject();
accountJson.set("star_account", adminContestVo.getStarAccount());
contest.setStarAccount(accountJson.toString());
boolean result = contestService.save(contest);
if (result) { // 添加成功
return CommonResult.successResponse(null, "添加成功!");
@ -153,7 +169,7 @@ public class AdminContestController {
@PutMapping("")
@RequiresAuthentication
@RequiresRoles(value = {"root", "admin", "problem_admin"}, logical = Logical.OR)
public CommonResult updateContest(@RequestBody Contest contest, HttpServletRequest request) {
public CommonResult updateContest(@RequestBody AdminContestVo adminContestVo, HttpServletRequest request) {
// 获取当前登录的用户
HttpSession session = request.getSession();
@ -161,10 +177,13 @@ public class AdminContestController {
// 是否为超级管理员
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
// 只有超级管理员和比赛拥有者才能操作
if (!isRoot && !userRolesVo.getUid().equals(contest.getUid())) {
if (!isRoot && !userRolesVo.getUid().equals(adminContestVo.getUid())) {
return CommonResult.errorResponse("对不起,你无权限操作!", CommonResult.STATUS_FORBIDDEN);
}
Contest contest = BeanUtil.copyProperties(adminContestVo, Contest.class, "starAccount");
JSONObject accountJson = new JSONObject();
accountJson.set("star_account", adminContestVo.getStarAccount());
contest.setStarAccount(accountJson.toString());
boolean result = contestService.saveOrUpdate(contest);
if (result) {
return CommonResult.successResponse(null, "修改成功!");

View File

@ -81,6 +81,7 @@ public class ContestFileController {
@RequiresAuthentication
public void downloadContestRank(@RequestParam("cid") Long cid,
@RequestParam("forceRefresh") Boolean forceRefresh,
@RequestParam(value = "removeStar", defaultValue = "false") Boolean removeStar,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
// 获取当前登录的用户
@ -118,21 +119,17 @@ public class ContestFileController {
if (contest.getType().intValue() == Constants.Contest.TYPE_ACM.getCode()) { // ACM比赛
List<ContestRecordVo> contestRecordList = contestRecordService.getACMContestRecord(contest.getAuthor(), contest.getId());
Assert.notEmpty(contestRecordList, "比赛暂无排行榜记录!");
List<ACMContestRankVo> acmContestRankVoList = contestRecordService.calcACMRank(isOpenSealRank, contest, contestRecordList);
List<ACMContestRankVo> acmContestRankVoList = contestRecordService.getACMContestScoreboard(isOpenSealRank, removeStar, contest);
EasyExcel.write(response.getOutputStream())
.head(fileService.getContestRankExcelHead(contestProblemDisplayIDList, true))
.sheet("rank")
.doWrite(fileService.changeACMContestRankToExcelRowList(acmContestRankVoList, contestProblemDisplayIDList));
.doWrite(fileService.changeACMContestRankToExcelRowList(acmContestRankVoList, contestProblemDisplayIDList,contest.getRankShowName()));
} else {
List<ContestRecordVo> oiContestRecord = contestRecordService.getOIContestRecord(cid, contest.getAuthor(), isOpenSealRank, contest.getSealRankTime(), contest.getStartTime(), contest.getEndTime());
Assert.notEmpty(oiContestRecord, "比赛暂无排行榜记录!");
List<OIContestRankVo> oiContestRankVoList = contestRecordService.calcOIRank(oiContestRecord);
List<OIContestRankVo> oiContestRankVoList = contestRecordService.getOIContestScoreboard(isOpenSealRank, removeStar, contest);
EasyExcel.write(response.getOutputStream())
.head(fileService.getContestRankExcelHead(contestProblemDisplayIDList, false))
.sheet("rank")
.doWrite(fileService.changOIContestRankToExcelRowList(oiContestRankVoList, contestProblemDisplayIDList));
.doWrite(fileService.changOIContestRankToExcelRowList(oiContestRankVoList, contestProblemDisplayIDList,contest.getRankShowName()));
}
}

View File

@ -135,7 +135,6 @@ public class AccountController {
* @Return
* @Since 2020/11/5
*/
@RequestMapping(value = "/check-username-or-email", method = RequestMethod.POST)
public CommonResult checkUsernameOrEmail(@RequestBody Map<String, Object> data) throws MessagingException {
String email = (String) data.get("email");

View File

@ -95,7 +95,6 @@ public class ContestController {
@RequestParam(value = "keyword", required = false) String keyword) {
// 页数每页题数若为空设置默认值
if (currentPage == null || currentPage < 1) currentPage = 1;
if (limit == null || limit < 1) limit = 10;
Page<ContestVo> contestList = contestService.getContestList(limit, currentPage, type, status, keyword);
@ -125,7 +124,6 @@ public class ContestController {
// 设置当前服务器系统时间
contestInfo.setNow(new Date());
return CommonResult.successResponse(contestInfo, "获取成功");
}
@ -483,6 +481,7 @@ public class ContestController {
@RequestParam(value = "limit", required = false) Integer limit,
@RequestParam(value = "currentPage", required = false) Integer currentPage,
@RequestParam(value = "forceRefresh") Boolean forceRefresh,
@RequestParam(value = "removeStar", defaultValue = "0") Boolean removeStar,
HttpServletRequest request) {
// 获取当前登录的用户
@ -512,12 +511,11 @@ public class ContestController {
if (contest.getType().intValue() == Constants.Contest.TYPE_ACM.getCode()) { // ACM比赛
// 进行排行榜计算以及排名分页
resultList = contestRecordService.getContestACMRank(isOpenSealRank, contest, currentPage, limit);
resultList = contestRecordService.getContestACMRank(isOpenSealRank, removeStar, contest, currentPage, limit);
} else { //OI比赛以最后一次提交得分作为该题得分
resultList = contestRecordService.getContestOIRank(cid, contest.getAuthor(), isOpenSealRank, contest.getSealRankTime(), contest.getStartTime(),
contest.getEndTime(), currentPage, limit);
resultList = contestRecordService.getContestOIRank(isOpenSealRank, removeStar, contest, currentPage, limit);
}
if (resultList == null || resultList.getSize() == 0) {
@ -649,4 +647,105 @@ public class ContestController {
}
/**
* @MethodName getContestOutsideInfo
* @param cid 比赛id
* @Description 提供比赛外榜所需的比赛信息和题目信息
* @Return
* @Since 2021/12/8
*/
@GetMapping("/get-contest-outsize-info")
public CommonResult getContestOutsideInfo(@RequestParam(value = "cid", required = true) Long cid) {
ContestVo contestInfo = contestService.getContestInfoById(cid);
if (contestInfo == null) {
return CommonResult.errorResponse("访问错误:该比赛不存在!");
}
if (!contestInfo.getOpenRank()) {
return CommonResult.errorResponse("本场比赛未开启外榜,禁止访问外榜!", CommonResult.STATUS_ACCESS_DENIED);
}
// 获取本场比赛的状态
if (contestInfo.getStatus().equals(Constants.Contest.STATUS_SCHEDULED.getCode())) {
return CommonResult.errorResponse("本场比赛正在筹备中,禁止访问外榜!", CommonResult.STATUS_ACCESS_DENIED);
}
contestInfo.setNow(new Date());
ContestOutsideInfo contestOutsideInfo = new ContestOutsideInfo();
contestOutsideInfo.setContest(contestInfo);
QueryWrapper<ContestProblem> contestProblemQueryWrapper = new QueryWrapper<>();
contestProblemQueryWrapper.eq("cid", cid);
List<ContestProblem> contestProblemList = contestProblemService.list(contestProblemQueryWrapper);
contestOutsideInfo.setProblemList(contestProblemList);
return CommonResult.successResponse(contestOutsideInfo, "success");
}
/**
* @param request
* @param cid 比赛id
* @param removeStar 是否移除打星队伍
* @param forceRefresh 是否强制实时榜单
* @MethodName getContestScoreBoard
* @Description 提供比赛外榜排名数据
* @Return
* @Since 2021/12/07
*/
@GetMapping("/get-contest-outside-scoreboard")
public CommonResult getContestOutsideScoreboard(@RequestParam(value = "cid", required = true) Long cid,
@RequestParam(value = "removeStar", defaultValue = "0") Boolean removeStar,
@RequestParam(value = "forceRefresh") Boolean forceRefresh,
HttpServletRequest request) {
// 获取本场比赛的状态
Contest contest = contestService.getById(cid);
if (contest == null) {
return CommonResult.errorResponse("访问错误:该比赛不存在!");
}
if (!contest.getOpenRank()) {
return CommonResult.errorResponse("本场比赛未开启外榜,禁止访问外榜!", CommonResult.STATUS_ACCESS_DENIED);
}
if (contest.getStatus().equals(Constants.Contest.STATUS_SCHEDULED.getCode())) {
return CommonResult.errorResponse("本场比赛正在筹备中,禁止访问外榜!", CommonResult.STATUS_ACCESS_DENIED);
}
// 获取当前登录的用户
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
// 超级管理员或者该比赛的创建者则为比赛管理者
boolean isRoot = false;
String currentUid = null;
if (userRolesVo != null) {
currentUid = userRolesVo.getUid();
isRoot = SecurityUtils.getSubject().hasRole("root");
// 不是比赛创建者或者超管无权限开启强制实时榜单
if (!isRoot && !contest.getUid().equals(currentUid)) {
forceRefresh = false;
}
}
// 校验该比赛是否开启了封榜模式超级管理员和比赛创建者可以直接看到实际榜单
boolean isOpenSealRank = contestService.isSealRank(currentUid, contest, forceRefresh, isRoot);
if (contest.getType().intValue() == Constants.Contest.TYPE_ACM.getCode()) { // ACM比赛
// 获取排行榜
List<ACMContestRankVo> acmContestScoreboard = contestRecordService.getACMContestScoreboard(isOpenSealRank, removeStar, contest);
return CommonResult.successResponse(acmContestScoreboard, "success");
} else { //OI比赛以最后一次提交得分作为该题得分
// 获取排行榜
List<OIContestRankVo> oiContestScoreboard = contestRecordService.getOIContestScoreboard(isOpenSealRank, removeStar, contest);
return CommonResult.successResponse(oiContestScoreboard, "success");
}
}
}

View File

@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.dao.JudgeMapper;
@ -446,8 +447,8 @@ public class JudgeController {
List<Long> submitIds = submitIdListDto.getSubmitIds();
if (submitIds.size() == 0) {
return CommonResult.successResponse(new HashMap<>(), "已无数据可查询!");
if (CollectionUtils.isEmpty(submitIds)) {
return CommonResult.successResponse(new HashMap<>(), "查询的提交id列表为空");
}
QueryWrapper<Judge> queryWrapper = new QueryWrapper<>();
@ -480,17 +481,23 @@ public class JudgeController {
return CommonResult.errorResponse("查询比赛ID不能为空");
}
if (CollectionUtils.isEmpty(submitIdListDto.getSubmitIds())) {
return CommonResult.successResponse(new HashMap<>(), "查询的提交id列表为空");
}
HttpSession session = request.getSession();
UserRolesVo userRolesVo = (UserRolesVo) session.getAttribute("userInfo");
boolean root = SecurityUtils.getSubject().hasRole("root"); // 是否为超级管理员
boolean isRoot = SecurityUtils.getSubject().hasRole("root"); // 是否为超级管理员
Contest contest = contestService.getById(submitIdListDto.getCid());
boolean isSealRank = false;
boolean isContestAdmin = isRoot || userRolesVo.getUid().equals(contest.getUid());
// 如果是封榜时间且不是比赛管理员和超级管理员
if (contest.getStatus().intValue() == Constants.Contest.STATUS_RUNNING.getCode() &&
contest.getSealRank() && contest.getSealRankTime().before(new Date()) &&
!root && !userRolesVo.getUid().equals(contest.getUid())) {
!isContestAdmin) {
isSealRank = true;
}
@ -508,7 +515,7 @@ public class JudgeController {
judge.setVjudgeUsername(null);
judge.setVjudgeSubmitId(null);
judge.setVjudgePassword(null);
if (!judge.getUid().equals(userRolesVo.getUid())){
if (!judge.getUid().equals(userRolesVo.getUid()) && !isContestAdmin) {
judge.setTime(null);
judge.setMemory(null);
judge.setLength(null);

View File

@ -2,7 +2,8 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.hcode.hoj.dao.ContestMapper">
<select id="getContestList" resultType="top.hcode.hoj.pojo.vo.ContestVo" useCache="true">
select c.id,c.author,c.title,c.description,c.type,c.status,c.source,c.auth,c.start_time,c.end_time,c.duration
select c.id,c.author,c.title,c.description,c.type,c.status,c.source,c.auth,c.start_time,
c.end_time,c.duration,c.open_rank
from contest c
<where>
c.visible=true
@ -20,7 +21,7 @@
</select>
<select id="getContestInfoById" resultType="top.hcode.hoj.pojo.vo.ContestVo" useCache="true">
select c.id,c.author,c.open_print,c.title,c.type,c.status,c.description,c.seal_rank,
c.seal_rank_time,c.source,c.auth,c.start_time,c.end_time,c.duration,c.rank_show_name
c.seal_rank_time,c.source,c.auth,c.start_time,c.end_time,c.duration,c.rank_show_name,c.open_rank
from contest c where c.id = #{cid} and c.visible=true
</select>
<select id="getWithinNext14DaysContests" resultType="top.hcode.hoj.pojo.vo.ContestVo">

View File

@ -114,7 +114,10 @@ public class Dispatcher {
.eq("oj", finalOj)
.eq("username", data.getUsername())
.set("status", true);
remoteJudgeAccountService.update(remoteJudgeAccountUpdateWrapper);
boolean isOK = remoteJudgeAccountService.update(remoteJudgeAccountUpdateWrapper);
if (!isOK) { // 重试8次
tryAgainUpdateAccount(remoteJudgeAccountUpdateWrapper, finalOj, data.getUsername());
}
}
checkResult(null, submitId);
scheduler.shutdown();
@ -166,11 +169,11 @@ public class Dispatcher {
judgeServerUpdateWrapper.setSql("task_number = task_number-1").eq("id", id);
boolean isOk = judgeServerService.update(judgeServerUpdateWrapper);
if (!isOk) { // 重试八次
tryAgainUpdate(judgeServerUpdateWrapper);
tryAgainUpdateJudge(judgeServerUpdateWrapper);
}
}
public void tryAgainUpdate(UpdateWrapper<JudgeServer> updateWrapper) {
public void tryAgainUpdateJudge(UpdateWrapper<JudgeServer> updateWrapper) {
boolean retryable;
int attemptNumber = 0;
do {
@ -191,4 +194,28 @@ public class Dispatcher {
}
} while (retryable);
}
public void tryAgainUpdateAccount(UpdateWrapper<RemoteJudgeAccount> updateWrapper, String remoteJudge, String username) {
boolean retryable;
int attemptNumber = 0;
do {
boolean success = remoteJudgeAccountService.update(updateWrapper);
if (success) {
return;
} else {
attemptNumber++;
retryable = attemptNumber < 8;
if (attemptNumber == 8) {
log.error("远程判题:修正账号为可用状态失败----------->{}", "oj:" + remoteJudge + ",username:" + username);
break;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} while (retryable);
}
}

View File

@ -4,7 +4,6 @@ import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@ -20,15 +19,14 @@ import top.hcode.hoj.utils.RedisUtils;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class RemoteJudgeReceiver {
@Autowired
@Lazy
private RemoteJudgeDispatcher remoteJudgeDispatcher;
@Autowired
private JudgeServiceImpl judgeService;
@ -74,96 +72,167 @@ public class RemoteJudgeReceiver {
}
}
public void handleJudgeMsg(Judge judge, String token, String remoteJudgeProblem, Boolean isContest, Integer tryAgainNum,
public void handleJudgeMsg(Judge judge, String token, String remoteJudgeProblem, Boolean isContest,
Integer tryAgainNum,
Boolean isHasSubmitIdRemoteReJudge, String remoteOJName) {
boolean isNeedAccountRejudge = remoteOJName.equals(Constants.RemoteOJ.POJ.getName())
&& isHasSubmitIdRemoteReJudge;
String remoteOJAccountType = remoteOJName; // GYM与CF共用账号
if (remoteOJName.equals(Constants.RemoteOJ.GYM.getName())) {
remoteOJAccountType = Constants.RemoteOJ.CODEFORCES.getName();
}
ToJudge toJudge = new ToJudge();
toJudge.setJudge(judge)
.setTryAgainNum(tryAgainNum)
.setToken(token)
.setRemoteJudgeProblem(remoteJudgeProblem);
if (isHasSubmitIdRemoteReJudge && !isNeedAccountRejudge) { // 除POJ外的vJudge如果有submitId则直接获取结果
if (remoteOJName.equals(Constants.RemoteOJ.CODEFORCES.getName())
|| remoteOJName.equals(Constants.RemoteOJ.GYM.getName())) { // GYM与CF共用账号
cfJudge(isHasSubmitIdRemoteReJudge, toJudge, judge);
} else if (remoteOJName.equals(Constants.RemoteOJ.POJ.getName())) {
pojJudge(isHasSubmitIdRemoteReJudge, toJudge, judge);
} else {
commonJudge(remoteOJName, isHasSubmitIdRemoteReJudge, toJudge, judge);
}
// 如果队列中还有任务则继续处理
processWaitingTask();
}
private void commonJudge(String OJName, Boolean isHasSubmitIdRemoteReJudge, ToJudge toJudge, Judge judge) {
if (isHasSubmitIdRemoteReJudge) {
toJudge.setIsHasSubmitIdRemoteReJudge(true);
toJudge.setUsername(judge.getVjudgeUsername());
toJudge.setPassword(judge.getVjudgePassword());
// 调用判题服务
dispatcher.dispatcher("judge", "/remote-judge", toJudge);
// 如果队列中还有任务则继续处理
processWaitingTask();
return;
}
} else {
RemoteJudgeAccount account = null;
int index = 0;
int size = 0;
if (remoteOJAccountType.equals(Constants.RemoteOJ.CODEFORCES.getName())) {
HashMap<String, Object> result = chooseUtils.chooseFixedAccount(remoteOJAccountType);
if (result != null) {
account = (RemoteJudgeAccount) result.get("account");
index = (int) result.get("index");
size = (int) result.get("size");
// 尝试600s
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
AtomicInteger tryNum = new AtomicInteger(0);
Runnable getResultTask = new Runnable() {
@Override
public void run() {
tryNum.getAndIncrement();
RemoteJudgeAccount account = chooseUtils.chooseRemoteAccount(OJName, judge.getVjudgeUsername(), false);
if (account != null) {
toJudge.setUsername(account.getUsername())
.setPassword(account.getPassword());
toJudge.setIsHasSubmitIdRemoteReJudge(false);
// 调用判题服务
dispatcher.dispatcher("judge", "/remote-judge", toJudge);
scheduler.shutdown();
return;
}
} else {
account = chooseUtils.chooseRemoteAccount(remoteOJAccountType, judge.getVjudgeUsername(), isNeedAccountRejudge);
}
if (account == null) {
if (tryAgainNum >= 200) {
if (tryNum.get() > 200) {
// 获取调用多次失败可能为系统忙碌判为提交失败
judge.setStatus(Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus());
judge.setErrorMessage("Submission failed! Please resubmit this submission again!" +
"Cause: Waiting for account scheduling timeout");
judgeService.updateById(judge);
} else {
scheduler.shutdown();
}
}
};
scheduler.scheduleAtFixedRate(getResultTask, 0, 3, TimeUnit.SECONDS);
}
if (isNeedAccountRejudge) {
if (StringUtils.isEmpty(judge.getVjudgeUsername())) {
// poj以往的账号丢失了那么只能重新从头到尾提交
remoteJudgeDispatcher.sendTask(judge, token, remoteJudgeProblem, isContest,
tryAgainNum + 1, false);
return;
}
QueryWrapper<RemoteJudgeAccount> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("oj", remoteOJAccountType).eq("username", judge.getVjudgeUsername());
int count = remoteJudgeAccountService.count(queryWrapper);
if (count == 0) {
// poj以往的账号丢失了那么只能重新从头到尾提交
remoteJudgeDispatcher.sendTask(judge, token, remoteJudgeProblem, isContest,
tryAgainNum + 1, false);
return;
}
}
private void pojJudge(Boolean isHasSubmitIdRemoteReJudge, ToJudge toJudge, Judge judge) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
remoteJudgeDispatcher.sendTask(judge, token, remoteJudgeProblem, isContest,
tryAgainNum + 1, isHasSubmitIdRemoteReJudge);
if (StringUtils.isEmpty(judge.getVjudgeUsername())) {
isHasSubmitIdRemoteReJudge = false;
}
if (isHasSubmitIdRemoteReJudge) {
QueryWrapper<RemoteJudgeAccount> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("oj", Constants.RemoteOJ.POJ.getName())
.eq("username", judge.getVjudgeUsername());
int count = remoteJudgeAccountService.count(queryWrapper);
if (count == 0) {
// poj以往的账号丢失了那么只能重新从头到尾提交
isHasSubmitIdRemoteReJudge = false;
}
}
// 尝试600s
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
AtomicInteger tryNum = new AtomicInteger(0);
boolean finalIsHasSubmitIdRemoteReJudge = isHasSubmitIdRemoteReJudge;
Runnable getResultTask = new Runnable() {
@Override
public void run() {
tryNum.getAndIncrement();
RemoteJudgeAccount account = chooseUtils.chooseRemoteAccount(Constants.RemoteOJ.POJ.getName()
, judge.getVjudgeUsername(), finalIsHasSubmitIdRemoteReJudge);
if (account != null) {
toJudge.setUsername(account.getUsername())
.setPassword(account.getPassword());
toJudge.setIsHasSubmitIdRemoteReJudge(finalIsHasSubmitIdRemoteReJudge);
// 调用判题服务
dispatcher.dispatcher("judge", "/remote-judge", toJudge);
scheduler.shutdown();
return;
}
} else {
toJudge.setUsername(account.getUsername())
.setPassword(account.getPassword());
toJudge.setIsHasSubmitIdRemoteReJudge(isNeedAccountRejudge);
toJudge.setIndex(index);
toJudge.setSize(size);
// 调用判题服务
dispatcher.dispatcher("judge", "/remote-judge", toJudge);
// 如果队列中还有任务则继续处理
processWaitingTask();
if (tryNum.get() > 200) {
// 获取调用多次失败可能为系统忙碌判为提交失败
judge.setStatus(Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus());
judge.setErrorMessage("Submission failed! Please resubmit this submission again!" +
"Cause: Waiting for account scheduling timeout");
judgeService.updateById(judge);
scheduler.shutdown();
}
}
};
scheduler.scheduleAtFixedRate(getResultTask, 0, 3, TimeUnit.SECONDS);
}
private void cfJudge(Boolean isHasSubmitIdRemoteReJudge, ToJudge toJudge, Judge judge) {
if (isHasSubmitIdRemoteReJudge) {
toJudge.setIsHasSubmitIdRemoteReJudge(true);
toJudge.setUsername(judge.getVjudgeUsername());
toJudge.setPassword(judge.getVjudgePassword());
// 调用判题服务
dispatcher.dispatcher("judge", "/remote-judge", toJudge);
return;
}
// 尝试600s
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
AtomicInteger tryNum = new AtomicInteger(0);
Runnable getResultTask = new Runnable() {
@Override
public void run() {
tryNum.getAndIncrement();
HashMap<String, Object> result = chooseUtils.chooseFixedAccount(Constants.RemoteOJ.CODEFORCES.getName());
if (result != null) {
RemoteJudgeAccount account = (RemoteJudgeAccount) result.get("account");
int index = (int) result.get("index");
int size = (int) result.get("size");
toJudge.setUsername(account.getUsername())
.setPassword(account.getPassword());
toJudge.setIsHasSubmitIdRemoteReJudge(false);
toJudge.setIndex(index);
toJudge.setSize(size);
// 调用判题服务
dispatcher.dispatcher("judge", "/remote-judge", toJudge);
scheduler.shutdown();
return;
}
if (tryNum.get() > 200) {
// 获取调用多次失败可能为系统忙碌判为提交失败
judge.setStatus(Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus());
judge.setErrorMessage("Submission failed! Please resubmit this submission again!" +
"Cause: Waiting for account scheduling timeout");
judgeService.updateById(judge);
scheduler.shutdown();
}
}
};
scheduler.scheduleAtFixedRate(getResultTask, 0, 3, TimeUnit.SECONDS);
}
}

View File

@ -15,6 +15,9 @@ import java.util.HashMap;
@Accessors(chain = true)
public class ACMContestRankVo {
@ApiModelProperty(value = "排名,排名为-1则为打星队伍")
private Integer rank;
@ApiModelProperty(value = "用户id")
private String uid;

View File

@ -0,0 +1,92 @@
package top.hcode.hoj.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* @Author: Himit_ZH
* @Date: 2021/12/7 19:45
* @Description:
*/
@ApiModel(value="管理比赛的回传实体", description="")
@Data
public class AdminContestVo {
@ApiModelProperty(value = "比赛id")
private Long id;
@ApiModelProperty(value = "比赛创建者id")
private String uid;
@ApiModelProperty(value = "比赛创建者的用户名")
private String author;
@ApiModelProperty(value = "比赛标题")
private String title;
@ApiModelProperty(value = "0为acm赛制1为比分赛制")
private Integer type;
@ApiModelProperty(value = "比赛说明")
private String description;
@ApiModelProperty(value = "比赛来源原创为0克隆赛为比赛id")
private Integer source;
@ApiModelProperty(value = "0为公开赛1为私有赛访问有密码2为保护赛提交有密码")
private Integer auth;
@ApiModelProperty(value = "比赛密码")
private String pwd;
@ApiModelProperty(value = "开始时间")
private Date startTime;
@ApiModelProperty(value = "结束时间")
private Date endTime;
@ApiModelProperty(value = "比赛时长s")
private Long duration;
@ApiModelProperty(value = "是否开启封榜")
private Boolean sealRank;
@ApiModelProperty(value = "封榜起始时间,一直到比赛结束,不刷新榜单")
private Date sealRankTime;
@ApiModelProperty(value = "比赛结束是否自动解除封榜,自动转换成真实榜单")
private Boolean autoRealRank;
@ApiModelProperty(value = "-1为未开始0为进行中1为已结束")
private Integer status;
@ApiModelProperty(value = "是否可见")
private Boolean visible;
@ApiModelProperty(value = "是否打开打印功能")
private Boolean openPrint;
@ApiModelProperty(value = "是否打开账号限制")
private Boolean openAccountLimit;
@ApiModelProperty(value = "账号限制规则 <prefix>**</prefix><suffix>**</suffix><start>**</start><end>**</end><extra>**</extra>")
private String accountLimitRule;
@ApiModelProperty(value = "排行榜显示username、nickname、realname")
private String rankShowName;
@ApiModelProperty(value = "打星用户列表")
private List<String> starAccount;
@ApiModelProperty(value = "是否开放比赛榜单")
private Boolean openRank;
private Date gmtCreate;
private Date gmtModified;
}

View File

@ -0,0 +1,24 @@
package top.hcode.hoj.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import top.hcode.hoj.pojo.entity.contest.ContestProblem;
import java.util.List;
/**
* @Author: Himit_ZH
* @Date: 2021/12/8 12:32
* @Description:
*/
@ApiModel(value = "赛外排行榜所需的比赛信息,同时包括题目题号、气球颜色", description = "")
@Data
public class ContestOutsideInfo {
@ApiModelProperty(value = "比赛信息")
private ContestVo contest;
@ApiModelProperty(value = "比赛题目信息列表")
private List<ContestProblem> problemList;
}

View File

@ -65,4 +65,7 @@ public class ContestVo implements Serializable {
@ApiModelProperty(value = "排行榜显示username、nickname、realname")
private String rankShowName;
@ApiModelProperty(value = "是否开放比赛榜单")
private Boolean openRank;
}

View File

@ -15,6 +15,10 @@ import java.util.HashMap;
@Data
@Accessors(chain = true)
public class OIContestRankVo {
@ApiModelProperty(value = "排名,排名为-1则为打星队伍")
private Integer rank;
@ApiModelProperty(value = "用户id")
private String uid;

View File

@ -285,14 +285,14 @@ public class ScheduleServiceImpl implements ScheduleService {
DateTime dateTime = DateUtil.offsetMonth(new Date(), -6);
String threeMonthsBeforeDate = dateTime.toString("yyyy-MM-dd HH:mm:ss");
sessionQueryWrapper.select("distinct uid");
sessionQueryWrapper.apply("UNIX_TIMESTAMP(gmt_create) >= UNIX_TIMESTAMP('{0}')", threeMonthsBeforeDate);
sessionQueryWrapper.apply("UNIX_TIMESTAMP(gmt_create) >= UNIX_TIMESTAMP('" + threeMonthsBeforeDate + "')");
List<Session> sessionList = sessionService.list(sessionQueryWrapper);
if (sessionList.size() > 0) {
List<String> uidList = sessionList.stream().map(Session::getUid).collect(Collectors.toList());
UpdateWrapper<Session> sessionUpdateWrapper = new UpdateWrapper<>();
sessionQueryWrapper.in("uid", uidList)
.and(t -> t.apply("UNIX_TIMESTAMP(gmt_create) < UNIX_TIMESTAMP('{0}')", threeMonthsBeforeDate));
.and(t -> t.apply("UNIX_TIMESTAMP(gmt_create) < UNIX_TIMESTAMP('" + threeMonthsBeforeDate + "')"));
boolean isSuccess = sessionService.remove(sessionUpdateWrapper);
if (!isSuccess) {

View File

@ -16,7 +16,11 @@ public interface FileService extends IService<File> {
List<List<String>> getContestRankExcelHead(List<String> contestProblemDisplayIDList, Boolean isACM);
List<List<Object>> changeACMContestRankToExcelRowList(List<ACMContestRankVo> acmContestRankVoList, List<String> contestProblemDisplayIDList);
List<List<Object>> changeACMContestRankToExcelRowList(List<ACMContestRankVo> acmContestRankVoList,
List<String> contestProblemDisplayIDList,
String rankShowName);
List<List<Object>> changOIContestRankToExcelRowList(List<OIContestRankVo> oiContestRankVoList, List<String> contestProblemDisplayIDList);
List<List<Object>> changOIContestRankToExcelRowList(List<OIContestRankVo> oiContestRankVoList,
List<String> contestProblemDisplayIDList,
String rankShowName);
}

View File

@ -34,37 +34,45 @@ public class FileServiceImpl extends ServiceImpl<FileMapper, File> implements Fi
}
@Override
public List<File> queryCarouselFileList(){
public List<File> queryCarouselFileList() {
return fileMapper.queryCarouselFileList();
}
@Override
public List<List<String>> getContestRankExcelHead(List<String> contestProblemDisplayIDList, Boolean isACM) {
List<List<String>> headList = new LinkedList<>();
List<String> head0 = new LinkedList<>();
head0.add("User ID");
head0.add("Rank");
List<String> head1 = new LinkedList<>();
head1.add("Username");
List<String> head2 = new LinkedList<>();
head2.add("Real Name");
head2.add("ShowName");
List<String> head3 = new LinkedList<>();
head3.add("Real Name");
List<String> head4 = new LinkedList<>();
head4.add("School");
headList.add(head0);
headList.add(head1);
headList.add(head2);
headList.add(head3);
headList.add(head4);
List<String> head3 = new LinkedList<>();
List<String> head5 = new LinkedList<>();
if (isACM) {
head3.add("AC");
List<String> head4 = new LinkedList<>();
head4.add("Total Submission");
List<String> head5 = new LinkedList<>();
head5.add("Total Penalty Time");
headList.add(head3);
headList.add(head4);
head5.add("AC");
List<String> head6 = new LinkedList<>();
head6.add("Total Submission");
List<String> head7 = new LinkedList<>();
head7.add("Total Penalty Time");
headList.add(head5);
headList.add(head6);
headList.add(head7);
} else {
head3.add("Total Score");
headList.add(head3);
head5.add("Total Score");
headList.add(head5);
}
// 添加题目头
@ -77,13 +85,25 @@ public class FileServiceImpl extends ServiceImpl<FileMapper, File> implements Fi
}
@Override
public List<List<Object>> changeACMContestRankToExcelRowList(List<ACMContestRankVo> acmContestRankVoList, List<String> contestProblemDisplayIDList) {
public List<List<Object>> changeACMContestRankToExcelRowList(List<ACMContestRankVo> acmContestRankVoList,
List<String> contestProblemDisplayIDList,
String rankShowName) {
List<List<Object>> allRowDataList = new LinkedList<>();
for (ACMContestRankVo acmContestRankVo : acmContestRankVoList) {
List<Object> rowData = new LinkedList<>();
rowData.add(acmContestRankVo.getUid());
rowData.add(acmContestRankVo.getRank() == -1 ? "*" : acmContestRankVo.getRank().toString());
rowData.add(acmContestRankVo.getUsername());
if ("username".equals(rankShowName)) {
rowData.add(acmContestRankVo.getUsername());
} else if ("realname".equals(rankShowName)) {
rowData.add(acmContestRankVo.getRealname());
} else if ("nickname".equals(rankShowName)) {
rowData.add(acmContestRankVo.getNickname());
} else {
rowData.add("");
}
rowData.add(acmContestRankVo.getRealname());
rowData.add(acmContestRankVo.getSchool());
rowData.add(acmContestRankVo.getAc());
rowData.add(acmContestRankVo.getTotal());
rowData.add(acmContestRankVo.getTotalTime());
@ -92,11 +112,23 @@ public class FileServiceImpl extends ServiceImpl<FileMapper, File> implements Fi
HashMap<String, Object> problemInfo = submissionInfo.getOrDefault(displayID, null);
if (problemInfo != null) { // 如果是有提交记录的
boolean isAC = (boolean) problemInfo.getOrDefault("isAC", false);
String info;
String info = "";
int errorNum = (int) problemInfo.getOrDefault("errorNum", 0);
int tryNum = (int) problemInfo.getOrDefault("tryNum", 0);
if (isAC) {
info = "AC(-" + problemInfo.getOrDefault("errorNum", 0) + ")";
if (errorNum == 0) {
info = "+(1)";
} else {
info = "-(" + (errorNum + 1) + ")";
}
} else {
info = "WA(-" + problemInfo.getOrDefault("errorNum", 0) + ")";
if (tryNum != 0 && errorNum != 0) {
info = "-(" + errorNum + "+" + tryNum + ")";
} else if (errorNum != 0) {
info = "-(" + errorNum + ")";
} else if (tryNum != 0) {
info = "?(" + tryNum + ")";
}
}
rowData.add(info);
} else {
@ -109,13 +141,24 @@ public class FileServiceImpl extends ServiceImpl<FileMapper, File> implements Fi
}
@Override
public List<List<Object>> changOIContestRankToExcelRowList(List<OIContestRankVo> oiContestRankVoList, List<String> contestProblemDisplayIDList) {
public List<List<Object>> changOIContestRankToExcelRowList(List<OIContestRankVo> oiContestRankVoList,
List<String> contestProblemDisplayIDList,
String rankShowName) {
List<List<Object>> allRowDataList = new LinkedList<>();
for (OIContestRankVo oiContestRankVo : oiContestRankVoList) {
List<Object> rowData = new LinkedList<>();
rowData.add(oiContestRankVo.getUid());
rowData.add(oiContestRankVo.getUsername());
rowData.add(oiContestRankVo.getRank() == -1 ? "*" : oiContestRankVo.getRank().toString());
if ("username".equals(rankShowName)) {
rowData.add(oiContestRankVo.getUsername());
} else if ("realname".equals(rankShowName)) {
rowData.add(oiContestRankVo.getRealname());
} else if ("nickname".equals(rankShowName)) {
rowData.add(oiContestRankVo.getNickname());
} else {
rowData.add("");
}
rowData.add(oiContestRankVo.getRealname());
rowData.add(oiContestRankVo.getSchool());
rowData.add(oiContestRankVo.getTotalScore());
HashMap<String, Integer> submissionInfo = oiContestRankVo.getSubmissionInfo();
for (String displayID : contestProblemDisplayIDList) {
@ -130,4 +173,6 @@ public class FileServiceImpl extends ServiceImpl<FileMapper, File> implements Fi
}
return allRowDataList;
}
}

View File

@ -30,16 +30,18 @@ public interface ContestRecordService extends IService<ContestRecord> {
IPage<ContestRecord> getACInfo(Integer currentPage, Integer limit, Integer status, Long cid, String contestCreatorId);
IPage<ACMContestRankVo> getContestACMRank(Boolean isOpenSealRank, Contest contest, int currentPage, int limit);
IPage<ACMContestRankVo> getContestACMRank(Boolean isOpenSealRank, Boolean removeStar, Contest contest, int currentPage, int limit);
IPage<OIContestRankVo> getContestOIRank(Long cid, String contestAuthor, Boolean isOpenSealRank, Date sealTime, Date startTime, Date endTime, int currentPage, int limit);
IPage<OIContestRankVo> getContestOIRank(Boolean isOpenSealRank, Boolean removeStar, Contest contest, int currentPage, int limit);
List<ContestRecordVo> getOIContestRecord(Long cid, String contestAuthor, Boolean isOpenSealRank, Date sealTime, Date startTime, Date endTime);
List<ContestRecordVo> getACMContestRecord(String username, Long cid);
List<UserInfo> getSuperAdminList();
List<ACMContestRankVo> getACMContestScoreboard(Boolean isOpenSealRank, Boolean removeStar, Contest contest);
List<OIContestRankVo> getOIContestScoreboard(Boolean isOpenSealRank, Boolean removeStar, Contest contest);
}

View File

@ -2,6 +2,8 @@ package top.hcode.hoj.service.contest.impl;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -9,6 +11,7 @@ import org.apache.shiro.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import top.hcode.hoj.common.result.CommonResult;
import top.hcode.hoj.dao.ContestProblemMapper;
import top.hcode.hoj.dao.JudgeMapper;
@ -211,15 +214,26 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
return userInfoMapper.getSuperAdminList();
}
@Override
public List<ACMContestRankVo> getACMContestScoreboard(Boolean isOpenSealRank, Boolean removeStar, Contest contest) {
List<ContestRecordVo> acmContestRecord = getACMContestRecord(contest.getAuthor(), contest.getId());
return calcACMRank(isOpenSealRank, removeStar, contest, acmContestRecord);
}
@Override
public IPage<ACMContestRankVo> getContestACMRank(Boolean isOpenSealRank, Contest contest,
public List<OIContestRankVo> getOIContestScoreboard(Boolean isOpenSealRank, Boolean removeStar, Contest contest) {
return getOIContestOrderRank(isOpenSealRank, removeStar, contest);
}
@Override
public IPage<ACMContestRankVo> getContestACMRank(Boolean isOpenSealRank, Boolean removeStar, Contest contest,
int currentPage, int limit) {
List<ContestRecordVo> acmContestRecord = getACMContestRecord(contest.getAuthor(), contest.getId());
// 进行排序计算
List<ACMContestRankVo> orderResultList = calcACMRank(isOpenSealRank, contest, acmContestRecord);
List<ACMContestRankVo> orderResultList = calcACMRank(isOpenSealRank, removeStar, contest, acmContestRecord);
// 计算好排行榜然后进行分页
Page<ACMContestRankVo> page = new Page<>(currentPage, limit);
int count = orderResultList.size();
@ -239,33 +253,9 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
@Override
public IPage<OIContestRankVo> getContestOIRank(Long cid, String contestAuthor, Boolean isOpenSealRank, Date sealTime,
Date startTime, Date endTime, int currentPage, int limit) {
// 获取每个用户每道题最近一次提交
if (!isOpenSealRank) {
// 超级管理员和比赛管理员选择强制刷新 或者 比赛结束
return getOIContestRank(cid, contestAuthor, false, sealTime, startTime, endTime, currentPage, limit);
} else {
String key = Constants.Contest.OI_CONTEST_RANK_CACHE.getName() + "_" + cid;
Page<OIContestRankVo> page = (Page<OIContestRankVo>) redisUtils.get(key);
if (page == null) {
page = getOIContestRank(cid, contestAuthor, true, sealTime, startTime, endTime, currentPage, limit);
redisUtils.set(key, page, 2 * 3600);
}
return page;
}
}
public Page<OIContestRankVo> getOIContestRank(Long cid, String contestAuthor, Boolean isOpenSealRank, Date sealTime,
Date startTime, Date endTime, int currentPage, int limit) {
List<ContestRecordVo> oiContestRecord = contestRecordMapper.getOIContestRecord(cid, contestAuthor, isOpenSealRank, sealTime, startTime, endTime);
// 计算排名
List<OIContestRankVo> orderResultList = calcOIRank(oiContestRecord);
public IPage<OIContestRankVo> getContestOIRank(Boolean isOpenSealRank, Boolean removeStar, Contest contest,
int currentPage, int limit) {
List<OIContestRankVo> orderResultList = getOIContestOrderRank(isOpenSealRank, removeStar, contest);
// 计算好排行榜然后进行分页
Page<OIContestRankVo> page = new Page<>(currentPage, limit);
int count = orderResultList.size();
@ -282,6 +272,29 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
return page;
}
public List<OIContestRankVo> getOIContestOrderRank(Boolean isOpenSealRank, Boolean removeStar, Contest contest) {
List<OIContestRankVo> orderResultList;
if (!isOpenSealRank) {
// 封榜解除 获取最新数据
// 获取每个用户每道题最近一次提交
List<ContestRecordVo> oiContestRecord = getOIContestRecord(contest.getId(),
contest.getAuthor(), false, contest.getSealRankTime(), contest.getStartTime(), contest.getEndTime());
// 计算排名
orderResultList = calcOIRank(oiContestRecord, contest, removeStar);
} else {
String key = Constants.Contest.OI_CONTEST_RANK_CACHE.getName() + "_" + contest.getId();
orderResultList = (List<OIContestRankVo>) redisUtils.get(key);
if (orderResultList == null) {
List<ContestRecordVo> oiContestRecord = getOIContestRecord(contest.getId(),
contest.getAuthor(), true, contest.getSealRankTime(), contest.getStartTime(), contest.getEndTime());
// 计算排名
orderResultList = calcOIRank(oiContestRecord, contest, removeStar);
redisUtils.set(key, orderResultList, 2 * 3600);
}
}
return orderResultList;
}
@Override
public List<ContestRecordVo> getOIContestRecord(Long cid, String contestAuthor, Boolean isOpenSealRank, Date sealTime, Date startTime, Date endTime) {
return contestRecordMapper.getOIContestRecord(cid, contestAuthor, isOpenSealRank, sealTime, startTime, endTime);
@ -293,7 +306,7 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
}
public List<ACMContestRankVo> calcACMRank(boolean isOpenSealRank, Contest contest, List<ContestRecordVo> contestRecordList) {
public List<ACMContestRankVo> calcACMRank(boolean isOpenSealRank, boolean removeStar, Contest contest, List<ContestRecordVo> contestRecordList) {
List<UserInfo> superAdminList = getSuperAdminList();
@ -403,10 +416,41 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
.thenComparing(ACMContestRankVo::getTotalTime) //再以总耗时升序
).collect(Collectors.toList());
// 需要打星的用户名列表
List<String> starAccountList = starAccountToList(contest.getStarAccount());
// 如果选择了移除打星队伍同时该用户属于打星队伍则将其移除
if (removeStar) {
orderResultList.removeIf(acmContestRankVo -> starAccountList.contains(acmContestRankVo.getUsername()));
}
int rankNum = 1;
int len = orderResultList.size();
for (int i = 0; i < len; i++) {
ACMContestRankVo currentACMRankVo = orderResultList.get(i);
if (starAccountList.contains(currentACMRankVo.getUsername())) {
// 打星队伍排名为-1
currentACMRankVo.setRank(-1);
} else {
if (i != 0) {
ACMContestRankVo lastACMRankVo = orderResultList.get(i - 1);
// 当前用户的总罚时和AC数跟前一个用户一样的话排名则一样
if (lastACMRankVo.getAc().equals(currentACMRankVo.getAc())
&& lastACMRankVo.getTotalTime().equals(currentACMRankVo.getTotalTime())) {
currentACMRankVo.setRank(lastACMRankVo.getRank());
} else {
currentACMRankVo.setRank(rankNum);
}
} else {
currentACMRankVo.setRank(rankNum);
}
rankNum++;
}
}
return orderResultList;
}
public List<OIContestRankVo> calcOIRank(List<ContestRecordVo> oiContestRecord) {
public List<OIContestRankVo> calcOIRank(List<ContestRecordVo> oiContestRecord, Contest contest, Boolean removeStar) {
List<UserInfo> superAdminList = getSuperAdminList();
@ -501,6 +545,38 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
.sorted(Comparator.comparing(OIContestRankVo::getTotalScore, Comparator.reverseOrder())
.thenComparing(OIContestRankVo::getTotalTime, Comparator.naturalOrder()))
.collect(Collectors.toList());
// 需要打星的用户名列表
List<String> starAccountList = starAccountToList(contest.getStarAccount());
// 如果选择了移除打星队伍同时该用户属于打星队伍则将其移除
if (removeStar) {
orderResultList.removeIf(acmContestRankVo -> starAccountList.contains(acmContestRankVo.getUsername()));
}
int rankNum = 1;
int len = orderResultList.size();
for (int i = 0; i < len; i++) {
OIContestRankVo currentOIRankVo = orderResultList.get(i);
if (starAccountList.contains(currentOIRankVo.getUsername())) {
// 打星队伍排名为-1
currentOIRankVo.setRank(-1);
} else {
if (i != 0) {
OIContestRankVo lastOIRankVo = orderResultList.get(i - 1);
// 当前用户的程序总运行时间和总得分跟前一个用户一样的话排名则一样
if (lastOIRankVo.getTotalScore().equals(currentOIRankVo.getTotalScore())
&& lastOIRankVo.getTotalTime().equals(currentOIRankVo.getTotalTime())) {
currentOIRankVo.setRank(lastOIRankVo.getRank());
} else {
currentOIRankVo.setRank(rankNum);
}
} else {
currentOIRankVo.setRank(rankNum);
}
rankNum++;
}
}
return orderResultList;
}
@ -508,4 +584,12 @@ public class ContestRecordServiceImpl extends ServiceImpl<ContestRecordMapper, C
private boolean isInSealTimeSubmission(Contest contest, Date submissionDate) {
return DateUtil.isIn(submissionDate, contest.getSealRankTime(), contest.getEndTime());
}
private List<String> starAccountToList(String starAccountStr) {
if (StringUtils.isEmpty(starAccountStr)) {
return new ArrayList<>();
}
JSONObject jsonObject = JSONUtil.parseObj(starAccountStr);
return jsonObject.get("star_account", List.class);
}
}

View File

@ -93,7 +93,7 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, Contest> impl
@Override
public Boolean isSealRank(String uid, Contest contest, Boolean forceRefresh, Boolean isRoot) {
// 如果是管理员同时选择强制刷新榜单则封榜无效
if (forceRefresh && (isRoot || uid.equals(contest.getUid()))) {
if (forceRefresh && (isRoot || contest.getUid().equals(uid))) {
return false;
} else if (contest.getSealRank() && contest.getSealRankTime() != null) { // 该比赛开启封榜模式
Date now = new Date();

View File

@ -386,6 +386,9 @@ public class ProblemServiceImpl extends ServiceImpl<ProblemMapper, Problem> impl
String testcaseDir = problemDto.getUploadTestcaseDir();
// 如果是io题目统计总分
List<ProblemCase> problemCases = problemDto.getSamples();
if (problemCases.size() == 0) {
throw new RuntimeException("The test cases of problem must not be empty!");
}
for (ProblemCase problemCase : problemCases) {
if (problemCase.getScore() != null) {
sumScore += problemCase.getScore();

View File

@ -7,16 +7,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import top.hcode.hoj.pojo.entity.judge.Judge;
import top.hcode.hoj.pojo.entity.judge.JudgeServer;
import top.hcode.hoj.pojo.entity.judge.RemoteJudgeAccount;
import top.hcode.hoj.remoteJudge.task.RemoteJudgeFactory;
import top.hcode.hoj.remoteJudge.task.RemoteJudgeStrategy;
import top.hcode.hoj.service.impl.JudgeServerServiceImpl;
import top.hcode.hoj.service.impl.JudgeServiceImpl;
import top.hcode.hoj.service.impl.RemoteJudgeAccountServiceImpl;
import top.hcode.hoj.service.impl.RemoteJudgeServiceImpl;
import top.hcode.hoj.util.Constants;
import java.util.Map;
@ -35,10 +32,7 @@ public class RemoteJudgeGetResult {
private JudgeServiceImpl judgeService;
@Autowired
private RemoteJudgeAccountServiceImpl remoteJudgeAccountService;
@Autowired
private JudgeServerServiceImpl judgeServerService;
private RemoteJudgeServiceImpl remoteJudgeService;
@Transactional
@Async
@ -65,8 +59,8 @@ public class RemoteJudgeGetResult {
.eq("submit_id", submitId);
judgeService.update(judgeUpdateWrapper);
changeAccountStatus(remoteJudge, username);
changeServerSubmitCFStatus(ip, port);
remoteJudgeService.changeAccountStatus(remoteJudge, username);
remoteJudgeService.changeServerSubmitCFStatus(ip, port);
scheduler.shutdown();
return;
@ -81,8 +75,8 @@ public class RemoteJudgeGetResult {
status.intValue() != Constants.Judge.STATUS_COMPILING.getStatus()) {
// 将账号变为可用
changeAccountStatus(remoteJudge, username);
changeServerSubmitCFStatus(ip, port);
remoteJudgeService.changeAccountStatus(remoteJudge, username);
remoteJudgeService.changeServerSubmitCFStatus(ip, port);
Integer time = (Integer) result.getOrDefault("time", null);
Integer memory = (Integer) result.getOrDefault("memory", null);
@ -141,41 +135,6 @@ public class RemoteJudgeGetResult {
}
public void changeAccountStatus(String remoteJudge, String username) {
UpdateWrapper<RemoteJudgeAccount> remoteJudgeAccountUpdateWrapper = new UpdateWrapper<>();
remoteJudgeAccountUpdateWrapper.set("status", true)
.eq("status", false)
.eq("username", username);
if (remoteJudge.equals("GYM")) {
remoteJudgeAccountUpdateWrapper.eq("oj", "CF");
} else {
remoteJudgeAccountUpdateWrapper.eq("oj", remoteJudge);
}
boolean isOk = remoteJudgeAccountService.update(remoteJudgeAccountUpdateWrapper);
if (!isOk) {
log.error("远程判题:修正账号为可用状态失败----------->{}", "oj:" + remoteJudge + ",username:" + username);
}
}
public void changeServerSubmitCFStatus(String ip, Integer port) {
if (StringUtils.isEmpty(ip) || port == null) {
return;
}
UpdateWrapper<JudgeServer> JudgeServerUpdateWrapper = new UpdateWrapper<>();
JudgeServerUpdateWrapper.set("cf_submittable", true)
.eq("ip", ip)
.eq("is_remote", true)
.eq("port", port);
boolean isOk = judgeServerService.update(JudgeServerUpdateWrapper);
if (!isOk) {
log.error("远程判题修正判题机对CF可提交状态为可用的状态失败----------->{}", "ip:" + ip + ",port:" + port);
}
}
}

View File

@ -7,13 +7,10 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import top.hcode.hoj.pojo.entity.judge.Judge;
import top.hcode.hoj.pojo.entity.judge.JudgeServer;
import top.hcode.hoj.pojo.entity.judge.RemoteJudgeAccount;
import top.hcode.hoj.remoteJudge.task.RemoteJudgeFactory;
import top.hcode.hoj.remoteJudge.task.RemoteJudgeStrategy;
import top.hcode.hoj.service.impl.JudgeServerServiceImpl;
import top.hcode.hoj.service.impl.JudgeServiceImpl;
import top.hcode.hoj.service.impl.RemoteJudgeAccountServiceImpl;
import top.hcode.hoj.service.impl.RemoteJudgeServiceImpl;
import top.hcode.hoj.util.Constants;
import java.util.Map;
@ -22,9 +19,6 @@ import java.util.Map;
@Slf4j(topic = "hoj")
public class RemoteJudgeToSubmit {
@Autowired
private RemoteJudgeAccountServiceImpl remoteJudgeAccountService;
@Autowired
private RemoteJudgeGetResult remoteJudgeGetResult;
@ -32,7 +26,7 @@ public class RemoteJudgeToSubmit {
private JudgeServiceImpl judgeService;
@Autowired
private JudgeServerServiceImpl judgeServerService;
private RemoteJudgeServiceImpl remoteJudgeService;
@Value("${hoj-judge-server.name}")
@ -63,18 +57,18 @@ public class RemoteJudgeToSubmit {
try {
submitResult = remoteJudgeStrategy.submit(username, password, remotePid, language, userCode);
} catch (Exception e) {
log.error("{%s}", e);
log.error("{}", e);
errLog = e.getMessage();
}
// 提交失败 前端手动按按钮再次提交 修改状态 STATUS_SUBMITTED_FAILED
if (submitResult == null || (Long) submitResult.getOrDefault("runId", -1L) == -1L) {
// 将使用的账号放回对应列表
changeAccountStatus(remoteJudge, username);
remoteJudgeService.changeAccountStatus(remoteJudge, username);
if (remoteJudge.equals(Constants.RemoteJudge.GYM_JUDGE.getName())
|| remoteJudge.equals(Constants.RemoteJudge.CF_JUDGE.getName())) {
// 对CF特殊归还账号及判题机权限
changeServerSubmitCFStatus(serverIp, serverPort);
remoteJudgeService.changeServerSubmitCFStatus(serverIp, serverPort);
}
// 更新此次提交状态为提交失败
@ -113,37 +107,4 @@ public class RemoteJudgeToSubmit {
vjudgeSubmitId, (String) submitResult.get("cookies"),serverIp,serverPort);
}
public void changeAccountStatus(String remoteJudge, String username) {
UpdateWrapper<RemoteJudgeAccount> remoteJudgeAccountUpdateWrapper = new UpdateWrapper<>();
remoteJudgeAccountUpdateWrapper.set("status", true)
.eq("status", false)
.eq("username", username);
if (remoteJudge.equals("GYM")) {
remoteJudgeAccountUpdateWrapper.eq("oj", "CF");
} else {
remoteJudgeAccountUpdateWrapper.eq("oj", remoteJudge);
}
boolean isOk = remoteJudgeAccountService.update(remoteJudgeAccountUpdateWrapper);
if (!isOk) {
log.error("远程判题:修正账号为可用状态失败----------->{}", "oj:" + remoteJudge + ",username:" + username);
}
}
public void changeServerSubmitCFStatus(String ip, Integer port) {
UpdateWrapper<JudgeServer> JudgeServerUpdateWrapper = new UpdateWrapper<>();
JudgeServerUpdateWrapper.set("cf_submittable", true)
.eq("ip", ip)
.eq("is_remote", true)
.eq("port", port);
boolean isOk = judgeServerService.update(JudgeServerUpdateWrapper);
if (!isOk) {
log.error("远程判题修正判题机对CF可提交状态为可用的状态失败----------->{}", "ip:" + ip + ",port:" + port);
}
}
}

View File

@ -90,6 +90,7 @@ public class CodeForcesJudge implements RemoteJudgeStrategy {
submitCode(contestId, problemNum, getLanguage(language), userCode);
} catch (HttpException e) {
// 如果提交出现403可能是cookie失效了再执行登录重新提交
this.cookies = null;
Map<String, Object> loginUtils = getLoginUtils(username, password);
int status = (int) loginUtils.get("status");
if (status != HttpStatus.SC_MOVED_TEMPORARILY) {
@ -144,7 +145,7 @@ public class CodeForcesJudge implements RemoteJudgeStrategy {
}
}
try {
TimeUnit.MILLISECONDS.sleep(2500);
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}

View File

@ -0,0 +1,8 @@
package top.hcode.hoj.service;
public interface RemoteJudgeService {
public void changeAccountStatus(String remoteJudge, String username);
public void changeServerSubmitCFStatus(String ip, Integer port);
}

View File

@ -0,0 +1,111 @@
package top.hcode.hoj.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import top.hcode.hoj.pojo.entity.judge.JudgeServer;
import top.hcode.hoj.pojo.entity.judge.RemoteJudgeAccount;
import top.hcode.hoj.service.JudgeServerService;
import top.hcode.hoj.service.RemoteJudgeAccountService;
import top.hcode.hoj.service.RemoteJudgeService;
/**
* @Author: Himit_ZH
* @Date: 2021/12/7 23:57
* @Description:
*/
@Service
@Slf4j(topic = "hoj")
public class RemoteJudgeServiceImpl implements RemoteJudgeService {
@Autowired
private RemoteJudgeAccountService remoteJudgeAccountService;
@Autowired
private JudgeServerService judgeServerService;
@Override
public void changeAccountStatus(String remoteJudge, String username) {
UpdateWrapper<RemoteJudgeAccount> remoteJudgeAccountUpdateWrapper = new UpdateWrapper<>();
remoteJudgeAccountUpdateWrapper.set("status", true)
.eq("status", false)
.eq("username", username);
if (remoteJudge.equals("GYM")) {
remoteJudge = "CF";
}
remoteJudgeAccountUpdateWrapper.eq("oj", remoteJudge);
boolean isOk = remoteJudgeAccountService.update(remoteJudgeAccountUpdateWrapper);
if (!isOk) { // 重试8次
tryAgainUpdateAccount(remoteJudgeAccountUpdateWrapper, remoteJudge, username);
}
}
private void tryAgainUpdateAccount(UpdateWrapper<RemoteJudgeAccount> updateWrapper, String remoteJudge, String username) {
boolean retryable;
int attemptNumber = 0;
do {
boolean success = remoteJudgeAccountService.update(updateWrapper);
if (success) {
return;
} else {
attemptNumber++;
retryable = attemptNumber < 8;
if (attemptNumber == 8) {
log.error("远程判题:修正账号为可用状态失败----------->{}", "oj:" + remoteJudge + ",username:" + username);
break;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} while (retryable);
}
@Override
public void changeServerSubmitCFStatus(String ip, Integer port) {
if (StringUtils.isEmpty(ip) || port == null) {
return;
}
UpdateWrapper<JudgeServer> judgeServerUpdateWrapper = new UpdateWrapper<>();
judgeServerUpdateWrapper.set("cf_submittable", true)
.eq("ip", ip)
.eq("is_remote", true)
.eq("port", port);
boolean isOk = judgeServerService.update(judgeServerUpdateWrapper);
if (!isOk) { // 重试8次
tryAgainUpdateServer(judgeServerUpdateWrapper, ip, port);
}
}
private void tryAgainUpdateServer(UpdateWrapper<JudgeServer> updateWrapper, String ip, Integer port) {
boolean retryable;
int attemptNumber = 0;
do {
boolean success = judgeServerService.update(updateWrapper);
if (success) {
return;
} else {
attemptNumber++;
retryable = attemptNumber < 8;
if (attemptNumber == 8) {
log.error("远程判题修正判题机对CF可提交状态为可用的状态失败----------->{}", "ip:" + ip + ",port:" + port);
break;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} while (retryable);
}
}

View File

@ -93,6 +93,12 @@ public class Contest implements Serializable {
@ApiModelProperty(value = "排行榜显示username、nickname、realname")
private String rankShowName;
@ApiModelProperty(value = "打星用户列表 {\"star_account\":['a','b']}")
private String starAccount;
@ApiModelProperty(value = "是否开放比赛榜单")
private Boolean openRank;
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;

View File

@ -274,7 +274,7 @@ a:hover {
font-weight: normal;
color: dimgrey;
}
.female-flag {
.contest-rank-flag {
margin-right: 20px !important;
background-color: rgb(255, 193, 10);
border-radius: 4px;
@ -285,6 +285,9 @@ a:hover {
.bg-female {
background-color: rgb(255, 153, 203);
}
.bg-star {
background-color: #ffffcc;
}
.oi-100 {
background-color: #19be6b;

View File

@ -411,6 +411,18 @@ const ojApi = {
params: {cid}
})
},
// 获取赛外榜单比赛的信息
getScoreBoardContestInfo(cid){
return ajax('/api/get-contest-outsize-info','get',{
params: {cid}
})
},
// 提供比赛外榜排名数据
getContestOutsideScoreboard(params){
return ajax('/api/get-contest-outside-scoreboard','get',{
params: params
})
},
// 注册私有比赛权限
registerContest(cid,password){
return ajax('/api/register-contest','post',{

View File

@ -40,7 +40,7 @@
layout="prev, pager, next"
@current-change="getPublicProblem"
:page-size="limit"
:current-page="page"
:current-page.sync="page"
:total="total"
>
</el-pagination>

View File

@ -319,13 +319,16 @@ export const m = {
Contest_Duration_Check:'The duration of the contest cannot be less than or equal to zero!',
Contets_Time_Check:'The start time should be earlier than the end time!',
Print_Func:'Print Function',
Open:'Open',
Not_Support_Print:'Not Support Print',
Support_Offline_Print:'Support Offline Print',
Add_Star_User_Error:'Please do not add existing star user repeatedly!',
Star_User_UserName:'Star User (Please use login username)',
Rank_Show_Name:'The Name Showed in The Rank',
Show_Username:'Username',
Show_Nickname:'Nickname',
Show_Realname:'Real name',
Account_Limit:'Account Limit',
Account_Limit:'Account Limit (Login Username)',
The_allowed_account_will_be:'The allowed username will be ',
// /views/admin/discussion/Discussion.vue

View File

@ -319,11 +319,14 @@ export const m = {
Print_Func:'打印功能',
Not_Support_Print:'不支持打印',
Support_Offline_Print:'支持线下打印',
Open:'开启',
Add_Star_User_Error:'请不要重复添加已有打星用户!',
Star_User_UserName:'打星用户(请使用登录用户名)',
Rank_Show_Name:'榜单显示用户名称',
Show_Username:'用户名',
Show_Nickname:'昵称',
Show_Realname:'真实姓名',
Account_Limit:'账号限制',
Account_Limit:'账号限制(登录用户名)',
The_allowed_account_will_be:'允许参加比赛的用户名是:',
// /views/admin/discussion/Discussion.vue

View File

@ -306,6 +306,7 @@ export const m = {
Public_Tips:'Public - Any one can see and submit.',
Private_Tips:'Private - Only users knowing contest password can see and submit.',
Protected_Tips:'Protected - Any one can see, but only users knowing contest password can submit.',
Contest_Outside_ScoreBoard:'OutSide Contest ScoreBoard',
// /views/oj/contest/ContestDetail.vue
StartAt: 'StartAt',
@ -334,9 +335,11 @@ export const m = {
RealName: 'RealName',
Force_Update: 'Force Update',
Download_as_CSV: 'Download as CSV',
TotalTime: 'TotalTime',
TotalTime: 'Time',
Top_10_Teams: 'Top 10 Teams',
save_as_image: 'save as image',
Contest_Rank_Seq:'Rank',
Star_User:'Star User',
// /views/oj/contest/children/ACMInfo.vue
AC_Time: 'AC Time',

View File

@ -309,6 +309,7 @@ export const m = {
Public_Tips:'公开赛 - 每个用户都可查看与提交',
Private_Tips:'私有赛 - 用户需要密码才可查看与提交',
Protected_Tips:'保护赛 - 每个用户都可查看,但是提交需要密码',
Contest_Outside_ScoreBoard:'赛外榜单',
// /views/oj/contest/ContestDetail.vue
StartAt: '开始时间',
@ -340,6 +341,8 @@ export const m = {
TotalTime: '总时间',
Top_10_Teams: 'Top 10 Teams',
save_as_image: '保存成图片',
Contest_Rank_Seq:'排名',
Star_User:'打星用户',
// /views/oj/contest/children/ACMInfo.vue
AC_Time: 'AC 时间',

View File

@ -7,6 +7,8 @@ import Logout from "@/views/oj/user/Logout.vue"
import SubmissionList from "@/views/oj/status/SubmissionList.vue"
import SubmissionDetails from "@/views/oj/status/SubmissionDetails.vue"
import ContestList from "@/views/oj/contest/ContestList.vue"
import ACMScoreBoard from "@/views/oj/contest/outside/ACMScoreBoard.vue"
import OIScoreBoard from "@/views/oj/contest/outside/OIScoreBoard.vue"
import Problem from "@/views/oj/problem/Problem.vue"
import ACMRank from "@/views/oj/rank/ACMRank.vue"
import OIRank from "@/views/oj/rank/OIRank.vue"
@ -74,6 +76,18 @@ const ojRoutes = [
component: ContestList,
meta: { title: 'Contest' }
},
{
path: '/contest/acm-scoreboard/:contestID',
name: 'ACMScoreBoard',
component: ACMScoreBoard,
meta: { title: 'ACM Contest ScoreBoard' }
},
{
path: '/contest/oi-scoreboard/:contestID',
name: 'OIScoreBoard',
component: OIScoreBoard,
meta: { title: 'OI Contest ScoreBoard' }
},
{
name: 'ContestDetails',
path: '/contest/:contestID/',

View File

@ -6,18 +6,19 @@ const state = {
now: moment(),
intoAccess: false, // 比赛进入权限
submitAccess:false, // 保护比赛的提交权限
forceUpdate: false,
forceUpdate: false, // 强制实时榜单
removeStar: false, // 榜单去除打星队伍
contest: {
auth: CONTEST_TYPE.PUBLIC,
openPrint: false,
rankShowName:'username'
rankShowName:'username',
},
contestProblems: [],
itemVisible: {
table: true,
chart: true,
},
disPlayIdMapColor:{} // 展示id对应的气球颜色
disPlayIdMapColor:{}, // 展示id对应的气球颜色
}
const getters = {
@ -154,6 +155,9 @@ const mutations = {
changeRankForceUpdate (state, payload) {
state.forceUpdate = payload.value
},
changeRankRemoveStar(state, payload){
state.removeStar = payload.value
},
changeContestProblems(state, payload) {
state.contestProblems = payload.contestProblems;
let tmp={};
@ -182,6 +186,7 @@ const mutations = {
realName: false
}
state.forceUpdate = false
state.removeStar = false
},
now(state, payload) {
state.now = payload.now
@ -209,6 +214,21 @@ const actions = {
})
})
},
getScoreBoardContestInfo ({commit, rootState, dispatch}) {
return new Promise((resolve, reject) => {
api.getScoreBoardContestInfo(rootState.route.params.contestID).then((res) => {
resolve(res)
let contest = res.data.data.contest;
let problemList = res.data.data.problemList;
commit('changeContest', {contest: contest})
commit('changeContestProblems', {contestProblems: problemList})
commit('now', {now: moment(contest.now)})
}, err => {
reject(err)
})
})
},
getContestProblems ({commit, rootState}) {
return new Promise((resolve, reject) => {
api.getContestProblemList(rootState.route.params.contestID).then(res => {

View File

@ -115,7 +115,7 @@
</el-form-item>
</el-col>
<el-col :span="24">
<el-col :md="8" :xs="24">
<el-form-item
:label="$t('m.Auto_Real_Rank')"
required
@ -130,7 +130,18 @@
</el-form-item>
</el-col>
<el-col :span="24">
<el-col :md="8" :xs="24">
<el-form-item :label="$t('m.Contest_Outside_ScoreBoard')" required>
<el-switch
v-model="contest.openRank"
:active-text="$t('m.Open')"
:inactive-text="$t('m.Close')"
>
</el-switch>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item :label="$t('m.Print_Func')" required>
<el-switch
v-model="contest.openPrint"
@ -157,6 +168,45 @@
</el-form-item>
</el-col>
<el-col>
<el-form-item :label="$t('m.Star_User_UserName')" required>
<el-tag
v-for="username in contest.starAccount"
closable
:close-transition="false"
:key="username"
type="warning"
size="medium"
@close="removeStarUser(username)"
style="margin-right: 7px;margin-top:4px"
>{{ username }}</el-tag
>
<el-input
v-if="inputVisible"
size="medium"
class="input-new-star-user"
v-model="starUserInput"
:trigger-on-focus="true"
@keyup.enter.native="addStarUser"
@blur="addStarUser"
>
</el-input>
<el-tooltip
effect="dark"
:content="$t('m.Add')"
placement="top"
v-else
>
<el-button
class="button-new-tag"
size="small"
@click="inputVisible = true"
icon="el-icon-plus"
></el-button>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :md="8" :xs="24">
<el-form-item :label="$t('m.Contest_Auth')" required>
<el-select v-model="contest.auth">
@ -298,6 +348,7 @@ export default {
rankShowName: 'username',
openAccountLimit: false,
accountLimitRule: '',
starAccount: [],
},
formRule: {
prefix: '',
@ -306,6 +357,8 @@ export default {
number_to: 10,
extra_account: '',
},
starUserInput: '',
inputVisible: false,
};
},
mounted() {
@ -500,6 +553,29 @@ export default {
extra_account: tmp[5],
};
},
addStarUser() {
if (this.starUserInput) {
for (var i = 0; i < this.contest.starAccount.length; i++) {
if (this.contest.starAccount[i] == this.starUserInput) {
myMessage.warning(this.$i18n.t('m.Add_Star_User_Error'));
this.starUserInput = '';
return;
}
}
this.contest.starAccount.push(this.starUserInput);
this.inputVisible = false;
this.starUserInput = '';
}
},
// UserName
removeStarUser(username) {
this.contest.starAccount.splice(
this.contest.starAccount.map((item) => item.name).indexOf(username),
1
);
},
},
};
</script>
@ -512,4 +588,7 @@ export default {
font-size: 16px;
margin-bottom: 10px;
}
.input-new-star-user {
width: 200px;
}
</style>

View File

@ -8,7 +8,7 @@
<span class="panel-title">{{ contest.title }}</span>
</div>
</div>
<el-row>
<el-row style="margin-top: 10px;">
<el-col :span="12" class="text-align:left">
<el-tooltip
v-if="contest.auth != null && contest.auth != undefined"
@ -91,7 +91,7 @@
<el-button
type="primary"
@click="checkPassword"
style="float:right;margin:5px"
style="float:right;"
>{{ $t('m.Enter') }}</el-button
>
</el-form>
@ -444,6 +444,11 @@ export default {
};
</script>
<style scoped>
.panel-title {
font-variant: small-caps;
font-size: 1.5rem !important;
font-weight: 500;
}
@media screen and (min-width: 768px) {
.contest-time .left {
text-align: left;

View File

@ -176,6 +176,23 @@
</el-tag>
</el-tooltip>
</li>
<li v-if="contest.openRank">
<el-tooltip
:content="$t('m.Contest_Outside_ScoreBoard')"
placement="top"
effect="dark"
>
<el-button
circle
size="small"
type="primary"
icon="el-icon-data-analysis"
@click="
toContestOutsideScoreBoard(contest.id, contest.type)
"
></el-button>
</el-tooltip>
</li>
</ul>
</el-col>
<el-col
@ -312,6 +329,19 @@ export default {
});
}
},
toContestOutsideScoreBoard(cid, type) {
if (type == 0) {
this.$router.push({
name: 'ACMScoreBoard',
params: { contestID: cid },
});
} else if (type == 1) {
this.$router.push({
name: 'OIScoreBoard',
params: { contestID: cid },
});
}
},
getDuration(startTime, endTime) {
return time.formatSpecificDuration(startTime, endTime);
},

View File

@ -14,6 +14,13 @@
<span>{{ $t('m.Table') }}</span>
<el-switch v-model="showTable"></el-switch>
</p>
<p>
<span>{{ $t('m.Star_User') }}</span>
<el-switch
v-model="showStarUser"
@change="getContestRankData(page)"
></el-switch>
</p>
<p>
<span>{{ $t('m.Auto_Refresh') }}(10s)</span>
<el-switch
@ -25,7 +32,10 @@
<template v-if="isContestAdmin">
<p>
<span>{{ $t('m.Force_Update') }}</span>
<el-switch v-model="forceUpdate"></el-switch>
<el-switch
v-model="forceUpdate"
@change="getContestRankData(page)"
></el-switch>
</p>
</template>
<template>
@ -49,14 +59,17 @@
align="center"
:data="dataRank"
:cell-class-name="cellClassName"
:seq-config="{ startIndex: (this.page - 1) * this.limit }"
>
<vxe-table-column
field="id"
type="seq"
field="rank"
width="50"
fixed="left"
></vxe-table-column>
:title="$t('m.Contest_Rank_Seq')"
>
<template v-slot="{ row }">
{{ row.rank == -1 ? '*' : row.rank }}
</template>
</vxe-table-column>
<vxe-table-column
field="username"
fixed="left"
@ -79,9 +92,13 @@
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="contest-username"
><span class="female-flag" v-if="row.gender == 'female'"
><span class="contest-rank-flag" v-if="row.rank == -1"
>Star</span
>
<span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>{{ row[contest.rankShowName] }}</span
>
{{ row[contest.rankShowName] }}</span
>
<span class="contest-school" v-if="row.school">{{
row.school
@ -111,9 +128,13 @@
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="contest-username"
><span class="female-flag" v-if="row.gender == 'female'"
><span class="contest-rank-flag" v-if="row.rank == -1"
>Star</span
>
<span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>{{ row[contest.rankShowName] }}</span
>
{{ row[contest.rankShowName] }}</span
>
<span class="contest-school" v-if="row.school">{{
row.school
@ -387,7 +408,7 @@ export default {
}
if (
column.property !== 'id' &&
column.property !== 'rank' &&
column.property !== 'rating' &&
column.property !== 'totalTime' &&
column.property !== 'username' &&
@ -432,6 +453,9 @@ export default {
}
});
dataRank[i].cellClassName = cellClass;
if (dataRank[i].rank == -1) {
dataRank[i].userCellClassName = 'bg-star';
}
if (dataRank[i].gender == 'female') {
dataRank[i].userCellClassName = 'bg-female';
}

View File

@ -14,6 +14,13 @@
<span>{{ $t('m.Table') }}</span>
<el-switch v-model="showTable"></el-switch>
</p>
<p>
<span>{{ $t('m.Star_User') }}</span>
<el-switch
v-model="showStarUser"
@change="getContestRankData(page)"
></el-switch>
</p>
<p>
<span>{{ $t('m.Auto_Refresh') }}(10s)</span>
<el-switch
@ -25,7 +32,10 @@
<template v-if="isContestAdmin">
<p>
<span>{{ $t('m.Force_Update') }}</span>
<el-switch v-model="forceUpdate"></el-switch>
<el-switch
v-model="forceUpdate"
@change="getContestRankData(page)"
></el-switch>
</p>
</template>
<template>
@ -50,14 +60,18 @@
ref="OIContestRank"
:data="dataRank"
:cell-class-name="cellClassName"
:seq-config="{ startIndex: (this.page - 1) * this.limit }"
>
<vxe-table-column
field="id"
type="seq"
min-width="50"
field="rank"
width="50"
fixed="left"
></vxe-table-column>
:title="$t('m.Contest_Rank_Seq')"
>
<template v-slot="{ row }">
{{ row.rank == -1 ? '*' : row.rank }}
</template>
</vxe-table-column>
<vxe-table-column
field="username"
fixed="left"
@ -80,9 +94,13 @@
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="contest-username"
><span class="female-flag" v-if="row.gender == 'female'"
><span class="contest-rank-flag" v-if="row.rank == -1"
>Star</span
>
<span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>{{ row[contest.rankShowName] }}</span
>
{{ row[contest.rankShowName] }}</span
>
<span class="contest-school" v-if="row.school">{{
row.school
@ -93,6 +111,7 @@
</vxe-table-column>
<vxe-table-column
field="username"
fixed="left"
v-else
min-width="300"
:title="$t('m.User')"
@ -112,9 +131,13 @@
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="contest-username"
><span class="female-flag" v-if="row.gender == 'female'"
><span class="contest-rank-flag" v-if="row.rank == -1"
>Star</span
>
<span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>{{ row[contest.rankShowName] }}</span
>
{{ row[contest.rankShowName] }}</span
>
<span class="contest-school" v-if="row.school">{{
row.school
@ -123,6 +146,7 @@
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="realname"
min-width="96"
@ -309,7 +333,7 @@ export default {
}
if (
column.property !== 'id' &&
column.property !== 'rank' &&
column.property !== 'totalScore' &&
column.property !== 'username' &&
column.property !== 'realname'
@ -372,6 +396,9 @@ export default {
}
});
dataRank[i].cellClassName = cellClass;
if (dataRank[i].rank == -1) {
dataRank[i].userCellClassName = 'bg-star';
}
if (dataRank[i].gender == 'female') {
dataRank[i].userCellClassName = 'bg-female';
}

View File

@ -12,7 +12,8 @@ export default {
currentPage:page,
limit: this.limit,
cid: this.$route.params.contestID,
forceRefresh: this.forceUpdate ? true: false
forceRefresh: this.forceUpdate ? true: false,
removeStar: !this.showStarUser
}
api.getContestRank(params).then(res => {
if (this.showChart && !refresh) {
@ -28,8 +29,7 @@ export default {
handleAutoRefresh (status) {
if (status == true) {
this.refreshFunc = setInterval(() => {
this.page = 1
this.getContestRankData(1, true)
this.getContestRankData(this.page, true)
}, 10000)
} else {
clearInterval(this.refreshFunc)
@ -50,6 +50,14 @@ export default {
this.$store.commit('changeContestItemVisible', {chart: value})
}
},
showStarUser:{
get () {
return !this.$store.state.contest.removeStar
},
set (value) {
this.$store.commit('changeRankRemoveStar', {value: !value})
}
},
showTable: {
get () {
return this.$store.state.contest.itemVisible.table
@ -67,7 +75,7 @@ export default {
}
},
refreshDisabled () {
return this.contest.status === CONTEST_STATUS.ENDED
return this.contest.status == CONTEST_STATUS.ENDED
}
},
beforeDestroy () {

View File

@ -0,0 +1,478 @@
<template>
<div>
<el-card shadow>
<div class="contest-title">
<div slot="header">
<span class="panel-title">{{ contest.title }}</span>
</div>
</div>
<el-row style="margin-top: 10px;">
<el-col :span="12" class="text-align:left">
<el-tooltip
v-if="contest.auth != null && contest.auth != undefined"
:content="$t('m.' + CONTEST_TYPE_REVERSE[contest.auth]['tips'])"
placement="top"
>
<el-tag
:type.sync="CONTEST_TYPE_REVERSE[contest.auth]['color']"
effect="plain"
>
{{ $t('m.' + CONTEST_TYPE_REVERSE[contest.auth]['name']) }}
</el-tag>
</el-tooltip>
</el-col>
<el-col :span="12" style="text-align:right">
<el-button size="small" plain v-if="contest != null">
{{ contest.type | parseContestType }}
</el-button>
</el-col>
</el-row>
<div class="contest-time">
<el-row>
<el-col :xs="24" :md="12" class="left">
<p>
<i class="fa fa-hourglass-start" aria-hidden="true"></i>
{{ $t('m.StartAt') }}{{ contest.startTime | localtime }}
</p>
</el-col>
<el-col :xs="24" :md="12" class="right">
<p>
<i class="fa fa-hourglass-end" aria-hidden="true"></i>
{{ $t('m.EndAt') }}{{ contest.endTime | localtime }}
</p>
</el-col>
</el-row>
</div>
<div class="slider">
<el-slider
v-model="progressValue"
:format-tooltip="formatTooltip"
:step="timeStep"
></el-slider>
</div>
<el-row>
<el-col :span="24" style="text-align:center">
<el-tag effect="dark" size="medium" :style="countdownColor">
<i class="fa fa-circle" aria-hidden="true"></i>
{{ countdown }}
</el-tag>
</el-col>
</el-row>
</el-card>
<el-card shadow style="margin-top:15px;">
<div class="contest-rank-switch">
<span style="float:right;">
<span>{{ $t('m.Auto_Refresh') }}(10s)</span>
<el-switch
:disabled="contestEnded"
@change="handleAutoRefresh"
v-model="autoRefresh"
></el-switch>
</span>
<span style="float:right;" v-if="isContestAdmin">
<span>{{ $t('m.Force_Update') }}</span>
<el-switch
v-model="forceUpdate"
@change="getContestOutsideScoreboard"
></el-switch>
</span>
<span style="float:right;">
<span>{{ $t('m.Star_User') }}</span>
<el-switch
v-model="showStarUser"
@change="getContestOutsideScoreboard"
></el-switch>
</span>
</div>
<vxe-table
round
border
auto-resize
size="small"
align="center"
:data="dataRank"
:cell-class-name="cellClassName"
>
<vxe-table-column
field="rank"
width="50"
fixed="left"
:title="$t('m.Contest_Rank_Seq')"
>
<template v-slot="{ row }">
{{ row.rank == -1 ? '*' : row.rank }}
</template>
</vxe-table-column>
<vxe-table-column
field="username"
fixed="left"
v-if="!isMobileView"
min-width="300"
:title="$t('m.User')"
header-align="center"
align="left"
>
<template v-slot="{ row }">
<avatar
:username="row[contest.rankShowName]"
:inline="true"
:size="37"
color="#FFF"
:src="row.avatar"
:title="row[contest.rankShowName]"
></avatar>
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="contest-username"
><span class="contest-rank-flag" v-if="row.rank == -1"
>Star</span
>
<span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>
{{ row[contest.rankShowName] }}</span
>
<span class="contest-school" v-if="row.school">{{
row.school
}}</span>
</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="username"
v-else
min-width="300"
:title="$t('m.User')"
header-align="center"
align="left"
>
<template v-slot="{ row }">
<avatar
:username="row[contest.rankShowName]"
:inline="true"
:size="37"
color="#FFF"
:src="row.avatar"
:title="row[contest.rankShowName]"
></avatar>
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="contest-username"
><span class="contest-rank-flag" v-if="row.rank == -1"
>Star</span
>
<span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>
{{ row[contest.rankShowName] }}</span
>
<span class="contest-school" v-if="row.school">{{
row.school
}}</span>
</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column field="rating" :title="$t('m.AC')" min-width="60">
<template v-slot="{ row }">
<span
style="color:rgb(87, 163, 243);font-weight: 600;font-size: 14px;"
>{{ row.ac }}
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="totalTime"
:title="$t('m.TotalTime')"
min-width="60"
>
<template v-slot="{ row }">
<el-tooltip effect="dark" placement="top">
<div slot="content">
{{ parseTimeToSpecific(row.totalTime) }}
</div>
<span>{{ parseInt(row.totalTime / 60) }}</span>
</el-tooltip>
</template>
</vxe-table-column>
<vxe-table-column
min-width="74"
v-for="problem in contestProblems"
:key="problem.displayId"
>
<template v-slot:header>
<span style="vertical-align: top;" v-if="problem.color">
<svg
t="1633685184463"
class="icon"
viewBox="0 0 1088 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5840"
width="25"
height="25"
>
<path
d="M575.872 849.408c-104.576 0-117.632-26.56-119.232-31.808-6.528-22.528 32.896-70.592 63.744-96.768l-1.728-2.624c137.6-42.688 243.648-290.112 243.648-433.472A284.544 284.544 0 0 0 478.016 0a284.544 284.544 0 0 0-284.288 284.736c0 150.4 116.352 415.104 263.744 438.336-25.152 29.568-50.368 70.784-39.104 108.928 12.608 43.136 62.72 63.232 157.632 63.232 7.872 0 11.52 9.408 4.352 19.52-21.248 29.248-77.888 63.424-167.68 63.424V1024c138.944 0 215.936-74.816 215.936-126.528a46.72 46.72 0 0 0-16.32-36.608 56.32 56.32 0 0 0-36.416-11.456zM297.152 297.472c0 44.032-38.144 25.344-38.144-38.656 0-108.032 85.248-195.712 190.592-195.712 62.592 0 81.216 39.232 38.08 39.232-105.152 0.064-190.528 87.04-190.528 195.136z"
:fill="problem.color"
p-id="5841"
></path>
</svg>
</span>
<span>
<el-tooltip effect="dark" placement="top">
<div slot="content">
{{ problem.displayId + '. ' + problem.displayTitle }}
</div>
<span class="emphasis" style="color:#495060;"
>{{ problem.displayId }}
</span>
</el-tooltip>
</span>
</template>
<template v-slot="{ row }">
<span v-if="row.submissionInfo[problem.displayId]">
<el-tooltip effect="dark" placement="top">
<div slot="content">
{{ row.submissionInfo[problem.displayId].specificTime }}
</div>
<span
v-if="row.submissionInfo[problem.displayId].isAC"
class="submission-time"
>{{ row.submissionInfo[problem.displayId].ACTime }}<br />
</span>
</el-tooltip>
<span
class="submission-error"
v-if="
row.submissionInfo[problem.displayId].tryNum == null &&
row.submissionInfo[problem.displayId].errorNum != 0
"
>
{{
row.submissionInfo[problem.displayId].errorNum > 1
? row.submissionInfo[problem.displayId].errorNum + ' tries'
: row.submissionInfo[problem.displayId].errorNum + ' try'
}}
</span>
<span v-if="row.submissionInfo[problem.displayId].tryNum != null"
><template
v-if="row.submissionInfo[problem.displayId].errorNum > 0"
>
{{
row.submissionInfo[problem.displayId].errorNum
}}+</template
>{{ row.submissionInfo[problem.displayId].tryNum
}}{{
row.submissionInfo[problem.displayId].errorNum +
row.submissionInfo[problem.displayId].tryNum >
1
? ' tries'
: ' try'
}}
</span>
</span>
</template>
</vxe-table-column>
</vxe-table>
</el-card>
</div>
</template>
<script>
import Avatar from 'vue-avatar';
import time from '@/common/time';
import ScoreBoardMixin from './scoreBoardMixin';
export default {
name: 'ACMScoreBoard',
mixins: [ScoreBoardMixin],
components: {
Avatar,
},
data() {
return {
autoRefresh: false,
removeStar: false,
contestID: '',
dataRank: [],
timer: null,
CONTEST_STATUS: {},
CONTEST_STATUS_REVERSE: {},
CONTEST_TYPE_REVERSE: {},
RULE_TYPE: {},
};
},
created() {
this.init();
},
mounted() {
this.getContestOutsideScoreboard();
},
methods: {
getUserHomeByUsername(uid, username) {
this.$router.push({
name: 'UserHome',
query: { username: username, uid: uid },
});
},
cellClassName({ row, rowIndex, column, columnIndex }) {
if (column.property === 'username' && row.userCellClassName) {
return row.userCellClassName;
}
if (
column.property !== 'rank' &&
column.property !== 'rating' &&
column.property !== 'totalTime' &&
column.property !== 'username'
) {
return row.cellClassName[
[this.contestProblems[columnIndex - 4].displayId]
];
}
},
applyToTable(data) {
let dataRank = JSON.parse(JSON.stringify(data));
dataRank.forEach((rank, i) => {
let info = rank.submissionInfo;
let cellClass = {};
Object.keys(info).forEach((problemID) => {
dataRank[i][problemID] = info[problemID];
if (dataRank[i][problemID].ACTime != null) {
dataRank[i][problemID].errorNum += 1;
dataRank[i][problemID].specificTime = this.parseTimeToSpecific(
dataRank[i][problemID].ACTime
);
dataRank[i][problemID].ACTime = parseInt(
dataRank[i][problemID].ACTime / 60
);
}
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].rank == -1) {
dataRank[i].userCellClassName = 'bg-star';
}
if (dataRank[i].gender == 'female') {
dataRank[i].userCellClassName = 'bg-female';
}
});
this.dataRank = dataRank;
},
parseTimeToSpecific(totalTime) {
return time.secondFormat(totalTime);
},
},
computed: {
isMobileView() {
return window.screen.width < 768;
},
},
};
</script>
<style>
.contest-rank-switch {
margin-bottom: 30px;
margin-top: -8px;
}
.contest-rank-switch span {
margin-left: 5px;
}
</style>
<style scoped>
.contest-title {
text-align: center;
}
.panel-title {
font-variant: small-caps;
font-size: 1.5rem !important;
font-weight: 500;
}
.contest-time {
width: 100%;
font-size: 16px;
}
@media screen and (min-width: 768px) {
.contest-time .left {
text-align: left;
}
.contest-time .right {
text-align: right;
}
}
@media screen and (max-width: 768px) {
.contest-time .left,
.contest-time .right {
text-align: center;
}
}
/deep/.el-slider__button {
width: 20px !important;
height: 20px !important;
background-color: #409eff !important;
}
/deep/.el-slider__button-wrapper {
z-index: 500;
}
/deep/.el-slider__bar {
height: 10px !important;
background-color: #09be24 !important;
}
.el-tag--dark {
border-color: #fff;
}
.el-tag {
color: rgb(25, 190, 107);
background: #fff;
border: 1px solid #e9eaec;
font-size: 18px;
}
/deep/.el-card__body {
padding: 15px !important;
padding-top: 20px !important;
}
.vxe-cell p,
.vxe-cell span {
margin: 0;
padding: 0;
}
/deep/.vxe-table .vxe-body--column {
line-height: 20px !important;
padding: 0 !important;
}
/deep/.vxe-body--column {
min-width: 0;
height: 48px;
box-sizing: border-box;
text-align: left;
text-overflow: ellipsis;
vertical-align: middle;
}
/deep/.vxe-table .vxe-cell {
padding-left: 5px !important;
padding-right: 5px !important;
}
.submission-time {
font-size: 15.6px;
font-family: Roboto, sans-serif;
}
.submission-error {
font-weight: 400;
}
</style>

View File

@ -0,0 +1,401 @@
<template>
<div>
<el-card shadow>
<div class="contest-title">
<div slot="header">
<span class="panel-title">{{ contest.title }}</span>
</div>
</div>
<el-row style="margin-top: 10px;">
<el-col :span="12" class="text-align:left">
<el-tooltip
v-if="contest.auth != null && contest.auth != undefined"
:content="$t('m.' + CONTEST_TYPE_REVERSE[contest.auth]['tips'])"
placement="top"
>
<el-tag
:type.sync="CONTEST_TYPE_REVERSE[contest.auth]['color']"
effect="plain"
>
{{ $t('m.' + CONTEST_TYPE_REVERSE[contest.auth]['name']) }}
</el-tag>
</el-tooltip>
</el-col>
<el-col :span="12" style="text-align:right">
<el-button size="small" plain v-if="contest != null">
{{ contest.type | parseContestType }}
</el-button>
</el-col>
</el-row>
<div class="contest-time">
<el-row>
<el-col :xs="24" :md="12" class="left">
<p>
<i class="fa fa-hourglass-start" aria-hidden="true"></i>
{{ $t('m.StartAt') }}{{ contest.startTime | localtime }}
</p>
</el-col>
<el-col :xs="24" :md="12" class="right">
<p>
<i class="fa fa-hourglass-end" aria-hidden="true"></i>
{{ $t('m.EndAt') }}{{ contest.endTime | localtime }}
</p>
</el-col>
</el-row>
</div>
<div class="slider">
<el-slider
v-model="progressValue"
:format-tooltip="formatTooltip"
:step="timeStep"
></el-slider>
</div>
<el-row>
<el-col :span="24" style="text-align:center">
<el-tag effect="dark" size="medium" :style="countdownColor">
<i class="fa fa-circle" aria-hidden="true"></i>
{{ countdown }}
</el-tag>
</el-col>
</el-row>
</el-card>
<el-card shadow style="margin-top:15px;">
<div class="contest-rank-switch">
<span style="float:right;">
<span>{{ $t('m.Auto_Refresh') }}(10s)</span>
<el-switch
:disabled="contestEnded"
@change="handleAutoRefresh"
v-model="autoRefresh"
></el-switch>
</span>
<span style="float:right;" v-if="isContestAdmin">
<span>{{ $t('m.Force_Update') }}</span>
<el-switch
v-model="forceUpdate"
@change="getContestOutsideScoreboard"
></el-switch>
</span>
<span style="float:right;">
<span>{{ $t('m.Star_User') }}</span>
<el-switch
v-model="showStarUser"
@change="getContestOutsideScoreboard"
></el-switch>
</span>
</div>
<vxe-table
round
border
auto-resize
size="small"
align="center"
ref="OIContestRank"
:data="dataRank"
:cell-class-name="cellClassName"
>
<vxe-table-column
field="rank"
width="50"
fixed="left"
:title="$t('m.Contest_Rank_Seq')"
>
<template v-slot="{ row }">
{{ row.rank == -1 ? '*' : row.rank }}
</template>
</vxe-table-column>
<vxe-table-column
field="username"
fixed="left"
v-if="!isMobileView"
min-width="300"
:title="$t('m.User')"
header-align="center"
align="left"
>
<template v-slot="{ row }">
<avatar
:username="row[contest.rankShowName]"
:inline="true"
:size="37"
color="#FFF"
:src="row.avatar"
:title="row[contest.rankShowName]"
></avatar>
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="contest-username"
><span class="contest-rank-flag" v-if="row.rank == -1"
>Star</span
>
<span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>
{{ row[contest.rankShowName] }}</span
>
<span class="contest-school" v-if="row.school">{{
row.school
}}</span>
</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="username"
v-else
min-width="300"
:title="$t('m.User')"
header-align="center"
align="left"
>
<template v-slot="{ row }">
<avatar
:username="row[contest.rankShowName]"
:inline="true"
:size="37"
color="#FFF"
:src="row.avatar"
:title="row[contest.rankShowName]"
></avatar>
<span style="float:right;text-align:right">
<a @click="getUserHomeByUsername(row.uid, row.username)">
<span class="contest-username"
><span class="contest-rank-flag" v-if="row.rank == -1"
>Star</span
>
<span class="contest-rank-flag" v-if="row.gender == 'female'"
>Girl</span
>
{{ row[contest.rankShowName] }}</span
>
<span class="contest-school" v-if="row.school">{{
row.school
}}</span>
</a>
</span>
</template>
</vxe-table-column>
<vxe-table-column
field="totalScore"
:title="$t('m.Total_Score')"
min-width="100"
>
<template v-slot="{ row }">
<span
><a style="color:rgb(87, 163, 243);">{{ row.totalScore }}</a>
<br />
<span class="problem-time">({{ row.totalTime }}ms)</span>
</span>
</template>
</vxe-table-column>
<vxe-table-column
min-width="120"
v-for="problem in contestProblems"
:key="problem.displayId"
>
<template v-slot:header>
<span style="vertical-align: top;" v-if="problem.color">
<svg
t="1633685184463"
class="icon"
viewBox="0 0 1088 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5840"
width="25"
height="25"
>
<path
d="M575.872 849.408c-104.576 0-117.632-26.56-119.232-31.808-6.528-22.528 32.896-70.592 63.744-96.768l-1.728-2.624c137.6-42.688 243.648-290.112 243.648-433.472A284.544 284.544 0 0 0 478.016 0a284.544 284.544 0 0 0-284.288 284.736c0 150.4 116.352 415.104 263.744 438.336-25.152 29.568-50.368 70.784-39.104 108.928 12.608 43.136 62.72 63.232 157.632 63.232 7.872 0 11.52 9.408 4.352 19.52-21.248 29.248-77.888 63.424-167.68 63.424V1024c138.944 0 215.936-74.816 215.936-126.528a46.72 46.72 0 0 0-16.32-36.608 56.32 56.32 0 0 0-36.416-11.456zM297.152 297.472c0 44.032-38.144 25.344-38.144-38.656 0-108.032 85.248-195.712 190.592-195.712 62.592 0 81.216 39.232 38.08 39.232-105.152 0.064-190.528 87.04-190.528 195.136z"
:fill="problem.color"
p-id="5841"
></path>
</svg>
</span>
<span>
<el-tooltip effect="dark" placement="top">
<div slot="content">
{{ problem.displayId + '. ' + problem.displayTitle }}
</div>
<span class="emphasis" style="color:#495060;"
>{{ problem.displayId }}
</span>
</el-tooltip>
</span>
</template>
<template v-slot="{ row }">
<div v-if="row.submissionInfo[problem.displayId]">
<span>{{ row.submissionInfo[problem.displayId] }}</span>
<br />
<span
v-if="row.timeInfo && row.timeInfo[problem.displayId]"
style="font-size:12px;"
>({{ row.timeInfo[problem.displayId] }}ms)</span
>
</div>
</template>
</vxe-table-column>
</vxe-table>
</el-card>
</div>
</template>
<script>
import Avatar from 'vue-avatar';
import ScoreBoardMixin from './scoreBoardMixin';
export default {
name: 'OIContestRank',
components: {
Avatar,
},
mixins: [ScoreBoardMixin],
data() {
return {
contestID: '',
dataRank: [],
autoRefresh: false,
timer: null,
CONTEST_STATUS: {},
CONTEST_STATUS_REVERSE: {},
CONTEST_TYPE_REVERSE: {},
RULE_TYPE: {},
};
},
created() {
this.init();
},
mounted() {
this.getContestOutsideScoreboard();
},
computed: {
isMobileView() {
return window.screen.width < 768;
},
},
methods: {
cellClassName({ row, rowIndex, column, columnIndex }) {
if (column.property === 'username' && row.userCellClassName) {
return row.userCellClassName;
}
if (
column.property !== 'rank' &&
column.property !== 'totalScore' &&
column.property !== 'username'
) {
return row.cellClassName[
[this.contestProblems[columnIndex - 3].displayId]
];
}
},
getUserHomeByUsername(uid, username) {
this.$router.push({
name: 'UserHome',
query: { username: username, uid: uid },
});
},
applyToTable(data) {
let dataRank = JSON.parse(JSON.stringify(data));
dataRank.forEach((rank, i) => {
let info = rank.submissionInfo;
let cellClass = {};
Object.keys(info).forEach((problemID) => {
dataRank[i][problemID] = info[problemID];
let score = info[problemID];
if (score == 0) {
cellClass[problemID] = 'oi-0';
} else if (score > 0 && score < 100) {
cellClass[problemID] = 'oi-between';
} else if (score == 100) {
cellClass[problemID] = 'oi-100';
}
});
dataRank[i].cellClassName = cellClass;
if (dataRank[i].rank == -1) {
dataRank[i].userCellClassName = 'bg-star';
}
if (dataRank[i].gender == 'female') {
dataRank[i].userCellClassName = 'bg-female';
}
});
this.dataRank = dataRank;
},
},
};
</script>
<style scoped>
.contest-title {
text-align: center;
}
.panel-title {
font-variant: small-caps;
font-size: 1.5rem !important;
font-weight: 500;
}
.contest-time {
width: 100%;
font-size: 16px;
}
@media screen and (min-width: 768px) {
.contest-time .left {
text-align: left;
}
.contest-time .right {
text-align: right;
}
}
@media screen and (max-width: 768px) {
.contest-time .left,
.contest-time .right {
text-align: center;
}
}
/deep/.el-slider__button {
width: 20px !important;
height: 20px !important;
background-color: #409eff !important;
}
/deep/.el-slider__button-wrapper {
z-index: 500;
}
/deep/.el-slider__bar {
height: 10px !important;
background-color: #09be24 !important;
}
.el-tag--dark {
border-color: #fff;
}
.el-tag {
color: rgb(25, 190, 107);
background: #fff;
border: 1px solid #e9eaec;
font-size: 18px;
}
/deep/.el-card__body {
padding: 15px !important;
padding-top: 20px !important;
}
.vxe-cell p,
.vxe-cell span {
margin: 0;
padding: 0;
}
/deep/.vxe-table .vxe-body--column {
line-height: 20px !important;
padding: 8px !important;
}
.problem-time {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
}
/deep/.vxe-table .vxe-cell {
padding-left: 5px !important;
padding-right: 5px !important;
}
</style>

View File

@ -0,0 +1,119 @@
import api from '@/common/api'
import time from '@/common/time';
import { CONTEST_STATUS,CONTEST_STATUS_REVERSE,CONTEST_TYPE_REVERSE,RULE_TYPE} from '@/common/constants'
import { mapState, mapGetters, mapActions } from 'vuex';
import moment from 'moment';
export default {
methods: {
init(){
this.contestID = this.$route.params.contestID;
this.CONTEST_TYPE_REVERSE = Object.assign({}, CONTEST_TYPE_REVERSE);
this.CONTEST_STATUS = Object.assign({}, CONTEST_STATUS);
this.CONTEST_STATUS_REVERSE = Object.assign({}, CONTEST_STATUS_REVERSE);
this.RULE_TYPE = Object.assign({}, RULE_TYPE);
this.$store.dispatch('getScoreBoardContestInfo').then((res) => {
if (!this.contestEnded) {
this.autoRefresh = true;
this.handleAutoRefresh(true);
}
this.changeDomTitle({ title: res.data.data.title });
let data = res.data.data;
let endTime = moment(data.endTime);
// 如果当前时间还是在比赛结束前的时间,需要计算倒计时,同时开启获取比赛公告的定时器
if (endTime.isAfter(moment(data.now))) {
// 实时更新时间
this.timer = setInterval(() => {
this.$store.commit('nowAdd1s');
}, 1000);
}
});
},
getContestOutsideScoreboard () {
let params = {
cid: this.$route.params.contestID,
forceRefresh: this.forceUpdate ? true: false,
removeStar: !this.showStarUser
}
api.getContestOutsideScoreboard(params).then(res => {
this.applyToTable(res.data.data)
},(err)=>{
if(this.refreshFunc){
this.autoRefresh = false;
clearInterval(this.refreshFunc)
}
})
},
handleAutoRefresh (status) {
if (status == true) {
this.refreshFunc = setInterval(() => {
this.getContestOutsideScoreboard()
}, 10000)
} else {
clearInterval(this.refreshFunc)
}
},
...mapActions(['changeDomTitle']),
formatTooltip(val) {
if (this.contest.status == -1) {
// 还未开始
return '00:00:00';
} else if (this.contest.status == 0) {
return time.secondFormat(this.BeginToNowDuration); // 格式化时间
} else {
return time.secondFormat(this.contest.duration);
}
},
},
computed: {
...mapState({
contest: (state) => state.contest.contest,
now: (state) => state.contest.now,
contestProblems: state => state.contest.contestProblems
}),
...mapGetters([
'countdown',
'BeginToNowDuration',
'isContestAdmin',
]),
forceUpdate: {
get () {
return this.$store.state.contest.forceUpdate
},
set (value) {
this.$store.commit('changeRankForceUpdate', {value: value})
}
},
showStarUser:{
get () {
return !this.$store.state.contest.removeStar
},
set (value) {
this.$store.commit('changeRankRemoveStar', {value: !value})
}
},
progressValue: {
get: function() {
return this.$store.getters.progressValue;
},
set: function() {},
},
timeStep() {
// 时间段平分滑条长度
return 100 / this.contest.duration;
},
countdownColor() {
if (this.contest.status) {
return 'color:' + CONTEST_STATUS_REVERSE[this.contest.status].color;
}
},
contestEnded() {
return this.contest.status == CONTEST_STATUS.ENDED;
},
},
beforeDestroy () {
clearInterval(this.refreshFunc)
clearInterval(this.timer);
this.$store.commit('clearContest');
}
}