准备工作
在开始调用 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, "&")
.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 {
@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;
}
}
最终效果: