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:
+