refactor(redis): 重构Redis配置以兼容fastjson格式

- 移除Jackson多态类型验证器配置
- 使用FastjsonCompatibleRedisSerializer替代GenericJacksonJsonRedisSerializer
- 添加Primary注解优化Bean注入
- 移除不必要的ValueOperations Bean定义
- 更新限流脚本中的变量名提高可读性
- 在TokenService中添加对多种缓存格式的兼容性支持
- 创建FastjsonCompatibleRedisSerializer类处理不同数据格式的反序列化
- 添加数据库迁移脚本为相关表增加基础字段和删除标识
This commit is contained in:
2026-06-11 14:49:19 +08:00
parent 9675106d4b
commit 773a485114
6 changed files with 125 additions and 46 deletions

View File

@@ -0,0 +1,79 @@
package com.core.framework.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.StandardCharsets;
/**
* Jackson Redis序列化器 - 兼容fastjson旧格式
*
* 新数据: 纯JSON (无类型包装),调用方用 convertValue 转换
* 旧fastjson: 去除L后缀后按JSON解析
* 旧Jackson activateDefaultTyping: 解包 ["className",{data}] 后取data部分
*/
public class FastjsonCompatibleRedisSerializer implements RedisSerializer<Object> {
private static final Logger log = LoggerFactory.getLogger(FastjsonCompatibleRedisSerializer.class);
private final ObjectMapper objectMapper;
/** 全局ObjectMapper供外部调用方做 convertValue 转换 */
private static final ObjectMapper sharedMapper = createMapper();
public FastjsonCompatibleRedisSerializer() { log.info("[INIT] FastjsonCompatibleRedisSerializer loaded - plain JSON mode");
this.objectMapper = createMapper();
}
private static ObjectMapper createMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
/** 获取共享ObjectMapper供 DictUtils / TokenService 等做 convertValue */
public static ObjectMapper getSharedMapper() {
return sharedMapper;
}
@Override
public byte[] serialize(Object object) throws SerializationException {
if (object == null) {
return new byte[0];
}
try {
return objectMapper.writeValueAsBytes(object);
} catch (Exception e) {
throw new SerializationException("Redis序列化失败: " + e.getMessage(), e);
}
}
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
String json = new String(bytes, StandardCharsets.UTF_8);
// 移除fastjson特有的Long L后缀: 123L -> 123
String cleaned = json.replaceAll("(\\d+)L", "$1");
try {
// 处理旧Jackson activateDefaultTyping格式: ["className", {data}]
if (cleaned.startsWith("[\"") && cleaned.length() > 10) {
com.fasterxml.jackson.databind.JsonNode node = objectMapper.readTree(cleaned);
if (node.isArray() && node.size() >= 2 && node.get(0).isTextual()) {
// 取data部分第2个元素忽略className
return objectMapper.treeToValue(node.get(1), Object.class);
}
}
return objectMapper.readValue(cleaned, Object.class);
} catch (Exception e) {
log.warn("Redis数据反序列化失败(已忽略): {}", e.getMessage());
return null;
}
}
}

View File

@@ -4,51 +4,24 @@ import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.DatabindContext;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.jsontype.PolymorphicTypeValidator;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
private static final PolymorphicTypeValidator ALLOW_ALL = new PolymorphicTypeValidator() {
@Override
public Validity validateBaseType(DatabindContext ctxt, JavaType baseType) {
return Validity.ALLOWED;
}
@Override
public Validity validateSubClassName(DatabindContext ctxt, JavaType baseType, String subClassName) {
return Validity.ALLOWED;
}
@Override
public Validity validateSubType(DatabindContext ctxt, JavaType baseType, JavaType subType) {
return Validity.ALLOWED;
}
};
@Bean
@Primary
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
tools.jackson.databind.ObjectMapper objectMapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.activateDefaultTyping(ALLOW_ALL, tools.jackson.databind.DefaultTyping.NON_FINAL)
.build();
GenericJacksonJsonRedisSerializer serializer = new GenericJacksonJsonRedisSerializer(objectMapper);
FastjsonCompatibleRedisSerializer serializer = new FastjsonCompatibleRedisSerializer();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
@@ -68,16 +41,18 @@ public class RedisConfig extends CachingConfigurerSupport {
return redisScript;
}
@Bean
public ValueOperations<Object, Object> valueOperations(RedisTemplate<Object, Object> redisTemplate) {
return redisTemplate.opsForValue();
}
private String limitScriptText() {
return "local key = KEYS[1]\n" + "local count = tonumber(ARGV[1])\n" + "local time = tonumber(ARGV[2])\n"
+ "local current = redis.call('get', key);\n" + "if current and tonumber(current) > count then\n"
+ " return tonumber(current);\n" + "end\n" + "current = redis.call('incr', key)\n"
+ "if tonumber(current) == 1 then\n" + " redis.call('expire', key, time)\n" + "end\n"
+ "return tonumber(current);";
return "local key = KEYS[1]\n" +
"local count = tonumber(ARGV[1])\n" +
"local ttl = tonumber(ARGV[2])\n" +
"local current = redis.call('get', KEYS[1]);\n" +
"if current and tonumber(current) > count then\n" +
" return tonumber(current);\n" +
"end\n" +
"current = redis.call('incr', KEYS[1]);\n" +
"if tonumber(current) == 1 then\n" +
" redis.call('expire', KEYS[1], ttl);\n" +
"end\n" +
"return tonumber(current);\n";
}
}

View File

@@ -63,8 +63,28 @@ public class TokenService {
// 解析对应的权限以及用户信息
String uuid = (String)claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
Object cached = redisCache.getCacheObject(userKey);
if (cached instanceof LoginUser) {
return (LoginUser) cached;
}
// 兼容旧Jackson activateDefaultTyping格式: ["className",{data}]
if (cached instanceof java.util.List<?> list && list.size() >= 2 && list.get(0) instanceof String) {
Object data = list.get(1);
if (data instanceof java.util.Map) {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper.convertValue(data, LoginUser.class);
}
}
// 兼容纯JSON格式: LinkedHashMap -> LoginUser
if (cached instanceof java.util.Map) {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper.convertValue(cached, LoginUser.class);
}
return null;
} catch (Exception e) {
log.error("获取用户信息异常'{}'", e.getMessage());
}

View File

@@ -1,4 +0,0 @@
-- 为 sys_audit_log 添加 del_flag 列以匹配 HisBaseEntity 逻辑删除
ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS del_flag CHAR(1) DEFAULT '0';
COMMENT ON COLUMN sys_audit_log.del_flag IS '删除标识(0=正常,1=删除)';
UPDATE sys_audit_log SET del_flag = '0' WHERE del_flag IS NULL;

View File

@@ -0,0 +1,5 @@
-- 为 clinical_pathway_execution 添加 HisBaseEntity 所需的基础字段
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS create_by VARCHAR(64);
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_by VARCHAR(64);
ALTER TABLE clinical_pathway_execution ADD COLUMN IF NOT EXISTS update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP;

View File

@@ -0,0 +1,4 @@
-- 为 prescription_intercept_log 添加 delete_flag 列以匹配 HisBaseEntity 默认映射
ALTER TABLE prescription_intercept_log ADD COLUMN IF NOT EXISTS delete_flag CHAR(1) DEFAULT '0';
COMMENT ON COLUMN prescription_intercept_log.delete_flag IS '删除标识(0=正常,1=删除)';
UPDATE prescription_intercept_log SET delete_flag = '0' WHERE delete_flag IS NULL;