sozsoft-platform/api/src/Sozsoft.Platform.Application/Menu/MenuAppService.cs
2026-10-02 16:58:22 +03:00

464 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Sozsoft.Languages;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Languages;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.ListForms;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.PermissionManagement;
using Volo.Abp.TenantManagement;
using static Sozsoft.Platform.Data.Seeds.SeedConsts;
namespace Sozsoft.Platform.Menus;
[Authorize]
public class MenuAppService : CrudAppService<
Menu,
MenuDto,
Guid,
PagedAndSortedResultRequestDto>
{
private readonly IRepository<Menu, Guid> _menuRepository;
private readonly IRepository<LanguageKey, Guid> _repositoryKey;
private readonly IRepository<LanguageText, Guid> _repositoryText;
private readonly ITenantRepository _tenantRepository;
private readonly IPermissionDefinitionRecordRepository _permissionRepository;
private readonly LanguageTextAppService _languageTextAppService;
private readonly IRepository<Module, string> _moduleRepository;
private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer;
public MenuAppService(
IRepository<Menu, Guid> menuRepository,
IRepository<LanguageKey, Guid> languageKeyRepository,
IRepository<LanguageText, Guid> languageTextRepository,
ITenantRepository tenantRepository,
IPermissionDefinitionRecordRepository permissionRepository,
LanguageTextAppService languageTextAppService,
IRepository<Module, string> moduleRepository,
IListFormSeedDataSynchronizer seedDataSynchronizer
) : base(menuRepository)
{
_menuRepository = menuRepository;
_repositoryKey = languageKeyRepository;
_repositoryText = languageTextRepository;
_tenantRepository = tenantRepository;
_permissionRepository = permissionRepository;
_languageTextAppService = languageTextAppService;
_moduleRepository = moduleRepository;
_seedDataSynchronizer = seedDataSynchronizer;
CreatePolicyName = $"{AppCodes.Menus.Menu}.Create";
UpdatePolicyName = $"{AppCodes.Menus.Menu}.Update";
DeletePolicyName = $"{AppCodes.Menus.Menu}.Delete";
}
public override async Task<PagedResultDto<MenuDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
var query = await CreateFilteredQueryAsync(input);
query = query.Where(a => !a.IsDisabled);
//Tenant üzerinden modül bilgisi alınıp yalnızca o modüllerin menüleri listelenecek
if (CurrentTenant.Id is Guid tenantId)
{
var tenant = await _tenantRepository.FindAsync(tenantId);
if (tenant != null)
{
var tenantModules = tenant.GetModules();
if (tenantModules.Length > 0)
{
var permissionRecords = await _permissionRepository.GetListAsync();
var allowedPermissionNames = permissionRecords
.Where(p => p.GetModules().Intersect(tenantModules).Any())
.Select(p => p.Name)
.Distinct()
.ToList();
query = query.Where(menu =>
string.IsNullOrEmpty(menu.RequiredPermissionName) ||
allowedPermissionNames.Contains(menu.RequiredPermissionName));
}
}
}
var totalCount = await AsyncExecuter.CountAsync(query);
var entities = new List<Menu>();
var entityDtos = new List<MenuDto>();
if (totalCount > 0)
{
query = ApplySorting(query, input);
query = ApplyPaging(query, input);
var items = await AsyncExecuter.ToListAsync(query);
// Tüm unique permission'ları topla
var uniquePermissions = items
.Where(x => !string.IsNullOrWhiteSpace(x.RequiredPermissionName))
.Select(x => x.RequiredPermissionName)
.Distinct()
.ToList();
// Tüm permission'ları bir kerede kontrol et (N+1 query problemini önler)
var grantedPermissions = new HashSet<string>();
foreach (var permission in uniquePermissions)
{
try
{
if (await AuthorizationService.IsGrantedAsync(permission))
{
grantedPermissions.Add(permission);
}
}
catch (Exception ex)
{
Logger.LogError(ex, $"Permission error for {permission}: {ex.Message}");
}
}
// Sadece yetkili menüleri filtrele
entities = items
.Where(item => string.IsNullOrWhiteSpace(item.RequiredPermissionName) ||
grantedPermissions.Contains(item.RequiredPermissionName))
.ToList();
entityDtos = await base.MapToGetListOutputDtosAsync(entities);
var languageKeys = entityDtos
.Select(item => string.IsNullOrWhiteSpace(item.DisplayName) ? item.Code : item.DisplayName)
.OfType<string>()
.Where(key => !string.IsNullOrWhiteSpace(key))
.Distinct()
.ToList();
var languageTexts = await _repositoryText.GetListAsync(text =>
text.ResourceName == PlatformConsts.AppName &&
text.Key != null && languageKeys.Contains(text.Key) &&
(text.CultureName == PlatformConsts.DefaultLanguage ||
text.CultureName == LanguageCodes.Tr));
foreach (var menu in entityDtos)
{
var languageKey = string.IsNullOrWhiteSpace(menu.DisplayName)
? menu.Code
: menu.DisplayName;
menu.MenuTextEn = languageTexts.FirstOrDefault(text =>
text.Key == languageKey &&
text.CultureName == PlatformConsts.DefaultLanguage)?.Value;
menu.MenuTextTr = languageTexts.FirstOrDefault(text =>
text.Key == languageKey &&
text.CultureName == LanguageCodes.Tr)?.Value;
}
}
return new PagedResultDto<MenuDto>(
totalCount,
entityDtos
);
}
public override async Task<MenuDto> CreateAsync(MenuDto input)
{
await CheckCreatePolicyAsync();
await EnsureModuleAsync(input.ModuleId);
var keyExists = await _repositoryKey.AnyAsync(
a => a.Key == input.DisplayName &&
a.ResourceName == PlatformConsts.AppName);
if (!keyExists)
{
await _repositoryKey.InsertAsync(new LanguageKey
{
Key = input.DisplayName,
ResourceName = PlatformConsts.AppName
});
}
var result = await base.CreateAsync(input);
await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Insert);
return result;
}
public override async Task<MenuDto> UpdateAsync(Guid id, MenuDto input)
{
await CheckUpdatePolicyAsync();
await EnsureModuleAsync(input.ModuleId);
var key = await _repositoryKey.FirstOrDefaultAsync(
a => a.Key == input.DisplayName &&
a.ResourceName == PlatformConsts.AppName);
if (key is null)
{
// Yeni Key'i olustur
await _repositoryKey.InsertAsync(new LanguageKey
{
Key = input.DisplayName,
ResourceName = PlatformConsts.AppName
});
}
var previousCode = (await _menuRepository.GetAsync(id)).Code;
var result = await base.UpdateAsync(id, input);
await SyncUpdatedMenuRowsAsync([(previousCode, result.Code)]);
return result;
}
public async Task<List<MenuDto>> UpdateAllAsync(List<MenuDto> inputs)
{
await CheckUpdatePolicyAsync();
if (!inputs.Any())
return new List<MenuDto>();
// Id kontrolü
if (inputs.Any(x => x.Id == Guid.Empty))
{
throw new ArgumentException("MenuDto içinde geçerli bir Id bulunmalıdır.");
}
// Tüm DisplayName'leri topla
var displayNames = inputs.Select(x => x.DisplayName).Distinct().ToList();
// Mevcut key'leri tek sorguda getir
var existingKeys = await AsyncExecuter.ToListAsync(
(await _repositoryKey.GetQueryableAsync())
.Where(a => displayNames.Contains(a.Key) && a.ResourceName == PlatformConsts.AppName));
var existingKeyNames = existingKeys.Select(x => x.Key).ToHashSet();
// Eksik key'leri toplu ekle
var newKeys = displayNames
.Where(name => !existingKeyNames.Contains(name))
.Select(name => new LanguageKey
{
Key = name,
ResourceName = PlatformConsts.AppName
})
.ToList();
if (newKeys.Any())
{
await _repositoryKey.InsertManyAsync(newKeys, autoSave: true);
}
var ids = inputs.Select(x => x.Id).ToList();
var previousCodes = (await _menuRepository.GetListAsync(a => ids.Contains(a.Id)))
.ToDictionary(a => a.Id, a => a.Code);
// Menüleri güncelle
var result = new List<MenuDto>();
foreach (var input in inputs)
{
var updated = await base.UpdateAsync(input.Id, input);
result.Add(updated);
}
await SyncUpdatedMenuRowsAsync(result.Select(x => (previousCodes.GetValueOrDefault(x.Id), x.Code)));
return result;
}
public override async Task DeleteAsync(Guid id)
{
await CheckDeletePolicyAsync();
var menu = await _menuRepository.GetAsync(id);
if (menu != null)
{
await _repositoryKey.DeleteAsync(
a => a.Key == menu.DisplayName &&
a.ResourceName == PlatformConsts.AppName);
}
await base.DeleteAsync(id);
if (menu != null)
await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, [menu.Code]);
}
public async Task<List<MenuDto>> GetListMainMenuAsync()
{
await CheckGetPolicyAsync();
var query = await _menuRepository.GetQueryableAsync();
// Public site menusu yonetim kenar cubugunda yer almaz; o agac ziyaretci
// duzenine aittir ve uygulama konfigurasyonu uzerinden tasinir.
query = query.Where(a => !a.IsDisabled && a.ModuleId != null &&
a.ModuleId != PlatformConsts.PublicSite.ModuleId);
var entities = await AsyncExecuter.ToListAsync(query);
var entityDtos = await MapToGetListOutputDtosAsync(entities);
return entityDtos;
}
/// <summary>
/// Menüyü tüm alanlarıyla günceller ve görünen adın dil metinlerini de yazar.
/// Kod (Code) dil anahtarı ve yetki eşleşmesi olduğu için değiştirilemez; gönderilen
/// değer yok sayılıp mevcut kod korunur.
/// </summary>
public async Task<MenuDto> UpdateWithLanguageKeyTextAsync(Guid id, MenuDto input)
{
await CheckUpdatePolicyAsync();
await EnsureModuleAsync(input.ModuleId);
var menu = await _menuRepository.GetAsync(id);
// Kod değiştirilemez: dil anahtarı, alt menülerin ParentCode'u ve permission
// kayıtları bu değere bağlıdır.
input.Code = menu.Code;
await UpsertMenuLanguageTextsAsync(menu.Code, input.MenuTextEn, input.MenuTextTr);
var result = await base.UpdateAsync(id, input);
await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Update);
return result;
}
/// <summary>
/// Menü koduna karşılık gelen dil anahtarını ve EN/TR metinlerini oluşturur veya günceller.
/// </summary>
private async Task UpsertMenuLanguageTextsAsync(string code, string menuTextEn, string menuTextTr)
{
var keyExists = await _repositoryKey.AnyAsync(
a => a.Key == code &&
a.ResourceName == PlatformConsts.AppName);
if (!keyExists)
{
await _repositoryKey.InsertAsync(new LanguageKey
{
Key = code,
ResourceName = PlatformConsts.AppName
}, autoSave: true);
}
var changedTextIds = new List<object>();
var insertedTextIds = new List<object>();
foreach (var (culture, value) in new[] { ("en", menuTextEn), ("tr", menuTextTr) })
{
if (value == null)
continue;
var existing = await _repositoryText.FirstOrDefaultAsync(
a => a.CultureName == culture &&
a.Key == code &&
a.ResourceName == PlatformConsts.AppName);
if (existing != null)
{
existing.Value = value;
await _repositoryText.UpdateAsync(existing, autoSave: true);
changedTextIds.Add(existing.Id);
}
else
{
var inserted = await _repositoryText.InsertAsync(new LanguageText
{
Key = code,
CultureName = culture,
Value = value,
ResourceName = PlatformConsts.AppName
}, autoSave: true);
insertedTextIds.Add(inserted.Id);
}
}
await _languageTextAppService.ClearRedisCacheAsync();
// Dil metinleri de kendi ekraninin seed dosyasinda yasar; menu adini runtime'da
// degistirmek DB sifirlandiginda geri gelmeli.
await _seedDataSynchronizer.SyncRowsAsync(AppCodes.Languages.LanguageText, changedTextIds, SeedSyncOperation.Update);
await _seedDataSynchronizer.SyncRowsAsync(AppCodes.Languages.LanguageText, insertedTextIds, SeedSyncOperation.Insert);
}
public async Task<MenuDto> CreateWithLanguageKeyTextAsync(MenuDto input)
{
await CheckCreatePolicyAsync();
await EnsureModuleAsync(input.ModuleId);
await UpsertMenuLanguageTextsAsync(input.Code, input.MenuTextEn, input.MenuTextTr);
var result = await base.CreateAsync(input);
await SyncMenuRowsAsync([result.Code], SeedSyncOperation.Insert);
return result;
}
/// <summary>
/// Menu Yoneticisi de <c>App.Menus.Menu</c> ekraniyla ayni seed dosyasini yazar; aksi halde
/// siralama ve duzenlemeler DB sifirlandiginda kaybolur. Dosyanin anahtari <c>Code</c>'dur.
/// </summary>
private Task SyncMenuRowsAsync(IReadOnlyCollection<string> codes, SeedSyncOperation operation) =>
_seedDataSynchronizer.SyncRowsAsync(AppCodes.Menus.Menu, [.. codes], operation);
/// <summary>
/// Kodu degisen menunun eski satiri dosyadan cikarilir; dosya Code anahtarli oldugu icin
/// yeni kod ayri bir satir olarak eklenir.
/// </summary>
private async Task SyncUpdatedMenuRowsAsync(IEnumerable<(string? PreviousCode, string Code)> changes)
{
var list = changes.ToList();
var renamed = list
.Where(x => x.PreviousCode != x.Code)
.Select(x => x.PreviousCode)
.OfType<string>()
.Where(code => !string.IsNullOrWhiteSpace(code))
.Cast<object>()
.ToArray();
if (renamed.Length > 0)
await _seedDataSynchronizer.RemoveRowsAsync(AppCodes.Menus.Menu, renamed);
await SyncMenuRowsAsync([.. list.Select(x => x.Code)], SeedSyncOperation.Update);
}
/// <summary>
/// Menude secilen modul tanimli degilse olusturur; kok menude girilen kisa kod
/// dogrudan yeni bir modul olur ve ModuleId yabanci anahtari bos kalmaz.
/// </summary>
private async Task EnsureModuleAsync(string moduleId)
{
var cleaned = moduleId?.Trim();
if (string.IsNullOrWhiteSpace(cleaned))
{
return;
}
if (await _moduleRepository.AnyAsync(a => a.Id == cleaned))
{
return;
}
await _moduleRepository.InsertAsync(new Module(cleaned) { Name = cleaned }, autoSave: true);
}
public async Task<List<LookupDataDto>> GetModulesAsync()
{
var modules = await _moduleRepository.GetListAsync();
return modules
.OrderBy(module => module.Name)
.Select(module => new LookupDataDto
{
Key = module.Id,
Name = module.Name
})
.ToList();
}
}