修复WebSocket身份认证问题

- 添加WebSocketAuthInterceptor处理token认证
- 修改WebSocket连接逻辑,支持token传递
- 统一用户身份识别,确保登录用户使用USER类型
- 修复前端环境变量配置,统一WebSocket URL
- 添加Token测试页面用于验证功能
- 更新聊天消息处理逻辑,正确识别用户身份

解决了登录用户发送消息时同时保存GUEST和USER两种类型数据的问题
This commit is contained in:
2025-07-24 17:51:38 +08:00
parent 6560e66959
commit 847f5126cf
30 changed files with 1447 additions and 216 deletions
+3 -3
View File
@@ -5,9 +5,9 @@ VITE_APP_TITLE=开心APP - 开发环境
VITE_APP_DESCRIPTION=你的情绪陪伴使者
# API配置 - 直接访问backend-single
VITE_API_BASE_URL=http://localhost:8080/api
VITE_UPLOAD_URL=http://localhost:8080/api/upload
VITE_WS_URL=http://localhost:8080/ws/chat
VITE_API_BASE_URL=http://localhost:19089/api
VITE_UPLOAD_URL=http://localhost:19089/api/upload
VITE_WS_URL=http://localhost:19089/api/ws/chat
# WebSocket配置
VITE_WS_RECONNECT_ATTEMPTS=5
+4 -4
View File
@@ -2,10 +2,10 @@
VITE_APP_TITLE=开心APP
VITE_APP_DESCRIPTION=你的情绪陪伴使者
# API配置 - 生产环境通过网关访问
VITE_API_BASE_URL=http://47.111.10.27:19000/api
VITE_UPLOAD_URL=http://47.111.10.27:19000/api/upload
VITE_WS_URL=http://47.111.10.27:19000/ws/chat
# API配置 - 生产环境直接访问backend-single
VITE_API_BASE_URL=http://47.111.10.27:19089/api
VITE_UPLOAD_URL=http://47.111.10.27:19089/api/upload
VITE_WS_URL=http://47.111.10.27:19089/api/ws/chat
# WebSocket配置
VITE_WS_RECONNECT_ATTEMPTS=10
@@ -0,0 +1,136 @@
# WebSocket聊天功能完善总结
## 概述
根据后端WebSocket接口和聊天接口,对前端聊天页面功能进行了全面完善,提升了用户体验和系统稳定性。
## 完成的功能改进
### 1. ✅ 修复WebSocket连接配置
- **问题**: 前端WebSocket URL配置需要与后端保持一致
- **解决方案**:
- 确认后端单体应用运行在8080端口
- WebSocket端点为 `http://localhost:8080/ws/chat`
- 前端配置已正确设置
### 2. ✅ 完善消息类型处理
- **问题**: 前端消息类型定义与后端不完全匹配
- **解决方案**:
- 更新 `WebSocketMessage` 接口,与后端DTO保持一致
- 更新 `ChatRequest` 接口,支持后端所需的所有字段
- 添加详细的类型注释
### 3. ✅ 优化AI回复显示
- **问题**: AI回复需要支持分段显示,模拟自然对话流
- **解决方案**:
- 实现 `splitAiReply()` 函数,支持 `\n``\n\n` 分割
- 实现 `addAiReplyMessages()` 函数,支持延时分段显示
- 每段消息间隔1秒显示,提升用户体验
### 4. ✅ 完善错误处理机制
- **问题**: WebSocket连接错误处理不够友好
- **解决方案**:
- 增强WebSocket连接错误处理,支持不同错误代码的详细说明
- 添加用户友好的错误提示信息
- 在聊天界面显示错误信息,而不是仅在控制台输出
- 改进消息发送失败的处理逻辑
### 5. ✅ 添加消息状态跟踪
- **问题**: 缺少消息发送状态的可视化反馈
- **解决方案**:
- 扩展 `ChatMessage` 类型,添加 `status``error` 字段
- 实现 `updateMessageStatus()` 函数,支持状态更新
- 在UI中显示消息状态:发送中、已发送、已送达、已读、发送失败
- 添加状态对应的样式和颜色区分
### 6. ✅ 完善会话管理
- **问题**: WebSocket连接时会话ID设置和多会话切换需要优化
- **解决方案**:
- 在创建新会话时自动设置WebSocket会话ID
- 在切换会话时更新WebSocket会话ID
- 添加 `getConversationId()` 方法获取当前会话ID
- 确保WebSocket连接状态与会话状态同步
## 技术实现细节
### WebSocket消息类型
```typescript
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
}
```
### 消息状态跟踪
- **发送中**: 用户点击发送按钮后立即显示
- **已发送**: WebSocket消息发送成功后更新
- **已送达**: 数据库保存成功后更新
- **已读**: 收到后端确认后更新(待后端支持)
- **发送失败**: 发送或保存失败时显示
### AI回复分段显示
```typescript
const splitAiReply = (content: string): string[] => {
const segments = content.split(/\n\n|\n/).filter(segment => segment.trim().length > 0)
return segments
}
```
### 错误处理增强
- WebSocket连接错误代码映射
- 用户友好的错误信息显示
- 自动重连机制优化
## 用户体验改进
1. **实时状态反馈**: 用户可以看到消息的发送状态
2. **自然对话流**: AI回复分段显示,模拟真实对话
3. **友好错误提示**: 连接问题时显示清晰的错误信息
4. **会话管理**: 支持多会话切换,状态同步
5. **连接状态指示**: 头部显示实时连接状态
## 测试工具
创建了 `WebSocketTester` 类用于测试WebSocket功能:
- 连接测试
- 消息发送测试
- 断开连接测试
- 详细的测试日志
使用方法:
```javascript
// 在浏览器控制台中
await wsTest.runConnectionTest()
await wsTest.testMessageSending()
wsTest.testDisconnection()
console.log(wsTest.getTestResults())
```
## 后续建议
1. **消息已读状态**: 需要后端支持消息已读确认
2. **离线消息**: 支持离线消息的缓存和同步
3. **文件上传**: 扩展支持图片和文件消息
4. **消息撤回**: 支持消息撤回功能
5. **群聊支持**: 扩展支持多人聊天
## 配置文件
确保以下配置正确:
- `.env.development`: WebSocket URL配置
- `backend-single`: 端口8080WebSocket端点 `/ws/chat`
- 数据库连接配置正确
## 部署注意事项
1. 确保后端WebSocket服务正常运行
2. 检查防火墙和代理配置
3. 验证WebSocket连接的跨域设置
4. 监控WebSocket连接的稳定性
+9
View File
@@ -110,6 +110,15 @@ const routes: RouteRecordRaw[] = [
requiresAuth: false
}
},
{
path: '/token-test',
name: 'TokenTest',
component: () => import('@/views/TokenTest.vue'),
meta: {
title: 'Token测试',
requiresAuth: false
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
+5 -8
View File
@@ -15,20 +15,17 @@ import type {
export const authService = {
// 获取验证码
async getCaptcha(): Promise<CaptchaResponse> {
const response = await request.get('/auth/captcha')
return response.data.data
return await request.get('/auth/captcha')
},
// 用户登录
async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> {
const response = await request.post('/auth/login', data)
return response.data
async login(data: LoginRequest): Promise<LoginResponse> {
return await request.post('/auth/login', data)
},
// 用户注册
async register(data: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
const response = await request.post('/auth/register', data)
return response.data
async register(data: RegisterRequest): Promise<LoginResponse> {
return await request.post('/auth/register', data)
},
// 刷新token
+37 -26
View File
@@ -2,39 +2,50 @@ import { request } from './api'
import type { ChatMessage, ChatSession, PaginatedResponse } from '@/types'
export const chatApi = {
// 发送消息
sendMessage: (content: string, sessionId?: string): Promise<ChatMessage> =>
request.post('/chat/message', { content, sessionId }),
// 发送AI聊天消息(REST备用,主用WebSocket
sendAiMessage: (conversationId: string, message: string, userId: string): Promise<any> =>
request.post('/ai/chat', { conversationId, message, userId }),
// 获取会话列表
getSessions: (): Promise<ChatSession[]> =>
request.get('/chat/sessions'),
// 创建会话
createSession: (userId: string, title: string): Promise<ChatSession> =>
request.post('/conversation', { userId, title }),
// 创建新会话
createSession: (title?: string): Promise<ChatSession> =>
request.post('/chat/session', { title }),
// 获取会话分页
getSessions: (params: { page: number, size: number, userId?: string }): Promise<PaginatedResponse<ChatSession>> =>
request.get('/conversation/page', { params }),
// 获取会话消息
getSessionMessages: (sessionId: string, page = 1, size = 50): Promise<PaginatedResponse<ChatMessage>> =>
request.get(`/chat/session/${sessionId}/messages`, { params: { page, size } }),
// 获取用户所有会话
getUserSessions: (userId: string): Promise<ChatSession[]> =>
request.get(`/conversation/user/${userId}`),
// 删除会话
deleteSession: (sessionId: string): Promise<void> =>
request.delete(`/chat/session/${sessionId}`),
deleteSession: (id: string): Promise<void> =>
request.delete(`/conversation/${id}`),
// 更新会话标题
updateSessionTitle: (sessionId: string, title: string): Promise<ChatSession> =>
request.put(`/chat/session/${sessionId}`, { title }),
updateSessionTitle: (id: string, title: string): Promise<ChatSession> =>
request.put(`/conversation/${id}`, { title }),
// 搜索消息
searchMessages: (keyword: string, sessionId?: string): Promise<ChatMessage[]> =>
request.get('/chat/search', { params: { keyword, sessionId } }),
// 获取会话消息分页
getSessionMessages: (conversationId: string, params: { page: number, size: number }): Promise<PaginatedResponse<ChatMessage>> =>
request.get(`/message/conversation/${conversationId}/page`, { params }),
// 获取聊天统计
getChatStats: (): Promise<{
totalSessions: number
totalMessages: number
todayMessages: number
}> =>
request.get('/chat/stats'),
// 获取会话所有消息
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 } }),
}
+60 -11
View File
@@ -2,7 +2,7 @@ import SockJS from 'sockjs-client'
import * as Stomp from 'stompjs'
import type { ChatMessage } from '@/types'
// WebSocket消息类型
// WebSocket消息类型 - 与后端保持一致
export interface WebSocketMessage {
messageId: string
conversationId?: string
@@ -15,7 +15,7 @@ export interface WebSocketMessage {
data?: any
}
// 聊天请求类型
// 聊天请求类型 - 与后端ChatRequest保持一致
export interface ChatRequest {
content: string
senderId: string
@@ -82,11 +82,17 @@ export class WebSocketService {
this.client.heartbeat.outgoing = 20000
this.client.heartbeat.incoming = 20000
// 连接配置
const connectHeaders = {
// 连接配置 - 添加token支持
const connectHeaders: any = {
'X-User-Id': this.userId
}
// 如果有token,添加到连接头中
const token = localStorage.getItem('token')
if (token) {
connectHeaders['Authorization'] = `Bearer ${token}`
}
this.client.connect(
connectHeaders,
(frame) => {
@@ -109,13 +115,37 @@ export class WebSocketService {
(error) => {
console.error('WebSocket连接失败:', error)
this.setStatus('ERROR')
this.callbacks.onError?.(error)
// 检查是否是网络错误
if (error && error.type === 'close' && error.code === 1006) {
console.log('WebSocket连接被异常关闭,尝试重连...')
// 详细的错误处理
let errorMessage = '连接失败'
if (error) {
if (error.type === 'close') {
switch (error.code) {
case 1006:
errorMessage = '连接异常断开,正在重连...'
break
case 1000:
errorMessage = '连接正常关闭'
break
case 1001:
errorMessage = '服务器正在重启,请稍后重试'
break
case 1002:
errorMessage = '协议错误'
break
case 1003:
errorMessage = '数据格式错误'
break
default:
errorMessage = `连接关闭 (代码: ${error.code})`
}
} else if (error.message) {
errorMessage = error.message
}
}
this.callbacks.onError?.({ ...error, userMessage: errorMessage })
// 尝试重连
this.scheduleReconnect()
reject(error)
@@ -150,13 +180,21 @@ export class WebSocketService {
*/
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: content.trim(),
senderId: this.userId!,
senderType: this.userId?.startsWith('guest_') ? 'GUEST' : 'USER',
messageType: 'TEXT',
@@ -169,7 +207,10 @@ export class WebSocketService {
console.log('发送聊天消息:', chatRequest)
} catch (error) {
console.error('发送消息失败:', error)
this.callbacks.onError?.(error)
this.callbacks.onError?.({
userMessage: '消息发送失败,请重试',
originalError: error
})
}
}
@@ -178,6 +219,14 @@ export class WebSocketService {
*/
setConversationId(conversationId: string): void {
this.conversationId = conversationId
console.log('WebSocket会话ID已更新:', conversationId)
}
/**
* 获取当前会话ID
*/
getConversationId(): string | null {
return this.conversationId
}
/**
@@ -319,7 +368,7 @@ export class WebSocketService {
}
// 创建WebSocket服务实例
const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:19000/ws/chat'
const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:19089/ws/chat'
export const webSocketService = new WebSocketService(wsUrl)
export default webSocketService
+127 -40
View File
@@ -3,6 +3,7 @@ import { ref, watch } from 'vue'
import type { ChatMessage, ChatSession } from '@/types'
import webSocketService, { type WebSocketMessage, type ConnectionStatus } from '@/services/websocket'
import { useUserStore } from './user'
import { chatApi } from '@/services/chat'
export const useChatStore = defineStore('chat', () => {
const userStore = useUserStore()
@@ -21,12 +22,25 @@ export const useChatStore = defineStore('chat', () => {
const newMessage: ChatMessage = {
...message,
id: Date.now().toString(),
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
status: message.type === 'user' ? 'sending' : 'sent'
}
messages.value.push(newMessage)
return newMessage
}
// 更新消息状态
const updateMessageStatus = (messageId: string, status: ChatMessage['status'], error?: string) => {
const message = messages.value.find(m => m.id === messageId)
if (message) {
message.status = status
if (error) {
message.error = error
}
}
}
// 发送消息:WebSocket推送+数据库保存
const sendMessage = async (content: string) => {
if (!wsConnected.value) {
console.error('WebSocket未连接,无法发送消息')
@@ -46,69 +60,110 @@ export const useChatStore = defineStore('chat', () => {
})
try {
// 通过WebSocket发送消息
// WebSocket推送
webSocketService.sendChatMessage(content, currentSession.value?.id)
console.log('消息已通过WebSocket发送:', content)
// 更新消息状态为已发送
updateMessageStatus(userMessage.id, 'sent')
// 数据库保存
if (currentSession.value?.id && userStore.user?.id) {
await chatApi.createMessage({
conversationId: currentSession.value.id,
userId: userStore.user.id,
content,
contentType: 'TEXT',
senderType: 'USER',
senderId: userStore.user.id
})
// 更新消息状态为已送达
updateMessageStatus(userMessage.id, 'delivered')
}
} catch (error) {
console.error('WebSocket发送消息失败:', error)
console.error('消息发送或保存失败:', error)
// 更新消息状态为失败
updateMessageStatus(userMessage.id, 'failed', '发送失败')
addMessage({
content: '抱歉,消息发送失败,请稍后重试。',
type: 'ai',
sessionId: currentSession.value?.id
})
}
return userMessage
}
const createSession = (title?: string) => {
const newSession: ChatSession = {
id: Date.now().toString(),
title: title || `对话 ${sessions.value.length + 1}`,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
messageCount: 0
// 创建会话:同步后端
const createSession = async (title?: string) => {
let newSession: ChatSession
if (userStore.user?.id) {
newSession = await chatApi.createSession(userStore.user.id, title || `对话${sessions.value.length + 1}`)
} else {
newSession = {
id: Date.now().toString(),
title: title || `对话${sessions.value.length + 1}`,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
messageCount: 0
}
}
sessions.value.unshift(newSession)
currentSession.value = newSession
messages.value = []
// 如果WebSocket已连接,设置新的会话ID
if (wsConnected.value) {
webSocketService.setConversationId(newSession.id)
}
return newSession
}
const switchSession = (sessionId: string) => {
// 切换会话:加载消息
const switchSession = async (sessionId: string) => {
const session = sessions.value.find(s => s.id === sessionId)
if (session) {
currentSession.value = session
// TODO: 加载该会话的消息
loadSessionMessages(sessionId)
await loadSessionMessages(sessionId)
// 如果WebSocket已连接,更新会话ID
if (wsConnected.value) {
webSocketService.setConversationId(sessionId)
}
}
}
// 加载会话消息:从后端获取
const loadSessionMessages = async (sessionId: string) => {
try {
// TODO: 从API加载消息
// const response = await chatApi.getSessionMessages(sessionId)
// messages.value = response.data
// 临时模拟数据
messages.value = []
const msgs = await chatApi.getAllSessionMessages(sessionId)
messages.value = msgs
} catch (error) {
console.error('Failed to load session messages:', error)
messages.value = []
}
}
const deleteSession = (sessionId: string) => {
const index = sessions.value.findIndex(s => s.id === sessionId)
if (index > -1) {
sessions.value.splice(index, 1)
if (currentSession.value?.id === sessionId) {
currentSession.value = sessions.value[0] || null
if (currentSession.value) {
loadSessionMessages(currentSession.value.id)
} else {
messages.value = []
// 删除会话:同步后端
const deleteSession = async (sessionId: string) => {
try {
await chatApi.deleteSession(sessionId)
const index = sessions.value.findIndex(s => s.id === sessionId)
if (index > -1) {
sessions.value.splice(index, 1)
if (currentSession.value?.id === sessionId) {
currentSession.value = sessions.value[0] || null
if (currentSession.value) {
await loadSessionMessages(currentSession.value.id)
} else {
messages.value = []
}
}
}
} catch (error) {
console.error('删除会话失败:', error)
}
}
@@ -122,6 +177,33 @@ export const useChatStore = defineStore('chat', () => {
)
}
// 分割AI回复为多条消息
const splitAiReply = (content: string): string[] => {
// 先按 \n\n 分割,再按 \n 分割
const segments = content.split(/\n\n|\n/).filter(segment => segment.trim().length > 0)
return segments
}
// 添加AI回复消息(支持分段显示)
const addAiReplyMessages = (content: string, delay: number = 1000) => {
const segments = splitAiReply(content)
segments.forEach((segment, index) => {
setTimeout(() => {
addMessage({
content: segment.trim(),
type: 'ai',
sessionId: currentSession.value?.id
})
// 最后一条消息后停止输入状态
if (index === segments.length - 1) {
isTyping.value = false
}
}, index * delay)
})
}
// WebSocket消息处理
const handleWebSocketMessage = (wsMessage: WebSocketMessage) => {
console.log('处理WebSocket消息:', wsMessage)
@@ -129,13 +211,8 @@ export const useChatStore = defineStore('chat', () => {
switch (wsMessage.type) {
case 'TEXT':
if (wsMessage.senderType === 'AI') {
// AI回复消息
addMessage({
content: wsMessage.content,
type: 'ai',
sessionId: currentSession.value?.id
})
isTyping.value = false
// AI回复消息 - 支持分段显示
addAiReplyMessages(wsMessage.content)
}
break
@@ -176,7 +253,8 @@ export const useChatStore = defineStore('chat', () => {
// WebSocket连接管理
const connectWebSocket = async () => {
try {
const userId = userStore.user?.id || undefined
// 优先使用userInfo中的用户ID,如果没有则使用user中的ID
const userId = userStore.userInfo?.id || userStore.user?.id || undefined
await webSocketService.connect(userId, {
onMessage: handleWebSocketMessage,
@@ -201,6 +279,15 @@ export const useChatStore = defineStore('chat', () => {
wsConnected.value = false
isConnected.value = false
isTyping.value = false
// 显示用户友好的错误信息
if (error.userMessage) {
addMessage({
content: error.userMessage,
type: 'ai',
sessionId: currentSession.value?.id
})
}
},
onStatusChange: (status) => {
connectionStatus.value = status
@@ -225,7 +312,7 @@ export const useChatStore = defineStore('chat', () => {
const initChat = async () => {
// 如果没有会话,创建一个默认会话
if (sessions.value.length === 0) {
createSession('与开开的对话')
await createSession('与开开的对话')
}
// 连接WebSocket
+5 -19
View File
@@ -41,26 +41,12 @@ export const useUserStore = defineStore('user', () => {
const loginWithAuth = async (loginData: LoginRequest) => {
isLoading.value = true
try {
const response = await authService.login(loginData)
console.log('登录API响应:', response)
// 修复:直接处理后端返回的数据格式 {code: 200, data: {...}}
if (response.code === 200 && response.data) {
// 使用store的方法来设置token和用户信息,确保响应式更新
setToken(response.data.accessToken)
setUserInfo(response.data.userInfo)
console.log('登录成功,用户信息已保存:', response.data.userInfo)
console.log('Token已保存:', response.data.accessToken.substring(0, 20) + '...')
return { code: 200, data: response.data, message: response.message }
} else {
return { code: response.code || 500, message: response.message || '登录失败' }
}
const data = await authService.login(loginData)
setToken(data.accessToken)
setUserInfo(data.userInfo)
return data
} catch (error: any) {
console.error('登录请求失败:', error)
return { code: 500, message: error.message || '登录失败' }
throw error
} finally {
isLoading.value = false
}
+3 -3
View File
@@ -41,9 +41,9 @@ export interface LoginResponse {
// 验证码响应
export interface CaptchaResponse {
key: string
image: string
expireTime: number
captchaKey: string
captchaImage: string
expiresIn: number
}
// API响应基础结构
+2
View File
@@ -17,6 +17,8 @@ export interface ChatMessage {
type: 'user' | 'ai'
timestamp: string
sessionId?: string
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'
error?: string
}
// 聊天会话类型
+12 -7
View File
@@ -53,13 +53,18 @@ request.interceptors.request.use(
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
console.log('收到响应:', {
url: response.config.url,
status: response.status,
data: response.data
})
return response
const { data } = response
// 标准后端格式: { code, message, data, timestamp }
if (typeof data === 'object' && data !== null && 'code' in data) {
if (data.code !== 200) {
message.error(data.message || '请求失败')
return Promise.reject(new Error(data.message || '请求失败'))
}
// 只返回data字段, 兼容验证码等所有接口
return data.data
}
// 兼容极特殊情况(如验证码图片流等)
return data
},
(error) => {
console.error('响应拦截器错误:', error)
+141
View File
@@ -0,0 +1,141 @@
/**
* WebSocket连接测试工具
* 用于测试WebSocket连接和消息发送功能
*/
import webSocketService from '@/services/websocket'
export class WebSocketTester {
private isConnected = false
private testResults: string[] = []
/**
* 运行WebSocket连接测试
*/
async runConnectionTest(): Promise<boolean> {
this.testResults = []
this.log('开始WebSocket连接测试...')
try {
// 测试连接
await webSocketService.connect('test_user_' + Date.now(), {
onConnect: () => {
this.isConnected = true
this.log('✅ WebSocket连接成功')
},
onDisconnect: () => {
this.isConnected = false
this.log('❌ WebSocket连接断开')
},
onError: (error) => {
this.log(`❌ WebSocket错误: ${error.userMessage || error.message || '未知错误'}`)
},
onMessage: (message) => {
this.log(`📨 收到消息: ${message.type} - ${message.content}`)
}
})
// 等待连接建立
await this.waitForConnection(5000)
if (this.isConnected) {
this.log('✅ 连接测试通过')
return true
} else {
this.log('❌ 连接测试失败')
return false
}
} catch (error) {
this.log(`❌ 连接测试异常: ${error}`)
return false
}
}
/**
* 测试消息发送
*/
async testMessageSending(): Promise<boolean> {
if (!this.isConnected) {
this.log('❌ 未连接,无法测试消息发送')
return false
}
try {
this.log('开始测试消息发送...')
// 设置测试会话ID
webSocketService.setConversationId('test_conversation_' + Date.now())
// 发送测试消息
webSocketService.sendChatMessage('这是一条测试消息')
this.log('✅ 消息发送成功')
return true
} catch (error) {
this.log(`❌ 消息发送失败: ${error}`)
return false
}
}
/**
* 断开连接测试
*/
testDisconnection(): void {
this.log('开始测试断开连接...')
webSocketService.disconnect()
this.log('✅ 断开连接完成')
}
/**
* 获取测试结果
*/
getTestResults(): string[] {
return [...this.testResults]
}
/**
* 清空测试结果
*/
clearResults(): void {
this.testResults = []
}
/**
* 记录测试日志
*/
private log(message: string): void {
const timestamp = new Date().toLocaleTimeString()
const logMessage = `[${timestamp}] ${message}`
this.testResults.push(logMessage)
console.log(logMessage)
}
/**
* 等待连接建立
*/
private waitForConnection(timeout: number): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now()
const checkConnection = () => {
if (this.isConnected) {
resolve()
} else if (Date.now() - startTime > timeout) {
reject(new Error('连接超时'))
} else {
setTimeout(checkConnection, 100)
}
}
checkConnection()
})
}
}
// 导出测试实例
export const wsTest = new WebSocketTester()
// 开发环境下添加到全局对象,方便调试
if (import.meta.env.DEV) {
(window as any).wsTest = wsTest
}
+55 -4
View File
@@ -74,7 +74,16 @@
</div>
<div class="message-content">
<div class="message-text">{{ message.content }}</div>
<div class="message-time">{{ formatTime.friendly(message.timestamp) }}</div>
<div class="message-meta">
<span class="message-time">{{ formatTime.friendly(message.timestamp) }}</span>
<span v-if="message.type === 'user' && message.status" class="message-status" :class="message.status">
<template v-if="message.status === 'sending'">发送中</template>
<template v-else-if="message.status === 'sent'">已发送</template>
<template v-else-if="message.status === 'delivered'">已送达</template>
<template v-else-if="message.status === 'read'">已读</template>
<template v-else-if="message.status === 'failed'">发送失败</template>
</span>
</div>
</div>
</div>
</div>
@@ -453,19 +462,61 @@
word-wrap: break-word;
}
.message-meta {
display: flex;
align-items: center;
gap: $spacing-xs;
margin-top: $spacing-xs;
}
.message-time {
font-size: $font-size-xs;
color: rgba(255, 255, 255, 0.7);
margin-top: $spacing-xs;
.user-message & {
color: rgba(255, 255, 255, 0.7);
}
.message-wrapper:not(.user-message) & {
color: $text-medium;
}
}
.message-status {
font-size: $font-size-xs;
padding: 2px 6px;
border-radius: 10px;
&.sending {
color: #faad14;
background: rgba(250, 173, 20, 0.1);
}
&.sent {
color: #52c41a;
background: rgba(82, 196, 26, 0.1);
}
&.delivered {
color: #1890ff;
background: rgba(24, 144, 255, 0.1);
}
&.read {
color: #722ed1;
background: rgba(114, 46, 209, 0.1);
}
&.failed {
color: #ff4d4f;
background: rgba(255, 77, 79, 0.1);
}
.user-message & {
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.2);
}
}
}
.typing-indicator {
+18 -44
View File
@@ -139,8 +139,8 @@
try {
const response = await authService.getCaptcha()
console.log('验证码响应:', response)
captchaImage.value = `data:image/png;base64,${response.image}`
captchaKey.value = response.key
captchaImage.value = response.captchaImage // 修正字段
captchaKey.value = response.captchaKey // 修正字段
console.log('验证码图片URL:', captchaImage.value.substring(0, 50) + '...')
} catch (error) {
console.error('获取验证码失败:', error)
@@ -161,51 +161,25 @@
...values,
captchaKey: captchaKey.value
}
const result = await userStore.loginWithAuth(loginData)
console.log('登录结果:', result)
console.log('用户store状态:', {
token: userStore.token,
userInfo: userStore.userInfo,
isLoggedIn: userStore.isLoggedIn
})
// 修复:检查 result.code === 200 而不是 result.success
if (result.code === 200) {
message.success('登录成功')
// 等待状态更新后再跳转
await nextTick()
// 跳转到首页或之前的页面
const redirect = router.currentRoute.value.query.redirect as string
const targetPath = redirect || '/'
console.log('准备跳转到:', targetPath)
// 延迟一下确保状态完全更新
setTimeout(() => {
try {
// 使用replace而不是push,避免路由守卫问题
router.replace(targetPath).then(() => {
console.log('路由跳转完成')
}).catch((error) => {
console.error('路由跳转失败,使用window.location:', error)
// 如果路由跳转失败,使用window.location作为备选
window.location.href = targetPath
})
} catch (error) {
console.error('路由跳转异常,使用window.location:', error)
const data = await userStore.loginWithAuth(loginData)
message.success('登录成功')
await nextTick()
const redirect = router.currentRoute.value.query.redirect as string
const targetPath = redirect || '/'
setTimeout(() => {
try {
router.replace(targetPath).then(() => {
console.log('路由跳转完成')
}).catch((error) => {
window.location.href = targetPath
}
}, 100)
} else {
message.error(result.message || '登录失败')
refreshCaptcha() // 刷新验证码
}
})
} catch (error) {
window.location.href = targetPath
}
}, 100)
} catch (error: any) {
message.error(error.message || '登录失败,请稍后重试')
refreshCaptcha() // 刷新验证码
refreshCaptcha()
} finally {
loginLoading.value = false
}
+8 -22
View File
@@ -156,8 +156,8 @@
try {
const response = await authService.getCaptcha()
console.log('验证码响应:', response)
captchaImage.value = `data:image/png;base64,${response.image}`
captchaKey.value = response.key
captchaImage.value = response.captchaImage // 修正字段
captchaKey.value = response.captchaKey // 修正字段
console.log('验证码图片URL:', captchaImage.value.substring(0, 50) + '...')
} catch (error) {
console.error('获取验证码失败:', error)
@@ -178,28 +178,14 @@
...values,
captchaKey: captchaKey.value
}
const response = await authService.register(registerData)
if (response.success) {
message.success('注册成功,已自动登录')
// 使用userStore的方法保存用户信息和token
userStore.setToken(response.data.accessToken)
userStore.setUserInfo(response.data.userInfo)
console.log('注册成功,用户信息:', response.data.userInfo)
console.log('Token已保存:', response.data.accessToken.substring(0, 20) + '...')
// 跳转到首页
router.push('/')
} else {
message.error(response.message || '注册失败')
refreshCaptcha() // 刷新验证码
}
const data = await authService.register(registerData)
message.success('注册成功,已自动登录')
userStore.setToken(data.accessToken)
userStore.setUserInfo(data.userInfo)
router.push('/')
} catch (error: any) {
message.error(error.message || '注册失败,请稍后重试')
refreshCaptcha() // 刷新验证码
refreshCaptcha()
} finally {
registerLoading.value = false
}
+185
View File
@@ -0,0 +1,185 @@
<template>
<div class="token-test">
<a-card title="Token和身份验证测试">
<div class="test-section">
<h3>当前状态</h3>
<a-descriptions :column="1" bordered>
<a-descriptions-item label="登录状态">
<a-tag :color="userStore.isLoggedIn ? 'green' : 'red'">
{{ userStore.isLoggedIn ? '已登录' : '未登录' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="Token">
<a-typography-text :code="true" :copyable="true">
{{ userStore.token || '无' }}
</a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="用户信息">
<pre>{{ JSON.stringify(userStore.userInfo || userStore.user, null, 2) }}</pre>
</a-descriptions-item>
<a-descriptions-item label="WebSocket状态">
<a-tag :color="chatStore.wsConnected ? 'green' : 'red'">
{{ chatStore.wsConnected ? '已连接' : '未连接' }}
</a-tag>
</a-descriptions-item>
</a-descriptions>
</div>
<div class="test-section">
<h3>操作测试</h3>
<a-space direction="vertical" style="width: 100%">
<a-button type="primary" @click="testLogin" :loading="loginLoading">
测试登录
</a-button>
<a-button @click="testWebSocketConnect" :loading="wsLoading">
测试WebSocket连接
</a-button>
<a-button @click="testSendMessage" :disabled="!chatStore.wsConnected">
发送测试消息
</a-button>
<a-button @click="checkLocalStorage">
检查本地存储
</a-button>
<a-button @click="testApiCall" :loading="apiLoading">
测试API调用
</a-button>
</a-space>
</div>
<div class="test-section">
<h3>测试结果</h3>
<a-textarea
v-model:value="testResults"
:rows="10"
readonly
placeholder="测试结果将显示在这里..."
/>
<a-button @click="clearResults" style="margin-top: 8px">
清空结果
</a-button>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useUserStore, useChatStore } from '@/stores'
import { request } from '@/services/api'
const userStore = useUserStore()
const chatStore = useChatStore()
const loginLoading = ref(false)
const wsLoading = ref(false)
const apiLoading = ref(false)
const testResults = ref('')
const addResult = (message: string) => {
const timestamp = new Date().toLocaleTimeString()
testResults.value += `[${timestamp}] ${message}\n`
}
const testLogin = async () => {
loginLoading.value = true
try {
addResult('开始测试登录...')
const result = await userStore.loginWithAuth({
account: 'test@example.com',
password: '123456',
captcha: '1234'
})
addResult(`登录成功: ${JSON.stringify(result)}`)
addResult(`Token: ${userStore.token}`)
addResult(`用户信息: ${JSON.stringify(userStore.userInfo)}`)
} catch (error: any) {
addResult(`登录失败: ${error.message}`)
} finally {
loginLoading.value = false
}
}
const testWebSocketConnect = async () => {
wsLoading.value = true
try {
addResult('开始测试WebSocket连接...')
await chatStore.connectWebSocket()
addResult(`WebSocket连接状态: ${chatStore.wsConnected}`)
addResult(`连接状态: ${chatStore.connectionStatus}`)
} catch (error: any) {
addResult(`WebSocket连接失败: ${error.message}`)
} finally {
wsLoading.value = false
}
}
const testSendMessage = async () => {
try {
addResult('发送测试消息...')
await chatStore.sendMessage('这是一条测试消息,用于验证用户身份识别')
addResult('消息发送成功')
} catch (error: any) {
addResult(`消息发送失败: ${error.message}`)
}
}
const checkLocalStorage = () => {
addResult('检查本地存储...')
addResult(`localStorage.token: ${localStorage.getItem('token')}`)
addResult(`localStorage.userInfo: ${localStorage.getItem('userInfo')}`)
}
const testApiCall = async () => {
apiLoading.value = true
try {
addResult('测试API调用...')
const response = await request.get('/health')
addResult(`API调用成功: ${JSON.stringify(response)}`)
} catch (error: any) {
addResult(`API调用失败: ${error.message}`)
} finally {
apiLoading.value = false
}
}
const clearResults = () => {
testResults.value = ''
}
</script>
<style lang="scss" scoped>
.token-test {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.test-section {
margin-bottom: 24px;
h3 {
margin-bottom: 16px;
color: #1890ff;
}
}
pre {
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
}
</style>
+1 -1
View File
@@ -28,7 +28,7 @@ export default defineConfig({
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
target: 'http://localhost:19089',
changeOrigin: true,
rewrite: (path) => path
}