Compare commits

4 Commits

Author SHA1 Message Date
dxc
57c2c532ca fix: skip column permission checks for super admin and IRIS
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-26 16:14:01 +08:00
dxc
dad7ffdc66 fix: tie column display to user permissions in material list
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-26 16:07:54 +08:00
dxc
b798c42abf feat: add permission control for material list page
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-26 15:58:23 +08:00
dxc
8698b2582c refactor: rebuild permission tree and improve assignment with error handling 2026-02-26 15:57:59 +08:00
3 changed files with 178 additions and 70 deletions

View File

@ -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

View File

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

View File

@ -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>