新增 用户身份的页面权限控制系统

This commit is contained in:
tianfeng 2025-08-22 11:57:19 +08:00
parent d59584fc28
commit 32730f9299
6 changed files with 739 additions and 102 deletions

View File

@ -0,0 +1,141 @@
<!-- 权限控制组件示例 -->
<template>
<div class="permission-guard">
<!-- 权限不足提示 -->
<div v-if="!hasPermission && showDeniedMessage" class="permission-denied">
<div class="denied-icon">🚫</div>
<h3>Access Denied</h3>
<p>You don't have permission to access this feature.</p>
<p>Current role: {{ roleDisplayName }}</p>
<button @click="redirectToDefaultPage" class="redirect-btn">
Go to {{ defaultPageName }}
</button>
</div>
<!-- 有权限时显示内容 -->
<slot v-else></slot>
</div>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { permissionManager, hasPermission, getDefaultPage, getPrimaryRole } from '../utils/permissionManager.js'
const props = defineProps({
//
requiredPage: {
type: String,
required: true
},
//
showDeniedMessage: {
type: Boolean,
default: true
},
//
autoRedirect: {
type: Boolean,
default: false
}
})
const router = useRouter()
//
const hasPagePermission = computed(() => {
return hasPermission(props.requiredPage)
})
//
const hasPermission = computed(() => {
return permissionManager.isLoaded() && hasPagePermission.value
})
//
const roleDisplayName = computed(() => {
return permissionManager.getRoleDisplayName()
})
//
const defaultPageName = computed(() => {
const defaultPage = getDefaultPage()
const pageNames = {
'/coin-seller': 'Coin Seller',
'/agency-center': 'Agency Center',
'/bd-center': 'BD Center',
'/host-center': 'Host Center',
'/apply': 'Application'
}
return pageNames[defaultPage] || 'Home'
})
//
const redirectToDefaultPage = () => {
const defaultPage = getDefaultPage()
router.push(defaultPage)
}
//
onMounted(() => {
if (props.autoRedirect && !hasPermission.value && permissionManager.isLoaded()) {
redirectToDefaultPage()
}
})
</script>
<style scoped>
.permission-guard {
width: 100%;
height: 100%;
}
.permission-denied {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
min-height: 300px;
}
.denied-icon {
font-size: 48px;
margin-bottom: 16px;
}
.permission-denied h3 {
margin: 0 0 12px 0;
font-size: 24px;
font-weight: 600;
color: #dc2626;
}
.permission-denied p {
margin: 0 0 8px 0;
font-size: 16px;
color: #6b7280;
}
.redirect-btn {
margin-top: 20px;
padding: 12px 24px;
background-color: #8b5cf6;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.redirect-btn:hover {
background-color: #7c3aed;
}
.redirect-btn:active {
transform: scale(0.98);
}
</style>

View File

@ -18,6 +18,9 @@ if (versionChecker.checkVersion()) {
// }
}
// 初始化权限系统
console.debug('🔐 Permission system initialized')
const app = createApp(App)
app.use(router)

View File

