fix(auth): 长任务活跃期间续期会话

This commit is contained in:
caoxiaozhu
2026-07-25 18:15:49 +08:00
parent 4f8aff5fc4
commit ea0013b99c
5 changed files with 70 additions and 9 deletions

View File

@@ -12,6 +12,10 @@ const confirmDialogPath = path.resolve(scriptDir, '../src/components/AppConfirmD
const layoutPath = path.resolve(scriptDir, '../src/layouts/MainLayout.vue') const layoutPath = path.resolve(scriptDir, '../src/layouts/MainLayout.vue')
const apiPath = path.resolve(scriptDir, '../src/api/modules/dataProcess.ts') const apiPath = path.resolve(scriptDir, '../src/api/modules/dataProcess.ts')
const contractTypesPath = path.resolve(scriptDir, '../src/types/dataProcess.ts') const contractTypesPath = path.resolve(scriptDir, '../src/types/dataProcess.ts')
const requestPath = path.resolve(scriptDir, '../src/api/request.ts')
const authStorePath = path.resolve(scriptDir, '../src/stores/auth.ts')
const routerPath = path.resolve(scriptDir, '../src/router/index.ts')
const sessionActivityPath = path.resolve(scriptDir, '../src/utils/sessionActivity.ts')
const sourceUploadWorkerPath = path.join(createDir, 'useDataProcessSourceUpload.ts') const sourceUploadWorkerPath = path.join(createDir, 'useDataProcessSourceUpload.ts')
const viewSource = await readFile(viewPath, 'utf8') const viewSource = await readFile(viewPath, 'utf8')
const layoutSource = await readFile(layoutPath, 'utf8') const layoutSource = await readFile(layoutPath, 'utf8')
@@ -25,6 +29,18 @@ const [stateSource, generationSource, previewBuildSource, sourceUploadWorkerSour
readFile(contractTypesPath, 'utf8'), readFile(contractTypesPath, 'utf8'),
]) ])
const implementationSource = [viewSource, stateSource, generationSource, previewBuildSource, sourceUploadWorkerSource].join('\n') const implementationSource = [viewSource, stateSource, generationSource, previewBuildSource, sourceUploadWorkerSource].join('\n')
const [requestSource, authStoreSource, routerSource, sessionActivitySource] = await Promise.all([
readFile(requestPath, 'utf8'),
readFile(authStorePath, 'utf8'),
readFile(routerPath, 'utf8'),
readFile(sessionActivityPath, 'utf8'),
])
assert.match(requestSource, /if \(res\.code === 0\) \{[\s\S]*?touchSessionActivity\(\)/, '成功 API 请求没有刷新会话活跃时间')
assert.match(authStoreSource, /const loginTime = sessionActivityTime/, '认证状态没有共享请求层的会话活跃时间')
assert.match(authStoreSource, /if \(currentUser\.value\) touchSessionActivity\(\)/, '会话续期仍可能在长任务结束后失效')
assert.match(routerSource, /const auth = useAuthStore\(\)[\s\S]*?auth\.syncSession\(\)[\s\S]*?if \(!auth\.isLoggedIn\)/, '路由守卫没有在登录判断前同步 API 活跃时间')
assert.match(sessionActivitySource, /export function touchSessionActivity\(\)/, '缺少统一会话活跃续期函数')
assert.ok(existsSync(confirmDialogPath), '缺少公共确认弹窗组件 AppConfirmDialog') assert.ok(existsSync(confirmDialogPath), '缺少公共确认弹窗组件 AppConfirmDialog')
const confirmDialogSource = await readFile(confirmDialogPath, 'utf8') const confirmDialogSource = await readFile(confirmDialogPath, 'utf8')

View File

@@ -1,5 +1,6 @@
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios' import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { touchSessionActivity } from '@/utils/sessionActivity'
/** /**
* 后端统一响应格式 * 后端统一响应格式
@@ -29,9 +30,12 @@ service.interceptors.response.use(
const res = response.data as ApiResult const res = response.data as ApiResult
// 二进制流等非 JSON 响应直接返回 // 二进制流等非 JSON 响应直接返回
if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') { if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') {
touchSessionActivity()
return response return response
} }
if (res.code === 0) { if (res.code === 0) {
// 生成进度轮询也属于用户正在使用系统,避免长任务结束后被误判为会话过期。
touchSessionActivity()
return res.data return res.data
} }
// 业务错误 // 业务错误

View File

@@ -298,6 +298,7 @@ function requiredPermission(path: string, explicit?: unknown) {
// 全局守卫:登录校验 + 会话超时 // 全局守卫:登录校验 + 会话超时
router.beforeEach((to, _from, next) => { router.beforeEach((to, _from, next) => {
const auth = useAuthStore() const auth = useAuthStore()
auth.syncSession()
document.title = to.meta.title ? `${to.meta.title} - 远光软件微调平台` : '远光软件微调平台' document.title = to.meta.title ? `${to.meta.title} - 远光软件微调平台` : '远光软件微调平台'
if (to.meta.public) { if (to.meta.public) {

View File

@@ -3,6 +3,13 @@ import { ref, computed } from 'vue'
import { login as loginApi } from '@/api/modules/system' import { login as loginApi } from '@/api/modules/system'
import { SESSION_TIMEOUT } from '@/constants' import { SESSION_TIMEOUT } from '@/constants'
import type { PermissionCode, SystemUser } from '@/types' import type { PermissionCode, SystemUser } from '@/types'
import {
clearSessionActivity,
sessionActivityTime,
startSessionActivity,
syncSessionActivity,
touchSessionActivity,
} from '@/utils/sessionActivity'
const USER_STORAGE_KEY = 'currentUser' const USER_STORAGE_KEY = 'currentUser'
@@ -60,7 +67,7 @@ export const useAuthStore = defineStore('auth', () => {
if (currentUser.value?.role === 'operator') return '操作员' if (currentUser.value?.role === 'operator') return '操作员'
return '观察员' return '观察员'
}) })
const loginTime = ref<number>(parseInt(localStorage.getItem('loginTime') || '0', 10) || 0) const loginTime = sessionActivityTime
const isLoggedIn = computed(() => { const isLoggedIn = computed(() => {
if (!loginTime.value) return false if (!loginTime.value) return false
@@ -71,10 +78,9 @@ export const useAuthStore = defineStore('auth', () => {
async function login(user: string, password: string) { async function login(user: string, password: string) {
const response = await loginApi(user, password) const response = await loginApi(user, password)
currentUser.value = response.user currentUser.value = response.user
loginTime.value = Date.now() startSessionActivity()
localStorage.setItem('username', response.user.username) localStorage.setItem('username', response.user.username)
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(response.user)) localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(response.user))
localStorage.setItem('loginTime', String(loginTime.value))
} }
/** 检查当前账号是否拥有指定模块权限。 */ /** 检查当前账号是否拥有指定模块权限。 */
@@ -85,19 +91,20 @@ export const useAuthStore = defineStore('auth', () => {
/** 续期会话(活跃时刷新) */ /** 续期会话(活跃时刷新) */
function refresh() { function refresh() {
if (isLoggedIn.value) { if (currentUser.value) touchSessionActivity()
loginTime.value = Date.now()
localStorage.setItem('loginTime', String(loginTime.value))
} }
/** 在路由判断前吸收其他标签页写入的最后活跃时间。 */
function syncSession() {
syncSessionActivity()
} }
/** 退出 */ /** 退出 */
function logout() { function logout() {
currentUser.value = null currentUser.value = null
loginTime.value = 0 clearSessionActivity()
localStorage.removeItem('username') localStorage.removeItem('username')
localStorage.removeItem(USER_STORAGE_KEY) localStorage.removeItem(USER_STORAGE_KEY)
localStorage.removeItem('loginTime')
} }
return { return {
@@ -110,6 +117,7 @@ export const useAuthStore = defineStore('auth', () => {
hasPermission, hasPermission,
login, login,
refresh, refresh,
syncSession,
logout, logout,
} }
}) })

View File

@@ -0,0 +1,32 @@
import { ref } from 'vue'
const LOGIN_TIME_STORAGE_KEY = 'loginTime'
function storedActivityTime() {
return Number.parseInt(localStorage.getItem(LOGIN_TIME_STORAGE_KEY) || '0', 10) || 0
}
/**
* 会话按“最后活跃时间”计算,而不是从首次登录起固定倒计时。
* 该 ref 被认证 store 与请求层共享,确保 API 活动可以立即影响路由守卫。
*/
export const sessionActivityTime = ref(storedActivityTime())
export function startSessionActivity() {
sessionActivityTime.value = Date.now()
localStorage.setItem(LOGIN_TIME_STORAGE_KEY, String(sessionActivityTime.value))
}
export function touchSessionActivity() {
if (!sessionActivityTime.value) return
startSessionActivity()
}
export function syncSessionActivity() {
sessionActivityTime.value = storedActivityTime()
}
export function clearSessionActivity() {
sessionActivityTime.value = 0
localStorage.removeItem(LOGIN_TIME_STORAGE_KEY)
}