From 033359f6f4041963b163f61971b2258634750661 Mon Sep 17 00:00:00 2001 From: XingQue Date: Tue, 23 Jun 2026 23:21:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=81=8A=E5=A4=A9=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E4=B8=8E=E5=88=97=E8=A1=A8=EF=BC=9A=E9=85=8D=E5=AF=B9?= =?UTF-8?q?=E7=BE=A4=E5=85=83=E6=95=B0=E6=8D=AE=E3=80=81=E6=8E=92=E5=BA=8F?= =?UTF-8?q?=E4=B8=8E=E8=BF=9E=E6=8E=A5=E7=A8=B3=E5=AE=9A=EF=BC=88miniprogr?= =?UTF-8?q?am=E7=9B=AE=E5=BD=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- miniprogram/pages/qunliaotian/qunliaotian.js | 511 ++++++++++++++ miniprogram/pages/xiaoxi/xiaoxi.js | 436 ++++++++++++ miniprogram/pages/xiaoxi/xiaoxi.wxml | 88 +++ miniprogram/pages/xiaoxi/xiaoxi.wxss | 47 ++ miniprogram/utils/chat-core.js | 672 +++++++++++++++++++ miniprogram/utils/group-chat.js | 186 +++++ miniprogram/utils/im-user.js | 50 ++ miniprogram/utils/message-sender.js | 172 +++++ miniprogram/utils/xiaoxilj.js | 180 +++++ 9 files changed, 2342 insertions(+) create mode 100644 miniprogram/pages/qunliaotian/qunliaotian.js create mode 100644 miniprogram/pages/xiaoxi/xiaoxi.js create mode 100644 miniprogram/pages/xiaoxi/xiaoxi.wxml create mode 100644 miniprogram/pages/xiaoxi/xiaoxi.wxss create mode 100644 miniprogram/utils/chat-core.js create mode 100644 miniprogram/utils/group-chat.js create mode 100644 miniprogram/utils/im-user.js create mode 100644 miniprogram/utils/message-sender.js create mode 100644 miniprogram/utils/xiaoxilj.js diff --git a/miniprogram/pages/qunliaotian/qunliaotian.js b/miniprogram/pages/qunliaotian/qunliaotian.js new file mode 100644 index 0000000..12a48f8 --- /dev/null +++ b/miniprogram/pages/qunliaotian/qunliaotian.js @@ -0,0 +1,511 @@ +const app = getApp(); +import { formatDate } from '../../static/lib/utils'; +import { sendGroupMessage } from '../../utils/message-sender'; +import { showConfirmByScene } from '../../utils/scriptService.js'; +import { normalizeGroupMessage } from '../../utils/group-chat.js'; +import { getFreshImUser } from '../../utils/im-user.js'; + +Page({ + data: { + groupId: '', + groupName: '', + groupAvatar: '', + orderId: '', + isCross: 0, + currentUser: null, + messages: [], + inputText: '', + showEmojiPanel: false, + showPlusPanel: false, + showOrderSender: false, + loadingHistory: false, + isRefreshing: false, + lastTimestamp: null, + hasMore: true, + scrollToView: '', + detailText: '', + showDetailModal: false, + lastTapTime: 0, + lastTapMsgId: '', + emojiList: ['😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','😗','😙','😚','🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹','🙁','😖','😞','😟','😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','🥵','🥶','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾'], + pendingImage: '', + keyboardHeight: 0, + bottomSafeHeight: 10 + }, + onLoad(options) { + let p = { groupId: '', groupName: '订单群聊', groupAvatar: '', orderId: '', isCross: 0, + currentUserId: '', currentUserName: '', currentUserAvatar: '' }; + if (options.data) { + try { + const d = JSON.parse(decodeURIComponent(options.data)); + p = { ...p, ...d }; + } catch (e) {} + } + this.setData({ + groupId: p.groupId, + groupName: p.groupName, + groupAvatar: p.groupAvatar, + orderId: p.orderId, + isCross: p.isCross + }); + wx.setNavigationBarTitle({ title: this.data.groupName }); + // 如果有外部传入的 currentUser 信息,直接用,不再依赖全局状态 + if (p.currentUserId) { + this.setData({ + currentUser: { + id: p.currentUserId, + name: p.currentUserName || `用户${p.currentUserId.replace(/^[A-Za-z]+/,'')}`, + avatar: p.currentUserAvatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang) + } + }); + } else { + this.initCurrentUser(); + } + }, + + /*onLoad(options) { + if (options.data) { + try { + const p = JSON.parse(decodeURIComponent(options.data)); + this.setData({ + groupId: p.groupId || '', + groupName: p.groupName || '订单群聊', + groupAvatar: p.groupAvatar || '', + orderId: p.orderId || '', + isCross: p.isCross || 0 + }); + } catch (e) {} + } + wx.setNavigationBarTitle({ title: this.data.groupName }); + this.initCurrentUser(); + },*/ + + onShow() { + this.ensureChatConnection(); + }, + + onHide() { + this.clearPageListeners(); + }, + + ensureChatConnection() { + const role = app.globalData.currentRole || 'normal'; + const uid = wx.getStorageSync('uid'); + const prefixMap = { normal:'Boss', dashou:'Ds', shangjia:'Sj', guanshi:'Gs', zuzhang:'Zz' }; + const targetUserId = (prefixMap[role] || 'Boss') + uid; + + const onReady = () => { + this.subscribeGroupIfNeeded(); + this.setupAllListeners(); + this.loadHistory(true).then(() => this.markGroupMessageAsRead()); + }; + + const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected'; + const currentUserId = wx.goEasy?.im?.userId; + + if ((status === 'connected' || status === 'reconnected') && currentUserId === targetUserId) { + onReady(); + return; + } + + if (app.connectWithIdentity) { + app.connectWithIdentity(role, targetUserId, true).then(onReady).catch(() => { + const s = wx.goEasy?.getConnectionStatus?.(); + if (s === 'connected' || s === 'reconnected') onReady(); + }); + } else { + this.connectGoEasy(targetUserId); + } + }, + + clearPageListeners() { + this.clearAllListeners(); + }, + + connectGoEasy(userId) { + const { currentUser } = this.data; + if (!currentUser) return; + wx.goEasy.connect({ + id: userId, + data: { + name: currentUser.name, + avatar: currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang) + }, + onSuccess: () => { + this.subscribeGroupIfNeeded(); + this.setupAllListeners(); + this.loadHistory(true).then(() => this.markGroupMessageAsRead()); + }, + onFailed: (error) => { + if (error.code === 408) { + this.subscribeGroupIfNeeded(); + this.setupAllListeners(); + this.loadHistory(true); + } else { + wx.showToast({ title: '连接失败,请重试', icon: 'none' }); + } + } + }); + }, + + subscribeGroupIfNeeded() { + const { groupId } = this.data; + if (!groupId) return; + wx.goEasy.im.subscribeGroup({ + groupIds: [groupId], + onSuccess: () => console.log(`[群聊页] 订阅成功: ${groupId}`), + onFailed: (err) => console.error(`[群聊页] 订阅失败: ${groupId}`, err) + }); + }, + + setupAllListeners() { + this.clearAllListeners(); + this.listenNewMsg(); + this.listenRecall(); + this.listenDelete(); + }, + + clearAllListeners() { + if (this._msgHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, this._msgHandler); this._msgHandler = null; } + if (this._recallHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_RECALLED, this._recallHandler); this._recallHandler = null; } + if (this._deleteHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_DELETED, this._deleteHandler); this._deleteHandler = null; } + }, + + initCurrentUser() { + const uid = wx.getStorageSync('uid'); + const role = app.globalData.currentRole || 'normal'; + const fresh = getFreshImUser(app, role, uid); + this.setData({ currentUser: { + id: fresh.id, + name: fresh.name, + avatar: fresh.avatar, + }}); + }, + + fixAvatar(url) { + if (!url) return app.globalData.ossImageUrl + app.globalData.morentouxiang; + if (url.startsWith('http')) return url; + return app.globalData.ossImageUrl + url; + }, + + isConnected() { + const s = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected'; + return s === 'connected' || s === 'reconnected'; + }, + + onPullDownRefresh() { + if (this.data.loadingHistory) return; + this.setData({ isRefreshing: true }); + this.loadHistory(false).finally(() => { this.setData({ isRefreshing: false }); wx.stopPullDownRefresh(); }); + }, + + loadHistory(refresh) { + if (!refresh && !this.data.hasMore) return Promise.resolve(); + return new Promise((resolve) => { + this.setData({ loadingHistory: true }); + const { groupId, lastTimestamp } = this.data; + const ts = refresh ? null : lastTimestamp; + wx.goEasy.im.history({ + type: wx.GoEasy.IM_SCENE.GROUP, + id: groupId, + lastTimestamp: ts, + limit: 20, + onSuccess: (res) => { + let list = res.content || []; + list.sort((a, b) => a.timestamp - b.timestamp); + list.forEach((m, idx) => { + normalizeGroupMessage(m); + m.formattedTime = formatDate(m.timestamp); + if (idx === 0) m.showTime = true; + else m.showTime = (m.timestamp - list[idx-1].timestamp) / 60000 > 5; + if (!m.senderData) m.senderData = { name: '未知用户', avatar: '' }; + }); + let final = refresh ? list : [...list, ...this.data.messages]; + if (!refresh) { + for (let i = 0; i < final.length; i++) { + if (i === 0) final[i].showTime = true; + else final[i].showTime = (final[i].timestamp - final[i-1].timestamp) / 60000 > 5; + } + } + const ids = new Set(); + const unique = []; + for (const m of final) { if (!ids.has(m.messageId)) { ids.add(m.messageId); unique.push(m); } } + const update = { + messages: unique, + hasMore: list.length >= 20, + lastTimestamp: unique.length ? unique[0].timestamp : lastTimestamp, + loadingHistory: false + }; + if (refresh) update.scrollToView = 'msg-bottom'; + this.setData(update); + resolve(); + }, + onFailed: () => { this.setData({ loadingHistory: false }); resolve(); } + }); + }); + }, + + listenNewMsg() { + if (this._msgHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, this._msgHandler); + this._msgHandler = (msg) => { + if (msg.groupId !== this.data.groupId) return; + if (msg.senderId === this.data.currentUser.id) return; + normalizeGroupMessage(msg); + msg.formattedTime = formatDate(msg.timestamp); + const msgs = this.data.messages; + const last = msgs.length ? msgs[msgs.length-1] : null; + msg.showTime = last ? (msg.timestamp - last.timestamp) / 60000 > 5 : true; + this.setData({ messages: [...msgs, msg], scrollToView: 'msg-bottom' }); + this.markGroupMessageAsRead(); + }; + wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, this._msgHandler); + this.markGroupMessageAsRead(); // ← 收到新消息就标记已读 + }, + + markGroupMessageAsRead() { + if (!this.data.groupId) return; + wx.goEasy.im.markMessageAsRead({ + type: wx.GoEasy.IM_SCENE.GROUP, + id: this.data.groupId + }); + }, + + listenRecall() { + if (this._recallHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_RECALLED, this._recallHandler); + this._recallHandler = (msgs) => { + if (!msgs || !msgs.length) return; + const ids = new Set(msgs.map(m => m.messageId)); + this.setData({ messages: this.data.messages.filter(m => !ids.has(m.messageId)) }); + }; + wx.goEasy.im.on(wx.GoEasy.IM_EVENT.MESSAGE_RECALLED, this._recallHandler); + }, + + listenDelete() { + if (this._deleteHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_DELETED, this._deleteHandler); + this._deleteHandler = (msgs) => { + if (!msgs || !msgs.length) return; + const ids = new Set(msgs.map(m => m.messageId)); + this.setData({ messages: this.data.messages.filter(m => !ids.has(m.messageId)) }); + }; + wx.goEasy.im.on(wx.GoEasy.IM_EVENT.MESSAGE_DELETED, this._deleteHandler); + }, + + onInput(e) { this.setData({ inputText: e.detail.value }); }, + + chooseImage() { + const that = this; + wx.chooseImage({ + count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], + success(res) { that.setData({ pendingImage: res.tempFilePaths[0] }); } + }); + }, + + clearPendingImage() { this.setData({ pendingImage: '' }); }, + + sendMessage() { + if (this.data.pendingImage) { + const file = this.data.pendingImage; + showConfirmByScene('chat_image_confirm', () => { + this.sendImageMsg(file); + this.setData({ pendingImage: '' }); + }); + return; + } + const text = this.data.inputText.trim(); + if (!text) return; + this.sendText(text); + this.setData({ inputText: '' }); + }, + + sendText(text) { + const that = this; + sendGroupMessage({ + msgData: { + type: 'text', + text, + currentUser: that.data.currentUser, + groupId: that.data.groupId, + orderId: that.data.orderId, + isCross: that.data.isCross, + groupName: that.data.groupName, + groupAvatar: that.data.groupAvatar + }, + onSendStart: (localMsg) => { + const msgs = that.data.messages; + if (msgs.length) { + localMsg.showTime = (localMsg.timestamp - msgs[msgs.length-1].timestamp) / 60000 > 5; + } else { + localMsg.showTime = true; + } + that.setData({ messages: [...msgs, localMsg], scrollToView: 'msg-bottom' }); + }, + onSuccess: (messageId, status) => { + that.updateMsg(messageId, status); + } + }); + }, + + sendImageMsg(file) { + const that = this; + sendGroupMessage({ + msgData: { + type: 'image', + filePath: file, + currentUser: that.data.currentUser, + groupId: that.data.groupId, + orderId: that.data.orderId, + isCross: that.data.isCross, + groupName: that.data.groupName, + groupAvatar: that.data.groupAvatar + }, + onSendStart: (localMsg) => { + const msgs = that.data.messages; + if (msgs.length) { + localMsg.showTime = (localMsg.timestamp - msgs[msgs.length-1].timestamp) / 60000 > 5; + } else { + localMsg.showTime = true; + } + that.setData({ messages: [...msgs, localMsg], scrollToView: 'msg-bottom' }); + }, + onSuccess: (messageId, status) => { + that.updateMsg(messageId, status); + } + }); + }, + + updateMsg(id, status) { + this.setData({ messages: this.data.messages.map(m => m.messageId === id ? { ...m, status } : m) }); + }, + + needShow(ts) { + const ms = this.data.messages; + if (!ms.length) return true; + const last = ms[ms.length-1]; + return (ts - last.timestamp) / 60000 > 5; + }, + + onBubbleTap(e) { + const msgId = e.currentTarget.dataset.messageid; + const text = e.currentTarget.dataset.text; + const now = Date.now(); + if (msgId === this.data.lastTapMsgId && now - this.data.lastTapTime < 350) { + this.setData({ detailText: text, showDetailModal: true, lastTapTime:0, lastTapMsgId:'' }); + } else { + this.data.lastTapTime = now; + this.data.lastTapMsgId = msgId; + } + }, + + hideDetail() { this.setData({ showDetailModal: false }); }, + copyDetail() { wx.setClipboardData({ data: this.data.detailText }); }, + + showAction(e) { + const msgId = e.currentTarget.dataset.messageid; + if (!msgId) return; + const msg = this.data.messages.find(m => m.messageId === msgId); + if (!msg || msg.messageId.startsWith('local-')) return; + const itemList = ['复制']; + if (msg.senderId === this.data.currentUser.id && (Date.now() - msg.timestamp < 120000) && !msg.recalled) { + itemList.push('撤回'); + } + itemList.push('删除'); + wx.showActionSheet({ + itemList, + success: (res) => { + if (itemList[res.tapIndex] === '复制') { + if (msg.type === 'text') wx.setClipboardData({ data: msg.payload.text }); + else if (msg.type === 'order') wx.setClipboardData({ data: JSON.stringify(msg.payload) }); + } else if (itemList[res.tapIndex] === '删除') { + this.deleteMsg(msg); + } else if (itemList[res.tapIndex] === '撤回') { + this.recallMsg(msg); + } + } + }); + }, + + deleteMsg(msg) { + wx.showModal({ + title: '提示', content: '仅自己看不到', + success: (res) => { + if (res.confirm) { + wx.goEasy.im.deleteMessage({ + messages: [msg], + onSuccess: () => { this.setData({ messages: this.data.messages.filter(m => m.messageId !== msg.messageId) }); }, + onFailed: () => { this.setData({ messages: this.data.messages.filter(m => m.messageId !== msg.messageId) }); } + }); + } + } + }); + }, + + recallMsg(msg) { + wx.showModal({ + title: '提示', content: '撤回后群内所有人都看不到该消息', + success: (res) => { + if (res.confirm) { + this.setData({ messages: this.data.messages.filter(m => m.messageId !== msg.messageId) }); + wx.goEasy.im.recallMessage({ messages: [msg], onFailed:()=>{} }); + } + } + }); + }, + + previewImage(e) { wx.previewImage({ urls: [e.currentTarget.dataset.url], current: e.currentTarget.dataset.url }); }, + + togglePlusPanel() { this.setData({ showPlusPanel: !this.data.showPlusPanel, showEmojiPanel: false }); }, + closePlusPanel() { this.setData({ showPlusPanel: false }); }, + + openEmojiFromPlus() { this.setData({ showPlusPanel: false, showEmojiPanel: true }); }, + toggleEmoji() { this.setData({ showEmojiPanel: !this.data.showEmojiPanel }); }, + insertEmoji(e) { this.setData({ inputText: this.data.inputText + e.currentTarget.dataset.emoji }); }, + closeEmojiPanel() { this.setData({ showEmojiPanel: false }); }, + + openOrderSender() { this.setData({ showOrderSender: true, showPlusPanel: false }); }, + closeOrderSender() { this.setData({ showOrderSender: false }); }, + + onSendOrder(e) { + const order = e.detail.order; + if (!order) return; + const that = this; + const payload = { + orderId: order.dingdan_id, + jieshao: order.jieshao, + jine: order.jine, + beizhu: order.beizhu + }; + sendGroupMessage({ + msgData: { + type: 'order', + orderPayload: payload, + currentUser: that.data.currentUser, + groupId: that.data.groupId, + orderId: that.data.orderId, + isCross: that.data.isCross, + groupName: that.data.groupName, + groupAvatar: that.data.groupAvatar + }, + onSendStart: (localMsg) => { + const msgs = that.data.messages; + if (msgs.length) { + localMsg.showTime = (localMsg.timestamp - msgs[msgs.length-1].timestamp) / 60000 > 5; + } else { + localMsg.showTime = true; + } + that.setData({ messages: [...msgs, localMsg], scrollToView: 'msg-bottom' }); + }, + onSuccess: (messageId, status) => { + that.updateMsg(messageId, status); + } + }); + }, + + viewOrderDetail(e) { + const payload = e.currentTarget.dataset.payload; + if (!payload) return; + const text = `订单ID: ${payload.orderId}\n内容: ${payload.jieshao}\n金额: ¥${payload.jine}\n备注: ${payload.beizhu}`; + this.setData({ detailText: text, showDetailModal: true }); + }, + + onKeyboardHeightChange(e) { this.setData({ keyboardHeight: e.detail.height }); } +}); \ No newline at end of file diff --git a/miniprogram/pages/xiaoxi/xiaoxi.js b/miniprogram/pages/xiaoxi/xiaoxi.js new file mode 100644 index 0000000..13a91bf --- /dev/null +++ b/miniprogram/pages/xiaoxi/xiaoxi.js @@ -0,0 +1,436 @@ +// pages/xiaoxi/xiaoxi.js +const app = getApp(); +import { formatDate } from '../../static/lib/utils'; +const { jianquanxian } = require('../../utils/imAuth/jianquanxian'); +import { enrichGroupConversation, isOrderGroupConversation, sortConversations, recordConversationOpen } from '../../utils/group-chat.js'; +import { loadGroupMetaCache } from '../../utils/im-user.js'; + +Page({ + data: { + allConversations: [], + filteredConversations: [], + activeTab: 0, + tabUnread: [0, 0, 0], + displayCount: 4, + searchKeyword: '', + actionPopup: { visible: false, conversation: null }, + currentUser: null, + notificationMuted: false, + scrollHeight: 0 + }, + + _permissionSeq: 0, + + onLoad() { + this.calculateScrollHeight(); + const muted = wx.getStorageSync('notificationMuted') || false; + this.setData({ notificationMuted: muted }); + loadGroupMetaCache(app); + this._onGlobalConvUpdated = (result) => { + const content = result?.content || result; + if (content && content.conversations) { + this.renderConversations(content); + } + }; + app.on('conversationsUpdated', this._onGlobalConvUpdated); + app.on('unreadCountChanged', this._onGlobalConvUpdated); + }, + + onUnload() { + if (this._onGlobalConvUpdated) { + app.off('conversationsUpdated', this._onGlobalConvUpdated); + app.off('unreadCountChanged', this._onGlobalConvUpdated); + } + }, + + onShow() { + if (!this.checkLoginStatus()) return; + loadGroupMetaCache(app); + if (app.globalData.messageManager?.unreadTotal != null) { + app.emitEvent('tabBarBadgeChanged', { + badgeText: app.globalData.messageManager.unreadTotal > 0 + ? String(app.globalData.messageManager.unreadTotal) : '' + }); + } + this.checkPermissionAndAutoConnect(); + }, + + // ❌ 原版注释保留:删除所有清理逻辑,监听器永久存活 + onHide() {}, + onUnload() {}, + + // ========== 鉴权检查 ========== + async checkPermissionAndAutoConnect() { + const seq = ++this._permissionSeq; + let quanxian; + try { + quanxian = await jianquanxian(app); + } catch (e) { + return; + } + if (seq !== this._permissionSeq) return; + + if (!quanxian.allowed) { + const currentRole = app.globalData.currentRole || 'normal'; + let content = '您没有权限使用消息功能'; + if (currentRole === 'dashou') content = '您需要先充值会员才能使用消息功能'; + wx.showModal({ + title: '权限不足', + content, + showCancel: false, + confirmText: '我知道了' + }); + return; + } + this.autoConnect(); + }, + + // ========== 登录状态检查 ========== + checkLoginStatus() { + const uid = wx.getStorageSync('uid'); + if (!uid) { + wx.showToast({ title: '请先登录', icon: 'none' }); + setTimeout(() => { wx.redirectTo({ url: '/pages/wode/wode' }); }, 1500); + return false; + } + let currentUser = app.globalData.currentUser; + if (!currentUser) { + currentUser = { + id: uid, + name: '用户' + uid.substring(0, 6), + avatar: app.globalData.ossImageUrl + app.globalData.morentouxiang + }; + app.globalData.currentUser = currentUser; + } + this.setData({ currentUser }); + return true; + }, + + // ========== 自动连接(复用全局连接,避免反复 disconnect) ========== + autoConnect() { + const role = app.globalData.currentRole || 'normal'; + const uid = wx.getStorageSync('uid'); + const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' }; + const targetUserId = (prefixMap[role] || 'Boss') + uid; + + const onReady = () => { + this.setupConversationListener(); + this.loadConversations(); + }; + + const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected'; + const currentUserId = wx.goEasy?.im?.userId; + + if ((status === 'connected' || status === 'reconnected') && currentUserId === targetUserId) { + onReady(); + return; + } + + if (currentUserId && currentUserId !== targetUserId && app.disconnectGoEasy) { + app.disconnectGoEasy().then(() => { + app.connectWithIdentity(role, targetUserId, true).then(onReady).catch(() => {}); + }); + return; + } + + if (!app.connectWithIdentity) { + this.connectGoEasy(targetUserId); + return; + } + + app.connectWithIdentity(role, targetUserId, true).then(onReady).catch(() => { + const s = wx.goEasy?.getConnectionStatus?.(); + if (s === 'connected' || s === 'reconnected') onReady(); + }); + }, + + connectGoEasy(userId) { + const { currentUser } = this.data; + if (!currentUser) return; + wx.goEasy.connect({ + id: userId, + data: { + name: currentUser.name, + avatar: currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang) + }, + onSuccess: () => { + try { + const connection = { + userId: userId, + identityType: app.globalData.currentRole, + lastConnectTime: Date.now(), + autoReconnect: true, + status: 'connected' + }; + wx.setStorageSync('savedGoEasyConnection', JSON.stringify(connection)); + wx.setStorageSync('goEasyUserId', userId); + } catch (e) {} + this.setupConversationListener(); + this.loadConversations(); + }, + onFailed: (error) => { + if (error.code === 408) { + this.setupConversationListener(); + this.loadConversations(); + } else { + wx.showToast({ title: '连接失败,请重试', icon: 'none' }); + } + } + }); + }, + + // ========== 会话监听(永不清理) ========== + setupConversationListener() { + if (this.conversationsUpdatedListener) { + wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, this.conversationsUpdatedListener); + } + this.conversationsUpdatedListener = (result) => { + this.renderConversations(result); + }; + wx.goEasy.im.on(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, this.conversationsUpdatedListener); + }, + + loadConversations() { + wx.goEasy.im.latestConversations({ + onSuccess: (result) => { + this.renderConversations(result.content); + }, + onFailed: () => { + wx.showToast({ title: '获取会话失败', icon: 'none' }); + } + }); + }, + + // ========== 会话渲染与排序 ========== + renderConversations(content) { + if (!content || !content.conversations) { + this.setData({ allConversations: [] }, () => this.applyFilter()); + return; + } + let list = content.conversations; + const defaultAvatar = app.globalData.ossImageUrl + app.globalData.morentouxiang; + const myGoEasyId = wx.goEasy?.im?.userId || ''; + const ossBase = app.globalData.ossImageUrl || ''; + const groupMeta = app.globalData.groupInfoMap || {}; + + list.forEach(item => { + if (item.type === 'group') { + const meta = groupMeta[item.groupId]; + if (meta) { + if (!item.data) item.data = {}; + Object.assign(item.data, meta); + } + if (item.data && item.data.dashouGoEasyId) { + if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {}; + app.globalData.groupInfoMap[item.groupId] = { + ...app.globalData.groupInfoMap[item.groupId], + ...item.data, + }; + } + enrichGroupConversation(item, myGoEasyId, defaultAvatar, ossBase); + } else if (item.data && item.data.avatar) { + if (!item.data.avatar.startsWith('http')) { + item.data.avatar = ossBase + item.data.avatar; + } + } else if (item.data) { + item.data.avatar = defaultAvatar; + } + if (item.lastMessage && item.lastMessage.timestamp) { + item.lastMessage.date = formatDate(item.lastMessage.timestamp); + } + }); + + const openTimes = wx.getStorageSync('conversationOpenTimes') || {}; + sortConversations(list, openTimes); + + const unreadTotal = content.unreadTotal !== undefined ? content.unreadTotal : list.reduce((sum, c) => sum + (c.unread || 0), 0); + if (app.globalData.messageManager) { + app.globalData.messageManager.unreadTotal = unreadTotal; + } + if (app && app.emitEvent) { + app.emitEvent('tabBarBadgeChanged', { badgeText: unreadTotal > 0 ? String(unreadTotal) : '' }); + app.emitEvent('unreadCountChanged', { unreadTotal }); + } + + this.setData({ allConversations: list, displayCount: 4 }, () => { + this.applyFilter(); + }); + + // 群组订阅(不重复断开连接) + this.subscribeGroupsIfNeeded(list); + }, + + /** + * 群组订阅(依赖:GoEasy IM) + * 根据官方文档,需要主动 subscribeGroup 才能接收群聊消息。 + * 为了应对切后台、网络波动等导致的订阅丢失,这里不再缓存订阅状态, + * 每次获取到群组列表时都直接订阅,保证消息不丢失。 + */ + subscribeGroupsIfNeeded(conversations) { + if (!wx.goEasy?.im || typeof wx.goEasy.im.subscribeGroup !== 'function') return; + + const groupIds = conversations + .filter(c => c.type === 'group' && c.groupId) + .map(c => c.groupId); + + if (groupIds.length === 0) return; + + // 不再判断缓存,直接订阅当前所有群组 + wx.goEasy.im.subscribeGroup({ + groupIds: groupIds, + onSuccess: () => { + // 可选的日志输出,不影响功能 + // console.log('群组订阅成功', groupIds); + }, + onFailed: (error) => { + console.error('群组订阅失败:', error); + } + }); + }, + + applyFilter() { + const { allConversations, activeTab, searchKeyword } = this.data; + let filtered = []; + if (activeTab === 0) { + filtered = allConversations.filter(c => isOrderGroupConversation(c)); + } else if (activeTab === 1) { + filtered = allConversations.filter(c => c.type === 'private'); + } else if (activeTab === 2) { + filtered = allConversations.filter(c => c.type === 'cs'); + } + + if (searchKeyword.trim()) { + const kw = searchKeyword.trim().toLowerCase(); + filtered = filtered.filter(c => { + const name = (c.data && c.data.name || '').toLowerCase(); + const orderId = (c.data && c.data.orderId || '').toLowerCase(); + const id = (c.userId || c.groupId || '').toLowerCase(); + return name.includes(kw) || id.includes(kw) || orderId.includes(kw); + }); + } + + const tabUnread = [0, 0, 0]; + allConversations.forEach(c => { + if (isOrderGroupConversation(c)) tabUnread[0] += c.unread || 0; + else if (c.type === 'private') tabUnread[1] += c.unread || 0; + else if (c.type === 'cs') tabUnread[2] += c.unread || 0; + }); + + this.setData({ filteredConversations: filtered, tabUnread }); + }, + + switchTab(e) { + const index = parseInt(e.currentTarget.dataset.index); + this.setData({ activeTab: index, displayCount: 4, searchKeyword: '' }, () => this.applyFilter()); + }, + + onSearchInput(e) { + this.setData({ searchKeyword: e.detail.value, displayCount: 4 }, () => this.applyFilter()); + }, + + onScrollToLower() { + const { displayCount, filteredConversations } = this.data; + if (displayCount < filteredConversations.length) { + this.setData({ displayCount: Math.min(displayCount + 4, filteredConversations.length) }); + } + }, + + onRefresh() { + this.loadConversations(); + }, + + chat(e) { + const conversation = e.currentTarget.dataset.conversation; + if (!conversation) return; + recordConversationOpen(conversation); + + if (conversation.type === 'private') { + const param = { + toUserId: conversation.userId, + toName: conversation.data.name, + toAvatar: conversation.data.avatar + }; + wx.navigateTo({ url: '/pages/liaotian/liaotian?data=' + encodeURIComponent(JSON.stringify(param)) }); + } else if (conversation.type === 'group') { + const param = { + groupId: conversation.groupId, + orderId: conversation.data.orderId || '', + groupName: conversation.data.name, + groupAvatar: conversation.data.avatar, + isCross: conversation.data.isCross || 0 + }; + wx.navigateTo({ url: '/pages/qunliaotian/qunliaotian?data=' + encodeURIComponent(JSON.stringify(param)) }); + } else if (conversation.type === 'cs') { + const param = { teamId: conversation.teamId || conversation.id }; + wx.navigateTo({ url: '/pages/kefuliaotian/kefuliaotian?data=' + encodeURIComponent(JSON.stringify(param)) }); + } + }, + + chatKefu() { + const param = { teamId: 'support_team' }; + wx.navigateTo({ url: '/pages/kefuliaotian/kefuliaotian?data=' + encodeURIComponent(JSON.stringify(param)) }); + }, + + showAction(e) { + const conversation = e.currentTarget.dataset.conversation; + this.setData({ ['actionPopup.conversation']: conversation, ['actionPopup.visible']: true }); + }, + closeMask() { + this.setData({ ['actionPopup.visible']: false }); + }, + topConversation() { + let conversation = this.data.actionPopup.conversation; + if (!conversation) return; + const top = !conversation.top; + wx.showLoading({ title: '处理中...' }); + if (conversation.type === 'private') { + wx.goEasy.im.topPrivateConversation({ + userId: conversation.userId, top, + onSuccess: () => { + wx.hideLoading(); this.loadConversations(); + wx.showToast({ title: top ? '已置顶' : '已取消置顶' }); + }, + onFailed: () => { wx.hideLoading(); wx.showToast({ title: '操作失败', icon: 'none' }); } + }); + } else { + wx.goEasy.im.topGroupConversation({ + groupId: conversation.groupId, top, + onSuccess: () => { + wx.hideLoading(); this.loadConversations(); + wx.showToast({ title: top ? '已置顶' : '已取消置顶' }); + }, + onFailed: () => { wx.hideLoading(); wx.showToast({ title: '操作失败', icon: 'none' }); } + }); + } + this.closeMask(); + }, + removeConversation() { + let conversation = this.data.actionPopup.conversation; + if (!conversation) return; + wx.showLoading({ title: '删除中...' }); + wx.goEasy.im.removeConversation({ + conversation: conversation, + onSuccess: () => { wx.hideLoading(); this.loadConversations(); wx.showToast({ title: '已删除' }); }, + onFailed: () => { wx.hideLoading(); wx.showToast({ title: '删除失败', icon: 'none' }); } + }); + this.closeMask(); + }, + + toggleNotification() { + const newMuted = !this.data.notificationMuted; + this.setData({ notificationMuted: newMuted }); + wx.setStorageSync('notificationMuted', newMuted); + wx.showToast({ title: newMuted ? '已静音' : '已开启通知', icon: 'none' }); + }, + + calculateScrollHeight() { + const systemInfo = wx.getSystemInfoSync(); + const windowHeight = systemInfo.windowHeight; + const windowWidth = systemInfo.windowWidth; + const rpxRatio = 750 / windowWidth; + const headerHeight = 150 / rpxRatio; + const tabBarHeight = 130 / rpxRatio; + const safeAreaBottom = systemInfo.safeArea ? (windowHeight - systemInfo.safeArea.bottom) : 0; + this.setData({ scrollHeight: windowHeight - headerHeight - tabBarHeight - safeAreaBottom }); + } +}); \ No newline at end of file diff --git a/miniprogram/pages/xiaoxi/xiaoxi.wxml b/miniprogram/pages/xiaoxi/xiaoxi.wxml new file mode 100644 index 0000000..46e169b --- /dev/null +++ b/miniprogram/pages/xiaoxi/xiaoxi.wxml @@ -0,0 +1,88 @@ + + + + + + 🔍 + + + {{notificationMuted ? '静音' : '通知'}} + + + + + + + 订单消息 + {{tabUnread[0]}} + + + 用户消息 + {{tabUnread[1]}} + + + 客服消息 + {{tabUnread[2]}} + + + + + + + + + + + {{item.data.name}} + {{item.lastMessage.date}} + + + + 对方 {{item.data.counterpartYonghuid}} {{item.data.name}} + + + 打手 {{item.data.dashouYonghuid}} {{item.data.dashouNicheng}} + + + {{item.data.partnerRoleLabel || '商家'}} {{item.data.partnerYonghuid}} {{item.data.partnerNicheng}} + + + 单号 {{item.data.orderId}} + {{item.data.orderDesc}} + {{item.data.orderZhuangtaiText}} + + + {{item.displayLastMsg || item.lastMessage.payload.text}} + {{item.unread > 99 ? '99+' : item.unread}} + + + + {{item.lastMessage.type === 'text' ? item.lastMessage.payload.text : '[其他消息]'}} + {{item.unread > 99 ? '99+' : item.unread}} + + + + + + + + + + 上拉加载更多 + + 暂无相关消息 + + + + + + + {{actionPopup.conversation.top ? '取消置顶' : '置顶聊天'}} + 删除聊天 + 取消 + + + + + + \ No newline at end of file diff --git a/miniprogram/pages/xiaoxi/xiaoxi.wxss b/miniprogram/pages/xiaoxi/xiaoxi.wxss new file mode 100644 index 0000000..c43c4f1 --- /dev/null +++ b/miniprogram/pages/xiaoxi/xiaoxi.wxss @@ -0,0 +1,47 @@ +.msg-page { background: #f5f5f5; min-height: 100vh; } + +.header { display: flex; align-items: center; padding: 20rpx 24rpx; background: #fff; border-bottom: 1rpx solid #e8e8e8; } +.search-bar { flex: 1; display: flex; align-items: center; background: #f0f0f0; border-radius: 40rpx; padding: 0 20rpx; height: 68rpx; } +.search-input { flex: 1; height: 68rpx; font-size: 28rpx; } +.search-icon { font-size: 32rpx; margin-left: 10rpx; color: #999; } +.notify-switch { margin-left: 20rpx; } +.notify-text { font-size: 26rpx; color: #666; padding: 10rpx 16rpx; background: #f0f0f0; border-radius: 30rpx; } + +.tab-row { display: flex; background: #fff; border-bottom: 1rpx solid #e8e8e8; } +.tab-item { flex: 1; display: flex; justify-content: center; align-items: center; padding: 24rpx 0; position: relative; color: #666; font-size: 28rpx; } +.tab-item.active { color: #07c160; font-weight: 600; } +.tab-item.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 60rpx; height: 6rpx; background: #07c160; border-radius: 3rpx; } +.tab-badge { background: #fa5151; color: #fff; font-size: 20rpx; border-radius: 20rpx; padding: 2rpx 12rpx; margin-left: 8rpx; min-width: 32rpx; text-align: center; } + +.conversation-list { background: #fff; } +.conversation-item { display: flex; align-items: flex-start; padding: 24rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; } +.order-group-item { min-height: 160rpx; padding: 28rpx 24rpx; } +.has-unread { background: #fafafa; } +.avatar { width: 96rpx; height: 96rpx; border-radius: 12rpx; margin-right: 20rpx; background: #eee; flex-shrink: 0; } +.avatar-round { border-radius: 50%; } +.info { flex: 1; min-width: 0; } +.top-line { display: flex; justify-content: space-between; margin-bottom: 8rpx; } +.name { font-size: 30rpx; color: #1a1a1a; font-weight: 500; } +.time { font-size: 22rpx; color: #999; flex-shrink: 0; margin-left: 12rpx; } +.user-meta-row { margin-bottom: 4rpx; } +.user-meta { font-size: 24rpx; color: #333; } +.user-meta.sub { color: #666; font-size: 22rpx; } +.order-block { background: #f7f8fa; border-radius: 12rpx; padding: 16rpx 18rpx; margin-bottom: 10rpx; } +.order-id { display: block; font-size: 24rpx; color: #576b95; margin-bottom: 6rpx; } +.order-desc { display: block; font-size: 26rpx; color: #333; line-height: 1.5; word-break: break-all; } +.order-status { display: inline-block; font-size: 22rpx; color: #07c160; margin-top: 8rpx; padding: 2rpx 12rpx; background: #e8f8ef; border-radius: 8rpx; } +.bottom-line { display: flex; justify-content: space-between; align-items: center; } +.last-msg { font-size: 26rpx; color: #888; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.unread-dot { background: #fa5151; color: #fff; font-size: 20rpx; border-radius: 50%; min-width: 36rpx; height: 36rpx; line-height: 36rpx; text-align: center; margin-left: 12rpx; } +.load-tip { text-align: center; padding: 24rpx; color: #999; font-size: 24rpx; } +.empty { text-align: center; padding: 200rpx 0; color: #bbb; font-size: 28rpx; } + +/* 客服入口 */ +.kefu-entry { text-align: center; padding: 100rpx 0; } +.kefu-btn { width: 300rpx; background: #07c160; color: #fff; border-radius: 40rpx; font-size: 28rpx; } + +/* 操作弹窗 */ +.action-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); z-index: 10000; } +.action-sheet { position: absolute; bottom: 0; left: 0; right: 0; background: #fff; border-radius: 24rpx 24rpx 0 0; padding: 20rpx 20rpx 150rpx 20rpx; } +.action-btn { text-align: center; padding: 28rpx 0; font-size: 32rpx; border-bottom: 1rpx solid #eee; } +.cancel { color: #999; margin-top: 12rpx; border-bottom: none; } \ No newline at end of file diff --git a/miniprogram/utils/chat-core.js b/miniprogram/utils/chat-core.js new file mode 100644 index 0000000..20f5265 --- /dev/null +++ b/miniprogram/utils/chat-core.js @@ -0,0 +1,672 @@ +// utils/chat-core.js +import GoEasy from '../static/lib/goeasy-2.13.24.esm.min'; +import { jianquanxian } from './imAuth/jianquanxian'; +import { getFreshImUser } from './im-user.js'; + +let _globalPrivateHandler = null; +let _globalGroupHandler = null; +let _globalConvHandler = null; + +function initGlobalMessageSystem(app) { + app.updateUnreadCount = (count) => updateUnreadCount(app, count); + app.disconnectGoEasy = () => disconnectGoEasy(app); + app.ensureConnection = () => ensureConnection(app); + app.connectWithIdentity = (identityType, userId, isAutoRestore) => connectWithIdentity(app, identityType, userId, isAutoRestore); + app.connectForCurrentRole = () => connectForCurrentRole(app); + app.switchRoleAndReconnect = (newRole) => switchRoleAndReconnect(app, newRole); + app.toggleDoNotDisturb = (enabled) => toggleDoNotDisturb(app, enabled); + app.toggleNotificationMute = (enabled) => toggleNotificationMute(app, enabled); + app.closeNotification = () => closeNotification(app); + app.loadConversations = () => loadConversations(app); + app.updateCurrentPageState = () => updateCurrentPageState(app); + app.getSavedConnection = () => getSavedConnection(app); + app.clearSavedConnection = () => clearSavedConnection(app); + app.saveConnectionState = () => saveConnectionState(app); + app.handleNewMessage = (message) => handleNewMessage(app, message); + + loadUserSettings(app); + initNetworkListener(app); + initAppStateListener(app); + checkAndRestoreConnection(app); + setupEventSystem(app); + setupMessageListeners(app); +} + +function getCurrentGoEasyUserId() { + return (wx.goEasy && wx.goEasy.im && wx.goEasy.im.userId) || null; +} + +function loadUserSettings(app) { + try { + const messageSettings = wx.getStorageSync(app.globalData.messageManager.cacheKeys.messageSettings); + if (messageSettings) { + app.globalData.messageManager = { ...app.globalData.messageManager, ...messageSettings }; + } + const connectionSettings = wx.getStorageSync(app.globalData.goEasyConnection.cacheKeys.savedConnection); + if (connectionSettings) { + app.globalData.goEasyConnection = { ...app.globalData.goEasyConnection, ...connectionSettings }; + } + const savedUnread = wx.getStorageSync(app.globalData.messageManager.cacheKeys.unreadTotal); + if (savedUnread !== '') { + app.globalData.messageManager.unreadTotal = savedUnread; + updateTabBarBadge(app, savedUnread); + } + const savedMessages = wx.getStorageSync(app.globalData.messageManager.cacheKeys.latestMessages); + if (savedMessages) { + app.globalData.messageManager.latestMessages = savedMessages; + } + const notificationMuted = wx.getStorageSync(app.globalData.messageManager.cacheKeys.notificationMuted); + if (notificationMuted !== '') { + app.globalData.messageManager.notificationMuted = notificationMuted; + } + } catch (error) { + console.warn('加载用户设置失败:', error); + } +} + +function initNetworkListener(app) { + wx.onNetworkStatusChange((res) => { + if (res.isConnected) { + ensureConnection(app); + } else { + app.globalData.goEasyConnection.status = 'disconnected'; + app.emitEvent('connectionChanged', { status: 'disconnected', reason: 'network' }); + } + }); +} + +function initAppStateListener(app) { + wx.onAppShow(() => { + ensureConnection(app); + updateCurrentPageState(app); + // ✅ 连接正常时,主动刷一次未读角标 + if (wx.goEasy.getConnectionStatus && wx.goEasy.getConnectionStatus() === 'connected') { + loadConversations(app); + } + }); + wx.onAppHide(() => { + pauseHeartbeat(app); + saveConnectionState(app); + }); +} + +function checkAndRestoreConnection(app) { + try { + const savedConnection = wx.getStorageSync(app.globalData.goEasyConnection.cacheKeys.savedConnection); + if (savedConnection && savedConnection.userId && savedConnection.identityType) { + const now = Date.now(); + const lastTime = savedConnection.lastConnectTime || 0; + const hoursDiff = (now - lastTime) / (1000 * 60 * 60); + const validityHours = app.globalData.goEasyConnection.config.cacheValidityHours || 12; + if (hoursDiff > validityHours) { + clearSavedConnection(app); + } else { + ensureConnection(app); + } + } + } catch (error) { + console.warn('检查保存的连接信息失败:', error); + } +} + +function setupEventSystem(app) { + app.globalData.eventListeners = {}; +} + +// 🔥 修改点:不依赖 saved 缓存,只要连接状态正常就刷新角标 +function ensureConnection(app) { + const connectionStatus = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected'; + if (connectionStatus === 'connected' || connectionStatus === 'reconnected') { + setupMessageListeners(app); + loadConversations(app); // 每次进入前台都刷新未读 + return; + } + + // 连接不存在时尝试恢复 + const saved = getSavedConnection(app); + if (!saved || !saved.userId || !saved.identityType) return; + + connectWithIdentity(app, saved.identityType, saved.userId, true); +} + +function getSavedConnection(app) { + try { + const saved = wx.getStorageSync(app.globalData.goEasyConnection.cacheKeys.savedConnection); + if (!saved) return null; + if (typeof saved === 'string') return JSON.parse(saved); + return saved; + } catch (error) { + console.error('获取保存的连接信息失败:', error); + return null; + } +} + +function saveConnectionState(app) { + try { + const { goEasyConnection } = app.globalData; + const connectionState = { + userId: goEasyConnection.userId, + identityType: goEasyConnection.identityType, + lastConnectTime: Date.now(), + autoReconnect: goEasyConnection.autoReconnect, + status: goEasyConnection.status, + }; + wx.setStorageSync(goEasyConnection.cacheKeys.savedConnection, JSON.stringify(connectionState)); + } catch (error) { + console.warn('保存连接状态失败:', error); + } +} + +function clearSavedConnection(app) { + const { cacheKeys } = app.globalData.goEasyConnection; + wx.removeStorageSync(cacheKeys.savedConnection); + wx.removeStorageSync(cacheKeys.userId); + wx.removeStorageSync(cacheKeys.identityType); + app.globalData.goEasyConnection.userId = ''; + app.globalData.goEasyConnection.identityType = ''; + app.globalData.goEasyConnection.lastConnectTime = 0; +} + +function disconnectGoEasy(app) { + return new Promise((resolve) => { + app.globalData.goEasyConnection.status = 'disconnected'; + app.globalData.goEasyConnection.autoReconnect = false; + stopHeartbeat(app); + removeGlobalMessageListeners(); + if (wx.goEasy && wx.goEasy.disconnect) { + wx.goEasy.disconnect({ + onSuccess: () => { + clearSavedConnection(app); + app.globalData.messageManager.unreadTotal = 0; + updateTabBarBadge(app, 0); + app.emitEvent('connectionChanged', { status: 'disconnected', manual: true }); + resolve(); + }, + onFailed: () => { + clearSavedConnection(app); + app.globalData.messageManager.unreadTotal = 0; + updateTabBarBadge(app, 0); + resolve(); + } + }); + } else { + clearSavedConnection(app); + app.globalData.messageManager.unreadTotal = 0; + updateTabBarBadge(app, 0); + resolve(); + } + }); +} + +async function connectWithIdentity(app, identityType, userId, isAutoRestore = false) { + const quanxian = await jianquanxian(app); + if (!quanxian.allowed) return Promise.reject(quanxian.reason); + + app.globalData.goEasyConnection.autoReconnect = true; + app.globalData.goEasyConnection.status = 'connecting'; + app.globalData.goEasyConnection.userId = userId; + app.globalData.goEasyConnection.identityType = identityType; + if (!isAutoRestore) app.globalData.goEasyConnection.reconnectAttempts = 0; + saveConnectionState(app); + + const uid = wx.getStorageSync('uid'); + const role = app.globalData.currentRole || identityType || 'normal'; + const freshUser = getFreshImUser(app, role, uid); + const currentUser = app.globalData.currentUser || { + id: uid, + name: freshUser.name, + avatar: freshUser.avatar, + }; + + return new Promise((resolve, reject) => { + wx.goEasy.connect({ + id: userId, + data: { + name: freshUser.name || currentUser.name, + avatar: freshUser.avatar || currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang), + identityType, + }, + onSuccess: () => { + app.globalData.goEasyConnection.status = 'connected'; + app.globalData.goEasyConnection.lastConnectTime = Date.now(); + app.globalData.goEasyConnection.reconnectAttempts = 0; + saveConnectionState(app); + startHeartbeat(app); + setupMessageListeners(app); + loadConversations(app); // 连接成功立刻拉未读数 + app.emitEvent('connectionChanged', { status: 'connected', identityType, userId }); + if (!isAutoRestore) { + wx.showToast({ title: '连接成功', icon: 'success', duration: 2000 }); + } + resolve(); + }, + onFailed: (error) => { + console.error('GoEasy连接失败:', error); + if (error.code === 900) { + app.globalData.goEasyConnection.autoReconnect = false; + app.globalData.goEasyConnection.status = 'disconnected'; + clearSavedConnection(app); + stopHeartbeat(app); + wx.showModal({ + title: '消息功能暂不可用', + content: '当前使用人数爆满,消息功能暂时无法使用。请联系管理员升级套餐后重试。', + showCancel: false, + confirmText: '我知道了' + }); + app.emitEvent('connectionChanged', { status: 'disconnected', error, reason: 'overcapacity' }); + reject(error); + return; + } + app.globalData.goEasyConnection.status = 'disconnected'; + app.emitEvent('connectionChanged', { status: 'disconnected', error, isAutoRestore }); + const { autoReconnect, reconnectAttempts, maxReconnectAttempts } = app.globalData.goEasyConnection; + if (autoReconnect && reconnectAttempts < maxReconnectAttempts) { + app.globalData.goEasyConnection.reconnectAttempts++; + setTimeout(() => { + if (app.globalData.goEasyConnection.autoReconnect) { + connectWithIdentity(app, identityType, userId, true); + } + }, app.globalData.goEasyConnection.config.reconnectDelay); + reject(error); + } else { + reject(error); + } + }, + }); + }); +} + +function connectForCurrentRole(app) { + const uid = wx.getStorageSync('uid'); + if (!uid) return; + const role = app.globalData.currentRole || 'normal'; + const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' , kaoheguan: 'Kh'}; + const prefix = prefixMap[role] || 'Boss'; + const targetUserId = prefix + uid; + + const currentUserId = getCurrentGoEasyUserId(); + if (currentUserId === targetUserId) { + loadConversations(app); + setupMessageListeners(app); + return; + } + if (currentUserId) { + wx.goEasy.disconnect({ onSuccess: () => {}, onFailed: () => {} }); + } + connectWithIdentity(app, role, targetUserId, false); +} + +async function switchRoleAndReconnect(app, newRole) { + try { + await disconnectGoEasy(app); + app.globalData.currentRole = newRole; + wx.setStorageSync('currentRole', newRole); + const uid = wx.getStorageSync('uid'); + const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' , kaoheguan: 'Kh'}; + const prefix = prefixMap[newRole] || 'Boss'; + const targetUserId = prefix + uid; + await connectWithIdentity(app, newRole, targetUserId, false); + return true; + } catch (error) { + wx.showToast({ title: '聊天功能不可用,可能是人数爆满或未充值会员', icon: 'none' }); + throw error; + } +} + +function startHeartbeat(app) { + const { goEasyConnection } = app.globalData; + stopHeartbeat(app); + goEasyConnection.heartbeatInterval = setInterval(() => { + if (goEasyConnection.status === 'connected' && wx.goEasy?.im) { + wx.goEasy.im.ping({ + onSuccess: () => {}, + onFailed: (error) => { + console.error('心跳失败:', error); + goEasyConnection.status = 'reconnecting'; + attemptReconnect(app); + }, + }); + } + }, goEasyConnection.config.heartbeatInterval); +} + +function stopHeartbeat(app) { + if (app.globalData.goEasyConnection.heartbeatInterval) { + clearInterval(app.globalData.goEasyConnection.heartbeatInterval); + app.globalData.goEasyConnection.heartbeatInterval = null; + } +} + +function pauseHeartbeat(app) { stopHeartbeat(app); } + +function attemptReconnect(app) { + const { goEasyConnection } = app.globalData; + if (!goEasyConnection.autoReconnect) return; + if (goEasyConnection.userId && goEasyConnection.identityType) { + connectWithIdentity(app, goEasyConnection.identityType, goEasyConnection.userId, true); + } +} + +/* ---------- 消息监听器 ---------- */ +function removeGlobalMessageListeners() { + if (_globalPrivateHandler) { + wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _globalPrivateHandler); + _globalPrivateHandler = null; + } + if (_globalGroupHandler) { + wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _globalGroupHandler); + _globalGroupHandler = null; + } + if (_globalConvHandler) { + wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _globalConvHandler); + _globalConvHandler = null; + } +} + +function setupMessageListeners(app) { + removeGlobalMessageListeners(); + + _globalPrivateHandler = (message) => { + app.handleNewMessage(message); + }; + _globalGroupHandler = (message) => { + app.handleNewMessage(message); + }; + _globalConvHandler = (conversations) => { + const unreadTotal = conversations.unreadTotal || 0; + app.globalData.messageManager.unreadTotal = unreadTotal; + updateTabBarBadge(app, unreadTotal); + wx.setStorageSync(app.globalData.messageManager.cacheKeys.unreadTotal, unreadTotal); + if (typeof app.emitEvent === 'function') { + app.emitEvent('conversationsUpdated', conversations); + } + }; + + wx.goEasy.im.on(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _globalPrivateHandler); + wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _globalGroupHandler); + wx.goEasy.im.on(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _globalConvHandler); +} + +function handleNewMessage(app, message) { + const messageId = message.messageId || message.id; + const fingerprint = `${message.senderId}_${message.timestamp}_${message.type}`; + const msgKey = messageId || fingerprint; + + if (!app._processedMessages) app._processedMessages = new Set(); + if (app._processedMessages.has(msgKey)) return; + app._processedMessages.add(msgKey); + if (app._processedMessages.size > 200) { + app._processedMessages.delete(app._processedMessages.values().next().value); + } + + const now = Date.now(); + const msgTime = message.timestamp || now; + if (now - msgTime > 60000) { + cacheMessage(app, message); + return; + } + + cacheMessage(app, message); + + if (shouldShowNotification(app)) { + let notificationType = 'private'; + let extraData = {}; + if (message.groupId) { + notificationType = 'group'; + extraData = { + groupId: message.groupId, + orderId: message.orderId || '', + groupName: '', + groupAvatar: '', + isCross: message.isCross || 0 + }; + const groupInfo = app.globalData.groupInfoMap?.[message.groupId]; + if (groupInfo) { + extraData.groupName = groupInfo.name || ''; + extraData.groupAvatar = groupInfo.avatar || ''; + } + } else if (message.teamId) { + notificationType = 'cs'; + extraData = { teamId: message.teamId }; + } + showNotificationDirect(app, { + senderId: message.senderId, + senderName: message.senderData?.name || '用户', + avatar: message.senderData?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang), + content: formatMessageForNotification(message), + message, + notificationType, + ...extraData + }); + } +} + +function shouldShowNotification(app) { + if (app.globalData.messageManager.notificationMuted) return false; + if (isDoNotDisturbTime(app)) return false; + if (app.globalData.pageState.isInChatPage) { + const pages = getCurrentPages(); + if (pages.length > 0) { + const currentPage = pages[pages.length - 1]; + if (currentPage.data && currentPage.data.currentChatId) { + if (isMessageFromCurrentChat(app, currentPage.data.currentChatId)) return false; + } + } + } + return true; +} + +function isDoNotDisturbTime(app) { + const { doNotDisturb, doNotDisturbStart, doNotDisturbEnd } = app.globalData.messageManager; + if (!doNotDisturb) return false; + const now = new Date(); + const currentTime = now.getHours() * 60 + now.getMinutes(); + const [sH, sM] = doNotDisturbStart.split(':').map(Number); + const [eH, eM] = doNotDisturbEnd.split(':').map(Number); + const startTime = sH * 60 + sM; + const endTime = eH * 60 + eM; + if (startTime < endTime) { + return currentTime >= startTime && currentTime < endTime; + } else { + return currentTime >= startTime || currentTime < endTime; + } +} + +function isMessageFromCurrentChat(app, currentChatId) { + return false; +} + +function showNotificationDirect(app, data) { + if (app.globalData.globalNotification?.show) { + try { + app.globalData.globalNotification.show(data); + return; + } catch (error) { + console.warn('全局通知显示失败:', error); + } + } + app.emitEvent('showNotification', data); +} + +async function updateUnreadCount(app, customCount) { + let unreadTotal = customCount; + if (unreadTotal === undefined) { + try { + const result = await getUnreadCount(app); + unreadTotal = result.unreadTotal || 0; + } catch (error) { + console.error('获取未读数失败:', error); + return; + } + } + app.globalData.messageManager.unreadTotal = unreadTotal; + updateTabBarBadge(app, unreadTotal); + wx.setStorageSync(app.globalData.messageManager.cacheKeys.unreadTotal, unreadTotal); + app.emitEvent('unreadCountChanged', { unreadTotal }); +} + +function getUnreadCount(app) { + return new Promise((resolve, reject) => { + if (!getCurrentGoEasyUserId()) { + resolve({ unreadTotal: 0 }); + return; + } + wx.goEasy.im.latestConversations({ + onSuccess: (result) => resolve({ + unreadTotal: result.unreadTotal || 0, + conversations: result.content?.conversations || [], + }), + onFailed: reject, + }); + }); +} + +function formatMessageForNotification(message) { + switch (message.type) { + case 'text': return message.payload.text || '[文本]'; + case 'image': return '[图片]'; + case 'audio': return '[语音]'; + case 'video': return '[视频]'; + case 'file': return '[文件]'; + case 'order': return '[订单]'; + default: return '[新消息]'; + } +} + +function cacheMessage(app, message) { + const { latestMessages } = app.globalData.messageManager; + latestMessages.unshift({ + id: message.messageId, + type: message.type, + senderId: message.senderId, + senderName: message.senderData?.name, + content: formatMessageForNotification(message), + timestamp: message.timestamp || Date.now(), + conversationId: message.conversationId, + }); + if (latestMessages.length > 50) latestMessages.length = 50; + wx.setStorageSync(app.globalData.messageManager.cacheKeys.latestMessages, latestMessages); +} + +function loadConversations(app) { + if (!getCurrentGoEasyUserId()) return; + wx.goEasy.im.latestConversations({ + onSuccess: (result) => { + if (result.unreadTotal !== undefined) { + updateUnreadCount(app, result.unreadTotal); + } + app.emitEvent('conversationsUpdated', result); + const conversations = result?.content?.conversations || result?.conversations || []; + if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {}; + conversations.forEach(c => { + if (c.type === 'group' && c.groupId) { + app.globalData.groupInfoMap[c.groupId] = c.data || {}; + } + }); + _ensureGroupSubscriptions(app, conversations); + }, + onFailed: (error) => console.error('加载会话列表失败:', error), + }); +} + +function _ensureGroupSubscriptions(app, conversations) { + const groupIds = conversations + .filter(c => c.type === 'group' && c.groupId) + .map(c => c.groupId); + + if (groupIds.length === 0) return; + if (!wx.goEasy?.im || typeof wx.goEasy.im.subscribeGroup !== 'function') { + console.error('subscribeGroup 不可用'); + return; + } + + wx.goEasy.im.subscribeGroup({ + groupIds: groupIds, + onSuccess: () => {}, + onFailed: (error) => console.error('全局订阅群组失败:', error), + }); +} + +function updateCurrentPageState(app) { + try { + const pages = getCurrentPages(); + if (pages.length > 0) { + const currentPage = pages[pages.length - 1]; + const chatPages = ['pages/liaotian/liaotian', 'pages/qunliaotian/qunliaotian', 'pages/kefuliaotian/kefuliaotian']; + app.globalData.pageState.currentPage = currentPage.route; + app.globalData.pageState.isInChatPage = chatPages.indexOf(currentPage.route) >= 0; + app.globalData.pageState.lastPageUpdate = Date.now(); + } + } catch (error) { + console.warn('更新页面状态失败:', error); + } +} + +function saveUserSettings(app) { + try { + wx.setStorageSync(app.globalData.messageManager.cacheKeys.messageSettings, { + soundEnabled: app.globalData.messageManager.soundEnabled, + vibrationEnabled: app.globalData.messageManager.vibrationEnabled, + doNotDisturb: app.globalData.messageManager.doNotDisturb, + doNotDisturbStart: app.globalData.messageManager.doNotDisturbStart, + doNotDisturbEnd: app.globalData.messageManager.doNotDisturbEnd, + notificationMuted: app.globalData.messageManager.notificationMuted, + notificationStyle: app.globalData.messageManager.notificationStyle, + }); + } catch (error) { + console.warn('保存用户设置失败:', error); + } +} + +function toggleDoNotDisturb(app, enabled) { + app.globalData.messageManager.doNotDisturb = enabled; + saveUserSettings(app); + wx.showToast({ title: `免打扰${enabled ? '开启' : '关闭'}`, icon: 'success', duration: 2000 }); +} + +function toggleNotificationMute(app, enabled) { + app.globalData.messageManager.notificationMuted = enabled; + wx.setStorageSync(app.globalData.messageManager.cacheKeys.notificationMuted, enabled); + saveUserSettings(app); + wx.showToast({ title: `通知${enabled ? '静音' : '开启'}`, icon: 'success', duration: 2000 }); +} + +function closeNotification(app) { + app.emitEvent('hideNotification'); +} + +function updateTabBarBadge(app, unreadCount) { + const { messageManager } = app.globalData; + if (!messageManager.showTabBarBadge) return; + let badgeText = ''; + if (unreadCount > 0) { + badgeText = unreadCount > 99 ? '99+' : unreadCount.toString(); + } + messageManager.tabBarBadgeText = badgeText; + app.emitEvent('tabBarBadgeChanged', { + index: messageManager.tabBarIndex, + badgeText, + showRedDot: unreadCount > 0, + forceUpdate: true, + }); + updateTabBarDirectly(app, badgeText); + saveTabBarBadgeToStorage(app, badgeText); +} + +function updateTabBarDirectly(app, badgeText) { + try { + const pages = getCurrentPages(); + if (pages.length > 0) { + const tabBar = pages[pages.length - 1].selectComponent('#custom-tab-bar'); + if (tabBar && tabBar.setData) tabBar.setData({ badgeText }); + } + } catch (error) { console.warn('直接更新TabBar失败:', error); } +} + +function saveTabBarBadgeToStorage(app, badgeText) { + try { + wx.setStorageSync('tabBarMessageBadge', badgeText); + } catch (error) { console.warn('保存TabBar徽章失败:', error); } +} + +module.exports = { initGlobalMessageSystem }; \ No newline at end of file diff --git a/miniprogram/utils/group-chat.js b/miniprogram/utils/group-chat.js new file mode 100644 index 0000000..dc81e6b --- /dev/null +++ b/miniprogram/utils/group-chat.js @@ -0,0 +1,186 @@ +/** + * 打手-商家/老板配对群:group_Ds{打手}_Sj{商家} + */ +export const ORDER_CARD_PREFIX = '[ORDER_CARD]'; + +const ZHUANGTAI_MAP = { + 1: '已下单', 2: '进行中', 3: '已完成', 4: '退款中', + 5: '已退款', 6: '退款失败', 7: '指定中', 8: '结算中', +}; + +export function isPairGroupId(groupId) { + return /^group_Ds\w+_(Sj|Boss)\w+$/.test(groupId || ''); +} + +export function parsePairGroupId(groupId) { + if (!groupId) return null; + let m = /^group_Ds(\w+)_Sj(\w+)$/.exec(groupId); + if (m) return { dashouId: `Ds${m[1]}`, partnerId: `Sj${m[2]}`, partnerRole: 'shangjia' }; + m = /^group_Ds(\w+)_Boss(\w+)$/.exec(groupId); + if (m) return { dashouId: `Ds${m[1]}`, partnerId: `Boss${m[2]}`, partnerRole: 'boss' }; + return null; +} + +export function getCounterpartGoEasyId(groupId, myGoEasyId) { + const parsed = parsePairGroupId(groupId); + if (!parsed || !myGoEasyId) return null; + if (myGoEasyId === parsed.dashouId) return parsed.partnerId; + if (myGoEasyId === parsed.partnerId) return parsed.dashouId; + return null; +} + +function fixAvatarUrl(avatar, defaultAvatar, ossBase) { + if (!avatar) return defaultAvatar; + if (avatar.startsWith('http')) return avatar; + return (ossBase || '') + avatar.replace(/^\//, ''); +} + +/** 根据群 to.data 与当前身份,解析对方/双方展示信息 */ +export function applyPairMetaToConversation(item, myGoEasyId, defaultAvatar, ossBase) { + if (!item || !item.data) return; + const d = item.data; + + if (d.dashouGoEasyId && d.partnerGoEasyId) { + const isDashou = myGoEasyId === d.dashouGoEasyId; + const isPartner = myGoEasyId === d.partnerGoEasyId; + d.dashouNicheng = d.dashouName || d.dashouNicheng || ''; + d.partnerNicheng = d.partnerName || d.partnerNicheng || ''; + d.dashouAvatar = fixAvatarUrl(d.dashouAvatar, defaultAvatar, ossBase); + d.partnerAvatar = fixAvatarUrl(d.partnerAvatar, defaultAvatar, ossBase); + + if (isDashou) { + d.counterpartId = d.partnerGoEasyId; + d.counterpartYonghuid = d.partnerYonghuid || d.partnerGoEasyId.replace(/^(Sj|Boss)/, ''); + d.partnerRoleLabel = d.partnerGoEasyId.startsWith('Boss') ? '老板' : '商家'; + d.name = d.partnerNicheng || `${d.partnerRoleLabel}${d.counterpartYonghuid}`; + d.avatar = d.partnerAvatar || defaultAvatar; + } else if (isPartner) { + d.counterpartId = d.dashouGoEasyId; + d.counterpartYonghuid = d.dashouYonghuid || d.dashouGoEasyId.replace(/^Ds/, ''); + d.name = d.dashouNicheng || `打手${d.counterpartYonghuid}`; + d.avatar = d.dashouAvatar || defaultAvatar; + } else { + const cpId = getCounterpartGoEasyId(item.groupId, myGoEasyId); + if (cpId === d.partnerGoEasyId) { + d.name = d.partnerNicheng; + d.avatar = d.partnerAvatar || defaultAvatar; + } else if (cpId === d.dashouGoEasyId) { + d.name = d.dashouNicheng; + d.avatar = d.dashouAvatar || defaultAvatar; + } + } + return; + } + + const counterpartId = getCounterpartGoEasyId(item.groupId, myGoEasyId); + if (counterpartId) { + d.counterpartId = counterpartId; + d.counterpartYonghuid = counterpartId.replace(/^(Ds|Sj|Boss)/, ''); + if (!d.name || d.name === item.groupId) { + d.name = `用户${d.counterpartYonghuid.slice(-6)}`; + } + } + + if (item.lastMessage && item.lastMessage.senderId && item.lastMessage.senderId !== myGoEasyId) { + const sd = item.lastMessage.senderData || {}; + if (sd.avatar) d.avatar = fixAvatarUrl(sd.avatar, defaultAvatar, ossBase); + if (sd.name) d.name = sd.name; + } + + d.avatar = fixAvatarUrl(d.avatar, defaultAvatar, ossBase); +} + +export function parseOrderCardText(text) { + if (!text || !text.startsWith(ORDER_CARD_PREFIX)) return null; + try { + const data = JSON.parse(text.slice(ORDER_CARD_PREFIX.length)); + return { + orderId: data.dingdan_id, + jieshao: data.jieshao || '', + jine: data.jine || '', + beizhu: data.beizhu || '', + zhuangtai: data.zhuangtai, + nicheng: data.nicheng || '', + create_time: data.create_time || '', + }; + } catch (e) { + return null; + } +} + +export function normalizeGroupMessage(msg) { + if (!msg) return msg; + if (msg.type === 'text' && msg.payload && msg.payload.text) { + const card = parseOrderCardText(msg.payload.text); + if (card) { + msg.type = 'order'; + msg.payload = card; + } + } + return msg; +} + +export function formatLastMessagePreview(msg) { + if (!msg) return ''; + if (msg.type === 'text' && msg.payload && msg.payload.text) { + const card = parseOrderCardText(msg.payload.text); + if (card) return `[订单] ${card.jieshao || card.orderId}`; + return msg.payload.text; + } + if (msg.type === 'order' && msg.payload) { + return `[订单] ${msg.payload.jieshao || msg.payload.orderId || ''}`; + } + if (msg.type === 'image') return '[图片]'; + return '[消息]'; +} + +export function enrichGroupConversation(item, myGoEasyId, defaultAvatar, ossBase) { + if (!item || item.type !== 'group') return item; + if (!item.data) item.data = {}; + + applyPairMetaToConversation(item, myGoEasyId, defaultAvatar, ossBase); + + if (item.data.orderZhuangtai != null) { + item.data.orderZhuangtaiText = ZHUANGTAI_MAP[item.data.orderZhuangtai] || ''; + } + + item.displayLastMsg = formatLastMessagePreview(item.lastMessage); + if (!item.data.avatar) item.data.avatar = defaultAvatar; + return item; +} + +export function isOrderGroupConversation(c) { + if (!c || c.type !== 'group') return false; + if (c.data && c.data.orderId) return true; + if (c.data && c.data.dashouGoEasyId) return true; + return isPairGroupId(c.groupId) || /^group_\w+$/.test(c.groupId || ''); +} + +export function getConversationSortTime(conversation, openTimes) { + const id = conversation.groupId || conversation.userId || ''; + const msgTs = conversation.lastMessage?.timestamp || 0; + const openTs = (openTimes && id && openTimes[id]) ? openTimes[id] : 0; + return Math.max(msgTs, openTs); +} + +export function sortConversations(list, openTimes) { + return list.sort((a, b) => { + if (a.top && !b.top) return -1; + if (!a.top && b.top) return 1; + if (a.unread > 0 && b.unread <= 0) return -1; + if (a.unread <= 0 && b.unread > 0) return 1; + const ta = getConversationSortTime(a, openTimes); + const tb = getConversationSortTime(b, openTimes); + return tb - ta; + }); +} + +export function recordConversationOpen(conversation) { + const id = conversation?.groupId || conversation?.userId; + if (!id) return; + try { + const times = wx.getStorageSync('conversationOpenTimes') || {}; + times[id] = Date.now(); + wx.setStorageSync('conversationOpenTimes', times); + } catch (e) {} +} diff --git a/miniprogram/utils/im-user.js b/miniprogram/utils/im-user.js new file mode 100644 index 0000000..2ea6b63 --- /dev/null +++ b/miniprogram/utils/im-user.js @@ -0,0 +1,50 @@ +/** + * IM 用户展示:从本地缓存读取最新头像,空则用默认图 + */ +export function getDefaultAvatar(app) { + const oss = app.globalData.ossImageUrl || ''; + return oss + (app.globalData.morentouxiang || ''); +} + +export function resolveAvatarUrl(raw, app) { + const def = getDefaultAvatar(app); + if (!raw) return def; + if (raw.startsWith('http')) return raw; + const oss = app.globalData.ossImageUrl || ''; + return oss + raw.replace(/^\//, ''); +} + +export function getFreshImUser(app, role, uid) { + const prefixMap = { + normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', + guanshi: 'Gs', zuzhang: 'Zz', kaoheguan: 'Kh', + }; + const prefix = prefixMap[role] || 'Boss'; + const touxiang = wx.getStorageSync('touxiang') || ''; + const avatar = resolveAvatarUrl(touxiang, app); + const name = app.globalData.currentUser?.name || `用户${(uid || '').slice(-6)}`; + return { + id: prefix + uid, + name, + avatar, + }; +} + +export function persistGroupMeta(app, groupId, meta) { + if (!groupId || !meta) return; + if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {}; + app.globalData.groupInfoMap[groupId] = { ...app.globalData.groupInfoMap[groupId], ...meta }; + try { + const cache = wx.getStorageSync('pairGroupMetaCache') || {}; + cache[groupId] = app.globalData.groupInfoMap[groupId]; + wx.setStorageSync('pairGroupMetaCache', cache); + } catch (e) {} +} + +export function loadGroupMetaCache(app) { + try { + const cache = wx.getStorageSync('pairGroupMetaCache') || {}; + if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {}; + Object.assign(app.globalData.groupInfoMap, cache); + } catch (e) {} +} diff --git a/miniprogram/utils/message-sender.js b/miniprogram/utils/message-sender.js new file mode 100644 index 0000000..be84948 --- /dev/null +++ b/miniprogram/utils/message-sender.js @@ -0,0 +1,172 @@ +// utils/message-sender.js +const app = getApp(); +import { formatDate } from '../static/lib/utils'; +import request from './request.js'; +import { getFreshImUser } from './im-user.js'; + +function sendGroupMessage(options) { + const { msgData, onSuccess, onSendStart } = options; + const role = app.globalData.currentRole || 'normal'; + const uid = wx.getStorageSync('uid'); + const freshUser = getFreshImUser(app, role, uid); + msgData.currentUser = { + ...msgData.currentUser, + name: freshUser.name, + avatar: freshUser.avatar, + }; + + const { type, currentUser, groupId, orderId, isCross, groupName, groupAvatar } = msgData; + + const localMsg = buildLocalMessage(msgData); + if (onSendStart) onSendStart(localMsg); + + // 只有 isCross 明确为 0 才走前端 SDK,其余一律走后端 + if (isCross === 0) { + sendViaSDK(localMsg, currentUser, groupId, onSuccess, orderId, isCross, groupName, groupAvatar); + return; + } + sendViaBackend(localMsg, currentUser, groupId, orderId, onSuccess); +} + +function buildLocalMessage(data) { + const { type, text, filePath, orderPayload, currentUser, groupId } = data; + const now = Date.now(); + const base = { + messageId: `${type}-${now}`, + timestamp: now, + senderId: currentUser.id, + groupId: groupId, + senderData: { + name: currentUser.name, + avatar: currentUser.avatar + }, + status: 'sending', + formattedTime: formatDate(now) + }; + + if (type === 'text') return { ...base, type: 'text', payload: { text } }; + if (type === 'image') return { ...base, type: 'image', payload: { url: filePath } }; + if (type === 'order') return { ...base, type: 'order', payload: orderPayload }; + return base; +} + +function sendViaSDK(localMsg, currentUser, groupId, onSuccess, orderId, isCross, groupName, groupAvatar) { + const groupMeta = (app.globalData.groupInfoMap && app.globalData.groupInfoMap[groupId]) || {}; + const toData = { + name: groupName || groupMeta.name || '订单群聊', + avatar: groupAvatar || groupMeta.avatar || currentUser.avatar, + orderId: orderId || groupMeta.orderId, + isCross: isCross || 0, + dashouGoEasyId: groupMeta.dashouGoEasyId, + partnerGoEasyId: groupMeta.partnerGoEasyId, + dashouYonghuid: groupMeta.dashouYonghuid, + partnerYonghuid: groupMeta.partnerYonghuid, + dashouName: groupMeta.dashouName, + partnerName: groupMeta.partnerName, + dashouAvatar: groupMeta.dashouAvatar, + partnerAvatar: groupMeta.partnerAvatar, + orderDesc: groupMeta.orderDesc, + }; + + const to = { + type: wx.GoEasy.IM_SCENE.GROUP, + id: groupId, + data: toData + }; + + let message; + if (localMsg.type === 'text') { + message = wx.goEasy.im.createTextMessage({ text: localMsg.payload.text, to }); + } else if (localMsg.type === 'image') { + message = wx.goEasy.im.createImageMessage({ file: localMsg.payload.url, to }); + } else if (localMsg.type === 'order') { + message = wx.goEasy.im.createCustomMessage({ type: 'order', payload: localMsg.payload, to }); + } + + wx.goEasy.im.sendMessage({ + message, + onSuccess: () => { if (onSuccess) onSuccess(localMsg.messageId, 'success'); }, + onFailed: () => { if (onSuccess) onSuccess(localMsg.messageId, 'failed'); } + }); +} + +function sendViaBackend(localMsg, currentUser, groupId, orderId, onSuccess) { + const apiUrl = '/dingdan/kptxxfs'; + + // 🔥 实时获取当前身份,不再依赖外部传入的 currentUser.id + const uid = wx.getStorageSync('uid'); + const role = app.globalData.currentRole || 'normal'; + const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' }; + const identityType = role === 'dashou' ? 'dashou' : (role === 'shangjia' ? 'shangjia' : 'boss'); + const prefix = prefixMap[role] || 'Boss'; + const senderId = prefix + uid; + const freshUser = getFreshImUser(app, role, uid); + const params = { + orderId: orderId, + groupId: groupId, + identityType: identityType, // 告诉后端当前是什么身份 + senderId: senderId, + senderName: freshUser.name || currentUser.name || ('用户' + uid), + senderAvatar: freshUser.avatar || currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang), + messageType: localMsg.type, + messageId: localMsg.messageId, + timestamp: localMsg.timestamp + }; + + if (localMsg.type === 'text' || localMsg.type === 'order') { + if (localMsg.type === 'text') { + params.text = localMsg.payload.text; + } else { + params.orderPayload = localMsg.payload; + } + request({ + url: apiUrl, + method: 'POST', + data: params + }).then((res) => { + if (res && res.data && res.data.code === 200) { + if (onSuccess) onSuccess(localMsg.messageId, 'success'); + } else { + if (onSuccess) onSuccess(localMsg.messageId, 'failed'); + } + }).catch(() => { + if (onSuccess) onSuccess(localMsg.messageId, 'failed'); + }); + return; + } + + // 图片消息 + if (localMsg.type === 'image') { + const token = wx.getStorageSync('token'); + const formData = { + ...params, + senderInfo: JSON.stringify({ id: senderId, name: params.senderName, avatar: params.senderAvatar }) + }; + wx.uploadFile({ + url: app.globalData.apiBaseUrl + apiUrl, + filePath: localMsg.payload.url, + name: 'file', + formData: formData, + header: { + 'Authorization': 'Bearer ' + (token || '') + }, + success(res) { + try { + const data = JSON.parse(res.data); + if (data.code === 200) { + if (onSuccess) onSuccess(localMsg.messageId, 'success'); + } else { + if (onSuccess) onSuccess(localMsg.messageId, 'failed'); + } + } catch (e) { + if (onSuccess) onSuccess(localMsg.messageId, 'failed'); + } + }, + fail() { + if (onSuccess) onSuccess(localMsg.messageId, 'failed'); + } + }); + } +} + +module.exports = { sendGroupMessage }; \ No newline at end of file diff --git a/miniprogram/utils/xiaoxilj.js b/miniprogram/utils/xiaoxilj.js new file mode 100644 index 0000000..2e65348 --- /dev/null +++ b/miniprogram/utils/xiaoxilj.js @@ -0,0 +1,180 @@ +/** + * 连接管理模块 - 订单群聊跳转(后端准备群 + 稳定连接) + */ +import request from './request'; +import { persistGroupMeta } from './im-user.js'; +const app = getApp(); + +function waitForConnection(expectedUserId, timeout = 15000) { + return new Promise((resolve, reject) => { + const status = wx.goEasy?.getConnectionStatus?.(); + const currentUserId = wx.goEasy?.im?.userId; + if ((status === 'connected' || status === 'reconnected') && currentUserId === expectedUserId) { + resolve(); + return; + } + + const timer = setTimeout(() => { + app.off('connectionChanged', handler); + reject(new Error('连接超时')); + }, timeout); + + const handler = (event) => { + if (event.status === 'connected') { + const uid = event.userId || wx.goEasy?.im?.userId; + if (uid === expectedUserId) { + clearTimeout(timer); + app.off('connectionChanged', handler); + resolve(); + } + } else if (event.status === 'disconnected' && event.manual) { + clearTimeout(timer); + app.off('connectionChanged', handler); + reject(new Error('连接已断开')); + } + }; + app.on('connectionChanged', handler); + }); +} + +async function ensureIdentityConnection(identityType, userId) { + app.globalData.currentRole = identityType; + wx.setStorageSync('currentRole', identityType); + + const uid = userId.replace(/^(Ds|Sj|Boss|Gs|Zz|Kh)/, ''); + const avatar = wx.getStorageSync('touxiang') || + (app.globalData.ossImageUrl + app.globalData.morentouxiang); + app.globalData.currentUser = { + id: userId, + name: app.globalData.currentUser?.name || `用户${uid}`, + avatar: avatar, + }; + + const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected'; + const currentUserId = wx.goEasy?.im?.userId; + + if ((status === 'connected' || status === 'reconnected') && currentUserId === userId) { + return; + } + + if (currentUserId && currentUserId !== userId && app.disconnectGoEasy) { + await app.disconnectGoEasy(); + } + + const waitPromise = waitForConnection(userId); + const connectPromise = app.connectWithIdentity(identityType, userId, true); + await waitPromise; + await connectPromise; +} + +class ConnectionManager { + async connectAndChat(params) { + const { identityType, userId, targetUser } = params; + if (!identityType || !userId || !targetUser || !targetUser.id) { + throw new Error('参数不完整'); + } + + await ensureIdentityConnection(identityType, userId); + + const currentUser = { + id: userId, + name: app.globalData.currentUser?.name || '用户', + avatar: app.globalData.currentUser?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang), + }; + + const to = { + id: targetUser.id, + name: targetUser.name || '用户', + avatar: targetUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang), + }; + + const param = { to, currentUser }; + const path = '/pages/liaotian/liaotian?data=' + encodeURIComponent(JSON.stringify(param)); + wx.navigateTo({ url: path }); + } + + async connectToGroupChat(params) { + const { identityType, userId, orderId, groupName, groupAvatar, isCross } = params; + if (!identityType || !userId || !orderId) { + throw new Error('参数不完整:identityType, userId, orderId 必填'); + } + + wx.showLoading({ title: '建立联系中...', mask: true }); + + try { + await ensureIdentityConnection(identityType, userId); + + const res = await request({ + url: '/dingdan/ltdhzb', + method: 'POST', + data: { + dingdan_id: orderId, + identityType, + push_order_card: true, + }, + }); + + const body = res?.data || {}; + if (body.code !== 0 || !body.data || !body.data.groupId) { + throw new Error(body.msg || '准备群聊失败'); + } + + const chatData = body.data; + const realGroupId = chatData.groupId; + + let avatar = chatData.counterpartAvatar || chatData.groupAvatar || groupAvatar || ''; + if (avatar && !avatar.startsWith('http')) { + avatar = app.globalData.ossImageUrl + avatar.replace(/^\//, ''); + } + + if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {}; + const meta = { + name: chatData.counterpartName || chatData.groupName || groupName, + avatar, + orderId: chatData.orderId || orderId, + orderDesc: chatData.orderDesc || '', + orderZhuangtai: chatData.orderZhuangtai, + dashouGoEasyId: chatData.dashouGoEasyId, + partnerGoEasyId: chatData.partnerGoEasyId, + dashouYonghuid: chatData.dashouYonghuid, + partnerYonghuid: chatData.partnerYonghuid, + dashouName: chatData.dashouName, + partnerName: chatData.partnerName, + dashouAvatar: chatData.dashouAvatar, + partnerAvatar: chatData.partnerAvatar, + counterpartId: chatData.counterpartId, + counterpartYonghuid: chatData.counterpartYonghuid, + }; + app.globalData.groupInfoMap[realGroupId] = meta; + persistGroupMeta(app, realGroupId, meta); + + await new Promise((resolve, reject) => { + wx.goEasy.im.subscribeGroup({ + groupIds: [realGroupId], + onSuccess: () => resolve(), + onFailed: (error) => reject(error), + }); + }); + + const param = { + groupId: realGroupId, + orderId: chatData.orderId || orderId, + groupName: chatData.counterpartName || chatData.groupName || groupName || '订单群聊', + groupAvatar: avatar, + isCross: chatData.isCross != null ? chatData.isCross : (isCross || 0), + }; + + wx.hideLoading(); + wx.navigateTo({ + url: '/pages/qunliaotian/qunliaotian?data=' + encodeURIComponent(JSON.stringify(param)), + }); + } catch (err) { + wx.hideLoading(); + console.error('跳转群聊失败:', err); + wx.showToast({ title: err.message || '进入聊天失败', icon: 'none' }); + throw err; + } + } +} + +export default new ConnectionManager();