feat: 计划作业执行记录 (#89)

顶部通栏黑夜模式开关
计划作业快捷预览面板
This commit is contained in:
2024-02-18 14:43:22 +08:00
committed by GitHub
parent 6f32acaacf
commit 6f89015198
71 changed files with 844 additions and 229 deletions

View File

@ -8,21 +8,13 @@ namespace NetAdmin.Application.Services;
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TLogger">日志类型</typeparam>
public abstract class RepositoryService<TEntity, TLogger> : ServiceBase<TLogger>
public abstract class RepositoryService<TEntity, TLogger>(DefaultRepository<TEntity> rpo) : ServiceBase<TLogger>
where TEntity : EntityBase
{
/// <summary>
/// Initializes a new instance of the <see cref="RepositoryService{TEntity, TLogger}" /> class.
/// </summary>
protected RepositoryService(DefaultRepository<TEntity> rpo) //
{
Rpo = rpo;
}
/// <summary>
/// 默认仓储
/// </summary>
protected DefaultRepository<TEntity> Rpo { get; }
protected DefaultRepository<TEntity> Rpo => rpo;
/// <summary>
/// 启用级联保存

View File

@ -5,21 +5,13 @@ namespace NetAdmin.Cache;
/// <summary>
/// 缓存基类
/// </summary>
public abstract class CacheBase<TCacheContainer, TService> : ICache<TCacheContainer, TService>
public abstract class CacheBase<TCacheContainer, TService>(TCacheContainer cache, TService service)
: ICache<TCacheContainer, TService>
where TService : IService
{
/// <summary>
/// Initializes a new instance of the <see cref="CacheBase{TCacheLoad, TService}" /> class.
/// </summary>
protected CacheBase(TCacheContainer cache, TService service)
{
Cache = cache;
Service = service;
}
/// <inheritdoc />
public TCacheContainer Cache => cache;
/// <inheritdoc />
public TCacheContainer Cache { get; }
/// <inheritdoc />
public TService Service { get; }
public TService Service => service;
}

View File

@ -6,15 +6,10 @@ namespace NetAdmin.Cache;
/// <summary>
/// 分布式缓存
/// </summary>
public abstract class DistributedCache<TService> : CacheBase<IDistributedCache, TService>
public abstract class DistributedCache<TService>(IDistributedCache cache, TService service)
: CacheBase<IDistributedCache, TService>(cache, service)
where TService : IService
{
/// <summary>
/// Initializes a new instance of the <see cref="DistributedCache{TService}" /> class.
/// </summary>
protected DistributedCache(IDistributedCache cache, TService service) //
: base(cache, service) { }
/// <summary>
/// 创建缓存
/// </summary>

View File

@ -5,12 +5,6 @@ namespace NetAdmin.Cache;
/// <summary>
/// 内存缓存
/// </summary>
public abstract class MemoryCache<TService> : CacheBase<IMemoryCache, TService>
where TService : IService
{
/// <summary>
/// Initializes a new instance of the <see cref="MemoryCache{TService}" /> class.
/// </summary>
protected MemoryCache(IMemoryCache cache, TService service) //
: base(cache, service) { }
}
public abstract class MemoryCache<TService>(IMemoryCache cache, TService service)
: CacheBase<IMemoryCache, TService>(cache, service)
where TService : IService;

View File

@ -8,11 +8,14 @@ public abstract record DataAbstraction
/// <summary>
/// 如果数据校验失败,抛出异常
/// </summary>
/// <exception cref="NetAdminInvalidInputException">NetAdminInvalidInputException</exception>
/// <exception cref="NetAdminValidateException">NetAdminValidateException</exception>
public void ThrowIfInvalid()
{
if (!this.TryValidate().IsValid) {
throw new NetAdminInvalidInputException(Ln.);
var validationResult = this.TryValidate();
if (!validationResult.IsValid) {
throw new NetAdminValidateException(validationResult.ValidationResults.ToDictionary( //
x => x.MemberNames.First() //
, x => new[] { x.ErrorMessage }));
}
}

View File

@ -20,12 +20,12 @@ public abstract record VersionEntity<T> : LiteVersionEntity<T>, IFieldModifiedUs
/// <inheritdoc />
[JsonIgnore]
[Column(CanUpdate = false, Position = -1)]
public long? CreatedUserId { get; init; }
public virtual long? CreatedUserId { get; init; }
/// <inheritdoc />
[JsonIgnore]
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)]
public string CreatedUserName { get; init; }
public virtual string CreatedUserName { get; init; }
/// <inheritdoc cref="IFieldPrimary{T}.Id" />
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
@ -34,10 +34,10 @@ public abstract record VersionEntity<T> : LiteVersionEntity<T>, IFieldModifiedUs
/// <inheritdoc cref="IFieldModifiedUser.ModifiedUserId" />
[JsonIgnore]
[Column(CanInsert = false, Position = -1)]
public long? ModifiedUserId { get; init; }
public virtual long? ModifiedUserId { get; init; }
/// <inheritdoc cref="IFieldModifiedUser.ModifiedUserName" />
[JsonIgnore]
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanInsert = false, Position = -1)]
public string ModifiedUserName { get; init; }
public virtual string ModifiedUserName { get; init; }
}

