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 渲染、代码高亮、对话导出等高级特性。

相关推荐
莫的感情30 分钟前
下载按钮点击一次却下载两个文件问题
前端
一个很帅的帅哥33 分钟前
JavaScript事件循环
开发语言·前端·javascript
摇滚侠34 分钟前
Spring Boot 3零基础教程,WEB 开发 自定义静态资源目录 笔记31
spring boot·笔记·后端·spring
Anthony_492636 分钟前
逻辑清晰地梳理Golang Context
后端·go
小宁爱Python38 分钟前
Django Web 开发系列(二):视图进阶、快捷函数与请求响应处理
前端·django·sqlite
fox_38 分钟前
深入理解React中的不可变性:原理、价值与实践
前端·react.js
武天40 分钟前
Vue项目中有封装过axios吗?怎么封装的?
vue.js
Github项目推荐40 分钟前
你的错误处理一团糟-是时候修复它了-🛠️
前端·后端
进击的圆儿44 分钟前
高并发内存池项目开发记录01
后端
左灯右行的爱情1 小时前
4-Spring SPI机制解读
java·后端·spring