Compare commits

..

24 Commits

Author SHA1 Message Date
a34168ef75 feat: 添加流控 2026-01-26 23:12:53 +08:00
45f8348395 feature: 添加sentinel 2026-01-26 23:12:38 +08:00
5681b6bcef feat: 实现题目服务完整校验责任链和流量控制
- 责任链校验系统
  * 题目创建参数校验(标题、内容、难度、判题配置、标签)
  * 题目编辑参数校验(可选字段校验)
  * 题目更新参数校验(管理员、存在性校验)
  * 题目提交参数校验(存在性、状态、语言、代码安全)

- Sentinel 流量控制
  * 添加 Sentinel 依赖和配置
  * 题目提交接口添加限流注解和降级处理

- 数据模型优化
  * QuestionResponseDTO 返回对象类型(JudgeConfig、JudgeCase)
  * 实现 Entity 与 DTO 的 JSON 转换

- 接口文档
  * 生成博客服务完整 API 文档

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 23:10:19 +08:00
c06cfc10ee refactor: use DTOs in QuestionController API responses
- Change getQuestion endpoint to return QuestionResponseDTO instead of Question entity
- Change listQuestions endpoint to return Page<QuestionResponseDTO> instead of Page<Question>
- Simplify createQuestion endpoint by using createQuestionWithChain method directly
- Add chain-related DTOs for question processing pipeline

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 21:58:06 +08:00
be709efa2e refactor: improve question service entity types and security config
- Change Question entity time fields from Date to LocalDateTime for Java 8+ time API consistency
- Add auto-fill annotation for updateTime field in Question and QuestionSubmit entities
- Simplify Serializable import in QuestionQueryRequestDTO
- Temporarily set SecurityConfiguration to permit all requests for development
- Remove generated .flattened-pom.xml build artifacts from version control

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 21:57:08 +08:00
17f58a7b45 feat: 添加响应体对象 2026-01-21 22:55:14 +08:00
9337540c77 feat: blog表结构 2026-01-21 22:50:15 +08:00
873fc3b149 feat: 实现博客服务基础架构
- 创建博客服务模块基础架构
- 实现 Blog 实体类和相关 Mapper
- 实现 RESTful 风格的 Controller 接口
- 添加完善的 Swagger 注解和校验
- 配置 Nacos 服务发现和 Redis 缓存
- 清理 Maven flatten 插件生成的临时文件

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 22:48:50 +08:00
cf0e326b0c feat: 实现题目服务基础架构
- 创建题目服务模块 aioj-backend-question-service
- 实现 Question、TestCase、QuestionSubmit 实体类
- 实现 RESTful 风格的 Controller 接口
- 添加完善的 Swagger 注解和校验
- 配置 Nacos 服务发现和 Redis 缓存
- 实现分页查询和条件过滤功能

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 22:47:36 +08:00
61fb847ac1 ai + blog 2026-01-20 17:20:03 +08:00
ef6b5cb11e refactor: 重构AI服务架构为基于gRPC的实现
- 将AI服务从OpenAI API调用重构为gRPC服务架构
- 添加gRPC相关依赖和Protobuf编译插件
- 更新application.yml配置为gRPC客户端设置
- 移除旧的AIServiceImpl实现

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 17:17:55 +08:00
51d16ea077 feat: 添加AI服务模块实现代码分析和评估功能 2026-01-20 16:40:21 +08:00
439fdf90c4 Merge remote-tracking branch 'origin/main' 2026-01-19 20:14:51 +08:00
08043672f9 feat: 实现用户邮箱管理和个人资料功能
- 修复邮箱验证码接口参数绑定问题 (@RequestParam -> @ModelAttribute)
- 实现异步邮件发送,使用独立线程池避免阻塞
- 完成邮箱绑定/解绑功能
- 实现修改密码功能
- 实现用户资料查询和更新功能
- 添加个人资料相关 DTO
- 更新网关过滤器使用新的服务名称

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-18 17:31:08 +08:00
c3c07ff1e7 refactor: 标准化微服务命名并添加日志配置
- 将所有微服务名称添加 aioj- 前缀 (auth-service -> aioj-auth-service)
- 更新网关路由配置以使用新的服务名称
- 为所有服务添加 logback-spring.xml 日志配置
- 更新 .gitignore 排除 uploads 和 logs 目录

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-18 15:49:38 +08:00
5522eaa1d6 Merge remote-tracking branch 'origin/main' 2026-01-12 23:04:40 +08:00
93759b4a1a fix: 更新AuthServiceImpl实现
Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 02:03:12 +08:00
c9e9a1a4c7 chore: 更新IDE配置和网关过滤器
- 更新IDEA配置文件(dataSources, db-forest, encodings)
- 更新网关AuthGlobalFilter
- 添加uploads目录

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 02:02:59 +08:00
a4575cebd4 refactor: 重构安全架构,提取通用安全模块到common-security
- 将JwtAuthenticationFilter、JwtUtil、JwtProperties从auth服务移至common-security模块
- 新增common-security通用安全模块,提供JWT认证、权限验证等核心安全功能
- 重命名SecurityConfiguration为AuthSecurityConfiguration,使用common-security的filter
- 新增JacksonConfiguration配置类,统一JSON序列化配置
- 新增头像更新功能AvatarUpdateRequestDTO
- 移除冗余的UserLoginResponseDTO类
- 更新各服务模块的依赖配置以引入common-security模块
- 新增README.md项目说明文档

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 01:54:21 +08:00
8bd56a6001 feat: 添加文件哈希检查功能,支持秒传
- 新增 HashCheckRespDTO 用于哈希检查响应
- 文件上传接口支持可选的 hash 参数,用于秒传
- 新增 /check 接口用于检查文件哈希是否存在
- 简化上传逻辑,移除同步/异步哈希计算配置

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-10 19:38:30 +08:00
637f125348 feat: 实现文件服务核心功能,支持本地和云存储
实现通用文件上传、存储和访问功能,支持文件去重和多种存储策略。

主要变更:
- 新增文件上传接口,支持小文件同步去重、大文件异步处理
- 实现本地存储和腾讯云COS存储策略
- 新增哈希计算服务,支持异步计算大文件哈希
- 新增文件访问控制器,提供文件访问能力
- 扩展附件实体和服务,实现完整的文件管理
- 新增配置类,支持灵活的存储策略切换
- 优化删除状态枚举类型从String改为Integer
- 配置文件上传大小限制和存储相关配置

技术细节:
- 小文件(<=10MB)同步计算SHA256哈希并去重
- 大文件异步计算哈希,提升上传响应速度
- 支持按日期自动组织文件目录结构
- 集成Hutool工具简化文件操作

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-10 16:49:34 +08:00
4ee3ebcbec feat: 添加通用附件管理功能,包括实体、服务、控制器及相关模板 2026-01-10 15:05:25 +08:00
2e2697140c feat :AIOJ 后端模块并更新项目结构
- 引入了新模块:gateway、judge service、question service、user service 和 UPMS。
- 在 gateway 和 user service 模块中创建了 package-info.java 文件用于文档说明。
- 更新了 pom.xml 文件以反映新的模块结构和依赖关系。
- 在 UserController 中实现了基本的用户资料管理端点。
- 为每个新模块添加了扁平化 POM 文件以有效管理依赖。
- 增强了项目属性,以实现更好的版本管理和模块间一致性。
2026-01-10 14:46:36 +08:00
3657f88970 feat: 实现邮箱绑定与解绑功能
- 实现邮箱绑定接口,支持通过验证码绑定邮箱
- 实现邮箱解绑接口,支持用户解绑已绑定的邮箱
- 添加BindEmailRequest DTO用于邮箱绑定请求
- 完善Swagger文档注解,提升API文档可读性

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-10 00:05:32 +08:00
292 changed files with 15888 additions and 775 deletions

View File

@@ -7,7 +7,12 @@
"Bash(mvn dependency:tree:*)",
"Bash(mvn spring-javaformat:apply:*)",
"Bash(git add:*)",
"Bash(git commit:*)"
"Bash(git commit:*)",
"Bash(git -C \"C:\\\\Users\\\\meowr\\\\Desktop\\\\bishe\\\\AI_OJ\" status)",
"Bash(git -C \"C:\\\\Users\\\\meowr\\\\Desktop\\\\bishe\\\\AI_OJ\" checkout 3da91e5 -- aioj-backend-ai-service/)",
"Bash(git -C \"C:\\\\Users\\\\meowr\\\\Desktop\\\\bishe\\\\AI_OJ\" push)",
"Bash(mvn compile:*)",
"Bash(mvn clean install:*)"
]
}
}

11
.gitignore vendored
View File

@@ -35,4 +35,13 @@ build/
.vscode/
### Mac OS ###
.DS_Store
.DS_Store
### mybatis plus generator
/generator/
### Uploads ###
/uploads/
### Logs ###
/logs/

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CoolRequestCommonStatePersistent">
<option name="searchCache" value="JwtAuthenticationFilter" />
<option name="searchCache" value="difficul" />
</component>
</project>

76
.idea/dataSources.xml generated
View File

