超级管理员能看到自定义菜单,普通角色登录后整组菜单消失,通常不是路由问题,而是菜单绑定的 ACL resource 没有被该角色授权。若所有角色都看不到,再检查模块和 XML 是否被加载。

menu.xml 与 acl.xml 的 ID 必须完全一致

<!-- etc/adminhtml/menu.xml -->
<add id="Vendor_Module::report"
     title="Report"
     module="Vendor_Module"
     sortOrder="50"
     parent="Magento_Backend::content"
     action="vendor/report/index"
     resource="Vendor_Module::report" />

<!-- etc/acl.xml -->
<resource id="Magento_Backend::admin">
    <resource id="Vendor_Module::report" title="Report" sortOrder="50" />
</resource>

大小写、模块前缀或尾部单复数任一不同,都会变成两个资源。不要为了让菜单出现而临时改成 Magento_Backend::admin,这相当于扩大权限。

确认模块和合并后的 ACL

bin/magento module:status Vendor_Module
bin/magento cache:clean config
bin/magento setup:di:compile
grep -R "Vendor_Module::report" app/code vendor/*/*/etc

开发环境 XML 能加载,不代表生产编译产物一定同步。多节点部署应确认代码版本一致;只清一个节点的 config cache 可能产生时有时无的菜单。

角色保存后要重新建立后台会话

在“系统 → 权限 → 角色”中为目标角色勾选资源并保存,然后让该管理员退出重新登录。旧会话可能保留授权树。若资源树里根本没有该节点,问题仍在 acl.xml,而不是角色选择。

控制器也要使用相同授权边界

class Index extends MagentoBackendAppAction
{
    public const ADMIN_RESOURCE = 'Vendor_Module::report';

    public function execute()
    {
        return $this->resultFactory->create(
            MagentoFrameworkControllerResultFactory::TYPE_PAGE
        );
    }
}

菜单显示但点击 Access Denied,常见原因是 Controller 的 ADMIN_RESOURCE 与菜单 resource 不一致。相反,控制器放宽而菜单收紧也会留下可直接访问的路由。

验证时至少使用两个账号:一个拥有该资源,一个明确不拥有。前者应看到并能访问,后者菜单不可见且直接输入 URL 也应被拒绝。只有界面和服务端授权同时成立,ACL 才算正确。