Compare commits
4 Commits
220f50dba6
...
57c2c532ca
| Author | SHA1 | Date | |
|---|---|---|---|
| 57c2c532ca | |||
| dad7ffdc66 | |||
| b798c42abf | |||
| 8698b2582c |
@ -1,7 +1,6 @@
|
|||||||
# inventory-backend/app/services/permission_service.py
|
|
||||||
|
|
||||||
from app.models.system import SysMenu, SysElement, SysRolePermission
|
from app.models.system import SysMenu, SysElement, SysRolePermission
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
|
||||||
class PermissionService:
|
class PermissionService:
|
||||||
@ -9,84 +8,123 @@ class PermissionService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_permission_tree():
|
def get_permission_tree():
|
||||||
"""
|
"""
|
||||||
获取完整的权限树(菜单 -> 元素)
|
获取完整的权限树(菜单嵌套菜单 + 菜单包含元素)
|
||||||
供前端权限配置页面展示
|
供前端权限配置页面展示
|
||||||
"""
|
"""
|
||||||
# 1. 获取所有菜单
|
# 1. 获取所有菜单 (按 parent_id 和 sort_order 排序,保证父子处理顺序)
|
||||||
menus = SysMenu.query.order_by(SysMenu.sort_order).all()
|
menus = SysMenu.query.order_by(SysMenu.parent_id, SysMenu.sort_order).all()
|
||||||
|
|
||||||
# 2. 获取所有元素
|
# 2. 获取所有元素
|
||||||
elements = SysElement.query.all()
|
elements = SysElement.query.all()
|
||||||
|
|
||||||
# 3. 组装树结构
|
# --- 核心逻辑:构建树形结构 ---
|
||||||
|
|
||||||
|
# 3. 创建一个 lookup 字典,方便通过 ID 查找菜单节点
|
||||||
|
# 同时将 SQLAlchemy 对象转为字典,方便后续操作
|
||||||
|
menu_map = {}
|
||||||
|
for m in menus:
|
||||||
|
m_dict = m.to_dict()
|
||||||
|
m_dict['children'] = [] # 初始化 children
|
||||||
|
menu_map[m.id] = m_dict
|
||||||
|
|
||||||
|
# 4. 创建 code 到 id 的映射,用于把 element 挂载到 menu 上
|
||||||
|
# 因为 SysElement 关联的是 menu_code,而不是 menu_id
|
||||||
|
code_to_id = {m.code: m.id for m in menus}
|
||||||
|
|
||||||
|
# 5. 将元素 (Elements) 挂载到对应的菜单 (Menu) 下
|
||||||
|
for el in elements:
|
||||||
|
# 找到该元素所属菜单的 ID
|
||||||
|
parent_menu_id = code_to_id.get(el.menu_code)
|
||||||
|
if parent_menu_id and parent_menu_id in menu_map:
|
||||||
|
el_dict = el.to_dict()
|
||||||
|
# 标记类型为 element,前端 transformData 需要用到
|
||||||
|
el_dict['type'] = 'element'
|
||||||
|
menu_map[parent_menu_id]['children'].append(el_dict)
|
||||||
|
|
||||||
|
# 6. 将子菜单挂载到父菜单下,并构建最终的树
|
||||||
tree_data = []
|
tree_data = []
|
||||||
|
for m in menus:
|
||||||
|
current_node = menu_map[m.id]
|
||||||
|
|
||||||
for menu in menus:
|
if m.parent_id == 0 or m.parent_id is None:
|
||||||
menu_dict = menu.to_dict()
|
# 如果是顶级菜单,直接放入结果集
|
||||||
|
tree_data.append(current_node)
|
||||||
# 找该菜单下的所有元素
|
else:
|
||||||
children = []
|
# 如果是子菜单,找到它的父级,把它塞进父级的 children 里
|
||||||
for el in elements:
|
if m.parent_id in menu_map:
|
||||||
if el.menu_code == menu.code:
|
menu_map[m.parent_id]['children'].append(current_node)
|
||||||
children.append(el.to_dict())
|
else:
|
||||||
|
# 如果找不到父级(比如父级被删了),为了防止数据丢失,暂时作为顶级显示
|
||||||
# 如果有子元素,加到 children
|
tree_data.append(current_node)
|
||||||
if children:
|
|
||||||
menu_dict['children'] = children
|
|
||||||
|
|
||||||
tree_data.append(menu_dict)
|
|
||||||
|
|
||||||
return tree_data
|
return tree_data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_role_permissions(role_code):
|
def get_role_permissions(role_code):
|
||||||
"""获取指定角色拥有的所有权限Code"""
|
"""获取指定角色拥有的所有权限Code"""
|
||||||
perms = SysRolePermission.query.filter_by(role_code=role_code).all()
|
try:
|
||||||
|
# === 新增逻辑:超级管理员上帝模式 ===
|
||||||
|
if role_code == 'SUPER_ADMIN':
|
||||||
|
# 直接获取所有菜单和元素,无视配置表
|
||||||
|
all_menus = [m.code for m in SysMenu.query.all()]
|
||||||
|
all_elements = [e.code for e in SysElement.query.all()]
|
||||||
|
return {
|
||||||
|
'menus': all_menus,
|
||||||
|
'elements': all_elements
|
||||||
|
}
|
||||||
|
# =================================
|
||||||
|
|
||||||
menu_codes = []
|
perms = SysRolePermission.query.filter_by(role_code=role_code).all()
|
||||||
element_codes = []
|
|
||||||
|
|
||||||
for p in perms:
|
menu_codes = []
|
||||||
if p.type == 'menu':
|
element_codes = []
|
||||||
menu_codes.append(p.target_code)
|
|
||||||
else:
|
|
||||||
element_codes.append(p.target_code)
|
|
||||||
|
|
||||||
return {
|
for p in perms:
|
||||||
'menus': menu_codes,
|
# 这里假设你的数据库存的是 target_code
|
||||||
'elements': element_codes
|
if p.type == 'menu':
|
||||||
}
|
menu_codes.append(p.target_code)
|
||||||
|
else:
|
||||||
|
element_codes.append(p.target_code)
|
||||||
|
|
||||||
|
# 前端 handleRoleSelect 会合并这两个数组,所以分开返回没问题
|
||||||
|
return {
|
||||||
|
'menus': menu_codes,
|
||||||
|
'elements': element_codes
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
# 记录日志或处理错误
|
||||||
|
print(f"Error fetching role permissions: {e}")
|
||||||
|
return {'menus': [], 'elements': []}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def assign_permissions(role_code, permission_codes):
|
def assign_permissions(role_code, permissions):
|
||||||
"""
|
"""
|
||||||
保存角色的权限
|
保存角色的权限
|
||||||
permission_codes: 前端传来的 list,包含 menu_code 和 element_code
|
permissions: 前端传来的 list,混合了 menu_code 和 element_code
|
||||||
"""
|
"""
|
||||||
if not role_code:
|
if not role_code:
|
||||||
raise ValueError("角色代码不能为空")
|
raise ValueError("角色代码不能为空")
|
||||||
|
|
||||||
|
session = db.session
|
||||||
try:
|
try:
|
||||||
# ========= 1️⃣ 先删除旧权限 =========
|
# 1. 开启事务 (Flask-SQLAlchemy 自动管理,但明确逻辑更好)
|
||||||
SysRolePermission.query.filter_by(role_code=role_code) \
|
|
||||||
.delete(synchronize_session=False)
|
|
||||||
|
|
||||||
# ========= 2️⃣ 去重(关键修复点) =========
|
# 2. 删除该角色旧的所有权限
|
||||||
# 防止前端传来重复 code 导致 UNIQUE 冲突
|
SysRolePermission.query.filter_by(role_code=role_code).delete()
|
||||||
unique_codes = set(permission_codes) if permission_codes else set()
|
|
||||||
|
|
||||||
# ========= 3️⃣ 批量添加新权限 =========
|
# 3. 准备新数据
|
||||||
if unique_codes:
|
if permissions:
|
||||||
# 预先获取所有菜单代码,用于判断类型
|
# 3.1 去重
|
||||||
all_menu_codes = {m.code for m in SysMenu.query.all()}
|
unique_codes = set(permissions)
|
||||||
|
|
||||||
|
# 3.2 预加载所有 Menu Code,用于区分是 Menu 还是 Element
|
||||||
|
# 这一步很重要,因为 SysRolePermission 表需要 type 字段
|
||||||
|
all_menu_codes = {res[0] for res in session.query(SysMenu.code).all()}
|
||||||
|
|
||||||
new_records = []
|
new_records = []
|
||||||
|
|
||||||
for code in unique_codes:
|
for code in unique_codes:
|
||||||
if not code:
|
if not code: continue
|
||||||
continue
|
|
||||||
|
|
||||||
# 判断类型
|
# 判断类型:如果 code 存在于菜单表中,就是 menu,否则就是 element
|
||||||
p_type = 'menu' if code in all_menu_codes else 'element'
|
p_type = 'menu' if code in all_menu_codes else 'element'
|
||||||
|
|
||||||
new_records.append(SysRolePermission(
|
new_records.append(SysRolePermission(
|
||||||
@ -95,14 +133,17 @@ class PermissionService:
|
|||||||
type=p_type
|
type=p_type
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# 3.3 批量插入
|
||||||
if new_records:
|
if new_records:
|
||||||
db.session.add_all(new_records)
|
session.add_all(new_records)
|
||||||
|
|
||||||
# ========= 4️⃣ 提交事务 =========
|
# 4. 提交
|
||||||
db.session.commit()
|
session.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
session.rollback()
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 发生异常时回滚,防止脏事务
|
session.rollback()
|
||||||
db.session.rollback()
|
raise e
|
||||||
raise e
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { login } from '@/api/auth'
|
import { login } from '@/api/auth'
|
||||||
|
import { getRolePermissions } from '@/api/system/permission'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
@ -7,6 +8,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const token = ref(localStorage.getItem('token') || '')
|
const token = ref(localStorage.getItem('token') || '')
|
||||||
const role = ref(localStorage.getItem('role') || '')
|
const role = ref(localStorage.getItem('role') || '')
|
||||||
const username = ref(localStorage.getItem('username') || '')
|
const username = ref(localStorage.getItem('username') || '')
|
||||||
|
const permissions = ref<string[]>(JSON.parse(localStorage.getItem('permissions') || '[]'))
|
||||||
|
|
||||||
// 2. Actions
|
// 2. Actions
|
||||||
// 登录逻辑
|
// 登录逻辑
|
||||||
@ -44,6 +46,25 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
// 持久化存储 Token
|
// 持久化存储 Token
|
||||||
localStorage.setItem('token', data.access_token)
|
localStorage.setItem('token', data.access_token)
|
||||||
|
|
||||||
|
// 登录成功后,根据角色获取权限
|
||||||
|
if (role.value) {
|
||||||
|
try {
|
||||||
|
const permRes = await getRolePermissions(role.value)
|
||||||
|
const permData = permRes.data || permRes
|
||||||
|
// 合并 menus 和 elements 两个数组
|
||||||
|
const allPerms = [
|
||||||
|
...(permData.menus || []),
|
||||||
|
...(permData.elements || [])
|
||||||
|
]
|
||||||
|
permissions.value = allPerms
|
||||||
|
localStorage.setItem('permissions', JSON.stringify(allPerms))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取权限失败:', error)
|
||||||
|
permissions.value = []
|
||||||
|
localStorage.setItem('permissions', '[]')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true // 返回 true 表示登录成功
|
return true // 返回 true 表示登录成功
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +74,13 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
token.value = ''
|
token.value = ''
|
||||||
role.value = ''
|
role.value = ''
|
||||||
username.value = ''
|
username.value = ''
|
||||||
|
permissions.value = []
|
||||||
|
|
||||||
// 2. 清空 LocalStorage (硬盘)
|
// 2. 清空 LocalStorage (硬盘)
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
localStorage.removeItem('role')
|
localStorage.removeItem('role')
|
||||||
localStorage.removeItem('username')
|
localStorage.removeItem('username')
|
||||||
|
localStorage.removeItem('permissions')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Getters / Helpers
|
// 3. Getters / Helpers
|
||||||
@ -66,12 +89,19 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
return roles.includes(role.value)
|
return roles.includes(role.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断当前用户是否拥有某个权限(菜单或元素)
|
||||||
|
const hasPermission = (code: string) => {
|
||||||
|
return permissions.value.includes(code)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
role,
|
role,
|
||||||
username,
|
username,
|
||||||
|
permissions,
|
||||||
handleLogin,
|
handleLogin,
|
||||||
logout,
|
logout,
|
||||||
hasRole
|
hasRole,
|
||||||
|
hasPermission
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -98,18 +98,18 @@
|
|||||||
<div style="font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px">
|
<div style="font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px">
|
||||||
列展示设置
|
列展示设置
|
||||||
</div>
|
</div>
|
||||||
<el-checkbox v-model="columns.id.visible" label="ID" />
|
<el-checkbox v-model="columns.id.visible" label="ID" :disabled="!userStore.hasPermission(permissionMap.id)" />
|
||||||
<el-checkbox v-model="columns.companyName.visible" label="所属公司" />
|
<el-checkbox v-model="columns.companyName.visible" label="所属公司" :disabled="!userStore.hasPermission(permissionMap.companyName)" />
|
||||||
<el-checkbox v-model="columns.name.visible" label="名称" />
|
<el-checkbox v-model="columns.name.visible" label="名称" :disabled="!userStore.hasPermission(permissionMap.name)" />
|
||||||
<el-checkbox v-model="columns.commonName.visible" label="俗名" />
|
<el-checkbox v-model="columns.commonName.visible" label="俗名" :disabled="!userStore.hasPermission(permissionMap.commonName)" />
|
||||||
<el-checkbox v-model="columns.category.visible" label="类别" />
|
<el-checkbox v-model="columns.category.visible" label="类别" :disabled="!userStore.hasPermission(permissionMap.category)" />
|
||||||
<el-checkbox v-model="columns.type.visible" label="类型" />
|
<el-checkbox v-model="columns.type.visible" label="类型" :disabled="!userStore.hasPermission(permissionMap.type)" />
|
||||||
<el-checkbox v-model="columns.spec.visible" label="规格型号" />
|
<el-checkbox v-model="columns.spec.visible" label="规格型号" :disabled="!userStore.hasPermission(permissionMap.spec)" />
|
||||||
<el-checkbox v-model="columns.unit.visible" label="单位" />
|
<el-checkbox v-model="columns.unit.visible" label="单位" :disabled="!userStore.hasPermission(permissionMap.unit)" />
|
||||||
<el-checkbox v-model="columns.inventory.visible" label="库存数" />
|
<el-checkbox v-model="columns.inventory.visible" label="库存数" :disabled="!userStore.hasPermission(permissionMap.inventory)" />
|
||||||
<el-checkbox v-model="columns.available.visible" label="可用数" />
|
<el-checkbox v-model="columns.available.visible" label="可用数" :disabled="!userStore.hasPermission(permissionMap.available)" />
|
||||||
<el-checkbox v-model="columns.files.visible" label="资料" />
|
<el-checkbox v-model="columns.files.visible" label="资料" :disabled="!userStore.hasPermission(permissionMap.files)" />
|
||||||
<el-checkbox v-model="columns.isEnabled.visible" label="状态" />
|
<el-checkbox v-model="columns.isEnabled.visible" label="状态" :disabled="!userStore.hasPermission(permissionMap.isEnabled)" />
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
@ -214,7 +214,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" min-width="150" fixed="right" align="center">
|
<el-table-column v-if="userStore.hasPermission('operation')" label="操作" min-width="150" fixed="right" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
<el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
<el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
@ -420,6 +420,7 @@ import { ref, reactive, onMounted, nextTick } from 'vue';
|
|||||||
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download } from '@element-plus/icons-vue';
|
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download } from '@element-plus/icons-vue';
|
||||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||||
import type { FormInstance, FormRules } from 'element-plus';
|
import type { FormInstance, FormRules } from 'element-plus';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
listMaterialBase,
|
listMaterialBase,
|
||||||
@ -432,6 +433,8 @@ import {
|
|||||||
import { uploadFile, deleteFile } from '@/api/common/upload';
|
import { uploadFile, deleteFile } from '@/api/common/upload';
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
// --- 类型定义 ---
|
// --- 类型定义 ---
|
||||||
interface MaterialBaseVO {
|
interface MaterialBaseVO {
|
||||||
id: number;
|
id: number;
|
||||||
@ -501,6 +504,38 @@ const columns = reactive({
|
|||||||
isEnabled: { visible: true }
|
isEnabled: { visible: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 列与权限Code的映射关系(数据库中的code)
|
||||||
|
const permissionMap: Record<string, string> = {
|
||||||
|
id: 'id',
|
||||||
|
companyName: 'companyName',
|
||||||
|
name: 'name',
|
||||||
|
commonName: 'commonName',
|
||||||
|
category: 'category',
|
||||||
|
type: 'type',
|
||||||
|
spec: 'spec',
|
||||||
|
unit: 'unit',
|
||||||
|
inventory: 'inventoryCount', // 前端变量是 inventory,数据库Code是 inventoryCount
|
||||||
|
available: 'availableCount', // 前端变量是 available,数据库Code是 availableCount
|
||||||
|
files: 'files',
|
||||||
|
isEnabled: 'isEnabled'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据用户权限初始化列显示状态
|
||||||
|
const initColumnPermissions = () => {
|
||||||
|
// 超级管理员跳过权限检查,显示所有列
|
||||||
|
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(columns).forEach(key => {
|
||||||
|
const code = permissionMap[key];
|
||||||
|
if (code) {
|
||||||
|
// 如果用户有该权限,则显示列(默认true);否则隐藏
|
||||||
|
columns[key].visible = userStore.hasPermission(code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const companyOptions = ref<string[]>([]);
|
const companyOptions = ref<string[]>([]);
|
||||||
const categoryOptions = ref<string[]>([]);
|
const categoryOptions = ref<string[]>([]);
|
||||||
const typeOptions = ref<string[]>([]);
|
const typeOptions = ref<string[]>([]);
|
||||||
@ -988,6 +1023,8 @@ const handleCameraConfirm = async (file: File) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 先根据权限初始化列显示状态
|
||||||
|
initColumnPermissions();
|
||||||
getList();
|
getList();
|
||||||
getOptionsList();
|
getOptionsList();
|
||||||
});
|
});
|
||||||
@ -1043,4 +1080,4 @@ onMounted(() => {
|
|||||||
.long-dropdown .el-select-dropdown__wrap {
|
.long-dropdown .el-select-dropdown__wrap {
|
||||||
max-height: 600px !important; /* 可以根据屏幕大小适当调整 */
|
max-height: 600px !important; /* 可以根据屏幕大小适当调整 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user