@ -1,115 +1,144 @@
import { createRouter, createWebHistory } from 'vue-router'
import { installRouteGuard } from '../utils/routeGuard.js'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/HostCenterView.vue'),
redirect: '/host-center' // 默认重定向,实际会被路由守卫处理
},
{
path: '/host-center',
name: 'host-center',
component: () => import('../views/HostCenterView.vue'),
meta: { requiresAuth: true }
},
{
path: '/agency-center',
name: 'agency-center',
component: () => import('../views/AgencyCenterView.vue'),
meta: { requiresAuth: true }
},
{
path: '/bd-center',
name: 'bd-center',
component: () => import('../views/BDCenterView.vue'),
meta: { requiresAuth: true }
},
{
path: '/coin-seller',
name: 'coin-seller',
component: () => import('../views/CoinSellerView.vue'),
meta: { requiresAuth: true }
},
{
path: '/apply',
name: 'apply',
component: () => import('../views/ApplyView.vue'),
meta: { requiresAuth: false }
},
{
path: '/message',
name: 'message',
component: () => import('../views/MessageView.vue'),
meta: { requiresAuth: true }
},
{
path: '/transfer',
name: 'transfer',
component: () => import('../views/TransferView.vue'),
meta: { requiresAuth: true }
},
{
path: '/exchange-gold-coins',
name: 'exchange-gold-coins',
component: () => import('../views/ExchangeGoldCoinsView.vue'),
meta: { requiresAuth: true }
},
// 辅助页面
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
},
{
path: '/apply',
name: 'apply',
component: () => import('../views/ApplyView.vue'),
},
{
path: '/host-center',
redirect: '/'
},
{
path: '/host-setting',
name: 'host-setting',
component: () => import('../views/HostSettingView.vue'),
},
{
path: '/transfer',
name: 'transfer',
component: () => import('../views/TransferView.vue'),
meta: { requiresAuth: true }
},
{
path: '/search-payee',
name: 'search-payee',
component: () => import('../views/SearchPayeeView.vue'),
meta: { requiresAuth: true }
},
{
path: '/information-details',
name: 'information-details',
component: () => import('../views/InformationDetailsView.vue'),
},
{
path: '/message',
name: 'message',
component: () => import('../views/MessageView.vue'),
meta: { requiresAuth: true }
},
{
path: '/invite-members',
name: 'invite-members',
component: () => import('../views/InviteMembersView.vue'),
meta: { requiresAuth: true }
},
{
path: '/team-bill',
name: 'team-bill',
component: () => import('../views/TeamBillView.vue'),
meta: { requiresAuth: true }
},
{
path: '/team-member',
name: 'team-member',
component: () => import('../views/TeamMemberView.vue'),
meta: { requiresAuth: true }
},
{
path: '/platform-policy',
name: 'platform-policy',
component: () => import('../views/PlatformPolicyView.vue'),
},
{
path: '/coin-seller',
name: 'coin-seller',
component: () => import('../views/CoinSellerView.vue'),
meta: { requiresAuth: true }
},
{
path: '/coin-seller-search',
name: 'coin-seller-search',
component: () => import('../views/CoinSellerSearchView.vue'),
meta: { requiresAuth: true }
},
{
path: '/seller-records',
name: 'seller-records',
component: () => import('../views/SellerRecordsView.vue'),
},
{
path: '/exchange-gold-coins',
name: 'exchange-gold-coins',
component: () => import('../views/ExchangeGoldCoinsView.vue'),
},
{
path: '/agency-center',
name: 'agency-center',
component: () => import('../views/AgencyCenterView.vue'),
},
{
path: '/bd-center',
name: 'bd-center',
component: () => import('../views/BDCenterView.vue'),
meta: { requiresAuth: true }
},
{
path: '/bd-setting',
name: 'bd-setting',
component: () => import('../views/BDSettingView.vue'),
meta: { requiresAuth: true }
},
// 公共页面
{
path: '/not_app',
name: 'not-app',
component: () => import('../views/NotAppView.vue'),
meta: { requiresAuth: false }
},
// 404 页面
{
path: '/:pathMatch(.*)*',
name: 'not-found',
redirect: '/apply'
}
],
})
// 安装路由守卫
installRouteGuard(router)
export default router

View File

