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

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

View File

@ -17,7 +17,7 @@
<LangVersion>preview</LangVersion> <LangVersion>preview</LangVersion>
<MinVerDefaultPreReleaseIdentifiers>beta</MinVerDefaultPreReleaseIdentifiers> <MinVerDefaultPreReleaseIdentifiers>beta</MinVerDefaultPreReleaseIdentifiers>
<MinVerTagPrefix>v</MinVerTagPrefix> <MinVerTagPrefix>v</MinVerTagPrefix>
<NoWarn>CA1707;IDE0005;IDE0008;IDE0010;IDE0028;IDE0055;IDE0160;IDE0300;IDE0305;RCS1141;RCS1142;RCS1181;S101;S1121;S1135;S125;S2094;S3604;S4663;SYSLIB1045;SA1010</NoWarn> <NoWarn>CA1707;IDE0005;IDE0008;IDE0010;IDE0028;IDE0055;IDE0160;IDE0300;IDE0305;RCS1141;RCS1142;RCS1181;S101;S1121;S1135;S125;S2094;S3604;S4663;S6561;SYSLIB1045;SA1010</NoWarn>
<Product>NetAdmin</Product> <Product>NetAdmin</Product>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/nsnail/NetAdmin.git</RepositoryUrl> <RepositoryUrl>https://github.com/nsnail/NetAdmin.git</RepositoryUrl>

View File

