Files
workspace/code/fms/fms-vue/src/stores/permissions.js
T
2026-09-20 22:05:20 +08:00

190 lines
7.1 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 { computed, ref, watch } from 'vue'
import { defineStore } from 'pinia'
import { loadDataApi } from '@/services/api'
import {
buildActionPowerCode,
buildMenuPowerCode,
buildModulePowerCode,
isStandardModuleAction,
} from '@/utils/powerCodes'
import { useAuthStore } from './auth'
/**
* 权限 store:按《FMS新系统核心表结构设计》§14 读取新表并暴露访问与功能点判断。
*
* s_power 系统有哪些权限点(menu.* / module.* / action.*)
* s_user_power 当前用户拥有哪些权限点(白名单:无记录即无权限)
*
* 判定口径(与后端 SqlPermissionService 一致):
* - s_power 中不存在该权限点、或任一侧 b_canuse = 0,授权一律不生效;
* - 标准操作(read / create / update / delete / export)由「模块使用权限」统一表达,
* 有 module.* 即允许(见 @/utils/powerCodes);
* - 业务动作(审核 / 反审 / 结算 …)按 action.{模块}.{动作} 单独授权;
* - 菜单入口(menu.*)只控制「能否进入页面」,与模块权限相互独立。
*
* - 管理员账号(g3soft)提权:全部启用模块可见、一切判断放行,不查授权表;
* - 同一主体(orgId:userId)加载结果去重,并发请求复用。
*/
export const usePermissionStore = defineStore('permissions', () => {
const authStore = useAuthStore()
const loadedPrincipalKey = ref('')
const modules = ref([])
/** 当前用户拥有的权限点编码(s_user_power ∩ s_power 启用行) */
const grantedPowerCodes = ref([])
let loadingPromise = null
let loadingPrincipalKey = ''
let loadVersion = 0
// 字符串业务键:模块编码即主键 b_id
const moduleByCode = computed(() => new Map(modules.value.map((m) => [String(m.b_id || ''), m])))
const grantedPowerSet = computed(() => new Set(grantedPowerCodes.value))
const isG3soft = computed(() => {
return (
String(authStore.loginInfo.user?.account || '')
.trim()
.toLowerCase() === 'g3soft'
)
})
/**
* 加载权限数据;重复调用对同一主体去重,并发调用共享同一请求。
*/
async function load() {
const userId = String(authStore.loginInfo.user?.id || '')
const principalKey = `${String(authStore.userInfo.orgId || '')}:${userId}`
// 管理员提权:直接加载全部启用模块,不查授权表
if (isG3soft.value) {
const elevatedKey = `g3soft:${principalKey}`
if (loadedPrincipalKey.value === elevatedKey) return
loadingPromise = null
loadingPrincipalKey = ''
const requestVersion = ++loadVersion
const request = loadDataApi('s_module', 'b_canuse = 1', 'b_id ASC')
.then((res) => {
if (requestVersion !== loadVersion) return
modules.value = res.data || []
grantedPowerCodes.value = []
loadedPrincipalKey.value = elevatedKey
})
.finally(() => {
if (requestVersion !== loadVersion) return
loadingPromise = null
loadingPrincipalKey = ''
})
loadingPromise = request
return request
}
if (loadedPrincipalKey.value === principalKey) return
if (loadingPromise && loadingPrincipalKey === principalKey) return loadingPromise
const requestVersion = ++loadVersion
loadingPrincipalKey = principalKey
// 授权表在部分机构库可能尚未建立:失败时按无授权处理,不阻塞登录主流程。
const failSafe = () => ({ data: [] })
const quotedUserId = `N'${String(userId).replaceAll("'", "''")}'`
const request = Promise.all([
loadDataApi('s_module', 'b_canuse = 1', 'b_id ASC'),
loadDataApi('s_power', 'b_canuse = 1', 'b_id ASC').catch(failSafe),
loadDataApi(
's_user_power',
`b_user_id = ${quotedUserId} AND b_canuse = 1`,
'b_power_id ASC',
).catch(failSafe),
])
.then(([moduleResponse, powerResponse, userPowerResponse]) => {
if (requestVersion !== loadVersion) return
// 只有 s_power 中启用的权限点、且用户被授权才算数(设计文档 14.2 判定规则 2)
const activePowers = new Set(
(powerResponse.data || []).map((row) => String(row.b_id || '')),
)
modules.value = moduleResponse.data || []
grantedPowerCodes.value = (userPowerResponse.data || [])
.map((row) => String(row.b_power_id || ''))
.filter((code) => code && activePowers.has(code))
loadedPrincipalKey.value = principalKey
})
.finally(() => {
if (requestVersion !== loadVersion) return
loadingPromise = null
loadingPrincipalKey = ''
})
loadingPromise = request
return request
}
/**
* 判断当前用户是否有某模块的使用权限(管理员恒为 true)。
* 白名单语义:没有 module.{模块编码} 授权即不可访问。
*/
function canAccess(moduleCode) {
if (isG3soft.value || !moduleCode) return true
if (!moduleByCode.value.has(String(moduleCode))) return false
return grantedPowerSet.value.has(buildModulePowerCode(moduleCode))
}
/**
* 判断当前用户是否有某模块的功能点权限。
* 标准操作(read / create / update / delete / export)由模块使用权限统一表达,
* 有 module.* 即允许;业务动作(audit 等)要求单独的 action.{模块}.{动作} 授权。
*/
function canPower(scopeCode, action) {
if (isG3soft.value) return true
const code = String(scopeCode || '')
const actionCode = String(action || '')
if (!code || !actionCode) return false
if (!canAccess(code)) return false
if (isStandardModuleAction(actionCode)) return true
return grantedPowerSet.value.has(buildActionPowerCode(code, actionCode))
}
/**
* 判断当前用户能否进入某个菜单页面(侧栏渲染与路由入口共用)。
* 白名单语义:没有 menu.{菜单编码} 授权即不可见、不可进入。
*/
function canEnterMenu(menuCode) {
if (isG3soft.value || !menuCode) return true
return grantedPowerSet.value.has(buildMenuPowerCode(menuCode))
}
function clear() {
// 仅当“正在进行中的请求属于别的主体”时才作废它(如切换账号),
// 否则保留进行中请求、只清空已加载数据,交由后续 load 结果填充。
// 这避免登录(setSession 后守卫立即 load)被自身的 watch(clear) 误作废,导致菜单空白。
const userId = String(authStore.loginInfo.user?.id || '')
const principalKey = `${String(authStore.userInfo.orgId || '')}:${userId}`
if (loadingPrincipalKey && loadingPrincipalKey !== principalKey) {
loadVersion++
loadingPromise = null
loadingPrincipalKey = ''
}
loadedPrincipalKey.value = ''
modules.value = []
grantedPowerCodes.value = []
}
// 登录主体变化(退出/切换账号)时清空权限缓存
watch(
() => [
authStore.userInfo.orgId,
authStore.loginInfo.user?.id,
authStore.loginInfo.user?.account,
authStore.loginInfo.token,
],
clear,
)
return {
load,
canAccess,
canPower,
canEnterMenu,
clear,
modules,
grantedPowerCodes,
isG3soft,
}
})