@ -0,0 +1,220 @@
/**
* 权限管理中心
* 负责用户身份检查权限验证页面路由等核心功能
*/
// 用户身份类型
export const USER_ROLES = {
FREIGHT_AGENT: 'freightAgent',
AGENT: 'agent',
BD: 'bd',
ANCHOR: 'anchor',
GUEST: 'guest'
}
// 页面路径常量
export const PAGES = {
COIN_SELLER: '/coin-seller',
AGENCY_CENTER: '/agency-center',
BD_CENTER: '/bd-center',
HOST_CENTER: '/host-center',
APPLY: '/apply',
MESSAGE: '/message',
TRANSFER: '/transfer',
EXCHANGE: '/exchange-gold-coins',
NOT_APP: '/not_app'
}
// 页面权限映射表(根据你的需求)
export const PAGE_PERMISSIONS = {
[PAGES.COIN_SELLER]: [USER_ROLES.FREIGHT_AGENT],
[PAGES.AGENCY_CENTER]: [USER_ROLES.AGENT],
[PAGES.BD_CENTER]: [USER_ROLES.BD],
[PAGES.HOST_CENTER]: [USER_ROLES.AGENT, USER_ROLES.ANCHOR],
[PAGES.APPLY]: [USER_ROLES.GUEST],
[PAGES.MESSAGE]: [USER_ROLES.AGENT],
[PAGES.TRANSFER]: [USER_ROLES.AGENT, USER_ROLES.BD, USER_ROLES.ANCHOR],
[PAGES.EXCHANGE]: [USER_ROLES.AGENT, USER_ROLES.BD, USER_ROLES.ANCHOR],
[PAGES.NOT_APP]: [] // 所有人都可以访问
}
// 身份优先级(数字越大优先级越高)
export const ROLE_PRIORITY = {
[USER_ROLES.FREIGHT_AGENT]: 4,
[USER_ROLES.AGENT]: 3,
[USER_ROLES.BD]: 2,
[USER_ROLES.ANCHOR]: 1,
[USER_ROLES.GUEST]: 0
}
// 身份对应的默认首页
export const ROLE_DEFAULT_PAGES = {
[USER_ROLES.FREIGHT_AGENT]: PAGES.COIN_SELLER,
[USER_ROLES.AGENT]: PAGES.AGENCY_CENTER,
[USER_ROLES.BD]: PAGES.BD_CENTER,
[USER_ROLES.ANCHOR]: PAGES.HOST_CENTER,
[USER_ROLES.GUEST]: PAGES.APPLY
}
/**
* 权限管理类
*/
class PermissionManager {
constructor() {
this.userIdentity = null
this.primaryRole = null
this.isIdentityLoaded = false
this.allowedPages = []
}
/**
* 设置用户身份信息
* @param {Object} identity - 身份信息对象
*/
setUserIdentity(identity) {
this.userIdentity = identity
this.primaryRole = this.calculatePrimaryRole(identity)
this.allowedPages = this.calculateAllowedPages(this.primaryRole)
this.isIdentityLoaded = true
console.debug('🔐 User identity updated:', {
identity: this.userIdentity,
primaryRole: this.primaryRole,
allowedPages: this.allowedPages
})
}
/**
* 计算用户主要身份优先级最高的身份
* @param {Object} identity - 身份信息
* @returns {string} 主要身份
*/
calculatePrimaryRole(identity) {
if (!identity) return USER_ROLES.GUEST
// 按优先级检查身份
if (identity.freightAgent) return USER_ROLES.FREIGHT_AGENT
if (identity.agent) return USER_ROLES.AGENT
if (identity.bd) return USER_ROLES.BD
if (identity.anchor) return USER_ROLES.ANCHOR
return USER_ROLES.GUEST
}
/**
* 计算用户允许访问的页面
* @param {string} primaryRole - 主要身份
* @returns {Array} 允许访问的页面列表
*/
calculateAllowedPages(primaryRole) {
const allowedPages = []
Object.entries(PAGE_PERMISSIONS).forEach(([page, roles]) => {
if (roles.length === 0 || roles.includes(primaryRole)) {
allowedPages.push(page)
}
})
return allowedPages
}
/**
* 检查页面访问权限
* @param {string} page - 页面路径
* @returns {boolean} 是否有权限访问
*/
hasPagePermission(page) {
if (!this.isIdentityLoaded) {
console.warn('⚠️ Identity not loaded yet')
return false
}
// 检查是否在允许列表中
const hasPermission = this.allowedPages.includes(page)
console.debug('🔍 Permission check:', {
page,
primaryRole: this.primaryRole,
hasPermission
})
return hasPermission
}
/**
* 获取用户默认首页
* @returns {string} 默认页面路径
*/
getDefaultPage() {
return ROLE_DEFAULT_PAGES[this.primaryRole] || PAGES.APPLY
}
/**
* 获取当前用户身份
* @returns {string} 当前身份
*/
getPrimaryRole() {
return this.primaryRole
}
/**
* 获取身份显示名称
* @param {string} role - 身份标识
* @returns {string} 显示名称
*/
getRoleDisplayName(role = this.primaryRole) {
const roleNames = {
[USER_ROLES.FREIGHT_AGENT]: 'Freight Agent',
[USER_ROLES.AGENT]: 'Agent',
[USER_ROLES.BD]: 'BD',
[USER_ROLES.ANCHOR]: 'Anchor',
[USER_ROLES.GUEST]: 'Guest'
}
return roleNames[role] || 'Unknown'
}
/**
* 检查是否有多重身份
* @returns {Array} 拥有的所有身份
*/
getAllRoles() {
if (!this.userIdentity) return [USER_ROLES.GUEST]
const roles = []
if (this.userIdentity.freightAgent) roles.push(USER_ROLES.FREIGHT_AGENT)
if (this.userIdentity.agent) roles.push(USER_ROLES.AGENT)
if (this.userIdentity.bd) roles.push(USER_ROLES.BD)
if (this.userIdentity.anchor) roles.push(USER_ROLES.ANCHOR)
return roles.length > 0 ? roles : [USER_ROLES.GUEST]
}
/**
* 重置权限信息
*/
reset() {
this.userIdentity = null
this.primaryRole = null
this.isIdentityLoaded = false
this.allowedPages = []
console.debug('🔄 Permission manager reset')
}
/**
* 检查是否身份加载完成
* @returns {boolean}
*/
isLoaded() {
return this.isIdentityLoaded
}
}
// 创建全局权限管理实例
export const permissionManager = new PermissionManager()
// 便捷方法
export const hasPermission = (page) => permissionManager.hasPagePermission(page)
export const getDefaultPage = () => permissionManager.getDefaultPage()
export const getPrimaryRole = () => permissionManager.getPrimaryRole()
export const setUserIdentity = (identity) => permissionManager.setUserIdentity(identity)
export const resetPermissions = () => permissionManager.reset()

