bugfix:未登录循环注册
This commit is contained in:
+3
-2
@@ -8,11 +8,12 @@ App<IAppOption>({
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
// 登录失效 / 账号禁用时统一清登录态并跳转登录页
|
||||
// 登录失效 / 账号禁用时统一清登录态。
|
||||
// 跳转登录页由 request.ts 的 redirectToLogin 单一入口负责(幂等去重),
|
||||
// 避免多个请求同时 401 时并发 reLaunch 打断 webview 路由。
|
||||
setAuthExpiredHandler(() => {
|
||||
auth.clearTokens()
|
||||
auth.clearUserInfo()
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { request } from '../utils/request'
|
||||
import * as auth from '../utils/auth'
|
||||
import { STORAGE_KEYS } from '../utils/config'
|
||||
|
||||
// 未读红点轮询最小间隔,避免频繁切 tab 时重复打接口
|
||||
const SYNC_INTERVAL = 30 * 1000
|
||||
let lastSyncAt = 0
|
||||
|
||||
Component({
|
||||
data: {
|
||||
@@ -15,25 +21,40 @@ Component({
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.refreshUnread()
|
||||
// 先用缓存渲染红点;未登录时不再发请求(否则会批量 401 并触发重定向)
|
||||
const cached = (wx.getStorageSync(STORAGE_KEYS.UNREAD) as number) || 0
|
||||
this.setData({ unread: cached })
|
||||
this.syncUnread()
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshUnread() {
|
||||
const unread = (wx.getStorageSync('zt_closet_unread') as number) || 0
|
||||
// 直接更新红点(供页面在已知数量时调用,不再触发二次请求)
|
||||
setUnread(n: number) {
|
||||
const unread = Number(n) || 0
|
||||
wx.setStorageSync(STORAGE_KEYS.UNREAD, unread)
|
||||
this.setData({ unread })
|
||||
this.fetchUnread()
|
||||
},
|
||||
fetchUnread() {
|
||||
request<{ unread_count: number }>({ url: '/notification/unread', showError: false })
|
||||
.then((res) => {
|
||||
const unread = res.unread_count || 0
|
||||
wx.setStorageSync('zt_closet_unread', unread)
|
||||
this.setData({ unread })
|
||||
})
|
||||
|
||||
// 拉取未读数:仅登录态才请求,且做节流
|
||||
syncUnread(force?: boolean) {
|
||||
if (!auth.isLoggedIn()) {
|
||||
this.setUnread(0)
|
||||
return
|
||||
}
|
||||
const now = Date.now()
|
||||
if (!force && now - lastSyncAt < SYNC_INTERVAL) return
|
||||
lastSyncAt = now
|
||||
|
||||
request<{ unread_count: number }>({
|
||||
url: '/notification/unread',
|
||||
showError: false,
|
||||
silentAuth: true,
|
||||
})
|
||||
.then((res) => this.setUnread(res.unread_count || 0))
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
switchTab(e: WechatMiniprogram.TouchEvent) {
|
||||
const { path, index } = e.currentTarget.dataset as { path: string; index: number }
|
||||
wx.switchTab({ url: path })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { request, redirectToLogin } from '../../utils/request'
|
||||
import * as auth from '../../utils/auth'
|
||||
import { PagedResult, ClosetShareItem } from '../../types/models'
|
||||
|
||||
Page({
|
||||
@@ -17,6 +18,11 @@ Page({
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
if (!auth.isLoggedIn()) {
|
||||
redirectToLogin()
|
||||
return
|
||||
}
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { request, redirectToLogin } from '../../utils/request'
|
||||
import * as auth from '../../utils/auth'
|
||||
import { HoroscopeData } from '../../types/models'
|
||||
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
@@ -25,6 +26,11 @@ Page({
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
if (!auth.isLoggedIn()) {
|
||||
redirectToLogin()
|
||||
return
|
||||
}
|
||||
if (!this.data.horoscope) this.fetchHoroscope(0)
|
||||
},
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ Page({
|
||||
expireTime: data.expire_time,
|
||||
})
|
||||
wx.setStorageSync(STORAGE_KEYS.USER_INFO, data.user_info)
|
||||
// 新登录用户重置未读红点,进入 tab 页后由 tabBar 拉取真实值
|
||||
wx.setStorageSync(STORAGE_KEYS.UNREAD, 0)
|
||||
this.setData({ userInfo: data.user_info })
|
||||
if (data.user_info.is_new_user) {
|
||||
this.showWelcomeGift(data.user_info)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { request, redirectToLogin } from '../../utils/request'
|
||||
import * as auth from '../../utils/auth'
|
||||
import { PagedResult, NotificationItem } from '../../types/models'
|
||||
import { STORAGE_KEYS } from '../../utils/config'
|
||||
import { formatRelativeTime } from '../../utils/format'
|
||||
|
||||
const FILTERS = [
|
||||
@@ -21,16 +23,27 @@ Page({
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
if (!auth.isLoggedIn()) {
|
||||
redirectToLogin()
|
||||
return
|
||||
}
|
||||
this.refresh()
|
||||
this.fetchUnread()
|
||||
},
|
||||
|
||||
fetchUnread() {
|
||||
request<{ unread_count: number }>({ url: '/notification/unread', showError: false })
|
||||
request<{ unread_count: number }>({
|
||||
url: '/notification/unread',
|
||||
showError: false,
|
||||
silentAuth: true,
|
||||
})
|
||||
.then((res) => {
|
||||
wx.setStorageSync('zt_closet_unread', res.unread_count)
|
||||
const unread = res.unread_count || 0
|
||||
wx.setStorageSync(STORAGE_KEYS.UNREAD, unread)
|
||||
const tabBar = (this as any).getTabBar && (this as any).getTabBar()
|
||||
if (tabBar && tabBar.refreshUnread) tabBar.refreshUnread()
|
||||
// 直接更新红点,避免 tabBar 内部再发一次请求
|
||||
if (tabBar && tabBar.setUnread) tabBar.setUnread(unread)
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { request, redirectToLogin } from '../../utils/request'
|
||||
import * as auth from '../../utils/auth'
|
||||
import { PagedResult, OutfitItem } from '../../types/models'
|
||||
import { OutfitSource, IsPublic } from '../../utils/config'
|
||||
|
||||
@@ -15,6 +16,11 @@ Page({
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
if (!auth.isLoggedIn()) {
|
||||
redirectToLogin()
|
||||
return
|
||||
}
|
||||
const openCollect = wx.getStorageSync('zt_open_collect')
|
||||
if (openCollect) {
|
||||
wx.removeStorageSync('zt_open_collect')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { request, uploadFile } from '../../utils/request'
|
||||
import { request, uploadFile, redirectToLogin } from '../../utils/request'
|
||||
import * as auth from '../../utils/auth'
|
||||
import { UserInfo, QuotaItem } from '../../types/models'
|
||||
import { QuotaType, STORAGE_KEYS } from '../../utils/config'
|
||||
import { formatDate } from '../../utils/format'
|
||||
@@ -46,16 +47,19 @@ Page({
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
if (!auth.isLoggedIn()) {
|
||||
redirectToLogin()
|
||||
return
|
||||
}
|
||||
this.fetchUser()
|
||||
this.fetchQuota()
|
||||
},
|
||||
|
||||
syncTabBar() {
|
||||
const tabBar = (this as any).getTabBar && (this as any).getTabBar()
|
||||
if (tabBar) {
|
||||
tabBar.setData({ selected: 4 })
|
||||
if (tabBar.refreshUnread) tabBar.refreshUnread()
|
||||
}
|
||||
// 红点由 tabBar 自身在登录态下同步,这里只更新选中态
|
||||
if (tabBar) tabBar.setData({ selected: 4 })
|
||||
},
|
||||
|
||||
fetchUser() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { request, redirectToLogin } from '../../utils/request'
|
||||
import * as auth from '../../utils/auth'
|
||||
import { PagedResult, ClothesItem, QuotaItem } from '../../types/models'
|
||||
import { QuotaType } from '../../utils/config'
|
||||
|
||||
@@ -28,6 +29,11 @@ Page({
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
if (!auth.isLoggedIn()) {
|
||||
redirectToLogin()
|
||||
return
|
||||
}
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export const STORAGE_KEYS = {
|
||||
REFRESH_TOKEN: 'zt_closet_refresh_token',
|
||||
EXPIRE_TIME: 'zt_closet_expire_time',
|
||||
USER_INFO: 'zt_closet_user_info',
|
||||
UNREAD: 'zt_closet_unread',
|
||||
}
|
||||
|
||||
// 衣物图片处理状态
|
||||
|
||||
@@ -19,8 +19,12 @@ export interface RequestOptions {
|
||||
header?: Record<string, any>
|
||||
auth?: boolean // 默认 true,需要鉴权
|
||||
showError?: boolean // 默认 true,业务错误自动 toast
|
||||
silentAuth?: boolean // 非关键请求(如未读红点轮询):401 只失败,不触发全局跳登录
|
||||
}
|
||||
|
||||
// 未登录错误码:调用方可通过 e.code === 11001 区分
|
||||
export const NOT_LOGIN_CODE = 11001
|
||||
|
||||
// 登录失效 / 账号禁用后的跳转处理(由 app.ts 注入,避免循环依赖)
|
||||
let onAuthExpired: (() => void) | null = null
|
||||
export function setAuthExpiredHandler(handler: () => void): void {
|
||||
@@ -76,12 +80,35 @@ function showToast(title: string): void {
|
||||
wx.showToast({ title, icon: 'none', duration: 2000 })
|
||||
}
|
||||
|
||||
function redirectToLogin(): void {
|
||||
if (onAuthExpired) {
|
||||
onAuthExpired()
|
||||
} else {
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
// 跳转登录页的幂等锁:并发的多个 401 只会触发一次路由,避免互相打断 webview 生命周期
|
||||
let redirecting = false
|
||||
|
||||
/**
|
||||
* 跳转登录页(全局唯一入口,幂等):
|
||||
* 1. 同一时间只允许一次 reLaunch,其余调用直接丢弃;
|
||||
* 2. 当前已在登录页时不重复跳转。
|
||||
*/
|
||||
export function redirectToLogin(): void {
|
||||
if (redirecting) return
|
||||
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
|
||||
const current = pages[pages.length - 1]
|
||||
if (current && current.route === 'pages/login/login') return
|
||||
|
||||
redirecting = true
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login',
|
||||
complete: () => {
|
||||
setTimeout(() => {
|
||||
redirecting = false
|
||||
}, 1000)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 登录失效统一处理:先清理登录态(由 app.ts 注入),再走幂等跳转
|
||||
function handleAuthExpired(): void {
|
||||
if (onAuthExpired) onAuthExpired()
|
||||
redirectToLogin()
|
||||
}
|
||||
|
||||
export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
@@ -92,11 +119,18 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
header = {},
|
||||
auth: needAuth = true,
|
||||
showError = true,
|
||||
silentAuth = false,
|
||||
} = options
|
||||
|
||||
const fullUrl = url.startsWith('http') ? url : `${BASE_URL}${API_PREFIX}${url}`
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// 未登录:直接失败,不发请求,避免无 token 时批量 401 引发重定向风暴
|
||||
if (needAuth && !auth.isLoggedIn()) {
|
||||
reject(new ApiError(NOT_LOGIN_CODE, '未登录'))
|
||||
return
|
||||
}
|
||||
|
||||
const doCall = (): void => {
|
||||
const finalHeader: Record<string, any> = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -138,12 +172,15 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
doRefresh().then((ok) => {
|
||||
if (ok) {
|
||||
doCall()
|
||||
} else {
|
||||
auth.clearTokens()
|
||||
redirectToLogin()
|
||||
if (showError) showToast('登录已过期,请重新登录')
|
||||
reject(new ApiError(code, body.msg || '登录已过期'))
|
||||
return
|
||||
}
|
||||
auth.clearTokens()
|
||||
// 非关键请求(红点轮询等)静默失败,不打断用户当前页面
|
||||
if (!silentAuth) {
|
||||
handleAuthExpired()
|
||||
if (showError) showToast('登录已过期,请重新登录')
|
||||
}
|
||||
reject(new ApiError(code, body.msg || '登录已过期'))
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -151,7 +188,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
// 账号禁用(HTTP 200,需单独处理)
|
||||
if (code === 21001) {
|
||||
auth.clearTokens()
|
||||
redirectToLogin()
|
||||
handleAuthExpired()
|
||||
if (showError) showToast(ERROR_MESSAGES[21001] || '账号已被禁用')
|
||||
reject(new ApiError(code, body.msg || '账号已被禁用'))
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user