@@ -25,5 +25,81 @@
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="AIOJAdminApplication" uuid="1323cc2e-0b2e-40de-abe6-d1f4c7567b1e">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="UserServiceApplication" uuid="c52f5e64-993d-4013-9e2b-838e23d604a2">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="AIOJAuthApplication" uuid="e757fbaf-3605-4bf2-9eb5-852d06273adc">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="AIOJAdminApplication" uuid="3a647305-fb45-441b-ba2b-a79ec82a3778">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="AIOJAuthApplication" uuid="38fda843-f467-435e-99e4-2a771f7af3f3">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="FileServiceApplication" uuid="8d957a30-3743-40eb-a916-c6503a783fb9">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="UserServiceApplication" uuid="38b7e47b-6235-4576-8b43-df28c967dbcc">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="aioj" uuid="cfc60cc1-f725-4d9c-b129-5b722771d69e">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:mysql://10.0.0.10/aioj_dev [DEBUG]" group="QuestionServiceApplication" uuid="5a11088e-1728-4471-a8db-deaeac511136">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.0.0.10/aioj_dev</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="db-tree-configuration">
<option name="data" value="1:0:AIOJAdminApplication&#10;3:0:UserServiceApplication&#10;5:0:AIOJAuthApplication&#10;----------------------------------------&#10;2:1:43cc61de-66e1-44cc-b4a2-b24d7e03b490&#10;4:3:903d03c4-df11-4cf8-939a-3e5fba0ab207&#10;6:5:2fd8684a-b9aa-4507-abb0-f7c259d91286&#10;" />
<option name="data" value="1:0:AIOJAdminApplication&#10;5:0:UserServiceApplication&#10;9:0:AIOJAuthApplication&#10;13:0:FileServiceApplication&#10;15:0:QuestionServiceApplication&#10;----------------------------------------&#10;2:1:43cc61de-66e1-44cc-b4a2-b24d7e03b490&#10;3:1:1323cc2e-0b2e-40de-abe6-d1f4c7567b1e&#10;4:1:3a647305-fb45-441b-ba2b-a79ec82a3778&#10;6:5:903d03c4-df11-4cf8-939a-3e5fba0ab207&#10;7:5:c52f5e64-993d-4013-9e2b-838e23d604a2&#10;8:5:38b7e47b-6235-4576-8b43-df28c967dbcc&#10;10:9:2fd8684a-b9aa-4507-abb0-f7c259d91286&#10;11:9:e757fbaf-3605-4bf2-9eb5-852d06273adc&#10;12:9:38fda843-f467-435e-99e4-2a771f7af3f3&#10;14:13:8d957a30-3743-40eb-a916-c6503a783fb9&#10;16:15:5a11088e-1728-4471-a8db-deaeac511136&#10;17:0:cfc60cc1-f725-4d9c-b129-5b722771d69e&#10;" />
</component>
</project>

10
.idea/easyCodeTableSettingEncode.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EasyCodeTableSetting">
<option name="tableInfoMap">
<map>
<entry key="aioj_dev.attachment" value="eyJuYW1lIjoiQXR0YWNobWVudCIsInByZU5hbWUiOiIiLCJjb21tZW50Ijoi6YCa55So6ZmE5Lu26KGoIiwidGVtcGxhdGVHcm91cE5hbWUiOiIiLCJmdWxsQ29sdW1uIjpbeyJuYW1lIjoiaWQiLCJjb21tZW50Ijoi5Li76ZSuIiwidHlwZSI6ImphdmEubGFuZy5Mb25nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJmaWxlTmFtZSIsImNvbW1lbnQiOiLljp/lp4vmlofku7blkI0iLCJ0eXBlIjoiamF2YS5sYW5nLlN0cmluZyIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoiZmlsZUV4dGVuc2lvbiIsImNvbW1lbnQiOiLmlofku7blkI7nvIDlkI0iLCJ0eXBlIjoiamF2YS5sYW5nLlN0cmluZyIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoiZmlsZVNpemUiLCJjb21tZW50Ijoi5paH5Lu25aSn5bCPKEJ5dGUpIiwidHlwZSI6ImphdmEubGFuZy5Mb25nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJmaWxlSGFzaCIsImNvbW1lbnQiOiLmlofku7blk4jluIwoTUQ1L1NIQTI1NinnlKjkuo7ljrvph40iLCJ0eXBlIjoiamF2YS5sYW5nLlN0cmluZyIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoibWltZVR5cGUiLCJjb21tZW50IjoiTUlNReexu+WeiyIsInR5cGUiOiJqYXZhLmxhbmcuU3RyaW5nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJzdG9yYWdlVHlwZSIsImNvbW1lbnQiOiLlrZjlgqjmlrnmoYg6IExPQ0FMLCBPU1MsIFMzLCBNSU5JTyIsInR5cGUiOiJqYXZhLmxhbmcuU3RyaW5nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJzdG9yYWdlUGF0aCIsImNvbW1lbnQiOiLniannkIblrZjlgqjot6/lvoTmiJblr7nosaHlrZjlgqhLZXkiLCJ0eXBlIjoiamF2YS5sYW5nLlN0cmluZyIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoiYnVzaW5lc3NUeXBlIiwiY29tbWVudCI6IuaJgOWxnuS4muWKoeaooeWdlyIsInR5cGUiOiJqYXZhLmxhbmcuU3RyaW5nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJidXNpbmVzc0lkIiwiY29tbWVudCI6IuaJgOWxnuS4muWKoWlkIiwidHlwZSI6ImphdmEubGFuZy5Mb25nIiwiY3VzdG9tIjpmYWxzZSwiZXh0Ijoie30ifSx7Im5hbWUiOiJ1c2VySWQiLCJjb21tZW50Ijoi5LiK5Lyg6ICFSUQiLCJ0eXBlIjoiamF2YS5sYW5nLkxvbmciLCJjdXN0b20iOmZhbHNlLCJleHQiOiJ7fSJ9LHsibmFtZSI6ImltYWdlSW5mbyIsImNvbW1lbnQiOiLlm77niYflrr3pq5jjgIFFWElG562J5YWD5pWw5o2uIiwidHlwZSI6ImphdmEubGFuZy5TdHJpbmciLCJjdXN0b20iOmZhbHNlLCJleHQiOiJ7fSJ9LHsibmFtZSI6ImlzRGVsZXRlZCIsImNvbW1lbnQiOiLpgLvovpHliKDpmaQoMC3mraPluLgsIDEt5bey5Yig6ZmkKSIsInR5cGUiOiJqYXZhLmxhbmcuSW50ZWdlciIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoiY3JlYXRlZEF0IiwiY29tbWVudCI6IuWIm+W7uuaXtumXtCIsInR5cGUiOiJqYXZhLnV0aWwuRGF0ZSIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In0seyJuYW1lIjoidXBkYXRlZEF0IiwiY29tbWVudCI6IuabtOaWsOaXtumXtCIsInR5cGUiOiJqYXZhLnV0aWwuRGF0ZSIsImN1c3RvbSI6ZmFsc2UsImV4dCI6Int9In1dLCJzYXZlUGFja2FnZU5hbWUiOiIiLCJzYXZlUGF0aCI6IiIsInNhdmVNb2RlbE5hbWUiOiIifQ==" />
</map>
</option>
</component>
</project>

5
.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/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-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/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-bom/src/main/java" charset="UTF-8" />
@@ -14,11 +15,13 @@
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-feign/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-log/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-mybatis/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-security/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-starter/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-starter/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-swagger/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-file-service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-judge-service/src/main/java" charset="UTF-8" />
@@ -36,5 +39,7 @@
<file url="file://$PROJECT_DIR$/aioj-backend-user-service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
<file url="file://$USER_HOME$/src/main/java" charset="UTF-8" />
<file url="file://$USER_HOME$/src/main/resources" charset="UTF-8" />
</component>
</project>

1
.idea/misc.xml generated
View File

@@ -10,6 +10,7 @@
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/aioj-backend-client/pom.xml" />
<option value="$PROJECT_DIR$/aioj-backend-common/aioj-backend-common-swagger/pom.xml" />
<option value="$PROJECT_DIR$/aioj-backend-model/pom.xml" />
<option value="$PROJECT_DIR$/aioj-backend-upms/aioj-upms-api/pom.xml" />
</set>

87
.idea/mybatisx/templates.xml generated Normal file
View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="TemplatesSettings">
<option name="templateConfigs">
<TemplateContext>
<option name="generateConfig">
<GenerateConfig>
<option name="annotationType" value="MYBATIS_PLUS3" />
<option name="basePackage" value="generator" />
<option name="basePath" value="src/main/java" />
<option name="classNameStrategy" value="camel" />
<option name="encoding" value="UTF-8" />
<option name="extraClassSuffix" value="" />
<option name="ignoreFieldPrefix" value="" />
<option name="ignoreFieldSuffix" value="" />
<option name="ignoreTablePrefix" value="" />
<option name="ignoreTableSuffix" value="" />
<option name="moduleName" value="ai-oj" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="moduleUIInfoList">
<list>
<ModuleInfoGo>
<option name="basePath" value="${domain.basePath}" />
<option name="configFileName" value="serviceImpl.ftl" />
<option name="configName" value="serviceImpl" />
<option name="encoding" value="${domain.encoding}" />
<option name="fileName" value="${domain.fileName}ServiceImpl" />
<option name="fileNameWithSuffix" value="${domain.fileName}ServiceImpl.java" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="packageName" value="${domain.basePackage}.service.impl" />
</ModuleInfoGo>
<ModuleInfoGo>
<option name="basePath" value="${domain.basePath}" />
<option name="configFileName" value="mapperInterface.ftl" />
<option name="configName" value="mapperInterface" />
<option name="encoding" value="${domain.encoding}" />
<option name="fileName" value="${domain.fileName}Mapper" />
<option name="fileNameWithSuffix" value="${domain.fileName}Mapper.java" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="packageName" value="${domain.basePackage}.mapper" />
</ModuleInfoGo>
<ModuleInfoGo>
<option name="basePath" value="${domain.basePath}" />
<option name="configFileName" value="serviceInterface.ftl" />
<option name="configName" value="serviceInterface" />
<option name="encoding" value="${domain.encoding}" />
<option name="fileName" value="${domain.fileName}Service" />
<option name="fileNameWithSuffix" value="${domain.fileName}Service.java" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="packageName" value="${domain.basePackage}.service" />
</ModuleInfoGo>
<ModuleInfoGo>
<option name="basePath" value="src/main/resources" />
<option name="configFileName" value="mapperXml.ftl" />
<option name="configName" value="mapperXml" />
<option name="encoding" value="${domain.encoding}" />
<option name="fileName" value="${domain.fileName}Mapper" />
<option name="fileNameWithSuffix" value="${domain.fileName}Mapper.xml" />
<option name="modulePath" value="$PROJECT_DIR$" />
<option name="packageName" value="${domain.basePackage}.mapper" />
</ModuleInfoGo>
</list>
</option>
<option name="needsComment" value="true" />
<option name="needsModel" value="true" />
<option name="relativePackage" value="domain" />
<option name="superClass" value="" />
<option name="tableUIInfoList">
<list>
<TableUIInfo>
<option name="className" value="Question" />
<option name="tableName" value="question" />
</TableUIInfo>
</list>
</option>
<option name="templatesName" value="mybatis-plus3" />
<option name="useActualColumns" value="true" />
<option name="useLombokPlugin" value="true" />
</GenerateConfig>
</option>
<option name="moduleName" value="ai-oj" />
<option name="projectPath" value="$PROJECT_DIR$" />
<option name="templateName" value="mybatis-plus3" />
</TemplateContext>
</option>
</component>
</project>

