本地备份:主包优化前完整版本,可随时回退

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-27 05:41:55 +08:00
parent a751708d6e
commit 7f4f279b3f
124 changed files with 15837 additions and 21958 deletions

View File

@@ -12,6 +12,7 @@ import {
refreshTabBar,
} from './role-tab-bar.js';
import { lockPrimaryRole, migrateLegacyCenterRole, enterLockedRole } from './primary-role.js';
import { isMerchantPortalUser } from './staff-api.js';
import { resolveAvatarFromStorage } from './avatar.js';
import { isApiSuccess, getApiMsg, extractUserData, parseSceneOptions } from './api-helper.js';
@@ -296,7 +297,10 @@ function createRoleCenterMixin(roleConfig) {
try {
const status = wx.getStorageSync(statusKey);
const uid = wx.getStorageSync('uid');
if (isRoleStatusActive(status)) {
const active = role === 'shangjia'
? isMerchantPortalUser()
: isRoleStatusActive(status);
if (active) {
this.setData({ [dataFlag]: true, uid: uid || '' });
this.loadAvatar();
if (this.checkGlobalData) this.checkGlobalData();

View File

@@ -165,13 +165,15 @@ function connectGoEasy(app, userId, identityType) {
loadConversations(app);
},
onFailed: (error) => {
console.error('GoEasy连接失败', error);
if (error.code === 408) {
// 408 多为 GoEasy 节点超时,开发者工具常见,不影响订单等业务接口
if (error && error.code === 408) {
console.warn('GoEasy 连接超时(408),将使用本地会话缓存');
bindGlobalListenersIfNeeded(app, userId);
loadConversations(app);
} else {
wx.showToast({ title: '连接失败,请重试', icon: 'none' });
return;
}
console.warn('GoEasy连接失败', error);
wx.showToast({ title: '消息服务连接失败', icon: 'none' });
},
});
}

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@@ -0,0 +1,31 @@
/**
* 从服务端刷新打手会员/押金/积分等到 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 || [];
}
/** 用户是否持有某会员类型(与抢单卡片判断一致) */
export function userHasHuiyuan(huiyuanList, huiyuanId) {
if (!huiyuanId) return true;
const list = huiyuanList || [];
return list.some((h) => String(h.huiyuanid) === String(huiyuanId));
}

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

View File

@@ -5,6 +5,8 @@ import {
getSessionToken,
isWechatLoginSuccess,
} from './role-tab-bar';
import { restoreStaffContextAfterAuth } from './staff-api.js';
import { CLUB_API, getClubId, setClubId, buildClubHeaders } from './club-context';
const app = getApp();
@@ -14,15 +16,14 @@ export function ensureLogin(options = {}) {
const token = getSessionToken(app);
if (token) {
backfillUserProfileCache(app);
restoreStaffContextAfterAuth();
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 });
@@ -34,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,
});
@@ -47,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 || '登录失败';
@@ -62,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',
},
});
}

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

