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.extensions import db
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
|
||||
class PermissionService:
|
||||
@ -9,84 +8,123 @@ class PermissionService:
|
||||
@staticmethod
|
||||
def get_permission_tree():
|
||||
"""
|
||||
获取完整的权限树(菜单 -> 元素)
|
||||
获取完整的权限树(菜单嵌套菜单 + 菜单包含元素)
|
||||
供前端权限配置页面展示
|
||||
"""
|
||||
# 1. 获取所有菜单
|
||||
menus = SysMenu.query.order_by(SysMenu.sort_order).all()
|
||||
|
||||
# 1. 获取所有菜单 (按 parent_id 和 sort_order 排序,保证父子处理顺序)
|
||||
menus = SysMenu.query.order_by(SysMenu.parent_id, SysMenu.sort_order).all()
|
||||
# 2. 获取所有元素
|
||||
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 = []
|
||||
for m in menus:
|
||||
current_node = menu_map[m.id]
|
||||
|
||||
for menu in menus:
|
||||
menu_dict = menu.to_dict()
|
||||
|
||||
# 找该菜单下的所有元素
|
||||
children = []
|
||||
for el in elements:
|
||||
if el.menu_code == menu.code:
|
||||
children.append(el.to_dict())
|
||||
|
||||
# 如果有子元素,加到 children
|
||||
if children:
|
||||
menu_dict['children'] = children
|
||||
|
||||
tree_data.append(menu_dict)
|
||||
if m.parent_id == 0 or m.parent_id is None:
|
||||
# 如果是顶级菜单,直接放入结果集
|
||||
tree_data.append(current_node)
|
||||
else:
|
||||
# 如果是子菜单,找到它的父级,把它塞进父级的 children 里
|
||||
if m.parent_id in menu_map:
|
||||
menu_map[m.parent_id]['children'].append(current_node)
|
||||
else:
|
||||
# 如果找不到父级(比如父级被删了),为了防止数据丢失,暂时作为顶级显示
|
||||
tree_data.append(current_node)
|
||||
|
||||
return tree_data
|
||||
|
||||
@staticmethod
|
||||
def get_role_permissions(role_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 = []
|
||||
element_codes = []
|
||||
perms = SysRolePermission.query.filter_by(role_code=role_code).all()
|
||||
|
||||
for p in perms:
|
||||
if p.type == 'menu':
|
||||
menu_codes.append(p.target_code)
|
||||
else:
|
||||
element_codes.append(p.target_code)
|
||||
menu_codes = []
|
||||
element_codes = []
|
||||
|
||||
return {
|
||||
'menus': menu_codes,
|
||||
'elements': element_codes
|
||||
}
|
||||
for p in perms:
|
||||
# 这里假设你的数据库存的是 target_code
|
||||
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
|
||||
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:
|
||||
raise ValueError("角色代码不能为空")
|
||||
|
||||
session = db.session
|
||||
try:
|
||||
# ========= 1️⃣ 先删除旧权限 =========
|
||||
SysRolePermission.query.filter_by(role_code=role_code) \
|
||||
.delete(synchronize_session=False)
|
||||
# 1. 开启事务 (Flask-SQLAlchemy 自动管理,但明确逻辑更好)
|
||||
|
||||
# ========= 2️⃣ 去重(关键修复点) =========
|
||||
# 防止前端传来重复 code 导致 UNIQUE 冲突
|
||||
unique_codes = set(permission_codes) if permission_codes else set()
|
||||
# 2. 删除该角色旧的所有权限
|
||||
SysRolePermission.query.filter_by(role_code=role_code).delete()
|
||||
|
||||
# ========= 3️⃣ 批量添加新权限 =========
|
||||
if unique_codes:
|
||||
# 预先获取所有菜单代码,用于判断类型
|
||||
all_menu_codes = {m.code for m in SysMenu.query.all()}
|
||||
# 3. 准备新数据
|
||||
if permissions:
|
||||
# 3.1 去重
|
||||
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 = []
|
||||
|
||||
for code in unique_codes:
|
||||
if not code:
|
||||
continue
|
||||
if not code: continue
|
||||
|
||||
# 判断类型
|
||||
# 判断类型:如果 code 存在于菜单表中,就是 menu,否则就是 element
|
||||
p_type = 'menu' if code in all_menu_codes else 'element'
|
||||
|
||||
new_records.append(SysRolePermission(
|
||||
@ -95,14 +133,17 @@ class PermissionService:
|
||||
type=p_type
|
||||
))
|
||||
|
||||
# 3.3 批量插入
|
||||
if new_records:
|
||||
db.session.add_all(new_records)
|
||||
session.add_all(new_records)
|
||||
|
||||
# ========= 4️⃣ 提交事务 =========
|
||||
db.session.commit()
|
||||
# 4. 提交
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
session.rollback()
|
||||
raise e
|
||||
except Exception as e:
|
||||
# 发生异常时回滚,防止脏事务
|
||||
db.session.rollback()
|
||||
raise e
|
||||
session.rollback()
|
||||
raise e
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { login } from '@/api/auth'
|
||||
import { getRolePermissions } from '@/api/system/permission'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
@ -7,6 +8,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const role = ref(localStorage.getItem('role') || '')
|
||||
const username = ref(localStorage.getItem('username') || '')
|
||||
const permissions = ref<string[]>(JSON.parse(localStorage.getItem('permissions') || '[]'))
|
||||
|
||||
// 2. Actions
|
||||
// 登录逻辑
|
||||
@ -44,6 +46,25 @@ export const useUserStore = defineStore('user', () => {
|
||||
// 持久化存储 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 表示登录成功
|
||||
}
|
||||
|
||||
@ -53,11 +74,13 @@ export const useUserStore = defineStore('user', () => {
|
||||
token.value = ''
|
||||
role.value = ''
|
||||
username.value = ''
|
||||
permissions.value = []
|
||||
|
||||
// 2. 清空 LocalStorage (硬盘)
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('role')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('permissions')
|
||||
}
|
||||
|
||||
// 3. Getters / Helpers
|
||||
@ -66,12 +89,19 @@ export const useUserStore = defineStore('user', () => {
|
||||
return roles.includes(role.value)
|
||||
}
|
||||
|
||||
// 判断当前用户是否拥有某个权限(菜单或元素)
|
||||
const hasPermission = (code: string) => {
|
||||
return permissions.value.includes(code)
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
role,
|
||||
username,
|
||||
permissions,
|
||||
handleLogin,
|
||||
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>
|
||||
<el-checkbox v-model="columns.id.visible" label="ID" />
|
||||
<el-checkbox v-model="columns.companyName.visible" label="所属公司" />
|
||||
<el-checkbox v-model="columns.name.visible" label="名称" />
|
||||
<el-checkbox v-model="columns.commonName.visible" label="俗名" />
|
||||
<el-checkbox v-model="columns.category.visible" label="类别" />
|
||||
<el-checkbox v-model="columns.type.visible" label="类型" />
|
||||
<el-checkbox v-model="columns.spec.visible" label="规格型号" />
|
||||
<el-checkbox v-model="columns.unit.visible" label="单位" />
|
||||
<el-checkbox v-model="columns.inventory.visible" label="库存数" />
|
||||
<el-checkbox v-model="columns.available.visible" label="可用数" />
|
||||
<el-checkbox v-model="columns.files.visible" label="资料" />
|
||||
<el-checkbox v-model="columns.isEnabled.visible" label="状态" />
|
||||
<el-checkbox v-model="columns.id.visible" label="ID" :disabled="!userStore.hasPermission(permissionMap.id)" />
|
||||
<el-checkbox v-model="columns.companyName.visible" label="所属公司" :disabled="!userStore.hasPermission(permissionMap.companyName)" />
|
||||
<el-checkbox v-model="columns.name.visible" label="名称" :disabled="!userStore.hasPermission(permissionMap.name)" />
|
||||
<el-checkbox v-model="columns.commonName.visible" label="俗名" :disabled="!userStore.hasPermission(permissionMap.commonName)" />
|
||||
<el-checkbox v-model="columns.category.visible" label="类别" :disabled="!userStore.hasPermission(permissionMap.category)" />
|
||||
<el-checkbox v-model="columns.type.visible" label="类型" :disabled="!userStore.hasPermission(permissionMap.type)" />
|
||||
<el-checkbox v-model="columns.spec.visible" label="规格型号" :disabled="!userStore.hasPermission(permissionMap.spec)" />
|
||||
<el-checkbox v-model="columns.unit.visible" label="单位" :disabled="!userStore.hasPermission(permissionMap.unit)" />
|
||||
<el-checkbox v-model="columns.inventory.visible" label="库存数" :disabled="!userStore.hasPermission(permissionMap.inventory)" />
|
||||
<el-checkbox v-model="columns.available.visible" label="可用数" :disabled="!userStore.hasPermission(permissionMap.available)" />
|
||||
<el-checkbox v-model="columns.files.visible" label="资料" :disabled="!userStore.hasPermission(permissionMap.files)" />
|
||||
<el-checkbox v-model="columns.isEnabled.visible" label="状态" :disabled="!userStore.hasPermission(permissionMap.isEnabled)" />
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
@ -214,7 +214,7 @@
|
||||
/>
|
||||
</template>
|
||||
</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">
|
||||
<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>
|
||||
@ -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 { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
import {
|
||||
listMaterialBase,
|
||||
@ -432,6 +433,8 @@ import {
|
||||
import { uploadFile, deleteFile } from '@/api/common/upload';
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface MaterialBaseVO {
|
||||
id: number;
|
||||
@ -501,6 +504,38 @@ const columns = reactive({
|
||||
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 categoryOptions = ref<string[]>([]);
|
||||
const typeOptions = ref<string[]>([]);
|
||||
@ -988,6 +1023,8 @@ const handleCameraConfirm = async (file: File) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 先根据权限初始化列显示状态
|
||||
initColumnPermissions();
|
||||
getList();
|
||||
getOptionsList();
|
||||
});
|
||||
@ -1043,4 +1080,4 @@ onMounted(() => {
|
||||
.long-dropdown .el-select-dropdown__wrap {
|
||||
max-height: 600px !important; /* 可以根据屏幕大小适当调整 */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user