restore: 恢复橙色逍遥梦UI版本(2ea2860)到主分支

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-09 00:17:03 +08:00
parent 267de7c780
commit 566aeaa3b7
228 changed files with 22076 additions and 11864 deletions

View File

@@ -1,4 +1,4 @@
/** 统一头像 URL无有效头像时始终返回 ossImageUrl + morentouxiang */
/** 统一头像 URL无有效头像时始终返回 ossImageUrl + morentouxiang(后端配置) */
function isBlankPath(p) {
if (p == null) return true;
@@ -7,28 +7,71 @@ function isBlankPath(p) {
return !s || s === 'null' || s === 'undefined' || s === 'none';
}
function readConfigFallback(app) {
if (!app || typeof app.readConfigFromStorage !== 'function') return null;
try {
return app.readConfigFromStorage();
} catch (e) {
return null;
}
}
function getOssBase(app) {
let oss = app.globalData?.ossImageUrl || '';
if (!oss) {
const cfg = readConfigFallback(app);
oss = cfg?.cos?.ossImageUrl || '';
}
return oss;
}
function getDefaultRelative(app) {
let def = app.globalData?.morentouxiang || '';
if (!def) {
const cfg = readConfigFallback(app);
def = cfg?.otherConfig?.morentouxiang || '';
}
return def || 'avatar/default.jpg';
}
export function getDefaultAvatarUrl(app) {
return resolveAvatarUrl('', app);
}
export function resolveAvatarUrl(rawPath, app) {
app = app || getApp();
const oss = app.globalData.ossImageUrl || '';
const def = app.globalData.morentouxiang || 'avatar/default.jpg';
const oss = getOssBase(app);
const def = getDefaultRelative(app);
if (!isBlankPath(rawPath)) {
if (rawPath.startsWith('http://') || rawPath.startsWith('https://')) {
return rawPath;
}
const path = rawPath.startsWith('/') ? rawPath : '/' + rawPath;
return oss + path;
const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
if (oss) return oss + path;
return path;
}
if (def.startsWith('http://') || def.startsWith('https://')) {
return def;
}
const defPath = def.startsWith('/') ? def : '/' + def;
return oss + defPath;
const defPath = def.startsWith('/') ? def : `/${def}`;
if (oss) return oss + defPath;
// 配置未就绪时仍返回相对路径,由 binderror 回退;有 oss 时必须拼完整 URL
return defPath;
}
/** 是否已是可加载的完整头像 URL */
export function isUsableAvatarUrl(url) {
return url && (url.startsWith('http://') || url.startsWith('https://'));
}
export function ensureAvatarUrl(rawPath, app) {
app = app || getApp();
const resolved = resolveAvatarUrl(rawPath, app);
if (isUsableAvatarUrl(resolved)) return resolved;
const def = getDefaultAvatarUrl(app);
return isUsableAvatarUrl(def) ? def : resolved;
}
export function resolveAvatarFromStorage(app) {
@@ -36,3 +79,19 @@ export function resolveAvatarFromStorage(app) {
const tx = wx.getStorageSync('touxiang') || '';
return resolveAvatarUrl(tx, app);
}
/** 配置加载后刷新页面头像(聊天页等订阅 configApplied */
export function subscribeConfigAvatarRefresh(page, refreshFn) {
const app = getApp();
if (!app || typeof refreshFn !== 'function') return null;
const handler = () => refreshFn();
if (app.on) app.on('configApplied', handler);
return () => { if (app.off) app.off('configApplied', handler); };
}
/** 消息列表/聊天页 image 加载失败时回退默认头像 */
export function onAvatarImageError(page, dataKey, app) {
if (!page || !dataKey) return;
const def = getDefaultAvatarUrl(app || getApp());
page.setData({ [dataKey]: def });
}

View File

@@ -272,6 +272,10 @@ const BASE_METHODS = {
const result = {};
Object.keys(urlMap).forEach((key) => {
const path = urlMap[key];
if (!path || typeof path !== 'string') {
result[key] = '';
return;
}
result[key] = path.startsWith('http') || path.startsWith('/') ? path : ossImageUrl + path;
});
return result;

File diff suppressed because it is too large Load Diff

121
utils/chat-history.js Normal file
View File

@@ -0,0 +1,121 @@
/**
* 聊天历史消息加载(私聊 / 群聊 / 客服共用)
* - 排除本地/欢迎语等合成消息的 timestamp避免下拉加载错位
* - 统一 IM 连接等待与 history API 封装
*/
const SYNTHETIC_MSG_ID = /^(local-|img-|order-|welcome-|auto-reply-)/;
export function isHistorySyntheticMessage(msg) {
return !!(msg && msg.messageId && SYNTHETIC_MSG_ID.test(String(msg.messageId)));
}
/** 取当前列表里最早一条「真实」服务端消息的时间戳,供 GoEasy history 分页 */
export function getOldestHistoryTimestamp(messages, fallback = null) {
if (!Array.isArray(messages)) return fallback;
let oldest = null;
for (const m of messages) {
if (!m || m.timestamp == null) continue;
if (isHistorySyntheticMessage(m)) continue;
if (oldest == null || m.timestamp < oldest) oldest = m.timestamp;
}
return oldest != null ? oldest : fallback;
}
/** 等待 IM 连接就绪(优先 ensureImForRole避免未连上就拉历史 */
export async function waitChatImReady(app, timeoutMs = 12000) {
const role = app?.globalData?.currentRole || 'normal';
if (app?.ensureImForRole) {
try {
await app.ensureImForRole(role);
if (isImConnected()) return true;
} catch (e) {
console.warn('[IM] ensureImForRole failed', e);
}
}
if (app?.ensureConnection) app.ensureConnection();
return waitImConnected(app, timeoutMs);
}
export function isImConnected() {
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
return (status === 'connected' || status === 'reconnected') && !!wx.goEasy?.im;
}
/** 等待 IM 连接就绪(下拉加载历史前必须先连上) */
export function waitImConnected(app, timeoutMs = 8000) {
return new Promise((resolve) => {
if (isImConnected()) {
resolve(true);
return;
}
if (app?.ensureConnection) app.ensureConnection();
const start = Date.now();
const timer = setInterval(() => {
if (isImConnected()) {
clearInterval(timer);
resolve(true);
} else if (Date.now() - start >= timeoutMs) {
clearInterval(timer);
resolve(false);
}
}, 200);
});
}
export function fetchHistoryMessages({ scene, id, lastTimestamp, limit = 20 }) {
return new Promise((resolve, reject) => {
if (!wx.goEasy?.im?.history) {
reject(new Error('IM未初始化'));
return;
}
if (!id) {
reject(new Error('会话ID为空'));
return;
}
wx.goEasy.im.history({
type: scene,
id: String(id),
lastTimestamp: lastTimestamp ?? null,
limit,
onSuccess: (res) => resolve(res?.content || []),
onFailed: (err) => reject(err || new Error('加载历史消息失败')),
});
});
}
/**
* 合并一页历史消息
* @returns {{ messages, hasMore, lastTimestamp }}
*/
export function mergeHistoryMessages({ incoming, existing, refresh, limit = 20, mapMessage }) {
let list = Array.isArray(incoming) ? [...incoming] : [];
list.sort((a, b) => a.timestamp - b.timestamp);
if (typeof mapMessage === 'function') {
list = list.map((m, idx) => mapMessage(m, idx, list));
}
const prev = Array.isArray(existing) ? existing : [];
let final = refresh ? list : [...list, ...prev];
if (!refresh) {
for (let i = 0; i < final.length; i += 1) {
if (i === 0) final[i].showTime = true;
else final[i].showTime = (final[i].timestamp - final[i - 1].timestamp) / 60000 > 5;
}
}
const ids = new Set();
const unique = [];
for (const m of final) {
if (!m?.messageId || ids.has(m.messageId)) continue;
ids.add(m.messageId);
unique.push(m);
}
return {
messages: unique,
hasMore: list.length >= limit,
lastTimestamp: getOldestHistoryTimestamp(unique, null),
};
}

64
utils/club-context.js Normal file
View File

@@ -0,0 +1,64 @@
/**
* 多俱乐部上下文(读取 config/club-config.js 中的固定 club_id
*/
import { CLUB_ID as CONFIG_CLUB_ID, WX_APP_ID } from '../config/club-config';
const CLUB_ID_KEY = 'club_id';
const CLUB_SCOPE_KEY = 'club_scope';
const DEFAULT_CLUB_ID = CONFIG_CLUB_ID || 'xq';
export const CLUB_API = {
wechatLogin: '/jituan/auth/wechat-login',
dashouRegister: '/jituan/auth/dashou-register',
clubList: '/jituan/club/list',
};
export function getConfiguredClubId() {
return DEFAULT_CLUB_ID;
}
export function getWxAppId() {
return WX_APP_ID || '';
}
export function getClubId(app) {
const configured = getConfiguredClubId();
const stored = wx.getStorageSync(CLUB_ID_KEY);
if (stored && stored === configured) {
return stored;
}
if (stored && stored !== configured) {
wx.removeStorageSync(CLUB_ID_KEY);
}
app = app || getApp();
if (app && app.globalData && app.globalData.clubId === configured) {
return app.globalData.clubId;
}
return configured;
}
export function setClubId(clubId, app) {
const configured = getConfiguredClubId();
const id = (clubId || configured).trim() || configured;
if (id !== configured) {
console.warn('[club] 登录返回 club_id 与工程配置不一致,以工程配置为准', id, configured);
}
const finalId = configured;
app = app || getApp();
wx.setStorageSync(CLUB_ID_KEY, finalId);
if (app && app.globalData) {
app.globalData.clubId = finalId;
}
return finalId;
}
export function getClubScope() {
return wx.getStorageSync(CLUB_SCOPE_KEY) || 'single';
}
export function buildClubHeaders(extra = {}) {
return {
...extra,
'X-Club-Id': getClubId(),
};
}

1
utils/cos-wx-sdk-v5.min.js vendored Normal file

File diff suppressed because one or more lines are too long

36
utils/dashou-profile.js Normal file
View File

@@ -0,0 +1,36 @@
/**
* 从服务端刷新打手会员/押金/积分等到 globalData抢单、考试等页共用
*/
import request from './request.js';
export async function refreshDashouMembership(app) {
app = app || getApp();
try {
const res = await request({ url: '/yonghu/dashouxinxi', method: 'POST' });
if (res && res.data && res.data.code == 200) {
const data = res.data.data || {};
const clumber = Array.isArray(data.clumber) ? data.clumber : [];
app.globalData.clumber = clumber;
if (data.yajin !== undefined) app.globalData.yajin = data.yajin;
if (data.jifen !== undefined) app.globalData.jinfen = data.jifen;
if (data.zhanghaostatus !== undefined) app.globalData.zhanghaoStatus = data.zhanghaostatus;
if (data.dashouzhuangtai !== undefined) app.globalData.dashouzhuangtai = data.dashouzhuangtai;
return clumber;
}
} catch (e) {
console.error('refreshDashouMembership failed', e);
}
return app.globalData.clumber || [];
}
/** 用户是否持有某会员类型(与抢单卡片判断一致;支持总会员 covers */
export function userHasHuiyuan(huiyuanList, huiyuanId) {
if (!huiyuanId) return true;
const list = huiyuanList || [];
const req = String(huiyuanId);
return list.some((h) => {
if (String(h.huiyuanid) === req || String(h.huiyuan_id) === req) return true;
const covers = h.included_huiyuan_ids || h.covers || [];
return Array.isArray(covers) && covers.some((id) => String(id) === req);
});
}

42
utils/display-config.js Normal file
View File

@@ -0,0 +1,42 @@
/**
* 按俱乐部拉取公告/轮播(必须带 X-Club-Id否则会落到默认 xq
*/
import { buildClubHeaders, getClubId } from './club-context';
export function isGonggaoCacheValid(app) {
const g = app.globalData || {};
const cid = getClubId(app);
return g._gonggaoClubId === cid && (g.shangpingonggao || (g.shangpinlunbo && g.shangpinlunbo.length));
}
export function applyGonggaoToGlobal(app, data) {
if (!data) return;
const g = app.globalData;
g.shangpingonggao = data.shangpingonggao || '';
g.shangpinlunbo = data.shangpinlunbo || [];
g._gonggaoClubId = getClubId(app);
}
export function fetchGonggaoLunbo(app, pageKey = 'order_pool') {
const apiBase = app.globalData && app.globalData.apiBaseUrl;
if (!apiBase) {
return Promise.resolve(null);
}
return new Promise((resolve, reject) => {
wx.request({
url: `${apiBase}/peizhi/shangpingonggao/`,
method: 'POST',
header: buildClubHeaders({ 'content-type': 'application/json' }),
data: { page_key: pageKey },
success: (res) => {
if (res.statusCode === 200 && res.data) {
applyGonggaoToGlobal(app, res.data);
resolve(res.data);
} else {
reject(new Error('shangpingonggao 请求失败'));
}
},
fail: reject,
});
});
}

25
utils/grab-order-gate.js Normal file
View File

@@ -0,0 +1,25 @@
/**
* 抢单前检查(仅点击抢单时调用)
*/
import request from './request.js';
/** 检查是否有待处理罚款(待缴纳 / 申诉中) */
export async function checkPenaltyForGrab() {
try {
const res = await request({ url: '/yonghu/cffktjhq', method: 'POST' });
if (res?.data?.code === 0) {
const fk = res.data.data?.fakuan || {};
const pending = Number(fk.daijiaona || 0) + Number(fk.shensuzhong || 0);
if (pending > 0) {
return {
blocked: true,
msg: '您有未缴纳或申诉中的罚单,请先处理后再抢单',
url: '/pages/penalty/penalty/penalty',
};
}
}
} catch (e) {
console.error('checkPenaltyForGrab failed', e);
}
return { blocked: false };
}

273
utils/group-chat.js Normal file
View File

@@ -0,0 +1,273 @@
/**
* 打手-商家/老板配对群group_Ds{打手}_Sj{商家}
*/
export const ORDER_CARD_PREFIX = '[ORDER_CARD]';
const ZHUANGTAI_MAP = {
1: '待抢单', 2: '进行中', 3: '已完成', 4: '退款中',
5: '已退款', 6: '退款失败', 7: '指定中', 8: '结算中',
9: '未支付', 14: '支付失败', 25: '已废弃',
};
export function getZhuangtaiText(zhuangtai) {
if (zhuangtai == null || zhuangtai === '') return '';
const n = Number(zhuangtai);
return ZHUANGTAI_MAP[n] || ZHUANGTAI_MAP[zhuangtai] || '';
}
export function buildDashouShangjiaGroupId(dashouUid, shangjiaUid) {
return `group_Ds${dashouUid}_Sj${shangjiaUid}`;
}
export function buildDashouBossGroupId(dashouUid, bossUid) {
return `group_Ds${dashouUid}_Boss${bossUid}`;
}
export function resolveLocalGroupId(identityType, myUid, partnerUid, fadanPingtai, orderId, isCross) {
const cross = isCross === 1 || isCross === true || String(isCross) === '1';
if (cross && orderId) {
return `group_${orderId}`;
}
if (!myUid) return orderId ? `group_${orderId}` : null;
if (!partnerUid) return orderId ? `group_${orderId}` : null;
if (identityType === 'shangjia') {
return buildDashouShangjiaGroupId(partnerUid, myUid);
}
if (identityType === 'dashou') {
if (fadanPingtai === 2 || String(fadanPingtai) === '2') {
return buildDashouShangjiaGroupId(myUid, partnerUid);
}
return buildDashouBossGroupId(myUid, partnerUid);
}
if (identityType === 'boss' || identityType === 'normal') {
if (partnerUid && myUid) {
return buildDashouBossGroupId(partnerUid, myUid);
}
return orderId ? `group_${orderId}` : null;
}
return orderId ? `group_${orderId}` : null;
}
export function isPairGroupId(groupId) {
return /^group_Ds\w+_(Sj|Boss)\w+$/.test(groupId || '');
}
export function parsePairGroupId(groupId) {
if (!groupId) return null;
let m = /^group_Ds(\w+)_Sj(\w+)$/.exec(groupId);
if (m) return { dashouId: `Ds${m[1]}`, partnerId: `Sj${m[2]}`, partnerRole: 'shangjia' };
m = /^group_Ds(\w+)_Boss(\w+)$/.exec(groupId);
if (m) return { dashouId: `Ds${m[1]}`, partnerId: `Boss${m[2]}`, partnerRole: 'boss' };
return null;
}
export function getCounterpartGoEasyId(groupId, myGoEasyId) {
const parsed = parsePairGroupId(groupId);
if (!parsed || !myGoEasyId) return null;
if (myGoEasyId === parsed.dashouId) return parsed.partnerId;
if (myGoEasyId === parsed.partnerId) return parsed.dashouId;
return null;
}
function fixAvatarUrl(avatar, defaultAvatar, ossBase) {
if (!avatar) return defaultAvatar;
if (avatar.startsWith('http')) return avatar;
return (ossBase || '') + avatar.replace(/^\//, '');
}
/** 根据群 to.data 与当前身份,解析对方/双方展示信息 */
export function applyPairMetaToConversation(item, myGoEasyId, defaultAvatar, ossBase) {
if (!item || !item.data) return;
const d = item.data;
if (d.dashouGoEasyId && d.partnerGoEasyId) {
const isDashou = myGoEasyId === d.dashouGoEasyId;
const isPartner = myGoEasyId === d.partnerGoEasyId;
d.dashouNicheng = d.dashouName || d.dashouNicheng || '';
d.partnerNicheng = d.partnerName || d.partnerNicheng || '';
d.dashouAvatar = fixAvatarUrl(d.dashouAvatar, defaultAvatar, ossBase);
d.partnerAvatar = fixAvatarUrl(d.partnerAvatar, defaultAvatar, ossBase);
if (isDashou) {
d.counterpartId = d.partnerGoEasyId;
d.counterpartYonghuid = d.partnerYonghuid || d.partnerGoEasyId.replace(/^(Sj|Boss)/, '');
d.partnerRoleLabel = d.partnerGoEasyId.startsWith('Boss') ? '老板' : '商家';
d.name = d.partnerNicheng || `${d.partnerRoleLabel}${d.counterpartYonghuid}`;
d.avatar = d.partnerAvatar || defaultAvatar;
} else if (isPartner) {
d.counterpartId = d.dashouGoEasyId;
d.counterpartYonghuid = d.dashouYonghuid || d.dashouGoEasyId.replace(/^Ds/, '');
d.name = d.dashouNicheng || `打手${d.counterpartYonghuid}`;
d.avatar = d.dashouAvatar || defaultAvatar;
} else {
const cpId = getCounterpartGoEasyId(item.groupId, myGoEasyId);
if (cpId === d.partnerGoEasyId) {
d.name = d.partnerNicheng;
d.avatar = d.partnerAvatar || defaultAvatar;
} else if (cpId === d.dashouGoEasyId) {
d.name = d.dashouNicheng;
d.avatar = d.dashouAvatar || defaultAvatar;
}
}
return;
}
const parsed = parsePairGroupId(item.groupId);
if (parsed && myGoEasyId) {
const isDashou = myGoEasyId === parsed.dashouId;
const isPartner = myGoEasyId === parsed.partnerId;
if (isDashou) {
d.counterpartId = parsed.partnerId;
d.counterpartYonghuid = parsed.partnerId.replace(/^(Sj|Boss)/, '');
d.partnerRoleLabel = parsed.partnerRole === 'boss' ? '老板' : '商家';
if (!d.name || d.name === item.groupId) {
d.name = d.partnerNicheng || `${d.partnerRoleLabel}${d.counterpartYonghuid.slice(-6)}`;
}
if (!d.avatar || d.avatar === defaultAvatar) {
d.avatar = d.partnerAvatar || defaultAvatar;
}
} else if (isPartner) {
d.counterpartId = parsed.dashouId;
d.counterpartYonghuid = parsed.dashouId.replace(/^Ds/, '');
if (!d.name || d.name === item.groupId) {
d.name = d.dashouNicheng || `打手${d.counterpartYonghuid.slice(-6)}`;
}
if (!d.avatar || d.avatar === defaultAvatar) {
d.avatar = d.dashouAvatar || defaultAvatar;
}
}
}
const counterpartId = getCounterpartGoEasyId(item.groupId, myGoEasyId);
if (counterpartId) {
d.counterpartId = counterpartId;
d.counterpartYonghuid = counterpartId.replace(/^(Ds|Sj|Boss)/, '');
if (!d.name || d.name === item.groupId) {
d.name = `用户${d.counterpartYonghuid.slice(-6)}`;
}
}
if (item.lastMessage && item.lastMessage.senderId && item.lastMessage.senderId !== myGoEasyId) {
const sd = item.lastMessage.senderData || {};
if (sd.avatar) d.avatar = fixAvatarUrl(sd.avatar, defaultAvatar, ossBase);
if (sd.name) d.name = sd.name;
}
d.avatar = fixAvatarUrl(d.avatar, defaultAvatar, ossBase);
}
export function parseOrderCardText(text) {
if (!text || !text.startsWith(ORDER_CARD_PREFIX)) return null;
try {
const data = JSON.parse(text.slice(ORDER_CARD_PREFIX.length));
return {
orderId: data.dingdan_id,
jieshao: data.jieshao || '',
jine: data.jine || '',
beizhu: data.beizhu || '',
zhuangtai: data.zhuangtai,
nicheng: data.nicheng || '',
create_time: data.create_time || '',
};
} catch (e) {
return null;
}
}
import { getChatImageUrl } from '../static/lib/utils.js';
export function normalizeGroupMessage(msg) {
if (!msg) return msg;
if (msg.type === 'text' && msg.payload && msg.payload.text) {
const card = parseOrderCardText(msg.payload.text);
if (card) {
msg.type = 'order';
msg.payload = card;
}
}
if (msg.type === 'custom' && msg.payload) {
const inner = msg.payload.payload || msg.payload;
if (inner && (inner.orderId || inner.dingdan_id)) {
msg.type = 'order';
msg.payload = {
orderId: inner.orderId || inner.dingdan_id,
jieshao: inner.jieshao || '',
jine: inner.jine || '',
beizhu: inner.beizhu || '',
zhuangtai: inner.zhuangtai,
nicheng: inner.nicheng || '',
create_time: inner.create_time || '',
};
}
}
if (msg.type === 'image') {
const url = getChatImageUrl(msg);
msg.payload = { ...(msg.payload || {}), url };
msg.imageUrl = url;
}
return msg;
}
export function formatLastMessagePreview(msg) {
if (!msg) return '';
if (msg.type === 'text' && msg.payload && msg.payload.text) {
const card = parseOrderCardText(msg.payload.text);
if (card) return `[订单] ${card.jieshao || card.orderId}`;
return msg.payload.text;
}
if (msg.type === 'order' && msg.payload) {
return `[订单] ${msg.payload.jieshao || msg.payload.orderId || ''}`;
}
if (msg.type === 'image') return '[图片]';
return '[消息]';
}
export function enrichGroupConversation(item, myGoEasyId, defaultAvatar, ossBase) {
if (!item || item.type !== 'group') return item;
if (!item.data) item.data = {};
applyPairMetaToConversation(item, myGoEasyId, defaultAvatar, ossBase);
if (item.data.orderZhuangtai != null) {
item.data.orderZhuangtaiText = ZHUANGTAI_MAP[item.data.orderZhuangtai] || '';
}
item.displayLastMsg = formatLastMessagePreview(item.lastMessage);
if (!item.data.avatar) item.data.avatar = defaultAvatar;
return item;
}
export function isOrderGroupConversation(c) {
if (!c || c.type !== 'group') return false;
if (c.data && c.data.orderId) return true;
if (c.data && c.data.dashouGoEasyId) return true;
return isPairGroupId(c.groupId) || /^group_\w+$/.test(c.groupId || '');
}
export function getConversationSortTime(conversation, openTimes) {
const id = conversation.groupId || conversation.userId || '';
const msgTs = conversation.lastMessage?.timestamp || 0;
const openTs = (openTimes && id && openTimes[id]) ? openTimes[id] : 0;
// QQ/微信:取「最后消息时间」与「最近进入会话时间」较大值;新消息到达会更新 msgTs 并置顶
return Math.max(msgTs, openTs);
}
export function sortConversations(list, openTimes) {
return list.sort((a, b) => {
if (a.top && !b.top) return -1;
if (!a.top && b.top) return 1;
const ta = getConversationSortTime(a, openTimes);
const tb = getConversationSortTime(b, openTimes);
return tb - ta;
});
}
export function recordConversationOpen(conversation) {
const id = conversation?.groupId || conversation?.userId;
if (!id) return;
try {
const times = wx.getStorageSync('conversationOpenTimes') || {};
times[id] = Date.now();
wx.setStorageSync('conversationOpenTimes', times);
} catch (e) {}
}

View File

@@ -2,6 +2,7 @@
import { getPrimaryRole } from './primary-role.js';
import { resolveAvatarUrl } from './avatar.js';
import { ensureRoleOnCenterPage } from './role-tab-bar.js';
import { isStaffMode } from './staff-api.js';
/** 联系对象前缀:管事/组长/考核官已废弃独立前缀,统一 Ds */
const PEER_PREFIX = {
@@ -14,15 +15,38 @@ const PEER_PREFIX = {
kaoheguan: 'Ds',
};
export const IM_ROLE_PREFIX = {
normal: 'Boss',
dashou: 'Ds',
shangjia: 'Sj',
guanshi: 'Ds',
zuzhang: 'Ds',
kaoheguan: 'Ds',
};
import { getDefaultAvatarUrl } from './avatar.js';
export function getDefaultAvatar(app) {
return getDefaultAvatarUrl(app || getApp());
}
export function getImPrefixForRole(role) {
if (isStaffMode()) return 'Sj';
return PEER_PREFIX[role] || 'Ds';
}
/** 当前端连接 GoEasy 用的 userId */
export function getImUserIdForRole(role, uid) {
if (!uid) return '';
const prefix = getImPrefixForRole(role);
return prefix + uid;
}
/** 当前端连接 GoEasy 用的 userId子客服仍用 Sj */
export function getLocalImUserId(app) {
app = app || getApp();
const uid = wx.getStorageSync('uid') || '';
if (!uid) return '';
if (isStaffMode()) return 'Sj' + uid;
const primary = getPrimaryRole(app);
if (primary === 'dashou') return 'Ds' + uid;
if (primary === 'shangjia') return 'Sj' + uid;
@@ -31,15 +55,43 @@ export function getLocalImUserId(app) {
export function getLocalImIdentity(app) {
app = app || getApp();
if (isStaffMode()) return 'shangjia';
return getPrimaryRole(app) || 'normal';
}
export function getFreshImUser(app, role, uid) {
const effectiveRole = isStaffMode() ? 'shangjia' : role;
const id = getImUserIdForRole(effectiveRole, uid);
const touxiang = wx.getStorageSync('touxiang') || '';
const avatar = resolveAvatarUrl(touxiang, app);
const name = app.globalData.currentUser?.name || `用户${(uid || '').slice(-6)}`;
return { id, name, avatar };
}
export function persistGroupMeta(app, groupId, meta) {
if (!groupId || !meta) return;
if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {};
app.globalData.groupInfoMap[groupId] = { ...app.globalData.groupInfoMap[groupId], ...meta };
try {
const cache = wx.getStorageSync('pairGroupMetaCache') || {};
cache[groupId] = app.globalData.groupInfoMap[groupId];
wx.setStorageSync('pairGroupMetaCache', cache);
} catch (e) {}
}
export function loadGroupMetaCache(app) {
try {
const cache = wx.getStorageSync('pairGroupMetaCache') || {};
if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {};
Object.assign(app.globalData.groupInfoMap, cache);
} catch (e) {}
}
export function buildPeerImUserId(uid, role = 'dashou') {
if (!uid) return '';
return getImPrefixForRole(role) + uid;
}
/** 联系邀请人/管事/组长:统一 Ds 前缀 */
export function buildGuanshiPeerId(uid) {
return buildDashouPeerId(uid);
}

View File

@@ -4,9 +4,9 @@ const imRequest = require('./imRequest.js');
/**
* 打手 IM 权限检测
* POST /yonghu/dshqltqx
* 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 }
*/
async function dashoujianquan(app) {
async function dashoujianquan(app, options = {}) {
const silent = options && options.silent === true;
const uid = wx.getStorageSync('uid');
if (!uid) return { allowed: false, reason: '未登录' };
@@ -21,7 +21,8 @@ async function dashoujianquan(app) {
const allow = res.data.allow;
if (allow === 1) {
return { allowed: true };
} else {
}
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '当前身份已不支持消息聊天功能,请充值会员或联系管理员后尝试',
@@ -30,22 +31,24 @@ async function dashoujianquan(app) {
mask: true
});
await new Promise(r => setTimeout(r, 2100));
return { allowed: false, reason: '接单员消息权限被限制' };
}
return { allowed: false, reason: '接单员消息权限被限制' };
}
throw new Error(res.data?.msg || '鉴权接口异常');
} catch (err) {
console.error('打手鉴权失败:', err);
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
}
return { allowed: false, reason: '鉴权请求失败' };
}
}
module.exports = { dashoujianquan };
module.exports = { dashoujianquan };

View File

@@ -14,21 +14,21 @@ const { dashoujianquan } = require('./dashoujianquan.js');
* @param {Object} app 全局 App 实例
* @returns {Promise<{ allowed: boolean, reason?: string }>}
*/
async function jianquanxian(app) {
async function jianquanxian(app, options = {}) {
const silent = options && options.silent === true;
// 1. 检测登录态(用 uid 或 token
const token = wx.getStorageSync('token') || wx.getStorageSync('uid');
if (!token) {
// 没登录:先强制断开(如果已连接)
await app.disconnectGoEasy();
// 弹窗提示,强制阅读 2 秒
wx.showToast({
title: '您当前处于未登录状态,消息功能暂不可用',
icon: 'none',
duration: 2000,
mask: true
});
// 等待提示结束,确保用户看到
await new Promise(resolve => setTimeout(resolve, 2100));
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '您当前处于未登录状态,消息功能暂不可用',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(resolve => setTimeout(resolve, 2100));
}
return { allowed: false, reason: '未登录' };
}
@@ -37,17 +37,16 @@ async function jianquanxian(app) {
// 3. 根据角色分发到具体鉴权模块
switch (role) {
case 'normal': // 点单老板
return laobanjianquan(app);
case 'normal':
return laobanjianquan(app, { silent });
case 'dashou': // 打手
return dashoujianquan(app);
case 'dashou':
return dashoujianquan(app, { silent });
// 其他角色暂未细分,统一放行(后续可新增对应文件)
case 'shangjia':
case 'guanshi':
case 'zuzhang':
case 'kaoheguan':
case 'kaoheguan':
return { allowed: true };
default:

View File

@@ -4,9 +4,9 @@ const imRequest = require('./imRequest.js');
/**
* 老板点单用户IM 权限检测
* POST /yonghu/lbhqltqx
* 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 }
*/
async function laobanjianquan(app) {
async function laobanjianquan(app, options = {}) {
const silent = options && options.silent === true;
const uid = wx.getStorageSync('uid');
if (!uid) return { allowed: false, reason: '未登录' };
@@ -21,7 +21,8 @@ async function laobanjianquan(app) {
const allow = res.data.allow;
if (allow === 1) {
return { allowed: true };
} else {
}
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '当前身份已不支持使用消息功能',
@@ -30,22 +31,24 @@ async function laobanjianquan(app) {
mask: true
});
await new Promise(r => setTimeout(r, 1100));
return { allowed: false, reason: '当前身份无消息权限' };
}
return { allowed: false, reason: '当前身份无消息权限' };
}
throw new Error(res.data?.msg || '鉴权接口异常');
} catch (err) {
console.error('老板鉴权失败:', err);
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
if (!silent) {
await app.disconnectGoEasy();
wx.showToast({
title: '消息服务暂不可用,请稍后重试',
icon: 'none',
duration: 2000,
mask: true
});
await new Promise(r => setTimeout(r, 2100));
}
return { allowed: false, reason: '鉴权请求失败' };
}
}
module.exports = { laobanjianquan };
module.exports = { laobanjianquan };

27
utils/kefu-nav.js Normal file
View File

@@ -0,0 +1,27 @@
/** 与消息页「联系客服」一致:跳转客服会话 */
export function openCustomerServiceChat() {
const app = getApp();
if (app.globalData.chatEnabled && app.ensureConnection) {
app.globalData.goEasyConnection.autoReconnect = true;
app.ensureConnection();
}
const param = { teamId: 'support_team' };
wx.navigateTo({
url: '/pages/cs-chat/cs-chat?data=' + encodeURIComponent(JSON.stringify(param)),
});
}
/** 与打手「我的」页在线客服同一图标beijing/tubiao/grzx_kefu.jpg */
export function resolveKefuIconUrl(app) {
app = app || getApp();
const rel = 'beijing/tubiao/grzx_kefu.jpg';
let base = (app.globalData && app.globalData.ossImageUrl) || '';
if (!base && typeof app.readConfigFromStorage === 'function') {
try {
const cached = app.readConfigFromStorage();
base = (cached && cached.cos && cached.cos.ossImageUrl) || '';
} catch (e) {}
}
if (!base) return '';
return base.endsWith('/') ? base + rel : base + rel;
}

View File

@@ -6,6 +6,7 @@ import {
isWechatLoginSuccess,
} from './role-tab-bar';
import { restoreStaffContextAfterAuth } from './staff-api.js';
import { CLUB_API, getClubId, setClubId, buildClubHeaders } from './club-context';
const app = getApp();
@@ -19,12 +20,10 @@ export function ensureLogin(options = {}) {
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 });
@@ -36,12 +35,15 @@ export function ensureLogin(options = {}) {
const apiBaseUrl = app.globalData.apiBaseUrl;
if (!apiBaseUrl) throw new Error('服务器配置错误');
const clubId = getClubId(app);
const appId = app.globalData.appId || '';
const res = await new Promise((resolve, reject) => {
wx.request({
url: apiBaseUrl + '/yonghu/wechatlogin',
url: apiBaseUrl + CLUB_API.wechatLogin,
method: 'POST',
data: { code: loginRes.code },
header: { 'content-type': 'application/json' },
data: { code: loginRes.code, club_id: clubId, app_id: appId },
header: buildClubHeaders({ 'content-type': 'application/json' }),
success: resolve,
fail: reject,
});
@@ -49,7 +51,11 @@ export function ensureLogin(options = {}) {
const userData = res.data && (res.data.data || res.data);
if (res.statusCode === 200 && isWechatLoginSuccess(res.data) && userData) {
if (userData.club_id) {
setClubId(userData.club_id, app);
}
applyWechatLoginData(userData, page);
restoreStaffContextAfterAuth();
resolve(true);
} else {
const msg = res.data?.msg || '登录失败';
@@ -64,12 +70,10 @@ export function ensureLogin(options = {}) {
}
});
// 存储全局登录 Promise供 request.js 感知
app.globalData.loginPromise = loginPromise;
// 无论成功或失败,完成后清空标记
loginPromise.finally(() => {
app.globalData.loginPromise = null;
});
return loginPromise;
}
}

View File

@@ -0,0 +1,95 @@
/**
* 商家订单统计:优先新接口,失败时回退订单列表中的 pending 字段
*/
import { STAFF_API, isStaffMode } from './staff-api.js';
const EMPTY_ORDER = {
pending_count: 0,
pending_amount: '0.00',
completed_count: 0,
completed_amount: '0.00',
refund_count: 0,
refund_amount: '0.00',
dispatch_count: 0,
dispatch_amount: '0.00',
};
export async function fetchMerchantOrderStats(request, params = {}) {
const statsUrl = isStaffMode() ? STAFF_API.orderStats : '/dingdan/sjshujutj';
try {
const res = await request({ url: statsUrl, method: 'POST', data: params });
if (res.statusCode === 200 && res.data && (res.data.code === 0 || res.data.code === 200)) {
return res.data.data || {};
}
} catch (e) {
console.warn('统计接口失败,尝试回退', e);
}
try {
const listUrl = isStaffMode() ? STAFF_API.orderList : '/dingdan/sjdingdanhq';
const fb = await request({
url: listUrl,
method: 'POST',
data: { zhuangtai_list: [8], page: 1, page_size: 1, ...params },
});
if (fb.statusCode === 200 && fb.data && fb.data.code === 0) {
const d = fb.data.data || {};
return {
order: {
...EMPTY_ORDER,
pending_count: d.pending_count || 0,
pending_amount: d.pending_amount || '0.00',
},
can_view_finance: true,
};
}
} catch (e) {
console.warn('统计回退失败', e);
}
return { order: { ...EMPTY_ORDER }, can_view_finance: true };
}
export function applyOrderStatsToPage(page, data) {
const order = data.order || data || {};
const penalty = data.penalty || {};
const fakuanP = penalty.fakuan_pending || 0;
const jifenP = penalty.jifen_pending || 0;
page.setData({
canViewFinance: data.can_view_finance !== false,
orderStats: {
pending_count: order.pending_count || 0,
pending_amount: order.pending_amount || '0.00',
completed_count: order.completed_count || 0,
completed_amount: order.completed_amount || '0.00',
refund_count: order.refund_count || 0,
refund_amount: order.refund_amount || '0.00',
dispatch_count: order.dispatch_count || 0,
dispatch_amount: order.dispatch_amount || '0.00',
},
penaltyStats: {
fakuan_pending: fakuanP,
fakuan_pending_amount: penalty.fakuan_pending_amount || '0.00',
jifen_pending: jifenP,
},
pendingCount: order.pending_count || 0,
punishPending: fakuanP + jifenP,
});
}
export function applyOrderStatsToHome(page, data, statsKey = 'stats') {
const order = data.order || data || {};
const base = page.data[statsKey] || {};
page.setData({
[statsKey]: {
...base,
pendingCount: order.pending_count || 0,
pendingAmount: order.pending_amount || '0.00',
completedCount: order.completed_count || 0,
completedAmount: order.completed_amount || '0.00',
refundCount: order.refund_count || 0,
refundAmount: order.refund_amount || '0.00',
dispatchCount: order.dispatch_count || 0,
dispatchAmount: order.dispatch_amount || '0.00',
},
});
}

238
utils/miniapp-icons.js Normal file
View File

@@ -0,0 +1,238 @@
/** 小程序可配置图标(启动时从 qdpzhq.miniappAssets 注入 globalData */
import { buildClubHeaders } from './club-context.js';
export const ICON_KEYS = {
MERCHANT_GOLD_BANNER: 'merchant_gold_banner',
MERCHANT_HOME_REGULAR: 'merchant_home_regular',
MERCHANT_HOME_CUSTOM: 'merchant_home_custom',
MERCHANT_HOME_LINK: 'merchant_home_link',
MERCHANT_HOME_KEFU_KEY: 'merchant_home_kefu_key',
MERCHANT_HOME_KEFU_LIST: 'merchant_home_kefu_list',
MERCHANT_HOME_NOTICE: 'merchant_home_notice',
MERCHANT_HOME_STAT_BG: 'merchant_home_stat_bg',
MERCHANT_REGULAR: 'merchant_regular_dispatch',
MERCHANT_CUSTOM: 'merchant_custom_dispatch',
MERCHANT_PENDING: 'merchant_pending_settle',
MERCHANT_KEFU_KEY: 'merchant_kefu_key',
MERCHANT_KEFU_LIST: 'merchant_kefu_list',
MERCHANT_LINK: 'merchant_link_dispatch',
MERCHANT_PUNISH: 'merchant_punish',
MERCHANT_RANK: 'merchant_rank',
MERCHANT_REFRESH: 'merchant_refresh',
FIGHTER_RECHARGE_MEMBER: 'fighter_recharge_member',
FIGHTER_RECHARGE_DEPOSIT: 'fighter_recharge_deposit',
FIGHTER_RECHARGE_VIP_BG: 'fighter_recharge_vip_bg',
FIGHTER_DEPOSIT_BG: 'fighter_deposit_bg',
FIGHTER_DEPOSIT_HERO: 'fighter_deposit_hero',
FIGHTER_DEPOSIT_BENEFIT_1: 'fighter_deposit_benefit_1',
FIGHTER_DEPOSIT_BENEFIT_2: 'fighter_deposit_benefit_2',
FIGHTER_DEPOSIT_BENEFIT_3: 'fighter_deposit_benefit_3',
FIGHTER_DEPOSIT_BENEFIT_4: 'fighter_deposit_benefit_4',
FIGHTER_RECHARGE_BENEFIT_1: 'fighter_recharge_benefit_1',
FIGHTER_RECHARGE_BENEFIT_2: 'fighter_recharge_benefit_2',
FIGHTER_RECHARGE_BENEFIT_3: 'fighter_recharge_benefit_3',
FIGHTER_RECHARGE_ADVANTAGE_1: 'fighter_recharge_advantage_1',
FIGHTER_RECHARGE_ADVANTAGE_2: 'fighter_recharge_advantage_2',
FIGHTER_RECHARGE_ADVANTAGE_3: 'fighter_recharge_advantage_3',
FIGHTER_RECHARGE_ADVANTAGE_4: 'fighter_recharge_advantage_4',
FIGHTER_ORDER_PENDING: 'fighter_order_pending',
FIGHTER_ORDER_SETTLING: 'fighter_order_settling',
FIGHTER_ORDER_DONE: 'fighter_order_done',
FIGHTER_ORDER_CLOSED: 'fighter_order_closed',
FIGHTER_TRADE_COMMISSION: 'fighter_trade_commission',
FIGHTER_TRADE_WITHDRAW: 'fighter_trade_withdraw',
FIGHTER_TRADE_SETTLE: 'fighter_trade_settle',
FIGHTER_TRADE_DEDUCT: 'fighter_trade_deduct',
FIGHTER_TRADE_RECHARGE: 'fighter_trade_recharge',
FIGHTER_INVITE_SUBORDINATE: 'fighter_invite_subordinate',
FIGHTER_INVITE_POSTER: 'fighter_invite_poster',
FIGHTER_INVITE_CODE: 'fighter_invite_code',
FIGHTER_KUAISHOU_CASH: 'fighter_kuaishou_cash',
FIGHTER_MINE_COPY: 'fighter_mine_copy',
FIGHTER_MINE_EDIT: 'fighter_mine_edit',
FIGHTER_MINE_QIEPIAN: 'fighter_mine_qiepian',
FIGHTER_MINE_PENALTY_TIP: 'fighter_mine_penalty_tip',
FIGHTER_MINE_KEFU: 'fighter_mine_kefu',
FIGHTER_MINE_SHANGJIA: 'fighter_mine_shangjia',
FIGHTER_MINE_PUNISH: 'fighter_mine_punish',
FIGHTER_MINE_MEDAL: 'fighter_mine_medal',
FIGHTER_MINE_BOOK: 'fighter_mine_book',
FIGHTER_MINE_RANK: 'fighter_mine_rank',
FIGHTER_MINE_INVITE: 'fighter_mine_invite',
FIGHTER_MINE_CONTACT: 'fighter_mine_contact',
FIGHTER_MINE_RECORD: 'fighter_mine_record',
FIGHTER_MINE_KAOHE_DAFEN: 'fighter_mine_kaohe_dafen',
FIGHTER_MINE_KAOHE_JILU: 'fighter_mine_kaohe_jilu',
FIGHTER_MINE_KAOHE_ZHONGXIN: 'fighter_mine_kaohe_zhongxin',
FIGHTER_MINE_DASHOU_AUTH: 'fighter_mine_dashou_auth',
FIGHTER_MINE_CLEAR: 'fighter_mine_clear',
FIGHTER_MINE_SWITCH: 'fighter_mine_switch',
MINE_PINDAO: 'mine_pindao',
ICON_REFRESH: 'icon_refresh',
};
/** 仅后台上传后才展示(页面大背景),横幅/功能图标走 resolveMiniappIcon */
export const CONFIG_ONLY_ICON_KEYS = new Set([
ICON_KEYS.FIGHTER_RECHARGE_VIP_BG,
ICON_KEYS.FIGHTER_DEPOSIT_BG,
ICON_KEYS.FIGHTER_DEPOSIT_HERO,
]);
export const ICON_FOLDER = 'beijing/tubiao/chengxutubiao';
/** 逍遥梦原版金牌横幅(硬编码时代 82.png */
export const MERCHANT_GOLD_BANNER_FALLBACK = 'https://bintao.xmxym88.com/xcx/bintao/82.png';
/** 频道入口图标默认相对路径(拼接 ossImageUrl后台上传 mine_pindao 后覆盖) */
export const PINDAO_ICON_FALLBACK = 'beijing/tubiao/grzx_pindao.jpg';
/** 打手中心图标默认相对路径(后台未上传时由后端 default 下发,此处仅离线兜底) */
export function defaultIconRel(key) {
return `${ICON_FOLDER}/${key}.png`;
}
function ossBase(app) {
return (app && app.globalData && app.globalData.ossImageUrl) || '';
}
export function applyMiniappAssetsFromConfig(app, config) {
if (!app || !config) return;
const assets = config.miniappAssets || {};
app.globalData.miniappIcons = assets.icons || {};
app.globalData.pindaoConfig = assets.pindao || { channel_no: '', title: '频道', images: [] };
app.globalData.posterConfig = assets.poster || {
guanshi_bg: 'beijing/haibaobeijing.jpg',
zuzhang_bg: 'beijing/zuzhangbeijing.jpg',
};
}
/** 后台已上传的可配置图标(无则返回空,不显示) */
export function resolveConfiguredIcon(app, key) {
const icons = (app && app.globalData && app.globalData.miniappIcons) || {};
const path = icons[key];
if (!path) return '';
if (path.startsWith('http')) return path;
return ossBase(app) + path;
}
/** 通用图标:有配置用配置,否则用 fallback */
export function resolveMiniappIcon(app, key, fallback = '') {
if (CONFIG_ONLY_ICON_KEYS.has(key)) {
return resolveConfiguredIcon(app, key);
}
const icons = (app && app.globalData && app.globalData.miniappIcons) || {};
const path = icons[key];
if (!path) {
if (!fallback) return '';
if (fallback.startsWith('http')) return fallback;
const base = ossBase(app);
return base ? base + fallback : fallback;
}
if (path.startsWith('http')) return path;
return ossBase(app) + path;
}
export function getPindaoConfig(app) {
return (app && app.globalData && app.globalData.pindaoConfig) || {
channel_no: '',
title: '频道',
images: [],
};
}
export function hasPindaoEntry(app) {
const cfg = getPindaoConfig(app);
if ((cfg.channel_no || '').trim()) return true;
return (cfg.images || []).length > 0;
}
export function resolvePindaoImages(app) {
const cfg = getPindaoConfig(app);
const base = ossBase(app);
return (cfg.images || []).map((url) => {
if (!url) return '';
return url.startsWith('http') ? url : base + url;
}).filter(Boolean);
}
export function preloadPindaoImages(urls) {
(urls || []).forEach((src) => {
if (!src) return;
wx.getImageInfo({ src, fail: () => {} });
});
}
export async function refreshPindaoConfig(app) {
app = app || getApp();
try {
if (typeof app.fetchConfigSafely === 'function') {
const config = await app.fetchConfigSafely();
if (config && typeof config === 'object') {
if (typeof app.applyDynamicConfig === 'function') {
app.applyDynamicConfig(config);
} else {
applyMiniappAssetsFromConfig(app, config);
}
if (typeof app.saveConfigToStorage === 'function') {
app.saveConfigToStorage(config);
}
}
}
} catch (e) {
console.warn('refreshPindaoConfig failed', e);
}
const cfg = getPindaoConfig(app);
const images = resolvePindaoImages(app);
preloadPindaoImages(images);
return { cfg, images };
}
export function warmupPindaoConfig(app) {
const cached = resolvePindaoImages(app || getApp());
if (cached.length) preloadPindaoImages(cached);
refreshPindaoConfig(app).catch(() => {});
}
export function getPosterConfig(app) {
return (app && app.globalData && app.globalData.posterConfig) || {
guanshi_bg: 'beijing/haibaobeijing.jpg',
zuzhang_bg: 'beijing/zuzhangbeijing.jpg',
};
}
export function resolvePosterBg(app, role) {
const cfg = getPosterConfig(app);
const rel = role === 'zuzhang' ? cfg.zuzhang_bg : cfg.guanshi_bg;
const base = ossBase(app);
const fallback = role === 'zuzhang' ? 'beijing/zuzhangbeijing.jpg' : 'beijing/haibaobeijing.jpg';
const path = rel || fallback;
return path.startsWith('http') ? path : base + path;
}
export async function refreshPosterConfig(app) {
app = app || getApp();
try {
const token = wx.getStorageSync('token');
const header = buildClubHeaders({ 'Content-Type': 'application/json' });
if (token) header.Authorization = 'Bearer ' + token;
const res = await new Promise((resolve, reject) => {
wx.request({
url: app.globalData.apiBaseUrl + '/peizhi/haibaopz',
method: 'POST',
header,
success: (r) => resolve(r),
fail: reject,
});
});
const body = res?.data;
if (body && body.code === 0 && body.data && body.data.poster) {
app.globalData.posterConfig = body.data.poster;
if (body.data.club_id) app.globalData.clubId = body.data.club_id;
return body.data.poster;
}
} catch (e) {
console.warn('refreshPosterConfig failed', e);
}
return getPosterConfig(app);
}

47
utils/order-tags.js Normal file
View File

@@ -0,0 +1,47 @@
/** 订单标签 texiao_json 规范化(兼容字符串 / 对象) */
export function parseTexiaoJson(raw) {
if (!raw) return {};
if (typeof raw === 'object') return { ...raw };
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw);
return typeof parsed === 'object' && parsed ? { ...parsed } : {};
} catch (e) {
return {};
}
}
return {};
}
/** 合并顶层 color / flash 与 texiao_json */
export function mergeTagTexiao(tag) {
const texiao = parseTexiaoJson(tag.texiao_json);
const color = tag.color || texiao.color || texiao.bg_color;
if (color) {
texiao.color = color;
}
if (tag.flash_enabled != null && texiao.flash == null) {
texiao.flash = !!tag.flash_enabled;
}
return texiao;
}
export function normalizeTagList(list) {
if (!Array.isArray(list)) return [];
return list.map((tag) => ({
...tag,
id: tag.id,
mingcheng: tag.mingcheng || tag.name || '',
texiao_json: mergeTagTexiao(tag),
}));
}
export function normalizeOrderTags(item) {
return {
xuqiu_biaoqian: normalizeTagList(item.xuqiu_biaoqian),
dashou_biaoqian: normalizeTagList(item.dashou_biaoqian),
shangjia_biaoqian: normalizeTagList(item.shangjia_biaoqian),
shangjia_identity_biaoqian: normalizeTagList(item.shangjia_identity_biaoqian),
zhiding_identity_biaoqian: normalizeTagList(item.zhiding_identity_biaoqian),
};
}

View File

@@ -1,7 +1,5 @@
/**
* 手机号强制认证 - 完全自包含逻辑模块
*
* check / ensurePhoneAuth 每次均请求后端 /peizhi/yhbdsjh 判断 need_auth
* 手机号强制认证:每次调用 check / ensurePhoneAuth 均请求后端 /peizhi/yhbdsjh
*/
const API_BASE = 'https://www.abas.asia/hqhd';
@@ -9,18 +7,29 @@ const AUTH_CHECK_URL = '/peizhi/yhbdsjh';
const AUTH_SUBMIT_URL = '/yonghu/xiugai';
const AUTH_PAGE = '/pages/phone-auth/phone-auth';
const RETURN_ROLE_KEY = 'phone_auth_return_role';
const RETURN_PAGE_KEY = 'phone_auth_return_page';
function buildHeaders(extra = {}) {
const token = wx.getStorageSync('token');
const header = { 'Content-Type': 'application/json', ...extra };
if (token) header['Authorization'] = 'Bearer ' + token;
try {
const { buildClubHeaders } = require('./club-context.js');
return buildClubHeaders(header);
} catch (e) {
const clubId = wx.getStorageSync('club_id') || 'xq';
header['X-Club-Id'] = clubId;
return header;
}
}
function innerRequest(url, data = {}) {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token');
const header = { 'Content-Type': 'application/json' };
if (token) header['Authorization'] = 'Bearer ' + token;
wx.request({
url: API_BASE + url,
method: 'POST',
data,
header,
header: buildHeaders(),
success: (res) => resolve(res),
fail: (err) => reject(err),
});
@@ -43,7 +52,7 @@ function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
filePath,
name: fileName,
formData,
header: { Authorization: 'Bearer ' + token },
header: buildHeaders({ Authorization: 'Bearer ' + token }),
success: (res) => {
if (res.statusCode === 200) {
try {
@@ -92,10 +101,29 @@ function parseCheckResponse(res) {
return { ok: true, needAuth: !!needAuth };
}
/**
* 询问后端是否需要手机号认证
* @returns {Promise<boolean>} true=需要认证
*/
/** 根据当前主端/身份推断认证完成后回跳角色 */
function resolveReturnRole() {
const primary = wx.getStorageSync('primaryRole') || wx.getStorageSync('currentRole') || '';
if (primary === 'dashou' || primary === 'shangjia') return primary;
if (Number(wx.getStorageSync('shangjiastatus')) === 1) return 'shangjia';
if (Number(wx.getStorageSync('staffstatus')) === 1) return 'shangjia';
if (
Number(wx.getStorageSync('dashoustatus')) === 1
|| Number(wx.getStorageSync('guanshistatus')) === 1
|| Number(wx.getStorageSync('zuzhangstatus')) === 1
|| Number(wx.getStorageSync('kaoheguanstatus')) === 1
) {
return 'dashou';
}
return 'mine';
}
function defaultReturnPage(role) {
if (role === 'dashou') return '/pages/accept-order/accept-order';
if (role === 'shangjia') return '/pages/merchant-home/merchant-home';
return '/pages/mine/mine';
}
async function check() {
const token = wx.getStorageSync('token');
if (!token) return false;
@@ -110,9 +138,13 @@ async function check() {
}
}
function redirectToPhoneAuth(returnRole) {
if (returnRole) {
wx.setStorageSync(RETURN_ROLE_KEY, returnRole);
function redirectToPhoneAuth(returnRole, returnPage) {
const role = returnRole || resolveReturnRole();
wx.setStorageSync(RETURN_ROLE_KEY, role);
if (returnPage) {
wx.setStorageSync(RETURN_PAGE_KEY, returnPage);
} else {
wx.removeStorageSync(RETURN_PAGE_KEY);
}
if (!isOnPhoneAuthPage()) {
wx.reLaunch({ url: AUTH_PAGE });
@@ -120,26 +152,32 @@ function redirectToPhoneAuth(returnRole) {
}
/**
* 打手端等场景:向后端确认,未认证则跳转认证页
* @returns {Promise<boolean>} true=已通过或无需认证false=已跳转认证页
* 向后端确认是否需要手机号认证;需要则跳转认证页
* @returns {Promise<boolean>} true=无需认证或已满足false=已跳转认证页
*/
async function ensurePhoneAuth(options = {}) {
const { redirect = true, role = 'dashou' } = options;
const { redirect = true, role, returnPage } = options;
const token = wx.getStorageSync('token');
if (!token) return true;
const needAuth = await check();
if (!needAuth) return true;
if (redirect) redirectToPhoneAuth(role);
if (redirect) {
redirectToPhoneAuth(role || resolveReturnRole(), returnPage);
}
return false;
}
function getPhoneAuthReturnPage() {
const role = wx.getStorageSync(RETURN_ROLE_KEY) || '';
const savedPage = wx.getStorageSync(RETURN_PAGE_KEY);
wx.removeStorageSync(RETURN_PAGE_KEY);
if (savedPage) {
wx.removeStorageSync(RETURN_ROLE_KEY);
return savedPage;
}
const role = wx.getStorageSync(RETURN_ROLE_KEY) || resolveReturnRole();
wx.removeStorageSync(RETURN_ROLE_KEY);
if (role === 'dashou') return '/pages/accept-order/accept-order';
if (role === 'shangjia') return '/pages/merchant-orders/merchant-orders';
return '/pages/mine/mine';
return defaultReturnPage(role);
}
async function submit(phoneCode, avatarPath) {
@@ -166,4 +204,5 @@ module.exports = {
ensurePhoneAuth,
redirectToPhoneAuth,
getPhoneAuthReturnPage,
resolveReturnRole,
};

30
utils/poster-page.js Normal file
View File

@@ -0,0 +1,30 @@
/** 推广海报页:按俱乐部分缓存二维码与背景 */
import { getClubId } from './club-context.js';
import { resolvePosterBg, refreshPosterConfig } from './miniapp-icons.js';
export function posterQrCacheKey(role) {
const clubId = getClubId();
return role === 'zuzhang'
? `zuzhang_haibao_url_${clubId}`
: `guanshi_haibao_url_${clubId}`;
}
export function posterInviteCacheKey() {
return `zuzhang_yaoqingma_${getClubId()}`;
}
export function getFullImageUrl(url, app) {
app = app || getApp();
if (!url) return '';
const oss = app.globalData.ossImageUrl || '';
return url.startsWith('http') ? url : oss + url;
}
export async function loadPosterPageConfig(role, app) {
app = app || getApp();
await refreshPosterConfig(app);
return {
bgUrl: resolvePosterBg(app, role),
qrCacheKey: posterQrCacheKey(role),
};
}

View File

@@ -1,5 +1,6 @@
/** 三端固定normal | dashou | shangjia认证后不可回退 */
import { isStaffMode } from './staff-api.js';
import { ensurePhoneAuth } from './phone-auth.js';
const VALID = ['normal', 'dashou', 'shangjia'];
@@ -55,6 +56,9 @@ export function getPrimaryRole(app) {
if (isRoleStatusActive(wx.getStorageSync('shangjiastatus'))) {
return 'shangjia';
}
if (isStaffMode()) {
return 'shangjia';
}
return 'normal';
}
@@ -85,7 +89,7 @@ export function clearPrimaryRole(app) {
export const PRIMARY_DEFAULT_PAGES = {
normal: '/pages/mine/mine',
dashou: '/pages/accept-order/accept-order',
shangjia: '/pages/merchant-orders/merchant-orders',
shangjia: '/pages/merchant-home/merchant-home',
};
/** 锁定主端并跳转到该端默认首页,同时建立 IM 长连接 */
@@ -107,8 +111,8 @@ function finishEnterLockedRole(role, app) {
export function enterLockedRole(role, app) {
if (!VALID.includes(role) || role === 'normal') return false;
if (role === 'dashou') {
ensurePhoneAuth({ redirect: true, role: 'dashou' }).then((ok) => {
if (role === 'dashou' || role === 'shangjia') {
ensurePhoneAuth({ redirect: true, role }).then((ok) => {
if (ok) finishEnterLockedRole(role, app);
});
return true;

View File

@@ -1,11 +1,11 @@
// utils/request.js
import { buildClubHeaders } from './club-context';
function request(options) {
const app = getApp();
const token = wx.getStorageSync('token');
// 🔥 没有 token 时不发送请求,直接返回一个“空响应”对象,不影响后续代码
if (!token) {
// 返回一个模拟的响应,状态码为 401但不会触发任何弹窗或跳转
return Promise.resolve({
statusCode: 401,
data: { code: 401, msg: '未登录' },
@@ -13,10 +13,10 @@ function request(options) {
});
}
const header = {
const header = buildClubHeaders({
'Content-Type': 'application/json',
...options.header,
};
});
if (token) {
header['Authorization'] = 'Bearer ' + token;
}
@@ -24,13 +24,12 @@ function request(options) {
return new Promise((resolve, reject) => {
wx.request({
url: app.globalData.apiBaseUrl + options.url,
method: options.method,
method: options.method || 'GET',
data: options.data || {},
header,
timeout: options.timeout || 15000,
success: async (res) => {
if (res.statusCode === 401 || res.statusCode === 403) {
// 注意:这里的弹窗逻辑只会在有 token 且 token 失效时触发
// 无 token 的情况已经在上面直接返回,不会走到这里
if (app.globalData.loginPromise) {
try {
await app.globalData.loginPromise;
@@ -65,9 +64,20 @@ function request(options) {
}
resolve(res);
},
fail: reject,
fail: (err) => {
// 网络层失败(超时/域名不可达等),避免未捕获 Promise 触发 MiniProgramError 红屏
console.warn('request fail:', options.url, err && err.errMsg);
resolve({
statusCode: 0,
data: {
code: 408,
msg: (err && err.errMsg) || '网络请求失败',
},
errMsg: (err && err.errMsg) || 'request:fail',
});
},
});
});
}
export default request;
export default request;

View File

@@ -183,6 +183,11 @@ export function resetGuestSession(page, options = {}) {
app.globalData.shangpinleixing = [];
app.globalData.shangpinzhuanqu = [];
app.globalData.shangpinliebiao = [];
app.globalData._acceptOrderSessionReady = false;
app.globalData._acceptOrderPageCache = null;
app.globalData._acceptOrderPopupDone = false;
app.globalData._dashouPhoneChecked = false;
app.globalData._shangjiaPhoneChecked = false;
app.emitEvent('currentRoleChanged', { role: 'normal' });
refreshTabBar(page);
@@ -244,8 +249,9 @@ export function reconnectForRole(role) {
const connected = status === 'connected' || status === 'reconnected';
if (connected && currentUserId === targetUserId) {
if (app.loadConversations) app.loadConversations();
if (app.restoreTabBarBadge) app.restoreTabBarBadge();
if (app.ensureConnection) app.ensureConnection();
if (app.loadConversations) app.loadConversations();
return;
}

127
utils/scriptService.js Normal file
View File

@@ -0,0 +1,127 @@
// utils/scriptService.js — 客服话术拉取与自动回复匹配
import request from './request.js';
const DEFAULT_SCRIPTS = {
chat_image_confirm: {
title: '温馨提示',
content: '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。严禁赌博、诈骗及其他非法行为,违者后果自负。',
confirm_text: '确认',
cancel_text: '取消',
},
cs_welcome: {
content: '你好,请问有什么可以帮到您的?',
},
};
const textCache = {};
const TEXT_CACHE_TTL = 5 * 60 * 1000;
function fitModalBtn(text, fallback) {
const t = (text || fallback || '确定').trim();
if (t && t.length <= 4) return t;
return fallback || '确定';
}
function openConfirmModal(script, onConfirm, onCancel) {
wx.showModal({
title: ((script.title || '提示').trim()).slice(0, 32),
content: script.content || '',
confirmText: fitModalBtn(script.confirm_text, '确认'),
cancelText: fitModalBtn(script.cancel_text, '取消'),
success: (res) => {
if (res.confirm) onConfirm && onConfirm();
else onCancel && onCancel();
},
fail: () => onCancel && onCancel(),
});
}
function mergeScript(sceneKey, remote) {
const base = DEFAULT_SCRIPTS[sceneKey] || {};
if (!remote) return { ...base };
return { ...base, ...remote };
}
export async function fetchScripts(sceneKeys) {
if (!sceneKeys || !sceneKeys.length) return {};
const now = Date.now();
const cached = {};
const needFetch = [];
sceneKeys.forEach((key) => {
const entry = textCache[key];
if (entry && now - entry.time < TEXT_CACHE_TTL) {
cached[key] = entry.data;
} else {
needFetch.push(key);
}
});
if (needFetch.length) {
try {
const res = await request({
url: '/peizhi/huashuhq',
method: 'POST',
data: { scene_keys: needFetch },
});
const payload = (res && res.data && res.data.code === 200) ? res.data.data || {} : {};
needFetch.forEach((key) => {
textCache[key] = { time: now, data: payload[key] };
cached[key] = payload[key];
});
} catch (e) {
console.warn('fetchScripts failed', e);
}
}
const result = {};
sceneKeys.forEach((key) => {
result[key] = mergeScript(key, cached[key]);
});
return result;
}
export async function showConfirmByScene(sceneKey, onConfirm, onCancel) {
try {
const data = await fetchScripts([sceneKey]);
openConfirmModal(data[sceneKey] || mergeScript(sceneKey, null), onConfirm, onCancel);
} catch (e) {
openConfirmModal(mergeScript(sceneKey, null), onConfirm, onCancel);
}
}
export async function matchAutoReply(userText) {
if (!userText || !userText.trim()) {
return { matched: false, content: '', reply_type: 'text' };
}
try {
const res = await request({
url: '/peizhi/huashu_match',
method: 'POST',
data: {
scene_key: 'cs_auto_reply',
user_text: userText.trim(),
},
});
if (res && res.data && res.data.code === 200 && res.data.data) {
return res.data.data;
}
} catch (e) {
console.warn('matchAutoReply failed', e);
}
return { matched: false, content: '', reply_type: 'text' };
}
export async function getWelcomeText() {
const data = await fetchScripts(['cs_welcome']);
const script = data.cs_welcome || DEFAULT_SCRIPTS.cs_welcome;
return script.content || DEFAULT_SCRIPTS.cs_welcome.content;
}
export default {
fetchScripts,
showConfirmByScene,
matchAutoReply,
getWelcomeText,
};

View File

@@ -46,6 +46,8 @@ export const STAFF_API = {
penaltyApply: `${STAFF_PREFIX}/order/penalty-apply`,
penaltyManage: `${STAFF_PREFIX}/order/penalty-manage`,
changePlayer: `${STAFF_PREFIX}/order/change-player`,
orderStats: `${STAFF_PREFIX}/order/stats`,
memberUpdateRemark: `${STAFF_PREFIX}/member/update-remark`,
};
export function getStaffContext() {
@@ -222,8 +224,9 @@ export function syncStaffUi(page) {
const ctx = staff ? (getStaffContext() || {}) : null;
const wallet = (ctx && ctx.wallet) || {};
const portal = isMerchantPortalUser();
const staffFlag = Number(wx.getStorageSync('staffstatus')) === 1;
page.setData({
isShangjia: portal,
isShangjia: portal || staffFlag || !!(page.data && page.data.isShangjia),
isStaffMode: staff,
isMerchantOwner: !staff && Number(wx.getStorageSync('shangjiastatus')) === 1,
staffRoleName: staff ? (ctx.role_name || '客服') : '',

View File

@@ -1,5 +1,6 @@
// utils/upload.js
import request from './request.js'
import { buildClubHeaders } from './club-context.js'
const app = getApp()
@@ -39,9 +40,9 @@ const upload = async (options) => {
filePath,
name: fileName,
formData,
header: {
header: buildClubHeaders({
'Authorization': `Bearer ${token}`
},
}),
success: (res) => {
if (res.statusCode === 200) {

View File

@@ -1,215 +1,296 @@
/**
* 连接管理模块 - xiaoxilj.js (根治发送失败:跳转前确保群组已订阅)
* 从详情页跳转订单群聊时,提前订阅群组,避免发送消息时订阅未完成而失败
* 订单群聊跳转:与文赫一致,新群用打手+商家/老板配对 groupId旧 group_{订单号} 会话保留
*/
import request from './request';
import { persistGroupMeta } from './im-user.js';
import { resolveLocalGroupId } from './group-chat.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);
function formatError(err) {
if (!err) return '进入聊天失败';
if (typeof err === 'string') return err;
if (err.message) return err.message;
if (err.msg) return String(err.msg);
if (err.content) return String(err.content);
if (err.code) return `连接失败(${err.code})`;
try {
return JSON.stringify(err);
} catch (e) {
return '进入聊天失败';
}
}
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);
function withTimeout(promise, ms, errMsg) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(errMsg || '操作超时')), ms);
promise
.then((v) => { clearTimeout(timer); resolve(v); })
.catch((e) => { clearTimeout(timer); reject(e); });
});
}
function mapIdentityForApi(identityType) {
if (identityType === 'dashou') return 'dashou';
if (identityType === 'shangjia') return 'shangjia';
return 'normal';
}
function validateIdentity(identityType, userId) {
if (identityType === 'dashou' && !userId.startsWith('Ds')) {
throw new Error('接单员 ID 必须以 Ds 开头');
}
if (identityType === 'shangjia' && !userId.startsWith('Sj')) {
throw new Error('商家 ID 必须以 Sj 开头');
}
if ((identityType === 'boss' || identityType === 'normal') && !userId.startsWith('Boss')) {
throw new Error('老板 ID 必须以 Boss 开头');
}
}
function getConnectedUserId() {
const fromState = app.globalData?.goEasyConnection?.userId;
const fromIm = wx.goEasy?.im?.userId;
return fromState || fromIm || '';
}
function isImConnected() {
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
return status === 'connected' || status === 'reconnected';
}
async function ensureIdentityConnection(identityType, userId) {
validateIdentity(identityType, userId);
app.globalData.currentRole = identityType;
wx.setStorageSync('currentRole', identityType);
const uid = userId.replace(/^(Ds|Sj|Boss)/, '');
const avatar = wx.getStorageSync('touxiang') ||
(app.globalData.ossImageUrl + app.globalData.morentouxiang);
app.globalData.currentUser = {
id: userId,
name: app.globalData.currentUser?.name || `用户${uid}`,
avatar,
};
if (!app.globalData.chatEnabled || !wx.goEasy?.im) {
throw new Error('聊天服务未就绪,请稍后重试');
}
if (isImConnected() && getConnectedUserId() === userId) {
return;
}
const connectedId = getConnectedUserId();
if (connectedId && connectedId !== userId && app.disconnectGoEasy) {
await app.disconnectGoEasy();
}
if (!app.connectWithIdentity) {
throw new Error('聊天功能未初始化');
}
await withTimeout(
app.connectWithIdentity(identityType, userId, true),
12000,
'IM连接超时'
);
if (!isImConnected()) {
throw new Error('IM连接失败请检查网络后重试');
}
}
async function prepareGroupChat(identityType, userId, orderId, partnerUid, fadanPingtai, groupName, isCross) {
const orderIdStr = String(orderId || '').replace(/^group_/, '');
let chatData = null;
try {
const res = await withTimeout(
request({
url: '/dingdan/ltdhzb',
method: 'POST',
data: {
dingdan_id: orderIdStr,
identityType: mapIdentityForApi(identityType),
push_order_card: false,
},
}),
12000,
'准备群聊超时'
);
const body = res?.data || {};
if ((body.code === 0 || body.code === 200) && body.data?.groupId) {
chatData = body.data;
} else if (body.msg) {
console.warn('ltdhzb:', body.msg);
}
} catch (e) {
console.warn('ltdhzb 请求失败,尝试本地配对 groupId', e);
}
if (!chatData?.groupId) {
const myUid = userId.replace(/^(Ds|Sj|Boss)/, '');
let localGroupId = resolveLocalGroupId(
identityType, myUid, partnerUid, fadanPingtai, orderIdStr, isCross
);
if (!localGroupId && orderIdStr) {
localGroupId = `group_${orderIdStr}`;
}
if (!localGroupId) {
throw new Error('无法确定群聊,请确认订单已接单且对方信息完整');
}
chatData = {
groupId: localGroupId,
orderId: orderIdStr,
groupName: groupName || '订单群聊',
isCross: isCross || fadanPingtai || 0,
};
try {
await request({
url: '/dingdan/ltdhzb',
method: 'POST',
data: {
dingdan_id: orderIdStr,
identityType: mapIdentityForApi(identityType),
push_order_card: false,
},
});
} catch (e) { /* ignore */ }
}
return chatData;
}
class ConnectionManager {
// 原有的私聊方法保持不变
async connectAndChat(params) {
const { identityType, userId, targetUser } = params;
if (!identityType || !userId || !targetUser || !targetUser.id) {
throw new Error('参数不完整');
}
if (identityType === 'dashou' && !userId.startsWith('Ds')) {
throw new Error(`接单员身份的用户ID必须以Ds开头实际为 ${userId}`);
}
if (identityType === 'shangjia' && !userId.startsWith('Sj')) {
throw new Error(`商家身份的用户ID必须以Sj开头实际为 ${userId}`);
}
if (identityType === 'boss' && !userId.startsWith('Boss')) {
throw new Error(`老板身份的用户ID必须以Boss开头实际为 ${userId}`);
}
await ensureIdentityConnection(identityType, userId);
wx.removeStorageSync('savedGoEasyConnection');
wx.removeStorageSync('goEasyUserId');
wx.removeStorageSync('currentGoEasyIdentity');
if (app.clearSavedConnection) app.clearSavedConnection();
const currentUser = {
id: userId,
name: app.globalData.currentUser?.name || '用户',
avatar: app.globalData.currentUser?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
};
if (wx.goEasy && wx.goEasy.getConnectionStatus() === 'connected') {
wx.goEasy.disconnect();
}
const to = {
id: targetUser.id,
name: targetUser.name || '用户',
avatar: targetUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
};
const waitPromise = waitForConnection(userId);
app.connectWithIdentity(identityType, userId, false);
try {
await waitPromise;
const currentUser = {
id: userId,
name: app.globalData.currentUser?.name || '用户',
avatar: app.globalData.currentUser?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang)
};
const to = {
id: targetUser.id,
name: targetUser.name || '用户',
avatar: targetUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang)
};
const param = { to, currentUser };
const path = '/pages/chat/chat?data=' + encodeURIComponent(JSON.stringify(param));
wx.navigateTo({ url: path });
} catch (err) {
console.error('连接失败:', err);
wx.showToast({ title: err.message || '连接失败', icon: 'none' });
throw err;
}
wx.navigateTo({
url: '/pages/chat/chat?data=' + encodeURIComponent(JSON.stringify({ to, currentUser })),
});
}
async connectToGroupChat(params) {
const { identityType, userId, orderId, groupName, groupAvatar, isCross } = params;
const {
identityType, userId, orderId, groupName, groupAvatar, isCross,
partnerUid, fadanPingtai,
} = params;
if (!identityType || !userId || !orderId) {
throw new Error('参数不完整identityType, userId, orderId 必填');
throw new Error('参数不完整');
}
// 校验 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
};
wx.showLoading({ title: '建立联系中...', mask: true });
// 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;
}
}
try {
const chatData = await prepareGroupChat(
identityType, userId, orderId, partnerUid, fadanPingtai || isCross, groupName, isCross
);
if (needConnect) {
wx.removeStorageSync('savedGoEasyConnection');
wx.removeStorageSync('goEasyUserId');
if (app.clearSavedConnection) app.clearSavedConnection();
if (wx.goEasy && wx.goEasy.getConnectionStatus() === 'connected') {
wx.goEasy.disconnect();
const realGroupId = chatData.groupId;
let avatar = chatData.counterpartAvatar || chatData.groupAvatar || groupAvatar || '';
if (avatar && !avatar.startsWith('http')) {
avatar = app.globalData.ossImageUrl + avatar.replace(/^\//, '');
}
const waitPromise = waitForConnection(userId);
app.connectWithIdentity(identityType, userId, false);
if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {};
const meta = {
name: chatData.counterpartName || chatData.groupName || groupName,
avatar,
orderId: chatData.orderId || String(orderId).replace(/^group_/, ''),
orderDesc: chatData.orderDesc || '',
orderZhuangtai: chatData.orderZhuangtai,
orderJine: chatData.orderJine,
dashouGoEasyId: chatData.dashouGoEasyId,
partnerGoEasyId: chatData.partnerGoEasyId,
dashouYonghuid: chatData.dashouYonghuid,
partnerYonghuid: chatData.partnerYonghuid,
dashouName: chatData.dashouName,
partnerName: chatData.partnerName,
dashouAvatar: chatData.dashouAvatar,
partnerAvatar: chatData.partnerAvatar,
counterpartId: chatData.counterpartId,
counterpartYonghuid: chatData.counterpartYonghuid,
};
app.globalData.groupInfoMap[realGroupId] = meta;
persistGroupMeta(app, realGroupId, meta);
let imReady = false;
try {
await waitPromise;
} catch (err) {
wx.showToast({ title: '连接失败', icon: 'none' });
throw err;
await ensureIdentityConnection(identityType, userId);
imReady = true;
try {
await withTimeout(
new Promise((resolve, reject) => {
wx.goEasy.im.subscribeGroup({
groupIds: [realGroupId],
onSuccess: () => resolve(),
onFailed: (error) => reject(error),
});
}),
8000,
'订阅群聊超时'
);
} catch (subErr) {
console.warn('订阅群聊失败,仍尝试进入页面', subErr);
}
} catch (imErr) {
console.warn('IM连接失败仍进入聊天页由页面重连', imErr);
}
}
// 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;
}
const uid = userId.replace(/^(Ds|Sj|Boss)/, '');
const param = {
groupId: realGroupId,
orderId: chatData.orderId || String(orderId).replace(/^group_/, ''),
groupName: chatData.counterpartName || chatData.groupName || groupName || '订单群聊',
groupAvatar: avatar,
isCross: chatData.isCross != null ? chatData.isCross : (isCross || 0),
currentUserId: userId,
currentUserName: app.globalData.currentUser?.name || `用户${uid}`,
currentUserAvatar: app.globalData.currentUser?.avatar ||
(wx.getStorageSync('touxiang') || (app.globalData.ossImageUrl + app.globalData.morentouxiang)),
orderZhuangtai: chatData.orderZhuangtai,
orderJine: chatData.orderJine,
orderDesc: chatData.orderDesc || '',
imReady,
};
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);
}
});
wx.hideLoading();
wx.navigateTo({
url: '/pages/group-chat/group-chat?data=' + encodeURIComponent(JSON.stringify(param)),
fail: (navErr) => {
wx.showToast({ title: formatError(navErr), icon: 'none', duration: 2500 });
},
});
} catch (err) {
wx.showToast({ title: '订阅群组失败', icon: 'none' });
wx.hideLoading();
console.error('跳转群聊失败:', err);
wx.showToast({ title: formatError(err), icon: 'none', duration: 2500 });
throw err;
}
// 5. 跳转群聊页(参数与消息列表完全一致)
const param = {
groupId: realGroupId,
orderId: orderId,
groupName: groupName || '订单群聊',
groupAvatar: groupAvatar || '',
isCross: isCross || 0
};
const path = '/pages/group-chat/group-chat?data=' + encodeURIComponent(JSON.stringify(param));
wx.navigateTo({ url: path });
}
}
export default new ConnectionManager();
export default new ConnectionManager();

53
utils/zhiding-order.js Normal file
View File

@@ -0,0 +1,53 @@
import request from './request.js';
import { getDefaultAvatarUrl } from './avatar.js';
export const ZHIDING_HF_MAP = { 1: '想接', 2: '已婉拒', 3: '等会接' };
export async function fetchMyZhidingOrders() {
try {
const res = await request({ url: '/dingdan/zdcx', method: 'POST', data: {} });
const body = res?.data;
if (body && (body.code === 200 || body.code === 0) && body.data) {
return body.data.list || [];
}
} catch (e) {
console.warn('fetchMyZhidingOrders failed', e);
}
return [];
}
export async function submitZhidingResponse(dingdanId, huifu, dengHuiNote = '') {
const res = await request({
url: '/dingdan/zdhf',
method: 'POST',
data: {
dingdan_id: dingdanId,
huifu,
deng_hui_note: dengHuiNote,
},
});
return res?.data;
}
export function processZhidingList(list, app, uid, highlightOrderId = '') {
const defAvatar = getDefaultAvatarUrl(app);
const oss = app.globalData?.ossImageUrl || '';
return (list || []).map((item) => {
let avatar = defAvatar;
if (item.zhiding_avatar) {
avatar = item.zhiding_avatar.startsWith('http') ? item.zhiding_avatar : oss + item.zhiding_avatar;
}
return {
...item,
isMyZhiding: true,
zhiding_avatar_full: avatar,
zhiding_hf_text: item.zhiding_hf_text || ZHIDING_HF_MAP[item.zhiding_hf] || '',
isHighlighted: highlightOrderId && item.dingdan_id === highlightOrderId,
};
}).filter((item) => String(item.zhiding_uid) === String(uid));
}
/** 横幅只展示尚未回复的指定单(已等会接/不想接的不占横幅) */
export function filterZhidingBannerList(list) {
return (list || []).filter((item) => !item.zhiding_hf);
}