bugfix
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { STORAGE_KEYS } from './config'
|
||||
import { logger } from './logger'
|
||||
|
||||
// 日志模块名
|
||||
const LOG = 'auth'
|
||||
|
||||
export interface TokenData {
|
||||
accessToken: string
|
||||
@@ -22,12 +26,15 @@ export function setTokens(data: TokenData): void {
|
||||
wx.setStorageSync(STORAGE_KEYS.ACCESS_TOKEN, data.accessToken)
|
||||
wx.setStorageSync(STORAGE_KEYS.REFRESH_TOKEN, data.refreshToken)
|
||||
wx.setStorageSync(STORAGE_KEYS.EXPIRE_TIME, data.expireTime)
|
||||
// 不打印 token 明文,只记录过期时间便于核对单位(秒/毫秒)
|
||||
logger.debug(LOG, 'token 已写入, expireTime=', data.expireTime)
|
||||
}
|
||||
|
||||
export function clearTokens(): void {
|
||||
wx.removeStorageSync(STORAGE_KEYS.ACCESS_TOKEN)
|
||||
wx.removeStorageSync(STORAGE_KEYS.REFRESH_TOKEN)
|
||||
wx.removeStorageSync(STORAGE_KEYS.EXPIRE_TIME)
|
||||
logger.info(LOG, 'token 已清理')
|
||||
}
|
||||
|
||||
export function isLoggedIn(): boolean {
|
||||
|
||||
@@ -154,3 +154,24 @@ export const ERROR_MESSAGES: Record<number, string> = {
|
||||
19003: '服务繁忙,请稍后重试',
|
||||
19999: '服务繁忙,请稍后重试',
|
||||
}
|
||||
|
||||
// ===== 日志配置 =====
|
||||
// 注意:本文件保持零 import(纯常量),LogLevel 定义在此以避免 config→logger→config 循环依赖
|
||||
|
||||
// 日志级别:数值越大越严重,低于当前级别的日志不输出
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
SILENT = 4, // 全部关闭
|
||||
}
|
||||
|
||||
// 全局日志级别开关:
|
||||
// null = 按运行环境自动判定(develop=DEBUG / trial=INFO / release=WARN)
|
||||
// LogLevel.DEBUG = 手动写死,临时调级用(调试完记得改回 null)
|
||||
export const LOG_LEVEL: LogLevel | null = null
|
||||
|
||||
// 是否写入微信实时日志(真机可在小程序后台「实时日志」查看线上问题)
|
||||
// 仅 WARN 及以上写入,避免击穿实时日志配额
|
||||
export const LOG_TO_REALTIME = true
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { LogLevel, LOG_LEVEL, LOG_TO_REALTIME } from './config'
|
||||
|
||||
/**
|
||||
* 全项目统一日志出口。
|
||||
*
|
||||
* 用法:logger.debug('模块名', '消息', 任意参数)
|
||||
* 级别:DEBUG < INFO < WARN < ERROR < SILENT,低于当前生效级别的调用直接返回,
|
||||
* 且级别判断前置,不做任何字符串拼接,热路径(如每次请求)零开销。
|
||||
*
|
||||
* 输出:
|
||||
* 1. console —— 按级别选用 log / warn / error,开发者工具与真机调试可见
|
||||
* 2. 微信实时日志 —— 仅 WARN 及以上写入,可在小程序后台「实时日志」查询线上问题
|
||||
*/
|
||||
|
||||
// 手动覆盖的级别(LOG_LEVEL 为 null 时按环境自动推导)
|
||||
let manualLevel: LogLevel | null = LOG_LEVEL
|
||||
|
||||
// 解析后的生效级别缓存,避免每次调用都读环境信息
|
||||
let resolvedLevel: LogLevel | null = null
|
||||
|
||||
const LEVEL_NAMES: Record<number, string> = {
|
||||
[LogLevel.DEBUG]: 'DEBUG',
|
||||
[LogLevel.INFO]: 'INFO',
|
||||
[LogLevel.WARN]: 'WARN',
|
||||
[LogLevel.ERROR]: 'ERROR',
|
||||
}
|
||||
|
||||
// 按运行环境推导默认级别:开发版最详细,正式版只看异常
|
||||
function resolveByEnv(): LogLevel {
|
||||
try {
|
||||
const info = wx.getAccountInfoSync()
|
||||
const env = info.miniProgram && info.miniProgram.envVersion
|
||||
if (env === 'develop') return LogLevel.DEBUG
|
||||
if (env === 'trial') return LogLevel.INFO
|
||||
return LogLevel.WARN
|
||||
} catch (e) {
|
||||
// 取不到环境信息时(低版本基础库等)退化为 DEBUG,保证开发期可见
|
||||
return LogLevel.DEBUG
|
||||
}
|
||||
}
|
||||
|
||||
// 当前生效级别
|
||||
export function getLevel(): LogLevel {
|
||||
if (resolvedLevel === null) {
|
||||
resolvedLevel = manualLevel === null ? resolveByEnv() : manualLevel
|
||||
}
|
||||
return resolvedLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时调整日志级别(调级后对全局生效)。
|
||||
* 传 null 恢复为「按环境自动」。
|
||||
*/
|
||||
export function setLevel(level: LogLevel | null): void {
|
||||
manualLevel = level
|
||||
resolvedLevel = null
|
||||
}
|
||||
|
||||
// 实时日志单例(惰性初始化,取不到时降级为仅 console,绝不因日志抛错影响业务)
|
||||
let realtimeManager: any = null
|
||||
let realtimeInited = false
|
||||
|
||||
function getRealtimeManager(): any {
|
||||
if (realtimeInited) return realtimeManager
|
||||
realtimeInited = true
|
||||
try {
|
||||
if (typeof wx.getRealtimeLogManager === 'function') {
|
||||
realtimeManager = wx.getRealtimeLogManager()
|
||||
}
|
||||
} catch (e) {
|
||||
realtimeManager = null
|
||||
}
|
||||
return realtimeManager
|
||||
}
|
||||
|
||||
// 序列化日志参数:实时日志有单条长度限制,做截断保护
|
||||
function stringify(args: any[]): string {
|
||||
try {
|
||||
return args
|
||||
.map((a) => {
|
||||
if (typeof a === 'string') return a
|
||||
try {
|
||||
return JSON.stringify(a)
|
||||
} catch (e) {
|
||||
return String(a)
|
||||
}
|
||||
})
|
||||
.join(' ')
|
||||
.slice(0, 1000)
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function emit(level: LogLevel, module: string, args: any[]): void {
|
||||
if (level < getLevel()) return
|
||||
|
||||
const prefix = `[${LEVEL_NAMES[level]}][${module}]`
|
||||
|
||||
// console 通道
|
||||
if (level === LogLevel.ERROR) {
|
||||
console.error(prefix, ...args)
|
||||
} else if (level === LogLevel.WARN) {
|
||||
console.warn(prefix, ...args)
|
||||
} else {
|
||||
console.log(prefix, ...args)
|
||||
}
|
||||
|
||||
// 实时日志通道:仅 WARN 及以上,避免击穿配额
|
||||
if (!LOG_TO_REALTIME || level < LogLevel.WARN) return
|
||||
const manager = getRealtimeManager()
|
||||
if (!manager) return
|
||||
const text = `${prefix} ${stringify(args)}`
|
||||
try {
|
||||
if (level === LogLevel.ERROR && typeof manager.error === 'function') manager.error(text)
|
||||
else if (typeof manager.warn === 'function') manager.warn(text)
|
||||
} catch (e) {
|
||||
// 日志写入失败不影响业务
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
/** 开发期流程细节:页面入参、请求发起/成功、数据条数 */
|
||||
debug(module: string, ...args: any[]): void {
|
||||
emit(LogLevel.DEBUG, module, args)
|
||||
},
|
||||
/** 关键流程节点:登录成功、刷新成功、下单/上传/分享成功 */
|
||||
info(module: string, ...args: any[]): void {
|
||||
emit(LogLevel.INFO, module, args)
|
||||
},
|
||||
/** 可恢复异常与静默降级:静默 catch、跳转失败、非关键请求失败 */
|
||||
warn(module: string, ...args: any[]): void {
|
||||
emit(LogLevel.WARN, module, args)
|
||||
},
|
||||
/** 真正的错误:网络异常、业务错误码、账号禁用、登录态失效 */
|
||||
error(module: string, ...args: any[]): void {
|
||||
emit(LogLevel.ERROR, module, args)
|
||||
},
|
||||
}
|
||||
|
||||
export default logger
|
||||
@@ -1,5 +1,9 @@
|
||||
import { BASE_URL, API_PREFIX, ERROR_MESSAGES } from './config'
|
||||
import * as auth from './auth'
|
||||
import { logger } from './logger'
|
||||
|
||||
// 日志模块名
|
||||
const LOG = 'request'
|
||||
|
||||
// 业务错误对象(携带 code,页面可按 code 定制处理)
|
||||
export class ApiError extends Error {
|
||||
@@ -44,12 +48,12 @@ function isAuthExpiredCode(code: number, statusCode?: number): boolean {
|
||||
// 登录失效后的收尾:非关键请求只 reject,不动登录态
|
||||
function failAuth(code: number, msg: string, silentAuth: boolean, showError: boolean) {
|
||||
if (!silentAuth) {
|
||||
console.log('[auth] failAuth 清理登录态并跳转登录页, code=', code, 'msg=', msg)
|
||||
logger.warn(LOG, '登录态失效,清理 token 并跳转登录页, code=', code, 'msg=', msg)
|
||||
auth.clearTokens()
|
||||
handleAuthExpired()
|
||||
if (showError) showToast('登录已过期,请重新登录')
|
||||
} else {
|
||||
console.log('[auth] failAuth 静默失败(不动登录态), code=', code)
|
||||
logger.warn(LOG, '登录态失效但静默处理(不动登录态), code=', code)
|
||||
}
|
||||
return new ApiError(code, msg || '登录已过期')
|
||||
}
|
||||
@@ -69,11 +73,11 @@ function doRefresh(): Promise<boolean> {
|
||||
const refreshToken = auth.getRefreshToken()
|
||||
const accessToken = auth.getAccessToken()
|
||||
if (!refreshToken || !accessToken) {
|
||||
console.log('[auth] doRefresh 无 token,放弃刷新')
|
||||
logger.warn(LOG, 'doRefresh 缺少 refresh/access token,放弃刷新')
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
console.log('[auth] doRefresh 发起刷新')
|
||||
logger.debug(LOG, 'doRefresh 发起刷新')
|
||||
wx.request({
|
||||
url: `${BASE_URL}${API_PREFIX}/user/refresh-token`,
|
||||
method: 'POST',
|
||||
@@ -88,15 +92,15 @@ function doRefresh(): Promise<boolean> {
|
||||
refreshToken: body.data.refresh_token,
|
||||
expireTime: body.data.expire_time,
|
||||
})
|
||||
console.log('[auth] doRefresh 成功')
|
||||
logger.debug(LOG, 'doRefresh 成功')
|
||||
resolve(true)
|
||||
} else {
|
||||
console.log('[auth] doRefresh 响应异常, code=', body && body.code)
|
||||
logger.error(LOG, 'doRefresh 响应异常, code=', body && body.code)
|
||||
resolve(false)
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
console.log('[auth] doRefresh 网络失败')
|
||||
logger.error(LOG, 'doRefresh 网络失败')
|
||||
resolve(false)
|
||||
},
|
||||
})
|
||||
@@ -127,17 +131,17 @@ let redirecting = false
|
||||
*/
|
||||
export function redirectToLogin(): void {
|
||||
if (redirecting) {
|
||||
console.log('[auth] redirectToLogin 幂等锁生效,丢弃本次跳转')
|
||||
logger.debug(LOG, 'redirectToLogin 幂等锁生效,丢弃本次跳转')
|
||||
return
|
||||
}
|
||||
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
|
||||
const current = pages[pages.length - 1]
|
||||
if (current && current.route === 'pages/login/login') {
|
||||
console.log('[auth] redirectToLogin 已在登录页,跳过')
|
||||
logger.debug(LOG, 'redirectToLogin 已在登录页,跳过')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[auth] redirectToLogin 跳转登录页, 当前页=', current && current.route, '栈深=', pages.length)
|
||||
logger.info(LOG, '跳转登录页, 当前页=', current && current.route, '栈深=', pages.length)
|
||||
redirecting = true
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login',
|
||||
@@ -171,6 +175,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// 未登录:直接失败,不发请求,避免无 token 时批量 401 引发重定向风暴
|
||||
if (needAuth && !auth.isLoggedIn()) {
|
||||
logger.debug(LOG, '未登录直接 reject,不发请求, url=', url)
|
||||
reject(new ApiError(NOT_LOGIN_CODE, '未登录'))
|
||||
return
|
||||
}
|
||||
@@ -185,6 +190,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
if (token) finalHeader['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
logger.debug(LOG, '请求发起', method, fullUrl, 'needAuth=', needAuth, 'retried=', retried)
|
||||
wx.request({
|
||||
url: fullUrl,
|
||||
method,
|
||||
@@ -196,6 +202,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
|
||||
// 网络层异常:响应体无 code
|
||||
if (!body || typeof body.code !== 'number') {
|
||||
logger.error(LOG, '响应体无 code(网络层异常)', method, fullUrl, 'statusCode=', res.statusCode)
|
||||
if (showError) showToast('网络异常,请稍后重试')
|
||||
reject(new Error('network error'))
|
||||
return
|
||||
@@ -205,6 +212,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
|
||||
// 成功
|
||||
if (code === 0) {
|
||||
logger.debug(LOG, '请求成功', method, fullUrl)
|
||||
resolve(body.data as T)
|
||||
return
|
||||
}
|
||||
@@ -215,6 +223,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
reject(failAuth(code, body.msg, silentAuth, showError))
|
||||
return
|
||||
}
|
||||
logger.info(LOG, '命中登录失效码,尝试刷新后重试, code=', code, 'url=', fullUrl)
|
||||
doRefresh().then((ok) => {
|
||||
if (ok) {
|
||||
doCall(true)
|
||||
@@ -229,6 +238,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
|
||||
// 账号禁用(HTTP 200,需单独处理)
|
||||
if (code === 21001) {
|
||||
logger.error(LOG, '账号被禁用,清理登录态, code=', code)
|
||||
auth.clearTokens()
|
||||
handleAuthExpired()
|
||||
if (showError) showToast(ERROR_MESSAGES[21001] || '账号已被禁用')
|
||||
@@ -237,6 +247,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
}
|
||||
|
||||
// 其余业务错误
|
||||
logger.warn(LOG, '业务错误, code=', code, 'url=', fullUrl, 'msg=', body.msg)
|
||||
if (showError) {
|
||||
const msg =
|
||||
(body.details && body.details[0]) || ERROR_MESSAGES[code] || body.msg || '请求失败'
|
||||
@@ -245,6 +256,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
reject(new ApiError(code, body.msg || '请求失败', body.details))
|
||||
},
|
||||
fail: () => {
|
||||
logger.error(LOG, 'wx.request 失败(网络层)', method, fullUrl)
|
||||
if (showError) showToast('网络异常,请检查网络后重试')
|
||||
reject(new Error('request fail'))
|
||||
},
|
||||
@@ -276,6 +288,7 @@ export function uploadFile<T = any>(
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// 未登录:直接失败,不发请求
|
||||
if (!auth.isLoggedIn()) {
|
||||
logger.debug(LOG, '未登录直接 reject,不上传, url=', url)
|
||||
reject(new ApiError(NOT_LOGIN_CODE, '未登录'))
|
||||
return
|
||||
}
|
||||
@@ -285,6 +298,7 @@ export function uploadFile<T = any>(
|
||||
const token = auth.getAccessToken()
|
||||
if (token) header['Authorization'] = `Bearer ${token}`
|
||||
|
||||
logger.debug(LOG, '上传发起, url=', url, 'retried=', retried)
|
||||
wx.uploadFile({
|
||||
url: url.startsWith('http') ? url : `${BASE_URL}${API_PREFIX}${url}`,
|
||||
filePath,
|
||||
@@ -300,12 +314,14 @@ export function uploadFile<T = any>(
|
||||
}
|
||||
|
||||
if (!body || typeof body.code !== 'number') {
|
||||
logger.error(LOG, '上传响应解析失败(非 JSON)', url, 'raw=', res.data)
|
||||
if (showError) showToast('上传失败,请稍后重试')
|
||||
reject(new Error('upload fail'))
|
||||
return
|
||||
}
|
||||
|
||||
if (body.code === 0) {
|
||||
logger.info(LOG, '上传成功, url=', url)
|
||||
resolve(body.data as T)
|
||||
return
|
||||
}
|
||||
@@ -316,6 +332,7 @@ export function uploadFile<T = any>(
|
||||
reject(failAuth(body.code, body.msg, silentAuth, showError))
|
||||
return
|
||||
}
|
||||
logger.info(LOG, '上传命中登录失效码,尝试刷新后重试, code=', body.code)
|
||||
doRefresh().then((ok) => {
|
||||
if (ok) {
|
||||
doUpload(true)
|
||||
@@ -326,12 +343,14 @@ export function uploadFile<T = any>(
|
||||
return
|
||||
}
|
||||
|
||||
logger.warn(LOG, '上传业务错误, code=', body.code, 'url=', url, 'msg=', body.msg)
|
||||
const msg =
|
||||
(body.details && body.details[0]) || ERROR_MESSAGES[body.code] || body.msg || '上传失败'
|
||||
if (showError) showToast(msg)
|
||||
reject(new ApiError(body.code, body.msg || '上传失败', body.details))
|
||||
},
|
||||
fail: () => {
|
||||
logger.error(LOG, 'wx.uploadFile 失败(网络层), url=', url)
|
||||
if (showError) showToast('上传失败,请检查网络后重试')
|
||||
reject(new Error('upload fail'))
|
||||
},
|
||||
|
||||
@@ -16,3 +16,12 @@ export function syncTabBarUnread(force = true): void {
|
||||
const tabBar = getTabBarInstance()
|
||||
if (tabBar && typeof tabBar.syncUnread === 'function') tabBar.syncUnread(force)
|
||||
}
|
||||
|
||||
// 更新选中态:走 tabBar 的 setSelected(内部保证首帧安全),
|
||||
// 老版本实例没有该方法时降级为 setData
|
||||
export function setTabBarSelected(index: number): void {
|
||||
const tabBar = getTabBarInstance()
|
||||
if (!tabBar) return
|
||||
if (typeof tabBar.setSelected === 'function') tabBar.setSelected(index)
|
||||
else tabBar.setData({ selected: index })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user