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:
zhuyongxin
2026-06-23 14:38:17 +08:00
parent 1de1e98ef8
commit 48132d297d
20 changed files with 1621 additions and 596 deletions
@@ -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;
}
}