View File

@ -98,6 +98,13 @@ public record Sys_Job : VersionEntity, IFieldEnabled, IFieldSummary
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_255)]
public virtual string Summary { get; init; }
/// <summary>
/// 执行用户
/// </summary>
[Navigate(nameof(UserId))]
[JsonIgnore]
public Sys_User User { get; init; }
/// <summary>
/// 执行用户编号
/// </summary>

View File

@ -15,68 +15,68 @@ public record Sys_JobRecord : LiteImmutableEntity
/// </summary>
[Column]
[JsonIgnore]
public long Duration { get; init; }
public virtual long Duration { get; init; }
/// <summary>
/// 请求方法
/// </summary>
[JsonIgnore]
[Column]
public HttpMethods HttpMethod { get; init; }
public virtual HttpMethods HttpMethod { get; init; }
/// <summary>
/// HTTP 状态码
/// </summary>
[Column]
[JsonIgnore]
public HttpStatusCode HttpStatusCode { get; init; }
public virtual int HttpStatusCode { get; init; }
/// <summary>
/// 作业编号
/// </summary>
[Column]
[JsonIgnore]
public long JobId { get; init; }
public virtual long JobId { get; init; }
/// <summary>
/// 请求体
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_255)]
[JsonIgnore]
public string RequestBody { get; init; }
public virtual string RequestBody { get; init; }
/// <summary>
/// 请求头
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_255)]
[JsonIgnore]
public string RequestHeader { get; init; }
public virtual string RequestHeader { get; init; }
/// <summary>
/// 请求的网络地址
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_127)]
[JsonIgnore]
public string RequestUrl { get; init; }
public virtual string RequestUrl { get; init; }
/// <summary>
/// 响应体
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_255)]
[JsonIgnore]
public string ResponseBody { get; init; }
public virtual string ResponseBody { get; init; }
/// <summary>
/// 响应头
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_255)]
[JsonIgnore]
public string ResponseHeader { get; init; }
public virtual string ResponseHeader { get; init; }
/// <summary>
/// 执行时间编号
/// </summary>
[Column]
[JsonIgnore]
public long TimeId { get; init; }
public virtual long TimeId { get; init; }
}

View File

@ -28,7 +28,7 @@ public record Sys_RequestLog : ImmutableEntity, IFieldCreatedClient
/// </summary>
[Column(Position = -1)]
[JsonIgnore]
public virtual int? CreatedClientIp { get; init; }
public int? CreatedClientIp { get; init; }
/// <summary>
/// 创建者来源地址

View File

@ -6,7 +6,7 @@ namespace NetAdmin.Domain.DbMaps.Sys;
/// 角色-接口映射表
/// </summary>
[Table(Name = Chars.FLG_TABLE_NAME_PREFIX + nameof(Sys_RoleApi))]
public sealed record Sys_RoleApi : ImmutableEntity
public record Sys_RoleApi : ImmutableEntity
{
/// <summary>
/// 关联的接口

View File

@ -7,7 +7,7 @@ namespace NetAdmin.Domain.DbMaps.Sys;
/// </summary>
[Table(Name = Chars.FLG_TABLE_NAME_PREFIX + nameof(Sys_RoleDept))]
[Index($"idx_{{tablename}}_{nameof(RoleId)}_{nameof(DeptId)}", $"{nameof(RoleId)},{nameof(DeptId)}", true)]
public sealed record Sys_RoleDept : ImmutableEntity
public record Sys_RoleDept : ImmutableEntity
{
/// <summary>
/// 关联的部门

View File

@ -6,7 +6,7 @@ namespace NetAdmin.Domain.DbMaps.Sys;
/// 用户-角色映射表
/// </summary>
[Table(Name = Chars.FLG_TABLE_NAME_PREFIX + nameof(Sys_UserRole))]
public sealed record Sys_UserRole : VersionEntity
public record Sys_UserRole : VersionEntity
{
/// <summary>
/// 关联的角色

View File

@ -5,7 +5,7 @@ namespace NetAdmin.Domain.Dto.Dependency;
/// <summary>
/// 动态过滤条件
/// </summary>
public record DynamicFilterInfo : DataAbstraction
public sealed record DynamicFilterInfo : DataAbstraction
{
/// <summary>
/// 字段名

View File

@ -6,10 +6,10 @@ namespace NetAdmin.Domain.Dto.Sys.Dept;
/// <summary>
/// 响应:查询部门
/// </summary>
public record QueryDeptRsp : Sys_Dept
public sealed record QueryDeptRsp : Sys_Dept
{
/// <inheritdoc cref="Sys_Dept.Children" />
public new virtual IEnumerable<QueryDeptRsp> Children { get; init; }
public new IEnumerable<QueryDeptRsp> Children { get; init; }
/// <inheritdoc cref="IFieldCreatedTime.CreatedTime" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]

View File

@ -1,5 +1,6 @@
using NetAdmin.Domain.DbMaps.Dependency.Fields;
using NetAdmin.Domain.DbMaps.Sys;
using NetAdmin.Domain.Dto.Sys.User;
using NetAdmin.Domain.Enums.Sys;
using HttpMethods = NetAdmin.Domain.Enums.HttpMethods;
@ -14,6 +15,14 @@ public sealed record QueryJobRsp : Sys_Job
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override DateTime CreatedTime { get; init; }
/// <inheritdoc cref="IFieldCreatedUser.CreatedUserId" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override long? CreatedUserId { get; init; }
/// <inheritdoc cref="IFieldCreatedUser.CreatedUserName" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string CreatedUserName { get; init; }
/// <inheritdoc cref="IFieldEnabled.Enabled" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override bool Enabled { get; init; }
@ -42,6 +51,18 @@ public sealed record QueryJobRsp : Sys_Job
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override HttpStatusCode? LastStatusCode { get; init; }
/// <inheritdoc cref="IFieldModifiedTime.ModifiedTime" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override DateTime? ModifiedTime { get; init; }
/// <inheritdoc cref="IFieldModifiedUser.ModifiedUserId" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override long? ModifiedUserId { get; init; }
/// <inheritdoc cref="IFieldModifiedUser.ModifiedUserName" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string ModifiedUserName { get; init; }
/// <inheritdoc cref="Sys_Job.NextExecTime" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override DateTime? NextExecTime { get; init; }
@ -70,6 +91,9 @@ public sealed record QueryJobRsp : Sys_Job
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string Summary { get; init; }
/// <inheritdoc cref="Sys_Job.User" />
public new QueryUserRsp User { get; init; }
/// <inheritdoc cref="Sys_Job.UserId" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long UserId { get; init; }

View File

@ -1,5 +1,6 @@
using NetAdmin.Domain.DbMaps.Dependency.Fields;
using NetAdmin.Domain.DbMaps.Sys;
using HttpMethods = NetAdmin.Domain.Enums.HttpMethods;
namespace NetAdmin.Domain.Dto.Sys.JobRecord;
@ -8,7 +9,51 @@ namespace NetAdmin.Domain.Dto.Sys.JobRecord;
/// </summary>
public sealed record QueryJobRecordRsp : Sys_JobRecord
{
/// <inheritdoc cref="IFieldCreatedTime.CreatedTime" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override DateTime CreatedTime { get; init; }
/// <inheritdoc cref="Sys_JobRecord.Duration" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Duration { get; init; }
/// <inheritdoc cref="Sys_JobRecord.HttpMethod" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override HttpMethods HttpMethod { get; init; }
/// <inheritdoc cref="Sys_JobRecord.HttpStatusCode" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override int HttpStatusCode { get; init; }
/// <inheritdoc cref="IFieldPrimary{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; }
/// <inheritdoc cref="Sys_JobRecord.JobId" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long JobId { get; init; }
/// <inheritdoc cref="Sys_JobRecord.RequestBody" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string RequestBody { get; init; }
/// <inheritdoc cref="Sys_JobRecord.RequestHeader" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string RequestHeader { get; init; }
/// <inheritdoc cref="Sys_JobRecord.RequestUrl" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string RequestUrl { get; init; }
/// <inheritdoc cref="Sys_JobRecord.ResponseBody" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string ResponseBody { get; init; }
/// <inheritdoc cref="Sys_JobRecord.ResponseHeader" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string ResponseHeader { get; init; }
/// <inheritdoc cref="Sys_JobRecord.TimeId" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long TimeId { get; init; }
}

View File

@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.Tool;
/// <summary>
/// 响应:获取模块信息
/// </summary>
public record GetModulesRsp : DataAbstraction
public sealed record GetModulesRsp : DataAbstraction
{
/// <summary>
/// 模块名称

View File

@ -6,7 +6,7 @@ namespace NetAdmin.Domain.Dto.Sys.User;
/// <summary>
/// 请求:创建用户
/// </summary>
public record CreateUserReq : CreateUpdateUserReq, IRegister
public sealed record CreateUserReq : CreateUpdateUserReq, IRegister
{
/// <inheritdoc cref="CreateUpdateUserReq.PasswordText" />
[Required(ErrorMessageResourceType = typeof(Ln), ErrorMessageResourceName = nameof(Ln.密码不能为空))]

View File

@ -5,7 +5,7 @@ namespace NetAdmin.Domain.Dto.Sys.User;
/// <summary>
/// 请求:密码登录
/// </summary>
public record LoginByPwdReq : DataAbstraction
public sealed record LoginByPwdReq : DataAbstraction
{
/// <summary>
/// 用户名、手机号、邮箱

View File

@ -5,4 +5,4 @@ namespace NetAdmin.Domain.Dto.Sys.User;
/// <summary>
/// 请求:短信登录
/// </summary>
public record LoginBySmsReq : VerifySmsCodeReq;
public sealed record LoginBySmsReq : VerifySmsCodeReq;

View File

@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.User;
/// <summary>
/// 响应:密码登录
/// </summary>
public record LoginRsp : DataAbstraction
public sealed record LoginRsp : DataAbstraction
{
/// <summary>
/// 访问令牌

View File

@ -7,7 +7,7 @@ namespace NetAdmin.Domain.Dto.Sys.User;
/// <summary>
/// 请求:注册用户
/// </summary>
public record RegisterUserReq : Sys_User
public sealed record RegisterUserReq : Sys_User
{
/// <summary>
/// 密码
@ -15,7 +15,7 @@ public record RegisterUserReq : Sys_User
[Required(ErrorMessageResourceType = typeof(Ln), ErrorMessageResourceName = nameof(Ln.密码不能为空))]
[Password]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public virtual string PasswordText { get; init; }
public string PasswordText { get; init; }
/// <summary>
/// 角色编号列表

View File

@ -7,7 +7,7 @@ namespace NetAdmin.Domain.Dto.Sys.User;
/// <summary>
/// 响应:当前用户信息
/// </summary>
public record UserInfoRsp : QueryUserRsp, IRegister
public sealed record UserInfoRsp : QueryUserRsp, IRegister
{
/// <inheritdoc cref="Sys_User.Dept" />
public override QueryDeptRsp Dept { get; init; }

View File

@ -18,7 +18,7 @@ public sealed record SqlCommandAfterEvent : SqlCommandBeforeEvent
/// <summary>
/// 耗时(单位:微秒)
/// </summary>
public long ElapsedMicroseconds { get; set; }
public long ElapsedMicroseconds { get; init; }
/// <inheritdoc />
public override string ToString()

View File

@ -5,19 +5,11 @@ namespace NetAdmin.Host.BackgroundRunning;
/// <summary>
/// 轮询工作
/// </summary>
public abstract class PollingWork<TWorkData> : WorkBase<TWorkData>
public abstract class PollingWork<TWorkData>(TWorkData workData) : WorkBase<TWorkData>
where TWorkData : DataAbstraction
{
/// <summary>
/// Initializes a new instance of the <see cref="PollingWork{TWorkData}" /> class.
/// </summary>
protected PollingWork(TWorkData workData)
{
WorkData = workData;
}
/// <summary>
/// 工作数据
/// </summary>
protected TWorkData WorkData { get; }
protected TWorkData WorkData => workData;
}

View File

@ -54,7 +54,7 @@ public abstract class WorkBase<TLogger>
/// 通用工作流
/// </summary>
/// <exception cref="NetAdminGetLockerException">加锁失败异常</exception>
protected async ValueTask WorkflowAsync(bool singleInstance, CancellationToken cancelToken)
protected virtual async ValueTask WorkflowAsync(bool singleInstance, CancellationToken cancelToken)
{
if (singleInstance) {
// 加锁

View File

@ -6,20 +6,12 @@ namespace NetAdmin.Host.Controllers;
/// <summary>
/// 控制器基类
/// </summary>
public abstract class ControllerBase<TCache, TService> : IDynamicApiController
public abstract class ControllerBase<TCache, TService>(TCache cache) : IDynamicApiController
where TCache : ICache<IDistributedCache, TService> //
where TService : IService
{
/// <summary>
/// Initializes a new instance of the <see cref="ControllerBase{TCache, TService}" /> class.
/// </summary>
protected ControllerBase(TCache cache)
{
Cache = cache;
}
/// <summary>
/// 关联的缓存
/// </summary>
protected TCache Cache { get; }
protected TCache Cache => cache;
}

View File

@ -24,8 +24,11 @@ public abstract class ApiResultHandler<T>
public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata)
{
var lineException = context.Exception switch { NetAdminException ex => ex, _ => null };
var errorCode = lineException?.Code ?? ErrorCodes.Unhandled;
var result = RestfulResult(errorCode, metadata.Data, lineException?.Message ?? errorCode.ResDesc<ErrorCodes>());
var errorCode = lineException?.Code ?? ErrorCodes.Unhandled;
var result = RestfulResult(errorCode, metadata.Data
, lineException is NetAdminValidateException vEx
? vEx.ValidateResults
: lineException?.Message ?? errorCode.ResDesc<ErrorCodes>());
SetErrorCodeToHeader(context.HttpContext, errorCode);

View File

@ -16,6 +16,7 @@ public static class Numbers
public const long DIC_CATALOG_ID_GEO_AREA = 379794295185413; // 字典目录编号-行政区划字典
public const int HEART_TIMEOUT_SECS = 600; // 心跳超时时间
public const int HTTP_STATUS_BIZ_FAIL = 900; // Http状态码-业务异常
public const int JOB_TIMEOUT_SECS = 600; // 作业超时时间
public const int QUERY_DEF_PAGE_SIZE = 20; // 分页查询默认的页容量
public const int QUERY_LIMIT = 100; // 非分页查询允许返回的最大条数
public const int QUERY_MAX_PAGE_NO = 1000; // 分页查询允许最大的页码

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Infrastructure.Exceptions;
/// <summary>
/// 验证失败异常
/// </summary>
/// <remarks>
/// 手动调用模型验证方法抛出
/// </remarks>
#pragma warning disable RCS1194
public sealed class NetAdminValidateException(Dictionary<string, string[]> validateResults)
: NetAdminException(ErrorCodes.InvalidInput)
#pragma warning restore RCS1194
{
/// <summary>
/// 验证结果
/// </summary>
public Dictionary<string, string[]> ValidateResults { get; } = validateResults;
}

View File

@ -13,25 +13,21 @@ public static class HttpRequestPartExtensions
public static HttpRequestPart SetLog<T>(this HttpRequestPart me, ILogger<T> logger
, Func<string, string> bodyHandle = null)
{
#pragma warning disable S1172
Task RequestHandle(HttpClient _, HttpRequestMessage req)
Task RequestHandleAsync(HttpClient _, HttpRequestMessage req)
{
return req.LogAsync(logger);
}
Task ExceptionHandle(HttpClient _, HttpResponseMessage rsp, string errors)
Task ExceptionHandleAsync(HttpClient _, HttpResponseMessage rsp, string errors)
{
return rsp.LogExceptionAsync(errors, logger, bodyHandle);
}
Task ResponseHandle(HttpClient _, HttpResponseMessage rsp)
Task ResponseHandleAsync(HttpClient _, HttpResponseMessage rsp)
{
return rsp.LogAsync(logger, bodyHandle);
}
#pragma warning restore S1172
return me.OnRequesting(RequestHandle).OnResponsing(ResponseHandle).OnException(ExceptionHandle);
return me.OnRequesting(RequestHandleAsync).OnResponsing(ResponseHandleAsync).OnException(ExceptionHandleAsync);
}
}

View File

@ -5,7 +5,8 @@ namespace NetAdmin.Infrastructure.Extensions;
/// </summary>
public static class StringExtensions
{
private static readonly Regex _regex = new("Options$");
private static readonly Regex _regex = new("Options$");
private static readonly Regex _regex2 = new("Async$");
/// <summary>
/// object -> json
@ -23,6 +24,16 @@ public static class StringExtensions
return me.Object(toType, GlobalStatic.JsonSerializerOptions);
}
/// <summary>
/// 去掉尾部字符串“Async”
/// </summary>
#pragma warning disable RCS1047, ASA002, VSTHRD200
public static string TrimEndAsync(this string me)
#pragma warning restore VSTHRD200, ASA002, RCS1047
{
return _regex2.Replace(me, string.Empty);
}
/// <summary>
/// 去掉尾部字符串“Options”
/// </summary>

View File

@ -6,14 +6,14 @@
<Import Project="$(SolutionDir)/build/copy.pkg.xml.comment.files.targets"/>
<Import Project="$(SolutionDir)/build/prebuild.targets"/>
<ItemGroup>
<PackageReference Include="Cronos" Version="0.8.2"/>
<PackageReference Include="Cronos" Version="0.8.3"/>
<PackageReference Include="FreeSql.DbContext.NS" Version="3.2.810-preview20231229-ns1"/>
<PackageReference Include="FreeSql.Provider.Sqlite.NS" Version="3.2.810-preview20231229-ns1"/>
<PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.9.1.24"/>
<PackageReference Include="Furion.Extras.ObjectMapper.Mapster.NS" Version="4.9.1.24-ns1"/>
<PackageReference Include="Furion.Pure.NS" Version="4.9.1.24-ns1"/>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.1"/>
<PackageReference Include="Minio" Version="6.0.1"/>
<PackageReference Include="Minio" Version="6.0.2"/>
<PackageReference Include="NSExt" Version="2.0.11"/>
<PackageReference Include="RedLock.net" Version="2.3.2"/>
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0"/>

View File

@ -1,6 +1,7 @@
using NetAdmin.Application.Modules;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.Job;
using NetAdmin.Domain.Dto.Sys.JobRecord;
namespace NetAdmin.SysComponent.Application.Modules.Sys;
@ -13,6 +14,16 @@ public interface IJobModule : ICrudModule<CreateJobReq, QueryJobRsp // 创建类
, DelReq // 删除类型
>
{
/// <summary>
/// 获取单个作业记录
/// </summary>
Task<QueryJobRecordRsp> RecordGetAsync(QueryJobRecordReq req);
/// <summary>
/// 分页查询作业记录
/// </summary>
Task<PagedQueryRsp<QueryJobRecordRsp>> RecordPagedQueryAsync(PagedQueryReq<QueryJobRecordReq> req);
/// <summary>
/// 启用/禁用作业
/// </summary>

View File

@ -18,4 +18,9 @@ public interface IJobService : IService, IJobModule
/// 获取下一个要执行的计划作业
/// </summary>
Task<QueryJobRsp> GetNextJobAsync();
/// <summary>
/// 释放卡死的任务
/// </summary>
Task<int> ReleaseStuckTaskAsync();
}

View File

@ -102,6 +102,9 @@ public sealed class JobRecordService(DefaultRepository<Sys_JobRecord> rpo) //
{
var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter)
.WhereDynamic(req.Filter)
.WhereIf( //
req.Keywords?.Length > 0
, a => a.JobId == req.Keywords.Int64Try(0) || a.Id == req.Keywords.Int64Try(0))
.OrderByPropertyNameIf(req.Prop?.Length > 0, req.Prop, req.Order == Orders.Ascending);
if (!req.Prop?.Equals(nameof(req.Filter.Id), StringComparison.OrdinalIgnoreCase) ?? true) {
ret = ret.OrderByDescending(a => a.Id);

View File

@ -4,6 +4,7 @@ using NetAdmin.Application.Services;
using NetAdmin.Domain.DbMaps.Sys;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.Job;
using NetAdmin.Domain.Dto.Sys.JobRecord;
using NetAdmin.Domain.Enums.Sys;
using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
using DataType = FreeSql.DataType;
@ -11,7 +12,7 @@ using DataType = FreeSql.DataType;
namespace NetAdmin.SysComponent.Application.Services.Sys;
/// <inheritdoc cref="IJobService" />
public sealed class JobService(DefaultRepository<Sys_Job> rpo) //
public sealed class JobService(DefaultRepository<Sys_Job> rpo, IJobRecordService jobRecordService) //
: RepositoryService<Sys_Job, IJobService>(rpo), IJobService
{
/// <inheritdoc />
@ -134,6 +135,28 @@ public sealed class JobService(DefaultRepository<Sys_Job> rpo) //
return ret.Adapt<IEnumerable<QueryJobRsp>>();
}
/// <inheritdoc />
public Task<QueryJobRecordRsp> RecordGetAsync(QueryJobRecordReq req)
{
req.ThrowIfInvalid();
return jobRecordService.GetAsync(req);
}
/// <inheritdoc />
public Task<PagedQueryRsp<QueryJobRecordRsp>> RecordPagedQueryAsync(PagedQueryReq<QueryJobRecordReq> req)
{
return jobRecordService.PagedQueryAsync(req);
}
/// <inheritdoc />
public Task<int> ReleaseStuckTaskAsync()
{
return Rpo.UpdateDiy.Set(a => a.Status == JobStatues.Idle)
.Where(a => a.Status == JobStatues.Running &&
a.LastExecTime < DateTime.Now.AddSeconds(-Numbers.JOB_TIMEOUT_SECS))
.ExecuteAffrowsAsync();
}
/// <inheritdoc />
public Task SetEnabledAsync(UpdateJobReq req)
{
@ -162,7 +185,8 @@ public sealed class JobService(DefaultRepository<Sys_Job> rpo) //
private ISelect<Sys_Job> QueryInternal(QueryReq<QueryJobReq> req, bool orderByRandom = false)
{
var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter)
var ret = Rpo.Select.Include(a => a.User)
.WhereDynamicFilter(req.DynamicFilter)
.WhereDynamic(req.Filter)
.WhereIf( //
req.Keywords?.Length > 0

View File

@ -40,7 +40,7 @@ public interface IUserCache : ICache<IDistributedCache, IUserService>, IUserModu
/// <summary>
/// 删除缓存 RegisterAsync
/// </summary>
Task RemoveRegisterAsync(RegisterUserReq userReq);
Task RemoveRegisterAsync(RegisterUserReq req);
/// <summary>
/// 删除缓存 ResetPasswordAsync

View File

@ -1,6 +1,7 @@
using NetAdmin.Cache;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.Job;
using NetAdmin.Domain.Dto.Sys.JobRecord;
using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
using NetAdmin.SysComponent.Cache.Sys.Dependency;
@ -52,6 +53,18 @@ public sealed class JobCache(IDistributedCache cache, IJobService service)
return Service.QueryAsync(req);
}
/// <inheritdoc />
public Task<QueryJobRecordRsp> RecordGetAsync(QueryJobRecordReq req)
{
return Service.RecordGetAsync(req);
}
/// <inheritdoc />
public Task<PagedQueryRsp<QueryJobRecordRsp>> RecordPagedQueryAsync(PagedQueryReq<QueryJobRecordReq> req)
{
return Service.RecordPagedQueryAsync(req);
}
/// <inheritdoc />
public Task SetEnabledAsync(UpdateJobReq req)
{

View File

@ -120,7 +120,7 @@ public sealed class UserCache(IDistributedCache cache, IUserService service, IVe
}
/// <inheritdoc />
public Task RemoveRegisterAsync(RegisterUserReq userReq)
public Task RemoveRegisterAsync(RegisterUserReq req)
{
throw new NotImplementedException();
}

View File

@ -1,5 +1,6 @@
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.Job;
using NetAdmin.Domain.Dto.Sys.JobRecord;
using NetAdmin.Host.Attributes;
using NetAdmin.Host.Controllers;
using NetAdmin.SysComponent.Application.Modules.Sys;
@ -73,6 +74,22 @@ public sealed class JobController(IJobCache cache) : ControllerBase<IJobCache, I
return Cache.QueryAsync(req);
}
/// <summary>
/// 获取单个作业记录
/// </summary>
public Task<QueryJobRecordRsp> RecordGetAsync(QueryJobRecordReq req)
{
return Cache.RecordGetAsync(req);
}
/// <summary>
/// 分页查询作业记录
/// </summary>
public Task<PagedQueryRsp<QueryJobRecordRsp>> RecordPagedQueryAsync(PagedQueryReq<QueryJobRecordReq> req)
{
return Cache.RecordPagedQueryAsync(req);
}
/// <summary>
/// 启用/禁用作业
/// </summary>

View File

@ -14,8 +14,11 @@ public static class ServiceCollectionExtensions
/// </summary>
public static IServiceCollection AddSchedules(this IServiceCollection me)
{
return me.AddSchedule( //
builder => builder //
.AddJob<ScheduledJob>(false, Triggers.PeriodSeconds(5).SetRunOnStart(true)));
return App.WebHostEnvironment.EnvironmentName != Environments.Production
? me
: me.AddSchedule( //
builder => builder //
.AddJob<ScheduledJob>(false, Triggers.PeriodSeconds(5).SetRunOnStart(true))
.AddJob<FreeScheduledJob>(false, Triggers.PeriodMinutes(1).SetRunOnStart(true)));
}
}

View File

@ -0,0 +1,42 @@
using Furion.Schedule;
using NetAdmin.Host.BackgroundRunning;
using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
namespace NetAdmin.SysComponent.Host.Jobs;
/// <summary>
/// 释放计划作业
/// </summary>
public sealed class FreeScheduledJob : WorkBase<FreeScheduledJob>, IJob
{
private readonly IJobService _jobService;
/// <summary>
/// Initializes a new instance of the <see cref="FreeScheduledJob" /> class.
/// </summary>
public FreeScheduledJob()
{
_jobService = ServiceProvider.GetService<IJobService>();
}
/// <summary>
/// 具体处理逻辑
/// </summary>
/// <param name="context">作业执行前上下文</param>
/// <param name="stoppingToken">取消任务 Token</param>
/// <exception cref="NetAdminGetLockerException">加锁失败异常</exception>
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
await WorkflowAsync(true, stoppingToken).ConfigureAwait(false);
}
/// <summary>
/// 通用工作流
/// </summary>
/// <exception cref="NotImplementedException">NotImplementedException</exception>
/// <exception cref="ArgumentOutOfRangeException">ArgumentOutOfRangeException</exception>
protected override async ValueTask WorkflowAsync(CancellationToken cancelToken)
{
_ = await _jobService.ReleaseStuckTaskAsync().ConfigureAwait(false);
}
}

View File

@ -87,7 +87,7 @@ public sealed class ScheduledJob : WorkBase<ScheduledJob>, IJob
{
Duration = sw.ElapsedMilliseconds
, HttpMethod = job.HttpMethod
, HttpStatusCode = rsp.StatusCode
, HttpStatusCode = (int)rsp.StatusCode
, JobId = job.Id
, RequestBody = job.RequestBody
, RequestHeader = _requestHeader

View File

@ -3,9 +3,9 @@
<ProjectReference Include="../NetAdmin.Host/NetAdmin.Host.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="2.6.6"/>
<PackageReference Include="xunit" Version="2.7.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>