├── judge ├── .gitignore ├── include │ ├── env_setup.h │ ├── common.h │ ├── utils.h │ └── runner.h ├── CMakeLists.txt ├── build ├── README.md └── src │ ├── common.cpp │ └── main.cpp ├── .assets ├── dark.png └── light.png ├── dev ├── sql │ └── Dockerfile ├── mvn.xml └── compose.yml ├── services ├── .gitignore ├── judge │ ├── run.sh │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── config │ │ │ │ │ ├── application-prod.yml │ │ │ │ │ ├── application-dev.yml │ │ │ │ │ └── application.yml │ │ │ │ └── META-INF │ │ │ │ │ └── additional-spring-configuration-metadata.json │ │ │ └── java │ │ │ │ └── cloud │ │ │ │ └── oj │ │ │ │ └── judge │ │ │ │ ├── error │ │ │ │ ├── UnsupportedLanguageError.java │ │ │ │ ├── GenericException.java │ │ │ │ ├── ErrorMessage.java │ │ │ │ └── GlobalErrorHandler.java │ │ │ │ ├── entity │ │ │ │ ├── Problem.java │ │ │ │ ├── Contest.java │ │ │ │ ├── SubmitData.java │ │ │ │ ├── Compile.java │ │ │ │ ├── Result.java │ │ │ │ └── Solution.java │ │ │ │ ├── constant │ │ │ │ ├── State.java │ │ │ │ └── Language.java │ │ │ │ ├── repo │ │ │ │ ├── SettingsRepo.java │ │ │ │ ├── SourceRepo.java │ │ │ │ ├── ContestRepo.java │ │ │ │ ├── InviteeRepo.java │ │ │ │ └── ProblemRepo.java │ │ │ │ ├── JudgeApp.java │ │ │ │ ├── config │ │ │ │ ├── LoggingConfig.java │ │ │ │ ├── RabbitConfig.java │ │ │ │ ├── AsyncConfig.java │ │ │ │ └── AppConfig.java │ │ │ │ ├── utils │ │ │ │ └── FileCleaner.java │ │ │ │ ├── component │ │ │ │ ├── ProcessUtil.java │ │ │ │ └── JudgementEntry.java │ │ │ │ ├── controller │ │ │ │ └── SubmitController.java │ │ │ │ ├── logging │ │ │ │ └── DBAppender.java │ │ │ │ └── receiver │ │ │ │ └── SolutionReceiver.java │ │ └── test │ │ │ └── java │ │ │ └── cloud │ │ │ └── oj │ │ │ └── judge │ │ │ └── JudgeAppTests.java │ ├── .gitignore │ └── pom.xml ├── gateway │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── config │ │ │ │ │ ├── application-prod.yml │ │ │ │ │ ├── application-dev.yml │ │ │ │ │ └── application.yml │ │ │ │ └── META-INF │ │ │ │ │ └── additional-spring-configuration-metadata.json │ │ │ └── java │ │ │ │ └── cloud │ │ │ │ └── oj │ │ │ │ └── gateway │ │ │ │ ├── entity │ │ │ │ ├── UsernamePasswd.java │ │ │ │ ├── Role.java │ │ │ │ └── User.java │ │ │ │ ├── error │ │ │ │ ├── GenericException.java │ │ │ │ ├── ErrorMessage.java │ │ │ │ └── GlobalErrorHandler.java │ │ │ │ ├── GatewayApp.java │ │ │ │ ├── config │ │ │ │ └── AuthConfig.java │ │ │ │ ├── filter │ │ │ │ ├── AuthConverter.java │ │ │ │ ├── MutableRequest.java │ │ │ │ └── JwtUtil.java │ │ │ │ ├── service │ │ │ │ └── UserService.java │ │ │ │ └── repo │ │ │ │ └── UserRepo.java │ │ └── test │ │ │ └── java │ │ │ └── cloud │ │ │ └── oj │ │ │ └── gateway │ │ │ └── GatewayAppTests.java │ ├── .gitignore │ └── pom.xml └── core │ ├── src │ ├── main │ │ ├── resources │ │ │ ├── config │ │ │ │ ├── application-prod.yml │ │ │ │ ├── application-dev.yml │ │ │ │ └── application.yml │ │ │ └── META-INF │ │ │ │ └── additional-spring-configuration-metadata.json │ │ └── java │ │ │ └── cloud │ │ │ └── oj │ │ │ └── core │ │ │ ├── entity │ │ │ ├── ContestFilter.java │ │ │ ├── SolutionFilter.java │ │ │ ├── UserFilter.java │ │ │ ├── SPJ.java │ │ │ ├── Language.java │ │ │ ├── Settings.java │ │ │ ├── AcCount.java │ │ │ ├── UserStatistics.java │ │ │ ├── DataConf.java │ │ │ ├── RankingContest.java │ │ │ ├── PageData.java │ │ │ ├── ProblemOrder.java │ │ │ ├── Results.java │ │ │ ├── User.java │ │ │ ├── ScoreDetail.java │ │ │ ├── Contest.java │ │ │ ├── ProblemData.java │ │ │ ├── Ranking.java │ │ │ ├── Problem.java │ │ │ ├── Log.java │ │ │ └── Solution.java │ │ │ ├── error │ │ │ ├── GenericException.java │ │ │ ├── ErrorMessage.java │ │ │ ├── SQLErrorHandler.java │ │ │ └── GlobalErrorHandler.java │ │ │ ├── repo │ │ │ ├── CommonRepo.java │ │ │ ├── SettingsRepo.java │ │ │ ├── InviteeRepo.java │ │ │ ├── LogRepo.java │ │ │ └── UserStatisticRepo.java │ │ │ ├── CoreApp.java │ │ │ ├── service │ │ │ ├── LogService.java │ │ │ └── SystemSettings.java │ │ │ ├── config │ │ │ ├── StaticResourceConfig.java │ │ │ └── AppConfig.java │ │ │ └── controller │ │ │ ├── SettingsController.java │ │ │ ├── LogController.java │ │ │ ├── RankingController.java │ │ │ ├── FileController.java │ │ │ └── SolutionController.java │ └── test │ │ └── java │ │ └── cloud │ │ └── oj │ │ └── core │ │ └── CoreAppTests.java │ ├── .gitignore │ └── pom.xml ├── web ├── public │ └── favicon.png ├── .vscode │ ├── extensions.json │ └── settings.json ├── .prettierrc.json ├── src │ ├── extensions.d.ts │ ├── views │ │ ├── layout │ │ │ ├── RouterLayout.vue │ │ │ ├── index.ts │ │ │ ├── BottomInfo.vue │ │ │ ├── ThemeSwitch.vue │ │ │ ├── TopNavbar.vue │ │ │ └── AdminNavbar.vue │ │ ├── components │ │ │ ├── Error.vue │ │ │ ├── Admin │ │ │ │ ├── Problem │ │ │ │ │ ├── spj.cpp │ │ │ │ │ └── help.md │ │ │ │ └── Overview │ │ │ │ │ ├── Index.vue │ │ │ │ │ ├── ServiceLog.vue │ │ │ │ │ └── QueuesInfoView.vue │ │ │ ├── NotFound.vue │ │ │ ├── Account │ │ │ │ ├── UserProfile.vue │ │ │ │ ├── Overview.vue │ │ │ │ ├── Timeline.vue │ │ │ │ └── ResultsPanel.vue │ │ │ ├── Submission │ │ │ │ └── Skeleton.vue │ │ │ └── Auth │ │ │ │ └── Index.vue │ │ └── FrontRoot.vue │ ├── vite-env.d.ts │ ├── store │ │ ├── index.ts │ │ ├── user.ts │ │ └── app.ts │ ├── main.ts │ ├── components │ │ ├── EmptyData.vue │ │ ├── index.ts │ │ ├── LanguageTag.vue │ │ ├── MarkdownView │ │ │ ├── markdown-img.ts │ │ │ └── markdown-katex.ts │ │ ├── Logo.vue │ │ ├── UserAvatar.vue │ │ ├── ErrorResult.vue │ │ └── ResultTag.vue │ ├── api │ │ ├── request │ │ │ ├── index.ts │ │ │ ├── log-api.ts │ │ │ ├── admin-api.ts │ │ │ ├── judge-api.ts │ │ │ ├── settings-api.ts │ │ │ ├── solution-api.ts │ │ │ ├── ranking-api.ts │ │ │ └── auth-api.ts │ │ └── index.ts │ ├── utils │ │ ├── LanguageUtil.ts │ │ ├── LogFormatter.ts │ │ └── index.ts │ ├── type │ │ └── index.ts │ └── style.scss ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.app.json ├── docker │ ├── config.sh │ ├── nginx.conf │ └── nginx.https.conf ├── index.html ├── .gitignore ├── vite.config.mts └── package.json ├── .gitattributes ├── .gitignore ├── docker └── .env ├── .dockerignore ├── dotnet.runtimeconfig.json ├── .run ├── dev.run.xml ├── docker.run.xml ├── CoreApp.run.xml ├── GatewayApp.run.xml └── JudgeApp.run.xml ├── .github └── workflows │ ├── cmake.yml │ ├── maven.yml │ └── node.js.yml ├── LICENSE ├── README.md └── doc ├── Dev.md └── Build&Setup.md /judge/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | *.iml 4 | cmake-build* -------------------------------------------------------------------------------- /.assets/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifyun/Cloud-OJ/HEAD/.assets/dark.png -------------------------------------------------------------------------------- /.assets/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifyun/Cloud-OJ/HEAD/.assets/light.png -------------------------------------------------------------------------------- /dev/sql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mariadb:11 2 | COPY schema.sql /docker-entrypoint-initdb.d/schema.sql -------------------------------------------------------------------------------- /services/.gitignore: -------------------------------------------------------------------------------- 1 | ### IntelliJ IDEA ### 2 | .idea 3 | *.iml 4 | 5 | ### VS Code ### 6 | .vscode -------------------------------------------------------------------------------- /web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ifyun/Cloud-OJ/HEAD/web/public/favicon.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.sh text eol=lf 3 | *.cmd text eol=crlf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### IntelliJ IDEA ### 2 | .idea/ 3 | *.iml 4 | 5 | ### VS Code ### 6 | .vscode/* 7 | 8 | .build/ -------------------------------------------------------------------------------- /services/judge/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xmx128m" 3 | -------------------------------------------------------------------------------- /services/gateway/src/main/resources/config/application-prod.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | root: warn 4 | cloud.oj: info -------------------------------------------------------------------------------- /web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true 4 | } 5 | -------------------------------------------------------------------------------- /docker/.env: -------------------------------------------------------------------------------- 1 | # CHANGE PASSWORD BEFORE USE. 2 | DB_USER=root 3 | DB_PASSWD=root 4 | RABBIT_PORT=5672 5 | RABBIT_USER=admin 6 | RABBIT_PASSWD=admin -------------------------------------------------------------------------------- /web/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": true, 3 | "semi": false, 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /services/core/src/main/resources/config/application-prod.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | root: warn 4 | cloud.oj: info 5 | app: 6 | file-dir: /var/lib/cloud-oj/ -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .assets/ 3 | .github/ 4 | **/.idea/ 5 | **/.vscode/ 6 | **/node_modules/ 7 | **/dist/ 8 | **/target/ 9 | **/cmake-build-*/ 10 | 11 | **/.gitignore 12 | **/*.iml 13 | *.md -------------------------------------------------------------------------------- /dotnet.runtimeconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtimeOptions": { 3 | "tfm": "netcoreapp8.0", 4 | "framework": { 5 | "name": "Microsoft.NETCore.App", 6 | "version": "8.0.0" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /services/judge/src/main/resources/config/application-prod.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | root: warn 4 | cloud.oj: info 5 | app: 6 | file-dir: /var/lib/cloud-oj/ 7 | judge-cpus: ${JUDGE_CPUS:1} -------------------------------------------------------------------------------- /web/src/extensions.d.ts: -------------------------------------------------------------------------------- 1 | import "pinia" 2 | import { Router } from "vue-router" 3 | 4 | declare module "pinia" { 5 | export interface PiniaCustomProperties { 6 | router: Router 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/ContestFilter.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | public class ContestFilter { 4 | public String keyword = ""; 5 | public Boolean hideEnded = false; 6 | } 7 | -------------------------------------------------------------------------------- /services/core/src/main/resources/config/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | consul: 4 | discovery: 5 | prefer-ip-address: true 6 | logging: 7 | level: 8 | root: info 9 | cloud.oj: info -------------------------------------------------------------------------------- /services/gateway/src/main/resources/config/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | consul: 4 | discovery: 5 | prefer-ip-address: true 6 | logging: 7 | level: 8 | root: info 9 | cloud.oj: info -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/SolutionFilter.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | public class SolutionFilter { 4 | public Integer pid; 5 | public String username; 6 | public Long date; 7 | } 8 | -------------------------------------------------------------------------------- /web/src/views/layout/RouterLayout.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import type { DefineComponent } from "vue" 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/UserFilter.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | public class UserFilter { 4 | public Integer type = 0; // 1: by username, 2: by nickname 5 | public String keyword = ""; 6 | } 7 | -------------------------------------------------------------------------------- /services/core/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "app.file-dir", 5 | "type": "java.lang.String", 6 | "description": "文件存放目录." 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /judge/include/env_setup.h: -------------------------------------------------------------------------------- 1 | #ifndef ENV_SETUP_H 2 | #define ENV_SETUP_H 1 3 | 4 | int exec_cmd(const char* fmt, ...); 5 | 6 | void setup_env(const char* work_dir); 7 | 8 | void end_env(const char* work_dir); 9 | 10 | #endif // ENV_SETUP_H 11 | -------------------------------------------------------------------------------- /services/judge/src/main/resources/config/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | consul: 4 | discovery: 5 | prefer-ip-address: true 6 | logging: 7 | level: 8 | root: info 9 | cloud.oj: info 10 | app: 11 | judge-cpus: 0,1 -------------------------------------------------------------------------------- /services/gateway/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "app.token-valid-time", 5 | "type": "java.lang.Integer", 6 | "description": "Token 有效时间." 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/SPJ.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class SPJ { 9 | private Integer pid; 10 | private String source; 11 | } 12 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/Language.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Language { 9 | private Integer language; 10 | private Integer count; 11 | } 12 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/error/UnsupportedLanguageError.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.error; 2 | 3 | public class UnsupportedLanguageError extends RuntimeException { 4 | public UnsupportedLanguageError(String msg) { 5 | super("不支持的语言: " + msg); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /web/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia" 2 | import { useAppStore } from "./app" 3 | import { useUserStore } from "./user" 4 | 5 | export const useStore = defineStore("store", { 6 | state: () => ({ 7 | app: useAppStore(), 8 | user: useUserStore() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /dev/mvn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CN 6 | Ali-Maven 7 | https://maven.aliyun.com/repository/central 8 | central 9 | 10 | 11 | -------------------------------------------------------------------------------- /services/core/src/test/java/cloud/oj/core/CoreAppTests.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class CoreAppTests { 8 | @Test 9 | void contextLoads() { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/entity/UsernamePasswd.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class UsernamePasswd { 9 | private String username; 10 | private String password; 11 | } 12 | -------------------------------------------------------------------------------- /services/judge/src/test/java/cloud/oj/judge/JudgeAppTests.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class JudgeAppTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /services/gateway/src/test/java/cloud/oj/gateway/GatewayAppTests.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class GatewayAppTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/entity/Problem.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Problem { 9 | private int timeout; 10 | private int memoryLimit; 11 | private int outputLimit; 12 | private int score; 13 | } 14 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/entity/Contest.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Contest { 9 | private String contestName; 10 | private boolean started; 11 | private boolean ended; 12 | private int languages; 13 | } 14 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import router from "@/router" 2 | import { createPinia } from "pinia" 3 | import "vfonts/FiraCode.css" 4 | import "vfonts/Inter.css" 5 | import { createApp } from "vue" 6 | import App from "./App.vue" 7 | import "./style.scss" 8 | 9 | const pinia = createPinia() 10 | pinia.use(() => ({ router })) 11 | 12 | createApp(App).use(router).use(pinia).mount("#app") 13 | -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "include": ["vite.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "noEmit": true, 7 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 8 | "module": "ESNext", 9 | "moduleResolution": "Bundler", 10 | "types": ["node"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/Settings.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Settings { 9 | private boolean alwaysShowRanking; 10 | private boolean showAllContest; 11 | private boolean showPassedPoints; 12 | private boolean autoDelSolutions; 13 | } 14 | -------------------------------------------------------------------------------- /web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/AcCount.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * 用户 AC 记录 8 | * 9 | *

按(problem_id,date)分组;按(problem_id,language)计数

