https://github.com/strangersinsist/onlineJudge
半成品,后续填坑(一定)
1 测评
- 代码沙箱(自主实现/部署开源项目)
- 判题 api judge0 api 选用,最初使用 python 测试运行
- 用 ai 做代码沙箱,理论上可以但是对于复杂的题目来说可能准确性不高,失去作为 OJ 的意义
- 移花接木,可以通过操作模拟浏览器的方式,用别人的 OJ 帮助判题,在别人的项目中提交代码并获取结果。比如 vjudge
2 问题
遇到的报错 :
1
| {code: '111', input: '222'} Analysis_A.vue:88 POST http://localhost:8080/api/runCode 404 (Not Found) request.js:55 errAxiosError: Request failed with status code 404
|
终端
1
| Received code: 111 Received input: 222
|
但是前端
1
| Payload to be sent: {code: '111', input: '222'}code: "111"input: "222"[[Prototype]]: Object Analysis_A.vue: 90 Response from backend: undefined
|
处理 :返回一个 JSON 格式的响应
遇到了问题,前端的 response.data
仍然是 undefined
,尽管后端已经正确返回了数据。问题很可能是出现在 Axios 自动解析响应体的过程中,也可能是后端的返回内容格式化不正确。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @CrossOrigin(origins = "http://localhost:8080") @RestController public class CodeExecutionController { @PostMapping("/api/runCode") public ResponseEntity<CodeExecutionResponse> runCode(@RequestBody CodeExecutionRequest request) { String code = request.getCode(); String input = request.getInput(); String output = "Code:\n" + code + "\n\nInput:\n" + input + "\n\nRepeated:\n" + code + "\n\n" + input; return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(new CodeExecutionResponse(output)); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| runCode() { console.log("Running code with input:", this.testCase); const payload = { code: this.codeInput, input: this.testCase }; console.log("Payload to be sent:", payload); request.post("http://localhost:9091/api/runCode", payload, { responseType: 'json' }) .then(response => { console.log("Full response from backend:", response); console.log("Response data:", response.data); if (response.data && response.data.output) { this.testOutput = response.data.output; console.log("Output:", this.testOutput); } else { console.error("Output is missing in the response", response.data); } }) .catch(error => { console.error("Error running code:", error); }); },
|
解决:
检查完整的 response
对象结构
从前面的日志看响应中存在数据,进一步打印 response
的完整结构,看看 response.data
是否被放在了其他位置。
修改代码,检查 response
对象的所有属性,而不仅仅是 response.data
:
3 调用 api
在 pom. xml 中增加AsyncHttpClient 和 JSON 解析库依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependencies> <dependency> <groupId>org.asynchttpclient</groupId> <artifactId>async-http-client</artifactId> <version>2.12.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.3</version> </dependency> </dependencies>
|
CodeExecutionController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| package com.example.demo.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.asynchttpclient.*; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.Base64; import java.util.concurrent.CompletableFuture; class CodeExecutionRequest { private String code; private String input; public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getInput() { return input; } public void setInput(String input) { this.input = input; } } class CodeExecutionResponse { private String output; public CodeExecutionResponse(String output) { this.output = output; } public String getOutput() { return output; } public void setOutput(String output) { this.output = output; } } @CrossOrigin(origins = "http://localhost:8080") @RestController public class CodeExecutionController { private static final String API_HOST = "judge0-ce.p.rapidapi.com"; private static final String API_KEY = "a3adbed817msh3baa8ad5c76fa09p137a87jsn636cf96b2a42"; @PostMapping("/api/runCode") public CompletableFuture<ResponseEntity<CodeExecutionResponse>> runCode(@RequestBody CodeExecutionRequest request) { String code = request.getCode(); String input = request.getInput(); String encodedCode = Base64.getEncoder().encodeToString(code.getBytes()); String encodedInput = Base64.getEncoder().encodeToString(input.getBytes()); String payload = String.format( "{\"language_id\": 52, \"source_code\": \"%s\", \"stdin\": \"%s\"}", encodedCode, encodedInput ); AsyncHttpClient client = Dsl.asyncHttpClient(); Request apiRequest = Dsl.post("https://" + API_HOST + "/submissions?base64_encoded=true&wait=true&fields=*") .setHeader("x-rapidapi-key", API_KEY) .setHeader("x-rapidapi-host", API_HOST) .setHeader("Content-Type", "application/json") .setBody(payload) .build(); return client.executeRequest(apiRequest).toCompletableFuture().thenApply(response -> { try { ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(response.getResponseBody()); String stdout = ""; if (jsonNode.has("stdout") && !jsonNode.get("stdout").isNull()) { String stdoutBase64 = jsonNode.get("stdout").asText(); stdout = new String(Base64.getDecoder().decode(stdoutBase64)); } else { stdout = "No output received."; } try { client.close(); } catch (IOException e) { e.printStackTrace(); } return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) .body(new CodeExecutionResponse(stdout)); } catch (Exception e) { e.printStackTrace(); try { client.close(); } catch (IOException ioException) { ioException.printStackTrace(); } return ResponseEntity.status(500).body(new CodeExecutionResponse("Error processing the request.")); } }); } }
|
4 提交如何测评?
手动给出一些输入样例+自动生成输入样例(用程序(上传题目时出题人要给出数据生成代码)或者 AI)
判题:将输入样例同时传入标准答案和用户答案,判断两者是否相同。
huanghongxun/Judger: Judge system for Sicily Online Judge, VMatrix Course/OJ, GDOI (github.com)
5 问题
在做上传题目的表格时,让 id 自动递增,刚开始没有保存这个表格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;
@TableName("ojquestion") @Data public class OJ { @TableId(type = IdType.AUTO) private Integer id; private String question; private String answer; private String title; }
|
application.properties:
1
| mybatis-plus.global-config.db-config.id-type=AUTO
|
6 两者答案对比
对提交增加一个方法,点击提交后将输入的code及输入数据返回后端,然后在后端调用api,分别将(输入的code及输入数据)和(后端数据库中的ojquestion表格对应的正确代码answer及前端的输入数据)提交给api运行,如果两者运行结果一致则在前端左侧的”提交“栏显示”答案正确“否则显示”答案错误“
用户操作:
用户在页面上输入代码和测试数据。
点击“提交”按钮。
前端逻辑:
发送请求到 /api/submitCode/{id}
接口。
后端逻辑:
获取用户代码和输入数据。
查询数据库中的正确答案代码。
分别运行两段代码,比较运行结果。
返回“答案正确”或“答案错误”给前端。
前端显示结果:
使用弹窗显示结果信息。
CodeExecution.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
| package com.example.demo.controller; import com.example.demo.entity.OJ; import com.example.demo.mapper.OjMapper; import jakarta.annotation.Resource; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.asynchttpclient.*; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; class CodeExecutionRequest { private String code; private String input; public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getInput() { return input; } public void setInput(String input) { this.input = input; } } class CodeExecutionResponse { private String output; public CodeExecutionResponse(String output) { this.output = output; } public String getOutput() { return output; } public void setOutput(String output) { this.output = output; } } @CrossOrigin(origins = "http://localhost:8080") @RestController public class CodeExecutionController { private static final String API_HOST = "judge0-ce.p.rapidapi.com"; private static final String API_KEY = "ecb28c3467mshf8bd712d1eaeae2p100845jsnb2a33783ad93"; @PostMapping("/api/runCode") public CompletableFuture<ResponseEntity<CodeExecutionResponse>> runCode(@RequestBody CodeExecutionRequest request) { String code = request.getCode(); String input = request.getInput(); String encodedCode = Base64.getEncoder().encodeToString(code.getBytes()); String encodedInput = Base64.getEncoder().encodeToString(input.getBytes()); String payload = String.format( "{\"language_id\": 52, \"source_code\": \"%s\", \"stdin\": \"%s\"}", encodedCode, encodedInput ); AsyncHttpClient client = Dsl.asyncHttpClient(); Request apiRequest = Dsl.post("https://" + API_HOST + "/submissions?base64_encoded=true&wait=true&fields=*") .setHeader("x-rapidapi-key", API_KEY) .setHeader("x-rapidapi-host", API_HOST) .setHeader("Content-Type", "application/json") .setBody(payload) .build(); return client.executeRequest(apiRequest).toCompletableFuture().thenApply(response -> { try { ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(response.getResponseBody()); String stdout = ""; if (jsonNode.has("stdout") && !jsonNode.get("stdout").isNull()) { String stdoutBase64 = jsonNode.get("stdout").asText(); stdout = new String(Base64.getDecoder().decode(stdoutBase64)); } else { stdout = "No output received."; } try { client.close(); } catch (IOException e) { e.printStackTrace(); } return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) .body(new CodeExecutionResponse(stdout)); } catch (Exception e) { e.printStackTrace(); try { client.close(); } catch (IOException ioException) { ioException.printStackTrace(); } return ResponseEntity.status(500).body(new CodeExecutionResponse("Error processing the request.")); } }); } @Resource private OjMapper ojMapper; @PostMapping("/api/submitCode/{id}") public CompletableFuture<ResponseEntity<Map<String, String>>> submitCode( @PathVariable Integer id, @RequestBody CodeExecutionRequest request) { String userCode = request.getCode(); String inputData = request.getInput(); OJ ojQuestion = ojMapper.selectById(id); String correctAnswer = ojQuestion.getAnswer(); CompletableFuture<String> userCodeExecution = executeCode(userCode, inputData); CompletableFuture<String> correctAnswerExecution = executeCode(correctAnswer, inputData); return userCodeExecution.thenCombine(correctAnswerExecution, (userOutput, correctOutput) -> { Map<String, String> result = new HashMap<>(); if (userOutput.equals(correctOutput)) { result.put("status", "correct"); result.put("message", "答案正确"); } else { result.put("status", "incorrect"); result.put("message", "答案错误"); } return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result); }).exceptionally(ex -> { ex.printStackTrace(); Map<String, String> errorResponse = new HashMap<>(); errorResponse.put("message", "代码运行出错"); return ResponseEntity.status(500).body(errorResponse); }); } private CompletableFuture<String> executeCode(String code, String input) { String encodedCode = Base64.getEncoder().encodeToString(code.getBytes()); String encodedInput = Base64.getEncoder().encodeToString(input.getBytes()); String payload = String.format( "{\"language_id\": 52, \"source_code\": \"%s\", \"stdin\": \"%s\"}", encodedCode, encodedInput ); AsyncHttpClient client = Dsl.asyncHttpClient(); Request apiRequest = Dsl.post("https://" + API_HOST + "/submissions?base64_encoded=true&wait=true&fields=*") .setHeader("x-rapidapi-key", API_KEY) .setHeader("x-rapidapi-host", API_HOST) .setHeader("Content-Type", "application/json") .setBody(payload) .build(); return client.executeRequest(apiRequest).toCompletableFuture().thenApply(response -> { try { JsonNode jsonNode = new ObjectMapper().readTree(response.getResponseBody()); String stdoutBase64 = jsonNode.has("stdout") && !jsonNode.get("stdout").isNull() ? jsonNode.get("stdout").asText() : ""; return new String(Base64.getDecoder().decode(stdoutBase64)); } catch (IOException e) { e.printStackTrace(); return "运行失败"; } finally { try { client.close(); } catch (IOException e) { e.printStackTrace(); } } }); } }
|
Analysis_A.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
| <template> <div class="main-container"> <div class="left-section"> <el-tabs> <el-tab-pane label="描述"> <div v-html="renderMarkdown(ojData.question)" class="markdown-content"></div> </el-tab-pane> <el-tab-pane label="提交">提交内容</el-tab-pane> <el-tab-pane label="排名">排名内容</el-tab-pane> </el-tabs> </div> <div class="right-section"> <div class="code-area"> <el-input type="textarea" v-model="codeInput" rows="10" placeholder="Enter your code here"></el-input> </div> <div class="test-output-section"> <div class="test-case-input"> <el-input type="textarea" v-model="testCase" rows="5" placeholder="在此处填写输入数据,也可留空 可以按 Ctrl+A 选中全部来一键删除"></el-input> </div> <div class="test-output"> <el-input type="textarea" v-model="testOutput" rows="5" placeholder="等待测试运行" disabled></el-input> </div> </div> <div class="button-section"> <el-button class="run-button" @click="runCode">测试运行</el-button> <el-button class="submit-button" @click="submitCode">提交</el-button> </div> </div> </div></template> <script> import { marked } from "marked"; import request from "@/utils/request"; export default { name: "Analysis_A", data() { return { ojData: { id: '', title: '', question: '' }, codeInput: '', testCase: '', testOutput: '' }; }, created() { const id = this.$route.query.id; if (id) { this.fetchOjData(id); } }, methods: { fetchOjData(id) { request.get(`/api/oj/${id}`).then(res => { this.ojData = res.data; }).catch(error => { console.error("Error fetching OJ data:", error); }); }, renderMarkdown(content) { return marked(content); }, runCode() { console.log("Running code with input:", this.testCase); const payload = { code: this.codeInput, input: this.testCase }; console.log("Payload to be sent:", payload); // 打印 payload request.post("http://localhost:9091/api/runCode", payload, { responseType: 'json' }) .then(response => { console.log("Full response object from backend:", response); if (response.data) { console.log("Response data:", response.data); if (response.data.output) { this.testOutput = response.data.output; console.log("Output:", this.testOutput); } else { console.error("Output is missing in the response:", response.data); } } else if (response.output) { // 如果数据直接在 response 中,而不是在 response.data 中 this.testOutput = response.output; console.log("Output directly in response:", this.testOutput); } else { console.error("No valid data in the response"); } }) .catch(error => { console.error("Error running code:", error); }); }, submitCode() { const id = this.$route.query.id; const payload = { code: this.codeInput, input: this.testCase }; request.post(`http://localhost:9091/api/submitCode/${id}`, payload, { timeout: 15000 }) .then(response => { console.log('Full response:', response); if (response.message && response.status) { console.log('Direct response message:', response.message); console.log('Direct response status:', response.status); this.showSubmitResult(response.message); } else if (response.data) { // 打印 response.data 检查其是否存在 console.log('Result data:', response.data); const result = response.data; if (result.status === "correct") { this.showSubmitResult(result.message); } else { this.showSubmitResult(result.message); } } else { this.showSubmitResult("无法解析服务器响应"); } }) .catch(error => { console.error("提交代码时出错:", error); this.showSubmitResult("提交失败,请重试"); }); }, showSubmitResult(message) { this.$alert(message, "提交结果", { confirmButtonText: "确定" }); } } } </script> <style scoped> .main-container { display: flex; height: 800px; } .left-section { flex: 1; padding: 10px; width: 1500px; } .right-section { flex: 2; display: flex; flex-direction: column; padding: 10px; width: 30%; } .code-area { height: 2000px; margin-bottom: 10px; } .test-output-section { display: flex; justify-content: space-between; flex: 1; } .test-case-input, .test-output { width: 48%; height: 200px; } .button-section { display: flex; justify-content: flex-end; margin-top: 10px; } .markdown-content { padding: 10px; background-color: #f9f9f9; border-radius: 5px; } .run-button { background-color: white; color: black; border: 1px solid #dcdfe6; } .run-button:hover { background-color: #f2f2f2; } .submit-button { background-color: #009999; color: white; border: none; } .submit-button:hover { background-color: #007777; } </style>
|
主要改动:后端增加了/api/submitCode,接口来处理用户的提交逻辑
后端应该返回一个有效的 JSON 响应
- 使用了
HashMap
来创建一个可变的映射(Map<String, String>
)。
- 使用
put
方法动态添加键值对,分别为 "status"
和 "message"
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @PostMapping("/api/submitCode/{id}") public CompletableFuture<ResponseEntity<Map<String, String>>> submitCode( @PathVariable Integer id, @RequestBody CodeExecutionRequest request) {
OJ ojQuestion = ojMapper.selectById(id); if (ojQuestion == null) { Map<String, String> errorResponse = new HashMap<>(); errorResponse.put("message", "题目不存在"); return CompletableFuture.completedFuture( ResponseEntity.status(404).body(errorResponse)); } String correctAnswer = ojQuestion.getAnswer();
String userCode = request.getCode(); String inputData = request.getInput();
CompletableFuture<String> userCodeExecution = executeCode(userCode, inputData); CompletableFuture<String> correctAnswerExecution = executeCode(correctAnswer, inputData);
return userCodeExecution.thenCombine(correctAnswerExecution, (userOutput, correctOutput) -> { Map<String, String> result = new HashMap<>(); if (userOutput.equals(correctOutput)) { result.put("status", "correct"); result.put("message", "答案正确"); } else { result.put("status", "incorrect"); result.put("message", "答案错误"); } return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result); }).exceptionally(ex -> { ex.printStackTrace(); Map<String, String> errorResponse = new HashMap<>(); errorResponse.put("message", "代码运行出错"); return ResponseEntity.status(500).body(errorResponse); }); }
|
使用Json的形式返回前端,但是现在前端提交正确代码时{code: 200, message: “success”, data: {id: 1,…}} code : 200 data : {id: 1,…} message : “success”,前端弹窗却显示答案错误
解决方法:
可能 Axios
响应对象中并没有 response.status
,通过 response.data
来获取实际返回的结果,而不是 response.status
分别打印
1
| console.log('Full response:', response);
|
1
| console.log('Result data:', result);
|
与问题 1 属于同一原因导致的,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| submitCode() { const id = this.$route.query.id; const payload = { code: this.codeInput, input: this.testCase };
request.post(`http://localhost:9091/api/submitCode/${id}`, payload, { timeout: 15000 }) .then(response => { console.log('Full response:', response); if (response.message && response.status) { console.log('Direct response message:', response.message); console.log('Direct response status:', response.status); this.showSubmitResult(response.message); } else if (response.data) { console.log('Result data:', response.data); const result = response.data; if (result.status === "correct") { this.showSubmitResult(result.message); } else { this.showSubmitResult(result.message); } } else { this.showSubmitResult("无法解析服务器响应"); } }) .catch(error => { console.error("提交代码时出错:", error); this.showSubmitResult("提交失败,请重试"); }); }
|
其实这里可以换一种方式实现,比如将数据库正确答案传至 api 后的结果作为正确输出,和用户输入的代码及测试用例一起再次传到 api,这样可以直接把 api 返回的 json 传至前端,也不用加判断正误的逻辑,不用手动实现传输 json。