├── 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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/services/core/src/main/java/cloud/oj/core/entity/DataConf.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.Map;
9 |
10 | @Setter
11 | @Getter
12 | @AllArgsConstructor
13 | @NoArgsConstructor
14 | public class DataConf {
15 | private Integer problemId;
16 | private Map conf;
17 | }
18 |
--------------------------------------------------------------------------------
/services/gateway/src/main/java/cloud/oj/gateway/error/GenericException.java:
--------------------------------------------------------------------------------
1 | package cloud.oj.gateway.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/constant/State.java:
--------------------------------------------------------------------------------
1 | package cloud.oj.judge.constant;
2 |
3 | import java.lang.annotation.Retention;
4 | import java.lang.annotation.RetentionPolicy;
5 |
6 | /**
7 | * 对应 solution 表中的 state 字段
8 | * MariaDB 的枚举值从 1 开始
9 | */
10 | @Retention(RetentionPolicy.SOURCE)
11 | public @interface State {
12 | int JUDGED = 1;
13 | int RUNNING = 2;
14 | int COMPILING = 3;
15 | int WAITING = 4;
16 | }
17 |
--------------------------------------------------------------------------------
/judge/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.16)
2 | project(judge)
3 |
4 | set(CMAKE_CXX_STANDARD 17)
5 | set(BIN_DIR /usr/local/bin)
6 |
7 | if (${CMAKE_BUILD_TYPE} STREQUAL "Debug")
8 | add_definitions(-DDEBUG)
9 | endif ()
10 |
11 | include_directories(
12 | include
13 | )
14 |
15 | aux_source_directory(src JUDGE)
16 | add_executable(judge ${JUDGE})
17 |
18 | install(
19 | TARGETS judge
20 | RUNTIME DESTINATION ${BIN_DIR}
21 | )
22 |
--------------------------------------------------------------------------------
/services/core/src/main/java/cloud/oj/core/entity/RankingContest.java:
--------------------------------------------------------------------------------
1 | package cloud.oj.core.entity;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 |
6 | import java.util.List;
7 |
8 | @Getter
9 | @Setter
10 | public class RankingContest {
11 | private Contest contest;
12 | private List problemIds;
13 | private List ranking;
14 |
15 | public RankingContest(Contest contest) {
16 | this.contest = contest;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/web/src/views/components/Error.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
6 |
7 |
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 |
5 |
6 |
7 |
8 |
9 |
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 |
5 |
6 |
7 |
8 |
9 |
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 |
2 |
3 |
4 |
5 | 没有数据
6 |
7 |
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 |
2 |
3 |
4 |
5 |
6 |
10 | {{ LanguageNames[language] }}
11 |
12 |
13 |
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 `
`
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 |
2 |
3 |
4 | 找不到页面了...
5 |
6 |
7 |
8 |
9 | 返回上页
10 |
11 |
12 |
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 |
2 |
3 |
4 |
5 | C
6 |
7 |
8 | Cloud
9 | OJ
10 |
11 |
12 |
13 |
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 |
2 |
10 |
11 | © Cloud
12 |
13 |
14 |
19 |
20 |
21 |
22 |
23 |
24 | GitHub
25 |
26 |
27 | MIT License
28 |
29 |
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 |
2 |
3 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 | 
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 |
2 |
9 |
10 |
15 | {{ nickname.slice(-1).toUpperCase() }}
16 |
17 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
4 |
5 | {{ errorText }}
6 |
7 |
8 |
9 |
10 | 返回上页
11 |
12 |
13 |
14 |
15 |
38 |
--------------------------------------------------------------------------------
/web/src/components/ResultTag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ text }}
7 |
8 |
9 |
10 |
45 |
--------------------------------------------------------------------------------
/.run/JudgeApp.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
2 |
3 |
8 |
9 |
10 | {{ user.nickname }}
11 |
12 | 修改个人信息
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ user.realName }}@{{ user.username }}
22 |
23 |
24 |
25 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
2 |
3 | 最近记录
4 |
5 |
6 |
13 |
14 |
15 |
17 |
18 | {{ `${s.problemId}.${s.title}` }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 extends GrantedAuthority> 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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
38 |
39 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
2 |
3 |
7 |
8 |
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 |
2 |
3 |
结果统计
4 |
5 |
6 |
10 |
18 |
19 | {{ names[index] }}
20 |
21 |
22 |
23 |
30 |
31 | {{ acRate }}%
正确率
32 |
33 |
34 |
35 |
36 |
37 |
38 |
78 |
--------------------------------------------------------------------------------
/web/src/views/components/Admin/Overview/QueuesInfoView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | | 待分发 |
9 |
10 |
11 | {{ value.messages_ready }}
12 | |
13 |
14 |
15 | | 未确认 |
16 |
17 |
18 | {{ value.messages_unacknowledged }}
19 | |
20 |
21 |
22 | | 消息总数 |
23 |
24 |
25 | {{ value.messages }}
26 | |
27 |
28 |
29 | | 消费者 |
30 | {{ value.consumers }} |
31 |
32 |
33 |
34 |
37 |
38 |
39 |
40 |
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 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 | 
10 | 
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 |
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 |
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