diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71761d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +/.classpath +/.project +/.settings/ +/target/ +/.idea +*.iml +.idea +*.project +*.classpath +.metadata +.settings +**/**/target +*.factorypath +out/ +logs/ +*.DS_Store +logs/ +log/ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..95b72ac --- /dev/null +++ b/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + net.educoder + automated_evaluation + 1.0-SNAPSHOT + 自动化评测接口异常通知 + + + org.springframework.boot + spring-boot-starter-parent + 2.6.6 + + + + 5.8.0.M2 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.springframework.boot + spring-boot-starter-test + + + + + mysql + mysql-connector-java + 5.1.47 + + + + com.alibaba + druid + 1.2.8 + + + + com.alibaba + fastjson + 1.2.79 + + + + cn.hutool + hutool-http + ${hutool.version} + + + + cn.hutool + hutool-core + ${hutool.version} + + + + + org.projectlombok + lombok + 1.18.22 + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + \ No newline at end of file diff --git a/src/main/java/net/educoder/AutomatedEvaluationApplication.java b/src/main/java/net/educoder/AutomatedEvaluationApplication.java new file mode 100644 index 0000000..c43490a --- /dev/null +++ b/src/main/java/net/educoder/AutomatedEvaluationApplication.java @@ -0,0 +1,29 @@ +package net.educoder; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @Author: youys + * @Date: 2022/4/2 + * @Description: 启动类 + */ +@RestController +@SpringBootApplication +@EnableScheduling +@EnableAsync +public class AutomatedEvaluationApplication { + + public static void main(String[] args) { + SpringApplication.run(AutomatedEvaluationApplication.class, args); + } + + @GetMapping("/") + public String hello() { + return "hello"; + } +} diff --git a/src/main/java/net/educoder/common/DingTalk.java b/src/main/java/net/educoder/common/DingTalk.java new file mode 100644 index 0000000..bf894e8 --- /dev/null +++ b/src/main/java/net/educoder/common/DingTalk.java @@ -0,0 +1,74 @@ +package net.educoder.common; + + +import cn.hutool.core.codec.Base64; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * @Author: youys + * @Date: 2022/4/2 + * @Description: 钉钉机器人发消息. + */ +@Slf4j +public class DingTalk { + + private String token; + private String secret; + + public DingTalk(String token, String secret) { + this.token = token; + this.secret = secret; + } + + /** + * 发送钉钉消息. + * + * @param message 消息内容 + * @return string + */ + public String sendMessage(String message) { + String url = getUrl(); + JSONObject jsonObject = new JSONObject(); + //固定参数 + jsonObject.put("msgtype", "text"); + JSONObject content = new JSONObject(); + //此处message是你想要发送到钉钉的信息 + content.put("content", message); + jsonObject.put("text", content); + try { + return HttpUtil.post(url, jsonObject.toJSONString()); + } catch (Exception e) { + log.error("发送钉钉消息异常", e); + return null; + } + } + + + private String getUrl() { + + String baseUrl = "https://oapi.dingtalk.com/robot/send?access_token="; + long timestamp = System.currentTimeMillis(); + String stringToSign = timestamp + "\n" + secret; + // MAC加密算法 + Mac mac = null; + try { + mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); + return baseUrl + token + "×tamp=" + timestamp + "&sign=" + + URLEncoder.encode(Base64.encode(signData), "UTF-8"); + } catch (Exception e) { + return null; + } + + } +} diff --git a/src/main/java/net/educoder/config/AppConfig.java b/src/main/java/net/educoder/config/AppConfig.java new file mode 100644 index 0000000..5aa19df --- /dev/null +++ b/src/main/java/net/educoder/config/AppConfig.java @@ -0,0 +1,53 @@ +package net.educoder.config; + +import net.educoder.common.DingTalk; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +/** + * @Author: youys + * @Date: 2022/4/2 + * @Description: 配置类 + */ +@Configuration +public class AppConfig { + + @Autowired + private PropertiesConfig propertiesConfig; + + @Bean("dingTalkEvaluation") + public DingTalk dingTalkEvaluation() { + return new DingTalk(propertiesConfig.getToken(), propertiesConfig.getSecret()); + } + + /** + * 异步线程池配置 + * @return + */ + @Bean("taskExecutor") + public ThreadPoolTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + // 设置核心线程数 + executor.setCorePoolSize(50); + // 设置最大线程数 + executor.setMaxPoolSize(50); + // 设置队列容量 + executor.setQueueCapacity(200); + // 设置线程活跃时间(秒) + executor.setKeepAliveSeconds(10); + // 设置默认线程名称 + executor.setThreadNamePrefix("task-"); + // 设置拒绝策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 等待所有任务结束后再关闭线程池 + executor.setWaitForTasksToCompleteOnShutdown(true); + return executor; + } + + +} diff --git a/src/main/java/net/educoder/config/PropertiesConfig.java b/src/main/java/net/educoder/config/PropertiesConfig.java new file mode 100644 index 0000000..439ff9b --- /dev/null +++ b/src/main/java/net/educoder/config/PropertiesConfig.java @@ -0,0 +1,29 @@ +package net.educoder.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +/** + * @Author: youys + * @Date: 2022/4/2 + * @Description: + */ +@Configuration +public class PropertiesConfig { + + @Value("${evaluation.token}") + private String token; + + @Value("${evaluation.secret}") + private String secret; + + + public String getToken() { + return token; + } + + public String getSecret() { + return secret; + } +} diff --git a/src/main/java/net/educoder/controller/CallbackController.java b/src/main/java/net/educoder/controller/CallbackController.java new file mode 100644 index 0000000..c7d2f0e --- /dev/null +++ b/src/main/java/net/educoder/controller/CallbackController.java @@ -0,0 +1,79 @@ +package net.educoder.controller; + +import cn.hutool.core.codec.Base64; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import net.educoder.common.DingTalk; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * @Author: youys + * @Date: 2022/4/2 + * @Description: 回调接口 + */ +@Slf4j +@RestController +@RequestMapping("/callback") +public class CallbackController { + + @Autowired + private DingTalk dingTalk; + + private static final String RESULT = "success"; + + + private final List ERROR_MSG_LIST = Arrays.asList("系统繁忙,请稍后重试", + "程序执行失败导致评测提前终止,请稍后重试或联系系统管理员", + "本次评测网络延迟较高,资源无法正常加载,请10s后重试。", + "实验环境存在问题或已被回收,请保存数据再重置实训重新评测。", + "当前实验环境正在更新中,请稍后重试或联系系统管理员!", + "当前网络较差,代码下载超时,请稍后重试!", + "评测脚本设置异常,建议您在实训的配置页面重新选择或修改评测脚本" + ); + + @PostMapping("/evaluation") + public String evaluation(@RequestBody Map params) { + log.info("回调请求参数:{}", params); + + if (params == null) { + return RESULT; + } + + JSONObject jsonTestDetails = JSONObject.parseObject(params.get("jsonTestDetails").toString()); + String outPut = jsonTestDetails.getString("outPut"); + String tpiID = jsonTestDetails.getString("tpiID"); + + String decodeOutPut = Base64.decodeStr(outPut); + log.info("tpiID:{}, decodeOutPut:{}", tpiID, decodeOutPut); + if (match(decodeOutPut)) { + log.info("tpiID:{},需要发送钉钉通知", tpiID); + // 钉钉通知 + StringBuilder sb = new StringBuilder(); + sb.append("【自动化评测】tpiID:").append(tpiID).append(" 错误:消息").append(decodeOutPut); + String result = dingTalk.sendMessage(sb.toString()); + log.info("发送钉钉通知结果:{}", result); + } + + return RESULT; + } + + /** + * 匹配输出包含了错误. + * + * @param output + * @return + */ + private boolean match(String output) { + for (String s : ERROR_MSG_LIST) { + if (output.contains(s)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/educoder/dao/AutoEvaParamConfigRepository.java b/src/main/java/net/educoder/dao/AutoEvaParamConfigRepository.java new file mode 100644 index 0000000..ecdc278 --- /dev/null +++ b/src/main/java/net/educoder/dao/AutoEvaParamConfigRepository.java @@ -0,0 +1,12 @@ +package net.educoder.dao; + +import net.educoder.model.AutoEvaParamConfig; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * @Author: youys + * @Date: 2022/4/2 + * @Description: + */ +public interface AutoEvaParamConfigRepository extends JpaRepository { +} diff --git a/src/main/java/net/educoder/model/AutoEvaParamConfig.java b/src/main/java/net/educoder/model/AutoEvaParamConfig.java new file mode 100644 index 0000000..514b549 --- /dev/null +++ b/src/main/java/net/educoder/model/AutoEvaParamConfig.java @@ -0,0 +1,109 @@ +package net.educoder.model; + +import org.hibernate.annotations.Table; + +import javax.annotation.Generated; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import java.util.Date; + +@Entity +public class AutoEvaParamConfig { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Integer type; + + private String tpiId; + + private String tpiGitUrl; + + private String buildId; + + private String testCases; + + private String extras; + + private Date createTime; + + private String codeFileContent; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Integer getType() { + return type; + } + + public void setType(Integer type) { + this.type = type; + } + + public String getTpiId() { + return tpiId; + } + + public void setTpiId(String tpiId) { + this.tpiId = tpiId == null ? null : tpiId.trim(); + } + + public String getTpiGitUrl() { + return tpiGitUrl; + } + + public void setTpiGitUrl(String tpiGitUrl) { + this.tpiGitUrl = tpiGitUrl == null ? null : tpiGitUrl.trim(); + } + + public String getBuildId() { + return buildId; + } + + public void setBuildId(String buildId) { + this.buildId = buildId == null ? null : buildId.trim(); + } + + public String getTestCases() { + return testCases; + } + + public void setTestCases(String testCases) { + this.testCases = testCases == null ? null : testCases.trim(); + } + + public String getExtras() { + return extras; + } + + public void setExtras(String extras) { + this.extras = extras == null ? null : extras.trim(); + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getCodeFileContent() { + return codeFileContent; + } + + public void setCodeFileContent(String codeFileContent) { + this.codeFileContent = codeFileContent == null ? null : codeFileContent.trim(); + } + + public static final Integer SX_EVA = 0; + public static final Integer OJ_EVA = 1; +} \ No newline at end of file diff --git a/src/main/java/net/educoder/service/AutoEvaParamConfigService.java b/src/main/java/net/educoder/service/AutoEvaParamConfigService.java new file mode 100644 index 0000000..c9e3ca4 --- /dev/null +++ b/src/main/java/net/educoder/service/AutoEvaParamConfigService.java @@ -0,0 +1,26 @@ +package net.educoder.service; + +import net.educoder.dao.AutoEvaParamConfigRepository; +import net.educoder.model.AutoEvaParamConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @Author: youys + * @Date: 2022/4/2 + * @Description: + */ +@Service +public class AutoEvaParamConfigService { + + @Autowired + private AutoEvaParamConfigRepository autoEvaParamConfigRepository; + + + public List findAll() { + return autoEvaParamConfigRepository.findAll(); + } + +} diff --git a/src/main/java/net/educoder/shedule/EvaCheckWarningTask.java b/src/main/java/net/educoder/shedule/EvaCheckWarningTask.java new file mode 100644 index 0000000..a180636 --- /dev/null +++ b/src/main/java/net/educoder/shedule/EvaCheckWarningTask.java @@ -0,0 +1,110 @@ +package net.educoder.shedule; + +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import net.educoder.model.AutoEvaParamConfig; +import net.educoder.service.AutoEvaParamConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * @Author: youys + * @Date: 2022/4/2 + * @Description: 评测检查告警任务 + */ +@Slf4j +@Component +public class EvaCheckWarningTask { + + + @Value("${bridge.evaluation}") + private String gameEvaluationUrl; + + @Value("${bridge.ojEvaluation}") + private String ojEvaluationUrl; + + @Value("${bridge.callbackUrl}") + private String callbackUrl; + + + @Autowired + private AutoEvaParamConfigService autoEvaParamConfigService; + + @Async + @Scheduled(initialDelay = 5 * 1000, fixedRate = 30 * 1000) + public void check() { + log.info("自动化评测接口告警检查---------start "); + List autoEvaParamConfigList = autoEvaParamConfigService.findAll(); + log.info(JSONObject.toJSONString(autoEvaParamConfigList)); + + for (AutoEvaParamConfig autoEvaParamConfig : autoEvaParamConfigList) { + try { + if (autoEvaParamConfig.getType() == 0) { + String extras = autoEvaParamConfig.getExtras(); + if (!StringUtils.hasLength(extras)) { + continue; + } + + postEvaluation(autoEvaParamConfig); + } else if (autoEvaParamConfig.getType() == 1) { + + if (!StringUtils.hasLength(autoEvaParamConfig.getCodeFileContent())) { + continue; + } + postOjEvaluation(autoEvaParamConfig); + } + } catch (Exception e) { + log.error("自动化评测接口告警检查任务异常", e); + } + } + log.info("自动化评测接口告警检查 ---------end "); + } + + + private void postEvaluation(AutoEvaParamConfig autoEvaParamConfig) { + JSONObject extrasObject = JSONObject.parseObject(autoEvaParamConfig.getExtras()); + + Map param = new HashMap<>(); + param.put("isPublished", extrasObject.getInteger("isPublished")); + param.put("trimBlank", extrasObject.getInteger("trimBlank")); + param.put("containers", extrasObject.getString("containers")); + param.put("instanceChallenge", extrasObject.getString("instanceChallenge")); + param.put("tpmScript", extrasObject.getString("tpmScript")); + param.put("timeLimit", extrasObject.getInteger("timeLimit")); + param.put("times", extrasObject.getInteger("times")); + param.put("resubmit", extrasObject.getInteger("resubmit")); + param.put("podType", extrasObject.getInteger("podType")); + param.put("content_modified", extrasObject.getInteger("content_modified")); + param.put("tpiID", autoEvaParamConfig.getTpiId()); + param.put("testCases", autoEvaParamConfig.getTestCases()); + param.put("tpiGitURL", autoEvaParamConfig.getTpiGitUrl()); + param.put("buildID", autoEvaParamConfig.getBuildId()); + param.put("sec_key", UUID.randomUUID().toString()); + param.put("callBackUrl", callbackUrl); + String result = HttpUtil.post(gameEvaluationUrl, param); + JSONObject jsonResult = JSONObject.parseObject(result); + log.info("实训评测接口返回:{},tpiId:{}", jsonResult, autoEvaParamConfig.getTpiId()); + } + + private void postOjEvaluation(AutoEvaParamConfig autoEvaParamConfig) { + Map param = new HashMap<>(8); + param.put("tpiID", autoEvaParamConfig.getTpiId()); + param.put("testCases", autoEvaParamConfig.getTestCases()); + param.put("codeFileContent", autoEvaParamConfig.getCodeFileContent()); + param.put("sec_key", UUID.randomUUID().toString()); + param.put("callBackUrl", callbackUrl); + String result = HttpUtil.post(ojEvaluationUrl, param); + JSONObject jsonResult = JSONObject.parseObject(result); + log.info("Oj评测接口返回:{},tpiId:{}", jsonResult, autoEvaParamConfig.getTpiId()); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..411bf58 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,9 @@ +## 评测钉钉机器人配置 +evaluation: + token: 1069c45d4dfab187bb95599faeaf3687b630f79eda0d6fff914a18ee9e25db6f + secret: SEC7431097248ee9c19b9d7a2c61883e29082bea79c88c3e8f02e5fa121c2837dbc + +bridge: + evaluation: http://pre-bridge.educoder.net/bridge/game/gameEvaluate + ojEvaluation: http://pre-bridge.educoder.net/bridge/ojs/evaluate + callbackUrl: http://1c32-120-228-142-52.ngrok.io/callback/evaluation \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..d3e9b7d --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,9 @@ +## 评测钉钉机器人配置 +evaluation: + token: 1069c45d4dfab187bb95599faeaf3687b630f79eda0d6fff914a18ee9e25db6f + secret: SEC7431097248ee9c19b9d7a2c61883e29082bea79c88c3e8f02e5fa121c2837dbc + +bridge: + evaluation: https://www.educoder.net/bridge/game/gameEvaluate + ojEvaluation: https://www.educoder.net/bridge/ojs/evaluate + callbackUrl: https://1c32-120-228-142-52.ngrok.io/callback/evaluation \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..ff10690 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,25 @@ +server: + port: 8002 + servlet: + context-path: / + +spring: + profiles: + active: dev + # 数据库配置 + datasource: + url: jdbc:mysql://rm-bp13v5020p7828r5rso.mysql.rds.aliyuncs.com:3306/testbridge?useUnicode=true&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false + username: testeducoder + password: TEST@123 + driver-class-name: com.mysql.jdbc.Driver + type: com.alibaba.druid.pool.DruidDataSource + initialSize: 50 + minIdle: 50 + maxActive: 100 + validationQuery: SELECT 1 + # redis配置 + redis: + host: 127.0.0.1 + port: 6379 + password: +