Просмотр исходного кода

feat: 聊天历史接口适配分页 + 支持 image 类型消息展示

- fetchHistory 改为分页请求,treeId 必填,返回 MyBatis-Plus Page 格式
- ChatWidget 支持翻页加载历史记录(顶部点击加载更多)
- 消息增加 type 字段,image 类型渲染为图片

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tanlie 2 недель назад
Родитель
Сommit
ec16d51ffe
13 измененных файлов с 142 добавлено и 55 удалено
  1. 2 0
      wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/ChatMessage.java
  2. BIN
      wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/ChatMessage.class
  3. BIN
      wishing-platform/platform-entity/platform-entity-wishing/target/platform-entity-wishing-1.0.0-SNAPSHOT.jar
  4. 23 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/constants/MsgTypeEnum.java
  5. 10 4
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/ChatController.java
  6. 11 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/mapper/ChatMessageMapper.java
  7. 23 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/ChatHistoryReq.java
  8. 7 9
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/ChatHistoryResp.java
  9. 3 1
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/ChatService.java
  10. 18 31
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/ChatServiceImpl.java
  11. 6 4
      wishing-tree-h5/src/api/chat.ts
  12. 39 6
      wishing-tree-h5/src/components/ChatWidget.vue
  13. BIN
      wishing-tree-h5/wish.zip

+ 2 - 0
wishing-platform/platform-entity/platform-entity-wishing/src/main/java/cn/qinys/platform/entity/wishing/ChatMessage.java

@@ -25,4 +25,6 @@ public class ChatMessage extends BaseEntity {
 
     /** 消息内容 */
     private String content;
+
+    private String type;
 }

BIN
wishing-platform/platform-entity/platform-entity-wishing/target/classes/cn/qinys/platform/entity/wishing/ChatMessage.class


BIN
wishing-platform/platform-entity/platform-entity-wishing/target/platform-entity-wishing-1.0.0-SNAPSHOT.jar


+ 23 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/constants/MsgTypeEnum.java

@@ -0,0 +1,23 @@
+package cn.qinys.platform.mobile.constants;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-14 10:01
+ **/
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public enum MsgTypeEnum {
+
+    TEXT("text", "test message"),
+    IMAGE("image", "image message");
+
+    private String value;
+    private String type;
+}

+ 10 - 4
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/ChatController.java

@@ -1,9 +1,11 @@
 package cn.qinys.platform.mobile.controller;
 
 import cn.qinys.platform.base.response.Result;
+import cn.qinys.platform.mobile.req.ChatHistoryReq;
 import cn.qinys.platform.mobile.req.ChatReq;
 import cn.qinys.platform.mobile.resp.ChatResp;
 import cn.qinys.platform.mobile.service.ChatService;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import jakarta.annotation.Resource;
 import jakarta.validation.Valid;
 import cn.qinys.platform.mobile.resp.ChatHistoryResp;
