feat: 增强情绪博物馆项目功能 - 新增用户评论和帖子功能,优化前端架构和WebSocket通信 - 更新文档和部署配置

This commit is contained in:
2025-07-29 07:38:47 +08:00
parent cc886cd4d5
commit 2f3d39fb00
142 changed files with 45645 additions and 0 deletions
@@ -0,0 +1,63 @@
package com.emotionmuseum.config;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 异步配置类
*
* @author emotion-museum
* @version 1.0.0
* @since 2024-01-01
*/
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
/**
* 默认异步执行器
*/
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(500);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("emotion-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
/**
* AI任务执行器
*/
@Bean("aiTaskExecutor")
public Executor aiTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("ai-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
/**
* 异步异常处理器
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler();
}
}
@@ -0,0 +1,64 @@
package com.emotionmuseum.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import jakarta.annotation.PostConstruct;
/**
* Coze配置类
*
* @author emotion-museum
* @version 1.0.0
* @since 2024-01-01
*/
@Configuration
@ConfigurationProperties(prefix = "emotion.coze")
@Data
@Slf4j
public class CozeConfig {
/**
* API密钥
*/
private String apiKey;
/**
* 机器人ID
*/
private String botId;
/**
* 基础URL
*/
private String baseUrl = "https://www.coze.cn/api";
/**
* 超时时间(毫秒)
*/
private int timeout = 30000;
/**
* 最大重试次数
*/
private int maxRetries = 3;
/**
* 验证配置
*/
@PostConstruct
public void validateConfig() {
if (!StringUtils.hasText(apiKey)) {
log.warn("Coze API Key未配置,AI功能可能无法正常使用");
}
if (!StringUtils.hasText(botId)) {
log.warn("Coze Bot ID未配置,AI功能可能无法正常使用");
}
if (StringUtils.hasText(apiKey) && StringUtils.hasText(botId)) {
log.info("Coze配置验证通过,Bot ID: {}", botId);
}
}
}
@@ -0,0 +1,55 @@
package com.emotionmuseum.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus配置类
*
* @author emotion-museum
* @version 1.0.0
* @since 2024-01-01
*/
@Configuration
public class MybatisPlusConfig {
/**
* MyBatis-Plus拦截器配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
/**
* 全局配置
*/
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
// 设置数据库类型
globalConfig.setDbConfig(new GlobalConfig.DbConfig());
globalConfig.getDbConfig().setIdType(com.baomidou.mybatisplus.annotation.IdType.ASSIGN_ID);
// 设置逻辑删除
globalConfig.getDbConfig().setLogicDeleteField("deleted");
globalConfig.getDbConfig().setLogicDeleteValue("1");
globalConfig.getDbConfig().setLogicNotDeleteValue("0");
return globalConfig;
}
}
@@ -0,0 +1,43 @@
package com.emotionmuseum.config;
import io.swagger.v3.oas.models.Components;
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.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* OpenAPI配置类
*
* @author emotion-museum
* @version 1.0.0
* @since 2024-01-01
*/
@Configuration
public class OpenApiConfig {
/**
* OpenAPI配置
*/
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("情绪博物馆API文档")
.version("1.0.0")
.description("情绪博物馆后端服务API文档")
.contact(new Contact()
.name("情绪博物馆团队")
.email("support@emotion-museum.com")))
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
.components(new Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
@@ -0,0 +1,76 @@
package com.emotionmuseum.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* Redis配置类
*
* @author emotion-museum
* @version 1.0.0
* @since 2024-01-01
*/
@Configuration
@EnableCaching
public class RedisConfig {
/**
* Redis模板配置
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
mapper.registerModule(new JavaTimeModule());
serializer.setObjectMapper(mapper);
// 设置序列化器
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
/**
* 缓存管理器配置
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
@@ -0,0 +1,91 @@
package com.emotionmuseum.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
/**
* Spring Security配置类
*
* @author emotion-museum
* @version 1.0.0
* @since 2024-01-01
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
/**
* 安全过滤器链配置
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF
.csrf(AbstractHttpConfigurer::disable)
// 配置CORS
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 会话管理
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 授权配置
.authorizeHttpRequests(authz -> authz
// 允许访问的路径
.requestMatchers("/health/**").permitAll()
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/ws/**").permitAll()
.requestMatchers("/ai/guest/**").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.requestMatchers("/swagger-ui.html").permitAll()
.requestMatchers("/favicon.ico").permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
);
return http.build();
}
/**
* CORS配置
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 允许的源
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
// 允许的方法
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
// 允许的头部
configuration.setAllowedHeaders(Arrays.asList("*"));
// 允许携带凭证
configuration.setAllowCredentials(true);
// 预检请求的有效期
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
@@ -0,0 +1,72 @@
package com.emotionmuseum.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.io.IOException;
/**
* HTTP客户端配置类
*
* @author emotion-museum
* @version 1.0.0
* @since 2024-01-01
*/
@Configuration
@Slf4j
public class WebClientConfig {
/**
* WebClient构建器
*/
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
.filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
log.debug("Request: {} {}", clientRequest.method(), clientRequest.url());
return Mono.just(clientRequest);
}))
.filter(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
log.debug("Response: {}", clientResponse.statusCode());
return Mono.just(clientResponse);
}));
}
/**
* RestTemplate配置
*/
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// 设置超时时间
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(30000);
factory.setReadTimeout(30000);
restTemplate.setRequestFactory(factory);
// 添加请求拦截器
restTemplate.getInterceptors().add(new ClientHttpRequestInterceptor() {
@Override
public org.springframework.http.client.ClientHttpResponse intercept(
org.springframework.http.HttpRequest request,
byte[] body,
org.springframework.http.client.ClientHttpRequestExecution execution) throws IOException {
log.debug("Request: {} {}", request.getMethod(), request.getURI());
org.springframework.http.client.ClientHttpResponse response = execution.execute(request, body);
log.debug("Response: {}", response.getStatusCode());
return response;
}
});
return restTemplate;
}
}
@@ -0,0 +1,43 @@
package com.emotionmuseum.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* WebSocket配置类
*
* @author emotion-museum
* @version 1.0.0
* @since 2024-01-01
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册STOMP端点
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
// 支持原生WebSocket
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 启用简单的消息代理
registry.enableSimpleBroker("/topic", "/queue");
// 设置应用程序目标前缀
registry.setApplicationDestinationPrefixes("/app");
// 设置用户目标前缀
registry.setUserDestinationPrefix("/user");
}
}