backup: 紫色UI换肤前完整备份(当前橙色逍遥梦主题)

保留改造前全部页面样式与功能代码,便于回滚。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-29 01:59:42 +08:00
parent a751708d6e
commit c03d22776f
49 changed files with 6025 additions and 3784 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

@@ -5,6 +5,7 @@ import {
getSessionToken,
isWechatLoginSuccess,
} from './role-tab-bar';
import { restoreStaffContextAfterAuth } from './staff-api.js';
const app = getApp();
@@ -14,6 +15,7 @@ export function ensureLogin(options = {}) {
const token = getSessionToken(app);
if (token) {
backfillUserProfileCache(app);
restoreStaffContextAfterAuth();
return Promise.resolve(true);
}

View File

@@ -1,57 +1,31 @@
/**
* 手机号强制认证 - 完全自包含逻辑模块
*
* 特点:
* 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';
// ==================== 内部封装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;
}
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,
success: (res) => {
// 正常响应直接 resolve 整个响应对象,让上层自己解析
resolve(res);
},
fail: (err) => {
reject(err);
}
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 +34,6 @@ function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
return;
}
// 先验证文件是否存在
wx.getFileInfo({
filePath,
success: () => {
@@ -69,15 +42,11 @@ function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
filePath,
name: fileName,
formData,
header: {
'Authorization': 'Bearer ' + token
},
header: { 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 +57,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,
};

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,73 @@
// 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,
});
});
}
// utils/request.js
function request(options) {
const app = getApp();
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;

View File

@@ -1,255 +1,266 @@
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.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);
}

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

@@ -0,0 +1,253 @@
/**
* 商家子客服 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`,
};
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;
}