Spring Boot + Vue 实现 DeepSeek 对话效果详细步骤

Spring Boot + Vue 实现 DeepSeek 对话效果详细步骤

一、整体架构设计

我们需要构建一个前后端分离的应用:

  • ​后端​:Spring Boot 提供 API 接口,处理与 AI 模型的交互
  • ​前端​:Vue 实现聊天界面,展示对话内容并发送用户输入

二、后端实现 (Spring Boot)

1. 创建 Spring Boot 项目

xml 复制代码
<!-- pom.xml 主要依赖 -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- 如果需要持久化存储 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

2. 设计 API 接口

less 复制代码
// 对话控制器
@RestController
@RequestMapping("/api/chat")
public class ChatController {
    
    @Autowired
    private ChatService chatService;
    
    @PostMapping("/message")
    public ResponseEntity<ChatResponse> sendMessage(@RequestBody ChatRequest request) {
        ChatResponse response = chatService.processMessage(request);
        return ResponseEntity.ok(response);
    }
    
    @GetMapping("/history/{sessionId}")
    public ResponseEntity<List<ChatMessage>> getHistory(@PathVariable String sessionId) {
        List<ChatMessage> history = chatService.getChatHistory(sessionId);
        return ResponseEntity.ok(history);
    }
}

3. 定义数据传输对象

arduino 复制代码
public class ChatRequest {
    private String sessionId;
    private String message;
    // getter, setter
}

public class ChatResponse {
    private String message;
    private String sessionId;
    // getter, setter
}

public class ChatMessage {
    private String role; // "user" 或 "assistant"
    private String content;
    // getter, setter
}

4. 实现对话服务

typescript 复制代码
@Service
public class ChatService {
    
    // 可以使用 Map 临时存储会话,生产环境建议使用数据库
    private Map<String, List<ChatMessage>> chatSessions = new ConcurrentHashMap<>();
    
    // 处理用户消息
    public ChatResponse processMessage(ChatRequest request) {
        String sessionId = request.getSessionId();
        if (sessionId == null || sessionId.isEmpty()) {
            sessionId = UUID.randomUUID().toString();
        }
        
        String userMessage = request.getMessage();
        
        // 保存用户消息
        ChatMessage userMsg = new ChatMessage("user", userMessage);
        
        // 调用 AI 模型 API
        String aiResponse = callAIApi(userMessage, sessionId);
        
        // 保存 AI 回复
        ChatMessage aiMsg = new ChatMessage("assistant", aiResponse);
        
        // 更新会话历史
        chatSessions.computeIfAbsent(sessionId, k -> new ArrayList<>()).add(userMsg);
        chatSessions.get(sessionId).add(aiMsg);
        
        return new ChatResponse(aiResponse, sessionId);
    }
    
    // 获取聊天历史
    public List<ChatMessage> getChatHistory(String sessionId) {
        return chatSessions.getOrDefault(sessionId, Collections.emptyList());
    }
    
    // 调用 AI 模型 API 的方法
    private String callAIApi(String message, String sessionId) {
        // 这里实现与 DeepSeek API 的实际交互
        // 可以使用 RestTemplate 或 WebClient
        // 示例代码:
        try {
            // 实际开发中替换为真实 API 调用
            return "这是 AI 对 "" + message + "" 的回复";
        } catch (Exception e) {
            return "抱歉,我遇到了一些问题,请稍后再试。";
        }
    }
}

5. 配置 CORS

typescript 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:5173") // Vue 默认端口
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true);
    }
}

三、前端实现 (Vue)

1. 创建 Vue 项目

perl 复制代码
# 使用 npm
npm create vue@latest my-chat-app
cd my-chat-app
npm install

# 安装必要的依赖
npm install axios

2. 创建聊天组件

