修复聊天头像与列表:配对群元数据、排序与连接稳定(miniprogram目录)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-23 23:21:20 +08:00
parent 1ea106f101
commit 033359f6f4
9 changed files with 2342 additions and 0 deletions

View File

@@ -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 }); }
});

View File

@@ -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 });
}
});

View File

@@ -0,0 +1,88 @@
<view class="msg-page">
<!-- 顶部搜索 + 通知 -->
<view class="header">
<view class="search-bar">
<input class="search-input" placeholder="搜索订单/用户ID/昵称" bindinput="onSearchInput" value="{{searchKeyword}}" />
<text class="search-icon">🔍</text>
</view>
<view class="notify-switch" bindtap="toggleNotification">
<text class="notify-text">{{notificationMuted ? '静音' : '通知'}}</text>
</view>
</view>
<!-- 标签栏 -->
<view class="tab-row">
<view class="tab-item {{activeTab === 0 ? 'active' : ''}}" data-index="0" bindtap="switchTab">
<text class="tab-label">订单消息</text>
<text class="tab-badge" wx:if="{{tabUnread[0] > 0}}">{{tabUnread[0]}}</text>
</view>
<view class="tab-item {{activeTab === 1 ? 'active' : ''}}" data-index="1" bindtap="switchTab">
<text class="tab-label">用户消息</text>
<text class="tab-badge" wx:if="{{tabUnread[1] > 0}}">{{tabUnread[1]}}</text>
</view>
<view class="tab-item {{activeTab === 2 ? 'active' : ''}}" data-index="2" bindtap="switchTab">
<text class="tab-label">客服消息</text>
<text class="tab-badge" wx:if="{{tabUnread[2] > 0}}">{{tabUnread[2]}}</text>
</view>
</view>
<!-- 会话列表 (上拉加载更多) -->
<scroll-view class="conversation-list" scroll-y="true" bindscrolltolower="onScrollToLower" style="height: {{scrollHeight}}px">
<block wx:for="{{filteredConversations}}" wx:key="key" wx:for-index="i" wx:if="{{i < displayCount}}">
<view class="conversation-item {{item.unread > 0 ? 'has-unread' : ''}} {{item.type === 'group' ? 'order-group-item' : ''}}" data-conversation="{{item}}" bindtap="chat" bindlongpress="showAction">
<image class="avatar {{item.type === 'group' ? 'avatar-round' : ''}}" src="{{item.data.avatar}}" mode="aspectFill" />
<view class="info">
<view class="top-line">
<text class="name">{{item.data.name}}</text>
<text class="time">{{item.lastMessage.date}}</text>
</view>
<block wx:if="{{item.type === 'group'}}">
<view class="user-meta-row" wx:if="{{item.data.counterpartYonghuid}}">
<text class="user-meta">对方 {{item.data.counterpartYonghuid}} {{item.data.name}}</text>
</view>
<view class="user-meta-row" wx:if="{{item.data.dashouYonghuid}}">
<text class="user-meta sub">打手 {{item.data.dashouYonghuid}} {{item.data.dashouNicheng}}</text>
</view>
<view class="user-meta-row" wx:if="{{item.data.partnerYonghuid}}">
<text class="user-meta sub">{{item.data.partnerRoleLabel || '商家'}} {{item.data.partnerYonghuid}} {{item.data.partnerNicheng}}</text>
</view>
<view class="order-block" wx:if="{{item.data.orderId || item.data.orderDesc}}">
<text class="order-id" wx:if="{{item.data.orderId}}">单号 {{item.data.orderId}}</text>
<text class="order-desc" wx:if="{{item.data.orderDesc}}">{{item.data.orderDesc}}</text>
<text class="order-status" wx:if="{{item.data.orderZhuangtaiText}}">{{item.data.orderZhuangtaiText}}</text>
</view>
<view class="bottom-line">
<text class="last-msg">{{item.displayLastMsg || item.lastMessage.payload.text}}</text>
<view class="unread-dot" wx:if="{{item.unread > 0}}">{{item.unread > 99 ? '99+' : item.unread}}</view>
</view>
</block>
<view class="bottom-line" wx:else>
<text class="last-msg">{{item.lastMessage.type === 'text' ? item.lastMessage.payload.text : '[其他消息]'}}</text>
<view class="unread-dot" wx:if="{{item.unread > 0}}">{{item.unread > 99 ? '99+' : item.unread}}</view>
</view>
</view>
</view>
</block>
<!-- 客服标签页空状态时显示联系客服按钮 -->
<view class="kefu-entry" wx:if="{{activeTab === 2 && filteredConversations.length === 0}}">
<button class="kefu-btn" bindtap="chatKefu">联系客服</button>
</view>
<view class="load-tip" wx:if="{{filteredConversations.length > displayCount}}">上拉加载更多</view>
<view class="empty" wx:if="{{filteredConversations.length === 0 && activeTab !== 2}}">
<text>暂无相关消息</text>
</view>
</scroll-view>
<!-- 操作菜单 -->
<view class="action-mask" wx:if="{{actionPopup.visible}}" catchtap="closeMask">
<view class="action-sheet">
<view class="action-btn" bindtap="topConversation">{{actionPopup.conversation.top ? '取消置顶' : '置顶聊天'}}</view>
<view class="action-btn" bindtap="removeConversation">删除聊天</view>
<view class="action-btn cancel" catchtap="closeMask">取消</view>
</view>
</view>
<!-- 底部自定义TabBar -->
<custom-tab-bar />
</view>

View File

@@ -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; }

View File

@@ -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 };

View File

@@ -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) {}
}

View File

@@ -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) {}
}

View File

@@ -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 };

View File

@@ -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();