@ -34,6 +34,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{BB0B
clean.ln.csx = scripts/clean.ln.csx clean.ln.csx = scripts/clean.ln.csx
code.clean.csx = scripts/code.clean.csx code.clean.csx = scripts/code.clean.csx
code.clean.ps1 = scripts/code.clean.ps1 code.clean.ps1 = scripts/code.clean.ps1
find.unused.ln.csx = scripts/find.unused.ln.csx
gen.cs.tt = scripts/gen.cs.tt gen.cs.tt = scripts/gen.cs.tt
gen.id.linq = scripts/gen.id.linq gen.id.linq = scripts/gen.id.linq
gen.ln.cmd = scripts/gen.ln.cmd gen.ln.cmd = scripts/gen.ln.cmd

View File

@ -1,8 +1,8 @@
[ [
{ {
"Enabled": true,
"Id": 372119301627909, "Id": 372119301627909,
"Name": "默认部门", "Name": "默认部门",
"Enabled": true,
"Sort": 100 "Sort": 100
} }
] ]

View File

@ -1,10 +1,10 @@
[ [
{ {
"DataScope": 1, "DataScope": 1,
"DisplayDashboard": true,
"Enabled": true, "Enabled": true,
"Id": 370943613149253, "Id": 370943613149253,
"IgnorePermissionControl": true, "IgnorePermissionControl": true,
"DisplayDashboard": true,
"Name": "超级管理员", "Name": "超级管理员",
"Sort": 100 "Sort": 100
}, },

View File

@ -1,10 +1,10 @@
[ [
{ {
"Content": "<p>尊敬的用户:</p>\n<p style=\"padding-left: 40px;\">欢迎您使用 NetAdmin 后台管理系统NetAdmin 是一款通用后台权限管理系统和快速开发框架,它基于 C#12/.NET8、Vue3/Vite、Element Plus 等现代技术构建,具有十分整洁、优雅的编码规范。</p>\n<p style=\"padding-left: 40px;\">NetAdmin 致力于为企业提供高效、安全、易用的解决方案,帮助您快速构建出符合业务需求的应用程序。系统提供了丰富的功能模块,包括用户管理、权限管理、日志管理、文件上传等,可以满足您日常管理的需求。</p>\n<p style=\"padding-left: 40px;\">在使用 NetAdmin 的过程中,我们真诚地希望您能够遵守以下规定:</p>\n<p style=\"padding-left: 80px;\">1. 不得利用 NetAdmin 进行非法活动或者侵犯他人权益;</p>\n<p style=\"padding-left: 80px;\">2. 不得对 NetAdmin 系统进行恶意攻击或者破坏;</p>\n<p style=\"padding-left: 80px;\">3. 不得将 NetAdmin 系统的任何部分用于商业目的或者未经授权的访问。</p>\n<p style=\"padding-left: 80px;\">4. 为了更好地为您提供服务NetAdmin 将不断进行优化和升级,同时也欢迎您提出宝贵的意见和建议。如果您在使用过程中遇到任何问题,可以通过官方网站或者技术支持团队进行咨询和解决。</p>\n<p style=\"padding-left: 40px;\">再次感谢您对 NetAdmin 的信任和支持我们相信在您的使用过程中NetAdmin 一定会成为您的得力助手,为您的事业发展提供强有力的支持!</p>\n<p style=\"text-align: right;\">NetAdmin 开发团队</p>", "Content": "<p>尊敬的用户:</p>\n<p style=\"padding-left: 40px;\">欢迎您使用 NetAdmin 后台管理系统NetAdmin 是一款通用后台权限管理系统和快速开发框架,它基于 C#12/.NET8、Vue3/Vite、Element Plus 等现代技术构建,具有十分整洁、优雅的编码规范。</p>\n<p style=\"padding-left: 40px;\">NetAdmin 致力于为企业提供高效、安全、易用的解决方案,帮助您快速构建出符合业务需求的应用程序。系统提供了丰富的功能模块,包括用户管理、权限管理、日志管理、文件上传等,可以满足您日常管理的需求。</p>\n<p style=\"padding-left: 40px;\">在使用 NetAdmin 的过程中,我们真诚地希望您能够遵守以下规定:</p>\n<p style=\"padding-left: 80px;\">1. 不得利用 NetAdmin 进行非法活动或者侵犯他人权益;</p>\n<p style=\"padding-left: 80px;\">2. 不得对 NetAdmin 系统进行恶意攻击或者破坏;</p>\n<p style=\"padding-left: 80px;\">3. 不得将 NetAdmin 系统的任何部分用于商业目的或者未经授权的访问。</p>\n<p style=\"padding-left: 80px;\">4. 为了更好地为您提供服务NetAdmin 将不断进行优化和升级,同时也欢迎您提出宝贵的意见和建议。如果您在使用过程中遇到任何问题,可以通过官方网站或者技术支持团队进行咨询和解决。</p>\n<p style=\"padding-left: 40px;\">再次感谢您对 NetAdmin 的信任和支持我们相信在您的使用过程中NetAdmin 一定会成为您的得力助手,为您的事业发展提供强有力的支持!</p>\n<p style=\"text-align: right;\">NetAdmin 开发团队</p>",
"CreatedUserId": 370942943322181,
"CreatedUserName": "root",
"MsgType": 2, "MsgType": 2,
"Summary": "尊敬的用户:\n欢迎您使用 NetAdmin 后台管理系统NetAdmin 是一款通用后台权限管理系统和快速开发框架,它基于 C#12/.NET8、Vue3/Vite、Element Plus 等现代", "Summary": "尊敬的用户:\n欢迎您使用 NetAdmin 后台管理系统NetAdmin 是一款通用后台权限管理系统和快速开发框架,它基于 C#12/.NET8、Vue3/Vite、Element Plus 等现代",
"Title": "欢迎使用 NetAdmin 后台管理系统", "Title": "欢迎使用 NetAdmin 后台管理系统",
"CreatedUserId": 370942943322181,
"CreatedUserName": "root",
} }
] ]

View File

@ -15,7 +15,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.9.1-alpha"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.9.28">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -1,7 +1,7 @@
cd .. cd ..
$types = @{ $types = @{
'1' = @('major', '主版本') '1' = @('major', '主版本')
'2' = @('minor', '版本') '2' = @('minor', '版本')
'3' = @('patch', '修订版本') '3' = @('patch', '修订版本')
} }
$prefix = '' $prefix = ''

View File

@ -8,21 +8,13 @@ namespace NetAdmin.Application.Services;
/// </summary> /// </summary>
/// <typeparam name="TEntity">实体类型</typeparam> /// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TLogger">日志类型</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 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>
/// 默认仓储 /// 默认仓储
/// </summary> /// </summary>
protected DefaultRepository<TEntity> Rpo { get; } protected DefaultRepository<TEntity> Rpo => rpo;
/// <summary> /// <summary>
/// 启用级联保存 /// 启用级联保存

View File

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

View File

@ -6,15 +6,10 @@ namespace NetAdmin.Cache;
/// <summary> /// <summary>
/// 分布式缓存 /// 分布式缓存
/// </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 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>
/// 创建缓存 /// 创建缓存
/// </summary> /// </summary>

View File

@ -5,12 +5,6 @@ namespace NetAdmin.Cache;
/// <summary> /// <summary>
/// 内存缓存 /// 内存缓存
/// </summary> /// </summary>
public abstract class MemoryCache<TService> : CacheBase<IMemoryCache, TService> public abstract class MemoryCache<TService>(IMemoryCache cache, TService service)
where TService : IService : CacheBase<IMemoryCache, TService>(cache, service)
{ 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) { }
}

View File

@ -8,11 +8,14 @@ public abstract record DataAbstraction
/// <summary> /// <summary>
/// 如果数据校验失败,抛出异常 /// 如果数据校验失败,抛出异常
/// </summary> /// </summary>
/// <exception cref="NetAdminInvalidInputException">NetAdminInvalidInputException</exception> /// <exception cref="NetAdminValidateException">NetAdminValidateException</exception>
public void ThrowIfInvalid() public void ThrowIfInvalid()
{ {
if (!this.TryValidate().IsValid) { var validationResult = this.TryValidate();
throw new NetAdminInvalidInputException(Ln.); 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 /> /// <inheritdoc />
[JsonIgnore] [JsonIgnore]
[Column(CanUpdate = false, Position = -1)] [Column(CanUpdate = false, Position = -1)]
public long? CreatedUserId { get; init; } public virtual long? CreatedUserId { get; init; }
/// <inheritdoc /> /// <inheritdoc />
[JsonIgnore] [JsonIgnore]
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)] [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" /> /// <inheritdoc cref="IFieldPrimary{T}.Id" />
[Column(IsIdentity = false, IsPrimary = true, Position = 1)] [Column(IsIdentity = false, IsPrimary = true, Position = 1)]
@ -34,10 +34,10 @@ public abstract record VersionEntity<T> : LiteVersionEntity<T>, IFieldModifiedUs
/// <inheritdoc cref="IFieldModifiedUser.ModifiedUserId" /> /// <inheritdoc cref="IFieldModifiedUser.ModifiedUserId" />
[JsonIgnore] [JsonIgnore]
[Column(CanInsert = false, Position = -1)] [Column(CanInsert = false, Position = -1)]
public long? ModifiedUserId { get; init; } public virtual long? ModifiedUserId { get; init; }
/// <inheritdoc cref="IFieldModifiedUser.ModifiedUserName" /> /// <inheritdoc cref="IFieldModifiedUser.ModifiedUserName" />
[JsonIgnore] [JsonIgnore]
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanInsert = false, Position = -1)] [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)] [Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_255)]
public virtual string Summary { get; init; } public virtual string Summary { get; init; }
/// <summary>
/// 执行用户
/// </summary>
[Navigate(nameof(UserId))]
[JsonIgnore]
public Sys_User User { get; init; }
/// <summary> /// <summary>
/// 执行用户编号 /// 执行用户编号
/// </summary> /// </summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
using NetAdmin.Domain.DbMaps.Dependency.Fields; using NetAdmin.Domain.DbMaps.Dependency.Fields;
using NetAdmin.Domain.DbMaps.Sys; using NetAdmin.Domain.DbMaps.Sys;
using HttpMethods = NetAdmin.Domain.Enums.HttpMethods;
namespace NetAdmin.Domain.Dto.Sys.JobRecord; namespace NetAdmin.Domain.Dto.Sys.JobRecord;
@ -8,7 +9,51 @@ namespace NetAdmin.Domain.Dto.Sys.JobRecord;
/// </summary> /// </summary>
public sealed record QueryJobRecordRsp : Sys_JobRecord 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" /> /// <inheritdoc cref="IFieldPrimary{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)] [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; } 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>
/// 响应:获取模块信息 /// 响应:获取模块信息
/// </summary> /// </summary>
public record GetModulesRsp : DataAbstraction public sealed record GetModulesRsp : DataAbstraction
{ {
/// <summary> /// <summary>
/// 模块名称 /// 模块名称

View File

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

View File

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

View File

@ -5,4 +5,4 @@ namespace NetAdmin.Domain.Dto.Sys.User;
/// <summary> /// <summary>
/// 请求:短信登录 /// 请求:短信登录
/// </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>
/// 响应:密码登录 /// 响应:密码登录
/// </summary> /// </summary>
public record LoginRsp : DataAbstraction public sealed record LoginRsp : DataAbstraction
{ {
/// <summary> /// <summary>
/// 访问令牌 /// 访问令牌

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,20 +6,12 @@ namespace NetAdmin.Host.Controllers;
/// <summary> /// <summary>
/// 控制器基类 /// 控制器基类
/// </summary> /// </summary>
public abstract class ControllerBase<TCache, TService> : IDynamicApiController public abstract class ControllerBase<TCache, TService>(TCache cache) : IDynamicApiController
where TCache : ICache<IDistributedCache, TService> // where TCache : ICache<IDistributedCache, TService> //
where TService : IService 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>
/// 关联的缓存 /// 关联的缓存
/// </summary> /// </summary>
protected TCache Cache { get; } protected TCache Cache => cache;
} }

View File

@ -25,7 +25,10 @@ public abstract class ApiResultHandler<T>
{ {
var lineException = context.Exception switch { NetAdminException ex => ex, _ => null }; var lineException = context.Exception switch { NetAdminException ex => ex, _ => null };
var errorCode = lineException?.Code ?? ErrorCodes.Unhandled; var errorCode = lineException?.Code ?? ErrorCodes.Unhandled;
var result = RestfulResult(errorCode, metadata.Data, lineException?.Message ?? errorCode.ResDesc<ErrorCodes>()); var result = RestfulResult(errorCode, metadata.Data
, lineException is NetAdminValidateException vEx
? vEx.ValidateResults
: lineException?.Message ?? errorCode.ResDesc<ErrorCodes>());
SetErrorCodeToHeader(context.HttpContext, errorCode); 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 long DIC_CATALOG_ID_GEO_AREA = 379794295185413; // 字典目录编号-行政区划字典
public const int HEART_TIMEOUT_SECS = 600; // 心跳超时时间 public const int HEART_TIMEOUT_SECS = 600; // 心跳超时时间
public const int HTTP_STATUS_BIZ_FAIL = 900; // Http状态码-业务异常 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_DEF_PAGE_SIZE = 20; // 分页查询默认的页容量
public const int QUERY_LIMIT = 100; // 非分页查询允许返回的最大条数 public const int QUERY_LIMIT = 100; // 非分页查询允许返回的最大条数
public const int QUERY_MAX_PAGE_NO = 1000; // 分页查询允许最大的页码 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 public static HttpRequestPart SetLog<T>(this HttpRequestPart me, ILogger<T> logger
, Func<string, string> bodyHandle = null) , Func<string, string> bodyHandle = null)
{ {
#pragma warning disable S1172 Task RequestHandleAsync(HttpClient _, HttpRequestMessage req)
Task RequestHandle(HttpClient _, HttpRequestMessage req)
{ {
return req.LogAsync(logger); return req.LogAsync(logger);
} }
Task ExceptionHandle(HttpClient _, HttpResponseMessage rsp, string errors) Task ExceptionHandleAsync(HttpClient _, HttpResponseMessage rsp, string errors)
{ {
return rsp.LogExceptionAsync(errors, logger, bodyHandle); return rsp.LogExceptionAsync(errors, logger, bodyHandle);
} }
Task ResponseHandle(HttpClient _, HttpResponseMessage rsp) Task ResponseHandleAsync(HttpClient _, HttpResponseMessage rsp)
{ {
return rsp.LogAsync(logger, bodyHandle); return rsp.LogAsync(logger, bodyHandle);
} }
#pragma warning restore S1172 return me.OnRequesting(RequestHandleAsync).OnResponsing(ResponseHandleAsync).OnException(ExceptionHandleAsync);
return me.OnRequesting(RequestHandle).OnResponsing(ResponseHandle).OnException(ExceptionHandle);
} }
} }

