# -*- coding: utf-8 -*- """ 一次性初始化:会员功能菜单(目录 + 页面菜单 + 按钮权限点) 推荐用法(在已配置好 env/.env.dev 或运行环境变量的情况下): python -m app.scripts.init_yifan_membership_menus 该脚本是幂等的:重复执行会跳过已存在的菜单/按钮。 """ from __future__ import annotations import asyncio import sqlalchemy as sa from app.core.database import async_db_session from app.api.v1.module_system.auth.schema import AuthSchema from app.api.v1.module_system.menu.crud import MenuCRUD from app.api.v1.module_system.menu.schema import MenuCreateSchema from app.core.logger import log # 关键:预先导入 RoleModel,避免 MenuModel 关系解析失败 # MenuModel.roles -> "RoleModel"(字符串引用),若未导入会导致 mapper 初始化报错 from app.api.v1.module_system.role.model import RoleModel, RoleMenusModel # noqa: F401 from app.api.v1.module_system.position.model import PositionModel # noqa: F401 from app.api.v1.module_system.dept.model import DeptModel # noqa: F401 from app.api.v1.module_system.user.model import UserModel, UserRolesModel # noqa: F401 # 固定父节点(用户提供):壹梵后台管理 ROOT_PARENT_ID = 131 async def _create_if_not_exists(auth: AuthSchema, data: dict, *, unique_by: dict) -> int: """ 幂等创建菜单: - unique_by: 用于判重的字段(如 {"route_path": "/x", "type": 2} 或 {"permission": "...", "type": 3}) """ crud = MenuCRUD(auth) # 禁用默认预加载,避免在脚本上下文触发复杂关系初始化 exists = await crud.get(preload=[], **unique_by) if exists: return exists.id created = await crud.create(data=MenuCreateSchema(**data)) return created.id async def init_yifan_membership_menus(parent_id: int = ROOT_PARENT_ID) -> dict: """ 初始化会员功能菜单(挂到 parent_id 下)。 返回创建结果的 id 汇总。 """ async with async_db_session() as session: async with session.begin(): # 0) 运行前检查:sys_menu 是否存在(否则无法初始化菜单) conn = await session.connection() insp = sa.inspect(conn.sync_connection) if not insp.has_table("sys_menu"): raise RuntimeError( "当前数据库缺少表 sys_menu,无法初始化菜单。请先初始化数据库结构(运行项目初始化/迁移脚本)," "确保 sys_menu/sys_role/sys_user 等核心表已创建。" ) auth = AuthSchema(db=session, check_data_scope=False, user=None) # 1) 创建父目录:会员功能(type=1) membership_dir = { "name": "yifan_membership", "type": 1, "icon": "menu", "order": 10, "permission": None, "route_name": "YifanMembership", "route_path": "/yifan/membership", "component_path": None, "redirect": "/yifan/membership/members", "parent_id": parent_id, "keep_alive": True, "hidden": False, "always_show": True, "title": "会员功能", "params": [], "affix": False, "status": "0", "description": "脚本自动创建", } PARENT_ID = await _create_if_not_exists( auth, membership_dir, unique_by={"route_path": "/yifan/membership", "type": 1}, ) # 2) 三个页面菜单(type=2) discount_menu = { "name": "yifan_membership_discount", "type": 2, "icon": "setting", "order": 1, "permission": "module_yifan:yifan_membership_discount:query", "route_name": "YifanMembershipDiscount", "route_path": "/yifan/membership/discount", "component_path": "module_yifan/yifan_membership_discount/index", "redirect": "", "parent_id": PARENT_ID, "keep_alive": True, "hidden": False, "always_show": False, "title": "会员折扣设置", "params": [], "affix": False, "status": "0", "description": "脚本自动创建", } DISCOUNT_MENU_ID = await _create_if_not_exists( auth, discount_menu, unique_by={"route_path": "/yifan/membership/discount", "type": 2}, ) member_menu = { "name": "yifan_membership_member", "type": 2, "icon": "user", "order": 2, "permission": "module_yifan:yifan_membership_member:query", "route_name": "YifanMembershipMember", "route_path": "/yifan/membership/members", "component_path": "module_yifan/yifan_membership_member/index", "redirect": "", "parent_id": PARENT_ID, "keep_alive": True, "hidden": False, "always_show": False, "title": "会员列表", "params": [], "affix": False, "status": "0", "description": "脚本自动创建", } MEMBER_MENU_ID = await _create_if_not_exists( auth, member_menu, unique_by={"route_path": "/yifan/membership/members", "type": 2}, ) user_center_menu = { "name": "yifan_membership_user_center", "type": 2, "icon": "user", "order": 3, "permission": "module_yifan:yifan_membership_user_center:query", "route_name": "YifanMembershipUserCenter", "route_path": "/yifan/membership/user-center", "component_path": "module_yifan/yifan_membership_user_center/index", "redirect": "", "parent_id": PARENT_ID, "keep_alive": True, "hidden": False, "always_show": False, "title": "用户中心(会员权益)", "params": [], "affix": False, "status": "0", "description": "脚本自动创建", } USER_CENTER_MENU_ID = await _create_if_not_exists( auth, user_center_menu, unique_by={"route_path": "/yifan/membership/user-center", "type": 2}, ) # 3) 按钮权限点(type=3,挂到对应页面菜单下) discount_btn = { "name": "修改折扣", "type": 3, "icon": None, "order": 1, "permission": "module_yifan:yifan_membership_discount:update", "route_name": None, "route_path": None, "component_path": None, "redirect": "", "parent_id": DISCOUNT_MENU_ID, "keep_alive": False, "hidden": True, "always_show": False, "title": "修改折扣", "params": [], "affix": False, "status": "0", "description": "脚本自动创建", } DISCOUNT_UPDATE_BTN_ID = await _create_if_not_exists( auth, discount_btn, unique_by={"permission": "module_yifan:yifan_membership_discount:update", "type": 3}, ) quota_btn = { "name": "名额调整", "type": 3, "icon": None, "order": 1, "permission": "module_yifan:yifan_membership_member:quota_adjust", "route_name": None, "route_path": None, "component_path": None, "redirect": "", "parent_id": MEMBER_MENU_ID, "keep_alive": False, "hidden": True, "always_show": False, "title": "名额调整", "params": [], "affix": False, "status": "0", "description": "脚本自动创建", } MEMBER_QUOTA_BTN_ID = await _create_if_not_exists( auth, quota_btn, unique_by={"permission": "module_yifan:yifan_membership_member:quota_adjust", "type": 3}, ) await session.commit() return { "parent_dir_id": PARENT_ID, "discount_menu_id": DISCOUNT_MENU_ID, "member_menu_id": MEMBER_MENU_ID, "user_center_menu_id": USER_CENTER_MENU_ID, "discount_update_button_id": DISCOUNT_UPDATE_BTN_ID, "member_quota_adjust_button_id": MEMBER_QUOTA_BTN_ID, } async def _amain() -> None: result = await init_yifan_membership_menus() log.info(f"✅ 会员功能菜单初始化完成: {result}") def main() -> None: asyncio.run(_amain()) if __name__ == "__main__": main()