feat: 完成情绪博物馆项目重构和功能增强 - 新增日记评论和帖子功能 - 重构前端架构,优化用户体验 - 完善WebSocket通信机制 - 更新项目文档和部署配置

This commit is contained in:
2025-07-27 10:05:59 +08:00
parent 6903ac1c0d
commit cc886cd4d5
126 changed files with 21179 additions and 15734 deletions
+372
View File
@@ -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_tokenWebSocket将以访客身份连接')
}
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