chore: 抢单端UI回退前备份当前完整工作区

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQueandCursor committed 2026-06-29 17:59:23 +08:00
1 parent 6e2f7bc39f
commit e8fb32c1fe
53 files changed
+3410 -1065

No files matched your search

+66 -7
View File
@@ -1,4 +1,4 @@
/** 统一头像 URL:无有效头像时始终返回 ossImageUrl + morentouxiang */
/** 统一头像 URL:无有效头像时始终返回 ossImageUrl + morentouxiang(后端配置) */
function isBlankPath(p) {
if (p == null) return true;
@@ -7,28 +7,71 @@ function isBlankPath(p) {
return !s || s === 'null' || s === 'undefined' || s === 'none';
}
function readConfigFallback(app) {
if (!app || typeof app.readConfigFromStorage !== 'function') return null;
try {
return app.readConfigFromStorage();
} catch (e) {
return null;
}
}
function getOssBase(app) {
let oss = app.globalData?.ossImageUrl || '';
if (!oss) {
const cfg = readConfigFallback(app);
oss = cfg?.cos?.ossImageUrl || '';
}
return oss;
}
function getDefaultRelative(app) {
let def = app.globalData?.morentouxiang || '';
if (!def) {
const cfg = readConfigFallback(app);
def = cfg?.otherConfig?.morentouxiang || '';
}
return def || 'avatar/default.jpg';
}
export function getDefaultAvatarUrl(app) {
return resolveAvatarUrl('', app);
}
export function resolveAvatarUrl(rawPath, app) {
app = app || getApp();
const oss = app.globalData.ossImageUrl || '';
const def = app.globalData.morentouxiang || 'avatar/default.jpg';
const oss = getOssBase(app);
const def = getDefaultRelative(app);
if (!isBlankPath(rawPath)) {
if (rawPath.startsWith('http://') || rawPath.startsWith('https://')) {
return rawPath;
}
const path = rawPath.startsWith('/') ? rawPath : '/' + rawPath;
return oss + path;
const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
if (oss) return oss + path;
return path;
}
if (def.startsWith('http://') || def.startsWith('https://')) {
return def;
}
const defPath = def.startsWith('/') ? def : '/' + def;
return oss + defPath;
const defPath = def.startsWith('/') ? def : `/${def}`;
if (oss) return oss + defPath;
// 配置未就绪时仍返回相对路径,由 binderror 回退;有 oss 时必须拼完整 URL
return defPath;
}
/** 是否已是可加载的完整头像 URL */
export function isUsableAvatarUrl(url) {
return url && (url.startsWith('http://') || url.startsWith('https://'));
}
export function ensureAvatarUrl(rawPath, app) {
app = app || getApp();
const resolved = resolveAvatarUrl(rawPath, app);
if (isUsableAvatarUrl(resolved)) return resolved;
const def = getDefaultAvatarUrl(app);
return isUsableAvatarUrl(def) ? def : resolved;
}
export function resolveAvatarFromStorage(app) {
@@ -36,3 +79,19 @@ export function resolveAvatarFromStorage(app) {
const tx = wx.getStorageSync('touxiang') || '';
return resolveAvatarUrl(tx, app);
}
/** 配置加载后刷新页面头像(聊天页等订阅 configApplied) */
export function subscribeConfigAvatarRefresh(page, refreshFn) {
const app = getApp();
if (!app || typeof refreshFn !== 'function') return null;
const handler = () => refreshFn();
if (app.on) app.on('configApplied', handler);
return () => { if (app.off) app.off('configApplied', handler); };
}
/** 消息列表/聊天页 image 加载失败时回退默认头像 */
export function onAvatarImageError(page, dataKey, app) {
if (!page || !dataKey) return;
const def = getDefaultAvatarUrl(app || getApp());
page.setData({ [dataKey]: def });
}
+4
View File
@@ -272,6 +272,10 @@ const BASE_METHODS = {
const result = {};
Object.keys(urlMap).forEach((key) => {
const path = urlMap[key];
if (!path || typeof path !== 'string') {
result[key] = '';
return;
}
result[key] = path.startsWith('http') || path.startsWith('/') ? path : ossImageUrl + path;
});
return result;
+74 -3
View File
@@ -42,6 +42,8 @@ function initGlobalMessageSystem(app) {
app.handleNewMessage = (message) => handleNewMessage(app, message);
app.ensureImForRole = (role) => ensureImForRole(app, role);
app.syncConnectionStatus = () => syncConnectionStatus(app);
app.restoreTabBarBadge = () => restoreCachedBadge(app);
app.startImWhenReady = () => startImWhenReady(app);
loadUserSettings(app);
initNetworkListener(app);
@@ -132,6 +134,7 @@ function initAppStateListener(app) {
wx.onAppShow(() => {
updateCurrentPageState(app);
app.globalData.goEasyConnection.autoReconnect = true;
restoreCachedBadge(app);
ensureConnection(app);
setTimeout(() => loadConversations(app), 400);
});
@@ -205,6 +208,9 @@ function syncConnectionStatus(app) {
app.globalData.goEasyConnection.status = 'connected';
app.globalData.goEasyConnection.userId = cur || expectedId;
app.globalData.goEasyConnection.autoReconnect = true;
setupMessageListeners(app);
loadConversations(app);
restoreCachedBadge(app);
app.emitEvent('connectionChanged', { status: 'connected' });
return;
}
@@ -607,6 +613,7 @@ function handleNewMessage(app, message) {
}
cacheMessage(app, message);
loadConversations(app);
if (shouldShowNotification(app, message)) {
let notificationType = 'private';
@@ -687,8 +694,57 @@ function showNotificationDirect(app, data) {
app.emitEvent('showNotification', data);
}
function startImWhenReady(app) {
if (!wx.getStorageSync('uid')) return;
app.globalData.goEasyConnection.autoReconnect = true;
restoreCachedBadge(app);
ensureConnection(app);
setTimeout(() => {
if (isSdkConnected()) {
setupMessageListeners(app);
loadConversations(app);
}
restoreCachedBadge(app);
}, 450);
}
function restoreCachedBadge(app) {
const { messageManager } = app.globalData;
let unread = messageManager.unreadTotal;
if (unread == null || unread === '') {
try {
const saved = wx.getStorageSync(messageManager.cacheKeys.unreadTotal);
if (saved !== '' && saved != null) unread = Number(saved) || 0;
} catch (e) {}
}
if (!unread) {
try {
const badgeStr = wx.getStorageSync('tabBarMessageBadge');
if (badgeStr && badgeStr !== '0') {
unread = badgeStr === '99+' ? 100 : (parseInt(badgeStr, 10) || 0);
}
} catch (e) {}
}
if (unread > 0) {
updateTabBarBadge(app, unread);
} else if (messageManager.tabBarBadgeText) {
app.emitEvent('tabBarBadgeChanged', {
index: messageManager.tabBarIndex,
badgeText: messageManager.tabBarBadgeText,
showRedDot: true,
forceUpdate: true,
});
updateTabBarDirectly(app, messageManager.tabBarBadgeText);
}
}
function applyUnreadTotal(app, unreadTotal) {
const prev = app.globalData.messageManager.unreadTotal;
const connected = isSdkConnected();
if (unreadTotal === 0 && !connected && prev > 0) {
restoreCachedBadge(app);
return;
}
app.globalData.messageManager.unreadTotal = unreadTotal;
updateTabBarBadge(app, unreadTotal);
wx.setStorageSync(app.globalData.messageManager.cacheKeys.unreadTotal, unreadTotal);
@@ -789,7 +845,13 @@ function loadConversations(app) {
function loadConversationsNow(app) {
if (_loadConvInFlight) return;
if (!isSdkConnected() || !wx.goEasy?.im?.latestConversations) return;
if (!isSdkConnected() || !wx.goEasy?.im?.latestConversations) {
restoreCachedBadge(app);
if (app.globalData.goEasyConnection.autoReconnect) {
ensureConnection(app);
}
return;
}
_loadConvInFlight = true;
wx.goEasy.im.latestConversations({
onSuccess: (result) => {
@@ -815,6 +877,7 @@ function loadConversationsNow(app) {
onFailed: (error) => {
_loadConvInFlight = false;
console.error('加载会话列表失败:', error);
restoreCachedBadge(app);
if (!isSdkConnected() && app.globalData.goEasyConnection.autoReconnect) {
attemptReconnect(app);
}
@@ -842,7 +905,14 @@ 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'];
const chatPages = [
'pages/chat/chat',
'pages/group-chat/group-chat',
'pages/cs-chat/cs-chat',
'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();
@@ -907,7 +977,8 @@ function updateTabBarDirectly(app, badgeText) {
try {
const pages = getCurrentPages();
for (let i = pages.length - 1; i >= 0; i--) {
const tabBar = pages[i].selectComponent('#custom-tab-bar');
const tabBar = pages[i].selectComponent('#custom-tab-bar')
|| pages[i].selectComponent('tab-bar');
if (tabBar && tabBar.setData) {
tabBar.setData({ badgeText });
break;
+106
View File
@@ -0,0 +1,106 @@
/**
* 聊天历史消息加载(私聊 / 群聊 / 客服共用)
* - 排除本地/欢迎语等合成消息的 timestamp,避免下拉加载错位
* - 统一 IM 连接等待与 history API 封装
*/
const SYNTHETIC_MSG_ID = /^(local-|img-|order-|welcome-)/;
export function isHistorySyntheticMessage(msg) {
return !!(msg && msg.messageId && SYNTHETIC_MSG_ID.test(String(msg.messageId)));
}
/** 取当前列表里最早一条「真实」服务端消息的时间戳,供 GoEasy history 分页 */
export function getOldestHistoryTimestamp(messages, fallback = null) {
if (!Array.isArray(messages)) return fallback;
let oldest = null;
for (const m of messages) {
if (!m || m.timestamp == null) continue;
if (isHistorySyntheticMessage(m)) continue;
if (oldest == null || m.timestamp < oldest) oldest = m.timestamp;
}
return oldest != null ? oldest : fallback;
}
export function isImConnected() {
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
return (status === 'connected' || status === 'reconnected') && !!wx.goEasy?.im;
}
/** 等待 IM 连接就绪(下拉加载历史前必须先连上) */
export function waitImConnected(app, timeoutMs = 8000) {
return new Promise((resolve) => {
if (isImConnected()) {
resolve(true);
return;
}
if (app?.ensureConnection) app.ensureConnection();
const start = Date.now();
const timer = setInterval(() => {
if (isImConnected()) {
clearInterval(timer);
resolve(true);
} else if (Date.now() - start >= timeoutMs) {
clearInterval(timer);
resolve(false);
}
}, 200);
});
}
export function fetchHistoryMessages({ scene, id, lastTimestamp, limit = 20 }) {
return new Promise((resolve, reject) => {
if (!wx.goEasy?.im?.history) {
reject(new Error('IM未初始化'));
return;
}
if (!id) {
reject(new Error('会话ID为空'));
return;
}
wx.goEasy.im.history({
type: scene,
id: String(id),
lastTimestamp: lastTimestamp ?? null,
limit,
onSuccess: (res) => resolve(res?.content || []),
onFailed: (err) => reject(err || new Error('加载历史消息失败')),
});
});
}
/**
* 合并一页历史消息
* @returns {{ messages, hasMore, lastTimestamp }}
*/
export function mergeHistoryMessages({ incoming, existing, refresh, limit = 20, mapMessage }) {
let list = Array.isArray(incoming) ? [...incoming] : [];
list.sort((a, b) => a.timestamp - b.timestamp);
if (typeof mapMessage === 'function') {
list = list.map((m, idx) => mapMessage(m, idx, list));
}
const prev = Array.isArray(existing) ? existing : [];
let final = refresh ? list : [...list, ...prev];
if (!refresh) {
for (let i = 0; i < final.length; i += 1) {
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 (!m?.messageId || ids.has(m.messageId)) continue;
ids.add(m.messageId);
unique.push(m);
}
return {
messages: unique,
hasMore: list.length >= limit,
lastTimestamp: getOldestHistoryTimestamp(unique, null),
};
}
+35 -3
View File
@@ -4,8 +4,9 @@
export const ORDER_CARD_PREFIX = '[ORDER_CARD]';
const ZHUANGTAI_MAP = {
1: '已下单', 2: '进行中', 3: '已完成', 4: '退款中',
1: '待抢单', 2: '进行中', 3: '已完成', 4: '退款中',
5: '已退款', 6: '退款失败', 7: '指定中', 8: '结算中',
9: '未支付', 14: '支付失败', 25: '已废弃',
};
export function getZhuangtaiText(zhuangtai) {
@@ -38,6 +39,12 @@ export function resolveLocalGroupId(identityType, myUid, partnerUid, fadanPingta
}
return buildDashouBossGroupId(myUid, partnerUid);
}
if (identityType === 'boss' || identityType === 'normal') {
if (partnerUid && myUid) {
return buildDashouBossGroupId(partnerUid, myUid);
}
return orderId ? `group_${orderId}` : null;
}
return orderId ? `group_${orderId}` : null;
}
@@ -105,6 +112,32 @@ export function applyPairMetaToConversation(item, myGoEasyId, defaultAvatar, oss
return;
}
const parsed = parsePairGroupId(item.groupId);
if (parsed && myGoEasyId) {
const isDashou = myGoEasyId === parsed.dashouId;
const isPartner = myGoEasyId === parsed.partnerId;
if (isDashou) {
d.counterpartId = parsed.partnerId;
d.counterpartYonghuid = parsed.partnerId.replace(/^(Sj|Boss)/, '');
d.partnerRoleLabel = parsed.partnerRole === 'boss' ? '老板' : '商家';
if (!d.name || d.name === item.groupId) {
d.name = d.partnerNicheng || `${d.partnerRoleLabel}${d.counterpartYonghuid.slice(-6)}`;
}
if (!d.avatar || d.avatar === defaultAvatar) {
d.avatar = d.partnerAvatar || defaultAvatar;
}
} else if (isPartner) {
d.counterpartId = parsed.dashouId;
d.counterpartYonghuid = parsed.dashouId.replace(/^Ds/, '');
if (!d.name || d.name === item.groupId) {
d.name = d.dashouNicheng || `打手${d.counterpartYonghuid.slice(-6)}`;
}
if (!d.avatar || d.avatar === defaultAvatar) {
d.avatar = d.dashouAvatar || defaultAvatar;
}
}
}
const counterpartId = getCounterpartGoEasyId(item.groupId, myGoEasyId);
if (counterpartId) {
d.counterpartId = counterpartId;
@@ -215,6 +248,7 @@ 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;
// QQ/微信:取「最后消息时间」与「最近进入会话时间」较大值;新消息到达会更新 msgTs 并置顶
return Math.max(msgTs, openTs);
}
@@ -222,8 +256,6 @@ 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;
+3 -2
View File
@@ -24,9 +24,10 @@ export const IM_ROLE_PREFIX = {
kaoheguan: 'Ds',
};
import { getDefaultAvatarUrl } from './avatar.js';
export function getDefaultAvatar(app) {
const oss = app.globalData.ossImageUrl || '';
return oss + (app.globalData.morentouxiang || '');
return getDefaultAvatarUrl(app || getApp());
}
export function getImPrefixForRole(role) {
+16 -13
View File
@@ -4,9 +4,9 @@ const imRequest = require('./imRequest.js');
/**
* 打手 IM 权限检测
* POST /yonghu/dshqltqx
* 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 }
*/
async function dashoujianquan(app) {
async function dashoujianquan(app, options = {}) {
const silent = options && options.silent === true;
const uid = wx.getStorageSync('uid');
if (!uid) return { allowed: false, reason: '未登录' };
@@ -21,7 +21,8 @@ async function dashoujianquan(app) {
const allow = res.data.allow;
if (allow === 1) {
return { allowed: true };
} else {
}
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '当前身份已不支持消息聊天功能,请充值会员或联系管理员后尝试',
@@ -30,22 +31,24 @@ async function dashoujianquan(app) {
mask: true
});
await new Promise(r => setTimeout(r, 2100));
return { allowed: false, reason: '接单员消息权限被限制' };
}
return { allowed: false, reason: '接单员消息权限被限制' };
}
throw new Error(res.data?.msg || '鉴权接口异常');
} catch (err) {
console.error('打手鉴权失败:', err);
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
}
return { allowed: false, reason: '鉴权请求失败' };
}
}
module.exports = { dashoujianquan };
module.exports = { dashoujianquan };
+17 -18
View File
@@ -14,21 +14,21 @@ const { dashoujianquan } = require('./dashoujianquan.js');
* @param {Object} app 全局 App 实例
* @returns {Promise<{ allowed: boolean, reason?: string }>}
*/
async function jianquanxian(app) {
async function jianquanxian(app, options = {}) {
const silent = options && options.silent === true;
// 1. 检测登录态(用 uid 或 token)
const token = wx.getStorageSync('token') || wx.getStorageSync('uid');
if (!token) {
// 没登录:先强制断开(如果已连接)
await app.disconnectGoEasy();
// 弹窗提示,强制阅读 2 秒
wx.showToast({
title: '您当前处于未登录状态,消息功能暂不可用',
icon: 'none',
duration: 2000,
mask: true
});
// 等待提示结束,确保用户看到
await new Promise(resolve => setTimeout(resolve, 2100));
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '您当前处于未登录状态,消息功能暂不可用',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(resolve => setTimeout(resolve, 2100));
}
return { allowed: false, reason: '未登录' };
}
@@ -37,17 +37,16 @@ async function jianquanxian(app) {
// 3. 根据角色分发到具体鉴权模块
switch (role) {
case 'normal': // 点单老板
return laobanjianquan(app);
case 'normal':
return laobanjianquan(app, { silent });
case 'dashou': // 打手
return dashoujianquan(app);
case 'dashou':
return dashoujianquan(app, { silent });
// 其他角色暂未细分,统一放行(后续可新增对应文件)
case 'shangjia':
case 'guanshi':
case 'zuzhang':
case 'kaoheguan':
case 'kaoheguan':
return { allowed: true };
default:
+16 -13
View File
@@ -4,9 +4,9 @@ const imRequest = require('./imRequest.js');
/**
* 老板(点单用户)IM 权限检测
* POST /yonghu/lbhqltqx
* 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 }
*/
async function laobanjianquan(app) {
async function laobanjianquan(app, options = {}) {
const silent = options && options.silent === true;
const uid = wx.getStorageSync('uid');
if (!uid) return { allowed: false, reason: '未登录' };
@@ -21,7 +21,8 @@ async function laobanjianquan(app) {
const allow = res.data.allow;
if (allow === 1) {
return { allowed: true };
} else {
}
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '当前身份已不支持使用消息功能',
@@ -30,22 +31,24 @@ async function laobanjianquan(app) {
mask: true
});
await new Promise(r => setTimeout(r, 1100));
return { allowed: false, reason: '当前身份无消息权限' };
}
return { allowed: false, reason: '当前身份无消息权限' };
}
throw new Error(res.data?.msg || '鉴权接口异常');
} catch (err) {
console.error('老板鉴权失败:', err);
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
}
return { allowed: false, reason: '鉴权请求失败' };
}
}
module.exports = { laobanjianquan };
module.exports = { laobanjianquan };
+12
View File
@@ -0,0 +1,12 @@
/** 与消息页「联系客服」一致:跳转客服会话 */
export function openCustomerServiceChat() {
const app = getApp();
if (app.globalData.chatEnabled && app.ensureConnection) {
app.globalData.goEasyConnection.autoReconnect = true;
app.ensureConnection();
}
const param = { teamId: 'support_team' };
wx.navigateTo({
url: '/pages/cs-chat/cs-chat?data=' + encodeURIComponent(JSON.stringify(param)),
});
}
+2 -1
View File
@@ -249,8 +249,9 @@ export function reconnectForRole(role) {
const connected = status === 'connected' || status === 'reconnected';
if (connected && currentUserId === targetUserId) {
if (app.loadConversations) app.loadConversations();
if (app.restoreTabBarBadge) app.restoreTabBarBadge();
if (app.ensureConnection) app.ensureConnection();
if (app.loadConversations) app.loadConversations();
return;
}
+2 -1
View File
@@ -224,8 +224,9 @@ export function syncStaffUi(page) {
const ctx = staff ? (getStaffContext() || {}) : null;
const wallet = (ctx && ctx.wallet) || {};
const portal = isMerchantPortalUser();
const staffFlag = Number(wx.getStorageSync('staffstatus')) === 1;
page.setData({
isShangjia: portal,
isShangjia: portal || staffFlag || !!(page.data && page.data.isShangjia),
isStaffMode: staff,
isMerchantOwner: !staff && Number(wx.getStorageSync('shangjiastatus')) === 1,
staffRoleName: staff ? (ctx.role_name || '客服') : '',
+296 -269
View File
@@ -1,269 +1,296 @@
/**
* 订单群聊跳转:先确定群 ID,IM 连接失败也尽量能进聊天页
*/
import request from './request';
import { persistGroupMeta } from './im-user.js';
import { resolveLocalGroupId } from './group-chat.js';
const app = getApp();
function formatError(err) {
if (!err) return '进入聊天失败';
if (typeof err === 'string') return err;
if (err.message) return err.message;
if (err.content) return String(err.content);
if (err.msg) return String(err.msg);
if (err.code) return `连接失败(${err.code})`;
try {
return JSON.stringify(err);
} catch (e) {
return '进入聊天失败';
}
}
function withTimeout(promise, ms, errMsg) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(errMsg || '操作超时')), ms);
promise
.then((v) => { clearTimeout(timer); resolve(v); })
.catch((e) => { clearTimeout(timer); reject(e); });
});
}
function getConnectedUserId() {
const fromState = app.globalData?.goEasyConnection?.userId;
const fromIm = wx.goEasy?.im?.userId;
return fromState || fromIm || '';
}
function isImConnected() {
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
return status === 'connected' || status === 'reconnected';
}
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,
};
if (!app.globalData.chatEnabled || !wx.goEasy?.im) {
throw new Error('聊天服务未就绪,请稍后重试');
}
if (isImConnected() && getConnectedUserId() === userId) {
return;
}
const connectedId = getConnectedUserId();
if (connectedId && connectedId !== userId && app.disconnectGoEasy) {
await app.disconnectGoEasy();
}
if (!app.connectWithIdentity) {
throw new Error('聊天功能未初始化');
}
await withTimeout(
app.connectWithIdentity(identityType, userId, true),
12000,
'IM连接超时'
);
if (!isImConnected()) {
throw new Error('IM连接失败,请检查网络后重试');
}
}
async function prepareGroupChat(identityType, userId, orderId, partnerUid, fadanPingtai, groupName, isCross) {
let chatData = null;
try {
const res = await withTimeout(
request({
url: '/dingdan/ltdhzb',
method: 'POST',
data: {
dingdan_id: orderId,
identityType,
push_order_card: false,
},
}),
12000,
'准备群聊超时'
);
const body = res?.data || {};
if (body.code === 0 && body.data?.groupId) {
chatData = body.data;
} else if (body.msg) {
console.warn('ltdhzb:', body.msg);
}
} catch (e) {
console.warn('ltdhzb 请求失败,尝试本地 groupId', e);
}
if (!chatData?.groupId) {
const myUid = userId.replace(/^(Ds|Sj|Boss)/, '');
let localGroupId = resolveLocalGroupId(
identityType, myUid, partnerUid, fadanPingtai, orderId, isCross
);
if (!localGroupId && orderId) {
localGroupId = `group_${orderId}`;
}
if (!localGroupId) {
throw new Error('无法确定群聊,请确认订单已接单且对方信息完整');
}
chatData = {
groupId: localGroupId,
orderId,
groupName: groupName || '订单群聊',
isCross: isCross || fadanPingtai || 0,
};
try {
await request({
url: '/dingdan/ltdhzb',
method: 'POST',
data: { dingdan_id: orderId, identityType, push_order_card: false },
});
} catch (e) { /* ignore */ }
}
return chatData;
}
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 };
wx.navigateTo({ url: '/pages/chat/chat?data=' + encodeURIComponent(JSON.stringify(param)) });
}
async connectToGroupChat(params) {
const {
identityType, userId, orderId, groupName, groupAvatar, isCross,
partnerUid, fadanPingtai,
} = params;
if (!identityType || !userId || !orderId) {
throw new Error('参数不完整');
}
app.globalData.currentRole = identityType;
wx.setStorageSync('currentRole', identityType);
wx.showLoading({ title: '建立联系中...', mask: true });
try {
const chatData = await prepareGroupChat(
identityType, userId, orderId, partnerUid, fadanPingtai || isCross, groupName, isCross
);
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);
let imReady = false;
try {
await ensureIdentityConnection(identityType, userId);
imReady = true;
try {
await withTimeout(
new Promise((resolve, reject) => {
wx.goEasy.im.subscribeGroup({
groupIds: [realGroupId],
onSuccess: () => resolve(),
onFailed: (error) => reject(error),
});
}),
8000,
'订阅群聊超时'
);
} catch (subErr) {
console.warn('订阅群聊失败,仍尝试进入页面', subErr);
}
} catch (imErr) {
console.warn('IM连接失败,仍进入聊天页由页面重连', imErr);
}
const uid = userId.replace(/^(Ds|Sj|Boss)/, '');
const param = {
groupId: realGroupId,
orderId: chatData.orderId || orderId,
groupName: chatData.counterpartName || chatData.groupName || groupName || '订单群聊',
groupAvatar: avatar,
isCross: chatData.isCross != null ? chatData.isCross : (isCross || 0),
currentUserId: userId,
currentUserName: app.globalData.currentUser?.name || `用户${uid}`,
currentUserAvatar: app.globalData.currentUser?.avatar ||
(wx.getStorageSync('touxiang') || (app.globalData.ossImageUrl + app.globalData.morentouxiang)),
orderZhuangtai: chatData.orderZhuangtai,
orderJine: chatData.orderJine,
orderDesc: chatData.orderDesc || '',
imReady,
};
wx.hideLoading();
wx.navigateTo({
url: '/pages/group-chat/group-chat?data=' + encodeURIComponent(JSON.stringify(param)),
fail: (navErr) => {
wx.showToast({ title: formatError(navErr), icon: 'none', duration: 2500 });
},
});
} catch (err) {
wx.hideLoading();
console.error('跳转群聊失败:', err);
wx.showToast({ title: formatError(err), icon: 'none', duration: 2500 });
throw err;
}
}
}
export default new ConnectionManager();
/**
* 订单群聊跳转:与文赫一致,新群用打手+商家/老板配对 groupId;旧 group_{订单号} 会话保留
*/
import request from './request';
import { persistGroupMeta } from './im-user.js';
import { resolveLocalGroupId } from './group-chat.js';
const app = getApp();
function formatError(err) {
if (!err) return '进入聊天失败';
if (typeof err === 'string') return err;
if (err.message) return err.message;
if (err.msg) return String(err.msg);
if (err.content) return String(err.content);
if (err.code) return `连接失败(${err.code})`;
try {
return JSON.stringify(err);
} catch (e) {
return '进入聊天失败';
}
}
function withTimeout(promise, ms, errMsg) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(errMsg || '操作超时')), ms);
promise
.then((v) => { clearTimeout(timer); resolve(v); })
.catch((e) => { clearTimeout(timer); reject(e); });
});
}
function mapIdentityForApi(identityType) {
if (identityType === 'dashou') return 'dashou';
if (identityType === 'shangjia') return 'shangjia';
return 'normal';
}
function validateIdentity(identityType, userId) {
if (identityType === 'dashou' && !userId.startsWith('Ds')) {
throw new Error('接单员 ID 必须以 Ds 开头');
}
if (identityType === 'shangjia' && !userId.startsWith('Sj')) {
throw new Error('商家 ID 必须以 Sj 开头');
}
if ((identityType === 'boss' || identityType === 'normal') && !userId.startsWith('Boss')) {
throw new Error('老板 ID 必须以 Boss 开头');
}
}
function getConnectedUserId() {
const fromState = app.globalData?.goEasyConnection?.userId;
const fromIm = wx.goEasy?.im?.userId;
return fromState || fromIm || '';
}
function isImConnected() {
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
return status === 'connected' || status === 'reconnected';
}
async function ensureIdentityConnection(identityType, userId) {
validateIdentity(identityType, userId);
app.globalData.currentRole = identityType;
wx.setStorageSync('currentRole', identityType);
const uid = userId.replace(/^(Ds|Sj|Boss)/, '');
const avatar = wx.getStorageSync('touxiang') ||
(app.globalData.ossImageUrl + app.globalData.morentouxiang);
app.globalData.currentUser = {
id: userId,
name: app.globalData.currentUser?.name || `用户${uid}`,
avatar,
};
if (!app.globalData.chatEnabled || !wx.goEasy?.im) {
throw new Error('聊天服务未就绪,请稍后重试');
}
if (isImConnected() && getConnectedUserId() === userId) {
return;
}
const connectedId = getConnectedUserId();
if (connectedId && connectedId !== userId && app.disconnectGoEasy) {
await app.disconnectGoEasy();
}
if (!app.connectWithIdentity) {
throw new Error('聊天功能未初始化');
}
await withTimeout(
app.connectWithIdentity(identityType, userId, true),
12000,
'IM连接超时'
);
if (!isImConnected()) {
throw new Error('IM连接失败,请检查网络后重试');
}
}
async function prepareGroupChat(identityType, userId, orderId, partnerUid, fadanPingtai, groupName, isCross) {
const orderIdStr = String(orderId || '').replace(/^group_/, '');
let chatData = null;
try {
const res = await withTimeout(
request({
url: '/dingdan/ltdhzb',
method: 'POST',
data: {
dingdan_id: orderIdStr,
identityType: mapIdentityForApi(identityType),
push_order_card: false,
},
}),
12000,
'准备群聊超时'
);
const body = res?.data || {};
if ((body.code === 0 || body.code === 200) && body.data?.groupId) {
chatData = body.data;
} else if (body.msg) {
console.warn('ltdhzb:', body.msg);
}
} catch (e) {
console.warn('ltdhzb 请求失败,尝试本地配对 groupId', e);
}
if (!chatData?.groupId) {
const myUid = userId.replace(/^(Ds|Sj|Boss)/, '');
let localGroupId = resolveLocalGroupId(
identityType, myUid, partnerUid, fadanPingtai, orderIdStr, isCross
);
if (!localGroupId && orderIdStr) {
localGroupId = `group_${orderIdStr}`;
}
if (!localGroupId) {
throw new Error('无法确定群聊,请确认订单已接单且对方信息完整');
}
chatData = {
groupId: localGroupId,
orderId: orderIdStr,
groupName: groupName || '订单群聊',
isCross: isCross || fadanPingtai || 0,
};
try {
await request({
url: '/dingdan/ltdhzb',
method: 'POST',
data: {
dingdan_id: orderIdStr,
identityType: mapIdentityForApi(identityType),
push_order_card: false,
},
});
} catch (e) { /* ignore */ }
}
return chatData;
}
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),
};
wx.navigateTo({
url: '/pages/chat/chat?data=' + encodeURIComponent(JSON.stringify({ to, currentUser })),
});
}
async connectToGroupChat(params) {
const {
identityType, userId, orderId, groupName, groupAvatar, isCross,
partnerUid, fadanPingtai,
} = params;
if (!identityType || !userId || !orderId) {
throw new Error('参数不完整');
}
app.globalData.currentRole = identityType;
wx.setStorageSync('currentRole', identityType);
wx.showLoading({ title: '建立联系中...', mask: true });
try {
const chatData = await prepareGroupChat(
identityType, userId, orderId, partnerUid, fadanPingtai || isCross, groupName, isCross
);
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 || String(orderId).replace(/^group_/, ''),
orderDesc: chatData.orderDesc || '',
orderZhuangtai: chatData.orderZhuangtai,
orderJine: chatData.orderJine,
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);
let imReady = false;
try {
await ensureIdentityConnection(identityType, userId);
imReady = true;
try {
await withTimeout(
new Promise((resolve, reject) => {
wx.goEasy.im.subscribeGroup({
groupIds: [realGroupId],
onSuccess: () => resolve(),
onFailed: (error) => reject(error),
});
}),
8000,
'订阅群聊超时'
);
} catch (subErr) {
console.warn('订阅群聊失败,仍尝试进入页面', subErr);
}
} catch (imErr) {
console.warn('IM连接失败,仍进入聊天页由页面重连', imErr);
}
const uid = userId.replace(/^(Ds|Sj|Boss)/, '');
const param = {
groupId: realGroupId,
orderId: chatData.orderId || String(orderId).replace(/^group_/, ''),
groupName: chatData.counterpartName || chatData.groupName || groupName || '订单群聊',
groupAvatar: avatar,
isCross: chatData.isCross != null ? chatData.isCross : (isCross || 0),
currentUserId: userId,
currentUserName: app.globalData.currentUser?.name || `用户${uid}`,
currentUserAvatar: app.globalData.currentUser?.avatar ||
(wx.getStorageSync('touxiang') || (app.globalData.ossImageUrl + app.globalData.morentouxiang)),
orderZhuangtai: chatData.orderZhuangtai,
orderJine: chatData.orderJine,
orderDesc: chatData.orderDesc || '',
imReady,
};
wx.hideLoading();
wx.navigateTo({
url: '/pages/group-chat/group-chat?data=' + encodeURIComponent(JSON.stringify(param)),
fail: (navErr) => {
wx.showToast({ title: formatError(navErr), icon: 'none', duration: 2500 });
},
});
} catch (err) {
wx.hideLoading();
console.error('跳转群聊失败:', err);
wx.showToast({ title: formatError(err), icon: 'none', duration: 2500 });
throw err;
}
}
}
export default new ConnectionManager();
+53
View File
@@ -0,0 +1,53 @@
import request from './request.js';
import { getDefaultAvatarUrl } from './avatar.js';
export const ZHIDING_HF_MAP = { 1: '想接', 2: '已婉拒', 3: '等会接' };
export async function fetchMyZhidingOrders() {
try {
const res = await request({ url: '/dingdan/zdcx', method: 'POST', data: {} });
const body = res?.data;
if (body && (body.code === 200 || body.code === 0) && body.data) {
return body.data.list || [];
}
} catch (e) {
console.warn('fetchMyZhidingOrders failed', e);
}
return [];
}
export async function submitZhidingResponse(dingdanId, huifu, dengHuiNote = '') {
const res = await request({
url: '/dingdan/zdhf',
method: 'POST',
data: {
dingdan_id: dingdanId,
huifu,
deng_hui_note: dengHuiNote,
},
});
return res?.data;
}
export function processZhidingList(list, app, uid, highlightOrderId = '') {
const defAvatar = getDefaultAvatarUrl(app);
const oss = app.globalData?.ossImageUrl || '';
return (list || []).map((item) => {
let avatar = defAvatar;
if (item.zhiding_avatar) {
avatar = item.zhiding_avatar.startsWith('http') ? item.zhiding_avatar : oss + item.zhiding_avatar;
}
return {
...item,
isMyZhiding: true,
zhiding_avatar_full: avatar,
zhiding_hf_text: item.zhiding_hf_text || ZHIDING_HF_MAP[item.zhiding_hf] || '',
isHighlighted: highlightOrderId && item.dingdan_id === highlightOrderId,
};
}).filter((item) => String(item.zhiding_uid) === String(uid));
}
/** 横幅只展示尚未回复的指定单(已等会接/不想接的不占横幅) */
export function filterZhidingBannerList(list) {
return (list || []).filter((item) => !item.zhiding_hf);
}