diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..18b6da3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(cat:*)", + "Bash(find ./aioj-backend-common -path \"*common-log*pom.xml\" -exec cat {} ;)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.idea/misc.xml b/.idea/misc.xml index 75693c8..efe95dd 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -15,7 +15,7 @@ - + \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3391e6b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,86 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is an AI-integrated OJ (Online Judge) judging system with a microservices architecture. The system allows users to submit code for programming problems, which is then judged automatically. It also includes AI features to assist with problem-solving and code evaluation. + +## Architecture + +The project follows a microservices architecture pattern built with Spring Boot and Spring Cloud Alibaba: + +### Core Modules: + +1. **aioj-backend-common**: Common utilities and dependencies shared across all modules. +2. **aioj-backend-gateway**: API gateway that routes requests to the appropriate microservices. +3. **aioj-backend-judge-service**: Handles code submission, compilation, and judging processes. +4. **aioj-backend-user-service**: Manages user accounts, authentication, and authorization. +5. **aioj-backend-question-service**: Manages programming problems and test cases. +6. **aioj-backend-ai-service**: Provides AI-assisted features such as code analysis and problem-solving. +7. **aioj-backend-auth**: Manages authentication and token issuance. +8. **aioj-backend-upms**: User Management System for administrative operations. + +## Database + +The system uses SQL databases. Database scripts can be found in the `db/` directory. + +## Build System + +The project uses Maven for build and dependency management. + +### Common Commands: + +- Compile the entire project: + ```bash + mvn clean compile + ``` + +- Compile a specific module: + ```bash + mvn clean compile -pl + ``` + +- Package the entire project: + ```bash + mvn clean package + ``` + +- Package a specific module: + ```bash + mvn clean package -pl + ``` + +## Development + +### Environment Profiles + +The project supports different environments (dev, test, prod) with corresponding configuration files: + +- Development: application-dev.yml +- Test: application-test.yml +- Production: application-prod.yml + +### Running a Service + +To run a specific microservice, use the Spring Boot Maven plugin: + +```bash +cd +mvn spring-boot:run +``` + +Or run the built JAR file: + +```bash +cd /target +java -jar -.jar +``` + +## Technologies Used + +- **Java 17**: Programming language +- **Spring Boot 3.5.7**: Framework for building microservices +- **Spring Cloud Alibaba 2025.0.0.0**: Microservices ecosystem +- **Maven**: Build tool +- **Lombok**: Java library to reduce boilerplate code \ No newline at end of file diff --git a/aioj-backend-common/aioj-backend-common-core/pom.xml b/aioj-backend-common/aioj-backend-common-core/pom.xml index 42062a4..e3d07b1 100644 --- a/aioj-backend-common/aioj-backend-common-core/pom.xml +++ b/aioj-backend-common/aioj-backend-common-core/pom.xml @@ -26,7 +26,20 @@ cn.hutool hutool-core - + + + jakarta.servlet + jakarta.servlet-api + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework + spring-webmvc + provided + cn.hutool hutool-extra diff --git a/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/utils/SpringContextHolder.java b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/utils/SpringContextHolder.java new file mode 100644 index 0000000..126b038 --- /dev/null +++ b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/utils/SpringContextHolder.java @@ -0,0 +1,77 @@ +package cn.meowrain.aioj.backend.framework.core.utils; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class SpringContextHolder implements ApplicationContextAware, EnvironmentAware, DisposableBean { + private static ApplicationContext applicationContext = null; + + private static Environment environment = null; + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + public static T getBean(String name) { + return (T) applicationContext.getBean(name); + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + public static T getBean(Class requiredType) { + return applicationContext.getBean(requiredType); + } + + @Override + @SneakyThrows + public void destroy() throws Exception { + clearHolder(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringContextHolder.applicationContext = applicationContext; + } + + @Override + public void setEnvironment(Environment environment) { + SpringContextHolder.environment = environment; + } + + /** + * 清除SpringContextHolder中的ApplicationContext为Null. + */ + public static void clearHolder() { + if (log.isDebugEnabled()) { + log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext); + } + applicationContext = null; + } + /** + * 发布事件 + * @param event + */ + public static void publishEvent(ApplicationEvent event) { + if (applicationContext == null) { + return; + } + applicationContext.publishEvent(event); + } + /** + * 是否是微服务 + * @return boolean + */ + public static boolean isMicro() { + return environment.getProperty("spring.cloud.nacos.discovery.enabled", Boolean.class, true); + } + +} diff --git a/aioj-backend-common/aioj-backend-common-log/pom.xml b/aioj-backend-common/aioj-backend-common-log/pom.xml index 9a870b8..77aca82 100644 --- a/aioj-backend-common/aioj-backend-common-log/pom.xml +++ b/aioj-backend-common/aioj-backend-common-log/pom.xml @@ -18,6 +18,23 @@ + + + org.springframework.security + spring-security-core + + + org.springframework.security + spring-security-oauth2-core + + + cn.hutool + hutool-extra + + + cn.hutool + hutool-http + cn.meowrain aioj-backend-common-core diff --git a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/LogAutoConfiguration.java b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/LogAutoConfiguration.java index aa7f98b..b8cf6bb 100644 --- a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/LogAutoConfiguration.java +++ b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/LogAutoConfiguration.java @@ -3,6 +3,8 @@ package cn.meowrain.aioj.backend.framework.log; import cn.meowrain.aioj.backend.framework.log.aspect.SysLogAspect; import cn.meowrain.aioj.backend.framework.log.config.AIOJLogPropertiesConfiguration; import cn.meowrain.aioj.backend.framework.log.event.SysLogListener; +import cn.meowrain.aioj.backend.upms.api.feign.RemoteLogService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -19,6 +21,7 @@ public class LogAutoConfiguration { * 创建并返回SysLogListener的Bean实例 */ @Bean + @ConditionalOnBean(RemoteLogService.class) public SysLogListener sysLogListener(AIOJLogPropertiesConfiguration logProperties, RemoteLogService remoteLogService) { return new SysLogListener(remoteLogService, logProperties); diff --git a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/aspect/SysLogAspect.java b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/aspect/SysLogAspect.java index 2dde676..99b6917 100644 --- a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/aspect/SysLogAspect.java +++ b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/aspect/SysLogAspect.java @@ -1,9 +1,14 @@ package cn.meowrain.aioj.backend.framework.log.aspect; import cn.hutool.core.util.StrUtil; +import cn.meowrain.aioj.backend.framework.core.utils.SpringContextHolder; import cn.meowrain.aioj.backend.framework.log.annotation.SysLog; +import cn.meowrain.aioj.backend.framework.log.enums.LogTypeEnum; +import cn.meowrain.aioj.backend.framework.log.event.SysLogEvent; +import cn.meowrain.aioj.backend.framework.log.event.SysLogEventSource; import cn.meowrain.aioj.backend.framework.log.utils.SysLogUtils; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -15,19 +20,60 @@ import org.springframework.expression.EvaluationContext; @Slf4j @RequiredArgsConstructor public class SysLogAspect { - + /** + * 环绕通知方法,用于处理系统日志记录 + * @param point 连接点对象 + * @param sysLog 系统日志注解 + * @return 目标方法执行结果 + * @throws Throwable 目标方法执行可能抛出的异常 + */ @Around("@annotation(sysLog)") - public Object around(ProceedingJoinPoint joinPoint, SysLog sysLog) throws Throwable { - String strClassName = joinPoint.getTarget().getClass().getName(); - String strMethodName = joinPoint.getSignature().getName(); + @SneakyThrows + public Object around(ProceedingJoinPoint point,SysLog sysLog) { + String strClassName = point.getTarget().getClass().getName(); + String strMethodName = point.getSignature().getName(); log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName); + String value = sysLog.value(); String expression = sysLog.expression(); + // 当前表达式存在 SPEL,会覆盖 value 的值 if (StrUtil.isNotBlank(expression)) { // 解析SPEL - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - EvaluationContext context = SysLogUtils.getContext(joinPoint.getArgs(), signature.getMethod()); + MethodSignature signature = (MethodSignature) point.getSignature(); + EvaluationContext context = SysLogUtils.getContext(point.getArgs(), signature.getMethod()); + try { + value = SysLogUtils.getValue(context, expression, String.class); + } + catch (Exception e) { + // SPEL 表达式异常,获取 value 的值 + log.error("@SysLog 解析SPEL {} 异常", expression); + } } - } + SysLogEventSource logVo = SysLogUtils.getSysLog(); + logVo.setTitle(value); + // 获取请求body参数 + if (StrUtil.isBlank(logVo.getParams())) { + logVo.setBody(point.getArgs()); + } + // 发送异步日志事件 + Long startTime = System.currentTimeMillis(); + Object obj; + + try { + obj = point.proceed(); + } + catch (Exception e) { + logVo.setLogType(LogTypeEnum.ERROR.getType()); + logVo.setException(e.getMessage()); + throw e; + } + finally { + Long endTime = System.currentTimeMillis(); + logVo.setTime(endTime - startTime); + SpringContextHolder.publishEvent(new SysLogEvent(logVo)); + } + + return obj; + } } diff --git a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/event/SysLogEvent.java b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/event/SysLogEvent.java index 1944b2e..0934408 100644 --- a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/event/SysLogEvent.java +++ b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/event/SysLogEvent.java @@ -1,6 +1,7 @@ package cn.meowrain.aioj.backend.framework.log.event; -import cn.meowrain.aioj.backend.framework.log.annotation.SysLog; + +import cn.meowrain.aioj.backend.upms.api.entity.SysLog; import org.springframework.context.ApplicationEvent; import java.io.Serial; diff --git a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/event/SysLogEventSource.java b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/event/SysLogEventSource.java index 805be73..1f8ce77 100644 --- a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/event/SysLogEventSource.java +++ b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/event/SysLogEventSource.java @@ -1,17 +1,17 @@ package cn.meowrain.aioj.backend.framework.log.event; +import cn.meowrain.aioj.backend.upms.api.entity.SysLog; import lombok.Data; import lombok.EqualsAndHashCode; import java.io.Serial; -import java.io.Serializable; /** * 系统 */ @Data @EqualsAndHashCode(callSuper = false) -public class SysLogEventSource extends SysLogEvent implements Serializable { +public class SysLogEventSource extends SysLog { @Serial private static final long serialVersionUID = 1L; diff --git a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/utils/SysLogUtils.java b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/utils/SysLogUtils.java index 246fb5b..1e146f0 100644 --- a/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/utils/SysLogUtils.java +++ b/aioj-backend-common/aioj-backend-common-log/src/main/java/cn/meowrain/aioj/backend/framework/log/utils/SysLogUtils.java @@ -1,11 +1,101 @@ package cn.meowrain.aioj.backend.framework.log.utils; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.extra.servlet.JakartaServletUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.http.HttpUtil; +import cn.meowrain.aioj.backend.framework.core.utils.SpringContextHolder; +import cn.meowrain.aioj.backend.framework.log.config.AIOJLogPropertiesConfiguration; +import cn.meowrain.aioj.backend.framework.log.enums.LogTypeEnum; import cn.meowrain.aioj.backend.framework.log.event.SysLogEventSource; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.StandardReflectionParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; public final class SysLogUtils { - public static SysLogEventSource getSysLog() { + /** + * 获取系统日志事件源 + * @return 系统日志事件源对象 + */ + public static SysLogEventSource getSysLog() { + HttpServletRequest request = ((ServletRequestAttributes) Objects + .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + SysLogEventSource sysLog = new SysLogEventSource(); + sysLog.setLogType(LogTypeEnum.NORMAL.getType()); + sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI())); + sysLog.setMethod(request.getMethod()); + sysLog.setRemoteAddr(JakartaServletUtil.getClientIP(request)); + sysLog.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT)); + sysLog.setCreateBy(getUsername()); + sysLog.setServiceId(SpringUtil.getProperty("spring.application.name")); + + // get 参数脱敏 + AIOJLogPropertiesConfiguration logProperties = SpringContextHolder.getBean(AIOJLogPropertiesConfiguration.class); + Map paramsMap = MapUtil.removeAny(new HashMap<>(request.getParameterMap()), + ArrayUtil.toArray(logProperties.getExcludeFields(), String.class)); + sysLog.setParams(HttpUtil.toParams(paramsMap)); + return sysLog; + } + + /** + * 获取用户名称 + * @return username + */ + private static String getUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getName(); + } + + /** + * 获取spel 定义的参数值 + * @param context 参数容器 + * @param key key + * @param clazz 需要返回的类型 + * @param 返回泛型 + * @return 参数值 + */ + public static T getValue(EvaluationContext context, String key, Class clazz) { + SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); + Expression expression = spelExpressionParser.parseExpression(key); + return expression.getValue(context, clazz); + } + + /** + * 获取参数容器 + * @param arguments 方法的参数列表 + * @param signatureMethod 被执行的方法体 + * @return 装载参数的容器 + */ + public static EvaluationContext getContext(Object[] arguments, Method signatureMethod) { + String[] parameterNames = new StandardReflectionParameterNameDiscoverer().getParameterNames(signatureMethod); + EvaluationContext context = new StandardEvaluationContext(); + if (parameterNames == null) { + return context; + } + for (int i = 0; i < arguments.length; i++) { + context.setVariable(parameterNames[i], arguments[i]); + } + return context; } } diff --git a/aioj-backend-upms/aioj-backend-upms-api/pom.xml b/aioj-backend-upms/aioj-backend-upms-api/pom.xml index 6520d36..b4d93f4 100644 --- a/aioj-backend-upms/aioj-backend-upms-api/pom.xml +++ b/aioj-backend-upms/aioj-backend-upms-api/pom.xml @@ -34,5 +34,10 @@ aioj-backend-common-feign 1.0-SNAPSHOT + + cn.meowrain + aioj-backend-upms-api + 1.0-SNAPSHOT + \ No newline at end of file diff --git a/aioj-backend-user-service/pom.xml b/aioj-backend-user-service/pom.xml index 5bb7bfe..936c3a0 100644 --- a/aioj-backend-user-service/pom.xml +++ b/aioj-backend-user-service/pom.xml @@ -32,6 +32,10 @@ org.springframework.boot spring-boot-starter-web + + cn.hutool + hutool-crypto + org.springframework.boot @@ -66,5 +70,11 @@ com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery + + + cn.meowrain + aioj-backend-common-core + 1.0-SNAPSHOT + \ No newline at end of file diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserRegisterRequestParamVerifyChain.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserRegisterRequestParamVerifyChain.java index 46ab6e7..90e0ac4 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserRegisterRequestParamVerifyChain.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/chains/UserRegisterRequestParamVerifyChain.java @@ -1,8 +1,8 @@ package cn.meowrain.aioj.backend.userservice.dto.chains; -import cn.meowrain.aioj.backend.framework.designpattern.chains.AbstractChianHandler; -import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode; -import cn.meowrain.aioj.backend.framework.exception.ClientException; +import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler; +import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode; +import cn.meowrain.aioj.backend.framework.core.exception.ClientException; import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO; import lombok.extern.slf4j.Slf4j;