@@ -0,0 +1,183 @@
/** 小程序可配置图标(启动时从 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',
MINE_PINDAO: 'mine_pindao',
ICON_REFRESH: 'icon_refresh',
};
/** 仅后台上传后才展示,不使用 CDN/本地兜底 */
export const CONFIG_ONLY_ICON_KEYS = new Set([
ICON_KEYS.FIGHTER_RECHARGE_MEMBER,
ICON_KEYS.FIGHTER_RECHARGE_DEPOSIT,
]);
/** 逍遥梦原版金牌横幅(硬编码时代 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';
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) return 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: () => {} });
});
}
/** 打开频道前拉取最新 qdpzhq 配置(避免本地缓存无 pindao 数据) */
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 };
}
/** 后台静默预热频道(不打断 UI */
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,57 +1,41 @@
/**
* 手机号强制认证 - 完全自包含逻辑模块
*
* 特点:
* 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);
* 手机号强制认证:每次调用 check / ensurePhoneAuth 均请求后端 /peizhi/yhbdsjh
*/
// ==================== 基础配置 ====================
const API_BASE = 'https://www.abas.asia/hqhd'; // 与你 app.js 里的 apiBaseUrl 一致
const AUTH_CHECK_URL = '/peizhi/yhbdsjh'; // 检查是否需要认证
const AUTH_SUBMIT_URL = '/yonghu/xiugai'; // 提交认证(复用修改信息接口)
const API_BASE = 'https://www.abas.asia/hqhd';
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;
}
}
// ==================== 内部封装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);
}
header: buildHeaders(),
success: (res) => 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');
@@ -60,7 +44,6 @@ function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
return;
}
// 先验证文件是否存在
wx.getFileInfo({
filePath,
success: () => {
@@ -69,15 +52,11 @@ function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
filePath,
name: fileName,
formData,
header: {
'Authorization': 'Bearer ' + token
},
header: buildHeaders({ Authorization: 'Bearer ' + token }),
success: (res) => {
// ✅ 修复wx.uploadFile 成功时 statusCode 为 200
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data);
resolve(data); // 直接返回解析后的对象,方便下游使用
resolve(JSON.parse(res.data));
} catch (e) {
reject(new Error('解析响应数据失败'));
}
@@ -88,78 +67,142 @@ function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
reject(new Error(`上传失败,状态码:${res.statusCode}`));
}
},
fail: (err) => {
reject(new Error('网络请求失败'));
}
fail: () => reject(new Error('网络请求失败')),
});
},
fail: () => {
reject(new Error('文件不存在或无法访问'));
}
fail: () => reject(new Error('文件不存在或无法访问')),
});
});
}
// ==================== 公开方法 ====================
function isOnPhoneAuthPage() {
const pages = getCurrentPages();
const cur = pages[pages.length - 1];
return !!(cur && cur.route === 'pages/phone-auth/phone-auth');
}
function parseCheckResponse(res) {
if (!res || res.statusCode === 401) {
return { ok: false, needAuth: false };
}
if (res.statusCode !== 200) {
return { ok: false, needAuth: false };
}
const body = res.data;
if (!body || typeof body !== 'object') {
return { ok: false, needAuth: false };
}
const code = Number(body.code);
if (code !== 0 && code !== 200) {
return { ok: false, needAuth: false };
}
const needAuth = body.need_auth === true
|| (body.data && body.data.need_auth === true);
return { ok: true, needAuth: !!needAuth };
}
/** 根据当前主端/身份推断认证完成后回跳角色 */
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';
}
/**
* 检查是否需要手机号认证
* - 无 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;
const parsed = parseCheckResponse(res);
if (!parsed.ok) return false;
return parsed.needAuth;
} catch (e) {
// 任何错误(网络、后端异常)都吞掉,不影响主流程
return false;
}
}
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 });
}
}
/**
* 提交认证(手机号 code + 头像文件)
* @param {string} phoneCode 微信 getPhoneNumber 返回的 code
* @param {string} avatarPath 头像临时路径
* @returns {Promise<object>} 后端返回的完整数据
* 向后端确认是否需要手机号认证;需要则跳转认证页
* @returns {Promise<boolean>} true=无需认证或已满足false=已跳转认证页
*/
async function ensurePhoneAuth(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 || resolveReturnRole(), returnPage);
}
return false;
}
function getPhoneAuthReturnPage() {
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);
return defaultReturnPage(role);
}
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, {
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);
}
if (respData.phone) wx.setStorageSync('phone', respData.phone);
if (respData.touxiang) wx.setStorageSync('touxiang', respData.touxiang);
return respData;
} else {
throw new Error((data && data.msg) || '认证失败');
}
throw new Error((data && data.msg) || '认证失败');
}
module.exports = { check, submit };
module.exports = {
check,
submit,
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,8 @@
/** 三端固定normal | dashou | shangjia认证后不可回退 */
import { isStaffMode } from './staff-api.js';
import { ensurePhoneAuth } from './phone-auth.js';
const VALID = ['normal', 'dashou', 'shangjia'];
const LEGACY_CENTER_ROLES = ['guanshi', 'zuzhang', 'kaoheguan'];
@@ -53,6 +56,9 @@ export function getPrimaryRole(app) {
if (isRoleStatusActive(wx.getStorageSync('shangjiastatus'))) {
return 'shangjia';
}
if (isStaffMode()) {
return 'shangjia';
}
return 'normal';
}
@@ -87,8 +93,7 @@ export const PRIMARY_DEFAULT_PAGES = {
};
/** 锁定主端并跳转到该端默认首页,同时建立 IM 长连接 */
export function enterLockedRole(role, app) {
if (!VALID.includes(role) || role === 'normal') return false;
function finishEnterLockedRole(role, app) {
app = app || getApp();
lockPrimaryRole(role, app);
wx.reLaunch({ url: PRIMARY_DEFAULT_PAGES[role] });
@@ -103,3 +108,14 @@ export function enterLockedRole(role, app) {
}, 600);
return true;
}
export function enterLockedRole(role, app) {
if (!VALID.includes(role) || role === 'normal') return false;
if (role === 'dashou' || role === 'shangjia') {
ensurePhoneAuth({ redirect: true, role }).then((ok) => {
if (ok) finishEnterLockedRole(role, app);
});
return true;
}
return finishEnterLockedRole(role, app);
}

View File

@@ -1,74 +1,83 @@
// utils/request.js
const app = getApp();
function request(options) {
const token = wx.getStorageSync('token');
// 🔥 没有 token 时不发送请求,直接返回一个“空响应”对象,不影响后续代码
if (!token) {
// 返回一个模拟的响应,状态码为 401但不会触发任何弹窗或跳转
return Promise.resolve({
statusCode: 401,
data: { code: 401, msg: '未登录' },
errMsg: 'request:ok'
});
}
const header = {
'Content-Type': 'application/json',
...options.header,
};
if (token) {
header['Authorization'] = 'Bearer ' + token;
}
return new Promise((resolve, reject) => {
wx.request({
url: app.globalData.apiBaseUrl + options.url,
method: options.method,
data: options.data || {},
header,
success: async (res) => {
if (res.statusCode === 401 || res.statusCode === 403) {
// 注意:这里的弹窗逻辑只会在有 token 且 token 失效时触发
// 无 token 的情况已经在上面直接返回,不会走到这里
if (app.globalData.loginPromise) {
try {
await app.globalData.loginPromise;
const retryRes = await request(options);
resolve(retryRes);
} catch (err) {
wx.showModal({
content: '操作失败,请先登录',
success: (modalRes) => {
if (modalRes.confirm) {
wx.removeStorageSync('token');
wx.switchTab({ url: '/pages/mine/mine' });
}
},
});
resolve(res);
}
return;
}
wx.showModal({
content: '操作失败,请先登录',
success: (modalRes) => {
if (modalRes.confirm) {
wx.removeStorageSync('token');
wx.reLaunch({ url: '/pages/mine/mine' });
}
},
});
resolve(res);
return;
}
resolve(res);
},
fail: reject,
});
});
}
export default request;
// utils/request.js
import { buildClubHeaders } from './club-context';
function request(options) {
const app = getApp();
const token = wx.getStorageSync('token');
if (!token) {
return Promise.resolve({
statusCode: 401,
data: { code: 401, msg: '未登录' },
errMsg: 'request:ok'
});
}
const header = buildClubHeaders({
'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 || 'GET',
data: options.data || {},
header,
timeout: options.timeout || 15000,
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.switchTab({ url: '/pages/mine/mine' });
}
},
});
resolve(res);
}
return;
}
wx.showModal({
content: '操作失败,请先登录',
success: (modalRes) => {
if (modalRes.confirm) {
wx.removeStorageSync('token');
wx.reLaunch({ url: '/pages/mine/mine' });
}
},
});
resolve(res);
return;
}
resolve(res);
},
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;

View File

@@ -1,255 +1,271 @@
import { clearPrimaryRole } from './primary-role';
import { getLocalImUserId } from './im-user.js';
export function isWechatLoginSuccess(body) {
if (!body) return false;
const code = Number(body.code);
return code === 0 || code === 200;
}
const SESSION_STATUS_KEYS = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus'];
/** 读取 tokenStorage 优先globalData 兜底并回写 Storage */
export function getSessionToken(app) {
app = app || getApp();
let token = wx.getStorageSync('token') || app.globalData.token || '';
if (token && !wx.getStorageSync('token')) {
wx.setStorageSync('token', token);
}
return token;
}
export function isUserLoggedIn(app) {
return !!getSessionToken(app);
}
/** 从 Storage / globalData 汇总展示用用户信息(兼容其他端注册后 nick 未落盘) */
export function getUserProfileForDisplay(app) {
app = app || getApp();
const token = getSessionToken(app);
const uid = wx.getStorageSync('uid') || app.globalData.uid || '';
let nick = wx.getStorageSync('nicheng') || app.globalData.nicheng || '';
if (!nick && app.globalData.dashouNicheng) {
nick = app.globalData.dashouNicheng;
}
const tx = wx.getStorageSync('touxiang') || '';
const def = app.globalData.morentouxiang || '';
const oss = app.globalData.ossImageUrl || '';
let avatarUrl = '';
if (tx) {
avatarUrl = tx.startsWith('http') ? tx : oss + tx;
} else if (def) {
avatarUrl = def.startsWith('http') ? def : oss + def;
}
return {
nick,
uid,
avatarUrl,
isLoggedIn: !!token,
};
}
/** 其他端登录/注册成功后,把 globalData 里已有字段补写回 Storage */
export function backfillUserProfileCache(app) {
app = app || getApp();
if (!isUserLoggedIn()) return;
const profile = getUserProfileForDisplay(app);
if (profile.nick && !wx.getStorageSync('nicheng')) {
wx.setStorageSync('nicheng', profile.nick);
app.globalData.nicheng = profile.nick;
}
if (profile.uid && !wx.getStorageSync('uid')) {
wx.setStorageSync('uid', profile.uid);
}
}
export function applyWechatLoginData(data, page) {
if (!data || typeof data !== 'object') return;
const app = getApp();
syncProfileFields(data);
if (data.token) {
wx.setStorageSync('token', data.token);
}
syncRoleStatuses(data);
syncGroupFields(data);
if (data.zuzhangstatus !== undefined) {
wx.setStorageSync('zuzhangstatus', data.zuzhangstatus);
app.globalData.zuzhangstatus = data.zuzhangstatus;
}
if (data.kaoheguanstatus !== undefined) {
wx.setStorageSync('kaoheguanstatus', data.kaoheguanstatus);
app.globalData.kaoheguanstatus = data.kaoheguanstatus;
}
if (data.dingdantiaoshu) {
app.globalData.dingdanTiaoshu = {
daifuwu: data.dingdantiaoshu.daifuwu || 0,
fuwuzhong: data.dingdantiaoshu.fuwuzhong || 0,
yiwancheng: data.dingdantiaoshu.yiwancheng || 0,
yituikuan: data.dingdantiaoshu.yituikuan || 0,
};
}
if (data.isJinpai !== undefined) {
wx.setStorageSync('isJinpai', Number(data.isJinpai));
}
const targetPage = page || _getCurrentPage();
refreshTabBar(targetPage);
}
function _getCurrentPage() {
const pages = getCurrentPages();
return pages.length ? pages[pages.length - 1] : null;
}
export function isRoleStatusActive(status) {
return Number(status) === 1;
}
export function isCenterPageActive(page, dataFlag, statusKey) {
return page.data[dataFlag] || isRoleStatusActive(wx.getStorageSync(statusKey));
}
export function syncRoleStatuses(data) {
if (!data || typeof data !== 'object') return;
const app = getApp();
const roleFields = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus'];
roleFields.forEach((field) => {
if (Object.prototype.hasOwnProperty.call(data, field)) {
const val = Number(data[field]);
const normalized = Number.isNaN(val) ? 0 : val;
wx.setStorageSync(field, normalized);
app.globalData[field] = normalized;
}
});
}
export function syncProfileFields(data) {
if (!data || typeof data !== 'object') return;
const app = getApp();
['nicheng', 'uid', 'touxiang', 'token'].forEach((field) => {
if (data[field] !== undefined && data[field] !== null) {
wx.setStorageSync(field, data[field]);
app.globalData[field] = data[field];
}
});
}
/** 清除缓存:清空登录与身份,回到点单端「我的」 */
export function clearCacheAndEnterNormal(page) {
resetGuestSession(page, { skipPageReset: true });
wx.reLaunch({ url: '/pages/mine/mine' });
}
/** 清除缓存并重置为点单端(不跳转,供当前已在点单端时使用) */
export function resetGuestSession(page, options = {}) {
const app = getApp();
const configKey = app.CONFIG_CACHE_KEY || 'app_dynamic_config';
let configCache = null;
try {
configCache = wx.getStorageSync(configKey);
} catch (e) {}
wx.clearStorageSync();
if (configCache && typeof configCache === 'object') {
wx.setStorageSync(configKey, configCache);
}
clearPrimaryRole(app);
app.globalData.token = '';
app.globalData.nicheng = '';
app.globalData.uid = '';
app.globalData.loginPromise = null;
app.globalData.currentUser = null;
app.globalData.dashouNicheng = '';
app.globalData.dingdanTiaoshu = { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 };
app.globalData.dashouqun = '';
app.globalData.dashouqunid = '';
app.globalData.guanshiqun = '';
app.globalData.guanshiqunid = '';
SESSION_STATUS_KEYS.forEach((key) => {
app.globalData[key] = 0;
});
app.globalData.shangpinleixing = [];
app.globalData.shangpinzhuanqu = [];
app.globalData.shangpinliebiao = [];
app.emitEvent('currentRoleChanged', { role: 'normal' });
refreshTabBar(page);
if (!options.skipPageReset && page && page.setData) {
page.setData({
avatarUrl: '',
userNicheng: '',
userUid: '',
showLoginBtn: true,
orderCounts: { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 },
dashouCertified: false,
shangjiaCertified: false,
});
}
}
export function syncGroupFields(data) {
if (!data || typeof data !== 'object') return;
const app = getApp();
['dashouqun', 'dashouqunid', 'guanshiqun', 'guanshiqunid'].forEach((field) => {
if (data[field] !== undefined) {
wx.setStorageSync(field, data[field]);
app.globalData[field] = data[field];
}
});
}
export function refreshTabBar(page) {
const tryRefresh = () => {
const tabBar = page.getTabBar && page.getTabBar();
if (tabBar && tabBar.refresh) tabBar.refresh();
};
tryRefresh();
[50, 150, 300, 500, 800].forEach((delay) => setTimeout(tryRefresh, delay));
}
export function setRoleAndRefreshTabBar(page, role) {
const app = getApp();
if (app.setCurrentRole) {
app.setCurrentRole(role);
} else {
app.globalData.currentRole = role;
wx.setStorageSync('currentRole', role);
app.emitEvent('currentRoleChanged', { role });
}
refreshTabBar(page);
}
export function reconnectForRole(role) {
const app = getApp();
if (!wx.getStorageSync('uid')) return;
const targetUserId = getLocalImUserId(app);
if (!targetUserId) return;
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
const currentUserId = wx.goEasy?.im?.userId || '';
const connected = status === 'connected' || status === 'reconnected';
if (connected && currentUserId === targetUserId) {
if (app.loadConversations) app.loadConversations();
if (app.ensureConnection) app.ensureConnection();
return;
}
if (app.switchRoleAndReconnect) {
app.switchRoleAndReconnect(role).catch(() => {
if (app.ensureConnection) app.ensureConnection();
});
} else if (app.connectForCurrentRole) {
app.connectForCurrentRole();
} else if (app.ensureConnection) {
app.ensureConnection();
}
}
export function ensureRoleOnCenterPage(page, role) {
setRoleAndRefreshTabBar(page, role);
reconnectForRole(role);
}
import { clearPrimaryRole } from './primary-role';
import { getLocalImUserId } from './im-user.js';
import { applyStaffFromLoginData, isStaffMode, restoreStaffContextAfterAuth, clearStaffSession } from './staff-api.js';
export function isWechatLoginSuccess(body) {
if (!body) return false;
const code = Number(body.code);
return code === 0 || code === 200;
}
const SESSION_STATUS_KEYS = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus'];
/** 读取 tokenStorage 优先globalData 兜底并回写 Storage */
export function getSessionToken(app) {
app = app || getApp();
let token = wx.getStorageSync('token') || app.globalData.token || '';
if (token && !wx.getStorageSync('token')) {
wx.setStorageSync('token', token);
}
return token;
}
export function isUserLoggedIn(app) {
return !!getSessionToken(app);
}
/** 从 Storage / globalData 汇总展示用用户信息(兼容其他端注册后 nick 未落盘) */
export function getUserProfileForDisplay(app) {
app = app || getApp();
const token = getSessionToken(app);
const uid = wx.getStorageSync('uid') || app.globalData.uid || '';
let nick = wx.getStorageSync('nicheng') || app.globalData.nicheng || '';
if (!nick && app.globalData.dashouNicheng) {
nick = app.globalData.dashouNicheng;
}
const tx = wx.getStorageSync('touxiang') || '';
const def = app.globalData.morentouxiang || '';
const oss = app.globalData.ossImageUrl || '';
let avatarUrl = '';
if (tx) {
avatarUrl = tx.startsWith('http') ? tx : oss + tx;
} else if (def) {
avatarUrl = def.startsWith('http') ? def : oss + def;
}
return {
nick,
uid,
avatarUrl,
isLoggedIn: !!token,
};
}
/** 其他端登录/注册成功后,把 globalData 里已有字段补写回 Storage */
export function backfillUserProfileCache(app) {
app = app || getApp();
if (!isUserLoggedIn()) return;
const profile = getUserProfileForDisplay(app);
if (profile.nick && !wx.getStorageSync('nicheng')) {
wx.setStorageSync('nicheng', profile.nick);
app.globalData.nicheng = profile.nick;
}
if (profile.uid && !wx.getStorageSync('uid')) {
wx.setStorageSync('uid', profile.uid);
}
}
export function applyWechatLoginData(data, page) {
if (!data || typeof data !== 'object') return;
const app = getApp();
syncProfileFields(data);
if (data.token) {
wx.setStorageSync('token', data.token);
}
syncRoleStatuses(data);
syncGroupFields(data);
if (data.zuzhangstatus !== undefined) {
wx.setStorageSync('zuzhangstatus', data.zuzhangstatus);
app.globalData.zuzhangstatus = data.zuzhangstatus;
}
if (data.kaoheguanstatus !== undefined) {
wx.setStorageSync('kaoheguanstatus', data.kaoheguanstatus);
app.globalData.kaoheguanstatus = data.kaoheguanstatus;
}
if (data.dingdantiaoshu) {
app.globalData.dingdanTiaoshu = {
daifuwu: data.dingdantiaoshu.daifuwu || 0,
fuwuzhong: data.dingdantiaoshu.fuwuzhong || 0,
yiwancheng: data.dingdantiaoshu.yiwancheng || 0,
yituikuan: data.dingdantiaoshu.yituikuan || 0,
};
}
if (data.isJinpai !== undefined) {
wx.setStorageSync('isJinpai', Number(data.isJinpai));
}
applyStaffFromLoginData(data);
const targetPage = page || _getCurrentPage();
refreshTabBar(targetPage);
restoreStaffContextAfterAuth().finally(() => {
refreshTabBar(targetPage);
if (app.emitEvent) {
app.emitEvent('staffContextChanged', {});
}
});
}
function _getCurrentPage() {
const pages = getCurrentPages();
return pages.length ? pages[pages.length - 1] : null;
}
export function isRoleStatusActive(status) {
return Number(status) === 1;
}
export function isCenterPageActive(page, dataFlag, statusKey) {
return page.data[dataFlag] || isRoleStatusActive(wx.getStorageSync(statusKey)) || isStaffMode();
}
export function syncRoleStatuses(data) {
if (!data || typeof data !== 'object') return;
const app = getApp();
const roleFields = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus'];
roleFields.forEach((field) => {
if (Object.prototype.hasOwnProperty.call(data, field)) {
const val = Number(data[field]);
const normalized = Number.isNaN(val) ? 0 : val;
wx.setStorageSync(field, normalized);
app.globalData[field] = normalized;
}
});
}
export function syncProfileFields(data) {
if (!data || typeof data !== 'object') return;
const app = getApp();
['nicheng', 'uid', 'touxiang', 'token'].forEach((field) => {
if (data[field] !== undefined && data[field] !== null) {
wx.setStorageSync(field, data[field]);
app.globalData[field] = data[field];
}
});
}
/** 清除缓存:清空登录与身份,回到点单端「我的」 */
export function clearCacheAndEnterNormal(page) {
resetGuestSession(page, { skipPageReset: true });
wx.reLaunch({ url: '/pages/mine/mine' });
}
/** 清除缓存并重置为点单端(不跳转,供当前已在点单端时使用) */
export function resetGuestSession(page, options = {}) {
const app = getApp();
const configKey = app.CONFIG_CACHE_KEY || 'app_dynamic_config';
let configCache = null;
try {
configCache = wx.getStorageSync(configKey);
} catch (e) {}
wx.clearStorageSync();
clearStaffSession();
if (configCache && typeof configCache === 'object') {
wx.setStorageSync(configKey, configCache);
}
clearPrimaryRole(app);
app.globalData.token = '';
app.globalData.nicheng = '';
app.globalData.uid = '';
app.globalData.loginPromise = null;
app.globalData.currentUser = null;
app.globalData.dashouNicheng = '';
app.globalData.dingdanTiaoshu = { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 };
app.globalData.dashouqun = '';
app.globalData.dashouqunid = '';
app.globalData.guanshiqun = '';
app.globalData.guanshiqunid = '';
SESSION_STATUS_KEYS.forEach((key) => {
app.globalData[key] = 0;
});
app.globalData.shangpinleixing = [];
app.globalData.shangpinzhuanqu = [];
app.globalData.shangpinliebiao = [];
app.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);
if (!options.skipPageReset && page && page.setData) {
page.setData({
avatarUrl: '',
userNicheng: '',
userUid: '',
showLoginBtn: true,
orderCounts: { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 },
dashouCertified: false,
shangjiaCertified: false,
});
}
}
export function syncGroupFields(data) {
if (!data || typeof data !== 'object') return;
const app = getApp();
['dashouqun', 'dashouqunid', 'guanshiqun', 'guanshiqunid'].forEach((field) => {
if (data[field] !== undefined) {
wx.setStorageSync(field, data[field]);
app.globalData[field] = data[field];
}
});
}
export function refreshTabBar(page) {
const tryRefresh = () => {
const tabBar = page.getTabBar && page.getTabBar();
if (tabBar && tabBar.refresh) tabBar.refresh();
};
tryRefresh();
[50, 150, 300, 500, 800].forEach((delay) => setTimeout(tryRefresh, delay));
}
export function setRoleAndRefreshTabBar(page, role) {
const app = getApp();
if (app.setCurrentRole) {
app.setCurrentRole(role);
} else {
app.globalData.currentRole = role;
wx.setStorageSync('currentRole', role);
app.emitEvent('currentRoleChanged', { role });
}
refreshTabBar(page);
}
export function reconnectForRole(role) {
const app = getApp();
if (!wx.getStorageSync('uid')) return;
const targetUserId = getLocalImUserId(app);
if (!targetUserId) return;
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
const currentUserId = wx.goEasy?.im?.userId || '';
const connected = status === 'connected' || status === 'reconnected';
if (connected && currentUserId === targetUserId) {
if (app.loadConversations) app.loadConversations();
if (app.ensureConnection) app.ensureConnection();
return;
}
if (app.switchRoleAndReconnect) {
app.switchRoleAndReconnect(role).catch(() => {
if (app.ensureConnection) app.ensureConnection();
});
} else if (app.connectForCurrentRole) {
app.connectForCurrentRole();
} else if (app.ensureConnection) {
app.ensureConnection();
}
}
export function ensureRoleOnCenterPage(page, role) {
setRoleAndRefreshTabBar(page, role);
reconnectForRole(role);
}

