from app.models.system import SysMenu, SysElement, SysRolePermission, SysUser from app.extensions import db from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import func, or_ class PermissionService: @staticmethod def get_permission_tree(): """ 获取完整的权限树(菜单嵌套菜单 + 菜单包含元素) 供前端权限配置页面展示 """ # 1. 获取所有可见菜单 (按 parent_id 和 sort_order 排序,保证父子处理顺序) menus = SysMenu.query.filter(SysMenu.is_visible == True).order_by(SysMenu.parent_id, SysMenu.sort_order).all() # 2. 获取所有元素 elements = SysElement.query.all() # --- 核心逻辑:构建树形结构 --- # 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] 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, company_name=None): """ 获取指定角色拥有的所有权限Code(支持公司级别隔离) 参数: company_name — None=不过滤, 具体值=查公司定制+全局模板权限 """ try: # === 超级管理员上帝模式 === if role_code == 'SUPER_ADMIN': all_menus = [m.code for m in SysMenu.query.filter(SysMenu.is_visible == True).all()] all_elements = [e.code for e in SysElement.query.all()] return { 'menus': all_menus, 'elements': all_elements } # 构建公司过滤条件 if company_name: company_filter = or_( SysRolePermission.company_name == company_name, SysRolePermission.company_name.is_(None), SysRolePermission.company_name == '', SysRolePermission.company_name == 'System' ) else: company_filter = True perms = SysRolePermission.query.filter( SysRolePermission.role_code == role_code, company_filter ).all() menu_codes = [] element_codes = [] for p in perms: if p.type == 'menu': menu_codes.append(p.target_code) else: element_codes.append(p.target_code) # ★ 诊断:打印入库操作权限 inbound_ops = [c for c in element_codes if 'inbound_buy' in c or 'inbound_semi' in c or 'inbound_product' in c] if inbound_ops: print(f"[诊断] {role_code} 拥有的入库操作权限: {inbound_ops}") 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, permissions, company_name=None): """ 保存角色的权限(支持公司级别隔离) 参数: role_code — 角色代码 permissions — 前端传来的 list,混合了 menu_code 和 element_code company_name — 所属公司(None=全局模板,具体值=公司定制权限) """ if not role_code: raise ValueError("角色代码不能为空") session = db.session try: # 1. 删除该角色的旧权限 # 删除逻辑需与 get_user_permissions 的查询逻辑对齐: # - company_name 为空 → 删 company_name IS NULL OR ''(超管/全局模板) # - company_name 有值 → 删 公司定制 + 全局模板(确保能完整替换) if company_name: old_perms = SysRolePermission.query.filter( SysRolePermission.role_code == role_code, or_( SysRolePermission.company_name == company_name, SysRolePermission.company_name.is_(None), SysRolePermission.company_name == '', ) ).all() else: old_perms = SysRolePermission.query.filter( SysRolePermission.role_code == role_code, or_( SysRolePermission.company_name.is_(None), SysRolePermission.company_name == '' ) ).all() for p in old_perms: db.session.delete(p) # 2. 准备新数据 if permissions: unique_codes = set(permissions) all_menu_codes = {res[0] for res in session.query(SysMenu.code).all()} # 全局权限码 — 不按公司隔离,始终写入 company_name='' GLOBAL_CODES = {'crossDomain', 'global:cross_company_op'} new_records = [] for code in unique_codes: if not code: continue p_type = 'menu' if code in all_menu_codes else 'element' # 全局权限码强制 company_name='',确保跨公司生效 target_company = '' if code in GLOBAL_CODES else company_name new_records.append(SysRolePermission( role_code=role_code, target_code=code, type=p_type, company_name=target_company )) if new_records: session.add_all(new_records) # 4. 提交 session.commit() return True except SQLAlchemyError as e: session.rollback() raise e except Exception as e: session.rollback() raise e @staticmethod def init_audit_menu(): """ 初始化审计日志菜单和超级管理员权限 防重复:只插入不存在的记录 """ try: # 1. 检查并创建审计日志菜单 menu_code = 'system_audit' existing_menu = SysMenu.query.filter_by(code=menu_code).first() if not existing_menu: new_menu = SysMenu( parent_id=0, name='审计日志', code=menu_code, path='/system/audit', sort_order=110, is_visible=True ) db.session.add(new_menu) db.session.flush() # 获取新插入的 ID print(f"✅ 审计日志菜单已创建 (code: {menu_code})") else: # ★★★ Dirty Check:仅当字段真正变化时才 add,避免触发 UPDATE 事件 is_dirty = False if existing_menu.parent_id != 0: existing_menu.parent_id = 0 is_dirty = True if existing_menu.name != '审计日志': existing_menu.name = '审计日志' is_dirty = True if existing_menu.path != '/system/audit': existing_menu.path = '/system/audit' is_dirty = True if existing_menu.sort_order != 110: existing_menu.sort_order = 110 is_dirty = True if existing_menu.is_visible != True: existing_menu.is_visible = True is_dirty = True if is_dirty: db.session.add(existing_menu) print(f"🔄 审计日志菜单已更新 (code: {menu_code})") else: print(f"ℹ️ 审计日志菜单已存在,无需更新 (code: {menu_code})") # 2. 为超级管理员赋予审计日志菜单权限 role_code = 'SUPER_ADMIN' existing_perm = SysRolePermission.query.filter_by( role_code=role_code, target_code=menu_code ).first() if not existing_perm: new_perm = SysRolePermission( role_code=role_code, target_code=menu_code, type='menu' ) db.session.add(new_perm) print(f"✅ 超级管理员已赋予审计日志权限") else: print(f"ℹ️ 超级管理员已拥有审计日志权限") # 3. 提交 db.session.commit() return True except Exception as e: db.session.rollback() print(f"❌ 初始化审计日志菜单失败: {str(e)}") raise e @staticmethod def init_stocktake_menus(): """ 初始化盘点管理菜单和权限 包括:顶级菜单、盲盘作业、盈亏调整、以及操作权限元素 """ try: role_code = 'SUPER_ADMIN' # 1. 创建顶级菜单:盘点管理 stocktake_mgmt_code = 'stocktake_mgmt' stocktake_menu = SysMenu.query.filter_by(code=stocktake_mgmt_code).first() if not stocktake_menu: stocktake_menu = SysMenu( parent_id=0, name='盘点管理', code=stocktake_mgmt_code, path='/stocktake', sort_order=30, is_visible=True ) db.session.add(stocktake_menu) db.session.flush() print(f"✅ 盘点管理顶级菜单已创建") else: is_dirty = False if stocktake_menu.parent_id != 0: stocktake_menu.parent_id = 0; is_dirty = True if stocktake_menu.name != '盘点管理': stocktake_menu.name = '盘点管理'; is_dirty = True if stocktake_menu.path != '/stocktake': stocktake_menu.path = '/stocktake'; is_dirty = True if stocktake_menu.sort_order != 30: stocktake_menu.sort_order = 30; is_dirty = True if stocktake_menu.is_visible != True: stocktake_menu.is_visible = True; is_dirty = True if is_dirty: db.session.add(stocktake_menu); print(f"🔄 盘点管理顶级菜单已更新") else: print(f"ℹ️ 盘点管理顶级菜单已存在") # 2. 创建子菜单:盲盘作业 stocktake_op_code = 'inventory_stocktake' stocktake_op_menu = SysMenu.query.filter_by(code=stocktake_op_code).first() if not stocktake_op_menu: stocktake_op_menu = SysMenu( parent_id=stocktake_menu.id, name='盲盘作业', code=stocktake_op_code, path='/stocktake/operation', sort_order=1, is_visible=True ) db.session.add(stocktake_op_menu) db.session.flush() print(f"✅ 盲盘作业菜单已创建") else: is_dirty = False if stocktake_op_menu.name != '盲盘作业': stocktake_op_menu.name = '盲盘作业'; is_dirty = True if stocktake_op_menu.path != '/stocktake/operation': stocktake_op_menu.path = '/stocktake/operation'; is_dirty = True if stocktake_op_menu.sort_order != 1: stocktake_op_menu.sort_order = 1; is_dirty = True if stocktake_op_menu.is_visible != True: stocktake_op_menu.is_visible = True; is_dirty = True if is_dirty: db.session.add(stocktake_op_menu); print(f"🔄 盲盘作业菜单已更新") else: print(f"ℹ️ 盲盘作业菜单已存在") # 3. 为盲盘作业添加操作权限元素 stocktake_op_element = SysElement.query.filter_by( menu_code=stocktake_op_code, code='inventory_stocktake:operation' ).first() if not stocktake_op_element: stocktake_op_element = SysElement( menu_code=stocktake_op_code, name='盲盘操作', code='inventory_stocktake:operation', element_type='operation' ) db.session.add(stocktake_op_element) print(f"✅ 盲盘作业操作权限已创建") else: is_dirty = False if stocktake_op_element.name != '盲盘操作': stocktake_op_element.name = '盲盘操作'; is_dirty = True if stocktake_op_element.element_type != 'operation': stocktake_op_element.element_type = 'operation'; is_dirty = True if is_dirty: db.session.add(stocktake_op_element); print(f"🔄 盲盘作业操作权限已更新") else: print(f"ℹ️ 盲盘作业操作权限已存在") # 4. 创建子菜单:盈亏调整 adjustment_code = 'stock_adjustment' adjustment_menu = SysMenu.query.filter_by(code=adjustment_code).first() if not adjustment_menu: adjustment_menu = SysMenu( parent_id=stocktake_menu.id, name='盈亏调整', code=adjustment_code, path='/stocktake/adjustment', sort_order=2, is_visible=True ) db.session.add(adjustment_menu) db.session.flush() print(f"✅ 盈亏调整菜单已创建") else: is_dirty = False if adjustment_menu.name != '盈亏调整': adjustment_menu.name = '盈亏调整'; is_dirty = True if adjustment_menu.path != '/stocktake/adjustment': adjustment_menu.path = '/stocktake/adjustment'; is_dirty = True if adjustment_menu.sort_order != 2: adjustment_menu.sort_order = 2; is_dirty = True if adjustment_menu.is_visible != True: adjustment_menu.is_visible = True; is_dirty = True if is_dirty: db.session.add(adjustment_menu); print(f"🔄 盈亏调整菜单已更新") else: print(f"ℹ️ 盈亏调整菜单已存在") # 5. 为盈亏调整添加列表权限元素 (stock_adjustment:list) adjustment_list_element = SysElement.query.filter_by( menu_code=adjustment_code, code='stock_adjustment:list' ).first() if not adjustment_list_element: adjustment_list_element = SysElement( menu_code=adjustment_code, name='盈亏列表', code='stock_adjustment:list', element_type='element' ) db.session.add(adjustment_list_element) print(f"✅ 盈亏调整列表权限已创建") else: is_dirty = False if adjustment_list_element.name != '盈亏列表': adjustment_list_element.name = '盈亏列表'; is_dirty = True if adjustment_list_element.element_type != 'element': adjustment_list_element.element_type = 'element'; is_dirty = True if is_dirty: db.session.add(adjustment_list_element); print(f"🔄 盈亏调整列表权限已更新") else: print(f"ℹ️ 盈亏调整列表权限已存在") # 6. 为盈亏调整添加操作权限元素 (stock_adjustment:operation) adjustment_op_element = SysElement.query.filter_by( menu_code=adjustment_code, code='stock_adjustment:operation' ).first() if not adjustment_op_element: adjustment_op_element = SysElement( menu_code=adjustment_code, name='盈亏操作', code='stock_adjustment:operation', element_type='operation' ) db.session.add(adjustment_op_element) print(f"✅ 盈亏调整操作权限已创建") else: is_dirty = False if adjustment_op_element.name != '盈亏操作': adjustment_op_element.name = '盈亏操作'; is_dirty = True if adjustment_op_element.element_type != 'operation': adjustment_op_element.element_type = 'operation'; is_dirty = True if is_dirty: db.session.add(adjustment_op_element); print(f"🔄 盈亏调整操作权限已更新") else: print(f"ℹ️ 盈亏调整操作权限已存在") # 7. 为超级管理员分配所有盘点相关权限 menu_codes = [stocktake_mgmt_code, stocktake_op_code, adjustment_code] for mc in menu_codes: existing_perm = SysRolePermission.query.filter_by( role_code=role_code, target_code=mc, type='menu' ).first() if not existing_perm: new_perm = SysRolePermission( role_code=role_code, target_code=mc, type='menu' ) db.session.add(new_perm) print(f"✅ 超级管理员已赋予 {mc} 菜单权限") # 8. 分配操作权限 op_codes = ['inventory_stocktake:operation', 'stock_adjustment:list', 'stock_adjustment:operation'] for oc in op_codes: existing_perm = SysRolePermission.query.filter_by( role_code=role_code, target_code=oc, type='element' ).first() if not existing_perm: new_perm = SysRolePermission( role_code=role_code, target_code=oc, type='element' ) db.session.add(new_perm) print(f"✅ 超级管理员已赋予 {oc} 操作权限") # 9. 提交 db.session.commit() return True except Exception as e: db.session.rollback() print(f"❌ 初始化盘点管理菜单失败: {str(e)}") raise e @staticmethod def cleanup_legacy_stocktake_menus(): """ 清理残留的旧版库存盘点菜单 如果数据库中存在挂在入库管理下的旧版库存盘点菜单,清理掉 """ try: # 查找可能存在的旧版库存盘点菜单(入库管理下的 stocktake) # 旧版可能在入库管理 (path like '%inventory%stocktake') 或者 code 包含 stocktake 但不是新的 legacy_menus = SysMenu.query.filter( SysMenu.code.in_(['stocktake', 'inventory_stocktake_old']) ).all() for menu in legacy_menus: # 删除关联的权限(改为对象级删除以触发审计事件) old_perms = SysRolePermission.query.filter_by(target_code=menu.code).all() for p in old_perms: db.session.delete(p) # 删除关联的元素(改为对象级删除以触发审计事件) old_elements = SysElement.query.filter_by(menu_code=menu.code).all() for e in old_elements: db.session.delete(e) # 删除菜单 db.session.delete(menu) print(f"🗑️ 已清理旧版库存盘点菜单: {menu.code} ({menu.name})") db.session.commit() return True except Exception as e: db.session.rollback() print(f"⚠️ 清理旧版菜单失败: {str(e)}") return False @staticmethod def init_all_menus(): """ 初始化所有菜单的层级结构,确保权限配置页面显示正确的树形结构 按照侧边栏顺序:基础信息 -> 入库 -> 盘点 -> 出库 -> BOM -> 借库 -> 报废 -> 系统 """ try: role_code = 'SUPER_ADMIN' # 定义菜单结构 (code, name, path, parent_code, sort_order) menu_defs = [ # 顶级菜单 (按侧边栏顺序) ('material_mgmt', '基础信息管理', '/material', None, 10), ('inventory_mgmt', '入库管理', '/inventory', None, 20), ('stocktake_mgmt', '盘点管理', '/stocktake', None, 30), ('outbound_mgmt', '出库管理', '/outbound', None, 40), ('bom_mgmt', 'BOM管理', '/bom', None, 50), ('operation_mgmt', '借库管理', '/operation', None, 60), ('scrap_mgmt', '报废管理', '/scrap', None, 70), ('system_mgmt', '系统管理', '/system', None, 80), # 基础信息子菜单 ('material_base', '基础信息', '/material/index', 'material_mgmt', 1), # 入库管理子菜单 ('inbound_purchase', '采购申请', '/purchase', 'inventory_mgmt', 0), ('inbound_buy', '采购入库', '/inventory/buy', 'inventory_mgmt', 1), ('inbound_semi', '半成品入库', '/inventory/semi', 'inventory_mgmt', 2), ('inbound_product', '成品入库', '/inventory/product', 'inventory_mgmt', 3), ('inbound_service', '服务权益', '/inventory/service', 'inventory_mgmt', 4), ('inbound_summary', '入库记录', '/inventory/summary', 'inventory_mgmt', 5), # 盘点管理子菜单 ('inventory_stocktake', '盲盘作业', '/stocktake/operation', 'stocktake_mgmt', 1), ('stock_adjustment', '盈亏调整', '/stocktake/adjustment', 'stocktake_mgmt', 2), # 出库管理子菜单 ('outbound_selection', '出库选单', '/outbound/selection', 'outbound_mgmt', 1), ('outbound_create', '扫码出库', '/outbound/create', 'outbound_mgmt', 2), ('outbound_list', '出库记录', '/outbound/index', 'outbound_mgmt', 3), ('outbound_approval', '出库审批', '/outbound/approval', 'outbound_mgmt', 4), # BOM管理子菜单 ('bom_manage', 'BOM配方管理', '/bom/manage', 'bom_mgmt', 1), # 借库管理子菜单 ('op_borrow', '借库操作', '/operation/borrow', 'operation_mgmt', 1), ('op_return', '归还操作', '/operation/repair', 'operation_mgmt', 2), ('op_records', '借还记录', '/operation/records', 'operation_mgmt', 3), # 报废管理子菜单 ('scrap_create', '新建报废', '/scrap/create', 'scrap_mgmt', 1), ('scrap_list', '报废记录', '/scrap/index', 'scrap_mgmt', 2), # 系统管理子菜单 ('system_user', '员工账号管理', '/system/user-create', 'system_mgmt', 1), ('system_permission', '权限分配', '/system/permission', 'system_mgmt', 2), ('system_audit', '审计日志', '/system/audit', 'system_mgmt', 3), ] # 第一步:清理根级别的冗余子菜单(这些本应是子节点,但可能之前被错误地创建为根节点) child_codes = [m[0] for m in menu_defs if m[3] is not None] # 所有子菜单的code orphaned_menus = SysMenu.query.filter( SysMenu.code.in_(child_codes), (SysMenu.parent_id == 0) | (SysMenu.parent_id.is_(None)) ).all() for menu in orphaned_menus: print(f"🗑️ 清理根级别冗余菜单: {menu.code} ({menu.name})") # 删除关联的权限(改为对象级删除以触发审计事件) old_perms = SysRolePermission.query.filter_by(target_code=menu.code).all() for p in old_perms: db.session.delete(p) db.session.delete(menu) # 第二步:清理重复菜单(同一个 code 存在多条记录,保留 ID 最小的) # 先找出所有可能重复的 code duplicate_check = db.session.query( SysMenu.code, func.count(SysMenu.id).label('cnt') ).group_by(SysMenu.code).having(func.count(SysMenu.id) > 1).all() for code, cnt in duplicate_check: # 获取该 code 的所有记录,按 id 排序,保留第一条,删除其余 duplicates = SysMenu.query.filter_by(code=code).order_by(SysMenu.id).all() # 保留第一条,删除其他 for dup in duplicates[1:]: print(f"🗑️ 清理重复菜单: {dup.code} (id={dup.id}, name={dup.name})") # 改为对象级删除以触发审计事件 old_perms = SysRolePermission.query.filter_by(target_code=dup.code).all() for p in old_perms: db.session.delete(p) old_elements = SysElement.query.filter_by(menu_code=dup.code).all() for e in old_elements: db.session.delete(e) db.session.delete(dup) # 第三步:仅当子菜单 parent_id 有误时才更新(Dirty Check) # 遍历所有子菜单,只在 parent_id 需要修正时才触碰对象 child_codes = [m[0] for m in menu_defs if m[3] is not None] child_menus = SysMenu.query.filter(SysMenu.code.in_(child_codes)).all() for m in child_menus: # 只有 parent_id 为 0 或 None(即没有正确挂载父菜单)时才更新 if m.parent_id == 0 or m.parent_id is None: m.parent_id = None # SQLAlchemy 设为 None 表示挂载到根(parent_id=None) db.session.add(m) print(f"🔧 修正子菜单 parent_id: {m.code} (parent_id → None)") # 第四步:创建或更新菜单(带字段级 Dirty Check) # 只有至少有一个字段真正变化了才 add,避免 SQLAlchemy 产生 UPDATE 事件 menu_map = {} # code -> menu obj for code, name, path, parent_code, sort_order in menu_defs: menu = SysMenu.query.filter_by(code=code).first() if not menu: menu = SysMenu(code=code, name=name, path=path, sort_order=sort_order, is_visible=True) db.session.add(menu) db.session.flush() print(f"✅ 菜单已创建: {name} ({code})") else: # ★★★ 字段级 Dirty Check:逐字段比较,仅在值真正变化时赋值 is_dirty = False if menu.name != name: menu.name = name is_dirty = True if menu.path != path: menu.path = path is_dirty = True if menu.sort_order != sort_order: menu.sort_order = sort_order is_dirty = True # 只有至少一个字段变化了才 add(触发 UPDATE) if is_dirty: db.session.add(menu) print(f"🔄 菜单已更新: {name} ({code})") menu_map[code] = menu # 第五步:设置 parent_id(带 Dirty Check,只在值真正变化时更新) for code, name, path, parent_code, sort_order in menu_defs: if parent_code and parent_code in menu_map: menu = menu_map[code] parent = menu_map[parent_code] # 只有 parent_id 实际变化了才赋值,避免重复触发 UPDATE if menu.parent_id != parent.id: menu.parent_id = parent.id db.session.add(menu) print(f"🔗 菜单 {code} 已挂载到父菜单 {parent_code} (id={parent.id})") # 第六步:为超级管理员分配顶级菜单权限(只做 INSERT,不触碰已存在的记录) for code, name, path, parent_code, sort_order in menu_defs: if parent_code is None: # 只分配顶级菜单 existing_perm = SysRolePermission.query.filter_by( role_code=role_code, target_code=code, type='menu' ).first() if not existing_perm: new_perm = SysRolePermission( role_code=role_code, target_code=code, type='menu' ) db.session.add(new_perm) # ★ 入库模块操作权限元素(之前缺失导致"可编辑"勾了也不能入库) inbound_op_elements = [ ('inbound_buy', 'inbound_buy:operation', '可编辑', 'operation'), ('inbound_semi', 'inbound_semi:operation', '可编辑', 'operation'), ('inbound_product', 'inbound_product:operation', '可编辑', 'operation'), ('inbound_service', 'inbound_service:operation', '可编辑', 'operation'), ] for menu_code, code, name, etype in inbound_op_elements: existing = SysElement.query.filter_by(menu_code=menu_code, code=code).first() if not existing: db.session.add(SysElement(menu_code=menu_code, name=name, code=code, element_type=etype)) print(f"✅ 入库操作元素已创建: {code}") # ★ 采购申请权限元素 purchase_elements = [ ('inbound_purchase:operation', '可编辑', 'operation'), ('inbound_purchase:unit_price', '采购单价', 'column'), ('inbound_purchase:total_price', '采购总价', 'column'), ('inbound_purchase:tax_rate', '税率', 'column'), # ★ 待采购清单(防重复采购)菜单权限。 # 注册成 element 而非新建 SysMenu:侧边栏与路由守卫只读前端 # meta.permissions + sys_role_permission,从不读 sys_menu; # 且 init_all_menus 的「冗余子菜单清理」会按 target_code 删权限行, # 新增 SysMenu 等于多一个被该清理逻辑波及的面。 ('inbound_purchase:pending_pool', '待采购清单', 'operation'), ] for code, name, etype in purchase_elements: existing = SysElement.query.filter_by( menu_code='inbound_purchase', code=code ).first() if not existing: db.session.add(SysElement( menu_code='inbound_purchase', name=name, code=code, element_type=etype )) print(f"✅ 采购申请元素已创建: {code}") db.session.commit() # ★ 诊断:打印入库模块的元素和权限状态 for mc in ('inbound_buy', 'inbound_semi', 'inbound_product', 'inbound_service'): elems = SysElement.query.filter_by(menu_code=mc).all() codes = [e.code for e in elems] print(f"[诊断] {mc} 的 sys_element: {codes}") role_count = SysRolePermission.query.filter_by( target_code=f'{mc}:operation', type='element' ).count() print(f"[诊断] {mc}:operation 已分配给 {role_count} 个角色") print(f"✅ 所有菜单初始化完成") return True except Exception as e: db.session.rollback() print(f"❌ 初始化菜单失败: {str(e)}") raise e @staticmethod def ensure_default_permissions(): """ 启动时自动为所有已知角色补充默认权限(仅补充,不覆盖已有权限)。 解决的问题: - 重新部署/重启后,非超级管理员角色的 sys_role_permission 表为空 - 管理员不必每次都手动重新分配权限 默认分配策略(保守,仅给菜单访问权,不给操作/编辑权): - 遍历 sys_user 表中所有非空 role 值 - 对每个角色,赋予其对应模块的顶级菜单权限 - 已有权限的角色不会被覆盖 """ try: # 1. 收集所有已知角色(从 sys_user.distinct role) roles = db.session.query(SysUser.role).filter( SysUser.role.isnot(None), SysUser.role != '', SysUser.status == 'active' ).distinct().all() known_roles = [r[0] for r in roles if r[0]] if not known_roles: print("[默认权限] 未找到活跃角色,跳过") return # 2. 角色 → 应拥有的菜单列表 ROLE_DEFAULT_MENUS = { 'WAREHOUSE_MGR': [ 'material_mgmt', 'inventory_mgmt', 'stocktake_mgmt', 'outbound_mgmt', 'bom_mgmt', 'operation_mgmt', 'scrap_mgmt', 'system_mgmt', ], 'WAREHOUSE_ADMIN': [ 'material_mgmt', 'inventory_mgmt', 'stocktake_mgmt', 'outbound_mgmt', 'bom_mgmt', 'operation_mgmt', 'scrap_mgmt', 'system_mgmt', ], 'SUPERVISOR': [ 'material_mgmt', 'inventory_mgmt', 'stocktake_mgmt', 'outbound_mgmt', 'bom_mgmt', 'operation_mgmt', 'scrap_mgmt', 'system_mgmt', ], 'PURCHASER': ['material_mgmt', 'inventory_mgmt'], 'INBOUND': ['inventory_mgmt'], 'SALES': ['outbound_mgmt'], 'OUTBOUND': ['outbound_mgmt'], 'WAREHOUSE_OP': ['inventory_mgmt', 'outbound_mgmt'], } # ★ SUPERVISOR 主管默认拥有采购申请的子菜单权限(确保能看到所有人的申请) SUPERVISOR_PURCHASE_MENUS = ['inbound_purchase'] total_inserted = 0 for role_code in known_roles: # 跳过超级管理员(已有全权限) if role_code.upper() == 'SUPER_ADMIN': continue # 检查该角色是否已有权限 existing_count = SysRolePermission.query.filter_by( role_code=role_code ).count() if existing_count > 0: print(f"[默认权限] {role_code} 已有 {existing_count} 条权限,跳过") continue # 获取该角色应获得的默认菜单 default_menus = ROLE_DEFAULT_MENUS.get( role_code, ['material_mgmt', 'inventory_mgmt', 'outbound_mgmt'] # 未知角色:只给基础三模块 ) for menu_code in default_menus: # 验证菜单存在 menu = SysMenu.query.filter_by(code=menu_code).first() if not menu: continue existing = SysRolePermission.query.filter_by( role_code=role_code, target_code=menu_code, type='menu' ).first() if existing: continue db.session.add(SysRolePermission( role_code=role_code, target_code=menu_code, type='menu' )) total_inserted += 1 print(f"[默认权限] {role_code} ← 已补充 {total_inserted} 条默认菜单权限") # ★ 自动迁移:已有 inbound_buy 权限的角色 → 自动获得 inbound_purchase for role_code in known_roles: if role_code.upper() == 'SUPER_ADMIN': continue has_inbound_buy = SysRolePermission.query.filter_by( role_code=role_code, target_code='inbound_buy', type='menu' ).first() if has_inbound_buy: has_purchase = SysRolePermission.query.filter_by( role_code=role_code, target_code='inbound_purchase', type='menu' ).first() if not has_purchase: db.session.add(SysRolePermission( role_code=role_code, target_code='inbound_purchase', type='menu' )) # 同时迁移操作权限 has_buy_op = SysRolePermission.query.filter_by( role_code=role_code, target_code='inbound_buy:operation', type='element' ).first() if has_buy_op: has_purchase_op = SysRolePermission.query.filter_by( role_code=role_code, target_code='inbound_purchase:operation', type='element' ).first() if not has_purchase_op: db.session.add(SysRolePermission( role_code=role_code, target_code='inbound_purchase:operation', type='element' )) print(f"[权限迁移] {role_code}: inbound_buy → inbound_purchase 已自动迁移") total_inserted += 1 # ★ 自动迁移:已有「采购申请」菜单权限的角色 → 自动获得「待采购清单」权限 # # 为什么必须独立成段:上面的默认分配逻辑在角色**已有任何权限**时 # 就整段跳过(见上方 existing_count > 0 的分支),存量角色根本走不到 # 那里。不写这一段,除了超管之外没有任何角色能拿到新权限。 # # 本段依赖 autoflush:上一个循环刚 add 的 inbound_purchase 行尚未 # commit,这里的查询必须能看见它们(扩展默认 autoflush=True 保证了这点)。 for role_code in known_roles: if role_code.upper() == 'SUPER_ADMIN': continue # 以「采购申请菜单权限」为门控 —— 能进采购页的角色就该看到待采购清单。 # ★ 同时镜像它的 company_name:若某角色的采购菜单是公司定制的 # (company_name='A'),补出的权限必须落在同一作用域;硬写 NULL # 会让其他公司的同角色用户凭空多出这个菜单。 src_perm = SysRolePermission.query.filter_by( role_code=role_code, target_code='inbound_purchase', type='menu' ).first() if not src_perm: continue # 幂等检查也必须带 company_name,否则 A 公司已有行会跳过 B 公司那条 already = SysRolePermission.query.filter_by( role_code=role_code, target_code='inbound_purchase:pending_pool', type='element', company_name=src_perm.company_name, ).first() if already: continue db.session.add(SysRolePermission( role_code=role_code, target_code='inbound_purchase:pending_pool', type='element', company_name=src_perm.company_name, )) total_inserted += 1 print(f"[权限迁移] {role_code}: 已补充「待采购清单」权限" f"(作用域={src_perm.company_name or '全局'})") # ★ SUPERVISOR 主管默认获得采购申请子菜单权限 for role_code in known_roles: if role_code.upper() == 'SUPERVISOR': for menu_code in SUPERVISOR_PURCHASE_MENUS: menu = SysMenu.query.filter_by(code=menu_code).first() if not menu: continue existing = SysRolePermission.query.filter_by( role_code=role_code, target_code=menu_code, type='menu' ).first() if not existing: db.session.add(SysRolePermission( role_code=role_code, target_code=menu_code, type='menu' )) total_inserted += 1 print(f"[默认权限] SUPERVISOR ← 已补充采购申请菜单权限") if total_inserted > 0: db.session.commit() print(f"[默认权限] 总计为 {sum(1 for r in known_roles if r.upper() != 'SUPER_ADMIN')} 个角色补充了默认权限") except Exception as e: db.session.rollback() print(f"[默认权限] 补充失败(不阻断启动): {e}")