10 changed files with 296 additions and 12 deletions
-
5pom.xml
-
16src/main/java/com/wok/supportbot/app/AssistantApp.java
-
60src/main/java/com/wok/supportbot/chatmemory/DatabaseChatMemory.java
-
44src/main/java/com/wok/supportbot/converter/MessageConverter.java
-
9src/main/java/com/wok/supportbot/dao/ChatMessageMapper.java
-
73src/main/java/com/wok/supportbot/entity/ChatMessage.java
-
25src/main/java/com/wok/supportbot/handler/MyMetaObjectHandler.java
-
61src/main/java/com/wok/supportbot/handler/PostgresJsonTypeHandler.java
-
11src/main/java/com/wok/supportbot/repository/ChatMessageRepository.java
-
4src/test/java/com/wok/supportbot/SupportBotApplicationTests.java
@ -0,0 +1,60 @@ |
|||
package com.wok.supportbot.chatmemory; |
|||
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.wok.supportbot.converter.MessageConverter; |
|||
import com.wok.supportbot.entity.ChatMessage; |
|||
import com.wok.supportbot.repository.ChatMessageRepository; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.ai.chat.memory.ChatMemory; |
|||
import org.springframework.ai.chat.messages.Message; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class DatabaseChatMemory implements ChatMemory { |
|||
|
|||
@Autowired |
|||
private final ChatMessageRepository chatMessageRepository; |
|||
|
|||
@Override |
|||
public void add(String conversationId, List<Message> messages) { |
|||
List<ChatMessage> chatMessages = messages.stream() |
|||
.map(message -> MessageConverter.toChatMessage(message, conversationId)) |
|||
.collect(Collectors.toList()); |
|||
|
|||
chatMessageRepository.saveBatch(chatMessages, chatMessages.size()); |
|||
} |
|||
|
|||
@Override |
|||
public List<Message> get(String conversationId, int lastN) { |
|||
LambdaQueryWrapper<ChatMessage> queryWrapper = new LambdaQueryWrapper<>(); |
|||
// 查询最近的 lastN 条消息 |
|||
queryWrapper.eq(ChatMessage::getConversationId, conversationId) |
|||
.orderByDesc(ChatMessage::getCreateTime) |
|||
.last(lastN > 0, "LIMIT " + lastN); |
|||
|
|||
List<ChatMessage> chatMessages = chatMessageRepository.list(queryWrapper); |
|||
|
|||
// 按照时间顺序返回 |
|||
if (!chatMessages.isEmpty()) { |
|||
Collections.reverse(chatMessages); |
|||
} |
|||
|
|||
return chatMessages |
|||
.stream() |
|||
.map(MessageConverter::toMessage) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
@Override |
|||
public void clear(String conversationId) { |
|||
LambdaQueryWrapper<ChatMessage> queryWrapper = new LambdaQueryWrapper<>(); |
|||
queryWrapper.eq(ChatMessage::getConversationId, conversationId); |
|||
chatMessageRepository.remove(queryWrapper); |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
package com.wok.supportbot.converter; |
|||
|
|||
import com.wok.supportbot.entity.ChatMessage; |
|||
import org.springframework.ai.chat.messages.*; |
|||
|
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* @Classname MessageConverter |
|||
* @Description |
|||
* @Version 1.0.0 |
|||
* @Date 2025/06/28 13:30 |
|||
* @Author lyx |
|||
*/ |
|||
public class MessageConverter { |
|||
|
|||
/** |
|||
* 将 Message 转换为 ChatMessage |
|||
*/ |
|||
public static ChatMessage toChatMessage(Message message, String conversationId) { |
|||
return ChatMessage.builder() |
|||
.conversationId(conversationId) |
|||
.messageType(message.getMessageType()) |
|||
.content(message.getText()) |
|||
.metadata(message.getMetadata()) |
|||
.build(); |
|||
} |
|||
|
|||
/** |
|||
* 将 ChatMessage 转换为 Message |
|||
*/ |
|||
public static Message toMessage(ChatMessage chatMessage) { |
|||
MessageType messageType = chatMessage.getMessageType(); |
|||
String text = chatMessage.getContent(); |
|||
Map<String, Object> metadata = chatMessage.getMetadata(); |
|||
return switch (messageType) { |
|||
case USER -> new UserMessage(text); |
|||
case ASSISTANT -> new AssistantMessage(text, metadata); |
|||
case SYSTEM -> new SystemMessage(text); |
|||
case TOOL -> new ToolResponseMessage(List.of(), metadata); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
package com.wok.supportbot.dao; |
|||
|
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import com.wok.supportbot.entity.ChatMessage; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
@Mapper |
|||
public interface ChatMessageMapper extends BaseMapper<ChatMessage> { |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
package com.wok.supportbot.entity; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import com.wok.supportbot.handler.PostgresJsonTypeHandler; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
import org.springframework.ai.chat.messages.MessageType; |
|||
|
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
import java.util.Map; |
|||
|
|||
@Data |
|||
@Builder |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
@TableName(value = "chat_message", autoResultMap = true) |
|||
public class ChatMessage implements Serializable { |
|||
|
|||
@Serial |
|||
@TableField(exist = false) |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
@TableId(value = "id", type = IdType.ASSIGN_ID) |
|||
private Long id; |
|||
|
|||
/** |
|||
* 会话ID |
|||
*/ |
|||
@TableField("conversation_id") |
|||
private String conversationId; |
|||
|
|||
/** |
|||
* 消息类型 |
|||
*/ |
|||
@TableField("message_type") |
|||
private MessageType messageType; |
|||
|
|||
/** |
|||
* 消息内容 |
|||
*/ |
|||
@TableField("content") |
|||
private String content; |
|||
|
|||
/** |
|||
* 元数据 |
|||
*/ |
|||
@TableField(value = "metadata", typeHandler = PostgresJsonTypeHandler.class) |
|||
private Map<String, Object> metadata; |
|||
|
|||
/** |
|||
* 创建时间 |
|||
*/ |
|||
@TableField(value = "create_time", fill = FieldFill.INSERT) |
|||
private Date createTime; |
|||
|
|||
/** |
|||
* 更新时间 |
|||
*/ |
|||
@Version |
|||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) |
|||
private Date updateTime; |
|||
|
|||
/** |
|||
* 是否删除 false-未删除 true-已删除 |
|||
*/ |
|||
@TableField("is_delete") |
|||
@TableLogic |
|||
private boolean isDelete; |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
package com.wok.supportbot.handler; |
|||
|
|||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; |
|||
import org.apache.ibatis.reflection.MetaObject; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.Date; |
|||
|
|||
/** |
|||
* 在PostgreSQL中,@TableField(fill = FieldFill.INSERT) 和 @TableField(fill = FieldFill.INSERT_UPDATE) 这样的注解本身并不能直接触发数据库级别的自动填充。 |
|||
* 这些注解是MyBatis-Plus框架的一部分,它们需要配合 MetaObjectHandler 实现类才能工作。这种机制是在Java应用层面实现的,而非数据库层面。 |
|||
*/ |
|||
@Component |
|||
public class MyMetaObjectHandler implements MetaObjectHandler { |
|||
@Override |
|||
public void insertFill(MetaObject metaObject) { |
|||
this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); |
|||
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date()); |
|||
} |
|||
|
|||
@Override |
|||
public void updateFill(MetaObject metaObject) { |
|||
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
package com.wok.supportbot.handler; |
|||
|
|||
import com.fasterxml.jackson.core.JsonProcessingException; |
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import org.apache.ibatis.type.BaseTypeHandler; |
|||
import org.apache.ibatis.type.JdbcType; |
|||
import org.apache.ibatis.type.MappedJdbcTypes; |
|||
import org.apache.ibatis.type.MappedTypes; |
|||
|
|||
import java.sql.*; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* PostgreSQL使用JSONB类型存储JSON数据,需要创建自定义类型处理器: |
|||
*/ |
|||
@MappedJdbcTypes(JdbcType.OTHER) |
|||
@MappedTypes({Map.class}) |
|||
public class PostgresJsonTypeHandler extends BaseTypeHandler<Map<String, Object>> { |
|||
private static final ObjectMapper objectMapper = new ObjectMapper(); |
|||
|
|||
@Override |
|||
public void setNonNullParameter(PreparedStatement ps, int i, Map<String, Object> parameter, JdbcType jdbcType) |
|||
throws SQLException { |
|||
try { |
|||
ps.setObject(i, objectMapper.writeValueAsString(parameter), Types.OTHER); |
|||
} catch (JsonProcessingException e) { |
|||
throw new SQLException("Error converting Map to JSON", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, Object> getNullableResult(ResultSet rs, String columnName) throws SQLException { |
|||
String json = rs.getString(columnName); |
|||
return parseJson(json); |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, Object> getNullableResult(ResultSet rs, int columnIndex) throws SQLException { |
|||
String json = rs.getString(columnIndex); |
|||
return parseJson(json); |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, Object> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { |
|||
String json = cs.getString(columnIndex); |
|||
return parseJson(json); |
|||
} |
|||
|
|||
private Map<String, Object> parseJson(String json) throws SQLException { |
|||
try { |
|||
if (json == null) { |
|||
return new HashMap<>(); |
|||
} |
|||
return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {}); |
|||
} catch (JsonProcessingException e) { |
|||
throw new SQLException("Error parsing JSON to Map", e); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
package com.wok.supportbot.repository; |
|||
|
|||
import com.baomidou.mybatisplus.extension.repository.CrudRepository; |
|||
import com.wok.supportbot.dao.ChatMessageMapper; |
|||
import com.wok.supportbot.entity.ChatMessage; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
@Component |
|||
public class ChatMessageRepository extends CrudRepository<ChatMessageMapper, ChatMessage> { |
|||
|
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue