command = Arrays.asList(
"docker", "run", "--rm",
"-v", hostFileDir + ":" + mountPath,
"--memory", languageEnum.getMemoryLimit(),
imageName,
execCmd
);
ProcessBuilder builder = new ProcessBuilder(command);
// 合并标准输出与标准错误
builder.redirectErrorStream(true);
long startTimeMillis = System.currentTimeMillis();
Process process = builder.start();
// 读取进程输出
StringBuilder fullOutputBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
fullOutputBuilder.append(line).append("\n");
}
}
int exitCode = process.waitFor();
long endTimeMillis = System.currentTimeMillis();
String fullOutput = fullOutputBuilder.toString().trim();
// 匹配内存、耗时信息
Matcher memMatcher = MEM_PATTERN.matcher(fullOutput);
Matcher timeMatcher = TIME_PATTERN.matcher(fullOutput);
String memoryUsage = memMatcher.find() ? memMatcher.group(1) : "-1";
String timeUsed = timeMatcher.find()
? timeMatcher.group(1)
: String.valueOf((endTimeMillis - startTimeMillis) / 1000.0);
int statIndex = fullOutput.indexOf("Command being timed:");
String trimmedOutput = (statIndex != -1)
? fullOutput.substring(0, statIndex).trim()
: fullOutput;
log.info("Exit code: {}", exitCode);
log.info("Raw output:\n{}", fullOutput);
String status = ExecuteMessage.getStatus(exitCode);
if (answer != null && ExecuteMessage.getStatus(exitCode).equals("Finished")) {
// 比较输出与答案
boolean isCorrect = trimmedOutput.equals(answer.trim());
if (isCorrect) {
status = "Accepted";
exitCode = 10;
} else {
status = "Wrong Answer";
exitCode = 11;
trimmedOutput += "\n```\nExpected: " + answer.trim();
}
}
return new ExecuteMessage()
.setExitValue(exitCode)
.setStatus(status)
.setMessage(ExecuteMessage.show(exitCode) ? trimmedOutput : "")
.setTime(Double.parseDouble(timeUsed))
.setMemory(Long.parseLong(memoryUsage));
} catch (Exception e) {
log.error("运行沙箱代码出错", e);
return new ExecuteMessage()
.setExitValue(1)
.setStatus("Runtime Error")
.setMessage("执行代码时发生错误: " + e.getMessage());
}
}
}
================================================
FILE: DOJ-BE/sandbox-service/src/main/java/com/decade/doj/sandbox/worker/JudgingWorker.java
================================================
package com.decade.doj.sandbox.worker;
import com.alibaba.fastjson.JSON;
import com.decade.doj.sandbox.domain.vo.JudgingTask;
import com.decade.doj.sandbox.service.ISandboxService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 判题任务消费者
*
* 使用单一调度线程从 Redis 队列轮询任务,并将任务分发到 JudgingThreadPool 执行。
* 实现 DisposableBean 接口支持优雅关闭。
*/
@Slf4j
@Component
public class JudgingWorker implements ApplicationRunner, DisposableBean {
private final StringRedisTemplate redisTemplate;
private final ISandboxService sandboxService;
private final ThreadPoolTaskExecutor judgingExecutor;
private static final String JUDGING_QUEUE_KEY = "judging:queue";
private static final int POLL_TIMEOUT_SECONDS = 5;
private final AtomicBoolean running = new AtomicBoolean(true);
private Thread schedulerThread;
public JudgingWorker(
StringRedisTemplate redisTemplate,
ISandboxService sandboxService,
@Qualifier("JudgingThreadPool") ThreadPoolTaskExecutor judgingExecutor
) {
this.redisTemplate = redisTemplate;
this.sandboxService = sandboxService;
this.judgingExecutor = judgingExecutor;
}
@Override
public void run(ApplicationArguments args) {
schedulerThread = new Thread(this::pollAndDispatch, "JudgingScheduler");
schedulerThread.start();
log.info("判题调度器已启动,任务将分发到 JudgingThreadPool 执行");
}
/**
* 轮询 Redis 队列并分发任务到线程池
*/
private void pollAndDispatch() {
while (running.get()) {
try {
String taskJson = redisTemplate.opsForList()
.rightPop(JUDGING_QUEUE_KEY, POLL_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (taskJson != null) {
JudgingTask task = JSON.parseObject(taskJson, JudgingTask.class);
log.info("收到判题任务: submissionId={}, problemId={}",
task.getSubmissionId(), task.getProblemId());
// 分发到线程池异步执行
judgingExecutor.execute(() -> executeTask(task));
}
} catch (Exception e) {
if (running.get()) {
log.error("轮询判题队列出现异常", e);
// 短暂休眠避免错误风暴
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
log.info("判题调度器已停止轮询");
}
/**
* 执行单个判题任务
*/
private void executeTask(JudgingTask task) {
try {
log.debug("开始执行判题任务: submissionId={}", task.getSubmissionId());
sandboxService.execute(task);
log.debug("判题任务执行完成: submissionId={}", task.getSubmissionId());
} catch (Exception e) {
log.error("执行判题任务异常: submissionId={}, problemId={}",
task.getSubmissionId(), task.getProblemId(), e);
}
}
@Override
public void destroy() {
log.info("正在关闭判题调度器...");
running.set(false);
if (schedulerThread != null) {
schedulerThread.interrupt();
try {
// 等待调度线程终止
schedulerThread.join(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
log.info("判题调度器已关闭,线程池将由 Spring 容器管理关闭");
}
}
================================================
FILE: DOJ-BE/sandbox-service/src/main/resources/application.yaml
================================================
server:
port: ${doj.port.sandbox-service}
management:
endpoints:
web:
exposure:
include: health,prometheus
doj:
mq:
host: ${DOJ_MQ_HOST:127.0.0.1}
redis:
host: ${DOJ_REDIS_HOST:127.0.0.1}
swagger:
title: 沙箱接口文档
scan: com.decade.doj.sandbox.controller
================================================
FILE: DOJ-BE/sandbox-service/src/main/resources/bootstrap.yaml
================================================
spring:
application:
name: sandbox-service
profiles:
active: dev, common
cloud:
nacos:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
config:
file-extension: yaml
shared-configs:
- dataId: shared-jdbc.yaml
- dataId: shared-swagger.yaml
- dataId: shared-jwt.yaml
- dataId: shared-rabbitmq.yaml
================================================
FILE: DOJ-BE/sandbox-service/src/main/resources/test/20240906.py
================================================
ans = []
res = 0
for i in range(10000000):
ans.append(i)
res += i
print(f"{res} test success nopt failed!")
================================================
FILE: DOJ-BE/sandbox-service/src/main/resources/test/main.cpp
================================================
#include
#include
using namespace std;
int main() {
long long ans = 0;
vector v;
for (int i = 0; i < 10000000; i++) {
ans += i;
v.push_back(i);
}
cout << ans << " test success!" << endl;
return 0;
}
================================================
FILE: DOJ-BE/sandbox-service/src/main/resources/test/main.java
================================================
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
long ans = 0;
List res = new ArrayList<>();
for (int i = 0;i < 10000000;i++){
ans += i;
res.add(i);
}
System.out.println(ans+" test success!");
}
}
================================================
FILE: DOJ-BE/sandbox-service/src/test/java/com/decade/doj/sandbox/SandboxTest.java
================================================
package com.decade.doj.sandbox;
import com.decade.doj.sandbox.domain.vo.ExecuteMessage;
import com.decade.doj.sandbox.service.ISandboxService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class SandboxTest {
@Autowired
private ISandboxService sandboxService;
@Test
public void testRunCode() {
String filePath = "/Users/qzj/Desktop/Development/D-OnlineJudge/static/docker-sandbox/python/Main.py"; // 替换为实际的文件路径
String filename = "Main.py"; // 替换为实际的文件名
String lang = "python";
// try {
// ExecuteMessage res = sandboxService.runCodeInSandbox(filePath, filename, lang);
// System.out.println(res);
// } catch (Exception e) {
// e.printStackTrace();
// }
}
}
================================================
FILE: DOJ-BE/submission-service/Dockerfile
================================================
# Stage 1: Build the application
FROM maven:3.8.4-openjdk-17 AS build
WORKDIR /app
# Copy all pom.xml files first to leverage Docker cache
COPY DOJ-BE/pom.xml .
COPY DOJ-BE/common/pom.xml ./common/
COPY DOJ-BE/gateway-service/pom.xml ./gateway-service/
COPY DOJ-BE/problem-service/pom.xml ./problem-service/
COPY DOJ-BE/sandbox-service/pom.xml ./sandbox-service/
COPY DOJ-BE/submission-service/pom.xml ./submission-service/
COPY DOJ-BE/user-service/pom.xml ./user-service/
# Download all dependencies
RUN mvn dependency:go-offline
# Copy all source code
COPY DOJ-BE/common/src ./common/src
COPY DOJ-BE/submission-service/src ./submission-service/src
# Build the specific service
RUN mvn -f ./pom.xml -pl submission-service -am clean package -DskipTests
RUN ls -al ./submission-service/target
# Stage 2: Create the runtime image
FROM eclipse-temurin:17-jre
WORKDIR /app
# Copy the executable jar from the build stage
COPY --from=build /app/submission-service/target/submission-service.jar ./app.jar
EXPOSE 8084
ENTRYPOINT ["java", "-jar", "./app.jar"]
================================================
FILE: DOJ-BE/submission-service/pom.xml
================================================
4.0.0
com.decade
DOJ-BE
1.0-SNAPSHOT
com.decade.doj
submission-service
17
17
UTF-8
com.decade
common
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
com.baomidou
mybatis-plus-boot-starter
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-websocket
${project.artifactId}
org.springframework.boot
spring-boot-maven-plugin
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/SubmissionApplication.java
================================================
package com.decade.doj.submission;
import com.decade.doj.common.config.custom.DefaultFeignConfig;
import com.decade.doj.common.config.custom.JwtTool;
import com.decade.doj.common.config.custom.MVCConfig;
import com.decade.doj.common.config.custom.MybatisConfig;
import com.decade.doj.common.interceptor.AdminCheckInterceptor;
import com.decade.doj.common.interceptor.IdentityInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@MapperScan("com.decade.doj.submission.mapper")
@EnableFeignClients(basePackages = "com.decade.doj.common.client", defaultConfiguration = DefaultFeignConfig.class)
@Import({JwtTool.class, MVCConfig.class, MybatisConfig.class, IdentityInterceptor.class, AdminCheckInterceptor.class})
public class SubmissionApplication {
public static void main(String[] args) {
SpringApplication.run(SubmissionApplication.class, args);
}
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/config/MqConfig.java
================================================
package com.decade.doj.submission.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqConfig {
@Bean
public TopicExchange topicExchange() {
return new TopicExchange("doj.topic");
}
@Bean
public Queue judgingResultQueue() {
return new Queue("judging.result.queue");
}
@Bean
public Binding binding() {
return BindingBuilder.bind(judgingResultQueue()).to(topicExchange()).with("judging.result");
}
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/config/WebSocketConfig.java
================================================
package com.decade.doj.submission.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
// 让 @ServerEndpoint 生效
return new ServerEndpointExporter();
}
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/controller/SubmissionController.java
================================================
package com.decade.doj.submission.controller;
import com.decade.doj.common.domain.PageDTO;
import com.decade.doj.common.domain.R;
import com.decade.doj.common.domain.vo.ExecuteMessage;
import com.decade.doj.common.domain.vo.SubmissionStatsVO;
import com.decade.doj.common.utils.UserContext;
import com.decade.doj.submission.domain.dto.SubmissionPageQueryDTO;
import com.decade.doj.submission.domain.po.Submission;
import com.decade.doj.submission.service.ISubmissionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/submission")
@Tag(name = "提交相关接口")
@Slf4j
@RequiredArgsConstructor
public class SubmissionController {
private final ISubmissionService submissionService;
@PostMapping("/submit")
@Operation(summary = "提交记录")
public R submit(@RequestBody Submission submission) {
submissionService.save(submission);
return R.ok(submission.getId());
}
@GetMapping("/page")
@Operation(summary = "分页获取提交列表")
public R> page(SubmissionPageQueryDTO problemPageQueryDTO) {
PageDTO res = submissionService.pageQuery(problemPageQueryDTO);
return R.ok(res);
}
@GetMapping("/stats")
@Operation(summary = "获取提交统计")
public R getStats() {
return R.ok(submissionService.getStats());
}
@GetMapping("/match/{id}")
@Operation(summary = "获取当前用户指定问题的提交详情")
public R getById(@PathVariable String id) {
List submissions = submissionService.lambdaQuery()
.eq(Submission::getProblemId, id)
.list();
int f = 0;
for (Submission submission : submissions) {
if (submission.getUserId() != null && submission.getUserId().equals(UserContext.getCurrentUser())) {
f = 1;
if (ExecuteMessage.getStatus(submission.getExitValue()).equals("Accepted")) {
return R.ok(1); // 已经提交过且通过
}
}
}
if (f == 1) {
return R.ok(2); // 已经提交过但未通过
}
return R.ok(0);
}
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/domain/dto/SubmissionPageQueryDTO.java
================================================
package com.decade.doj.submission.domain.dto;
import com.decade.doj.common.domain.PageQueryDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class SubmissionPageQueryDTO extends PageQueryDTO {
@Schema(description = "提交ID")
private Long submissionId;
@Schema(description = "用户ID")
private String userId;
@Schema(description = "题目ID")
private String problemId;
@Schema(description = "语言")
private String language;
@Schema(description = "判题状态")
private String status;
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/domain/po/Submission.java
================================================
package com.decade.doj.submission.domain.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.Data;
/**
* 代码提交记录表
* @TableName submission
*/
@TableName(value ="submission")
@Data
public class Submission {
/**
* 提交记录主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户ID(来源于 doj_user.user)
*/
private Long userId;
/**
* 题目ID(来源于 doj_problem.problem)
*/
private Long problemId;
private String userName;
private String problemName;
/**
* 编程语言
*/
private String language;
/**
* 提交的代码文本内容
*/
private String code;
/**
* 程序退出码
*/
private Integer exitValue;
/**
* 判题状态
*/
private String status;
/**
* 判题详细信息
*/
private String message;
/**
* 运行时间(单位:秒)
*/
private Double time;
/**
* 内存使用(单位:KB)
*/
private Long memory;
/**
* 提交时间
*/
private Date submitTime;
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/mapper/SubmissionMapper.java
================================================
package com.decade.doj.submission.mapper;
import com.decade.doj.submission.domain.po.Submission;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author qzj
* @description 针对表【submission(代码提交记录表)】的数据库操作Mapper
* @createDate 2025-06-05 13:28:52
* @Entity com.decade.doj.submission.domain.po.Submission
*/
public interface SubmissionMapper extends BaseMapper {
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/mq/ResultListener.java
================================================
package com.decade.doj.submission.mq;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.decade.doj.common.client.ProblemClient;
import com.decade.doj.common.client.UserClient;
import com.decade.doj.common.domain.po.Problem;
import com.decade.doj.common.domain.po.User;
import com.decade.doj.common.utils.UserContext;
import com.decade.doj.submission.domain.po.Submission;
import com.decade.doj.submission.service.ISubmissionService;
import com.decade.doj.submission.websocket.SubmissionWSServer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class ResultListener {
private final ISubmissionService submissionService;
private final ProblemClient problemClient;
private final UserClient userClient;
private final RabbitTemplate rabbitTemplate;
@RabbitListener(queues = "judging.result.queue")
public void onMessage(Map message) {
log.info("从 RabbitMQ 收到判题结果消息: {}", message);
try {
Long submissionId = Long.valueOf(message.get("submissionId").toString());
Map executeMessage = (Map) message.get("executeMessage");
if (executeMessage == null) {
log.error("消息格式错误,缺少 submissionId 或 executeMessage");
return;
}
// 1. 更新数据库
Submission submission = submissionService.getById(submissionId);
UserContext.setCurrentUser(submission.getUserId());
User user = userClient.getUser(submission.getUserId()).getData();
if (user == null) {
log.error("用户 {} 不存在,无法更新提交记录", submission.getUserId());
return;
}
submission.setUserName(user.getUsername());
Problem problem = problemClient.getProblemById(submission.getProblemId()).getData();
if (problem == null) {
log.error("题目 {} 不存在,无法更新提交记录", submission.getProblemId());
return;
}
submission.setProblemName(problem.getName());
submission.setId(submissionId);
submission.setStatus((String) executeMessage.get("status"));
submission.setExitValue((Integer) executeMessage.get("exitValue"));
submission.setMessage((String) executeMessage.get("message"));
submission.setTime((Double) executeMessage.get("time"));
submission.setMemory(((Number) executeMessage.get("memory")).longValue());
submissionService.updateById(submission);
log.info("提交记录 {} 已更新", submissionId);
// 2. 通过 WebSocket 推送给前端
SubmissionWSServer.sendMessage(submissionId, JSON.toJSONString(submission));
// 3. 发送 RabbitMQ 事件,用于更新用户和题目统计
Map submissionMessage = Map.of(
"problemId", submission.getProblemId(),
"isAccepted", "Accepted".equals(submission.getStatus())
);
rabbitTemplate.convertAndSend("doj.topic", "submission.created", submissionMessage);
if ("Accepted".equals(submission.getStatus())) {
long acCount = submissionService.lambdaQuery()
.eq(Submission::getUserId, submission.getUserId())
.eq(Submission::getProblemId, submission.getProblemId())
.eq(Submission::getStatus, "Accepted")
.count();
if (acCount == 1) {
Map solvedMessage = Map.of(
"userId", submission.getUserId(),
"problemId", submission.getProblemId()
);
rabbitTemplate.convertAndSend("doj.topic", "problem.solved", solvedMessage);
log.info("用户 {} 首次 AC 题目 {},已发送 problem.solved 事件", submission.getUserId(), submission.getProblemId());
}
}
} catch (Exception e) {
log.error("处理判题结果消息时发生异常", e);
}
}
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/service/ISubmissionService.java
================================================
package com.decade.doj.submission.service;
import com.decade.doj.common.domain.PageDTO;
import com.decade.doj.common.domain.vo.SubmissionStatsVO;
import com.decade.doj.submission.domain.dto.SubmissionPageQueryDTO;
import com.decade.doj.submission.domain.po.Submission;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @author qzj
* @description 针对表【submission(代码提交记录表)】的数据库操作Service
* @createDate 2025-06-05 13:28:52
*/
public interface ISubmissionService extends IService {
PageDTO pageQuery(SubmissionPageQueryDTO submissionPageQueryDTO);
SubmissionStatsVO getStats();
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/service/impl/ISubmissionServiceImpl.java
================================================
package com.decade.doj.submission.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.decade.doj.common.domain.PageDTO;
import com.decade.doj.common.domain.vo.SubmissionStatsVO;
import com.decade.doj.submission.domain.dto.SubmissionPageQueryDTO;
import com.decade.doj.submission.domain.po.Submission;
import com.decade.doj.submission.service.ISubmissionService;
import com.decade.doj.submission.mapper.SubmissionMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @author qzj
* @description 针对表【submission(代码提交记录表)】的数据库操作Service实现
* @createDate 2025-06-05 13:28:52
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class ISubmissionServiceImpl extends ServiceImpl
implements ISubmissionService {
public PageDTO pageQuery(SubmissionPageQueryDTO submissionPageQueryDTO) {
// 如果 submissionId 存在,则执行精确查询
if (submissionPageQueryDTO.getSubmissionId() != null) {
Submission submission = this.getById(submissionPageQueryDTO.getSubmissionId());
if (submission == null) {
return PageDTO.empty(0L, 0L);
}
return PageDTO.fullPage(1L, 1L, List.of(submission));
}
log.info("分页查询提交列表: {}", submissionPageQueryDTO);
log.info("userId={}, problemId={}, status={}, language={}",
submissionPageQueryDTO.getUserId(),
submissionPageQueryDTO.getProblemId(),
submissionPageQueryDTO.getStatus(),
submissionPageQueryDTO.getLanguage());
String user = submissionPageQueryDTO.getUserId();
String problem = submissionPageQueryDTO.getProblemId();
Page submissionList = lambdaQuery()
.like(user != null && !user.isBlank(), Submission::getUserName, submissionPageQueryDTO.getUserId())
.like(problem != null && !problem.isBlank(), Submission::getProblemName, submissionPageQueryDTO.getProblemId())
.eq(submissionPageQueryDTO.getStatus() != null, Submission::getStatus, submissionPageQueryDTO.getStatus())
.eq(submissionPageQueryDTO.getLanguage() != null, Submission::getLanguage, submissionPageQueryDTO.getLanguage())
.page(submissionPageQueryDTO.toMpPage("submit_time", false));
return PageDTO.fullPage(submissionList.getTotal(), submissionList.getPages(), submissionList.getRecords());
}
@Override
public SubmissionStatsVO getStats() {
// 获取总提交数
long totalSubmissions = this.count();
// 获取今日提交数
LocalDate today = LocalDate.now();
Date startOfDay = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
long todaySubmissions = this.lambdaQuery()
.ge(Submission::getSubmitTime, startOfDay)
.count();
return new SubmissionStatsVO(totalSubmissions, todaySubmissions);
}
}
================================================
FILE: DOJ-BE/submission-service/src/main/java/com/decade/doj/submission/websocket/SubmissionWSServer.java
================================================
package com.decade.doj.submission.websocket;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@ServerEndpoint("/ws/submission")
public class SubmissionWSServer {
private static final Map ONLINE_SESSIONS = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session) {
log.info("WebSocket 连接已建立: {}", session.getId());
}
@OnMessage
public void onMessage(String message, Session session) {
try {
Map data = JSON.parseObject(message);
Long submissionId = Long.valueOf(data.get("submissionId").toString());
ONLINE_SESSIONS.put(submissionId, session);
log.info("submissionId: {} 已订阅 WebSocket 通知", submissionId);
} catch (Exception e) {
log.error("处理 WebSocket 消息时出错: {}", message, e);
}
}
@OnClose
public void onClose(Session session) {
// 清理无效的 session
ONLINE_SESSIONS.values().removeIf(s -> !s.isOpen());
log.info("WebSocket 连接已关闭: {}", session.getId());
}
public static void sendMessage(Long submissionId, String message) {
Session session = ONLINE_SESSIONS.get(submissionId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
log.info("成功向 submissionId: {} 推送消息", submissionId);
// 推送成功后可以移除,避免重复推送
ONLINE_SESSIONS.remove(submissionId);
} catch (IOException e) {
log.error("向 submissionId: {} 推送消息失败", submissionId, e);
}
}
}
}
================================================
FILE: DOJ-BE/submission-service/src/main/resources/application.yaml
================================================
server:
port: ${doj.port.submission-service}
management:
endpoints:
web:
exposure:
include: health,prometheus
doj:
db:
host: ${DOJ_DB_HOST:127.0.0.1}
name: doj_submission
user: ${DOJ_DB_USER:root}
pwd: ${DOJ_DB_PWD:123}
mq:
host: ${DOJ_MQ_HOST:127.0.0.1}
swagger:
title: 沙箱接口文档
scan: com.decade.doj.submission.controller
================================================
FILE: DOJ-BE/submission-service/src/main/resources/bootstrap.yaml
================================================
spring:
application:
name: submission-service
profiles:
active: dev, common
cloud:
nacos:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
config:
file-extension: yaml
shared-configs:
- dataId: shared-jdbc.yaml
- dataId: shared-swagger.yaml
- dataId: shared-jwt.yaml
- dataId: shared-rabbitmq.yaml
================================================
FILE: DOJ-BE/submission-service/src/main/resources/com/decade/doj/submission/mapper/SubmissionMapper.xml
================================================
id,user_id,problem_id,language,code,exit_value,
status,message,time,memory,submit_time
================================================
FILE: DOJ-BE/submission-service/src/test/java/com/decade/doj/submission/SubmissionTest.java
================================================
package com.decade.doj.submission;
import com.decade.doj.submission.domain.po.Submission;
import com.decade.doj.submission.service.ISubmissionService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
@SpringBootTest
public class SubmissionTest {
@Autowired
private ISubmissionService submissionService;
@Test
public void testSubmissionService() {
Submission submission = new Submission();
submission.setUserId(3L);
submission.setProblemId(1L);
submission.setLanguage("Java");
submission.setCode("public class Solution { public int add(int a, int b) { return a + b; } }");
submission.setStatus("Wrong");
submission.setMessage("Compilation Error");
submission.setMemory(12L);
submission.setTime(100.0);
submission.setSubmitTime(new Date());
boolean result = submissionService.save(submission);
assert result : "Submission should be saved successfully";
}
}
================================================
FILE: DOJ-BE/user-service/Dockerfile
================================================
# Stage 1: Build the application
FROM maven:3.8.4-openjdk-17 AS build
WORKDIR /app
# Copy all pom.xml files first to leverage Docker cache
COPY DOJ-BE/pom.xml .
COPY DOJ-BE/common/pom.xml ./common/
COPY DOJ-BE/gateway-service/pom.xml ./gateway-service/
COPY DOJ-BE/problem-service/pom.xml ./problem-service/
COPY DOJ-BE/sandbox-service/pom.xml ./sandbox-service/
COPY DOJ-BE/submission-service/pom.xml ./submission-service/
COPY DOJ-BE/user-service/pom.xml ./user-service/
# Download all dependencies
RUN mvn dependency:go-offline
# Copy all source code
COPY DOJ-BE/common/src ./common/src
COPY DOJ-BE/user-service/src ./user-service/src
# Build the specific service
RUN mvn -f ./pom.xml -pl user-service -am clean package -DskipTests
RUN ls -al ./user-service/target
# Stage 2: Create the runtime image
FROM eclipse-temurin:17-jre
WORKDIR /app
# Copy the executable jar from the build stage
COPY --from=build /app/user-service/target/user-service.jar ./app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "./app.jar"]
================================================
FILE: DOJ-BE/user-service/pom.xml
================================================
4.0.0
com.decade
DOJ-BE
1.0-SNAPSHOT
user-service
17
17
UTF-8
com.decade
common
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
com.baomidou
mybatis-plus-boot-starter
org.springframework.boot
spring-boot-starter-test
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
${project.artifactId}
org.springframework.boot
spring-boot-maven-plugin
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/UserApplication.java
================================================
package com.decade.doj.user;
import com.decade.doj.common.config.custom.DefaultFeignConfig;
import com.decade.doj.common.config.custom.JwtTool;
import com.decade.doj.common.config.custom.MVCConfig;
import com.decade.doj.common.config.custom.MybatisConfig;
import com.decade.doj.common.config.properties.AppNameProperties;
import com.decade.doj.common.interceptor.AdminCheckInterceptor;
import com.decade.doj.common.interceptor.IdentityInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@MapperScan("com.decade.doj.user.mapper")
@EnableFeignClients(basePackages = "com.decade.doj.common.client", defaultConfiguration = DefaultFeignConfig.class)
@Import({JwtTool.class, MVCConfig.class, MybatisConfig.class, IdentityInterceptor.class, AppNameProperties.class, AdminCheckInterceptor.class})
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/controller/AnnouncementController.java
================================================
package com.decade.doj.user.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.decade.doj.common.annotation.AdminRequired;
import com.decade.doj.common.domain.R;
import com.decade.doj.user.domain.po.Announcement;
import com.decade.doj.user.service.IAnnouncementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
@RestController
@RequestMapping("/user/announcement")
@Tag(name = "公告接口")
@RequiredArgsConstructor
public class AnnouncementController {
private final IAnnouncementService announcementService;
@GetMapping("/list")
@Operation(summary = "获取公告列表")
public R> list() {
return R.ok(announcementService.list(new LambdaQueryWrapper()
.eq(Announcement::getDeleted, false)
.orderByDesc(Announcement::getCreateTime)));
}
// Admin endpoints (simplified, assuming standard auth/admin check or internal use if gateway handles it)
// For now, I'll allow creation to support the requirement
@PostMapping
@AdminRequired
@Operation(summary = "新增公告")
public R create(@RequestBody Announcement announcement) {
announcement.setCreateTime(LocalDateTime.now());
announcement.setUpdateTime(LocalDateTime.now());
announcementService.save(announcement);
return R.ok();
}
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/controller/UserController.java
================================================
package com.decade.doj.user.controller;
import cn.hutool.core.bean.BeanUtil;
import com.decade.doj.common.config.properties.ResourceProperties;
import com.decade.doj.common.domain.PageQueryDTO;
import com.decade.doj.common.domain.PageDTO;
import com.decade.doj.common.domain.R;
import com.decade.doj.common.domain.vo.StatsVO;
import com.decade.doj.user.domain.dto.LoginDTO;
import com.decade.doj.user.domain.dto.RegisterDTO;
import com.decade.doj.user.domain.dto.UpdPwdDTO;
import com.decade.doj.user.domain.po.User;
import com.decade.doj.user.domain.vo.InfoVO;
import com.decade.doj.user.domain.vo.LoginVO;
import com.decade.doj.user.domain.vo.RankVO;
import com.decade.doj.user.service.IUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.NotNull;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
/**
*
* 前端控制器
*
*
* @author
* @since 2024-08-26
*/
@RestController
@RequestMapping("/user")
@Tag(name = "用户相关接口")
@Slf4j
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;
private final ResourceProperties resourceProperties;
@PutMapping("/pwd")
@Operation(summary = "修改密码接口")
public R updatePwd(@RequestBody @Validated UpdPwdDTO updPwdDTO) {
return userService.updatePwd(updPwdDTO);
}
@PostMapping("/avatar")
@Operation(summary = "上传头像接口")
public R uploadAvatar(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return R.error("文件为空!");
}
String filename = UUID.randomUUID() + file.getOriginalFilename();
Path path = Paths.get(resourceProperties.getLocation() + filename);
try {
file.transferTo(path);
} catch (Exception e) {
return R.error("文件上传失败!");
}
return R.ok(resourceProperties.getRequest()+filename);
}
@PostMapping("/register")
@Operation(summary = "用户注册接口")
public R register(@RequestBody @Validated RegisterDTO registerDTO) {
return userService.register(registerDTO);
}
@PostMapping("/login")
@Operation(summary = "用户登录接口")
public R login(@RequestBody @Validated LoginDTO loginDTO) {
return userService.login(loginDTO);
}
@PostMapping("/refresh")
@Operation(summary = "刷新令牌接口")
public R refreshToken(@RequestHeader("Authorization") String refreshToken) {
return userService.refreshToken(refreshToken);
}
@GetMapping("/rankings")
@Operation(summary = "获取排行榜")
public R> getRankings(PageQueryDTO pageQueryDTO) {
return userService.getRankings(pageQueryDTO);
}
@GetMapping("/stats")
@Operation(summary = "获取首页统计数据")
public R getStats() {
return R.ok(userService.getStats());
}
@GetMapping("/{id}")
@Operation(summary = "查询用户接口")
public R getUser(@PathVariable("id") @NotNull Long id) {
User user = userService.getById(id);
if (user == null) {
return R.error("用户不存在!");
}
InfoVO infoVO = BeanUtil.copyProperties(user, InfoVO.class);
return R.ok(infoVO);
}
@PutMapping()
@Operation(summary = "修改用户接口")
public R updateUser(@RequestBody User user) {
return userService.updateUser(user);
}
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/dto/LoginDTO.java
================================================
package com.decade.doj.user.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
@Data
@Accessors(chain = true)
@Schema(description = "登录信息")
public class LoginDTO {
@Schema(description = "用户名", required = true)
@NotBlank(message = "用户名不能为空")
private String username;
@Schema(description = "密码", required = true)
@NotBlank(message = "密码不能为空")
private String password;
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/dto/RegisterDTO.java
================================================
package com.decade.doj.user.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Data
@Accessors(chain = true)
@Schema(description = "注册信息")
public class RegisterDTO {
@Schema(description = "用户名", required = true)
@NotBlank(message = "用户名不能为空")
private String username;
@Schema(description = "密码", required = true)
@NotBlank(message = "密码不能为空")
private String password;
@Schema(description = "邮箱", required = true)
@Email(message = "邮箱格式不正确")
private String email;
@Schema(description = "签名")
private String sign;
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/dto/UpdPwdDTO.java
================================================
package com.decade.doj.user.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
@Data
@Accessors(chain = true)
@Schema(description = "修改密码DTO")
public class UpdPwdDTO {
@Schema(description = "旧密码", required = true)
@NotBlank(message = "旧密码不能为空")
private String oldPassword;
@Schema(description = "新密码", required = true)
@NotBlank(message = "新密码不能为空")
private String newPassword;
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/po/Announcement.java
================================================
package com.decade.doj.user.domain.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
*
* 公告表
*
*
* @author Antigravity
* @since 2026-01-24
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("announcement")
public class Announcement {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String title;
private String content;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_time")
private LocalDateTime updateTime;
@TableField("creator_id")
private Long creatorId;
private Boolean deleted = false;
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/po/User.java
================================================
package com.decade.doj.user.domain.po;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
*
*
*
*
* @author
* @since 2024-08-26
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String username;
private String avatar = "";
private String email;
private String password;
private Integer score = 0;
private Integer ranks = 0;
private String school = "";
private Boolean gender = true;
private Integer easySolve = 0;
private Integer middleSolve = 0;
private Integer hardSolve = 0;
private Boolean role = true;
private String url = "";
private String sign = "这个人很懒,什么都没留下";
private Long fans = 0L;
private Long subscribe = 0L;
private Boolean ban = false;
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/vo/InfoVO.java
================================================
package com.decade.doj.user.domain.vo;
import com.decade.doj.user.domain.po.User;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class InfoVO extends User {
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/vo/LoginVO.java
================================================
package com.decade.doj.user.domain.vo;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class LoginVO {
private String accessToken;
private String refreshToken;
private Long userId;
private String username;
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/domain/vo/RankVO.java
================================================
package com.decade.doj.user.domain.vo;
import lombok.Data;
@Data
public class RankVO {
private Long rank;
private Long userId;
private String username;
private String avatar;
private Integer score;
private Integer easySolve;
private Integer middleSolve;
private Integer hardSolve;
private String mostUsedLanguage;
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/mapper/AnnouncementMapper.java
================================================
package com.decade.doj.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.decade.doj.user.domain.po.Announcement;
/**
*
* 公告表 Mapper 接口
*
*
* @author Antigravity
* @since 2026-01-24
*/
public interface AnnouncementMapper extends BaseMapper {
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/mapper/UserMapper.java
================================================
package com.decade.doj.user.mapper;
import com.decade.doj.user.domain.po.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
*
* Mapper 接口
*
*
* @author
* @since 2024-08-26
*/
public interface UserMapper extends BaseMapper {
@Select("select id, username, sign, easy_solve from user where id = #{id}")
User chooseById(Integer id);
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/mq/MqConfig.java
================================================
package com.decade.doj.user.mq;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqConfig {
@Bean
public TopicExchange topicExchange() {
return new TopicExchange("doj.topic");
}
@Bean
public Queue statsUpdateQueue() {
return new Queue("stats.update.queue");
}
@Bean
public Binding binding() {
return BindingBuilder.bind(statsUpdateQueue()).to(topicExchange()).with("problem.solved");
}
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/mq/StatsUpdateListener.java
================================================
package com.decade.doj.user.mq;
import com.decade.doj.common.utils.UserContext;
import com.decade.doj.user.service.IUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class StatsUpdateListener {
private final IUserService userService;
@RabbitListener(queues = "stats.update.queue")
public void listenStatsUpdateQueue(Map message) {
if (message == null) {
return;
}
Long userId = message.get("userId");
Long problemId = message.get("problemId");
if (userId == null || problemId == null) {
log.error("接收到无效的消息:{}", message);
return;
}
log.info("接收到用户解题消息,开始更新用户统计数据。userId: {}, problemId: {}", userId, problemId);
// 调用业务方法处理
userService.handleProblemSolved(userId, problemId);
}
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/service/IAnnouncementService.java
================================================
package com.decade.doj.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.decade.doj.user.domain.po.Announcement;
/**
*
* 公告表 服务类
*
*
* @author Antigravity
* @since 2026-01-24
*/
public interface IAnnouncementService extends IService {
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/service/IUserService.java
================================================
package com.decade.doj.user.service;
import com.decade.doj.common.domain.PageDTO;
import com.decade.doj.common.domain.PageQueryDTO;
import com.decade.doj.common.domain.R;
import com.decade.doj.common.domain.vo.StatsVO;
import com.decade.doj.user.domain.dto.LoginDTO;
import com.decade.doj.user.domain.dto.RegisterDTO;
import com.decade.doj.user.domain.dto.UpdPwdDTO;
import com.decade.doj.user.domain.po.User;
import com.baomidou.mybatisplus.extension.service.IService;
import com.decade.doj.user.domain.vo.LoginVO;
import com.decade.doj.user.domain.vo.RankVO;
/**
*
* 服务类
*
*
* @author
* @since 2024-08-26
*/
public interface IUserService extends IService {
R login(LoginDTO loginDTO);
R refreshToken(String refreshToken);
R register(RegisterDTO registerDTO);
R updateUser(User user);
R updatePwd(UpdPwdDTO updPwdDTO);
R> getRankings(PageQueryDTO pageQueryDTO);
void handleProblemSolved(Long userId, Long problemId);
StatsVO getStats();
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/service/impl/AnnouncementServiceImpl.java
================================================
package com.decade.doj.user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.decade.doj.user.domain.po.Announcement;
import com.decade.doj.user.mapper.AnnouncementMapper;
import com.decade.doj.user.service.IAnnouncementService;
import org.springframework.stereotype.Service;
/**
*
* 公告表 服务实现类
*
*
* @author Antigravity
* @since 2026-01-24
*/
@Service
public class AnnouncementServiceImpl extends ServiceImpl implements IAnnouncementService {
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/service/impl/UserServiceImpl.java
================================================
package com.decade.doj.user.service.impl;
import com.decade.doj.common.client.ProblemClient;
import com.decade.doj.common.client.SubmissionClient;
import com.decade.doj.common.config.properties.JwtProperties;
import com.decade.doj.common.domain.PageDTO;
import com.decade.doj.common.domain.PageQueryDTO;
import com.decade.doj.common.domain.R;
import com.decade.doj.common.domain.po.Problem;
import com.decade.doj.common.domain.vo.StatsVO;
import com.decade.doj.common.domain.vo.SubmissionStatsVO;
import com.decade.doj.common.exception.BadRequestException;
import com.decade.doj.common.exception.ForbiddenException;
import com.decade.doj.common.config.custom.JwtTool;
import com.decade.doj.common.exception.UnauthorizedException;
import com.decade.doj.common.utils.UserContext;
import com.decade.doj.user.domain.dto.LoginDTO;
import com.decade.doj.user.domain.dto.RegisterDTO;
import com.decade.doj.user.domain.dto.UpdPwdDTO;
import com.decade.doj.user.domain.vo.LoginVO;
import com.decade.doj.user.domain.vo.RankVO;
import com.decade.doj.user.mapper.UserMapper;
import com.decade.doj.user.domain.po.User;
import com.decade.doj.user.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.decade.doj.common.config.properties.AppNameProperties;
import com.decade.doj.user.utils.AESTool;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.Random;
/**
*
* 服务实现类
*
*
* @author
* @since 2024-08-26
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl extends ServiceImpl implements IUserService {
private final AESTool aesTool;
private final JwtTool jwtTool;
private final JwtProperties jwtProperties;
private final StringRedisTemplate redisTemplate;
private final AppNameProperties appNameProperties;
private final ProblemClient problemClient;
private final SubmissionClient submissionClient;
@PostConstruct
public void initRankings() {
// 服务启动时,将所有用户数据同步到 Redis 排行榜
List users = this.list();
if (users.isEmpty()) {
return;
}
Set> tuples = users.stream()
.map(user -> ZSetOperations.TypedTuple.of(user.getId().toString(), user.getScore().doubleValue()))
.collect(Collectors.toSet());
redisTemplate.opsForZSet().add(getRankingKey(), tuples);
log.info("用户排行榜数据已同步到 Redis。");
}
private String getRedisKeyPrefix() {
return appNameProperties.getName() + ":refresh_token:";
}
private String getRankingKey() {
return appNameProperties.getName() + ":ranks";
}
@Override
public R login(LoginDTO loginDTO) {
String username = loginDTO.getUsername();
String password = loginDTO.getPassword();
User user = lambdaQuery().eq(User::getUsername, username).one();
if (user == null) {
throw new BadRequestException("用户名错误");
}
if (!aesTool.match(password, user.getPassword())) {
throw new BadRequestException("密码错误");
}
if (user.getBan()) {
throw new ForbiddenException("用户被冻结");
}
// 1. 生成访问令牌 (Access Token)
String accessToken = jwtTool.createToken(user.getId(), jwtProperties.getTokenTTL());
// 2. 生成刷新令牌 (Refresh Token)
String refreshToken = UUID.randomUUID().toString();
// 3. 将 Refresh Token 存入 Redis
String redisKey = getRedisKeyPrefix() + refreshToken;
redisTemplate.opsForValue().set(redisKey, user.getId().toString(), jwtProperties.getRefreshTokenTTL());
// 4. 封装并返回双令牌
LoginVO loginVO = new LoginVO()
.setAccessToken(accessToken)
.setRefreshToken(refreshToken)
.setUserId(user.getId())
.setUsername(user.getUsername());
return R.ok(loginVO);
}
@Override
public R refreshToken(String refreshToken) {
String redisKey = getRedisKeyPrefix() + refreshToken;
String userIdStr = redisTemplate.opsForValue().get(redisKey);
if (userIdStr == null) {
throw new UnauthorizedException("无效的刷新令牌");
}
Long userId = Long.valueOf(userIdStr);
String accessToken = jwtTool.createToken(userId, jwtProperties.getTokenTTL());
return R.ok(accessToken);
}
@Override
public R> getRankings(PageQueryDTO pageQueryDTO) {
long start = (long) (pageQueryDTO.getPageNo() - 1) * pageQueryDTO.getPageSize();
long end = start + pageQueryDTO.getPageSize() - 1;
// 1. 从 Redis 的 Sorted Set 中获取排名和分数
Set> tuples = redisTemplate.opsForZSet().reverseRangeWithScores(getRankingKey(), start, end);
if (tuples == null || tuples.isEmpty()) {
return R.ok(PageDTO.empty(0L, 0L));
}
// 2. 提取用户ID并批量查询用户信息
List userIds = tuples.stream().map(tuple -> Long.valueOf(Objects.requireNonNull(tuple.getValue()))).toList();
Map userMap = this.listByIds(userIds).stream().collect(Collectors.toMap(User::getId, user -> user));
// 3. 组装VO
List rankVOs = new ArrayList<>();
long currentRank = start + 1;
List languages = List.of("cpp", "java", "python");
Random random = new Random();
for (ZSetOperations.TypedTuple tuple : tuples) {
Long userId = Long.valueOf(Objects.requireNonNull(tuple.getValue()));
User user = userMap.get(userId);
if (user != null) {
RankVO rankVO = new RankVO();
rankVO.setRank(currentRank++);
rankVO.setUserId(user.getId());
rankVO.setUsername(user.getUsername());
rankVO.setAvatar(user.getAvatar());
rankVO.setScore(user.getScore());
rankVO.setEasySolve(user.getEasySolve());
rankVO.setMiddleSolve(user.getMiddleSolve());
rankVO.setHardSolve(user.getHardSolve());
// 随机设置常用语言用于前端展示
rankVO.setMostUsedLanguage(languages.get(random.nextInt(languages.size())));
rankVOs.add(rankVO);
}
}
Long total = redisTemplate.opsForZSet().zCard(getRankingKey());
long pages = (total + pageQueryDTO.getPageSize() - 1) / pageQueryDTO.getPageSize();
return R.ok(PageDTO.fullPage(total, pages, rankVOs));
}
@Override
public void handleProblemSolved(Long userId, Long problemId) {
UserContext.setCurrentUser(userId);
Problem problem = problemClient.getProblemById(problemId).getData();
if (problem == null) return;
User user = this.getById(userId);
if (user == null) return;
int scoreChange = switch (problem.getDifficulty()) {
case "简单" -> {
user.setEasySolve(user.getEasySolve() + 1);
yield 500;
}
case "中等" -> {
user.setMiddleSolve(user.getMiddleSolve() + 1);
yield 1000;
}
case "困难" -> {
user.setHardSolve(user.getHardSolve() + 1);
yield 2000;
}
default -> 0;
};
user.setScore(user.getScore() + scoreChange);
this.updateById(user);
redisTemplate.opsForZSet().add(getRankingKey(), user.getId().toString(), user.getScore());
log.info("用户 {} 分数及统计数据更新成功", userId);
}
@Override
public R register(RegisterDTO registerDTO) {
String username = registerDTO.getUsername();
String password = registerDTO.getPassword();
String email = registerDTO.getEmail();
String signature = registerDTO.getSign();
boolean exist = lambdaQuery().eq(User::getUsername, username).exists();
if (exist) {
throw new BadRequestException("用户名已存在");
}
User user = new User()
.setUsername(username)
.setPassword(aesTool.encode(password, aesTool.fnv1aHash(password)))
.setEmail(email)
.setSign(signature);
save(user);
// 将新用户同步到排行榜
redisTemplate.opsForZSet().add(getRankingKey(), user.getId().toString(), 0.0);
return R.ok();
}
@Override
public R updatePwd(UpdPwdDTO updPwdDTO) {
String oldPassword = updPwdDTO.getOldPassword();
String newPassword = updPwdDTO.getNewPassword();
User user = getById(UserContext.getCurrentUser());
if (user == null) {
return R.error("用户不存在!");
}
if (!aesTool.match(oldPassword, user.getPassword())) {
return R.error("原密码错误!");
}
user.setPassword(aesTool.encode(newPassword, aesTool.fnv1aHash(newPassword)));
updateById(user);
return R.ok();
}
@Override
public R updateUser(User user) {
user.setId(UserContext.getCurrentUser());
User col = lambdaQuery().eq(User::getUsername, user.getUsername()).one();
if (col != null && !col.getId().equals(user.getId())) {
return R.error("用户名已存在!");
}
user.setBan(null)
.setPassword(null)
.setScore(null)
.setRanks(null)
.setEasySolve(null)
.setMiddleSolve(null)
.setHardSolve(null)
.setRole(null)
.setFans(null)
.setSubscribe(null);
updateById(user);
return R.ok();
}
@Override
public StatsVO getStats() {
// 获取提交统计
R submissionStatsR = submissionClient.getStats();
SubmissionStatsVO submissionStats = submissionStatsR.success() ? submissionStatsR.getData() : new SubmissionStatsVO(0L, 0L);
// 获取题目总数
R problemCountR = problemClient.getCount();
Long problemCount = problemCountR.success() ? problemCountR.getData() : 0L;
// 获取用户总数 (活跃用户)
long activeUsers = this.count();
return StatsVO.builder()
.totalSubmissions(submissionStats.getTotalSubmissions())
.todaySubmissions(submissionStats.getTodaySubmissions())
.totalProblems(problemCount)
.activeUsers(activeUsers)
.build();
}
}
================================================
FILE: DOJ-BE/user-service/src/main/java/com/decade/doj/user/utils/AESTool.java
================================================
package com.decade.doj.user.utils;
import com.decade.doj.common.exception.UnauthorizedException;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
@Component
public class AESTool {
private final String AES_ECB = "AES/ECB/PKCS5Padding";
private final int KEY_SIZE = 128; // 128-bit key size
private final int NUM_KEYS = 8; // Number of random keys
private List candidateKeys = new ArrayList<>();
@PostConstruct
public void init() {
String[] keys = {
"6A2B5E1D9F6C2A7E",
"3D5B6F4E9C7A1B2C",
"4E8A3D7B2F9C1D5A",
"7C2B4D1E6F9A5E8C",
"9F1A4B7E3C5D6B2A",
"2D7C9B4E1A5F6E3B",
"5A1C7E2B4F3D9C8A",
"8C3D1E7B6F5A9B2E"
};
for (String key : keys) {
candidateKeys.add(key.getBytes(StandardCharsets.UTF_8));
}
}
public int fnv1aHash(String str) {
final int FNV_prime = 0x01000193;
final int FNV_offset_basis = 0x811c9dc5;
int hash = FNV_offset_basis;
for (int i = 0; i < str.length(); i++) {
hash ^= str.charAt(i);
hash *= FNV_prime;
}
return Math.abs(hash) % NUM_KEYS;
}
private String _encode(String data) {
int index = fnv1aHash(data);
byte[] key = candidateKeys.get(index);
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
try {
Cipher cipher = Cipher.getInstance(AES_ECB);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] ret = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(ret);
} catch (Exception e) {
throw new UnauthorizedException("Failed to initialize AES cipher", e);
}
}
public boolean match(String raw, String encoded) {
String ret = _encode(raw);
return ret.equals(encoded);
}
public String encode(String data, int index) {
byte[] key = candidateKeys.get(index);
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
try {
Cipher cipher = Cipher.getInstance(AES_ECB);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] ret = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(ret);
} catch (Exception e) {
throw new UnauthorizedException("Failed to initialize AES cipher", e);
}
}
public String decode(String data) {
int index = NUM_KEYS - 1;
byte[] key = candidateKeys.get(index);
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
try {
Cipher cipher = Cipher.getInstance(AES_ECB);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] ret = cipher.doFinal(Base64.getDecoder().decode(data));
return new String(ret, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new UnauthorizedException("Failed to initialize AES cipher", e);
}
}
}
================================================
FILE: DOJ-BE/user-service/src/main/resources/application.yaml
================================================
server:
port: ${doj.port.user-service}
management:
endpoints:
web:
exposure:
include: health,prometheus
doj:
db:
host: ${DOJ_DB_HOST:127.0.0.1}
name: doj_user
user: ${DOJ_DB_USER:root}
pwd: ${DOJ_DB_PWD:123}
mq:
host: ${DOJ_MQ_HOST:127.0.0.1}
redis:
host: ${DOJ_REDIS_HOST:127.0.0.1}
swagger:
title: 用户服务接口文档
scan: com.decade.doj.user.controller
================================================
FILE: DOJ-BE/user-service/src/main/resources/bootstrap.yaml
================================================
spring:
application:
name: user-service
profiles:
active: dev, common
cloud:
nacos:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
config:
file-extension: yaml
shared-configs:
- dataId: shared-jdbc.yaml
- dataId: shared-swagger.yaml
- dataId: shared-jwt.yaml
- dataId: shared-rabbitmq.yaml
# shared-jdbc.yaml
#s
# shared-swagger.yaml
#logging:
# level:
# com.decade: debug
# pattern:
# dateformat: HH:mm:ss:SSS
#knife4j:
# enable: true
# openapi:
# title: ${doj.swagger.title}
# description: "Duck Online Judge API"
# email: "decade-qzj@foxmail.com"
# version: v1.0.0
# group:
# default:
# group-name: default
# api-rule: package
# api-rule-resources:
# - ${doj.swagger.scan}
# shared-jwt.yaml
#doj:
# jwt:
# location: classpath:doj.jks
# alias: decade
# password: doj123
# tokenTTL: 30m
# authorization: "authorization"
# secret-key: "uid"
================================================
FILE: DOJ-BE/user-service/src/test/java/com/decade/doj/user/UserTest.java
================================================
package com.decade.doj.user;
import com.decade.doj.user.domain.po.User;
import com.decade.doj.user.mapper.UserMapper;
import com.decade.doj.user.service.IUserService;
import com.decade.doj.user.utils.AESTool;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.sql.SQLOutput;
@SpringBootTest
public class UserTest {
@Autowired
private IUserService userService;
@Autowired
private UserMapper userMapper;
@Autowired
private AESTool aesTool;
@Test
public void testAes() {
String pods = "123";
System.out.println(aesTool.encode(pods, aesTool.fnv1aHash(pods)));
System.out.println(aesTool.match(pods, "1okxVZ0I1b0jrfTp7wyGpg=="));
}
@Test
public void testSaveUser() {
User user = userService.getById(1);
System.out.println(user);
}
}
================================================
FILE: DOJ-FE/.eslintignore
================================================
dist
node_modules
================================================
FILE: DOJ-FE/.eslintrc.cjs
================================================
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true,
},
/* 指定如何解析语法 */
parser: "vue-eslint-parser",
/** 优先级低于 parse 的语法解析配置 */
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
parser: "@typescript-eslint/parser",
jsxPragma: "React",
ecmaFeatures: {
jsx: true,
},
},
/* 继承已有的规则 */
extends: [
"plugin:vue/vue3-essential",
// "eslint:recommended",
// "plugin:@typescript-eslint/recommended",
],
plugins: ["vue", "@typescript-eslint"],
/*
* "off" 或 0 ==> 关闭规则
* "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行)
* "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错)
*/
rules: {
// eslint(https://eslint.bootcss.com/docs/rules/)
"no-var": "error", // 要求使用 let 或 const 而不是 var
"no-multiple-empty-lines": "off", // 不允许多个空行
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
"no-unexpected-multiline": "error", // 禁止空余的多行
"no-useless-escape": "off", // 禁止不必要的转义字符
// typeScript (https://typescript-eslint.io/rules)
"@typescript-eslint/no-unused-vars": "off", // 禁止定义未使用的变量
"@typescript-eslint/prefer-ts-expect-error": "off", // 禁止使用 @ts-ignore
"@typescript-eslint/no-explicit-any": "off", // 禁止使用 any 类型
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-namespace": "off", // 禁止使用自定义 TypeScript 模块和命名空间。
"@typescript-eslint/semi": "off",
// eslint-plugin-vue (https://eslint.vuejs.org/rules/)
"vue/multi-word-component-names": "off", // 要求组件名称始终为 “-” 链接的单词
"vue/script-setup-uses-vars": "error", // 防止