View File

@ -6,6 +6,7 @@ namespace NetAdmin.Infrastructure.Extensions;
public static class StringExtensions 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> /// <summary>
/// object -> json /// object -> json
@ -23,6 +24,16 @@ public static class StringExtensions
return me.Object(toType, GlobalStatic.JsonSerializerOptions); 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> /// <summary>
/// 去掉尾部字符串“Options” /// 去掉尾部字符串“Options”
/// </summary> /// </summary>

View File

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

View File

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

View File

@ -18,4 +18,9 @@ public interface IJobService : IService, IJobModule
/// 获取下一个要执行的计划作业 /// 获取下一个要执行的计划作业
/// </summary> /// </summary>
Task<QueryJobRsp> GetNextJobAsync(); 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) var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter)
.WhereDynamic(req.Filter) .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); .OrderByPropertyNameIf(req.Prop?.Length > 0, req.Prop, req.Order == Orders.Ascending);
if (!req.Prop?.Equals(nameof(req.Filter.Id), StringComparison.OrdinalIgnoreCase) ?? true) { if (!req.Prop?.Equals(nameof(req.Filter.Id), StringComparison.OrdinalIgnoreCase) ?? true) {
ret = ret.OrderByDescending(a => a.Id); ret = ret.OrderByDescending(a => a.Id);

View File

@ -4,6 +4,7 @@ using NetAdmin.Application.Services;
using NetAdmin.Domain.DbMaps.Sys; using NetAdmin.Domain.DbMaps.Sys;
using NetAdmin.Domain.Dto.Dependency; using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.Job; using NetAdmin.Domain.Dto.Sys.Job;
using NetAdmin.Domain.Dto.Sys.JobRecord;
using NetAdmin.Domain.Enums.Sys; using NetAdmin.Domain.Enums.Sys;
using NetAdmin.SysComponent.Application.Services.Sys.Dependency; using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
using DataType = FreeSql.DataType; using DataType = FreeSql.DataType;
@ -11,7 +12,7 @@ using DataType = FreeSql.DataType;
namespace NetAdmin.SysComponent.Application.Services.Sys; namespace NetAdmin.SysComponent.Application.Services.Sys;
/// <inheritdoc cref="IJobService" /> /// <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 : RepositoryService<Sys_Job, IJobService>(rpo), IJobService
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -134,6 +135,28 @@ public sealed class JobService(DefaultRepository<Sys_Job> rpo) //
return ret.Adapt<IEnumerable<QueryJobRsp>>(); 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 /> /// <inheritdoc />
public Task SetEnabledAsync(UpdateJobReq req) 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) 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) .WhereDynamic(req.Filter)
.WhereIf( // .WhereIf( //
req.Keywords?.Length > 0 req.Keywords?.Length > 0

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
using NetAdmin.Domain.Dto.Dependency; using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.Job; using NetAdmin.Domain.Dto.Sys.Job;
using NetAdmin.Domain.Dto.Sys.JobRecord;
using NetAdmin.Host.Attributes; using NetAdmin.Host.Attributes;
using NetAdmin.Host.Controllers; using NetAdmin.Host.Controllers;
using NetAdmin.SysComponent.Application.Modules.Sys; using NetAdmin.SysComponent.Application.Modules.Sys;
@ -73,6 +74,22 @@ public sealed class JobController(IJobCache cache) : ControllerBase<IJobCache, I
return Cache.QueryAsync(req); 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>
/// 启用/禁用作业 /// 启用/禁用作业
/// </summary> /// </summary>

View File

@ -14,8 +14,11 @@ public static class ServiceCollectionExtensions
/// </summary> /// </summary>
public static IServiceCollection AddSchedules(this IServiceCollection me) public static IServiceCollection AddSchedules(this IServiceCollection me)
{ {
return me.AddSchedule( // return App.WebHostEnvironment.EnvironmentName != Environments.Production
? me
: me.AddSchedule( //
builder => builder // builder => builder //
.AddJob<ScheduledJob>(false, Triggers.PeriodSeconds(5).SetRunOnStart(true))); .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 Duration = sw.ElapsedMilliseconds
, HttpMethod = job.HttpMethod , HttpMethod = job.HttpMethod
, HttpStatusCode = rsp.StatusCode , HttpStatusCode = (int)rsp.StatusCode
, JobId = job.Id , JobId = job.Id
, RequestBody = job.RequestBody , RequestBody = job.RequestBody
, RequestHeader = _requestHeader , RequestHeader = _requestHeader

View File

@ -3,9 +3,9 @@
<ProjectReference Include="../NetAdmin.Host/NetAdmin.Host.csproj"/> <ProjectReference Include="../NetAdmin.Host/NetAdmin.Host.csproj"/>
</ItemGroup> </ItemGroup>
<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="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> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -3,7 +3,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build --mode production",
"build:test": "vite build --mode test",
"preview": "vite preview", "preview": "vite preview",
"prettier": "prettier --write ." "prettier": "prettier --write ."
}, },
@ -12,34 +13,34 @@
"@tinymce/tinymce-vue": "^5.1.1", "@tinymce/tinymce-vue": "^5.1.1",
"axios": "^1.6.7", "axios": "^1.6.7",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"core-js": "^3.35.1", "core-js": "^3.36.0",
"cropperjs": "^1.6.1", "cropperjs": "^1.6.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"element-plus": "^2.5.3", "element-plus": "^2.5.5",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"json5-to-table": "^0.1.8", "json5-to-table": "^0.1.8",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinyin-match": "^1.2.5", "pinyin-match": "^1.2.5",
"qrcodejs2": "^0.0.2", "qrcodejs2": "^0.0.2",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"tinymce": "^6.8.2", "tinymce": "^6.8.3",
"vue": "^3.4.15", "vue": "^3.4.19",
"vue-i18n": "^9.9.0", "vue-i18n": "^9.9.1",
"vue-router": "^4.2.5", "vue-router": "^4.2.5",
"vue3-json-viewer": "^2.2.2", "vue3-json-viewer": "^2.2.2",
"vuedraggable": "^4.0.3", "vuedraggable": "^4.0.3",
"vuex": "^4.1.0", "vuex": "^4.1.0",
"xgplayer": "^3.0.11", "xgplayer": "^3.0.12",
"xgplayer-hls": "^3.0.11" "xgplayer-hls": "^3.0.12"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue": "^5.0.4",
"prettier": "^3.2.4", "prettier": "^3.2.5",
"prettier-plugin-organize-attributes": "^1.0.0", "prettier-plugin-organize-attributes": "^1.0.0",
"sass": "^1.70.0", "sass": "^1.71.0",
"terser": "^5.27.0", "terser": "^5.27.1",
"vite": "^5.0.12" "vite": "^5.1.3"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

View File

@ -82,6 +82,28 @@ export default {
}, },
}, },
/**
* 获取单个作业记录
*/
recordGet: {
url: `${config.API_URL}/api/sys/job/record.get`,
name: `获取单个作业记录`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 分页查询作业记录
*/
recordPagedQuery: {
url: `${config.API_URL}/api/sys/job/record.paged.query`,
name: `分页查询作业记录`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/** /**
* 启用/禁用作业 * 启用/禁用作业
*/ */

View File

@ -0,0 +1,10 @@
<template>
<svg class="icon" height="128" p-id="4304" t="1708217899083" version="1.1" viewBox="0 0 1024 1024" width="128" xmlns="http://www.w3.org/2000/svg">
<path
d="M497.3 409.6c-1.6 3.7-4.8 7-9.6 10s-9.3 4.8-13.6 5.5c-4.2 0.7-9 1-14.3 0.8h-25.7v57.4h59.3V740h64.4V400.2L502 400l-4.7 9.6z"
p-id="4305"></path>
<path
d="M880 160H738v-56c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v56H350v-56c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v56H144c-17.7 0-32 14.3-32 32v688c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-32 680c0 4.4-3.6 8-8 8H184c-4.4 0-8-3.6-8-8V232c0-4.4 3.6-8 8-8h102v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48h324v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48h110v616z"
p-id="4306"></path>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg class="icon" height="128" p-id="5158" t="1708217788265" version="1.1" viewBox="0 0 1024 1024" width="128" xmlns="http://www.w3.org/2000/svg">
<path
d="M228.254587 913.306922 66.10251 913.306922l0-240.783948 162.152077 0L228.254587 913.306922zM471.464795 913.306922 309.318858 913.306922 309.318858 351.483166l162.145937 0L471.464795 913.306922zM714.681142 913.306922 552.540322 913.306922 552.540322 592.267115 714.681142 592.267115 714.681142 913.306922zM957.896466 913.306922 795.744389 913.306922 795.744389 110.693078l162.152077 0L957.896466 913.306922z"
p-id="5159"></path>
</svg>
</template>

View File

@ -54,3 +54,5 @@ export { default as Upload } from './Upload.vue'
export { default as Vue } from './Vue.vue' export { default as Vue } from './Vue.vue'
export { default as Warning } from './Warning.vue' export { default as Warning } from './Warning.vue'
export { default as Wechat } from './Wechat.vue' export { default as Wechat } from './Wechat.vue'
export { default as Report } from './Report.vue'
export { default as Daily } from './Daily.vue'

View File

@ -3,7 +3,7 @@
<template #default="scope"> <template #default="scope">
<template v-for="(item, i) in Array.isArray(scope.row[prop]) ? scope.row[prop] : [scope.row[prop]]" :key="i"> <template v-for="(item, i) in Array.isArray(scope.row[prop]) ? scope.row[prop] : [scope.row[prop]]" :key="i">
<el-tag v-if="item" @click="$emit('click', item)"> <el-tag v-if="item" @click="$emit('click', item)">
{{ item ? item[field] : console.error(scope.row) }} {{ item ? item[field] : '' }}
</el-tag> </el-tag>
</template> </template>
</template> </template>

View File

@ -0,0 +1,50 @@
<template>
<el-table-column v-bind="$attrs">
<template #default="scope">
<div @click="click(tool.getNestedProperty(scope.row, $attrs.prop))" class="avatar" style="cursor: pointer">
<el-avatar :src="getAvatar(scope)" size="small"></el-avatar>
<el-text tag="ins">{{ tool.getNestedProperty(scope.row, $attrs.nestProp) }}</el-text>
</div>
<save-dialog v-if="dialog.save" @closed="dialog.save = false" ref="saveDialog"></save-dialog>
</template>
</el-table-column>
</template>
<style scoped>
.avatar {
display: flex;
gap: 0.5rem;
}
</style>
<script>
import saveDialog from '@/views/sys/user/save.vue'
import tool from '@/utils/tool'
export default {
emits: ['click'],
props: {},
data() {
return {
dialog: { save: false },
}
},
mounted() {},
created() {},
components: { saveDialog },
computed: {
tool() {
return tool
},
},
methods: {
async click(id) {
this.dialog.save = true
await this.$nextTick()
await this.$refs.saveDialog.open('view', { id: id })
},
//
getAvatar(scope) {
return scope.row.avatar ? scope.row.avatar : this.$CONFIG.DEFAULT_AVATAR
},
},
}
</script>

File diff suppressed because one or more lines are too long

View File

@ -75,7 +75,7 @@ Object.assign(DEFAULT_CONFIG, MY_CONFIG)
// 如果生产模式就合并动态的APP_CONFIG // 如果生产模式就合并动态的APP_CONFIG
// public/config.js // public/config.js
if (import.meta.env.PROD) { if (import.meta.env.MODE === 'production' || import.meta.env.MODE === 'test') {
Object.assign(DEFAULT_CONFIG, APP_CONFIG) Object.assign(DEFAULT_CONFIG, APP_CONFIG)
} }

View File

@ -1,59 +1,60 @@
<template> <template>
<el-container v-loading="loading"> <el-container v-loading="loading">
<el-main> <el-main>
<el-empty v-if="tasks.length === 0" :image-size="120"> <el-empty v-if="jobs.length === 0" :image-size="120">
<template #description> <template #description>
<h2>没有正在执行的任务</h2> <h2>没有正在执行的任务</h2>
</template> </template>
<p style="font-size: 14px; color: #999; line-height: 1.5; margin: 0 40px"> <p style="color: #999; line-height: 1.5; margin: 0 3rem">
在处理耗时过久的任务时为了不阻碍正在处理的工作可在任务中心进行异步执行 在处理耗时过久的任务时为了不阻碍正在处理的工作可在任务中心进行异步执行
</p> </p>
</el-empty> </el-empty>
<el-card v-for="task in tasks" :key="task.id" class="user-bar-tasks-item" shadow="hover"> <el-card v-for="job in jobs" :key="job.id" class="user-bar-jobs-item" shadow="hover">
<div class="user-bar-tasks-item-body"> <div class="user-bar-jobs-item-body">
<div class="taskIcon"> <div class="jobIcon">
<el-icon v-if="task.type === 'export'" :size="20"> {{ job.lastStatusCode }}
<el-icon-paperclip />
</el-icon>
<el-icon v-if="task.type === 'report'" :size="20">
<el-icon-dataAnalysis />
</el-icon>
</div> </div>
<div class="taskMain"> <div class="jobMain">
<div class="title"> <div class="title">
<h2>{{ task.taskName }}</h2> <h2>{{ job.jobName }}</h2>
<p><span v-time.tip="task.createDate"></span> 创建</p> <p>上次执行<span v-time.tip="job.lastExecTime"></span></p>
<p>
下次执行<span>{{ job.nextExecTime }}</span>
</p>
</div> </div>
<div class="bottom"> <div class="bottom">
<div class="state"> <div class="status">
<el-tag v-if="task.state === '0'" type="info">执行中</el-tag> <el-tag v-if="job.status === 'running'" type="info">执行中</el-tag>
<el-tag v-if="task.state === '1'">完成</el-tag> <el-tag v-if="job.status === 'idle'">空闲</el-tag>
</div> </div>
<div class="handler"> <div class="handler">
<el-button <el-button v-if="job.status === 'idle'" @click="view(job)" circle icon="el-icon-view" type="primary"></el-button>
v-if="task.state === '1'"
@click="download(task)"
circle
icon="el-icon-download"
type="primary"></el-button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</el-card> </el-card>
</el-main> </el-main>
<el-footer style="padding: 10px; text-align: right"> <el-footer style="text-align: right">
<el-button @click="refresh" circle icon="el-icon-refresh"></el-button> <el-button @click="refresh" circle icon="el-icon-refresh"></el-button>
</el-footer> </el-footer>
</el-container> </el-container>
<save-dialog v-if="dialog.save" @closed="dialog.save = false" ref="saveDialog"></save-dialog>
</template> </template>
<script> <script>
import saveDialog from '@/views/sys/job/save.vue'
export default { export default {
components: {
saveDialog,
},
data() { data() {
return { return {
dialog: {
save: false,
},
loading: false, loading: false,
tasks: [], jobs: [],
} }
}, },
mounted() { mounted() {
@ -62,69 +63,64 @@ export default {
methods: { methods: {
async getData() { async getData() {
this.loading = true this.loading = true
var res = await this.$API.system.tasks.list.get() const res = await this.$API.sys_job.query.post({ prop: 'lastExecTime', order: 'descending' })
this.tasks = res.data this.jobs = res.data
this.loading = false this.loading = false
}, },
refresh() { refresh() {
this.getData() this.getData()
}, },
download(row) { async view(job) {
let a = document.createElement('a') this.dialog.save = true
a.style = 'display: none' await this.$nextTick()
a.target = '_blank' await this.$refs.saveDialog.open('view', { id: job.id })
a.href = row.result
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}, },
}, },
} }
</script> </script>
<style scoped> <style scoped>
.user-bar-tasks-item { .user-bar-jobs-item {
margin-bottom: 10px; margin-bottom: 0.5rem;
} }
.user-bar-tasks-item:hover { .user-bar-jobs-item:hover {
border-color: var(--el-color-primary); border-color: var(--el-color-primary);
} }
.user-bar-tasks-item-body { .user-bar-jobs-item-body {
display: flex; display: flex;
} }
.user-bar-tasks-item-body .taskIcon { .user-bar-jobs-item-body .jobIcon {
width: 45px; width: 3rem;
height: 45px; height: 3rem;
background: var(--el-color-primary-light-9); background: var(--el-color-primary-light-9);
margin-right: 20px; margin-right: 2rem;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: var(--el-color-primary); color: var(--el-color-primary);
border-radius: 20px; border-radius: 1.5rem;
} }
.user-bar-tasks-item-body .taskMain { .user-bar-jobs-item-body .jobMain {
flex: 1; flex: 1;
} }
.user-bar-tasks-item-body .title h2 { .user-bar-jobs-item-body .title h2 {
font-size: 1rem; font-size: 1rem;
} }
.user-bar-tasks-item-body .title p { .user-bar-jobs-item-body .title p {
font-size: 12px; font-size: 1rem;
color: #999; color: #999;
margin-top: 5px; margin-top: 0.5rem;
} }
.user-bar-tasks-item-body .bottom { .user-bar-jobs-item-body .bottom {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-top: 20px;
} }
</style> </style>

View File

@ -1,5 +1,10 @@
<template> <template>
<div class="user-bar"> <div class="user-bar">
<div @click="configDark" class="tasks panel-item">
<el-icon>
<component :is="config.dark ? 'el-icon-sunny' : 'el-icon-moon'" />
</el-icon>
</div>
<div @click="search" class="panel-item hidden-sm-and-down"> <div @click="search" class="panel-item hidden-sm-and-down">
<el-icon> <el-icon>
<el-icon-search /> <el-icon-search />
@ -12,7 +17,7 @@
</div> </div>
<div @click="tasks" class="tasks panel-item"> <div @click="tasks" class="tasks panel-item">
<el-icon> <el-icon>
<el-icon-sort /> <sc-icon-ScheduledJob />
</el-icon> </el-icon>
</div> </div>
<div @click="showMsg" class="msg panel-item"> <div @click="showMsg" class="msg panel-item">
@ -69,8 +74,22 @@ export default {
tasks, tasks,
message, message,
}, },
watch: {
'config.dark'(val) {
if (val) {
document.documentElement.classList.add('dark')
this.$TOOL.data.set('APP_DARK', val)
} else {
document.documentElement.classList.remove('dark')
this.$TOOL.data.remove('APP_DARK')
}
},
},
data() { data() {
return { return {
config: {
dark: this.$TOOL.data.get('APP_DARK') || false,
},
user: {}, user: {},
userName: '', userName: '',
userNameF: '', userNameF: '',
@ -86,6 +105,9 @@ export default {
this.unreadCnt = res.data this.unreadCnt = res.data
}, },
methods: { methods: {
configDark() {
this.config.dark = !this.config.dark
},
gotoMsgCenter() { gotoMsgCenter() {
this.$router.push({ path: '/profile/message' }) this.$router.push({ path: '/profile/message' })
this.msg = false this.msg = false

View File

@ -17,7 +17,7 @@
return { value: x[0], label: x[1][1] } return { value: x[0], label: x[1][1] }
}), }),
placeholder: $t('作业状态'), placeholder: $t('作业状态'),
style: 'width:15rem', style: 'width:10rem',
}, },
{ {
type: 'select', type: 'select',
@ -26,7 +26,7 @@
return { value: x[0], label: x[1][1] } return { value: x[0], label: x[1][1] }
}), }),
placeholder: $t('请求方式'), placeholder: $t('请求方式'),
style: 'width:15rem', style: 'width:10rem',
}, },
{ {
type: 'select', type: 'select',
@ -36,7 +36,7 @@
{ label: '禁用', value: false }, { label: '禁用', value: false },
], ],
placeholder: '状态', placeholder: '状态',
style: 'width:15rem', style: 'width:10rem',
}, },
]" ]"
:vue="this" :vue="this"
@ -51,7 +51,7 @@
<sc-table <sc-table
v-loading="loading" v-loading="loading"
:apiObj="$API.sys_job.pagedQuery" :apiObj="$API.sys_job.pagedQuery"
:default-sort="{ prop: 'createdTime', order: 'descending' }" :default-sort="{ prop: 'lastExecTime', order: 'descending' }"
:params="query" :params="query"
@selection-change=" @selection-change="
(items) => { (items) => {
@ -61,6 +61,7 @@
ref="table" ref="table"
remote-filter remote-filter
remote-sort remote-sort
row-key="id"
stripe> stripe>
<el-table-column type="selection"></el-table-column> <el-table-column type="selection"></el-table-column>
<el-table-column :label="$t('作业编号')" prop="id" width="150" /> <el-table-column :label="$t('作业编号')" prop="id" width="150" />
@ -80,16 +81,17 @@
return { value: x[0], text: x[1][1] } return { value: x[0], text: x[1][1] }
}) })
" "
prop="httpMethod" /> prop="httpMethod"
<el-table-column :label="$t('上次执行时间')" prop="lastExecTime" sortable="custom" /> width="100" />
<el-table-column :label="$t('上次执行状态')" prop="lastStatusCode" sortable="custom" /> <el-table-column :label="$t('上次执行时间')" prop="lastExecTime" sortable="custom" width="170" />
<el-table-column :label="$t('下次执行时间')" prop="nextExecTime" sortable="custom" /> <el-table-column :label="$t('上次执行状态')" prop="lastStatusCode" sortable="custom" width="150" />
<el-table-column :label="$t('创建时间')" prop="createdTime" sortable="custom" /> <el-table-column :label="$t('下次执行时间')" prop="nextExecTime" sortable="custom" width="170" />
<el-table-column :label="$t('启用')" prop="enabled"> <el-table-column :label="$t('启用')" prop="enabled" sortable="custom" width="100">
<template #default="scope"> <template #default="scope">
<el-switch v-model="scope.row.enabled" @change="changeSwitch($event, scope.row)"></el-switch> <el-switch v-model="scope.row.enabled" @change="changeSwitch($event, scope.row)"></el-switch>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('创建时间')" prop="createdTime" sortable="custom" width="170" />
<na-col-operation <na-col-operation
:buttons=" :buttons="
naColOperation.buttons.concat({ naColOperation.buttons.concat({
@ -128,6 +130,7 @@ export default {
filters: [], filters: [],
}, },
filter: {}, filter: {},
prop: 'lastExecTime',
}, },
dialog: { dialog: {
save: false, save: false,

View File

@ -0,0 +1,200 @@
<template>
<el-container>
<el-header>
<div class="left-panel">
<na-search
:controls="[
{
type: 'input',
field: ['root', 'keywords'],
placeholder: $t('作业编号 / 执行编号'),
style: 'width:20rem',
},
{
type: 'select',
field: ['dy', 'httpMethod'],
options: Object.entries(this.$GLOBAL.enums.httpMethods).map((x) => {
return { value: x[0], label: x[1][1] }
}),
placeholder: $t('请求方式'),
style: 'width:10rem',
},
{
multiple: true,
type: 'select',
field: ['dy', 'httpStatusCode'],
options: [
{ label: '20x', value: '200,299' },
{ label: '30x', value: '300,399' },
{ label: '40x', value: '400,499' },
{ label: '50x', value: '500,599' },
{ label: '90x', value: '900,999' },
],
placeholder: '状态码',
style: 'width:20rem',
},
]"
:vue="this"
@search="onSearch"
ref="search" />
</div>
<div class="right-panel"></div>
</el-header>
<el-main class="nopadding">
<sc-table
v-loading="loading"
:apiObj="$API.sys_job.recordPagedQuery"
:default-sort="{ prop: 'createdTime', order: 'descending' }"
:params="query"
@selection-change="
(items) => {
selection = items
}
"
ref="table"
remote-filter
remote-sort
row-key="id"
stripe>
<el-table-column :label="$t('唯一编码')" prop="id" sortable="custom" width="150" />
<el-table-column :label="$t('执行耗时(毫秒)')" align="right" prop="duration" sortable="custom" width="150" />
<el-table-column :label="$t('请求方法')" prop="httpMethod" sortable="custom" width="100" />
<el-table-column :label="$t('HTTP 状态码')" align="right" prop="httpStatusCode" sortable="custom" width="150" />
<el-table-column :label="$t('请求的网络地址')" prop="requestUrl" sortable="custom" />
<el-table-column :label="$t('创建时间')" prop="createdTime" sortable="custom" width="170" />
<na-col-operation :buttons="[naColOperation.buttons[0]]" :vue="this" width="100" />
</sc-table>
</el-main>
</el-container>
<save-dialog
v-if="dialog.save"
@closed="dialog.save = false"
@success="(data, mode) => table.handleUpdate($refs.table, data, mode)"
ref="saveDialog"></save-dialog>
</template>
<script>
import saveDialog from './save'
import table from '@/config/table'
import naColOperation from '@/config/naColOperation'
export default {
props: ['keywords'],
components: {
saveDialog,
},
data() {
return {
loading: false,
query: {
dynamicFilter: {
filters: [],
},
filter: {},
},
dialog: {
save: false,
},
selection: [],
}
},
watch: {},
computed: {
naColOperation() {
return naColOperation
},
table() {
return table
},
},
mounted() {
if (this.keywords) {
this.$refs.search.form.root.keywords = this.keywords
}
},
created() {
if (this.keywords) {
this.query.keywords = this.keywords
}
},
methods: {
//
async changeSwitch(event, row) {
try {
await this.$API.sys_job.setEnabled.post(row)
this.$message.success(`操作成功`)
} catch {
//
}
this.$refs.table.refresh()
},
//
async rowDel(row) {
try {
const res = await this.$API.sys_job.delete.post({ id: row.id })
this.$refs.table.refresh()
this.$message.success(`删除 ${res.data}`)
} catch {
//
}
},
//
async batchDel() {
let loading
try {
await this.$confirm(`确定删除选中的 ${this.selection.length} 项吗?`, '提示', {
type: 'warning',
})
loading = this.$loading()
const res = await this.$API.sys_job.bulkDelete.post({
items: this.selection,
})
this.$refs.table.refresh()
this.$message.success(`删除 ${res.data}`)
} catch {
//
}
loading?.close()
},
//
onSearch(form) {
if (Array.isArray(form.dy.createdTime)) {
this.query.dynamicFilter.filters.push({
field: 'createdTime',
operator: 'dateRange',
value: form.dy.createdTime,
})
}
if (typeof form.dy.httpMethod === 'string' && form.dy.httpMethod.trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'httpMethod',
operator: 'eq',
value: form.dy.httpMethod,
})
}
if (Array.isArray(form.dy.httpStatusCode) && form.dy.httpStatusCode.length !== 0) {
const filters = []
for (const code of form.dy.httpStatusCode) {
filters.push({
field: 'httpStatusCode',
operator: 'range',
value: code,
})
}
this.query.dynamicFilter.filters.push({
logic: 'or',
filters: filters,
})
}
this.$refs.table.upData()
},
},
}
</script>
<style scoped></style>

