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