统一排行榜对齐星雀UI,页面资源后台配置实时刷新

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-27 01:26:11 +08:00
parent 1ab2e2080a
commit 21112173f2
292 changed files with 64010 additions and 81 deletions

View File

@@ -2,7 +2,7 @@
import GoEasy from '../static/lib/goeasy-2.13.24.esm.min';
import { jianquanxian } from './imAuth/jianquanxian';
import { parseOrderCardText } from './group-chat.js';
import { persistGroupMeta } from './im-user.js';
import { persistGroupMeta, getFreshImUser } from './im-user.js';
let _globalPrivateHandler = null;
let _globalGroupHandler = null;
@@ -28,9 +28,27 @@ function initGlobalMessageSystem(app) {
loadUserSettings(app);
initNetworkListener(app);
initAppStateListener(app);
checkAndRestoreConnection(app);
bindGoEasyLifecycle(app);
setupEventSystem(app);
setupMessageListeners(app);
// 监听器在连接成功 / onShow 时注册,不在 IM 未就绪时空绑
checkAndRestoreConnection(app);
}
function bindGoEasyLifecycle(app) {
if (!wx.goEasy || app._goEasyLifecycleBound) return;
app._goEasyLifecycleBound = true;
try {
wx.goEasy.on('connected', () => {
app.globalData.goEasyConnection.status = 'connected';
setupMessageListeners(app);
loadConversations(app);
});
wx.goEasy.on('disconnected', () => {
app.globalData.goEasyConnection.status = 'disconnected';
});
} catch (e) {
console.warn('绑定 GoEasy 生命周期失败:', e);
}
}
function getCurrentGoEasyUserId() {
@@ -78,12 +96,8 @@ function initNetworkListener(app) {
function initAppStateListener(app) {
wx.onAppShow(() => {
ensureConnection(app);
updateCurrentPageState(app);
// ✅ 连接正常时,主动刷一次未读角标
if (wx.goEasy.getConnectionStatus && wx.goEasy.getConnectionStatus() === 'connected') {
loadConversations(app);
}
ensureConnection(app);
});
wx.onAppHide(() => {
pauseHeartbeat(app);
@@ -101,25 +115,34 @@ function checkAndRestoreConnection(app) {
const validityHours = app.globalData.goEasyConnection.config.cacheValidityHours || 12;
if (hoursDiff > validityHours) {
clearSavedConnection(app);
} else {
ensureConnection(app);
}
}
ensureConnection(app);
} catch (error) {
console.warn('检查保存的连接信息失败:', error);
}
}
function setupEventSystem(app) {
app.globalData.eventListeners = {};
if (!app.globalData.eventListeners) {
app.globalData.eventListeners = {};
}
}
// 🔥 修改点:不依赖 saved 缓存,只要连接状态正常就刷新角标
function isImConnected() {
if (!wx.goEasy?.getConnectionStatus) return false;
const s = wx.goEasy.getConnectionStatus();
return s === 'connected' || s === 'reconnected';
}
// 进入小程序即尝试连接并监听(不依赖历史缓存)
function ensureConnection(app) {
const connectionStatus = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
if (connectionStatus === 'connected' || connectionStatus === 'reconnected') {
if (!wx.goEasy?.connect) return;
if (app.globalData.chatEnabled === false) return;
if (isImConnected()) {
setupMessageListeners(app);
loadConversations(app); // 每次进入前台都刷新未读
loadConversations(app);
return;
}
@@ -206,9 +229,21 @@ function disconnectGoEasy(app) {
}
async function connectWithIdentity(app, identityType, userId, isAutoRestore = false) {
const quanxian = await jianquanxian(app);
if (!quanxian.allowed) {
return Promise.reject(new Error(quanxian.reason || '无聊天权限'));
if (!wx.goEasy?.connect) {
return Promise.reject(new Error('聊天服务未就绪'));
}
// 冷启动/后台恢复:先连上 IM 才能收消息;完整鉴权留给消息页/发消息
if (!isAutoRestore) {
const quanxian = await jianquanxian(app);
if (!quanxian.allowed) {
return Promise.reject(new Error(quanxian.reason || '无聊天权限'));
}
} else {
const uid = wx.getStorageSync('uid');
if (!uid) {
return Promise.reject(new Error('未登录'));
}
}
app.globalData.goEasyConnection.autoReconnect = true;
@@ -302,7 +337,7 @@ function connectForCurrentRole(app) {
if (currentUserId) {
wx.goEasy.disconnect({ onSuccess: () => {}, onFailed: () => {} });
}
connectWithIdentity(app, role, targetUserId, false);
connectWithIdentity(app, role, targetUserId, true);
}
async function switchRoleAndReconnect(app, newRole) {
@@ -577,7 +612,7 @@ function cacheMessage(app, message) {
}
function loadConversations(app) {
if (!getCurrentGoEasyUserId()) return;
if (!getCurrentGoEasyUserId() || !wx.goEasy?.im?.latestConversations) return;
wx.goEasy.im.latestConversations({
onSuccess: (result) => {
if (result.unreadTotal !== undefined) {

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,39 @@ const ZHUANGTAI_MAP = {
5: '已退款', 6: '退款失败', 7: '指定中', 8: '结算中',
};
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);
}
return orderId ? `group_${orderId}` : null;
}
export function isPairGroupId(groupId) {
return /^group_Ds\w+_(Sj|Boss)\w+$/.test(groupId || '');
}
@@ -108,6 +141,8 @@ export function parseOrderCardText(text) {
}
}
import { getChatImageUrl } from '../static/lib/utils.js';
export function normalizeGroupMessage(msg) {
if (!msg) return msg;
if (msg.type === 'text' && msg.payload && msg.payload.text) {
@@ -117,6 +152,26 @@ export function normalizeGroupMessage(msg) {
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;
}

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

View File

@@ -0,0 +1,67 @@
// utils/imAuth/imRequest.js
// 专用于 IM 鉴权模块的请求封装
// 与 utils/request.js 功能完全一致,但不依赖 getApp()API 域名硬编码
const API_BASE_URL = 'https://6ccy.top/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) {
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.switchTab({ url: '/pages/wode/wode' });
}
},
});
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 };

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

View File

@@ -3,6 +3,11 @@ const app = getApp();
import { formatDate } from '../static/lib/utils';
import request from './request.js';
import { getFreshImUser } from './im-user.js';
import { sendGoEasyImage } from './chatImageSend.js';
function isCrossPlatformOrder(isCross) {
return isCross === 1 || isCross === true || String(isCross) === '1';
}
function sendGroupMessage(options) {
const { msgData, onSuccess, onSendStart } = options;
@@ -20,8 +25,7 @@ function sendGroupMessage(options) {
const localMsg = buildLocalMessage(msgData);
if (onSendStart) onSendStart(localMsg);
// 只有 isCross 明确为 0 才走前端 SDK其余一律走后端
if (isCross === 0) {
if (!isCrossPlatformOrder(isCross)) {
sendViaSDK(localMsg, currentUser, groupId, onSuccess, orderId, isCross, groupName, groupAvatar);
return;
}
@@ -32,7 +36,7 @@ function buildLocalMessage(data) {
const { type, text, filePath, orderPayload, currentUser, groupId } = data;
const now = Date.now();
const base = {
messageId: `${type}-${now}`,
messageId: `local-${type}-${now}`,
timestamp: now,
senderId: currentUser.id,
groupId: groupId,
@@ -45,7 +49,7 @@ function buildLocalMessage(data) {
};
if (type === 'text') return { ...base, type: 'text', payload: { text } };
if (type === 'image') return { ...base, type: 'image', payload: { url: filePath } };
if (type === 'image') return { ...base, type: 'image', payload: { url: filePath }, imageUrl: filePath };
if (type === 'order') return { ...base, type: 'order', payload: orderPayload };
return base;
}
@@ -66,6 +70,7 @@ function sendViaSDK(localMsg, currentUser, groupId, onSuccess, orderId, isCross,
dashouAvatar: groupMeta.dashouAvatar,
partnerAvatar: groupMeta.partnerAvatar,
orderDesc: groupMeta.orderDesc,
orderZhuangtai: groupMeta.orderZhuangtai,
};
const to = {
@@ -74,18 +79,35 @@ function sendViaSDK(localMsg, currentUser, groupId, onSuccess, orderId, isCross,
data: toData
};
if (localMsg.type === 'image') {
sendGoEasyImage({
file: localMsg.payload.url,
to,
onSuccess: (res) => {
const serverMsg = res && res.content;
if (serverMsg && onSuccess) {
onSuccess(localMsg.messageId, 'success', serverMsg);
} else if (onSuccess) {
onSuccess(localMsg.messageId, 'success');
}
},
onFailed: () => { if (onSuccess) onSuccess(localMsg.messageId, 'failed'); }
});
return;
}
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'); },
onSuccess: (res) => {
if (onSuccess) onSuccess(localMsg.messageId, 'success', res && res.content);
},
onFailed: () => { if (onSuccess) onSuccess(localMsg.messageId, 'failed'); }
});
}
@@ -93,18 +115,17 @@ function sendViaSDK(localMsg, currentUser, groupId, onSuccess, orderId, isCross,
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 prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' };
const identityType = role === 'dashou' ? 'dashou' : (role === 'shangjia' ? 'shangjia' : 'boss');
const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' };
const prefix = prefixMap[role] || 'Boss';
const senderId = prefix + uid;
const freshUser = getFreshImUser(app, role, uid);
const params = {
orderId: orderId,
groupId: groupId,
identityType: identityType, // 告诉后端当前是什么身份
identityType: identityType,
senderId: senderId,
senderName: freshUser.name || currentUser.name || ('用户' + uid),
senderAvatar: freshUser.avatar || currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
@@ -135,7 +156,6 @@ function sendViaBackend(localMsg, currentUser, groupId, orderId, onSuccess) {
return;
}
// 图片消息
if (localMsg.type === 'image') {
const token = wx.getStorageSync('token');
const formData = {
@@ -169,4 +189,4 @@ function sendViaBackend(localMsg, currentUser, groupId, orderId, onSuccess) {
}
}
module.exports = { sendGroupMessage };
module.exports = { sendGroupMessage };

View File

@@ -0,0 +1,76 @@
/** 从 globalData.pageAssets / paihangAssets 拼 OSS 完整 URL */
export function buildOssUrl(path) {
if (!path) return ''
if (String(path).startsWith('http')) return path
const g = getApp().globalData
const oss = (g.ossImageUrl || '').replace(/\/$/, '')
if (!oss) return ''
return `${oss}/${String(path).replace(/^\//, '')}`
}
export function getPaihangImgUrls() {
const g = getApp().globalData
const assets = g.paihangAssets || {}
const def = {
pageBg: 'beijing/paihangbang/page-bg.jpg',
cardBg: 'beijing/paihangbang/list-card-bg.png',
emptyIcon: 'beijing/paihangbang/empty.png',
refreshIcon: 'beijing/paihangbang/icon-refresh.png',
}
return {
pageBg: buildOssUrl(assets.pageBg || def.pageBg),
cardBg: buildOssUrl(assets.cardBg || def.cardBg),
emptyIcon: buildOssUrl(assets.emptyIcon || def.emptyIcon),
refreshIcon: buildOssUrl(assets.refreshIcon || def.refreshIcon),
}
}
/** 按资源组从后台配置读取图标/背景(无配置时用 defaultPaths */
export function getGroupImgUrls(group, defaultPaths) {
const cfg = getApp().globalData.pageAssets?.[group] || {}
const urls = {}
Object.keys(defaultPaths).forEach((key) => {
urls[key] = buildOssUrl(cfg[key] || defaultPaths[key])
})
return urls
}
export const SHANGJIADUAN_DEFAULTS = {
pageBg: 'beijing/shangjiaduan/page_bg.png',
avatarCardBg: 'beijing/shangjiaduan/avatar_card_bg.png',
assetCardBg: 'beijing/shangjiaduan/asset_card_bg.png',
statCardBg: 'beijing/shangjiaduan/stat_card_bg.png',
funcItemBg: 'beijing/shangjiaduan/func_item_bg.png',
primaryFuncBg: 'beijing/shangjiaduan/primary_func_bg.png',
highlightFuncBg: 'beijing/shangjiaduan/highlight_func_bg.png',
iconRelease: 'beijing/shangjiaduan/icon_release.png',
iconFast: 'beijing/shangjiaduan/icon_fast.png',
iconOrders: 'beijing/shangjiaduan/icon_orders.png',
iconService: 'beijing/shangjiaduan/icon_service.png',
iconPunish: 'beijing/shangjiaduan/icon_punish.png',
iconRank: 'beijing/shangjiaduan/icon_rank.png',
iconRecharge: 'beijing/shangjiaduan/icon_recharge.png',
iconWithdraw: 'beijing/shangjiaduan/icon_withdraw.png',
iconRefresh: 'beijing/shangjiaduan/icon_refresh.png',
iconCopy: 'beijing/shangjiaduan/icon_copy.png',
avatarFrame: 'beijing/shangjiaduan/avatar_frame.png',
}
export const GUANSHIDUAN_DEFAULTS = {
pageBg: 'beijing/guanshiduan/page-bg.jpg',
topDecor: 'beijing/guanshiduan/top-decor.png',
cardBg1: 'beijing/guanshiduan/card-bg1.png',
cardBg2: 'beijing/guanshiduan/card-bg2.png',
iconRefresh: 'beijing/guanshiduan/icon-refresh.png',
iconCopy: 'beijing/guanshiduan/icon-copy.png',
avatarDefault: 'beijing/morentouxiang.jpg',
iconInvite: 'beijing/guanshiduan/icon-invite.png',
iconSub: 'beijing/guanshiduan/icon-sub.png',
iconRecord: 'beijing/guanshiduan/icon-record.png',
iconRank: 'beijing/guanshiduan/icon-rank.png',
iconPoster: 'beijing/guanshiduan/icon-poster.png',
iconWithdraw: 'beijing/guanshiduan/icon-withdraw.png',
iconContact: 'beijing/guanshiduan/icon-contact.png',
iconArrow: 'beijing/guanshiduan/icon-arrow.png',
}

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://6ccy.top/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 };

View File

@@ -0,0 +1,66 @@
// utils/request.js
const app = getApp();
function request(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: app.globalData.apiBaseUrl + options.url,
method: options.method,
data: options.data || {},
header,
success: async (res) => {
if (res.statusCode === 401 || res.statusCode === 403) {
// 如果当前正在执行静默登录,等待登录完成后重试一次
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.reLaunch({ 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;

View File

View File

@@ -0,0 +1,147 @@
// utils/scriptService.js — 话术拉取、确认弹窗、客服自动回复匹配
import request from './request.js';
const DEFAULT_SCRIPTS = {
chat_image_confirm: {
title: '温馨提示',
content: '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。严禁赌博、诈骗及其他非法行为,违者后果自负。',
confirm_text: '我已知晓,继续发送',
cancel_text: '取消',
},
cs_welcome: {
content: '你好,请问有什么可以帮到您的?',
},
merchant_dispatch_confirm: {
title: '派单确认',
content: '请确认订单信息无误。派单成功后将扣除相应余额,请确保订单描述合法合规。',
confirm_text: '确认派单',
cancel_text: '取消',
},
};
const textCache = {};
const TEXT_CACHE_TTL = 5 * 60 * 1000;
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) {
const result = {};
sceneKeys.forEach((key) => {
result[key] = mergeScript(key, cached[key]);
});
return result;
}
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) => {
const raw = payload[key];
const normalized = raw && raw.item_type === 'auto_reply' ? raw : raw;
textCache[key] = { time: now, data: normalized };
cached[key] = normalized;
});
} catch (e) {
console.warn('fetchScripts failed', e);
}
const result = {};
sceneKeys.forEach((key) => {
result[key] = mergeScript(key, cached[key]);
});
return result;
}
export function showConfirmByScene(sceneKey, onConfirm, onCancel) {
fetchScripts([sceneKey]).then((data) => {
const script = data[sceneKey] || DEFAULT_SCRIPTS[sceneKey] || {};
wx.showModal({
title: script.title || '提示',
content: script.content || '',
confirmText: script.confirm_text || '确定',
cancelText: script.cancel_text || '取消',
success: (res) => {
if (res.confirm) {
onConfirm && onConfirm();
} else {
onCancel && onCancel();
}
},
fail: () => {
onCancel && onCancel();
},
});
}).catch(() => {
const script = DEFAULT_SCRIPTS[sceneKey] || {};
wx.showModal({
title: script.title || '提示',
content: script.content || '',
confirmText: script.confirm_text || '确定',
cancelText: script.cancel_text || '取消',
success: (res) => {
if (res.confirm) onConfirm && onConfirm();
else onCancel && onCancel();
},
});
});
}
export async function matchAutoReply(userText) {
if (!userText || !userText.trim()) {
return { matched: false, content: '' };
}
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: '' };
}
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

@@ -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