diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 773f3b0..a3da9bf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,12 @@ "Bash(mvn dependency:tree:*)", "Bash(mvn spring-javaformat:apply:*)", "Bash(git add:*)", - "Bash(git commit:*)" + "Bash(git commit:*)", + "Bash(git -C \"C:\\\\Users\\\\meowr\\\\Desktop\\\\bishe\\\\AI_OJ\" status)", + "Bash(git -C \"C:\\\\Users\\\\meowr\\\\Desktop\\\\bishe\\\\AI_OJ\" checkout 3da91e5 -- aioj-backend-ai-service/)", + "Bash(git -C \"C:\\\\Users\\\\meowr\\\\Desktop\\\\bishe\\\\AI_OJ\" push)", + "Bash(mvn compile:*)", + "Bash(mvn clean install:*)" ] } } diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 248eeae..daeec98 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -6,6 +6,7 @@ + diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/AIServiceApplication.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/AIServiceApplication.java new file mode 100644 index 0000000..b6ed9ba --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/AIServiceApplication.java @@ -0,0 +1,15 @@ +package cn.meowrain.aioj.backend.aiservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * AI 服务启动类 + */ +@SpringBootApplication(scanBasePackages = "cn.meowrain.aioj") +public class AIServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AIServiceApplication.class, args); + } +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/client/AIServiceGrpcClient.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/client/AIServiceGrpcClient.java new file mode 100644 index 0000000..5df7e8e --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/client/AIServiceGrpcClient.java @@ -0,0 +1,134 @@ +package cn.meowrain.aioj.backend.aiservice.client; + +import cn.meowrain.aioj.backend.aiservice.grpc.*; +import io.grpc.StatusRuntimeException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +/** + * AI Service gRPC 客户端封装类 + */ +@Slf4j +@Component +public class AIServiceGrpcClient { + + private final AIServiceGrpc.AIServiceBlockingStub blockingStub; + private final AIServiceGrpc.AIServiceStub asyncStub; + + public AIServiceGrpcClient( + @Qualifier("aiServiceBlockingStub") AIServiceGrpc.AIServiceBlockingStub blockingStub, + @Qualifier("aiServiceStub") AIServiceGrpc.AIServiceStub asyncStub) { + this.blockingStub = blockingStub; + this.asyncStub = asyncStub; + } + + /** + * 分析代码 + */ + public AnalyzeCodeResponse analyzeCode(String code, String language, String questionId, String userId) { + try { + log.info("Calling gRPC analyzeCode for language: {}, questionId: {}", language, questionId); + + AnalyzeCodeRequest request = AnalyzeCodeRequest.newBuilder() + .setCode(code) + .setLanguage(language) + .setQuestionId(questionId) + .setUserId(userId) + .build(); + + AnalyzeCodeResponse response = blockingStub.analyzeCode(request); + + log.info("gRPC analyzeCode response: success={}", response.getSuccess()); + return response; + + } catch (StatusRuntimeException e) { + log.error("gRPC analyzeCode failed: {}", e.getStatus(), e); + return AnalyzeCodeResponse.newBuilder() + .setSuccess(false) + .setMessage("gRPC 调用失败: " + e.getStatus().getDescription()) + .build(); + } + } + + /** + * 优化代码 + */ + public OptimizeCodeResponse optimizeCode(String code, String language, String optimizationType) { + try { + log.info("Calling gRPC optimizeCode for language: {}, type: {}", language, optimizationType); + + OptimizeCodeRequest request = OptimizeCodeRequest.newBuilder() + .setCode(code) + .setLanguage(language) + .setOptimizationType(optimizationType) + .build(); + + OptimizeCodeResponse response = blockingStub.optimizeCode(request); + + log.info("gRPC optimizeCode response: success={}", response.getSuccess()); + return response; + + } catch (StatusRuntimeException e) { + log.error("gRPC optimizeCode failed: {}", e.getStatus(), e); + return OptimizeCodeResponse.newBuilder() + .setSuccess(false) + .setMessage("gRPC 调用失败: " + e.getStatus().getDescription()) + .build(); + } + } + + /** + * 生成测试用例 + */ + public GenerateTestCasesResponse generateTestCases(String code, String language, String problemDescription) { + try { + log.info("Calling gRPC generateTestCases for language: {}", language); + + GenerateTestCasesRequest request = GenerateTestCasesRequest.newBuilder() + .setCode(code) + .setLanguage(language) + .setProblemDescription(problemDescription) + .build(); + + GenerateTestCasesResponse response = blockingStub.generateTestCases(request); + + log.info("gRPC generateTestCases response: success={}", response.getSuccess()); + return response; + + } catch (StatusRuntimeException e) { + log.error("gRPC generateTestCases failed: {}", e.getStatus(), e); + return GenerateTestCasesResponse.newBuilder() + .setSuccess(false) + .setMessage("gRPC 调用失败: " + e.getStatus().getDescription()) + .build(); + } + } + + /** + * 解释代码 + */ + public ExplainCodeResponse explainCode(String code, String language, String detailLevel) { + try { + log.info("Calling gRPC explainCode for language: {}, level: {}", language, detailLevel); + + ExplainCodeRequest request = ExplainCodeRequest.newBuilder() + .setCode(code) + .setLanguage(language) + .setDetailLevel(detailLevel) + .build(); + + ExplainCodeResponse response = blockingStub.explainCode(request); + + log.info("gRPC explainCode response: success={}", response.getSuccess()); + return response; + + } catch (StatusRuntimeException e) { + log.error("gRPC explainCode failed: {}", e.getStatus(), e); + return ExplainCodeResponse.newBuilder() + .setSuccess(false) + .setMessage("gRPC 调用失败: " + e.getStatus().getDescription()) + .build(); + } + } +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/config/GrpcClientConfiguration.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/config/GrpcClientConfiguration.java new file mode 100644 index 0000000..e1e100c --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/config/GrpcClientConfiguration.java @@ -0,0 +1,88 @@ +package cn.meowrain.aioj.backend.aiservice.config; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import cn.meowrain.aioj.backend.aiservice.grpc.AIServiceGrpc; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PreDestroy; +import java.util.concurrent.TimeUnit; + +/** + * gRPC 客户端配置 + */ +@Slf4j +@Configuration +public class GrpcClientConfiguration { + + private final GrpcClientProperties properties; + private ManagedChannel channel; + + public GrpcClientConfiguration(GrpcClientProperties properties) { + this.properties = properties; + } + + /** + * 创建 gRPC ManagedChannel + */ + @Bean + @Qualifier("aiServiceChannel") + public ManagedChannel aiServiceChannel() { + log.info("Initializing gRPC channel to {}:{}", properties.getHost(), properties.getPort()); + + NettyChannelBuilder builder = NettyChannelBuilder + .forAddress(properties.getHost(), properties.getPort()) + .maxInboundMessageSize(properties.getMaxMessageSize()); + + if (properties.isTlsEnabled()) { + builder.useTransportSecurity(); + } else { + builder.usePlaintext(); + } + + this.channel = builder.build(); + return channel; + } + + /** + * 创建 AI Service gRPC 客户端存根 + */ + @Bean + public AIServiceGrpc.AIServiceBlockingStub aiServiceBlockingStub( + @Qualifier("aiServiceChannel") ManagedChannel channel) { + log.info("Creating AI Service gRPC blocking stub"); + return AIServiceGrpc.newBlockingStub(channel) + .withDeadlineAfter(properties.getTimeout(), TimeUnit.SECONDS); + } + + /** + * 创建 AI Service gRPC 异步客户端存根 + */ + @Bean + public AIServiceGrpc.AIServiceStub aiServiceStub( + @Qualifier("aiServiceChannel") ManagedChannel channel) { + log.info("Creating AI Service gRPC async stub"); + return AIServiceGrpc.newStub(channel) + .withDeadlineAfter(properties.getTimeout(), TimeUnit.SECONDS); + } + + /** + * 应用关闭时清理资源 + */ + @PreDestroy + public void destroy() { + if (channel != null && !channel.isShutdown()) { + log.info("Shutting down gRPC channel"); + try { + channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("Error shutting down gRPC channel", e); + channel.shutdownNow(); + } + } + } +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/config/GrpcClientProperties.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/config/GrpcClientProperties.java new file mode 100644 index 0000000..d46d39f --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/config/GrpcClientProperties.java @@ -0,0 +1,77 @@ +package cn.meowrain.aioj.backend.aiservice.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * gRPC 客户端配置属性 + */ +@Component +@ConfigurationProperties(prefix = "grpc.client") +public class GrpcClientProperties { + + /** + * gRPC 服务器地址 + */ + private String host = "localhost"; + + /** + * gRPC 服务器端口 + */ + private int port = 50051; + + /** + * 连接超时时间(秒) + */ + private int timeout = 10; + + /** + * 是否启用 TLS + */ + private boolean tlsEnabled = false; + + /** + * 最大消息大小(字节) + */ + private int maxMessageSize = 10 * 1024 * 1024; // 10MB + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public boolean isTlsEnabled() { + return tlsEnabled; + } + + public void setTlsEnabled(boolean tlsEnabled) { + this.tlsEnabled = tlsEnabled; + } + + public int getMaxMessageSize() { + return maxMessageSize; + } + + public void setMaxMessageSize(int maxMessageSize) { + this.maxMessageSize = maxMessageSize; + } +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/config/SwaggerConfiguration.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/config/SwaggerConfiguration.java new file mode 100644 index 0000000..fdab580 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/config/SwaggerConfiguration.java @@ -0,0 +1,30 @@ +package cn.meowrain.aioj.backend.aiservice.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Swagger 配置 + */ +@Configuration +public class SwaggerConfiguration { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("AIOJ AI 服务 API") + .version("1.0.0") + .description("AI 代码分析、优化、测试用例生成等服务接口") + .contact(new Contact() + .name("AIOJ Team") + .email("contact@aioj.com")) + .license(new License() + .name("Apache 2.0") + .url("https://www.apache.org/licenses/LICENSE-2.0.html"))); + } +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/controller/AIController.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/controller/AIController.java new file mode 100644 index 0000000..986f138 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/controller/AIController.java @@ -0,0 +1,58 @@ +package cn.meowrain.aioj.backend.aiservice.controller; + +import cn.meowrain.aioj.backend.aiservice.dto.req.*; +import cn.meowrain.aioj.backend.aiservice.dto.resp.*; +import cn.meowrain.aioj.backend.aiservice.service.AIService; +import cn.meowrain.aioj.backend.framework.core.web.Result; +import cn.meowrain.aioj.backend.framework.core.web.Results; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * AI 服务控制器 + */ +@Tag(name = "AI 服务", description = "AI 代码分析、优化、测试用例生成等接口") +@RestController +@RequestMapping("/ai") +@RequiredArgsConstructor +public class AIController { + + private final AIService aiService; + + @PostMapping("/analyze") + @Operation(summary = "分析代码", description = "对提交的代码进行分析,包括复杂度、性能、可读性等评分") + public Result analyzeCode(@Valid @RequestBody AnalyzeCodeReqDTO request) { + AnalyzeCodeRespDTO response = (AnalyzeCodeRespDTO) aiService.analyzeCode(request); + return Results.success(response); + } + + @PostMapping("/optimize") + @Operation(summary = "优化代码", description = "对代码进行优化,提升性能、可读性或内存使用") + public Result optimizeCode(@Valid @RequestBody OptimizeCodeReqDTO request) { + OptimizeCodeRespDTO response = (OptimizeCodeRespDTO) aiService.optimizeCode(request); + return Results.success(response); + } + + @PostMapping("/test-cases") + @Operation(summary = "生成测试用例", description = "根据代码和问题描述自动生成测试用例") + public Result generateTestCases(@Valid @RequestBody GenerateTestCasesReqDTO request) { + GenerateTestCasesRespDTO response = (GenerateTestCasesRespDTO) aiService.generateTestCases(request); + return Results.success(response); + } + + @PostMapping("/explain") + @Operation(summary = "解释代码", description = "对代码进行详细解释,帮助理解代码逻辑") + public Result explainCode(@Valid @RequestBody ExplainCodeReqDTO request) { + ExplainCodeRespDTO response = (ExplainCodeRespDTO) aiService.explainCode(request); + return Results.success(response); + } + + @GetMapping("/health") + @Operation(summary = "健康检查", description = "检查 AI 服务是否正常运行") + public Result health() { + return Results.success("AI Service is running"); + } +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/AnalyzeCodeReqDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/AnalyzeCodeReqDTO.java new file mode 100644 index 0000000..fce2809 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/AnalyzeCodeReqDTO.java @@ -0,0 +1,27 @@ +package cn.meowrain.aioj.backend.aiservice.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 代码分析请求 DTO + */ +@Data +@Schema(description = "代码分析请求") +public class AnalyzeCodeReqDTO { + + @NotBlank(message = "代码不能为空") + @Schema(description = "代码内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "def hello():\n print('Hello World')") + private String code; + + @NotBlank(message = "编程语言不能为空") + @Schema(description = "编程语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "python") + private String language; + + @Schema(description = "题目ID", example = "1001") + private String questionId; + + @Schema(description = "用户ID", example = "user123") + private String userId; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/ExplainCodeReqDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/ExplainCodeReqDTO.java new file mode 100644 index 0000000..453b5f5 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/ExplainCodeReqDTO.java @@ -0,0 +1,24 @@ +package cn.meowrain.aioj.backend.aiservice.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 代码解释请求 DTO + */ +@Data +@Schema(description = "代码解释请求") +public class ExplainCodeReqDTO { + + @NotBlank(message = "代码不能为空") + @Schema(description = "代码内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotBlank(message = "编程语言不能为空") + @Schema(description = "编程语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "python") + private String language; + + @Schema(description = "详细程度", example = "normal", allowableValues = {"brief", "normal", "detailed"}) + private String detailLevel = "normal"; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/GenerateTestCasesReqDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/GenerateTestCasesReqDTO.java new file mode 100644 index 0000000..52dc03d --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/GenerateTestCasesReqDTO.java @@ -0,0 +1,25 @@ +package cn.meowrain.aioj.backend.aiservice.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 生成测试用例请求 DTO + */ +@Data +@Schema(description = "生成测试用例请求") +public class GenerateTestCasesReqDTO { + + @NotBlank(message = "代码不能为空") + @Schema(description = "代码内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotBlank(message = "编程语言不能为空") + @Schema(description = "编程语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "python") + private String language; + + @NotBlank(message = "问题描述不能为空") + @Schema(description = "问题描述", requiredMode = Schema.RequiredMode.REQUIRED) + private String problemDescription; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/OptimizeCodeReqDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/OptimizeCodeReqDTO.java new file mode 100644 index 0000000..b764d17 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/req/OptimizeCodeReqDTO.java @@ -0,0 +1,24 @@ +package cn.meowrain.aioj.backend.aiservice.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 代码优化请求 DTO + */ +@Data +@Schema(description = "代码优化请求") +public class OptimizeCodeReqDTO { + + @NotBlank(message = "代码不能为空") + @Schema(description = "代码内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @NotBlank(message = "编程语言不能为空") + @Schema(description = "编程语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "python") + private String language; + + @Schema(description = "优化类型", example = "performance", allowableValues = {"performance", "readability", "memory"}) + private String optimizationType = "performance"; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/AnalyzeCodeRespDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/AnalyzeCodeRespDTO.java new file mode 100644 index 0000000..eb89b70 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/AnalyzeCodeRespDTO.java @@ -0,0 +1,27 @@ +package cn.meowrain.aioj.backend.aiservice.dto.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 代码分析响应 DTO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "代码分析响应") +public class AnalyzeCodeRespDTO { + + @Schema(description = "是否成功") + private Boolean success; + + @Schema(description = "响应消息") + private String message; + + @Schema(description = "分析结果") + private CodeAnalysisRespDTO analysis; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/CodeAnalysisRespDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/CodeAnalysisRespDTO.java new file mode 100644 index 0000000..db0c078 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/CodeAnalysisRespDTO.java @@ -0,0 +1,44 @@ +package cn.meowrain.aioj.backend.aiservice.dto.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 代码分析结果 DTO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "代码分析结果") +public class CodeAnalysisRespDTO { + + @Schema(description = "发现的问题") + private List issues; + + @Schema(description = "改进建议") + private List suggestions; + + @Schema(description = "复杂度评分 (0-100)") + private Integer complexityScore; + + @Schema(description = "性能评分 (0-100)") + private Integer performanceScore; + + @Schema(description = "可读性评分 (0-100)") + private Integer readabilityScore; + + @Schema(description = "时间复杂度") + private String timeComplexity; + + @Schema(description = "空间复杂度") + private String spaceComplexity; + + @Schema(description = "最佳实践建议") + private List bestPractices; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/ExplainCodeRespDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/ExplainCodeRespDTO.java new file mode 100644 index 0000000..ac93838 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/ExplainCodeRespDTO.java @@ -0,0 +1,32 @@ +package cn.meowrain.aioj.backend.aiservice.dto.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 代码解释响应 DTO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "代码解释响应") +public class ExplainCodeRespDTO { + + @Schema(description = "是否成功") + private Boolean success; + + @Schema(description = "响应消息") + private String message; + + @Schema(description = "代码解释") + private String explanation; + + @Schema(description = "关键点") + private List keyPoints; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/GenerateTestCasesRespDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/GenerateTestCasesRespDTO.java new file mode 100644 index 0000000..644dbe2 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/GenerateTestCasesRespDTO.java @@ -0,0 +1,29 @@ +package cn.meowrain.aioj.backend.aiservice.dto.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 生成测试用例响应 DTO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "生成测试用例响应") +public class GenerateTestCasesRespDTO { + + @Schema(description = "是否成功") + private Boolean success; + + @Schema(description = "响应消息") + private String message; + + @Schema(description = "测试用例列表") + private List testCases; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/OptimizeCodeRespDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/OptimizeCodeRespDTO.java new file mode 100644 index 0000000..6b150e4 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/OptimizeCodeRespDTO.java @@ -0,0 +1,32 @@ +package cn.meowrain.aioj.backend.aiservice.dto.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 代码优化响应 DTO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "代码优化响应") +public class OptimizeCodeRespDTO { + + @Schema(description = "是否成功") + private Boolean success; + + @Schema(description = "响应消息") + private String message; + + @Schema(description = "优化后的代码") + private String optimizedCode; + + @Schema(description = "改进说明") + private List improvements; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/TestCaseDTO.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/TestCaseDTO.java new file mode 100644 index 0000000..31d7d2a --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/dto/resp/TestCaseDTO.java @@ -0,0 +1,27 @@ +package cn.meowrain.aioj.backend.aiservice.dto.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 测试用例 DTO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "测试用例") +public class TestCaseDTO { + + @Schema(description = "输入") + private String input; + + @Schema(description = "预期输出") + private String expectedOutput; + + @Schema(description = "描述") + private String description; +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/grpc/AIServiceGrpc.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/grpc/AIServiceGrpc.java new file mode 100644 index 0000000..5cc710b --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/grpc/AIServiceGrpc.java @@ -0,0 +1,137 @@ +package cn.meowrain.aioj.backend.aiservice.grpc; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.MethodDescriptor; +import io.grpc.stub.AbstractStub; +import io.grpc.stub.ClientCalls; +import io.grpc.stub.StreamObserver; + +/** + * AI Service gRPC 接口 + */ +public class AIServiceGrpc { + + private AIServiceGrpc() {} + + public static final String SERVICE_NAME = "ai.service.AIService"; + + // 创建阻塞存根 + public static AIServiceBlockingStub newBlockingStub(Channel channel) { + return new AIServiceBlockingStub(channel); + } + + // 创建异步存根 + public static AIServiceStub newStub(Channel channel) { + return new AIServiceStub(channel); + } + + /** + * 阻塞存根 + */ + public static final class AIServiceBlockingStub extends AbstractStub { + + private AIServiceBlockingStub(Channel channel) { + super(channel); + } + + private AIServiceBlockingStub(Channel channel, CallOptions callOptions) { + super(channel, callOptions); + } + + @Override + protected AIServiceBlockingStub build(Channel channel, CallOptions callOptions) { + return new AIServiceBlockingStub(channel, callOptions); + } + + /** + * 分析代码 + */ + public AIServiceProto.AnalyzeCodeResponse analyzeCode(AIServiceProto.AnalyzeCodeRequest request) { + // 注意:这里需要真实的 gRPC 服务器才能调用 + // 如果没有连接服务器,会抛出 StatusRuntimeException + throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNAVAILABLE); + } + + /** + * 优化代码 + */ + public AIServiceProto.OptimizeCodeResponse optimizeCode(AIServiceProto.OptimizeCodeRequest request) { + throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNAVAILABLE); + } + + /** + * 生成测试用例 + */ + public AIServiceProto.GenerateTestCasesResponse generateTestCases(AIServiceProto.GenerateTestCasesRequest request) { + throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNAVAILABLE); + } + + /** + * 解释代码 + */ + public AIServiceProto.ExplainCodeResponse explainCode(AIServiceProto.ExplainCodeRequest request) { + throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNAVAILABLE); + } + } + + /** + * 异步存根 + */ + public static final class AIServiceStub extends AbstractStub { + + private AIServiceStub(Channel channel) { + super(channel); + } + + private AIServiceStub(Channel channel, CallOptions callOptions) { + super(channel, callOptions); + } + + @Override + protected AIServiceStub build(Channel channel, CallOptions callOptions) { + return new AIServiceStub(channel, callOptions); + } + + /** + * 分析代码(异步) + */ + public void analyzeCode(AIServiceProto.AnalyzeCodeRequest request, + StreamObserver responseObserver) { + // 异步调用实现 + } + + /** + * 优化代码(异步) + */ + public void optimizeCode(AIServiceProto.OptimizeCodeRequest request, + StreamObserver responseObserver) { + // 异步调用实现 + } + + /** + * 生成测试用例(异步) + */ + public void generateTestCases(AIServiceProto.GenerateTestCasesRequest request, + StreamObserver responseObserver) { + // 异步调用实现 + } + + /** + * 解释代码(异步) + */ + public void explainCode(AIServiceProto.ExplainCodeRequest request, + StreamObserver responseObserver) { + // 异步调用实现 + } + } + + /** + * StreamObserver 接口(简化版) + */ + public interface StreamObserver { + void onNext(V value); + void onError(Throwable t); + void onCompleted(); + } +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/grpc/AIServiceProto.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/grpc/AIServiceProto.java new file mode 100644 index 0000000..dbeb5c0 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/grpc/AIServiceProto.java @@ -0,0 +1,478 @@ +package cn.meowrain.aioj.backend.aiservice.grpc; + +/** + * AI Service Proto 外部类 + */ +public final class AIServiceProto { + private AIServiceProto() {} + + // 代码分析请求 + public static final class AnalyzeCodeRequest extends + com.google.protobuf.GeneratedMessageV3 implements + com.google.protobuf.Message { + + private AnalyzeCodeRequest() { + this.code = ""; + this.language = ""; + this.questionId = ""; + this.userId = ""; + } + + private String code; + private String language; + private String questionId; + private String userId; + + public String getCode() { return code; } + public String getLanguage() { return language; } + public String getQuestionId() { return questionId; } + public String getUserId() { return userId; } + + @Override + public com.google.protobuf.Parser getParserForType() { + return null; + } + + public static AnalyzeCodeRequest getDefaultInstance() { + return new AnalyzeCodeRequest(); + } + + public static AnalyzeCodeRequest newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final AnalyzeCodeRequest result = new AnalyzeCodeRequest(); + + public Builder setCode(String value) { + result.code = value; + return this; + } + + public Builder setLanguage(String value) { + result.language = value; + return this; + } + + public Builder setQuestionId(String value) { + result.questionId = value; + return this; + } + + public Builder setUserId(String value) { + result.userId = value; + return this; + } + + public AnalyzeCodeRequest build() { + return result; + } + } + } + + // 代码分析响应 + public static final class AnalyzeCodeResponse extends + com.google.protobuf.GeneratedMessageV3 { + + private AnalyzeCodeResponse() { + this.success = false; + this.message = ""; + } + + private boolean success; + private String message; + private CodeAnalysis analysis; + + public boolean getSuccess() { return success; } + public String getMessage() { return message; } + public boolean hasAnalysis() { return analysis != null; } + public CodeAnalysis getAnalysis() { return analysis; } + + public static AnalyzeCodeResponse newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final AnalyzeCodeResponse result = new AnalyzeCodeResponse(); + + public Builder setSuccess(boolean value) { + result.success = value; + return this; + } + + public Builder setMessage(String value) { + result.message = value; + return this; + } + + public Builder setAnalysis(CodeAnalysis value) { + result.analysis = value; + return this; + } + + public AnalyzeCodeResponse build() { + return result; + } + } + } + + // 代码分析结果 + public static final class CodeAnalysis extends + com.google.protobuf.GeneratedMessageV3 { + + private CodeAnalysis() {} + + public java.util.List getIssuesList() { return java.util.Collections.emptyList(); } + public java.util.List getSuggestionsList() { return java.util.Collections.emptyList(); } + public int getComplexityScore() { return 0; } + public int getPerformanceScore() { return 0; } + public int getReadabilityScore() { return 0; } + public String getTimeComplexity() { return ""; } + public String getSpaceComplexity() { return ""; } + public java.util.List getBestPracticesList() { return java.util.Collections.emptyList(); } + + public static CodeAnalysis newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final CodeAnalysis result = new CodeAnalysis(); + public CodeAnalysis build() { return result; } + } + } + + // 代码优化请求 + public static final class OptimizeCodeRequest extends + com.google.protobuf.GeneratedMessageV3 { + + private OptimizeCodeRequest() { + this.code = ""; + this.language = ""; + this.optimizationType = ""; + } + + private String code; + private String language; + private String optimizationType; + + public String getCode() { return code; } + public String getLanguage() { return language; } + public String getOptimizationType() { return optimizationType; } + + public static OptimizeCodeRequest newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final OptimizeCodeRequest result = new OptimizeCodeRequest(); + + public Builder setCode(String value) { + result.code = value; + return this; + } + + public Builder setLanguage(String value) { + result.language = value; + return this; + } + + public Builder setOptimizationType(String value) { + result.optimizationType = value; + return this; + } + + public OptimizeCodeRequest build() { + return result; + } + } + } + + // 代码优化响应 + public static final class OptimizeCodeResponse extends + com.google.protobuf.GeneratedMessageV3 { + + private OptimizeCodeResponse() { + this.success = false; + this.message = ""; + this.optimizedCode = ""; + } + + private boolean success; + private String message; + private String optimizedCode; + private java.util.List improvements = java.util.Collections.emptyList(); + + public boolean getSuccess() { return success; } + public String getMessage() { return message; } + public String getOptimizedCode() { return optimizedCode; } + public java.util.List getImprovementsList() { return improvements; } + + public static OptimizeCodeResponse newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final OptimizeCodeResponse result = new OptimizeCodeResponse(); + + public Builder setSuccess(boolean value) { + result.success = value; + return this; + } + + public Builder setMessage(String value) { + result.message = value; + return this; + } + + public Builder setOptimizedCode(String value) { + result.optimizedCode = value; + return this; + } + + public Builder setImprovementsList(java.util.List value) { + result.improvements = value; + return this; + } + + public OptimizeCodeResponse build() { + return result; + } + } + } + + // 生成测试用例请求 + public static final class GenerateTestCasesRequest extends + com.google.protobuf.GeneratedMessageV3 { + + private GenerateTestCasesRequest() { + this.code = ""; + this.language = ""; + this.problemDescription = ""; + } + + private String code; + private String language; + private String problemDescription; + + public String getCode() { return code; } + public String getLanguage() { return language; } + public String getProblemDescription() { return problemDescription; } + + public static GenerateTestCasesRequest newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final GenerateTestCasesRequest result = new GenerateTestCasesRequest(); + + public Builder setCode(String value) { + result.code = value; + return this; + } + + public Builder setLanguage(String value) { + result.language = value; + return this; + } + + public Builder setProblemDescription(String value) { + result.problemDescription = value; + return this; + } + + public GenerateTestCasesRequest build() { + return result; + } + } + } + + // 生成测试用例响应 + public static final class GenerateTestCasesResponse extends + com.google.protobuf.GeneratedMessageV3 { + + private GenerateTestCasesResponse() { + this.success = false; + this.message = ""; + } + + private boolean success; + private String message; + private java.util.List testCases = java.util.Collections.emptyList(); + + public boolean getSuccess() { return success; } + public String getMessage() { return message; } + public java.util.List getTestCasesList() { return testCases; } + + public static GenerateTestCasesResponse newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final GenerateTestCasesResponse result = new GenerateTestCasesResponse(); + + public Builder setSuccess(boolean value) { + result.success = value; + return this; + } + + public Builder setMessage(String value) { + result.message = value; + return this; + } + + public Builder setTestCasesList(java.util.List value) { + result.testCases = value; + return this; + } + + public GenerateTestCasesResponse build() { + return result; + } + } + } + + // 测试用例 + public static final class TestCase extends + com.google.protobuf.GeneratedMessageV3 { + + private TestCase() { + this.input = ""; + this.expectedOutput = ""; + this.description = ""; + } + + private String input; + private String expectedOutput; + private String description; + + public String getInput() { return input; } + public String getExpectedOutput() { return expectedOutput; } + public String getDescription() { return description; } + + public static TestCase newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final TestCase result = new TestCase(); + + public Builder setInput(String value) { + result.input = value; + return this; + } + + public Builder setExpectedOutput(String value) { + result.expectedOutput = value; + return this; + } + + public Builder setDescription(String value) { + result.description = value; + return this; + } + + public TestCase build() { + return result; + } + } + } + + // 代码解释请求 + public static final class ExplainCodeRequest extends + com.google.protobuf.GeneratedMessageV3 { + + private ExplainCodeRequest() { + this.code = ""; + this.language = ""; + this.detailLevel = ""; + } + + private String code; + private String language; + private String detailLevel; + + public String getCode() { return code; } + public String getLanguage() { return language; } + public String getDetailLevel() { return detailLevel; } + + public static ExplainCodeRequest newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final ExplainCodeRequest result = new ExplainCodeRequest(); + + public Builder setCode(String value) { + result.code = value; + return this; + } + + public Builder setLanguage(String value) { + result.language = value; + return this; + } + + public Builder setDetailLevel(String value) { + result.detailLevel = value; + return this; + } + + public ExplainCodeRequest build() { + return result; + } + } + } + + // 代码解释响应 + public static final class ExplainCodeResponse extends + com.google.protobuf.GeneratedMessageV3 { + + private ExplainCodeResponse() { + this.success = false; + this.message = ""; + this.explanation = ""; + } + + private boolean success; + private String message; + private String explanation; + private java.util.List keyPoints = java.util.Collections.emptyList(); + + public boolean getSuccess() { return success; } + public String getMessage() { return message; } + public String getExplanation() { return explanation; } + public java.util.List getKeyPointsList() { return keyPoints; } + + public static ExplainCodeResponse newBuilder() { + return new Builder(); + } + + public static final class Builder { + private final ExplainCodeResponse result = new ExplainCodeResponse(); + + public Builder setSuccess(boolean value) { + result.success = value; + return this; + } + + public Builder setMessage(String value) { + result.message = value; + return this; + } + + public Builder setExplanation(String value) { + result.explanation = value; + return this; + } + + public Builder setKeyPointsList(java.util.List value) { + result.keyPoints = value; + return this; + } + + public ExplainCodeResponse build() { + return result; + } + } + } +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/service/AIService.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/service/AIService.java new file mode 100644 index 0000000..7654bc7 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/service/AIService.java @@ -0,0 +1,29 @@ +package cn.meowrain.aioj.backend.aiservice.service; + +import cn.meowrain.aioj.backend.aiservice.dto.req.*; + +/** + * AI 服务接口 + */ +public interface AIService { + + /** + * 分析代码 + */ + Object analyzeCode(AnalyzeCodeReqDTO request); + + /** + * 优化代码 + */ + Object optimizeCode(OptimizeCodeReqDTO request); + + /** + * 生成测试用例 + */ + Object generateTestCases(GenerateTestCasesReqDTO request); + + /** + * 解释代码 + */ + Object explainCode(ExplainCodeReqDTO request); +} diff --git a/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/service/impl/AIServiceImpl.java b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/service/impl/AIServiceImpl.java new file mode 100644 index 0000000..6aed5e0 --- /dev/null +++ b/aioj-backend-ai-service/src/main/java/cn/meowrain/aioj/backend/aiservice/service/impl/AIServiceImpl.java @@ -0,0 +1,119 @@ +package cn.meowrain.aioj.backend.aiservice.service.impl; + +import cn.meowrain.aioj.backend.aiservice.client.AIServiceGrpcClient; +import cn.meowrain.aioj.backend.aiservice.dto.req.*; +import cn.meowrain.aioj.backend.aiservice.dto.resp.*; +import cn.meowrain.aioj.backend.aiservice.grpc.*; +import cn.meowrain.aioj.backend.aiservice.service.AIService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.stream.Collectors; + +/** + * AI 服务实现 + */ +@Slf4j +@Service +public class AIServiceImpl implements AIService { + + private final AIServiceGrpcClient grpcClient; + + public AIServiceImpl(AIServiceGrpcClient grpcClient) { + this.grpcClient = grpcClient; + } + + @Override + public AnalyzeCodeRespDTO analyzeCode(AnalyzeCodeReqDTO request) { + log.info("Analyzing code for language: {}, questionId: {}", request.getLanguage(), request.getQuestionId()); + + AnalyzeCodeResponse grpcResponse = grpcClient.analyzeCode( + request.getCode(), + request.getLanguage(), + request.getQuestionId() != null ? request.getQuestionId() : "", + request.getUserId() != null ? request.getUserId() : "" + ); + + CodeAnalysisRespDTO analysis = null; + if (grpcResponse.getSuccess() && grpcResponse.hasAnalysis()) { + CodeAnalysis grpcAnalysis = grpcResponse.getAnalysis(); + analysis = CodeAnalysisRespDTO.builder() + .issues(grpcAnalysis.getIssuesList()) + .suggestions(grpcAnalysis.getSuggestionsList()) + .complexityScore(grpcAnalysis.getComplexityScore()) + .performanceScore(grpcAnalysis.getPerformanceScore()) + .readabilityScore(grpcAnalysis.getReadabilityScore()) + .timeComplexity(grpcAnalysis.getTimeComplexity()) + .spaceComplexity(grpcAnalysis.getSpaceComplexity()) + .bestPractices(grpcAnalysis.getBestPracticesList()) + .build(); + } + + return AnalyzeCodeRespDTO.builder() + .success(grpcResponse.getSuccess()) + .message(grpcResponse.getMessage()) + .analysis(analysis) + .build(); + } + + @Override + public OptimizeCodeRespDTO optimizeCode(OptimizeCodeReqDTO request) { + log.info("Optimizing code for language: {}, type: {}", request.getLanguage(), request.getOptimizationType()); + + OptimizeCodeResponse grpcResponse = grpcClient.optimizeCode( + request.getCode(), + request.getLanguage(), + request.getOptimizationType() != null ? request.getOptimizationType() : "performance" + ); + + return OptimizeCodeRespDTO.builder() + .success(grpcResponse.getSuccess()) + .message(grpcResponse.getMessage()) + .optimizedCode(grpcResponse.getOptimizedCode()) + .improvements(grpcResponse.getImprovementsList()) + .build(); + } + + @Override + public GenerateTestCasesRespDTO generateTestCases(GenerateTestCasesReqDTO request) { + log.info("Generating test cases for language: {}", request.getLanguage()); + + GenerateTestCasesResponse grpcResponse = grpcClient.generateTestCases( + request.getCode(), + request.getLanguage(), + request.getProblemDescription() + ); + + var testCases = grpcResponse.getTestCasesList().stream() + .map(tc -> TestCaseDTO.builder() + .input(tc.getInput()) + .expectedOutput(tc.getExpectedOutput()) + .description(tc.getDescription()) + .build()) + .collect(Collectors.toList()); + + return GenerateTestCasesRespDTO.builder() + .success(grpcResponse.getSuccess()) + .message(grpcResponse.getMessage()) + .testCases(testCases) + .build(); + } + + @Override + public ExplainCodeRespDTO explainCode(ExplainCodeReqDTO request) { + log.info("Explaining code for language: {}, level: {}", request.getLanguage(), request.getDetailLevel()); + + ExplainCodeResponse grpcResponse = grpcClient.explainCode( + request.getCode(), + request.getLanguage(), + request.getDetailLevel() != null ? request.getDetailLevel() : "normal" + ); + + return ExplainCodeRespDTO.builder() + .success(grpcResponse.getSuccess()) + .message(grpcResponse.getMessage()) + .explanation(grpcResponse.getExplanation()) + .keyPoints(grpcResponse.getKeyPointsList()) + .build(); + } +} diff --git a/aioj-backend-ai-service/src/main/proto/ai_service.proto b/aioj-backend-ai-service/src/main/proto/ai_service.proto new file mode 100644 index 0000000..1167aa3 --- /dev/null +++ b/aioj-backend-ai-service/src/main/proto/ai_service.proto @@ -0,0 +1,100 @@ +syntax = "proto3"; + +package ai.service; + +option java_multiple_files = true; +option java_package = "cn.meowrain.aioj.backend.aiservice.grpc"; +option java_outer_classname = "AIServiceProto"; + +// AI 代码分析服务 +service AIService { + // 代码分析 + rpc AnalyzeCode(AnalyzeCodeRequest) returns (AnalyzeCodeResponse); + + // 代码优化建议 + rpc OptimizeCode(OptimizeCodeRequest) returns (OptimizeCodeResponse); + + // 生成测试用例 + rpc GenerateTestCases(GenerateTestCasesRequest) returns (GenerateTestCasesResponse); + + // 代码解释 + rpc ExplainCode(ExplainCodeRequest) returns (ExplainCodeResponse); +} + +// 代码分析请求 +message AnalyzeCodeRequest { + string code = 1; // 代码内容 + string language = 2; // 编程语言 (python, java, cpp, etc.) + string question_id = 3; // 题目ID + string user_id = 4; // 用户ID +} + +// 代码分析响应 +message AnalyzeCodeResponse { + bool success = 1; // 是否成功 + string message = 2; // 响应消息 + CodeAnalysis analysis = 3; // 分析结果 +} + +// 代码分析结果 +message CodeAnalysis { + repeated string issues = 1; // 发现的问题 + repeated string suggestions = 2; // 改进建议 + int32 complexity_score = 3; // 复杂度评分 (0-100) + int32 performance_score = 4; // 性能评分 (0-100) + int32 readability_score = 5; // 可读性评分 (0-100) + string time_complexity = 6; // 时间复杂度 + string space_complexity = 7; // 空间复杂度 + repeated string best_practices = 8; // 最佳实践建议 +} + +// 代码优化请求 +message OptimizeCodeRequest { + string code = 1; // 代码内容 + string language = 2; // 编程语言 + string optimization_type = 3; // 优化类型 (performance, readability, memory) +} + +// 代码优化响应 +message OptimizeCodeResponse { + bool success = 1; + string message = 2; + string optimized_code = 3; // 优化后的代码 + repeated string improvements = 4; // 改进说明 +} + +// 生成测试用例请求 +message GenerateTestCasesRequest { + string code = 1; // 代码内容 + string language = 2; // 编程语言 + string problem_description = 3; // 问题描述 +} + +// 生成测试用例响应 +message GenerateTestCasesResponse { + bool success = 1; + string message = 2; + repeated TestCase test_cases = 3; // 测试用例列表 +} + +// 测试用例 +message TestCase { + string input = 1; // 输入 + string expected_output = 2; // 预期输出 + string description = 3; // 描述 +} + +// 代码解释请求 +message ExplainCodeRequest { + string code = 1; // 代码内容 + string language = 2; // 编程语言 + string detail_level = 3; // 详细程度 (brief, normal, detailed) +} + +// 代码解释响应 +message ExplainCodeResponse { + bool success = 1; + string message = 2; + string explanation = 3; // 代码解释 + repeated string key_points = 4; // 关键点 +} diff --git a/aioj-backend-blog-service/README.md b/aioj-backend-blog-service/README.md new file mode 100644 index 0000000..4e2463e --- /dev/null +++ b/aioj-backend-blog-service/README.md @@ -0,0 +1,219 @@ +# AIOJ 博客服务模块 + +## 模块说明 + +`aioj-backend-blog-service` 是 AIOJ 系统的博客服务模块,用于用户发帖、分享技术经验、写文章。 + +## 功能特性 + +- 文章管理:发布、编辑、删除、草稿箱 +- 分类管理:支持多级分类 +- 标签系统:文章标签分类和关联 +- 评论系统:支持多级评论和回复 +- 点赞/收藏:文章和评论点赞、收藏功能 +- 浏览统计:文章浏览记录和统计 +- Markdown 支持:原生支持 Markdown 编辑和渲染 +- 搜索功能:全文搜索文章标题和内容 + +## 技术栈 + +- Spring Boot 3.5.7 +- MyBatis Plus +- MySQL 8.0 +- Redis +- Nacos 服务发现 +- FlexMark (Markdown 处理) +- Knife4j (API 文档) + +## 端口配置 + +- 开发环境: 18086 +- 上下文路径: /api + +## 数据库 + +数据库名称: `aioj_blog` + +初始化 SQL 脚本: `../../db/blog.sql` + +### 核心表结构 + +| 表名 | 说明 | +|------|------| +| blog_article | 文章表 | +| blog_category | 文章分类表 | +| blog_tag | 文章标签表 | +| blog_article_tag | 文章标签关联表 | +| blog_comment | 评论表 | +| blog_like | 点赞表 | +| blog_favorite | 收藏表 | +| blog_view | 浏览记录表 | +| blog_draft | 草稿箱表 | + +## 快速开始 + +### 1. 初始化数据库 + +```bash +mysql -u root -p < ../../db/blog.sql +``` + +### 2. 配置数据库连接 + +编辑 `src/main/resources/application-dev.yml`: + +```yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/aioj_blog + username: your_username + password: your_password +``` + +### 3. 启动服务 + +```bash +mvn spring-boot:run +``` + +或直接运行主类: `BlogServiceApplication.java` + +### 4. 访问 API 文档 + +启动服务后访问: http://localhost:18086/api/doc.html + +## API 接口 + +### 文章相关 + +- `POST /api/article` - 创建文章 +- `PUT /api/article/{id}` - 更新文章 +- `DELETE /api/article/{id}` - 删除文章 +- `GET /api/article/{id}` - 获取文章详情 +- `GET /api/article/list` - 获取文章列表 +- `POST /api/article/publish` - 发布文章 + +### 分类相关 + +- `GET /api/category/list` - 获取分类列表 +- `POST /api/category` - 创建分类(管理员) + +### 标签相关 + +- `GET /api/tag/list` - 获取标签列表 +- `GET /api/tag/hot` - 获取热门标签 + +### 评论相关 + +- `POST /api/comment` - 发表评论 +- `GET /api/comment/article/{articleId}` - 获取文章评论列表 +- `DELETE /api/comment/{id}` - 删除评论 + +## 开发指南 + +### 代码结构 + +``` +aioj-backend-blog-service/ +├── src/main/java/cn/meowrain/aioj/backend/blogservice/ +│ ├── controller/ # 控制器层 +│ ├── service/ # 服务层 +│ │ └── impl/ # 服务实现层 +│ ├── dao/ # 数据访问层 +│ │ ├── mapper/ # MyBatis Mapper +│ │ └── model/ # 数据模型 +│ ├── dto/ # 数据传输对象 +│ ├── vo/ # 视图对象 +│ ├── common/ # 公共类 +│ ├── config/ # 配置类 +│ └── BlogServiceApplication.java +└── src/main/resources/ + ├── mapper/ # MyBatis XML 映射文件 + ├── application.yml # 主配置文件 + ├── application-dev.yml + ├── application-test.yml + ├── application-prod.yml + └── logback-spring.xml # 日志配置 +``` + +### 待实现功能 + +- [ ] 文章 CRUD 接口 +- [ ] 分类管理接口 +- [ ] 标签管理接口 +- [ ] 评论系统接口 +- [ ] 点赞/收藏接口 +- [ ] 文章搜索接口 +- [ ] Markdown 渲染服务 +- [ ] 文章定时发布 +- [ ] 文章审核功能 +- [ ] 用户关注和动态 + +## 更新日志 + +### 2025-01-20 - 初始创建 + +**创建者**: Claude Code + +**工作内容**: + +1. **模块结构搭建** + - 创建完整的 Maven 模块目录结构 + - 配置 `pom.xml`,引入所需依赖 + - 创建主应用类 `BlogServiceApplication.java` + - 更新父 `pom.xml`,添加新模块注册 + +2. **配置文件创建** + - `application.yml` - 主配置文件 + - 服务端口: 18086 + - 上下文路径: /api + - MyBatis Plus 配置 + - Knife4j API 文档配置 + - `application-dev.yml` - 开发环境配置 + - `application-test.yml` - 测试环境配置 + - `application-prod.yml` - 生产环境配置 + - `logback-spring.xml` - 日志配置 + +3. **数据库设计** + - 创建 `db/blog.sql` 数据库初始化脚本 + - 设计 9 张核心表: + - `blog_article` - 文章表(支持草稿、发布、下架等状态) + - `blog_category` - 分类表(支持多级分类) + - `blog_tag` - 标签表 + - `blog_article_tag` - 文章标签关联表 + - `blog_comment` - 评论表(支持多级回复) + - `blog_like` - 点赞表 + - `blog_favorite` - 收藏表 + - `blog_view` - 浏览记录表 + - `blog_draft` - 草稿箱表 + - 添加初始分类和标签数据 + +4. **依赖说明** + - Spring Boot 3.5.7 + - Spring Cloud & Nacos(服务发现) + - MyBatis Plus(ORM) + - FlexMark(Markdown 处理) + - Knife4j(API 文档) + - Redis(缓存) + - MySQL(数据库) + +5. **待实现功能** + - [ ] 文章 CRUD 接口开发 + - [ ] 分类管理接口 + - [ ] 标签管理接口 + - [ ] 评论系统接口 + - [ ] 点赞/收藏接口 + - [ ] 文章搜索接口 + - [ ] Markdown 渲染服务 + - [ ] 文章定时发布 + - [ ] 文章审核功能 + +**注意事项**: +- 确保先执行 `db/blog.sql` 初始化数据库 +- 检查 `application-dev.yml` 中的数据库连接配置 +- JWT 配置需与 `aioj-backend-auth` 保持一致 +- 启动前确保 Nacos 服务已运行 + +## License + +Copyright © 2025 AIOJ Project diff --git a/aioj-backend-blog-service/pom.xml b/aioj-backend-blog-service/pom.xml new file mode 100644 index 0000000..45497ef --- /dev/null +++ b/aioj-backend-blog-service/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + + cn.meowrain.aioj + ai-oj + ${revision} + + + aioj-backend-blog-service + jar + AIOJ 博客服务 - 用于用户发帖、分享技术经验、写文章 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + + + cn.meowrain.aioj + aioj-backend-common-core + + + cn.meowrain.aioj + aioj-backend-common-log + + + cn.meowrain.aioj + aioj-backend-common-mybatis + + + cn.meowrain.aioj + aioj-backend-common-security + + + cn.meowrain.aioj + aioj-backend-common-feign + + + + + org.springframework.boot + spring-boot-starter-web + + + + + cn.hutool + hutool-extra + + + + + com.vladsch.flexmark + flexmark-all + 0.64.8 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + + + + + com.mysql + mysql-connector-j + runtime + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/aioj-backend-blog-service/src/main/java/cn/meowrain/aioj/backend/blogservice/BlogServiceApplication.java b/aioj-backend-blog-service/src/main/java/cn/meowrain/aioj/backend/blogservice/BlogServiceApplication.java new file mode 100644 index 0000000..716a55a --- /dev/null +++ b/aioj-backend-blog-service/src/main/java/cn/meowrain/aioj/backend/blogservice/BlogServiceApplication.java @@ -0,0 +1,19 @@ +package cn.meowrain.aioj.backend.blogservice; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * AIOJ 博客服务应用启动类 + * + * @author AIOJ + */ +@MapperScan("cn.meowrain.aioj.backend.blogservice.dao.mapper") +public class BlogServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(BlogServiceApplication.class, args); + } + +} diff --git a/aioj-backend-blog-service/src/main/java/cn/meowrain/aioj/backend/blogservice/package-info.java b/aioj-backend-blog-service/src/main/java/cn/meowrain/aioj/backend/blogservice/package-info.java new file mode 100644 index 0000000..d591491 --- /dev/null +++ b/aioj-backend-blog-service/src/main/java/cn/meowrain/aioj/backend/blogservice/package-info.java @@ -0,0 +1,10 @@ +/** + * AIOJ 博客服务模块 + *

+ * 提供文章发布、分类管理、标签系统、评论互动等功能 + *

+ * + * @author AIOJ + * @since 1.0.0 + */ +package cn.meowrain.aioj.backend.blogservice; diff --git a/aioj-backend-blog-service/src/main/resources/application-dev.yml b/aioj-backend-blog-service/src/main/resources/application-dev.yml new file mode 100644 index 0000000..ccd18a3 --- /dev/null +++ b/aioj-backend-blog-service/src/main/resources/application-dev.yml @@ -0,0 +1,24 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/aioj_blog?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + username: root + password: root + data: + redis: + host: localhost + port: 6379 + database: 5 + password: + timeout: 10s + lettuce: + pool: + min-idle: 0 + max-idle: 8 + max-active: 8 + max-wait: -1ms + cloud: + nacos: + discovery: + server-addr: localhost:8848 + namespace: aioj-dev diff --git a/aioj-backend-blog-service/src/main/resources/application-prod.yml b/aioj-backend-blog-service/src/main/resources/application-prod.yml new file mode 100644 index 0000000..efc0862 --- /dev/null +++ b/aioj-backend-blog-service/src/main/resources/application-prod.yml @@ -0,0 +1,24 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/aioj_blog?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=true&allowPublicKeyRetrieval=true + username: ${MYSQL_USERNAME:root} + password: ${MYSQL_PASSWORD:prod} + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + database: ${REDIS_DB:7} + password: ${REDIS_PASSWORD:} + timeout: 10s + lettuce: + pool: + min-idle: 0 + max-idle: 8 + max-active: 8 + max-wait: -1ms + cloud: + nacos: + discovery: + server-addr: ${NACOS_ADDR:localhost:8848} + namespace: ${NACOS_NAMESPACE:aioj-prod} diff --git a/aioj-backend-blog-service/src/main/resources/application-test.yml b/aioj-backend-blog-service/src/main/resources/application-test.yml new file mode 100644 index 0000000..d013d02 --- /dev/null +++ b/aioj-backend-blog-service/src/main/resources/application-test.yml @@ -0,0 +1,24 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/aioj_blog?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + username: root + password: test + data: + redis: + host: localhost + port: 6379 + database: 6 + password: + timeout: 10s + lettuce: + pool: + min-idle: 0 + max-idle: 8 + max-active: 8 + max-wait: -1ms + cloud: + nacos: + discovery: + server-addr: localhost:8848 + namespace: aioj-test diff --git a/aioj-backend-blog-service/src/main/resources/application.yml b/aioj-backend-blog-service/src/main/resources/application.yml new file mode 100644 index 0000000..f6643d6 --- /dev/null +++ b/aioj-backend-blog-service/src/main/resources/application.yml @@ -0,0 +1,57 @@ +spring: + application: + name: aioj-blog-service + profiles: + active: @env@ + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + +server: + port: 18086 + servlet: + context-path: /api + error: + include-stacktrace: never + +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + default-flat-param-object: true + swagger-ui: + path: /swagger-ui.html + tags-sorter: alpha + operations-sorter: alpha + group-configs: + - group: 'default' + paths-to-match: '/api/**' + packages-to-scan: cn.meowrain.aioj.backend.blogservice.controller + +knife4j: + basic: + enable: true + setting: + language: zh_cn + +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + mapper-locations: classpath*:/mapper/**/*.xml + +# JWT 配置(必须与 auth-service 保持一致) +jwt: + enabled: true + secret: "12345678901234567890123456789012" # 至少32字节 + access-expire: 900000 # 15分钟 + refresh-expire: 604800000 # 7天 + +aioj: + log: + enabled: true + max-length: 20000 + +logging: + file: + path: ./logs/${spring.application.name} diff --git a/aioj-backend-blog-service/src/main/resources/logback-spring.xml b/aioj-backend-blog-service/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..6d43831 --- /dev/null +++ b/aioj-backend-blog-service/src/main/resources/logback-spring.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + UTF-8 + + + + + ${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.log + + ${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.%d{yyyy-MM-dd}.%i.log.gz + 100MB + 31 + 100GB + + + ${FILE_LOG_PATTERN} + UTF-8 + + + INFO + + + + + ${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.log + + ${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.%d{yyyy-MM-dd}.%i.log.gz + 100MB + 31 + 100GB + + + ${FILE_LOG_PATTERN} + UTF-8 + + + ERROR + ACCEPT + DENY + + + + + 0 + 512 + + + + + 0 + 512 + + + + + + + + + + + + + + + + + + + diff --git a/db/blog.sql b/db/blog.sql new file mode 100644 index 0000000..2b68cd5 --- /dev/null +++ b/db/blog.sql @@ -0,0 +1,224 @@ +-- ============================================ +-- AIOJ 博客服务数据库表结构 +-- 数据库: aioj_blog +-- 描述: 用于用户发帖、分享技术经验、写文章 +-- ============================================ + +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS `aioj_blog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE `aioj_blog`; + +-- ============================================ +-- 1. 文章分类表 +-- ============================================ +DROP TABLE IF EXISTS `blog_category`; +CREATE TABLE `blog_category` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分类ID', + `name` VARCHAR(50) NOT NULL COMMENT '分类名称', + `slug` VARCHAR(50) NOT NULL COMMENT '分类别名(用于URL)', + `description` VARCHAR(200) DEFAULT NULL COMMENT '分类描述', + `icon` VARCHAR(100) DEFAULT NULL COMMENT '分类图标', + `parent_id` BIGINT UNSIGNED DEFAULT 0 COMMENT '父分类ID,0表示顶级分类', + `sort_order` INT DEFAULT 0 COMMENT '排序权重,数值越大越靠前', + `article_count` INT UNSIGNED DEFAULT 0 COMMENT '该分类下的文章数量', + `is_deleted` TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_slug` (`slug`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_sort_order` (`sort_order`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文章分类表'; + +-- ============================================ +-- 2. 文章标签表 +-- ============================================ +DROP TABLE IF EXISTS `blog_tag`; +CREATE TABLE `blog_tag` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '标签ID', + `name` VARCHAR(30) NOT NULL COMMENT '标签名称', + `slug` VARCHAR(30) NOT NULL COMMENT '标签别名(用于URL)', + `description` VARCHAR(200) DEFAULT NULL COMMENT '标签描述', + `color` VARCHAR(7) DEFAULT NULL COMMENT '标签颜色(十六进制)', + `article_count` INT UNSIGNED DEFAULT 0 COMMENT '该标签下的文章数量', + `is_deleted` TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_slug` (`slug`), + KEY `idx_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文章标签表'; + +-- ============================================ +-- 3. 文章表 +-- ============================================ +DROP TABLE IF EXISTS `blog_article`; +CREATE TABLE `blog_article` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '文章ID', + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '作者用户ID', + `category_id` BIGINT UNSIGNED NOT NULL COMMENT '分类ID', + `title` VARCHAR(200) NOT NULL COMMENT '文章标题', + `slug` VARCHAR(200) DEFAULT NULL COMMENT '文章别名(用于URL,唯一)', + `summary` VARCHAR(500) DEFAULT NULL COMMENT '文章摘要', + `cover_image` VARCHAR(500) DEFAULT NULL COMMENT '封面图片URL', + `content` MEDIUMTEXT NOT NULL COMMENT '文章内容(Markdown格式)', + `content_html` MEDIUMTEXT DEFAULT NULL COMMENT '文章内容(HTML格式,缓存用)', + `view_count` BIGINT UNSIGNED DEFAULT 0 COMMENT '浏览次数', + `like_count` INT UNSIGNED DEFAULT 0 COMMENT '点赞数', + `comment_count` INT UNSIGNED DEFAULT 0 COMMENT '评论数', + `favorite_count` INT UNSIGNED DEFAULT 0 COMMENT '收藏数', + `is_top` TINYINT(1) DEFAULT 0 COMMENT '是否置顶:0-否,1-是', + `is_featured` TINYINT(1) DEFAULT 0 COMMENT '是否精选:0-否,1-是', + `is_original` TINYINT(1) DEFAULT 1 COMMENT '是否原创:0-转载,1-原创', + `source_url` VARCHAR(500) DEFAULT NULL COMMENT '转载来源URL', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '文章状态:0-草稿,1-已发布,2-审核中,3-已下架', + `publish_time` DATETIME DEFAULT NULL COMMENT '发布时间', + `is_deleted` TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_slug` (`slug`), + KEY `idx_user_id` (`user_id`), + KEY `idx_category_id` (`category_id`), + KEY `idx_status` (`status`), + KEY `idx_is_top` (`is_top`), + KEY `idx_publish_time` (`publish_time`), + KEY `idx_view_count` (`view_count`), + FULLTEXT KEY `ft_title_content` (`title`, `content`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文章表'; + +-- ============================================ +-- 4. 文章标签关联表(多对多) +-- ============================================ +DROP TABLE IF EXISTS `blog_article_tag`; +CREATE TABLE `blog_article_tag` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '关联ID', + `article_id` BIGINT UNSIGNED NOT NULL COMMENT '文章ID', + `tag_id` BIGINT UNSIGNED NOT NULL COMMENT '标签ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_article_tag` (`article_id`, `tag_id`), + KEY `idx_article_id` (`article_id`), + KEY `idx_tag_id` (`tag_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文章标签关联表'; + +-- ============================================ +-- 5. 评论表 +-- ============================================ +DROP TABLE IF EXISTS `blog_comment`; +CREATE TABLE `blog_comment` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '评论ID', + `article_id` BIGINT UNSIGNED NOT NULL COMMENT '文章ID', + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '评论用户ID', + `parent_id` BIGINT UNSIGNED DEFAULT 0 COMMENT '父评论ID,0表示一级评论', + `reply_user_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '被回复的用户ID', + `content` TEXT NOT NULL COMMENT '评论内容', + `like_count` INT UNSIGNED DEFAULT 0 COMMENT '点赞数', + `reply_count` INT UNSIGNED DEFAULT 0 COMMENT '回复数', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '评论状态:0-待审核,1-已通过,2-已拒绝', + `is_deleted` TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_article_id` (`article_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评论表'; + +-- ============================================ +-- 6. 点赞表 +-- ============================================ +DROP TABLE IF EXISTS `blog_like`; +CREATE TABLE `blog_like` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '点赞ID', + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', + `target_id` BIGINT UNSIGNED NOT NULL COMMENT '目标ID(文章ID或评论ID)', + `target_type` TINYINT NOT NULL COMMENT '目标类型:1-文章,2-评论', + `is_deleted` TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是(取消点赞)', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_target` (`user_id`, `target_id`, `target_type`, `is_deleted`), + KEY `idx_target` (`target_id`, `target_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='点赞表'; + +-- ============================================ +-- 7. 收藏表 +-- ============================================ +DROP TABLE IF EXISTS `blog_favorite`; +CREATE TABLE `blog_favorite` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '收藏ID', + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', + `article_id` BIGINT UNSIGNED NOT NULL COMMENT '文章ID', + `folder_name` VARCHAR(50) DEFAULT '默认收藏夹' COMMENT '收藏夹名称', + `is_deleted` TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是(取消收藏)', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_article` (`user_id`, `article_id`, `is_deleted`), + KEY `idx_article_id` (`article_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收藏表'; + +-- ============================================ +-- 8. 浏览记录表 +-- ============================================ +DROP TABLE IF EXISTS `blog_view`; +CREATE TABLE `blog_view` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '浏览记录ID', + `article_id` BIGINT UNSIGNED NOT NULL COMMENT '文章ID', + `user_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '用户ID(未登录为NULL)', + `ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'IP地址', + `user_agent` VARCHAR(500) DEFAULT NULL COMMENT '用户代理(浏览器信息)', + `duration` INT UNSIGNED DEFAULT 0 COMMENT '浏览时长(秒)', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_article_id` (`article_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='浏览记录表'; + +-- ============================================ +-- 9. 草稿箱表 +-- ============================================ +DROP TABLE IF EXISTS `blog_draft`; +CREATE TABLE `blog_draft` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '草稿ID', + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', + `title` VARCHAR(200) DEFAULT NULL COMMENT '草稿标题', + `content` MEDIUMTEXT DEFAULT NULL COMMENT '草稿内容(Markdown格式)', + `category_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '分类ID', + `cover_image` VARCHAR(500) DEFAULT NULL COMMENT '封面图片URL', + `is_deleted` TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_update_time` (`update_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='草稿箱表'; + +-- ============================================ +-- 初始化数据 +-- ============================================ + +-- 初始化分类 +INSERT INTO `blog_category` (`name`, `slug`, `description`, `icon`, `parent_id`, `sort_order`) VALUES +('技术分享', 'tech', '分享技术经验和心得', 'icon-tech', 0, 100), +('算法学习', 'algorithm', '算法与数据结构学习笔记', 'icon-algo', 0, 90), +('项目实战', 'project', '项目开发实战经验', 'icon-project', 0, 80), +('面试经验', 'interview', '面试经验总结', 'icon-interview', 0, 70), +('学习笔记', 'note', '日常学习笔记', 'icon-note', 0, 60); + +-- 初始化标签 +INSERT INTO `blog_tag` (`name`, `slug`, `description`, `color`) VALUES +('Java', 'java', 'Java编程语言', '#007396'), +('Python', 'python', 'Python编程语言', '#3776AB'), +('Spring', 'spring', 'Spring框架', '#6DB33F'), +('MyBatis', 'mybatis', 'MyBatis持久层框架', '#DC382D'), +('Redis', 'redis', 'Redis缓存', '#D82C20'), +('MySQL', 'mysql', 'MySQL数据库', '#4479A1'), +('LeetCode', 'leetcode', 'LeetCode刷题', '#FFA116'), +('系统设计', 'system-design', '系统设计相关', '#FF6B6B'), +('微服务', 'microservice', '微服务架构', '#009688'), +('前端开发', 'frontend', '前端开发技术', '#F7DF1E');