10 | */ 11 | @Getter 12 | @Setter 13 | public class AcCount { 14 | private Integer pid; 15 | private String date; 16 | private Integer count; 17 | } 18 | -------------------------------------------------------------------------------- /web/docker/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ME=$(basename "$0") 3 | conf_file_path='/etc/nginx/templates/default.conf.template' 4 | mkdir /etc/nginx/templates; 5 | if [ "$ENABLE_HTTPS" = 'true' ]; then 6 | echo "$ME: HTTPS ON"; 7 | echo "$ME: EXTERNAL_URL $EXTERNAL_URL" 8 | cp /templates/nginx.https.conf $conf_file_path; 9 | else 10 | echo "$ME: HTTPS OFF"; 11 | cp /templates/nginx.conf $conf_file_path; 12 | fi -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/UserStatistics.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | @Getter 10 | @Setter 11 | public class UserStatistics { 12 | private Results results; 13 | private List preference; 14 | private Map activities; 15 | } 16 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cloud OJ 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /judge/include/common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H 2 | #define COMMON_H 1 3 | 4 | #include "runner.h" 5 | 6 | /// 解析参数 7 | int get_args(int argc, char* argv[], char* cmd, char workdir[], char datadir[], Config& config); 8 | 9 | /** 10 | * 分割字符串到数组 11 | * @param arr 保存结果的字符串数组 12 | * @param str 被分割的字符串 13 | * @param separator 分隔符 14 | */ 15 | void split(char** arr, char* str, const char* separator); 16 | 17 | #endif // COMMON_H 18 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/entity/SubmitData.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class SubmitData { 9 | private Integer uid; 10 | private Integer problemId; 11 | private Integer contestId; 12 | private String sourceCode; 13 | private Integer language; 14 | private Long submitTime; 15 | } 16 | -------------------------------------------------------------------------------- /web/src/views/layout/index.ts: -------------------------------------------------------------------------------- 1 | import TopNavbar from "./TopNavbar.vue" 2 | import BottomInfo from "./BottomInfo.vue" 3 | import UserMenu from "./UserMenu.vue" 4 | import AdminNavbar from "./AdminNavbar.vue" 5 | import ThemeSwitch from "./ThemeSwitch.vue" 6 | import RouterLayout from "./RouterLayout.vue" 7 | 8 | export { 9 | TopNavbar, 10 | BottomInfo, 11 | UserMenu, 12 | AdminNavbar, 13 | ThemeSwitch, 14 | RouterLayout 15 | } 16 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/error/GenericException.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.error; 2 | 3 | import lombok.Getter; 4 | import org.springframework.http.HttpStatus; 5 | 6 | @Getter 7 | public class GenericException extends RuntimeException { 8 | private final HttpStatus status; 9 | 10 | public GenericException(HttpStatus status, String msg) { 11 | super(msg); 12 | this.status = status; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/error/GenericException.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.error; 2 | 3 | import lombok.Getter; 4 | import org.springframework.http.HttpStatus; 5 | 6 | @Getter 7 | public class GenericException extends RuntimeException { 8 | private final HttpStatus status; 9 | 10 | public GenericException(HttpStatus status, String msg) { 11 | super(msg); 12 | this.status = status; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.run/dev.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | dist-ssr 5 | coverage 6 | *.local 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | pnpm-debug.log* 19 | lerna-debug.log* 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | !.vscode/settings.json 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | -------------------------------------------------------------------------------- /.run/docker.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /services/core/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | .mvn/ 4 | mvnw 5 | mvnw.cmd 6 | !**/src/main/** 7 | !**/src/test/** 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /services/judge/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | .mvn/ 4 | mvnw 5 | mvnw.cmd 6 | !**/src/main/** 7 | !**/src/test/** 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /services/gateway/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | .mvn/ 4 | mvnw 5 | mvnw.cmd 6 | !**/src/main/** 7 | !**/src/test/** 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /.run/CoreApp.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/GatewayApp.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @EnableDiscoveryClient 8 | @SpringBootApplication 9 | public class GatewayApp { 10 | public static void main(String[] args) { 11 | SpringApplication.run(GatewayApp.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.run/GatewayApp.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/PageData.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * 分页数据 12 | * 13 | * @param 数据类型 14 | */ 15 | @Getter 16 | @Setter 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | public class PageData { 20 | private List data; // 当前页的数据 21 | private Integer total; // 总数 22 | } 23 | -------------------------------------------------------------------------------- /services/judge/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "app", 5 | "type": "cloud.oj.judge.config.AppConfig", 6 | "description": "应用程序配置." 7 | }, 8 | { 9 | "name": "app.judge-cpus", 10 | "type": "java.lang.String", 11 | "description": "判题使用的 CPU 核心, 逗号分隔." 12 | }, 13 | { 14 | "name": "app.file-dir", 15 | "type": "java.lang.String", 16 | "description": "文件存放目录." 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /web/src/components/EmptyData.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /web/src/api/request/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AuthApi } from "./auth-api" 2 | export { default as ProblemApi } from "./problem-api" 3 | export { default as ContestApi } from "./contest-api" 4 | export { default as RankingApi } from "./ranking-api" 5 | export { default as UserApi } from "./user-api" 6 | export { default as JudgeApi } from "./judge-api" 7 | export { default as SettingsApi } from "./settings-api" 8 | export { default as LogApi } from "./log-api" 9 | export { default as SolutionApi } from "./solution-api" 10 | export { QueuesInfoPoller } from "./admin-api" 11 | -------------------------------------------------------------------------------- /judge/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | build_dir="cmake-build-release" 4 | 5 | if [ -d ${build_dir} ]; then 6 | rm -rf ${build_dir} 7 | fi 8 | 9 | mkdir ${build_dir} 10 | 11 | cmake -B ${build_dir} \ 12 | -DCMAKE_BUILD_TYPE=Release \ 13 | -DCMAKE_SYSTEM_NAME=Linux \ 14 | -DCMAKE_SYSTEM_PROCESSOR=x86_64 \ 15 | -DCMAKE_C_FLAGS=-m64 \ 16 | -DCMAKE_CXX_FLAGS=-m64 \ 17 | -G "Unix Makefiles" 18 | 19 | if [ "$1" == "install" ]; then 20 | sudo cmake --build ${build_dir} --target all install 21 | sudo rm -r ${build_dir} 22 | else 23 | cmake --build ${build_dir} --target all 24 | fi 25 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/entity/Role.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import org.springframework.security.core.GrantedAuthority; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class Role implements GrantedAuthority { 14 | 15 | private int roleId; 16 | private String roleName; 17 | 18 | @Override 19 | public String getAuthority() { 20 | return roleName; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/ProblemOrder.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @NoArgsConstructor 10 | public class ProblemOrder implements Comparable { 11 | private Integer problemId; 12 | private Integer order; 13 | 14 | public ProblemOrder(Integer order) { 15 | this.order = order; 16 | } 17 | 18 | @Override 19 | public int compareTo(ProblemOrder o) { 20 | return this.order.compareTo(o.order); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CodeEditor } from "./CodeEditor.vue" 2 | export { default as EmptyData } from "./EmptyData.vue" 3 | export { default as ErrorResult } from "./ErrorResult.vue" 4 | export { default as LanguageTag } from "./LanguageTag.vue" 5 | export { default as Logo } from "./Logo.vue" 6 | export { default as MarkdownView } from "./MarkdownView/Index.vue" 7 | export { default as MarkdownEditor } from "./MarkdownEditor/Index.vue" 8 | export { default as ResultTag } from "./ResultTag.vue" 9 | export { default as SourceCodeView } from "./SourceCodeView.vue" 10 | export { default as UserAvatar } from "./UserAvatar.vue" 11 | -------------------------------------------------------------------------------- /web/src/views/components/Admin/Problem/spj.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /** 4 | * @brief 实现此函数完成 Special Judge 5 | * 6 | * 没有输入数据时 in 指向 /dev/null 7 | * 8 | * 此函数发生错误归类为内部错误(IE) 9 | * 10 | * @param in: 输入 11 | * @param out: 用户输出 12 | * @param ans: 正确输出 13 | * @return @c true AC @c false WA 14 | */ 15 | extern "C" bool spj(std::ifstream *in, std::ifstream *out, std::ifstream *ans) { 16 | /*---- A + B SPJ 示例 ----* 17 | int a, b, r; 18 | 19 | *in >> a; 20 | *in >> b; 21 | *out >> r; 22 | 23 | if (a + b == r) { 24 | return true; 25 | } 26 | 27 | return false; 28 | *------------------------*/ 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | BUILD_TYPE: Release 11 | WORKING_DIR: judge 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Configure CMake 20 | run: cmake -B cmake-build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -G "Unix Makefiles" 21 | working-directory: ${{ env.WORKING_DIR }} 22 | 23 | - name: Build 24 | run: cmake --build cmake-build --config ${{env.BUILD_TYPE}} 25 | working-directory: ${{ env.WORKING_DIR }} 26 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | WORKING_DIR: services 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up JDK 17 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: '17' 22 | distribution: 'microsoft' 23 | cache: 'maven' 24 | 25 | - name: Build with Maven 26 | run: mvn -B package '-Dmaven.test.skip=true' --file pom.xml 27 | working-directory: ${{ env.WORKING_DIR }} 28 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/Results.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class Results { 10 | @JsonProperty("AC") 11 | private Integer AC; 12 | 13 | @JsonProperty("WA") 14 | private Integer WA; 15 | 16 | @JsonProperty("CE") 17 | private Integer CE; 18 | 19 | @JsonProperty("RE") 20 | private Integer RE; 21 | 22 | @JsonProperty("MLE") 23 | private Integer MLE; 24 | 25 | @JsonProperty("TLE") 26 | private Integer TLE; 27 | 28 | private Integer total; 29 | } 30 | -------------------------------------------------------------------------------- /web/docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name _; 5 | 6 | access_log /var/log/nginx/access.log main; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | index index.html index.htm; 11 | try_files $uri $uri/ /index.html; 12 | gzip_static on; 13 | } 14 | 15 | location /api/ { 16 | proxy_pass http://${API_HOST}; 17 | proxy_buffering off; 18 | proxy_cache off; 19 | client_max_body_size 128M; 20 | } 21 | 22 | error_page 500 502 503 504 /50x.html; 23 | location = /50x.html { 24 | root /usr/share/nginx/html; 25 | } 26 | } -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/repo/CommonRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.repo; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.jdbc.core.simple.JdbcClient; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | @RequiredArgsConstructor 9 | public class CommonRepo { 10 | 11 | private final JdbcClient client; 12 | 13 | /** 14 | * 设置当前会话的时区 15 | * 16 | * @param timezone 时区偏移,eg: +8:00 17 | */ 18 | public void setTimezone(String timezone) { 19 | client.sql("set time_zone = :timezone") 20 | .param("timezone", timezone) 21 | .query(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/src/components/LanguageTag.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/entity/Compile.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigInteger; 7 | 8 | @Getter 9 | @Setter 10 | public class Compile { 11 | private Integer id; 12 | private BigInteger solutionId; 13 | private Integer state; 14 | private String info; 15 | 16 | public Compile(String solutionId, Integer state) { 17 | this(solutionId, state, null); 18 | } 19 | 20 | public Compile(String solutionId, Integer state, String info) { 21 | this.solutionId = new BigInteger(solutionId); 22 | this.state = state; 23 | this.info = info; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/repo/SettingsRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.repo; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.jdbc.core.simple.JdbcClient; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | @RequiredArgsConstructor 9 | public class SettingsRepo { 10 | 11 | private final JdbcClient client; 12 | 13 | /** 14 | * 查询是否自动删除题解临时文件 15 | * 16 | * @return {@link Boolean} 17 | */ 18 | public Boolean autoDelSolutions() { 19 | return client.sql("select auto_del_solutions from settings where id = 0") 20 | .query(Boolean.class) 21 | .single(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/src/api/request/log-api.ts: -------------------------------------------------------------------------------- 1 | import axios, { ApiPath, resolveError } from "@/api" 2 | import type { Log } from "@/api/type" 3 | 4 | const LogApi = { 5 | getLatest10Logs(time: number | null = null): Promise> { 6 | return new Promise>((resolve, reject) => { 7 | axios({ 8 | url: ApiPath.LOG, 9 | method: "GET", 10 | params: { 11 | time 12 | } 13 | }) 14 | .then((res) => { 15 | if (res.status === 204) { 16 | resolve([]) 17 | } 18 | resolve(res.data) 19 | }) 20 | .catch((error) => reject(resolveError(error))) 21 | }) 22 | } 23 | } 24 | 25 | export default LogApi 26 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/error/ErrorMessage.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.error; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @Setter 9 | public class ErrorMessage { 10 | private Long timestamp; 11 | private Integer status; 12 | private String error; 13 | private String message; 14 | 15 | public ErrorMessage() { 16 | timestamp = System.currentTimeMillis(); 17 | } 18 | 19 | public ErrorMessage(HttpStatus status, String message) { 20 | this(); 21 | this.status = status.value(); 22 | this.message = message; 23 | error = status.getReasonPhrase(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/error/ErrorMessage.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.error; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @Setter 9 | public class ErrorMessage { 10 | private Long timestamp; 11 | private Integer status; 12 | private String error; 13 | private String message; 14 | 15 | public ErrorMessage() { 16 | timestamp = System.currentTimeMillis(); 17 | } 18 | 19 | public ErrorMessage(HttpStatus status, String message) { 20 | this(); 21 | this.status = status.value(); 22 | this.message = message; 23 | error = status.getReasonPhrase(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/User.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class User { 10 | @JsonIgnore 11 | public Integer _total; 12 | 13 | private Integer uid; 14 | private String username; 15 | private String nickname; 16 | private String realName; 17 | private String password; 18 | private String secret; 19 | private String email; 20 | private String section; 21 | private Boolean hasAvatar; 22 | private Boolean star; 23 | private Integer role; 24 | // UNIX 时间戳(10位) 25 | private Long createAt; 26 | } 27 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/error/ErrorMessage.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.error; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @Setter 9 | public class ErrorMessage { 10 | private Long timestamp; 11 | private Integer status; 12 | private String error; 13 | private String message; 14 | 15 | public ErrorMessage() { 16 | timestamp = System.currentTimeMillis(); 17 | } 18 | 19 | public ErrorMessage(HttpStatus status, String message) { 20 | this(); 21 | this.status = status.value(); 22 | this.message = message; 23 | error = status.getReasonPhrase(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/src/components/MarkdownView/markdown-img.ts: -------------------------------------------------------------------------------- 1 | import { ApiPath } from "@/api" 2 | 3 | /** 4 | * markdown-it 图片插件 5 | */ 6 | export const ImgPlugin = (md: any) => { 7 | const image = md.renderer.rules.image.bind(md.renderer.rules) 8 | 9 | md.renderer.rules.image = ( 10 | tokens: any, 11 | index: number, 12 | options: any, 13 | env: any, 14 | slf: any 15 | ) => { 16 | const attrs = tokens[index].attrs as Array 17 | const src = attrs[0][1] 18 | const alt = attrs[1][1] 19 | 20 | if (src.startsWith("http:") || src.startsWith("https:")) { 21 | return image(tokens, index, options, env, slf) 22 | } 23 | 24 | // 对用户上传的图片加上路径 25 | return `${alt}` 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/repo/SourceRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.repo; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.jdbc.core.simple.JdbcClient; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | @RequiredArgsConstructor 9 | public class SourceRepo { 10 | 11 | private final JdbcClient client; 12 | 13 | /** 14 | * 写入题解代码 15 | * 16 | * @param sid 题解 Id 17 | * @param source 源代码 18 | */ 19 | public void insert(String sid, String source) { 20 | client.sql("insert into source_code(solution_id, code) values(:sid, :source)") 21 | .param("sid", sid) 22 | .param("source", source) 23 | .update(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/CoreApp.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core; 2 | 3 | import cloud.oj.core.config.AppConfig; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 8 | import org.springframework.transaction.annotation.EnableTransactionManagement; 9 | 10 | @SpringBootApplication 11 | @EnableDiscoveryClient 12 | @EnableTransactionManagement 13 | @EnableConfigurationProperties(AppConfig.class) 14 | public class CoreApp { 15 | public static void main(String[] args) { 16 | SpringApplication.run(CoreApp.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/service/LogService.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.service; 2 | 3 | import cloud.oj.core.entity.Log; 4 | import cloud.oj.core.repo.LogRepo; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | @RequiredArgsConstructor 12 | public class LogService { 13 | 14 | private final LogRepo logRepo; 15 | 16 | public List getLatest10(Long time) { 17 | return logRepo.selectLatest10(time == null ? 0 : time); 18 | } 19 | 20 | public List getRange(Long start, Long end) { 21 | return logRepo.selectRange(start, end) 22 | .stream() 23 | .map(Log::toString) 24 | .toList(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/src/utils/LanguageUtil.ts: -------------------------------------------------------------------------------- 1 | const LANGUAGE_LENGTH = 8 2 | 3 | const LanguageUtil = { 4 | /** 5 | * 列出语言,将二进制表示的语言列表转换为数组 6 | * 7 | * @param languages 语言列表 8 | */ 9 | toArray(languages: number): Array { 10 | const arr: Array = [] 11 | 12 | for (let i = 0; i <= LANGUAGE_LENGTH; i += 1) { 13 | const t = 1 << i 14 | if ((languages & t) === t) { 15 | arr.push(i) 16 | } 17 | } 18 | 19 | return arr 20 | }, 21 | 22 | /** 23 | * 将数组表示的语言转化为数字 24 | * 25 | * @param arr 语言数组 26 | */ 27 | toNumber(arr: Array) { 28 | let languages = 0 29 | 30 | arr.forEach((v) => { 31 | languages = languages | (1 << v) 32 | }) 33 | 34 | return languages 35 | } 36 | } 37 | 38 | export default LanguageUtil 39 | -------------------------------------------------------------------------------- /judge/include/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 1 3 | 4 | #include 5 | #include 6 | #include "runner.h" 7 | 8 | class Utils 9 | { 10 | /** 11 | * 返回去除文件末尾所有空格/换行符后的偏移量 12 | * @return 偏移量 13 | */ 14 | static __off_t get_rtrim_offset(int fd); 15 | 16 | public: 17 | /** 18 | * 从指定目录获取测试数据文件的路径 19 | */ 20 | static std::vector get_files(const std::string&, const std::string&); 21 | 22 | /** 23 | * 比较文件 24 | * @return @c true - 相同, @c false - 不同 25 | */ 26 | static bool compare(const Config& config, spj_func spj); 27 | 28 | /** 29 | * 计算结果 30 | */ 31 | static void calc_results(RTN& rtn, const std::vector& results, const std::vector& test_points); 32 | }; 33 | 34 | #endif // UTILS_H 35 | -------------------------------------------------------------------------------- /web/src/views/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/service/SystemSettings.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.service; 2 | 3 | import cloud.oj.core.entity.Settings; 4 | import cloud.oj.core.error.GenericException; 5 | import cloud.oj.core.repo.SettingsRepo; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | @RequiredArgsConstructor 12 | public class SystemSettings { 13 | 14 | private final SettingsRepo settingsRepo; 15 | 16 | public Settings getSettings() { 17 | return settingsRepo.select(); 18 | } 19 | 20 | public void setSettings(Settings settings) { 21 | if (settingsRepo.update(settings) == 0) { 22 | throw new GenericException(HttpStatus.BAD_REQUEST, "操作失败"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/vite.config.mts: -------------------------------------------------------------------------------- 1 | import vue from "@vitejs/plugin-vue" 2 | import vueJsx from "@vitejs/plugin-vue-jsx" 3 | import { fileURLToPath, URL } from "node:url" 4 | import { defineConfig } from "vite" 5 | import viteCompression from "vite-plugin-compression" 6 | import vueDevTools from "vite-plugin-vue-devtools" 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue(), 12 | vueJsx(), 13 | vueDevTools(), 14 | viteCompression({ 15 | threshold: 20480 16 | }) 17 | ], 18 | resolve: { 19 | alias: { 20 | "@": fileURLToPath(new URL("./src", import.meta.url)) 21 | } 22 | }, 23 | server: { 24 | host: true, 25 | proxy: { 26 | "/api": { 27 | target: "http://127.0.0.1:8080", 28 | changeOrigin: true 29 | } 30 | } 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /dev/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | consul: 3 | image: consul:1.15 4 | command: 5 | - "agent" 6 | - "-dev" 7 | - "-client=0.0.0.0" 8 | - "-log-level=err" 9 | ports: 10 | - "8500:8500" 11 | mariadb: 12 | image: mariadb:11 13 | ports: 14 | - "3306:3306" 15 | environment: 16 | MARIADB_ROOT_PASSWORD: "root" 17 | MARIADB_ROOT_HOST: "%" 18 | volumes: 19 | - "mariadb:/var/lib/mysql" 20 | - "./sql:/docker-entrypoint-initdb.d" 21 | rabbitmq: 22 | image: rabbitmq:4-management-alpine 23 | ports: 24 | - "5672:5672" 25 | - "15672:15672" 26 | environment: 27 | RABBITMQ_DEFAULT_USER: "admin" 28 | RABBITMQ_DEFAULT_PASS: "admin" 29 | volumes: 30 | - "rabbitmq:/var/lib/rabbitmq/mnesia" 31 | volumes: 32 | mariadb: 33 | rabbitmq: 34 | -------------------------------------------------------------------------------- /web/src/api/request/admin-api.ts: -------------------------------------------------------------------------------- 1 | import { ApiPath } from "@/api" 2 | import { type QueuesInfo } from "@/api/type" 3 | 4 | class QueuesInfoPoller { 5 | private sse: EventSource 6 | 7 | constructor( 8 | token: string | undefined, 9 | onMessage: (data: QueuesInfo) => void, 10 | onError: (error: string) => void 11 | ) { 12 | this.sse = new EventSource(`${ApiPath.QUEUE_INFO}?token=${token}`) 13 | this.sse.onmessage = (event) => { 14 | const data = JSON.parse(event.data) as QueuesInfo 15 | onMessage(data) 16 | } 17 | 18 | this.sse.onerror = (event) => { 19 | const data = (event as MessageEvent).data 20 | if (data) { 21 | onError(data) 22 | } 23 | } 24 | } 25 | 26 | // 组件解除挂载时调用 27 | public close() { 28 | this.sse.close() 29 | } 30 | } 31 | 32 | export { QueuesInfoPoller } 33 | -------------------------------------------------------------------------------- /web/src/api/request/judge-api.ts: -------------------------------------------------------------------------------- 1 | import axios, { ApiPath, resolveError } from "@/api" 2 | import type { SubmitData } from "@/api/type" 3 | import { useStore } from "@/store" 4 | 5 | const JudgeApi = { 6 | /** 7 | * 提交代码 8 | */ 9 | submit(data: SubmitData): Promise { 10 | return new Promise((resolve, reject) => { 11 | const role = useStore().user.userInfo!.role 12 | const path = role === 1 ? ApiPath.SUBMIT : ApiPath.ADMIN_SUBMIT 13 | 14 | axios({ 15 | url: path, 16 | method: "POST", 17 | data: JSON.stringify(data, (_, v) => v ?? undefined) 18 | }) 19 | .then((res) => { 20 | resolve(res.data as number) 21 | }) 22 | .catch((error) => { 23 | reject(resolveError(error)) 24 | }) 25 | }) 26 | } 27 | } 28 | 29 | export default JudgeApi 30 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | WORKING_DIR: web 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [20.x] 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: 'npm' 26 | cache-dependency-path: ${{ env.WORKING_DIR }}/package-lock.json 27 | 28 | - name: Install Dependencies 29 | run: npm ci 30 | working-directory: ${{ env.WORKING_DIR }} 31 | 32 | - name: Vite Build 33 | run: npm run build --if-present 34 | working-directory: ${{ env.WORKING_DIR }} 35 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/JudgeApp.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge; 2 | 3 | import cloud.oj.judge.config.AppConfig; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 8 | import org.springframework.transaction.annotation.EnableTransactionManagement; 9 | 10 | /** 11 | * 使用 ROOT 权限运行 12 | *