255
utils/staff-api.js Normal file
View File

@@ -0,0 +1,255 @@
/**
* 商家子客服 API 与身份上下文
*
* 身份说明(互斥,不冲突):
* - 商家老板shangjiastatus=1走原商家接口扣商家余额
* - 子客服(含总管/财务/派单员staff_context.active走 merchant-staff 接口,扣个人额度
* - 同账号不能既是老板又是子客服;换角色只改权限,额度/统计仍归属同一 member 记录
*/
import { isApiSuccess } from './api-helper.js';
const STAFF_PREFIX = '/yonghu/merchant-staff';
export const STAFF_API = {
me: `${STAFF_PREFIX}/me`,
bind: `${STAFF_PREFIX}/bind`,
inviteCreate: `${STAFF_PREFIX}/invite/create`,
inviteList: `${STAFF_PREFIX}/invite/list`,
inviteRevoke: `${STAFF_PREFIX}/invite/revoke`,
memberList: `${STAFF_PREFIX}/member/list`,
memberUpdateRole: `${STAFF_PREFIX}/member/update-role`,
memberRemove: `${STAFF_PREFIX}/member/remove`,
memberDisable: `${STAFF_PREFIX}/member/disable`,
roleList: `${STAFF_PREFIX}/role/list`,
roleSetPerms: `${STAFF_PREFIX}/role/set-perms`,
walletAllocate: `${STAFF_PREFIX}/wallet/allocate`,
walletRevoke: `${STAFF_PREFIX}/wallet/revoke`,
walletLedger: `${STAFF_PREFIX}/wallet/ledger`,
auditList: `${STAFF_PREFIX}/audit/list`,
rank: `${STAFF_PREFIX}/rank`,
roleCreate: `${STAFF_PREFIX}/role/create`,
roleSetPerms: `${STAFF_PREFIX}/role/set-perms`,
memberUpdatePerms: `${STAFF_PREFIX}/member/update-perms`,
templateList: `${STAFF_PREFIX}/template/list`,
templateCreate: `${STAFF_PREFIX}/template/create`,
templateUpdate: `${STAFF_PREFIX}/template/update`,
templateDelete: `${STAFF_PREFIX}/template/delete`,
linkGenerate: `${STAFF_PREFIX}/link/generate`,
linkList: `${STAFF_PREFIX}/link/list`,
orderList: `${STAFF_PREFIX}/order/list`,
orderDetail: `${STAFF_PREFIX}/order/detail`,
orderDispatch: `${STAFF_PREFIX}/order/dispatch`,
productTypes: `${STAFF_PREFIX}/order/product-types`,
orderSettle: `${STAFF_PREFIX}/order/settle`,
orderCancel: `${STAFF_PREFIX}/order/cancel`,
orderRefund: `${STAFF_PREFIX}/order/refund`,
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() {
return wx.getStorageSync('staff_context') || null;
}
export function setStaffContext(ctx) {
if (ctx && ctx.active) {
wx.setStorageSync('staff_context', ctx);
wx.setStorageSync('staffstatus', 1);
} else {
wx.removeStorageSync('staff_context');
wx.setStorageSync('staffstatus', 0);
}
}
export function isStaffMode() {
// 商家老板与子客服互斥:已认证商家时一律按老板身份
if (Number(wx.getStorageSync('shangjiastatus')) === 1) {
return false;
}
const ctx = getStaffContext();
if (ctx && ctx.active) {
return true;
}
// 绑定/登录已写入 staffstatuscontext 异步刷新期间仍视为客服
return Number(wx.getStorageSync('staffstatus')) === 1;
}
export function clearStaffSession() {
wx.removeStorageSync('staff_context');
wx.setStorageSync('staffstatus', 0);
}
/** 是否仅为商家老板(非子客服) */
export function isMerchantOwnerOnly() {
return Number(wx.getStorageSync('shangjiastatus')) === 1 && !isStaffMode();
}
/** 子客服/非老板禁止进入商家充值提现等页面 */
export function guardMerchantOwnerAction(msg) {
if (isMerchantOwnerOnly()) return true;
wx.showToast({
title: msg || '仅商家老板可充值或提现,子客服请联系老板',
icon: 'none',
});
return false;
}
export function staffHasPerm(code) {
const ctx = getStaffContext();
if (!ctx || !ctx.permissions) return false;
return ctx.permissions.indexOf(code) >= 0;
}
function loadRequest() {
// 延迟加载,避免 App 未注册时 request 模块被提前初始化
return require('./request.js').default;
}
export async function refreshStaffContext(req) {
const cached = getStaffContext();
const token = wx.getStorageSync('token');
if (!token) return null;
const doRequest = req || loadRequest();
try {
const res = await doRequest({ url: STAFF_API.me, method: 'POST' });
const body = res.data || {};
if (res.statusCode === 401) {
return cached;
}
if (isApiSuccess(body) && body.data && body.data.active) {
setStaffContext(body.data);
return body.data;
}
if (body.code === 404 || body.code === 403) {
// 已有本地客服身份时勿清空,避免误入「商家入驻」
if (cached && cached.active) {
return cached;
}
if (Number(wx.getStorageSync('staffstatus')) === 1) {
return cached;
}
clearStaffSession();
return null;
}
return cached;
} catch (e) {
return cached;
}
}
/** 登录/注册响应写入子客服身份 */
export function applyStaffFromLoginData(data) {
if (!data || typeof data !== 'object') return;
if (data.staff_context && data.staff_context.active) {
setStaffContext(data.staff_context);
return;
}
if (Number(data.staffstatus) === 1) {
wx.setStorageSync('staffstatus', 1);
return;
}
if (Object.prototype.hasOwnProperty.call(data, 'staffstatus') && Number(data.staffstatus) !== 1) {
clearStaffSession();
}
}
/** 登录成功后从 /me 拉取最新子客服身份 */
export async function restoreStaffContextAfterAuth() {
const token = wx.getStorageSync('token');
if (!token) return null;
return refreshStaffContext(loadRequest());
}
/** 客服模式下返回客服专用 URL否则返回商家原 URL */
export function merchantApi(staffPath, ownerPath) {
return isStaffMode() ? staffPath : ownerPath;
}
export function staffCan(action) {
if (!isStaffMode()) return true;
const map = {
dispatch: 'order_dispatch',
detail: 'order_detail',
settle: 'order_settle',
cancel: 'order_cancel',
refund: 'order_refund_apply',
penalty: 'penalty_apply',
penaltyManage: 'penalty_manage',
changePlayer: 'order_change_player',
staffManage: 'staff_manage',
audit: 'audit_view',
finance: 'finance_view',
walletAllocate: 'wallet_allocate',
walletRevoke: 'wallet_revoke',
template: 'template_manage',
imChat: 'im_chat',
};
return staffHasPerm(map[action] || action);
}
/** 商家老板或有效子客服,均可进入商家端工作台 */
export function isMerchantPortalUser() {
return Number(wx.getStorageSync('shangjiastatus')) === 1 || isStaffMode();
}
/** 将 /me 返回的 stats 写入页面 */
export function applyStaffStatsToPage(page, ctx) {
const st = (ctx && ctx.stats) || {};
if (!st || typeof page.setData !== 'function') return;
const patch = {};
if (page.data.stats) {
patch.stats = {
...page.data.stats,
todayDispatch: st.today_dispatch_count || 0,
todayRefund: st.today_refund_count || 0,
};
}
if ('jinridingdan' in page.data) {
patch.jinridingdan = st.today_dispatch_count || 0;
patch.jinrituikuan = st.today_refund_count || 0;
patch.fabu = st.dispatch_count || 0;
patch.tuikuan = st.refund_count || 0;
}
if (Object.keys(patch).length) {
page.setData(patch);
}
}
/** 同步子客服 UI 权限到页面 data并确保 isShangjia 对客服为 true */
export function syncStaffUi(page) {
const staff = isStaffMode();
const ctx = staff ? (getStaffContext() || {}) : null;
const wallet = (ctx && ctx.wallet) || {};
const portal = isMerchantPortalUser();
page.setData({
isShangjia: portal,
isStaffMode: staff,
isMerchantOwner: !staff && Number(wx.getStorageSync('shangjiastatus')) === 1,
staffRoleName: staff ? (ctx.role_name || '客服') : '',
staffMerchantId: staff ? (ctx.merchant_id || '') : '',
quotaAvailable: staff ? (wallet.quota_available || '0.00') : '',
canDispatch: staffCan('dispatch'),
canSettle: staffCan('settle'),
canCancel: staffCan('cancel'),
canRefund: staffCan('refund'),
canPenalty: staffCan('penalty'),
canPenaltyManage: staffCan('penaltyManage'),
canChangePlayer: staffCan('changePlayer'),
canStaffManage: staffCan('staffManage'),
canWalletAllocate: staffCan('walletAllocate'),
canWalletRevoke: staffCan('walletRevoke'),
canFinance: staffCan('finance'),
canTemplate: staffCan('template'),
canAudit: !staff || staffCan('audit'),
canImChat: !staff || staffCan('imChat'),
showRechargeWithdraw: isMerchantOwnerOnly(),
});
}
/** 客服模式走 staff 专用 URL老板走原商家 URL */
export function staffOrOwner(staffUrl, ownerUrl) {
return isStaffMode() ? staffUrl : ownerUrl;
}

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) {