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, } })