13 | * sudo mvn spring-boot:run 14 | *

15 | */ 16 | @SpringBootApplication 17 | @EnableDiscoveryClient 18 | @EnableTransactionManagement 19 | @EnableConfigurationProperties(AppConfig.class) 20 | public class JudgeApp { 21 | 22 | public static void main(String[] args) { 23 | SpringApplication.run(JudgeApp.class, args); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | 24 | 29 | 30 | 44 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/config/LoggingConfig.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.config; 2 | 3 | import ch.qos.logback.classic.LoggerContext; 4 | import cloud.oj.judge.logging.DBAppender; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.jdbc.core.simple.JdbcClient; 10 | 11 | @Configuration 12 | public class LoggingConfig { 13 | 14 | @Bean 15 | public DBAppender dbAppender(Environment env, JdbcClient client) { 16 | var appender = new DBAppender(env, client); 17 | appender.start(); 18 | var loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); 19 | var logger = loggerContext.getLogger("ROOT"); 20 | logger.addAppender(appender); 21 | return appender; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/src/api/request/settings-api.ts: -------------------------------------------------------------------------------- 1 | import axios, { ApiPath, resolveError } from "@/api" 2 | import { Settings } from "@/api/type" 3 | import type { AxiosResponse } from "axios" 4 | 5 | const SettingsApi = { 6 | get(): Promise { 7 | return new Promise((resolve, reject) => { 8 | axios({ 9 | url: ApiPath.SETTINGS, 10 | method: "GET" 11 | }) 12 | .then((res) => resolve(res.data)) 13 | .catch((error) => reject(resolveError(error))) 14 | }) 15 | }, 16 | 17 | save(settings: Settings): Promise { 18 | return new Promise((resolve, reject) => { 19 | axios({ 20 | url: ApiPath.SETTINGS, 21 | method: "PUT", 22 | data: JSON.stringify(settings) 23 | }) 24 | .then((res) => resolve(res)) 25 | .catch((error) => reject(resolveError(error))) 26 | }) 27 | } 28 | } 29 | 30 | export default SettingsApi 31 | -------------------------------------------------------------------------------- /web/src/views/layout/BottomInfo.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 35 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/ScoreDetail.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonUnwrapped; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | public class ScoreDetail implements Comparable { 12 | private Integer problemId; 13 | private Integer result; 14 | private Double score; 15 | 16 | @JsonUnwrapped 17 | private Integer order; 18 | 19 | public ScoreDetail(Integer problemId, Integer order) { 20 | this.problemId = problemId; 21 | this.order = order; 22 | } 23 | 24 | /** 25 | *

按 order 排序,相同则按 problemId 排序

26 | */ 27 | @Override 28 | public int compareTo(ScoreDetail o) { 29 | var r = this.order.compareTo(o.order); 30 | return r == 0 ? this.problemId.compareTo(o.problemId) : r; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web/src/views/FrontRoot.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 28 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/error/SQLErrorHandler.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.error; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.core.annotation.Order; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.bind.annotation.RestControllerAdvice; 9 | 10 | import java.sql.SQLException; 11 | 12 | /** 13 | * SQL 异常处理 14 | */ 15 | @Slf4j 16 | @Order(1) 17 | @RestControllerAdvice 18 | public class SQLErrorHandler { 19 | 20 | @ExceptionHandler(SQLException.class) 21 | public ResponseEntity sqlErrorHandler(SQLException e) { 22 | log.error("数据库错误: code={}, msg={}", e.getErrorCode(), e.getMessage()); 23 | 24 | var status = HttpStatus.INTERNAL_SERVER_ERROR; 25 | var msg = new ErrorMessage(status, "数据库异常"); 26 | 27 | return ResponseEntity.status(status).body(msg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/docker/nginx.https.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | 5 | server_name ${EXTERNAL_URL}; 6 | return 308 https://${EXTERNAL_URL}$request_uri; 7 | } 8 | 9 | server { 10 | listen 443 ssl; 11 | listen [::]:443 ssl; 12 | 13 | ssl_certificate /ssl/cert.pem; 14 | ssl_certificate_key /ssl/private.key; 15 | ssl_session_timeout 240m; 16 | 17 | access_log /var/log/nginx/access.log main; 18 | 19 | server_name ${EXTERNAL_URL}; 20 | 21 | location / { 22 | root /usr/share/nginx/html; 23 | index index.html index.htm; 24 | try_files $uri $uri/ /index.html; 25 | gzip_static on; 26 | } 27 | 28 | location /api/ { 29 | proxy_pass http://${API_HOST}; 30 | proxy_buffering off; 31 | proxy_cache off; 32 | client_max_body_size 128M; 33 | } 34 | 35 | error_page 500 502 503 504 /50x.html; 36 | location = /50x.html { 37 | root /usr/share/nginx/html; 38 | } 39 | } -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/config/StaticResourceConfig.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 6 | 7 | /** 8 | * 静态文件服务配置 9 | */ 10 | @Configuration 11 | public class StaticResourceConfig implements WebMvcConfigurer { 12 | 13 | private final String fileDir; 14 | 15 | public StaticResourceConfig(AppConfig appConfig) { 16 | this.fileDir = appConfig.getFileDir(); 17 | } 18 | 19 | @Override 20 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 21 | registry.addResourceHandler("/file/data/download/**") 22 | .addResourceLocations("file:" + fileDir + "data/"); 23 | registry.addResourceHandler("/file/img/**") 24 | .addResourceLocations("file:" + fileDir + "image/"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/Contest.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | import java.util.List; 10 | 11 | @Getter 12 | @Setter 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class Contest { 16 | @JsonIgnore 17 | public Integer _total; 18 | 19 | private Integer contestId; 20 | private String contestName; 21 | private String inviteKey; 22 | private Integer problemCount; 23 | // UNIX 时间戳(10 位) 24 | private Long startAt; 25 | private Long endAt; 26 | private Integer languages; 27 | private boolean started; 28 | private boolean ended; 29 | // UNIX 时间戳(10 位) 30 | private Long createAt; 31 | private List ranking; 32 | 33 | public Contest withoutKey() { 34 | this.inviteKey = null; 35 | return this; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/ProblemData.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * 题目的测试数据和 SPJ 代码 13 | */ 14 | @Getter 15 | @Setter 16 | public class ProblemData { 17 | private Integer pid; 18 | private String title; 19 | private Boolean spj = false; 20 | @JsonProperty("SPJSource") 21 | private String SPJSource; 22 | private List testData = new ArrayList<>(); 23 | 24 | @Getter 25 | @Setter 26 | @NoArgsConstructor 27 | public static class TestData { 28 | private String fileName; 29 | private long size; 30 | private Integer score; 31 | 32 | public TestData(String fileName, long size) { 33 | this.fileName = fileName; 34 | this.size = size; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/controller/SettingsController.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.controller; 2 | 3 | import cloud.oj.core.entity.Settings; 4 | import cloud.oj.core.service.SystemSettings; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | @RestController 10 | @RequestMapping("settings") 11 | @RequiredArgsConstructor 12 | public class SettingsController { 13 | 14 | private final SystemSettings systemSettings; 15 | 16 | /** 17 | * 获取系统设置 18 | */ 19 | @GetMapping 20 | public ResponseEntity getSettings() { 21 | return ResponseEntity.ok(systemSettings.getSettings()); 22 | } 23 | 24 | /** 25 | * 更新系统设置 26 | */ 27 | @PutMapping(consumes = "application/json") 28 | public ResponseEntity updateSettings(@RequestBody Settings settings) { 29 | systemSettings.setSettings(settings); 30 | return ResponseEntity.ok().build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/entity/Result.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * 对应判题程序返回结果 12 | */ 13 | @Getter 14 | @Setter 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class Result { 18 | public static final int CE = 6; // 编译错误 19 | public static final int RE = 7; // 运行错误 20 | public static final int IE = 8; // 内部错误 21 | 22 | private Integer result; 23 | private Integer total; 24 | private Integer passed; 25 | private Double passRate; 26 | private Long time; 27 | private Long memory; 28 | private String error; 29 | private List detail; 30 | 31 | public static Result withError(Integer result, String error) { 32 | var instance = new Result(); 33 | instance.result = result; 34 | instance.error = error; 35 | return instance; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/constant/Language.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.constant; 2 | 3 | import cloud.oj.judge.error.UnsupportedLanguageError; 4 | 5 | public class Language { 6 | public static final int C = 0; 7 | public static final int CPP = 1; 8 | public static final int JAVA = 2; 9 | public static final int PYTHON = 3; 10 | public static final int BASH = 4; 11 | public static final int C_SHARP = 5; 12 | public static final int JAVA_SCRIPT = 6; 13 | public static final int KOTLIN = 7; 14 | public static final int GO = 8; 15 | 16 | private static final String[] extensions = {".c", ".cpp", ".java", ".py", ".sh", ".cs", ".js", ".kt", ".go"}; 17 | 18 | public static void check(Integer code) throws UnsupportedLanguageError { 19 | if (code == null || code < 0 || code > 8) { 20 | throw new UnsupportedLanguageError(String.valueOf(code)); 21 | } 22 | } 23 | 24 | public static String getExt(int code) { 25 | return extensions[code]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/utils/FileCleaner.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.utils; 2 | 3 | import cloud.oj.judge.config.AppConfig; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.commons.io.FileUtils; 6 | import org.springframework.scheduling.annotation.Async; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | 12 | /** 13 | * 文件清理,删除判题产生的临时代码文件 14 | */ 15 | @Slf4j 16 | @Component 17 | public class FileCleaner { 18 | 19 | private final AppConfig appConfig; 20 | 21 | public FileCleaner(AppConfig appConfig) { 22 | this.appConfig = appConfig; 23 | FileUtils.deleteQuietly(new File(appConfig.getCodeDir())); 24 | } 25 | 26 | @Async 27 | public void deleteTempFile(String sid) { 28 | try { 29 | FileUtils.deleteDirectory(new File(appConfig.getCodeDir() + sid)); 30 | } catch (IOException e) { 31 | log.error("删除临时文件失败(sid={}): {}", sid, e.getMessage()); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/error/GlobalErrorHandler.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.error; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.commons.lang3.exception.ExceptionUtils; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.bind.annotation.RestControllerAdvice; 9 | 10 | /** 11 | * 全局异常处理 12 | */ 13 | @Slf4j 14 | @RestControllerAdvice 15 | public class GlobalErrorHandler { 16 | @ExceptionHandler(RuntimeException.class) 17 | public ResponseEntity otherErrorHandler(RuntimeException e) { 18 | var status = HttpStatus.INTERNAL_SERVER_ERROR; 19 | var msg = ExceptionUtils.getRootCause(e).getMessage(); 20 | 21 | if (e instanceof GenericException) { 22 | status = ((GenericException) e).getStatus(); 23 | } 24 | 25 | log.error(msg); 26 | return ResponseEntity.status(status).body(new ErrorMessage(status, msg)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/src/views/components/Admin/Problem/help.md: -------------------------------------------------------------------------------- 1 | ## 二级标题 2 | 3 | ### 三级标题 4 | 5 | #### 四级标题 6 | 7 | ::: info 8 | 提示:为了排版美观,最好使用三级及以下标题。 9 | ::: 10 | 11 | ::: warning 12 | 警告:快去学习! 13 | ::: 14 | 15 | > 我是一段引用 16 | 17 | #### 列表 18 | 19 | 无序列表: 20 | 21 | - 翻译翻译 22 | - 什么是惊喜 23 | 24 | 有序列表: 25 | 26 | 1. 惊喜就是 27 | 2. 编译后 28 | 3. 0 error(s), 0 warning(s) 29 | 30 | #### 表格 31 | 32 | | Left | Center | Right | 33 | | :--- | :----: | ----: | 34 | | Left | Center | Right | 35 | 36 | #### 图片 37 | 38 | ![](https://avatars.githubusercontent.com/u/22742796?v=4) 39 | 40 | #### 代码 41 | 42 | 行内代码:`printf("%d", 233)` 43 | 44 | 代码块: 45 | 46 | ```c 47 | #include 48 | 49 | int main(int argc, char *argv[]) { 50 | printf("Hello, World!"); 51 | return 0; 52 | } 53 | ``` 54 | 55 | #### 数学公式(KaTeX) 56 | 57 | 行内公式:`$a, b, c \neq \{ \{ a\}, b, c\}$` 58 | 59 | 行内公式:`$x \leq 0, a_i \geq 10$` 60 | 61 | 块级公式: 62 | 63 | ```math 64 | x=\frac{-b\pm\sqrt{b^2-4ac}}{2a} 65 | ``` 66 | 67 | ```math 68 | \begin{bmatrix} 69 | 1&2&3\\ 70 | 4&5&6\\ 71 | 7&8&9 72 | \end{bmatrix} 73 | ``` 74 | -------------------------------------------------------------------------------- /web/src/store/user.ts: -------------------------------------------------------------------------------- 1 | import { UserInfo } from "@/api/type" 2 | import { defineStore } from "pinia" 3 | 4 | const TOKEN = "userToken" 5 | const token = localStorage.getItem(TOKEN) 6 | 7 | /** 8 | * 解析 JWT 中的用户信息 9 | * @param token Base64Url 10 | * @returns 11 | */ 12 | function resolveToken(token: string): UserInfo { 13 | const claims = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/") 14 | const userInfo = JSON.parse(decodeURIComponent(escape(window.atob(claims)))) 15 | userInfo.token = token 16 | 17 | return userInfo 18 | } 19 | 20 | export const useUserStore = defineStore("userInfo", { 21 | state: () => ({ 22 | userInfo: token == null ? null : resolveToken(token) 23 | }), 24 | getters: { 25 | isLoggedIn(): boolean { 26 | return this.userInfo != null 27 | } 28 | }, 29 | actions: { 30 | saveToken(token: string) { 31 | localStorage.setItem(TOKEN, token) 32 | this.userInfo = resolveToken(token) 33 | }, 34 | clearToken() { 35 | localStorage.removeItem(TOKEN) 36 | this.userInfo = null 37 | } 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/repo/ContestRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.repo; 2 | 3 | import cloud.oj.judge.entity.Contest; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.jdbc.core.simple.JdbcClient; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | @RequiredArgsConstructor 12 | public class ContestRepo { 13 | 14 | private final JdbcClient client; 15 | 16 | public Optional selectById(Integer cid) { 17 | return client.sql(""" 18 | select contest_name, 19 | if(start_at <= unix_timestamp(now()), true, false) as started, 20 | if(end_at <= unix_timestamp(now()), true, false) as ended, 21 | languages 22 | from contest 23 | where contest_id = :cid; 24 | """) 25 | .param("cid", cid) 26 | .query(Contest.class) 27 | .optional(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/repo/InviteeRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.repo; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.jdbc.core.simple.JdbcClient; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | @RequiredArgsConstructor 9 | public class InviteeRepo { 10 | 11 | private final JdbcClient client; 12 | 13 | /** 14 | * 检查用户是否已加入竞赛 15 | * 16 | * @param cid 竞赛 Id 17 | * @param uid 用户 Id 18 | * @return {@link Boolean} 19 | */ 20 | public Boolean checkInvitee(Integer cid, Integer uid) { 21 | return client.sql(""" 22 | select exists( 23 | select 1 24 | from invitee 25 | where contest_id = :cid 26 | and uid = :uid 27 | ) 28 | """) 29 | .param("cid", cid) 30 | .param("uid", uid) 31 | .query(Boolean.class) 32 | .single(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/config/AuthConfig.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.config; 2 | 3 | import cloud.oj.gateway.service.UserService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.authentication.ProviderManager; 9 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11 | 12 | @Configuration 13 | @RequiredArgsConstructor 14 | public class AuthConfig { 15 | 16 | private final UserService userService; 17 | 18 | @Bean 19 | public AuthenticationManager authenticationManager() { 20 | var authProvider = new DaoAuthenticationProvider(); 21 | 22 | authProvider.setUserDetailsService(userService); 23 | authProvider.setPasswordEncoder(new BCryptPasswordEncoder()); 24 | 25 | return new ProviderManager(authProvider); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/src/components/UserAvatar.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cloud Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /services/core/src/main/resources/config/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8180 3 | spring: 4 | profiles: 5 | active: dev 6 | application: 7 | name: core 8 | datasource: 9 | type: com.zaxxer.hikari.HikariDataSource 10 | driver-class-name: org.mariadb.jdbc.Driver 11 | url: jdbc:mariadb://${DB_HOST:localhost:3306}/cloud_oj?serverTimezone=UTC 12 | username: ${DB_USER:root} 13 | password: ${DB_PASSWORD:root} 14 | hikari: 15 | minimum-idle: ${DB_POOL_SIZE:6} 16 | maximum-pool-size: ${DB_POOL_SIZE:6} 17 | jackson: 18 | default-property-inclusion: non_null 19 | cloud: 20 | consul: 21 | host: ${CONSUL_HOST:localhost} 22 | port: ${CONSUL_PORT:8500} 23 | discovery: 24 | query-passing: true 25 | prefer-ip-address: ${USE_IP:false} 26 | ip-address: ${SERVICE_IP:${spring.cloud.client.ip-address}} 27 | instance-id: ${spring.application.name}-${server.port}-${random.int} 28 | health-check-critical-timeout: 5m 29 | servlet: 30 | multipart: 31 | max-file-size: 100MB 32 | max-request-size: 128MB 33 | management: 34 | endpoint: 35 | health: 36 | show-details: ALWAYS -------------------------------------------------------------------------------- /web/src/views/layout/ThemeSwitch.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/component/ProcessUtil.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.component; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | 8 | @Slf4j 9 | public class ProcessUtil { 10 | public static void watchProcess(int seconds, Process process, AtomicBoolean timeout) { 11 | final int[] time = {seconds}; 12 | new Thread(() -> { 13 | while (time[0] > 0) { 14 | if (!process.isAlive()) { 15 | // 进程已结束 16 | break; 17 | } 18 | 19 | try { 20 | TimeUnit.SECONDS.sleep(1); 21 | } catch (InterruptedException e) { 22 | log.error(e.getMessage()); 23 | process.destroyForcibly(); 24 | break; 25 | } 26 | 27 | time[0]--; 28 | } 29 | 30 | if (time[0] == 0) { 31 | // 超时 destroy 32 | timeout.set(true); 33 | process.destroyForcibly(); 34 | } 35 | }).start(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/repo/SettingsRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.repo; 2 | 3 | import cloud.oj.core.entity.Settings; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.jdbc.core.simple.JdbcClient; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | @RequiredArgsConstructor 10 | public class SettingsRepo { 11 | 12 | private final JdbcClient client; 13 | 14 | public Settings select() { 15 | return client.sql("select * from settings where id = 0 for update") 16 | .query(Settings.class) 17 | .single(); 18 | } 19 | 20 | public Integer update(Settings settings) { 21 | return client.sql(""" 22 | update settings 23 | set always_show_ranking = :alwaysShowRanking, 24 | show_all_contest = :showAllContest, 25 | show_passed_points = :showPassedPoints, 26 | auto_del_solutions = :autoDelSolutions 27 | where id = 0 28 | """) 29 | .paramSource(settings) 30 | .update(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.config; 2 | 3 | import lombok.Getter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.ApplicationContext; 8 | 9 | import java.io.File; 10 | 11 | @Slf4j 12 | @Getter 13 | @ConfigurationProperties("app") 14 | public class AppConfig { 15 | 16 | private final String fileDir; 17 | 18 | public AppConfig(ApplicationContext context, String fileDir) { 19 | var home = System.getProperty("user.home"); 20 | 21 | if (fileDir == null) { 22 | this.fileDir = home + "/.local/cloud-oj/"; 23 | } else if (!fileDir.endsWith("/")) { 24 | this.fileDir = fileDir + "/"; 25 | } else { 26 | this.fileDir = fileDir; 27 | } 28 | 29 | var dir = new File(this.fileDir); 30 | 31 | if (!dir.exists() && !dir.mkdirs()) { 32 | log.error("创建目录失败: {}", fileDir); 33 | SpringApplication.exit(context, () -> -1); 34 | } 35 | 36 | log.info("数据文件目录: {}", this.fileDir); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/src/components/ErrorResult.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 38 | -------------------------------------------------------------------------------- /web/src/components/ResultTag.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 45 | -------------------------------------------------------------------------------- /.run/JudgeApp.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 24 | -------------------------------------------------------------------------------- /web/src/utils/LogFormatter.ts: -------------------------------------------------------------------------------- 1 | import { Log } from "@/api/type" 2 | import dayjs from "dayjs" 3 | 4 | function shortenClassName(className: string): string { 5 | let len = className.length 6 | if (len > 40) { 7 | const arr = className.split(".") 8 | for (let i = 0; i < arr.length - 1; i++) { 9 | if (arr[i].startsWith("[")) { 10 | continue 11 | } 12 | 13 | const iLen = arr[i].length 14 | arr[i] = arr[i].substring(0, 1) 15 | len -= iLen - 1 16 | 17 | if (len <= 40) { 18 | break 19 | } 20 | } 21 | 22 | return arr.join(".") 23 | } 24 | 25 | return className 26 | } 27 | 28 | const LogFormatter = { 29 | format(log: Log) { 30 | return ( 31 | `${dayjs(log.time).format("YYYY-MM-DD HH:mm:ss.SSS")} ` + 32 | `${log.level.padStart(5)} - ` + 33 | `[${log.instanceId.padStart(23)}] ` + 34 | `[${log.thread.slice(-15).padStart(15)}] ` + 35 | `${shortenClassName(log.className).padEnd(50)} ` + 36 | `: ${log.message}` 37 | ) 38 | } 39 | } 40 | 41 | export default LogFormatter 42 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/repo/InviteeRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.repo; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.jdbc.core.simple.JdbcClient; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | @RequiredArgsConstructor 9 | public class InviteeRepo { 10 | 11 | private final JdbcClient client; 12 | 13 | public Boolean isInvited(Integer cid, Integer uid) { 14 | return client.sql(""" 15 | select exists( 16 | select 1 17 | from invitee 18 | where contest_id = :cid 19 | and uid = :uid 20 | ) 21 | """) 22 | .param("cid", cid) 23 | .param("uid", uid) 24 | .query(Boolean.class) 25 | .single(); 26 | } 27 | 28 | public Integer invite(Integer cid, Integer uid) { 29 | return client.sql(""" 30 | insert into invitee(contest_id, uid) 31 | values (:cid, :uid) 32 | """) 33 | .param("cid", cid) 34 | .param("uid", uid) 35 | .update(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /web/src/store/app.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage } from "@/api/type" 2 | import { darkTheme, type GlobalTheme } from "naive-ui" 3 | import { defineStore } from "pinia" 4 | 5 | const THEME = "theme" 6 | const theme = localStorage.getItem(THEME) 7 | 8 | export interface State { 9 | menuCollapsed: boolean 10 | breadcrumb: Array | null 11 | theme: GlobalTheme | null 12 | error: ErrorMessage | null 13 | } 14 | 15 | export const useAppStore = defineStore("app", { 16 | state: (): State => ({ 17 | menuCollapsed: false, 18 | breadcrumb: null, 19 | theme: theme === "dark" ? darkTheme : null, 20 | error: null 21 | }), 22 | actions: { 23 | setBreadcrumb(value: Array | null) { 24 | this.breadcrumb = value 25 | }, 26 | menuCollapse() { 27 | this.menuCollapsed = !this.menuCollapsed 28 | }, 29 | setTheme(value: string | null) { 30 | if (value === "dark") { 31 | this.theme = darkTheme 32 | localStorage.setItem(THEME, "dark") 33 | } else { 34 | this.theme = null 35 | localStorage.removeItem(THEME) 36 | } 37 | }, 38 | setError(value: ErrorMessage | null) { 39 | this.error = value 40 | if (value != null) { 41 | this.router.replace({ name: "error" }) 42 | } 43 | } 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /web/src/views/components/Account/UserProfile.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /web/src/api/request/solution-api.ts: -------------------------------------------------------------------------------- 1 | import { JudgeResult, type Page, SolutionFilter } from "@/api/type" 2 | import axios, { ApiPath, resolveError } from "@/api" 3 | 4 | const SolutionApi = { 5 | getByFilter( 6 | page: number, 7 | size: number, 8 | filter: SolutionFilter | null = null 9 | ): Promise> { 10 | return new Promise>((resolve, reject) => { 11 | axios({ 12 | url: `${ApiPath.SOLUTION_ADMIN}/queries`, 13 | method: "POST", 14 | params: { 15 | page, 16 | size 17 | }, 18 | data: JSON.stringify(filter) 19 | }) 20 | .then((res) => { 21 | resolve(res.status === 200 ? res.data : { data: [], count: 0 }) 22 | }) 23 | .catch((error) => { 24 | reject(resolveError(error)) 25 | }) 26 | }) 27 | }, 28 | 29 | getById(sid: string): Promise { 30 | return new Promise((resolve, reject) => { 31 | axios({ 32 | url: `${ApiPath.SOLUTION_ADMIN}/${sid}`, 33 | method: "GET" 34 | }) 35 | .then((res) => { 36 | resolve(res.data) 37 | }) 38 | .catch((error) => { 39 | reject(resolveError(error)) 40 | }) 41 | }) 42 | } 43 | } 44 | 45 | export default SolutionApi 46 | -------------------------------------------------------------------------------- /services/judge/src/main/resources/config/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8280 3 | spring: 4 | profiles: 5 | active: dev 6 | application: 7 | name: judge 8 | datasource: 9 | type: com.zaxxer.hikari.HikariDataSource 10 | driver-class-name: org.mariadb.jdbc.Driver 11 | url: jdbc:mariadb://${DB_HOST:localhost:3306}/cloud_oj?serverTimezone=UTC 12 | username: ${DB_USER:root} 13 | password: ${DB_PASSWORD:root} 14 | hikari: 15 | minimum-idle: ${DB_POOL_SIZE:6} 16 | maximum-pool-size: ${DB_POOL_SIZE:6} 17 | rabbitmq: 18 | host: ${RABBIT_URL:localhost} 19 | port: ${RABBIT_PORT:5672} 20 | username: ${RABBIT_USER:admin} 21 | password: ${RABBIT_PASSWORD:admin} 22 | listener: 23 | simple: 24 | prefetch: 1 25 | acknowledge-mode: manual 26 | jackson: 27 | default-property-inclusion: non_null 28 | cloud: 29 | consul: 30 | host: ${CONSUL_HOST:localhost} 31 | port: ${CONSUL_PORT:8500} 32 | discovery: 33 | query-passing: true 34 | prefer-ip-address: ${USE_IP:false} 35 | ip-address: ${SERVICE_IP:${spring.cloud.client.ip-address}} 36 | instance-id: ${spring.application.name}-${server.port}-${random.int} 37 | health-check-critical-timeout: 5m 38 | management: 39 | endpoint: 40 | health: 41 | show-details: always -------------------------------------------------------------------------------- /web/src/type/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 代码编辑器触发提交时的数据 3 | */ 4 | export type SourceCode = { 5 | language: number 6 | code: string 7 | } 8 | 9 | export type LanguageOption = { 10 | value: number 11 | label: string 12 | } 13 | 14 | export const LanguageOptions: Array = [ 15 | { value: 0, label: "C" }, 16 | { value: 1, label: "C++" }, 17 | { value: 2, label: "Java" }, 18 | { value: 3, label: "Python" }, 19 | { value: 4, label: "Bash Shell" }, 20 | { value: 5, label: "C#" }, 21 | { value: 6, label: "JavaScript" }, 22 | { value: 7, label: "Kotlin" }, 23 | { value: 8, label: "Go" } 24 | ] 25 | 26 | export const LanguageNames: { [key: number]: string } = { 27 | 0: "C", 28 | 1: "C++", 29 | 2: "Java", 30 | 3: "Python", 31 | 4: "Bash", 32 | 5: "C#", 33 | 6: "JavaScript", 34 | 7: "Kotlin", 35 | 8: "Go" 36 | } 37 | 38 | export const LanguageColors: { [key: number]: string } = { 39 | 0: "#555555", 40 | 1: "#F34B7D", 41 | 2: "#B07219", 42 | 3: "#3572A5", 43 | 4: "#89E051", 44 | 5: "#178600", 45 | 6: "#F1E05A", 46 | 7: "#A97BFF", 47 | 8: "#00ADD8" 48 | } 49 | 50 | export const ResultTypes: { [key: number]: any } = { 51 | 0: "success", 52 | 1: "warning", 53 | 2: "warning", 54 | 3: "warning", 55 | 4: "error", 56 | 5: "error", 57 | 6: "error", 58 | 7: "error", 59 | 8: "error" 60 | } 61 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/controller/LogController.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.controller; 2 | 3 | import cloud.oj.core.service.LogService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * 日志 API 15 | */ 16 | @RestController 17 | @RequestMapping("log") 18 | @RequiredArgsConstructor 19 | public class LogController { 20 | 21 | private final LogService logService; 22 | 23 | @GetMapping 24 | public ResponseEntity getLatest10Logs(@RequestParam(required = false) Long time) { 25 | var data = logService.getLatest10(time); 26 | return data.isEmpty() ? 27 | ResponseEntity.noContent().build() 28 | : ResponseEntity.ok(data); 29 | } 30 | 31 | @GetMapping(path = "range") 32 | public ResponseEntity> getRangeLogs(Long start, Long end) { 33 | var data = logService.getRange(start, end); 34 | return data.isEmpty() ? 35 | ResponseEntity.noContent().build() 36 | : ResponseEntity.ok(data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/Ranking.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonGetter; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | import java.util.Set; 9 | 10 | @Getter 11 | @Setter 12 | public class Ranking { 13 | @JsonIgnore 14 | public Integer _total; 15 | 16 | private Integer rank; 17 | private Integer uid; 18 | private String username; 19 | private String nickname; 20 | private String realName; 21 | private Integer committed; 22 | private Integer passed; 23 | private Double score; 24 | private Boolean hasAvatar; 25 | private Boolean star; 26 | 27 | private Set details; 28 | 29 | @JsonGetter 30 | public String badge() { 31 | // 🏅 32 | if (rank == 1) { 33 | return "\uD83C\uDFC5"; 34 | } 35 | 36 | // 🥇 37 | if (rank < 5) { 38 | return "\uD83E\uDD47"; 39 | } 40 | 41 | // 🥈 42 | if (rank < 10) { 43 | return "\uD83E\uDD48"; 44 | } 45 | 46 | // 🥉 47 | if (rank < 25) { 48 | return "\uD83E\uDD49"; 49 | } 50 | 51 | // 🎉 52 | if (rank < 35) { 53 | return "\uD83C\uDF89"; 54 | } 55 | 56 | return ""; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/filter/AuthConverter.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.filter; 2 | 3 | import cloud.oj.gateway.entity.UsernamePasswd; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.web.authentication.AuthenticationConverter; 9 | 10 | /** 11 | * 认证转换器 12 | * 13 | *

将登录 POST 数据转换为 {@link UsernamePasswd}

14 | */ 15 | public class AuthConverter implements AuthenticationConverter { 16 | 17 | private final ObjectMapper mapper; 18 | 19 | public AuthConverter(ObjectMapper mapper) { 20 | this.mapper = mapper; 21 | } 22 | 23 | @Override 24 | public Authentication convert(HttpServletRequest request) { 25 | var contentType = request.getHeader("Content-Type"); 26 | 27 | try { 28 | if (contentType != null && contentType.contains("application/json")) { 29 | var o = mapper.readValue(request.getInputStream(), UsernamePasswd.class); 30 | return new UsernamePasswordAuthenticationToken(o.getUsername(), o.getPassword()); 31 | } else { 32 | return null; 33 | } 34 | } catch (Exception e) { 35 | return null; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/src/views/components/Admin/Overview/Index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 56 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/Problem.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import static cloud.oj.core.entity.Solution.R; 8 | import static cloud.oj.core.entity.Solution.S; 9 | 10 | @Getter 11 | @Setter 12 | public class Problem { 13 | @JsonIgnore 14 | public Integer _total; 15 | 16 | private Integer contestId; 17 | private Boolean enable; 18 | private Integer problemId; 19 | private Integer passed; 20 | private Integer memoryLimit; 21 | private Integer outputLimit; 22 | private Integer languages; 23 | private Integer score; 24 | private Long timeout; 25 | // UNIX 时间戳(10 位) 26 | private Long startAt; 27 | private Long endAt; 28 | private Long createAt; 29 | private String contestName; 30 | private String title; 31 | private String description; 32 | private String category; 33 | private Integer state; 34 | private Integer result; 35 | private String stateText; 36 | private String resultText; 37 | 38 | public void setState(Integer state) { 39 | this.state = state; 40 | this.stateText = S[state]; 41 | } 42 | 43 | public void setResult(Integer result) { 44 | if (result == null) { 45 | return; 46 | } 47 | 48 | this.result = result; 49 | this.resultText = R[result]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/controller/SubmitController.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.controller; 2 | 3 | import cloud.oj.judge.entity.SubmitData; 4 | import cloud.oj.judge.service.SubmitService; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestHeader; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | /** 12 | * 提交代码接口 13 | */ 14 | @RestController 15 | public class SubmitController { 16 | 17 | private final SubmitService submitService; 18 | 19 | public SubmitController(SubmitService submitService) { 20 | this.submitService = submitService; 21 | } 22 | 23 | /** 24 | * 提交代码,普通用户 25 | */ 26 | @PostMapping("submit") 27 | public ResponseEntity submit(@RequestHeader Integer uid, @RequestBody SubmitData data) { 28 | data.setUid(uid); 29 | var time = submitService.submitCode(data, false); 30 | return ResponseEntity.accepted().body(time); 31 | } 32 | 33 | /** 34 | * 提交代码,管理员 35 | */ 36 | @PostMapping("admin/submit") 37 | public ResponseEntity adminSubmit(@RequestHeader Integer uid, @RequestBody SubmitData data) { 38 | data.setUid(uid); 39 | var time = submitService.submitCode(data, true); 40 | return ResponseEntity.accepted().body(time); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/repo/LogRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.repo; 2 | 3 | import cloud.oj.core.entity.Log; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.jdbc.core.simple.JdbcClient; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.List; 9 | 10 | @Repository 11 | @RequiredArgsConstructor 12 | public class LogRepo { 13 | 14 | private final JdbcClient client; 15 | 16 | /** 17 | * 查询最近的 10 条日志 18 | * 19 | * @param time 起始时间 20 | * @return {@link List} of {@link Log} 21 | */ 22 | public List selectLatest10(long time) { 23 | return client.sql(""" 24 | select * 25 | from (select * from log where time > :time order by time desc limit 10) t 26 | order by time 27 | """) 28 | .param("time", time) 29 | .query(Log.class) 30 | .list(); 31 | } 32 | 33 | /** 34 | * 按时间范围查询日志 35 | * 36 | * @param start 其实时间 37 | * @param end 结束时间 38 | * @return {@link List} of {@link Log} 39 | */ 40 | public List selectRange(long start, long end) { 41 | return client.sql(""" 42 | select * from log where time >= :start and time <= :end 43 | """) 44 | .param("start", start) 45 | .param("end", end) 46 | .query(Log.class) 47 | .list(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud-oj", 3 | "version": "0.1.11", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "preview": "vite preview", 8 | "build": "vite build", 9 | "type-check": "vue-tsc --build", 10 | "watch": "vue-tsc --noEmit --watch", 11 | "format": "prettier --write src/" 12 | }, 13 | "dependencies": { 14 | "axios": "^1.13.2", 15 | "codemirror": "^5.65.6", 16 | "dayjs": "^1.11.19", 17 | "echarts": "^5.6.0", 18 | "highlight.js": "^11.11.1", 19 | "katex": "^0.16.25", 20 | "lodash": "^4.17.21", 21 | "markdown-it": "^14.1.0", 22 | "markdown-it-container": "^4.0.0", 23 | "pinia": "^3.0.4", 24 | "vue": "^3.5.24", 25 | "vue-router": "^4.6.3" 26 | }, 27 | "devDependencies": { 28 | "@tsconfig/node22": "^22.0.3", 29 | "@types/codemirror": "^5.60.17", 30 | "@types/katex": "^0.16.7", 31 | "@types/lodash": "^4.17.20", 32 | "@types/markdown-it": "^14.1.2", 33 | "@types/markdown-it-container": "^2.0.10", 34 | "@types/node": "^22.19.1", 35 | "@vicons/fa": "^0.13.0", 36 | "@vicons/material": "^0.13.0", 37 | "@vitejs/plugin-vue": "^6.0.1", 38 | "@vitejs/plugin-vue-jsx": "^5.1.1", 39 | "@vue/tsconfig": "^0.8.1", 40 | "naive-ui": "^2.43.1", 41 | "prettier": "^3.6.2", 42 | "sass": "^1.94.0", 43 | "typescript": "^5.9.3", 44 | "vfonts": "^0.0.3", 45 | "vite": "^7.2.2", 46 | "vite-plugin-compression": "^0.5.1", 47 | "vite-plugin-vue-devtools": "^8.0.3", 48 | "vue-tsc": "^3.1.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /web/src/views/components/Account/Overview.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | 43 | 56 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/filter/MutableRequest.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.filter; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletRequestWrapper; 5 | 6 | import java.util.*; 7 | 8 | /** 9 | * HttpServletRequest 包装,用于修改 Headers 10 | */ 11 | public class MutableRequest extends HttpServletRequestWrapper { 12 | 13 | private final Map headers; 14 | 15 | public MutableRequest(HttpServletRequest request) { 16 | super(request); 17 | headers = new HashMap<>(); 18 | } 19 | 20 | public void putHeader(String name, String value) { 21 | headers.put(name, value); 22 | } 23 | 24 | @Override 25 | public String getHeader(String name) { 26 | var value = headers.get(name); 27 | 28 | if (value != null) { 29 | return value; 30 | } 31 | 32 | return super.getHeader(name); 33 | } 34 | 35 | @Override 36 | public Enumeration getHeaderNames() { 37 | var set = new HashSet(); 38 | 39 | var e = super.getHeaderNames(); 40 | 41 | while (e.hasMoreElements()) { 42 | set.add(e.nextElement()); 43 | } 44 | 45 | set.addAll(headers.keySet()); 46 | 47 | return Collections.enumeration(set); 48 | } 49 | 50 | @Override 51 | public Enumeration getHeaders(String name) { 52 | if (headers.containsKey(name)) { 53 | return Collections.enumeration(Collections.singletonList(headers.get(name))); 54 | } 55 | 56 | return super.getHeaders(name); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /web/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { h } from "vue" 2 | import { NIcon } from "naive-ui" 3 | import { Contest } from "@/api/type" 4 | import { InfoRound, PlayArrowRound } from "@vicons/material" 5 | 6 | type StateTag = { 7 | type: "info" | "error" | "success" 8 | state: string 9 | icon: any 10 | } 11 | 12 | function setTitle(title: string) { 13 | document.title = `${title} - Cloud OJ` 14 | } 15 | 16 | const renderIcon = (icon: any, color: string | undefined = undefined) => { 17 | return () => 18 | h(NIcon, color == null ? null : { color }, { 19 | default: () => h(icon) 20 | }) 21 | } 22 | 23 | function stateTag(c: Contest): StateTag { 24 | if (c.ended) { 25 | return { 26 | type: "error", 27 | state: "已结束", 28 | icon: InfoRound 29 | } 30 | } else if (c.started) { 31 | return { 32 | type: "success", 33 | state: "进行中", 34 | icon: PlayArrowRound 35 | } 36 | } else { 37 | return { 38 | type: "info", 39 | state: "未开始", 40 | icon: InfoRound 41 | } 42 | } 43 | } 44 | 45 | function timeUsage(val?: number): string { 46 | if (val) { 47 | return `${(val / 1000).toFixed(2)} ms` 48 | } 49 | 50 | return "-" 51 | } 52 | 53 | function ramUsage(val?: number): string { 54 | if (val) { 55 | if (val >= 1024) { 56 | return `${(val / 1024).toFixed(2)} MB` 57 | } else { 58 | return `${val} KB` 59 | } 60 | } 61 | 62 | return "-" 63 | } 64 | 65 | export { default as LanguageUtil } from "./LanguageUtil" 66 | export { default as LogFormatter } from "./LogFormatter" 67 | export { setTitle, renderIcon, stateTag, timeUsage, ramUsage } 68 | export type { StateTag } 69 | -------------------------------------------------------------------------------- /web/src/views/components/Account/Timeline.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /web/src/api/request/ranking-api.ts: -------------------------------------------------------------------------------- 1 | import axios, { ApiPath, resolveError } from "@/api" 2 | import type { Page, Ranking, RankingContest } from "@/api/type" 3 | import { useStore } from "@/store" 4 | 5 | const RankingApi = { 6 | get(page: number, size: number): Promise> { 7 | const userInfo = useStore().user.userInfo 8 | const path = 9 | userInfo == null || userInfo.role === 1 10 | ? ApiPath.RANKING 11 | : ApiPath.RANKING_ADMIN 12 | 13 | return new Promise>((resolve, reject) => { 14 | axios({ 15 | url: path, 16 | method: "GET", 17 | params: { 18 | page, 19 | size 20 | } 21 | }) 22 | .then((res) => { 23 | if (res.status === 200) { 24 | resolve(res.data as Page) 25 | } else { 26 | resolve({ data: [], total: 0 }) 27 | } 28 | }) 29 | .catch((error) => { 30 | reject(resolveError(error)) 31 | }) 32 | }) 33 | }, 34 | 35 | getContestRanking(cid: number): Promise { 36 | const userInfo = useStore().user.userInfo 37 | const path = 38 | userInfo == null || userInfo.role === 1 39 | ? ApiPath.CONTEST_RANKING 40 | : ApiPath.CONTEST_RANKING_ADMIN 41 | 42 | return new Promise((resolve, reject) => { 43 | axios({ 44 | url: `${path}/${cid}`, 45 | method: "GET" 46 | }) 47 | .then((res) => { 48 | resolve(res.data as RankingContest) 49 | }) 50 | .catch((error) => { 51 | reject(resolveError(error)) 52 | }) 53 | }) 54 | } 55 | } 56 | 57 | export default RankingApi 58 | -------------------------------------------------------------------------------- /web/src/views/components/Admin/Overview/ServiceLog.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | 44 | 77 | -------------------------------------------------------------------------------- /services/core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cloud.oj 8 | cloud-oj 9 | 1.0.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | core 13 | Core Service 14 | Cloud OJ Core Service 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-configuration-processor 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-web 23 | 24 | 25 | org.springframework.security 26 | spring-security-crypto 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-jdbc 31 | 32 | 33 | org.mariadb.jdbc 34 | mariadb-java-client 35 | 36 | 37 | org.apache.commons 38 | commons-lang3 39 | 40 | 41 | commons-io 42 | commons-io 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/entity/Solution.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | import java.math.BigInteger; 8 | 9 | import static cloud.oj.judge.constant.State.JUDGED; 10 | import static cloud.oj.judge.constant.State.WAITING; 11 | 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | public class Solution { 16 | private BigInteger solutionId; 17 | private Integer uid; 18 | private Integer problemId; 19 | private Integer contestId; 20 | private Integer language; 21 | private Integer state; 22 | private Integer result; 23 | private Integer total = 0; 24 | private Integer passed = 0; 25 | private Double passRate = 0D; 26 | private Double score = 0D; 27 | private Long time = 0L; 28 | private Long memory = 0L; 29 | private String errorInfo; 30 | private Long submitTime; 31 | // 用于队列,不属于数据库字段 32 | private String sourceCode; 33 | 34 | public Solution(Integer uid, Integer problemId, Integer contestId, 35 | Integer language, Long submitTime, String sourceCode) { 36 | this.uid = uid; 37 | this.problemId = problemId; 38 | this.contestId = contestId; 39 | this.language = language; 40 | this.submitTime = submitTime; 41 | this.sourceCode = sourceCode; 42 | this.state = WAITING; 43 | } 44 | 45 | public void endWithError(Integer result, String info) { 46 | this.result = result; 47 | state = JUDGED; 48 | errorInfo = info; 49 | } 50 | 51 | public String getId() { 52 | return solutionId.toString(); 53 | } 54 | 55 | public void setId(String id) { 56 | solutionId = new BigInteger(id); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/entity/User.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | 10 | import java.util.Collection; 11 | import java.util.List; 12 | 13 | @Getter 14 | @Setter 15 | @NoArgsConstructor 16 | public class User implements UserDetails { 17 | private Integer uid; 18 | 19 | private String username; 20 | 21 | private String nickname; 22 | 23 | private String password; 24 | 25 | @JsonIgnore 26 | private String secret; 27 | 28 | private String email; 29 | 30 | private String section; 31 | 32 | private Boolean hasAvatar; 33 | 34 | private int role; 35 | 36 | @JsonIgnore 37 | private List roles; 38 | 39 | @Override 40 | @JsonIgnore 41 | public Collection getAuthorities() { 42 | return roles; 43 | } 44 | 45 | @Override 46 | @JsonIgnore 47 | public String getPassword() { 48 | return password; 49 | } 50 | 51 | @Override 52 | @JsonIgnore 53 | public String getUsername() { 54 | return username; 55 | } 56 | 57 | @Override 58 | @JsonIgnore 59 | public boolean isAccountNonExpired() { 60 | return true; 61 | } 62 | 63 | @Override 64 | @JsonIgnore 65 | public boolean isAccountNonLocked() { 66 | return true; 67 | } 68 | 69 | @Override 70 | @JsonIgnore 71 | public boolean isCredentialsNonExpired() { 72 | return true; 73 | } 74 | 75 | @Override 76 | @JsonIgnore 77 | public boolean isEnabled() { 78 | return true; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/Log.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | import java.text.SimpleDateFormat; 9 | 10 | @Getter 11 | @Setter 12 | public class Log { 13 | @JsonIgnore 14 | private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 15 | private String service; 16 | private String instanceId; 17 | private String level; 18 | private String thread; 19 | private String className; 20 | private String message; 21 | private long time; 22 | 23 | /** 24 | * 全限定名过长时,转为缩写 25 | */ 26 | private String shortenClassName(String className) { 27 | var len = className.length(); 28 | if (len > 40) { 29 | var arr = className.split("\\."); 30 | for (int i = 0; i < arr.length - 1; i++) { 31 | if (arr[i].contains("[")) { 32 | continue; 33 | } 34 | 35 | var iLen = arr[i].length(); 36 | arr[i] = arr[i].substring(0, 1); 37 | len -= iLen - 1; 38 | 39 | if (len <= 40) { 40 | break; 41 | } 42 | } 43 | 44 | return String.join(".", arr); 45 | } 46 | 47 | return className; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return dateFormat.format(time) + " " + 53 | StringUtils.leftPad(level, 5) + " --- [" + 54 | StringUtils.leftPad(instanceId, 24) + "] [" + 55 | StringUtils.leftPad(StringUtils.right(thread, 15), 15) + "] " + 56 | StringUtils.rightPad(shortenClassName(className), 40) + " : " + 57 | message; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/logging/DBAppender.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.logging; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.AppenderBase; 5 | import org.springframework.core.env.Environment; 6 | import org.springframework.jdbc.core.simple.JdbcClient; 7 | 8 | import java.util.Objects; 9 | 10 | /** 11 | * 自定义 Log Appender,将日志保存到数据库 12 | */ 13 | public class DBAppender extends AppenderBase { 14 | 15 | private final String appName; 16 | 17 | private final String instanceId; 18 | 19 | private final JdbcClient client; 20 | 21 | public DBAppender(Environment env, JdbcClient client) { 22 | appName = Objects.requireNonNull(env.getProperty("spring.application.name")).toUpperCase(); 23 | instanceId = Objects.requireNonNull(env.getProperty("spring.cloud.consul.discovery.instance-id")).toUpperCase(); 24 | this.client = client; 25 | } 26 | 27 | @Override 28 | protected void append(ILoggingEvent eventObject) { 29 | var sql = """ 30 | insert into log(service, instance_id, level, thread, class_name, message, time) 31 | values (:service, :instanceId, :level, :thread, :className, :message, :time) 32 | """; 33 | var level = eventObject.getLevel().levelStr; 34 | var thread = eventObject.getThreadName(); 35 | var className = eventObject.getLoggerName(); 36 | var msg = eventObject.getFormattedMessage(); 37 | var time = eventObject.getTimeStamp(); 38 | 39 | client.sql(sql) 40 | .param("service", appName) 41 | .param("instanceId", instanceId) 42 | .param("level", level) 43 | .param("thread", thread) 44 | .param("className", className) 45 | .param("message", msg) 46 | .param("time", time) 47 | .update(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /services/gateway/src/main/resources/config/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | spring: 4 | profiles: 5 | active: dev 6 | application: 7 | name: gateway 8 | datasource: 9 | type: com.zaxxer.hikari.HikariDataSource 10 | driver-class-name: org.mariadb.jdbc.Driver 11 | url: jdbc:mariadb://${DB_HOST:localhost:3306}/cloud_oj?serverTimezone=UTC 12 | username: ${DB_USER:root} 13 | password: ${DB_PASSWORD:root} 14 | hikari: 15 | minimum-idle: ${DB_POOL_SIZE:6} 16 | maximum-pool-size: ${DB_POOL_SIZE:6} 17 | jackson: 18 | default-property-inclusion: non_null 19 | cache: 20 | type: caffeine 21 | caffeine: 22 | spec: maximumSize=1000,expireAfterWrite=20m 23 | cloud: 24 | consul: 25 | host: ${CONSUL_HOST:localhost} 26 | port: ${CONSUL_PORT:8500} 27 | discovery: 28 | query-passing: true 29 | prefer-ip-address: ${USE_IP:false} 30 | ip-address: ${SERVICE_IP:${spring.cloud.client.ip-address}} 31 | instance-id: ${spring.application.name}-${server.port}-${random.int} 32 | health-check-critical-timeout: 5m 33 | gateway: 34 | server: 35 | webmvc: 36 | routes: 37 | - id: core 38 | uri: lb://core 39 | filters: 40 | - StripPrefix=2 41 | predicates: 42 | - Path=/api/core/** 43 | - id: judge 44 | uri: lb://judge 45 | filters: 46 | - StripPrefix=2 47 | predicates: 48 | - Path=/api/judge/** 49 | - id: auth 50 | uri: lb://gateway 51 | filters: 52 | - StripPrefix=2 53 | predicates: 54 | - Path=/api/auth/** 55 | discovery: 56 | enabled: true 57 | management: 58 | endpoint: 59 | health: 60 | show-details: always 61 | app: 62 | token-valid-time: ${TOKEN_VALID_TIME:4} -------------------------------------------------------------------------------- /services/judge/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cloud.oj 8 | cloud-oj 9 | 1.0.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | judge 13 | Judge Service 14 | Cloud OJ Judge Service 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-configuration-processor 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-web 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-jdbc 27 | 28 | 29 | org.springframework.amqp 30 | spring-rabbit 31 | 32 | 33 | org.mariadb.jdbc 34 | mariadb-java-client 35 | 36 | 37 | com.rabbitmq 38 | http-client 39 | 5.4.0 40 | 41 | 42 | org.apache.commons 43 | commons-lang3 44 | 45 | 46 | commons-io 47 | commons-io 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /web/src/views/components/Submission/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 44 | 45 | 61 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/component/JudgementEntry.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.component; 2 | 3 | import cloud.oj.judge.entity.Solution; 4 | import cloud.oj.judge.repo.SettingsRepo; 5 | import cloud.oj.judge.repo.SolutionRepo; 6 | import cloud.oj.judge.utils.FileCleaner; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.lang3.exception.ExceptionUtils; 10 | import org.springframework.scheduling.annotation.Async; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.io.IOException; 14 | 15 | import static cloud.oj.judge.entity.Result.IE; 16 | 17 | @Slf4j 18 | @Component 19 | @RequiredArgsConstructor 20 | public class JudgementEntry { 21 | 22 | private final SettingsRepo settingsRepo; 23 | 24 | private final SolutionRepo solutionRepo; 25 | 26 | private final Judgement judgement; 27 | 28 | private final FileCleaner fileCleaner; 29 | 30 | @FunctionalInterface 31 | public interface JudgeComplete { 32 | void run() throws IOException; 33 | } 34 | 35 | /** 36 | * 判题入口 37 | */ 38 | @Async("judgeExecutor") 39 | public void judge(Solution solution, JudgeComplete onComplete) throws IOException { 40 | try { 41 | judgement.judge(solution); 42 | } catch (Exception e) { 43 | var msg = ExceptionUtils.getRootCause(e).getMessage(); 44 | log.error("判题事务异常: {}", msg); 45 | // 判题发生异常,将结果设置为内部错误 46 | solution.endWithError(IE, msg); 47 | solutionRepo.updateResult(solution); 48 | } finally { 49 | onComplete.run(); 50 | var t = Thread.currentThread().getName(); 51 | log.debug("判题完成: thread={}, sid={}, pid={}", t, solution.getSolutionId(), solution.getProblemId()); 52 | if (settingsRepo.autoDelSolutions()) { 53 | fileCleaner.deleteTempFile(solution.getId()); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /judge/README.md: -------------------------------------------------------------------------------- 1 | # Judge Runner 2 | 3 | Online Judge 判题程序。 4 | 5 | ## Build 6 | 7 | 1. 准备 Linux 环境并安装 cmake、make、gcc、g++,Debian 系可以使用以下命令: 8 | 9 | ```bash 10 | sudo apt install cmake build-essential 11 | ``` 12 | 13 | 2. 运行 `./build` 生成可执行文件 14 | 15 | ## Install 16 | 17 | ```bash 18 | ./build install 19 | ``` 20 | 21 | - 可执行文件将被复制到 `/usr/local/bin` 目录 22 | 23 | ## 用法 24 | 25 | ```bash 26 | judge 27 | ``` 28 | 29 | - `--cmd`: 要执行的命令, 用 `@` 代替 空格 30 | - `--time`: CPU 时间限制,单位:毫秒 31 | - `--ram`: 内存限制,此项用于判断是否超限,单位:MiB 32 | - `--output`: 输出限制,单位:MiB 33 | - `--workdir`: 工作目录,用户程序所在目录 34 | - `--data`: 测试数据目录,包含 `*.in`、 `*.out` 文件 35 | - `--cpu`: 用哪个 CPU 核心 36 | 37 | ### 示例 38 | 39 | 长参数: 40 | 41 | ```bash 42 | judge --cmd=python3@Solution.py \ 43 | --time=100 \ 44 | --ram=16 \ 45 | --output=1 \ 46 | --workdir=/tmp/solution \ 47 | --data=/tmp/data 48 | ``` 49 | 50 | 短参数: 51 | 52 | ```bash 53 | judge -c java@-Xmx256m@Solution \ 54 | -t 200 \ 55 | -m 64 \ 56 | -o 8 \ 57 | -w /tmp/solution \ 58 | -d /tmp/data 59 | ``` 60 | 61 | ### 判题结果 62 | 63 | - 用户的输出保存在工作目录的 `*.out` 64 | - 判题结果输出到 `stdout` 65 | 66 | 示例: 67 | 68 | ```json 69 | { 70 | "result": 1, 71 | "desc": "AC", 72 | "total": 2, 73 | "passed": 2, 74 | "passRate": 1, 75 | "time": 980, 76 | "memory": 560, 77 | "detail": [ "00-foo.out", "01-bar.out" ] 78 | } 79 | ``` 80 | 81 | - `time`: 运行时间,单位:微秒 82 | - `memory`: 内存占用,单位:KiB 83 | - `total`: 测试点数量 84 | - `passed`: 通过测试点数量 85 | - `detail`: 通过的测试点文件名 86 | 87 | > `time` 和 `memory` 为所有测试点中的最大值。 88 | 89 | `result` 的取值如下: 90 | 91 | - 1: 通过(AC) 92 | - 2: 超时(TLE) 93 | - 3: 内存超限(MLE) 94 | - 4: 部分通过(PA) 95 | - 5: 答案错误(WA) 96 | - 7: 运行错误(RE) 97 | - 8: 内部错误(IE) 98 | - 9: 输出超限(OLE) 99 | 100 | 如果发生错误(IE),输出如下: 101 | 102 | ```json 103 | { 104 | "result": 8, 105 | "error": "错误信息..." 106 | } 107 | ``` 108 | -------------------------------------------------------------------------------- /web/src/api/request/auth-api.ts: -------------------------------------------------------------------------------- 1 | import axios, { ApiPath, resolveError } from "@/api" 2 | import type { UsernamePassword } from "@/api/type" 3 | import type { AxiosResponse } from "axios" 4 | 5 | /** 6 | * 授权验证接口 7 | */ 8 | const AuthApi = { 9 | /** 10 | * 登录 11 | */ 12 | login(user: UsernamePassword): Promise { 13 | return new Promise((resolve, reject) => { 14 | axios({ 15 | url: ApiPath.LOGIN, 16 | method: "POST", 17 | data: JSON.stringify(user) 18 | }) 19 | .then((res) => { 20 | resolve(res.data) 21 | }) 22 | .catch((error) => { 23 | reject(resolveError(error)) 24 | }) 25 | }) 26 | }, 27 | 28 | /** 29 | * 登出 30 | */ 31 | logoff(): Promise { 32 | return new Promise((resolve, reject) => { 33 | axios({ 34 | url: ApiPath.LOGOFF, 35 | method: "DELETE" 36 | }) 37 | .then((res) => { 38 | resolve(res) 39 | }) 40 | .catch((error) => { 41 | reject(resolveError(error)) 42 | }) 43 | }) 44 | }, 45 | 46 | /** 47 | * 刷新 Token 48 | */ 49 | refresh_token(): Promise { 50 | return new Promise((resolve, reject) => { 51 | axios({ 52 | url: ApiPath.REFRESH_TOKEN, 53 | method: "GET" 54 | }) 55 | .then((res) => { 56 | resolve(res.data) 57 | }) 58 | .catch((error) => { 59 | reject(resolveError(error)) 60 | }) 61 | }) 62 | }, 63 | 64 | /** 65 | * 验证 Token 66 | */ 67 | verify(): Promise { 68 | return new Promise((resolve, reject) => { 69 | axios({ 70 | url: ApiPath.VERIFY, 71 | method: "GET" 72 | }) 73 | .then((res) => { 74 | resolve(res) 75 | }) 76 | .catch((error) => { 77 | reject(resolveError(error)) 78 | }) 79 | }) 80 | } 81 | } 82 | 83 | export default AuthApi 84 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/error/GlobalErrorHandler.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.error; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.apache.commons.lang3.exception.ExceptionUtils; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | import org.springframework.web.bind.annotation.RestControllerAdvice; 12 | 13 | import java.io.IOException; 14 | 15 | @Slf4j 16 | @RestControllerAdvice 17 | @RequiredArgsConstructor 18 | public class GlobalErrorHandler { 19 | 20 | private final ObjectMapper mapper; 21 | 22 | @ExceptionHandler(RuntimeException.class) 23 | public void otherErrorHandler(HttpServletRequest request, HttpServletResponse response, RuntimeException e) 24 | throws IOException { 25 | var status = HttpStatus.INTERNAL_SERVER_ERROR; 26 | var msg = ExceptionUtils.getRootCause(e).getMessage(); 27 | log.error(msg); 28 | 29 | if (e instanceof GenericException) { 30 | status = ((GenericException) e).getStatus(); 31 | } 32 | 33 | response.setCharacterEncoding("UTF-8"); 34 | var writer = response.getWriter(); 35 | 36 | if (request.getHeader("Accept").equals("text/event-stream")) { 37 | // 请求类型为 EventSource,发送一个 error 事件 38 | response.setContentType("text/event-stream"); 39 | writer.print("event: error\n"); 40 | writer.print("data: " + msg + "\n\n"); 41 | } else { 42 | var body = mapper.writeValueAsString(new ErrorMessage(status, msg)); 43 | response.setStatus(status.value()); 44 | response.setContentType("application/json"); 45 | writer.print(body); 46 | } 47 | 48 | writer.flush(); 49 | writer.close(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/entity/Solution.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonGetter; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import jakarta.annotation.Nullable; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | import java.math.BigInteger; 10 | 11 | @Getter 12 | @Setter 13 | @SuppressWarnings("unused") 14 | public class Solution { 15 | @JsonIgnore 16 | public static final String[] S = {"运行完成", "正在运行", "正在编译", "等待判题"}; 17 | 18 | @JsonIgnore 19 | public static final String[] R = { 20 | "完全正确", "时间超限", "内存超限", 21 | "部分通过", "答案错误", "编译错误", 22 | "运行错误", "内部错误", "输出超限" 23 | }; 24 | 25 | @JsonIgnore 26 | public Integer _total; 27 | 28 | private BigInteger solutionId; 29 | private Integer problemId; 30 | private String title; 31 | private Integer uid; 32 | // username, nickname, realName 管理查询时存在 33 | private String username; 34 | private String nickname; 35 | private String realName; 36 | private Integer passed; 37 | private Integer total; 38 | private Double passRate; 39 | private Double score; 40 | private Long time; 41 | private Long memory; 42 | private Integer language; 43 | private Integer state; 44 | private Integer result; 45 | private String stateText; 46 | private String resultText; 47 | // UNIX 时间戳(13 位) 48 | private Long submitTime; 49 | private String errorInfo; 50 | private String sourceCode; 51 | 52 | @JsonGetter("solutionId") 53 | public String getId() { 54 | return solutionId == null ? null : solutionId.toString(); 55 | } 56 | 57 | public void setState(Integer state) { 58 | this.state = state; 59 | this.stateText = S[state]; 60 | } 61 | 62 | public void setResult(@Nullable Integer result) { 63 | this.result = result; 64 | 65 | if (result != null) { 66 | this.resultText = R[result]; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web/src/views/layout/TopNavbar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 51 | 52 | 77 | -------------------------------------------------------------------------------- /judge/include/runner.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_H 2 | #define RUNNER_H 1 3 | 4 | #include 5 | #include 6 | 7 | #define AC 1 8 | #define TLE 2 9 | #define MLE 3 10 | #define PA 4 11 | #define WA 5 12 | #define RE 7 13 | #define IE 8 14 | #define OLE 9 15 | 16 | #define ALARM_SECONDS 20 17 | 18 | // Special Judge Define 19 | typedef bool (*spj_func)(std::ifstream*, std::ifstream*, std::ifstream*); 20 | 21 | /** 22 | * 运行配置(资源限制和文件路径) 23 | */ 24 | struct Config 25 | { 26 | long timeout{}; // 运行时间(μs) 27 | long memory{}; // 内存限制(KiB),用于判断是否超出限制 28 | long output_size{}; // 输出限制(KiB) 29 | int cpu = 0; // CPU 核心,将进程绑定到指定核心减少切换 30 | int std_in{}; // 输入文件 fd(用于重定向 stdin) 31 | int std_out{}; // 用户输出 fd(用于重定向 stdout) 32 | int in_fd{}; // 输入文件 fd 33 | int out_fd{}; // 用户输出 fd(用于对比) 34 | int ans_fd{}; // 正确输出 fd(用于对比) 35 | std::ifstream* in{}; 36 | std::ifstream* out{}; 37 | std::ifstream* ans{}; 38 | }; 39 | 40 | /** 41 | * 每个测试点的运行结果 42 | */ 43 | struct Result 44 | { 45 | int status; 46 | long time; // μs 47 | long mem; // KiB 48 | char err[128]; 49 | }; 50 | 51 | /** 52 | * 最终结果 53 | */ 54 | struct RTN 55 | { 56 | int result; 57 | int total; 58 | int passed; 59 | double passRate; 60 | long long time; // μs 61 | long long memory; // KiB 62 | char err[128]; // 错误信息 63 | std::vector detail; // 存储通过的测试点文件名 64 | }; 65 | 66 | class Runner 67 | { 68 | int root_fd; 69 | // exec 参数 70 | char* argv[16]{}; 71 | // 工作目录(用户程序所在目录) 72 | char* work_dir; 73 | // 测试数据目录 74 | char* data_dir; 75 | Config config; 76 | // SPJ 动态链接库 77 | void* dl_handler = nullptr; 78 | // SPJ 函数 79 | spj_func spj = nullptr; 80 | void run_cmd() const; 81 | 82 | void watch_result(pid_t pid, Result* res) const; 83 | 84 | void run(Result* res) const; 85 | 86 | public: 87 | Runner(char* cmd, char* work_dir, char* data_dir, const Config& config); 88 | 89 | ~Runner(); 90 | 91 | RTN judge(); 92 | }; 93 | 94 | #endif // RUNNER_H 95 | -------------------------------------------------------------------------------- /web/src/views/layout/AdminNavbar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 83 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/controller/RankingController.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.controller; 2 | 3 | import cloud.oj.core.service.RankingService; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.*; 6 | 7 | @RestController 8 | @RequestMapping("ranking") 9 | public class RankingController { 10 | 11 | private final RankingService rankingService; 12 | 13 | public RankingController(RankingService rankingService) { 14 | this.rankingService = rankingService; 15 | } 16 | 17 | /** 18 | * 获取排行榜 19 | */ 20 | @GetMapping(produces = "application/json") 21 | public ResponseEntity getRanking(@RequestParam(defaultValue = "1") Integer page, 22 | @RequestParam(defaultValue = "15") Integer size) { 23 | var data = rankingService.getRanking(page, size, false); 24 | return data.getTotal() > 0 ? ResponseEntity.ok(data) : ResponseEntity.noContent().build(); 25 | } 26 | 27 | /** 28 | * 获取排行榜 29 | */ 30 | @GetMapping(path = "admin", produces = "application/json") 31 | public ResponseEntity getRankingAdmin(@RequestParam(defaultValue = "1") Integer page, 32 | @RequestParam(defaultValue = "15") Integer size) { 33 | var data = rankingService.getRanking(page, size, true); 34 | return data.getTotal() > 0 ? ResponseEntity.ok(data) : ResponseEntity.noContent().build(); 35 | } 36 | 37 | /** 38 | * 获取竞赛排行榜 39 | */ 40 | @GetMapping(path = "contest/{cid}") 41 | public ResponseEntity getContestRanking(@PathVariable Integer cid) { 42 | var scoreboard = rankingService.getContestRanking(cid, false); 43 | return ResponseEntity.ok(scoreboard); 44 | } 45 | 46 | /** 47 | * 获取竞赛排行榜(管理员) 48 | */ 49 | @GetMapping(path = "admin/contest/{contestId}") 50 | public ResponseEntity getRankingListAdmin(@PathVariable Integer contestId) { 51 | return ResponseEntity.ok(rankingService.getContestRanking(contestId, true)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /judge/src/common.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "common.h" 4 | 5 | void split(char** arr, char* str, const char* separator) 6 | { 7 | char* s = strtok(str, separator); 8 | 9 | while (s != nullptr) 10 | { 11 | *arr++ = s; 12 | s = strtok(nullptr, separator); 13 | } 14 | 15 | *arr = nullptr; 16 | } 17 | 18 | static option long_options[] = { 19 | {"cmd", 1, nullptr, 'c'}, 20 | {"time", 1, nullptr, 't'}, 21 | {"ram", 1, nullptr, 'm'}, 22 | {"output", 1, nullptr, 'o'}, 23 | {"workdir", 1, nullptr, 'w'}, 24 | {"data", 1, nullptr, 'd'}, 25 | {"cpu", 1, nullptr, 'u'}, 26 | {nullptr, 0, nullptr, 0} 27 | }; 28 | 29 | static auto short_options = "c:r:t:m:o:w:d:u:"; 30 | 31 | int get_args(const int argc, char* argv[], char* cmd, char workdir[], char datadir[], Config& config) 32 | { 33 | int opt; 34 | int index = 0; 35 | int count = 0; 36 | 37 | while ((opt = getopt_long_only(argc, argv, short_options, long_options, &index)) != EOF) 38 | { 39 | switch (opt) 40 | { 41 | case 'c': 42 | strcpy(cmd, optarg); 43 | count++; 44 | break; 45 | case 'w': 46 | strcpy(workdir, optarg); 47 | count++; 48 | break; 49 | case 'd': 50 | strcpy(datadir, optarg); 51 | count++; 52 | break; 53 | case 'u': 54 | config.cpu = (int)strtol(optarg, nullptr, 10); 55 | count++; 56 | break; 57 | case 't': 58 | config.timeout = (int)strtol(optarg, nullptr, 10) * 1000; 59 | count++; 60 | break; 61 | case 'm': 62 | config.memory = (int)strtol(optarg, nullptr, 10) << 10; 63 | count++; 64 | break; 65 | case 'o': 66 | config.output_size = (int)strtol(optarg, nullptr, 10) << 10; 67 | count++; 68 | break; 69 | case '?': 70 | default: 71 | return -1; 72 | } 73 | } 74 | 75 | if (count < 7) 76 | { 77 | return -1; 78 | } 79 | 80 | return 0; 81 | } 82 | -------------------------------------------------------------------------------- /web/src/views/components/Account/ResultsPanel.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 78 | -------------------------------------------------------------------------------- /web/src/views/components/Admin/Overview/QueuesInfoView.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 54 | 55 | 89 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/receiver/SolutionReceiver.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.receiver; 2 | 3 | import cloud.oj.judge.component.JudgementEntry; 4 | import cloud.oj.judge.config.RabbitConfig; 5 | import cloud.oj.judge.entity.Solution; 6 | import cloud.oj.judge.entity.SubmitData; 7 | import cloud.oj.judge.service.SubmitService; 8 | import com.rabbitmq.client.Channel; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 12 | import org.springframework.amqp.support.AmqpHeaders; 13 | import org.springframework.messaging.handler.annotation.Headers; 14 | import org.springframework.messaging.handler.annotation.Payload; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.io.IOException; 18 | import java.util.Map; 19 | 20 | /** 21 | * 消息接收(提交和判题) 22 | */ 23 | @Slf4j 24 | @Component 25 | @RequiredArgsConstructor 26 | public class SolutionReceiver { 27 | 28 | private final SubmitService submitService; 29 | 30 | private final JudgementEntry judgementEntry; 31 | 32 | /** 33 | * 监听判题队列 34 | */ 35 | @RabbitListener(queues = RabbitConfig.JUDGE_QUEUE, ackMode = "MANUAL", concurrency = "1") 36 | public void handleJudgement(@Payload Solution solution, @Headers Map headers, Channel channel) 37 | throws IOException { 38 | judgementEntry.judge(solution, () -> channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false)); 39 | } 40 | 41 | /** 42 | * 监听提交队列 43 | */ 44 | @RabbitListener(queues = RabbitConfig.SUBMIT_QUEUE, ackMode = "MANUAL", concurrency = "2") 45 | public void handleSubmission(@Payload SubmitData data, @Headers Map headers, Channel channel) 46 | throws IOException { 47 | try { 48 | submitService.submit(data); 49 | channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false); 50 | } catch (Exception e) { 51 | // submit 失败,重新入队 52 | log.error("提交失败(已重新入队): {}", e.getMessage()); 53 | channel.basicNack((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false, true); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/service/UserService.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.service; 2 | 3 | import cloud.oj.gateway.entity.Role; 4 | import cloud.oj.gateway.entity.User; 5 | import cloud.oj.gateway.repo.UserRepo; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.core.userdetails.UserDetailsService; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.Optional; 16 | 17 | @Slf4j 18 | @Service 19 | public class UserService implements UserDetailsService { 20 | 21 | private final UserRepo userRepo; 22 | 23 | private static final List ROLE_LIST = new ArrayList<>() { 24 | { 25 | add(new Role(0, "ROLE_ADMIN")); 26 | add(new Role(1, "ROLE_USER")); 27 | } 28 | }; 29 | 30 | public UserService(UserRepo userRepo) { 31 | this.userRepo = userRepo; 32 | } 33 | 34 | @Override 35 | public UserDetails loadUserByUsername(String username) { 36 | var user = userRepo.findByUsername(username); 37 | 38 | if (user.isEmpty()) { 39 | var error = String.format("User(id=%s) not found", username); 40 | log.error(error); 41 | throw new UsernameNotFoundException(error); 42 | } 43 | 44 | int role = user.get().getRole(); 45 | List roles; 46 | 47 | if (role == 0) { 48 | // ADMIN ROLE 49 | roles = ROLE_LIST; 50 | } else { 51 | roles = Arrays.asList(ROLE_LIST.get(1), ROLE_LIST.get(role)); 52 | } 53 | 54 | user.get().setRoles(roles); 55 | 56 | return user.get(); 57 | } 58 | 59 | public Optional findById(Integer uid) { 60 | return userRepo.findById(uid); 61 | } 62 | 63 | public Optional getSecret(Integer uid) { 64 | return userRepo.getSecret(uid); 65 | } 66 | 67 | public void updateSecret(Integer uid, String newSecret) { 68 | userRepo.updateSecret(uid, newSecret); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/error/GlobalErrorHandler.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.error; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.apache.commons.lang3.exception.ExceptionUtils; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | import org.springframework.web.bind.annotation.RestControllerAdvice; 12 | import org.springframework.web.client.HttpServerErrorException; 13 | 14 | import java.io.IOException; 15 | 16 | @Slf4j 17 | @RestControllerAdvice 18 | @RequiredArgsConstructor 19 | public class GlobalErrorHandler { 20 | 21 | private final ObjectMapper mapper; 22 | 23 | @ExceptionHandler(RuntimeException.class) 24 | public void errorHandler(HttpServletRequest request, HttpServletResponse response, RuntimeException e) 25 | throws IOException { 26 | var status = HttpStatus.INTERNAL_SERVER_ERROR; 27 | var msg = ExceptionUtils.getRootCause(e).getMessage(); 28 | log.error(msg); 29 | 30 | if (e instanceof HttpServerErrorException) { 31 | // eg: 503 Service Unavailable 32 | status = (HttpStatus) ((HttpServerErrorException) e).getStatusCode(); 33 | msg = status.getReasonPhrase(); 34 | } else if (e instanceof GenericException) { 35 | status = ((GenericException) e).getStatus(); 36 | } 37 | 38 | response.setCharacterEncoding("UTF-8"); 39 | var writer = response.getWriter(); 40 | 41 | if (request.getHeader("Accept").equals("text/event-stream")) { 42 | // 请求类型为 EventSource,发送一个 error 事件 43 | response.setContentType("text/event-stream"); 44 | writer.print("event: error\n"); 45 | writer.print("data: " + msg + "\n\n"); 46 | } else { 47 | var body = mapper.writeValueAsString(new ErrorMessage(status, msg)); 48 | response.setStatus(status.value()); 49 | response.setContentType("application/json"); 50 | writer.print(body); 51 | } 52 | 53 | writer.flush(); 54 | writer.close(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/config/RabbitConfig.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.config; 2 | 3 | import com.rabbitmq.http.client.Client; 4 | import com.rabbitmq.http.client.domain.PolicyInfo; 5 | import com.rabbitmq.http.client.domain.QueueInfo; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.amqp.core.Queue; 8 | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; 9 | import org.springframework.amqp.support.converter.MessageConverter; 10 | import org.springframework.boot.autoconfigure.amqp.RabbitProperties; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | 14 | import java.net.MalformedURLException; 15 | import java.net.URISyntaxException; 16 | import java.net.URL; 17 | import java.util.Map; 18 | 19 | @Configuration 20 | @RequiredArgsConstructor 21 | public class RabbitConfig { 22 | 23 | public static final String SUBMIT_QUEUE = "Submit"; 24 | 25 | public static final String JUDGE_QUEUE = "Judge"; 26 | 27 | private static final Map DEFAULT_POLICY = Map.of( 28 | "max-length", 5000, 29 | "overflow", "reject-publish" 30 | ); 31 | 32 | private final RabbitProperties rabbitProperties; 33 | 34 | @Bean 35 | public Client rabbitClient() throws MalformedURLException, URISyntaxException { 36 | var url = new URL("http", rabbitProperties.getHost(), 15672, "/api"); 37 | return new Client(url, rabbitProperties.getUsername(), rabbitProperties.getPassword()); 38 | } 39 | 40 | @Bean 41 | public Queue submitQueue(Client rabbitClient) { 42 | rabbitClient.declareQueue("/", SUBMIT_QUEUE, new QueueInfo(true, false, false)); 43 | 44 | if (!"limit".equals(rabbitClient.getQueue("/", RabbitConfig.SUBMIT_QUEUE).getPolicy())) { 45 | var policy = new PolicyInfo(RabbitConfig.SUBMIT_QUEUE, 1, "queues", DEFAULT_POLICY); 46 | rabbitClient.declarePolicy("/", "limit", policy); 47 | } 48 | 49 | return new Queue(SUBMIT_QUEUE); 50 | } 51 | 52 | @Bean 53 | public Queue judgeQueue() { 54 | return new Queue(JUDGE_QUEUE); 55 | } 56 | 57 | @Bean 58 | public MessageConverter jsonMessageConverter() { 59 | return new Jackson2JsonMessageConverter(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /web/src/style.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --layout-padding: 12px; 3 | --header-height: 57px; 4 | --footer-height: 55px; 5 | --primary-color: #18a058; 6 | } 7 | 8 | html { 9 | body { 10 | overflow: hidden; 11 | } 12 | } 13 | 14 | #app { 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | height: 100%; 19 | 20 | .header { 21 | z-index: 1; 22 | } 23 | 24 | .aside { 25 | z-index: 2; 26 | } 27 | 28 | nav[class="n-breadcrumb"] { 29 | line-height: normal; 30 | } 31 | 32 | td[class="n-data-table-td"] { 33 | vertical-align: middle; 34 | } 35 | 36 | .main { 37 | &.admin { 38 | top: var(--header-height); 39 | } 40 | 41 | .wrap { 42 | width: calc(100% - var(--layout-padding) * 4); 43 | max-width: 1200px; 44 | padding: calc(var(--layout-padding) * 2) 0; 45 | } 46 | 47 | .admin-wrap { 48 | width: calc(100% - var(--layout-padding) * 2); 49 | padding: var(--layout-padding); 50 | } 51 | } 52 | 53 | .n-scrollbar.global { 54 | > .n-scrollbar-container { 55 | > .n-scrollbar-content { 56 | display: flex; 57 | flex-direction: column; 58 | align-items: center; 59 | min-height: calc(100vh - var(--header-height)); 60 | } 61 | } 62 | } 63 | 64 | .input-prefix-icon { 65 | margin-right: 4px; 66 | } 67 | 68 | .tag { 69 | margin: 1px 3px; 70 | cursor: pointer; 71 | 72 | &:hover { 73 | opacity: 0.6; 74 | } 75 | } 76 | 77 | a { 78 | color: inherit; 79 | text-decoration: none; 80 | } 81 | } 82 | 83 | .cm-s-ttcn { 84 | border: 1px solid #ededef; 85 | 86 | .CodeMirror-gutters { 87 | border-right: 1px solid #ededef; 88 | } 89 | } 90 | 91 | .cm-s-material-darker { 92 | border-top: 1px solid #161b22; 93 | border-right: 1px solid #161b22; 94 | border-bottom: 1px solid #161b22; 95 | 96 | .CodeMirror-scroll { 97 | background-color: #0d1117; 98 | } 99 | 100 | .CodeMirror-gutter { 101 | background-color: #161b22; 102 | } 103 | 104 | .CodeMirror-linenumber { 105 | background-color: #161b22; 106 | } 107 | } 108 | 109 | .layout-max-height { 110 | height: calc(100% - var(--layout-padding)); 111 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud OJ 2 | 3 | ![Stars](https://img.shields.io/github/stars/ifyun/Cloud-OJ?logo=github&style=flat) 4 | ![Top Languages](https://img.shields.io/github/languages/top/ifyun/Cloud-OJ?logo=github) 5 | ![Last Commit](https://img.shields.io/github/last-commit/ifyun/Cloud-OJ?logo=github) 6 | ![Codacy Badge](https://img.shields.io/codacy/grade/3fb7e4c059c5431799b8863218750095?logo=codacy) 7 | ![Platform](https://img.shields.io/badge/platform-linux--64-blueviolet?logo=linux&logoColor=white) 8 | ![CMake Build](https://img.shields.io/github/actions/workflow/status/ifyun/Cloud-OJ/cmake.yml?label=cmake%20build&logo=cmake&logoColor=blue) 9 | ![Maven Build](https://img.shields.io/github/actions/workflow/status/ifyun/Cloud-OJ/maven.yml?label=maven%20build&logo=apache-maven&logoColor=red) 10 | ![Vite Build](https://img.shields.io/github/actions/workflow/status/ifyun/Cloud-OJ/node.js.yml?label=vite%20build&logo=vite) 11 | 12 | Cloud OJ 是一个“微”服务架构的 Online Judge 系统,基于 Spring Cloud、Vue.js、UNIX API 13 | 14 | - 容器化运行 15 | - 代码高亮 16 | - 亮色/暗色主题 17 | - 可扩展的判题节点 18 | - Special Judge 19 | 20 | 21 | 22 | 23 | 24 | 25 |
lightdark
26 | 27 | ## 语言支持 28 | 29 | - C 30 | - C++ 31 | - Java 32 | - Python 33 | - Bash Shell 34 | - C# 35 | - JavaScript 36 | - Kotlin 37 | - Go 38 | 39 | ## 文档 40 | 41 | - [构建 / 安装运行](doc/Build&Setup.md) 42 | - [搭建开发环境 / Debug](doc/Dev.md) 43 | 44 | ## 相关资源 45 | 46 | - [Spring](https://spring.io/) 47 | - [Consul](https://www.consul.io/) 48 | - [MariaDB](https://mariadb.org/) 49 | - [RabbitMQ](https://www.rabbitmq.com/) 50 | - [Vite](https://vitejs.dev/) 51 | - [Vue.js](https://vuejs.org/) 52 | - [Pinia](https://pinia.vuejs.org/) 53 | - [Naive UI](https://naiveui.com/) 54 | - [Axios](https://github.com/axios/axios) 55 | - [Day.js](https://day.js.org/) 56 | - [CodeMirror 5](https://codemirror.net/5/) 57 | - [KaTeX](https://katex.org/) 58 | - [Apache Echarts](https://echarts.apache.org/) 59 | - [highlight.js](https://highlightjs.org/) 60 | - [markdown-it](https://github.com/markdown-it/) 61 | - [xicons](https://www.xicons.org/) 62 | 63 | Thanks to JetBrains for providing the Open Source Development license. 64 | 65 | 66 | JetBrains Logo. 67 | 68 | -------------------------------------------------------------------------------- /doc/Dev.md: -------------------------------------------------------------------------------- 1 | # 设置开发环境 2 | 3 | 判题程序需要 Linux 环境,建议使用 Debian。 4 | 5 | 使用 fnm 安装 Node.js: 6 | 7 | ```bash 8 | curl -o- https://fnm.vercel.app/install | bash 9 | fnm install 22 10 | ``` 11 | 12 | 使用包管理器安装 C / C++ / CMake / Java / Maven 13 | 14 | ```bash 15 | sudo apt install cmake build-essential openjdk-17-jdk-headless maven 16 | ``` 17 | 18 | 创建必要的目录: 19 | 20 | ```bash 21 | sudo mkdir /opt/.m2 /opt/cloud-oj 22 | sudo chmod 777 /opt/.m2 23 | sudo chmod 777 /opt/cloud-oj 24 | ln -s /opt/.m2 /root/.m2 25 | ln -s /opt/.m2 ~/.m2 26 | ln -s /opt/cloud-oj /root/.local/cloud-oj 27 | ln -s /opt/cloud-oj ~/.local/cloud-oj 28 | ``` 29 | 30 | ## 设置 Judge 运行环境 31 | 32 | C / C++ / Java / JS 环境已在前面的步骤中安装。 33 | 34 | ### Kotlin Native 35 | 36 | ```bash 37 | curl -LJO https://github.com/JetBrains/kotlin/releases/download/v2.1.10/kotlin-native-prebuilt-linux-x86_64-2.1.10.tar.gz \ 38 | && sudo tar -C /usr/local -xzf kotlin-native-prebuilt-linux-x86_64-2.1.10.tar.gz \ 39 | --transform 's/kotlin-native-prebuilt-linux-x86_64-2.1.10/kotlin/' \ 40 | && sudo ln -s /usr/local/kotlin/bin/kotlinc-native /usr/bin/kotlinc-native \ 41 | && sudo ln -s /usr/local/kotlin/bin/run_konan /usr/bin/run_konan 42 | ``` 43 | 44 | 编译一个不存在的文件触发依赖下载: 45 | 46 | ```bash 47 | sudo kotlinc-native nothing.kt 48 | ``` 49 | 50 | > Kotlin Native 依赖会下载到 `~` 目录,而 Judge Service 在 `root` 权限下运行,因此需要以 `root` 运行以上命令。 51 | 52 | ### Golang 53 | 54 | ```bash 55 | curl -LJO https://golang.google.cn/dl/go1.24.1.linux-amd64.tar.gz \ 56 | && tar -C /usr/local -xzf go1.24.1.linux-amd64.tar.gz \ 57 | && ln -s /usr/local/go/bin/go /usr/bin/go 58 | ``` 59 | 60 | ### C# 61 | 62 | C# 使用 `dotnet-sdk-8.0` 编译和运行。 63 | 64 | 1.配置软件源: 65 | 66 | ```bash 67 | wget https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ 68 | -O packages-microsoft-prod.deb \ 69 | && sudo dpkg -i packages-microsoft-prod.deb \ 70 | && rm packages-microsoft-prod.deb 71 | ``` 72 | 73 | 2.安装 SDK: 74 | 75 | ```bash 76 | sudo apt-get update \ 77 | && sudo apt-get install -y dotnet-sdk-8.0 78 | ``` 79 | 80 | 3.创建单文件编译脚本: 81 | 82 | ```bash 83 | sudo echo -n "dotnet /usr/share/dotnet/sdk/$(dotnet --version)/Roslyn/bincore/csc.dll " > /bin/csc \ 84 | && echo -n "/r:/usr/share/dotnet/sdk/$(dotnet --version)/ref/netstandard.dll " >> /bin/csc \ 85 | && echo '"$@"' >> /bin/csc 86 | ``` 87 | 88 | 4.创建运行时配置文件: 89 | 90 | 将项目根目录中的 `dotnet.runtimeconfig.json` 复制到 `/etc` 目录,运行时配置文件将在判题时链接到工作目录。 91 | -------------------------------------------------------------------------------- /web/src/components/MarkdownView/markdown-katex.ts: -------------------------------------------------------------------------------- 1 | import katex from "katex" 2 | import "katex/dist/katex.css" 3 | 4 | /** 5 | * markdown-it KaTex 插件 6 | */ 7 | export const KatexPlugin = (md: any) => { 8 | const inline_code = md.renderer.rules.code_inline.bind(md.renderer.rules) 9 | const block_code = md.renderer.rules.fence.bind(md.renderer.rules) 10 | const text = md.renderer.rules.text.bind(md.renderer.rules) 11 | 12 | /** 13 | * 渲染行内代码块中的公式, eg: `$x \leq y^2$` 14 | */ 15 | md.renderer.rules.code_inline = ( 16 | tokens: any, 17 | index: number, 18 | options: any, 19 | env: any, 20 | slf: any 21 | ) => { 22 | let code = tokens[index].content as string 23 | 24 | if (code.startsWith("$") && code.endsWith("$")) { 25 | code = code.substring(1, code.length - 1) 26 | try { 27 | return katex.renderToString(code) 28 | } catch { 29 | return `语法错误` 30 | } 31 | } 32 | 33 | return inline_code(tokens, index, options, env, slf) 34 | } 35 | 36 | /** 37 | * 渲染行内公式, eg: $x \leq y^2$ 38 | */ 39 | md.renderer.rules.text = ( 40 | tokens: any, 41 | index: number, 42 | options: any, 43 | env: any, 44 | slf: any 45 | ) => { 46 | let content = tokens[index].content as string 47 | const match = content.match(/\$+([^$\n]+?)\$+/g) 48 | 49 | if (match) { 50 | try { 51 | match.forEach((value) => { 52 | const katexElement = katex.renderToString( 53 | value.substring(1, value.length - 1) 54 | ) 55 | content = content.replace(value, katexElement) 56 | }) 57 | 58 | return content 59 | } catch { 60 | return `语法错误` 61 | } 62 | } 63 | 64 | return text(tokens, index, options, env, slf) 65 | } 66 | 67 | /** 68 | * 渲染代码块中的公式 69 | */ 70 | md.renderer.rules.fence = ( 71 | tokens: any, 72 | index: number, 73 | options: any, 74 | env: any, 75 | slf: any 76 | ) => { 77 | const token = tokens[index] 78 | const code = (token.content as string).trim() 79 | 80 | if ( 81 | token.info === "math" || 82 | token.info === "katex" || 83 | token.info === "latex" 84 | ) { 85 | try { 86 | return `
${katex.renderToString(code)}
` 87 | } catch { 88 | return `
语法错误
` 89 | } 90 | } 91 | 92 | return block_code(tokens, index, options, env, slf) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /services/gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cloud.oj 8 | cloud-oj 9 | 1.0.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | gateway 13 | Gateway 14 | Cloud OJ API Gateway 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-configuration-processor 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-web 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-security 27 | 28 | 29 | org.springframework.cloud 30 | spring-cloud-gateway-server-webmvc 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-cache 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-jdbc 39 | 40 | 41 | org.mariadb.jdbc 42 | mariadb-java-client 43 | 44 | 45 | org.apache.commons 46 | commons-lang3 47 | 48 | 49 | io.jsonwebtoken 50 | jjwt-api 51 | 0.13.0 52 | 53 | 54 | io.jsonwebtoken 55 | jjwt-impl 56 | 0.13.0 57 | runtime 58 | 59 | 60 | io.jsonwebtoken 61 | jjwt-jackson 62 | 0.13.0 63 | runtime 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/repo/UserRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.repo; 2 | 3 | import cloud.oj.gateway.entity.User; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.jdbc.core.simple.JdbcClient; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | @RequiredArgsConstructor 12 | public class UserRepo { 13 | 14 | private final JdbcClient client; 15 | 16 | public Optional findById(Integer uid) { 17 | return client.sql(""" 18 | select uid, 19 | username, 20 | nickname, 21 | password, 22 | secret, 23 | email, 24 | section, 25 | has_avatar, 26 | role 27 | from user 28 | where uid = :uid 29 | and deleted = false 30 | """ 31 | ).param("uid", uid) 32 | .query(User.class) 33 | .optional(); 34 | } 35 | 36 | public Optional findByUsername(String username) { 37 | return client.sql(""" 38 | select uid, 39 | username, 40 | nickname, 41 | password, 42 | secret, 43 | email, 44 | section, 45 | has_avatar, 46 | role 47 | from user 48 | where username = :username 49 | and deleted = false 50 | """ 51 | ).param("username", username) 52 | .query(User.class) 53 | .optional(); 54 | } 55 | 56 | public Optional getSecret(Integer uid) { 57 | return client.sql("select secret from user where uid = :uid") 58 | .param("uid", uid) 59 | .query(String.class) 60 | .optional(); 61 | } 62 | 63 | public void updateSecret(Integer uid, String secret) { 64 | client.sql("update user set secret = :secret where uid = :uid") 65 | .param("secret", secret) 66 | .param("uid", uid) 67 | .update(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/repo/ProblemRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.repo; 2 | 3 | import cloud.oj.judge.entity.Problem; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.core.type.TypeReference; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.jdbc.core.simple.JdbcClient; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import java.util.Map; 12 | import java.util.Optional; 13 | 14 | @Repository 15 | @RequiredArgsConstructor 16 | public class ProblemRepo { 17 | 18 | private final ObjectMapper mapper; 19 | 20 | private final JdbcClient client; 21 | 22 | /** 23 | * 查询题目的资源限制 24 | * 25 | * @param pid 题目 Id 26 | * @return {@link Problem} 27 | */ 28 | public Problem selectById(int pid) { 29 | return client.sql(""" 30 | select timeout, 31 | memory_limit, 32 | output_limit, 33 | score 34 | from problem 35 | where problem_id = :pid 36 | """) 37 | .param("pid", pid) 38 | .query(Problem.class) 39 | .single(); 40 | } 41 | 42 | /** 43 | * 查询指定题目的测试数据配置 44 | * 45 | * @param pid 题目 Id 46 | * @return {@link Map} 47 | */ 48 | public Optional> selectDataConf(Integer pid) { 49 | return client.sql(""" 50 | select * from data_conf where problem_id = :pid 51 | """) 52 | .param("pid", pid) 53 | .query((rs, rowNum) -> { 54 | Map conf; 55 | 56 | try { 57 | conf = mapper.readValue(rs.getString("conf"), new TypeReference<>() { 58 | }); 59 | } catch (JsonProcessingException e) { 60 | throw new RuntimeException(e); 61 | } 62 | 63 | return conf; 64 | }) 65 | .optional(); 66 | } 67 | 68 | /** 69 | * 检查题目是否开放 70 | * 71 | * @param pid 题目 Id 72 | * @return {@link Boolean} 73 | */ 74 | public Boolean isEnable(int pid) { 75 | return client.sql("select enable from problem where problem_id = :pid") 76 | .param("pid", pid) 77 | .query(Boolean.class) 78 | .single(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/config/AsyncConfig.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.config; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.scheduling.annotation.EnableAsync; 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 8 | 9 | import java.util.HashMap; 10 | import java.util.concurrent.Executor; 11 | import java.util.concurrent.RejectedExecutionException; 12 | import java.util.concurrent.RejectedExecutionHandler; 13 | import java.util.concurrent.ThreadPoolExecutor; 14 | 15 | @Slf4j 16 | @EnableAsync 17 | @Configuration 18 | public class AsyncConfig { 19 | private final static String THREAD_PREFIX = "JUDGE-"; 20 | 21 | private final AppConfig appConfig; 22 | 23 | public AsyncConfig(AppConfig appConfig) { 24 | this.appConfig = appConfig; 25 | } 26 | 27 | /** 28 | * 阻塞策略,线程都被占用时阻塞提交 29 | */ 30 | private static class BlockPolicy implements RejectedExecutionHandler { 31 | @Override 32 | public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { 33 | if (!executor.isShutdown()) { 34 | try { 35 | executor.getQueue().put(r); 36 | } catch (InterruptedException e) { 37 | log.error(e.getMessage()); 38 | throw new RejectedExecutionException("Task " + r + " rejected"); 39 | } 40 | } 41 | } 42 | } 43 | 44 | @Bean 45 | public Executor judgeExecutor() { 46 | var threads = appConfig.getCpus().size(); 47 | var executor = new ThreadPoolTaskExecutor(); 48 | 49 | executor.setThreadNamePrefix(THREAD_PREFIX); 50 | executor.setCorePoolSize(threads); 51 | executor.setMaxPoolSize(threads); 52 | // 队列由 RabbitMQ 承担,容量设为 0 构造同步队列 53 | executor.setQueueCapacity(0); 54 | executor.setRejectedExecutionHandler(new BlockPolicy()); 55 | executor.initialize(); 56 | 57 | return executor; 58 | } 59 | 60 | @Bean 61 | public HashMap cpus() { 62 | var cpuMap = new HashMap(); 63 | var cpuList = appConfig.getCpus(); 64 | 65 | // 将 CPU 与线程名称绑定 66 | // 注意:线程池 ID 从 1 开始 67 | for (int i = 0; i < cpuList.size(); i++) { 68 | log.info("{} -> CPU-{}", THREAD_PREFIX + (i + 1), cpuList.get(i)); 69 | cpuMap.put(THREAD_PREFIX + (i + 1), cpuList.get(i)); 70 | } 71 | 72 | return cpuMap; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /doc/Build&Setup.md: -------------------------------------------------------------------------------- 1 | # 构建 / 运行 2 | 3 | 构建过程在容器中进行,你只需要安装 [Docker Engine](https://docs.docker.com/engine/install/) 4 | 5 | ## 准备依赖 6 | 7 | 在 `.build` 目录中放入以下文件: 8 | 9 | - `go1.25.4.linux-amd64.tar.gz` 10 | - `kotlinc-compiler-2.2.21`(解压自 `kotlinc-compiler-2.2.21.zip`) 11 | - `quickjs-linux-x86_64-2025-09-13`(解压自 `quickjs-linux-x86_64-2025-09-13.zip`) 12 | 13 | ## 构建 Docker 镜像 14 | 15 | ```bash 16 | ./build --docker 17 | ``` 18 | 19 | 构建完成后,编排文件将复制到 `~/cloud-oj` 目录 20 | 21 | ### 仅构建指定的镜像 22 | 23 | 仅构建 `cloud-oj:web` 镜像: 24 | 25 | ```bash 26 | ./build --docker --target=web 27 | ``` 28 | 29 | `--target` 可选参数: 30 | 31 | - web 32 | - core 33 | - gateway 34 | - judge 35 | - mariadb 36 | 37 | 如果构建时有一些镜像失败,可以用这种方式重新构建。 38 | 39 | ## 使用在线镜像 40 | 41 | 如果你不想自己构建镜像,可以使用 `ghcr.io/ifyun/cloud-oj:tag`,`tag` 对应前面的 `--target` 参数。 42 | 43 | ## 运行 44 | 45 | 若没有 `~/cloud-oj` 目录,请复制 `docker` 目录: 46 | 47 | ```bash 48 | cp -r docker/. "${HOME}/cloud-oj/" 49 | ``` 50 | 51 | 启动: 52 | 53 | ```bash 54 | cd ~/cloud-oj 55 | docker compose up -d 56 | ``` 57 | 58 | 首次运行时,可以在 `.env` 文件中修改 MariaDB、RabbitMQ 的用户名和密码 59 | 60 | 部分端口未映射到宿主机,如有必要可参考下表: 61 | 62 | | Service | Port(s) | 63 | |----------|-------------| 64 | | consul | 8500 | 65 | | gateway | 8080 | 66 | | core | 8180 | 67 | | judge | 8280 | 68 | | mariadb | 3306 | 69 | | rabbitmq | 5672, 15672 | 70 | 71 | **系统默认管理员账号和密码均为 `admin`**。 72 | 73 | ## 开启 HTTPS 74 | 75 | 建立一个目录存放你的证书和私钥(命名为 `cert.pem`, `private.key`) 76 | 77 | 修改编排文件的 `web` 部分: 78 | 79 | ```yaml 80 | ports: 81 | - "80:80" 82 | - "443:443" 83 | volumes: 84 | - "宿主机证书目录:/ssl" 85 | environment: 86 | API_HOST: "gateway:8080" 87 | ENABLE_HTTPS: "true" 88 | EXTERNAL_URL: "你的域名" 89 | ``` 90 | 91 | 开启 HTTPS 后,80 端口会重定向到 443 92 | 93 | ## 环境变量说明 94 | 95 | `TOKEN_VALID_TIME` 96 | 97 | JWT 有效时间,默认值为 `4`,单位:小时 98 | 99 | `JUDGE_CPUS` 100 | 101 | 判题线程使用的 CPU,多个值用逗号分隔: 102 | 103 | ``` 104 | JUDGE_CPUS=0 # 使用所有 CPU 105 | JUDGE_CPUS=n # 使用 n 个 CPU,从 CPU-0 开始 106 | JUDGE_CPUS=0,1,2 # 使用 CPU-0, CPU-1, CPU-2 107 | ``` 108 | 109 | 默认使用 1 个线程,请根据 CPU 和可用内存量来设置 110 | 111 | `DB_POOL_SIZE` 112 | 113 | 数据库连接池大小,在 JUDGE 服务中,`DB_POOL_SIZE` 必须大于 `JUDGE_CPUS` + 2 114 | 115 | `API_HOST` 116 | 117 | gateway 服务的地址 + 端口(仅 web 容器使用) 118 | 119 | `JVM_OPTS` 120 | 121 | Java 虚拟机参数,eg: -Xmx500m 122 | 123 | `CONSUL_HOST` 124 | 125 | Consul 注册中心的地址 126 | 127 | `USE_IP` 128 | 129 | 使用 IP 地址注册服务,默认值为 `false` 130 | 131 | `SERVICE_IP` 132 | 133 | `USE_IP` 为 `true` 时生效,指定当前服务向注册中心注册的 IP 地址,默认值为 `spring.cloud.client.ip-address` 134 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/controller/FileController.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.controller; 2 | 3 | import cloud.oj.core.entity.SPJ; 4 | import cloud.oj.core.service.FileService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | @RestController 12 | @RequestMapping("file") 13 | @RequiredArgsConstructor 14 | public class FileController { 15 | 16 | private final FileService fileService; 17 | 18 | /** 19 | * 上传头像 20 | */ 21 | @PostMapping(path = "img/avatar") 22 | public ResponseEntity uploadAvatar(@RequestHeader Integer uid, @RequestParam MultipartFile file) { 23 | fileService.saveAvatar(uid, file); 24 | return ResponseEntity.status(HttpStatus.CREATED).build(); 25 | } 26 | 27 | /** 28 | * 上传题目图片 29 | */ 30 | @PostMapping(path = "img/problem") 31 | public ResponseEntity uploadProblemImage(@RequestParam MultipartFile file) { 32 | return ResponseEntity.status(HttpStatus.CREATED) 33 | .body(fileService.saveProblemImage(file)); 34 | } 35 | 36 | /** 37 | * 获取题目数据 38 | */ 39 | @GetMapping(path = "data/{pid}") 40 | public ResponseEntity getProblemData(@PathVariable Integer pid) { 41 | return ResponseEntity.ok(fileService.getProblemData(pid)); 42 | } 43 | 44 | /** 45 | * 上传测试数据 46 | */ 47 | @PostMapping(path = "data") 48 | public ResponseEntity uploadTestData(@RequestParam Integer pid, 49 | @RequestParam("file") MultipartFile file) { 50 | fileService.saveTestData(pid, file); 51 | return ResponseEntity.status(HttpStatus.CREATED).build(); 52 | } 53 | 54 | /** 55 | * 删除测试数据 56 | */ 57 | @DeleteMapping(path = "data/{pid}") 58 | public ResponseEntity deleteTestData(@PathVariable Integer pid, String name) { 59 | fileService.deleteTestData(pid, name); 60 | return ResponseEntity.noContent().build(); 61 | } 62 | 63 | /** 64 | * 上传 SPJ 代码 65 | */ 66 | @PostMapping(path = "spj") 67 | public ResponseEntity uploadSPJ(@RequestBody SPJ spj) { 68 | fileService.saveSPJ(spj); 69 | return ResponseEntity.ok().build(); 70 | } 71 | 72 | /** 73 | * 删除 SPJ 74 | */ 75 | @DeleteMapping(path = "spj/{pid}") 76 | public ResponseEntity deleteSPJ(@PathVariable Integer pid) { 77 | fileService.removeSPJ(pid); 78 | return ResponseEntity.noContent().build(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /web/src/views/components/Auth/Index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 90 | -------------------------------------------------------------------------------- /web/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from "@/store" 2 | import _axios, { AxiosError } from "axios" 3 | import { ErrorMessage } from "./type" 4 | 5 | const ApiPath = { 6 | LOGIN: "/api/auth/login", 7 | LOGOFF: "/api/auth/logoff", 8 | REFRESH_TOKEN: "/api/auth/refresh_token", 9 | VERIFY: "/api/auth/verify", 10 | IMAGE: "/api/core/file/img", 11 | AVATAR: "/api/core/file/img/avatar", 12 | TEST_DATA: "/api/core/file/data", 13 | SPJ: "/api/core/file/spj", 14 | PROBLEM_IMAGE: "/api/core/file/img/problem", 15 | PROBLEM_ADMIN: "/api/core/problem/admin", 16 | PROBLEM: "/api/core/problem", 17 | USER: "/api/core/user", 18 | USER_ADMIN: "/api/core/user/admin", 19 | CONTEST: "/api/core/contest", 20 | CONTEST_ADMIN: "/api/core/contest/admin", 21 | CONTEST_INVITATION: "/api/core/contest/invitation", 22 | CONTEST_PROBLEM: "/api/core/contest/problem", 23 | CONTEST_KEY: "/api/core/contest/admin/key", 24 | CONTEST_PROBLEM_ORDER: "/api/core/contest/admin/problem/order", 25 | CONTEST_RANKING: "/api/core/ranking/contest", 26 | CONTEST_RANKING_ADMIN: "/api/core/ranking/admin/contest", 27 | RANKING: "/api/core/ranking", 28 | RANKING_ADMIN: "/api/core/ranking/admin", 29 | SOLUTION: "/api/core/solution", 30 | SOLUTION_ADMIN: "/api/core/solution/admin", 31 | SUBMIT: "/api/judge/submit", 32 | ADMIN_SUBMIT: "/api/judge/admin/submit", 33 | QUEUE_INFO: "/api/judge/admin/queue_info", 34 | PROFILE: "/api/core/user/profile", 35 | PROFILE_ADMIN: "/api/core/user/admin/profile", 36 | OVERVIEW: "/api/core/user/overview", 37 | SETTINGS: "/api/core/settings", 38 | LOG: "/api/core/log" 39 | } 40 | 41 | function resolveError(error: any): ErrorMessage { 42 | const err = error as AxiosError 43 | if (err.response) { 44 | if ( 45 | err.response.data && 46 | (err.response.headers["content-type"] as string).includes( 47 | "application/json" 48 | ) 49 | ) { 50 | return ErrorMessage.from(err.response.data) 51 | } else { 52 | return new ErrorMessage(err.response.status, "发生了错误") 53 | } 54 | } else if (err.request) { 55 | return new ErrorMessage(0, "请求失败") 56 | } else { 57 | return new ErrorMessage(-1, err.message) 58 | } 59 | } 60 | 61 | const axios = _axios.create() 62 | 63 | // 请求拦截器 64 | axios.interceptors.request.use( 65 | (config) => { 66 | const user = useStore().user 67 | if (user.userInfo) { 68 | config.headers.Authorization = `Baerer ${user.userInfo.token}` 69 | } 70 | 71 | if (!config.headers["Content-Type"]) { 72 | config.headers["Content-Type"] = "application/json" 73 | } 74 | 75 | return config 76 | }, 77 | (error) => { 78 | return Promise.reject(error) 79 | } 80 | ) 81 | 82 | export default axios 83 | export { ApiPath, resolveError } 84 | -------------------------------------------------------------------------------- /judge/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "runner.h" 6 | #include "common.h" 7 | 8 | const char* RESULT_STR[] = { 9 | "", "AC", "TLE", "MLE", "PA", 10 | "WA", "CE", "RE", "IE", "OLE" 11 | }; 12 | 13 | int main(const int argc, char* argv[]) 14 | { 15 | char cmd[128]; 16 | char work_dir[128]; 17 | char data_dir[128]; 18 | Config config; 19 | std::stringstream ss; 20 | 21 | if (getuid() != 0) 22 | { 23 | ss << "{\n" 24 | << R"( "result": )" << IE << ",\n" 25 | << R"( "error": "RUN AS ROOT")" << "\n" 26 | << "}\n"; 27 | std::cout << ss.str(); 28 | return 0; 29 | } 30 | 31 | if (get_args(argc, argv, cmd, work_dir, data_dir, config) != 0) 32 | { 33 | ss << "{\n" 34 | << R"( "result": )" << IE << ",\n" 35 | << R"( "error": "INVALID ARGS")" << "\n" 36 | << "}\n"; 37 | std::cout << ss.str(); 38 | return 0; 39 | } 40 | 41 | RTN rtn = Runner(cmd, work_dir, data_dir, config).judge(); 42 | 43 | if (rtn.result == IE) 44 | { 45 | ss << "{\n" 46 | << R"( "result": )" << rtn.result << ",\n" 47 | << R"( "error": ")" << rtn.err << "\"\n" 48 | << "}\n"; 49 | } 50 | else 51 | { 52 | ss << "{\n" 53 | << R"( "result": )" << rtn.result << ",\n" 54 | << R"( "desc": ")" << RESULT_STR[rtn.result] << "\",\n"; 55 | 56 | if (strlen(rtn.err) > 0) 57 | { 58 | ss << R"( "error": ")" << rtn.err << "\",\n"; 59 | } 60 | 61 | ss << R"( "total": )" << rtn.total << ",\n" 62 | << R"( "passed": )" << rtn.passed << ",\n" 63 | << R"( "passRate": )" << rtn.passRate << ",\n" 64 | << R"( "time": )" << rtn.time << ",\n" 65 | << R"( "memory": )" << rtn.memory << ",\n"; 66 | 67 | if (!rtn.detail.empty()) 68 | { 69 | ss << R"( "detail": )" << "[ "; 70 | auto iter = rtn.detail.begin(); 71 | 72 | while (iter != rtn.detail.end()) 73 | { 74 | const auto index = iter->find_last_of('/'); 75 | auto name = iter->substr(index + 1, iter->length() - index - 1); 76 | ss << "\"" << name << "\""; 77 | ++iter; 78 | 79 | if (iter != rtn.detail.end()) 80 | { 81 | ss << ", "; 82 | } 83 | } 84 | 85 | ss << " ]\n"; 86 | } 87 | else 88 | { 89 | ss << R"( "detail": [])" << "\n"; 90 | } 91 | 92 | ss << "}\n"; 93 | } 94 | 95 | std::cout << ss.str(); 96 | return 0; 97 | } 98 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/controller/SolutionController.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.controller; 2 | 3 | import cloud.oj.core.entity.PageData; 4 | import cloud.oj.core.entity.Solution; 5 | import cloud.oj.core.entity.SolutionFilter; 6 | import cloud.oj.core.service.SolutionService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; 11 | 12 | /** 13 | * 提交记录/判题结果接口 14 | */ 15 | @RestController 16 | @RequestMapping("solution") 17 | @RequiredArgsConstructor 18 | public class SolutionController { 19 | 20 | private final SolutionService solutionService; 21 | 22 | /** 23 | * (User) 根据过滤条件获取提交记录 24 | */ 25 | @GetMapping 26 | public ResponseEntity> getAll(@RequestHeader Integer uid, 27 | @RequestParam(defaultValue = "1") int page, 28 | @RequestParam(defaultValue = "15") int size, 29 | Integer filter, 30 | String filterValue) { 31 | var data = solutionService.getSolutionsByUidAndFilter(uid, page, size, filter, filterValue); 32 | return data.getTotal() > 0 ? 33 | ResponseEntity.ok(data) : 34 | ResponseEntity.noContent().build(); 35 | } 36 | 37 | /** 38 | * (Admin) 根据过滤条件获取提交记录 39 | */ 40 | @RequestMapping(path = "admin/queries", method = {RequestMethod.GET, RequestMethod.POST}) 41 | public ResponseEntity> getAllByFilter( 42 | @RequestParam(defaultValue = "1") int page, 43 | @RequestParam(defaultValue = "15") int size, 44 | @RequestBody(required = false) SolutionFilter filter 45 | ) { 46 | var data = solutionService.getSolutionsByAdmin(filter, page, size); 47 | return data.getTotal() > 0 ? 48 | ResponseEntity.ok(data) : 49 | ResponseEntity.noContent().build(); 50 | } 51 | 52 | @GetMapping("admin/{sid}") 53 | public ResponseEntity getSolutionById(@PathVariable String sid) { 54 | return ResponseEntity.ok(solutionService.getSolutionById(sid)); 55 | } 56 | 57 | /** 58 | * 获取判题结果 59 | */ 60 | @GetMapping("time/{time}") 61 | public SseEmitter getByUidAndTime(@RequestHeader Integer uid, @PathVariable Long time) { 62 | return solutionService.getSolutionByUidAndTime(uid, time); 63 | } 64 | 65 | @GetMapping("{sid}") 66 | public ResponseEntity getBySolutionByUser(@RequestHeader Integer uid, @PathVariable String sid) { 67 | return ResponseEntity.ok(solutionService.getSolutionByUser(uid, sid)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /services/gateway/src/main/java/cloud/oj/gateway/filter/JwtUtil.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.gateway.filter; 2 | 3 | import cloud.oj.gateway.entity.User; 4 | import com.fasterxml.jackson.core.JacksonException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import io.jsonwebtoken.Claims; 7 | import io.jsonwebtoken.JwtException; 8 | import io.jsonwebtoken.Jwts; 9 | import io.jsonwebtoken.security.Keys; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import javax.crypto.SecretKey; 13 | import java.util.Base64; 14 | import java.util.Date; 15 | 16 | @Slf4j 17 | public class JwtUtil { 18 | private final static ObjectMapper mapper = new ObjectMapper(); 19 | 20 | private static SecretKey stringToSecretKey(String secret) { 21 | return Keys.hmacShaKeyFor(secret.getBytes()); 22 | } 23 | 24 | public static String createJwt(User user, Object authorities, Integer validTime) { 25 | var now = System.currentTimeMillis(); 26 | var expire = now + validTime * 3600000L; 27 | 28 | return Jwts.builder().issuer("Cloud OJ") 29 | .issuedAt(new Date(now)) 30 | .expiration(new Date(expire)) 31 | .claim("uid", user.getUid()) 32 | .claim("username", user.getUsername()) 33 | .claim("nickname", user.getNickname()) 34 | .claim("email", user.getEmail()) 35 | .claim("section", user.getSection()) 36 | .claim("hasAvatar", user.getHasAvatar()) 37 | .claim("role", user.getRole()) 38 | .claim("authorities", authorities) 39 | .signWith(stringToSecretKey(user.getSecret())) 40 | .compact(); 41 | } 42 | 43 | /** 44 | * 验证并返回 JWT Body 45 | * 46 | * @return {@link Claims} 47 | */ 48 | public static Claims getClaims(String jwt, String secret) 49 | throws JwtException, IllegalArgumentException { 50 | return Jwts.parser() 51 | .verifyWith(stringToSecretKey(secret)) 52 | .build() 53 | .parseSignedClaims(jwt) 54 | .getPayload(); 55 | } 56 | 57 | /** 58 | * 从 JWT 中取出 uid 59 | *

此操作不验证 JWT 签名

60 | * 61 | * @return uid or null 62 | */ 63 | public static Integer getUid(String jwt) { 64 | // JWT 是 Base64Url 编码 65 | var base64 = jwt.substring(jwt.indexOf('.') + 1, jwt.lastIndexOf('.')) 66 | .replaceAll("-", "+") 67 | .replaceAll("_", "/"); 68 | 69 | var payload = new String(Base64.getDecoder().decode(base64)); 70 | 71 | try { 72 | var node = mapper.readTree(payload); 73 | return node.get("uid").intValue(); 74 | } catch (JacksonException e) { 75 | log.error("Cannot read uid from token"); 76 | return null; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /services/core/src/main/java/cloud/oj/core/repo/UserStatisticRepo.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.core.repo; 2 | 3 | import cloud.oj.core.entity.AcCount; 4 | import cloud.oj.core.entity.Language; 5 | import cloud.oj.core.entity.Results; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.jdbc.core.simple.JdbcClient; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import java.util.List; 11 | 12 | @Repository 13 | @RequiredArgsConstructor 14 | public class UserStatisticRepo { 15 | 16 | private final JdbcClient client; 17 | 18 | public List selectLanguages(Integer uid) { 19 | return client.sql(""" 20 | select language, count(solution_id) as count 21 | from solution 22 | where contest_id is null 23 | and uid = :uid 24 | group by language 25 | """) 26 | .param("uid", uid) 27 | .query(Language.class) 28 | .list(); 29 | } 30 | 31 | public List selectActivities(Integer uid, Integer year) { 32 | return client.sql(""" 33 | select problem_id as pid, 34 | DATE_FORMAT(from_unixtime(submit_time / 1000), '%Y-%m-%d') as date, 35 | count(distinct problem_id, language) as count 36 | from solution 37 | where contest_id is null 38 | and uid = :uid 39 | and year(from_unixtime(submit_time / 1000)) = :year 40 | and result = 'AC' 41 | group by problem_id, date 42 | order by date 43 | """) 44 | .param("uid", uid) 45 | .param("year", year) 46 | .query(AcCount.class) 47 | .list(); 48 | } 49 | 50 | public Results selectResults(Integer uid) { 51 | return client.sql(""" 52 | select count(result = 'AC' or null) as AC, 53 | count(result = 'WA' or null) as WA, 54 | count(result = 'TLE' or null) as TLE, 55 | count(result = 'MLE' or null) as MLE, 56 | count(result = 'RE' or null) as RE, 57 | count(result = 'CE' or null) as CE, 58 | count(solution_id) as total 59 | from solution 60 | where uid = :uid 61 | and contest_id is null 62 | and result <> 'JUDGE_ERROR' 63 | """) 64 | .param("uid", uid) 65 | .query(Results.class) 66 | .single(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /services/judge/src/main/java/cloud/oj/judge/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package cloud.oj.judge.config; 2 | 3 | import lombok.Getter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.ApplicationContext; 8 | 9 | import java.io.File; 10 | import java.util.*; 11 | 12 | @Slf4j 13 | @Getter 14 | @ConfigurationProperties("app") 15 | public class AppConfig { 16 | private final ApplicationContext applicationContext; 17 | 18 | private final String fileDir; 19 | 20 | private final String codeDir; 21 | 22 | private final String judgeCpus; 23 | 24 | private List cpus; 25 | 26 | public AppConfig(ApplicationContext context, String fileDir, String judgeCpus) { 27 | var home = System.getProperty("user.home"); 28 | this.applicationContext = context; 29 | this.judgeCpus = Optional.ofNullable(judgeCpus).orElse("1"); 30 | 31 | if (fileDir == null) { 32 | this.fileDir = home + "/.local/cloud-oj/"; 33 | } else if (!fileDir.endsWith("/")) { 34 | this.fileDir = fileDir + "/"; 35 | } else { 36 | this.fileDir = fileDir; 37 | } 38 | 39 | this.codeDir = this.fileDir + "code/"; 40 | 41 | createDir(this.fileDir + "data"); 42 | createDir(this.codeDir); 43 | configCpus(); 44 | 45 | log.info("数据文件目录: {}", this.fileDir); 46 | log.info("临时代码目录: {}", this.codeDir); 47 | } 48 | 49 | private void configCpus() { 50 | var cpuList = Arrays.stream(judgeCpus.split(",")).map(Integer::valueOf).toList(); 51 | var availableCpus = Runtime.getRuntime().availableProcessors(); 52 | 53 | // 只有一个元素,表示 CPU 数量 54 | if (cpuList.size() == 1) { 55 | var count = cpuList.get(0); 56 | cpus = new ArrayList<>(count); 57 | 58 | if (count >= availableCpus || count <= 0) { 59 | count = availableCpus; 60 | } 61 | 62 | for (int i = 0; i < count; i++) { 63 | cpus.add(i); 64 | } 65 | } else { 66 | // 有多个元素,每个对应一个 CPU 67 | var max = cpuList.stream().max(Comparator.comparing(Integer::intValue)); 68 | var min = cpuList.stream().min(Comparator.comparing(Integer::intValue)); 69 | 70 | if (max.isEmpty() || max.get() >= availableCpus || min.get() < 0) { 71 | log.error("Bad value: judge-cpus"); 72 | SpringApplication.exit(applicationContext, () -> -1); 73 | } 74 | 75 | cpus = cpuList; 76 | } 77 | } 78 | 79 | private void createDir(String path) { 80 | File dir = new File(path); 81 | 82 | if (!dir.exists() && !dir.mkdirs()) { 83 | log.error("创建目录失败: {}", path); 84 | SpringApplication.exit(applicationContext, () -> -1); 85 | } 86 | } 87 | } 88 | --------------------------------------------------------------------------------