feat: 完成Nacos配置优化和WebSocket集成

主要更新:
1. 统一所有微服务端口配置(19000-19008)
2. 为所有服务创建本地/测试/生产三套环境配置
3. 配置Nacos认证密码(本地:Peanut2817*#, 测试/生产:EmotionMuseum2025)
4. 优化网关路由配置,支持负载均衡和WebSocket
5. 新增emotion-websocket模块,支持实时聊天
6. 前端集成WebSocket,替代HTTP轮询
7. 添加配置验证和管理工具脚本

技术特性:
- 完整的环境隔离和服务发现
- WebSocket实时通信支持
- 负载均衡路由配置
- 跨域和安全配置
- 自动重连和心跳检测
This commit is contained in:
2025-07-17 18:10:45 +08:00
parent 9a3a8267b5
commit c77352877d
391 changed files with 46585 additions and 4294 deletions
+116
View File
@@ -0,0 +1,116 @@
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 Error:', data.message)
return Promise.reject(new Error(data.message))
}
return response
},
(error) => {
// 处理HTTP错误
if (error.response) {
const { status, data } = error.response
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)
}
export default api
+150
View File
@@ -0,0 +1,150 @@
import axios from 'axios'
import type {
LoginRequest,
LoginResponse,
RegisterRequest,
CaptchaResponse,
ApiResponse,
RefreshTokenRequest,
ChangePasswordRequest,
ForgotPasswordRequest,
ResetPasswordRequest,
UserInfo
} from '@/types/auth'
// 创建axios实例
const authApi = axios.create({
baseURL: '/api/auth',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
authApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
authApi.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
if (error.response?.status === 401) {
// token过期,清除本地存储并跳转到登录页
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
window.location.href = '/login'
}
return Promise.reject(error.response?.data || error)
}
)
export const authService = {
// 获取验证码
async getCaptcha(): Promise<CaptchaResponse> {
const response: ApiResponse<CaptchaResponse> = await authApi.get('/captcha')
return response.data
},
// 用户登录
async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> {
return await authApi.post('/login', data)
},
// 用户注册
async register(data: RegisterRequest): Promise<ApiResponse<UserInfo>> {
return await authApi.post('/register', data)
},
// 刷新token
async refreshToken(data: RefreshTokenRequest): Promise<ApiResponse<LoginResponse>> {
return await authApi.post('/refresh-token', data)
},
// 用户登出
async logout(): Promise<ApiResponse<void>> {
return await authApi.post('/logout')
},
// 获取用户信息
async getUserInfo(): Promise<ApiResponse<UserInfo>> {
return await authApi.get('/user-info')
},
// 修改密码
async changePassword(data: ChangePasswordRequest): Promise<ApiResponse<void>> {
return await authApi.post('/change-password', data)
},
// 忘记密码
async forgotPassword(data: ForgotPasswordRequest): Promise<ApiResponse<void>> {
return await authApi.post('/forgot-password', data)
},
// 重置密码
async resetPassword(data: ResetPasswordRequest): Promise<ApiResponse<void>> {
return await authApi.post('/reset-password', data)
},
// 验证token有效性
async validateToken(): Promise<ApiResponse<boolean>> {
return await authApi.get('/validate-token')
},
// 检查账号是否存在
async checkAccount(account: string): Promise<ApiResponse<boolean>> {
return await authApi.get(`/check-account?account=${account}`)
}
}
// 工具函数
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()
}
}
+40
View File
@@ -0,0 +1,40 @@
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 }),
// 获取会话列表
getSessions: (): Promise<ChatSession[]> =>
request.get('/chat/sessions'),
// 创建新会话
createSession: (title?: string): Promise<ChatSession> =>
request.post('/chat/session', { title }),
// 获取会话消息
getSessionMessages: (sessionId: string, page = 1, size = 50): Promise<PaginatedResponse<ChatMessage>> =>
request.get(`/chat/session/${sessionId}/messages`, { params: { page, size } }),
// 删除会话
deleteSession: (sessionId: string): Promise<void> =>
request.delete(`/chat/session/${sessionId}`),
// 更新会话标题
updateSessionTitle: (sessionId: string, title: string): Promise<ChatSession> =>
request.put(`/chat/session/${sessionId}`, { title }),
// 搜索消息
searchMessages: (keyword: string, sessionId?: string): Promise<ChatMessage[]> =>
request.get('/chat/search', { params: { keyword, sessionId } }),
// 获取聊天统计
getChatStats: (): Promise<{
totalSessions: number
totalMessages: number
todayMessages: number
}> =>
request.get('/chat/stats'),
}
+293
View File
@@ -0,0 +1,293 @@
import SockJS from 'sockjs-client'
import { Stomp, Client } 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
}
// 聊天请求类型
export interface ChatRequest {
conversationId?: string
content: string
senderId: string
senderType: 'USER' | 'GUEST'
messageType: 'TEXT'
}
// 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: 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)
this.client = Stomp.over(socket)
// 禁用调试日志
this.client.debug = () => {}
// 连接配置
const connectHeaders = {
'X-User-Id': this.userId
}
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')
this.callbacks.onError?.(error)
// 尝试重连
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) {
console.error('WebSocket未连接')
return
}
const chatRequest: ChatRequest = {
content,
senderId: this.userId!,
senderType: this.userId?.startsWith('guest_') ? 'GUEST' : 'USER',
messageType: 'TEXT',
conversationId: conversationId || this.conversationId || undefined
}
try {
this.client.send('/app/chat.send', {}, JSON.stringify(chatRequest))
console.log('发送聊天消息:', chatRequest)
} catch (error) {
console.error('发送消息失败:', error)
this.callbacks.onError?.(error)
}
}
/**
* 设置会话ID
*/
setConversationId(conversationId: string): void {
this.conversationId = conversationId
}
/**
* 获取连接状态
*/
getStatus(): ConnectionStatus {
return this.status
}
/**
* 检查是否已连接
*/
isConnected(): boolean {
return this.status === 'CONNECTED' && this.client?.connected === true
}
/**
* 订阅消息
*/
private subscribeToMessages(): void {
if (!this.client?.connected) return
// 订阅用户私有消息
this.client.subscribe('/user/queue/messages', (message) => {
try {
const wsMessage: WebSocketMessage = JSON.parse(message.body)
console.log('收到WebSocket消息:', wsMessage)
this.callbacks.onMessage?.(wsMessage)
} catch (error) {
console.error('解析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)
}
})
}
/**
* 发送连接消息
*/
private sendConnectMessage(): void {
if (!this.client?.connected) return
try {
this.client.send('/app/chat.connect', {}, JSON.stringify({}))
} 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:19000/ws/chat'
export const webSocketService = new WebSocketService(wsUrl)
export default webSocketService