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');