Compare commits
3 Commits
2cdbdfeaf2
...
8632dcd282
| Author | SHA1 | Date | |
|---|---|---|---|
| 8632dcd282 | |||
| 090cb65c0b | |||
| a73a92c0c2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,3 +38,4 @@ build/
|
|||||||
/.claude/agents/backend-architect.md
|
/.claude/agents/backend-architect.md
|
||||||
/.dockerignore
|
/.dockerignore
|
||||||
/Dockerfile
|
/Dockerfile
|
||||||
|
/.claude/ralph-loop.local.md
|
||||||
|
|||||||
141
README.md
141
README.md
@@ -1,26 +1,123 @@
|
|||||||
# SpringBoot 项目初始模板
|
# Keyborad Backend
|
||||||
|
|
||||||
> Java SpringBoot 项目初始模板,整合了常用框架和示例代码,大家可以在此基础上快速开发自己的项目。
|
基于 Spring Boot 3.5.5 的后端服务,集成了 AI 能力、向量搜索、Apple 登录等功能。
|
||||||
|
|
||||||
## 模板功能
|
## 技术栈
|
||||||
|
|
||||||
- Spring Boot 2.7.0(贼新)
|
- **Java 17** + **Spring Boot 3.5.5**
|
||||||
- Spring MVC
|
- **Spring AI** - LLM 对话和文本嵌入(OpenAI 兼容 API)
|
||||||
- MySQL 驱动
|
- **Qdrant** - 向量数据库,支持语义搜索
|
||||||
- MyBatis
|
- **PostgreSQL** - 关系型数据库
|
||||||
- MyBatis Plus
|
- **MyBatis Plus** - ORM 框架
|
||||||
- Spring Session Redis 分布式登录
|
- **Redis** - 会话存储和缓存
|
||||||
- Spring AOP
|
- **Sa-Token** - 认证授权框架
|
||||||
- Apache Commons Lang3 工具类
|
- **Knife4j** - API 文档
|
||||||
- Lombok 注解
|
- **X-File-Storage** - 文件上传(Cloudflare R2)
|
||||||
- Swagger + Knife4j 接口文档
|
- **MailerSend** - 邮件服务
|
||||||
- Spring Boot 调试工具和项目处理器
|
|
||||||
- 全局请求响应拦截器(记录日志)
|
|
||||||
- 全局异常处理器
|
|
||||||
- 自定义错误码
|
|
||||||
- 封装通用响应类
|
|
||||||
- 示例用户注册、登录、搜索功能
|
|
||||||
- 示例单元测试类
|
|
||||||
- 示例 SQL(用户表)
|
|
||||||
|
|
||||||
访问 localhost:7529/api/doc.html 就能在线调试接口了,不需要前端配合啦~
|
## 核心功能
|
||||||
|
|
||||||
|
### 认证系统
|
||||||
|
- Apple Sign-In JWT 验证
|
||||||
|
- Sa-Token 会话管理
|
||||||
|
- 请求签名校验(防篡改/防重放)
|
||||||
|
|
||||||
|
### AI 能力
|
||||||
|
- LLM 对话(支持流式响应)
|
||||||
|
- 文本嵌入(1536 维向量)
|
||||||
|
- 语义搜索(Qdrant 向量检索)
|
||||||
|
|
||||||
|
### 通用功能
|
||||||
|
- 统一响应格式
|
||||||
|
- 全局异常处理
|
||||||
|
- 国际化支持(i18n)
|
||||||
|
- 请求日志记录
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- JDK 17+
|
||||||
|
- Maven 3.8+
|
||||||
|
- PostgreSQL 14+
|
||||||
|
- Redis 6+
|
||||||
|
|
||||||
|
### 本地运行
|
||||||
|
|
||||||
|
1. 克隆项目
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd keyborad-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 配置数据库和 Redis
|
||||||
|
```yaml
|
||||||
|
# 修改 src/main/resources/application-dev.yml
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5432/keyborad_db
|
||||||
|
username: your_username
|
||||||
|
password: your_password
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 启动应用
|
||||||
|
```bash
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 访问 API 文档
|
||||||
|
```
|
||||||
|
http://localhost:7529/api/doc.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/main/java/com/yolo/keyborad/
|
||||||
|
├── controller/ # REST API 端点
|
||||||
|
├── service/ # 业务逻辑层
|
||||||
|
│ └── impl/ # 服务实现
|
||||||
|
├── mapper/ # MyBatis 数据库映射
|
||||||
|
├── model/
|
||||||
|
│ ├── entity/ # 数据库实体
|
||||||
|
│ ├── dto/ # 请求数据传输对象
|
||||||
|
│ └── vo/ # 响应视图对象
|
||||||
|
├── config/ # Spring 配置类
|
||||||
|
├── aop/ # AOP 拦截器
|
||||||
|
├── Interceptor/ # 请求拦截器
|
||||||
|
├── filter/ # Servlet 过滤器
|
||||||
|
├── exception/ # 异常处理
|
||||||
|
├── common/ # 通用工具类
|
||||||
|
└── utils/ # 工具类
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
| 配置项 | 说明 | 默认值 |
|
||||||
|
|-------|------|-------|
|
||||||
|
| `server.port` | 服务端口 | 7529 |
|
||||||
|
| `server.servlet.context-path` | 上下文路径 | /api |
|
||||||
|
| `spring.profiles.active` | 激活配置文件 | dev |
|
||||||
|
|
||||||
|
## API 认证
|
||||||
|
|
||||||
|
### Sa-Token 认证
|
||||||
|
需要在请求头中携带 `satoken` 字段。
|
||||||
|
|
||||||
|
### 请求签名
|
||||||
|
部分接口需要签名校验,请求头需包含:
|
||||||
|
- `X-App-Id` - 应用 ID
|
||||||
|
- `X-Timestamp` - 时间戳
|
||||||
|
- `X-Nonce` - 随机数
|
||||||
|
- `X-Sign` - 签名
|
||||||
|
|
||||||
|
## 开发指南
|
||||||
|
|
||||||
|
详细的开发指南请参考 [CLAUDE.md](./CLAUDE.md)。
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|||||||
21
pom.xml
21
pom.xml
@@ -280,6 +280,27 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>17</source>
|
||||||
|
<target>17</target>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.38</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.yolo.keyborad.Interceptor;
|
package com.yolo.keyborad.interceptor;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.yolo.keyborad.utils.SignUtils;
|
import com.yolo.keyborad.utils.SignUtils;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import cn.dev33.satoken.interceptor.SaInterceptor;
|
|||||||
import cn.dev33.satoken.router.SaHttpMethod;
|
import cn.dev33.satoken.router.SaHttpMethod;
|
||||||
import cn.dev33.satoken.router.SaRouter;
|
import cn.dev33.satoken.router.SaRouter;
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.yolo.keyborad.Interceptor.SignInterceptor;
|
import com.yolo.keyborad.interceptor.SignInterceptor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -64,7 +64,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
"/demo/embed",
|
"/demo/embed",
|
||||||
"/demo/testSaveEmbed",
|
"/demo/testSaveEmbed",
|
||||||
"/demo/testSearch",
|
"/demo/testSearch",
|
||||||
"/demo/tsetSearchText",
|
"/demo/testSearchText",
|
||||||
"/file/upload",
|
"/file/upload",
|
||||||
"/user/logout",
|
"/user/logout",
|
||||||
"/tag/list",
|
"/tag/list",
|
||||||
@@ -73,6 +73,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
"/character/listByUser",
|
"/character/listByUser",
|
||||||
"/user/detail",
|
"/user/detail",
|
||||||
"/user/register",
|
"/user/register",
|
||||||
|
"/user/updateInfo",
|
||||||
"/character/updateUserCharacterSort",
|
"/character/updateUserCharacterSort",
|
||||||
"/character/delUserCharacter",
|
"/character/delUserCharacter",
|
||||||
"/user/sendVerifyMail",
|
"/user/sendVerifyMail",
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ public class CharacterController {
|
|||||||
@GetMapping("/detail")
|
@GetMapping("/detail")
|
||||||
@Operation(summary = "人设详情", description = "人设详情接口")
|
@Operation(summary = "人设详情", description = "人设详情接口")
|
||||||
public BaseResponse<KeyboardCharacterRespVO> detail(@RequestParam("id") Long id) {
|
public BaseResponse<KeyboardCharacterRespVO> detail(@RequestParam("id") Long id) {
|
||||||
KeyboardCharacter character = characterService.getById(id);
|
return ResultUtils.success(characterService.getDetailById(id));
|
||||||
return ResultUtils.success(BeanUtil.copyProperties(character, KeyboardCharacterRespVO.class));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/listByTag")
|
@GetMapping("/listByTag")
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ public class FileController {
|
|||||||
|
|
||||||
@PostMapping("/upload")
|
@PostMapping("/upload")
|
||||||
@Operation(summary = "上传文件", description = "上传文件接口")
|
@Operation(summary = "上传文件", description = "上传文件接口")
|
||||||
@Parameter(name = "file",required = true,description = "上传的文件")
|
public BaseResponse<String> upload(@RequestPart("file") MultipartFile file){
|
||||||
public BaseResponse<String> upload(@RequestParam("file") MultipartFile file){
|
|
||||||
String fileUrl = fileService.upload(file);
|
String fileUrl = fileService.upload(file);
|
||||||
return ResultUtils.success(fileUrl);
|
return ResultUtils.success(fileUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.multipart.support.MissingServletRequestPartException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局异常处理器
|
* 全局异常处理器
|
||||||
@@ -25,6 +26,20 @@ public class GlobalExceptionHandler {
|
|||||||
this.i18nService = i18nService;
|
this.i18nService = i18nService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MissingServletRequestPartException.class)
|
||||||
|
public BaseResponse<?> missingServletRequestPartExceptionHandler(MissingServletRequestPartException e, HttpServletRequest request) {
|
||||||
|
log.error("missingServletRequestPartException: " + e.getMessage(), e);
|
||||||
|
|
||||||
|
String acceptLanguage = request.getHeader("Accept-Language");
|
||||||
|
String errorMessage = i18nService.getMessageWithAcceptLanguage(String.valueOf(ErrorCode.FILE_IS_EMPTY.getCode()), acceptLanguage);
|
||||||
|
|
||||||
|
if (errorMessage == null) {
|
||||||
|
errorMessage = ErrorCode.FILE_IS_EMPTY.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResultUtils.error(ErrorCode.FILE_IS_EMPTY.getCode(), errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public BaseResponse<?> businessExceptionHandler(BusinessException e, HttpServletRequest request) {
|
public BaseResponse<?> businessExceptionHandler(BusinessException e, HttpServletRequest request) {
|
||||||
log.error("businessException: " + e.getMessage(), e);
|
log.error("businessException: " + e.getMessage(), e);
|
||||||
|
|||||||
@@ -18,15 +18,27 @@ public class RequestBodyCacheFilter extends OncePerRequestFilter {
|
|||||||
FilterChain filterChain)
|
FilterChain filterChain)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
// 只缓存一次
|
// 获取请求的内容类型
|
||||||
if (!(request instanceof CachedBodyHttpServletRequest)) {
|
String contentType = request.getContentType();
|
||||||
|
|
||||||
|
// 跳过 multipart 请求,避免破坏文件上传功能
|
||||||
|
if (contentType != null && contentType.toLowerCase().startsWith("multipart/")) {
|
||||||
|
// 对于文件上传请求,直接放行,不进行请求体缓存
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经进行过请求体缓存,避免重复缓存
|
||||||
|
if (!(request instanceof CachedBodyHttpServletRequest)) {
|
||||||
|
// 创建缓存请求对象,包装原始请求以支持多次读取请求体
|
||||||
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request);
|
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request);
|
||||||
|
|
||||||
|
// 使用缓存的请求对象继续执行过滤器链
|
||||||
filterChain.doFilter(cachedRequest, response);
|
filterChain.doFilter(cachedRequest, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果已经是缓存过的请求,则直接执行过滤器链
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,6 @@ public interface KeyboardCharacterService extends IService<KeyboardCharacter>{
|
|||||||
List<KeyboardCharacterRespVO> selectListByTagWithNotLogin(Long tagId);
|
List<KeyboardCharacterRespVO> selectListByTagWithNotLogin(Long tagId);
|
||||||
|
|
||||||
void addDefaultUserCharacter(Long userId);
|
void addDefaultUserCharacter(Long userId);
|
||||||
|
|
||||||
|
KeyboardCharacterRespVO getDetailById(Long id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,4 +288,22 @@ public class KeyboardCharacterServiceImpl extends ServiceImpl<KeyboardCharacterM
|
|||||||
this.addUserCharacter(keyboardUserCharacterAddDTO, userId);
|
this.addUserCharacter(keyboardUserCharacterAddDTO, userId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyboardCharacterRespVO getDetailById(Long id) {
|
||||||
|
// 根据ID获取人设信息,优先从缓存获取
|
||||||
|
KeyboardCharacter character = this.getById(id);
|
||||||
|
// 将实体对象转换为响应对象
|
||||||
|
KeyboardCharacterRespVO respVO = BeanUtil.copyProperties(character, KeyboardCharacterRespVO.class);
|
||||||
|
// 判断当前用户是否已添加该人设
|
||||||
|
long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
// 查询用户人设关联表,判断当前用户是否已添加该人设
|
||||||
|
KeyboardUserCharacter userCharacter = keyboardUserCharacterMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<KeyboardUserCharacter>()
|
||||||
|
.eq(KeyboardUserCharacter::getCharacterId, id)
|
||||||
|
.eq(KeyboardUserCharacter::getUserId, userId));
|
||||||
|
// 设置是否已添加的标记
|
||||||
|
respVO.setAdded(userCharacter != null);
|
||||||
|
return respVO;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeyboardUser keyboardUser = BeanUtil.copyProperties(keyboardUserReq, KeyboardUser.class);
|
KeyboardUser keyboardUser = BeanUtil.copyProperties(keyboardUserReq, KeyboardUser.class);
|
||||||
Integer i = keyboardUserMapper.updateByuid(keyboardUser);
|
keyboardUser.setId(loginIdAsLong);
|
||||||
|
int i = keyboardUserMapper.updateById(keyboardUser);
|
||||||
if (i <=0 ) {
|
if (i <=0 ) {
|
||||||
throw new BusinessException(ErrorCode.USER_INFO_UPDATE_FAILED);
|
throw new BusinessException(ErrorCode.USER_INFO_UPDATE_FAILED);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user