feat: 完成情绪博物馆项目重构和功能增强 - 新增日记评论和帖子功能 - 重构前端架构,优化用户体验 - 完善WebSocket通信机制 - 更新项目文档和部署配置
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
import { Client, IMessage } from '@stomp/stompjs'
|
||||
import SockJS from 'sockjs-client'
|
||||
import { envConfig } from '@/config/env'
|
||||
|
||||
// WebSocket消息类型
|
||||
export interface WebSocketMessage {
|
||||
messageId?: string
|
||||
conversationId?: string
|
||||
type: 'TEXT' | 'TYPING' | 'SYSTEM' | 'ERROR' | 'HEARTBEAT' | 'CONNECTION' | 'AI_THINKING'
|
||||
content: string
|
||||
senderId: string
|
||||
senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM'
|
||||
status?: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED'
|
||||
createTime?: string
|
||||
timestamp?: number
|
||||
data?: any
|
||||
}
|
||||
|
||||
// 聊天请求类型 - 完全匹配后端ChatRequest
|
||||
export interface ChatRequest {
|
||||
content: string
|
||||
senderId: string
|
||||
senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM'
|
||||
messageType: 'TEXT' | 'IMAGE' | 'FILE' | 'SYSTEM' | 'HEARTBEAT'
|
||||
conversationId?: string
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
// 连接状态
|
||||
export type ConnectionStatus = 'CONNECTING' | 'CONNECTED' | 'DISCONNECTED' | 'ERROR'
|
||||
|
||||
// 事件回调类型
|
||||
export interface WebSocketCallbacks {
|
||||
onMessage?: (message: WebSocketMessage) => void
|
||||
onConnect?: () => void
|
||||
onDisconnect?: () => void
|
||||
onError?: (error: any) => void
|
||||
onStatusChange?: (status: ConnectionStatus) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* STOMP WebSocket服务类
|
||||
* 使用STOMP协议与后端Spring WebSocket通信
|
||||
*/
|
||||
export class StompWebSocketService {
|
||||
private client: Client | null = null
|
||||
private callbacks: WebSocketCallbacks = {}
|
||||
private status: ConnectionStatus = 'DISCONNECTED'
|
||||
private reconnectAttempts = 0
|
||||
private maxReconnectAttempts = 5
|
||||
private reconnectInterval = 3000
|
||||
private userId: string | null = null
|
||||
private conversationId: string | null = null
|
||||
|
||||
constructor() {
|
||||
// 构建WebSocket URL
|
||||
const wsUrl = `${envConfig.apiBaseUrl}/ws/chat`
|
||||
console.log('STOMP WebSocket URL:', wsUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接WebSocket
|
||||
*/
|
||||
connect(userId?: string, callbacks?: WebSocketCallbacks): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.callbacks = { ...callbacks }
|
||||
|
||||
// 设置用户ID和类型
|
||||
if (userId) {
|
||||
this.userId = userId
|
||||
console.log('🔌 使用登录用户ID:', userId)
|
||||
} else {
|
||||
this.userId = `guest_${Date.now()}`
|
||||
console.log('🔌 使用访客ID:', this.userId)
|
||||
}
|
||||
|
||||
this.setStatus('CONNECTING')
|
||||
|
||||
// 创建STOMP客户端
|
||||
this.client = new Client({
|
||||
webSocketFactory: () => {
|
||||
const wsUrl = `${envConfig.apiBaseUrl}/ws/chat`
|
||||
return new SockJS(wsUrl)
|
||||
},
|
||||
|
||||
// 连接头信息
|
||||
connectHeaders: this.getConnectHeaders(),
|
||||
|
||||
// 调试信息
|
||||
debug: (str) => {
|
||||
console.log('STOMP Debug:', str)
|
||||
},
|
||||
|
||||
// 重连配置
|
||||
reconnectDelay: this.reconnectInterval,
|
||||
heartbeatIncoming: 4000,
|
||||
heartbeatOutgoing: 4000,
|
||||
})
|
||||
|
||||
// 连接成功回调
|
||||
this.client.onConnect = (frame) => {
|
||||
console.log('✅ STOMP WebSocket连接成功:', frame)
|
||||
this.setStatus('CONNECTED')
|
||||
this.reconnectAttempts = 0
|
||||
|
||||
// 订阅消息
|
||||
this.subscribeToMessages()
|
||||
|
||||
// 发送连接消息
|
||||
this.sendConnectMessage()
|
||||
|
||||
this.callbacks.onConnect?.()
|
||||
resolve()
|
||||
}
|
||||
|
||||
// 连接错误回调
|
||||
this.client.onStompError = (frame) => {
|
||||
console.error('❌ STOMP连接错误:', frame)
|
||||
this.setStatus('ERROR')
|
||||
this.callbacks.onError?.(frame)
|
||||
reject(new Error(`STOMP连接错误: ${frame.headers['message']}`))
|
||||
}
|
||||
|
||||
// WebSocket错误回调
|
||||
this.client.onWebSocketError = (error) => {
|
||||
console.error('❌ WebSocket错误:', error)
|
||||
this.setStatus('ERROR')
|
||||
this.callbacks.onError?.(error)
|
||||
}
|
||||
|
||||
// 连接关闭回调
|
||||
this.client.onWebSocketClose = (event) => {
|
||||
console.log('🔌 WebSocket连接关闭:', event)
|
||||
this.setStatus('DISCONNECTED')
|
||||
this.callbacks.onDisconnect?.()
|
||||
|
||||
// 自动重连
|
||||
if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// 激活连接
|
||||
this.client.activate()
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ STOMP连接异常:', error)
|
||||
this.setStatus('ERROR')
|
||||
this.callbacks.onError?.(error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.client) {
|
||||
this.client.deactivate()
|
||||
this.client = null
|
||||
}
|
||||
this.userId = null
|
||||
this.conversationId = null
|
||||
this.setStatus('DISCONNECTED')
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
*/
|
||||
sendChatMessage(content: string, conversationId?: string): void {
|
||||
if (!this.client || !this.client.connected) {
|
||||
const error = new Error('STOMP客户端未连接,无法发送消息')
|
||||
console.error('STOMP未连接')
|
||||
this.callbacks.onError?.({ userMessage: '连接已断开,请等待重连后再试', originalError: error })
|
||||
return
|
||||
}
|
||||
|
||||
if (!content.trim()) {
|
||||
const error = new Error('消息内容不能为空')
|
||||
this.callbacks.onError?.({ userMessage: '消息内容不能为空', originalError: error })
|
||||
return
|
||||
}
|
||||
|
||||
// 判断用户类型
|
||||
const isGuest = !this.userId || this.userId.startsWith('guest_')
|
||||
const senderType = isGuest ? 'GUEST' : 'USER'
|
||||
|
||||
console.log('📤 发送STOMP聊天消息,用户信息:', {
|
||||
userId: this.userId,
|
||||
senderType,
|
||||
isGuest,
|
||||
content: content.trim()
|
||||
})
|
||||
|
||||
// 创建聊天请求 - 匹配后端ChatRequest格式
|
||||
const chatRequest: ChatRequest = {
|
||||
content: content.trim(),
|
||||
senderId: this.userId!,
|
||||
senderType,
|
||||
messageType: 'TEXT',
|
||||
conversationId: conversationId || this.conversationId || undefined,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
console.log('📤 准备发送的聊天请求:', chatRequest)
|
||||
|
||||
try {
|
||||
// 发送到后端的/app/chat.send端点
|
||||
this.client.publish({
|
||||
destination: '/app/chat.send',
|
||||
body: JSON.stringify(chatRequest)
|
||||
})
|
||||
console.log('✅ STOMP聊天消息发送成功:', chatRequest)
|
||||
} catch (error) {
|
||||
console.error('❌ STOMP消息发送失败:', error)
|
||||
this.callbacks.onError?.({
|
||||
userMessage: '消息发送失败,请重试',
|
||||
originalError: error
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话ID
|
||||
*/
|
||||
setConversationId(conversationId: string): void {
|
||||
this.conversationId = conversationId
|
||||
console.log('设置会话ID:', conversationId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
getStatus(): ConnectionStatus {
|
||||
return this.status
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已连接
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.status === 'CONNECTED' && this.client?.connected === true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接头信息
|
||||
*/
|
||||
private getConnectHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {}
|
||||
|
||||
// 添加用户ID
|
||||
if (this.userId) {
|
||||
headers['X-User-Id'] = this.userId
|
||||
}
|
||||
|
||||
// 添加JWT token - 修复:使用正确的localStorage key
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
console.log('🔐 添加Authorization头到STOMP连接,token预览:', token.substring(0, 20) + '...')
|
||||
} else {
|
||||
console.warn('🔐 未找到access_token,WebSocket将以访客身份连接')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅消息
|
||||
*/
|
||||
private subscribeToMessages(): void {
|
||||
if (!this.client?.connected) return
|
||||
|
||||
// 订阅用户私有消息
|
||||
if (this.userId) {
|
||||
const userQueuePath = `/user/${this.userId}/queue/messages`
|
||||
console.log('📨 订阅用户私有队列:', userQueuePath)
|
||||
|
||||
this.client.subscribe(userQueuePath, (message: IMessage) => {
|
||||
this.handleMessage(message)
|
||||
})
|
||||
}
|
||||
|
||||
// 订阅广播消息
|
||||
this.client.subscribe('/topic/broadcast', (message: IMessage) => {
|
||||
this.handleMessage(message)
|
||||
})
|
||||
|
||||
// 如果有会话ID,订阅会话特定消息
|
||||
if (this.conversationId) {
|
||||
this.client.subscribe(`/topic/conversation/${this.conversationId}`, (message: IMessage) => {
|
||||
this.handleMessage(message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理收到的消息
|
||||
*/
|
||||
private handleMessage(message: IMessage): void {
|
||||
try {
|
||||
const wsMessage: WebSocketMessage = JSON.parse(message.body)
|
||||
console.log('📨 收到STOMP消息:', wsMessage)
|
||||
this.callbacks.onMessage?.(wsMessage)
|
||||
} catch (error) {
|
||||
console.error('❌ 解析STOMP消息失败:', error, message.body)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送连接消息
|
||||
*/
|
||||
private sendConnectMessage(): void {
|
||||
if (!this.client?.connected) return
|
||||
|
||||
const connectRequest = {
|
||||
userId: this.userId,
|
||||
clientType: 'web',
|
||||
clientVersion: '1.0.0',
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
try {
|
||||
this.client.publish({
|
||||
destination: '/app/chat.connect',
|
||||
body: JSON.stringify(connectRequest)
|
||||
})
|
||||
console.log('✅ STOMP连接消息发送成功:', connectRequest)
|
||||
} catch (error) {
|
||||
console.error('❌ STOMP连接消息发送失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接状态
|
||||
*/
|
||||
private setStatus(status: ConnectionStatus): void {
|
||||
this.status = status
|
||||
this.callbacks.onStatusChange?.(status)
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排重连
|
||||
*/
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.error('达到最大重连次数,停止重连')
|
||||
return
|
||||
}
|
||||
|
||||
this.reconnectAttempts++
|
||||
console.log(`${this.reconnectInterval}ms后尝试第${this.reconnectAttempts}次重连`)
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.status !== 'CONNECTED') {
|
||||
this.connect(this.userId!, this.callbacks).catch(() => {
|
||||
// 重连失败会自动安排下次重连
|
||||
})
|
||||
}
|
||||
}, this.reconnectInterval)
|
||||
|
||||
// 递增重连间隔
|
||||
this.reconnectInterval = Math.min(this.reconnectInterval * 1.5, 30000)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建STOMP WebSocket服务实例
|
||||
export const stompWebSocketService = new StompWebSocketService()
|
||||
|
||||
export default stompWebSocketService
|
||||
Reference in New Issue
Block a user