topfans/frontend/store/modules/user.js
zerosaturation ab1810e433 fix(frontend): clear all daily_login_completed_* keys on logout
登出时清理 daily_login 本地缓存的 key 写错:实际清的是
daily_login_completed_${today},但 Header.vue 写入的 key 是
daily_login_completed_${date}_${uid}_${starId},格式不匹配导致
根本没清掉任何东西,跨账号/重装场景会留旧去重残留,新一天本应触发
的 daily_login 上报被本地 cache 误判成'已报'。

改成模糊匹配所有 daily_login_completed_*(uni 没有'按前缀删'接口,
getStorageInfoSync + filter + 循环 remove,模式同
utils/preloadApi/storage.js#clearForUser)。

注:daily_login 仍保留前端去重(spec §3 要求);其他 daily 任务
(daily_browse_asset / daily_place_asset) 本来就没前端去重,完全
靠后端 ProcessTaskEvent 幂等(status=completed/claimed 跳过)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 16:36:58 +08:00

246 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { loginApi, registerApi } from '@/utils/api'
import { resetAllGuides } from '@/utils/guideConfig'
import { getPreloadApi } from '@/utils/preloadApi/index'
import { clearAllSandboxTmpDirs } from '@/utils/ioPath'
const state = {
token: uni.getStorageSync('access_token') || '',
starId: uni.getStorageSync('star_id') || null,
userInfo: (() => {
const userStr = uni.getStorageSync('user')
if (userStr) {
try {
return JSON.parse(userStr)
} catch (e) {
return null
}
}
return null
})()
}
const getters = {
isLoggedIn: (state) => {
return !!state.token
}
}
const mutations = {
SET_TOKEN(state, token) {
state.token = token
// 缓存到 storage
if (token) {
uni.setStorageSync('access_token', token)
} else {
uni.removeStorageSync('access_token')
}
},
SET_USER_INFO(state, userInfo) {
// ★ 新增:检查用户切换,清理旧用户缓存
const oldUserStr = uni.getStorageSync('user')
let oldUserId = null
if (oldUserStr) {
try { const old = JSON.parse(oldUserStr); oldUserId = old?.uid || null } catch (e) {}
}
const newUserId = userInfo?.uid || null
if (oldUserId && newUserId && oldUserId !== newUserId) {
try {
const api = getPreloadApi()
if (api) { api.invalidatePrefix('me.'); api.clearForUser(oldUserId) }
} catch (e) {}
}
state.userInfo = userInfo
// 缓存到 storage
if (userInfo) {
uni.setStorageSync('user', JSON.stringify(userInfo))
// 单独缓存 star_id方便其他地方直接读取
const starId = userInfo?.fan_identity?.star_id ?? null
state.starId = starId
if (starId !== null) {
uni.setStorageSync('star_id', starId)
} else {
uni.removeStorageSync('star_id')
}
} else {
uni.removeStorageSync('user')
uni.removeStorageSync('star_id')
state.starId = null
}
},
CLEAR_AUTH(state) {
// ★ 新增:先清理 preload 缓存(在清 token 之前spec §6.5 constraint #2
const userStr = uni.getStorageSync('user')
let userId = null
if (userStr) {
try { const user = JSON.parse(userStr); userId = user?.uid || null } catch (e) {}
}
if (userId) {
try {
const api = getPreloadApi()
if (api) api.clearUser(userId)
} catch (e) {}
}
state.token = ''
state.userInfo = null
state.starId = null
// 清除所有引导相关存储(传入 userId 确保正确清除)
// 注userId 已在上面 preload 清理块中从 storage 读取,此处复用
resetAllGuides(userId)
// 清除 storage
uni.removeStorageSync('access_token')
uni.removeStorageSync('user')
uni.removeStorageSync('star_id')
// 清除登录手机号
uni.removeStorageSync('login_mobile')
// 清除 gallery_owner_id
uni.removeStorageSync('gallery_owner_id')
// 清除每日登录打卡记录(模糊匹配所有 daily_login_completed_*,避免 key 格式拼接错位)
// Header.vue 写入的 key 是 daily_login_completed_${date}_${uid}_${starId},
// 按单日 key 删除会漏清历史/跨账号残留,必须按前缀清。
// 注意:uni 没有"按前缀删"接口,需 getStorageInfoSync 拿所有 key 后过滤(同 preloadApi/storage.js#clearForUser 模式)
try {
const info = uni.getStorageInfoSync()
const toRemove = (info.keys || []).filter((k) => k.startsWith('daily_login_completed_'))
toRemove.forEach((k) => {
try { uni.removeStorageSync(k) } catch (_) { /* skip 单个失败 */ }
})
} catch (e) {
console.warn('[user/logout] clear daily_login cache failed:', e)
}
// 重置 TopfansWelcome 相关标记让登出后再登录quickLogin / 注册)能再次展示欢迎页
// 同时清掉 storage 和内存标记,避免 password 登录误触发(见 store/modules/user.js 注释表)
uni.removeStorageSync('needs_welcome')
try {
const app = getApp()
if (app && app.globalData) {
app.globalData.welcomeShownThisSession = false
}
} catch (e) {
// getApp() 在非 App 上下文不可用,忽略即可
}
}
}
const actions = {
// 登录
async login({ commit }, { mobile, password }) {
try {
const res = await loginApi(mobile, password)
if (res.code === 0 && res.data) {
// 缓存 access_token
const accessToken = res.data.access_token
uni.setStorageSync('access_token', accessToken)
commit('SET_TOKEN', accessToken)
// 缓存用户信息
const user = res.data.user
// 缓存登录手机号
const loginMobile = mobile
uni.setStorageSync('login_mobile', loginMobile)
uni.setStorageSync('user', JSON.stringify(user))
commit('SET_USER_INFO', user)
return Promise.resolve(res)
} else {
// 返回错误信息,包含 message
return Promise.reject(new Error(res.message || '登录失败'))
}
} catch (error) {
// 处理网络错误或HTTP错误
// 如果错误响应包含 message使用它否则使用默认错误信息
if (error.message) {
return Promise.reject(new Error(error.message))
}
return Promise.reject(new Error('登录失败,请检查网络连接'))
}
},
// 注册
async register({ commit }, { mobile, password, star_id, nickname, verify_token = '', avatar_url = '' }) {
try {
const res = await registerApi(mobile, password, star_id, nickname, verify_token, avatar_url)
if (res.code === 0 && res.data) {
// 缓存 access_token
const accessToken = res.data.access_token
uni.setStorageSync('access_token', accessToken)
commit('SET_TOKEN', accessToken)
// 缓存用户信息
const user = res.data.user
// 缓存登录手机号
const loginMobile = mobile
// uni.setStorageSync('login_mobile', loginMobile)
uni.setStorageSync('user', JSON.stringify(user))
commit('SET_USER_INFO', user)
return Promise.resolve(res)
} else {
// 处理昵称已存在的情况
if (res.code === 3) {
return Promise.reject({ code: 3, message: res.message || '昵称已存在' })
}
return Promise.reject(new Error(res.message || '注册失败'))
}
} catch (error) {
// 处理HTTP 409错误
if (error.statusCode === 409 || error.code === 3) {
return Promise.reject({ code: 3, message: error.message || '昵称已存在' })
}
return Promise.reject(error)
}
},
// 登出
logout({ commit }) {
commit('CLEAR_AUTH')
// 清理沙盒 doc/ 下的业务临时目录,避免下一个登录用户看到上一个用户的临时文件(隐私)
// 白名单(preload/share/image)由工具保护,不清
// #ifdef APP-PLUS
clearAllSandboxTmpDirs().then((r) => {
console.log('[logout] cleared sandbox tmp dirs:', r.cleared)
}).catch((e) => {
console.warn('[logout] clear sandbox tmp dirs failed:', e?.message)
})
// #endif
},
// 检查登录状态(从 storage 恢复)
checkLoginStatus({ commit, state }) {
// 从 storage 恢复 token
const token = uni.getStorageSync('access_token')
if (token && token !== state.token) {
commit('SET_TOKEN', token)
}
// 从 storage 恢复 userInfo
const userStr = uni.getStorageSync('user')
if (userStr) {
try {
const userInfo = JSON.parse(userStr)
if (userInfo && (!state.userInfo || JSON.stringify(userInfo) !== JSON.stringify(state.userInfo))) {
commit('SET_USER_INFO', userInfo)
}
} catch (e) {
console.error('恢复用户信息失败:', e)
}
}
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}