diff --git a/Directory.Build.props b/Directory.Build.props
index 0cdea9d..c42ffd7 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,5 +1,7 @@
5.1.4
+ 2.0.593
+ 6.0.*
\ No newline at end of file
diff --git a/Sanhe.Abp.Framework.sln b/Sanhe.Abp.Framework.sln
index 830e651..00d2952 100644
--- a/Sanhe.Abp.Framework.sln
+++ b/Sanhe.Abp.Framework.sln
@@ -25,6 +25,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.ExceptionHandling
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.ExceptionHandling.Emailing", "modules\common\Sanhe.Abp.ExceptionHandling.Emailing\Sanhe.Abp.ExceptionHandling.Emailing.csproj", "{0692C2EA-7119-4065-8504-D7FDA70135A9}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.Features.LimitValidation", "modules\common\Sanhe.Abp.Features.LimitValidation\Sanhe.Abp.Features.LimitValidation.csproj", "{BE246DD7-2DDB-4064-9017-7E1DD2E67195}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.Features.LimitValidation.Redis", "modules\common\Sanhe.Abp.Features.LimitValidation.Redis\Sanhe.Abp.Features.LimitValidation.Redis.csproj", "{A2BD1C66-505E-48BB-A356-38D74AA3AE0F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.Features.LimitValidation.Redis.Client", "modules\common\Sanhe.Abp.Features.LimitValidation.Redis.Client\Sanhe.Abp.Features.LimitValidation.Redis.Client.csproj", "{97DDB479-946C-489D-9089-6EAEBFEE97C6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -51,6 +57,18 @@ Global
{0692C2EA-7119-4065-8504-D7FDA70135A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0692C2EA-7119-4065-8504-D7FDA70135A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0692C2EA-7119-4065-8504-D7FDA70135A9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BE246DD7-2DDB-4064-9017-7E1DD2E67195}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BE246DD7-2DDB-4064-9017-7E1DD2E67195}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BE246DD7-2DDB-4064-9017-7E1DD2E67195}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BE246DD7-2DDB-4064-9017-7E1DD2E67195}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A2BD1C66-505E-48BB-A356-38D74AA3AE0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2BD1C66-505E-48BB-A356-38D74AA3AE0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2BD1C66-505E-48BB-A356-38D74AA3AE0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2BD1C66-505E-48BB-A356-38D74AA3AE0F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {97DDB479-946C-489D-9089-6EAEBFEE97C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {97DDB479-946C-489D-9089-6EAEBFEE97C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {97DDB479-946C-489D-9089-6EAEBFEE97C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {97DDB479-946C-489D-9089-6EAEBFEE97C6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -65,6 +83,9 @@ Global
{64178F61-A488-4182-A409-C32AE51E33A1} = {928FDD8C-1EE8-455E-952F-11039B52FE03}
{FBEB7703-CF8A-4E5B-B1C7-ED9DC1ABC7BD} = {2A768109-31B7-4C52-928C-3023DAB9F254}
{0692C2EA-7119-4065-8504-D7FDA70135A9} = {2A768109-31B7-4C52-928C-3023DAB9F254}
+ {BE246DD7-2DDB-4064-9017-7E1DD2E67195} = {2A768109-31B7-4C52-928C-3023DAB9F254}
+ {A2BD1C66-505E-48BB-A356-38D74AA3AE0F} = {2A768109-31B7-4C52-928C-3023DAB9F254}
+ {97DDB479-946C-489D-9089-6EAEBFEE97C6} = {2A768109-31B7-4C52-928C-3023DAB9F254}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB69BFDE-9DDB-4D16-8CB8-72472C0319CD}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/FodyWeavers.xml b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/FodyWeavers.xml
new file mode 100644
index 0000000..1715698
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe.Abp.Features.LimitValidation.Redis.Client.csproj b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe.Abp.Features.LimitValidation.Redis.Client.csproj
new file mode 100644
index 0000000..0c7c64d
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe.Abp.Features.LimitValidation.Redis.Client.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/AbpFeaturesValidationRedisClientModule.cs b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/AbpFeaturesValidationRedisClientModule.cs
new file mode 100644
index 0000000..7ac8014
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/AbpFeaturesValidationRedisClientModule.cs
@@ -0,0 +1,9 @@
+using Volo.Abp.Modularity;
+
+namespace Sanhe.Abp.Features.LimitValidation.Redis.Client;
+
+[DependsOn(typeof(AbpFeaturesValidationRedisModule))]
+public class AbpFeaturesValidationRedisClientModule : AbpModule
+{
+
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/RedisClientLimitFeatureNamingNormalizer.cs b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/RedisClientLimitFeatureNamingNormalizer.cs
new file mode 100644
index 0000000..bcb1210
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis.Client/Sanhe/Abp/Features/LimitValidation/Redis/Client/RedisClientLimitFeatureNamingNormalizer.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Clients;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+
+namespace Sanhe.Abp.Features.LimitValidation.Redis.Client;
+
+[Dependency(ServiceLifetime.Singleton, ReplaceServices = true)]
+[ExposeServices(
+ typeof(IRedisLimitFeatureNamingNormalizer),
+ typeof(RedisLimitFeatureNamingNormalizer))]
+public class RedisClientLimitFeatureNamingNormalizer : RedisLimitFeatureNamingNormalizer
+{
+ protected ICurrentClient CurrentClient { get; }
+
+ public RedisClientLimitFeatureNamingNormalizer(
+ ICurrentClient currentClient,
+ ICurrentTenant currentTenant) : base(currentTenant)
+ {
+ CurrentClient = currentClient;
+ }
+
+ public override string NormalizeFeatureName(string instance, RequiresLimitFeatureContext context)
+ {
+ if (CurrentClient.IsAuthenticated)
+ {
+ return CurrentTenant.IsAvailable
+ ? $"{instance}t:RequiresLimitFeature;t:{CurrentTenant.Id};c:{CurrentClient.Id};f:{context.LimitFeature}"
+ : $"{instance}tc:RequiresLimitFeature;c:{CurrentClient.Id};f:{context.LimitFeature}";
+ }
+ return base.NormalizeFeatureName(instance, context);
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/FodyWeavers.xml b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/FodyWeavers.xml
new file mode 100644
index 0000000..1715698
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe.Abp.Features.LimitValidation.Redis.csproj b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe.Abp.Features.LimitValidation.Redis.csproj
new file mode 100644
index 0000000..16c520c
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe.Abp.Features.LimitValidation.Redis.csproj
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpFeaturesValidationRedisModule.cs b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpFeaturesValidationRedisModule.cs
new file mode 100644
index 0000000..31bf600
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpFeaturesValidationRedisModule.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace Sanhe.Abp.Features.LimitValidation.Redis;
+
+[DependsOn(
+ typeof(AbpFeaturesLimitValidationModule))]
+public class AbpFeaturesValidationRedisModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ var configuration = context.Services.GetConfiguration();
+ Configure(configuration.GetSection("Features:Validation:Redis"));
+
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ context.Services.Replace(ServiceDescriptor.Singleton());
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpRedisRequiresLimitFeatureOptions.cs b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpRedisRequiresLimitFeatureOptions.cs
new file mode 100644
index 0000000..7ab5351
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/AbpRedisRequiresLimitFeatureOptions.cs
@@ -0,0 +1,22 @@
+using Microsoft.Extensions.Options;
+using StackExchange.Redis;
+
+namespace Sanhe.Abp.Features.LimitValidation.Redis;
+
+public class AbpRedisRequiresLimitFeatureOptions : IOptions
+{
+ ///
+ /// Redis连接字符串
+ ///
+ public string Configuration { get; set; }
+ ///
+ /// 前缀
+ ///
+ public string InstanceName { get; set; }
+ ///
+ /// Redis连接配置
+ ///
+ public ConfigurationOptions ConfigurationOptions { get; set; }
+
+ AbpRedisRequiresLimitFeatureOptions IOptions.Value => this;
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/IRedisLimitFeatureNamingNormalizer.cs b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/IRedisLimitFeatureNamingNormalizer.cs
new file mode 100644
index 0000000..0b27a56
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/IRedisLimitFeatureNamingNormalizer.cs
@@ -0,0 +1,6 @@
+namespace Sanhe.Abp.Features.LimitValidation.Redis;
+
+public interface IRedisLimitFeatureNamingNormalizer
+{
+ string NormalizeFeatureName(string instance, RequiresLimitFeatureContext context);
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/Lua/check.lua b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/Lua/check.lua
new file mode 100644
index 0000000..d0ade3b
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/Lua/check.lua
@@ -0,0 +1,4 @@
+if (redis.call('EXISTS', KEYS[1]) == 0) then
+ return 0
+end
+return tonumber(redis.call('GET', KEYS[1]))
\ No newline at end of file
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/Lua/process.lua b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/Lua/process.lua
new file mode 100644
index 0000000..fc8ef53
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/Lua/process.lua
@@ -0,0 +1,6 @@
+if (redis.call('EXISTS',KEYS[1]) ~= 0) then
+ redis.call('INCRBY',KEYS[1], 1)
+else
+ redis.call('SETEX',KEYS[1],ARGV[1],1)
+end
+return tonumber(redis.call('GET',KEYS[1]))
\ No newline at end of file
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisLimitFeatureNamingNormalizer.cs b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisLimitFeatureNamingNormalizer.cs
new file mode 100644
index 0000000..500d15e
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisLimitFeatureNamingNormalizer.cs
@@ -0,0 +1,24 @@
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+
+namespace Sanhe.Abp.Features.LimitValidation.Redis;
+
+public class RedisLimitFeatureNamingNormalizer : IRedisLimitFeatureNamingNormalizer, ISingletonDependency
+{
+ protected ICurrentTenant CurrentTenant { get; }
+
+ public RedisLimitFeatureNamingNormalizer(ICurrentTenant currentTenant)
+ {
+ CurrentTenant = currentTenant;
+ }
+
+ public virtual string NormalizeFeatureName(string instance, RequiresLimitFeatureContext context)
+ {
+ if (CurrentTenant.IsAvailable)
+ {
+ return $"{instance}t:RequiresLimitFeature;t:{CurrentTenant.Id};f:{context.LimitFeature}";
+ }
+
+ return $"{instance}c:RequiresLimitFeature;f:{context.LimitFeature}";
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisRequiresLimitFeatureChecker.cs b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisRequiresLimitFeatureChecker.cs
new file mode 100644
index 0000000..11a050e
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/Sanhe/Abp/Features/LimitValidation/Redis/RedisRequiresLimitFeatureChecker.cs
@@ -0,0 +1,135 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using StackExchange.Redis;
+using System;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.VirtualFileSystem;
+
+namespace Sanhe.Abp.Features.LimitValidation.Redis;
+
+[DisableConventionalRegistration]
+public class RedisRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker
+{
+ private const string CHECK_LUA_SCRIPT = "/Sanhe/Abp/Features/LimitValidation/Redis/Lua/check.lua";
+ private const string PROCESS_LUA_SCRIPT = "/Sanhe/Abp/Features/LimitValidation/Redis/Lua/process.lua";
+
+ public ILogger Logger { protected get; set; }
+
+ private volatile ConnectionMultiplexer _connection;
+ private volatile ConfigurationOptions _redisConfig;
+ private IDatabaseAsync _redis;
+ private IServer _server;
+
+ private readonly IVirtualFileProvider _virtualFileProvider;
+ private readonly IRedisLimitFeatureNamingNormalizer _featureNamingNormalizer;
+ private readonly AbpRedisRequiresLimitFeatureOptions _options;
+ private readonly string _instance;
+
+ private readonly SemaphoreSlim _connectionLock = new(initialCount: 1, maxCount: 1);
+
+ public RedisRequiresLimitFeatureChecker(
+ IVirtualFileProvider virtualFileProvider,
+ IRedisLimitFeatureNamingNormalizer featureNamingNormalizer,
+ IOptions optionsAccessor)
+ {
+ if (optionsAccessor == null)
+ {
+ throw new ArgumentNullException(nameof(optionsAccessor));
+ }
+
+ _options = optionsAccessor.Value;
+ _virtualFileProvider = virtualFileProvider;
+ _featureNamingNormalizer = featureNamingNormalizer;
+
+ _instance = _options.InstanceName ?? string.Empty;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public virtual async Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ await ConnectAsync(cancellation);
+
+ var result = await EvaluateAsync(CHECK_LUA_SCRIPT, context, cancellation);
+ return result + 1 <= context.Limit;
+ }
+
+ public virtual async Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ await ConnectAsync(cancellation);
+
+ await EvaluateAsync(PROCESS_LUA_SCRIPT, context, cancellation);
+ }
+
+ private async Task EvaluateAsync(string luaScriptFilePath, RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ var luaScriptFile = _virtualFileProvider.GetFileInfo(luaScriptFilePath);
+ using var luaScriptFileStream = luaScriptFile.CreateReadStream();
+ var fileBytes = await luaScriptFileStream.GetAllBytesAsync(cancellation);
+
+ var luaSha1 = fileBytes.Sha1();
+ if (!await _server.ScriptExistsAsync(luaSha1))
+ {
+ var luaScript = Encoding.UTF8.GetString(fileBytes);
+ luaSha1 = await _server.ScriptLoadAsync(luaScript);
+ }
+
+ var keys = new RedisKey[1] { NormalizeKey(context) };
+ var values = new RedisValue[] { context.GetEffectTicks() };
+ var result = await _redis.ScriptEvaluateAsync(luaSha1, keys, values);
+
+ if (result.Type == ResultType.Error)
+ {
+ throw new AbpException($"Script evaluate error: {result}");
+ }
+
+ return (int)result;
+ }
+
+ private string NormalizeKey(RequiresLimitFeatureContext context)
+ {
+ return _featureNamingNormalizer.NormalizeFeatureName(_instance, context);
+ }
+
+ private async Task ConnectAsync(CancellationToken token = default)
+ {
+ token.ThrowIfCancellationRequested();
+
+ if (_redis != null)
+ {
+ return;
+ }
+
+ await _connectionLock.WaitAsync(token);
+ try
+ {
+ if (_redis == null)
+ {
+ if (_options.ConfigurationOptions != null)
+ {
+ _redisConfig = _options.ConfigurationOptions;
+ }
+ else
+ {
+ _redisConfig = ConfigurationOptions.Parse(_options.Configuration);
+ }
+ _redisConfig.AllowAdmin = true;
+ _redisConfig.SetDefaultPorts();
+ _connection = await ConnectionMultiplexer.ConnectAsync(_redisConfig);
+ // fix: 无需关注redis连接事件
+ _redis = _connection.GetDatabase();
+ _server = _connection.GetServer(_redisConfig.EndPoints[0]);
+ }
+ }
+ finally
+ {
+ _connectionLock.Release();
+ }
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/System/BytesExtensions.cs b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/System/BytesExtensions.cs
new file mode 100644
index 0000000..cba8afd
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation.Redis/System/BytesExtensions.cs
@@ -0,0 +1,15 @@
+using System.Security.Cryptography;
+
+namespace System;
+
+internal static class BytesExtensions
+{
+ public static byte[] Sha1(this byte[] data)
+ {
+ using (var sha = SHA1.Create())
+ {
+ var hashBytes = sha.ComputeHash(data);
+ return hashBytes;
+ }
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/FodyWeavers.xml b/modules/common/Sanhe.Abp.Features.LimitValidation/FodyWeavers.xml
new file mode 100644
index 0000000..1715698
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/README.md b/modules/common/Sanhe.Abp.Features.LimitValidation/README.md
new file mode 100644
index 0000000..1e419d7
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/README.md
@@ -0,0 +1,85 @@
+# Sanhe.Abp.Features.LimitValidation
+
+功能上限验证组件
+
+检查定义的功能调用次数,来限制特定的实体(租户、用户、客户端等)对于应用程序的调用
+
+预先设定了如下几个策略
+
+LimitPolicy.Minute 按分钟计算流量
+LimitPolicy.Hours 按小时计算流量
+LimitPolicy.Days 按天数计算流量
+LimitPolicy.Weeks 按周数计算流量
+LimitPolicy.Month 按月数计算流量
+LimitPolicy.Years 按年数计算流量
+
+## 配置使用
+
+
+```csharp
+[DependsOn(typeof(AbpFeaturesLimitValidationModule))]
+public class YouProjectModule : AbpModule
+{
+ // other
+}
+
+public static class FakeFeatureNames
+{
+ public const string GroupName = "FakeFeature.Tests";
+ // 类型限制调用次数功能名称
+ public const string ClassLimitFeature = GroupName + ".LimitFeature";
+ // 方法限制调用次数功能名称
+ public const string MethodLimitFeature = GroupName + ".MethodLimitFeature";
+ // 限制调用间隔功能名称
+ public const string IntervalFeature = GroupName + ".IntervalFeature";
+}
+
+// 流量限制依赖自定义功能
+public class FakeFeatureDefinitionProvider : FeatureDefinitionProvider
+{
+ public override void Define(IFeatureDefinitionContext context)
+ {
+ var featureGroup = context.AddGroup(FakeFeatureNames.GroupName);
+ featureGroup.AddFeature(
+ name: FakeFeatureNames.ClassLimitFeature,
+ defaultValue: 1000.ToString(), // 周期内最大允许调用1000次
+ valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000)));
+ featureGroup.AddFeature(
+ name: FakeFeatureNames.MethodLimitFeature,
+ defaultValue: 100.ToString(), // 周期内最大允许调用100次
+ valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000)));
+ featureGroup.AddFeature(
+ name: FakeFeatureNames.IntervalFeature,
+ defaultValue: 1.ToString(), // 限制周期
+ valueType: new ToggleStringValueType(new NumericValueValidator(1, 1000)));
+ }
+}
+
+// 按照预设的参数,类型在一天钟内仅允许调用1000次
+[RequiresLimitFeature(FakeFeatureNames.ClassLimitFeature, FakeFeatureNames.IntervalFeature, LimitPolicy.Days)]
+public class FakeLimitClass
+{
+ // 按照预设的参数,方法在一分钟内仅允许调用100次
+ [RequiresLimitFeature(FakeFeatureNames.MethodLimitFeature, FakeFeatureNames.IntervalFeature, LimitPolicy.Minute)]
+ public void LimitMethod()
+ {
+ // other...
+ }
+}
+```
+
+如果需要自行处理功能限制策略时长,请覆盖对应策略的默认策略,返回的时钟刻度单位始终是秒
+
+```csharp
+[DependsOn(typeof(AbpFeaturesLimitValidationModule))]
+public class YouProjectModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.MapEffectPolicy(LimitPolicy.Minute, (time) => return 60;); // 表示不管多少分钟(time),都只会限制60秒
+ });
+ }
+}
+```
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe.Abp.Features.LimitValidation.csproj b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe.Abp.Features.LimitValidation.csproj
new file mode 100644
index 0000000..6e9bfe8
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe.Abp.Features.LimitValidation.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeatureLimitException.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeatureLimitException.cs
new file mode 100644
index 0000000..babfd5a
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeatureLimitException.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.Localization;
+using Sanhe.Abp.Features.LimitValidation.Localization;
+using Volo.Abp;
+using Volo.Abp.ExceptionHandling;
+using Volo.Abp.Localization;
+
+namespace Sanhe.Abp.Features.LimitValidation;
+
+public class AbpFeatureLimitException : AbpException, ILocalizeErrorMessage, IBusinessException
+{
+ ///
+ /// 功能名称名称
+ ///
+ public string Feature { get; }
+ ///
+ /// 上限
+ ///
+ public int Limit { get; }
+
+ public AbpFeatureLimitException(string feature, int limit)
+ : base($"Features {feature} has exceeded the maximum number of calls {limit}, please apply for the appropriate permission")
+ {
+ Feature = feature;
+ Limit = limit;
+ }
+
+ public string LocalizeMessage(LocalizationContext context)
+ {
+ var localizer = context.LocalizerFactory.Create();
+
+ return localizer["FeaturesLimitException", Limit];
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationModule.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationModule.cs
new file mode 100644
index 0000000..53d447c
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationModule.cs
@@ -0,0 +1,34 @@
+using Microsoft.Extensions.DependencyInjection;
+using Sanhe.Abp.Features.LimitValidation.Localization;
+using Volo.Abp.Features;
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace Sanhe.Abp.Features.LimitValidation;
+
+[DependsOn(typeof(AbpFeaturesModule))]
+public class AbpFeaturesLimitValidationModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.OnRegistred(FeaturesLimitValidationInterceptorRegistrar.RegisterIfNeeded);
+
+ Configure(options =>
+ {
+ options.MapDefaultEffectPolicys();
+ });
+
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Add("en")
+ .AddVirtualJson("/Sanhe/Abp/Features/LimitValidation/Localization/Resources");
+ });
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationOptions.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationOptions.cs
new file mode 100644
index 0000000..7bc9df5
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/AbpFeaturesLimitValidationOptions.cs
@@ -0,0 +1,48 @@
+using JetBrains.Annotations;
+using System;
+using System.Collections.Generic;
+using Volo.Abp;
+
+namespace Sanhe.Abp.Features.LimitValidation;
+
+public class AbpFeaturesLimitValidationOptions
+{
+ public IDictionary> EffectPolicys { get; }
+
+ public AbpFeaturesLimitValidationOptions()
+ {
+ EffectPolicys = new Dictionary>();
+ }
+
+ ///
+ /// 变更功能限制策略时长计算方法
+ ///
+ /// 限制策略
+ /// 自定义的计算方法
+ ///
+ /// 返回值一定要是秒钟刻度
+ ///
+ public void MapEffectPolicy(LimitPolicy policy, [NotNull] Func func)
+ {
+ Check.NotNull(func, nameof(func));
+
+ if (EffectPolicys.ContainsKey(policy))
+ {
+ EffectPolicys[policy] = func;
+ }
+ else
+ {
+ EffectPolicys.Add(policy, func);
+ }
+ }
+
+ internal void MapDefaultEffectPolicys()
+ {
+ MapEffectPolicy(LimitPolicy.Minute, (time) => { return (long)(DateTimeOffset.UtcNow.AddMinutes(time) - DateTimeOffset.UtcNow).TotalSeconds; });
+ MapEffectPolicy(LimitPolicy.Hours, (time) => { return (long)(DateTimeOffset.UtcNow.AddHours(time) - DateTimeOffset.UtcNow).TotalSeconds; });
+ MapEffectPolicy(LimitPolicy.Days, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time) - DateTimeOffset.UtcNow).TotalSeconds; });
+ MapEffectPolicy(LimitPolicy.Weeks, (time) => { return (long)(DateTimeOffset.UtcNow.AddDays(time * 7) - DateTimeOffset.UtcNow).TotalSeconds; });
+ MapEffectPolicy(LimitPolicy.Month, (time) => { return (long)(DateTimeOffset.UtcNow.AddMonths(time) - DateTimeOffset.UtcNow).TotalSeconds; });
+ MapEffectPolicy(LimitPolicy.Years, (time) => { return (long)(DateTimeOffset.UtcNow.AddYears(time) - DateTimeOffset.UtcNow).TotalSeconds; });
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptor.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptor.cs
new file mode 100644
index 0000000..007a460
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptor.cs
@@ -0,0 +1,99 @@
+using Microsoft.Extensions.Options;
+using System.Reflection;
+using System.Threading.Tasks;
+using Volo.Abp.Aspects;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+using Volo.Abp.Features;
+using Volo.Abp.Validation.StringValues;
+
+namespace Sanhe.Abp.Features.LimitValidation;
+
+public class FeaturesLimitValidationInterceptor : AbpInterceptor, ITransientDependency
+{
+ private readonly IFeatureChecker _featureChecker;
+ private readonly AbpFeaturesLimitValidationOptions _options;
+ private readonly IRequiresLimitFeatureChecker _limitFeatureChecker;
+ private readonly IFeatureDefinitionManager _featureDefinitionManager;
+
+ public FeaturesLimitValidationInterceptor(
+ IFeatureChecker featureChecker,
+ IRequiresLimitFeatureChecker limitFeatureChecker,
+ IFeatureDefinitionManager featureDefinitionManager,
+ IOptions options)
+ {
+ _options = options.Value;
+ _featureChecker = featureChecker;
+ _limitFeatureChecker = limitFeatureChecker;
+ _featureDefinitionManager = featureDefinitionManager;
+ }
+
+ public override async Task InterceptAsync(IAbpMethodInvocation invocation)
+ {
+ if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.FeatureChecking))
+ {
+ await invocation.ProceedAsync();
+ return;
+ }
+
+ var limitFeature = GetRequiresLimitFeature(invocation.Method);
+
+ if (limitFeature == null)
+ {
+ await invocation.ProceedAsync();
+ return;
+ }
+
+ // 获取功能限制上限
+ var limit = await _featureChecker.GetAsync(limitFeature.LimitFeature, limitFeature.DefaultLimit);
+ // 获取功能限制时长
+ var interval = await _featureChecker.GetAsync(limitFeature.IntervalFeature, limitFeature.DefaultInterval);
+ // 必要的上下文参数
+ var limitFeatureContext = new RequiresLimitFeatureContext(limitFeature.LimitFeature, _options, limitFeature.Policy, interval, limit);
+ // 检查次数限制
+ await PreCheckFeatureAsync(limitFeatureContext);
+ // 执行代理方法
+ await invocation.ProceedAsync();
+ // 调用次数递增
+ // TODO: 使用Redis结合Lua脚本?
+ await PostCheckFeatureAsync(limitFeatureContext);
+ }
+
+ protected virtual async Task PreCheckFeatureAsync(RequiresLimitFeatureContext context)
+ {
+ var allowed = await _limitFeatureChecker.CheckAsync(context);
+ if (!allowed)
+ {
+ throw new AbpFeatureLimitException(context.LimitFeature, context.Limit);
+ }
+ }
+
+ protected virtual async Task PostCheckFeatureAsync(RequiresLimitFeatureContext context)
+ {
+ await _limitFeatureChecker.ProcessAsync(context);
+ }
+
+ protected virtual RequiresLimitFeatureAttribute GetRequiresLimitFeature(MethodInfo methodInfo)
+ {
+ var limitFeature = methodInfo.GetCustomAttribute(false);
+ if (limitFeature != null)
+ {
+ // 限制次数定义的不是范围参数,则不参与限制功能
+ var featureLimitDefinition = _featureDefinitionManager.GetOrNull(limitFeature.LimitFeature);
+ if (featureLimitDefinition == null ||
+ !typeof(NumericValueValidator).IsAssignableFrom(featureLimitDefinition.ValueType.Validator.GetType()))
+ {
+ return null;
+ }
+
+ // 时长刻度定义的不是范围参数,则不参与限制功能
+ var featureIntervalDefinition = _featureDefinitionManager.GetOrNull(limitFeature.IntervalFeature);
+ if (featureIntervalDefinition == null ||
+ !typeof(NumericValueValidator).IsAssignableFrom(featureIntervalDefinition.ValueType.Validator.GetType()))
+ {
+ return null;
+ }
+ }
+ return limitFeature;
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptorRegistrar.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptorRegistrar.cs
new file mode 100644
index 0000000..029e9f5
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/FeaturesLimitValidationInterceptorRegistrar.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+
+namespace Sanhe.Abp.Features.LimitValidation;
+
+public static class FeaturesLimitValidationInterceptorRegistrar
+{
+ public static void RegisterIfNeeded(IOnServiceRegistredContext context)
+ {
+ if (ShouldIntercept(context.ImplementationType))
+ {
+ context.Interceptors.TryAdd();
+ }
+ }
+
+ private static bool ShouldIntercept(Type type)
+ {
+ return !DynamicProxyIgnoreTypes.Contains(type) &&
+ (type.IsDefined(typeof(RequiresLimitFeatureAttribute), true) ||
+ AnyMethodHasRequiresLimitFeatureAttribute(type));
+ }
+
+ private static bool AnyMethodHasRequiresLimitFeatureAttribute(Type implementationType)
+ {
+ return implementationType
+ .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ .Any(HasRequiresLimitFeatureAttribute);
+ }
+
+ private static bool HasRequiresLimitFeatureAttribute(MemberInfo methodInfo)
+ {
+ return methodInfo.IsDefined(typeof(RequiresLimitFeatureAttribute), true);
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/IRequiresLimitFeatureChecker.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/IRequiresLimitFeatureChecker.cs
new file mode 100644
index 0000000..73eeccd
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/IRequiresLimitFeatureChecker.cs
@@ -0,0 +1,11 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Sanhe.Abp.Features.LimitValidation;
+
+public interface IRequiresLimitFeatureChecker
+{
+ Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default);
+
+ Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default);
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/LimitPolicy.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/LimitPolicy.cs
new file mode 100644
index 0000000..0c31ca0
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/LimitPolicy.cs
@@ -0,0 +1,32 @@
+namespace Sanhe.Abp.Features.LimitValidation;
+
+///
+/// 限制策略。
+///
+public enum LimitPolicy : byte
+{
+ ///
+ /// 按分钟限制
+ ///
+ Minute = 0,
+ ///
+ /// 按小时限制
+ ///
+ Hours = 10,
+ ///
+ /// 按天限制
+ ///
+ Days = 20,
+ ///
+ /// 按周限制
+ ///
+ Weeks = 30,
+ ///
+ /// 按月限制
+ ///
+ Month = 40,
+ ///
+ /// 按年限制
+ ///
+ Years = 50
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/FeaturesLimitValidationResource.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/FeaturesLimitValidationResource.cs
new file mode 100644
index 0000000..6a73e27
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/FeaturesLimitValidationResource.cs
@@ -0,0 +1,8 @@
+using Volo.Abp.Localization;
+
+namespace Sanhe.Abp.Features.LimitValidation.Localization;
+
+[LocalizationResourceName("AbpFeaturesLimitValidation")]
+public class FeaturesLimitValidationResource
+{
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/en.json b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/en.json
new file mode 100644
index 0000000..46799f1
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/en.json
@@ -0,0 +1,6 @@
+{
+ "culture": "en",
+ "texts": {
+ "FeaturesLimitException": "Service has exceeded the maximum number of calls {0}. Please apply for the appropriate permissions"
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/zh-Hans.json b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/zh-Hans.json
new file mode 100644
index 0000000..cb94820
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/Localization/Resources/zh-Hans.json
@@ -0,0 +1,6 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "FeaturesLimitException": "服务已超过最大调用次数 {0},请申请适当的权限"
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/NullRequiresLimitFeatureChecker.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/NullRequiresLimitFeatureChecker.cs
new file mode 100644
index 0000000..b9e8df9
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/NullRequiresLimitFeatureChecker.cs
@@ -0,0 +1,18 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace Sanhe.Abp.Features.LimitValidation;
+
+public class NullRequiresLimitFeatureChecker : IRequiresLimitFeatureChecker, ISingletonDependency
+{
+ public Task CheckAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ return Task.FromResult(true);
+ }
+
+ public Task ProcessAsync(RequiresLimitFeatureContext context, CancellationToken cancellation = default)
+ {
+ return Task.CompletedTask;
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureAttribute.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureAttribute.cs
new file mode 100644
index 0000000..b03b596
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureAttribute.cs
@@ -0,0 +1,53 @@
+using JetBrains.Annotations;
+using System;
+using Volo.Abp;
+
+namespace Sanhe.Abp.Features.LimitValidation;
+
+///
+/// 单个功能的调用量限制
+///
+///
+/// 需要对于限制时长和限制上限功能区分,以便于更细粒度的限制
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
+public class RequiresLimitFeatureAttribute : Attribute
+{
+ ///
+ /// 功能限制策略
+ ///
+ public LimitPolicy Policy { get; }
+ ///
+ /// 默认限制时长
+ ///
+ public int DefaultLimit { get; }
+ ///
+ /// 限制上限名称
+ ///
+ public string LimitFeature { get; }
+ ///
+ /// 默认限制时长
+ ///
+ public int DefaultInterval { get; }
+ ///
+ /// 限制时长名称
+ ///
+ public string IntervalFeature { get; }
+
+ public RequiresLimitFeatureAttribute(
+ [NotNull] string limitFeature,
+ [NotNull] string intervalFeature,
+ LimitPolicy policy = LimitPolicy.Month,
+ int defaultLimit = 1,
+ int defaultInterval = 1)
+ {
+ Check.NotNullOrWhiteSpace(limitFeature, nameof(limitFeature));
+ Check.NotNullOrWhiteSpace(intervalFeature, nameof(intervalFeature));
+
+ Policy = policy;
+ LimitFeature = limitFeature;
+ DefaultLimit = defaultLimit;
+ IntervalFeature = intervalFeature;
+ DefaultInterval = defaultInterval;
+ }
+}
diff --git a/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureContext.cs b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureContext.cs
new file mode 100644
index 0000000..a9e745a
--- /dev/null
+++ b/modules/common/Sanhe.Abp.Features.LimitValidation/Sanhe/Abp/Features/LimitValidation/RequiresLimitFeatureContext.cs
@@ -0,0 +1,46 @@
+namespace Sanhe.Abp.Features.LimitValidation;
+
+public class RequiresLimitFeatureContext
+{
+ ///
+ /// 功能限制策略
+ ///
+ public LimitPolicy Policy { get; }
+ ///
+ /// 限制时长
+ ///
+ public int Interval { get; }
+ ///
+ /// 功能限制次数
+ ///
+ public int Limit { get; }
+ ///
+ /// 功能限制次数名称
+ ///
+ public string LimitFeature { get; }
+
+ public AbpFeaturesLimitValidationOptions Options { get; }
+
+ public RequiresLimitFeatureContext(
+ string limitFeature,
+ AbpFeaturesLimitValidationOptions options,
+ LimitPolicy policy = LimitPolicy.Month,
+ int interval = 1,
+ int limit = 1)
+ {
+ Limit = limit;
+ Policy = policy;
+ Interval = interval;
+ LimitFeature = limitFeature;
+ Options = options;
+ }
+
+ ///
+ /// 获取生效时间跨度,单位:s
+ ///
+ ///
+ public long GetEffectTicks()
+ {
+ return Options.EffectPolicys[Policy](Interval);
+ }
+}