我們來創建動態菜單吧 首先,先對動態菜單的概念、操作、流程進行約束:1.Host和各個Tenant有自己的自定義菜單2.Host和各個Tenant的許可權與自定義菜單相關聯2.Tenant有一套預設的菜單,規定對應的TenantId=-1,在添加租戶時自動將標準菜單和標準菜單的許可權初始化到添加的租戶 ...
我們來創建動態菜單吧
首先,先對動態菜單的概念、操作、流程進行約束:
1.Host和各個Tenant有自己的自定義菜單
2.Host和各個Tenant的許可權與自定義菜單相關聯
2.Tenant有一套預設的菜單,規定對應的TenantId=-1,在添加租戶時自動將標準菜單和標準菜單的許可權初始化到添加的租戶
一、先實現菜單在資料庫中的增刪改查
第一步:創建表、實體,添加DbContext
我們需要創建一個菜單表,延續Abp的命名方法,表名叫AbpMenus吧(菜單和許可權、驗證我們要關聯,所以文件儘量放在Authorization文件夾下)
把創建的實體放在AbpLearn.Core/Authorization下麵,新建一個Menus文件夾,再創建Menus實體
public class AbpMenus : Entity<int> { public string MenuName { set; get; } public string PageName { set; get; } public string Name { set; get; } public string Url { set; get; } public string Icon { set; get; } public int ParentId { set; get; } public bool IsActive { set; get; } public int Orders { set; get; } public int? TenantId { set; get; } }
如果翻過源碼中實體的定義,可以發現很多實體的繼承,例如:
1.繼承介面 IMayHaveTenant,繼承後生成的sql語句將自動增加TenantId的查詢條件,表中必須包含TenantId列
2.繼承介面 IPassivable,繼承後表中必須包含IsActive列
3.繼承介面 FullAuditedEntity<TPrimaryKey> TPrimaryKey可以是long、int等值類型,必須包含IsDeleted、DeleterUserId、DeletionTime,其中這個介面
還繼承了AuditedEntity<TPrimaryKey>, IFullAudited, IAudited, ICreationAudited, IHasCreationTime, IModificationAudited, IHasModificationTime, IDeletionAudited, IHasDeletionTime, ISoftDelete,這些父類型、介面的定義自己F12就可以看到
AbpLearn.EntityFrameworkCore/EntityFrameworkCore/AbpLearnDbContext.cs增加DbSet
public class AbpLearnDbContext : AbpZeroDbContext<Tenant, Role, User, AbpLearnDbContext> { /* Define a DbSet for each entity of the application */ public AbpLearnDbContext(DbContextOptions<AbpLearnDbContext> options) : base(options) { } public DbSet<AbpMenus> AbpMenus { set; get; } }
再去資料庫中添加AbpMenus表 欄位長度請自行調整
DROP TABLE IF EXISTS `AbpMenus`;
CREATE TABLE `AbpMenus` (
`Id` int NOT NULL AUTO_INCREMENT,
`MenuName` varchar(50) DEFAULT NULL,
`PageName` varchar(50) DEFAULT NULL,
`LName` varchar(50) DEFAULT NULL,
`Url` varchar(50) DEFAULT NULL,
`Icon` varchar(20) DEFAULT NULL,
`ParentId` int DEFAULT NULL,
`IsActive` bit(1) NOT NULL DEFAULT b'0',
`Orders` int DEFAULT NULL,
`TenantId` int DEFAULT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
第二步:添加Service和Dto
AbpLearn.Application/Authorization下添加Menus文件夾,然後添加IMenusAppService、MenusAppService,然後添加Dto文件夾
第三步:添加控制器和前臺頁面、js
Controller文件,MenusController.cs
前臺添加Menus及對應的js文件,可以簡單省事的把其他文件夾複製粘貼一份,然後關鍵詞修改下
這些文件太多了,我會把這套代碼上傳到github中,文章最低部會把鏈接掛出來
添加完之後我們就可以生成預覽一下Menus,因為SetNavigation中未將Menus的url加進去,我們自己手打鏈接進入
此時, 我們的菜單這一塊的crud已經做好了,我們可以看到有一個Host管理員這個部分是什麼意思哪?
我們為了在當前Host中可以控制所有租戶的菜單和許可權,將當前Host、標準菜單、租戶做一個select,代碼如下
public class ChangeModalViewModel { public int? TenantId { get; set; } public string TenancyName { get; set; } public int? TenantMenuType { get; set; } public List<ComboboxItemDto> TeneacyItems { get; set; } }
public async Task<IActionResult> IndexAsync(int? id = 0) { var loginTenant = id <= 0 ? null : _tenantManager.GetById((int)id); var viewModel = new ChangeModalViewModel { TenancyName = loginTenant?.TenancyName, TenantId = id }; viewModel.TeneacyItems = _tenantManager.Tenants .Select(p => new ComboboxItemDto(p.Id.ToString(), p.Name) { IsSelected = viewModel.TenancyName == p.TenancyName }) .ToList(); viewModel.TeneacyItems.Add(new ComboboxItemDto("0","Host管理員") { IsSelected = id == 0 }); viewModel.TeneacyItems.Add(new ComboboxItemDto("-1", "預設菜單") { IsSelected = id == -1 }); ViewBag.LoginInfo = await _sessionAppService.GetCurrentLoginInformations(); return View(viewModel); }
然後在Index.cshtml中添加或修改
@model ChangeModalViewModel // 添加
@await Html.PartialAsync("~/Views/Menus/Index.AdvancedSearch.cshtml", Model) //修改
@await Html.PartialAsync("~/Views/Menus/_CreateModal.cshtml",Model.TenantId) //修改
//添加
$("#ChangeTenancyName").change(function (e) {
location.href = "/Menus/Index/" + this.options[this.selectedIndex].value;
});
修改_CreateModal.cshtml
@using Abp.Authorization.Users @using Abp.MultiTenancy @using AbpLearn.MultiTenancy @using AbpLearn.Web.Models.Common.Modals @model int @{ Layout = null; } <div class="modal fade" id="MenuCreateModal" tabindex="-1" role="dialog" aria-labelledby="MenuCreateModalLabel" data-backdrop="static"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> @await Html.PartialAsync("~/Views/Shared/Modals/_ModalHeader.cshtml", new ModalHeaderViewModel(L("CreateNewMenu"))) <form name="systemMenuCreateForm" role="form" class="form-horizontal"> <div class="modal-body"> <div class="form-group row required"> <label class="col-md-3 col-form-label">@L("MenuName")</label> <div class="col-md-9"> <input type="text" name="MenuName" class="form-control" required minlength="2"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label">@L("LName")</label> <div class="col-md-9"> <input type="text" name="LName" class="form-control" required> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label">@L("Url")</label> <div class="col-md-9"> <input type="text" name="Url" class="form-control"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label">@L("PageName")</label> <div class="col-md-9"> <input type="text" name="PageName" class="form-control"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label">@L("ParentId")</label> <div class="col-md-9"> <input type="text" name="ParentId" class="form-control"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label">@L("Orders")</label> <div class="col-md-9"> <input type="text" name="Orders" class="form-control"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label" for="CreateMenuIsActive">@L("IsActive")</label> <div class="col-md-9"> <input id="CreateMenuIsActive" type="checkbox" name="IsActive" value="true" checked /> </div> </div> </div> <input type="hidden" name="TenantId" value="@(Model)" /> @await Html.PartialAsync("~/Views/Shared/Modals/_ModalFooterWithSaveAndCancel.cshtml") </form> </div> </div> </div>View Code
修改_EditModal.cshtml
@using AbpLearn.Authorization.Menus.Dto @using AbpLearn.Web.Models.Common.Modals @model MenuDto @{ Layout = null; } @await Html.PartialAsync("~/Views/Shared/Modals/_ModalHeader.cshtml", new ModalHeaderViewModel(L("EditMenu"))) <form name="MenuEditForm" role="form" class="form-horizontal"> <input type="hidden" name="Id" value="@Model.Id" /> <div class="modal-body"> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="tenancy-name">@L("MenuName")</label> <div class="col-md-9"> <input id="tenancy-name" type="text" class="form-control" name="MenuName" value="@Model.MenuName" required maxlength="64" minlength="2"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("LName")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="LName" value="@Model.LName" required maxlength="128"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("Url")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="Url" value="@Model.Url" required maxlength="128"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("PageName")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="PageName" value="@Model.PageName" required maxlength="128"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("ParentId")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="ParentId" value="@Model.ParentId" required maxlength="128"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("Orders")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="Orders" value="@Model.Orders" required maxlength="128"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label" for="isactive">@L("IsActive")</label> <div class="col-md-9"> <input id="isactive" type="checkbox" name="IsActive" value="true" @(Model.IsActive ? "checked" : "") /> </div> </div> </div> @await Html.PartialAsync("~/Views/Shared/Modals/_ModalFooterWithSaveAndCancel.cshtml") </form> <script src="~/view-resources/Views/Menus/_EditModal.js" asp-append-version="true"></script>View Code
修改Index.AdvancedSearch.cshtml
@using AbpLearn.Web.Views.Shared.Components.TenantChange @using Abp.Application.Services.Dto @model ChangeModalViewModel <div class="abp-advanced-search"> <form id="MenusSearchForm" class="form-horizontal"> <input type="hidden" name="TenantId" value="@Model.TenantId" /> </form> <div class="form-horizontal"> <div class="form-group"> @Html.DropDownList( "ChangeTenancyNames", Model.TeneacyItems.Select(i => i.ToSelectListItem()), new { @class = "form-control edited", id = "ChangeTenancyName" }) </div> </div> </div>
因為在abp裡面載入當前列表調用的是abp.services.app.menus.getAll方法,我們還需要對MenusAppService中的GetAllAsync做一下修改
[Serializable] public class MenusPagedResultRequestDto: PagedResultRequestDto, IPagedAndSortedResultRequest { public virtual int? TenantId { get; set; } public virtual string Sorting { get; set; } public virtual bool ShowAll { get; set; } }
#region 查詢全部菜單 /// <summary> /// 查詢全部菜單 /// </summary> /// <param name="input"></param> /// <returns></returns> public override async Task<PagedResultDto<MenuDto>> GetAllAsync(MenusPagedResultRequestDto input) { IQueryable<AbpMenus> query; query = CreateFilteredQuery(input).Where(o => o.TenantId == (input.TenantId == 0 ? null : input.TenantId)); var totalCount = await AsyncQueryableExecuter.CountAsync(query); query = ApplySorting(query, input); if (!input.ShowAll) query = ApplyPaging(query, input); var entities = await AsyncQueryableExecuter.ToListAsync(query); return new PagedResultDto<MenuDto>( totalCount, entities.Select(MapToEntityDto).ToList() ); } #endregion
這樣,我們在選中下麵中的任意一個Tenant時,將會跳到對應的菜單裡面了
我們先把Host管理員菜單和預設菜單配置一下
二、實現添加租戶時,初始化標準菜單和許可權
首先我們找到添加租戶的地方,去TenantAppService裡面去找,可以看到有CreateAsync的重寫
public override async Task<TenantDto> CreateAsync(CreateTenantDto input) { CheckCreatePermission(); // Create tenant var tenant = ObjectMapper.Map<Tenant>(input); tenant.ConnectionString = input.ConnectionString.IsNullOrEmpty() ? null : SimpleStringCipher.Instance.Encrypt(input.ConnectionString); var defaultEdition = await _editionManager.FindByNameAsync(EditionManager.DefaultEditionName); if (defaultEdition != null) { tenant.EditionId = defaultEdition.Id; } await _tenantManager.CreateAsync(tenant); await CurrentUnitOfWork.SaveChangesAsync(); // To get new tenant's id. // Create tenant database _abpZeroDbMigrator.CreateOrMigrateForTenant(tenant); // We are working entities of new tenant, so changing tenant filter using (CurrentUnitOfWork.SetTenantId(tenant.Id)) { // Create static roles for new tenant CheckErrors(await _roleManager.CreateStaticRoles(tenant.Id)); await CurrentUnitOfWork.SaveChangesAsync(); // To get static role ids // Grant all permissions to admin role var adminRole = _roleManager.Roles.Single(r => r.Name == StaticRoleNames.Tenants.Admin); await _roleManager.GrantAllPermissionsAsync(adminRole); // Create admin user for the tenant var adminUser = User.CreateTenantAdminUser(tenant.Id, input.AdminEmailAddress); await _userManager.InitializeOptionsAsync(tenant.Id); CheckErrors(await _userManager.CreateAsync(adminUser, User.DefaultPassword)); await CurrentUnitOfWork.SaveChangesAsync(); // To get admin user's id // Assign admin user to role! CheckErrors(await _userManager.AddToRoleAsync(adminUser, adminRole.Name)); await CurrentUnitOfWork.SaveChangesAsync(); } return MapToEntityDto(tenant); }
我們需要做的是,在 using (CurrentUnitOfWork.SetTenantId(tenant.Id)) 的內部尾部添加賦予菜單和許可權的方法即可
賦予菜單和許可權的方法我們分開寫,都放在MenusAppService中,
public interface IMenusAppService : IAsyncCrudAppService<MenuDto, int, MenusPagedResultRequestDto, CreateMenuDto, MenuDto> { /// <summary> /// 賦予預設菜單 /// </summary> /// <param name="input"></param> /// <returns></returns> Task GiveMenusAsync(EntityDto<int> input); /// <summary> /// 賦予當前租戶Admin角色菜單許可權 /// </summary> /// <param name="input"></param> /// <returns></returns> Task GivePermissionsAsync(EntityDto<int> input); }
#region 賦予預設菜單 public async Task GiveMenusAsync(EntityDto<int> input) { if (input.Id > 0) { var tenant = await _tenantManager.GetByIdAsync(input.Id); using (_unitOfWorkManager.Current.SetTenantId(tenant.Id)) { var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == tenant.Id); var systemMenus = await AsyncQueryableExecuter.ToListAsync(query); if (!systemMenus.Any()) { query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == -1); var defaultMenus = await AsyncQueryableExecuter.ToListAsync(query); if (defaultMenus.Any()) { List<MenusInsert> GetMenusInserts(List<AbpMenus> abpMenus,int parentId = 0) { List<MenusInsert> menusInserts = new List<MenusInsert>(); foreach (var entity in abpMenus.Where(o => o.ParentId == parentId)) { var insert = new MenusInsert() { LName = entity.LName, MenuName = entity.MenuName, PageName = entity.PageName, Icon = entity.Icon, Url = entity.Url, IsActive = entity.IsActive, Orders = entity.Orders, ParentId = entity.ParentId, TenantId = tenant.Id }; insert.menusInserts = GetMenusInserts(abpMenus, entity.Id); menusInserts.Add(insert); } return menusInserts; } async Task InsertMenusAsync(List<MenusInsert> inserts,int parentId = 0) { foreach (var insert in inserts) { var entity = await CreateAsync(new AbpMenus() { LName = insert.LName, MenuName = insert.MenuName, PageName = insert.PageName, Icon = insert.Icon, Url = insert.Url, IsActive = insert.IsActive, Orders = insert.Orders, ParentId = parentId, TenantId = tenant.Id }); if (insert.menusInserts.Any()) { await InsertMenusAsync(insert.menusInserts, entity.Id); } } } await InsertMenusAsync(GetMenusInserts(defaultMenus)); } } } } } #endregion #region 賦予當前租戶Admin角色菜單許可權 /// <summary> /// 賦予當前租戶Admin角色菜單許可權 /// </summary> /// <param name="input"></param> /// <returns></returns> public async Task GivePermissionsAsync(EntityDto<int> input) { if (input.Id > 0) { var tenant = await _tenantManager.GetByIdAsync(input.Id); using (_unitOfWorkManager.Current.SetTenantId(tenant.Id)) { var adminRoles = await _roleRepository.GetAllListAsync(o => o.Name == StaticRoleNames.Tenants.Admin && o.TenantId == tenant.Id); if (adminRoles.FirstOrDefault() != null) { var adminRole = adminRoles.FirstOrDefault(); var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == tenant.Id); var systemMenus = await AsyncQueryableExecuter.ToListAsync(query); var permissions = ConvertTenantPermissions(systemMenus); //await _roleManager.ResetAllPermissionsAsync(adminRole.FirstOrDefault()); //重置授權 var active_BatchCount = 10; var active_permissions = ConvertTenantPermissions(systemMenus.Where(o => o.IsActive).ToList()); for (int i = 0; i < active_permissions.Count(); i += 10)//每次後移5位 { //await _roleManager.SetGrantedPermissionsAsync(adminRole.FirstOrDefault().Id, active_permissions.Take(active_BatchCount).Skip(i)); foreach (var notActive_permission in active_permissions.Take(active_BatchCount).Skip(i)) { await _roleManager.GrantPermissionAsync(adminRole, notActive_permission); } active_BatchCount += 10;//每次從數組中選出N+10位,skip前N位 } var notActive_BatchCount = 10; var notActive_permissions = ConvertTenantPermissions(systemMenus.Where(o => !o.IsActive).ToList()); for (int i = 0; i < notActive_permissions.Count(); i += 10)//每次後移5位 { foreach (var notActive_permission in notActive_permissions.Take(notActive_BatchCount).Skip(i)) { await _roleManager.ProhibitPermissionAsync(adminRole, notActive_permission); } notActive_BatchCount += 10;//每次從數組中選出N+10位,skip前N位 } } else { throw new AbpDbConcurrencyException("未獲取到當前租戶的Admin角色!"); } } } else { var adminRoles = await _roleRepository.GetAllListAsync(o => o.Name == StaticRoleNames.Tenants.Admin && o.TenantId == null); if (adminRoles.FirstOrDefault() != null) { var adminRole = adminRoles.FirstOrDefault(); var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == null || o.TenantId == 0);