294
src/utils/routeGuard.js Normal file
View File

@ -0,0 +1,294 @@
/**
* 路由守卫
* 提供页面访问权限控制和自动重定向功能
*/
import { permissionManager, PAGES, USER_ROLES } from './permissionManager.js'
import { getUserIdentity } from '../api/wallet.js'
import { getUserId } from './userStore.js'
/**
* 身份检查器
*/
class IdentityChecker {
constructor() {
this.isChecking = false
this.checkPromise = null
}
/**
* 检查并设置用户身份
* @param {string} userId - 用户ID
* @returns {Promise<string>} 返回主要身份
*/
async checkUserIdentity(userId) {
if (!userId) {
console.warn('⚠️ No user ID provided')
permissionManager.setUserIdentity(null)
return USER_ROLES.GUEST
}
// 如果正在检查中,等待现有的检查完成
if (this.isChecking && this.checkPromise) {
console.debug('⏳ Identity check already in progress, waiting...')
return await this.checkPromise
}
this.isChecking = true
this.checkPromise = this._performIdentityCheck(userId)
try {
const primaryRole = await this.checkPromise
return primaryRole
} finally {
this.isChecking = false
this.checkPromise = null
}
}
/**
* 执行实际的身份检查
* @param {string} userId - 用户ID
* @returns {Promise<string>} 主要身份
*/
async _performIdentityCheck(userId) {
try {
console.debug('🔍 Checking user identity for:', userId)
const response = await getUserIdentity(userId)
if (response && response.status && response.body) {
const identity = response.body
console.debug('✅ Identity fetched:', identity)
// 设置权限信息
permissionManager.setUserIdentity(identity)
return permissionManager.getPrimaryRole()
} else {
console.warn('⚠️ Invalid identity response')
permissionManager.setUserIdentity(null)
return USER_ROLES.GUEST
}
} catch (error) {
console.error('❌ Failed to fetch user identity:', error)
permissionManager.setUserIdentity(null)
return USER_ROLES.GUEST
}
}
/**
* 重置检查状态
*/
reset() {
this.isChecking = false
this.checkPromise = null
}
}
/**
* 页面重定向器
*/
class PageRedirector {
/**
* 根据身份自动重定向到正确页面
* @param {Object} router - Vue Router 实例
* @param {string} targetPath - 目标页面路径
* @param {string} currentPath - 当前页面路径
* @returns {Promise<boolean>} 是否进行了重定向
*/
async redirectToCorrectPage(router, targetPath, currentPath) {
const primaryRole = permissionManager.getPrimaryRole()
// 检查目标页面权限
const hasPermission = permissionManager.hasPagePermission(targetPath)
console.debug('🔄 Redirect check:', {
targetPath,
currentPath,
primaryRole,
hasPermission
})
if (hasPermission) {
// 有权限访问目标页面
if (targetPath !== currentPath) {
console.debug('✅ Access granted, navigating to:', targetPath)
return false // 不需要重定向,让路由正常进行
}
return false
} else {
// 无权限访问,重定向到默认页面
const defaultPage = permissionManager.getDefaultPage()
console.debug('❌ Access denied, redirecting to:', defaultPage)
if (defaultPage !== currentPath) {
await router.replace(defaultPage)
return true // 已进行重定向
}
return false
}
}
/**
* 处理越权访问
* @param {Object} router - Vue Router 实例
* @param {string} attemptedPage - 尝试访问的页面
*/
async handleUnauthorizedAccess(router, attemptedPage) {
const primaryRole = permissionManager.getPrimaryRole()
const defaultPage = permissionManager.getDefaultPage()
console.warn('🚫 Unauthorized access attempt:', {
attemptedPage,
primaryRole,
redirectingTo: defaultPage
})
// 重定向到用户有权限的默认页面
await router.replace(defaultPage)
}
}
/**
* 路由守卫类
*/
class RouteGuard {
constructor() {
this.identityChecker = new IdentityChecker()
this.pageRedirector = new PageRedirector()
this.isGuardActive = false
}
/**
* 安装路由守卫
* @param {Object} router - Vue Router 实例
*/
install(router) {
router.beforeEach(async (to, from, next) => {
await this.handleRouteChange(to, from, next)
})
this.isGuardActive = true
console.debug('🛡️ Route guard installed')
}
/**
* 处理路由变化
* @param {Object} to - 目标路由
* @param {Object} from - 来源路由
* @param {Function} next - 路由继续函数
*/
async handleRouteChange(to, from, next) {
console.group('🛡️ Route Guard Check')
console.debug('📍 Route change:', from.path, '→', to.path)
try {
// 1. 检查是否需要身份验证的页面
if (this.isPublicPage(to.path)) {
console.debug('🌐 Public page, allowing access')
console.groupEnd()
next()
return
}
// 2. 检查用户身份是否已加载
if (!permissionManager.isLoaded()) {
console.debug('🔍 Identity not loaded, checking...')
const userId = getUserId()
if (!userId) {
console.debug('❌ No user ID, redirecting to apply')
console.groupEnd()
next(PAGES.APPLY)
return
}
// 检查用户身份
await this.identityChecker.checkUserIdentity(userId)
}
// 3. 检查页面访问权限
const hasPermission = permissionManager.hasPagePermission(to.path)
if (hasPermission) {
console.debug('✅ Permission granted')
console.groupEnd()
next()
} else {
console.debug('❌ Permission denied')
const defaultPage = permissionManager.getDefaultPage()
console.debug('🔄 Redirecting to:', defaultPage)
console.groupEnd()
next(defaultPage)
}
} catch (error) {
console.error('❌ Route guard error:', error)
console.groupEnd()
next(PAGES.APPLY) // 出错时重定向到申请页面
}
}
/**
* 检查是否为公共页面无需权限验证
* @param {string} path - 页面路径
* @returns {boolean}
*/
isPublicPage(path) {
const publicPages = [
PAGES.NOT_APP,
'/404',
'/error'
]
return publicPages.includes(path)
}
/**
* 手动触发身份检查和重定向
* @param {Object} router - Vue Router 实例
* @param {string} userId - 用户ID
*/
async checkAndRedirect(router, userId) {
try {
console.debug('🔄 Manual identity check and redirect')
// 检查身份
const primaryRole = await this.identityChecker.checkUserIdentity(userId)
// 获取当前页面
const currentPath = router.currentRoute.value.path
// 检查当前页面权限
const hasPermission = permissionManager.hasPagePermission(currentPath)
if (!hasPermission) {
// 重定向到默认页面
await this.pageRedirector.redirectToCorrectPage(router, currentPath, currentPath)
}
return primaryRole
} catch (error) {
console.error('❌ Manual check and redirect failed:', error)
throw error
}
}
/**
* 重置守卫状态
*/
reset() {
this.identityChecker.reset()
permissionManager.reset()
console.debug('🔄 Route guard reset')
}
}
// 创建全局路由守卫实例
export const routeGuard = new RouteGuard()
// 便捷方法
export const installRouteGuard = (router) => routeGuard.install(router)
export const checkAndRedirect = (router, userId) => routeGuard.checkAndRedirect(router, userId)
export const resetRouteGuard = () => routeGuard.reset()

