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
-184
View File
@@ -1,184 +0,0 @@
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { ApiResponse } from '@/types'
// 创建axios实例
const api: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 添加认证token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加请求时间戳
config.headers['X-Request-Time'] = Date.now().toString()
return config
},
(error) => {
console.error('Request error:', error)
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const { data } = response
// 检查业务状态码
if (data.code !== 200) {
console.error('API Business Error:', {
code: data.code,
message: data.message,
url: response.config.url
})
// 对于认证错误,特殊处理
if (data.code === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
const error = new Error(data.message || '请求失败') as any
error.response = response
return Promise.reject(error)
}
return response
},
(error) => {
// 处理HTTP错误
if (error.response) {
const { status, data } = error.response
console.error('HTTP Error:', {
status,
url: error.config?.url,
message: data?.message || error.message
})
switch (status) {
case 401:
// 未授权,清除token并跳转到登录页
localStorage.removeItem('token')
window.location.href = '/login'
break
case 403:
console.error('Access forbidden')
break
case 404:
console.error('Resource not found')
break
case 500:
console.error('Server error')
break
default:
console.error('HTTP Error:', status, data?.message || error.message)
}
} else if (error.request) {
console.error('Network error:', error.message)
} else {
console.error('Request setup error:', error.message)
}
return Promise.reject(error)
}
)
// 通用请求方法
export const request = {
get: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> =>
api.get(url, config).then(res => res.data.data),
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
api.post(url, data, config).then(res => res.data.data),
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
api.put(url, data, config).then(res => res.data.data),
delete: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> =>
api.delete(url, config).then(res => res.data.data),
patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
api.patch(url, data, config).then(res => res.data.data),
}
// 文件上传
export const uploadFile = (file: File, onProgress?: (progress: number) => void): Promise<string> => {
const formData = new FormData()
formData.append('file', file)
return api.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
onProgress(progress)
}
},
}).then(res => res.data.data.url)
}
// 消息相关API
export const messageApi = {
// 获取用户消息分页
getUserMessages: (current: number = 1, size: number = 20) =>
request.get(`/message/user/page`, { params: { current, size } }),
// 搜索用户消息
searchUserMessages: (keyword: string, limit: number = 50) =>
request.post(`/message/user/search`, { keyword, limit }),
// 获取用户最近的聊天记录
getRecentMessages: (limit: number = 10) =>
request.post(`/message/user/recent`, { limit }),
// 获取消息详情
getMessageById: (id: string) =>
request.get(`/message/${id}`)
}
// 情绪记录相关API
export const emotionRecordApi = {
// 获取用户情绪记录分页
getUserEmotionRecords: (current: number = 1, size: number = 10) =>
request.get(`/emotion-records/user`, { params: { current, size } }),
// 获取用户最近情绪记录
getUserRecentEmotionRecords: (limit: number = 5) =>
request.get(`/emotion-records/user/recent`, { params: { limit } }),
// 获取情绪记录详情
getEmotionRecordById: (id: string) =>
request.get(`/emotion-records/${id}`),
// 删除情绪记录
deleteEmotionRecord: (id: string) =>
request.delete(`/emotion-records/${id}`)
}
// 情绪总结相关API
export const emotionSummaryApi = {
// 生成情绪记录总结
generateEmotionSummary: () =>
request.post(`/emotion-summary/generate`),
// 获取情绪记录总结状态
getEmotionSummaryStatus: () =>
request.get(`/emotion-summary/status`)
}
export default api
+198 -91
View File
@@ -1,114 +1,221 @@
import request from '@/utils/request'
/**
* 认证相关API服务
*/
import { http } from '@/utils/request'
import type {
LoginRequest,
LoginResponse,
RegisterRequest,
AuthResponse,
UserInfo,
CaptchaResponse,
ApiResponse,
RefreshTokenRequest,
ChangePasswordRequest,
ForgotPasswordRequest,
ResetPasswordRequest,
UserInfo
SendCodeRequest,
VerifyCodeRequest,
OAuthLoginRequest,
BindOAuthRequest,
UnbindOAuthRequest,
OAuthInfo,
LoginHistory,
OnlineUser
} from '@/types/auth'
export const authService = {
// 获取验证码
async getCaptcha(): Promise<CaptchaResponse> {
return await request.get('/auth/captcha')
},
// 用户登录
async login(data: LoginRequest): Promise<LoginResponse> {
return await request.post('/auth/login', data)
},
// 用户注册
async register(data: RegisterRequest): Promise<LoginResponse> {
return await request.post('/auth/register', data)
},
// 刷新token
async refreshToken(data: RefreshTokenRequest): Promise<ApiResponse<LoginResponse>> {
const response = await request.post('/auth/refresh-token', data)
/**
* 认证API服务类
*/
export class AuthService {
/**
* 用户登录
*/
static async login(data: LoginRequest): Promise<AuthResponse> {
const response = await http.post<AuthResponse>('/auth/login', data)
return response.data
},
}
// 用户登出
async logout(): Promise<ApiResponse<void>> {
const response = await request.post('/auth/logout')
/**
* 用户注册
*/
static async register(data: RegisterRequest): Promise<AuthResponse> {
const response = await http.post<AuthResponse>('/auth/register', data)
return response.data
},
}
// 获取用户信息
async getUserInfo(): Promise<ApiResponse<UserInfo>> {
const response = await request.get('/auth/user-info')
/**
* 获取验证码
*/
static async getCaptcha(): Promise<CaptchaResponse> {
const response = await http.get<CaptchaResponse>('/auth/captcha')
return response.data
},
}
// 修改密码
async changePassword(data: ChangePasswordRequest): Promise<ApiResponse<void>> {
const response = await request.post('/auth/change-password', data)
/**
* 用户登出
*/
static async logout(): Promise<void> {
const response = await http.post<void>('/auth/logout')
return response.data
},
}
// 忘记密码
async forgotPassword(data: ForgotPasswordRequest): Promise<ApiResponse<void>> {
return await request.post('/forgot-password', data)
},
/**
* 刷新Token
*/
static async refreshToken(data: RefreshTokenRequest): Promise<AuthResponse> {
const response = await http.post<AuthResponse>('/auth/refresh-token', data)
return response.data
}
// 重置密码
async resetPassword(data: ResetPasswordRequest): Promise<ApiResponse<void>> {
return await request.post('/reset-password', data)
},
/**
* 获取当前用户信息
*/
static async getCurrentUserInfo(): Promise<UserInfo> {
const response = await http.get<UserInfo>('/auth/user/info')
return response.data
}
// 验证token有效性
async validateToken(): Promise<ApiResponse<boolean>> {
return await request.get('/validate-token')
},
/**
* 验证Token是否有效
*/
static async validateToken(): Promise<boolean> {
const response = await http.get<boolean>('/auth/validate-token')
return response.data
}
// 检查账号是否存在
async checkAccount(account: string): Promise<ApiResponse<boolean>> {
return await request.get(`/check-account?account=${account}`)
/**
* 修改密码
*/
static async changePassword(data: ChangePasswordRequest): Promise<void> {
return http.post<void>('/auth/change-password', data)
}
/**
* 重置密码
*/
static async resetPassword(data: ResetPasswordRequest): Promise<void> {
return http.post<void>('/auth/reset-password', data)
}
/**
* 发送验证码
*/
static async sendCode(data: SendCodeRequest): Promise<void> {
return http.post<void>('/auth/send-code', data)
}
/**
* 验证验证码
*/
static async verifyCode(data: VerifyCodeRequest): Promise<boolean> {
return http.post<boolean>('/auth/verify-code', data)
}
/**
* 第三方登录
*/
static async oauthLogin(data: OAuthLoginRequest): Promise<AuthResponse> {
return http.post<AuthResponse>('/auth/oauth/login', data)
}
/**
* 绑定第三方账号
*/
static async bindOAuth(data: BindOAuthRequest): Promise<void> {
return http.post<void>('/auth/oauth/bind', data)
}
/**
* 解绑第三方账号
*/
static async unbindOAuth(data: UnbindOAuthRequest): Promise<void> {
return http.post<void>('/auth/oauth/unbind', data)
}
/**
* 获取第三方账号绑定信息
*/
static async getOAuthInfo(): Promise<OAuthInfo[]> {
return http.get<OAuthInfo[]>('/auth/oauth/info')
}
// 移除权限接口,该接口不存在
/**
* 获取登录历史
*/
static async getLoginHistory(page = 1, size = 10): Promise<{
list: LoginHistory[]
total: number
page: number
size: number
}> {
return http.get<{
list: LoginHistory[]
total: number
page: number
size: number
}>('/auth/login-history', {
params: { page, size }
})
}
/**
* 获取在线用户列表(管理员功能)
*/
static async getOnlineUsers(page = 1, size = 10): Promise<{
list: OnlineUser[]
total: number
page: number
size: number
}> {
return http.get<{
list: OnlineUser[]
total: number
page: number
size: number
}>('/auth/online-users', {
params: { page, size }
})
}
/**
* 强制下线用户(管理员功能)
*/
static async forceLogout(userId: string): Promise<void> {
return http.post<void>(`/auth/force-logout/${userId}`)
}
/**
* 检查账号是否存在
*/
static async checkAccountExists(account: string): Promise<boolean> {
const response = await http.get<boolean>('/auth/check-account', {
params: { account }
})
return response.data
}
/**
* 检查邮箱是否存在
*/
static async checkEmailExists(email: string): Promise<boolean> {
const response = await http.get<boolean>('/auth/check-email', {
params: { email }
})
return response.data
}
/**
* 检查手机号是否存在
*/
static async checkPhoneExists(phone: string): Promise<boolean> {
const response = await http.get<boolean>('/auth/check-phone', {
params: { phone }
})
return response.data
}
}
// 工具函数
export const authUtils = {
// 获取token
getToken(): string | null {
return localStorage.getItem('token')
},
// 设置token
setToken(token: string): void {
localStorage.setItem('token', token)
},
// 移除token
removeToken(): void {
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
},
// 获取用户信息
getUserInfo(): UserInfo | null {
const userInfo = localStorage.getItem('userInfo')
return userInfo ? JSON.parse(userInfo) : null
},
// 设置用户信息
setUserInfo(userInfo: UserInfo): void {
localStorage.setItem('userInfo', JSON.stringify(userInfo))
},
// 检查是否已登录
isLoggedIn(): boolean {
return !!this.getToken()
},
// 清除所有认证信息
clearAuth(): void {
this.removeToken()
}
}
// 导出默认实例
export default AuthService
+196 -49
View File
@@ -1,51 +1,198 @@
import { request } from './api'
import type { ChatMessage, ChatSession, PaginatedResponse } from '@/types'
import { http } from '@/utils/request'
import type { ChatMessage, ChatSession } from '@/types'
export const chatApi = {
// 发送AI聊天消息(REST备用,主用WebSocket
sendAiMessage: (conversationId: string, message: string, userId: string): Promise<any> =>
request.post('/ai/chat', { conversationId, message, userId }),
// 创建会话
createSession: (userId: string, title: string): Promise<ChatSession> =>
request.post('/conversation', { userId, title }),
// 获取会话分页
getSessions: (params: { page: number, size: number, userId?: string }): Promise<PaginatedResponse<ChatSession>> =>
request.get('/conversation/page', { params }),
// 获取用户所有会话
getUserSessions: (userId: string): Promise<ChatSession[]> =>
request.get(`/conversation/user/${userId}`),
// 删除会话
deleteSession: (id: string): Promise<void> =>
request.delete(`/conversation/${id}`),
// 更新会话标题
updateSessionTitle: (id: string, title: string): Promise<ChatSession> =>
request.put(`/conversation/${id}`, { title }),
// 获取会话消息分页
getSessionMessages: (conversationId: string, params: { page: number, size: number }): Promise<PaginatedResponse<ChatMessage>> =>
request.get(`/message/conversation/${conversationId}/page`, { params }),
// 获取会话所有消息
getAllSessionMessages: (conversationId: string): Promise<ChatMessage[]> =>
request.get(`/message/conversation/${conversationId}`),
// 创建消息(保存到数据库)
createMessage: (data: {
conversationId: string,
userId: string,
content: string,
contentType?: string,
senderType?: string,
senderId?: string
}): Promise<ChatMessage> =>
request.post('/message', data),
// 聊天统计
getChatStats: (userId?: string, conversationId?: string): Promise<any> =>
request.get('/ai/stats', { params: { userId, conversationId } }),
export interface CreateSessionRequest {
userId: string
title?: string
}
export interface CreateSessionResponse {
id: string
title: string
userId: string
createTime: string
updateTime: string
messageCount: number
}
export interface GetSessionsResponse {
sessions: ChatSession[]
total: number
}
export interface GetMessagesResponse {
messages: ChatMessage[]
total: number
}
// 后端实际返回的会话数据结构
export interface ConversationResponse {
id: string
userId: string
userType: string
title: string
type: string
status: string
startTime: string
endTime?: string
lastActiveTime: string
messageCount: number
createTime: string
updateTime: string
}
/**
* 聊天API服务
*/
export class ChatApiService {
/**
* 创建聊天会话
*/
async createSession(userId: string, title?: string): Promise<ChatSession> {
try {
console.log('📝 创建会话API调用:', { userId, title })
const response = await http.post<ConversationResponse>('/conversation', {
userId,
title: title || `对话${Date.now()}`
})
console.log('📝 创建会话API响应:', response)
// 处理HTTP响应的data字段
const data = (response as any).data || response
// 转换后端数据格式为前端格式 - 完整匹配ChatSession接口
return {
id: data.id,
title: data.title,
userId: data.userId || data.user_id,
userType: data.userType || data.user_type,
type: data.type,
status: data.status,
createTime: data.createTime || data.create_time,
updateTime: data.updateTime || data.update_time,
messageCount: data.messageCount || data.message_count || 0,
startTime: data.startTime || data.start_time,
endTime: data.endTime || data.end_time,
lastActiveTime: data.lastActiveTime || data.last_active_time
}
} catch (error) {
console.error('❌ 创建会话失败:', error)
throw error
}
}
/**
* 获取用户的所有会话
*/
async getUserSessions(userId: string): Promise<ChatSession[]> {
try {
console.log('📂 获取用户会话API调用:', { userId })
const response = await http.get<ConversationResponse[]>(`/conversation/user/${userId}`)
console.log('📂 获取用户会话API响应:', response)
// 处理HTTP响应的data字段
const data = (response as any).data || response
// 后端返回ConversationResponse数组,需要转换为ChatSession格式
if (Array.isArray(data)) {
return data.map((conv: any) => ({
id: conv.id,
title: conv.title,
userId: conv.userId || conv.user_id, // 兼容不同的字段名
createTime: conv.createTime || conv.create_time,
updateTime: conv.updateTime || conv.update_time,
messageCount: conv.messageCount || conv.message_count || 0
}))
}
return []
} catch (error) {
console.error('❌ 获取用户会话失败:', error)
return []
}
}
/**
* 获取会话的所有消息
* 注意:后端没有按会话ID获取消息的接口,这里返回空数组
* 实际的消息加载通过messageApi的接口实现
*/
async getAllSessionMessages(sessionId: string): Promise<ChatMessage[]> {
console.log('⚠️ getAllSessionMessages: 后端没有此接口,返回空数组。会话ID:', sessionId)
console.log('💡 建议使用messageApi.getRecentMessages()或messageApi.getUserMessages()获取消息')
// 返回空数组,避免404错误
// 实际的消息加载由messageApi处理
return []
}
/**
* 删除会话
*/
async deleteSession(sessionId: string): Promise<void> {
try {
console.log('🗑️ 删除会话API调用:', { sessionId })
await http.delete(`/conversation/${sessionId}`)
console.log('✅ 删除会话成功')
} catch (error) {
console.error('❌ 删除会话失败:', error)
throw error
}
}
/**
* 更新会话标题
*/
async updateSessionTitle(sessionId: string, title: string): Promise<void> {
try {
console.log('✏️ 更新会话标题API调用:', { sessionId, title })
await http.put(`/conversation/${sessionId}`, { title })
console.log('✅ 更新会话标题成功')
} catch (error) {
console.error('❌ 更新会话标题失败:', error)
throw error
}
}
/**
* 获取最近的消息
* 注意:这个方法已废弃,请使用messageApi.getRecentMessages()
*/
async getRecentMessages(params: { limit?: number } = {}): Promise<ChatMessage[]> {
try {
console.log('⚠️ getRecentMessages: 此方法已废弃,请使用messageApi.getRecentMessages()')
console.log('💡 建议直接调用messageApi.getRecentMessages(limit)')
console.log('📝 参数:', params)
// 返回空数组,避免调用不存在的接口
return []
} catch (error) {
console.error('❌ getRecentMessages错误:', error)
return []
}
}
/**
* 搜索消息
* 注意:这个方法已废弃,请使用messageApi.searchUserMessages()
*/
async searchMessages(keyword: string, sessionId?: string): Promise<ChatMessage[]> {
try {
console.log('⚠️ searchMessages: 此方法已废弃,请使用messageApi.searchUserMessages()')
console.log('💡 建议直接调用messageApi.searchUserMessages(keyword, limit)')
console.log('📝 参数:', { keyword, sessionId })
// 返回空数组,避免调用不存在的接口
return []
} catch (error) {
console.error('❌ searchMessages错误:', error)
return []
}
}
}
// 创建聊天API服务实例
export const chatApi = new ChatApiService()
export default chatApi
+11
View File
@@ -0,0 +1,11 @@
import request from '@/utils/request';
export function publishDiary(data: any) {
return request.post('/diary-post/publish', data);
}
export function getUserDiaries(userId: string, page = 1, size = 10) {
return request.get(`/diary-post/user/${userId}/page`, {
params: { current: page, size }
});
}
+176
View File
@@ -0,0 +1,176 @@
import { http } from '@/utils/request'
import type { ChatMessage } from '@/types'
// 消息相关类型定义
export interface MessageResponse {
id: string
conversationId: string
content: string
type: string
sender: string
isRead: number
aiReply?: string
emotionAnalysis?: string
createTime: string
updateTime: string
}
export interface PageResult<T> {
records: T[]
total: number
size: number
current: number
pages: number
}
export interface MessagePageRequest {
current: number
size: number
conversationId?: string
type?: string
sender?: string
startTime?: string
endTime?: string
}
export interface MessageSearchRequest {
keyword: string
limit: number
conversationId?: string
type?: string
sender?: string
startTime?: string
endTime?: string
}
export interface MessageRecentRequest {
limit: number
conversationId?: string
type?: string
sender?: string
}
/**
* 消息API服务 - 与web项目保持一致的接口
*/
export const messageApi = {
// 获取用户消息分页
getUserMessages: async (current: number = 1, size: number = 20) => {
console.log('📨 调用getUserMessages API:', { current, size })
const response = await http.get(`/message/user/page`, { params: { current, size } })
console.log('📨 getUserMessages API响应:', response)
return response
},
// 搜索用户消息
searchUserMessages: async (keyword: string, limit: number = 50) => {
console.log('🔍 调用searchUserMessages API:', { keyword, limit })
const response = await http.post(`/message/user/search`, { keyword, limit })
console.log('🔍 searchUserMessages API响应:', response)
return response
},
// 获取用户最近的聊天记录 - 修复:使用POST请求匹配后端接口
getRecentMessages: async (limit: number = 10) => {
console.log('📝 调用getRecentMessages API:', { limit })
const response = await http.post(`/message/user/recent`, { limit })
console.log('📝 getRecentMessages API响应:', response)
return response
},
// 获取消息详情
getMessageById: async (id: string) => {
console.log('📄 调用getMessageById API:', { id })
const response = await http.get(`/message/${id}`)
console.log('📄 getMessageById API响应:', response)
return response
}
}
/**
* 消息服务类 - 提供高级封装
*/
class MessageService {
/**
* 获取用户消息分页
*/
static async getUserMessages(current: number = 1, size: number = 20): Promise<PageResult<MessageResponse>> {
const response = await messageApi.getUserMessages(current, size)
return response.data || response
}
/**
* 搜索用户消息
*/
static async searchUserMessages(request: MessageSearchRequest): Promise<MessageResponse[]> {
const response = await messageApi.searchUserMessages(request.keyword, request.limit)
return response.data || response
}
/**
* 获取用户最近的聊天记录
*/
static async getRecentMessages(request: MessageRecentRequest): Promise<MessageResponse[]> {
console.log('📝 MessageService.getRecentMessages 调用:', request)
const response = await messageApi.getRecentMessages(request.limit)
console.log('📝 MessageService.getRecentMessages 响应:', response)
// 处理响应数据结构
const messageList = response.data || response || []
return Array.isArray(messageList) ? messageList : []
}
/**
* 根据ID获取消息详情
*/
static async getMessageById(id: string): Promise<MessageResponse> {
const response = await messageApi.getMessageById(id)
return response.data || response
}
/**
* 转换消息格式为聊天消息格式 - 完整匹配后端字段
*/
static convertToChatMessage(msg: MessageResponse): ChatMessage {
console.log('🔄 转换消息格式:', msg)
// 处理时间格式 - 后端返回 "2025-07-26 22:09:10" 格式
// 直接保持原始字符串格式,让前端UI层处理
let timestamp = msg.createTime
console.log('🕐 保持原始时间格式:', msg.createTime)
const chatMessage: ChatMessage = {
id: msg.id,
content: msg.content,
type: msg.sender === 'user' ? 'user' : (msg.sender === 'ai' ? 'ai' : 'system'),
timestamp: timestamp,
conversationId: msg.conversationId,
sessionId: msg.conversationId, // 别名,保持兼容性
status: 'sent',
sender: msg.sender as 'user' | 'ai' | 'system',
isRead: msg.isRead,
role: msg.sender === 'user' ? 'user' : 'assistant' // 用于UI显示
}
console.log('✅ 转换后的消息详情:', {
原始sender: msg.sender,
转换后role: chatMessage.role,
转换后type: chatMessage.type,
时间: msg.createTime + ' -> ' + timestamp
})
console.log('✅ 转换后的消息:', chatMessage)
return chatMessage
}
/**
* 批量转换消息格式
*/
static convertToChatMessages(messages: MessageResponse[]): ChatMessage[] {
console.log('🔄 批量转换消息格式,数量:', messages.length)
const chatMessages = messages.map(msg => this.convertToChatMessage(msg))
console.log('✅ 批量转换完成,结果:', chatMessages)
return chatMessages
}
}
export default MessageService
+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
+130
View File
@@ -0,0 +1,130 @@
import { http } from '@/utils/request'
// 用户相关类型定义
export interface UserProfile {
id: string
account: string
username: string
nickname?: string // 可选,如果为空则使用username
email?: string // 可选
phone?: string // 可选
avatar?: string // 可选,头像URL
birthDate?: string // 可选,生日
location?: string // 可选,所在地
bio?: string // 可选,个人简介
memberLevel?: string // 可选,会员等级
totalDays?: number // 可选,使用天数
selfAwareness?: number // 可选,自我感知
emotionalResilience?: number // 可选,情绪韧性
actionPower?: number // 可选,行动力
empathy?: number // 可选,共情力
lifeEnthusiasm?: number // 可选,生活热度
status: number
isVerified?: number // 可选,是否已验证
createTime: string
updateTime: string
lastActiveTime?: string // 可选,最后活跃时间
}
export interface UserProfileUpdateRequest {
nickname?: string
email?: string
phone?: string
avatar?: string
birthDate?: string
location?: string
bio?: string
}
export interface GrowthStats {
selfAwareness: number
emotionalResilience: number
actionPower: number
empathy: number
lifeEnthusiasm: number
}
/**
* 用户服务
*/
class UserService {
/**
* 获取当前用户个人资料
*/
static async getCurrentUserProfile(): Promise<UserProfile> {
const response = await http.get<UserProfile>('/user/profile')
return response.data
}
/**
* 更新当前用户个人资料
*/
static async updateCurrentUserProfile(data: UserProfileUpdateRequest): Promise<UserProfile> {
const response = await http.put<UserProfile>('/user/profile', data)
return response.data
}
/**
* 上传头像
*/
static async uploadAvatar(file: File): Promise<string> {
const formData = new FormData()
formData.append('file', file)
const response = await http.post<{ url: string }>('/user/avatar/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
return response.data.url
}
/**
* 获取用户成长数据
*/
static async getUserGrowthStats(): Promise<GrowthStats> {
const response = await http.get<GrowthStats>('/user/growth-stats')
return response.data
}
/**
* 更新用户密码
*/
static async updatePassword(data: {
oldPassword: string
newPassword: string
confirmPassword: string
}): Promise<void> {
await http.put('/user/password', data)
}
/**
* 验证邮箱
*/
static async verifyEmail(code: string): Promise<void> {
await http.post('/user/email/verify', { code })
}
/**
* 发送邮箱验证码
*/
static async sendEmailVerificationCode(): Promise<void> {
await http.post('/user/email/send-code')
}
/**
* 验证手机号
*/
static async verifyPhone(code: string): Promise<void> {
await http.post('/user/phone/verify', { code })
}
/**
* 发送手机验证码
*/
static async sendPhoneVerificationCode(): Promise<void> {
await http.post('/user/phone/send-code')
}
}
export default UserService
-423
View File
@@ -1,423 +0,0 @@
import SockJS from 'sockjs-client'
import * as Stomp from 'stompjs'
// import type { ChatMessage } from '@/types' // 暂时注释,未使用
// 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
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 interface ConnectRequest {
userId?: string
username?: string
clientType?: string
clientVersion?: string
timestamp?: number
}
// WebSocket连接状态
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
}
export class WebSocketService {
private client: Stomp.Client | null = null
private callbacks: WebSocketCallbacks = {}
private status: ConnectionStatus = 'DISCONNECTED'
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectInterval = 3000
private heartbeatTimer: number | null = null
private userId: string | null = null
private conversationId: string | null = null
constructor(private wsUrl: string) {}
/**
* 连接WebSocket
*/
connect(userId?: string, callbacks?: WebSocketCallbacks): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.callbacks = { ...callbacks }
this.userId = userId || `guest_${Date.now()}`
this.setStatus('CONNECTING')
// 创建SockJS连接
const socket = new SockJS(this.wsUrl, null, {
transports: ['websocket', 'xhr-streaming', 'xhr-polling']
})
this.client = Stomp.over(socket)
// 禁用调试日志
this.client.debug = () => {}
// 设置心跳
this.client.heartbeat.outgoing = 20000
this.client.heartbeat.incoming = 20000
// 连接配置 - 添加token支持
const connectHeaders: any = {
'X-User-Id': this.userId
}
// 如果有token,添加到连接头中
const token = localStorage.getItem('token')
if (token) {
connectHeaders['Authorization'] = `Bearer ${token}`
}
console.log('WebSocket连接配置:', {
userId: this.userId,
hasToken: !!token,
headers: connectHeaders
})
this.client.connect(
connectHeaders,
(frame) => {
console.log('WebSocket连接成功:', frame)
this.setStatus('CONNECTED')
this.reconnectAttempts = 0
// 订阅用户消息
this.subscribeToMessages()
// 发送连接消息
this.sendConnectMessage()
// 启动心跳
this.startHeartbeat()
this.callbacks.onConnect?.()
resolve()
},
(error) => {
console.error('WebSocket连接失败:', error)
this.setStatus('ERROR')
// 详细的错误处理
let errorMessage = '连接失败'
if (error && typeof error === 'object') {
const errorObj = error as any
if (errorObj.type === 'close') {
switch (errorObj.code) {
case 1006:
errorMessage = '连接异常断开,正在重连...'
break
case 1000:
errorMessage = '连接正常关闭'
break
case 1001:
errorMessage = '服务器正在重启,请稍后重试'
break
case 1002:
errorMessage = '协议错误'
break
case 1003:
errorMessage = '数据格式错误'
break
default:
errorMessage = `连接关闭 (代码: ${errorObj.code})`
}
} else if (errorObj.message) {
errorMessage = errorObj.message
}
} else if (typeof error === 'string') {
errorMessage = error
}
this.callbacks.onError?.({ error, userMessage: errorMessage })
// 尝试重连
this.scheduleReconnect()
reject(error)
}
)
} catch (error) {
console.error('WebSocket初始化失败:', error)
this.setStatus('ERROR')
reject(error)
}
})
}
/**
* 断开连接
*/
disconnect(): void {
if (this.client?.connected) {
this.sendDisconnectMessage()
this.client.disconnect(() => {
console.log('WebSocket已断开连接')
})
}
this.stopHeartbeat()
this.setStatus('DISCONNECTED')
this.callbacks.onDisconnect?.()
}
/**
* 发送聊天消息
*/
sendChatMessage(content: string, conversationId?: string): void {
if (!this.client?.connected) {
const error = new Error('WebSocket连接已断开,无法发送消息')
console.error('WebSocket未连接')
this.callbacks.onError?.({ userMessage: '连接已断开,请等待重连后再试', originalError: error })
return
}
if (!content.trim()) {
const error = new Error('消息内容不能为空')
this.callbacks.onError?.({ userMessage: '消息内容不能为空', originalError: error })
return
}
// 使用新的后端接口格式
const chatRequest: ChatRequest = {
content: content.trim(),
senderId: this.userId!,
senderType: this.userId?.startsWith('guest_') ? 'GUEST' : 'USER',
messageType: 'TEXT',
conversationId: conversationId || this.conversationId || undefined,
timestamp: Date.now()
}
try {
this.client.send('/app/chat.send', {}, JSON.stringify(chatRequest))
console.log('发送聊天消息:', {
...chatRequest,
currentUserId: this.userId,
expectedSubscriptionPath: '/user/queue/messages'
})
} catch (error) {
console.error('发送消息失败:', error)
this.callbacks.onError?.({
userMessage: '消息发送失败,请重试',
originalError: error
})
}
}
/**
* 设置会话ID
*/
setConversationId(conversationId: string): void {
this.conversationId = conversationId
console.log('WebSocket会话ID已更新:', conversationId)
}
/**
* 获取当前会话ID
*/
getConversationId(): string | null {
return this.conversationId
}
/**
* 获取连接状态
*/
getStatus(): ConnectionStatus {
return this.status
}
/**
* 检查是否已连接
*/
isConnected(): boolean {
return this.status === 'CONNECTED' && this.client?.connected === true
}
/**
* 订阅消息
*/
private subscribeToMessages(): void {
if (!this.client?.connected) return
// 订阅用户私有消息 - 包含用户ID的完整路径
if (this.userId) {
const userQueuePath = `/user/${this.userId}/queue/messages`
console.log('订阅用户私有队列:', userQueuePath)
this.client.subscribe(userQueuePath, (message) => {
try {
const wsMessage: WebSocketMessage = JSON.parse(message.body)
console.log('收到用户私有WebSocket消息:', wsMessage)
this.callbacks.onMessage?.(wsMessage)
} catch (error) {
console.error('解析用户私有WebSocket消息失败:', error)
}
})
}
// 同时订阅基于sessionId的队列(从日志看后端也在使用这个)
if (this.client.ws && this.client.ws.url) {
// 从WebSocket URL中提取sessionId
const urlParts = this.client.ws.url.split('/')
const sessionId = urlParts[urlParts.length - 2] // 倒数第二个部分是sessionId
if (sessionId) {
console.log('订阅基于sessionId的队列:', `/queue/messages-user${sessionId}`)
this.client.subscribe(`/queue/messages-user${sessionId}`, (message) => {
try {
const wsMessage: WebSocketMessage = JSON.parse(message.body)
console.log('收到基于sessionId的WebSocket消息:', wsMessage)
this.callbacks.onMessage?.(wsMessage)
} catch (error) {
console.error('解析基于sessionId的WebSocket消息失败:', error)
}
})
}
}
// 订阅广播消息
this.client.subscribe('/topic/broadcast', (message) => {
try {
const wsMessage: WebSocketMessage = JSON.parse(message.body)
console.log('收到广播消息:', wsMessage)
this.callbacks.onMessage?.(wsMessage)
} catch (error) {
console.error('解析广播消息失败:', error)
}
})
// 如果有会话ID,也订阅会话特定的消息
if (this.conversationId) {
this.client.subscribe(`/topic/conversation/${this.conversationId}`, (message) => {
try {
const wsMessage: WebSocketMessage = JSON.parse(message.body)
console.log('收到会话WebSocket消息:', wsMessage)
this.callbacks.onMessage?.(wsMessage)
} catch (error) {
console.error('解析会话WebSocket消息失败:', error)
}
})
}
}
/**
* 发送连接消息
*/
private sendConnectMessage(): void {
if (!this.client?.connected) return
const connectRequest: ConnectRequest = {
userId: this.userId!,
username: this.userId!,
clientType: 'web',
clientVersion: '1.0.0',
timestamp: Date.now()
}
try {
this.client.send('/app/chat.connect', {}, JSON.stringify(connectRequest))
console.log('发送连接消息:', connectRequest)
} catch (error) {
console.error('发送连接消息失败:', error)
}
}
/**
* 发送断开连接消息
*/
private sendDisconnectMessage(): void {
if (!this.client?.connected) return
try {
this.client.send('/app/chat.disconnect', {}, JSON.stringify({}))
} catch (error) {
console.error('发送断开连接消息失败:', error)
}
}
/**
* 启动心跳
*/
private startHeartbeat(): void {
this.stopHeartbeat()
this.heartbeatTimer = window.setInterval(() => {
if (this.client?.connected) {
try {
this.client.send('/app/chat.heartbeat', {}, JSON.stringify({}))
} catch (error) {
console.error('心跳发送失败:', error)
}
}
}, 30000) // 30秒心跳间隔
}
/**
* 停止心跳
*/
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
/**
* 设置连接状态
*/
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)
}
}
// 创建WebSocket服务实例
const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:19089/ws/chat'
export const webSocketService = new WebSocketService(wsUrl)
export default webSocketService