历史记录
本文档详细介绍了校园领域搜索问答系统中的历史记录管理功能模块的设计与实现。该模块旨在记录和管理用户的对话历史,支持用户查看历史记录和对特定对话内容进行点赞操作。本文将涵盖接口设计、数据库表结构、控制器实现以及服务层实现。
功能概述
历史记录管理模块主要负责以下功能:
- 查看历史记录:允许用户查看特定会话的对话历史,支持分页查询。
- 点赞功能:用户可以对特定的提问和回答进行点赞或取消点赞,帮助系统优化推荐和回答质量。
接口设计
1. 获取历史记录
请求方法:
GET
接口路径:
/api/v1/chat/history
请求参数:
参数 类型 描述 是否必填 session_id Long 会话 ID 是 offset Integer 页码(默认值为 1) 否 limit Integer 每页数量(默认值为 100) 否 请求示例:
GET /api/v1/chat/history?session_id=12345&offset=1&limit=20
响应格式:
{
"ok": true,
"data":
[
{
"metadata": null,
"images": null,
"role": "user",
"citations": null,
"create_time": 1752400107534,
"model": null,
"id": "534035886523015168",
"content": "What has Ant Design X upgraded?",
"liked": null
},
{
"metadata": null,
"images": null,
"role": "assistant",
"citations": null,
"create_time": 1752400119390,
"model": "deepseek-v3-250324",
"id": "534035886619484160",
"content": "Ant Design X is an advanced version or extension of **Ant Design** (a popular React UI library). While there isn't an official \"Ant Design X\" release as of my knowledge cutoff (July 2025), here’s what hypothetical or future upgrades might include based on Ant Design’s evolution trends:\n\n### Possible Upgrades in Ant Design X:\n1. **Enhanced Performance** \n - Faster rendering, reduced bundle size, and optimized animations.\n\n2. **New Components** \n - Advanced data visualization (e.g., charts, graphs), AI-assisted UI components, or accessibility-focused widgets.\n\n3. **Design System Improvements** \n - Dark mode refinements, dynamic themes, or Figma integration for smoother design-to-code workflows.\n\n4. **Developer Experience (DX)** \n - Better TypeScript support, simplified APIs, or CLI tools for scaffolding projects.\n\n5. **Micro-Frontend Compatibility** \n - Improved support for modular architectures like micro-apps.\n\n6. **Accessibility (a11y)** \n - WCAG 2.2 compliance and screen-reader optimizations.\n\n7. **State Management Integration** \n - Built-in hooks for Zustand/Jotai or seamless Redux Toolkit compatibility.\n\n8. **Mobile-First Enhancements** \n - Responsive utilities or dedicated mobile components.\n\nIf you’re referring to a specific update, check the [Ant Design GitHub](https://github.com/ant-design/ant-design) or official blog for release notes. Let me know if you meant a different \"X\" upgrade!",
"liked": null
}
]
}
2. 点赞/取消点赞
请求方法:
POST
接口路径:
/api/v1/chat/like
请求头:
Content-Type: application/json
请求体:
{ "question_id": "452075106336137216", "answer_id": "452075122110914560", "like": true }
请求示例:
POST /api/v1/chat/like Content-Type: application/json { "question_id": "452075106336137216", "answer_id": "452075122110914560", "like": true }
响应格式:
{ "data": null, "code": 1, "msg": null, "ok": true }
控制器实现
ApiChatHandler
类负责处理与历史记录管理相关的 HTTP 请求,包括获取历史记录和点赞操作。以下是该类的实现代码及说明:
package com.litongjava.llm.handler;
import java.util.List;
import java.util.stream.Collectors;
import com.alibaba.fastjson2.JSONObject;
import com.jfinal.kit.Kv;
import com.litongjava.db.TableResult;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.llm.consts.AgentTableNames;
import com.litongjava.llm.services.LlmChatHistoryService;
import com.litongjava.llm.services.LlmChatSessionService;
import com.litongjava.llm.services.LlmQuestionRecommendService;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.model.page.Page;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.server.util.CORSUtils;
import com.litongjava.tio.utils.hutool.StrUtil;
import com.litongjava.tio.utils.json.FastJson2Utils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ApiChatHandler {
/**
* 处理推荐问题请求
*
* @param request HTTP请求对象
* @return HTTP响应对象
*/
public HttpResponse recommend(HttpRequest request) {
// 获取响应对象
HttpResponse response = TioRequestContext.getResponse();
// 启用CORS
CORSUtils.enableCORS(response);
// 获取请求参数num,默认值为4
Integer num = request.getInt("num");
if (num == null) {
num = 4;
}
// 调用服务层获取推荐问题数据
TableResult<Page<Row>> tr = Aop.get(LlmQuestionRecommendService.class).page(num);
RespBodyVo respBodyVo;
if (tr.isOk()) {
// 获取数据列表
List<Row> list = tr.getData().getList();
// 将Row转换为Kv对象
List<Kv> kvs = list.stream().map(Row::toKv).collect(Collectors.toList());
// 构建成功响应
respBodyVo = RespBodyVo.ok(kvs);
} else {
// 构建失败响应
respBodyVo = RespBodyVo.fail(tr.getMsg());
}
// 设置响应的JSON内容
return response.setJson(respBodyVo);
}
/**
* 创建会话
*
* @param request HTTP请求对象
* @return HTTP响应对象
*/
public HttpResponse createSession(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
CORSUtils.enableCORS(response);
String userId = TioRequestContext.getUserIdString();
String name = request.getParam("name");
String schoolIdString = request.getParam("school_id");
Integer chatType = request.getInt("chat_type");
String type = request.getString("type");
Long appId = request.getLong("app_id");
if (chatType == null) {
chatType = 0;
}
RespBodyVo respBodyVo = null;
if (StrUtil.isBlank(name)) {
response.setStatus(400);
respBodyVo = RespBodyVo.fail("name cannot be empty");
}
if (StrUtil.isBlank(schoolIdString)) {
schoolIdString = "1";
}
Long school_id = Long.parseLong(schoolIdString);
TableResult<Kv> tr = Aop.get(LlmChatSessionService.class).create(userId, name, school_id, type, chatType, appId);
if (tr.isOk()) {
respBodyVo = RespBodyVo.ok(tr.getData());
} else {
respBodyVo = RespBodyVo.fail();
}
return response.setJson(respBodyVo);
}
/**
* 删除会话
*
* @param request HTTP请求对象
* @return HTTP响应对象
*/
public HttpResponse deleteSession(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
CORSUtils.enableCORS(response);
Long session_id = request.getLong("session_id");
log.info("session_id: {}", session_id);
if (session_id == null) {
return response.fail(RespBodyVo.fail("session_id can not be empty"));
}
String userId = TioRequestContext.getUserIdString();
int updateResult = Aop.get(LlmChatSessionService.class).softDelete(session_id, userId);
if (updateResult > 0) {
response.setJson(RespBodyVo.ok());
} else {
response.setJson(RespBodyVo.fail());
}
return response;
}
/**
* 列出会话
*
* @param request HTTP请求对象
* @return HTTP响应对象
*/
public HttpResponse listSession(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
CORSUtils.enableCORS(response);
Integer pageNo = request.getInt("offset");
Integer pageSize = request.getInt("limit");
Long schoolId = request.getLong("school_id");
Integer chatType = request.getInt("chat_type");
if (pageNo == null) {
pageNo = 1;
}
if (pageSize == null) {
pageSize = 10;
}
if (chatType == null) {
chatType = 0;
}
String userId = TioRequestContext.getUserIdString();
List<Kv> list = Aop.get(LlmChatSessionService.class).page(pageNo, pageSize, userId, schoolId, chatType);
return response.setJson(RespBodyVo.ok(list));
}
/**
* 设置会话名称
*
* @param request HTTP请求对象
* @return HTTP响应对象
*/
public HttpResponse setSessionName(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
CORSUtils.enableCORS(response);
Long sessionId = request.getLong("session_id");
String name = request.getParam("name");
if (sessionId == null) {
return response.fail(RespBodyVo.fail("session_id can not be empty"));
}
if (StrUtil.isEmpty(name)) {
return response.fail(RespBodyVo.fail("name can not be empty"));
}
String userId = TioRequestContext.getUserIdString();
boolean exists = Aop.get(LlmChatSessionService.class).exists(sessionId, userId);
if (!exists) {
return response.fail(RespBodyVo.fail("invalid session"));
}
int updateResult = Aop.get(LlmChatSessionService.class).updateSessionName(name, sessionId, userId);
RespBodyVo respBodyVo = null;
if (updateResult > 0) {
respBodyVo = RespBodyVo.ok();
} else {
respBodyVo = RespBodyVo.fail();
}
return response.setJson(respBodyVo);
}
/**
* 获取历史记录
*
* @param request HTTP请求对象
* @return HTTP响应对象
*/
public HttpResponse getChatHistory(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
CORSUtils.enableCORS(response);
Long session_id = request.getLong("session_id");
if (session_id == null) {
return response.fail(RespBodyVo.fail("chat_id can not be empty"));
}
Integer pageNo = request.getInt("offset");
Integer pageSize = request.getInt("limit");
if (pageNo == null) {
pageNo = 1;
}
if (pageSize == null) {
pageSize = 100;
}
String userId = TioRequestContext.getUserIdString();
LlmChatSessionService llmChatSessionService = Aop.get(LlmChatSessionService.class);
boolean exists = llmChatSessionService.exists(session_id, userId);
if (!exists) {
log.info("invalid session:{},{}", session_id, userId);
return response.fail(RespBodyVo.fail("invalid session"));
}
LlmChatHistoryService chatHistoryService = Aop.get(LlmChatHistoryService.class);
RespBodyVo ok = chatHistoryService.getHistory(session_id, pageNo, pageSize);
return response.setJson(ok);
}
/**
* 点赞或取消点赞
*
* @param request HTTP请求对象
* @return HTTP响应对象
*/
public HttpResponse like(HttpRequest request) {
HttpResponse response = TioRequestContext.getResponse();
CORSUtils.enableCORS(response);
String bodyString = request.getBodyString();
JSONObject requestVo = FastJson2Utils.parseObject(bodyString);
Long questionId = requestVo.getLong("question_id");
Long answerId = requestVo.getLong("answer_id");
Boolean like = requestVo.getBoolean("like");
if (questionId == null) {
return response.fail(RespBodyVo.fail("question_id can not be empty"));
}
if (answerId == null) {
return response.fail(RespBodyVo.fail("answer_id can not be empty"));
}
if (like == null) {
return response.fail(RespBodyVo.fail("like can not be empty"));
}
String userId = TioRequestContext.getUserIdString();
boolean exists = Db.exists(AgentTableNames.llm_chat_history, "id", questionId);
if (!exists) {
return response.fail(RespBodyVo.fail("invalid question id"));
}
exists = Db.exists(AgentTableNames.llm_chat_history, "id", answerId);
if (!exists) {
return response.fail(RespBodyVo.fail("invalid answer id"));
}
Aop.get(LlmChatHistoryService.class).like(questionId, answerId, like, userId);
return response.setJson(RespBodyVo.ok());
}
}
控制器代码说明
getChatHistory:
- 处理
/api/v1/chat/history
的GET
请求。 - 从请求中获取参数
session_id
、offset
和limit
,设置默认值为 1 和 100。 - 校验
session_id
的有效性,确保会话存在且属于当前用户。 - 调用服务层
LlmChatHistoryService.getHistory
方法获取历史记录记录。 - 返回查询结果的响应。
- 处理
like:
- 处理
/api/v1/chat/like
的POST
请求。 - 从请求体中解析
question_id
、answer_id
和like
字段。 - 校验所有参数的有效性,确保
question_id
和answer_id
存在。 - 调用服务层
LlmChatHistoryService.like
方法执行点赞或取消点赞操作。 - 返回操作结果的响应。
- 处理
服务层实现
LlmChatHistoryService
类负责与数据库交互,处理历史记录记录的查询和点赞操作。以下是该类的实现代码及说明:
package com.litongjava.llm.services;
import java.util.ArrayList;
import java.util.List;
import com.jfinal.kit.Kv;
import com.litongjava.db.TableInput;
import com.litongjava.db.TableResult;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.llm.consts.AgentTableNames;
import com.litongjava.llm.utils.LarkBotUserThumbUtils;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.model.page.Page;
import com.litongjava.table.services.ApiTable;
import com.litongjava.tio.utils.thread.TioThreadUtils;
public class LlmChatHistoryService {
/**
* 获取历史记录记录
*
* @param session_id 会话ID
* @param pageNo 页码
* @param pageSize 每页数量
* @return 包含历史记录的响应对象
*/
public RespBodyVo getHistory(Long session_id, int pageNo, int pageSize) {
TableInput ti = TableInput.create().setColumns("id,role,content,liked,metadata,create_time")
//
.setJsonFields("metadata").set("session_id", session_id).set("hidden", false).orderBy("create_time")
//
.asc(true).pageNo(pageNo).pageSize(pageSize);
TableResult<Page<Row>> ts = ApiTable.page(AgentTableNames.llm_chat_history, ti);
List<Row> list = ts.getData().getList();
List<Kv> kvs = new ArrayList<>();
for (Row record : list) {
kvs.add(record.toKv());
}
return RespBodyVo.ok(kvs);
}
/**
* 保存用户提问
*
* @param id 提问ID
* @param sessionId 会话ID
* @param textQuestion 用户提问内容
* @return 保存结果
*/
public TableResult<Kv> saveUser(Long id, Long sessionId, String textQuestion) {
TableInput ti = TableInput.by("id", id).set("content", textQuestion).set("role", "user").set("session_id", sessionId);
TableResult<Kv> ts = ApiTable.save(AgentTableNames.llm_chat_history, ti);
return ts;
}
/**
* 保存助手回答
*
* @param id 回答ID
* @param sessionId 会话ID
* @param message 助手回答内容
* @return 保存结果
*/
public TableResult<Kv> saveAssistant(Long id, Long sessionId, String message) {
TableInput ti = TableInput.by("id", id).set("content", message).set("role", "assistant").set("session_id", sessionId);
TableResult<Kv> ts = ApiTable.save(AgentTableNames.llm_chat_history, ti);
return ts;
}
/**
* 点赞或取消点赞
*
* @param questionId 问题ID
* @param answerId 回答ID
* @param like 是否点赞
* @param userId 用户ID
*/
public void like(Long questionId, Long answerId, Boolean like, String userId) {
String sql = "update %s set liked=? where id=?";
sql = String.format(sql, AgentTableNames.llm_chat_history);
Db.updateBySql(sql, like, questionId);
Db.updateBySql(sql, like, answerId);
if (like) {
TioThreadUtils.submit(() -> {
String queryContentSql = "select content from %s where id=?";
queryContentSql = String.format(queryContentSql, AgentTableNames.llm_chat_history);
String question = Db.queryStr(queryContentSql, questionId);
String answer = Db.queryStr(queryContentSql, answerId);
StringBuffer messageText = new StringBuffer();
if (like) {
messageText.append("like").append("\r\n");
} else {
messageText.append("dislike").append("\r\n");
}
messageText.append("user_id:").append(userId).append("\r\n");
messageText.append("question_id:").append(questionId).append("\r\n");
messageText.append("question:").append(question).append("\r\n\r\n");
messageText.append("answer_id:").append(answerId).append("\r\n");
messageText.append("answer:").append(answer).append("\r\n");
LarkBotUserThumbUtils.send(messageText.toString());
});
}
}
}
服务层代码说明
getHistory:
- 构建查询条件,设置需要查询的字段
id
,role
,content
,liked
,metadata
,create_time
。 - 过滤条件包括
session_id
和hidden = false
,确保只获取当前会话且未隐藏的对话记录。 - 设置分页参数
pageNo
和pageSize
,并按照create_time
升序排序。 - 调用
ApiTable.page
方法进行分页查询。 - 将查询结果转换为
List<Kv>
格式,便于前端使用。 - 返回包含历史记录的响应对象。
- 构建查询条件,设置需要查询的字段
saveUser:
- 构建用户提问的
Row
对象,设置id
、content
、role
和session_id
。 - 调用
ApiTable.save
方法将提问记录保存到数据库。 - 返回保存结果。
- 构建用户提问的
saveAssistant:
- 构建助手回答的
Row
对象,设置id
、content
、role
和session_id
。 - 调用
ApiTable.save
方法将回答记录保存到数据库。 - 返回保存结果。
- 构建助手回答的
like:
- 根据传入的
questionId
和answerId
,更新对应记录的liked
字段。 - 如果是点赞操作,异步执行以下步骤:
- 查询问题和回答的内容。
- 构建点赞信息的消息文本,包括用户 ID、问题 ID 及内容、回答 ID 及内容。
- 调用
LarkBotUserThumbUtils.send
方法将消息发送到指定渠道(如企业微信或钉钉机器人)。
- 该功能有助于系统收集用户反馈,优化回答质量和推荐算法。
- 根据传入的
总结
本文档详细介绍了校园领域搜索问答系统中历史记录管理功能模块的设计与实现。通过定义清晰的 API 接口、设计合理的数据库表结构以及实现高效的控制器和服务层代码,该模块能够有效地记录和管理用户的对话历史,支持用户查看历史记录和进行点赞操作。未来,可以进一步扩展历史记录的功能,例如支持关键字搜索、导出历史记录以及与其他模块的深度集成,以提升系统的整体用户体验和功能丰富性。