# 项目结构示例

## 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 启动类
```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.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
```java
@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 统一返回结果
```java
@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 全局异常处理
```java
@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 异步配置
```java
@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客户端
```java
@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配置
```java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new ChatWebSocketHandler(), "/ws/chat")
                .setAllowedOrigins("*");
    }
}
```

### 2.8 Spring Security配置
```java
@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认证过滤器
```java
@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 聊天控制器
```java
@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 聊天服务实现
```java
@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
```yaml
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
```yaml
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
<?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核心依赖
```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日志、异步处理、全局异常处理、统一返回结果等。该框架具有以下特点：

1. **安全性**: 集成Spring Security和JWT，提供完整的认证授权机制
2. **可观测性**: 使用Log4j2提供结构化日志记录和日志分级管理
3. **高性能**: 支持异步处理、缓存和数据库优化
4. **可扩展性**: 模块化设计，易于扩展和维护
5. **标准化**: 统一的代码规范和异常处理机制
6. **分层清晰**: 严格遵循分层架构，Controller层只负责请求处理，所有业务逻辑都在Service层实现