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

相关推荐
用户841794814562 分钟前
vxe-gantt table 甘特图如何设置任务视图每一行的背景色
vue.js
前端老宋Running3 分钟前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔4 分钟前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654266 分钟前
Android的自定义View
前端
WILLF6 分钟前
HTML iframe 标签
前端·javascript
用户638982245897 分钟前
使用Hutool的ExcelWriter导出复杂模板,支持下拉选项级联筛选
后端
程序员鱼皮9 分钟前
10个免费的网站分析工具,竟然比付费的更香?
后端·程序员·数据分析
码一行17 分钟前
Eino AI 实战: Eino 的文档加载与解析
后端·go
码一行17 分钟前
Eino AI 实战:DuckDuckGo 搜索工具 V1 与 V2
后端·go
未秃头的程序猿18 分钟前
🚀 设计模式在复杂支付系统中的应用:策略+工厂+模板方法模式实战
后端·设计模式