怎樣優雅地增刪查改(八):按使用者關係查詢

2023-07-19 12:01:22

@


使用者關係(Relation)是描述業務系統中人員與人員之間的關係,如:簽約、關注,或者朋友關係。

之前我們在擴充套件身份管理模組的時候,已經實現了使用者關係管理,可以檢視本系列博文之前的內容。怎樣優雅地增刪查改(二):擴充套件身份管理模組

原理

查詢依據

使用者之間的關係通過Relation表來儲存。模型如下圖所示:

  • 關係型別由Type來定義

  • 關係指向由UserId與RelatedUserId來描述

    人員之間的關係是單項的,也就是說可以A是B的好友,但B不一定是A的好友

    正向關係:User -> RelatedUser

    反向關係:RelatedUser -> User

查詢目標業務物件HealthAlarm關聯了業務使用者HealthClient,因業務使用者與鑑權使用者IdentityUser共用同一個Id,因此可以通過查詢使用者關係關聯的User,查詢到業務物件。

實現

正向使用者關係

定義按正向使用者關係查詢(IRelationToOrientedFilter)介面

public interface IRelationToOrientedFilter
{
    Guid? RelationToUserId { get; set; }
    
    public string EntityUserIdIdiom { get; }

    string RelationType { get; set; }

}

  • EntityUserIdIdiom:語意上的UserId,用於指定業務實體中用於描述「使用者Id」欄位的名稱,若不指定,則預設為「UserId」;
  • RelationToUserId:正向關係使用者Id,若為Guid.Empty,則使用當前登入使用者的Id;
  • RelationType:關係型別,如:「attach」為簽約,「follow」為關注,可自定義。

對於Relation服務,其依賴關係在應用層,查詢指定使用者的關係使用者將在CurdAppServiceBase的子類實現。建立一個抽象方法GetUserIdsByRelatedToAsync

protected abstruct Task<IEnumerable<Guid>> GetUserIdsByRelatedToAsync(Guid userId, string relationType);

建立應用過濾條件方法:ApplyRelationToOrientedFiltered,在此實現拼接LINQ表示式,

ICurrentUser是Abp的一個服務,用於獲取當前登入使用者的資訊

程式碼如下:

protected virtual async Task<IQueryable<TEntity>> ApplyRelationToOrientedFiltered(IQueryable<TEntity> query, TGetListInput input)
{
    if (input is IRelationToOrientedFilter)
    {
        var filteredInput = input as IRelationToOrientedFilter;
        var entityUserIdIdiom = filteredInput.EntityUserIdIdiom;
        if (string.IsNullOrEmpty(entityUserIdIdiom))
        {
            entityUserIdIdiom = "UserId";
        }
        if (HasProperty<TEntity>(entityUserIdIdiom))
        {
            var property = typeof(TEntity).GetProperty(entityUserIdIdiom);
            if (filteredInput != null && filteredInput.RelationToUserId.HasValue && !string.IsNullOrEmpty(filteredInput.RelationType))
            {

                Guid userId = default;
                if (filteredInput.RelationToUserId.Value == Guid.Empty)
                {
                    using (var scope = ServiceProvider.CreateScope())
                    {
                        var currentUser = scope.ServiceProvider.GetRequiredService<ICurrentUser>();
                        if (currentUser != null)
                        {
                            userId = currentUser.GetId();
                        }
                    }
                }
                else
                {
                    userId = filteredInput.RelationToUserId.Value;
                }

                var ids = await GetUserIdsByRelatedToAsync(userId, filteredInput.RelationType);
                Expression originalExpression = null;
                var parameter = Expression.Parameter(typeof(TEntity), "p");
                foreach (var id in ids)
                {
                    var keyConstantExpression = Expression.Constant(id, typeof(Guid));
                    var propertyAccess = Expression.MakeMemberAccess(parameter, property);
                    var expressionSegment = Expression.Equal(propertyAccess, keyConstantExpression);

                    if (originalExpression == null)
                    {
                        originalExpression = expressionSegment;
                    }
                    else
                    {
                        originalExpression = Expression.Or(originalExpression, expressionSegment);
                    }
                }

                var equalExpression = originalExpression != null ?
                        Expression.Lambda<Func<TEntity, bool>>(originalExpression, parameter)
                        : p => false;

                query = query.Where(equalExpression);

            }

        }
    }
    return query;
}


反向使用者關係

定義按反向使用者關係查詢(IRelationFromOrientedFilter)介面

public interface IRelationFromOrientedFilter
{
    Guid? RelationFromUserId { get; set; }
    
    public string EntityUserIdIdiom { get; }

    string RelationType { get; set; }

}

  • EntityUserIdIdiom:語意上的UserId,用於指定業務實體中用於描述「使用者Id」欄位的名稱,若不指定,則預設為「UserId」;
  • RelationFromUserId:反向關係使用者Id,若為Guid.Empty,則使用當前登入使用者的Id;
  • RelationType:關係型別,如:「attach」為簽約,「follow」為關注,可自定義。

