Procházet zdrojové kódy

feat(backend): 启用 Spring Cache + RedisCacheManager,缓存系统配置与营养分类/品牌列表

Xiaogang Liao před 6 dny
rodič
revize
433c28c92b

+ 6 - 0
cfc-backend/pom.xml

@@ -96,6 +96,12 @@
             <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
 
+        <!-- Spring Cache (Redis 缓存抽象) -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+        </dependency>
+
         <!-- Spring Security Crypto (BCrypt) -->
         <dependency>
             <groupId>org.springframework.security</groupId>

+ 43 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/RedisConfig.java

@@ -1,12 +1,25 @@
 package com.etotem.cfc.config;
 
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+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.RedisSerializationContext;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
+import java.time.Duration;
+
 @Configuration
+@EnableCaching
 public class RedisConfig {
 
     @Bean
@@ -21,4 +34,33 @@ public class RedisConfig {
         template.afterPropertiesSet();
         return template;
     }
-}
+
+    /**
+     * Spring Cache 的 Redis 缓存管理器。
+     * - value 使用 GenericJackson2JsonRedisSerializer 序列化,支持缓存复杂对象
+     * - 默认 TTL 10 分钟(缓存名带 "#{ttl}" 后缀可自定义,如 "products#3600")
+     * - 禁用空值缓存(防止缓存穿透)
+     */
+    @Bean
+    public CacheManager cacheManager(RedisConnectionFactory factory) {
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
+                ObjectMapper.DefaultTyping.NON_FINAL);
+        GenericJackson2JsonRedisSerializer jsonSerializer =
+                new GenericJackson2JsonRedisSerializer(mapper);
+
+        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
+                .serializeKeysWith(RedisSerializationContext.SerializationPair
+                        .fromSerializer(new StringRedisSerializer()))
+                .serializeValuesWith(RedisSerializationContext.SerializationPair
+                        .fromSerializer(jsonSerializer))
+                .entryTtl(Duration.ofMinutes(10))
+                .disableCachingNullValues();
+
+        return RedisCacheManager.builder(factory)
+                .cacheDefaults(config)
+                .transactionAware()
+                .build();
+    }
+}

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/nutrition/NutritionProductController.java

@@ -10,6 +10,7 @@ import com.etotem.cfc.service.NutritionProductService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -34,12 +35,14 @@ public class NutritionProductController {
 
     @Operation(summary = "分类列表")
     @PostMapping("/category/list")
+    @Cacheable(value = "nutritionCategory", key = "\"all\"")
     public Result<List<NutritionCategory>> listCategories() {
         return Result.success(nutritionProductService.listCategories());
     }
 
     @Operation(summary = "品牌列表(含产品数)")
     @PostMapping("/brand/list")
+    @Cacheable(value = "nutritionBrand", key = "\"all\"")
     public Result<List<Map<String, Object>>> listBrands() {
         return Result.success(nutritionProductService.listBrands());
     }

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SysConfigService.java

@@ -5,6 +5,8 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.SysConfigDTO;
 import com.etotem.cfc.entity.SysConfig;
 import com.etotem.cfc.mapper.SysConfigMapper;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -23,6 +25,7 @@ public class SysConfigService {
     /**
      * 获取所有配置(key-value 形式)
      */
+    @Cacheable(value = "sysConfig", key = "\"allMap\"")
     public Result<Map<String, String>> getAllAsMap() {
         List<SysConfig> list = configMapper.selectList(null);
         Map<String, String> map = new HashMap<>();
@@ -63,6 +66,7 @@ public class SysConfigService {
     /**
      * 更新或创建配置(upsert),支持描述
      */
+    @CacheEvict(value = "sysConfig", allEntries = true)
     public Result<String> update(String key, String value, String description) {
         SysConfig config = configMapper.selectOne(
             new LambdaQueryWrapper<SysConfig>().eq(SysConfig::getConfigKey, key));
@@ -87,6 +91,7 @@ public class SysConfigService {
     /**
      * 内部方法:获取配置值(供其他 Service 调用)
      */
+    @Cacheable(value = "sysConfig", key = "#key")
     public String getValue(String key) {
         SysConfig config = configMapper.selectOne(
             new LambdaQueryWrapper<SysConfig>().eq(SysConfig::getConfigKey, key));