View File

@ -0,0 +1,87 @@
<template>
<sc-dialog v-model="visible" :title="titleMap[mode]" :width="800" @closed="$emit('closed')" destroy-on-close full-screen>
<el-form
v-loading="loading"
:disabled="mode === 'view'"
:model="form"
:rules="rules"
label-position="right"
label-width="150px"
ref="dialogForm">
<el-tabs tab-position="top">
<el-tab-pane :label="$t('基本信息')">
<el-form-item :label="$t('唯一编码')" prop="id"><el-input v-model="form.id" clearable /></el-form-item
><el-form-item :label="$t('执行耗时(毫秒)')" prop="duration"><el-input v-model="form.duration" clearable /></el-form-item
><el-form-item :label="$t('请求方法')" prop="httpMethod"><el-input v-model="form.httpMethod" clearable /></el-form-item
><el-form-item :label="$t('HTTP 状态码')" prop="httpStatusCode"><el-input v-model="form.httpStatusCode" clearable /></el-form-item
><el-form-item :label="$t('作业编号')" prop="jobId"><el-input v-model="form.jobId" clearable /></el-form-item
><el-form-item :label="$t('请求体')" prop="requestBody"
><el-input v-model="form.requestBody" clearable rows="5" type="textarea" /></el-form-item
><el-form-item :label="$t('请求头')" prop="requestHeader">
<el-input v-model="form.requestHeader" clearable rows="5" type="textarea" /></el-form-item
><el-form-item :label="$t('请求的网络地址')" prop="requestUrl"><el-input v-model="form.requestUrl" clearable /></el-form-item
><el-form-item :label="$t('响应体')" prop="responseBody"
><el-input v-model="form.responseBody" clearable rows="5" type="textarea" /></el-form-item
><el-form-item :label="$t('响应头')" prop="responseHeader">
<el-input v-model="form.responseHeader" clearable rows="5" type="textarea" /></el-form-item
><el-form-item :label="$t('执行时间编号')" prop="timeId"><el-input v-model="form.timeId" clearable /></el-form-item
><el-form-item :label="$t('创建时间')" prop="createdTime"><el-input v-model="form.createdTime" clearable /></el-form-item>
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')">
<json-viewer
:expand-depth="5"
:expanded="true"
:theme="this.$TOOL.data.get('APP_DARK') ? 'dark' : 'light'"
:value="form"
copyable
sort></json-viewer>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="visible = false"> </el-button>
</template>
</sc-dialog>
</template>
<script>
import scEditor from '@/components/scEditor/index.vue'
export default {
components: {
scEditor,
},
emits: ['success', 'closed'],
data() {
return {
mode: 'add',
titleMap: {
view: this.$t('查看作业记录'),
add: this.$t('新增作业记录'),
edit: this.$t('编辑作业记录'),
},
visible: false,
loading: false,
//
form: {},
//
rules: {},
}
},
mounted() {},
methods: {
//
async open(mode = 'add', data) {
this.visible = true
this.loading = true
this.mode = mode
if (data) {
const res = await this.$API.sys_job.recordGet.post({ id: data.id })
Object.assign(this.form, res.data)
}
this.loading = false
return this
},
},
}
</script>