View File

@ -128,21 +128,20 @@
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MobileHeader from '../components/MobileHeader.vue'
import { getBankBalance, getUserIdentity, getTeamEntry, getTeamMemberWorkStatistics } from '../api/wallet.js'
import { getBankBalance, getTeamEntry, getTeamMemberWorkStatistics } from '../api/wallet.js'
import { connectApplication, parseAccessOrigin, parseHeader, setHttpHeaders, isInApp } from '../utils/appBridge.js'
import { appConnectionManager, isAppConnected, getAppHeaderInfo } from '../utils/appConnectionManager.js'
import { checkAndRedirect } from '../utils/routeGuard.js'
import {getUserId, setUserInfo} from '../utils/userStore.js'
import {showError} from "@/utils/toast.js";
const router = useRouter()
//
const currentIdentity = ref('host')
const loading = ref(false)
const appConnected = ref(false)
const headerInfo = ref({})
const userProfile = ref(null)
const identityChecked = ref(false)
// APP
const connectToApp = async () => {
@ -177,6 +176,9 @@ const connectToApp = async () => {
if (teamEntryResult != null) {
await fetchBankBalance()
await fetchWorkStatistics()
// 使
await checkAndRedirect(router, teamEntryResult)
}
console.groupEnd()
return
@ -224,6 +226,9 @@ const connectToApp = async () => {
if (teamEntryResult != null) {
await fetchBankBalance()
await fetchWorkStatistics()
// 使
await checkAndRedirect(router, teamEntryResult)
}
resolve()
@ -354,13 +359,7 @@ const fetchTeamEntry = async () => {
userInfo.userAvatar = response.body.memberProfile.userAvatar
}
// ID
const userId = response.body.memberProfile?.id
if (userId) {
await checkUserIdentity(userId)
}
return userId
return response.body.memberProfile?.id
}
} catch (error) {
console.error('获取团队信息失败:', error)
@ -380,55 +379,6 @@ const fetchTeamEntry = async () => {
return null
}
//
const checkUserIdentity = async (userId) => {
if (!userId) {
console.error('用户ID不存在')
return
}
//
if (identityChecked.value) {
console.debug('身份已检查,跳过')
return
}
try {
identityChecked.value = true
const response = await getUserIdentity(userId)
if (response && response.status && response.body) {
const { freightAgent, anchor, agent, bd } = response.body
console.debug('用户身份权限:', { freightAgent, anchor, agent, bd })
// freightAgent > agent > anchor > bd
if (freightAgent) {
console.debug('跳转到 coin-seller')
router.replace('/coin-seller')
} else if (agent) {
console.debug('跳转到 agency-center')
router.replace('/agency-center')
} else if (bd) {
console.debug('跳转到 bd-center')
router.replace('/bd-center')
} else if (anchor) {
// Host
console.debug('保持在 host-center')
currentIdentity.value = 'host'
} else {
// Host
console.debug('无特殊权限,保持在 host-center')
currentIdentity.value = 'host'
}
}
} catch (error) {
console.error('获取用户身份失败:', error)
// Host
currentIdentity.value = 'host'
}
}
//
const handleImageError = (event) => {
// userAvatar