xml 复制代码
<!-- ChatWindow.vue -->
<template>
  <div class="chat-container">
    <div class="chat-header">
      <h2>AI 助手对话</h2>
    </div>
    
    <div class="chat-messages" ref="messageContainer">
      <div v-for="(msg, index) in messages" :key="index" 
           :class="['message', msg.role === 'user' ? 'user-message' : 'assistant-message']">
        <div class="message-content">
          {{ msg.content }}
        </div>
      </div>
      <div v-if="loading" class="message assistant-message">
        <div class="message-content">
          <span class="loading-dots">思考中<span>.</span><span>.</span><span>.</span></span>
        </div>
      </div>
    </div>
    
    <div class="chat-input">
      <textarea 
        v-model="userInput" 
        placeholder="请输入您的问题..." 
        @keyup.enter.ctrl="sendMessage"
      ></textarea>
      <button @click="sendMessage" :disabled="loading || !userInput.trim()">
        发送
      </button>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'ChatWindow',
  data() {
    return {
      messages: [],
      userInput: '',
      loading: false,
      sessionId: this.generateSessionId(),
      apiBaseUrl: 'http://localhost:8080/api/chat'
    };
  },
  methods: {
    generateSessionId() {
      return Math.random().toString(36).substring(2, 15);
    },
    async sendMessage() {
      if (!this.userInput.trim() || this.loading) return;
      
      const userMessage = this.userInput.trim();
      this.messages.push({ role: 'user', content: userMessage });
      this.userInput = '';
      this.loading = true;
      
      // 滚动到底部
      this.$nextTick(() => {
        this.scrollToBottom();
      });
      
      try {
        const response = await axios.post(`${this.apiBaseUrl}/message`, {
          sessionId: this.sessionId,
          message: userMessage
        });
        
        this.messages.push({ role: 'assistant', content: response.data.message });
        this.sessionId = response.data.sessionId;
      } catch (error) {
        console.error('Error sending message:', error);
        this.messages.push({ 
          role: 'assistant', 
          content: '抱歉,发生了错误,请稍后再试。' 
        });
      } finally {
        this.loading = false;
        this.$nextTick(() => {
          this.scrollToBottom();
        });
      }
    },
    scrollToBottom() {
      if (this.$refs.messageContainer) {
        this.$refs.messageContainer.scrollTop = this.$refs.messageContainer.scrollHeight;
      }
    },
    loadHistory() {
      axios.get(`${this.apiBaseUrl}/history/${this.sessionId}`)
        .then(response => {
          this.messages = response.data;
          this.$nextTick(() => {
            this.scrollToBottom();
          });
        })
        .catch(error => {
          console.error('Error loading history:', error);
        });
    }
  },
  mounted() {
    // 在实际应用中,可以从 URL 参数或本地存储获取 sessionId
    // this.loadHistory();
  }
}
</script>

<style scoped>
.chat-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  max-width: 800px;
  margin: 0 auto;
  padding: 1rem;
}

.chat-header {
  text-align: center;
  padding: 1rem 0;
  border-bottom: 1px solid #eee;
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.message {
  max-width: 75%;
  padding: 0.75rem;
  border-radius: 0.5rem;
  margin-bottom: 0.5rem;
}

.user-message {
  align-self: flex-end;
  background-color: #e1f5fe;
}

.assistant-message {
  align-self: flex-start;
  background-color: #f5f5f5;
}

.chat-input {
  display: flex;
  padding: 1rem 0;
  gap: 0.5rem;
}

.chat-input textarea {
  flex: 1;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 0.5rem;
  resize: none;
  min-height: 60px;
}

.chat-input button {
  padding: 0.75rem 1.5rem;
  background-color: #2196f3;
  color: white;
  border: none;
  border-radius: 0.5rem;
  cursor: pointer;
}

.chat-input button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.loading-dots span {
  animation: loading 1.4s infinite;
  display: inline-block;
}

.loading-dots span:nth-child(2) {
  animation-delay: 0.2s;
}

.loading-dots span:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes loading {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 1; }
}
</style>

3. 在主应用中使用聊天组件

xml 复制代码
<!-- App.vue -->
<template>
  <div class="app">
    <ChatWindow />
  </div>
</template>

<script>
import ChatWindow from './components/ChatWindow.vue'

export default {
  name: 'App',
  components: {
    ChatWindow
  }
}
</script>

<style>
body {
  margin: 0;
  padding: 0;
  font-family: Arial, sans-serif;
}

.app {
  width: 100%;
  height: 100vh;
}
</style>

四、前后端集成与部署

1. 开发环境配置

  1. ​启动后端服务​

    arduino 复制代码
    ./mvnw spring-boot:run
  2. ​启动前端开发服务器​

    arduino 复制代码
    npm run dev

