mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-08-06 10:57:25 +08:00
直播功能
This commit is contained in:
323
pages/live/utils/liveChat.js
Normal file
323
pages/live/utils/liveChat.js
Normal file
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* 直播腾讯云 IM(由 saas-uni-v3 useChat 迁移)
|
||||
*/
|
||||
import TencentCloudChat from "@tencentcloud/chat";
|
||||
import { fetchLiveSetting } from "./liveSetting.js";
|
||||
|
||||
const MAX_MESSAGE_COUNT = 100;
|
||||
|
||||
export class LiveChatService {
|
||||
constructor(options = {}) {
|
||||
this.liveId = options.liveId || "";
|
||||
this.userInfo = options.userInfo || {};
|
||||
this.defaultAvatar = options.defaultAvatar || "";
|
||||
this.onMessagesChange = options.onMessagesChange || (() => {});
|
||||
this.onScrollToBottom = options.onScrollToBottom || (() => {});
|
||||
|
||||
this.chat = null;
|
||||
this.groupID = "";
|
||||
this.messageList = [];
|
||||
this.messageIdSet = new Set();
|
||||
this.savedViewLiveResult = null;
|
||||
this.isAtBottom = true;
|
||||
this.unreadCount = 0;
|
||||
this.isLoadingHistory = false;
|
||||
this.hasMoreHistory = false;
|
||||
this.nextReqMessageID = "";
|
||||
|
||||
this._onMessageReceived = this.onMessageReceived.bind(this);
|
||||
this._onSdkReady = this.onSdkReady.bind(this);
|
||||
this._onSdkNotReady = this.onSdkNotReady.bind(this);
|
||||
this._onKickedOut = this.onKickedOut.bind(this);
|
||||
}
|
||||
|
||||
setGroupID(id) {
|
||||
this.groupID = id ? String(id) : "";
|
||||
}
|
||||
|
||||
async initTencentIm(viewLiveResult) {
|
||||
if (!this.groupID) {
|
||||
console.warn("groupID 不存在,跳过 IM 初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
const setting = await fetchLiveSetting();
|
||||
if (!setting?.imSdkAppid) {
|
||||
console.warn("IM SDKAppID 未配置,跳过 IM 初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewLiveResult?.userSig) {
|
||||
this.savedViewLiveResult = viewLiveResult;
|
||||
}
|
||||
|
||||
if (this.chat) {
|
||||
this.loginChat(viewLiveResult);
|
||||
return;
|
||||
}
|
||||
|
||||
const chatInstance = TencentCloudChat.create({
|
||||
SDKAppID: Number(setting.imSdkAppid),
|
||||
});
|
||||
this.chat = chatInstance;
|
||||
chatInstance.setLogLevel(1);
|
||||
|
||||
chatInstance.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, this._onMessageReceived);
|
||||
chatInstance.on(TencentCloudChat.EVENT.SDK_READY, this._onSdkReady);
|
||||
chatInstance.on(TencentCloudChat.EVENT.SDK_NOT_READY, this._onSdkNotReady);
|
||||
chatInstance.on(TencentCloudChat.EVENT.KICKED_OUT, this._onKickedOut);
|
||||
|
||||
this.loginChat(viewLiveResult);
|
||||
}
|
||||
|
||||
onSdkNotReady() {
|
||||
console.log("[SDK Not Ready,重新登录]");
|
||||
this.loginChat();
|
||||
}
|
||||
|
||||
loginChat(viewLiveResult) {
|
||||
if (!this.chat) return;
|
||||
|
||||
const source =
|
||||
(viewLiveResult?.userSig ? viewLiveResult : null) || this.savedViewLiveResult;
|
||||
const userId = source?.userId || this.userInfo?.id;
|
||||
const userSig = source?.userSig;
|
||||
|
||||
if (!userId || !userSig) {
|
||||
console.warn("用户ID或签名不存在", { userId, userSig });
|
||||
return;
|
||||
}
|
||||
|
||||
this.chat
|
||||
.login({
|
||||
userID: `${userId}`,
|
||||
userSig,
|
||||
})
|
||||
.then((imResponse) => {
|
||||
if (imResponse.data.repeatLogin === true) {
|
||||
this.joinGroup();
|
||||
this.getHistoryMessageList("");
|
||||
}
|
||||
})
|
||||
.catch((imError) => {
|
||||
console.warn("login error:", imError);
|
||||
});
|
||||
}
|
||||
|
||||
onSdkReady() {
|
||||
console.log("[SDK Ready]");
|
||||
this.updateUserInfo();
|
||||
this.joinGroup();
|
||||
this.getHistoryMessageList("");
|
||||
}
|
||||
|
||||
onKickedOut() {
|
||||
console.log("[用户被踢下线,重新登录]");
|
||||
this.loginChat();
|
||||
}
|
||||
|
||||
updateUserInfo() {
|
||||
if (!this.chat || !this.userInfo) return;
|
||||
|
||||
this.chat.updateMyProfile({
|
||||
nick: this.userInfo.nickName || this.userInfo.username || "用户",
|
||||
avatar: this.userInfo.face || this.defaultAvatar,
|
||||
gender: TencentCloudChat.TYPES.GENDER_UNKNOWN,
|
||||
allowType: TencentCloudChat.TYPES.ALLOW_TYPE_ALLOW_ANY,
|
||||
});
|
||||
}
|
||||
|
||||
joinGroup() {
|
||||
if (!this.chat || !this.groupID) return;
|
||||
|
||||
this.chat
|
||||
.joinGroup({
|
||||
groupID: this.groupID,
|
||||
type: TencentCloudChat.TYPES.GRP_AVCHATROOM,
|
||||
})
|
||||
.then((imResponse) => {
|
||||
switch (imResponse.data.status) {
|
||||
case TencentCloudChat.TYPES.JOIN_STATUS_SUCCESS:
|
||||
case TencentCloudChat.TYPES.JOIN_STATUS_ALREADY_IN_GROUP:
|
||||
this.getHistoryMessageList("");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
})
|
||||
.catch((imError) => {
|
||||
console.warn("joinGroup error:", imError);
|
||||
this.getHistoryMessageList("");
|
||||
});
|
||||
}
|
||||
|
||||
async getHistoryMessageList(nextReqId) {
|
||||
if (this.isLoadingHistory || !this.chat || !this.groupID) return;
|
||||
|
||||
this.isLoadingHistory = true;
|
||||
|
||||
const requestParams = {
|
||||
conversationID: `GROUP${this.groupID}`,
|
||||
direction: 0,
|
||||
};
|
||||
if (nextReqId) {
|
||||
requestParams.nextReqMessageID = nextReqId;
|
||||
}
|
||||
|
||||
try {
|
||||
const imResponse = await this.chat.getMessageList(requestParams);
|
||||
const msgList = imResponse.data.messageList || [];
|
||||
const nextId = imResponse.data.nextReqMessageID;
|
||||
const isCompleted = imResponse.data.isCompleted;
|
||||
|
||||
msgList.forEach((msg) => {
|
||||
this.addMessageToList(this.formatReceivedMessage(msg), "append");
|
||||
});
|
||||
|
||||
if (!this.nextReqMessageID) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
this.nextReqMessageID = nextId;
|
||||
this.hasMoreHistory = !isCompleted;
|
||||
} catch (imError) {
|
||||
console.warn("getMessageList error:", imError);
|
||||
} finally {
|
||||
this.isLoadingHistory = false;
|
||||
}
|
||||
}
|
||||
|
||||
formatReceivedMessage(message) {
|
||||
let messageText = "";
|
||||
let username = "";
|
||||
let isSystem = false;
|
||||
|
||||
if (message.from === "@TIM#SYSTEM") {
|
||||
username = "系统消息";
|
||||
isSystem = true;
|
||||
} else {
|
||||
username = message.nick || message.from || "用户";
|
||||
}
|
||||
|
||||
const avatar = message.avatar || this.defaultAvatar || null;
|
||||
|
||||
if (message.type === TencentCloudChat.TYPES.MSG_TEXT || message.type === "TIMTextElem") {
|
||||
messageText = message.payload?.text || "";
|
||||
} else if (
|
||||
message.type === TencentCloudChat.TYPES.MSG_CUSTOM ||
|
||||
message.type === "TIMCustomElem"
|
||||
) {
|
||||
try {
|
||||
const customData = JSON.parse(message.payload.data);
|
||||
messageText = customData.content || "系统消息";
|
||||
username = "系统消息";
|
||||
isSystem = true;
|
||||
} catch (e) {
|
||||
messageText = "系统消息";
|
||||
username = "系统消息";
|
||||
isSystem = true;
|
||||
}
|
||||
} else {
|
||||
messageText = "欢迎加入直播间";
|
||||
}
|
||||
|
||||
return {
|
||||
username,
|
||||
message: messageText,
|
||||
avatar: isSystem ? this.defaultAvatar : avatar,
|
||||
messageId: message.ID,
|
||||
time: message.time,
|
||||
type: message.type,
|
||||
isSystem,
|
||||
};
|
||||
}
|
||||
|
||||
toRoomMessage(item) {
|
||||
return {
|
||||
id: item.messageId,
|
||||
userName: item.username,
|
||||
userFace: item.avatar || this.defaultAvatar,
|
||||
message: item.message,
|
||||
isSystem: item.isSystem,
|
||||
};
|
||||
}
|
||||
|
||||
emitMessagesChange() {
|
||||
this.onMessagesChange(
|
||||
this.messageList.map((item) => this.toRoomMessage(item))
|
||||
);
|
||||
}
|
||||
|
||||
onMessageReceived(event) {
|
||||
const msgList = event.data || [];
|
||||
msgList.forEach((message) => {
|
||||
if (message.type === TencentCloudChat.TYPES.MSG_TEXT) {
|
||||
const formattedMessage = this.formatReceivedMessage(message);
|
||||
this.addMessageToList(formattedMessage, "append");
|
||||
|
||||
if (this.isAtBottom) {
|
||||
this.scrollToBottom();
|
||||
} else {
|
||||
this.unreadCount += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addMessageToList(message, mode = "append") {
|
||||
if (!message.messageId || this.messageIdSet.has(message.messageId)) return;
|
||||
|
||||
this.messageIdSet.add(message.messageId);
|
||||
|
||||
if (mode === "append") {
|
||||
this.messageList.push(message);
|
||||
if (this.messageList.length > MAX_MESSAGE_COUNT) {
|
||||
const removeCount = this.messageList.length - MAX_MESSAGE_COUNT;
|
||||
const removed = this.messageList.splice(0, removeCount);
|
||||
removed.forEach((item) => this.messageIdSet.delete(item.messageId));
|
||||
}
|
||||
} else {
|
||||
this.messageList.unshift(message);
|
||||
if (this.messageList.length > MAX_MESSAGE_COUNT) {
|
||||
const removeCount = this.messageList.length - MAX_MESSAGE_COUNT;
|
||||
const removed = this.messageList.splice(-removeCount, removeCount);
|
||||
removed.forEach((item) => this.messageIdSet.delete(item.messageId));
|
||||
}
|
||||
}
|
||||
|
||||
this.emitMessagesChange();
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.isAtBottom = true;
|
||||
this.unreadCount = 0;
|
||||
setTimeout(() => {
|
||||
this.onScrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
markAtBottom(isBottom) {
|
||||
this.isAtBottom = isBottom;
|
||||
if (isBottom) {
|
||||
this.unreadCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.chat) {
|
||||
try {
|
||||
this.chat.off(TencentCloudChat.EVENT.MESSAGE_RECEIVED, this._onMessageReceived);
|
||||
this.chat.off(TencentCloudChat.EVENT.SDK_READY, this._onSdkReady);
|
||||
this.chat.off(TencentCloudChat.EVENT.SDK_NOT_READY, this._onSdkNotReady);
|
||||
this.chat.off(TencentCloudChat.EVENT.KICKED_OUT, this._onKickedOut);
|
||||
this.chat.logout?.();
|
||||
} catch (error) {
|
||||
console.error("清理腾讯云IM失败:", error);
|
||||
}
|
||||
this.chat = null;
|
||||
}
|
||||
this.messageList = [];
|
||||
this.messageIdSet.clear();
|
||||
this.savedViewLiveResult = null;
|
||||
}
|
||||
}
|
||||
222
pages/live/utils/liveMqtt.js
Normal file
222
pages/live/utils/liveMqtt.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 直播 MQTT 连接管理(由 saas-uni-v3 useMqtt 迁移)
|
||||
*/
|
||||
import { createMqttClient } from "../mqtt/mqtt.js";
|
||||
|
||||
export class LiveMqttService {
|
||||
constructor(liveId, userId) {
|
||||
this.liveId = liveId;
|
||||
this.userId = userId;
|
||||
this.mqttClient = null;
|
||||
this.isMqttConnected = false;
|
||||
this.fallbackMode = false;
|
||||
this.reconnectAttempts = 0;
|
||||
this.mqttReconnectTimer = null;
|
||||
this.lastHeartbeatTime = null;
|
||||
this.heartbeatCheckTimer = null;
|
||||
this.maxReconnectAttempts = 60;
|
||||
this.reconnectInterval = 10000;
|
||||
this.messageHandlers = {};
|
||||
}
|
||||
|
||||
setMessageHandlers(handlers) {
|
||||
this.messageHandlers = handlers || {};
|
||||
}
|
||||
|
||||
initMqtt() {
|
||||
if (this.mqttClient) return;
|
||||
|
||||
if (!this.liveId || !this.userId) {
|
||||
console.warn("直播ID或用户ID不完整,跳过MQTT初始化");
|
||||
this.fallbackMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = `${this.userId}-${this.liveId}-${Date.now()}`;
|
||||
|
||||
try {
|
||||
const topicList = [
|
||||
{
|
||||
topic: `live/goods/${this.liveId}`,
|
||||
qos: 1,
|
||||
handler: (msg, topic) => this.messageHandlers.onGoodsMessage?.(msg, topic),
|
||||
},
|
||||
{
|
||||
topic: `live/goods/${this.liveId}/#`,
|
||||
qos: 1,
|
||||
handler: (msg, topic) => this.messageHandlers.onGoodsMessage?.(msg, topic),
|
||||
},
|
||||
{
|
||||
topic: `live/coupon/${this.liveId}`,
|
||||
qos: 1,
|
||||
handler: (msg, topic) => this.messageHandlers.onCouponMessage?.(msg, topic),
|
||||
},
|
||||
{
|
||||
topic: `live/coupon/${this.liveId}/#`,
|
||||
qos: 1,
|
||||
handler: (msg, topic) => this.messageHandlers.onCouponMessage?.(msg, topic),
|
||||
},
|
||||
{
|
||||
topic: `live/live/${this.liveId}`,
|
||||
qos: 1,
|
||||
handler: (msg, topic) => this.messageHandlers.onLiveMessage?.(msg, topic),
|
||||
},
|
||||
{
|
||||
topic: `live/blacklist/${this.liveId}/${this.userId}`,
|
||||
qos: 1,
|
||||
handler: (msg, topic) => this.messageHandlers.onBlacklistMessage?.(msg, topic),
|
||||
},
|
||||
{
|
||||
topic: "live/keepalive",
|
||||
qos: 1,
|
||||
handler: (msg) => this.handleKeepaliveMessage(msg),
|
||||
},
|
||||
{
|
||||
topic: `live/push/${this.liveId}`,
|
||||
qos: 1,
|
||||
handler: (msg, topic) => this.messageHandlers.onPushStream?.(msg, topic),
|
||||
},
|
||||
];
|
||||
|
||||
this.mqttClient = createMqttClient({
|
||||
clientId,
|
||||
topicHandlers: topicList,
|
||||
});
|
||||
|
||||
this.mqttClient
|
||||
.onConnected(() => {
|
||||
console.log("✅ MQTT连接成功");
|
||||
this.isMqttConnected = true;
|
||||
this.fallbackMode = false;
|
||||
this.stopMqttReconnect();
|
||||
this.reconnectAttempts = 0;
|
||||
this.startHeartbeatCheck();
|
||||
})
|
||||
.onErrorOccurred((error) => this.handleMqttFailure(error))
|
||||
.onClosed(() => this.handleMqttFailure())
|
||||
.onClientOffline(() => this.handleMqttFailure());
|
||||
|
||||
this.mqttClient.connect();
|
||||
} catch (error) {
|
||||
console.error("❌ MQTT初始化失败:", error);
|
||||
this.handleMqttFailure(error);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeepaliveMessage(message) {
|
||||
console.log("[MQTT keepalive]", message);
|
||||
this.lastHeartbeatTime = Date.now();
|
||||
}
|
||||
|
||||
handleMqttFailure(error) {
|
||||
if (error) console.error("MQTT错误:", error);
|
||||
|
||||
this.disconnectMqtt(false);
|
||||
|
||||
if (!this.fallbackMode) {
|
||||
console.log("❗MQTT异常,启动备用模式和重连机制");
|
||||
this.fallbackMode = true;
|
||||
}
|
||||
|
||||
this.isMqttConnected = false;
|
||||
this.startMqttReconnect();
|
||||
}
|
||||
|
||||
disconnectMqtt(resetFallback = true) {
|
||||
try {
|
||||
if (this.mqttClient) {
|
||||
this.mqttClient.disconnect();
|
||||
this.mqttClient = null;
|
||||
console.log("🔌 MQTT连接已断开");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("断开MQTT连接时出错:", error);
|
||||
}
|
||||
|
||||
this.isMqttConnected = false;
|
||||
this.stopMqttReconnect();
|
||||
if (resetFallback) {
|
||||
this.fallbackMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
startMqttReconnect() {
|
||||
if (this.mqttReconnectTimer) return;
|
||||
|
||||
console.log(`开始MQTT重连机制,间隔${this.reconnectInterval}ms`);
|
||||
|
||||
this.mqttReconnectTimer = setInterval(() => {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.log(`已达到最大重连次数${this.maxReconnectAttempts},停止重连`);
|
||||
this.stopMqttReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
console.log(`MQTT重连尝试 ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
|
||||
|
||||
try {
|
||||
if (this.mqttClient) {
|
||||
this.mqttClient.disconnect();
|
||||
this.mqttClient = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("断开旧MQTT连接失败:", error);
|
||||
}
|
||||
|
||||
this.initMqtt();
|
||||
}, this.reconnectInterval);
|
||||
}
|
||||
|
||||
stopMqttReconnect() {
|
||||
if (this.mqttReconnectTimer) {
|
||||
clearInterval(this.mqttReconnectTimer);
|
||||
this.mqttReconnectTimer = null;
|
||||
}
|
||||
this.reconnectAttempts = 0;
|
||||
}
|
||||
|
||||
startHeartbeatCheck() {
|
||||
if (this.heartbeatCheckTimer) return;
|
||||
|
||||
this.heartbeatCheckTimer = setInterval(() => {
|
||||
if (!this.isMqttConnected || !this.lastHeartbeatTime) return;
|
||||
|
||||
const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeatTime;
|
||||
if (timeSinceLastHeartbeat > 30000) {
|
||||
console.warn("⚠️ MQTT心跳超时,主动重连");
|
||||
|
||||
if (this.mqttClient) {
|
||||
try {
|
||||
this.mqttClient.disconnect();
|
||||
this.mqttClient = null;
|
||||
} catch (error) {
|
||||
console.warn("断开假死MQTT连接失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
this.isMqttConnected = false;
|
||||
this.lastHeartbeatTime = null;
|
||||
this.mqttClient = null;
|
||||
this.initMqtt();
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
stopHeartbeatCheck() {
|
||||
if (this.heartbeatCheckTimer) {
|
||||
clearInterval(this.heartbeatCheckTimer);
|
||||
this.heartbeatCheckTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
return this.isMqttConnected;
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.stopHeartbeatCheck();
|
||||
this.stopMqttReconnect();
|
||||
this.disconnectMqtt();
|
||||
}
|
||||
}
|
||||
27
pages/live/utils/liveRecommend.js
Normal file
27
pages/live/utils/liveRecommend.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 直播 MQTT 推荐消息解析(商品 / 优惠券)
|
||||
*/
|
||||
|
||||
export function toBoolean(value) {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return value === 1;
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === "true" || normalized === "1";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function parseRecommendMessageArray(message) {
|
||||
try {
|
||||
const parsed = JSON.parse(message);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
console.warn("MQTT 消息解析失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function findRecommendPayload(list) {
|
||||
return list.find((item) => toBoolean(item.recommend) && !toBoolean(item.hideFlag));
|
||||
}
|
||||
34
pages/live/utils/liveSetting.js
Normal file
34
pages/live/utils/liveSetting.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { getLiveSetting } from "@/api/live.js";
|
||||
|
||||
let cachedSetting = null;
|
||||
let fetchingPromise = null;
|
||||
|
||||
export async function fetchLiveSetting(force = false) {
|
||||
if (!force && cachedSetting) return cachedSetting;
|
||||
if (!force && fetchingPromise) return fetchingPromise;
|
||||
|
||||
fetchingPromise = (async () => {
|
||||
try {
|
||||
const res = await getLiveSetting();
|
||||
if (res.data?.success && res.data?.result) {
|
||||
cachedSetting = res.data.result;
|
||||
return cachedSetting;
|
||||
}
|
||||
console.warn("获取直播配置失败", res.data);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取直播配置异常:", error);
|
||||
return null;
|
||||
} finally {
|
||||
fetchingPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return fetchingPromise;
|
||||
}
|
||||
|
||||
export function resolveSdkAppId(setting) {
|
||||
if (!setting?.imSdkAppid) return 0;
|
||||
const id = Number(setting.imSdkAppid);
|
||||
return Number.isNaN(id) ? 0 : id;
|
||||
}
|
||||
Reference in New Issue
Block a user