diff --git a/.gitignore b/.gitignore
index add57be707d1a627fd960286263733b8f2df2dcb..069a617f93730bd368c046ebf7a353ec188eda50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,6 @@ bin/
obj/
/packages/
riderModule.iml
-/_ReSharper.Caches/
\ No newline at end of file
+/_ReSharper.Caches/
+wwwroot/
+**/appsettings.Development.json
\ No newline at end of file
diff --git a/Athena.Infrastructure.DataPermission/Athena.Infrastructure.DataPermission.csproj b/Athena.Infrastructure.DataPermission/Athena.Infrastructure.DataPermission.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..2cd34f5787b4237bbb6018125ff67bfcf84220cc
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/Athena.Infrastructure.DataPermission.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Athena.Infrastructure.DataPermission/Attributes/DataPermissionAttribute.cs b/Athena.Infrastructure.DataPermission/Attributes/DataPermissionAttribute.cs
new file mode 100644
index 0000000000000000000000000000000000000000..f3e0b0088ad8665f66794ef936c0bb7b1e01b9e9
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/Attributes/DataPermissionAttribute.cs
@@ -0,0 +1,47 @@
+namespace Athena.Infrastructure.DataPermission.Attributes;
+
+///
+/// 数据权限属性
+///
+[AttributeUsage(AttributeTargets.Class)]
+public class DataPermissionAttribute : Attribute
+{
+ ///
+ /// 数据权限属性
+ ///
+ public DataPermissionAttribute(Type baseType)
+ {
+ BaseType = baseType;
+ }
+
+ ///
+ /// 数据权限属性
+ ///
+ /// 基础类型,用于控制查询
+ /// 显示名
+ public DataPermissionAttribute(Type baseType, string displayName)
+ {
+ DisplayName = displayName;
+ BaseType = baseType;
+ }
+
+ ///
+ /// 权限名称
+ ///
+ public string? DisplayName { get; set; }
+
+ ///
+ /// KEY
+ ///
+ public string? Key { get; set; }
+
+ ///
+ /// 基础类型
+ ///
+ public Type BaseType { get; set; }
+
+ ///
+ /// 分组
+ ///
+ public string? Group { get; set; }
+}
\ No newline at end of file
diff --git a/Athena.Infrastructure.DataPermission/Basics/BasicDataPermissionFactory.cs b/Athena.Infrastructure.DataPermission/Basics/BasicDataPermissionFactory.cs
new file mode 100644
index 0000000000000000000000000000000000000000..f8c5c8b5c2c6fa5110c36b81355f32dfc58904c1
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/Basics/BasicDataPermissionFactory.cs
@@ -0,0 +1,44 @@
+namespace Athena.Infrastructure.DataPermission.Basics;
+
+///
+/// 基础数据权限工厂类
+///
+public class BasicDataPermissionFactory
+{
+ private readonly IEnumerable _services;
+
+ ///
+ /// 构造函数
+ ///
+ ///
+ public BasicDataPermissionFactory(IEnumerable services)
+ {
+ _services = services;
+ }
+
+ ///
+ /// 获取实例列表
+ ///
+ ///
+ public IEnumerable GetInstanceList()
+ {
+ return _services.ToList();
+ }
+
+ ///
+ /// 获取下拉选择框列表
+ ///
+ ///
+ public IList GetSelectList()
+ {
+ var list = new List();
+ list.AddRange(_services
+ .Select(p => new
+ {
+ p.Label,
+ p.Value
+ })
+ );
+ return list;
+ }
+}
\ No newline at end of file
diff --git a/Athena.Infrastructure.DataPermission/Basics/IBasicDataPermission.cs b/Athena.Infrastructure.DataPermission/Basics/IBasicDataPermission.cs
new file mode 100644
index 0000000000000000000000000000000000000000..47790a5531b5b8d940a4926d5e3e58798f9cdae2
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/Basics/IBasicDataPermission.cs
@@ -0,0 +1,25 @@
+namespace Athena.Infrastructure.DataPermission.Basics;
+
+///
+/// 基础数据权限接口
+///
+public interface IBasicDataPermission
+{
+ ///
+ /// LABEL
+ ///
+ string Label { get; }
+
+ ///
+ /// Value
+ ///
+ string Value { get; }
+
+ ///
+ /// 获取查询条件
+ ///
+ /// 标识ID
+ /// Sql语句需要返回Id字段,返回其他可能会出异常或达不到查询预期
+ ///
+ string GetSqlString(string identityId);
+}
\ No newline at end of file
diff --git a/Athena.Infrastructure.DataPermission/DataPermissionFactory.cs b/Athena.Infrastructure.DataPermission/DataPermissionFactory.cs
new file mode 100644
index 0000000000000000000000000000000000000000..59d9d161a698813751a30f162b4fd0e8659fdd0a
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/DataPermissionFactory.cs
@@ -0,0 +1,45 @@
+namespace Athena.Infrastructure.DataPermission;
+
+///
+/// 数据权限工厂类
+///
+public class DataPermissionFactory
+{
+ private readonly IEnumerable _services;
+
+ ///
+ /// 构造函数
+ ///
+ ///
+ public DataPermissionFactory(IEnumerable services)
+ {
+ _services = services;
+ }
+
+ ///
+ /// 获取实例列表
+ ///
+ ///
+ public IList GetInstances()
+ {
+ return _services.ToList();
+ }
+
+ ///
+ /// 获取下拉选择框列表
+ ///
+ ///
+ public IList GetSelectList()
+ {
+ var list = new List();
+ list.AddRange(_services
+ .Select(p => new
+ {
+ p.Label,
+ p.Key,
+ p.Value
+ })
+ );
+ return list;
+ }
+}
\ No newline at end of file
diff --git a/Athena.Infrastructure.DataPermission/DataPermissionHelper.cs b/Athena.Infrastructure.DataPermission/DataPermissionHelper.cs
new file mode 100644
index 0000000000000000000000000000000000000000..1aab78375aae9016bebd875944c06b3d7a0773cb
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/DataPermissionHelper.cs
@@ -0,0 +1,204 @@
+using System.Reflection;
+using System.Xml;
+using System.Xml.XPath;
+using Athena.Infrastructure.DataPermission.Attributes;
+using Athena.Infrastructure.Enums;
+using Athena.Infrastructure.Summaries;
+
+namespace Athena.Infrastructure.DataPermission;
+
+public abstract class DataPermissionHelper
+{
+ ///
+ /// 获取树形结构列表
+ ///
+ ///
+ private static List GetTreeSelectList(string assemblyName)
+ {
+ var asm = Assembly.Load(assemblyName);
+ var types = asm.GetExportedTypes();
+ bool IsMyAttribute(IEnumerable o) => o.OfType().Any();
+ var typeList = types
+ .Where(o => IsMyAttribute(Attribute.GetCustomAttributes(o, false)))
+ .ToList();
+ XPathNavigator? xmlNavigator = null;
+ var path = AppDomain.CurrentDomain.BaseDirectory;
+ // 找出里面的xml
+ var fileInfo = new DirectoryInfo(path).GetFiles()
+ // 读取文件后缀名为.xml的文件信息
+ .Where(p => p.Extension.ToLower() == ".xml")
+ .FirstOrDefault(n => assemblyName.Contains(n.Name.Replace(n.Extension, "")));
+ if (fileInfo != null)
+ {
+ var document = new XmlDocument();
+ document.Load(fileInfo.OpenRead());
+ xmlNavigator = document.CreateNavigator();
+ }
+
+ var summaryList = new List();
+ foreach (var type in typeList)
+ {
+ var summaryName = GetTypeSummaryName(xmlNavigator, type);
+
+ var summary = new TreeSelectInfo
+ {
+ Label = summaryName,
+ Key = type.Name,
+ Value = type.Name,
+ PropertyType = "type",
+ Children = new List()
+ };
+ foreach (var property in type.GetProperties())
+ {
+ var label = GetPropertySummaryName(xmlNavigator, property);
+
+ var dataType = "";
+ var enumOptions = new List();
+ if (
+ property.PropertyType == typeof(int) ||
+ property.PropertyType == typeof(int?) ||
+ property.PropertyType == typeof(long) ||
+ property.PropertyType == typeof(long?) ||
+ property.PropertyType == typeof(decimal) ||
+ property.PropertyType == typeof(decimal?) ||
+ property.PropertyType == typeof(double) ||
+ property.PropertyType == typeof(double?)
+ )
+ {
+ dataType = "number";
+ }
+ else if (
+ property.PropertyType == typeof(DateTime) ||
+ property.PropertyType == typeof(DateTime?)
+ )
+ {
+ dataType = "dateTime";
+ }
+ else if (
+ property.PropertyType == typeof(bool) ||
+ property.PropertyType == typeof(bool?)
+ )
+ {
+ dataType = "boolean";
+ }
+ else if (property.PropertyType.IsEnum)
+ {
+ dataType = "enum";
+ var enums = Enum.GetValues(property.PropertyType);
+ var enumList = enums.OfType().ToList();
+ enumOptions.AddRange(enumList.Select(p => new
+ {
+ Label = p.ToDescription(),
+ Value = p.GetHashCode().ToString()
+ }));
+ }
+ else if (
+ property.PropertyType == typeof(string) ||
+ property.PropertyType == typeof(Guid) ||
+ property.PropertyType == typeof(Guid?))
+ {
+ dataType = "string";
+ }
+
+ if (string.IsNullOrEmpty(dataType))
+ {
+ continue;
+ }
+
+
+ summary.Children.Add(new TreeSelectInfo
+ {
+ PropertyType = dataType,
+ Key = property.Name,
+ Value = property.Name,
+ Label = label,
+ EnumOptions = enumOptions
+ });
+ }
+
+ summaryList.Add(summary);
+ }
+
+ return summaryList;
+ }
+
+ ///
+ /// 读取类注释
+ ///
+ ///
+ ///
+ ///
+ private static string GetTypeSummaryName(
+ XPathNavigator? xmlNavigator,
+ Type type)
+ {
+ if (xmlNavigator == null)
+ {
+ return type.Name;
+ }
+
+ var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(type);
+ var propertySummaryNode =
+ xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']/summary");
+ return propertySummaryNode != null ? XmlCommentsTextHelper.Humanize(propertySummaryNode.InnerXml) : type.Name;
+ }
+
+ ///
+ /// 读取属性注释
+ ///
+ ///
+ ///
+ ///
+ private static string GetPropertySummaryName(
+ XPathNavigator? xmlNavigator,
+ MemberInfo property)
+ {
+ if (xmlNavigator == null)
+ {
+ return property.Name;
+ }
+
+ var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(property);
+ var propertySummaryNode =
+ xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']/summary");
+ return propertySummaryNode != null
+ ? XmlCommentsTextHelper.Humanize(propertySummaryNode.InnerXml)
+ : property.Name;
+ }
+}
+
+///
+/// Select Info
+///
+public class TreeSelectInfo
+{
+ ///
+ /// 显示名称
+ ///
+ public string Label { get; set; } = null!;
+
+ ///
+ /// 值
+ ///
+ public string Value { get; set; } = null!;
+
+ ///
+ /// Key
+ ///
+ public string Key { get; set; } = Guid.NewGuid().ToString("N");
+
+ ///
+ /// 属性类型
+ ///
+ public string? PropertyType { get; set; }
+
+ ///
+ /// 子项
+ ///
+ public List? Children { get; set; }
+
+ ///
+ /// 枚举选项列表
+ ///
+ public List? EnumOptions { get; set; }
+}
\ No newline at end of file
diff --git a/Athena.Infrastructure.DataPermission/Extras/ExtraDataPermissionFactory.cs b/Athena.Infrastructure.DataPermission/Extras/ExtraDataPermissionFactory.cs
new file mode 100644
index 0000000000000000000000000000000000000000..34f0216a7338bd7105e54774e170199ae56dd89d
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/Extras/ExtraDataPermissionFactory.cs
@@ -0,0 +1,45 @@
+namespace Athena.Infrastructure.DataPermission.Extras;
+
+///
+/// 扩展数据权限工厂类
+///
+public class ExtraDataPermissionFactory
+{
+ private readonly IEnumerable _services;
+
+ ///
+ /// 构造函数
+ ///
+ ///
+ public ExtraDataPermissionFactory(IEnumerable services)
+ {
+ _services = services;
+ }
+
+ ///
+ /// 获取实例列表
+ ///
+ ///
+ public IList GetInstances()
+ {
+ return _services.ToList();
+ }
+
+ ///
+ /// 获取下拉选择框列表
+ ///
+ ///
+ public IList GetSelectList()
+ {
+ var list = new List();
+ list.AddRange(_services
+ .Select(p => new
+ {
+ p.Label,
+ p.Key,
+ p.Value
+ })
+ );
+ return list;
+ }
+}
\ No newline at end of file
diff --git a/Athena.Infrastructure.DataPermission/Extras/IExtraDataPermission.cs b/Athena.Infrastructure.DataPermission/Extras/IExtraDataPermission.cs
new file mode 100644
index 0000000000000000000000000000000000000000..3406f95a132fa28022865b6580b729917bf4e598
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/Extras/IExtraDataPermission.cs
@@ -0,0 +1,29 @@
+namespace Athena.Infrastructure.DataPermission.Extras;
+
+///
+/// 扩展数据权限接口
+///
+public interface IExtraDataPermission
+{
+ ///
+ /// LABEL
+ ///
+ string Label { get; }
+
+ ///
+ /// KEY
+ ///
+ string Key { get; }
+
+ ///
+ /// 占位符
+ ///
+ string Value { get; }
+
+ ///
+ /// 获取查询SQL语句
+ ///
+ /// Sql语句需要返回Id字段,返回其他可能会出异常或达不到查询预期
+ ///
+ string GetSqlString();
+}
\ No newline at end of file
diff --git a/Athena.Infrastructure.DataPermission/IDataPermission.cs b/Athena.Infrastructure.DataPermission/IDataPermission.cs
new file mode 100644
index 0000000000000000000000000000000000000000..25147b378e03f5c69d7a976eb44844cb08d6c105
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/IDataPermission.cs
@@ -0,0 +1,29 @@
+namespace Athena.Infrastructure.DataPermission;
+
+///
+/// 数据权限接口
+///
+public interface IDataPermission
+{
+ ///
+ /// LABEL
+ ///
+ string Label { get; }
+
+ ///
+ /// KEY
+ ///
+ string Key { get; }
+
+ ///
+ /// 占位符
+ ///
+ string Value { get; }
+
+ ///
+ /// 获取查询SQL语句
+ ///
+ /// Sql语句需要返回Id字段,返回其他可能会出异常或达不到查询预期
+ ///
+ string GetSqlString();
+}
\ No newline at end of file
diff --git a/Athena.Infrastructure.DataPermission/IDataPermissionService.cs b/Athena.Infrastructure.DataPermission/IDataPermissionService.cs
new file mode 100644
index 0000000000000000000000000000000000000000..905598cd9dbde2e602b133007031630dda3fed15
--- /dev/null
+++ b/Athena.Infrastructure.DataPermission/IDataPermissionService.cs
@@ -0,0 +1,34 @@
+using Athena.Infrastructure.QueryFilters;
+
+namespace Athena.Infrastructure.DataPermission;
+
+///
+/// 数据权限服务接口
+///
+public interface IDataPermissionService
+{
+ ///
+ /// 读取用户已有的策略查询过滤器组列表
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task> GetPolicyQueryFilterGroupsAsync(string userId, string resourceKey, string? appId);
+
+ ///
+ /// 获取用户所在组织列表
+ ///
+ /// 关联的用户ID
+ ///
+ ///
+ Task> GetUserOrganizationIdsAsync(string userId, string? appId);
+
+ ///
+ /// 获取用户所有组织及下级组织列表
+ ///
+ /// 关联的用户ID
+ ///
+ ///
+ Task> GetUserOrganizationIdsTreeAsync(string userId, string? appId);
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/BasicPlatform.AppService.FreeSql.csproj b/BasicPlatform.AppService.FreeSql/BasicPlatform.AppService.FreeSql.csproj
index fc63f2b415818c7fca2912bdbcac3e5876b69cd7..4c914e1385b04052c0bd379932bf9d06502e018f 100644
--- a/BasicPlatform.AppService.FreeSql/BasicPlatform.AppService.FreeSql.csproj
+++ b/BasicPlatform.AppService.FreeSql/BasicPlatform.AppService.FreeSql.csproj
@@ -7,12 +7,12 @@
-
+
-
-
+
+
diff --git a/BasicPlatform.AppService.FreeSql/CacheNotificationHandler.cs b/BasicPlatform.AppService.FreeSql/CacheNotificationHandler.cs
new file mode 100644
index 0000000000000000000000000000000000000000..11e0a9ae3fdc3153620671b67a267ed6e1482989
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/CacheNotificationHandler.cs
@@ -0,0 +1,32 @@
+using Athena.Infrastructure.FreeSql.Interfaces;
+using BasicPlatform.Domain.Events.Users;
+
+namespace BasicPlatform.AppService.FreeSql;
+
+///
+/// 缓存通知处理器
+///
+public class CacheNotificationHandler :
+ IDomainEventHandler
+{
+ private readonly ICacheManager _cacheManager;
+
+ public CacheNotificationHandler(ICacheManager cacheManager)
+ {
+ _cacheManager = cacheManager;
+ }
+
+ ///
+ /// 用户更新成功事件
+ ///
+ ///
+ ///
+ ///
+ public async Task Handle(UserUpdatedEvent notification, CancellationToken cancellationToken)
+ {
+ // 匹配用户缓存
+ var patternKey = string.Format(CacheConstant.UserCacheKeys, notification.Id);
+ // 移除用户所有缓存
+ await _cacheManager.RemovePatternAsync(patternKey, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/CommonService.cs b/BasicPlatform.AppService.FreeSql/CommonService.cs
new file mode 100644
index 0000000000000000000000000000000000000000..ba86edbd2cc5ad006d0d781ed6b8eb7e4b0c2af5
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/CommonService.cs
@@ -0,0 +1,62 @@
+using BasicPlatform.AppService.TableColumns;
+using BasicPlatform.AppService.Users;
+
+namespace BasicPlatform.AppService.FreeSql;
+
+///
+/// 通用服务接口实现类
+///
+[Component]
+public class CommonService : ICommonService
+{
+ private readonly IUserQueryService _userQueryService;
+
+ public CommonService(IUserQueryService userQueryService)
+ {
+ _userQueryService = userQueryService;
+ }
+
+ ///
+ /// 读取表格列信息
+ ///
+ ///
+ ///
+ ///
+ public async Task GetColumnsAsync() where T : class
+ {
+ var moduleName = typeof(T).Name;
+ var sources = TableColumnReader.GetTableColumns(typeof(T));
+ // 读取用户保存的数据
+ var userCustoms = await _userQueryService.GetCurrentUserCustomColumnsAsync(typeof(T).Name);
+ if (userCustoms.Count == 0)
+ {
+ return new GetTableColumnsResponse
+ {
+ ModuleName = moduleName,
+ Columns = sources.OrderBy(p => p.Sort).ToList()
+ };
+ }
+
+ // 合并数据,以用户的为主
+ foreach (var source in sources)
+ {
+ var item = userCustoms.FirstOrDefault(p => p.DataIndex == source.DataIndex);
+ if (item == null)
+ {
+ continue;
+ }
+
+ source.DataIndex = item.DataIndex;
+ source.Width = item.Width;
+ source.HideInTable = !item.Show;
+ source.Fixed = item.Fixed;
+ source.Sort = item.Sort;
+ }
+
+ return new GetTableColumnsResponse
+ {
+ ModuleName = moduleName,
+ Columns = sources.OrderBy(p => p.Sort).ToList()
+ };
+ }
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/Commons/AppQueryServiceBase.cs b/BasicPlatform.AppService.FreeSql/Commons/AppQueryServiceBase.cs
new file mode 100644
index 0000000000000000000000000000000000000000..5d824de513bb7fbe70f3fd8793db6c8ff3090cd9
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/Commons/AppQueryServiceBase.cs
@@ -0,0 +1,39 @@
+namespace BasicPlatform.AppService.FreeSql.Commons;
+
+///
+///
+///
+///
+public class AppQueryServiceBase : QueryServiceBase where T : EntityCore, new()
+{
+ private readonly ISecurityContextAccessor _accessor;
+ private readonly IFreeSql _freeSql;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public AppQueryServiceBase(IFreeSql freeSql, ISecurityContextAccessor accessor) :
+ base(freeSql)
+ {
+ _freeSql = freeSql;
+ _accessor = accessor;
+ }
+
+
+ ///
+ /// 用户ID
+ ///
+ protected string? UserId => _accessor.UserId;
+
+ ///
+ /// 是否为开发者帐号
+ ///
+ protected bool IsRoot => _accessor.IsRoot;
+
+ ///
+ /// 租户ID
+ ///
+ protected string? TenantId => _accessor.TenantId;
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/Commons/AppServiceBase.cs b/BasicPlatform.AppService.FreeSql/Commons/AppServiceBase.cs
new file mode 100644
index 0000000000000000000000000000000000000000..653fdf638a428d3026ef45a90a0b59090241cbc6
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/Commons/AppServiceBase.cs
@@ -0,0 +1,62 @@
+namespace BasicPlatform.AppService.FreeSql.Commons;
+
+///
+///
+///
+///
+public class AppServiceBase : ServiceBase where T : EntityCore, new()
+{
+ private readonly ISecurityContextAccessor _accessor;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public AppServiceBase(UnitOfWorkManager unitOfWorkManager, ISecurityContextAccessor accessor) :
+ base(unitOfWorkManager)
+ {
+ _accessor = accessor;
+ }
+
+
+ ///
+ /// 用户ID
+ ///
+ protected string? UserId => _accessor.UserId;
+
+ ///
+ /// 是否为开发者帐号
+ ///
+ protected bool IsRoot => _accessor.IsRoot;
+
+ ///
+ /// 租户ID
+ ///
+ protected string? TenantId => _accessor.TenantId;
+
+ ///
+ /// IP地址
+ ///
+ protected string IpAddress => _accessor.IpAddress;
+
+ ///
+ /// 读取信息
+ ///
+ ///
+ ///
+ ///
+ protected async Task GetForEditAsync(string? id)
+ {
+ var entity = await Queryable
+ .Where(p => p.Id == id)
+ .ToOneAsync();
+
+ if (entity == null)
+ {
+ throw FriendlyException.Of("无权限操作或找不到数据。");
+ }
+
+ return entity;
+ }
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/Commons/DataPermissionQueryServiceBase.cs b/BasicPlatform.AppService.FreeSql/Commons/DataPermissionQueryServiceBase.cs
new file mode 100644
index 0000000000000000000000000000000000000000..1fe2a89d831502d3dbcb794a9947373a80aa7ba9
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/Commons/DataPermissionQueryServiceBase.cs
@@ -0,0 +1,455 @@
+using System.Linq.Expressions;
+using Athena.Infrastructure.QueryFilters;
+using BasicPlatform.AppService.DataPermissions.Models;
+using BasicPlatform.Infrastructure.Enums;
+
+namespace BasicPlatform.AppService.FreeSql.Commons;
+
+///
+/// 数据权限查询服务基类
+///
+///
+public class DataPermissionQueryServiceBase : QueryServiceBase where T : FullEntityCore, new()
+{
+ private readonly ISecurityContextAccessor _accessor;
+ private readonly IFreeSql _freeSql;
+ private readonly ICacheManager? _cacheManager;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public DataPermissionQueryServiceBase(
+ IFreeSql freeSql,
+ ISecurityContextAccessor accessor
+ ) :
+ base(freeSql)
+ {
+ _freeSql = freeSql;
+ _accessor = accessor;
+ _cacheManager = ServiceLocator.Instance?.GetService(typeof(ICacheManager)) as ICacheManager;
+ }
+
+ ///
+ /// 查询对象
+ ///
+ protected override ISelect Queryable => QueryWithPermission();
+
+ ///
+ /// 查询对象
+ ///
+ protected override ISelect QueryableNoTracking => QueryNoTrackingWithPermission();
+
+ ///
+ /// 查询对象
+ ///
+ ///
+ protected override ISelect Query()
+ {
+ return QueryWithPermission();
+ }
+
+ ///
+ /// 查询对象
+ ///
+ ///
+ protected override ISelect QueryNoTracking()
+ {
+ return QueryNoTrackingWithPermission();
+ }
+
+ ///
+ /// 查询对象
+ ///
+ ///
+ ///
+ protected override ISelect Query()
+ {
+ return QueryWithPermission();
+ }
+
+ ///
+ /// 查询对象
+ ///
+ ///
+ ///
+ protected override ISelect QueryNoTracking()
+ {
+ return QueryNoTrackingWithPermission();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ private ISelect QueryWithPermission() where T1 : class
+ {
+ var query = _freeSql.Queryable();
+ return QueryWithPermission(query);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ private ISelect QueryNoTrackingWithPermission() where T1 : class
+ {
+ var query = _freeSql.Queryable().NoTracking();
+ return QueryWithPermission(query);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ private ISelect QueryWithPermission(ISelect query) where T1 : class
+ {
+ // 如果是开发者帐号。则不需要过滤
+ if (IsRoot)
+ {
+ return query;
+ }
+
+ // 数据访问范围
+ var dataScopeList = GetUserDataScopes();
+ // 读取当前模块的数据访问范围
+ var dataScope = dataScopeList
+ .FirstOrDefault(p => typeof(T1).Name == p.ResourceKey);
+ // 如果该模块有全部数据的权限则不需要过滤
+ if (dataScope == null)
+ {
+ var emptyResourceKeyDataPermissions = dataScopeList
+ .Where(p => string.IsNullOrEmpty(p.ResourceKey))
+ .Select(p => new DataPermission
+ {
+ DataScope = p.DataScope,
+ DataScopeCustom = p.DataScopeCustom
+ })
+ .ToList();
+
+ // 查询通用设置,如果包含全部的数据。则不需要过滤
+ if (emptyResourceKeyDataPermissions.Any(dp => dp.DataScope == RoleDataScope.All))
+ {
+ return query;
+ }
+
+ var filterWhere1 = GenerateFilterWhere(emptyResourceKeyDataPermissions);
+ // 如果没有任何数据权限,则返回空
+ return query.Where(filterWhere1 ?? (p => false));
+ }
+
+ // 当前模块有全部数据的权限则不需要过滤
+ if (dataScope.DataScope == RoleDataScope.All)
+ {
+ return query;
+ }
+
+ var dataPermissions = new List
+ {
+ dataScope
+ };
+ var filterWhere = GenerateFilterWhere(dataPermissions);
+ // 如果没有任何数据权限,则返回空
+ return query.Where(filterWhere ?? (p => false));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static bool HasProperty(string propertyName)
+ {
+ return typeof(T1).GetProperties().Any(p => p.Name == propertyName);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private Expression>? GenerateFilterWhere(ICollection dataPermissions)
+ {
+ if (dataPermissions.Count == 0)
+ {
+ return null;
+ }
+
+ var filters = new List();
+ var organizationIds = new List();
+ foreach (var data in dataPermissions)
+ {
+ if (data.DataScope == RoleDataScope.Self)
+ {
+ if (!HasProperty("CreatedUserId"))
+ {
+ continue;
+ }
+
+ filters.Add(new QueryFilter
+ {
+ Key = "CreatedUserId",
+ Operator = "==",
+ Value = UserId!,
+ XOR = "or"
+ });
+ continue;
+ }
+
+ if (!HasProperty("OrganizationalUnitIds"))
+ {
+ continue;
+ }
+
+ var orgIds = data.DataScope switch
+ {
+ RoleDataScope.Department => GetUserOrganizationIds(),
+ RoleDataScope.DepartmentAndSub => GetUserOrganizationIdsTree(),
+ RoleDataScope.Custom => data.DataScopeCustoms,
+ _ => null
+ };
+
+ if (orgIds == null || orgIds.Count == 0)
+ {
+ continue;
+ }
+
+ organizationIds.AddRange(orgIds);
+ }
+
+ if (organizationIds.Count <= 0)
+ {
+ return QueryableExtensions.MakeFilterWhere(filters, false);
+ }
+
+ // 去重
+ organizationIds = organizationIds.GroupBy(p => p).Select(p => p.Key).ToList();
+
+ foreach (var orgId in organizationIds)
+ {
+ filters.Add(new QueryFilter
+ {
+ Key = "OrganizationalUnitIds",
+ Operator = "contains",
+ Value = orgId,
+ XOR = "or"
+ });
+ }
+
+ return QueryableExtensions.MakeFilterWhere(filters, false);
+ }
+
+ ///
+ /// 用户ID
+ ///
+ protected string? UserId => _accessor.UserId;
+
+ ///
+ /// 是否为开发者帐号
+ ///
+ protected bool IsRoot => _accessor.IsRoot;
+
+ ///
+ /// 租户ID
+ ///
+ protected string? TenantId => _accessor.TenantId;
+
+ #region 数据查询权限相关
+
+ ///
+ /// 读取用户组织架构ID列表
+ ///
+ ///
+ protected List GetUserOrganizationIds(string? userId = null)
+ {
+ List QueryFunc()
+ {
+ userId ??= UserId;
+ // 兼任职信息表
+ var orgIds = _freeSql.Select()
+ .Where(p => p.UserId == userId)
+ .ToList(p => p.OrganizationId);
+
+ // 用户组织
+ var orgId = _freeSql.Select()
+ .Where(p => p.Id == userId)
+ .First(p => p.OrganizationId);
+
+ if (!string.IsNullOrEmpty(orgId))
+ {
+ orgIds.Add(orgId);
+ }
+
+ return orgIds;
+ }
+
+ if (_cacheManager == null)
+ {
+ return QueryFunc();
+ }
+
+ // Key
+ var key = string.Format(CacheConstant.UserOrganizationKey, userId ?? UserId);
+ // 过期时间
+ var expireTime = TimeSpan.FromMinutes(30);
+
+ return _cacheManager.GetOrCreate(key, QueryFunc, expireTime) ?? new List();
+ }
+
+ ///
+ /// 读取用户组织架构及下级组织架构ID列表
+ ///
+ ///
+ protected List GetUserOrganizationIdsTree(string? userId = null)
+ {
+ List QueryFunc()
+ {
+ userId ??= UserId;
+ // 查询用户所在的组织
+ var list = GetUserOrganizationIds(userId);
+
+ if (list.Count == 0)
+ {
+ return list;
+ }
+
+ var filters = list.Select(p => new QueryFilter
+ {
+ Key = "ParentPath",
+ Operator = "contains",
+ Value = p,
+ XOR = "or"
+ }).ToList();
+ // 生成查询条件
+ var filterWhere = QueryableExtensions.MakeFilterWhere(filters, false);
+ // 查询用户组织架构的下级组织
+ var orgIds = _freeSql.Select()
+ .Where(filterWhere)
+ .ToList(p => p.Id);
+ list.AddRange(orgIds);
+
+ // 数据去重
+ return list.GroupBy(p => p).Select(p => p.Key).ToList();
+ }
+
+ if (_cacheManager == null)
+ {
+ return QueryFunc();
+ }
+
+ // Key
+ var key = string.Format(CacheConstant.UserOrganizationsKey, userId ?? UserId);
+ // 过期时间
+ var expireTime = TimeSpan.FromMinutes(30);
+ return _cacheManager.GetOrCreate(key, QueryFunc, expireTime) ?? new List();
+ }
+
+ ///
+ /// 读取用户角色的数据范围列表
+ ///
+ ///
+ private List GetUserDataScopes(string? userId = null)
+ {
+ if (_cacheManager == null)
+ {
+ return QueryFunc();
+ }
+ List QueryFunc()
+ {
+ userId ??= UserId;
+ var dataScopeList = _freeSql.Select()
+ .Where(p => _freeSql
+ .Select()
+ .As("c")
+ .Where(c => c.UserId == userId)
+ .Any(c => c.RoleId == p.Id)
+ )
+ .ToList(p => new
+ {
+ p.DataScope,
+ p.DataScopeCustom
+ });
+
+ // 去重
+ dataScopeList = dataScopeList.GroupBy(p => p).Select(p => p.Key).ToList();
+
+ // 读取用户的角色数据权限
+ var list = _freeSql.Select()
+ // 读取用户的角色
+ .Where(p => _freeSql
+ .Select()
+ .As("c")
+ .Where(c => c.UserId == userId)
+ .Any(c => c.RoleId == p.RoleId)
+ )
+ // 启用的
+ .Where(p => p.Enabled)
+ .ToList(p => new DataPermission
+ {
+ ResourceKey = p.ResourceKey,
+ DataScope = p.DataScope
+ });
+
+ // 读取用户的数据权限
+ var userPermissionList = _freeSql.Select()
+ .Where(p => p.UserId == userId)
+ // 启用的
+ .Where(p => p.Enabled)
+ // 读取未过期的
+ .Where(p => p.ExpireAt == null || p.ExpireAt > DateTime.Now)
+ .ToList(p => new DataPermission
+ {
+ ResourceKey = p.ResourceKey,
+ DataScope = p.DataScope
+ });
+
+ // 以用户的为准,因为可对用户进行个性化设置
+ foreach (var item in userPermissionList)
+ {
+ // 查询
+ var single = list
+ .Where(p => p.DataScope != item.DataScope)
+ .FirstOrDefault(p => p.ResourceKey == item.ResourceKey);
+ if (single == null)
+ {
+ list.Add(item);
+ continue;
+ }
+
+ single.DataScope = item.DataScope;
+ single.DataScopeCustom = item.DataScopeCustom;
+ }
+
+ // 去重
+ list = list
+ .GroupBy(p => p.ResourceKey)
+ .Select(p => p.First())
+ .ToList();
+
+ // 添加通用的数据范围
+ foreach (var item in dataScopeList)
+ {
+ list.Add(new DataPermission
+ {
+ DataScope = item.DataScope
+ });
+ }
+
+ return list;
+ }
+
+ // Key
+ var key = string.Format(CacheConstant.UserDataScopesKey, userId ?? UserId);
+ // 过期时间
+ var expireTime = TimeSpan.FromMinutes(30);
+ return _cacheManager.GetOrCreate(key, QueryFunc, expireTime) ?? new List();
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/Commons/DataPermissionServiceBase.cs b/BasicPlatform.AppService.FreeSql/Commons/DataPermissionServiceBase.cs
new file mode 100644
index 0000000000000000000000000000000000000000..7b34d9f801dc74da5cb5b7338fc9e4913dd7f375
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/Commons/DataPermissionServiceBase.cs
@@ -0,0 +1,181 @@
+namespace BasicPlatform.AppService.FreeSql.Commons;
+
+///
+/// 数据权限服务基类
+///
+///
+public class DataPermissionServiceBase : ServiceBase where T : FullEntityCore, new()
+{
+ private readonly ISecurityContextAccessor _accessor;
+ private readonly ICacheManager? _cacheManager;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public DataPermissionServiceBase(UnitOfWorkManager unitOfWorkManager, ISecurityContextAccessor accessor) :
+ base(unitOfWorkManager)
+ {
+ _accessor = accessor;
+ _cacheManager = ServiceLocator.Instance?.GetService(typeof(ICacheManager)) as ICacheManager;
+ }
+
+ #region 新增
+
+ ///
+ /// 新增
+ ///
+ ///
+ ///
+ protected override T RegisterNew(T entity)
+ {
+ entity.CreatedUserId = UserId;
+ entity.OrganizationalUnitIds = OrganizationalUnitIds;
+ return base.RegisterNew(entity);
+ }
+
+ ///
+ /// 新增
+ ///
+ ///
+ ///
+ ///
+ protected override Task RegisterNewAsync(T entity, CancellationToken cancellationToken = default)
+ {
+ entity.CreatedUserId = UserId;
+ entity.OrganizationalUnitIds = OrganizationalUnitIds;
+ return base.RegisterNewAsync(entity, cancellationToken);
+ }
+
+ ///
+ /// 批量新增
+ ///
+ ///
+ ///
+ protected override List RegisterNewRange(List entities)
+ {
+ foreach (var entity in entities)
+ {
+ entity.CreatedUserId = UserId;
+ entity.OrganizationalUnitIds = OrganizationalUnitIds;
+ }
+
+ return base.RegisterNewRange(entities);
+ }
+
+ ///
+ /// 批量新增
+ ///
+ ///
+ ///
+ ///
+ protected override Task> RegisterNewRangeAsync(List entities, CancellationToken cancellationToken)
+ {
+ foreach (var entity in entities)
+ {
+ entity.CreatedUserId = UserId;
+ entity.OrganizationalUnitIds = OrganizationalUnitIds;
+ }
+
+ return base.RegisterNewRangeAsync(entities, cancellationToken);
+ }
+
+ #endregion
+
+ ///
+ /// 用户ID
+ ///
+ protected string? UserId => _accessor.UserId;
+
+ ///
+ /// 是否为开发者帐号
+ ///
+ protected bool IsRoot => _accessor.IsRoot;
+
+ ///
+ /// 租户ID
+ ///
+ protected string? TenantId => _accessor.TenantId;
+
+ ///
+ /// IP地址
+ ///
+ protected string IpAddress => _accessor.IpAddress;
+
+ ///
+ /// 读取信息
+ ///
+ ///
+ ///
+ ///
+ protected async Task GetForEditAsync(string? id)
+ {
+ var entity = await Queryable
+ .Where(p => p.Id == id)
+ .ToOneAsync();
+
+ if (entity == null)
+ {
+ throw FriendlyException.Of("无权限操作或找不到数据。");
+ }
+
+ return entity;
+ }
+
+ ///
+ /// 创建人组织架构Ids
+ ///
+ private string? OrganizationalUnitIds
+ {
+ get
+ {
+ if (IsRoot)
+ {
+ return null;
+ }
+
+ var orgList = GetUserOrganizationIds();
+ return orgList.Count == 0 ? null : string.Join(",", orgList);
+ }
+ }
+
+ ///
+ /// 读取用户组织架构ID列表
+ ///
+ ///
+ private List GetUserOrganizationIds()
+ {
+ // Key
+ var key = string.Format(CacheConstant.UserOrganizationKey, UserId);
+ // 过期时间
+ var expireTime = TimeSpan.FromMinutes(30);
+
+ List QueryFunc()
+ {
+ // 兼任职信息表
+ var orgIds = QueryNoTracking()
+ .Where(p => p.UserId == UserId)
+ .ToList(p => p.OrganizationId);
+
+ // 用户组织
+ var orgId = QueryNoTracking()
+ .Where(p => p.Id == UserId)
+ .First(p => p.OrganizationId);
+
+ if (!string.IsNullOrEmpty(orgId))
+ {
+ orgIds.Add(orgId);
+ }
+
+ return orgIds;
+ }
+
+ if (_cacheManager == null)
+ {
+ return QueryFunc();
+ }
+
+ return _cacheManager.GetOrCreate(key, QueryFunc, expireTime) ?? new List();
+ }
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/DefaultDataPermissionService.cs b/BasicPlatform.AppService.FreeSql/DefaultDataPermissionService.cs
new file mode 100644
index 0000000000000000000000000000000000000000..ef9e0d12e5d432ede1059dc20030fabcc0f6d3ff
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/DefaultDataPermissionService.cs
@@ -0,0 +1,179 @@
+using Athena.Infrastructure.QueryFilters;
+using BasicPlatform.AppService.DataPermissions;
+
+namespace BasicPlatform.AppService.FreeSql;
+
+///
+/// 数据权限服务
+///
+[Component(LifeStyle.Singleton)]
+public class DefaultDataPermissionService : IDataPermissionService
+{
+ private readonly IFreeSql _freeSql;
+ private readonly ICacheManager? _cacheManager;
+
+ public DefaultDataPermissionService(IFreeSql freeSql)
+ {
+ _freeSql = freeSql;
+ _cacheManager = ServiceLocator.Instance?.GetService(typeof(ICacheManager)) as ICacheManager;
+ }
+
+ ///
+ /// 读取用户已有的策略查询过滤器组列表
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task> GetPolicyQueryFilterGroupsAsync(
+ string userId,
+ string resourceKey,
+ string? appId)
+ {
+ async Task> QueryFunc()
+ {
+ // 组装查询过滤器
+ var result = new List();
+
+ // 读取用户配置的策略
+ var userPolicies = await _freeSql.Select()
+ .Where(p => p.UserId == userId)
+ // 启用的
+ .Where(p => p.Enabled)
+ .Where(p => p.PolicyResourceKey == resourceKey)
+ // 未过期的
+ .Where(p => p.ExpireAt == null || p.ExpireAt > DateTime.Now)
+ .ToListAsync();
+
+ // 用户
+ foreach (var userPolicy in userPolicies)
+ {
+ result.AddRange(userPolicy.Policies);
+ }
+
+ var userPolicyResourceKeys = userPolicies
+ .Select(p => p.PolicyResourceKey)
+ .ToList();
+
+ // 读取用户角色配置的策略
+ var rolePolicies = await _freeSql.Select()
+ .Where(p => _freeSql.Select()
+ .As("c")
+ .Where(c => c.UserId == userId)
+ .ToList(c => c.RoleId)
+ .Contains(p.RoleId)
+ )
+ .Where(p => p.Enabled)
+ .Where(p => p.PolicyResourceKey == resourceKey)
+ // 如果已经在用户策略中配置了,则不再读取角色策略
+ .WhereIf(userPolicyResourceKeys.Count > 0,
+ p => !userPolicyResourceKeys.Contains(p.PolicyResourceKey)
+ )
+ .ToListAsync();
+ // 角色
+ foreach (var rolePolicy in rolePolicies)
+ {
+ result.AddRange(rolePolicy.Policies);
+ }
+
+ return result;
+ }
+
+ if (_cacheManager == null)
+ {
+ return await QueryFunc();
+ }
+
+ // Key
+ var key = string.Format(CacheConstant.UserPolicyFilterGroupQuery, userId, resourceKey);
+ // 过期时间
+ var expireTime = TimeSpan.FromMinutes(30);
+
+ return await _cacheManager.GetOrCreateAsync(key, QueryFunc, expireTime) ?? new List();
+ }
+
+ ///
+ /// 获取用户所在组织/部门列表
+ ///
+ ///
+ ///
+ ///
+ public async Task> GetUserOrganizationIdsAsync(string userId, string? appId)
+ {
+ async Task> QueryFunc()
+ {
+ // 兼任职信息表
+ var orgIds = await _freeSql.Select()
+ .Where(p => p.UserId == userId)
+ .ToListAsync(p => p.OrganizationId);
+
+ // 用户组织
+ var orgId = _freeSql.Select()
+ .Where(p => p.Id == userId)
+ .First(p => p.OrganizationId);
+
+ if (!string.IsNullOrEmpty(orgId))
+ {
+ orgIds.Add(orgId);
+ }
+
+ return orgIds;
+ }
+
+ if (_cacheManager == null)
+ {
+ return await QueryFunc();
+ }
+
+ // Key
+ var key = string.Format(CacheConstant.UserOrganizationKey, userId);
+ // 过期时间
+ var expireTime = TimeSpan.FromMinutes(30);
+
+ return await _cacheManager.GetOrCreateAsync(key, QueryFunc, expireTime) ?? new List();
+ }
+
+ ///
+ /// 获取用户所有组织/部门及下级组织/部门列表
+ ///
+ ///
+ ///
+ ///
+ public async Task> GetUserOrganizationIdsTreeAsync(string userId, string? appId)
+ {
+ async Task> QueryFunc()
+ {
+ // 查询用户所在的组织
+ var list = await GetUserOrganizationIdsAsync(userId, appId);
+
+ if (list.Count == 0)
+ {
+ return list;
+ }
+
+ var filters = list.Select(p => new QueryFilter
+ {Key = "ParentPath", Operator = "contains", Value = p, XOR = "or"}).ToList();
+ // 生成查询条件
+ var filterWhere = QueryableExtensions.MakeFilterWhere(filters, false);
+ // 查询用户组织架构的下级组织
+ var orgIds = _freeSql.Select()
+ .Where(filterWhere)
+ .ToList(p => p.Id);
+ list.AddRange(orgIds);
+
+ // 数据去重
+ return list.GroupBy(p => p).Select(p => p.Key).ToList();
+ }
+
+ if (_cacheManager == null)
+ {
+ return await QueryFunc();
+ }
+
+ // Key
+ var key = string.Format(CacheConstant.UserOrganizationsKey, userId);
+ // 过期时间
+ var expireTime = TimeSpan.FromMinutes(30);
+ return await _cacheManager.GetOrCreateAsync(key, QueryFunc, expireTime) ?? new List();
+ }
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/ExternalPages/ExternalPageQueryService.cs b/BasicPlatform.AppService.FreeSql/ExternalPages/ExternalPageQueryService.cs
new file mode 100644
index 0000000000000000000000000000000000000000..64b97f2c9bc597c56af165023c23b14543251e74
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/ExternalPages/ExternalPageQueryService.cs
@@ -0,0 +1,131 @@
+using BasicPlatform.AppService.ExternalPages;
+using BasicPlatform.AppService.ExternalPages.Requests;
+using BasicPlatform.AppService.ExternalPages.Responses;
+
+namespace BasicPlatform.AppService.FreeSql.ExternalPages;
+
+///
+///
+///
+[Component(LifeStyle.Transient)]
+public class ExternalPageQueryService : AppQueryServiceBase, IExternalPageQueryService
+{
+ public ExternalPageQueryService(IFreeSql freeSql, ISecurityContextAccessor accessor) : base(freeSql, accessor)
+ {
+ }
+
+ #region 查询
+
+ ///
+ /// 读取分页列表
+ ///
+ ///
+ ///
+ public async Task> GetPagingAsync(
+ GetExternalPagePagingRequest request)
+ {
+ var result = await QueryableNoTracking
+ .HasWhere(request.Keyword, p => p.Name.Contains(request.Keyword!))
+ .HasWhere(request.ParentId, p => p.ParentId == request.ParentId)
+ .HasWhere(!IsRoot, p => p.OwnerId == UserId || string.IsNullOrEmpty(p.OwnerId))
+ .ToPagingAsync(request, p => new GetExternalPagePagingResponse
+ {
+ CreatedUserName = p.CreatedUser!.RealName,
+ UpdatedUserName = p.UpdatedUser!.RealName,
+ });
+
+ return result;
+ }
+
+ ///
+ /// 读取信息
+ ///
+ ///
+ ///
+ public async Task GetAsync(string id)
+ {
+ var result = await QueryableNoTracking
+ .Where(p => p.Id == id)
+ .HasWhere(!IsRoot, p => p.OwnerId == UserId || string.IsNullOrEmpty(p.OwnerId))
+ .ToOneAsync();
+
+ // 如果为一级页面
+ if (string.IsNullOrEmpty(result.ParentId))
+ {
+ // 如果有子页面,则为分组
+ result.IsGroup = await QueryableNoTracking
+ .AnyAsync(p => p.ParentId == result.Id);
+ }
+
+ return result;
+ }
+
+ ///
+ /// 读取选择框数据列表
+ ///
+ ///
+ public async Task> GetSelectListAsync()
+ {
+ var result = await QueryableNoTracking
+ .Where(p => string.IsNullOrEmpty(p.ParentId))
+ .HasWhere(!IsRoot, p => p.OwnerId == UserId)
+ .ToListAsync(p => new SelectViewModel
+ {
+ Label = p.Name,
+ Value = p.Id
+ });
+ return result;
+ }
+
+ ///
+ /// 读取树形数据列表
+ ///
+ ///
+ public async Task> GetTreeListAsync()
+ {
+ var list = await QueryableNoTracking
+ .HasWhere(!IsRoot, p => p.OwnerId == UserId || string.IsNullOrEmpty(p.OwnerId))
+ .ToListAsync();
+ var result = new List();
+ // 递归读取
+ GetTreeChildren(list, result);
+ return result;
+ }
+
+ #region 私有方法
+
+ ///
+ /// 递归读取
+ ///
+ ///
+ ///
+ ///
+ private static void GetTreeChildren(IList entities,
+ ICollection results,
+ string? parentId = null)
+ {
+ IList result = string.IsNullOrEmpty(parentId)
+ ? entities.Where(p => string.IsNullOrEmpty(p.ParentId)).ToList()
+ : entities.Where(p => p.ParentId == parentId).ToList();
+
+ foreach (var t1 in result.OrderBy(p => p.Sort))
+ {
+ var res = new TreeViewModel
+ {
+ Title = t1.Name,
+ Key = t1.Id
+ };
+ if (entities.Any(p => p.ParentId == t1.Id))
+ {
+ res.Children = new List();
+ GetTreeChildren(entities, res.Children, t1.Id);
+ }
+
+ results.Add(res);
+ }
+ }
+
+ #endregion
+
+ #endregion
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/ExternalPages/ExternalPageRequestHandler.cs b/BasicPlatform.AppService.FreeSql/ExternalPages/ExternalPageRequestHandler.cs
new file mode 100644
index 0000000000000000000000000000000000000000..3c9df89b6225d668e38f135a51d8cf8408675293
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/ExternalPages/ExternalPageRequestHandler.cs
@@ -0,0 +1,153 @@
+using BasicPlatform.AppService.ExternalPages.Requests;
+
+namespace BasicPlatform.AppService.FreeSql.ExternalPages;
+
+///
+/// 组织架构请求处理程序
+///
+public class ExternalPageRequestHandler : AppServiceBase,
+ IRequestHandler,
+ IRequestHandler,
+ IRequestHandler
+{
+ public ExternalPageRequestHandler(
+ UnitOfWorkManager unitOfWorkManager,
+ ISecurityContextAccessor accessor) : base(unitOfWorkManager, accessor)
+ {
+ }
+
+ ///
+ /// 创建组织架构
+ ///
+ ///
+ ///
+ ///
+ public async Task Handle(CreateExternalPageRequest request, CancellationToken cancellationToken)
+ {
+ // 检查跳转地址是否重复
+ bool exists;
+ if (request.IsPublic)
+ {
+ exists = await QueryableNoTracking
+ .Where(x => x.Path == request.Path && string.IsNullOrEmpty(x.OwnerId))
+ .AnyAsync(cancellationToken);
+ }
+ else
+ {
+ exists = await QueryableNoTracking
+ .Where(x => x.Path == request.Path && x.OwnerId != UserId)
+ .AnyAsync(cancellationToken);
+ }
+
+ if (exists)
+ {
+ throw FriendlyException.Of("跳转地址已存在");
+ }
+
+ var ownerId = IsRoot && request.IsPublic ? null : UserId;
+ var entity = new ExternalPage(
+ request.ParentId,
+ ownerId,
+ request.Name,
+ request.Type,
+ request.Path,
+ request.Icon,
+ request.Layout,
+ request.Sort,
+ request.Remarks,
+ UserId
+ );
+
+ await RegisterNewAsync(entity, cancellationToken);
+ return entity.Id;
+ }
+
+ ///
+ /// 更新
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task Handle(UpdateExternalPageRequest request, CancellationToken cancellationToken)
+ {
+ if (request.ParentId == request.Id)
+ {
+ throw FriendlyException.Of("不能选择自己作为上级");
+ }
+
+ // 检查跳转地址是否重复
+ bool exists;
+ if (request.IsPublic)
+ {
+ exists = await QueryableNoTracking
+ .Where(p => p.Id != request.Id && p.Path == request.Path && string.IsNullOrEmpty(p.OwnerId))
+ .AnyAsync(cancellationToken);
+ }
+ else
+ {
+ exists = await QueryableNoTracking
+ .Where(p => p.Id != request.Id && p.Path == request.Path && p.OwnerId != UserId)
+ .AnyAsync(cancellationToken);
+ }
+
+ if (exists)
+ {
+ throw FriendlyException.Of("跳转地址已存在");
+ }
+
+ // 封装实体对象
+ var entity = await GetForUpdateAsync(request.Id, cancellationToken);
+ var ownerId = IsRoot && request.IsPublic ? null : UserId;
+ // 更新
+ entity.Update(
+ request.ParentId,
+ ownerId,
+ request.Name,
+ request.Type,
+ request.Path,
+ request.Icon,
+ request.Layout,
+ request.Sort,
+ request.Remarks,
+ UserId
+ );
+ await RegisterDirtyAsync(entity, cancellationToken);
+ return entity.Id;
+ }
+
+ ///
+ /// 删除
+ ///
+ ///
+ ///
+ ///
+ public async Task Handle(DeleteExternalPageRequest request, CancellationToken cancellationToken)
+ {
+ // 检查是否有子页面
+ var hasChild = await QueryableNoTracking
+ .Where(p => p.ParentId == request.Id)
+ .AnyAsync(cancellationToken);
+ if (hasChild)
+ {
+ throw FriendlyException.Of("请先删除子页面");
+ }
+
+ if (!IsRoot)
+ {
+ var flag = await QueryableNoTracking
+ .Where(p => p.OwnerId == UserId)
+ .Where(p => p.Id == request.Id)
+ .AnyAsync(cancellationToken);
+
+ if (!flag)
+ {
+ throw FriendlyException.Of("没有权限");
+ }
+ }
+
+ // 删除
+ await RegisterDeleteAsync(p => p.Id == request.Id, cancellationToken);
+ return request.Id;
+ }
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/Organizations/OrganizationQueryService.cs b/BasicPlatform.AppService.FreeSql/Organizations/OrganizationQueryService.cs
new file mode 100644
index 0000000000000000000000000000000000000000..eb3b0e27a89c88d5aaeba02fcfa7761d0ca2db51
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/Organizations/OrganizationQueryService.cs
@@ -0,0 +1,285 @@
+using BasicPlatform.AppService.Organizations;
+using BasicPlatform.AppService.Organizations.Requests;
+using BasicPlatform.AppService.Organizations.Responses;
+
+namespace BasicPlatform.AppService.FreeSql.Organizations;
+
+///
+///
+///
+[Component(LifeStyle.Transient)]
+public class OrganizationQueryService : AppQueryServiceBase, IOrganizationQueryService
+{
+ public OrganizationQueryService(IFreeSql freeSql, ISecurityContextAccessor accessor) : base(freeSql, accessor)
+ {
+ }
+
+ #region 查询
+
+ ///
+ /// 读取分页列表
+ ///
+ ///
+ ///
+ public async Task> GetPagingAsync(
+ GetOrganizationPagingRequest request)
+ {
+ ISelect? organizationQuery = null;
+ if (request.ParentId != null)
+ {
+ organizationQuery = QueryNoTracking()
+ .As("o")
+ // 当前组织架构及下级组织架构
+ .Where(p => p.ParentPath.Contains(request.ParentId!) || p.Id == request.ParentId);
+ }
+
+ var result = await QueryableNoTracking
+ .HasWhere(request.Keyword, p => p.Name.Contains(request.Keyword!))
+ .HasWhere(organizationQuery, p => organizationQuery!.Any(o => o.Id == p.ParentId))
+ .HasWhere(request.Status, p => request.Status!.Contains(p.Status))
+ .ToPagingAsync(request, p => new GetOrganizationPagingResponse
+ {
+ CreatedUserName = p.CreatedUser!.RealName,
+ UpdatedUserName = p.UpdatedUser!.RealName,
+ LeaderName = p.Leader!.RealName
+ });
+
+ return result;
+ }
+
+ ///
+ /// 读取信息
+ ///
+ ///
+ ///
+ public async Task GetAsync(string id)
+ {
+ var result = await QueryableNoTracking
+ .Where(p => p.Id == id)
+ .ToOneAsync(p => new GetOrganizationByIdResponse
+ {
+ LeaderName = p.Leader!.RealName
+ });
+
+ // 组织架构角色
+ var roleIds = await Query()
+ .Where(p => p.OrganizationId == id)
+ .ToListAsync(p => p.RoleId);
+
+ result?.RoleIds.AddRange(roleIds);
+
+ return result;
+ }
+
+ ///
+ /// 读取树形数据列表
+ ///
+ ///
+ public async Task> GetTreeListAsync()
+ {
+ var list = await QueryableNoTracking.ToListAsync();
+ var result = new List();
+ var parentId = list.MinBy(p => p.ParentPath.Length)?.ParentId;
+ // 递归读取
+ GetTreeChildren(list, result, parentId);
+ return result;
+ }
+
+ ///
+ /// 读取树形选择框数据列表
+ ///
+ ///
+ public async Task> GetTreeSelectListAsync()
+ {
+ var list = await QueryableNoTracking.ToListAsync();
+ var result = new List();
+ // 递归读取
+ GetTreeChildrenBySelect(list, result);
+ return result;
+ }
+
+ ///
+ /// 获取全部组织架构人员树
+ ///
+ ///
+ // ReSharper disable once IdentifierTypo
+ public async Task> GetCascaderListAsync()
+ {
+ var list = await QueryableNoTracking.ToListAsync();
+ var result = new List();
+ var parentId = list.MinBy(p => p.ParentPath.Length)?.ParentId;
+ // 递归读取
+ GetTreeChildrenCascader(list, result, parentId);
+ return result;
+ }
+
+ ///
+ /// 读取选择框数据列表
+ ///
+ ///
+ public async Task> GetSelectListAsync(string? parentId)
+ {
+ ISelect? organizationQuery = null;
+ if (parentId != null)
+ {
+ organizationQuery = QueryNoTracking()
+ .As("o")
+ // 当前组织架构及下级组织架构
+ .Where(p => p.ParentPath.Contains(parentId) || p.Id == parentId);
+ }
+
+ var result = await Queryable
+ .Where(p => p.Status == Status.Enabled)
+ .HasWhere(organizationQuery, p => organizationQuery!.Any(o => o.Id == p.ParentId))
+ .ToListAsync(t1 => new SelectViewModel
+ {
+ Label = t1.Name,
+ Value = t1.Id,
+ Disabled = t1.Status == Status.Disabled
+ });
+
+ return result;
+ }
+
+ #region 私有方法
+
+ ///
+ /// 递归读取
+ ///
+ ///
+ ///
+ ///
+ // ReSharper disable once IdentifierTypo
+ private static void GetTreeChildrenCascader(IList entities,
+ ICollection results,
+ string? parentId = null)
+ {
+ IList result = string.IsNullOrEmpty(parentId)
+ ? entities.Where(p => string.IsNullOrEmpty(p.ParentId)).ToList()
+ : entities.Where(p => p.ParentId == parentId).ToList();
+ foreach (var t1 in result)
+ {
+ var res = new CascaderViewModel
+ {
+ Label = t1.Name,
+ Value = t1.Id,
+ Disabled = t1.Status == Status.Disabled,
+ };
+ if (entities.Any(p => p.ParentId == t1.Id))
+ {
+ res.Children = new List();
+ GetTreeChildrenCascader(entities, res.Children, t1.Id);
+ }
+
+ results.Add(res);
+ }
+ }
+
+ ///
+ /// 递归读取
+ ///
+ ///
+ ///
+ ///
+ private static void GetTreeChildrenBySelect(IList entities,
+ ICollection results,
+ string? parentId = null)
+ {
+ IList result = string.IsNullOrEmpty(parentId)
+ ? entities.Where(p => string.IsNullOrEmpty(p.ParentId)).ToList()
+ : entities.Where(p => p.ParentId == parentId).ToList();
+
+ foreach (var t1 in result)
+ {
+ var res = new TreeSelectViewModel
+ {
+ Id = t1.Id,
+ ParentId = t1.ParentId,
+ Title = t1.Name,
+ Value = t1.Id,
+ Disabled = t1.Status == Status.Disabled,
+ IsLeaf = !string.IsNullOrEmpty(t1.ParentId),
+ Children = new List()
+ };
+ if (entities.Any(p => p.ParentId == t1.Id))
+ {
+ GetTreeChildrenBySelect(entities, res.Children, t1.Id);
+ }
+
+ results.Add(res);
+ }
+ }
+
+ ///
+ /// 递归读取
+ ///
+ ///
+ ///
+ ///
+ private static void GetTreeChildren(IList entities,
+ ICollection results,
+ string? parentId = null)
+ {
+ IList result = string.IsNullOrEmpty(parentId)
+ ? entities.Where(p => string.IsNullOrEmpty(p.ParentId)).ToList()
+ : entities.Where(p => p.ParentId == parentId).ToList();
+
+ foreach (var t1 in result.OrderBy(p => p.Sort))
+ {
+ var res = new TreeViewModel
+ {
+ Title = t1.Name,
+ Key = t1.Id,
+ Disabled = t1.Status == Status.Disabled
+ };
+ if (entities.Any(p => p.ParentId == t1.Id))
+ {
+ res.Children = new List();
+ GetTreeChildren(entities, res.Children, t1.Id);
+ }
+
+ results.Add(res);
+ }
+ }
+
+ ///
+ /// 递归读取
+ ///
+ ///
+ ///
+ ///
+ private static void GetTreeChildren(IList entities,
+ ICollection results,
+ string? parentId = null)
+ {
+ IList result = string.IsNullOrEmpty(parentId)
+ ? entities.Where(p => string.IsNullOrEmpty(p.ParentId)).ToList()
+ : entities.Where(p => p.ParentId == parentId).ToList();
+
+ foreach (var item in result)
+ {
+ var res = new GetOrganizationTreeDataResponse
+ {
+ Id = item.Id,
+ ParentId = item.ParentId,
+ Name = item.Name,
+ Remarks = item.Remarks,
+ CreatedOn = item.CreatedOn,
+ UpdatedOn = item.UpdatedOn,
+ Status = item.Status,
+ CreatedUserName = item.CreatedUserName
+ };
+ if (entities.Any(p => p.ParentId == item.Id))
+ {
+ res.Children = new List();
+ GetTreeChildren(entities, res.Children, item.Id!);
+ }
+
+ results.Add(res);
+ }
+ }
+
+ #endregion
+
+ #endregion
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/Organizations/OrganizationRequestHandler.cs b/BasicPlatform.AppService.FreeSql/Organizations/OrganizationRequestHandler.cs
new file mode 100644
index 0000000000000000000000000000000000000000..0ade3e0007f5bddfa738d9c73d09b33827d94472
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/Organizations/OrganizationRequestHandler.cs
@@ -0,0 +1,140 @@
+using BasicPlatform.AppService.Organizations.Requests;
+
+namespace BasicPlatform.AppService.FreeSql.Organizations;
+
+///
+/// 组织架构请求处理程序
+///
+public class OrganizationRequestHandler : AppServiceBase,
+ IRequestHandler,
+ IRequestHandler,
+ IRequestHandler
+{
+ public OrganizationRequestHandler(
+ UnitOfWorkManager unitOfWorkManager,
+ ISecurityContextAccessor accessor) : base(unitOfWorkManager, accessor)
+ {
+ }
+
+ ///
+ /// 创建组织架构
+ ///
+ ///
+ ///
+ ///
+ public async Task Handle(CreateOrganizationRequest request, CancellationToken cancellationToken)
+ {
+ var entity = new Organization(
+ request.ParentId,
+ request.Name,
+ request.LeaderId,
+ request.Remarks,
+ request.Status,
+ request.Sort,
+ UserId
+ );
+
+ await RegisterNewAsync(entity, cancellationToken);
+
+ // 新增关联数据
+ if (request.RoleIds.Count <= 0)
+ {
+ return entity.Id;
+ }
+
+ var organizationRoles = request
+ .RoleIds
+ .Select(roleId =>
+ new OrganizationRole(entity.Id, roleId)
+ ).ToList();
+ // 添加关联数据
+ await RegisterNewRangeValueObjectAsync(organizationRoles, cancellationToken);
+ return entity.Id;
+ }
+
+ ///
+ /// 更新
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task Handle(UpdateOrganizationRequest request, CancellationToken cancellationToken)
+ {
+ if (request.ParentId == request.Id)
+ {
+ throw FriendlyException.Of("不能选择自己作为上级");
+ }
+
+ // 封装实体对象
+ var entity = await GetForUpdateAsync(request.Id, cancellationToken);
+
+ if (!string.IsNullOrWhiteSpace(request.ParentId))
+ {
+ entity.ParentPath = await GetParentPathAsync(request.ParentId);
+ }
+
+ // 更新
+ entity.Update(request.ParentId, request.Name, request.LeaderId, request.Remarks, request.Sort, UserId);
+ await RegisterDirtyAsync(entity, cancellationToken);
+ // 删除旧数据
+ await RegisterDeleteValueObjectAsync(p => p.OrganizationId == entity.Id, cancellationToken);
+ // 新增关联数据
+ if (request.RoleIds.Count <= 0)
+ {
+ return entity.Id;
+ }
+
+ var organizationRoles = request
+ .RoleIds
+ .Select(roleId =>
+ new OrganizationRole(entity.Id, roleId)
+ ).ToList();
+ // 新增关联数据
+ await RegisterNewRangeValueObjectAsync(organizationRoles, cancellationToken);
+
+ return entity.Id;
+ }
+
+ ///
+ /// 变更状态
+ ///
+ ///
+ ///
+ ///
+ public async Task Handle(OrganizationStatusChangeRequest request, CancellationToken cancellationToken)
+ {
+ var entity = await GetForUpdateAsync(request.Id, cancellationToken);
+ // 变更状态
+ entity.StatusChange(UserId);
+ await RegisterDirtyAsync(entity, cancellationToken);
+ return entity.Id;
+ }
+
+
+ #region 私有方法
+
+ ///
+ /// 读取ParentPath
+ ///
+ ///
+ ///
+ private async Task GetParentPathAsync(string parentId)
+ {
+ // 读取上级信息
+ var parent = await Queryable
+ .Where(p => p.Id == parentId)
+ .ToOneAsync();
+
+ if (parent == null)
+ {
+ throw FriendlyException.Of("找不到上级组织架构");
+ }
+
+ return string.IsNullOrEmpty(parent.ParentPath)
+ ? parentId
+ : $"{parent.ParentPath},{parentId}";
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/BasicPlatform.AppService.FreeSql/PolicyQueryFilterService.cs b/BasicPlatform.AppService.FreeSql/PolicyQueryFilterService.cs
new file mode 100644
index 0000000000000000000000000000000000000000..110445236d7fad86905615d2d7f227fae6be76b2
--- /dev/null
+++ b/BasicPlatform.AppService.FreeSql/PolicyQueryFilterService.cs
@@ -0,0 +1,287 @@
+using Athena.Infrastructure.QueryFilters;
+using BasicPlatform.AppService.DataPermissions;
+using Microsoft.Extensions.Logging;
+
+namespace BasicPlatform.AppService.FreeSql;
+
+///
+/// 策略查询过滤器服务实现类
+///
+[Component(LifeStyle.Singleton)]
+public class PolicyQueryFilterService : IQueryFilterService
+{
+ private readonly ICacheManager _cacheManager;
+ private readonly IFreeSql _freeSql;
+ private readonly ILogger _logger;
+ private readonly DataPermissionFactory _dataPermissionFactory;
+ private readonly IDataPermissionService _dataPermissionService;
+
+ public PolicyQueryFilterService(
+ ICacheManager cacheManager, IFreeSql freeSql,
+ ILoggerFactory loggerFactory,
+ IEnumerable dataPermissions,
+ IDataPermissionService dataPermissionService)
+ {
+ _cacheManager = cacheManager;
+ _freeSql = freeSql;
+ _dataPermissionService = dataPermissionService;
+ _logger = loggerFactory.CreateLogger();
+ _dataPermissionFactory = new DataPermissionFactory(dataPermissions);
+ }
+
+ ///
+ /// 读取查询过滤器
+ ///
+ ///
+ ///
+ ///
+ public async Task?> GetAsync(string userId, Type type)
+ {
+ var resourceKey = type.Name;
+ string? appId = null;
+ // 缓存Key
+ var cacheKey = string.Format(CacheConstant.UserPolicyQueryKey, userId, resourceKey);
+ // 缓存过期时间
+ var expireTime = TimeSpan.FromMinutes(30);
+ // 读取缓存,如果缓存中存在,则直接返回,否则从数据库中读取,并写入缓存
+ var result = await _cacheManager.GetOrCreateAsync(cacheKey, async () =>
+ {
+ var result = await _dataPermissionService.GetPolicyQueryFilterGroupsAsync(userId, resourceKey, appId);
+ if (result.Count == 0)
+ {
+ return null;
+ }
+
+ // 基础权限列表
+ var basicList = new List
+ {
+ // 当前登录用户
+ "{SelfUserId}",
+ // 当前登录用户所在组织
+ "{SelfOrganizationId}",
+ // 当前登录用户所在组织的下级组织
+ "{SelfOrganizationChildrenIds}",
+ };
+ var hasSelfOrganizationId = false;
+ var hasSelfOrganizationChildrenIds = false;
+ var extraSqlList = new List();
+
+ // 处理占位符
+ foreach (var group in result)
+ {
+ foreach (var filter in group.Filters)
+ {
+ // 如果包含基础的占位符
+ if (basicList.Any(key => key == filter.Value))
+ {
+ switch (filter.Value)
+ {
+ // 当前登录人
+ case "{SelfUserId}":
+ filter.Value = userId;
+ break;
+ // 当前登录人部门
+ case "{SelfOrganizationId}":
+ hasSelfOrganizationId = true;
+ break;
+ // 当前登录人部门及下级部门
+ case "{SelfOrganizationChildrenIds}":
+ hasSelfOrganizationChildrenIds = true;
+ break;
+ }
+
+ continue;
+ }
+
+ // 动态查询条件
+ var dataPermission = _dataPermissionFactory.GetInstances()
+ .Where(p => p.Key == filter.Key)
+ .FirstOrDefault(p => p.Value == filter.Value);
+ if (dataPermission == null)
+ {
+ continue;
+ }
+
+ // 获取查询的SQL
+ extraSqlList.Add(dataPermission.GetSqlString());
+ }
+ }
+
+ // 有本部门查询条件
+ IList? selfOrganizationIds = null;
+ // 有本部门及下级部门查询条件
+ IList? selfOrganizationChildrenIds = null;
+ // 读取基本权限
+ if (hasSelfOrganizationId)
+ {
+ selfOrganizationIds = await _dataPermissionService.GetUserOrganizationIdsAsync(userId, appId);
+ }
+
+ if (hasSelfOrganizationChildrenIds)
+ {
+ selfOrganizationChildrenIds =
+ await _dataPermissionService.GetUserOrganizationIdsTreeAsync(userId, appId);
+ }
+
+ List<(string Id, string MapKey)>? dynamicList = null;
+ if (extraSqlList.Count > 0)
+ {
+ // 构建查询表达式
+ var query = _freeSql.Queryable