25 KiB
25 KiB
项目结构示例
1. 目录结构
src/main/java/com/emotionmuseum/
├── EmotionMuseumApplication.java # 启动类
├── config/ # 配置类
│ ├── AsyncConfig.java # 异步配置
│ ├── MybatisPlusConfig.java # MyBatis-Plus配置
│ ├── RedisConfig.java # Redis配置
│ ├── WebSocketConfig.java # WebSocket配置
│ ├── SecurityConfig.java # Spring Security配置
│ ├── JwtConfig.java # JWT配置
│ └── SwaggerConfig.java # API文档配置
├── controller/ # 控制器层
│ ├── UserController.java # 用户控制器
│ ├── ChatController.java # 聊天控制器
│ └── AuthController.java # 认证控制器
├── service/ # 服务层
│ ├── UserService.java # 用户服务接口
│ ├── ChatService.java # 聊天服务接口
│ ├── AuthService.java # 认证服务接口
│ └── impl/ # 服务实现
│ ├── UserServiceImpl.java
│ ├── ChatServiceImpl.java
│ └── AuthServiceImpl.java
├── mapper/ # 数据访问层
│ ├── UserMapper.java
│ ├── ChatMapper.java
│ └── MessageMapper.java
├── entity/ # 实体类
│ ├── User.java
│ ├── Chat.java
│ ├── Message.java
│ └── BaseEntity.java
├── dto/ # 数据传输对象
│ ├── request/ # 请求对象
│ │ ├── CreateUserRequest.java
│ │ ├── LoginRequest.java
│ │ ├── ChatRequest.java
│ │ └── PageRequest.java
│ └── response/ # 响应对象
│ ├── UserResponse.java
│ ├── LoginResponse.java
│ ├── ChatResponse.java
│ └── PageResult.java
├── common/ # 公共组件
│ ├── base/ # 基础类
│ │ ├── BaseEntity.java
│ │ └── BaseService.java
│ ├── exception/ # 异常处理
│ │ ├── GlobalExceptionHandler.java
│ │ ├── BusinessException.java
│ │ └── UserNotFoundException.java
│ ├── result/ # 统一返回结果
│ │ ├── Result.java
│ │ └── ResultCode.java
│ ├── security/ # 安全相关
│ │ ├── JwtAuthenticationFilter.java # JWT认证过滤器
│ │ ├── JwtTokenProvider.java # JWT令牌提供者
│ │ ├── UserDetailsServiceImpl.java # 用户详情服务实现
│ │ ├── AuthenticationEntryPointImpl.java # 认证失败处理
│ │ └── AccessDeniedHandlerImpl.java # 访问拒绝处理
│ └── util/ # 工具类
│ ├── JwtUtil.java
│ ├── RedisUtil.java
│ └── DateUtil.java
├── websocket/ # WebSocket相关
│ ├── WebSocketHandler.java
│ ├── ChatWebSocketHandler.java
│ └── dto/
│ ├── ChatMessage.java
│ └── WebSocketMessage.java
└── ai/ # AI相关
├── CozeClient.java # Coze客户端
├── ChatClient.java # 聊天客户端
└── config/
└── CozeConfig.java # Coze配置
3. 核心代码示例
3.1 启动类
@SpringBootApplication
@EnableAsync
@EnableCaching
@EnableGlobalMethodSecurity(prePostEnabled = true)
@MapperScan("com.emotionmuseum.mapper")
public class EmotionMuseumApplication {
public static void main(String[] args) {
SpringApplication.run(EmotionMuseumApplication.class, args);
}
}
## 2. 核心代码示例
### 2.1 启动类
```java
@SpringBootApplication
@EnableAsync
@EnableCaching
@EnableGlobalMethodSecurity(prePostEnabled = true)
@MapperScan("com.emotionmuseum.mapper")
public class EmotionMuseumApplication {
public static void main(String[] args) {
SpringApplication.run(EmotionMuseumApplication.class, args);
}
}
2.2 BaseEntity
@Data
@MappedSuperclass
public abstract class BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableLogic
private Integer deleted;
}
2.3 统一返回结果
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(ResultCode.SUCCESS.getMessage());
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(ResultCode.ERROR.getCode());
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}
2.4 全局异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.error(e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.warn("参数验证失败: {}", message);
return Result.error(message);
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error("系统异常,请稍后重试");
}
}
2.5 异步配置
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
2.6 Coze客户端
@Component
@Slf4j
public class CozeClient {
@Autowired
private RestTemplate restTemplate;
@Value("${coze.api.url}")
private String cozeApiUrl;
@Value("${coze.api.key}")
private String cozeApiKey;
public String chat(String message, String sessionId) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + cozeApiKey);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("message", message);
requestBody.put("session_id", sessionId);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
cozeApiUrl + "/chat", request, Map.class);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return (String) response.getBody().get("response");
}
throw new BusinessException("调用Coze API失败");
} catch (Exception e) {
log.error("调用Coze API异常", e);
throw new BusinessException("AI服务暂时不可用");
}
}
}
2.7 WebSocket配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatWebSocketHandler(), "/ws/chat")
.setAllowedOrigins("*");
}
}
2.8 Spring Security配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthFilter;
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/ws/**").permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.9 JWT认证过滤器
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
String username = jwtTokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
log.error("无法设置用户认证: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
2.10 聊天控制器
@RestController
@RequestMapping("/api/chat")
@Validated
@Slf4j
public class ChatController {
@Autowired
private ChatService chatService;
@PreAuthorize("hasRole('USER')")
@PostMapping("/send")
public Result<ChatResponse> sendMessage(@Valid @RequestBody ChatRequest request) {
// Controller层只负责:参数校验、调用Service、返回结果
ChatResponse response = chatService.sendMessage(request);
return Result.success(response);
}
@PreAuthorize("hasRole('USER')")
@GetMapping("/history")
public Result<PageResult<ChatResponse>> getChatHistory(
@Valid PageRequest pageRequest,
@RequestParam(required = false) String sessionId) {
// Controller层只负责:参数校验、调用Service、返回结果
PageResult<ChatResponse> result = chatService.getChatHistory(pageRequest, sessionId);
return Result.success(result);
}
}
2.11 聊天服务实现
@Service
@Transactional
@Slf4j
public class ChatServiceImpl implements ChatService {
@Autowired
private ChatMapper chatMapper;
@Autowired
private CozeClient cozeClient;
@Override
public ChatResponse sendMessage(ChatRequest request) {
// 1. 业务参数校验
validateChatRequest(request);
// 2. 业务逻辑:调用AI服务
String aiResponse = cozeClient.chat(request.getMessage(), request.getSessionId());
// 3. 业务逻辑:保存聊天记录
Chat chat = new Chat();
chat.setUserId(getCurrentUserId());
chat.setMessage(request.getMessage());
chat.setResponse(aiResponse);
chat.setSessionId(request.getSessionId());
chat.setCreateTime(LocalDateTime.now());
chatMapper.insert(chat);
// 4. 返回结果转换
return convertToChatResponse(chat);
}
@Override
public PageResult<ChatResponse> getChatHistory(PageRequest pageRequest, String sessionId) {
// 1. 业务逻辑:查询聊天历史
Page<Chat> page = new Page<>(pageRequest.getPageNum(), pageRequest.getPageSize());
LambdaQueryWrapper<Chat> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Chat::getUserId, getCurrentUserId());
if (StringUtils.hasText(sessionId)) {
wrapper.eq(Chat::getSessionId, sessionId);
}
wrapper.orderByDesc(Chat::getCreateTime);
Page<Chat> chatPage = chatMapper.selectPage(page, wrapper);
// 2. 数据转换
List<ChatResponse> responses = chatPage.getRecords().stream()
.map(this::convertToChatResponse)
.collect(Collectors.toList());
// 3. 返回分页结果
return new PageResult<>(responses, chatPage.getTotal(), pageRequest.getPageNum(), pageRequest.getPageSize());
}
// 私有方法:业务校验
private void validateChatRequest(ChatRequest request) {
if (request == null) {
throw new IllegalArgumentException("请求参数不能为空");
}
if (!StringUtils.hasText(request.getMessage())) {
throw new IllegalArgumentException("消息内容不能为空");
}
if (request.getMessage().length() > 1000) {
throw new IllegalArgumentException("消息内容长度不能超过1000字符");
}
}
// 私有方法:获取当前用户ID
private Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 根据用户名获取用户ID的逻辑
return getUserService().getUserIdByUsername(userDetails.getUsername());
}
throw new UnauthorizedException("用户未登录");
}
// 私有方法:数据转换
private ChatResponse convertToChatResponse(Chat chat) {
ChatResponse response = new ChatResponse();
response.setId(chat.getId());
response.setMessage(chat.getMessage());
response.setResponse(chat.getResponse());
response.setSessionId(chat.getSessionId());
response.setCreateTime(chat.getCreateTime());
return response;
}
}
3. 配置文件示例
3.1 application.yml
spring:
profiles:
active: dev
application:
name: emotion-museum
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: password
hikari:
maximum-pool-size: 20
minimum-idle: 5
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# JWT配置
jwt:
secret: your-secret-key-here-must-be-very-long-and-secure
expiration: 86400000 # 24小时
coze:
api:
url: https://api.coze.com
key: your-coze-api-key
# Log4j2配置
logging:
config: classpath:log4j2-spring.xml
3.2 application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/emotion_museum_dev?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
redis:
host: localhost
port: 6379
# 开发环境JWT配置
jwt:
secret: dev-secret-key-not-for-production
expiration: 86400000
3.3 log4j2-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Properties>
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
<Property name="LOG_FILE_PATH">logs</Property>
</Properties>
<Appenders>
<!-- 控制台输出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
<!-- 文件输出 -->
<RollingFile name="FileAppender" fileName="${LOG_FILE_PATH}/app.log"
filePattern="${LOG_FILE_PATH}/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="100MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
<!-- 错误日志文件 -->
<RollingFile name="ErrorFileAppender" fileName="${LOG_FILE_PATH}/error.log"
filePattern="${LOG_FILE_PATH}/error-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="100MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
<!-- 安全日志文件 -->
<RollingFile name="SecurityFileAppender" fileName="${LOG_FILE_PATH}/security.log"
filePattern="${LOG_FILE_PATH}/security-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="100MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
</Appenders>
<Loggers>
<!-- 应用日志 -->
<Logger name="com.emotionmuseum" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Logger>
<!-- 安全日志 -->
<Logger name="com.emotionmuseum.common.security" level="info" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="SecurityFileAppender"/>
</Logger>
<!-- 错误日志 -->
<Logger name="com.emotionmuseum" level="error" additivity="false">
<AppenderRef ref="ErrorFileAppender"/>
</Logger>
<!-- Spring Security日志 -->
<Logger name="org.springframework.security" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="SecurityFileAppender"/>
</Logger>
<!-- 根日志器 -->
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Root>
</Loggers>
</Configuration>
4. Maven依赖
4.1 pom.xml核心依赖
<properties>
<java.version>21</java.version>
<spring-boot.version>3.2.0</spring-boot.version>
<spring-ai.version>0.8.0</spring-ai.version>
<mybatis-plus.version>3.5.4</mybatis-plus.version>
<mysql.version>8.0.33</mysql.version>
<redis.version>3.2.0</redis.version>
<spring-security.version>6.1.0</spring-security.version>
<log4j2.version>2.20.0</log4j2.version>
<jwt.version>0.11.5</jwt.version>
</properties>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
这个项目结构示例提供了一个完整的SpringBoot单体后端服务的基础框架,包含了您要求的所有技术组件:Spring AI、WebSocket、Redis、MySQL、MyBatis-Plus、Spring Security、JWT认证、Log4j2日志、异步处理、全局异常处理、统一返回结果等。该框架具有以下特点:
- 安全性: 集成Spring Security和JWT,提供完整的认证授权机制
- 可观测性: 使用Log4j2提供结构化日志记录和日志分级管理
- 高性能: 支持异步处理、缓存和数据库优化
- 可扩展性: 模块化设计,易于扩展和维护
- 标准化: 统一的代码规范和异常处理机制
- 分层清晰: 严格遵循分层架构,Controller层只负责请求处理,所有业务逻辑都在Service层实现