ai + blog

This commit is contained in:
2026-01-20 17:20:03 +08:00
parent ef6b5cb11e
commit 61fb847ac1
33 changed files with 2346 additions and 1 deletions

View File

@@ -7,7 +7,12 @@
"Bash(mvn dependency:tree:*)", "Bash(mvn dependency:tree:*)",
"Bash(mvn spring-javaformat:apply:*)", "Bash(mvn spring-javaformat:apply:*)",
"Bash(git add:*)", "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:*)"
] ]
} }
} }

1
.idea/encodings.xml generated
View File

@@ -6,6 +6,7 @@
<file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-auth/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-blog-service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-bom/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-bom/src/main/java" charset="UTF-8" />

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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")));
}
}

View File

@@ -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<AnalyzeCodeRespDTO> analyzeCode(@Valid @RequestBody AnalyzeCodeReqDTO request) {
AnalyzeCodeRespDTO response = (AnalyzeCodeRespDTO) aiService.analyzeCode(request);
return Results.success(response);
}
@PostMapping("/optimize")
@Operation(summary = "优化代码", description = "对代码进行优化,提升性能、可读性或内存使用")
public Result<OptimizeCodeRespDTO> optimizeCode(@Valid @RequestBody OptimizeCodeReqDTO request) {
OptimizeCodeRespDTO response = (OptimizeCodeRespDTO) aiService.optimizeCode(request);
return Results.success(response);
}
@PostMapping("/test-cases")
@Operation(summary = "生成测试用例", description = "根据代码和问题描述自动生成测试用例")
public Result<GenerateTestCasesRespDTO> generateTestCases(@Valid @RequestBody GenerateTestCasesReqDTO request) {
GenerateTestCasesRespDTO response = (GenerateTestCasesRespDTO) aiService.generateTestCases(request);
return Results.success(response);
}
@PostMapping("/explain")
@Operation(summary = "解释代码", description = "对代码进行详细解释,帮助理解代码逻辑")
public Result<ExplainCodeRespDTO> explainCode(@Valid @RequestBody ExplainCodeReqDTO request) {
ExplainCodeRespDTO response = (ExplainCodeRespDTO) aiService.explainCode(request);
return Results.success(response);
}
@GetMapping("/health")
@Operation(summary = "健康检查", description = "检查 AI 服务是否正常运行")
public Result<String> health() {
return Results.success("AI Service is running");
}
}

View File

@@ -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;
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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<String> issues;
@Schema(description = "改进建议")
private List<String> 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<String> bestPractices;
}

View File

@@ -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<String> keyPoints;
}

View File

@@ -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<TestCaseDTO> testCases;
}

View File

@@ -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<String> improvements;
}

View File

@@ -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;
}

View File

@@ -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<AIServiceBlockingStub> {
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<AIServiceStub> {
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<AIServiceProto.AnalyzeCodeResponse> responseObserver) {
// 异步调用实现
}
/**
* 优化代码(异步)
*/
public void optimizeCode(AIServiceProto.OptimizeCodeRequest request,
StreamObserver<AIServiceProto.OptimizeCodeResponse> responseObserver) {
// 异步调用实现
}
/**
* 生成测试用例(异步)
*/
public void generateTestCases(AIServiceProto.GenerateTestCasesRequest request,
StreamObserver<AIServiceProto.GenerateTestCasesResponse> responseObserver) {
// 异步调用实现
}
/**
* 解释代码(异步)
*/
public void explainCode(AIServiceProto.ExplainCodeRequest request,
StreamObserver<AIServiceProto.ExplainCodeResponse> responseObserver) {
// 异步调用实现
}
}
/**
* StreamObserver 接口(简化版)
*/
public interface StreamObserver<V> {
void onNext(V value);
void onError(Throwable t);
void onCompleted();
}
}

View File

@@ -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<AnalyzeCodeRequest> 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<String> getIssuesList() { return java.util.Collections.emptyList(); }
public java.util.List<String> 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<String> 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<String> improvements = java.util.Collections.emptyList();
public boolean getSuccess() { return success; }
public String getMessage() { return message; }
public String getOptimizedCode() { return optimizedCode; }
public java.util.List<String> 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<String> 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<TestCase> testCases = java.util.Collections.emptyList();
public boolean getSuccess() { return success; }
public String getMessage() { return message; }
public java.util.List<TestCase> 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<TestCase> 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<String> keyPoints = java.util.Collections.emptyList();
public boolean getSuccess() { return success; }
public String getMessage() { return message; }
public String getExplanation() { return explanation; }
public java.util.List<String> 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<String> value) {
result.keyPoints = value;
return this;
}
public ExplainCodeResponse build() {
return result;
}
}
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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; // 关键点
}

View File

@@ -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 PlusORM
- FlexMarkMarkdown 处理)
- Knife4jAPI 文档)
- Redis缓存
- MySQL数据库
5. **待实现功能**
- [ ] 文章 CRUD 接口开发
- [ ] 分类管理接口
- [ ] 标签管理接口
- [ ] 评论系统接口
- [ ] 点赞/收藏接口
- [ ] 文章搜索接口
- [ ] Markdown 渲染服务
- [ ] 文章定时发布
- [ ] 文章审核功能
**注意事项**:
- 确保先执行 `db/blog.sql` 初始化数据库
- 检查 `application-dev.yml` 中的数据库连接配置
- JWT 配置需与 `aioj-backend-auth` 保持一致
- 启动前确保 Nacos 服务已运行
## License
Copyright © 2025 AIOJ Project

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>ai-oj</artifactId>
<version>${revision}</version>
</parent>
<artifactId>aioj-backend-blog-service</artifactId>
<packaging>jar</packaging>
<description>AIOJ 博客服务 - 用于用户发帖、分享技术经验、写文章</description>
<dependencies>
<!-- ==================== API文档 ==================== -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<!-- ==================== 内部模块 ==================== -->
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-core</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-log</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-security</artifactId>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-feign</artifactId>
</dependency>
<!-- ==================== Web ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ==================== 工具类 ==================== -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
</dependency>
<!-- ==================== Markdown 处理 ==================== -->
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>0.64.8</version>
</dependency>
<!-- ==================== Redis & 缓存 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- ==================== 数据库 ==================== -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- ==================== Spring Cloud 服务发现 ==================== -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- ==================== 测试 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,10 @@
/**
* AIOJ 博客服务模块
* <p>
* 提供文章发布、分类管理、标签系统、评论互动等功能
* </p>
*
* @author AIOJ
* @since 1.0.0
*/
package cn.meowrain.aioj.backend.blogservice;

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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}

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds" debug="false">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<timestamp key="time-month" datePattern="yyyy-MM"/>
<timestamp key="time-month-day" datePattern="yyyy-MM-dd"/>
<property name="LOG_FILE_PATH" value="${LOG_PATH:-./logs/${APP_NAME}}"/>
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -[%X{traceId:-}] %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${time-month}/${time-month-day}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>31</maxHistory>
<totalSizeCap>100GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="ASYNC_INFO" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="INFO_FILE"/>
</appender>
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="ERROR_FILE"/>
</appender>
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ERROR"/>
<appender-ref ref="ASYNC_INFO"/>
</root>
</springProfile>
</configuration>

224
db/blog.sql Normal file
View File

@@ -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 '父分类ID0表示顶级分类',
`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 '父评论ID0表示一级评论',
`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');