操作步骤:
1.安装部署ollama
2.安装deepseek
3.vue代码
4.springboot Java代码,sse接口流式传输
成果展示:
1.java代码:
package com.qeoten.sms.voluntarilyReport.controller;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.RequestBody;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
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.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/deepseek")
@Slf4j
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class TestController {
private final String API_URL = "http://127.0.0.1:11434/api/chat";
@GetMapping(value = "/sseChat", produces = "text/event-stream;charset=UTF-8")
public SseEmitter chat(@RequestParam String message, HttpServletResponse response) {
SseEmitter emitter = new SseEmitter(60_000L);
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
response.setContentType("text/event-stream;charset=UTF-8");
Executors.newSingleThreadExecutor().submit(() -> {
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpPost post = new HttpPost(API_URL);
post.setHeader("Content-Type", "application/json");
post.setHeader("Accept", "text/event-stream");
String jsonBody = String.format(
"{\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]" +
",\"model\":\"deepseek-r1:1.5b\"" +
",\"stream\":true}",
message
);
post.setEntity(new StringEntity(jsonBody));
try (CloseableHttpResponse apiResponse = client.execute(post);
BufferedReader reader = new BufferedReader(
new InputStreamReader(apiResponse.getEntity().getContent()))) {
String line;
while ((line = reader.readLine()) != null) {
// 按SSE格式包装数据:{content}\n\n
System.out.println("line: " + line);
String sseFormatted = "" + line + "\n\n";
emitter.send(SseEmitter.event().data(sseFormatted,
MediaType.parseMediaType("text/plain;charset=UTF-8"))); // 指定媒体类型和编码
// emitter.send(SseEmitter.event().data(sseFormatted, MediaType.TEXT_PLAIN));
}
emitter.complete();
}
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
2.vue代码:
<template>
<div class="chat-container">
<!-- 对话框主体 -->
<div class="chat-box">
<div class="chat-header">Deepseek智能助手</div>
<div class="chat-content" ref="chatContent">
<div v-for="(msg, index) in messages" :key="index"
:class="['message', msg.role]">
<div class="bubble">{{ msg.content }}</div>
<div class="time">{{ msg.time }}</div>
</div>
</div>
<div class="chat-input">
<textarea v-model="inputText" @keyup.enter.exact="sendMessage"
placeholder="输入您的问题..."></textarea>
<button @click="sendMessage" :disabled="isLoading">
{{ isLoading ? '思考中...' : '发送' }}
</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
messages: [],
inputText: '',
isLoading: false,
eventSource: null
}
},
methods: {
async sendMessage() {
if (!this.inputText.trim() || this.isLoading) return
const question = this.inputText.trim()
this.messages.push({
role: 'user',
content: question,
time: this.getCurrentTime()
})
this.inputText = ''
this.isLoading = true
// 流式请求处理
try {
// this.eventSource = new EventSource(`http://xxxxx:8090/api/sms-info/deepseek/sseChat?message=${encodeURIComponent(question)}`);
this.eventSource = new EventSource(`http://localhost:10006/deepseek/sseChat?message=${encodeURIComponent(question)}`);
let aiResponse = { role: 'ai', content: '', time: this.getCurrentTime() }
this.messages.push(aiResponse)
this.eventSource.onmessage = (event) => {
console.log('服务器回复:', event.data)
let data = JSON.parse(event.data)
if (data.done) {
console.log('回答完成')
// 提示回答完成
this.$message({
type: 'success',
message: '回答完成!'
})
this.eventSource.close()
this.isLoading = false
return
}
aiResponse.content += data.message.content
this.$nextTick(() => {
this.scrollToBottom()
})
}
this.eventSource.onerror = () => {
this.isLoading = false
this.eventSource.close()
}
} catch (error) {
console.error('请求失败:', error)
this.isLoading = false
}
},
getCurrentTime() {
return new Date().toLocaleTimeString()
},
scrollToBottom() {
this.$refs.chatContent.scrollTop = this.$refs.chatContent.scrollHeight
}
},
beforeDestroy() {
if (this.eventSource) {
this.eventSource.close()
}
}
}
</script>
<style scoped>
.chat-container {
width: 100%;
max-width: 50vw;
margin: 50px auto;
background: linear-gradient(145deg, #e6e6e6, #ffffff);
border-radius: 20px;
box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.1), -10px -10px 20px rgba(255, 255, 255, 0.7);
transition: all 0.3s ease;
}
.chat-container:hover {
box-shadow: 15px 15px 30px rgba(0, 0, 0, 0.1), -15px -15px 30px rgba(255, 255, 255, 0.8);
}
.chat-box {
display: flex;
flex-direction: column;
height: 700px;
padding: 30px;
font-family: 'Helvetica Neue', Arial, sans-serif;
}
.chat-header {
text-align: center;
padding-bottom: 20px;
color: #333;
font-size: 1.8em;
font-weight: bold;
letter-spacing: 1.5px;
}
.chat-content {
flex: 1;
overflow-y: auto;
padding-right: 10px;
scroll-behavior: smooth;
}
.message {
margin: 20px 0;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.message.ai {
align-items: flex-start;
}
.bubble {
padding: 20px;
border-radius: 25px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
word-break: break-word;
transition: transform 0.3s cubic-bezier(.25,.8,.25,1), box-shadow 0.3s cubic-bezier(.25,.8,.25,1);
}
.bubble:hover {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
transform: translateY(-4px);
}
.message.ai .bubble {
background-color: #f9f9f9;
color: #333;
}
.message.user .bubble {
background-color: #007bff;
color: white;
}
.time {
font-size: 0.85em;
color: #a0a0a0;
margin-top: 10px;
opacity: 0.8;
transition: color 0.3s;
}
.time:hover {
color: #777;
}
.chat-input {
display: flex;
gap: 20px;
padding-top: 20px;
}
textarea {
flex: 1;
padding: 18px;
border: none;
border-radius: 15px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
font-size: 1em;
transition: box-shadow 0.3s;
}
textarea:focus {
outline: none;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
}
button {
padding: 18px 30px;
background-color: #007bff;
color: white;
border: none;
border-radius: 15px;
cursor: pointer;
transition: background-color 0.3s, transform 0.1s;
font-size: 1em;
font-weight: bold;
letter-spacing: 1px;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
button:active:not(:disabled) {
background-color: #004080;
}
</style>
3.本地部署ollama
解压ollama压缩包至某个目录
sudo tar -xzf ollama-linux-arm64.tgz -C /home/ollama
4.将ollama作为service(.service文件)
指定ollama /bin目录
[Unit]
Description=Ollama Service
After=network-online.target
[Service]
ExecStart=/home/ollama/bin/ollama serve
User=root
Group=root
Restart=always
RestartSec=3
Environment="OLLAMA_HOST=0.0.0.0"
Environment="OLLAMA_ORIGINS=*"
[Install]
WantedBy=default.target
将ollama设定为service服务,开机自启动,重启ollama
4.安装deepseek
sudo systemctl daemon-reload
sudo systemctl enable ollama
sudo systemctl start ollama
查看状态就是 status 停服务就是 stop