feat: 完成情绪博物馆项目重构和功能增强 - 新增日记评论和帖子功能 - 重构前端架构,优化用户体验 - 完善WebSocket通信机制 - 更新项目文档和部署配置
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user