怎样优雅地增删查改(八):按用户关系查询

用户关系(Relation)是描述业务系统中人员与人员之间的关系,如:签约、关注,或者朋友关系。

之前我们在扩展身份管理模块的时候,已经实现了用户关系管理,可以查看本系列博文之前的内容。怎样优雅地增删查改(二):扩展身份管理模块

原理

查询依据

用户之间的关系通过Relation表来存储。模型如下图所示:

在这里插入图片描述

  • 关系类型由Type来定义

  • 关系指向由UserId与RelatedUserId来描述

    人员之间的关系是单项的,也就是说可以A是B的好友,但B不一定是A的好友

    正向关系:User -> RelatedUser

    反向关系:RelatedUser -> User

查询目标业务对象HealthAlarm关联了业务用户HealthClient,因业务用户与鉴权用户IdentityUser共享同一个Id,因此可以通过查询用户关系关联的User,查询到业务对象。

在这里插入图片描述

实现

正向用户关系

定义按正向用户关系查询(IRelationToOrientedFilter)接口

1
2
3
4
5
6
7
8
9
10
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

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

创建应用过滤条件方法:ApplyRelationToOrientedFiltered,在此实现拼接LINQ表达式,

ICurrentUser是Abp的一个服务,用于获取当前登录用户的信息

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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)接口

1
2
3
4
5
6
7
8
9
10
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

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

创建应用过滤条件方法:ApplyRelationFromOrientedFiltered,在此实现拼接LINQ表达式,

ICurrentUser是Abp的一个服务,用于获取当前登录用户的信息

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
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接口,代码如下:

1
2
3
4
5
6
7
8
9
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如下图:

在这里插入图片描述

怎样优雅地增删查改(八):按用户关系查询

https://blog.matoapp.net/posts/300308a1/

作者

林晓lx

发布于

2023-07-19

更新于

2024-09-11

许可协议

评论