78
README.md Normal file
View File

@@ -0,0 +1,78 @@
# AIOJ - Online Judge System
基于 Spring Boot 微服务架构的在线判题系统。
## 服务端口配置
| 服务名称 | 端口 | 说明 |
|---------|------|------|
| Gateway | 18085 | API 网关服务 |
| Auth Service | 18081 | 认证授权服务 |
| User Service | 18082 | 用户服务 |
| UPMS | 18083 | 用户权限管理服务 |
| File Service | 18066 | 文件服务 |
## 模块结构
### 核心模块 (aioj-backend-common)
- **aioj-backend-common-bom** - 依赖管理
- **aioj-backend-common-core** - 核心工具类
- **aioj-backend-common-feign** - Feign 客户端配置
- **aioj-backend-common-log** - 日志框架
- **aioj-backend-common-mybatis** - MyBatis 扩展
- **aioj-backend-common-starter** - 自动配置启动器
### 服务模块
- **aioj-backend-gateway** - API 网关
- **aioj-backend-auth** - 认证服务
- **aioj-backend-user-service** - 用户服务
- **aioj-backend-upms** - 权限管理服务
- **aioj-backend-file-service** - 文件服务
- **aioj-backend-judge-service** - 判题服务(开发中)
- **aioj-backend-question-service** - 题库服务(开发中)
- **aioj-backend-ai-service** - AI 服务(开发中)
## 快速开始
### 构建项目
```bash
mvn clean compile
```
### 运行服务
```bash
# 运行网关
mvn spring-boot:run -pl aioj-backend-gateway
# 运行认证服务
mvn spring-boot:run -pl aioj-backend-auth
# 运行用户服务
mvn spring-boot:run -pl aioj-backend-user-service
```
### 访问地址
- Gateway: http://localhost:18085
- Auth Service: http://localhost:18081/api
- User Service: http://localhost:18082/api
- UPMS: http://localhost:18083/api
- File Service: http://localhost:18066/api
## 常用命令
### 代码格式化
```bash
mvn spring-javaformat:apply
```
### 运行测试
```bash
mvn test
```

View File

@@ -3,18 +3,122 @@
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</groupId>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>ai-oj</artifactId>
<version>1.0-SNAPSHOT</version>
<version>${revision}</version>
</parent>
<artifactId>aioj-backend-ai-service</artifactId>
<packaging>jar</packaging>
<description>AIOJ AI服务</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<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>
</project>
<!-- ==================== 内部模块 ==================== -->
<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>
<!-- ==================== Web ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ==================== gRPC ==================== -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.68.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.68.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.68.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-services</artifactId>
<version>1.68.1</version>
</dependency>
<!-- Protobuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.29.2</version>
</dependency>
<!-- 对于 Java 9+ 需要 javax.annotation -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.53</version>
<scope>provided</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>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<!-- Protobuf 编译插件 -->
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:4.29.2:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.68.1:exe:${os.detected.classifier}</pluginArtifact>
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

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,28 @@
spring:
data:
redis:
host: 10.0.0.10
port: 6379
password: 123456
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.0.0.10/aioj_dev
username: root
password: root
cloud:
nacos:
discovery:
enabled: true
register-enabled: true
server-addr: 10.0.0.10:8848
username: nacos
password: nacos
# AI 服务配置 - 开发环境
ai:
openai:
api-key: ${OPENAI_API_KEY:sk-dev-key}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
model: gpt-4
max-tokens: 2000
temperature: 0.7

View File

@@ -0,0 +1,28 @@
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD}
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
cloud:
nacos:
discovery:
enabled: true
register-enabled: true
server-addr: ${NACOS_ADDR}
username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD}
# AI 服务配置 - 生产环境
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
model: ${OPENAI_MODEL:gpt-4}
max-tokens: ${OPENAI_MAX_TOKENS:2000}
temperature: ${OPENAI_TEMPERATURE:0.7}

View File

@@ -0,0 +1,25 @@
spring:
data:
redis:
host: localhost
port: 6379
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost/aioj_test
username: root
password: root
cloud:
nacos:
discovery:
enabled: true
register-enabled: true
server-addr: localhost:8848
# AI 服务配置 - 测试环境
ai:
openai:
api-key: ${OPENAI_API_KEY:sk-test-key}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
model: gpt-4
max-tokens: 2000
temperature: 0.7

View File

@@ -0,0 +1,57 @@
spring:
application:
name: aioj-ai-service
profiles:
active: @env@
server:
port: 18084
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.aiservice.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天
# gRPC 客户端配置
grpc:
client:
host: ${GRPC_SERVER_HOST:localhost}
port: ${GRPC_SERVER_PORT:50051}
timeout: ${GRPC_TIMEOUT:10}
tls-enabled: ${GRPC_TLS_ENABLED:false}
max-message-size: ${GRPC_MAX_MESSAGE_SIZE:10485760}
aioj:
log:
enabled: true
max-length: 20000
logging:
file:
path: ./logs/${spring.application.name}

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志输出位置 -->
<springProperty scope="context" name="LOG_HOME" source="logging.file.path" defaultValue="./logs/aioj-ai-service"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/aioj-ai-service.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/aioj-ai-service.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 错误日志单独输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/aioj-ai-service-error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/aioj-ai-service-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 日志级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<!-- SQL日志 -->
<logger name="cn.meowrain.aioj.backend.aiservice.dao.mapper" level="DEBUG"/>
</configuration>

View File

@@ -1,68 +1,49 @@
<?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</groupId>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>ai-oj</artifactId>
<version>1.0-SNAPSHOT</version>
<version>${revision}</version>
</parent>
<artifactId>aioj-backend-auth</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<packaging>jar</packaging>
<description>AIOJ 认证授权服务</description>
<dependencies>
<!-- SpringDoc OpenAPI - 显式指定版本以兼容 Spring Boot 3.5.x -->
<!-- ==================== API文档 ==================== -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.4</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
<exclusions>
<exclusion>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</exclusion>
<exclusion>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-common</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 核心模块 -->
<!-- ==================== 内部模块 ==================== -->
<dependency>
<groupId>cn.meowrain</groupId>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-feign</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.meowrain</groupId>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.meowrain.aioj</groupId>
<artifactId>aioj-backend-common-security</artifactId>
</dependency>
<!-- 工具类 -->
<!-- ==================== 工具类 ==================== -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
@@ -76,48 +57,26 @@
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Spring Cloud服务发现 -->
<!-- ==================== Spring Cloud 服务发现 ==================== -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Web -->
<!-- ==================== Web ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OAuth2 & Spring Security -->
<!-- ==================== OAuth2 & Spring Security ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 和 Spring Security 依赖已在 common-security 中包含 -->
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<!-- Feign客户端 -->
<!-- ==================== Feign 客户端 ==================== -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
@@ -127,13 +86,13 @@
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Redis用于存储refreshToken -->
<!-- ==================== Redis ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 开发工具 -->
<!-- ==================== 开发工具 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
@@ -141,7 +100,7 @@
<optional>true</optional>
</dependency>
<!-- 测试 -->
<!-- ==================== 测试 ==================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -6,13 +6,13 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "user-service", path = "/api/v1/user")
@FeignClient(name = "aioj-user-service", path = "/api/v1/user")
public interface UserClient {
@GetMapping("/inner/get-by-username")
Result<UserAuthRespDTO> getUserByUserName(@RequestParam("userAccount") String userAccount);
@GetMapping("/inner/get-by-userid")
Result<UserAuthRespDTO> getUserById(@RequestParam("userId") String userId);
Result<UserAuthRespDTO> getUserById(@RequestParam("userId") Long userId);
}

View File

@@ -1,33 +1,37 @@
package cn.meowrain.aioj.backend.auth.config;
import cn.meowrain.aioj.backend.auth.filter.JwtAuthenticationFilter;
import cn.meowrain.aioj.backend.framework.security.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Auth Service Security 配置
* 覆盖 common-security 的默认 filterChain添加 auth 服务自定义白名单
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
public class AuthSecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// Auth 服务自定义白名单
.requestMatchers("/v1/auth/**", "/oauth2/**", "/.well-known/**", "/doc.html", "/swagger-ui/**",
"/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico","/v1/user/email/send-code")
"/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico",
"/v1/user/email/send-code")
.permitAll()
.anyRequest()
.authenticated())

View File

@@ -56,15 +56,9 @@ public class AuthController {
}
@GetMapping("/getUserInfo")
public Result<UserAuthRespDTO> getUserInfo(@RequestHeader(value = "Authorization", required = false) String authorization) {
String token = null;
if(authorization != null && authorization.startsWith("Bearer ")){
token = authorization.substring(7);
}
if(token != null && sessionService.isTokenBlacklisted(token)) {
return Results.success(null);
}
return Results.success(authService.getUserInfo(token));
public Result<UserAuthRespDTO> getUserInfo() {
return Results.success(authService.getUserInfo());
}
}

View File

@@ -2,13 +2,14 @@ package cn.meowrain.aioj.backend.auth.dto.resp;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 用户认证响应体
*/
@Data
public class UserAuthRespDTO {
public class UserAuthRespDTO implements Serializable {
/**
* id
@@ -24,7 +25,6 @@ public class UserAuthRespDTO {
* 用户密码
*/
private String userPassword;
/**
* 开放平台id
*/
@@ -43,7 +43,7 @@ public class UserAuthRespDTO {
/**
* 用户头像
*/
private String userAvatar;
private Long userAvatar;
/**
* 用户简介
@@ -55,6 +55,16 @@ public class UserAuthRespDTO {
*/
private String userRole;
/**
* 用户邮箱
*/
private String userEmail;
/**
* 用户邮箱是否验证 0 未验证 1已验证
*/
private Integer userEmailVerified;
/**
* 创建时间
*/
@@ -65,4 +75,8 @@ public class UserAuthRespDTO {
*/
private Date updateTime;
private static final long serialVersionUID = 1L;
}

View File

@@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.oauth2.entity.OAuth2Client;
import cn.meowrain.aioj.backend.auth.oauth2.exception.OAuth2Exception;
import cn.meowrain.aioj.backend.auth.oauth2.service.OAuth2ClientService;
import cn.meowrain.aioj.backend.auth.oauth2.service.OAuth2SessionService;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

View File

@@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.clients.UserClient;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.oauth2.dto.UserInfoResponse;
import cn.meowrain.aioj.backend.auth.oauth2.exception.OAuth2Exception;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import io.jsonwebtoken.Claims;
@@ -64,7 +64,7 @@ public class OAuth2UserInfoController {
Long userId = Long.parseLong(userIdStr);
// 4. 调用 user-service 获取用户信息
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult == null || userResult.getData() == null) {
log.error("获取用户信息失败: userId={}", userId);
throw new OAuth2Exception("server_error", "获取用户信息失败", 500);
@@ -80,7 +80,7 @@ public class OAuth2UserInfoController {
.preferredUsername(user.getUserAccount()) // 用户名
.email(null) // TODO: 从用户信息中获取邮箱
.emailVerified(false) // TODO: 从用户信息中获取邮箱验证状态
.picture(user.getUserAvatar()) // 用户头像
.picture(null) // 用户头像
.role(user.getUserRole()) // 用户角色
.build();

View File

@@ -4,7 +4,7 @@ import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.oauth2.dto.OAuth2TokenResponse;
import cn.meowrain.aioj.backend.auth.oauth2.entity.OAuth2Client;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.auth.utils.AuthServiceJwtUtil;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import lombok.RequiredArgsConstructor;
@@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class OAuth2TokenService {
private final JwtUtil jwtUtil;
private final AuthServiceJwtUtil jwtUtil;
private final UserClient userClient;
@@ -45,7 +45,7 @@ public class OAuth2TokenService {
*/
public OAuth2TokenResponse generateTokenResponse(OAuth2Client client, Long userId, String scope, String nonce) {
// 1. 调用 user-service 获取用户信息
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult == null || userResult.getData() == null) {
throw new RuntimeException("获取用户信息失败");
}
@@ -110,7 +110,7 @@ public class OAuth2TokenService {
}
// 4. 调用 user-service 获取最新用户信息
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(Long.valueOf(userId));
if (userResult == null || userResult.getData() == null) {
throw new RuntimeException("获取用户信息失败");
}

View File

@@ -0,0 +1,21 @@
/**
* AIOJ 认证授权模块
* <p>
* 提供系统的认证与授权功能,包括:
* <ul>
* <li>OAuth2 认证</li>
* <li>JWT Token 生成与验证</li>
* <li>用户登录认证</li>
* <li>权限校验</li>
* </ul>
* </p>
*
* @author meowrain
* @since 1.0.0
*/
@NonNullApi
@NonNullFields
package cn.meowrain.aioj.backend.auth;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -31,8 +31,7 @@ public interface AuthService {
/**
* 根据accessToken获取用户信息
* @param accessToken
* @return {@link Result<UserAuthRespDTO>}
*/
UserAuthRespDTO getUserInfo(String accessToken);
UserAuthRespDTO getUserInfo();
}

View File

@@ -5,13 +5,14 @@ import cn.hutool.crypto.digest.BCrypt;
import cn.meowrain.aioj.backend.auth.clients.UserClient;
import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.auth.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.auth.config.properties.JwtPropertiesConfiguration;
import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils;
import cn.meowrain.aioj.backend.framework.security.properties.JwtPropertiesConfiguration;
import cn.meowrain.aioj.backend.auth.dto.chains.context.UserLoginRequestParamVerifyContext;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
import cn.meowrain.aioj.backend.auth.service.AuthService;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.auth.utils.AuthServiceJwtUtil;
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
import cn.meowrain.aioj.backend.framework.core.exception.ServiceException;
@@ -28,7 +29,7 @@ import java.util.concurrent.TimeUnit;
@Slf4j
public class AuthServiceImpl implements AuthService {
private final JwtUtil jwtUtil;
private final AuthServiceJwtUtil jwtUtil;
private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext;
@@ -106,12 +107,12 @@ public class AuthServiceImpl implements AuthService {
String cacheValue = stringRedisTemplate.opsForValue().get(cacheKey);
if (cacheValue == null || !cacheValue.equals(refreshToken)) {
throw new ServiceException("Refresh Token 已失效");
throw new ServiceException(ErrorCode.NO_AUTH_ERROR);
}
// 再次签发新的 Access Token
// 此处你需要查用户,拿 userName, role
Result<UserAuthRespDTO> userResult = userClient.getUserById(String.valueOf(userId));
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
if (userResult.isFail()) {
log.error("通过id查找用户失败:{}", userResult.getMessage());
throw new ServiceException(ErrorCode.SYSTEM_ERROR);
@@ -156,7 +157,7 @@ public class AuthServiceImpl implements AuthService {
}
// 4. 验证用户是否存在(可选,增加安全性)
Result<UserAuthRespDTO> userResult = userClient.getUserById(userId);
Result<UserAuthRespDTO> userResult = userClient.getUserById(Long.valueOf(userId));
if (userResult.isFail() || userResult.getData() == null) {
log.warn("User not found for id: {}", userId);
return false;
@@ -171,37 +172,13 @@ public class AuthServiceImpl implements AuthService {
}
@Override
public UserAuthRespDTO getUserInfo(String accessToken) {
public UserAuthRespDTO getUserInfo() {
Long currentUserId = ContextHolderUtils.getCurrentUserId();
// 1. 参数校验
if (accessToken == null || accessToken.isBlank()) {
log.warn("Access token is null or empty");
throw new ClientException(ErrorCode.PARAMS_ERROR);
}
// 2. token 校验
if (!jwtUtil.isTokenValid(accessToken)) {
log.warn("Access token is invalid or expired");
throw new ClientException(ErrorCode.NOT_LOGIN_ERROR);
}
// 3. 解析 token
String userId;
try {
userId = jwtUtil.parseClaims(accessToken).getSubject();
} catch (Exception e) {
log.warn("Failed to parse access token", e);
throw new ClientException(ErrorCode.NOT_LOGIN_ERROR);
}
if (userId == null) {
throw new ClientException(ErrorCode.NOT_LOGIN_ERROR);
}
// 4. 查询用户信息IO 操作)
// 查询用户信息IO 操作)
Result<UserAuthRespDTO> userResult;
try {
userResult = userClient.getUserById(userId);
userResult = userClient.getUserById(currentUserId);
} catch (Exception e) {
log.error("Failed to call user service", e);
throw new ClientException(ErrorCode.SYSTEM_ERROR);

View File

@@ -0,0 +1,75 @@
package cn.meowrain.aioj.backend.auth.utils;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* Auth Service 专用的 JWT 工具类
* 包装 common-security 中的 JwtUtil提供接受 UserAuthRespDTO 的便捷方法
*/
@Component
@RequiredArgsConstructor
public class AuthServiceJwtUtil {
private final JwtUtil jwtUtil;
/**
* 生成 Access Token
* @param user 用户信息
* @return JWT Token
*/
public String generateAccessToken(UserAuthRespDTO user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("userName", user.getUserName());
claims.put("role", user.getUserRole());
return jwtUtil.generateAccessToken(user.getId(), claims);
}
/**
* 生成 Refresh Token
* @param userId 用户ID
* @return JWT Token
*/
public String generateRefreshToken(Long userId) {
return jwtUtil.generateRefreshToken(userId);
}
/**
* 生成 OIDC ID Token
* @param user 用户信息
* @param clientId 客户端IDaud
* @param nonce 防重放参数
* @return ID Token
*/
public String generateIdToken(UserAuthRespDTO user, String clientId, String nonce) {
Map<String, Object> claims = new HashMap<>();
claims.put("aud", clientId);
claims.put("name", user.getUserName());
claims.put("preferred_username", user.getUserAccount());
if (nonce != null) {
claims.put("nonce", nonce);
}
return jwtUtil.generateAccessToken(user.getId(), claims);
}
/**
* 校验 Token 是否过期
*/
public boolean isTokenValid(String token) {
return jwtUtil.isTokenValid(token);
}
/**
* 解析 Token
*/
public io.jsonwebtoken.Claims parseClaims(String token) {
return jwtUtil.parseClaims(token);
}
}

View File

@@ -1,6 +1,6 @@
spring:
application:
name: auth-service
name: aioj-auth-service
data:
redis:
host: 10.0.0.10

View File

@@ -1,13 +1,13 @@
spring:
application:
name: auth-service
name: aioj-auth-service
profiles:
active: @env@
devtools:
livereload:
enabled: true
server:
port: 10011
port: 18081
servlet:
context-path: /api
springdoc:
@@ -33,6 +33,10 @@ mybatis-plus:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
jwt:
enabled: true
secret: "12345678901234567890123456789012" # 至少32字节
access-expire: 900000 # 24小时
refresh-expire: 604800000 # 7天
refresh-expire: 604800000 # 7天
logging:
file:
path: ./logs/${spring.application.name}

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--debug="false" 表示关闭 Logback 框架自身的内部状态信息打印。-->
<configuration scan="true" scanPeriod="10 seconds" debug="false">
<!-- 引入 spring boot 默认日志颜色和基础配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义变量 APP_NAME从 Spring 环境变量中获取 spring.application.name 的值-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<!-- 定义时间格式yyyy-MM 表示年-yyyy-MM-dd 表示年---->
<timestamp key="time-month" datePattern="yyyy-MM"/>
<timestamp key="time-month-day" datePattern="yyyy-MM-dd"/>
<!-- 定义变量 LOG_FILE_PATH默认值为 ./logs/${APP_NAME},可以通过环境变量 LOG_PATH 覆盖 日志存储路径-->
<property name="LOG_FILE_PATH" value="${LOG_PATH:-./logs/${APP_NAME}}"/>
<!-- 定义日志格式
格式说明:
%d{yyyy-MM-dd HH:mm:ss.SSS}:日志记录时间,格式为年--日 时:分:秒.毫秒
[%thread]:日志记录线程名称
%-5level日志级别左对齐占用 5 个字符宽度
%logger{50}:日志记录器名称,最多显示 50 个字符
-[%X{traceId:-}]:从 MDC 中获取 traceId 变量值,如果不存在则显示为空
%msg%n日志消息内容换行符
-->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -[%X{traceId:-}] %msg%n"/>
<!-- appender 控制台输出-->
<!--
<appender>: 这是 Logback 配置的根标签之一,用于定义一个日志输出目的地。
name="CONSOLE": 为这个 Appender 赋予一个唯一的名称,方便在 <root> 或 <logger> 标签中引用它。
class="ch.qos.logback.core.ConsoleAppender": 指定使用的实现类。
ConsoleAppender 是 Logback 库中专门用于将日志事件写入 System.out (标准输出) 或 System.err (标准错误) 的类。
这是我们在本地开发和测试时最常用的 Appender。
CONSOLE_LOG_PATTERN 是上面include的默认日志格式这里直接引用即可
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 全量info日志-->
<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">
<!-- 只记录 INFO 级别以及以上的日志的日志 -->
<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">
<!-- 只记录 ERROR 级别以及以上的日志的日志 -->
<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>

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,23 @@
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")
@SpringBootApplication(scanBasePackages = {
"cn.meowrain.aioj.backend.blogservice",
"cn.meowrain.aioj.backend.framework"
})
public class BlogServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BlogServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,112 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.ArticleCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.ArticleQueryRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.ArticleUpdateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.ArticleListResponseDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.ArticleResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.ArticleService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 文章管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/article")
@Tag(name = "文章管理", description = "文章的创建、查询、更新、删除等接口")
public class ArticleController {
private final ArticleService articleService;
@PostMapping("/create")
@Operation(summary = "创建文章", description = "创建新文章支持Markdown格式")
public Result<Long> createArticle(
@Parameter(description = "文章创建信息", required = true)
@Valid @RequestBody ArticleCreateRequestDTO request) {
Long articleId = articleService.createArticle(request);
return Results.success(articleId);
}
@PutMapping("/update")
@Operation(summary = "更新文章", description = "更新已存在的文章信息")
public Result<Void> updateArticle(
@Parameter(description = "文章更新信息", required = true)
@Valid @RequestBody ArticleUpdateRequestDTO request) {
articleService.updateArticle(request);
return Results.success();
}
@DeleteMapping("/delete/{articleId}")
@Operation(summary = "删除文章", description = "逻辑删除指定文章")
public Result<Void> deleteArticle(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
articleService.deleteArticle(articleId);
return Results.success();
}
@GetMapping("/{articleId}")
@Operation(summary = "查询文章详情", description = "根据文章ID查询文章详细信息")
public Result<ArticleResponseDTO> getArticleById(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
ArticleResponseDTO article = articleService.getArticleById(articleId);
return Results.success(article);
}
@GetMapping("/slug/{slug}")
@Operation(summary = "根据slug查询文章", description = "根据文章别名查询文章详细信息")
public Result<ArticleResponseDTO> getArticleBySlug(
@Parameter(description = "文章别名", required = true)
@PathVariable("slug") String slug) {
ArticleResponseDTO article = articleService.getArticleBySlug(slug);
return Results.success(article);
}
@PostMapping("/list")
@Operation(summary = "分页查询文章列表", description = "支持多条件查询和排序")
public Result<IPage<ArticleListResponseDTO>> getArticleList(
@Parameter(description = "查询条件")
@RequestBody ArticleQueryRequestDTO request) {
IPage<ArticleListResponseDTO> page = articleService.getArticleList(request);
return Results.success(page);
}
@PutMapping("/publish/{articleId}")
@Operation(summary = "发布文章", description = "将草稿状态的文章发布")
public Result<Void> publishArticle(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
articleService.publishArticle(articleId);
return Results.success();
}
@PutMapping("/unpublish/{articleId}")
@Operation(summary = "取消发布文章", description = "将已发布的文章转为草稿状态")
public Result<Void> unpublishArticle(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
articleService.unpublishArticle(articleId);
return Results.success();
}
@PostMapping("/view/{articleId}")
@Operation(summary = "增加文章浏览量", description = "记录文章浏览并增加浏览计数")
public Result<Void> incrementViewCount(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
articleService.incrementViewCount(articleId);
return Results.success();
}
}

View File

@@ -0,0 +1,89 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.CategoryCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.CategoryUpdateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.CategoryResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.CategoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 文章分类控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/category")
@Tag(name = "文章分类管理", description = "文章分类的创建、查询、更新、删除等接口")
public class CategoryController {
private final CategoryService categoryService;
@PostMapping("/create")
@Operation(summary = "创建分类", description = "创建新的文章分类")
public Result<Long> createCategory(
@Parameter(description = "分类创建信息", required = true)
@Valid @RequestBody CategoryCreateRequestDTO request) {
Long categoryId = categoryService.createCategory(request);
return Results.success(categoryId);
}
@PutMapping("/update")
@Operation(summary = "更新分类", description = "更新已存在的分类信息")
public Result<Void> updateCategory(
@Parameter(description = "分类更新信息", required = true)
@Valid @RequestBody CategoryUpdateRequestDTO request) {
categoryService.updateCategory(request);
return Results.success();
}
@DeleteMapping("/delete/{categoryId}")
@Operation(summary = "删除分类", description = "删除指定分类")
public Result<Void> deleteCategory(
@Parameter(description = "分类ID", required = true)
@PathVariable("categoryId") Long categoryId) {
categoryService.deleteCategory(categoryId);
return Results.success();
}
@GetMapping("/{categoryId}")
@Operation(summary = "查询分类详情", description = "根据分类ID查询分类详细信息")
public Result<CategoryResponseDTO> getCategoryById(
@Parameter(description = "分类ID", required = true)
@PathVariable("categoryId") Long categoryId) {
CategoryResponseDTO category = categoryService.getCategoryById(categoryId);
return Results.success(category);
}
@GetMapping("/slug/{slug}")
@Operation(summary = "根据slug查询分类", description = "根据分类别名查询分类详细信息")
public Result<CategoryResponseDTO> getCategoryBySlug(
@Parameter(description = "分类别名", required = true)
@PathVariable("slug") String slug) {
CategoryResponseDTO category = categoryService.getCategoryBySlug(slug);
return Results.success(category);
}
@GetMapping("/list/all")
@Operation(summary = "查询所有分类", description = "获取所有分类列表")
public Result<List<CategoryResponseDTO>> getAllCategories() {
List<CategoryResponseDTO> categories = categoryService.getAllCategories();
return Results.success(categories);
}
@GetMapping("/list/enabled")
@Operation(summary = "查询启用的分类", description = "获取所有启用状态的分类列表")
public Result<List<CategoryResponseDTO>> getEnabledCategories() {
List<CategoryResponseDTO> categories = categoryService.getEnabledCategories();
return Results.success(categories);
}
}

View File

@@ -0,0 +1,98 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.CollectionFolderCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.CollectionRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.ArticleListResponseDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.CollectionFolderResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.CollectionService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 收藏管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/collection")
@Tag(name = "收藏管理", description = "文章收藏、收藏夹管理等接口")
public class CollectionController {
private final CollectionService collectionService;
@PostMapping("/collect")
@Operation(summary = "收藏文章", description = "将文章添加到收藏夹")
public Result<Void> collectArticle(
@Parameter(description = "收藏信息", required = true)
@Valid @RequestBody CollectionRequestDTO request) {
collectionService.collectArticle(request);
return Results.success();
}
@DeleteMapping("/uncollect/{articleId}")
@Operation(summary = "取消收藏文章", description = "取消收藏指定文章")
public Result<Void> uncollectArticle(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
collectionService.uncollectArticle(articleId);
return Results.success();
}
@GetMapping("/check/{articleId}")
@Operation(summary = "查询收藏状态", description = "查询当前用户是否收藏了指定文章")
public Result<Boolean> isCollected(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
Long currentUserId = cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils.getCurrentUserId();
Boolean collected = collectionService.isCollected(currentUserId, articleId);
return Results.success(collected);
}
@PostMapping("/folder/create")
@Operation(summary = "创建收藏夹", description = "创建新的收藏夹")
public Result<Long> createCollectionFolder(
@Parameter(description = "收藏夹创建信息", required = true)
@Valid @RequestBody CollectionFolderCreateRequestDTO request) {
Long folderId = collectionService.createCollectionFolder(request);
return Results.success(folderId);
}
@DeleteMapping("/folder/delete/{folderId}")
@Operation(summary = "删除收藏夹", description = "删除指定收藏夹")
public Result<Void> deleteCollectionFolder(
@Parameter(description = "收藏夹ID", required = true)
@PathVariable("folderId") Long folderId) {
collectionService.deleteCollectionFolder(folderId);
return Results.success();
}
@GetMapping("/folder/list")
@Operation(summary = "查询收藏夹列表", description = "获取当前用户的所有收藏夹")
public Result<List<CollectionFolderResponseDTO>> getCollectionFolders() {
List<CollectionFolderResponseDTO> folders = collectionService.getCollectionFolders();
return Results.success(folders);
}
@GetMapping("/list")
@Operation(summary = "分页查询收藏列表", description = "获取当前用户收藏的文章列表")
public Result<IPage<ArticleListResponseDTO>> getCollectedArticles(
@Parameter(description = "页码", required = true)
@RequestParam("current") Integer current,
@Parameter(description = "每页数量", required = true)
@RequestParam("size") Integer size,
@Parameter(description = "收藏夹ID可选")
@RequestParam(value = "folderId", required = false) Long folderId) {
IPage<ArticleListResponseDTO> page = collectionService.getCollectedArticles(current, size, folderId);
return Results.success(page);
}
}

View File

@@ -0,0 +1,94 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.CommentCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.CommentQueryRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.CommentResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.CommentService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 评论管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/comment")
@Tag(name = "评论管理", description = "评论的发表、查询、删除等接口")
public class CommentController {
private final CommentService commentService;
@PostMapping("/create")
@Operation(summary = "发表评论", description = "发表文章评论支持Markdown格式")
public Result<Long> createComment(
@Parameter(description = "评论创建信息", required = true)
@Valid @RequestBody CommentCreateRequestDTO request) {
Long commentId = commentService.createComment(request);
return Results.success(commentId);
}
@PostMapping("/reply")
@Operation(summary = "回复评论", description = "回复指定评论")
public Result<Long> replyComment(
@Parameter(description = "评论回复信息", required = true)
@Valid @RequestBody CommentCreateRequestDTO request) {
Long commentId = commentService.replyComment(request);
return Results.success(commentId);
}
@DeleteMapping("/delete/{commentId}")
@Operation(summary = "删除评论", description = "删除指定评论")
public Result<Void> deleteComment(
@Parameter(description = "评论ID", required = true)
@PathVariable("commentId") Long commentId) {
commentService.deleteComment(commentId);
return Results.success();
}
@GetMapping("/{commentId}")
@Operation(summary = "查询评论详情", description = "根据评论ID查询评论详细信息")
public Result<CommentResponseDTO> getCommentById(
@Parameter(description = "评论ID", required = true)
@PathVariable("commentId") Long commentId) {
CommentResponseDTO comment = commentService.getCommentById(commentId);
return Results.success(comment);
}
@GetMapping("/article/{articleId}")
@Operation(summary = "查询文章评论", description = "根据文章ID查询该文章的所有评论树形结构")
public Result<List<CommentResponseDTO>> getCommentsByArticleId(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
List<CommentResponseDTO> comments = commentService.getCommentsByArticleId(articleId);
return Results.success(comments);
}
@PostMapping("/list")
@Operation(summary = "分页查询评论列表", description = "支持多条件查询")
public Result<IPage<CommentResponseDTO>> getCommentList(
@Parameter(description = "查询条件")
@RequestBody CommentQueryRequestDTO request) {
IPage<CommentResponseDTO> page = commentService.getCommentList(request);
return Results.success(page);
}
@GetMapping("/children/{parentId}")
@Operation(summary = "查询子评论", description = "查询指定评论的所有子评论")
public Result<List<CommentResponseDTO>> getChildComments(
@Parameter(description = "父评论ID", required = true)
@PathVariable("parentId") Long parentId) {
List<CommentResponseDTO> comments = commentService.getChildComments(parentId);
return Results.success(comments);
}
}

View File

@@ -0,0 +1,73 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.DraftSaveRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.DraftResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.DraftService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 草稿箱控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/draft")
@Tag(name = "草稿箱管理", description = "草稿保存、查询、删除等接口")
public class DraftController {
private final DraftService draftService;
@PostMapping("/save")
@Operation(summary = "保存草稿", description = "保存或更新文章草稿")
public Result<Long> saveDraft(
@Parameter(description = "草稿信息", required = true)
@Valid @RequestBody DraftSaveRequestDTO request) {
Long draftId = draftService.saveDraft(request);
return Results.success(draftId);
}
@DeleteMapping("/delete/{draftId}")
@Operation(summary = "删除草稿", description = "删除指定草稿")
public Result<Void> deleteDraft(
@Parameter(description = "草稿ID", required = true)
@PathVariable("draftId") Long draftId) {
draftService.deleteDraft(draftId);
return Results.success();
}
@GetMapping("/{draftId}")
@Operation(summary = "查询草稿详情", description = "根据草稿ID查询草稿详细信息")
public Result<DraftResponseDTO> getDraftById(
@Parameter(description = "草稿ID", required = true)
@PathVariable("draftId") Long draftId) {
DraftResponseDTO draft = draftService.getDraftById(draftId);
return Results.success(draft);
}
@GetMapping("/list")
@Operation(summary = "分页查询草稿列表", description = "获取当前用户的草稿列表")
public Result<IPage<DraftResponseDTO>> getDraftList(
@Parameter(description = "页码", required = true)
@RequestParam("current") Integer current,
@Parameter(description = "每页数量", required = true)
@RequestParam("size") Integer size) {
IPage<DraftResponseDTO> page = draftService.getDraftList(current, size);
return Results.success(page);
}
@GetMapping("/latest")
@Operation(summary = "查询最新草稿", description = "获取最新的自动保存草稿")
public Result<DraftResponseDTO> getLatestDraft() {
DraftResponseDTO draft = draftService.getLatestDraft();
return Results.success(draft);
}
}

View File

@@ -0,0 +1,99 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.FollowRequestDTO;
import cn.meowrain.aioj.backend.blogservice.service.FollowService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 用户关注控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/follow")
@Tag(name = "用户关注管理", description = "关注、取消关注、查询关注列表等接口")
public class FollowController {
private final FollowService followService;
@PostMapping("/follow")
@Operation(summary = "关注用户", description = "关注指定用户")
public Result<Void> followUser(
@Parameter(description = "关注信息", required = true)
@Valid @RequestBody FollowRequestDTO request) {
followService.followUser(request);
return Results.success();
}
@DeleteMapping("/unfollow/{followingId}")
@Operation(summary = "取消关注用户", description = "取消关注指定用户")
public Result<Void> unfollowUser(
@Parameter(description = "被关注者ID", required = true)
@PathVariable("followingId") Long followingId) {
followService.unfollowUser(followingId);
return Results.success();
}
@GetMapping("/check/{followingId}")
@Operation(summary = "查询关注状态", description = "查询当前用户是否关注了指定用户")
public Result<Boolean> isFollowing(
@Parameter(description = "被关注者ID", required = true)
@PathVariable("followingId") Long followingId) {
Long currentUserId = cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils.getCurrentUserId();
Boolean following = followService.isFollowing(currentUserId, followingId);
return Results.success(following);
}
@GetMapping("/following/list")
@Operation(summary = "查询关注列表", description = "分页查询当前用户关注的人")
public Result<IPage<Long>> getFollowingList(
@Parameter(description = "用户ID", required = true)
@RequestParam("userId") Long userId,
@Parameter(description = "页码", required = true)
@RequestParam("current") Integer current,
@Parameter(description = "每页数量", required = true)
@RequestParam("size") Integer size) {
IPage<Long> page = followService.getFollowingList(userId, current, size);
return Results.success(page);
}
@GetMapping("/followers/list")
@Operation(summary = "查询粉丝列表", description = "分页查询当前用户的粉丝")
public Result<IPage<Long>> getFollowerList(
@Parameter(description = "用户ID", required = true)
@RequestParam("userId") Long userId,
@Parameter(description = "页码", required = true)
@RequestParam("current") Integer current,
@Parameter(description = "每页数量", required = true)
@RequestParam("size") Integer size) {
IPage<Long> page = followService.getFollowerList(userId, current, size);
return Results.success(page);
}
@GetMapping("/following/count")
@Operation(summary = "查询关注数", description = "获取用户的关注数量")
public Result<Long> getFollowingCount(
@Parameter(description = "用户ID", required = true)
@RequestParam("userId") Long userId) {
Long count = followService.getFollowingCount(userId);
return Results.success(count);
}
@GetMapping("/followers/count")
@Operation(summary = "查询粉丝数", description = "获取用户的粉丝数量")
public Result<Long> getFollowerCount(
@Parameter(description = "用户ID", required = true)
@RequestParam("userId") Long userId) {
Long count = followService.getFollowerCount(userId);
return Results.success(count);
}
}

View File

@@ -0,0 +1,65 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.LikeRequestDTO;
import cn.meowrain.aioj.backend.blogservice.service.LikeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 点赞管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/like")
@Tag(name = "点赞管理", description = "文章和评论的点赞、取消点赞等接口")
public class LikeController {
private final LikeService likeService;
@PostMapping("/like")
@Operation(summary = "点赞", description = "对文章或评论进行点赞")
public Result<Void> like(
@Parameter(description = "点赞信息", required = true)
@Valid @RequestBody LikeRequestDTO request) {
likeService.like(request);
return Results.success();
}
@PostMapping("/unlike")
@Operation(summary = "取消点赞", description = "取消对文章或评论的点赞")
public Result<Void> unlike(
@Parameter(description = "点赞信息", required = true)
@Valid @RequestBody LikeRequestDTO request) {
likeService.unlike(request);
return Results.success();
}
@GetMapping("/check")
@Operation(summary = "查询点赞状态", description = "查询当前用户对指定目标的点赞状态")
public Result<Boolean> isLiked(
@Parameter(description = "目标ID", required = true)
@RequestParam("targetId") Long targetId,
@Parameter(description = "目标类型1=文章2=评论", required = true)
@RequestParam("targetType") Integer targetType) {
Long currentUserId = cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils.getCurrentUserId();
Boolean liked = likeService.isLiked(currentUserId, targetId, targetType);
return Results.success(liked);
}
@PostMapping("/toggle")
@Operation(summary = "切换点赞状态", description = "点赞或取消点赞,根据当前状态自动切换")
public Result<Void> toggleLike(
@Parameter(description = "点赞信息", required = true)
@Valid @RequestBody LikeRequestDTO request) {
likeService.toggleLike(request);
return Results.success();
}
}

View File

@@ -0,0 +1,75 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.NotificationQueryRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.NotificationResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.NotificationService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 通知管理控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/notification")
@Tag(name = "通知管理", description = "通知查询、标记已读、清空通知等接口")
public class NotificationController {
private final NotificationService notificationService;
@PostMapping("/list")
@Operation(summary = "分页查询通知列表", description = "支持多条件查询")
public Result<IPage<NotificationResponseDTO>> getNotificationList(
@Parameter(description = "查询条件")
@RequestBody NotificationQueryRequestDTO request) {
IPage<NotificationResponseDTO> page = notificationService.getNotificationList(request);
return Results.success(page);
}
@PutMapping("/read/{notificationId}")
@Operation(summary = "标记通知为已读", description = "将指定通知标记为已读状态")
public Result<Void> markAsRead(
@Parameter(description = "通知ID", required = true)
@PathVariable("notificationId") Long notificationId) {
notificationService.markAsRead(notificationId);
return Results.success();
}
@PutMapping("/read/batch")
@Operation(summary = "批量标记通知为已读", description = "将多个通知批量标记为已读状态")
public Result<Void> batchMarkAsRead(
@Parameter(description = "通知ID列表", required = true)
@RequestBody java.util.List<Long> notificationIds) {
notificationService.batchMarkAsRead(notificationIds);
return Results.success();
}
@PutMapping("/read/all")
@Operation(summary = "标记所有通知为已读", description = "将当前用户的所有未读通知标记为已读")
public Result<Void> markAllAsRead() {
notificationService.markAllAsRead();
return Results.success();
}
@DeleteMapping("/clear/all")
@Operation(summary = "清空所有通知", description = "清空当前用户的所有通知")
public Result<Void> clearAllNotifications() {
notificationService.clearAllNotifications();
return Results.success();
}
@GetMapping("/unread/count")
@Operation(summary = "查询未读通知数量", description = "获取当前用户的未读通知数量")
public Result<Long> getUnreadCount() {
Long count = notificationService.getUnreadCount();
return Results.success(count);
}
}

View File

@@ -0,0 +1,98 @@
package cn.meowrain.aioj.backend.blogservice.controller;
import cn.meowrain.aioj.backend.framework.core.web.Result;
import cn.meowrain.aioj.backend.framework.core.web.Results;
import cn.meowrain.aioj.backend.blogservice.dto.req.TagCreateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.req.TagUpdateRequestDTO;
import cn.meowrain.aioj.backend.blogservice.dto.resp.TagResponseDTO;
import cn.meowrain.aioj.backend.blogservice.service.TagService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 文章标签控制器
*
* @author AIOJ
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/tag")
@Tag(name = "文章标签管理", description = "文章标签的创建、查询、更新、删除等接口")
public class TagController {
private final TagService tagService;
@PostMapping("/create")
@Operation(summary = "创建标签", description = "创建新的文章标签")
public Result<Long> createTag(
@Parameter(description = "标签创建信息", required = true)
@Valid @RequestBody TagCreateRequestDTO request) {
Long tagId = tagService.createTag(request);
return Results.success(tagId);
}
@PutMapping("/update")
@Operation(summary = "更新标签", description = "更新已存在的标签信息")
public Result<Void> updateTag(
@Parameter(description = "标签更新信息", required = true)
@Valid @RequestBody TagUpdateRequestDTO request) {
tagService.updateTag(request);
return Results.success();
}
@DeleteMapping("/delete/{tagId}")
@Operation(summary = "删除标签", description = "删除指定标签")
public Result<Void> deleteTag(
@Parameter(description = "标签ID", required = true)
@PathVariable("tagId") Long tagId) {
tagService.deleteTag(tagId);
return Results.success();
}
@GetMapping("/{tagId}")
@Operation(summary = "查询标签详情", description = "根据标签ID查询标签详细信息")
public Result<TagResponseDTO> getTagById(
@Parameter(description = "标签ID", required = true)
@PathVariable("tagId") Long tagId) {
TagResponseDTO tag = tagService.getTagById(tagId);
return Results.success(tag);
}
@GetMapping("/slug/{slug}")
@Operation(summary = "根据slug查询标签", description = "根据标签别名查询标签详细信息")
public Result<TagResponseDTO> getTagBySlug(
@Parameter(description = "标签别名", required = true)
@PathVariable("slug") String slug) {
TagResponseDTO tag = tagService.getTagBySlug(slug);
return Results.success(tag);
}
@GetMapping("/list/all")
@Operation(summary = "查询所有标签", description = "获取所有标签列表")
public Result<List<TagResponseDTO>> getAllTags() {
List<TagResponseDTO> tags = tagService.getAllTags();
return Results.success(tags);
}
@GetMapping("/list/enabled")
@Operation(summary = "查询启用的标签", description = "获取所有启用状态的标签列表")
public Result<List<TagResponseDTO>> getEnabledTags() {
List<TagResponseDTO> tags = tagService.getEnabledTags();
return Results.success(tags);
}
@GetMapping("/article/{articleId}")
@Operation(summary = "查询文章的标签", description = "根据文章ID查询该文章的所有标签")
public Result<List<TagResponseDTO>> getTagsByArticleId(
@Parameter(description = "文章ID", required = true)
@PathVariable("articleId") Long articleId) {
List<TagResponseDTO> tags = tagService.getTagsByArticleId(articleId);
return Results.success(tags);
}
}

View File

@@ -0,0 +1,123 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 文章实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_article")
@Accessors(chain = true)
public class Article implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 文章标题
*/
private String title;
/**
* 文章别名URL友好标识用于SEO
*/
private String slug;
/**
* 文章摘要
*/
private String summary;
/**
* 文章内容Markdown格式
*/
private String content;
/**
* 渲染后的HTML内容可选缓存
*/
private String contentHtml;
/**
* 封面图片URL
*/
private String coverImage;
/**
* 作者用户ID
*/
private Long authorId;
/**
* 分类ID
*/
private Long categoryId;
/**
* 浏览次数
*/
private Integer viewCount;
/**
* 点赞数(冗余字段,便于查询)
*/
private Integer likeCount;
/**
* 评论数(冗余字段,便于查询)
*/
private Integer commentCount;
/**
* 收藏数(冗余字段,便于查询)
*/
private Integer collectCount;
/**
* 是否置顶1=置顶0=普通)
*/
private Integer isTop;
/**
* 是否精华1=精华0=普通)
*/
private Integer isEssence;
/**
* 是否发布1=已发布0=草稿)
*/
private Integer isPublished;
/**
* 文章状态1=正常2=审核中3=已关闭4=已删除
*/
private Integer status;
/**
* 发布时间
*/
private Date publishTime;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,43 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 文章标签关联实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_article_tag")
@Accessors(chain = true)
public class ArticleTag implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 文章ID
*/
private Long articleId;
/**
* 标签ID
*/
private Long tagId;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,78 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 文章分类实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_category")
@Accessors(chain = true)
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 分类名称
*/
private String name;
/**
* 分类别名URL友好标识
*/
private String slug;
/**
* 分类描述
*/
private String description;
/**
* 分类图标
*/
private String icon;
/**
* 排序序号(越小越靠前)
*/
private Integer sortOrder;
/**
* 父分类ID0表示顶级分类
*/
private Long parentId;
/**
* 该分类下的文章数量
*/
private Integer articleCount;
/**
* 是否启用1=启用0=禁用)
*/
private Integer isEnabled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,63 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 收藏实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_collection")
@Accessors(chain = true)
public class Collection implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 文章ID
*/
private Long articleId;
/**
* 收藏夹ID0表示默认收藏夹
*/
private Long folderId;
/**
* 收藏备注
*/
private String note;
/**
* 是否已取消1=已取消0=有效)
*/
private Integer isCancelled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,63 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 收藏夹实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_collection_folder")
@Accessors(chain = true)
public class CollectionFolder implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 收藏夹名称
*/
private String name;
/**
* 收藏夹描述
*/
private String description;
/**
* 是否公开1=公开0=私有)
*/
private Integer isPublic;
/**
* 收藏数量
*/
private Integer collectCount;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,98 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 评论实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_comment")
@Accessors(chain = true)
public class Comment implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 文章ID
*/
private Long articleId;
/**
* 评论用户ID
*/
private Long userId;
/**
* 父评论ID0表示一级评论
*/
private Long parentId;
/**
* 回复的评论ID用于@提醒)
*/
private Long replyToId;
/**
* 评论内容支持Markdown
*/
private String content;
/**
* 渲染后的HTML内容
*/
private String contentHtml;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 回复数
*/
private Integer replyCount;
/**
* 是否为作者评论1=是0=否)
*/
private Integer isAuthor;
/**
* 状态1=正常2=待审核3=已删除
*/
private Integer status;
/**
* IP地址
*/
private String ipAddress;
/**
* 用户代理
*/
private String userAgent;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,68 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 草稿箱实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_draft")
@Accessors(chain = true)
public class Draft implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 文章标题
*/
private String title;
/**
* 文章内容Markdown格式
*/
private String content;
/**
* 分类ID
*/
private Long categoryId;
/**
* 封面图片URL
*/
private String coverImage;
/**
* 是否自动保存1=是0=否)
*/
private Integer autoSave;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,53 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 用户关注实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_follow")
@Accessors(chain = true)
public class Follow implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 关注者ID
*/
private Long followerId;
/**
* 被关注者ID
*/
private Long followingId;
/**
* 是否已取消1=已取消0=有效)
*/
private Integer isCancelled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,58 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 点赞实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_like")
@Accessors(chain = true)
public class Like implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 目标ID文章ID或评论ID
*/
private Long targetId;
/**
* 目标类型1=文章2=评论
*/
private Integer targetType;
/**
* 是否已取消1=已取消0=有效)
*/
private Integer isCancelled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,73 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 通知实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_notification")
@Accessors(chain = true)
public class Notification implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 接收用户ID
*/
private Long userId;
/**
* 通知类型1=评论2=点赞3=收藏4=关注5=回复6=系统通知
*/
private Integer type;
/**
* 通知标题
*/
private String title;
/**
* 通知内容
*/
private String content;
/**
* 跳转链接
*/
private String linkUrl;
/**
* 发送者ID系统通知为NULL
*/
private Long senderId;
/**
* 关联目标ID文章ID、评论ID等
*/
private Long targetId;
/**
* 是否已读1=已读0=未读)
*/
private Integer isRead;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,68 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 文章标签实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_tag")
@Accessors(chain = true)
public class Tag implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 标签名称
*/
private String name;
/**
* 标签别名URL友好标识
*/
private String slug;
/**
* 标签描述
*/
private String description;
/**
* 标签颜色(十六进制)
*/
private String color;
/**
* 使用该标签的文章数量
*/
private Integer articleCount;
/**
* 是否启用1=启用0=禁用)
*/
private Integer isEnabled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,88 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 用户社区统计实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_user_stat")
@Accessors(chain = true)
public class UserStat implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 发布文章数
*/
private Integer articleCount;
/**
* 发表评论数
*/
private Integer commentCount;
/**
* 获得点赞数
*/
private Integer likeCount;
/**
* 获得收藏数
*/
private Integer collectCount;
/**
* 文章被浏览数
*/
private Integer viewCount;
/**
* 粉丝数
*/
private Integer followerCount;
/**
* 关注数
*/
private Integer followingCount;
/**
* 用户等级
*/
private Integer level;
/**
* 经验值
*/
private Integer experience;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,58 @@
package cn.meowrain.aioj.backend.blogservice.dao.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 浏览记录实体类
*
* @author AIOJ
*/
@Data
@TableName(value = "blog_view")
@Accessors(chain = true)
public class View implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 文章ID
*/
private Long articleId;
/**
* 用户IDNULL表示游客
*/
private Long userId;
/**
* IP地址
*/
private String ipAddress;
/**
* 用户代理
*/
private String userAgent;
/**
* 浏览时长(秒)
*/
private Integer duration;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Article;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 文章Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface ArticleMapper extends BaseMapper<Article> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.ArticleTag;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 文章标签关联Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface ArticleTagMapper extends BaseMapper<ArticleTag> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Category;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 文章分类Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.CollectionFolder;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 收藏夹Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface CollectionFolderMapper extends BaseMapper<CollectionFolder> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Collection;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 收藏Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface CollectionMapper extends BaseMapper<Collection> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Comment;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 评论Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface CommentMapper extends BaseMapper<Comment> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Draft;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 草稿箱Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface DraftMapper extends BaseMapper<Draft> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Follow;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户关注Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface FollowMapper extends BaseMapper<Follow> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Like;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 点赞Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface LikeMapper extends BaseMapper<Like> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Notification;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 通知Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface NotificationMapper extends BaseMapper<Notification> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.Tag;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 文章标签Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface TagMapper extends BaseMapper<Tag> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.UserStat;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户社区统计Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface UserStatMapper extends BaseMapper<UserStat> {
}

View File

@@ -0,0 +1,15 @@
package cn.meowrain.aioj.backend.blogservice.dao.mapper;
import cn.meowrain.aioj.backend.blogservice.dao.entity.View;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 浏览记录Mapper接口
*
* @author AIOJ
*/
@Mapper
public interface ViewMapper extends BaseMapper<View> {
}

View File

@@ -0,0 +1,51 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 文章创建请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "文章创建请求")
public class ArticleCreateRequestDTO {
@NotBlank(message = "文章标题不能为空")
@Schema(description = "文章标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "Spring Boot 3.5.7 新特性详解")
private String title;
@Schema(description = "文章别名URL友好标识", example = "spring-boot-3-5-7-features")
private String slug;
@Schema(description = "文章摘要", example = "本文详细介绍Spring Boot 3.5.7版本的新特性...")
private String summary;
@NotBlank(message = "文章内容不能为空")
@Schema(description = "文章内容Markdown格式", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "封面图片URL", example = "https://example.com/images/cover.jpg")
private String coverImage;
@NotNull(message = "分类ID不能为空")
@Schema(description = "分类ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long categoryId;
@Schema(description = "标签ID列表", example = "[1, 2, 3]")
private List<Long> tagIds;
@Schema(description = "是否置顶", example = "0")
private Integer isTop;
@Schema(description = "是否精华", example = "0")
private Integer isEssence;
@Schema(description = "是否发布1=发布0=草稿)", example = "1")
private Integer isPublished;
}

View File

@@ -0,0 +1,53 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 文章查询请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "文章查询请求")
public class ArticleQueryRequestDTO {
@Schema(description = "页码", example = "1")
private Integer current = 1;
@Schema(description = "每页数量", example = "10")
private Integer size = 10;
@Schema(description = "文章ID", example = "1")
private Long id;
@Schema(description = "文章标题(模糊查询)", example = "Spring Boot")
private String title;
@Schema(description = "分类ID", example = "1")
private Long categoryId;
@Schema(description = "标签ID", example = "1")
private Long tagId;
@Schema(description = "作者ID", example = "1")
private Long authorId;
@Schema(description = "是否置顶", example = "0")
private Integer isTop;
@Schema(description = "是否精华", example = "0")
private Integer isEssence;
@Schema(description = "是否发布1=发布0=草稿)", example = "1")
private Integer isPublished;
@Schema(description = "文章状态1=正常2=审核中3=已关闭4=已删除", example = "1")
private Integer status;
@Schema(description = "排序字段view_count/like_count/comment_count/publish_time", example = "publish_time")
private String sortField;
@Schema(description = "排序方式asc/desc", example = "desc")
private String sortOrder;
}

View File

@@ -0,0 +1,55 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 文章更新请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "文章更新请求")
public class ArticleUpdateRequestDTO {
@NotNull(message = "文章ID不能为空")
@Schema(description = "文章ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@NotBlank(message = "文章标题不能为空")
@Schema(description = "文章标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "Spring Boot 3.5.7 新特性详解(更新版)")
private String title;
@Schema(description = "文章别名URL友好标识", example = "spring-boot-3-5-7-features")
private String slug;
@Schema(description = "文章摘要", example = "本文详细介绍Spring Boot 3.5.7版本的新特性...")
private String summary;
@NotBlank(message = "文章内容不能为空")
@Schema(description = "文章内容Markdown格式", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "封面图片URL", example = "https://example.com/images/cover.jpg")
private String coverImage;
@NotNull(message = "分类ID不能为空")
@Schema(description = "分类ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long categoryId;
@Schema(description = "标签ID列表", example = "[1, 2, 3]")
private List<Long> tagIds;
@Schema(description = "是否置顶", example = "0")
private Integer isTop;
@Schema(description = "是否精华", example = "0")
private Integer isEssence;
@Schema(description = "是否发布1=发布0=草稿)", example = "1")
private Integer isPublished;
}

View File

@@ -0,0 +1,40 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 分类创建请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "分类创建请求")
public class CategoryCreateRequestDTO {
@NotBlank(message = "分类名称不能为空")
@Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "算法题解")
private String name;
@NotBlank(message = "分类别名不能为空")
@Schema(description = "分类别名URL友好标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "algorithm-solutions")
private String slug;
@Schema(description = "分类描述", example = "算法题目解题思路和代码分享")
private String description;
@Schema(description = "分类图标", example = "💡")
private String icon;
@NotNull(message = "排序序号不能为空")
@Schema(description = "排序序号(越小越靠前)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer sortOrder;
@Schema(description = "父分类ID0表示顶级分类", example = "0")
private Long parentId = 0L;
@Schema(description = "是否启用1=启用0=禁用)", example = "1")
private Integer isEnabled = 1;
}

View File

@@ -0,0 +1,44 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 分类更新请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "分类更新请求")
public class CategoryUpdateRequestDTO {
@NotNull(message = "分类ID不能为空")
@Schema(description = "分类ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@NotBlank(message = "分类名称不能为空")
@Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "算法题解")
private String name;
@NotBlank(message = "分类别名不能为空")
@Schema(description = "分类别名URL友好标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "algorithm-solutions")
private String slug;
@Schema(description = "分类描述", example = "算法题目解题思路和代码分享")
private String description;
@Schema(description = "分类图标", example = "💡")
private String icon;
@NotNull(message = "排序序号不能为空")
@Schema(description = "排序序号(越小越靠前)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer sortOrder;
@Schema(description = "父分类ID0表示顶级分类", example = "0")
private Long parentId;
@Schema(description = "是否启用1=启用0=禁用)", example = "1")
private Integer isEnabled;
}

View File

@@ -0,0 +1,25 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 收藏夹创建请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "收藏夹创建请求")
public class CollectionFolderCreateRequestDTO {
@NotBlank(message = "收藏夹名称不能为空")
@Schema(description = "收藏夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我的技术收藏")
private String name;
@Schema(description = "收藏夹描述", example = "收集技术相关的优质文章")
private String description;
@Schema(description = "是否公开1=公开0=私有)", example = "0")
private Integer isPublic = 0;
}

View File

@@ -0,0 +1,25 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 收藏请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "收藏请求")
public class CollectionRequestDTO {
@NotNull(message = "文章ID不能为空")
@Schema(description = "文章ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long articleId;
@Schema(description = "收藏夹ID0表示默认收藏夹", example = "0")
private Long folderId = 0L;
@Schema(description = "收藏备注", example = "很好的文章,值得学习")
private String note;
}

View File

@@ -0,0 +1,30 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 评论创建请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "评论创建请求")
public class CommentCreateRequestDTO {
@NotNull(message = "文章ID不能为空")
@Schema(description = "文章ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long articleId;
@NotBlank(message = "评论内容不能为空")
@Schema(description = "评论内容支持Markdown", requiredMode = Schema.RequiredMode.REQUIRED, example = "这篇文章写得很好!")
private String content;
@Schema(description = "父评论ID0表示一级评论", example = "0")
private Long parentId = 0L;
@Schema(description = "回复的评论ID用于@提醒)", example = "0")
private Long replyToId = 0L;
}

View File

@@ -0,0 +1,32 @@
package cn.meowrain.aioj.backend.blogservice.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 评论查询请求DTO
*
* @author AIOJ
*/
@Data
@Schema(description = "评论查询请求")
public class CommentQueryRequestDTO {
@Schema(description = "页码", example = "1")
private Integer current = 1;
@Schema(description = "每页数量", example = "10")
private Integer size = 10;
@Schema(description = "文章ID", example = "1")
private Long articleId;
@Schema(description = "用户ID", example = "1")
private Long userId;
@Schema(description = "父评论ID0表示查询一级评论", example = "0")
private Long parentId;
@Schema(description = "状态1=正常2=待审核3=已删除", example = "1")
private Integer status;
}

Some files were not shown because too many files have changed in this diff Show More