优化聊天:配对群组、稳定连接、消息列表与订单详情跳转
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -131,7 +131,9 @@ function ensureConnection(app) {
|
||||
function getSavedConnection(app) {
|
||||
try {
|
||||
const saved = wx.getStorageSync(app.globalData.goEasyConnection.cacheKeys.savedConnection);
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
if (!saved) return null;
|
||||
if (typeof saved === 'string') return JSON.parse(saved);
|
||||
return saved;
|
||||
} catch (error) {
|
||||
console.error('获取保存的连接信息失败:', error);
|
||||
return null;
|
||||
@@ -583,8 +585,9 @@ function updateCurrentPageState(app) {
|
||||
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 = currentPage.route === 'pages/liaotian/liaotian';
|
||||
app.globalData.pageState.isInChatPage = chatPages.indexOf(currentPage.route) >= 0;
|
||||
app.globalData.pageState.lastPageUpdate = Date.now();
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
112
utils/group-chat.js
Normal file
112
utils/group-chat.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 打手-商家/老板配对群:group_Ds{打手}_Sj{商家} 或 group_Ds{打手}_Boss{老板}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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 = {};
|
||||
|
||||
const counterpartId = getCounterpartGoEasyId(item.groupId, myGoEasyId);
|
||||
if (counterpartId) {
|
||||
item.data.counterpartId = counterpartId;
|
||||
if (!item.data.name || item.data.name === item.groupId) {
|
||||
item.data.name = `用户${counterpartId.replace(/^(Ds|Sj|Boss)/, '').slice(-6)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.lastMessage && item.lastMessage.senderId && item.lastMessage.senderId !== myGoEasyId) {
|
||||
const sd = item.lastMessage.senderData || {};
|
||||
if (sd.avatar) item.data.avatar = sd.avatar;
|
||||
if (sd.name) item.data.name = sd.name;
|
||||
}
|
||||
|
||||
let avatar = item.data.avatar || '';
|
||||
if (avatar && !avatar.startsWith('http')) {
|
||||
avatar = (ossBase || '') + avatar.replace(/^\//, '');
|
||||
}
|
||||
item.data.avatar = avatar || defaultAvatar;
|
||||
|
||||
if (item.data.orderZhuangtai != null) {
|
||||
item.data.orderZhuangtaiText = ZHUANGTAI_MAP[item.data.orderZhuangtai] || '';
|
||||
}
|
||||
|
||||
item.displayLastMsg = formatLastMessagePreview(item.lastMessage);
|
||||
return item;
|
||||
}
|
||||
|
||||
export function isOrderGroupConversation(c) {
|
||||
if (!c || c.type !== 'group') return false;
|
||||
if (c.data && c.data.orderId) return true;
|
||||
return isPairGroupId(c.groupId) || /^group_\w+$/.test(c.groupId || '');
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
/**
|
||||
* 连接管理模块 - xiaoxilj.js (根治发送失败:跳转前确保群组已订阅)
|
||||
* 从详情页跳转订单群聊时,提前订阅群组,避免发送消息时订阅未完成而失败
|
||||
* 连接管理模块 - 订单群聊跳转(后端准备群 + 稳定连接)
|
||||
*/
|
||||
import request from './request';
|
||||
const app = getApp();
|
||||
|
||||
function waitForConnection(expectedUserId, timeout = 10000) {
|
||||
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('连接超时'));
|
||||
@@ -13,87 +20,76 @@ function waitForConnection(expectedUserId, timeout = 10000) {
|
||||
|
||||
const handler = (event) => {
|
||||
if (event.status === 'connected') {
|
||||
if (event.userId === expectedUserId) {
|
||||
const uid = event.userId || wx.goEasy?.im?.userId;
|
||||
if (uid === expectedUserId) {
|
||||
clearTimeout(timer);
|
||||
app.off('connectionChanged', handler);
|
||||
resolve();
|
||||
} else {
|
||||
console.error(`连接身份不匹配: 期望 ${expectedUserId}, 实际 ${event.userId}`);
|
||||
wx.removeStorageSync('savedGoEasyConnection');
|
||||
wx.removeStorageSync('goEasyUserId');
|
||||
wx.removeStorageSync('currentGoEasyIdentity');
|
||||
if (app.clearSavedConnection) app.clearSavedConnection();
|
||||
clearTimeout(timer);
|
||||
app.off('connectionChanged', handler);
|
||||
reject(new Error(`连接身份不匹配: 期望 ${expectedUserId}, 实际 ${event.userId}`));
|
||||
}
|
||||
} else if (event.status === 'disconnected' && !event.manual) {
|
||||
wx.removeStorageSync('savedGoEasyConnection');
|
||||
wx.removeStorageSync('goEasyUserId');
|
||||
wx.removeStorageSync('currentGoEasyIdentity');
|
||||
if (app.clearSavedConnection) app.clearSavedConnection();
|
||||
} else if (event.status === 'disconnected' && event.manual) {
|
||||
clearTimeout(timer);
|
||||
app.off('connectionChanged', handler);
|
||||
reject(new Error('连接失败'));
|
||||
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('参数不完整');
|
||||
}
|
||||
|
||||
if (identityType === 'dashou' && !userId.startsWith('Ds')) {
|
||||
throw new Error(`打手身份的用户ID必须以Ds开头,实际为 ${userId}`);
|
||||
}
|
||||
if (identityType === 'shangjia' && !userId.startsWith('Sj')) {
|
||||
throw new Error(`商家身份的用户ID必须以Sj开头,实际为 ${userId}`);
|
||||
}
|
||||
if (identityType === 'boss' && !userId.startsWith('Boss')) {
|
||||
throw new Error(`老板身份的用户ID必须以Boss开头,实际为 ${userId}`);
|
||||
}
|
||||
await ensureIdentityConnection(identityType, userId);
|
||||
|
||||
wx.removeStorageSync('savedGoEasyConnection');
|
||||
wx.removeStorageSync('goEasyUserId');
|
||||
wx.removeStorageSync('currentGoEasyIdentity');
|
||||
if (app.clearSavedConnection) app.clearSavedConnection();
|
||||
const currentUser = {
|
||||
id: userId,
|
||||
name: app.globalData.currentUser?.name || '用户',
|
||||
avatar: app.globalData.currentUser?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
|
||||
};
|
||||
|
||||
if (wx.goEasy && wx.goEasy.getConnectionStatus() === 'connected') {
|
||||
wx.goEasy.disconnect();
|
||||
}
|
||||
const to = {
|
||||
id: targetUser.id,
|
||||
name: targetUser.name || '用户',
|
||||
avatar: targetUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
|
||||
};
|
||||
|
||||
const waitPromise = waitForConnection(userId);
|
||||
app.connectWithIdentity(identityType, userId, false);
|
||||
|
||||
try {
|
||||
await waitPromise;
|
||||
|
||||
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 });
|
||||
} catch (err) {
|
||||
console.error('连接失败:', err);
|
||||
wx.showToast({ title: err.message || '连接失败', icon: 'none' });
|
||||
throw err;
|
||||
}
|
||||
const param = { to, currentUser };
|
||||
const path = '/pages/liaotian/liaotian?data=' + encodeURIComponent(JSON.stringify(param));
|
||||
wx.navigateTo({ url: path });
|
||||
}
|
||||
|
||||
async connectToGroupChat(params) {
|
||||
@@ -102,113 +98,70 @@ class ConnectionManager {
|
||||
throw new Error('参数不完整:identityType, userId, orderId 必填');
|
||||
}
|
||||
|
||||
// 校验 userId 前缀
|
||||
if (identityType === 'dashou' && !userId.startsWith('Ds')) {
|
||||
throw new Error(`打手身份的用户ID必须以Ds开头,实际为 ${userId}`);
|
||||
}
|
||||
if (identityType === 'shangjia' && !userId.startsWith('Sj')) {
|
||||
throw new Error(`商家身份的用户ID必须以Sj开头,实际为 ${userId}`);
|
||||
}
|
||||
if (identityType === 'boss' && !userId.startsWith('Boss')) {
|
||||
throw new Error(`老板身份的用户ID必须以Boss开头,实际为 ${userId}`);
|
||||
}
|
||||
wx.showLoading({ title: '建立联系中...', mask: true });
|
||||
|
||||
// 1. 同步全局角色,确保群聊页初始化正确
|
||||
app.globalData.currentRole = identityType;
|
||||
wx.setStorageSync('currentRole', identityType);
|
||||
|
||||
const uid = userId.replace(/^(Ds|Sj|Boss|Gs|Zz)/, '');
|
||||
const avatar = wx.getStorageSync('touxiang') ||
|
||||
(app.globalData.ossImageUrl + app.globalData.morentouxiang);
|
||||
app.globalData.currentUser = {
|
||||
id: userId,
|
||||
name: app.globalData.currentUser?.name || `用户${uid}`,
|
||||
avatar: avatar
|
||||
};
|
||||
|
||||
// 2. 确保 GoEasy 已使用目标身份连接
|
||||
let needConnect = false;
|
||||
if (!wx.goEasy || !wx.goEasy.getConnectionStatus || wx.goEasy.getConnectionStatus() !== 'connected') {
|
||||
needConnect = true;
|
||||
} else {
|
||||
const currentUserId = wx.goEasy.im?.userId;
|
||||
if (currentUserId !== userId) {
|
||||
needConnect = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needConnect) {
|
||||
wx.removeStorageSync('savedGoEasyConnection');
|
||||
wx.removeStorageSync('goEasyUserId');
|
||||
if (app.clearSavedConnection) app.clearSavedConnection();
|
||||
if (wx.goEasy && wx.goEasy.getConnectionStatus() === 'connected') {
|
||||
wx.goEasy.disconnect();
|
||||
}
|
||||
const waitPromise = waitForConnection(userId);
|
||||
app.connectWithIdentity(identityType, userId, false);
|
||||
try {
|
||||
await waitPromise;
|
||||
} catch (err) {
|
||||
wx.showToast({ title: '连接失败', icon: 'none' });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 从会话列表查找真实群组ID
|
||||
let realGroupId = null;
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
wx.goEasy.im.latestConversations({
|
||||
onSuccess: (res) => {
|
||||
const conversations = res.content?.conversations || [];
|
||||
const found = conversations.find(c => c.type === 'group' && c.data && c.data.orderId === orderId);
|
||||
resolve(found ? found.groupId : null);
|
||||
},
|
||||
onFailed: reject
|
||||
});
|
||||
await ensureIdentityConnection(identityType, userId);
|
||||
|
||||
const res = await request({
|
||||
url: '/dingdan/ltdhzb',
|
||||
method: 'POST',
|
||||
data: {
|
||||
dingdan_id: orderId,
|
||||
identityType,
|
||||
push_order_card: true,
|
||||
},
|
||||
});
|
||||
realGroupId = result;
|
||||
} catch (err) {
|
||||
console.error('获取会话列表失败:', err);
|
||||
wx.showToast({ title: '获取群聊信息失败', icon: 'none' });
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!realGroupId) {
|
||||
wx.showToast({ title: '暂未创建群聊', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
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 = {};
|
||||
app.globalData.groupInfoMap[realGroupId] = {
|
||||
name: chatData.counterpartName || chatData.groupName || groupName,
|
||||
avatar,
|
||||
orderId: chatData.orderId || orderId,
|
||||
orderDesc: chatData.orderDesc || '',
|
||||
orderZhuangtai: chatData.orderZhuangtai,
|
||||
};
|
||||
|
||||
// 4. 🔥 关键:主动订阅群组并等待成功,确保发送消息时订阅已完成
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
wx.goEasy.im.subscribeGroup({
|
||||
groupIds: [realGroupId],
|
||||
onSuccess: () => {
|
||||
resolve();
|
||||
},
|
||||
onFailed: (error) => {
|
||||
console.error('订阅群组失败:', error);
|
||||
reject(error);
|
||||
}
|
||||
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.showToast({ title: '订阅群组失败', icon: 'none' });
|
||||
wx.hideLoading();
|
||||
console.error('跳转群聊失败:', err);
|
||||
wx.showToast({ title: err.message || '进入聊天失败', icon: 'none' });
|
||||
throw err;
|
||||
}
|
||||
|
||||
// 5. 跳转群聊页(参数与消息列表完全一致)
|
||||
const param = {
|
||||
groupId: realGroupId,
|
||||
orderId: orderId,
|
||||
groupName: groupName || '订单群聊',
|
||||
groupAvatar: groupAvatar || '',
|
||||
isCross: isCross || 0
|
||||
};
|
||||
const path = '/pages/qunliaotian/qunliaotian?data=' + encodeURIComponent(JSON.stringify(param));
|
||||
wx.navigateTo({ url: path });
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConnectionManager();
|
||||
export default new ConnectionManager();
|
||||
|
||||
Reference in New Issue
Block a user