基于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">
        <div
          v-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">
        <input
          type="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, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .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 {
    @Resource
    private 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
     */
    @Override
    public 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 API
                try (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;
    }
}

最终效果:

相关推荐
柴薪之王、睥睨众生1 分钟前
(自用)Java学习-5.15(模糊搜索,收藏,购物车)
java·开发语言·spring boot·学习
全职计算机毕业设计9 分钟前
基于SpringBoot的美食分享平台设计与开发(Vue MySQL)
vue.js·spring boot·美食
ZhuAiQuan38 分钟前
[Vue]浅浅了解vue3响应式的基本原理
前端·javascript·vue.js
有很多梦想要实现1 小时前
Nest全栈到失业(一):Nest基础知识扫盲
前端·javascript·后端·typescript·webstorm
在未来等你1 小时前
互联网大厂Java求职面试:AI与大模型应用集成中的架构难题与解决方案
spring boot·graalvm·java面试·大模型集成·微服务设计·ai架构·rag系统
程序猿熊跃晖2 小时前
如何在Vue中实现延迟刷新列表:以Element UI的el-switch为例
javascript·vue.js·ui
不争先.2 小时前
Pycharm and Flask 的学习心得(10)重定向
后端·python·flask
zeijiershuai2 小时前
springBoot项目测试时浏览器返回406问题解决方案
java·spring boot·后端
知还2152 小时前
在springboot,禁止查询数据库种的某字段
java·spring boot·spring
ALex_zry2 小时前
Protocol Buffers 复杂嵌套编译指南:生成 C++ 代码
linux·服务器·开发语言·c++·后端