feat: 切换管理端聊天记录至智能体会话

- 管理端固定筛选 Agent 会话并拒绝旧 Bot 历史访问

- 筛选候选及页面语义切换为智能体并补充前后端回归测试
This commit is contained in:
2026-07-21 18:55:11 +08:00
parent f658f120e6
commit 9436cc5397
8 changed files with 414 additions and 21 deletions

View File

@@ -5,9 +5,13 @@ import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigInteger;
import java.util.Date;
/**
* 管理端聊天会话筛选条件。
*/
public class ChatSessionFilterQuery extends ChatPageQuery {
private BigInteger assistantId;
private String assistantCode;
private BigInteger userId;
private String userAccount;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -15,42 +19,110 @@ public class ChatSessionFilterQuery extends ChatPageQuery {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
/**
* 获取助手 ID。
*
* @return 助手 ID
*/
public BigInteger getAssistantId() {
return assistantId;
}
/**
* 设置助手 ID。
*
* @param assistantId 助手 ID
*/
public void setAssistantId(BigInteger assistantId) {
this.assistantId = assistantId;
}
/**
* 获取助手类型编码。
*
* @return 助手类型编码
*/
public String getAssistantCode() {
return assistantCode;
}
/**
* 设置助手类型编码。
*
* @param assistantCode 助手类型编码
*/
public void setAssistantCode(String assistantCode) {
this.assistantCode = assistantCode;
}
/**
* 获取用户 ID。
*
* @return 用户 ID
*/
public BigInteger getUserId() {
return userId;
}
/**
* 设置用户 ID。
*
* @param userId 用户 ID
*/
public void setUserId(BigInteger userId) {
this.userId = userId;
}
/**
* 获取用户账号筛选值。
*
* @return 用户账号筛选值
*/
public String getUserAccount() {
return userAccount;
}
/**
* 设置用户账号筛选值。
*
* @param userAccount 用户账号筛选值
*/
public void setUserAccount(String userAccount) {
this.userAccount = userAccount;
}
/**
* 获取开始时间。
*
* @return 开始时间
*/
public Date getStartTime() {
return startTime;
}
/**
* 设置开始时间。
*
* @param startTime 开始时间
*/
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
/**
* 获取结束时间。
*
* @return 结束时间
*/
public Date getEndTime() {
return endTime;
}
/**
* 设置结束时间。
*
* @param endTime 结束时间
*/
public void setEndTime(Date endTime) {
this.endTime = endTime;
}

View File

@@ -732,6 +732,10 @@ public class ChatAnalyticalDBRepository {
if (query == null) {
return;
}
if (StringUtils.hasText(query.getAssistantCode())) {
sql.append(" AND assistant_code=?");
args.add(query.getAssistantCode().trim());
}
if (query.getAssistantId() != null) {
sql.append(" AND assistant_id=?");
args.add(query.getAssistantId());

View File

@@ -21,6 +21,8 @@ import java.math.BigInteger;
@Service
public class ChatHistoryManageServiceImpl implements ChatHistoryManageService {
private static final String ADMIN_ASSISTANT_CODE = "AGENT";
private final ChatSessionQueryService chatSessionQueryService;
private final ChatSessionCommandService chatSessionCommandService;
private final ChatHistoryQueryService chatHistoryQueryService;
@@ -46,7 +48,10 @@ public class ChatHistoryManageServiceImpl implements ChatHistoryManageService {
@Override
public ChatSessionPage queryAdminSessions(ChatSessionFilterQuery query) {
return chatAnalyticalDBRepository.pageSessions(query);
ChatSessionFilterQuery effectiveQuery = query == null ? new ChatSessionFilterQuery() : query;
// 管理端聊天历史已经切换为 Agent 专属入口,类型由服务端固定,避免客户端绕过。
effectiveQuery.setAssistantCode(ADMIN_ASSISTANT_CODE);
return chatAnalyticalDBRepository.pageSessions(effectiveQuery);
}
@Override
@@ -64,8 +69,10 @@ public class ChatHistoryManageServiceImpl implements ChatHistoryManageService {
@Override
public ChatSessionSummary getAdminSession(BigInteger sessionId) {
ChatSessionSummary summary = chatAnalyticalDBRepository.getSession(sessionId);
if (summary == null || summary.getIsDeleted() != null && summary.getIsDeleted() == 1) {
throw new BusinessException("会话不存在");
if (summary == null
|| Integer.valueOf(1).equals(summary.getIsDeleted())
|| !ADMIN_ASSISTANT_CODE.equals(summary.getAssistantCode())) {
throw new BusinessException("Agent 会话不存在");
}
return summary;
}

View File

@@ -8,6 +8,7 @@ import org.springframework.beans.factory.support.StaticListableBeanFactory;
import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
import tech.easyflow.chatlog.support.ChatJsonSupport;
import tech.easyflow.common.analyticaldb.config.AnalyticalDBFlywayProperties;
import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations;
@@ -26,6 +27,27 @@ import java.util.List;
*/
public class ChatAnalyticalDBRepositoryTest {
/**
* 验证会话分页的统计 SQL 与数据 SQL 使用相同的 Agent 类型和 ID 条件。
*/
@Test
public void shouldApplyAssistantCodeAndIdToSessionPageQueries() {
RecordingAnalyticalDBOperations operations = new RecordingAnalyticalDBOperations();
ChatSessionFilterQuery query = new ChatSessionFilterQuery();
query.setAssistantCode("AGENT");
query.setAssistantId(BigInteger.TEN);
ChatAnalyticalDBRepository repository = newRepository(operations);
repository.pageSessions(query);
Assert.assertTrue(operations.lastPageCountSql.contains("assistant_code=?"));
Assert.assertTrue(operations.lastPageCountSql.contains("assistant_id=?"));
Assert.assertTrue(operations.lastPageDataSql.contains("assistant_code=?"));
Assert.assertTrue(operations.lastPageDataSql.contains("assistant_id=?"));
Assert.assertArrayEquals(new Object[]{"AGENT", BigInteger.TEN}, operations.lastPageCountArgs);
Assert.assertArrayEquals(new Object[]{"AGENT", BigInteger.TEN}, operations.lastPageDataArgs);
}
/**
* 验证工作台汇总使用跨天去重的 session 口径。
*/
@@ -149,6 +171,10 @@ public class ChatAnalyticalDBRepositoryTest {
private String lastQueryOneSql;
private String lastQuerySql;
private String lastPageCountSql;
private String lastPageDataSql;
private Object[] lastPageCountArgs;
private Object[] lastPageDataArgs;
private ChatDashboardSummary queryOneResult;
@Override
@@ -204,7 +230,11 @@ public class ChatAnalyticalDBRepositoryTest {
Object[] dataArgs,
AnalyticalDBPageRequest pageRequest,
RowMapper<T> rowMapper) {
return null;
this.lastPageCountSql = countSql;
this.lastPageCountArgs = countArgs;
this.lastPageDataSql = dataSql;
this.lastPageDataArgs = dataArgs;
return new AnalyticalDBPageResult<>();
}
}
}

View File

@@ -0,0 +1,242 @@
package tech.easyflow.chatlog.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.support.StaticListableBeanFactory;
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
import tech.easyflow.chatlog.repository.analyticaldb.ChatAnalyticalDBRepository;
import tech.easyflow.chatlog.service.ChatHistoryQueryService;
import tech.easyflow.chatlog.service.ChatRoundOperateService;
import tech.easyflow.chatlog.service.ChatSessionCommandService;
import tech.easyflow.chatlog.service.ChatSessionQueryService;
import tech.easyflow.chatlog.support.ChatJsonSupport;
import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
/**
* {@link ChatHistoryManageServiceImpl} 管理端 Agent 会话边界测试。
*/
public class ChatHistoryManageServiceImplTest {
private StubChatAnalyticalDBRepository chatAnalyticalDBRepository;
private ChatHistoryManageServiceImpl service;
/**
* 初始化管理端聊天历史服务及其依赖。
*/
@Before
public void setUp() {
ChatSessionQueryService chatSessionQueryService = unusedDependency(ChatSessionQueryService.class);
ChatSessionCommandService chatSessionCommandService = unusedDependency(ChatSessionCommandService.class);
ChatHistoryQueryService chatHistoryQueryService = unusedDependency(ChatHistoryQueryService.class);
ChatRoundOperateService chatRoundOperateService = unusedDependency(ChatRoundOperateService.class);
chatAnalyticalDBRepository = new StubChatAnalyticalDBRepository();
service = new ChatHistoryManageServiceImpl(
chatSessionQueryService,
chatSessionCommandService,
chatHistoryQueryService,
chatRoundOperateService,
chatAnalyticalDBRepository
);
}
/**
* 验证管理端列表始终覆盖客户端传入的会话类型为 Agent。
*/
@Test
public void queryAdminSessionsShouldForceAgentAssistantCode() {
ChatSessionFilterQuery query = new ChatSessionFilterQuery();
query.setAssistantCode("BOT");
service.queryAdminSessions(query);
Assert.assertEquals("AGENT", query.getAssistantCode());
Assert.assertSame(query, chatAnalyticalDBRepository.lastPageQuery);
}
/**
* 验证管理端可以读取正式 Agent 会话。
*/
@Test
public void getAdminSessionShouldReturnAgentSession() {
BigInteger sessionId = BigInteger.valueOf(1001);
ChatSessionSummary summary = session(sessionId, "AGENT", 0);
chatAnalyticalDBRepository.sessionResult = summary;
ChatSessionSummary result = service.getAdminSession(sessionId);
Assert.assertSame(summary, result);
}
/**
* 验证正式 Agent 会话可以继续读取历史消息。
*/
@Test
public void queryAdminMessagesShouldReturnAgentHistory() {
BigInteger sessionId = BigInteger.valueOf(1004);
ChatHistoryPage expectedPage = new ChatHistoryPage();
chatAnalyticalDBRepository.sessionResult = session(sessionId, "AGENT", 0);
service = new ChatHistoryManageServiceImpl(
unusedDependency(ChatSessionQueryService.class),
unusedDependency(ChatSessionCommandService.class),
(requestedSessionId, query) -> expectedPage,
unusedDependency(ChatRoundOperateService.class),
chatAnalyticalDBRepository
);
ChatPageQuery query = new ChatPageQuery();
query.setPageNumber(2);
ChatHistoryPage result = service.queryAdminMessages(sessionId, query);
Assert.assertSame(expectedPage, result);
}
/**
* 验证管理端拒绝旧 Bot 会话详情。
*/
@Test
public void getAdminSessionShouldRejectBotSession() {
BigInteger sessionId = BigInteger.valueOf(1002);
chatAnalyticalDBRepository.sessionResult = session(sessionId, "BOT", 0);
BusinessException exception = Assert.assertThrows(
BusinessException.class,
() -> service.getAdminSession(sessionId)
);
Assert.assertEquals("Agent 会话不存在", exception.getMessage());
}
/**
* 验证旧 Bot 会话在消息查询前即被拒绝,不会继续读取历史消息。
*/
@Test
public void queryAdminMessagesShouldRejectBotBeforeHistoryLookup() {
BigInteger sessionId = BigInteger.valueOf(1003);
chatAnalyticalDBRepository.sessionResult = session(sessionId, "BOT", 0);
Assert.assertThrows(
BusinessException.class,
() -> service.queryAdminMessages(sessionId, new ChatPageQuery())
);
}
/**
* 验证旧 Bot 会话在答案版本列表查询前即被拒绝。
*/
@Test
public void listAdminRoundVariantsShouldRejectBotBeforeRoundLookup() {
BigInteger sessionId = BigInteger.valueOf(1005);
chatAnalyticalDBRepository.sessionResult = session(sessionId, "BOT", 0);
Assert.assertThrows(
BusinessException.class,
() -> service.listAdminRoundVariants(sessionId, BigInteger.ONE)
);
}
/**
* 验证旧 Bot 会话在答案版本选择前即被拒绝。
*/
@Test
public void selectAdminRoundVariantShouldRejectBotBeforeRoundUpdate() {
BigInteger sessionId = BigInteger.valueOf(1006);
chatAnalyticalDBRepository.sessionResult = session(sessionId, "BOT", 0);
Assert.assertThrows(
BusinessException.class,
() -> service.selectAdminRoundVariant(
sessionId,
BigInteger.ONE,
1,
BigInteger.TEN
)
);
}
/**
* 创建不应在当前测试路径中被调用的接口依赖。
*
* @param dependencyType 依赖接口类型
* @param <T> 依赖接口类型
* @return 调用任意方法即失败的代理对象
*/
private <T> T unusedDependency(Class<T> dependencyType) {
Object proxy = Proxy.newProxyInstance(
dependencyType.getClassLoader(),
new Class<?>[]{dependencyType},
(instance, method, args) -> {
throw new AssertionError("测试路径不应调用依赖方法: " + method.getName());
}
);
return dependencyType.cast(proxy);
}
/**
* 构造会话摘要。
*
* @param sessionId 会话 ID
* @param assistantCode 助手类型编码
* @param isDeleted 删除标识
* @return 会话摘要
*/
private ChatSessionSummary session(BigInteger sessionId, String assistantCode, int isDeleted) {
ChatSessionSummary summary = new ChatSessionSummary();
summary.setId(sessionId);
summary.setAssistantCode(assistantCode);
summary.setIsDeleted(isDeleted);
return summary;
}
/**
* 仅记录管理端会话查询参数和返回值的分析库仓储桩。
*/
private static class StubChatAnalyticalDBRepository extends ChatAnalyticalDBRepository {
private ChatSessionFilterQuery lastPageQuery;
private ChatSessionSummary sessionResult;
/**
* 创建不连接真实分析库的仓储桩。
*/
private StubChatAnalyticalDBRepository() {
super(
new StaticListableBeanFactory().getBeanProvider(AnalyticalDBOperations.class),
null,
new ChatJsonSupport(new ObjectMapper())
);
}
/**
* 记录分页筛选参数。
*
* @param query 会话筛选条件
* @return 空分页结果
*/
@Override
public ChatSessionPage pageSessions(ChatSessionFilterQuery query) {
lastPageQuery = query;
return new ChatSessionPage();
}
/**
* 返回预设的会话摘要。
*
* @param sessionId 会话 ID
* @return 预设会话摘要
*/
@Override
public ChatSessionSummary getSession(BigInteger sessionId) {
return sessionResult;
}
}
}