44 changed files with 720 additions and 559 deletions
@ -1,14 +0,0 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.BackgroundJobs" Version="4.3.0" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -1,43 +0,0 @@ |
|||
namespace LINGYUN.Abp.BackgroundJobs |
|||
{ |
|||
public class RetryAsyncBackgroundJobArgs<TArgs> |
|||
{ |
|||
/// <summary>
|
|||
/// 重试次数
|
|||
/// </summary>
|
|||
public int RetryCount { get; set; } = 0; |
|||
/// <summary>
|
|||
/// 重试间隔(毫秒)
|
|||
/// 默认 300000ms = 5min
|
|||
/// </summary>
|
|||
public double RetryIntervalMillisecond { get; set; } = 300000d; |
|||
/// <summary>
|
|||
/// 最大重试次数
|
|||
/// 默认 20
|
|||
/// </summary>
|
|||
public int MaxRetryCount { get; set; } = 20; |
|||
/// <summary>
|
|||
/// 作业参数
|
|||
/// </summary>
|
|||
public TArgs JobArgs { get; set; } |
|||
|
|||
public RetryAsyncBackgroundJobArgs() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public RetryAsyncBackgroundJobArgs(TArgs jobArgs) |
|||
{ |
|||
JobArgs = jobArgs; |
|||
} |
|||
|
|||
public RetryAsyncBackgroundJobArgs(TArgs jobArgs, int retryCount = 0, double interval = 300000d, int maxRetryCount = 20) |
|||
{ |
|||
JobArgs = jobArgs; |
|||
|
|||
RetryCount = retryCount; |
|||
RetryIntervalMillisecond = interval; |
|||
MaxRetryCount = maxRetryCount; |
|||
} |
|||
} |
|||
} |
|||
@ -1,80 +0,0 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BackgroundJobs; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundJobs |
|||
{ |
|||
public abstract class RetryAsyncBackgroundJobBase<TArgs> : IAsyncBackgroundJob<RetryAsyncBackgroundJobArgs<TArgs>> |
|||
{ |
|||
public ILogger<RetryAsyncBackgroundJobBase<TArgs>> Logger { get; set; } |
|||
|
|||
protected IBackgroundJobManager BackgroundJobManager { get; } |
|||
|
|||
protected RetryAsyncBackgroundJobBase( |
|||
IBackgroundJobManager backgroundJobManager) |
|||
{ |
|||
BackgroundJobManager = backgroundJobManager; |
|||
|
|||
Logger = NullLogger<RetryAsyncBackgroundJobBase<TArgs>>.Instance; |
|||
} |
|||
|
|||
public async Task ExecuteAsync(RetryAsyncBackgroundJobArgs<TArgs> args) |
|||
{ |
|||
if (args.RetryCount > args.MaxRetryCount) |
|||
{ |
|||
Logger.LogWarning("Job has failed and the maximum number of retries has been reached. The failure callback is about to enter"); |
|||
// 任务执行失败次数已达上限,调用用户定义回调,并不再执行
|
|||
await OnJobExecuteFailedAsync(args.JobArgs); |
|||
return; |
|||
} |
|||
try |
|||
{ |
|||
// 执行任务
|
|||
await ExecuteAsync(args.JobArgs, args.RetryCount); |
|||
// 执行完成后回调
|
|||
await OnJobExecuteCompletedAsync(args.JobArgs); |
|||
} |
|||
catch(Exception ex) |
|||
{ |
|||
Logger.LogWarning("Job execution has failed and a retry is imminent"); |
|||
Logger.LogWarning("Job running error:{0}", ex.Message); |
|||
|
|||
// 每次重试 间隔时间增加1.1倍
|
|||
var retryInterval = args.RetryIntervalMillisecond * 1.1; |
|||
var retryJobArgs = new RetryAsyncBackgroundJobArgs<TArgs>(args.JobArgs, |
|||
args.RetryCount + 1, retryInterval, args.MaxRetryCount); |
|||
|
|||
Logger.LogDebug("Job task is queued for the next execution"); |
|||
|
|||
// 计算优先级
|
|||
BackgroundJobPriority priority = BackgroundJobPriority.Normal; |
|||
|
|||
if (args.RetryCount <= (args.MaxRetryCount / 2) && |
|||
args.RetryCount > (args.MaxRetryCount / 3)) |
|||
{ |
|||
priority = BackgroundJobPriority.BelowNormal; |
|||
} |
|||
else if (args.RetryCount > (args.MaxRetryCount / 1.5)) |
|||
{ |
|||
priority = BackgroundJobPriority.Low; |
|||
} |
|||
// 延迟入队,等待下一次运行
|
|||
await BackgroundJobManager.EnqueueAsync(retryJobArgs, priority, delay: TimeSpan.FromMilliseconds(retryInterval)); |
|||
} |
|||
} |
|||
|
|||
protected abstract Task ExecuteAsync(TArgs args, int retryCount); |
|||
|
|||
protected virtual Task OnJobExecuteFailedAsync(TArgs args) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
protected virtual Task OnJobExecuteCompletedAsync(TArgs args) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.HangFire" Version="4.3.0" /> |
|||
<PackageReference Include="Volo.Abp.BackgroundWorkers" Version="4.3.0" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,19 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.BackgroundWorkers; |
|||
using Volo.Abp.Hangfire; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundWorkers.Hangfire |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpBackgroundWorkersModule), |
|||
typeof(AbpHangfireModule) |
|||
)] |
|||
public class AbpBackgroundWorkersHangfireModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddSingleton(typeof(HangfireBackgroundWorkerAdapter<>)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using JetBrains.Annotations; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundWorkers.Hangfire |
|||
{ |
|||
internal static class Check |
|||
{ |
|||
public static int Range( |
|||
int value, |
|||
[InvokerParameterName][NotNull] string parameterName, |
|||
int minimum = int.MinValue, |
|||
int maximum = int.MaxValue) |
|||
{ |
|||
if (value < minimum) |
|||
{ |
|||
throw new ArgumentException($"{parameterName} must be equal to or lower than {minimum}!", parameterName); |
|||
} |
|||
if (value > maximum) |
|||
{ |
|||
throw new ArgumentException($"{parameterName} must be equal to or greater than {maximum}!", parameterName); |
|||
} |
|||
return value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,171 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundWorkers.Hangfire |
|||
{ |
|||
public class CronGenerator |
|||
{ |
|||
public const long MilliSecondsOfYear = 315_3600_0000L; |
|||
public const long MilliSecondsOfMonth = 26_7840_0000L; |
|||
public const int MilliSecondsOfWeek = 6_0480_0000; |
|||
public const int MilliSecondsOfDays = 8640_0000; |
|||
public const int MilliSecondsOfHours = 360_0000; |
|||
public const int MilliSecondsOfMinutes = 60000; |
|||
|
|||
/// <summary>
|
|||
/// 从毫秒计算Cron表达式
|
|||
/// </summary>
|
|||
/// <param name="milliseconds"></param>
|
|||
/// <returns></returns>
|
|||
public static string FormMilliseconds(long milliseconds = 1000) |
|||
{ |
|||
if (milliseconds <= 1000) |
|||
{ |
|||
return Seconds(0, 1); |
|||
} |
|||
|
|||
if (milliseconds >= MilliSecondsOfYear) |
|||
{ |
|||
return Year(1, 1, 0, 0, 2001, (int)(milliseconds / MilliSecondsOfYear)); |
|||
} |
|||
|
|||
if (milliseconds >= MilliSecondsOfMonth) |
|||
{ |
|||
return Month(1, 0, 0, 1, (int)(milliseconds / MilliSecondsOfMonth)); |
|||
} |
|||
|
|||
// TODO: 以周为单位的任务Cron
|
|||
if (milliseconds >= MilliSecondsOfWeek) |
|||
{ |
|||
return Day(0, 0, 1, (int)(milliseconds / MilliSecondsOfWeek)); |
|||
} |
|||
|
|||
if (milliseconds >= MilliSecondsOfDays) |
|||
{ |
|||
return Day(0, 0, 1, (int)(milliseconds / MilliSecondsOfDays)); |
|||
} |
|||
|
|||
if (milliseconds >= MilliSecondsOfHours) |
|||
{ |
|||
return Hour(0, 0, (int)(milliseconds / MilliSecondsOfHours)); |
|||
} |
|||
|
|||
if (milliseconds >= MilliSecondsOfMinutes) |
|||
{ |
|||
return Minute(0, 0, (int)(milliseconds / MilliSecondsOfMinutes)); |
|||
} |
|||
|
|||
return Seconds(0, (int)(milliseconds / 1000)); |
|||
} |
|||
/// <summary>
|
|||
/// 周期性为秒钟的任务
|
|||
/// </summary>
|
|||
/// <param name="second">第几秒开始,默认为第0秒</param>
|
|||
/// <param name="interval">执行周期的间隔,默认为每5秒一次</param>
|
|||
/// <returns></returns>
|
|||
public static string Seconds(int second = 0, int interval = 5) |
|||
{ |
|||
Check.Range(second, nameof(second), 0, 59); |
|||
|
|||
return $"{second}/{interval} * * * * ? "; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 周期性为分钟的任务
|
|||
/// </summary>
|
|||
/// <param name="second">第几秒开始,默认为第0秒</param>
|
|||
/// <param name="minute">第几分钟开始,默认为第0分钟</param>
|
|||
/// <param name="interval">执行周期的间隔,默认为每分钟一次</param>
|
|||
/// <returns></returns>
|
|||
public static string Minute(int second = 0, int minute = 0, int interval = 1) |
|||
{ |
|||
Check.Range(second, nameof(second), 0, 59); |
|||
Check.Range(minute, nameof(minute), 0, 59); |
|||
|
|||
return $"{second} {minute}/{interval} * * * ? "; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 周期性为小时的任务
|
|||
/// </summary>
|
|||
/// <param name="minute">第几分钟开始,默认为第0分钟</param>
|
|||
/// <param name="hour">第几小时开始,默认为0点</param>
|
|||
/// <param name="interval">执行周期的间隔,默认为每小时一次</param>
|
|||
/// <returns></returns>
|
|||
public static string Hour(int minute = 0, int hour = 0, int interval = 1) |
|||
{ |
|||
Check.Range(hour, nameof(hour), 0, 23); |
|||
Check.Range(minute, nameof(minute), 0, 59); |
|||
|
|||
return $"0 {minute} {hour}/{interval} * * ? "; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 周期性为天的任务
|
|||
/// </summary>
|
|||
/// <param name="hour">第几小时开始,默认从0点开始</param>
|
|||
/// <param name="minute">第几分钟开始,默认从第0分钟开始</param>
|
|||
/// <param name="day">第几天开始,默认从第1天开始</param>
|
|||
/// <param name="interval">执行周期的间隔,默认为每天一次</param>
|
|||
/// <returns></returns>
|
|||
public static string Day(int hour = 0, int minute = 0, int day = 1, int interval = 1) |
|||
{ |
|||
Check.Range(hour, nameof(hour), 0, 23); |
|||
Check.Range(minute, nameof(minute), 0, 59); |
|||
Check.Range(day, nameof(day), 1, 31); |
|||
|
|||
return $"0 {minute} {hour} {day}/{interval} * ? "; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 周期性为周的任务
|
|||
/// </summary>
|
|||
/// <param name="dayOfWeek">星期几开始,默认从星期一点开始</param>
|
|||
/// <param name="hour">第几小时开始,默认从0点开始</param>
|
|||
/// <param name="minute">第几分钟开始,默认从第0分钟开始</param>
|
|||
/// <returns></returns>
|
|||
public static string Week(DayOfWeek dayOfWeek = DayOfWeek.Monday, int hour = 0, int minute = 0) |
|||
{ |
|||
Check.Range(hour, nameof(hour), 0, 23); |
|||
Check.Range(minute, nameof(minute), 0, 59); |
|||
|
|||
return $"{minute} {hour} * * {dayOfWeek.ToString().Substring(0, 3)}"; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 周期性为月的任务
|
|||
/// </summary>
|
|||
/// <param name="day">几号开始,默认从一号开始</param>
|
|||
/// <param name="hour">第几小时开始,默认从0点开始</param>
|
|||
/// <param name="minute">第几分钟开始,默认从第0分钟开始</param>
|
|||
/// <param name="month">第几月开始,默认从第1月开始</param>
|
|||
/// <param name="interval">月份间隔</param>
|
|||
/// <returns></returns>
|
|||
public static string Month(int day = 1, int hour = 0, int minute = 0, int month = 1, int interval = 1) |
|||
{ |
|||
Check.Range(hour, nameof(hour), 0, 23); |
|||
Check.Range(minute, nameof(minute), 0, 59); |
|||
Check.Range(day, nameof(day), 1, 31); |
|||
|
|||
return $"0 {minute} {hour} {day} {month}/{interval} ?"; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 周期性为年的任务
|
|||
/// </summary>
|
|||
/// <param name="month">几月开始,默认从一月开始</param>
|
|||
/// <param name="day">几号开始,默认从一号开始</param>
|
|||
/// <param name="hour">第几小时开始,默认从0点开始</param>
|
|||
/// <param name="year">第几年开始,默认从2001年开始</param>
|
|||
/// <param name="minute">第几分钟开始,默认从第0分钟开始</param>
|
|||
/// <returns></returns>
|
|||
public static string Year(int month = 1, int day = 1, int hour = 0, int minute = 0, int year = 2001, int interval = 1) |
|||
{ |
|||
Check.Range(hour, nameof(hour), 0, 23); |
|||
Check.Range(minute, nameof(minute), 0, 59); |
|||
Check.Range(day, nameof(day), 1, 31); |
|||
Check.Range(month, nameof(month), 1, 12); |
|||
|
|||
return $"0 {minute} {hour} {day} {month} ? {year}/{interval}"; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
using System.Reflection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BackgroundWorkers; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundWorkers.Hangfire |
|||
{ |
|||
public class HangfireBackgroundWorkerAdapter<TWorker> : BackgroundWorkerBase, IHangfireBackgroundWorkerAdapter |
|||
where TWorker : IBackgroundWorker |
|||
{ |
|||
private readonly MethodInfo _doWorkAsyncMethod; |
|||
private readonly MethodInfo _doWorkMethod; |
|||
|
|||
public HangfireBackgroundWorkerAdapter() |
|||
{ |
|||
_doWorkAsyncMethod = typeof(TWorker).GetMethod("DoWorkAsync", BindingFlags.Instance | BindingFlags.NonPublic); |
|||
_doWorkMethod = typeof(TWorker).GetMethod("DoWork", BindingFlags.Instance | BindingFlags.NonPublic); |
|||
} |
|||
|
|||
public virtual async Task ExecuteAsync() |
|||
{ |
|||
var worker = (IBackgroundWorker)ServiceProvider.GetService(typeof(TWorker)); |
|||
var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider); |
|||
|
|||
switch (worker) |
|||
{ |
|||
case AsyncPeriodicBackgroundWorkerBase asyncWorker: |
|||
{ |
|||
if (_doWorkAsyncMethod != null) |
|||
{ |
|||
await (Task)_doWorkAsyncMethod.Invoke(asyncWorker, new object[] { workerContext }); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
case PeriodicBackgroundWorkerBase syncWorker: |
|||
{ |
|||
if (_doWorkMethod != null) |
|||
{ |
|||
_doWorkMethod.Invoke(syncWorker, new object[] { workerContext }); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using Hangfire; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System; |
|||
using System.Reflection; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BackgroundWorkers; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundWorkers.Hangfire |
|||
{ |
|||
[Dependency(ReplaceServices = true)] |
|||
public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDependency |
|||
{ |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
public HangfireBackgroundWorkerManager( |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
ServiceProvider = serviceProvider; |
|||
} |
|||
|
|||
public void Add(IBackgroundWorker worker) |
|||
{ |
|||
var timer = worker.GetType() |
|||
.GetProperty("Timer", BindingFlags.NonPublic | BindingFlags.Instance)? |
|||
.GetValue(worker); |
|||
|
|||
if (timer == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var period = timer.GetType() |
|||
.GetProperty("Period", BindingFlags.Public | BindingFlags.Instance)? |
|||
.GetValue(timer)? |
|||
.To<int>(); |
|||
|
|||
if (!period.HasValue) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var adapterType = typeof(HangfireBackgroundWorkerAdapter<>).MakeGenericType(worker.GetType()); |
|||
var workerAdapter = ServiceProvider.GetRequiredService(adapterType) as IHangfireBackgroundWorkerAdapter; |
|||
|
|||
RecurringJob.AddOrUpdate( |
|||
recurringJobId: worker.GetType().FullName, |
|||
methodCall: () => workerAdapter.ExecuteAsync(), |
|||
cronExpression: CronGenerator.FormMilliseconds(period.Value)); |
|||
} |
|||
|
|||
public Task StartAsync(CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task StopAsync(CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BackgroundWorkers; |
|||
|
|||
namespace LINGYUN.Abp.BackgroundWorkers.Hangfire |
|||
{ |
|||
public interface IHangfireBackgroundWorkerAdapter : IBackgroundWorker |
|||
{ |
|||
Task ExecuteAsync(); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\Hangfire\Dashboard\Localization\Resources\en.json" /> |
|||
<None Remove="LINGYUN\Abp\Hangfire\Dashboard\Localization\Resources\zh-Hans.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Hangfire\Dashboard\Localization\Resources\en.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Hangfire\Dashboard\Localization\Resources\zh-Hans.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Authorization" Version="4.3.0" /> |
|||
<PackageReference Include="Volo.Abp.Hangfire" Version="4.3.0" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,36 @@ |
|||
using LINGYUN.Abp.Hangfire.Dashboard.Localization; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Authorization; |
|||
using Volo.Abp.Hangfire; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.Hangfire.Dashboard |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpLocalizationModule), |
|||
typeof(AbpAuthorizationModule), |
|||
typeof(AbpHangfireModule))] |
|||
public class AbpHangfireDashboardModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpHangfireDashboardModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources.Add<HangfireDashboardResource>(); |
|||
}); |
|||
|
|||
context.Services.AddTransient(serviceProvider => |
|||
{ |
|||
var options = serviceProvider.GetRequiredService<AbpHangfireDashboardOptionsProvider>().Get(); |
|||
return context.Services.ExecutePreConfiguredActions(options); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using Hangfire; |
|||
using Hangfire.Dashboard; |
|||
using LINGYUN.Abp.Hangfire.Dashboard.Authorization; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace LINGYUN.Abp.Hangfire.Dashboard |
|||
{ |
|||
public class AbpHangfireDashboardOptionsProvider : ITransientDependency |
|||
{ |
|||
public virtual DashboardOptions Get() |
|||
{ |
|||
return new DashboardOptions |
|||
{ |
|||
Authorization = new IDashboardAuthorizationFilter[] |
|||
{ |
|||
new DashboardAuthorizationFilter() |
|||
}, |
|||
IsReadOnlyFunc = (context) => |
|||
{ |
|||
var httpContext = context.GetHttpContext(); |
|||
var permissionChecker = httpContext.RequestServices.GetRequiredService<IPermissionChecker>(); |
|||
|
|||
return !AsyncHelper.RunSync(async () => |
|||
await permissionChecker.IsGrantedAsync(HangfireDashboardPermissions.Dashboard.ManageJobs)); |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using Hangfire.Annotations; |
|||
using Hangfire.Dashboard; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System.Linq; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace LINGYUN.Abp.Hangfire.Dashboard.Authorization |
|||
{ |
|||
public class DashboardAuthorizationFilter : IDashboardAuthorizationFilter |
|||
{ |
|||
internal readonly static string[] AllowRoutePrefixs = new string[] |
|||
{ |
|||
"/stats", |
|||
"/js", |
|||
"/css", |
|||
"/fonts" |
|||
}; |
|||
public bool Authorize([NotNull] DashboardContext context) |
|||
{ |
|||
if (AllowRoutePrefixs.Any(url => context.Request.Path.StartsWith(url))) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
var httpContext = context.GetHttpContext(); |
|||
var permissionChecker = httpContext.RequestServices.GetRequiredService<IPermissionChecker>(); |
|||
return AsyncHelper.RunSync(async () => |
|||
await permissionChecker.IsGrantedAsync(httpContext.User, HangfireDashboardPermissions.Dashboard.Default)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using LINGYUN.Abp.Hangfire.Dashboard.Localization; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace LINGYUN.Abp.Hangfire.Dashboard.Authorization |
|||
{ |
|||
public class HangfireDashboardPermissionDefinitionProvider : PermissionDefinitionProvider |
|||
{ |
|||
public override void Define(IPermissionDefinitionContext context) |
|||
{ |
|||
var group = context.AddGroup( |
|||
HangfireDashboardPermissions.GroupName, |
|||
L("Permission:Hangfire"), |
|||
MultiTenancySides.Host); // 除非对Hangfire Api进行改造,否则不能区分租户
|
|||
|
|||
var dashboard = group.AddPermission( |
|||
HangfireDashboardPermissions.Dashboard.Default, |
|||
L("Permission:Dashboard"), |
|||
MultiTenancySides.Host); |
|||
|
|||
dashboard.AddChild( |
|||
HangfireDashboardPermissions.Dashboard.ManageJobs, |
|||
L("Permission:ManageJobs"), |
|||
MultiTenancySides.Host); |
|||
} |
|||
|
|||
private static LocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<HangfireDashboardResource>(name); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
namespace LINGYUN.Abp.Hangfire.Dashboard.Authorization |
|||
{ |
|||
public static class HangfireDashboardPermissions |
|||
{ |
|||
public const string GroupName = "Hangfire"; |
|||
|
|||
public static class Dashboard |
|||
{ |
|||
public const string Default = GroupName + ".Dashboard"; |
|||
|
|||
public const string ManageJobs = Default + ".ManageJobs"; |
|||
// TODO: other pages...
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.Hangfire.Dashboard.Localization |
|||
{ |
|||
[LocalizationResourceName("HangfireDashboard")] |
|||
public class HangfireDashboardResource |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Permission:Hangfire": "Hangfire", |
|||
"Permission:Dashboard": "Dashboard" |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"Permission:Hangfire": "Hangfire", |
|||
"Permission:Dashboard": "仪表板" |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using Microsoft.AspNetCore.Http; |
|||
|
|||
namespace Microsoft.AspNetCore.Builder |
|||
{ |
|||
public static class ApplicationBuilderAbpHangfireAuthoricationMiddlewareExtension |
|||
{ |
|||
public static IApplicationBuilder UseHangfireAuthorication(this IApplicationBuilder app) |
|||
{ |
|||
return app.UseMiddleware<HangfireAuthoricationMiddleware>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Microsoft.AspNetCore.Http |
|||
{ |
|||
public class HangfireAuthoricationMiddleware : IMiddleware, ITransientDependency |
|||
{ |
|||
public async Task InvokeAsync(HttpContext context, RequestDelegate next) |
|||
{ |
|||
// 通过 iframe 加载页面的话,需要手动传递 access_token 到参数列表
|
|||
if (context.Request.Path.StartsWithSegments("/hangfire") && |
|||
context.User.Identity?.IsAuthenticated != true) |
|||
{ |
|||
if (context.Request.Query.TryGetValue("access_token", out var accessTokens)) |
|||
{ |
|||
context.Request.Headers.Add("Authorization", accessTokens); |
|||
context.Response.Cookies.Append("access_token", accessTokens); |
|||
} |
|||
else if (context.Request.Cookies.TryGetValue("access_token", out string tokens)) |
|||
{ |
|||
context.Request.Headers.Add("Authorization", tokens); |
|||
} |
|||
} |
|||
await next(context); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.Notifications.Workers |
|||
{ |
|||
public class Class1 |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Notifications\LINGYUN.Abp.Notifications.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace LINGYUN.Abp.Notifications.Workers.LINGYUN.Abp.Notifications.Workers |
|||
{ |
|||
class AbpNotificationsWorkersModule |
|||
{ |
|||
} |
|||
} |
|||
@ -1,73 +0,0 @@ |
|||
using Hangfire; |
|||
using Hangfire.Annotations; |
|||
using Hangfire.Dashboard; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using NUglify.Helpers; |
|||
using System.Linq; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace LINGYUN.Abp.MessageService.Authorization |
|||
{ |
|||
public class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter |
|||
{ |
|||
protected string[] AllowGrantPath { get; } |
|||
public HangfireDashboardAuthorizationFilter() |
|||
{ |
|||
AllowGrantPath = new string[] { "/css", "/js", "/fonts", "/stats" }; |
|||
} |
|||
|
|||
public bool Authorize([NotNull] DashboardContext context) |
|||
{ |
|||
// 放行路径
|
|||
if (AllowGrantPath.Contains(context.Request.Path)) |
|||
{ |
|||
return true; |
|||
} |
|||
var httpContext = context.GetHttpContext(); |
|||
|
|||
var options = httpContext.RequestServices.GetService<IOptions<HangfireDashboardRouteOptions>>()?.Value; |
|||
|
|||
if (options != null) |
|||
{ |
|||
// 白名单检查
|
|||
if (!context.Request.RemoteIpAddress.IsNullOrWhiteSpace() |
|||
&& options.IpAllow(context.Request.RemoteIpAddress)) |
|||
{ |
|||
return true; |
|||
} |
|||
// 请求路径对应的权限检查
|
|||
// TODO: 怎么来传递用户身份令牌?
|
|||
var permission = options.GetPermission(context.Request.Path); |
|||
if (!permission.IsNullOrWhiteSpace()) |
|||
{ |
|||
var permissionChecker = httpContext.RequestServices.GetRequiredService<IPermissionChecker>(); |
|||
return AsyncHelper.RunSync(async () => await permissionChecker.IsGrantedAsync(permission)); |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
// 类型相同就行了
|
|||
return GetType().FullName.GetHashCode(); |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (obj == null) |
|||
{ |
|||
return false; |
|||
} |
|||
// 类型相同就行了
|
|||
if (GetType().Equals(obj.GetType())) |
|||
{ |
|||
return true; |
|||
} |
|||
return base.Equals(obj); |
|||
} |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
using LINGYUN.Abp.Notifications; |
|||
using Microsoft.Extensions.Logging; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BackgroundJobs; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.MessageService.BackgroundJobs |
|||
{ |
|||
internal class NotificationCleanupExpritionJob : AsyncBackgroundJob<NotificationCleanupExpritionJobArgs>, ITransientDependency |
|||
{ |
|||
protected INotificationStore Store { get; } |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
public NotificationCleanupExpritionJob( |
|||
INotificationStore store, |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
Store = store; |
|||
ServiceProvider = serviceProvider; |
|||
} |
|||
|
|||
public override async Task ExecuteAsync(NotificationCleanupExpritionJobArgs args) |
|||
{ |
|||
try |
|||
{ |
|||
Logger.LogDebug("Before cleanup exprition jobs..."); |
|||
await Store.DeleteNotificationAsync(args.Count); |
|||
Logger.LogDebug("Exprition jobs cleanup job was successful..."); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogWarning("Exprition jobs cleanup job was failed..."); |
|||
Logger.LogWarning("Error:{0}", ex.Message); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
using Volo.Abp.BackgroundJobs; |
|||
|
|||
namespace LINGYUN.Abp.MessageService.BackgroundJobs |
|||
{ |
|||
[BackgroundJobName("定时清理过期通知消息任务")] |
|||
internal class NotificationCleanupExpritionJobArgs |
|||
{ |
|||
/// <summary>
|
|||
/// 清理大小
|
|||
/// </summary>
|
|||
public int Count { get; set; } |
|||
|
|||
public NotificationCleanupExpritionJobArgs() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public NotificationCleanupExpritionJobArgs(int count = 200) |
|||
{ |
|||
Count = count; |
|||
} |
|||
} |
|||
} |
|||
@ -1,74 +0,0 @@ |
|||
using Hangfire.Dashboard; |
|||
using JetBrains.Annotations; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp; |
|||
|
|||
namespace Hangfire |
|||
{ |
|||
public static class DashboardOptionsExtensions |
|||
{ |
|||
public static DashboardOptions AddAuthorization( |
|||
[NotNull] this DashboardOptions options, |
|||
[NotNull] IDashboardAuthorizationFilter authorizationFilter) |
|||
{ |
|||
Check.NotNull(options, nameof(options)); |
|||
Check.NotNull(authorizationFilter, nameof(authorizationFilter)); |
|||
|
|||
List<IDashboardAuthorizationFilter> filters = new List<IDashboardAuthorizationFilter>(); |
|||
filters.AddRange(options.Authorization); |
|||
filters.AddIfNotContains(authorizationFilter); |
|||
|
|||
options.Authorization = filters; |
|||
|
|||
return options; |
|||
} |
|||
|
|||
public static DashboardOptions AddAuthorizations( |
|||
[NotNull] this DashboardOptions options, |
|||
[NotNull] IEnumerable<IDashboardAuthorizationFilter> authorizationFilters) |
|||
{ |
|||
Check.NotNull(options, nameof(options)); |
|||
Check.NotNull(authorizationFilters, nameof(authorizationFilters)); |
|||
|
|||
List<IDashboardAuthorizationFilter> filters = new List<IDashboardAuthorizationFilter>(); |
|||
filters.AddRange(options.Authorization); |
|||
filters.AddIfNotContains(authorizationFilters); |
|||
|
|||
options.Authorization = filters; |
|||
|
|||
return options; |
|||
} |
|||
|
|||
public static DashboardOptions UseAuthorization( |
|||
[NotNull] this DashboardOptions options, |
|||
[NotNull] IDashboardAuthorizationFilter authorizationFilter) |
|||
{ |
|||
Check.NotNull(options, nameof(options)); |
|||
Check.NotNull(authorizationFilter, nameof(authorizationFilter)); |
|||
|
|||
List<IDashboardAuthorizationFilter> filters = new List<IDashboardAuthorizationFilter> |
|||
{ |
|||
authorizationFilter |
|||
}; |
|||
|
|||
options.Authorization = filters; |
|||
|
|||
return options; |
|||
} |
|||
|
|||
public static DashboardOptions UseAuthorizations( |
|||
[NotNull] this DashboardOptions options, |
|||
[NotNull] IEnumerable<IDashboardAuthorizationFilter> authorizationFilters) |
|||
{ |
|||
Check.NotNull(options, nameof(options)); |
|||
Check.NotNull(authorizationFilters, nameof(authorizationFilters)); |
|||
|
|||
List<IDashboardAuthorizationFilter> filters = new List<IDashboardAuthorizationFilter>(); |
|||
filters.AddRange(authorizationFilters); |
|||
|
|||
options.Authorization = filters; |
|||
|
|||
return options; |
|||
} |
|||
} |
|||
} |
|||
@ -1,47 +0,0 @@ |
|||
using JetBrains.Annotations; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using System; |
|||
using Volo.Abp; |
|||
|
|||
namespace Hangfire |
|||
{ |
|||
public static class HangfireApplicationBuilderExtensions |
|||
{ |
|||
public static IApplicationBuilder UseHangfireJwtToken( |
|||
[NotNull] this IApplicationBuilder app) |
|||
{ |
|||
return app.UseMiddleware<HangfireJwtTokenMiddleware>(); |
|||
} |
|||
|
|||
public static IApplicationBuilder UseHangfireDashboard( |
|||
[NotNull] this IApplicationBuilder app, |
|||
[CanBeNull] Action<DashboardOptions> setup = null) |
|||
{ |
|||
Check.NotNull(app, nameof(app)); |
|||
return app.UseHangfireDashboard("/hangfire", setup, null); |
|||
} |
|||
|
|||
public static IApplicationBuilder UseHangfireDashboard( |
|||
[NotNull] this IApplicationBuilder app, |
|||
[CanBeNull] string pathMatch = "/hangfire", |
|||
[CanBeNull] Action<DashboardOptions> setup = null) |
|||
{ |
|||
Check.NotNull(app, nameof(app)); |
|||
return app.UseHangfireDashboard(pathMatch, setup, null); |
|||
} |
|||
|
|||
public static IApplicationBuilder UseHangfireDashboard( |
|||
[NotNull] this IApplicationBuilder app, |
|||
[CanBeNull] string pathMatch = "/hangfire", |
|||
[CanBeNull] Action<DashboardOptions> setup = null, |
|||
[CanBeNull] JobStorage storage = null) |
|||
{ |
|||
Check.NotNull(app, nameof(app)); |
|||
|
|||
var options = new DashboardOptions(); |
|||
setup?.Invoke(options); |
|||
|
|||
return app.UseHangfireDashboard(pathMatch: pathMatch, options: options, storage: storage); |
|||
} |
|||
} |
|||
} |
|||
@ -1,74 +0,0 @@ |
|||
using LINGYUN.Abp.MessageService.Permissions; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Hangfire |
|||
{ |
|||
public class HangfireDashboardRouteOptions |
|||
{ |
|||
public IList<string> AllowFrameOrigins { get; } |
|||
/// <summary>
|
|||
/// 白名单
|
|||
/// 添加网关地址
|
|||
/// </summary>
|
|||
public IList<string> WhiteList { get; } |
|||
public IDictionary<string, string> RoutePermissions { get; } |
|||
public HangfireDashboardRouteOptions() |
|||
{ |
|||
WhiteList = new List<string>(); |
|||
AllowFrameOrigins = new List<string>(); |
|||
RoutePermissions = new Dictionary<string, string>(); |
|||
InitDefaultRoutes(); |
|||
WithWhite("127.0.0.1"); |
|||
WithWhite("::1"); |
|||
} |
|||
|
|||
public bool IpAllow(string ipaddress) |
|||
{ |
|||
return WhiteList.Any(ip => ip == ipaddress); |
|||
} |
|||
|
|||
public void WithWhite(params string[] wgites) |
|||
{ |
|||
WhiteList.AddIfNotContains(wgites); |
|||
} |
|||
|
|||
public void WithOrigins(params string[] origins) |
|||
{ |
|||
AllowFrameOrigins.AddIfNotContains(origins); |
|||
} |
|||
|
|||
public void WithPermission(string route, string permission) |
|||
{ |
|||
RoutePermissions.Add(route, permission); |
|||
} |
|||
|
|||
public string GetPermission(string route) |
|||
{ |
|||
var permission = RoutePermissions |
|||
.Where(x => x.Key.StartsWith(route)) |
|||
.Select(x => x.Value) |
|||
.FirstOrDefault(); |
|||
|
|||
return permission; |
|||
} |
|||
|
|||
private void InitDefaultRoutes() |
|||
{ |
|||
WithPermission("/hangfire", MessageServicePermissions.Hangfire.Default); |
|||
WithPermission("/stats", MessageServicePermissions.Hangfire.Default); |
|||
WithPermission("/servers", MessageServicePermissions.Hangfire.Default); |
|||
WithPermission("/retries", MessageServicePermissions.Hangfire.Default); |
|||
WithPermission("/recurring", MessageServicePermissions.Hangfire.Default); |
|||
WithPermission("/jobs/enqueued", MessageServicePermissions.Hangfire.ManageQueue); |
|||
WithPermission("/jobs/processing", MessageServicePermissions.Hangfire.ManageQueue); |
|||
WithPermission("/jobs/scheduled", MessageServicePermissions.Hangfire.ManageQueue); |
|||
WithPermission("/jobs/failed", MessageServicePermissions.Hangfire.ManageQueue); |
|||
WithPermission("/jobs/deleted", MessageServicePermissions.Hangfire.ManageQueue); |
|||
WithPermission("/jobs/awaiting", MessageServicePermissions.Hangfire.ManageQueue); |
|||
WithPermission("/jobs/actions", MessageServicePermissions.Hangfire.ManageQueue); |
|||
WithPermission("/jobs/details", MessageServicePermissions.Hangfire.ManageQueue); |
|||
} |
|||
} |
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Hangfire |
|||
{ |
|||
public class HangfireJwtTokenMiddleware : IMiddleware, ITransientDependency |
|||
{ |
|||
public async Task InvokeAsync(HttpContext context, RequestDelegate next) |
|||
{ |
|||
// 通过 iframe 加载页面的话,需要手动传递 access_token 到参数列表
|
|||
if (context.Request.Path.StartsWithSegments("/hangfire") && context.User.Identity?.IsAuthenticated != true) |
|||
{ |
|||
if (context.Request.Query.TryGetValue("access_token", out var accessTokens)) |
|||
{ |
|||
context.Request.Headers.Add("Authorization", accessTokens); |
|||
} |
|||
var options = context.RequestServices.GetService<IOptions<HangfireDashboardRouteOptions>>()?.Value; |
|||
if (options != null && options.AllowFrameOrigins.Count > 0) |
|||
{ |
|||
// 跨域 iframe
|
|||
context.Response.Headers.TryAdd("X-Frame-Options", $"\"ALLOW-FROM {options.AllowFrameOrigins.JoinAsString(",")}\""); |
|||
} |
|||
} |
|||
await next(context); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue