feat(phase1): 完成 Repository 测试和 Redis 会话管理
Task 2: 完成 Repository 层测试 - 实现 CaseLibraryRepositoryTest (6 个测试) - 实现 ApiDocumentRepositoryTest (7 个测试) - 所有 Repository 测试通过 (19/19) Task 3: 完成 Redis 会话管理 - 创建 SessionContext 和 ToolCall 数据类 - 创建 SessionManager 接口 - 实现 RedisSessionManager (基于 Redis 的会话管理) - 创建 SessionConfiguration (Redis 序列化配置) - 实现 RedisSessionManagerTest (8 个测试全部通过) 测试结果: - ApiDocumentRepositoryTest: 7/7 通过 - CaseLibraryRepositoryTest: 6/6 通过 - DiagnosisRecordRepositoryTest: 6/6 通过 - RedisSessionManagerTest: 8/8 通过 - 总计: 27/27 测试通过 功能特性: - 会话创建、查询、更新、删除 - 会话过期时间管理 - 工具调用记录追踪 - 会话状态管理 - 基于 Redis 的分布式会话存储 Progress: 20/33 tasks completed (61%)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package org.example.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 会话配置
|
||||
*/
|
||||
@Configuration
|
||||
public class SessionConfiguration {
|
||||
|
||||
/**
|
||||
* 配置 RedisTemplate
|
||||
* 使用 JSON 序列化存储会话对象
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 配置 ObjectMapper 支持 Java 8 时间类型
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
|
||||
// 启用类型信息,支持多态反序列化
|
||||
objectMapper.activateDefaultTyping(
|
||||
LaissezFaireSubTypeValidator.instance,
|
||||
ObjectMapper.DefaultTyping.NON_FINAL,
|
||||
JsonTypeInfo.As.PROPERTY
|
||||
);
|
||||
|
||||
// 使用 JSON 序列化器
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer =
|
||||
new GenericJackson2JsonRedisSerializer(objectMapper);
|
||||
|
||||
// Key 使用 String 序列化
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
// Value 使用 JSON 序列化
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.example.domain.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会话上下文数据类
|
||||
* 存储在 Redis 中的会话数据
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SessionContext implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 业务ID(订单号/请求ID等)
|
||||
*/
|
||||
private String businessId;
|
||||
|
||||
/**
|
||||
* 链路追踪ID
|
||||
*/
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* 会话状态(ACTIVE/COMPLETED/EXPIRED)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 工具调用历史
|
||||
*/
|
||||
@Builder.Default
|
||||
private List<ToolCall> toolCalls = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 会话创建时间
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
private LocalDateTime lastActiveAt;
|
||||
|
||||
/**
|
||||
* 会话过期时间(秒)
|
||||
*/
|
||||
private Long ttl;
|
||||
|
||||
/**
|
||||
* 添加工具调用记录
|
||||
*/
|
||||
public void addToolCall(ToolCall toolCall) {
|
||||
if (this.toolCalls == null) {
|
||||
this.toolCalls = new ArrayList<>();
|
||||
}
|
||||
this.toolCalls.add(toolCall);
|
||||
this.lastActiveAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新最后活跃时间
|
||||
*/
|
||||
public void updateLastActiveTime() {
|
||||
this.lastActiveAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.example.domain.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工具调用记录数据类
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ToolCall implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 工具名称
|
||||
*/
|
||||
private String toolName;
|
||||
|
||||
/**
|
||||
* 工具参数
|
||||
*/
|
||||
private Map<String, Object> arguments;
|
||||
|
||||
/**
|
||||
* 工具返回结果
|
||||
*/
|
||||
private String result;
|
||||
|
||||
/**
|
||||
* 执行状态(SUCCESS/FAILED/TIMEOUT)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 错误信息(如果失败)
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 执行耗时(毫秒)
|
||||
*/
|
||||
private Long duration;
|
||||
|
||||
/**
|
||||
* 调用时间
|
||||
*/
|
||||
private LocalDateTime calledAt;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.example.service.session;
|
||||
|
||||
import org.example.domain.model.SessionContext;
|
||||
import org.example.domain.model.ToolCall;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 会话管理器接口
|
||||
* 负责会话的创建、读取、更新和删除
|
||||
*/
|
||||
public interface SessionManager {
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*
|
||||
* @param sessionContext 会话上下文
|
||||
* @param ttlSeconds 会话过期时间(秒)
|
||||
* @return 会话ID
|
||||
*/
|
||||
String createSession(SessionContext sessionContext, long ttlSeconds);
|
||||
|
||||
/**
|
||||
* 获取会话
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 会话上下文(如果存在)
|
||||
*/
|
||||
Optional<SessionContext> getSession(String sessionId);
|
||||
|
||||
/**
|
||||
* 更新会话
|
||||
*
|
||||
* @param sessionContext 会话上下文
|
||||
*/
|
||||
void updateSession(SessionContext sessionContext);
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
*/
|
||||
void deleteSession(String sessionId);
|
||||
|
||||
/**
|
||||
* 检查会话是否存在
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return true 如果会话存在
|
||||
*/
|
||||
boolean exists(String sessionId);
|
||||
|
||||
/**
|
||||
* 刷新会话过期时间
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param ttlSeconds 新的过期时间(秒)
|
||||
* @return true 如果刷新成功
|
||||
*/
|
||||
boolean refreshSession(String sessionId, long ttlSeconds);
|
||||
|
||||
/**
|
||||
* 添加工具调用记录到会话
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param toolCall 工具调用记录
|
||||
*/
|
||||
void addToolCall(String sessionId, ToolCall toolCall);
|
||||
|
||||
/**
|
||||
* 更新会话状态
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param status 新状态
|
||||
*/
|
||||
void updateStatus(String sessionId, String status);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package org.example.service.session.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.domain.model.SessionContext;
|
||||
import org.example.domain.model.ToolCall;
|
||||
import org.example.service.session.SessionManager;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis 会话管理器实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RedisSessionManager implements SessionManager {
|
||||
|
||||
private static final String SESSION_KEY_PREFIX = "session:";
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public String createSession(SessionContext sessionContext, long ttlSeconds) {
|
||||
String sessionId = sessionContext.getSessionId();
|
||||
if (sessionId == null || sessionId.isEmpty()) {
|
||||
throw new IllegalArgumentException("Session ID cannot be null or empty");
|
||||
}
|
||||
|
||||
sessionContext.setCreatedAt(LocalDateTime.now());
|
||||
sessionContext.setLastActiveAt(LocalDateTime.now());
|
||||
sessionContext.setTtl(ttlSeconds);
|
||||
sessionContext.setStatus("ACTIVE");
|
||||
|
||||
String key = buildKey(sessionId);
|
||||
redisTemplate.opsForValue().set(key, sessionContext, ttlSeconds, TimeUnit.SECONDS);
|
||||
|
||||
log.info("创建会话成功: sessionId={}, ttl={}秒", sessionId, ttlSeconds);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<SessionContext> getSession(String sessionId) {
|
||||
String key = buildKey(sessionId);
|
||||
Object value = redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (value instanceof SessionContext) {
|
||||
SessionContext context = (SessionContext) value;
|
||||
log.debug("获取会话成功: sessionId={}", sessionId);
|
||||
return Optional.of(context);
|
||||
}
|
||||
|
||||
log.debug("会话不存在: sessionId={}", sessionId);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSession(SessionContext sessionContext) {
|
||||
String sessionId = sessionContext.getSessionId();
|
||||
String key = buildKey(sessionId);
|
||||
|
||||
// 获取剩余 TTL
|
||||
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
|
||||
if (ttl == null || ttl <= 0) {
|
||||
ttl = sessionContext.getTtl() != null ? sessionContext.getTtl() : 3600L;
|
||||
}
|
||||
|
||||
sessionContext.setLastActiveAt(LocalDateTime.now());
|
||||
redisTemplate.opsForValue().set(key, sessionContext, ttl, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("更新会话成功: sessionId={}", sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSession(String sessionId) {
|
||||
String key = buildKey(sessionId);
|
||||
Boolean deleted = redisTemplate.delete(key);
|
||||
|
||||
if (Boolean.TRUE.equals(deleted)) {
|
||||
log.info("删除会话成功: sessionId={}", sessionId);
|
||||
} else {
|
||||
log.warn("删除会话失败,会话可能不存在: sessionId={}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sessionId) {
|
||||
String key = buildKey(sessionId);
|
||||
Boolean exists = redisTemplate.hasKey(key);
|
||||
return Boolean.TRUE.equals(exists);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshSession(String sessionId, long ttlSeconds) {
|
||||
String key = buildKey(sessionId);
|
||||
Boolean refreshed = redisTemplate.expire(key, ttlSeconds, TimeUnit.SECONDS);
|
||||
|
||||
if (Boolean.TRUE.equals(refreshed)) {
|
||||
log.debug("刷新会话过期时间成功: sessionId={}, newTtl={}秒", sessionId, ttlSeconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
log.warn("刷新会话过期时间失败,会话可能不存在: sessionId={}", sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToolCall(String sessionId, ToolCall toolCall) {
|
||||
Optional<SessionContext> sessionOpt = getSession(sessionId);
|
||||
|
||||
if (sessionOpt.isPresent()) {
|
||||
SessionContext context = sessionOpt.get();
|
||||
context.addToolCall(toolCall);
|
||||
updateSession(context);
|
||||
|
||||
log.debug("添加工具调用记录成功: sessionId={}, toolName={}", sessionId, toolCall.getToolName());
|
||||
} else {
|
||||
log.warn("会话不存在,无法添加工具调用记录: sessionId={}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(String sessionId, String status) {
|
||||
Optional<SessionContext> sessionOpt = getSession(sessionId);
|
||||
|
||||
if (sessionOpt.isPresent()) {
|
||||
SessionContext context = sessionOpt.get();
|
||||
context.setStatus(status);
|
||||
updateSession(context);
|
||||
|
||||
log.debug("更新会话状态成功: sessionId={}, status={}", sessionId, status);
|
||||
} else {
|
||||
log.warn("会话不存在,无法更新状态: sessionId={}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Redis key
|
||||
*/
|
||||
private String buildKey(String sessionId) {
|
||||
return SESSION_KEY_PREFIX + sessionId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user