對於Relation服務,其依賴關係在應用層,查詢指定使用者的關係使用者將在CurdAppServiceBase的子類實現。建立一個抽象方法GetUserIdsByRelatedFromAsync

protected abstruct Task<IEnumerable<Guid>> GetUserIdsByRelatedFromAsync(Guid userId, string relationType);

建立應用過濾條件方法:ApplyRelationFromOrientedFiltered,在此實現拼接LINQ表示式,

ICurrentUser是Abp的一個服務,用於獲取當前登入使用者的資訊

程式碼如下:

protected virtual async Task<IQueryable<TEntity>> ApplyRelationFromOrientedFiltered(IQueryable<TEntity> query, TGetListInput input)
{
    if (input is IRelationFromOrientedFilter)
    {
        var filteredInput = input as IRelationFromOrientedFilter;
        var entityUserIdIdiom = filteredInput.EntityUserIdIdiom;
        if (string.IsNullOrEmpty(entityUserIdIdiom))
        {
            entityUserIdIdiom = "UserId";
        }
        if (HasProperty<TEntity>(entityUserIdIdiom))
        {
            var property = typeof(TEntity).GetProperty(entityUserIdIdiom);
            if (filteredInput != null && filteredInput.RelationFromUserId.HasValue && !string.IsNullOrEmpty(filteredInput.RelationType))
            {

                Guid userId = default;
                if (filteredInput.RelationFromUserId.Value == Guid.Empty)
                {
                    using (var scope = ServiceProvider.CreateScope())
                    {
                        var currentUser = scope.ServiceProvider.GetRequiredService<ICurrentUser>();
                        if (currentUser != null)
                        {
                            userId = currentUser.GetId();
                        }
                    }
                }
                else
                {
                    userId = filteredInput.RelationFromUserId.Value;
                }

                var ids = await GetUserIdsByRelatedFromAsync(userId, filteredInput.RelationType);
                Expression originalExpression = null;
                var parameter = Expression.Parameter(typeof(TEntity), "p");
                foreach (var id in ids)
                {
                    var keyConstantExpression = Expression.Constant(id, typeof(Guid));
                    var propertyAccess = Expression.MakeMemberAccess(parameter, property);
                    var expressionSegment = Expression.Equal(propertyAccess, keyConstantExpression);

                    if (originalExpression == null)
                    {
                        originalExpression = expressionSegment;
                    }
                    else
                    {
                        originalExpression = Expression.Or(originalExpression, expressionSegment);
                    }
                }

                var equalExpression = originalExpression != null ?
                        Expression.Lambda<Func<TEntity, bool>>(originalExpression, parameter)
                        : p => false;

                query = query.Where(equalExpression);

            }
        }
    }
    return query;
}

IRelationToOrientedFilter 和 IRelationFromOrientedFilter介面實現上並非互斥。

請注意,可應用過濾的條件為:

  1. input需實現IRelationToOrientedFilter介面;
  2. 實體必須關聯使用者。

否則將原封不動返回IQueryable物件。

使用

在應用層中,實現GetUserIdsByRelatedToAsync

protected override async Task<IEnumerable<Guid>> GetUserIdsByRelatedToAsync(Guid userId, string relationType)
{
    var ids = await relationAppService.GetRelatedToUserIdsAsync(new GetRelatedUsersInput()
    {
        UserId = userId,
        Type = relationType
    });
    return ids;

}

或GetUserIdsByRelatedFromAsync

protected override async Task<IEnumerable<Guid>> GetUserIdsByRelatedFromAsync(Guid userId, string relationType)
{
    var ids = await relationAppService.GetRelatedFromUserIdsAsync(new GetRelatedUsersInput()
    {
        UserId = userId,
        Type = relationType
    });
    return ids;

}

在GetAllAlarmInput中實現IRelationToOrientedFilter或GetUserIdsByRelatedFromAsync介面,程式碼如下:

public class GetAllAlarmInput : PagedAndSortedResultRequestDto, IRelationToOrientedFilter
{ 
    public Guid? RelationToUserId { get ; set ; }
    public string RelationType { get; set; }
    public string EntityUserIdIdiom { get; }

    ...
}

測試

建立一些客戶(Client)

進入客戶管理,在右側客戶列表中點選「檢視詳情」

開啟客戶詳情頁面,點選管理 - 設定簽約員工

選擇一個使用者,此時該客戶會簽約至該使用者賬號下,這裡我們將客戶1和客戶3簽約至當前賬號admin下。

登入簽約使用者(admin)的賬號,點選「我的」 - 客戶 - 簽約客戶

在客戶列表中可見,客戶1和客戶3已簽約至當前賬號下。

組合查詢的報文Payload如下圖: