第一次提交:微信小程序前端最新完整代码

This commit is contained in:
XingQue
2026-06-12 21:55:06 +08:00
commit 84be8489b4
330 changed files with 74263 additions and 0 deletions

38
utils/avatar.js Normal file
View 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
View 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 };

View 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

File diff suppressed because one or more lines are too long

78
utils/im-user.js Normal file
View 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);
});
}

View 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
View 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;

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'];
/** 读取 tokenStorage 优先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
View 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
View 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
View 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();