@@ -16,7 +18,9 @@ public class ChatController {
     @Resource
     private ChatService chatService;
 
-    /** 发送消息 */
+    /**
+     * 发送消息
+     */
     @PostMapping
     public Result<ChatResp> chat(@RequestBody @Valid ChatReq req) {
         String reply = chatService.chat(req);
@@ -25,10 +29,12 @@ public class ChatController {
         return new Result<>(resp);
     }
 
-    /** 获取聊天历史(按许愿树过滤) */
+    /**
+     * 获取聊天历史(按许愿树过滤)
+     */
     @GetMapping("/history")
-    public Result<ChatHistoryResp> history(@RequestParam(required = false) Integer treeId) {
-        ChatHistoryResp resp = chatService.getHistory(treeId);
+    public Result<Page<ChatHistoryResp>> history(@Valid ChatHistoryReq req) {
+        Page<ChatHistoryResp> resp = chatService.getHistory(req);
         return new Result<>(resp);
     }
 }

+ 11 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/mapper/ChatMessageMapper.java

@@ -1,9 +1,20 @@
 package cn.qinys.platform.mobile.mapper;
 
 import cn.qinys.platform.entity.wishing.ChatMessage;
+import cn.qinys.platform.mobile.resp.ChatHistoryResp;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 @Mapper
 public interface ChatMessageMapper extends BaseMapper<ChatMessage> {
+
+
+    @Select("SELECT * FROM chat_message ${ew.customSqlSegment}")
+    Page<ChatHistoryResp> getChatHistoryPage(Page<ChatMessage> page, @Param(Constants.WRAPPER) Wrapper<ChatMessage> wrapper);
+
 }

+ 23 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/ChatHistoryReq.java

@@ -0,0 +1,23 @@
+package cn.qinys.platform.mobile.req;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-14 10:22
+ **/
+@Data
+public class ChatHistoryReq implements Serializable {
+
+    private Integer page = 1;
+
+    private Integer pageSize = 10;
+
+    @NotNull(message = "tree id can not be null")
+    private Integer treeId;
+
+}

+ 7 - 9
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/ChatHistoryResp.java

@@ -1,20 +1,18 @@
 package cn.qinys.platform.mobile.resp;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.io.Serializable;
 import java.time.LocalDateTime;
-import java.util.List;
 
 @Data
 public class ChatHistoryResp implements Serializable {
 
-    private List<ChatMsg> list;
-
-    @Data
-    public static class ChatMsg implements Serializable {
-        private String role;
-        private String content;
-        private LocalDateTime createdAt;
-    }
+    private String id;
+    private String role;
+    private String content;
+    private String type;
+    @JsonFormat(pattern = "MM-dd HH:mm")
+    private LocalDateTime createdAt;
 }

+ 3 - 1
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/ChatService.java

@@ -1,7 +1,9 @@
 package cn.qinys.platform.mobile.service;
 
+import cn.qinys.platform.mobile.req.ChatHistoryReq;
 import cn.qinys.platform.mobile.req.ChatReq;
 import cn.qinys.platform.mobile.resp.ChatHistoryResp;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 
 /**
  * 智能聊天客服服务
@@ -16,5 +18,5 @@ public interface ChatService {
     /**
      * 获取当前用户在某棵树下的聊天历史
      */
-    ChatHistoryResp getHistory(Integer treeId);
+    Page<ChatHistoryResp> getHistory(ChatHistoryReq req);
 }

+ 18 - 31
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/ChatServiceImpl.java

@@ -2,13 +2,17 @@ package cn.qinys.platform.mobile.service.impl;
 
 import cn.qinys.platform.base.security.utils.CurrentUtils;
 import cn.qinys.platform.entity.wishing.ChatMessage;
+import cn.qinys.platform.mobile.constants.MsgTypeEnum;
 import cn.qinys.platform.mobile.mapper.ChatMessageMapper;
+import cn.qinys.platform.mobile.req.ChatHistoryReq;
 import cn.qinys.platform.mobile.req.ChatReq;
 import cn.qinys.platform.mobile.resp.ChatHistoryResp;
 import cn.qinys.platform.mobile.service.ChatService;
 import cn.qinys.platform.mobile.tool.DateTimeTools;
 import cn.qinys.platform.mobile.tool.WeatherTools;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.ai.chat.client.ChatClient;
@@ -35,7 +39,7 @@ public class ChatServiceImpl implements ChatService {
             req.setConversationId(userId);
         }
         // 保存用户消息
-        saveMessage(userId, req.getTreeId(), "user", req.getMessage());
+        saveMessage(userId, req.getTreeId(), "user", req.getMessage(), MsgTypeEnum.TEXT.getValue());
         String reply;
         try {
             reply = chatClient.prompt()
@@ -50,7 +54,7 @@ public class ChatServiceImpl implements ChatService {
         }
 
         // 保存 AI 回复
-        saveMessage(userId, req.getTreeId(), "ai", reply);
+        saveMessage(userId, req.getTreeId(), "ai", reply, MsgTypeEnum.TEXT.getValue());
 
         return reply;
     }
@@ -58,47 +62,30 @@ public class ChatServiceImpl implements ChatService {
     // ...existing code...
 
     @Override
-    public ChatHistoryResp getHistory(Integer treeId) {
+    public Page<ChatHistoryResp> getHistory(ChatHistoryReq req) {
         String userId = getUserId();
-        ChatHistoryResp resp = new ChatHistoryResp();
-        if ("0".equals(userId) || "anonymous".equals(userId)) {
-            resp.setList(List.of());
-            return resp;
+        if ("0".equals(userId)) {
+            return new Page<>(0, 0);
         }
-        LambdaQueryWrapper<ChatMessage> wrapper = new LambdaQueryWrapper<ChatMessage>()
-                .eq(ChatMessage::getUserId, userId)
-                .orderByAsc(ChatMessage::getCreatedAt);
-        if (treeId != null) {
-            wrapper.eq(ChatMessage::getTreeId, treeId);
-        }
-        List<ChatMessage> messages = chatMessageMapper.selectList(wrapper);
-
-        List<ChatHistoryResp.ChatMsg> list = messages.stream().map(m -> {
-            ChatHistoryResp.ChatMsg msg = new ChatHistoryResp.ChatMsg();
-            msg.setRole(m.getRole());
-            msg.setContent(m.getContent());
-            msg.setCreatedAt(m.getCreatedAt());
-            return msg;
-        }).toList();
-
-        resp.setList(list);
-        return resp;
+        Page<ChatMessage> page = new Page<>(req.getPage(), req.getPageSize());
+        QueryWrapper<ChatMessage> wrapper = new QueryWrapper<>();
+        wrapper.eq("tree_id", req.getTreeId());
+        wrapper.eq("user_id", userId);
+        wrapper.orderByDesc("created_at");
+        return chatMessageMapper.getChatHistoryPage(page, wrapper);
     }
 
-    private void saveMessage(String userId, Integer treeId, String role, String content) {
+    private void saveMessage(String userId, Integer treeId, String role, String content, String type) {
         ChatMessage msg = new ChatMessage();
         msg.setUserId(userId);
         msg.setTreeId(treeId);
         msg.setRole(role);
         msg.setContent(content);
+        msg.setType(type);
         chatMessageMapper.insert(msg);
     }
 
     private String getUserId() {
-        try {
-            return CurrentUtils.getCurrentUserId();
-        } catch (Exception e) {
-            return "anonymous";
-        }
+        return CurrentUtils.getCurrentUserId();
     }
 }

+ 6 - 4
wishing-tree-h5/src/api/chat.ts

@@ -3,6 +3,7 @@ import { request } from './request'
 export interface ChatMsg {
   role: 'user' | 'ai'
   content: string
+  type?: string
   createdAt: string
 }
 
@@ -14,8 +15,9 @@ export async function sendMessage(message: string, conversationId: string, treeI
   return res.data.reply
 }
 
-export async function fetchHistory(treeId?: number): Promise<ChatMsg[]> {
-  const query = treeId != null ? `?treeId=${treeId}` : ''
-  const res = await request<{ list: ChatMsg[] }>(`/mobile/chat/history${query}`)
-  return res.data.list || []
+export async function fetchHistory(treeId: number, page = 1, pageSize = 10): Promise<{ records: ChatMsg[]; total: number }> {
+  const res = await request<{ records: ChatMsg[]; total: number }>(
+    `/mobile/chat/history?treeId=${treeId}&page=${page}&pageSize=${pageSize}`,
+  )
+  return { records: res.data.records || [], total: res.data.total || 0 }
 }

+ 39 - 6
wishing-tree-h5/src/components/ChatWidget.vue

@@ -19,6 +19,11 @@
       </div>
 
       <div class="chat-body" ref="msgBody">
+        <!-- 加载更多历史记录 -->
+        <div v-if="!historyFinished && historyLoaded" class="load-more" @click="loadHistory">
+          <span v-if="historyLoading">加载中...</span>
+          <span v-else>点击加载更多历史</span>
+        </div>
         <div v-if="messages.length === 0" class="chat-empty">
           <p>👋 你好,我是许愿树小助手"小愿"</p>
           <p>有什么关于许愿的问题都可以问我~</p>
@@ -28,7 +33,10 @@
           :key="i"
           :class="['chat-msg', msg.role, { 'has-wish': msg.wish }]"
         >
-          <div class="chat-bubble">{{ msg.content }}</div>
+          <div v-if="msg.type === 'image'" class="chat-bubble chat-bubble-image">
+            <van-image :src="msg.content" fit="cover" radius="8" style="max-width:200px" />
+          </div>
+          <div v-else class="chat-bubble">{{ msg.content }}</div>
           <!-- AI 许愿建议卡 -->
           <div v-if="msg.wish" class="wish-card">
             <div class="wish-card-header">✨ 愿望概览</div>
@@ -98,6 +106,7 @@ interface WishSuggestion {
 interface ChatMsg {
   role: 'user' | 'ai'
   content: string
+  type?: string
   wish?: WishSuggestion
   wishSubmitting?: boolean
 }
@@ -111,6 +120,9 @@ const sending = ref(false)
 const messages = ref<ChatMsg[]>([])
 const msgBody = ref<HTMLElement | null>(null)
 const historyLoaded = ref(false)
+const historyLoading = ref(false)
+const historyFinished = ref(false)
+let historyPage = 1
 const conversationId = ref(crypto.randomUUID())
 
 /** 从 AI 回复中提取 [WISH]...[/WISH] 标记,兼容各种格式变体 */
@@ -139,15 +151,24 @@ function parseWish(content: string): { text: string; wish: WishSuggestion | null
 }
 
 async function loadHistory() {
+  if (!props.treeId || historyLoading.value || historyFinished.value) return
+  historyLoading.value = true
   try {
-    const list = await fetchHistory(props.treeId)
-    if (list.length > 0) {
-      messages.value = list.map((m) => ({ role: m.role, content: m.content }))
+    const { records, total } = await fetchHistory(props.treeId, historyPage, 15)
+    if (records.length > 0) {
+      // 后端返回 DESC(最新在前),反转后插入消息列表开头
+      const oldMessages = messages.value
+      const historyMsgs = records.reverse().map((m) => ({ role: m.role, content: m.content, type: m.type }))
+      messages.value = [...historyMsgs, ...oldMessages]
     }
+    historyFinished.value = records.length < 15 || messages.value.length >= total
+    historyPage++
   } catch {
-    // 加载失败不影响使用
+    historyFinished.value = true
+  } finally {
+    historyLoading.value = false
+    historyLoaded.value = true
   }
-  historyLoaded.value = true
 }
 
 function toggleChat() {
@@ -306,6 +327,13 @@ watch(showChat, (val) => {
   gap: 10px;
 }
 
+.load-more {
+  text-align: center;
+  color: #999;
+  font-size: 12px;
+  padding: 8px;
+  cursor: pointer;
+}
 .chat-empty {
   text-align: center;
   color: #999;
@@ -356,6 +384,11 @@ watch(showChat, (val) => {
   color: #999;
   padding: 10px 18px;
 }
+.chat-bubble-image {
+  padding: 4px !important;
+  background: transparent !important;
+  box-shadow: none !important;
+}
 
 .chat-footer {
   display: flex;

BIN
wishing-tree-h5/wish.zip