View File

@ -1,5 +1,7 @@
<template> <template>
<sc-dialog v-model="visible" :title="titleMap[mode]" :width="800" @closed="$emit('closed')" destroy-on-close full-screen> <sc-dialog v-model="visible" :title="titleMap[mode]" :width="800" @closed="$emit('closed')" destroy-on-close full-screen>
<el-tabs v-model="tabIndex" tab-position="top">
<el-tab-pane :label="$t('基本信息')" :name="0">
<el-form <el-form
v-loading="loading" v-loading="loading"
:disabled="mode === 'view'" :disabled="mode === 'view'"
@ -8,8 +10,6 @@
label-position="right" label-position="right"
label-width="150px" label-width="150px"
ref="dialogForm"> ref="dialogForm">
<el-tabs tab-position="top">
<el-tab-pane :label="$t('基本信息')">
<el-form-item v-if="mode === 'view'" :label="$t('作业编号')" prop="id"> <el-form-item v-if="mode === 'view'" :label="$t('作业编号')" prop="id">
<el-input v-model="form.id" clearable /> <el-input v-model="form.id" clearable />
</el-form-item> </el-form-item>
@ -48,8 +48,8 @@
<el-form-item v-if="mode === 'view'" :label="$t('执行用户编号')" prop="userId"> <el-form-item v-if="mode === 'view'" :label="$t('执行用户编号')" prop="userId">
<el-input v-model="form.userId" clearable /> <el-input v-model="form.userId" clearable />
</el-form-item> </el-form-item>
<el-form-item v-else :label="$t('执行用户')" prop="userId"> <el-form-item v-else :label="$t('执行用户')" prop="user">
<na-user-select v-model="form.userId"></na-user-select> <na-user-select v-model="form.user"></na-user-select>
</el-form-item> </el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('创建时间')" prop="createdTime"> <el-form-item v-if="mode === 'view'" :label="$t('创建时间')" prop="createdTime">
<el-input v-model="form.createdTime" clearable /> <el-input v-model="form.createdTime" clearable />
@ -72,8 +72,12 @@
<el-form-item v-if="mode === 'view'" :label="$t('修改者用户名')" prop="modifiedUserName"> <el-form-item v-if="mode === 'view'" :label="$t('修改者用户名')" prop="modifiedUserName">
<el-input v-model="form.modifiedUserName" clearable /> <el-input v-model="form.modifiedUserName" clearable />
</el-form-item> </el-form-item>
</el-form>
</el-tab-pane> </el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')"> <el-tab-pane v-if="mode === 'view'" :label="$t('执行记录')" :name="1">
<record v-if="tabIndex === 1" :keywords="form.id" />
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')" :name="2">
<json-viewer <json-viewer
:expand-depth="5" :expand-depth="5"
:expanded="true" :expanded="true"
@ -83,7 +87,7 @@
sort></json-viewer> sort></json-viewer>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-form>
<template #footer> <template #footer>
<el-button @click="visible = false"> </el-button> <el-button @click="visible = false"> </el-button>
<el-button v-if="mode !== 'view'" :loading="loading" @click="submit" type="primary"> </el-button> <el-button v-if="mode !== 'view'" :loading="loading" @click="submit" type="primary"> </el-button>
@ -93,14 +97,17 @@
<script> <script>
import scEditor from '@/components/scEditor/index.vue' import scEditor from '@/components/scEditor/index.vue'
import Record from '@/views/sys/job/record/index.vue'
export default { export default {
components: { components: {
Record,
scEditor, scEditor,
}, },
emits: ['success', 'closed'], emits: ['success', 'closed'],
data() { data() {
return { return {
tabIndex: 0,
mode: 'add', mode: 'add',
titleMap: { titleMap: {
view: this.$t('查看作业'), view: this.$t('查看作业'),
@ -110,7 +117,12 @@ export default {
visible: false, visible: false,
loading: false, loading: false,
// //
form: {}, form: {
executionCron: '* * * * *',
httpMethod: 'Post',
requestHeader: `{ "Content-Type": "application/json" }`,
requestBody: '{}',
},
// //
rules: { rules: {
executionCron: [ executionCron: [
@ -132,6 +144,19 @@ export default {
message: this.$t('作业名称不能为空'), message: this.$t('作业名称不能为空'),
}, },
], ],
requestHeader: [
{
validator: (rule, value, callback) => {
if (!value) return callback()
try {
JSON.parse(value)
} catch {
return callback(this.$t('请求头不正确'))
}
return callback()
},
},
],
requestUrl: [ requestUrl: [
{ {
required: true, required: true,
@ -139,7 +164,7 @@ export default {
message: this.$t('请求的网络地址不正确'), message: this.$t('请求的网络地址不正确'),
}, },
], ],
userId: [ user: [
{ {
required: true, required: true,
trigger: 'blur', trigger: 'blur',
@ -178,7 +203,9 @@ export default {
try { try {
const method = this.mode === 'add' ? this.$API.sys_job.create : this.$API.sys_job.update const method = this.mode === 'add' ? this.$API.sys_job.create : this.$API.sys_job.update
this.loading = true this.loading = true
const res = await method.post(Object.assign({}, this.form, { userId: this.form.userId.id })) const res = await method.post(
Object.assign({}, this.form, { userId: this.form.user.id, requestHeaders: JSON.parse(this.form.requestHeader) }),
)
this.loading = false this.loading = false
this.$emit('success', res.data, this.mode) this.$emit('success', res.data, this.mode)
this.visible = false this.visible = false

View File

@ -44,6 +44,7 @@
ref="table" ref="table"
remoteFilter remoteFilter
remoteSort remoteSort
row-key="id"
stripe> stripe>
<el-table-column :label="$t('日志编号')" prop="id" sortable="custom"></el-table-column> <el-table-column :label="$t('日志编号')" prop="id" sortable="custom"></el-table-column>
<el-table-column :label="$t('日志时间')" prop="createdTime" sortable="custom"></el-table-column> <el-table-column :label="$t('日志时间')" prop="createdTime" sortable="custom"></el-table-column>

View File

@ -61,6 +61,7 @@
ref="table" ref="table"
remoteFilter remoteFilter
remoteSort remoteSort
row-key="id"
stripe> stripe>
<el-table-column :label="$t('日志编号')" prop="id" sortable="custom" width="150"></el-table-column> <el-table-column :label="$t('日志编号')" prop="id" sortable="custom" width="150"></el-table-column>
<el-table-column :label="$t('日志时间')" prop="createdTime" sortable="custom" width="170"></el-table-column> <el-table-column :label="$t('日志时间')" prop="createdTime" sortable="custom" width="170"></el-table-column>

View File

@ -42,6 +42,7 @@
ref="table" ref="table"
remote-filter remote-filter
remote-sort remote-sort
row-key="id"
stripe> stripe>
<el-table-column type="selection"></el-table-column> <el-table-column type="selection"></el-table-column>
<el-table-column :label="$t('消息编号')" prop="id" width="150" /> <el-table-column :label="$t('消息编号')" prop="id" width="150" />

View File

@ -58,6 +58,7 @@
ref="table" ref="table"
remote-filter remote-filter
remote-sort remote-sort
row-key="id"
stripe> stripe>
<el-table-column type="selection"></el-table-column> <el-table-column type="selection"></el-table-column>
<el-table-column :label="$t('用户编号')" prop="id" sortable="custom" width="150"></el-table-column> <el-table-column :label="$t('用户编号')" prop="id" sortable="custom" width="150"></el-table-column>