2. 生产环境部署

  1. ​构建前端应用​

    arduino 复制代码
    npm run build
  2. ​配置后端服务提供静态文件​

    java 复制代码
    // Spring Boot 配置类
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        
        @Value("${frontend.resources.path:${user.home}/my-chat-app/dist}")
        private Resource frontendResources;
        
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/**")
                    .addResourceLocations("file:" + frontendResources.getFile().getAbsolutePath() + "/")
                    .resourceChain(true)
                    .addResolver(new PathResourceResolver() {
                        @Override
                        protected Resource getResource(String resourcePath, Resource location) throws IOException {
                            Resource requestedResource = location.createRelative(resourcePath);
                            return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
                                    : new ClassPathResource("/static/index.html");
                        }
                    });
        }
    }
  3. ​将前端构建文件复制到后端资源目录​

五、进阶优化

1. WebSocket 实现实时通信

less 复制代码
// 后端 WebSocket 配置
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
               .setAllowedOriginPatterns("*")
               .withSockJS();
    }
}

// WebSocket 控制器
@Controller
public class WebSocketController {
    
    @Autowired
    private ChatService chatService;
    
    @MessageMapping("/chat.sendMessage")
    @SendToUser("/queue/reply")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage, Principal principal) {
        return chatService.processWebSocketMessage(chatMessage);
    }
}

2. 使用 Redis 缓存会话历史

less 复制代码
@Configuration
@EnableRedisHttpSession
public class SessionConfig {
    
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory();
    }
}

3. 前端连接 WebSocket

kotlin 复制代码
// 在 ChatWindow.vue 中添加 WebSocket 连接
export default {
  // ... 其他代码 ...
  data() {
    return {
      // ... 其他数据 ...
      stompClient: null
    };
  },
  methods: {
    // ... 其他方法 ...
    
    connectWebSocket() {
      const socket = new SockJS(this.apiBaseUrl.replace('/api', ''));
      this.stompClient = Stomp.over(socket);
      
      this.stompClient.connect({}, frame => {
        console.log('Connected: ' + frame);
        this.stompClient.subscribe(`/user/queue/reply`, response => {
          const receivedMessage = JSON.parse(response.body);
          this.messages.push(receivedMessage);
          this.$nextTick(() => {
            this.scrollToBottom();
          });
        });
      }, error => {
        console.error('WebSocket Error: ' + error);
        // 重连逻辑
        setTimeout(() => {
          this.connectWebSocket();
        }, 5000);
      });
    },
    
    disconnectWebSocket() {
      if (this.stompClient !== null) {
        this.stompClient.disconnect();
      }
    },
    
    sendWebSocketMessage() {
      if (!this.userInput.trim() || this.loading) return;
      
      const userMessage = this.userInput.trim();
      
      this.stompClient.send("/app/chat.sendMessage", {}, JSON.stringify({
        sessionId: this.sessionId,
        message: userMessage
      }));
      
      this.messages.push({ role: 'user', content: userMessage });
      this.userInput = '';
    }
  },
  mounted() {
    // ... 其他代码 ...
    this.connectWebSocket();
  },
  beforeUnmount() {
    this.disconnectWebSocket();
  }
}

总结

通过上述步骤,你可以实现一个基于 Spring Boot 和 Vue 的对话系统,类似 DeepSeek 的交互效果。这个实现包含了:

  1. 后端 API 设计与实现
  2. 前端聊天界面设计
  3. 会话管理与历史记录
  4. WebSocket 实现实时通信(可选)
  5. 部署配置

根据实际需求,你可以进一步扩展功能,如支持 Markdown 渲染、代码高亮、对话导出等高级特性。

相关推荐
LaoZhangAI几秒前
ComfyUI集成GPT-Image-1完全指南:8步实现AI图像创作革命【2025最新】
前端·后端
LaoZhangAI1 分钟前
Cline + Gemini API 完整配置与使用指南【2025最新】
前端·后端
Java&Develop6 分钟前
防止电脑息屏 html
前端·javascript·html
Maybyy9 分钟前
javaScript中数组常用的函数方法
开发语言·前端·javascript
国王不在家11 分钟前
组件-多行文本省略-展开收起
前端·javascript·html
夏兮颜☆12 分钟前
【electron】electron实现窗口的最大化、最小化、还原、关闭
前端·javascript·electron
LaoZhangAI13 分钟前
Cline + Claude API 完全指南:2025年智能编程最佳实践
前端·后端
LBY_XK14 分钟前
前端实现 web获取麦克风权限 录制音频 (需求:ai对话问答)
前端·音视频
小离a_a14 分钟前
vue实现el-table-column中自定义label
前端·javascript·vue.js