第一次提交:微信小程序前端最新完整代码
This commit is contained in:
38
utils/avatar.js
Normal file
38
utils/avatar.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/** 统一头像 URL:无有效头像时始终返回 ossImageUrl + morentouxiang */
|
||||
|
||||
function isBlankPath(p) {
|
||||
if (p == null) return true;
|
||||
if (typeof p !== 'string') return true;
|
||||
const s = p.trim();
|
||||
return !s || s === 'null' || s === 'undefined' || s === 'none';
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
if (!isBlankPath(rawPath)) {
|
||||
if (rawPath.startsWith('http://') || rawPath.startsWith('https://')) {
|
||||
return rawPath;
|
||||
}
|
||||
const path = rawPath.startsWith('/') ? rawPath : '/' + rawPath;
|
||||
return oss + path;
|
||||
}
|
||||
|
||||
if (def.startsWith('http://') || def.startsWith('https://')) {
|
||||
return def;
|
||||
}
|
||||
const defPath = def.startsWith('/') ? def : '/' + def;
|
||||
return oss + defPath;
|
||||
}
|
||||
|
||||
export function resolveAvatarFromStorage(app) {
|
||||
app = app || getApp();
|
||||
const tx = wx.getStorageSync('touxiang') || '';
|
||||
return resolveAvatarUrl(tx, app);
|
||||
}
|
||||
400
utils/chat-core.js
Normal file
400
utils/chat-core.js
Normal file
@@ -0,0 +1,400 @@
|
||||
// utils/chat-core.js — 全局 IM 连接、监听、角标(稳定版)
|
||||
import GoEasy from '../static/lib/goeasy-2.13.24.esm.min';
|
||||
import { resolveAvatarUrl } from './avatar.js';
|
||||
import { getLocalImUserId, getLocalImIdentity } from './im-user.js';
|
||||
|
||||
let _convListener = null;
|
||||
let _privateListener = null;
|
||||
let _groupListener = null;
|
||||
let _notificationHandler = null;
|
||||
let _heartbeatTimer = null;
|
||||
let _listenersBoundForUser = null;
|
||||
let _lastGroupSubscribeAt = 0;
|
||||
const _notifiedMessageIds = new Map();
|
||||
const NOTIFY_MAX_AGE_MS = 15000;
|
||||
|
||||
function shouldShowNotification(message) {
|
||||
if (!message) return false;
|
||||
const id = message.messageId || `${message.senderId}_${message.timestamp}`;
|
||||
if (_notifiedMessageIds.has(id)) return false;
|
||||
if (message.timestamp && Date.now() - message.timestamp > NOTIFY_MAX_AGE_MS) return false;
|
||||
_notifiedMessageIds.set(id, Date.now());
|
||||
if (_notifiedMessageIds.size > 400) {
|
||||
const cutoff = Date.now() - 60000;
|
||||
_notifiedMessageIds.forEach((t, k) => {
|
||||
if (t < cutoff) _notifiedMessageIds.delete(k);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function bindGlobalListenersIfNeeded(app, userId) {
|
||||
if (_listenersBoundForUser === userId && _convListener) return;
|
||||
registerGlobalListeners(app);
|
||||
registerNotification(app);
|
||||
_listenersBoundForUser = userId;
|
||||
}
|
||||
|
||||
function initGlobalMessageSystem(app) {
|
||||
app.ensureConnection = () => ensureConnection(app);
|
||||
app.connectForCurrentRole = () => ensureConnection(app);
|
||||
app.loadConversations = () => loadConversations(app);
|
||||
app.disconnectGoEasy = () => disconnectGoEasy(app);
|
||||
app.switchRoleAndReconnect = (newRole) => switchRoleAndReconnect(app, newRole);
|
||||
app.updateUnreadCount = (count) => updateTabBarBadge(app, count || 0);
|
||||
app.reregisterNotification = () => registerNotification(app);
|
||||
app.startImWhenReady = () => startImWhenReady(app);
|
||||
|
||||
if (!app.globalData.messageManager) {
|
||||
app.globalData.messageManager = {};
|
||||
}
|
||||
try {
|
||||
const muted = wx.getStorageSync('notificationMuted');
|
||||
if (muted !== undefined) app.globalData.messageManager.notificationMuted = muted;
|
||||
} catch (e) {}
|
||||
|
||||
initAppStateListener(app);
|
||||
initNetworkListener(app);
|
||||
startHeartbeat(app);
|
||||
startImWhenReady(app);
|
||||
}
|
||||
|
||||
/** GoEasy 未就绪时轮询,登录后尽早建立监听 */
|
||||
function startImWhenReady(app, maxAttempts = 40) {
|
||||
let attempts = 0;
|
||||
const tick = () => {
|
||||
if (!wx.getStorageSync('uid')) return;
|
||||
attempts += 1;
|
||||
if (!wx.goEasy) {
|
||||
if (attempts < maxAttempts) setTimeout(tick, 250);
|
||||
return;
|
||||
}
|
||||
ensureConnection(app);
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
function getCurrentUserId() {
|
||||
return (wx.goEasy && wx.goEasy.im && wx.goEasy.im.userId) || null;
|
||||
}
|
||||
|
||||
function getConnectionStatus() {
|
||||
if (!wx.goEasy || !wx.goEasy.getConnectionStatus) return 'disconnected';
|
||||
return wx.goEasy.getConnectionStatus();
|
||||
}
|
||||
|
||||
function isConnected() {
|
||||
const s = getConnectionStatus();
|
||||
return s === 'connected' || s === 'reconnected';
|
||||
}
|
||||
|
||||
/** 统一会话数据结构,避免不同回调格式导致列表空白 */
|
||||
function normalizeConversationsPayload(result) {
|
||||
if (!result) return { conversations: [], unreadTotal: 0 };
|
||||
const content = result.content || result;
|
||||
const conversations = content.conversations || result.conversations || [];
|
||||
const unreadTotal = content.unreadTotal ?? result.unreadTotal ?? 0;
|
||||
return { conversations, unreadTotal };
|
||||
}
|
||||
|
||||
function emitConversationsUpdated(app, result) {
|
||||
const payload = normalizeConversationsPayload(result);
|
||||
updateTabBarBadge(app, payload.unreadTotal);
|
||||
app.emitEvent('conversationsUpdated', {
|
||||
content: payload,
|
||||
unreadTotal: payload.unreadTotal,
|
||||
raw: result,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureConnection(app) {
|
||||
const uid = wx.getStorageSync('uid');
|
||||
if (!uid || !wx.goEasy) return;
|
||||
|
||||
const targetUserId = getLocalImUserId(app);
|
||||
const identityType = getLocalImIdentity(app);
|
||||
if (!targetUserId) return;
|
||||
|
||||
if (isConnected()) {
|
||||
const currentUserId = getCurrentUserId();
|
||||
if (currentUserId === targetUserId) {
|
||||
bindGlobalListenersIfNeeded(app, targetUserId);
|
||||
loadConversations(app);
|
||||
const now = Date.now();
|
||||
if (now - _lastGroupSubscribeAt > 60000) {
|
||||
_lastGroupSubscribeAt = now;
|
||||
await subscribeAllGroups(app);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
wx.goEasy.disconnect();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
connectGoEasy(app, targetUserId, identityType);
|
||||
}
|
||||
|
||||
function connectGoEasy(app, userId, identityType) {
|
||||
const defAvatar = resolveAvatarUrl('', app);
|
||||
const tx = wx.getStorageSync('touxiang') || '';
|
||||
const uid = wx.getStorageSync('uid') || '';
|
||||
const nick = wx.getStorageSync('nicheng') || app.globalData.nicheng || app.globalData.dashouNicheng || '';
|
||||
const name = nick || ('用户' + (uid || '').substring(0, 6));
|
||||
const avatar = resolveAvatarUrl(tx, app) || defAvatar;
|
||||
|
||||
wx.goEasy.connect({
|
||||
id: userId,
|
||||
data: {
|
||||
name,
|
||||
avatar,
|
||||
identityType: identityType === 'normal' ? 'boss' : identityType,
|
||||
},
|
||||
onSuccess: async () => {
|
||||
try {
|
||||
wx.setStorageSync('savedGoEasyConnection', JSON.stringify({
|
||||
userId,
|
||||
identityType,
|
||||
lastConnectTime: Date.now(),
|
||||
status: 'connected',
|
||||
}));
|
||||
} catch (e) {}
|
||||
bindGlobalListenersIfNeeded(app, userId);
|
||||
_lastGroupSubscribeAt = Date.now();
|
||||
await subscribeAllGroups(app);
|
||||
loadConversations(app);
|
||||
},
|
||||
onFailed: (error) => {
|
||||
console.error('GoEasy连接失败', error);
|
||||
if (error.code === 408) {
|
||||
bindGlobalListenersIfNeeded(app, userId);
|
||||
loadConversations(app);
|
||||
} else {
|
||||
wx.showToast({ title: '连接失败,请重试', icon: 'none' });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeAllGroups(app) {
|
||||
return new Promise((resolve) => {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return resolve();
|
||||
wx.goEasy.im.latestConversations({
|
||||
onSuccess: (result) => {
|
||||
const payload = normalizeConversationsPayload(result);
|
||||
const groupIds = payload.conversations
|
||||
.filter((c) => c.type === 'group' && c.groupId)
|
||||
.map((c) => c.groupId);
|
||||
if (groupIds.length && wx.goEasy.im.subscribeGroup) {
|
||||
wx.goEasy.im.subscribeGroup({
|
||||
groupIds,
|
||||
onSuccess: () => resolve(),
|
||||
onFailed: () => resolve(),
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
onFailed: () => resolve(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerGlobalListeners(app) {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return;
|
||||
|
||||
if (_convListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _convListener);
|
||||
}
|
||||
if (_privateListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _privateListener);
|
||||
}
|
||||
if (_groupListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _groupListener);
|
||||
}
|
||||
|
||||
_convListener = (result) => {
|
||||
emitConversationsUpdated(app, result);
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _convListener);
|
||||
|
||||
_privateListener = () => {
|
||||
loadConversations(app);
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _privateListener);
|
||||
|
||||
_groupListener = () => {
|
||||
loadConversations(app);
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _groupListener);
|
||||
}
|
||||
|
||||
function loadConversations(app) {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return;
|
||||
if (!getCurrentUserId()) return;
|
||||
|
||||
wx.goEasy.im.latestConversations({
|
||||
onSuccess: (result) => {
|
||||
emitConversationsUpdated(app, result);
|
||||
subscribeAllGroups(app);
|
||||
},
|
||||
onFailed: (err) => {
|
||||
console.error('加载会话失败', err);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateTabBarBadge(app, unreadCount) {
|
||||
const badgeText = unreadCount > 0 ? (unreadCount > 99 ? '99+' : String(unreadCount)) : '';
|
||||
if (!app.globalData.messageManager) app.globalData.messageManager = {};
|
||||
app.globalData.messageManager.unreadTotal = unreadCount;
|
||||
app.globalData.messageManager.tabBarBadgeText = badgeText;
|
||||
app.emitEvent('tabBarBadgeChanged', { badgeText, showRedDot: unreadCount > 0 });
|
||||
|
||||
try {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length) {
|
||||
const page = pages[pages.length - 1];
|
||||
const tabBar = page.selectComponent('#custom-tab-bar');
|
||||
if (tabBar && tabBar.setData) tabBar.setData({ badgeText });
|
||||
if (page.getTabBar) {
|
||||
const tb = page.getTabBar();
|
||||
if (tb && tb.setData) tb.setData({ badgeText });
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
wx.setStorageSync('messageUnreadTotal', unreadCount);
|
||||
}
|
||||
|
||||
function registerNotification(app) {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return;
|
||||
|
||||
if (_notificationHandler) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _notificationHandler);
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _notificationHandler);
|
||||
}
|
||||
|
||||
_notificationHandler = (message) => {
|
||||
if (app.globalData.messageManager && app.globalData.messageManager.notificationMuted) return;
|
||||
if (!shouldShowNotification(message)) return;
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length) {
|
||||
const curPage = pages[pages.length - 1];
|
||||
const route = curPage.route || '';
|
||||
if (route === 'pages/liaotian/liaotian' || route === 'pages/qunliaotian/qunliaotian') return;
|
||||
}
|
||||
const senderName = message.senderData?.name || (message.senderId || '用户');
|
||||
let content = '';
|
||||
if (message.type === 'text') content = message.payload?.text || '[文本]';
|
||||
else if (message.type === 'image') content = '[图片]';
|
||||
else if (message.type === 'audio') content = '[语音]';
|
||||
else content = '[新消息]';
|
||||
const avatar = resolveAvatarUrl(message.senderData?.avatar, app);
|
||||
|
||||
if (app.globalData.globalNotification && app.globalData.globalNotification.show) {
|
||||
app.globalData.globalNotification.show({
|
||||
senderName,
|
||||
content,
|
||||
avatar,
|
||||
message,
|
||||
notificationType: message.groupId ? 'group' : 'private',
|
||||
groupId: message.groupId,
|
||||
orderId: message.orderId,
|
||||
});
|
||||
} else {
|
||||
wx.showToast({ title: `${senderName}: ${content}`, icon: 'none', duration: 2000 });
|
||||
}
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _notificationHandler);
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _notificationHandler);
|
||||
}
|
||||
|
||||
function detachGlobalListeners() {
|
||||
if (!wx.goEasy || !wx.goEasy.im) return;
|
||||
if (_convListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _convListener);
|
||||
_convListener = null;
|
||||
}
|
||||
if (_privateListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _privateListener);
|
||||
_privateListener = null;
|
||||
}
|
||||
if (_groupListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _groupListener);
|
||||
_groupListener = null;
|
||||
}
|
||||
if (_notificationHandler) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _notificationHandler);
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _notificationHandler);
|
||||
_notificationHandler = null;
|
||||
}
|
||||
_listenersBoundForUser = null;
|
||||
}
|
||||
|
||||
function disconnectGoEasy(app) {
|
||||
stopHeartbeat(app);
|
||||
detachGlobalListeners();
|
||||
if (wx.goEasy && wx.goEasy.disconnect) {
|
||||
wx.goEasy.disconnect({ onSuccess: () => {}, onFailed: () => {} });
|
||||
}
|
||||
wx.removeStorageSync('savedGoEasyConnection');
|
||||
}
|
||||
|
||||
async function switchRoleAndReconnect(app, newRole) {
|
||||
disconnectGoEasy(app);
|
||||
app.globalData.currentRole = newRole;
|
||||
wx.setStorageSync('currentRole', newRole);
|
||||
if (app.globalData.primaryRole !== newRole && ['normal', 'dashou', 'shangjia'].includes(newRole)) {
|
||||
app.globalData.primaryRole = newRole;
|
||||
wx.setStorageSync('primaryRole', newRole);
|
||||
}
|
||||
startHeartbeat(app);
|
||||
await ensureConnection(app);
|
||||
return true;
|
||||
}
|
||||
|
||||
function startHeartbeat(app) {
|
||||
stopHeartbeat(app);
|
||||
_heartbeatTimer = setInterval(() => {
|
||||
if (!wx.getStorageSync('uid')) return;
|
||||
if (isConnected()) {
|
||||
loadConversations(app);
|
||||
} else {
|
||||
ensureConnection(app);
|
||||
}
|
||||
}, 45000);
|
||||
}
|
||||
|
||||
function stopHeartbeat(app) {
|
||||
if (_heartbeatTimer) {
|
||||
clearInterval(_heartbeatTimer);
|
||||
_heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function initAppStateListener(app) {
|
||||
wx.onAppShow(() => {
|
||||
setTimeout(() => {
|
||||
if (!wx.getStorageSync('uid')) return;
|
||||
if (!isConnected()) {
|
||||
ensureConnection(app);
|
||||
} else {
|
||||
loadConversations(app);
|
||||
setTimeout(() => {
|
||||
const unread = (app.globalData.messageManager && app.globalData.messageManager.unreadTotal) || 0;
|
||||
updateTabBarBadge(app, unread);
|
||||
}, 300);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
function initNetworkListener(app) {
|
||||
wx.onNetworkStatusChange((res) => {
|
||||
if (res.isConnected) {
|
||||
setTimeout(() => ensureConnection(app), 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { initGlobalMessageSystem };
|
||||
85
utils/conversation-display.js
Normal file
85
utils/conversation-display.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/** 会话列表展示:私聊固定显示对方昵称/头像,避免来回变成自己的 */
|
||||
import { resolveAvatarUrl } from './avatar.js';
|
||||
import { getLocalImUserId } from './im-user.js';
|
||||
|
||||
function getLocalDisplayName(app) {
|
||||
app = app || getApp();
|
||||
return (
|
||||
wx.getStorageSync('nicheng')
|
||||
|| app.globalData.dashouNicheng
|
||||
|| app.globalData.nicheng
|
||||
|| ''
|
||||
);
|
||||
}
|
||||
|
||||
function isLikelySelfProfile(name, avatar, app) {
|
||||
const myName = getLocalDisplayName(app);
|
||||
if (myName && name && name === myName) return true;
|
||||
const myTx = wx.getStorageSync('touxiang') || '';
|
||||
if (!myTx || !avatar) return false;
|
||||
const a = String(avatar);
|
||||
const t = String(myTx);
|
||||
return a.includes(t) || t.includes(a.replace(/^https?:\/\/[^/]+/, ''));
|
||||
}
|
||||
|
||||
function stripImPrefix(id) {
|
||||
if (!id) return '';
|
||||
return String(id).replace(/^(Ds|Sj|Boss|Gs|Zz|Kh)/, '') || id;
|
||||
}
|
||||
|
||||
export function normalizeConversationItem(item, app, peerCache = {}) {
|
||||
if (!item) return item;
|
||||
if (!item.data) item.data = {};
|
||||
|
||||
if (item.type === 'private' && item.userId) {
|
||||
const peerId = item.userId;
|
||||
const lastMsg = item.lastMessage;
|
||||
let peerName = '';
|
||||
let peerAvatar = '';
|
||||
|
||||
if (lastMsg && lastMsg.senderId === peerId && lastMsg.senderData) {
|
||||
peerName = lastMsg.senderData.name || '';
|
||||
peerAvatar = lastMsg.senderData.avatar || '';
|
||||
}
|
||||
|
||||
if (peerCache[peerId]) {
|
||||
if (!peerName) peerName = peerCache[peerId].name || '';
|
||||
if (!peerAvatar) peerAvatar = peerCache[peerId].avatar || '';
|
||||
}
|
||||
|
||||
if (item.data.name && !isLikelySelfProfile(item.data.name, item.data.avatar, app)) {
|
||||
if (!peerName) peerName = item.data.name;
|
||||
if (!peerAvatar) peerAvatar = item.data.avatar || '';
|
||||
}
|
||||
|
||||
if (!peerName || isLikelySelfProfile(peerName, peerAvatar, app)) {
|
||||
peerName = stripImPrefix(peerId) || peerId;
|
||||
}
|
||||
|
||||
item.data.name = peerName;
|
||||
item.data.avatar = resolveAvatarUrl(peerAvatar, app);
|
||||
item.key = 'private_' + peerId;
|
||||
|
||||
if (peerName && !isLikelySelfProfile(peerName, peerAvatar, app)) {
|
||||
peerCache[peerId] = { name: peerName, avatar: peerAvatar };
|
||||
}
|
||||
} else if (item.type === 'group') {
|
||||
item.data.avatar = resolveAvatarUrl(item.data.avatar, app);
|
||||
if (!item.data.name) {
|
||||
item.data.name = item.data.orderId || item.groupId || '订单群聊';
|
||||
}
|
||||
item.key = 'group_' + (item.groupId || item.data.orderId || '');
|
||||
} else if (item.type === 'cs') {
|
||||
item.data.avatar = resolveAvatarUrl(item.data.avatar, app);
|
||||
if (!item.data.name) item.data.name = '在线客服';
|
||||
item.key = 'cs_' + (item.teamId || 'default');
|
||||
} else {
|
||||
item.key = item.userId || item.groupId || String(item.lastMessage?.timestamp || Math.random());
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
export function normalizeConversationsList(conversations, app, peerCache) {
|
||||
return (conversations || []).map((c) => normalizeConversationItem(c, app, peerCache));
|
||||
}
|
||||
13296
utils/cos-wx-sdk-v5.js
Normal file
13296
utils/cos-wx-sdk-v5.js
Normal file
File diff suppressed because one or more lines are too long
78
utils/im-user.js
Normal file
78
utils/im-user.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/** IM 用户 ID:三端连接 + 管事/组长/考核官统一走打手 Ds 前缀 */
|
||||
import { getPrimaryRole } from './primary-role.js';
|
||||
import { resolveAvatarUrl } from './avatar.js';
|
||||
import { ensureRoleOnCenterPage } from './role-tab-bar.js';
|
||||
|
||||
/** 联系对象前缀:管事/组长/考核官已废弃独立前缀,统一 Ds */
|
||||
const PEER_PREFIX = {
|
||||
boss: 'Boss',
|
||||
normal: 'Boss',
|
||||
dashou: 'Ds',
|
||||
shangjia: 'Sj',
|
||||
guanshi: 'Ds',
|
||||
zuzhang: 'Ds',
|
||||
kaoheguan: 'Ds',
|
||||
};
|
||||
|
||||
export function getImPrefixForRole(role) {
|
||||
return PEER_PREFIX[role] || 'Ds';
|
||||
}
|
||||
|
||||
/** 当前端连接 GoEasy 用的 userId */
|
||||
export function getLocalImUserId(app) {
|
||||
app = app || getApp();
|
||||
const uid = wx.getStorageSync('uid') || '';
|
||||
if (!uid) return '';
|
||||
const primary = getPrimaryRole(app);
|
||||
if (primary === 'dashou') return 'Ds' + uid;
|
||||
if (primary === 'shangjia') return 'Sj' + uid;
|
||||
return 'Boss' + uid;
|
||||
}
|
||||
|
||||
export function getLocalImIdentity(app) {
|
||||
app = app || getApp();
|
||||
return getPrimaryRole(app) || 'normal';
|
||||
}
|
||||
|
||||
export function buildPeerImUserId(uid, role = 'dashou') {
|
||||
if (!uid) return '';
|
||||
return getImPrefixForRole(role) + uid;
|
||||
}
|
||||
|
||||
/** 联系邀请人/管事/组长:统一 Ds 前缀 */
|
||||
export function buildGuanshiPeerId(uid) {
|
||||
return buildDashouPeerId(uid);
|
||||
}
|
||||
|
||||
export function buildDashouPeerId(uid) {
|
||||
return buildPeerImUserId(uid, 'dashou');
|
||||
}
|
||||
|
||||
export function buildInviterPeerId(uid) {
|
||||
return buildDashouPeerId(uid);
|
||||
}
|
||||
|
||||
export function ensureDashouImReady(page) {
|
||||
const app = getApp();
|
||||
ensureRoleOnCenterPage(page, 'dashou');
|
||||
if (app.ensureConnection) app.ensureConnection();
|
||||
}
|
||||
|
||||
export function openPrivateChat(page, { toUserId, toName, toAvatar, delay = 350 }) {
|
||||
ensureDashouImReady(page);
|
||||
const avatar = resolveAvatarUrl(toAvatar);
|
||||
const param = {
|
||||
toUserId,
|
||||
toName: toName || '用户',
|
||||
toAvatar: avatar,
|
||||
};
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({
|
||||
url: '/pages/liaotian/liaotian?data=' + encodeURIComponent(JSON.stringify(param)),
|
||||
fail: () => wx.showToast({ title: '打开聊天失败', icon: 'none' }),
|
||||
complete: resolve,
|
||||
});
|
||||
}, delay);
|
||||
});
|
||||
}
|
||||
51
utils/imAuth/dashoujianquan.js
Normal file
51
utils/imAuth/dashoujianquan.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// utils/imAuth/dashoujianquan.js
|
||||
const imRequest = require('./imRequest.js');
|
||||
|
||||
/**
|
||||
* 打手 IM 权限检测
|
||||
* POST /yonghu/dshqltqx
|
||||
* 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 }
|
||||
*/
|
||||
async function dashoujianquan(app) {
|
||||
const uid = wx.getStorageSync('uid');
|
||||
if (!uid) return { allowed: false, reason: '未登录' };
|
||||
|
||||
try {
|
||||
const res = await imRequest({
|
||||
url: '/yonghu/dshqltqx',
|
||||
method: 'POST',
|
||||
data: { uid }
|
||||
});
|
||||
|
||||
if (res && res.data && res.data.code === 0) {
|
||||
const allow = res.data.allow;
|
||||
if (allow === 1) {
|
||||
return { allowed: true };
|
||||
} else {
|
||||
await app.disconnectGoEasy();
|
||||
wx.showToast({
|
||||
title: '当前身份已不支持消息聊天功能,请充值会员或联系管理员后尝试',
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
mask: true
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 2100));
|
||||
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));
|
||||
return { allowed: false, reason: '鉴权请求失败' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { dashoujianquan };
|
||||
50
utils/imAuth/imRequest.js
Normal file
50
utils/imAuth/imRequest.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// utils/imAuth/imRequest.js
|
||||
// 专用于 IM 鉴权模块的请求封装
|
||||
// 与 utils/request.js 功能完全一致,但不依赖 getApp(),API 域名硬编码
|
||||
const API_BASE_URL = 'https://www.abas.asia/hqhd';
|
||||
|
||||
function imRequest(options) {
|
||||
const token = wx.getStorageSync('token');
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.header,
|
||||
};
|
||||
if (token) {
|
||||
header['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: API_BASE_URL + options.url,
|
||||
method: options.method,
|
||||
data: options.data || {},
|
||||
header,
|
||||
success: async (res) => {
|
||||
if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
// 如果全局有登录 Promise 在等待,则等待完成后重试
|
||||
const app = getApp();
|
||||
if (app && app.globalData && app.globalData.loginPromise) {
|
||||
try {
|
||||
await app.globalData.loginPromise;
|
||||
const retryRes = await imRequest(options);
|
||||
resolve(retryRes);
|
||||
} catch (err) {
|
||||
// 重试失败,静默处理,不再弹窗
|
||||
resolve(res);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 静默处理,不弹窗,不跳转
|
||||
resolve(res);
|
||||
return;
|
||||
}
|
||||
// 正常响应
|
||||
resolve(res);
|
||||
},
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = imRequest;
|
||||
58
utils/imAuth/jianquanxian.js
Normal file
58
utils/imAuth/jianquanxian.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// utils/imAuth/jianquanxian.js
|
||||
// 原:require('./laobanjianquan')
|
||||
// 改:
|
||||
const { laobanjianquan } = require('./laobanjianquan.js');
|
||||
const { dashoujianquan } = require('./dashoujianquan.js');
|
||||
|
||||
/**
|
||||
* IM 长连接权限统一入口
|
||||
* 按顺序进行:
|
||||
* 1. 检查本地 token/uid 是否存在
|
||||
* 2. 若不存在,强制断开已有连接,弹窗提示未登录(强制阅读 2 秒)
|
||||
* 3. 若存在,根据当前角色调用具体的鉴权模块
|
||||
*
|
||||
* @param {Object} app 全局 App 实例
|
||||
* @returns {Promise<{ allowed: boolean, reason?: string }>}
|
||||
*/
|
||||
async function jianquanxian(app) {
|
||||
// 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));
|
||||
return { allowed: false, reason: '未登录' };
|
||||
}
|
||||
|
||||
// 2. 获取当前选中的角色
|
||||
const role = app.globalData.currentRole || 'normal';
|
||||
|
||||
// 3. 根据角色分发到具体鉴权模块
|
||||
switch (role) {
|
||||
case 'normal': // 点单老板
|
||||
return laobanjianquan(app);
|
||||
|
||||
case 'dashou': // 打手
|
||||
return dashoujianquan(app);
|
||||
|
||||
// 其他角色暂未细分,统一放行(后续可新增对应文件)
|
||||
case 'shangjia':
|
||||
case 'guanshi':
|
||||
case 'zuzhang':
|
||||
case 'kaoheguan':
|
||||
return { allowed: true };
|
||||
|
||||
default:
|
||||
return { allowed: false, reason: '未知角色' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { jianquanxian };
|
||||
51
utils/imAuth/laobanjianquan.js
Normal file
51
utils/imAuth/laobanjianquan.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// utils/imAuth/laobanjianquan.js
|
||||
const imRequest = require('./imRequest.js');
|
||||
|
||||
/**
|
||||
* 老板(点单用户)IM 权限检测
|
||||
* POST /yonghu/lbhqltqx
|
||||
* 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 }
|
||||
*/
|
||||
async function laobanjianquan(app) {
|
||||
const uid = wx.getStorageSync('uid');
|
||||
if (!uid) return { allowed: false, reason: '未登录' };
|
||||
|
||||
try {
|
||||
const res = await imRequest({
|
||||
url: '/yonghu/lbhqltqx',
|
||||
method: 'POST',
|
||||
data: { uid }
|
||||
});
|
||||
|
||||
if (res && res.data && res.data.code === 0) {
|
||||
const allow = res.data.allow;
|
||||
if (allow === 1) {
|
||||
return { allowed: true };
|
||||
} else {
|
||||
await app.disconnectGoEasy();
|
||||
wx.showToast({
|
||||
title: '当前身份已不支持使用消息功能',
|
||||
icon: 'none',
|
||||
duration: 1000,
|
||||
mask: true
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 1100));
|
||||
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));
|
||||
return { allowed: false, reason: '鉴权请求失败' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { laobanjianquan };
|
||||
74
utils/imageUrl.js
Normal file
74
utils/imageUrl.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// /utils/imageUrl.js - 统一的COS图片路径管理
|
||||
const app = getApp();
|
||||
|
||||
/**
|
||||
* 获取完整的COS图片URL
|
||||
* @param {string} path - 图片相对路径,如 'a_long/images/common/arrow-down.png'
|
||||
* @returns {string} 完整的URL
|
||||
*/
|
||||
export function getImageUrl(path) {
|
||||
if (!app || !app.globalData || !app.globalData.ossImageUrl) {
|
||||
console.warn('⚠️ App.globalData.ossImageUrl 未初始化');
|
||||
return path; // 返回原路径作为fallback
|
||||
}
|
||||
|
||||
// 如果已经是完整URL,直接返回
|
||||
if (path.startsWith('http')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// 拼接完整URL
|
||||
return app.globalData.ossImageUrl + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取常用图片的URL
|
||||
*/
|
||||
export const imageUrls = {
|
||||
// 身份相关
|
||||
identityIcon: 'a_long/images/identity/identity-icon.png',
|
||||
arrowDown: 'a_long/images/common/arrow-down.png',
|
||||
bossIcon: 'a_long/images/identity/boss-icon.png',
|
||||
dashouIcon: 'a_long/images/identity/dashou-icon.png',
|
||||
shangjiaIcon: 'a_long/images/identity/shangjia-icon.png',
|
||||
|
||||
// 公共图标
|
||||
actionIcon: 'a_long/images/common/action.png',
|
||||
emptyMessage: 'a_long/images/common/empty-message.png',
|
||||
|
||||
// 通知相关
|
||||
notificationOn: 'a_long/images/notifications/notification-on.png',
|
||||
notificationOff: 'a_long/images/notifications/notification-off.png',
|
||||
|
||||
// TabBar图标
|
||||
tabbarHome: 'a_long/images/tabbar/home.png',
|
||||
tabbarHomeActive: 'a_long/images/tabbar/home-active.png',
|
||||
tabbarCategory: 'a_long/images/tabbar/category.png',
|
||||
tabbarCategoryActive: 'a_long/images/tabbar/category-active.png',
|
||||
tabbarMessage: 'a_long/images/tabbar/message.png',
|
||||
tabbarMessageActive: 'a_long/images/tabbar/message-active.png',
|
||||
tabbarProfile: 'a_long/images/tabbar/profile.png',
|
||||
tabbarProfileActive: 'a_long/images/tabbar/profile-active.png',
|
||||
|
||||
// 用户相关
|
||||
defaultAvatar: 'a_long/images/common/default-avatar.png'
|
||||
};
|
||||
|
||||
/**
|
||||
* 预加载常用图片
|
||||
*/
|
||||
export function preloadCommonImages() {
|
||||
const images = Object.values(imageUrls).map(path => getImageUrl(path));
|
||||
|
||||
images.forEach(src => {
|
||||
wx.getImageInfo({
|
||||
src: src,
|
||||
success: () => {
|
||||
console.log('✅ 图片预加载成功:', src);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.warn('⚠️ 图片预加载失败:', src, err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
73
utils/login.js
Normal file
73
utils/login.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// utils/login.js
|
||||
import {
|
||||
applyWechatLoginData,
|
||||
backfillUserProfileCache,
|
||||
getSessionToken,
|
||||
isWechatLoginSuccess,
|
||||
} from './role-tab-bar';
|
||||
|
||||
const app = getApp();
|
||||
|
||||
export function ensureLogin(options = {}) {
|
||||
const silent = options.silent !== false;
|
||||
const page = options.page || null;
|
||||
const token = getSessionToken(app);
|
||||
if (token) {
|
||||
backfillUserProfileCache(app);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 关键:如果已有登录流程正在执行,直接返回那个 Promise,避免并发登录
|
||||
if (app.globalData.loginPromise) {
|
||||
return app.globalData.loginPromise;
|
||||
}
|
||||
|
||||
// 开始新的登录流程
|
||||
const loginPromise = new Promise(async (resolve, reject) => {
|
||||
if (silent) {
|
||||
wx.showLoading({ title: '自动登录中...', mask: true });
|
||||
}
|
||||
try {
|
||||
const loginRes = await wx.login();
|
||||
if (!loginRes.code) throw new Error('获取微信登录码失败');
|
||||
|
||||
const apiBaseUrl = app.globalData.apiBaseUrl;
|
||||
if (!apiBaseUrl) throw new Error('服务器配置错误');
|
||||
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: apiBaseUrl + '/yonghu/wechatlogin',
|
||||
method: 'POST',
|
||||
data: { code: loginRes.code },
|
||||
header: { 'content-type': 'application/json' },
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
|
||||
const userData = res.data && (res.data.data || res.data);
|
||||
if (res.statusCode === 200 && isWechatLoginSuccess(res.data) && userData) {
|
||||
applyWechatLoginData(userData, page);
|
||||
resolve(true);
|
||||
} else {
|
||||
const msg = res.data?.msg || '登录失败';
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
reject(new Error(msg));
|
||||
}
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message || '登录失败', icon: 'none' });
|
||||
reject(error);
|
||||
} finally {
|
||||
if (silent) wx.hideLoading();
|
||||
}
|
||||
});
|
||||
|
||||
// 存储全局登录 Promise,供 request.js 感知
|
||||
app.globalData.loginPromise = loginPromise;
|
||||
// 无论成功或失败,完成后清空标记
|
||||
loginPromise.finally(() => {
|
||||
app.globalData.loginPromise = null;
|
||||
});
|
||||
|
||||
return loginPromise;
|
||||
}
|
||||
152
utils/message-sender.js
Normal file
152
utils/message-sender.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// utils/message-sender.js
|
||||
const app = getApp();
|
||||
import { formatDate } from '../static/lib/utils';
|
||||
import request from './request.js';
|
||||
import { getLocalImUserId } from './im-user.js';
|
||||
import { resolveAvatarUrl } from './avatar.js';
|
||||
|
||||
function sendGroupMessage(options) {
|
||||
const { msgData, onSuccess, onSendStart } = options;
|
||||
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 toData = {
|
||||
name: groupName || '订单群聊',
|
||||
avatar: groupAvatar || currentUser.avatar
|
||||
};
|
||||
if (orderId) toData.orderId = orderId;
|
||||
toData.isCross = isCross || 0;
|
||||
|
||||
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 identityType = role === 'dashou' ? 'dashou' : (role === 'shangjia' ? 'shangjia' : 'boss');
|
||||
const senderId = getLocalImUserId(app) || ('Boss' + uid);
|
||||
|
||||
const params = {
|
||||
orderId: orderId,
|
||||
groupId: groupId,
|
||||
identityType: identityType, // 告诉后端当前是什么身份
|
||||
senderId: senderId,
|
||||
senderName: currentUser.name || ('用户' + uid),
|
||||
senderAvatar: resolveAvatarUrl(currentUser.avatar, app),
|
||||
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 };
|
||||
165
utils/phone-auth.js
Normal file
165
utils/phone-auth.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 手机号强制认证 - 完全自包含逻辑模块
|
||||
*
|
||||
* 特点:
|
||||
* 1. 不引用项目中的 request.js / upload.js,避免其顶层 getApp() 带来的加载顺序错误
|
||||
* 2. 所有请求直接使用 wx.request / wx.uploadFile,基础 URL 写死(与 app.globalData.apiBaseUrl 保持一致)
|
||||
* 3. check() 内部捕获所有异常,永远返回 false 或 true,不会抛出错误
|
||||
* 4. submit() 抛出错误由认证页面自己处理,不影响主流程
|
||||
*
|
||||
* 用法:
|
||||
* app.js 中:
|
||||
* try {
|
||||
* const { check } = require('./utils/phone-auth');
|
||||
* if (await check()) { wx.reLaunch(...); return; }
|
||||
* } catch(e) {}
|
||||
*
|
||||
* 认证页面中:
|
||||
* const { submit } = require('../../utils/phone-auth');
|
||||
* await submit(phoneCode, avatarPath);
|
||||
*/
|
||||
|
||||
// ==================== 基础配置 ====================
|
||||
const API_BASE = 'https://www.abas.asia/hqhd'; // 与你 app.js 里的 apiBaseUrl 一致
|
||||
const AUTH_CHECK_URL = '/peizhi/yhbdsjh'; // 检查是否需要认证
|
||||
const AUTH_SUBMIT_URL = '/yonghu/xiugai'; // 提交认证(复用修改信息接口)
|
||||
|
||||
// ==================== 内部封装:wx.request ====================
|
||||
function innerRequest(url, data = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token'); // 运行时获取 token,不提前取
|
||||
const header = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (token) {
|
||||
header['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
|
||||
wx.request({
|
||||
url: API_BASE + url,
|
||||
method: 'POST',
|
||||
data,
|
||||
header,
|
||||
success: (res) => {
|
||||
// 正常响应直接 resolve 整个响应对象,让上层自己解析
|
||||
resolve(res);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 内部封装:wx.uploadFile ====================
|
||||
function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token');
|
||||
if (!token) {
|
||||
reject(new Error('未登录'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 先验证文件是否存在
|
||||
wx.getFileInfo({
|
||||
filePath,
|
||||
success: () => {
|
||||
wx.uploadFile({
|
||||
url: API_BASE + url,
|
||||
filePath,
|
||||
name: fileName,
|
||||
formData,
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
success: (res) => {
|
||||
// ✅ 修复:wx.uploadFile 成功时 statusCode 为 200
|
||||
if (res.statusCode === 200) {
|
||||
try {
|
||||
const data = JSON.parse(res.data);
|
||||
resolve(data); // 直接返回解析后的对象,方便下游使用
|
||||
} catch (e) {
|
||||
reject(new Error('解析响应数据失败'));
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
wx.removeStorageSync('token');
|
||||
reject(new Error('认证失败'));
|
||||
} else {
|
||||
reject(new Error(`上传失败,状态码:${res.statusCode}`));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error('网络请求失败'));
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
reject(new Error('文件不存在或无法访问'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 公开方法 ====================
|
||||
|
||||
/**
|
||||
* 检查是否需要手机号认证
|
||||
* - 无 token 或 任何异常 → 返回 false
|
||||
* - 后端明确 need_auth: true → 返回 true
|
||||
*/
|
||||
async function check() {
|
||||
const token = wx.getStorageSync('token');
|
||||
if (!token) return false;
|
||||
|
||||
try {
|
||||
const res = await innerRequest(AUTH_CHECK_URL, {});
|
||||
// 假设后端返回格式:{ data: { need_auth: true/false } } 或直接在顶层
|
||||
const body = res.data;
|
||||
return body && body.need_auth === true;
|
||||
} catch (e) {
|
||||
// 任何错误(网络、后端异常)都吞掉,不影响主流程
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交认证(手机号 code + 头像文件)
|
||||
* @param {string} phoneCode 微信 getPhoneNumber 返回的 code
|
||||
* @param {string} avatarPath 头像临时路径
|
||||
* @returns {Promise<object>} 后端返回的完整数据
|
||||
*/
|
||||
async function submit(phoneCode, avatarPath) {
|
||||
if (!phoneCode || !avatarPath) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 将手机号 code 放入 formData
|
||||
const formData = {
|
||||
shoujihao_code: phoneCode
|
||||
};
|
||||
|
||||
// 调用上传
|
||||
const data = await innerUpload(AUTH_SUBMIT_URL, avatarPath, formData);
|
||||
|
||||
// ✅ 修复:正确判断后端返回的 code 字段,后端成功时 code === 0
|
||||
if (data && data.code === 0) {
|
||||
// data.data 包含 { touxiang: '相对路径', phone: '...' }
|
||||
const respData = data.data || data;
|
||||
|
||||
// 存储手机号
|
||||
if (respData.phone) {
|
||||
wx.setStorageSync('phone', respData.phone);
|
||||
}
|
||||
|
||||
// ✅ 修复:存储头像相对路径,与 xiugai 页面处理方式一致
|
||||
if (respData.touxiang) {
|
||||
wx.setStorageSync('touxiang', respData.touxiang);
|
||||
}
|
||||
|
||||
return respData;
|
||||
} else {
|
||||
throw new Error((data && data.msg) || '认证失败');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { check, submit };
|
||||
105
utils/primary-role.js
Normal file
105
utils/primary-role.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/** 三端固定:normal | dashou | shangjia,认证后不可回退 */
|
||||
|
||||
const VALID = ['normal', 'dashou', 'shangjia'];
|
||||
const LEGACY_CENTER_ROLES = ['guanshi', 'zuzhang', 'kaoheguan'];
|
||||
|
||||
/** 旧版管事/组长/考核官主端标识 → 统一回落打手 Tab */
|
||||
export function migrateLegacyCenterRole(app) {
|
||||
app = app || getApp();
|
||||
const cur = wx.getStorageSync('currentRole') || app.globalData.currentRole || '';
|
||||
const pri = wx.getStorageSync('primaryRole') || '';
|
||||
const isLegacy = LEGACY_CENTER_ROLES.includes(cur) || LEGACY_CENTER_ROLES.includes(pri);
|
||||
if (!isLegacy) return false;
|
||||
|
||||
const hasDashouSide =
|
||||
isRoleStatusActive(wx.getStorageSync('dashoustatus'))
|
||||
|| isRoleStatusActive(wx.getStorageSync('guanshistatus'))
|
||||
|| isRoleStatusActive(wx.getStorageSync('zuzhangstatus'))
|
||||
|| isRoleStatusActive(wx.getStorageSync('kaoheguanstatus'));
|
||||
|
||||
if (hasDashouSide) {
|
||||
lockPrimaryRole('dashou', app);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isRoleStatusActive(status) {
|
||||
return Number(status) === 1;
|
||||
}
|
||||
|
||||
/** 读取主端(兼容旧 currentRole / 管事组长落地页) */
|
||||
export function getPrimaryRole(app) {
|
||||
app = app || getApp();
|
||||
const stored = wx.getStorageSync('primaryRole');
|
||||
if (VALID.includes(stored)) {
|
||||
return stored;
|
||||
}
|
||||
const legacy = wx.getStorageSync('currentRole') || app.globalData.currentRole || 'normal';
|
||||
if (legacy === 'dashou' || legacy === 'shangjia') {
|
||||
return legacy;
|
||||
}
|
||||
if (legacy === 'guanshi' || legacy === 'zuzhang' || legacy === 'kaoheguan') {
|
||||
if (isRoleStatusActive(wx.getStorageSync('dashoustatus'))) {
|
||||
return 'dashou';
|
||||
}
|
||||
if (isRoleStatusActive(wx.getStorageSync('shangjiastatus'))) {
|
||||
return 'shangjia';
|
||||
}
|
||||
}
|
||||
if (isRoleStatusActive(wx.getStorageSync('dashoustatus'))) {
|
||||
return 'dashou';
|
||||
}
|
||||
if (isRoleStatusActive(wx.getStorageSync('shangjiastatus'))) {
|
||||
return 'shangjia';
|
||||
}
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/** 锁定主端并同步 TabBar 角色 */
|
||||
export function lockPrimaryRole(role, app) {
|
||||
if (!VALID.includes(role)) return;
|
||||
app = app || getApp();
|
||||
wx.setStorageSync('primaryRole', role);
|
||||
wx.setStorageSync('currentRole', role);
|
||||
app.globalData.primaryRole = role;
|
||||
app.globalData.currentRole = role;
|
||||
if (app.emitEvent) {
|
||||
app.emitEvent('currentRoleChanged', { role });
|
||||
}
|
||||
}
|
||||
|
||||
/** TabBar / IM 使用的有效角色(仅三端) */
|
||||
export function getTabRole(app) {
|
||||
return getPrimaryRole(app);
|
||||
}
|
||||
|
||||
export function clearPrimaryRole(app) {
|
||||
app = app || getApp();
|
||||
wx.removeStorageSync('primaryRole');
|
||||
lockPrimaryRole('normal', app);
|
||||
}
|
||||
|
||||
export const PRIMARY_DEFAULT_PAGES = {
|
||||
normal: '/pages/wode/wode',
|
||||
dashou: '/pages/jiedan/jiedan',
|
||||
shangjia: '/pages/sjdingdan/sjdingdan',
|
||||
};
|
||||
|
||||
/** 锁定主端并跳转到该端默认首页,同时建立 IM 长连接 */
|
||||
export function enterLockedRole(role, app) {
|
||||
if (!VALID.includes(role) || role === 'normal') return false;
|
||||
app = app || getApp();
|
||||
lockPrimaryRole(role, app);
|
||||
wx.reLaunch({ url: PRIMARY_DEFAULT_PAGES[role] });
|
||||
setTimeout(() => {
|
||||
if (app.switchRoleAndReconnect) {
|
||||
app.switchRoleAndReconnect(role).catch(() => {
|
||||
if (app.connectForCurrentRole) app.connectForCurrentRole();
|
||||
});
|
||||
} else if (app.connectForCurrentRole) {
|
||||
app.connectForCurrentRole();
|
||||
}
|
||||
}, 600);
|
||||
return true;
|
||||
}
|
||||
74
utils/request.js
Normal file
74
utils/request.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// utils/request.js
|
||||
const app = getApp();
|
||||
|
||||
function request(options) {
|
||||
const token = wx.getStorageSync('token');
|
||||
|
||||
// 🔥 没有 token 时不发送请求,直接返回一个“空响应”对象,不影响后续代码
|
||||
if (!token) {
|
||||
// 返回一个模拟的响应,状态码为 401,但不会触发任何弹窗或跳转
|
||||
return Promise.resolve({
|
||||
statusCode: 401,
|
||||
data: { code: 401, msg: '未登录' },
|
||||
errMsg: 'request:ok'
|
||||
});
|
||||
}
|
||||
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.header,
|
||||
};
|
||||
if (token) {
|
||||
header['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: app.globalData.apiBaseUrl + options.url,
|
||||
method: options.method,
|
||||
data: options.data || {},
|
||||
header,
|
||||
success: async (res) => {
|
||||
if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
// 注意:这里的弹窗逻辑只会在有 token 且 token 失效时触发
|
||||
// 无 token 的情况已经在上面直接返回,不会走到这里
|
||||
if (app.globalData.loginPromise) {
|
||||
try {
|
||||
await app.globalData.loginPromise;
|
||||
const retryRes = await request(options);
|
||||
resolve(retryRes);
|
||||
} catch (err) {
|
||||
wx.showModal({
|
||||
content: '操作失败,请先登录',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
wx.removeStorageSync('token');
|
||||
wx.switchTab({ url: '/pages/wode/wode' });
|
||||
}
|
||||
},
|
||||
});
|
||||
resolve(res);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
content: '操作失败,请先登录',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
wx.removeStorageSync('token');
|
||||
wx.reLaunch({ url: '/pages/wode/wode' });
|
||||
}
|
||||
},
|
||||
});
|
||||
resolve(res);
|
||||
return;
|
||||
}
|
||||
resolve(res);
|
||||
},
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default request;
|
||||
255
utils/role-tab-bar.js
Normal file
255
utils/role-tab-bar.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import { clearPrimaryRole } from './primary-role';
|
||||
import { getLocalImUserId } from './im-user.js';
|
||||
export function isWechatLoginSuccess(body) {
|
||||
if (!body) return false;
|
||||
const code = Number(body.code);
|
||||
return code === 0 || code === 200;
|
||||
}
|
||||
|
||||
const SESSION_STATUS_KEYS = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus'];
|
||||
|
||||
/** 读取 token(Storage 优先,globalData 兜底并回写 Storage) */
|
||||
export function getSessionToken(app) {
|
||||
app = app || getApp();
|
||||
let token = wx.getStorageSync('token') || app.globalData.token || '';
|
||||
if (token && !wx.getStorageSync('token')) {
|
||||
wx.setStorageSync('token', token);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
export function isUserLoggedIn(app) {
|
||||
return !!getSessionToken(app);
|
||||
}
|
||||
|
||||
/** 从 Storage / globalData 汇总展示用用户信息(兼容其他端注册后 nick 未落盘) */
|
||||
export function getUserProfileForDisplay(app) {
|
||||
app = app || getApp();
|
||||
const token = getSessionToken(app);
|
||||
const uid = wx.getStorageSync('uid') || app.globalData.uid || '';
|
||||
let nick = wx.getStorageSync('nicheng') || app.globalData.nicheng || '';
|
||||
if (!nick && app.globalData.dashouNicheng) {
|
||||
nick = app.globalData.dashouNicheng;
|
||||
}
|
||||
const tx = wx.getStorageSync('touxiang') || '';
|
||||
const def = app.globalData.morentouxiang || '';
|
||||
const oss = app.globalData.ossImageUrl || '';
|
||||
let avatarUrl = '';
|
||||
if (tx) {
|
||||
avatarUrl = tx.startsWith('http') ? tx : oss + tx;
|
||||
} else if (def) {
|
||||
avatarUrl = def.startsWith('http') ? def : oss + def;
|
||||
}
|
||||
return {
|
||||
nick,
|
||||
uid,
|
||||
avatarUrl,
|
||||
isLoggedIn: !!token,
|
||||
};
|
||||
}
|
||||
|
||||
/** 其他端登录/注册成功后,把 globalData 里已有字段补写回 Storage */
|
||||
export function backfillUserProfileCache(app) {
|
||||
app = app || getApp();
|
||||
if (!isUserLoggedIn()) return;
|
||||
const profile = getUserProfileForDisplay(app);
|
||||
if (profile.nick && !wx.getStorageSync('nicheng')) {
|
||||
wx.setStorageSync('nicheng', profile.nick);
|
||||
app.globalData.nicheng = profile.nick;
|
||||
}
|
||||
if (profile.uid && !wx.getStorageSync('uid')) {
|
||||
wx.setStorageSync('uid', profile.uid);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyWechatLoginData(data, page) {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
const app = getApp();
|
||||
syncProfileFields(data);
|
||||
if (data.token) {
|
||||
wx.setStorageSync('token', data.token);
|
||||
}
|
||||
syncRoleStatuses(data);
|
||||
syncGroupFields(data);
|
||||
if (data.zuzhangstatus !== undefined) {
|
||||
wx.setStorageSync('zuzhangstatus', data.zuzhangstatus);
|
||||
app.globalData.zuzhangstatus = data.zuzhangstatus;
|
||||
}
|
||||
if (data.kaoheguanstatus !== undefined) {
|
||||
wx.setStorageSync('kaoheguanstatus', data.kaoheguanstatus);
|
||||
app.globalData.kaoheguanstatus = data.kaoheguanstatus;
|
||||
}
|
||||
if (data.dingdantiaoshu) {
|
||||
app.globalData.dingdanTiaoshu = {
|
||||
daifuwu: data.dingdantiaoshu.daifuwu || 0,
|
||||
fuwuzhong: data.dingdantiaoshu.fuwuzhong || 0,
|
||||
yiwancheng: data.dingdantiaoshu.yiwancheng || 0,
|
||||
yituikuan: data.dingdantiaoshu.yituikuan || 0,
|
||||
};
|
||||
}
|
||||
if (data.isJinpai !== undefined) {
|
||||
wx.setStorageSync('isJinpai', Number(data.isJinpai));
|
||||
}
|
||||
|
||||
const targetPage = page || _getCurrentPage();
|
||||
refreshTabBar(targetPage);
|
||||
}
|
||||
|
||||
function _getCurrentPage() {
|
||||
const pages = getCurrentPages();
|
||||
return pages.length ? pages[pages.length - 1] : null;
|
||||
}
|
||||
|
||||
export function isRoleStatusActive(status) {
|
||||
return Number(status) === 1;
|
||||
}
|
||||
|
||||
export function isCenterPageActive(page, dataFlag, statusKey) {
|
||||
return page.data[dataFlag] || isRoleStatusActive(wx.getStorageSync(statusKey));
|
||||
}
|
||||
|
||||
export function syncRoleStatuses(data) {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
const app = getApp();
|
||||
const roleFields = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus'];
|
||||
roleFields.forEach((field) => {
|
||||
if (Object.prototype.hasOwnProperty.call(data, field)) {
|
||||
const val = Number(data[field]);
|
||||
const normalized = Number.isNaN(val) ? 0 : val;
|
||||
wx.setStorageSync(field, normalized);
|
||||
app.globalData[field] = normalized;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function syncProfileFields(data) {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
const app = getApp();
|
||||
['nicheng', 'uid', 'touxiang', 'token'].forEach((field) => {
|
||||
if (data[field] !== undefined && data[field] !== null) {
|
||||
wx.setStorageSync(field, data[field]);
|
||||
app.globalData[field] = data[field];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 清除缓存:清空登录与身份,回到点单端「我的」 */
|
||||
export function clearCacheAndEnterNormal(page) {
|
||||
resetGuestSession(page, { skipPageReset: true });
|
||||
wx.reLaunch({ url: '/pages/wode/wode' });
|
||||
}
|
||||
|
||||
/** 清除缓存并重置为点单端(不跳转,供当前已在点单端时使用) */
|
||||
export function resetGuestSession(page, options = {}) {
|
||||
const app = getApp();
|
||||
const configKey = app.CONFIG_CACHE_KEY || 'app_dynamic_config';
|
||||
let configCache = null;
|
||||
try {
|
||||
configCache = wx.getStorageSync(configKey);
|
||||
} catch (e) {}
|
||||
|
||||
wx.clearStorageSync();
|
||||
|
||||
if (configCache && typeof configCache === 'object') {
|
||||
wx.setStorageSync(configKey, configCache);
|
||||
}
|
||||
|
||||
clearPrimaryRole(app);
|
||||
app.globalData.token = '';
|
||||
app.globalData.nicheng = '';
|
||||
app.globalData.uid = '';
|
||||
app.globalData.loginPromise = null;
|
||||
app.globalData.currentUser = null;
|
||||
app.globalData.dashouNicheng = '';
|
||||
app.globalData.dingdanTiaoshu = { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 };
|
||||
app.globalData.dashouqun = '';
|
||||
app.globalData.dashouqunid = '';
|
||||
app.globalData.guanshiqun = '';
|
||||
app.globalData.guanshiqunid = '';
|
||||
SESSION_STATUS_KEYS.forEach((key) => {
|
||||
app.globalData[key] = 0;
|
||||
});
|
||||
app.globalData.shangpinleixing = [];
|
||||
app.globalData.shangpinzhuanqu = [];
|
||||
app.globalData.shangpinliebiao = [];
|
||||
|
||||
app.emitEvent('currentRoleChanged', { role: 'normal' });
|
||||
refreshTabBar(page);
|
||||
|
||||
if (!options.skipPageReset && page && page.setData) {
|
||||
page.setData({
|
||||
avatarUrl: '',
|
||||
userNicheng: '',
|
||||
userUid: '',
|
||||
showLoginBtn: true,
|
||||
orderCounts: { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 },
|
||||
dashouCertified: false,
|
||||
shangjiaCertified: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function syncGroupFields(data) {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
const app = getApp();
|
||||
['dashouqun', 'dashouqunid', 'guanshiqun', 'guanshiqunid'].forEach((field) => {
|
||||
if (data[field] !== undefined) {
|
||||
wx.setStorageSync(field, data[field]);
|
||||
app.globalData[field] = data[field];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function refreshTabBar(page) {
|
||||
const tryRefresh = () => {
|
||||
const tabBar = page.getTabBar && page.getTabBar();
|
||||
if (tabBar && tabBar.refresh) tabBar.refresh();
|
||||
};
|
||||
tryRefresh();
|
||||
[50, 150, 300, 500, 800].forEach((delay) => setTimeout(tryRefresh, delay));
|
||||
}
|
||||
|
||||
export function setRoleAndRefreshTabBar(page, role) {
|
||||
const app = getApp();
|
||||
if (app.setCurrentRole) {
|
||||
app.setCurrentRole(role);
|
||||
} else {
|
||||
app.globalData.currentRole = role;
|
||||
wx.setStorageSync('currentRole', role);
|
||||
app.emitEvent('currentRoleChanged', { role });
|
||||
}
|
||||
refreshTabBar(page);
|
||||
}
|
||||
|
||||
export function reconnectForRole(role) {
|
||||
const app = getApp();
|
||||
if (!wx.getStorageSync('uid')) return;
|
||||
|
||||
const targetUserId = getLocalImUserId(app);
|
||||
if (!targetUserId) return;
|
||||
|
||||
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
|
||||
const currentUserId = wx.goEasy?.im?.userId || '';
|
||||
const connected = status === 'connected' || status === 'reconnected';
|
||||
|
||||
if (connected && currentUserId === targetUserId) {
|
||||
if (app.loadConversations) app.loadConversations();
|
||||
if (app.ensureConnection) app.ensureConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (app.switchRoleAndReconnect) {
|
||||
app.switchRoleAndReconnect(role).catch(() => {
|
||||
if (app.ensureConnection) app.ensureConnection();
|
||||
});
|
||||
} else if (app.connectForCurrentRole) {
|
||||
app.connectForCurrentRole();
|
||||
} else if (app.ensureConnection) {
|
||||
app.ensureConnection();
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureRoleOnCenterPage(page, role) {
|
||||
setRoleAndRefreshTabBar(page, role);
|
||||
reconnectForRole(role);
|
||||
}
|
||||
73
utils/shop.js
Normal file
73
utils/shop.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// utils/shop.js
|
||||
import { ensureLogin } from './login';
|
||||
import request from './request'; // 封装后的请求对象,路径请按实际调整
|
||||
|
||||
/**
|
||||
* 获取当前用户绑定的店铺商品数据
|
||||
* 自动确保登录,然后请求 /shangdian/hqsp 接口
|
||||
* 注意:本方法不涉及公告和轮播图,公告轮播图由页面独立调用原接口
|
||||
* @returns {Promise<Object>} 返回后端商品数据(包含 shangpinleixing, shangpinzhuanqu, shangpinliebiao)
|
||||
*/
|
||||
export async function fetchShopGoods() {
|
||||
// 1. 确保已登录
|
||||
await ensureLogin();
|
||||
|
||||
// 2. 请求商品数据
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
url: '/shangdian/hqsp',
|
||||
method: 'POST',
|
||||
data: {} // 无需传参,后端从 token 解析用户及绑定店铺
|
||||
})
|
||||
.then(res => {
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
const data = res.data.data;
|
||||
// 后端返回的商品数据应当包含 shangpinleixing, shangpinzhuanqu, shangpinliebiao
|
||||
resolve(data);
|
||||
} else {
|
||||
const msg = res.data?.msg || '获取商品失败';
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
reject(new Error(msg));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('获取商品失败:', err);
|
||||
wx.showToast({ title: '网络错误,请重试', icon: 'none' });
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定店铺(扫码时调用)
|
||||
* @param {string} dianpuId 店铺ID(从二维码解析)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function bindShop(dianpuId) {
|
||||
if (!dianpuId) return;
|
||||
|
||||
await ensureLogin(); // 确保登录
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
url: '/shangdian/bdsd',
|
||||
method: 'POST',
|
||||
data: { dianpu_id: dianpuId }
|
||||
})
|
||||
.then(res => {
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
// 绑定成功,后端已记录关系
|
||||
resolve();
|
||||
} else {
|
||||
const msg = res.data?.msg || '店铺绑定失败';
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
reject(new Error(msg));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('绑定店铺失败:', err);
|
||||
wx.showToast({ title: '网络错误,绑定失败', icon: 'none' });
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
95
utils/upload.js
Normal file
95
utils/upload.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// utils/upload.js
|
||||
import request from './request.js'
|
||||
|
||||
const app = getApp()
|
||||
|
||||
/**
|
||||
* 统一的异步上传方法
|
||||
* @param {object} options 配置项
|
||||
* @returns {Promise} Promise对象
|
||||
*/
|
||||
const upload = async (options) => {
|
||||
const {
|
||||
url,
|
||||
formData = {},
|
||||
filePath = null,
|
||||
fileName = 'file'
|
||||
} = options
|
||||
|
||||
// 获取token
|
||||
const token = wx.getStorageSync('token')
|
||||
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
// 拼接完整URL
|
||||
const fullUrl = `${app.globalData.apiBaseUrl}${url}`
|
||||
|
||||
if (filePath) {
|
||||
// 有文件上传,使用wx.uploadFile
|
||||
return new Promise((resolve, reject) => {
|
||||
// 先验证文件是否存在
|
||||
wx.getFileInfo({
|
||||
filePath,
|
||||
success: () => {
|
||||
// 文件存在,开始上传
|
||||
wx.uploadFile({
|
||||
url: fullUrl,
|
||||
filePath,
|
||||
name: fileName,
|
||||
formData,
|
||||
header: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
success: (res) => {
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
data
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('解析JSON失败:', e, '原始数据:', res.data)
|
||||
reject(new Error('解析响应数据失败'))
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
// token过期或无效
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
reject(new Error('认证失败'))
|
||||
} else {
|
||||
console.error('上传失败,状态码:', res.statusCode, '响应:', res)
|
||||
reject(new Error(`上传失败,状态码:${res.statusCode}`))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('wx.uploadFile失败:', err)
|
||||
reject(new Error('网络请求失败'))
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('文件不存在或无法访问:', err)
|
||||
reject(new Error('文件不存在或无法访问'))
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 没有文件,使用封装的request
|
||||
// 注意:这里应该使用传入的url,而不是硬编码
|
||||
return request({
|
||||
url, // 使用传入的url
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default upload
|
||||
215
utils/xiaoxilj.js
Normal file
215
utils/xiaoxilj.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 连接管理模块 - xiaoxilj.js (根治发送失败:跳转前确保群组已订阅)
|
||||
* 从详情页跳转订单群聊时,提前订阅群组,避免发送消息时订阅未完成而失败
|
||||
*/
|
||||
const app = getApp();
|
||||
|
||||
function waitForConnection(expectedUserId, timeout = 10000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
app.off('connectionChanged', handler);
|
||||
reject(new Error('连接超时'));
|
||||
}, timeout);
|
||||
|
||||
const handler = (event) => {
|
||||
if (event.status === 'connected') {
|
||||
if (event.userId === 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();
|
||||
clearTimeout(timer);
|
||||
app.off('connectionChanged', handler);
|
||||
reject(new Error('连接失败'));
|
||||
}
|
||||
};
|
||||
app.on('connectionChanged', handler);
|
||||
});
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
wx.removeStorageSync('savedGoEasyConnection');
|
||||
wx.removeStorageSync('goEasyUserId');
|
||||
wx.removeStorageSync('currentGoEasyIdentity');
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async connectToGroupChat(params) {
|
||||
const { identityType, userId, orderId, groupName, groupAvatar, isCross } = params;
|
||||
if (!identityType || !userId || !orderId) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
});
|
||||
});
|
||||
realGroupId = result;
|
||||
} catch (err) {
|
||||
console.error('获取会话列表失败:', err);
|
||||
wx.showToast({ title: '获取群聊信息失败', icon: 'none' });
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!realGroupId) {
|
||||
wx.showToast({ title: '暂未创建群聊', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 🔥 关键:主动订阅群组并等待成功,确保发送消息时订阅已完成
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
wx.goEasy.im.subscribeGroup({
|
||||
groupIds: [realGroupId],
|
||||
onSuccess: () => {
|
||||
resolve();
|
||||
},
|
||||
onFailed: (error) => {
|
||||
console.error('订阅群组失败:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
wx.showToast({ title: '订阅群组失败', 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();
|
||||
Reference in New Issue
Block a user