Files
cunkebao_v3/Touchkebao/src/store/module/websocket/websocket.ts

587 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createPersistStore } from "@/store/createPersistStore";
import { Toast } from "antd-mobile";
import { useUserStore } from "../user";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
const { getAccountId } = useCkChatStore.getState();
import { msgManageCore } from "./msgManage";
// WebSocket消息类型
export interface WebSocketMessage {
cmdType?: string;
seq?: number;
wechatAccountIds?: string[];
content?: any;
[key: string]: any;
}
// WebSocket连接状态
export enum WebSocketStatus {
DISCONNECTED = "disconnected",
CONNECTING = "connecting",
CONNECTED = "connected",
RECONNECTING = "reconnecting",
ERROR = "error",
}
// WebSocket配置
interface WebSocketConfig {
url: string;
client: string;
accountId: number;
accessToken: string;
autoReconnect: boolean;
cmdType: string;
seq: number;
reconnectInterval: number;
maxReconnectAttempts: number;
[key: string]: any;
}
interface WebSocketState {
// 连接状态
status: WebSocketStatus;
ws: WebSocket | null;
// 配置信息
config: WebSocketConfig | null;
// 消息相关
messages: WebSocketMessage[];
unreadCount: number;
// 重连相关
reconnectAttempts: number;
reconnectTimer: NodeJS.Timeout | null;
aliveStatusTimer: NodeJS.Timeout | null; // 客服用户状态查询定时器
// 方法
connect: (config: Partial<WebSocketConfig>) => void;
disconnect: () => void;
sendMessage: (message: Omit<WebSocketMessage, "id" | "timestamp">) => void;
sendCommand: (cmdType: string, data?: any) => void;
clearMessages: () => void;
markAsRead: () => void;
reconnect: () => void;
clearConnectionState: () => void; // 清空连接状态
// 内部方法
_handleOpen: () => void;
_handleMessage: (event: MessageEvent) => void;
_handleClose: (event: CloseEvent) => void;
_handleError: (event: Event) => void;
_startReconnectTimer: () => void;
_stopReconnectTimer: () => void;
_startAliveStatusTimer: () => void; // 启动客服状态查询定时器
_stopAliveStatusTimer: () => void; // 停止客服状态查询定时器
}
// 默认配置
const DEFAULT_CONFIG: WebSocketConfig = {
url: "wss://kf.quwanzhi.com:9993",
client: "kefu-client",
accountId: 0,
accessToken: "",
autoReconnect: true,
cmdType: "", // 添加默认的命令类型
seq: +new Date(), // 添加默认的序列号
reconnectInterval: 3000,
maxReconnectAttempts: 5,
};
export const useWebSocketStore = createPersistStore<WebSocketState>(
(set, get) => ({
status: WebSocketStatus.DISCONNECTED,
ws: null,
config: null,
messages: [],
unreadCount: 0,
reconnectAttempts: 0,
reconnectTimer: null,
aliveStatusTimer: null,
// 连接WebSocket
connect: (config: Partial<WebSocketConfig>) => {
const currentState = get();
// 检查当前连接状态,避免重复连接
if (
currentState.status === WebSocketStatus.CONNECTED ||
currentState.status === WebSocketStatus.CONNECTING
) {
// console.log("WebSocket已连接或正在连接跳过重复连接", {
// currentStatus: currentState.status,
// hasWebSocket: !!currentState.ws,
// });
return;
}
// 如果已经有WebSocket实例先断开
if (currentState.ws) {
// console.log("断开现有WebSocket连接");
currentState.disconnect();
}
// 合并配置
const fullConfig: WebSocketConfig = {
...DEFAULT_CONFIG,
...config,
};
// 获取用户信息
const { token2 } = useUserStore.getState();
if (!token2) {
// Toast.show({ content: "未找到有效的访问令牌", position: "top" });
return;
}
// 构建WebSocket URL
const params = new URLSearchParams({
client: fullConfig.client.toString(),
accountId: getAccountId().toString(),
accessToken: token2,
t: Date.now().toString(),
});
const wsUrl = fullConfig.url + "?" + params;
// 检查URL是否为localhost如果是则不连接
if (wsUrl.includes("localhost") || wsUrl.includes("127.0.0.1")) {
// console.error("WebSocket连接被拦截不允许连接到本地地址", wsUrl);
// Toast.show({
// content: "WebSocket连接被拦截不允许连接到本地地址",
// position: "top",
// });
set({ status: WebSocketStatus.ERROR });
return;
}
set({
status: WebSocketStatus.CONNECTING,
config: fullConfig,
});
try {
const ws = new WebSocket(wsUrl);
// 绑定事件处理器
ws.onopen = () => get()._handleOpen();
ws.onmessage = event => get()._handleMessage(event);
ws.onclose = event => get()._handleClose(event);
ws.onerror = event => get()._handleError(event);
set({ ws });
// console.log("WebSocket连接创建成功", wsUrl);
} catch (error) {
// console.error("WebSocket连接失败:", error);
set({ status: WebSocketStatus.ERROR });
// Toast.show({ content: "WebSocket连接失败", position: "top" });
}
},
// 断开连接
disconnect: () => {
const currentState = get();
if (currentState.ws) {
currentState.ws.close();
}
currentState._stopReconnectTimer();
currentState._stopAliveStatusTimer();
set({
status: WebSocketStatus.DISCONNECTED,
ws: null,
reconnectAttempts: 0,
});
// console.log("WebSocket连接已断开");
},
// 发送消息
sendMessage: message => {
const currentState = get();
if (
currentState.status !== WebSocketStatus.CONNECTED ||
!currentState.ws
) {
// Toast.show({ content: "WebSocket未连接", position: "top" });
return;
}
const fullMessage: WebSocketMessage = {
...message,
};
try {
currentState.ws.send(JSON.stringify(fullMessage));
// console.log("消息发送成功:", fullMessage);
} catch (error) {
// console.error("消息发送失败:", error);
// Toast.show({ content: "消息发送失败", position: "top" });
}
},
// 发送命令
sendCommand: (cmdType: string, data?: any) => {
const currentState = get();
if (
currentState.status !== WebSocketStatus.CONNECTED ||
!currentState.ws
) {
// Toast.show({
// content: "WebSocket未连接正在重新连接...",
// position: "top",
// });
// 重置连接状态并发起重新连接
set({ status: WebSocketStatus.DISCONNECTED });
if (currentState.config) {
currentState.connect(currentState.config);
}
return;
}
const command = {
cmdType,
...data,
seq: +new Date(),
};
try {
currentState.ws.send(JSON.stringify(command));
// console.log("命令发送成功:", command);
} catch (error) {
// console.error("命令发送失败:", error);
// Toast.show({ content: "命令发送失败", position: "top" });
// 发送失败时也尝试重新连接
set({ status: WebSocketStatus.DISCONNECTED });
if (currentState.config) {
currentState.connect(currentState.config);
}
}
},
// 清除消息
clearMessages: () => {
set({ messages: [], unreadCount: 0 });
},
// 标记为已读
markAsRead: () => {
set({ unreadCount: 0 });
},
// 重连
reconnect: () => {
const currentState = get();
if (currentState.config) {
// 检查是否允许重连
if (!currentState.config.autoReconnect) {
// console.log("自动重连已禁用,不再尝试重连");
return;
}
currentState.connect(currentState.config);
}
},
// 清空连接状态(用于退出登录时)
clearConnectionState: () => {
const currentState = get();
// 断开现有连接
if (currentState.ws) {
currentState.ws.close();
}
// 停止所有定时器
currentState._stopReconnectTimer();
currentState._stopAliveStatusTimer();
// 重置所有状态
set({
status: WebSocketStatus.DISCONNECTED,
ws: null,
config: null,
messages: [],
unreadCount: 0,
reconnectAttempts: 0,
reconnectTimer: null,
aliveStatusTimer: null,
});
// console.log("WebSocket连接状态已清空");
},
// 内部方法:处理连接打开
_handleOpen: () => {
const currentState = get();
set({
status: WebSocketStatus.CONNECTED,
reconnectAttempts: 0,
});
// console.log("WebSocket连接成功");
const { token2 } = useUserStore.getState();
// 发送登录命令
if (currentState.config) {
currentState.sendCommand("CmdSignIn", {
accessToken: token2,
accountId: Number(getAccountId()),
client: currentState.config?.client || "kefu-client",
seq: +new Date(),
});
}
// Toast.show({ content: "WebSocket连接成功", position: "top" });
// 启动客服状态查询定时器
currentState._startAliveStatusTimer();
},
// 内部方法:处理消息接收
_handleMessage: (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
// console.log("收到WebSocket消息:", data);
// 处理特定的通知消息
if (data.cmdType === "CmdNotify") {
// 处理Auth failed通知
if (data.notify === "Auth failed" || data.notify === "Kicked out") {
// console.error(`WebSocket ${data.notify},断开连接`);
// Toast.show({
// content: `WebSocket ${data.notify},断开连接`,
// position: "top",
// });
// 禁用自动重连
if (get().config) {
set({
config: {
...get().config!,
autoReconnect: false,
},
});
}
// 停止客服状态查询定时器
get()._stopAliveStatusTimer();
// 断开连接
get().disconnect();
return;
}
}
const currentState = get();
const newMessage: WebSocketMessage = {
id: Date.now().toString(),
type: data.type || "message",
content: data,
timestamp: Date.now(),
sender: data.sender,
receiver: data.receiver,
};
set({
messages: [...currentState.messages, newMessage],
unreadCount: currentState.config.unreadCount + 1,
});
//消息处理器
msgManageCore(data);
// 可以在这里添加消息处理逻辑
// 比如播放提示音、显示通知等
} catch (error) {
// console.error("解析WebSocket消息失败:", error);
}
},
// 内部方法:处理连接关闭
_handleClose: (event: CloseEvent) => {
const currentState = get();
// console.log("WebSocket连接关闭:", event.code, event.reason);
set({
status: WebSocketStatus.DISCONNECTED,
ws: null,
});
// 自动重连逻辑
if (
currentState.config?.autoReconnect &&
currentState.reconnectAttempts <
(currentState.config?.maxReconnectAttempts || 5)
) {
// console.log("尝试自动重连...");
currentState._startReconnectTimer();
} else if (!currentState.config?.autoReconnect) {
// console.log("自动重连已禁用,不再尝试重连");
// 重置重连计数
set({ reconnectAttempts: 0 });
}
},
// 内部方法:处理连接错误
_handleError: (event: Event) => {
// console.error("WebSocket连接错误:", event);
set({ status: WebSocketStatus.ERROR });
// Toast.show({ content: "WebSocket连接错误", position: "top" });
},
// 内部方法:启动重连定时器
_startReconnectTimer: () => {
const currentState = get();
currentState._stopReconnectTimer();
set({
status: WebSocketStatus.RECONNECTING,
reconnectAttempts: currentState.reconnectAttempts + 1,
});
const timer = setTimeout(() => {
// console.log(
// `尝试重连 (${currentState.reconnectAttempts + 1}/${currentState.config?.maxReconnectAttempts})`,
// );
currentState.reconnect();
}, currentState.config?.reconnectInterval || 3000);
set({ reconnectTimer: timer });
},
// 内部方法:停止重连定时器
_stopReconnectTimer: () => {
const currentState = get();
if (currentState.reconnectTimer) {
clearTimeout(currentState.reconnectTimer);
set({ reconnectTimer: null });
}
},
// 内部方法:启动客服状态查询定时器
_startAliveStatusTimer: () => {
const currentState = get();
// 先停止现有定时器
currentState._stopAliveStatusTimer();
// 获取客服用户列表
const { kfUserList } = useCkChatStore.getState();
// 如果没有客服用户,不启动定时器
if (!kfUserList || kfUserList.length === 0) {
return;
}
// 启动定时器每5秒查询一次
const timer = setInterval(() => {
const state = get();
// 检查连接状态
if (state.status === WebSocketStatus.CONNECTED) {
const { kfUserList: currentKfUserList } = useCkChatStore.getState();
if (currentKfUserList && currentKfUserList.length > 0) {
state.sendCommand("CmdRequestWechatAccountsAliveStatus", {
wechatAccountIds: currentKfUserList.map(v => v.id),
});
}
} else {
// 如果连接断开,停止定时器
state._stopAliveStatusTimer();
}
}, 5 * 1000);
set({ aliveStatusTimer: timer });
},
// 内部方法:停止客服状态查询定时器
_stopAliveStatusTimer: () => {
const currentState = get();
if (currentState.aliveStatusTimer) {
clearInterval(currentState.aliveStatusTimer);
set({ aliveStatusTimer: null });
}
},
}),
{
name: "websocket-store",
partialize: state => ({
// 只持久化必要的状态不持久化WebSocket实例
status: state.status,
config: state.config,
messages: state.messages.slice(-100), // 只保留最近100条消息
unreadCount: state.unreadCount,
reconnectAttempts: state.reconnectAttempts,
// 注意:定时器不需要持久化,重新连接时会重新创建
}),
onRehydrateStorage: () => state => {
// 页面刷新后,如果之前是连接状态,尝试重新连接
if (state && state.status === WebSocketStatus.CONNECTED && state.config) {
// console.log("页面刷新后恢复WebSocket连接", {
// persistedConfig: state.config,
// currentDefaultConfig: DEFAULT_CONFIG,
// });
// 使用最新的默认配置,而不是持久化的配置
const freshConfig = {
...DEFAULT_CONFIG,
client: state.config.client,
accountId: state.config.accountId,
accessToken: state.config.accessToken,
autoReconnect: state.config.autoReconnect,
};
// console.log("使用刷新后的配置重连:", freshConfig);
// 延迟一下再重连,确保页面完全加载
// 同时检查当前状态,避免重复连接
setTimeout(() => {
// 重新获取最新的状态而不是使用闭包中的state
const currentState = useWebSocketStore.getState();
// console.log("页面刷新后检查状态", {
// status: currentState.status,
// hasWs: !!currentState.ws,
// });
// 强制重置状态为disconnected因为页面刷新后WebSocket实例已失效
if (
currentState.status === WebSocketStatus.CONNECTED &&
!currentState.ws
) {
// console.log("检测到状态不一致重置为disconnected");
useWebSocketStore.setState({
status: WebSocketStatus.DISCONNECTED,
});
}
// 重新获取状态进行连接
const latestState = useWebSocketStore.getState();
if (
latestState.status === WebSocketStatus.DISCONNECTED ||
latestState.status === WebSocketStatus.ERROR
) {
// console.log("页面刷新后开始重连");
latestState.connect(freshConfig);
} else {
// console.log("WebSocket已连接或正在连接跳过页面刷新重连", {
// status: latestState.status,
// });
}
}, 1000);
}
},
},
);