基于Spring Boot + Vue 项目中引入deepseek方法
准备工作
在开始调用 DeepSeek API 之前,你需要完成以下准备工作:
1.访问 DeepSeek 官网,注册一个账号。
2.获取 API 密钥:登录 DeepSeek 平台,进入 API 管理 页面。创建一个新的 API 密钥(API Key),并妥善保存。
3.阅读 API 文档:
访问 DeepSeek 的 API 文档,了解支持的 API 端点、请求参数和返回格式
开始调用
1.前端只需要简单的渲染页面渲染,将所需问题传递到后端,页面效果:
代码:
<template><div class="total"><div class="chat-container"><div class="chat-header"><h1>快来和我聊天吧~~~</h1></div><div class="chat-messages" ref="messagesContainer"><divv-for="(message, index) in messages":key="index":class="['chat-message', message.sender]"><div v-html="formatContent(message.content)"></div></div></div><div class="chat-input"><inputtype="text"v-model="inputText"placeholder="输入消息..."@keydown.enter="sendMessage":disabled="isLoading"/><button @click="sendMessage" :disabled="!canSend"><svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /></svg>发送</button></div></div></div>
</template><script>
import { getToken } from "@/utils/storage.js";
const token = getToken();export default {data() {return {inputText: "",messages: [],isLoading: false,};},computed: {canSend() {return this.inputText.trim() && !this.isLoading;},},methods: {formatContent(text) {// 基础Markdown转换return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>").replace(/\*(.*?)\*/g, "<em>$1</em>").replace(/`(.*?)`/g, "<code>$1</code>");},async sendMessage() {if (!this.canSend) return;const question = this.inputText.trim();this.inputText = "";// 添加用户消息this.messages.push({sender: "user",content: question,});// 添加初始bot消息const botMessage = {sender: "bot",content: "",};this.messages.push(botMessage);this.scrollToBottom();try {this.isLoading = true;const response = await fetch("http://localhost:21090/api/online-travel-sys/v1.0/deepSeek/chat",{method: "POST",headers: {"Content-Type": "application/json",token: token,},body: JSON.stringify({ question }),});if (!response.ok || !response.body) {throw new Error("请求失败");}const reader = response.body.getReader();const decoder = new TextDecoder();let buffer = "";while (true) {const { done, value } = await reader.read();if (done) break;buffer += decoder.decode(value, { stream: true });// 处理SSE格式数据let position;while ((position = buffer.indexOf("\n\n")) >= 0) {const chunk = buffer.slice(0, position);buffer = buffer.slice(position + 2);const event = this.parseSSEEvent(chunk);if (event && event.data) {if (event.data === "[DONE]") {break; // 流结束}botMessage.content += event.data;this.scrollToBottom();}}}} catch (error) {botMessage.content = "请求出错,请稍后重试";} finally {this.isLoading = false;this.scrollToBottom();}},parseSSEEvent(chunk) {const lines = chunk.split("\n");const event = {};lines.forEach((line) => {if (line.startsWith("data:")) {event.data = line.replace(/^data:\s*/, "");}});return event;},scrollToBottom() {this.$nextTick(() => {const container = this.$refs.messagesContainer;if (container) {container.scrollTop = container.scrollHeight;}});},},
};
</script><style scoped>
/* 新增的样式 */
/* body 样式 */
html,
body {margin: 0;padding: 0;height: 100%; /* 确保高度占满整个视口 */
}.total {width: 100%;height: 100%; /* 继承父容器的高度 */display: flex;justify-content: center;align-items: center;background-image: url("../../assets/img/seek.jpg");background-size: cover;background-position: center;
}
.chat-container {width: 100%;max-width: 800px;height: 75vh;background: rgba(255, 255, 255, 0.5);border-radius: 20px;box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);backdrop-filter: blur(10px);display: flex;flex-direction: column;overflow: hidden;margin: auto; /* 水平居中 */margin-top: 95px;margin-bottom: 20px;
}
.chat-header {padding: 24px;background: linear-gradient(135deg, #497bf1 0%, #4874ed 100%);color: white;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}.chat-header h1 {margin: 0;font-size: 17px;font-weight: 400;letter-spacing: -0.5px;
}.chat-messages {flex: 1;padding: 20px;overflow-y: auto;display: flex;flex-direction: column;gap: 12px;
}.chat-message {max-width: 75%;padding: 16px 20px;border-radius: 20px;line-height: 1.5;animation: messageAppear 0.3s ease-out;position: relative;word-break: break-word;
}.chat-message.user {background: linear-gradient(135deg, #5b8cff 0%, #3d6ef7 100%);color: white;align-self: flex-end;border-bottom-right-radius: 4px;box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}.chat-message.bot {background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%);color: #2d3748;align-self: flex-start;border-bottom-left-radius: 4px;box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}.chat-input {padding: 20px;background: rgba(255, 255, 255, 0.9);border-top: 1px solid rgba(0, 0, 0, 0.05);display: flex;gap: 12px;
}.chat-input input {flex: 1;padding: 14px 20px;border: 2px solid rgba(0, 0, 0, 0.1);border-radius: 16px;font-size: 1rem;transition: all 0.2s ease;background: rgba(255, 255, 255, 0.8);
}.chat-input input:focus {outline: none;border-color: #5b8cff;box-shadow: 0 0 0 3px rgba(91, 140, 255, 0.2);
}.chat-input button {padding: 12px 24px;border: none;border-radius: 16px;background: #5b8cff;color: white;font-size: 1rem;font-weight: 500;cursor: pointer;transition: all 0.2s ease;display: flex;align-items: center;gap: 8px;
}.chat-input button:hover:not(:disabled) {background: #406cff;transform: translateY(-1px);
}.chat-input button:disabled {background: #c2d1ff;cursor: not-allowed;transform: none;
}.chat-input button svg {width: 18px;height: 18px;fill: currentColor;
}.typing-indicator {display: inline-flex;gap: 6px;padding: 12px 20px;background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%);border-radius: 20px;
}.typing-dot {width: 8px;height: 8px;background: rgba(0, 0, 0, 0.3);border-radius: 50%;animation: typing 1.4s infinite ease-in-out;
}.typing-dot:nth-child(2) {animation-delay: 0.2s;
}.typing-dot:nth-child(3) {animation-delay: 0.4s;
}@keyframes messageAppear {from {opacity: 0;transform: translateY(10px);}to {opacity: 1;transform: translateY(0);}
}@keyframes typing {0%,100% {transform: translateY(0);}50% {transform: translateY(-4px);}
}@media (max-width: 640px) {.chat-container {height: 95vh;border-radius: 16px;}.chat-message {max-width: 85%;}
}/* Markdown内容样式 */
.chat-message :deep(pre) {background: rgba(0, 0, 0, 0.05);padding: 12px;border-radius: 8px;overflow-x: auto;margin: 8px 0;
}.chat-message :deep(code) {font-family: monospace;background: rgba(0, 0, 0, 0.08);padding: 2px 4px;border-radius: 4px;
}.chat-message :deep(strong) {font-weight: 600;
}.chat-message :deep(em) {font-style: italic;
}.chat-message :deep(blockquote) {border-left: 3px solid #5b8cff;margin: 8px 0;padding-left: 12px;color: #666;
}.chat-message :deep(a) {color: #5b8cff;text-decoration: none;border-bottom: 1px solid transparent;transition: all 0.2s;
}.chat-message :deep(a:hover) {border-bottom-color: currentColor;
}
</style>
后端,这里并没有将历史记录保存到数据库,如果有需要根据实际情况自行添加:
1.在yml文件中添加相关配置
ds:key: 填写在官方自己申请的key,需要一定费用但很少url: https://api.deepseek.com/chat/completions
2.控制层:
package cn.kmbeast.controller;import cn.kmbeast.service.DsChatService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;import javax.annotation.Resource;/*** DsController* @author senfel* @version 1.0* @date 2025/3/13 17:21*/
@RestController
@RequestMapping("/deepSeek")
@Slf4j
public class DsController {@Resourceprivate DsChatService dsChatService;/*** chat page* @param modelAndView* @author senfel* @date 2025/3/13 17:39* @return org.springframework.web.servlet.ModelAndView*/@GetMapping()public ModelAndView chat(ModelAndView modelAndView) {modelAndView.setViewName("chat");return modelAndView;}/*** chat* @param question* @author senfel* @date 2025/3/13 17:39* @return org.springframework.web.servlet.mvc.method.annotation.SseEmitter*/@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public SseEmitter chat(@RequestBody String question) {//TODO 默认用户ID,实际场景从token获取String userId = "senfel";return dsChatService.chat(userId, question);}
}
3.service层:
package cn.kmbeast.service;import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;/*** DsChatService* @author senfel* @version 1.0* @date 2025/4/13 17:30*/
public interface DsChatService {/*** chat* @param userId* @param question* @author senfel* @date 2025/3/13 17:30* @return org.springframework.web.servlet.mvc.method.annotation.SseEmitter*/SseEmitter chat(String userId, String question);
}
4.service实现:
package cn.kmbeast.service.impl;import cn.kmbeast.service.DsChatService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** DsChatServiceImpl* @author senfel* @version 1.0* @date 2025/3/13 17:31*/
@Service
@Slf4j
public class DsChatServiceImpl implements DsChatService {@Value("${ds.key}")private String dsKey;@Value("${ds.url}")private String dsUrl;// 用于保存每个用户的对话历史private final Map<Object, ArrayList<Map<String, String>>> sessionHistory = new ConcurrentHashMap<Object, ArrayList<Map<String, String>>>();private final ExecutorService executorService = Executors.newCachedThreadPool();private final ObjectMapper objectMapper = new ObjectMapper();/*** chat* @param userId* @param question* @author senfel* @date 2025/3/13 17:36* @return org.springframework.web.servlet.mvc.method.annotation.SseEmitter*/@Overridepublic SseEmitter chat(String userId,String question) {SseEmitter emitter = new SseEmitter(-1L);executorService.execute(() -> {try {log.info("流式回答开始, 问题: {}", question);// 获取当前用户的对话历史ArrayList<Map<String, String>> messages = sessionHistory.getOrDefault(userId, new ArrayList<Map<String, String>>());// 添加用户的新问题到对话历史Map<String, String> userMessage = new HashMap<>();userMessage.put("role", "user");userMessage.put("content", question);Map<String, String> systemMessage = new HashMap<>();systemMessage.put("role", "system");systemMessage.put("content", "senfel的AI助手");messages.add(userMessage);messages.add(systemMessage);// 调用 DeepSeek APItry (CloseableHttpClient client = HttpClients.createDefault()) {HttpPost request = new HttpPost(dsUrl);request.setHeader("Content-Type", "application/json");request.setHeader("Authorization", "Bearer " + dsKey);Map<String, Object> requestMap = new HashMap<>();requestMap.put("model", "deepseek-chat");requestMap.put("messages", messages);requestMap.put("stream", true);String requestBody = objectMapper.writeValueAsString(requestMap);request.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));try (CloseableHttpResponse response = client.execute(request);BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))) {StringBuilder aiResponse = new StringBuilder();String line;while ((line = reader.readLine()) != null) {if (line.startsWith("data: ")) {System.err.println(line);String jsonData = line.substring(6);if ("[DONE]".equals(jsonData)) {break;}JsonNode node = objectMapper.readTree(jsonData);String content = node.path("choices").path(0).path("delta").path("content").asText("");if (!content.isEmpty()) {emitter.send(content);aiResponse.append(content); // 收集 AI 的回复}}}// 将 AI 的回复添加到对话历史Map<String, String> aiMessage = new HashMap<>();aiMessage.put("role", "assistant");aiMessage.put("content", aiResponse.toString());messages.add(aiMessage);// 更新会话状态sessionHistory.put(userId, messages);log.info("流式回答结束, 问题: {}", question);emitter.complete();}} catch (Exception e) {log.error("处理 DeepSeek 请求时发生错误", e);emitter.completeWithError(e);}} catch (Exception e) {log.error("处理 DeepSeek 请求时发生错误", e);emitter.completeWithError(e);}});return emitter;}
}
最终效果: